Compare commits

...

3 Commits

Author SHA1 Message Date
Joey Hines 4b7570b24c Merge pull request 'msg_refactor' (#4) from msg_refactor into main
Reviewed-on: #4
2023-01-06 05:07:03 +00:00
Joey Hines 9a91a16e0d Message router refactor
+ Created the WoxlfMessage struct to streamline interface
+ Message tasks are now joined at once instead of sequentially
+ Clippy + fmt
2023-01-06 05:07:03 +00:00
Joey Hines 3c219f5bff Initial refactor of message handling
+ Split all message handling into message_router.rs
+ Added whisper command
+ Updated serenity version
+ Fmt, but clippy failing
2023-01-06 05:07:03 +00:00
11 changed files with 767 additions and 551 deletions

569
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,7 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
config = "0.12.0" config = "0.13.3"
structopt = "0.3.26" structopt = "0.3.26"
chrono = {version="0.4.19", features=["serde"]} chrono = {version="0.4.19", features=["serde"]}
serde = "1.0.136" serde = "1.0.136"
@ -16,9 +16,10 @@ regex = "1.5.5"
futures = "0.3.21" futures = "0.3.21"
reqwest = "0.11.10" reqwest = "0.11.10"
tera = "1.15.0" tera = "1.15.0"
bitflags = "1.3.2"
[dependencies.serenity] [dependencies.serenity]
version = "0.10.10" version = "0.11.5"
features = ["framework", "standard_framework"] features = ["framework", "standard_framework"]
[dependencies.tokio] [dependencies.tokio]

View File

@ -15,10 +15,12 @@ pub struct Args {
pub struct GameConfig { pub struct GameConfig {
pub game_name: String, pub game_name: String,
pub bot_name: String, pub bot_name: String,
pub bot_profile_pic: String,
pub vote_phase_name: String, pub vote_phase_name: String,
pub enemy_phase_name: String, pub enemy_phase_name: String,
pub player_group_name: String, pub player_group_name: String,
pub profile_album_hash: String, pub profile_album_hash: String,
pub whispers_allowed: bool,
pub first_name: Vec<String>, pub first_name: Vec<String>,
pub last_name: Vec<String>, pub last_name: Vec<String>,
pub messages: MessageConfig, pub messages: MessageConfig,
@ -43,6 +45,7 @@ pub struct DiscordConfig {
pub host_webhook_id: u64, pub host_webhook_id: u64,
pub vote_channel: u64, pub vote_channel: u64,
pub category: u64, pub category: u64,
pub guild_id: u64,
} }
#[derive(Debug, Deserialize, Serialize, Clone)] #[derive(Debug, Deserialize, Serialize, Clone)]

View File

@ -14,16 +14,20 @@ use serenity::model::prelude::{Message, UserId};
use serenity::prelude::Context; use serenity::prelude::Context;
use serenity::utils::MessageBuilder; use serenity::utils::MessageBuilder;
use crate::discord::helper::{add_user_to_game, parse_duration_arg, send_msg_to_player_channels}; use crate::discord::helper::{add_user_to_game, parse_duration_arg};
use crate::error::{Result, WoxlfError}; use crate::error::{Result, WoxlfError};
use crate::game::global_data::GlobalData; use crate::game::global_data::GlobalData;
use crate::game::message_router::{
dispatch_message, Median, MessageDest, MessageSource, WoxlfMessage,
};
use crate::game::player_data::PlayerData; use crate::game::player_data::PlayerData;
use crate::game::MessageSource;
use crate::game::Phase; use crate::game::Phase;
use crate::messages::DiscordUser; use crate::messages::DiscordUser;
#[group] #[group]
#[commands(start, say, end, broadcast, next_phase, kill, add_time, test_theme)] #[commands(
start, say, end, broadcast, next_phase, kill, add_time, test_theme, whisper
)]
struct Host; struct Host;
#[command] #[command]
@ -34,7 +38,7 @@ async fn start(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let mut data = ctx.data.write().await; let mut data = ctx.data.write().await;
let global_data = data.get_mut::<GlobalData>().unwrap(); let global_data = data.get_mut::<GlobalData>().unwrap();
let guild = msg.guild(&ctx.cache).await.unwrap(); let guild = msg.guild(&ctx.cache).unwrap();
let mut global_data = global_data.lock().await; let mut global_data = global_data.lock().await;
let game_name = args.single::<String>()?; let game_name = args.single::<String>()?;
@ -136,7 +140,7 @@ async fn start(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
async fn end(ctx: &Context, msg: &Message, mut _args: Args) -> CommandResult { async fn end(ctx: &Context, msg: &Message, mut _args: Args) -> CommandResult {
let mut data = ctx.data.write().await; let mut data = ctx.data.write().await;
let global_data = data.get_mut::<GlobalData>().unwrap(); let global_data = data.get_mut::<GlobalData>().unwrap();
let guild = msg.guild(&ctx.cache).await.unwrap(); let guild = msg.guild(&ctx.cache).unwrap();
let mut global_data = global_data.lock().await; let mut global_data = global_data.lock().await;
@ -159,23 +163,20 @@ async fn end(ctx: &Context, msg: &Message, mut _args: Args) -> CommandResult {
#[command] #[command]
#[only_in(guilds)] #[only_in(guilds)]
#[allowed_roles("wolfx host")] #[allowed_roles("wolfx host")]
async fn say(ctx: &Context, msg: &Message, args: Args) -> CommandResult { async fn say(ctx: &Context, _msg: &Message, args: Args) -> CommandResult {
let mut data = ctx.data.write().await; let data = ctx.data.read().await;
let global_data = data.get_mut::<GlobalData>().unwrap(); let global_data = data.get::<GlobalData>().unwrap();
let guild = msg.guild(&ctx.cache).await.unwrap();
let mut global_data = global_data.lock().await; let global_data = global_data.lock().await;
send_msg_to_player_channels( let msg = WoxlfMessage::default()
ctx, .source(MessageSource::Host)
&guild, .dest(MessageDest::Broadcast)
&mut global_data, .median(Median::Webhook)
MessageSource::Host, .content(args.rest())
args.rest(), .clone();
None,
false, dispatch_message(ctx, &global_data, msg).await?;
)
.await?;
Ok(()) Ok(())
} }
@ -183,27 +184,24 @@ async fn say(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
#[command] #[command]
#[only_in(guilds)] #[only_in(guilds)]
#[allowed_roles("wolfx host")] #[allowed_roles("wolfx host")]
async fn broadcast(ctx: &Context, msg: &Message, args: Args) -> CommandResult { async fn broadcast(ctx: &Context, _msg: &Message, args: Args) -> CommandResult {
let mut data = ctx.data.write().await; let data = ctx.data.read().await;
let global_data = data.get_mut::<GlobalData>().unwrap(); let global_data = data.get::<GlobalData>().unwrap();
let guild = msg.guild(&ctx.cache).await.unwrap();
let mut global_data = global_data.lock().await; let global_data = global_data.lock().await;
let msg = global_data let broadcast = global_data
.templates()? .templates()?
.build_announcement(&global_data, args.rest())?; .build_announcement(&global_data, args.rest())?;
send_msg_to_player_channels( let woxlf_msg = WoxlfMessage::default()
ctx, .source(MessageSource::Automated)
&guild, .dest(MessageDest::Broadcast)
&mut global_data, .content(&broadcast)
MessageSource::Automated, .median(Median::Webhook)
&msg, .clone();
None,
true, dispatch_message(ctx, &global_data, woxlf_msg).await?;
)
.await?;
Ok(()) Ok(())
} }
@ -214,7 +212,7 @@ async fn broadcast(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
async fn next_phase(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { async fn next_phase(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let mut data = ctx.data.write().await; let mut data = ctx.data.write().await;
let global_data = data.get_mut::<GlobalData>().unwrap(); let global_data = data.get_mut::<GlobalData>().unwrap();
let guild = msg.guild(&ctx.cache).await.unwrap(); let guild = msg.guild(&ctx.cache).unwrap();
let mut global_data = global_data.lock().await; let mut global_data = global_data.lock().await;
@ -232,16 +230,14 @@ async fn next_phase(ctx: &Context, msg: &Message, mut args: Args) -> CommandResu
.templates()? .templates()?
.build_announcement(&global_data, &broadcast)?; .build_announcement(&global_data, &broadcast)?;
send_msg_to_player_channels( let woxlf_msg = WoxlfMessage::default()
ctx, .source(MessageSource::Automated)
&guild, .dest(MessageDest::Broadcast)
&mut global_data, .median(Median::Webhook)
MessageSource::Automated, .content(&broadcast)
&broadcast, .clone();
None,
true, dispatch_message(ctx, &global_data, woxlf_msg).await?;
)
.await?;
if global_data.game_state_mut()?.current_phase == Phase::Day { if global_data.game_state_mut()?.current_phase == Phase::Day {
let vote_channel = guild let vote_channel = guild
@ -251,6 +247,7 @@ async fn next_phase(ctx: &Context, msg: &Message, mut args: Args) -> CommandResu
)) ))
.unwrap(); .unwrap();
vote_channel vote_channel
.id()
.send_message(&ctx.http, |m| { .send_message(&ctx.http, |m| {
m.content(format!( m.content(format!(
"**{} {} Votes:**", "**{} {} Votes:**",
@ -308,7 +305,6 @@ async fn kill(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
async fn add_time(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { async fn add_time(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let mut data = ctx.data.write().await; let mut data = ctx.data.write().await;
let global_data = data.get_mut::<GlobalData>().unwrap(); let global_data = data.get_mut::<GlobalData>().unwrap();
let guild = msg.guild(&ctx.cache).await.unwrap();
let mut global_data = global_data.lock().await; let mut global_data = global_data.lock().await;
@ -332,16 +328,14 @@ async fn add_time(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
.templates()? .templates()?
.build_announcement(&global_data, &broadcast)?; .build_announcement(&global_data, &broadcast)?;
send_msg_to_player_channels( let woxlf_msg = WoxlfMessage::default()
ctx, .source(MessageSource::Automated)
&guild, .dest(MessageDest::Broadcast)
&mut global_data, .median(Median::Webhook)
MessageSource::Automated, .content(&broadcast)
&broadcast, .clone();
None,
true, dispatch_message(ctx, &global_data, woxlf_msg).await?;
)
.await?;
msg.reply(&ctx.http, "Phase has been updated") msg.reply(&ctx.http, "Phase has been updated")
.await .await
@ -501,7 +495,7 @@ struct Player;
async fn vote(ctx: &Context, msg: &Message, args: Args) -> CommandResult { async fn vote(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
let mut data = ctx.data.write().await; let mut data = ctx.data.write().await;
let global_data = data.get_mut::<GlobalData>().unwrap(); let global_data = data.get_mut::<GlobalData>().unwrap();
let guild = msg.guild(&ctx.cache).await.unwrap(); let guild = msg.guild(&ctx.cache).unwrap();
let mut global_data = global_data.lock().await; let mut global_data = global_data.lock().await;
@ -555,6 +549,7 @@ async fn vote(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
)?; )?;
vote_channel vote_channel
.id()
.send_message(&ctx.http, |m| m.content(vote_msg)) .send_message(&ctx.http, |m| m.content(vote_msg))
.await?; .await?;
} else { } else {
@ -626,7 +621,7 @@ async fn players(ctx: &Context, msg: &Message, _args: Args) -> CommandResult {
.push(alive_status); .push(alive_status);
if msg.channel_id.0 == global_data.cfg.discord_config.host_channel { if msg.channel_id.0 == global_data.cfg.discord_config.host_channel {
let guild = msg.guild(&ctx.cache).await.unwrap(); let guild = msg.guild(&ctx.cache).unwrap();
let member = guild.members.get(&UserId::from(player.discord_id)).unwrap(); let member = guild.members.get(&UserId::from(player.discord_id)).unwrap();
msg_builder.push_line(format!(" ({})", member.display_name())); msg_builder.push_line(format!(" ({})", member.display_name()));
} else { } else {
@ -639,6 +634,59 @@ async fn players(ctx: &Context, msg: &Message, _args: Args) -> CommandResult {
Ok(()) Ok(())
} }
#[command]
#[aliases("pm", "w")]
#[description = "Send a private message to another player."]
async fn whisper(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
if args.len() < 2 {
msg.reply(&ctx.http, "Need a recipient and message!")
.await?;
} else {
let data = ctx.data.read().await;
let global_data = data.get::<GlobalData>().unwrap();
let global_data = global_data.lock().await;
let target = args.single::<String>()?;
let pm = args.rest();
let src_player = match global_data
.game_state()?
.get_player_from_discord_id(msg.author.id.0)
{
None => {
msg.reply(&ctx.http, "You are not in the game!").await?;
return Ok(());
}
Some(player) => player,
};
if let Some(target_player) = global_data.game_state()?.get_player_by_codename(&target) {
if src_player.discord_id == target_player.discord_id {
msg.reply(&ctx.http, "You can't send messages to yourself!")
.await?;
return Ok(());
}
let woxlf_msg = WoxlfMessage::default()
.source(MessageSource::Player(Box::new(src_player.clone())))
.dest(MessageDest::Player(Box::new(target_player.clone())))
.median(Median::DirectMessage)
.content(pm)
.clone();
dispatch_message(ctx, &global_data, woxlf_msg).await?;
} else {
msg.reply(
&ctx.http,
format!("Could not find a player with codename {}.", target),
)
.await?;
}
}
Ok(())
}
#[help] #[help]
#[individual_command_tip = "If you want more information about a specific command, just pass the command as argument."] #[individual_command_tip = "If you want more information about a specific command, just pass the command as argument."]
#[command_not_found_text = "Could not find: `{}`."] #[command_not_found_text = "Could not find: `{}`."]

View File

@ -1,13 +1,13 @@
use serenity::async_trait; use serenity::async_trait;
use serenity::client::{Context, EventHandler}; use serenity::client::{Context, EventHandler};
use serenity::http::AttachmentType;
use serenity::model::channel::Message; use serenity::model::channel::Message;
use serenity::model::gateway::Ready; use serenity::model::gateway::Ready;
use serenity::model::prelude::AttachmentType;
use serenity::utils::parse_emoji; use serenity::utils::parse_emoji;
use crate::discord::helper::send_webhook_msg_to_player_channels;
use crate::game::global_data::GlobalData; use crate::game::global_data::GlobalData;
use crate::game::MessageSource; use crate::game::message_router::{dispatch_message, MessageDest};
use crate::game::message_router::{Median, MessageSource, WoxlfMessage};
pub struct Handler {} pub struct Handler {}
@ -26,7 +26,7 @@ impl EventHandler for Handler {
let global_data = data.get::<GlobalData>().unwrap(); let global_data = data.get::<GlobalData>().unwrap();
let mut global_data = global_data.lock().await; let global_data = global_data.lock().await;
if global_data.game_state.is_none() { if global_data.game_state.is_none() {
// no game in progress // no game in progress
@ -43,7 +43,6 @@ impl EventHandler for Handler {
return; return;
} }
let guild = msg.guild(&ctx.cache).await.unwrap();
let user_msg = msg.content.clone(); let user_msg = msg.content.clone();
let re = regex::Regex::new(r"<a?:.+:\d+>").unwrap(); let re = regex::Regex::new(r"<a?:.+:\d+>").unwrap();
@ -52,7 +51,6 @@ impl EventHandler for Handler {
if let Some(emoji) = parse_emoji(&emoji_cap[0]) { if let Some(emoji) = parse_emoji(&emoji_cap[0]) {
if !msg if !msg
.guild(&ctx.cache) .guild(&ctx.cache)
.await
.unwrap() .unwrap()
.emojis .emojis
.contains_key(&emoji.id) .contains_key(&emoji.id)
@ -66,21 +64,20 @@ impl EventHandler for Handler {
let attachments: Vec<AttachmentType> = msg let attachments: Vec<AttachmentType> = msg
.attachments .attachments
.iter() .iter()
.map(|a| AttachmentType::Image(&a.url)) .map(|a| AttachmentType::Image((a.url).parse().unwrap()))
.collect(); .collect();
let msg_source = MessageSource::Player(Box::new(player_data.clone())); let woxlf_msg = WoxlfMessage::default()
.source(MessageSource::Player(Box::new(player_data.clone())))
.dest(MessageDest::Broadcast)
.median(Median::Webhook)
.content(&user_msg)
.attachments(attachments)
.clone();
send_webhook_msg_to_player_channels( dispatch_message(&ctx, &global_data, woxlf_msg)
&ctx, .await
&guild, .expect("Unable to send message to players");
&mut global_data,
msg_source,
&user_msg,
Some(attachments),
)
.await
.expect("Unable to send message to players");
} }
} }

View File

@ -1,9 +1,8 @@
use serenity::client::Context; use serenity::client::Context;
use serenity::framework::standard::Args; use serenity::framework::standard::Args;
use serenity::http::{AttachmentType, Http}; use serenity::model::channel::{PermissionOverwrite, PermissionOverwriteType};
use serenity::model::channel::{Message, PermissionOverwrite, PermissionOverwriteType};
use serenity::model::guild::{Guild, Member}; use serenity::model::guild::{Guild, Member};
use serenity::model::id::{ChannelId, UserId}; use serenity::model::id::ChannelId;
use serenity::model::Permissions; use serenity::model::Permissions;
use crate::error; use crate::error;
@ -11,201 +10,7 @@ use crate::error::WoxlfError;
use crate::game::game_state::PhaseDuration; use crate::game::game_state::PhaseDuration;
use crate::game::global_data::GlobalData; use crate::game::global_data::GlobalData;
use crate::game::player_data::PlayerData; use crate::game::player_data::PlayerData;
use crate::game::MessageSource;
use crate::imgur::Image; use crate::imgur::Image;
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,
global_data: &mut GlobalData,
msg_source: MessageSource,
msg: &str,
attachment: Option<Vec<AttachmentType<'_>>>,
pin: bool,
) -> error::Result<()> {
let msg_tasks = global_data
.game_state_mut()?
.player_data
.iter()
.filter(|player| filter_source_channel(player, &msg_source))
.map(|player_data| {
let channel = guild
.channels
.get(&ChannelId::from(player_data.channel))
.unwrap();
channel.send_message(&ctx.http, |m| {
m.content(&msg);
if let Some(attachment) = attachment.clone() {
m.add_files(attachment);
}
m
})
});
let msgs: Result<Vec<Message>, SerenityError> = futures::future::join_all(msg_tasks)
.await
.into_iter()
.collect();
let msgs = msgs?;
if pin {
let pin_tasks = msgs.iter().map(|msg| msg.pin(&ctx.http));
let pins: Result<(), SerenityError> = futures::future::join_all(pin_tasks)
.await
.into_iter()
.collect();
pins?;
}
let host_channel = guild
.channels
.get(&ChannelId::from(
global_data.cfg.discord_config.host_channel,
))
.unwrap();
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(),
};
host_channel
.send_message(&ctx.http, |m| {
m.content(format!("({}): {}", source, msg));
if let Some(attachment) = attachment {
m.add_files(attachment);
}
m
})
.await?;
Ok(())
}
pub async fn send_webhook_msg(
http: &Http,
webhook_id: u64,
username: &str,
profile_pic_url: Option<String>,
msg: &str,
attachment: Option<Vec<AttachmentType<'_>>>,
) -> 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);
}
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<Vec<AttachmentType<'_>>>,
) -> 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 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| filter_source_channel(player, &msg_source))
.map(|player_data| {
send_webhook_msg(
&ctx.http,
player_data.channel_webhook_id,
&msg_username,
profile_pic.clone(),
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.cfg.discord_config.host_webhook_id,
&host_channel_username,
profile_pic,
msg,
attachment,
)
.await?;
Ok(())
}
pub async fn add_user_to_game( pub async fn add_user_to_game(
ctx: &Context, ctx: &Context,
@ -237,7 +42,7 @@ pub async fn add_user_to_game(
.await?; .await?;
let allow = let allow =
Permissions::SEND_MESSAGES | Permissions::READ_MESSAGE_HISTORY | Permissions::READ_MESSAGES; Permissions::SEND_MESSAGES | Permissions::READ_MESSAGE_HISTORY | Permissions::VIEW_CHANNEL;
let overwrite = PermissionOverwrite { let overwrite = PermissionOverwrite {
allow, allow,

View File

@ -19,6 +19,7 @@ pub enum WoxlfError {
RanOutOfCodenames, RanOutOfCodenames,
TemplateError(tera::Error), TemplateError(tera::Error),
RanOutOfProfilePics, RanOutOfProfilePics,
UnsupportedMsgMedium,
} }
impl std::error::Error for WoxlfError {} impl std::error::Error for WoxlfError {}
@ -41,6 +42,9 @@ impl Display for WoxlfError {
} }
WoxlfError::TemplateError(e) => format!("Template error: {}", e), WoxlfError::TemplateError(e) => format!("Template error: {}", e),
WoxlfError::RanOutOfProfilePics => "Ran out of user profile pics".to_string(), WoxlfError::RanOutOfProfilePics => "Ran out of user profile pics".to_string(),
WoxlfError::UnsupportedMsgMedium => {
"Tried to send a message over an unsupported medium".to_string()
}
}; };
write!(f, "Woxlf Error: {}", msg) write!(f, "Woxlf Error: {}", msg)

View File

@ -0,0 +1,315 @@
use crate::error;
use crate::error::WoxlfError;
use crate::game::global_data::GlobalData;
use crate::game::player_data::PlayerData;
use bitflags::bitflags;
use serenity::client::Context;
use serenity::http::{CacheHttp, Http};
use serenity::model::guild::Guild;
use serenity::model::id::{ChannelId, UserId};
use serenity::model::prelude::{AttachmentType, GuildId, WebhookId};
use serenity::utils::MessageBuilder;
#[derive(Debug, Clone)]
pub enum MessageSource {
Player(Box<PlayerData>),
Host,
Automated,
}
impl Default for MessageSource {
fn default() -> Self {
Self::Automated
}
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub enum MessageDest {
Player(Box<PlayerData>),
Host,
Broadcast,
}
impl Default for MessageDest {
fn default() -> Self {
Self::Broadcast
}
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub enum Median {
DirectMessage,
Webhook,
StandardMessage,
}
impl Default for Median {
fn default() -> Self {
Self::Webhook
}
}
bitflags! {
#[derive(Default)]
pub struct MsgFlags: u32 {
const PIN_MSG = 0b00000001;
}
}
#[derive(Debug, Clone, Default)]
pub struct WoxlfMessage<'a> {
pub source: MessageSource,
pub dest: MessageDest,
pub median: Median,
pub content: String,
pub attachments: Option<Vec<AttachmentType<'a>>>,
pub flags: MsgFlags,
}
#[allow(dead_code)]
impl<'a> WoxlfMessage<'a> {
pub fn get_profile_pic(&self, global_data: &GlobalData) -> Result<String, WoxlfError> {
Ok(match &self.source {
MessageSource::Player(p) => p.profile_pic_url.clone(),
MessageSource::Host | MessageSource::Automated => {
global_data.game_cfg()?.bot_profile_pic.clone()
}
})
}
pub fn get_message_username(&self, global_data: &GlobalData) -> Result<String, WoxlfError> {
Ok(match &self.source {
MessageSource::Player(p) => p.codename.clone(),
MessageSource::Host => global_data.game_cfg()?.bot_name.clone(),
MessageSource::Automated => "Woxlf System Message".to_string(),
})
}
pub fn source(mut self, source: MessageSource) -> Self {
self.source = source;
self
}
pub fn dest(mut self, dest: MessageDest) -> Self {
self.dest = dest;
self
}
pub fn median(mut self, median: Median) -> Self {
self.median = median;
self
}
pub fn content(mut self, content: &str) -> Self {
self.content = content.to_string();
self
}
pub fn attachments(mut self, attachments: Vec<AttachmentType<'a>>) -> Self {
self.attachments = Some(attachments);
self
}
pub fn flags(mut self, flags: MsgFlags) -> Self {
self.flags = flags;
self
}
pub fn pin(mut self) -> Self {
self.flags |= MsgFlags::PIN_MSG;
self
}
}
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
}
async fn send_webhook_msg(
http: &Http,
webhook_id: WebhookId,
username: &str,
profile_pic_url: Option<String>,
msg: &str,
attachments: &Option<Vec<AttachmentType<'_>>>,
) -> error::Result<()> {
let webhook = http.get_webhook(webhook_id.0).await?;
webhook
.execute(http, false, move |w| {
w.content(&msg).username(username);
if let Some(profile_pic_url) = profile_pic_url {
w.avatar_url(profile_pic_url);
}
if let Some(attachments) = attachments {
w.add_files(attachments.clone());
}
w
})
.await?;
Ok(())
}
async fn send_private_message(
http: &Http,
src_username: &str,
dest: UserId,
msg: &str,
attachments: &Option<Vec<AttachmentType<'_>>>,
) -> error::Result<()> {
let dest_user = dest.to_user(http).await?;
let mut dm_message = MessageBuilder::new();
dm_message.push_bold_line_safe(format!("{} has sent you a DM:", src_username));
dm_message.push(msg);
dest_user
.dm(http, |msg| {
msg.content(dm_message);
if let Some(attachments) = attachments {
msg.add_files(attachments.clone());
}
msg
})
.await?;
Ok(())
}
async fn send_to_host_channel(
http: &Http,
guild: &Guild,
global_data: &GlobalData,
msg: WoxlfMessage<'_>,
) -> error::Result<()> {
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 dest = match &msg.median {
Median::DirectMessage => {
if let MessageDest::Player(dest_player) = &msg.dest {
let name = guild
.members
.get(&UserId::from(dest_player.discord_id))
.unwrap()
.display_name();
format!(" to {} ({})", dest_player.codename, name)
} else {
"".to_string()
}
}
_ => "".to_string(),
};
let host_channel_username = format!(
"{} ({}){}",
msg.get_message_username(global_data)?,
source,
dest
);
send_webhook_msg(
http,
WebhookId::from(global_data.cfg.discord_config.host_webhook_id),
&host_channel_username,
Some(msg.get_profile_pic(global_data)?),
&msg.content,
&msg.attachments,
)
.await?;
Ok(())
}
pub async fn send_message(
ctx: &Context,
global_data: &GlobalData,
msg: &WoxlfMessage<'_>,
dest_player: &PlayerData,
) -> Result<(), WoxlfError> {
match &msg.median {
Median::Webhook => {
send_webhook_msg(
&ctx.http,
WebhookId::from(dest_player.channel_webhook_id),
&msg.get_message_username(global_data)?,
Some(msg.get_profile_pic(global_data)?),
&msg.content,
&msg.attachments,
)
.await?;
}
Median::DirectMessage => {
send_private_message(
&ctx.http,
&msg.get_message_username(global_data)?,
UserId(dest_player.discord_id),
&msg.content,
&msg.attachments,
)
.await?;
}
Median::StandardMessage => {
let channel = ChannelId::from(dest_player.channel);
channel.say(&ctx.http(), &msg.content).await?;
}
};
Ok(())
}
pub async fn dispatch_message(
ctx: &Context,
global_data: &GlobalData,
msg: WoxlfMessage<'_>,
) -> error::Result<()> {
let guild_id = global_data.cfg.discord_config.guild_id;
let guild = GuildId::from(guild_id).to_guild_cached(&ctx.cache).unwrap();
let msg_tasks = global_data
.game_state()?
.player_data
.iter()
.filter(|player| filter_source_channel(player, &msg.source))
.filter(|player| match &msg.dest {
MessageDest::Player(dest_user) => dest_user.discord_id == player.discord_id,
MessageDest::Host => false,
MessageDest::Broadcast => true,
})
.map(|p| send_message(ctx, global_data, &msg, p));
let results: Result<(), WoxlfError> = futures::future::join_all(msg_tasks)
.await
.into_iter()
.collect();
results?;
send_to_host_channel(&ctx.http, &guild, global_data, msg).await?;
Ok(())
}

View File

@ -1,9 +1,8 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::game::player_data::PlayerData;
pub mod game_state; pub mod game_state;
pub mod global_data; pub mod global_data;
pub mod message_router;
pub mod player_data; pub mod player_data;
#[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq, Copy)] #[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq, Copy)]
@ -17,10 +16,3 @@ impl Default for Phase {
Self::Night Self::Night
} }
} }
#[derive(Debug, Clone)]
pub enum MessageSource {
Player(Box<PlayerData>),
Host,
Automated,
}

View File

@ -1,6 +1,5 @@
use std::sync::Arc; use std::sync::Arc;
use serenity::client::bridge::gateway::GatewayIntents;
use serenity::prelude::*; use serenity::prelude::*;
use structopt::StructOpt; use structopt::StructOpt;
@ -32,10 +31,9 @@ async fn main() {
.expect("Unable to open saved game state."); .expect("Unable to open saved game state.");
} }
let mut client = Client::builder(&bot_cfg.discord_config.token) let mut client = Client::builder(&bot_cfg.discord_config.token, GatewayIntents::all())
.event_handler(Handler {}) .event_handler(Handler {})
.framework(command_framework()) .framework(command_framework())
.intents(GatewayIntents::all())
.type_map_insert::<GlobalData>(Arc::new(Mutex::new(global_data))) .type_map_insert::<GlobalData>(Arc::new(Mutex::new(global_data)))
.await .await
.expect("Err creating client"); .expect("Err creating client");

View File

@ -8,9 +8,9 @@ use serenity::prelude::Mentionable;
use std::collections::HashMap; use std::collections::HashMap;
use tera::{Tera, Value}; use tera::{Tera, Value};
fn time_to_discord_time( type TeraFnRet = Box<dyn Fn(&HashMap<String, Value>) -> tera::Result<Value> + Send + Sync>;
time_flag: &str,
) -> Box<dyn Fn(&HashMap<String, Value>) -> tera::Result<Value> + Send + Sync> { fn time_to_discord_time(time_flag: &str) -> TeraFnRet {
let time_flag = time_flag.to_string(); let time_flag = time_flag.to_string();
Box::new( Box::new(