Initial refactor of message handling

+ Split all message handling into message_router.rs
+ Added whisper command
+ Updated serenity version
+ Fmt, but clippy failing
main
Joey Hines 2023-01-03 20:06:56 -07:00 committed by Gitea
parent 2aec084712
commit 3c219f5bff
11 changed files with 637 additions and 498 deletions

568
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
[dependencies]
config = "0.12.0"
config = "0.13.3"
structopt = "0.3.26"
chrono = {version="0.4.19", features=["serde"]}
serde = "1.0.136"
@ -18,7 +18,7 @@ reqwest = "0.11.10"
tera = "1.15.0"
[dependencies.serenity]
version = "0.10.10"
version = "0.11.5"
features = ["framework", "standard_framework"]
[dependencies.tokio]

View File

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

View File

@ -10,20 +10,22 @@ use serenity::framework::standard::{
use serenity::framework::StandardFramework;
use serenity::model::guild::Member;
use serenity::model::id::ChannelId;
use serenity::model::prelude::{Message, UserId};
use serenity::model::prelude::{GuildId, Message, UserId};
use serenity::prelude::Context;
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::game::global_data::GlobalData;
use crate::game::message_router::{dispatch_message, MessageDest, MessageSource};
use crate::game::player_data::PlayerData;
use crate::game::MessageSource;
use crate::game::Phase;
use crate::messages::DiscordUser;
#[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;
#[command]
@ -34,7 +36,7 @@ async fn start(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let mut data = ctx.data.write().await;
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 game_name = args.single::<String>()?;
@ -136,7 +138,7 @@ async fn start(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 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;
@ -162,18 +164,18 @@ async fn end(ctx: &Context, msg: &Message, mut _args: Args) -> CommandResult {
async fn say(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
let mut data = ctx.data.write().await;
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;
send_msg_to_player_channels(
dispatch_message(
ctx,
&guild,
&mut global_data,
MessageSource::Host,
MessageDest::Broadcast,
args.rest(),
None,
false,
)
.await?;
@ -186,7 +188,7 @@ async fn say(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 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;
@ -194,14 +196,14 @@ async fn broadcast(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
.templates()?
.build_announcement(&global_data, args.rest())?;
send_msg_to_player_channels(
dispatch_message(
ctx,
&guild,
&mut global_data,
MessageSource::Automated,
MessageDest::Broadcast,
&msg,
None,
true,
)
.await?;
@ -214,7 +216,7 @@ async fn broadcast(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
async fn next_phase(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let mut data = ctx.data.write().await;
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;
@ -232,14 +234,14 @@ async fn next_phase(ctx: &Context, msg: &Message, mut args: Args) -> CommandResu
.templates()?
.build_announcement(&global_data, &broadcast)?;
send_msg_to_player_channels(
dispatch_message(
ctx,
&guild,
&mut global_data,
MessageSource::Automated,
MessageDest::Broadcast,
&broadcast,
None,
true,
)
.await?;
@ -251,6 +253,7 @@ async fn next_phase(ctx: &Context, msg: &Message, mut args: Args) -> CommandResu
))
.unwrap();
vote_channel
.id()
.send_message(&ctx.http, |m| {
m.content(format!(
"**{} {} Votes:**",
@ -308,7 +311,7 @@ async fn kill(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
async fn add_time(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let mut data = ctx.data.write().await;
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;
@ -332,14 +335,14 @@ async fn add_time(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
.templates()?
.build_announcement(&global_data, &broadcast)?;
send_msg_to_player_channels(
dispatch_message(
ctx,
&guild,
&mut global_data,
MessageSource::Automated,
MessageDest::Broadcast,
&broadcast,
None,
true,
)
.await?;
@ -501,7 +504,7 @@ struct Player;
async fn vote(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
let mut data = ctx.data.write().await;
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;
@ -555,6 +558,7 @@ async fn vote(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
)?;
vote_channel
.id()
.send_message(&ctx.http, |m| m.content(vote_msg))
.await?;
} else {
@ -626,7 +630,7 @@ async fn players(ctx: &Context, msg: &Message, _args: Args) -> CommandResult {
.push(alive_status);
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();
msg_builder.push_line(format!(" ({})", member.display_name()));
} else {
@ -639,6 +643,58 @@ async fn players(ctx: &Context, msg: &Message, _args: Args) -> CommandResult {
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.write().await;
let global_data = data.get::<GlobalData>().unwrap();
let mut global_data = global_data.lock().await;
let guild = GuildId::from(global_data.cfg.discord_config.guild_id)
.to_guild_cached(&ctx.cache)
.unwrap();
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 msg_src = MessageSource::Player(Box::new(src_player.clone()));
let msg_dest = MessageDest::PlayerDm(Box::new(target_player.clone()));
dispatch_message(ctx, &guild, &mut global_data, msg_src, msg_dest, pm, None).await?;
} else {
msg.reply(
&ctx.http,
format!("Could not find a player with codename {}.", target),
)
.await?;
}
}
Ok(())
}
#[help]
#[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: `{}`."]

View File

@ -1,13 +1,13 @@
use serenity::async_trait;
use serenity::client::{Context, EventHandler};
use serenity::http::AttachmentType;
use serenity::model::channel::Message;
use serenity::model::gateway::Ready;
use serenity::model::prelude::AttachmentType;
use serenity::utils::parse_emoji;
use crate::discord::helper::send_webhook_msg_to_player_channels;
use crate::game::global_data::GlobalData;
use crate::game::MessageSource;
use crate::game::message_router::MessageSource;
use crate::game::message_router::{dispatch_message, MessageDest};
pub struct Handler {}
@ -43,7 +43,7 @@ impl EventHandler for Handler {
return;
}
let guild = msg.guild(&ctx.cache).await.unwrap();
let guild = msg.guild(&ctx.cache).unwrap();
let user_msg = msg.content.clone();
let re = regex::Regex::new(r"<a?:.+:\d+>").unwrap();
@ -52,7 +52,6 @@ impl EventHandler for Handler {
if let Some(emoji) = parse_emoji(&emoji_cap[0]) {
if !msg
.guild(&ctx.cache)
.await
.unwrap()
.emojis
.contains_key(&emoji.id)
@ -66,16 +65,17 @@ impl EventHandler for Handler {
let attachments: Vec<AttachmentType> = msg
.attachments
.iter()
.map(|a| AttachmentType::Image(&a.url))
.map(|a| AttachmentType::Image((a.url).parse().unwrap()))
.collect();
let msg_source = MessageSource::Player(Box::new(player_data.clone()));
send_webhook_msg_to_player_channels(
dispatch_message(
&ctx,
&guild,
&mut global_data,
msg_source,
MessageDest::Broadcast,
&user_msg,
Some(attachments),
)

View File

@ -1,9 +1,8 @@
use serenity::client::Context;
use serenity::framework::standard::Args;
use serenity::http::{AttachmentType, Http};
use serenity::model::channel::{Message, PermissionOverwrite, PermissionOverwriteType};
use serenity::model::channel::{PermissionOverwrite, PermissionOverwriteType};
use serenity::model::guild::{Guild, Member};
use serenity::model::id::{ChannelId, UserId};
use serenity::model::id::ChannelId;
use serenity::model::Permissions;
use crate::error;
@ -11,201 +10,7 @@ use crate::error::WoxlfError;
use crate::game::game_state::PhaseDuration;
use crate::game::global_data::GlobalData;
use crate::game::player_data::PlayerData;
use crate::game::MessageSource;
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(
ctx: &Context,
@ -237,7 +42,7 @@ pub async fn add_user_to_game(
.await?;
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 {
allow,

View File

@ -19,6 +19,7 @@ pub enum WoxlfError {
RanOutOfCodenames,
TemplateError(tera::Error),
RanOutOfProfilePics,
UnsupportedMsgMedium,
}
impl std::error::Error for WoxlfError {}
@ -41,6 +42,9 @@ impl Display for WoxlfError {
}
WoxlfError::TemplateError(e) => format!("Template error: {}", e),
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)

View File

@ -0,0 +1,227 @@
use crate::error;
use crate::game::global_data::GlobalData;
use crate::game::player_data::PlayerData;
use serenity::client::Context;
use serenity::http::Http;
use serenity::model::guild::Guild;
use serenity::model::id::UserId;
use serenity::model::prelude::AttachmentType;
use serenity::utils::MessageBuilder;
#[derive(Debug, Clone)]
pub enum MessageSource {
Player(Box<PlayerData>),
Host,
Automated,
}
#[derive(Debug, Clone)]
pub enum MessageDest {
PlayerChannel(Box<PlayerData>),
PlayerDm(Box<PlayerData>),
Broadcast,
}
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: u64,
username: &str,
profile_pic_url: Option<String>,
msg: &str,
attachments: &Option<Vec<AttachmentType<'_>>>,
) -> error::Result<()> {
let webhook = http.get_webhook(webhook_id).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: &mut GlobalData,
msg_username: &str,
msg_source: MessageSource,
msg_dest: MessageDest,
profile_pic_url: Option<String>,
msg: &str,
attachments: &Option<Vec<AttachmentType<'_>>>,
) -> 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_dest {
MessageDest::PlayerChannel(p) | MessageDest::PlayerDm(p) => {
let name = guild
.members
.get(&UserId::from(p.discord_id))
.unwrap()
.display_name();
format!(" to {} ({})", p.codename, name)
}
MessageDest::Broadcast => "".to_string(),
};
let host_channel_username = format!("{} ({}){}", msg_username, source, dest);
send_webhook_msg(
http,
global_data.cfg.discord_config.host_webhook_id,
&host_channel_username,
profile_pic_url,
msg,
attachments,
)
.await?;
Ok(())
}
pub async fn dispatch_message(
ctx: &Context,
guild: &Guild,
global_data: &mut GlobalData,
msg_source: MessageSource,
msg_dest: MessageDest,
msg: &str,
attachments: 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 => {
Some(global_data.game_cfg()?.bot_profile_pic.clone())
}
};
let msg_tasks: Vec<&PlayerData> = global_data
.game_state_mut()?
.player_data
.iter()
.filter(|player| filter_source_channel(player, &msg_source))
.collect();
for player_data in msg_tasks {
match &msg_dest {
MessageDest::PlayerChannel(dest_player) => {
if dest_player.discord_id == player_data.discord_id {
send_webhook_msg(
&ctx.http,
player_data.channel_webhook_id,
&msg_username,
profile_pic.clone(),
msg,
&attachments,
)
.await?;
}
}
MessageDest::PlayerDm(dest_player) => {
send_private_message(
&ctx.http,
&msg_username,
UserId(dest_player.discord_id),
msg,
&attachments,
)
.await?
}
MessageDest::Broadcast => {
send_webhook_msg(
&ctx.http,
player_data.channel_webhook_id,
&msg_username,
profile_pic.clone(),
msg,
&attachments,
)
.await?
}
}
}
send_to_host_channel(
&ctx.http,
guild,
global_data,
&msg_username,
msg_source,
msg_dest,
profile_pic,
msg,
&attachments,
)
.await?;
Ok(())
}

View File

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

View File

@ -1,6 +1,5 @@
use std::sync::Arc;
use serenity::client::bridge::gateway::GatewayIntents;
use serenity::prelude::*;
use structopt::StructOpt;
@ -32,10 +31,9 @@ async fn main() {
.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 {})
.framework(command_framework())
.intents(GatewayIntents::all())
.type_map_insert::<GlobalData>(Arc::new(Mutex::new(global_data)))
.await
.expect("Err creating client");

View File

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