From ea0be5c7087d3db931f552b24eba8895ca220738 Mon Sep 17 00:00:00 2001 From: Joey Hines Date: Sun, 20 Mar 2022 17:42:50 -0600 Subject: [PATCH] Added profile pic support + fixed an issue with webhook serailization + Added imgur module to handle interacting with the imgur api + Users now get a random profile pic from an imgur album + Switched to using webhook ids instead of storing the full webhook in toml, serenity does not seem to like the toml deserialization --- Cargo.lock | 218 +++++++++++++++++++++++++++++++++-- Cargo.toml | 1 + src/config.rs | 13 ++- src/discord/event_handler.rs | 16 +-- src/discord/helper.rs | 52 +++++---- src/error.rs | 9 ++ src/game/global_data.rs | 15 ++- src/game/mod.rs | 17 ++- src/game/player_data.rs | 4 +- src/imgur/mod.rs | 76 ++++++++++++ src/main.rs | 1 + 11 files changed, 363 insertions(+), 59 deletions(-) create mode 100644 src/imgur/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 027bff3..665fc99 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -225,6 +225,22 @@ dependencies = [ "yaml-rust", ] +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + [[package]] name = "cpufeatures" version = "0.2.1" @@ -285,6 +301,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" +[[package]] +name = "fastrand" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" +dependencies = [ + "instant", +] + [[package]] name = "flate2" version = "1.0.22" @@ -303,6 +328,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.0.1" @@ -566,6 +606,19 @@ dependencies = [ "tokio-rustls 0.23.2", ] +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes 1.1.0", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + [[package]] name = "idna" version = "0.2.3" @@ -596,6 +649,15 @@ dependencies = [ "bytes 0.5.6", ] +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + [[package]] name = "ipnet" version = "2.3.1" @@ -727,6 +789,24 @@ dependencies = [ "winapi", ] +[[package]] +name = "native-tls" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48ba9f7719b5a0f42f338907614285fb5fd70e53858141f69898a1fb7203b24d" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "nom" version = "7.1.0" @@ -794,6 +874,39 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "openssl" +version = "0.10.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7ae222234c30df141154f159066c5093ff73b63204dcda7121eb082fc56a95" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-sys", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e46109c383602735fa0a2e48dd2b7c892b048e1bf69e5c3b1d804b7d9c203cb" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "ordered-multimap" version = "0.3.1" @@ -891,6 +1004,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" + [[package]] name = "ppv-lite86" version = "0.2.16" @@ -1010,6 +1129,15 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "redox_syscall" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8380fe0152551244f0747b1bf41737e0f8a74f97a14ccefd1148187271634f3c" +dependencies = [ + "bitflags", +] + [[package]] name = "regex" version = "1.5.5" @@ -1028,10 +1156,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" [[package]] -name = "reqwest" -version = "0.11.9" +name = "remove_dir_all" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f242f1488a539a79bac6dbe7c8609ae43b7914b7736210f239a37cccb32525" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "reqwest" +version = "0.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a1f7aa4f35e5e8b4160449f51afc758f0ce6454315a9fa7d0d113e958c41eb" dependencies = [ "base64 0.13.0", "bytes 1.1.0", @@ -1043,12 +1180,14 @@ dependencies = [ "http-body", "hyper", "hyper-rustls", + "hyper-tls", "ipnet", "js-sys", "lazy_static", "log", "mime", "mime_guess", + "native-tls", "percent-encoding", "pin-project-lite", "rustls 0.20.4", @@ -1057,6 +1196,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "tokio", + "tokio-native-tls", "tokio-rustls 0.23.2", "tokio-util", "url", @@ -1130,9 +1270,9 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "0.2.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eebeaeb360c87bfb72e84abdb3447159c0eaececf1bef2aecd65a8be949d1c9" +checksum = "1ee86d63972a7c661d1536fefe8c3c8407321c3df668891286de28abcd087360" dependencies = [ "base64 0.13.0", ] @@ -1143,6 +1283,16 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" +[[package]] +name = "schannel" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +dependencies = [ + "lazy_static", + "winapi", +] + [[package]] name = "sct" version = "0.6.1" @@ -1163,6 +1313,29 @@ dependencies = [ "untrusted", ] +[[package]] +name = "security-framework" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "serde" version = "1.0.136" @@ -1327,6 +1500,20 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + [[package]] name = "textwrap" version = "0.11.0" @@ -1390,6 +1577,16 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.22.0" @@ -1589,6 +1786,12 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4bf03e0ca70d626ecc4ba6b0763b934b6f2976e8c744088bb3c1d646fbb1ad0" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "vec_map" version = "0.8.2" @@ -1761,9 +1964,9 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "winreg" -version = "0.7.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" dependencies = [ "winapi", ] @@ -1777,6 +1980,7 @@ dependencies = [ "futures", "rand 0.8.5", "regex", + "reqwest", "serde", "serenity", "structopt", diff --git a/Cargo.toml b/Cargo.toml index fa975e0..e561950 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ rand = "0.8.5" toml = "0.5.8" regex = "1.5.5" futures = "0.3.21" +reqwest = "0.11.10" [dependencies.serenity] version = "0.10.10" diff --git a/src/config.rs b/src/config.rs index a4b6d69..65b6fe0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -11,17 +11,24 @@ pub struct Args { pub cfg_path: PathBuf, } +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct GameConfig { + pub occupation: Vec, + pub adjective: Vec, + pub profile_album_hash: String, +} + #[derive(Debug, Deserialize, Serialize, Clone)] pub struct BotConfig { pub token: String, pub app_id: u64, pub host_channel: u64, - pub host_webhook: String, + pub host_webhook_id: u64, pub vote_channel: u64, pub category: u64, + pub imgur_client_id: String, pub game_state_dir: PathBuf, - pub occupation: Vec, - pub adjective: Vec, + pub game_config: GameConfig, } impl BotConfig { diff --git a/src/discord/event_handler.rs b/src/discord/event_handler.rs index 40597c2..c007024 100644 --- a/src/discord/event_handler.rs +++ b/src/discord/event_handler.rs @@ -61,21 +61,7 @@ impl EventHandler for Handler { } } - async fn ready(&self, ctx: Context, ready: Ready) { - let mut data = ctx.data.write().await; - - let global_data = data.get_mut::().unwrap(); - - let mut global_data = global_data.lock().await; - - let host_webhook = ctx - .http - .get_webhook_from_url(&global_data.cfg.host_webhook) - .await - .expect("Unable to open host webhook"); - - global_data.host_webhook = Some(host_webhook); - + async fn ready(&self, _ctx: Context, ready: Ready) { println!("{} is connected!", ready.user.name); } } diff --git a/src/discord/helper.rs b/src/discord/helper.rs index 2d0eb73..4a2e78f 100644 --- a/src/discord/helper.rs +++ b/src/discord/helper.rs @@ -13,9 +13,17 @@ use crate::game::global_data::GlobalData; use crate::game::player_data::PlayerData; use crate::game::MessageSource; use crate::{error, game}; -use serenity::model::prelude::Webhook; use serenity::prelude::SerenityError; +fn filter_source_channel(player_data: &&PlayerData, msg_source: &MessageSource) -> bool { + if let MessageSource::Player(source_player) = &msg_source { + if source_player.channel == player_data.channel { + return false; + } + } + true +} + pub async fn send_msg_to_player_channels( ctx: &Context, guild: &Guild, @@ -29,14 +37,7 @@ pub async fn send_msg_to_player_channels( .game_state_mut()? .player_data .iter() - .filter(|player_data| { - if let MessageSource::Player(source_player) = &msg_source { - if source_player.channel == player_data.channel { - return false; - } - } - true - }) + .filter(|player| filter_source_channel(player, &msg_source)) .map(|player_data| { let channel = guild .channels @@ -107,15 +108,23 @@ pub async fn send_msg_to_player_channels( pub async fn send_webhook_msg( http: &Http, - webhook: &Webhook, + webhook_id: u64, username: &str, + profile_pic_url: Option, msg: &str, attachment: Option>>, ) -> error::Result<()> { + + let webhook = http.get_webhook(webhook_id).await?; + webhook .execute(http, false, |w| { w.content(&msg).username(username); + if let Some(profile_pic_url) = profile_pic_url { + w.avatar_url(profile_pic_url); + } + if let Some(attachment) = attachment.clone() { w.add_files(attachment); } @@ -141,23 +150,22 @@ pub async fn send_webhook_msg_to_player_channels( MessageSource::Automated => "Woxlf System Message".to_string(), }; + let profile_pic = match &msg_source { + MessageSource::Player(p) => Some(p.profile_pic_url.clone()), + MessageSource::Host | MessageSource::Automated => None, + }; + let msg_tasks = global_data .game_state_mut()? .player_data .iter() - .filter(|player_data| { - if let MessageSource::Player(source_player) = &msg_source { - if source_player.channel == player_data.channel { - return false; - } - } - true - }) + .filter(|player| filter_source_channel(player, &msg_source)) .map(|player_data| { send_webhook_msg( &ctx.http, - &player_data.channel_webhook, + player_data.channel_webhook_id, &msg_username, + profile_pic.clone(), msg, attachment.clone(), ) @@ -188,8 +196,9 @@ pub async fn send_webhook_msg_to_player_channels( send_webhook_msg( &ctx.http, - global_data.host_webhook()?, + global_data.cfg.host_webhook_id, &host_channel_username, + profile_pic, msg, attachment, ) @@ -257,7 +266,8 @@ pub async fn add_user_to_game( discord_id: discord_user.user.id.0, vote_target: None, codename, - channel_webhook: webhook, + channel_webhook_id: webhook.id.0, + profile_pic_url: global_data.get_profile_pic_url().await?, }; global_data.game_state_mut()?.player_data.push(player_data); diff --git a/src/error.rs b/src/error.rs index d702e84..98b0c53 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,3 +1,4 @@ +use crate::imgur::ImgurError; use serenity::prelude::SerenityError; use std::fmt::{Display, Formatter}; @@ -13,6 +14,7 @@ pub enum WoxlfError { DiscordIdParseError(String), GameNotInProgress, HostWebhookError, + ImgurError(ImgurError), } impl std::error::Error for WoxlfError {} @@ -28,6 +30,7 @@ impl Display for WoxlfError { WoxlfError::DiscordIdParseError(e) => format!("Unable to parse player id {}", e), WoxlfError::GameNotInProgress => "A game is not currently in progress".to_string(), WoxlfError::HostWebhookError => "Unable to communicate to the host webhook".to_string(), + WoxlfError::ImgurError(err) => format!("Imgur module error: {}", err.to_string()), }; write!(f, "Woxlf Error: {}", msg) @@ -57,3 +60,9 @@ impl From for WoxlfError { Self::GameStateSerializeError(err) } } + +impl From for WoxlfError { + fn from(err: ImgurError) -> Self { + Self::ImgurError(err) + } +} diff --git a/src/game/global_data.rs b/src/game/global_data.rs index ab0c365..a9d0837 100644 --- a/src/game/global_data.rs +++ b/src/game/global_data.rs @@ -9,15 +9,15 @@ use crate::config::BotConfig; use crate::error::{Result, WoxlfError}; use crate::game::game_state::GameState; use crate::game::Phase; +use crate::imgur::{get_album_images, Image}; use chrono::Duration; -use serenity::model::prelude::Webhook; +use rand::prelude::SliceRandom; use serenity::utils::MessageBuilder; #[derive(Debug, Deserialize, Serialize, Clone)] pub struct GlobalData { pub cfg: BotConfig, pub game_state: Option, - pub host_webhook: Option, } impl GlobalData { @@ -25,7 +25,6 @@ impl GlobalData { Self { cfg, game_state: None, - host_webhook: None, } } @@ -105,10 +104,14 @@ impl GlobalData { } } - pub fn host_webhook(&self) -> Result<&Webhook> { - let webhook = &self.host_webhook; + pub async fn get_profile_pic_url(&self) -> Result { + let images: Vec = get_album_images( + &self.cfg.imgur_client_id, + &self.cfg.game_config.profile_album_hash, + ) + .await?; - webhook.as_ref().ok_or(WoxlfError::HostWebhookError) + Ok(images.choose(&mut rand::thread_rng()).unwrap().link.clone()) } } diff --git a/src/game/mod.rs b/src/game/mod.rs index c94a165..6c5adde 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -1,10 +1,11 @@ use std::fmt::{Display, Formatter}; -use rand::Rng; +use rand::thread_rng; use serde::{Deserialize, Serialize}; use crate::config::BotConfig; use crate::game::player_data::PlayerData; +use rand::prelude::SliceRandom; pub mod game_state; pub mod global_data; @@ -34,10 +35,16 @@ impl Display for Phase { } pub fn generate_codename(config: &BotConfig) -> String { - let mut rng = rand::thread_rng(); - - let occupation = &config.occupation[rng.gen_range(0..config.occupation.len())]; - let adj = &config.adjective[rng.gen_range(0..config.adjective.len())]; + let occupation = &config + .game_config + .occupation + .choose(&mut thread_rng()) + .unwrap(); + let adj = &config + .game_config + .adjective + .choose(&mut thread_rng()) + .unwrap(); format!("{} {}", adj, occupation) } diff --git a/src/game/player_data.rs b/src/game/player_data.rs index b2b9c86..fec5c88 100644 --- a/src/game/player_data.rs +++ b/src/game/player_data.rs @@ -1,5 +1,4 @@ use serde::{Deserialize, Serialize}; -use serenity::model::prelude::Webhook; #[derive(Debug, Deserialize, Serialize, Clone)] pub struct PlayerData { @@ -7,7 +6,8 @@ pub struct PlayerData { pub discord_id: u64, pub codename: String, pub vote_target: Option, - pub channel_webhook: Webhook, + pub profile_pic_url: String, + pub channel_webhook_id: u64, } impl PlayerData { diff --git a/src/imgur/mod.rs b/src/imgur/mod.rs new file mode 100644 index 0000000..0d1f09c --- /dev/null +++ b/src/imgur/mod.rs @@ -0,0 +1,76 @@ +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; + +#[derive(Debug)] +pub enum ImgurError { + ReqwestError(reqwest::Error), + ImgurRequestError(String), +} + +impl From for ImgurError { + fn from(e: reqwest::Error) -> Self { + Self::ReqwestError(e) + } +} + +impl Display for ImgurError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let msg = match self { + ImgurError::ReqwestError(err) => format!("Reqwest error: {}", err.to_string()), + ImgurError::ImgurRequestError(msg) => format!("Imgur request error: {}", msg), + }; + + write!(f, "{}", msg) + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct AlbumData { + images: Option>, + error: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct AlbumResponse { + data: AlbumData, + success: bool, + status: i32, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Image { + pub id: String, + pub title: Option, + pub description: Option, + #[serde(rename = "type")] + pub img_type: String, + pub animated: bool, + pub width: i32, + pub height: i32, + pub size: i32, + pub link: String, +} + +pub async fn get_album_images(client_id: &str, album_hash: &str) -> Result, ImgurError> { + let client = Client::new(); + + let res = client + .get(format!( + "https://api.imgur.com/3/album/{}", + album_hash + )) + .header("Authorization", format!("Client-ID {}", client_id)) + .send() + .await?; + + let album_response: AlbumResponse = res.json().await?; + + if album_response.success { + Ok(album_response.data.images.unwrap()) + } else { + Err(ImgurError::ImgurRequestError( + album_response.data.error.unwrap(), + )) + } +} diff --git a/src/main.rs b/src/main.rs index 441a311..3ace8a1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,6 +14,7 @@ mod config; mod discord; mod error; mod game; +mod imgur; #[tokio::main] async fn main() {