diff --git a/src/config.rs b/src/config.rs index 8295a49..a4b6d69 100644 --- a/src/config.rs +++ b/src/config.rs @@ -16,6 +16,7 @@ pub struct BotConfig { pub token: String, pub app_id: u64, pub host_channel: u64, + pub host_webhook: String, pub vote_channel: u64, pub category: u64, pub game_state_dir: PathBuf, diff --git a/src/discord/commands.rs b/src/discord/commands.rs index 43ba8b5..689c418 100644 --- a/src/discord/commands.rs +++ b/src/discord/commands.rs @@ -153,7 +153,7 @@ async fn broadcast(ctx: &Context, msg: &Message, args: Args) -> CommandResult { ctx, &guild, &mut global_data, - MessageSource::Host, + MessageSource::Automated, &msg, None, true, @@ -189,7 +189,7 @@ async fn next_phase(ctx: &Context, msg: &Message, mut args: Args) -> CommandResu ctx, &guild, &mut global_data, - MessageSource::Host, + MessageSource::Automated, &broadcast, None, true, @@ -296,7 +296,7 @@ async fn add_time(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult ctx, &guild, &mut global_data, - MessageSource::Host, + MessageSource::Automated, &broadcast, None, true, diff --git a/src/discord/event_handler.rs b/src/discord/event_handler.rs index ed3ad4d..40597c2 100644 --- a/src/discord/event_handler.rs +++ b/src/discord/event_handler.rs @@ -4,7 +4,7 @@ use serenity::http::AttachmentType; use serenity::model::channel::Message; use serenity::model::gateway::Ready; -use crate::discord::helper::send_msg_to_player_channels; +use crate::discord::helper::send_webhook_msg_to_player_channels; use crate::game::global_data::GlobalData; use crate::game::MessageSource; @@ -38,7 +38,7 @@ impl EventHandler for Handler { .get_player_from_channel(msg.channel_id.0) { let guild = msg.guild(&ctx.cache).await.unwrap(); - let user_msg = format!("**{}** > {}", player_data.codename, msg.content); + let user_msg = msg.content.clone(); let attachments: Vec = msg .attachments @@ -46,21 +46,36 @@ impl EventHandler for Handler { .map(|a| AttachmentType::Image(&a.url)) .collect(); - send_msg_to_player_channels( + let msg_source = MessageSource::Player(Box::new(player_data.clone())); + + send_webhook_msg_to_player_channels( &ctx, &guild, &mut global_data, - MessageSource::Player(msg.channel_id.0), + msg_source, &user_msg, Some(attachments), - false, ) .await .expect("Unable to send message to players"); } } - async fn ready(&self, _ctx: Context, ready: Ready) { + 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); + println!("{} is connected!", ready.user.name); } } diff --git a/src/discord/helper.rs b/src/discord/helper.rs index d796372..2d0eb73 100644 --- a/src/discord/helper.rs +++ b/src/discord/helper.rs @@ -1,6 +1,6 @@ use serenity::client::Context; use serenity::framework::standard::Args; -use serenity::http::AttachmentType; +use serenity::http::{AttachmentType, Http}; use serenity::model::channel::{Message, PermissionOverwrite, PermissionOverwriteType}; use serenity::model::guild::{Guild, Member}; use serenity::model::id::{ChannelId, UserId}; @@ -13,6 +13,7 @@ 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; pub async fn send_msg_to_player_channels( @@ -29,8 +30,8 @@ pub async fn send_msg_to_player_channels( .player_data .iter() .filter(|player_data| { - if let MessageSource::Player(channel_id) = msg_source { - if channel_id == player_data.channel { + if let MessageSource::Player(source_player) = &msg_source { + if source_player.channel == player_data.channel { return false; } } @@ -77,15 +78,10 @@ pub async fn send_msg_to_player_channels( .unwrap(); let source = match msg_source { - MessageSource::Player(channel_id) => { - let discord_id = global_data - .game_state_mut()? - .get_player_from_channel(channel_id) - .unwrap() - .discord_id; + MessageSource::Player(player_data) => { let name = guild .members - .get(&UserId::from(discord_id)) + .get(&UserId::from(player_data.discord_id)) .unwrap() .display_name(); @@ -109,6 +105,98 @@ pub async fn send_msg_to_player_channels( Ok(()) } +pub async fn send_webhook_msg( + http: &Http, + webhook: &Webhook, + username: &str, + msg: &str, + attachment: Option>>, +) -> error::Result<()> { + webhook + .execute(http, false, |w| { + w.content(&msg).username(username); + + if let Some(attachment) = attachment.clone() { + w.add_files(attachment); + } + + w + }) + .await?; + + Ok(()) +} + +pub async fn send_webhook_msg_to_player_channels( + ctx: &Context, + guild: &Guild, + global_data: &mut GlobalData, + msg_source: MessageSource, + msg: &str, + attachment: Option>>, +) -> error::Result<()> { + let msg_username = match &msg_source { + MessageSource::Player(p) => p.codename.clone(), + MessageSource::Host => "Woxlf".to_string(), + MessageSource::Automated => "Woxlf System Message".to_string(), + }; + + 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 + }) + .map(|player_data| { + send_webhook_msg( + &ctx.http, + &player_data.channel_webhook, + &msg_username, + msg, + attachment.clone(), + ) + }); + + let msgs: Result<(), WoxlfError> = futures::future::join_all(msg_tasks) + .await + .into_iter() + .collect(); + + msgs?; + + let source = match &msg_source { + MessageSource::Player(player_data) => { + let name = guild + .members + .get(&UserId::from(player_data.discord_id)) + .unwrap() + .display_name(); + + name.to_string() + } + MessageSource::Host => "Host".to_string(), + MessageSource::Automated => "Automated".to_string(), + }; + + let host_channel_username = format!("{} ({})", msg_username, source); + + send_webhook_msg( + &ctx.http, + global_data.host_webhook()?, + &host_channel_username, + msg, + attachment, + ) + .await?; + Ok(()) +} + pub async fn add_user_to_game( ctx: &Context, guild: &Guild, @@ -139,6 +227,13 @@ pub async fn add_user_to_game( channel.create_permission(&ctx.http, &overwrite).await?; + let webhook = channel + .create_webhook( + &ctx.http, + format!("{}'s Woxlf Webhook", discord_user.display_name()), + ) + .await?; + let msg = channel.send_message(&ctx.http, |m| { m.content(MessageBuilder::new() .push("Welcome ") @@ -162,6 +257,7 @@ pub async fn add_user_to_game( discord_id: discord_user.user.id.0, vote_target: None, codename, + channel_webhook: webhook, }; global_data.game_state_mut()?.player_data.push(player_data); diff --git a/src/discord/mod.rs b/src/discord/mod.rs index 2b8ae18..cf99ab5 100644 --- a/src/discord/mod.rs +++ b/src/discord/mod.rs @@ -1,3 +1,3 @@ pub mod commands; -pub mod helper; pub mod event_handler; +pub mod helper; diff --git a/src/error.rs b/src/error.rs index 1f62916..d702e84 100644 --- a/src/error.rs +++ b/src/error.rs @@ -12,6 +12,7 @@ pub enum WoxlfError { SerenityError(serenity::Error), DiscordIdParseError(String), GameNotInProgress, + HostWebhookError, } impl std::error::Error for WoxlfError {} @@ -26,6 +27,7 @@ impl Display for WoxlfError { WoxlfError::SerenityError(e) => format!("Serenity error: {}", e), 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(), }; write!(f, "Woxlf Error: {}", msg) diff --git a/src/game/global_data.rs b/src/game/global_data.rs index 4287847..ab0c365 100644 --- a/src/game/global_data.rs +++ b/src/game/global_data.rs @@ -10,12 +10,14 @@ use crate::error::{Result, WoxlfError}; use crate::game::game_state::GameState; use crate::game::Phase; use chrono::Duration; +use serenity::model::prelude::Webhook; 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 { @@ -23,6 +25,7 @@ impl GlobalData { Self { cfg, game_state: None, + host_webhook: None, } } @@ -101,6 +104,12 @@ impl GlobalData { "Game not in progress.".to_string() } } + + pub fn host_webhook(&self) -> Result<&Webhook> { + let webhook = &self.host_webhook; + + webhook.as_ref().ok_or(WoxlfError::HostWebhookError) + } } impl TypeMapKey for GlobalData { diff --git a/src/game/mod.rs b/src/game/mod.rs index 37fb959..c94a165 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -4,6 +4,7 @@ use rand::Rng; use serde::{Deserialize, Serialize}; use crate::config::BotConfig; +use crate::game::player_data::PlayerData; pub mod game_state; pub mod global_data; @@ -41,9 +42,9 @@ pub fn generate_codename(config: &BotConfig) -> String { format!("{} {}", adj, occupation) } -#[derive(Debug, Deserialize, Serialize, Clone)] +#[derive(Debug, Clone)] pub enum MessageSource { - Player(u64), + Player(Box), Host, Automated, } diff --git a/src/game/player_data.rs b/src/game/player_data.rs index dff2039..b2b9c86 100644 --- a/src/game/player_data.rs +++ b/src/game/player_data.rs @@ -1,11 +1,13 @@ use serde::{Deserialize, Serialize}; +use serenity::model::prelude::Webhook; -#[derive(Debug, Deserialize, Serialize, Clone, Default, Hash)] +#[derive(Debug, Deserialize, Serialize, Clone)] pub struct PlayerData { pub channel: u64, pub discord_id: u64, pub codename: String, pub vote_target: Option, + pub channel_webhook: Webhook, } impl PlayerData {