Compare commits

..

No commits in common. "4b7570b24ca110dc4f09377face6aa59194e980e" and "2aec0847120c38ed2c5982bf0a40efa6ced3af3f" have entirely different histories.

11 changed files with 548 additions and 764 deletions

563
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.13.3" config = "0.12.0"
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,10 +16,9 @@ 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.11.5" version = "0.10.10"
features = ["framework", "standard_framework"] features = ["framework", "standard_framework"]
[dependencies.tokio] [dependencies.tokio]

View File

@ -15,12 +15,10 @@ 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,
@ -45,7 +43,6 @@ 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,20 +14,16 @@ 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}; use crate::discord::helper::{add_user_to_game, parse_duration_arg, send_msg_to_player_channels};
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( #[commands(start, say, end, broadcast, next_phase, kill, add_time, test_theme)]
start, say, end, broadcast, next_phase, kill, add_time, test_theme, whisper
)]
struct Host; struct Host;
#[command] #[command]
@ -38,7 +34,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).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;
let game_name = args.single::<String>()?; let game_name = args.single::<String>()?;
@ -140,7 +136,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).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;
@ -163,20 +159,23 @@ 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 data = ctx.data.read().await; let mut data = ctx.data.write().await;
let global_data = data.get::<GlobalData>().unwrap(); let global_data = data.get_mut::<GlobalData>().unwrap();
let guild = msg.guild(&ctx.cache).await.unwrap();
let global_data = global_data.lock().await; let mut global_data = global_data.lock().await;
let msg = WoxlfMessage::default() send_msg_to_player_channels(
.source(MessageSource::Host) ctx,
.dest(MessageDest::Broadcast) &guild,
.median(Median::Webhook) &mut global_data,
.content(args.rest()) MessageSource::Host,
.clone(); args.rest(),
None,
dispatch_message(ctx, &global_data, msg).await?; false,
)
.await?;
Ok(()) Ok(())
} }
@ -184,24 +183,27 @@ 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 data = ctx.data.read().await; let mut data = ctx.data.write().await;
let global_data = data.get::<GlobalData>().unwrap(); let global_data = data.get_mut::<GlobalData>().unwrap();
let guild = msg.guild(&ctx.cache).await.unwrap();
let global_data = global_data.lock().await; let mut global_data = global_data.lock().await;
let broadcast = global_data let msg = global_data
.templates()? .templates()?
.build_announcement(&global_data, args.rest())?; .build_announcement(&global_data, args.rest())?;
let woxlf_msg = WoxlfMessage::default() send_msg_to_player_channels(
.source(MessageSource::Automated) ctx,
.dest(MessageDest::Broadcast) &guild,
.content(&broadcast) &mut global_data,
.median(Median::Webhook) MessageSource::Automated,
.clone(); &msg,
None,
dispatch_message(ctx, &global_data, woxlf_msg).await?; true,
)
.await?;
Ok(()) Ok(())
} }
@ -212,7 +214,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).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;
@ -230,14 +232,16 @@ async fn next_phase(ctx: &Context, msg: &Message, mut args: Args) -> CommandResu
.templates()? .templates()?
.build_announcement(&global_data, &broadcast)?; .build_announcement(&global_data, &broadcast)?;
let woxlf_msg = WoxlfMessage::default() send_msg_to_player_channels(
.source(MessageSource::Automated) ctx,
.dest(MessageDest::Broadcast) &guild,
.median(Median::Webhook) &mut global_data,
.content(&broadcast) MessageSource::Automated,
.clone(); &broadcast,
None,
dispatch_message(ctx, &global_data, woxlf_msg).await?; true,
)
.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
@ -247,7 +251,6 @@ 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:**",
@ -305,6 +308,7 @@ 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;
@ -328,14 +332,16 @@ async fn add_time(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
.templates()? .templates()?
.build_announcement(&global_data, &broadcast)?; .build_announcement(&global_data, &broadcast)?;
let woxlf_msg = WoxlfMessage::default() send_msg_to_player_channels(
.source(MessageSource::Automated) ctx,
.dest(MessageDest::Broadcast) &guild,
.median(Median::Webhook) &mut global_data,
.content(&broadcast) MessageSource::Automated,
.clone(); &broadcast,
None,
dispatch_message(ctx, &global_data, woxlf_msg).await?; true,
)
.await?;
msg.reply(&ctx.http, "Phase has been updated") msg.reply(&ctx.http, "Phase has been updated")
.await .await
@ -495,7 +501,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).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;
@ -549,7 +555,6 @@ 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 {
@ -621,7 +626,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).unwrap(); let guild = msg.guild(&ctx.cache).await.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 {
@ -634,59 +639,6 @@ 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::message_router::{dispatch_message, MessageDest}; use crate::game::MessageSource;
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 global_data = global_data.lock().await; let mut 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,6 +43,7 @@ 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();
@ -51,6 +52,7 @@ 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)
@ -64,20 +66,21 @@ impl EventHandler for Handler {
let attachments: Vec<AttachmentType> = msg let attachments: Vec<AttachmentType> = msg
.attachments .attachments
.iter() .iter()
.map(|a| AttachmentType::Image((a.url).parse().unwrap())) .map(|a| AttachmentType::Image(&a.url))
.collect(); .collect();
let woxlf_msg = WoxlfMessage::default() let msg_source = MessageSource::Player(Box::new(player_data.clone()));
.source(MessageSource::Player(Box::new(player_data.clone())))
.dest(MessageDest::Broadcast)
.median(Median::Webhook)
.content(&user_msg)
.attachments(attachments)
.clone();
dispatch_message(&ctx, &global_data, woxlf_msg) send_webhook_msg_to_player_channels(
.await &ctx,
.expect("Unable to send message to players"); &guild,
&mut global_data,
msg_source,
&user_msg,
Some(attachments),
)
.await
.expect("Unable to send message to players");
} }
} }

View File

@ -1,8 +1,9 @@
use serenity::client::Context; use serenity::client::Context;
use serenity::framework::standard::Args; use serenity::framework::standard::Args;
use serenity::model::channel::{PermissionOverwrite, PermissionOverwriteType}; use serenity::http::{AttachmentType, Http};
use serenity::model::channel::{Message, PermissionOverwrite, PermissionOverwriteType};
use serenity::model::guild::{Guild, Member}; use serenity::model::guild::{Guild, Member};
use serenity::model::id::ChannelId; use serenity::model::id::{ChannelId, UserId};
use serenity::model::Permissions; use serenity::model::Permissions;
use crate::error; use crate::error;
@ -10,7 +11,201 @@ 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,
@ -42,7 +237,7 @@ pub async fn add_user_to_game(
.await?; .await?;
let allow = let allow =
Permissions::SEND_MESSAGES | Permissions::READ_MESSAGE_HISTORY | Permissions::VIEW_CHANNEL; Permissions::SEND_MESSAGES | Permissions::READ_MESSAGE_HISTORY | Permissions::READ_MESSAGES;
let overwrite = PermissionOverwrite { let overwrite = PermissionOverwrite {
allow, allow,

View File

@ -19,7 +19,6 @@ 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 {}
@ -42,9 +41,6 @@ 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

@ -1,315 +0,0 @@
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,8 +1,9 @@
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)]
@ -16,3 +17,10 @@ impl Default for Phase {
Self::Night Self::Night
} }
} }
#[derive(Debug, Clone)]
pub enum MessageSource {
Player(Box<PlayerData>),
Host,
Automated,
}

View File

@ -1,5 +1,6 @@
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;
@ -31,9 +32,10 @@ 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, GatewayIntents::all()) let mut client = Client::builder(&bot_cfg.discord_config.token)
.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};
type TeraFnRet = Box<dyn Fn(&HashMap<String, Value>) -> tera::Result<Value> + Send + Sync>; fn time_to_discord_time(
time_flag: &str,
fn time_to_discord_time(time_flag: &str) -> TeraFnRet { ) -> Box<dyn Fn(&HashMap<String, Value>) -> tera::Result<Value> + Send + Sync> {
let time_flag = time_flag.to_string(); let time_flag = time_flag.to_string();
Box::new( Box::new(