Added commands + persistence

+ `say`: used by the host to speak in the game chat
+ `end`: ends the game and cleans up
+ `broadcast`: Send an important game message that that is pinned in each channel
+ Persistent allows games to continue across bot restarts
+ clippy + fmt
msg_refactor
Joey Hines 2022-03-06 10:03:49 -07:00
parent fd878c3721
commit e72d603149
No known key found for this signature in database
GPG Key ID: 80F567B5C968F91B
7 changed files with 278 additions and 83 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
/target /target
config.toml config.toml
wOxlf_data.toml
.idea .idea

1
Cargo.lock generated
View File

@ -1729,6 +1729,7 @@ dependencies = [
"serenity", "serenity",
"structopt", "structopt",
"tokio", "tokio",
"toml",
] ]
[[package]] [[package]]

View File

@ -11,6 +11,7 @@ structopt = "0.3.26"
chrono = "0.4.19" chrono = "0.4.19"
serde = "1.0.136" serde = "1.0.136"
rand = "0.8.5" rand = "0.8.5"
toml = "0.5.8"
[dependencies.serenity] [dependencies.serenity]
version = "0.10.10" version = "0.10.10"

View File

@ -1,18 +1,19 @@
use serenity::prelude::Context; use crate::data::{BotConfig, GlobalData, MessageSource, PlayerData};
use serenity::model::prelude::{Message, UserId}; use crate::helper::{clear_game_state, save_game_state, send_msg_to_player_channels};
use serenity::framework::standard::{CommandResult, Args};
use serenity::framework::StandardFramework;
use serenity::framework::standard::macros::{command, group};
use crate::data::{GlobalData, BotConfig, PlayerData};
use rand::Rng; use rand::Rng;
use serenity::model::guild::{Member, Guild}; use serenity::framework::standard::macros::{command, group};
use serenity::model::id::ChannelId; use serenity::framework::standard::{Args, CommandResult};
use serenity::model::Permissions; use serenity::framework::StandardFramework;
use serenity::model::channel::{PermissionOverwrite, PermissionOverwriteType}; use serenity::model::channel::{PermissionOverwrite, PermissionOverwriteType};
use serenity::model::guild::{Guild, Member};
use serenity::model::id::ChannelId;
use serenity::model::prelude::{Message, UserId};
use serenity::model::Permissions;
use serenity::prelude::Context;
use serenity::utils::MessageBuilder; use serenity::utils::MessageBuilder;
#[group] #[group]
#[commands(start)] #[commands(start, say, end, broadcast)]
struct Host; struct Host;
fn generate_codename(config: &BotConfig) -> String { fn generate_codename(config: &BotConfig) -> String {
@ -24,21 +25,28 @@ fn generate_codename(config: &BotConfig) -> String {
format!("{} {}", adj, occupation) format!("{} {}", adj, occupation)
} }
async fn add_user_to_game(ctx: &Context, guild: &Guild, global_data: &mut GlobalData, discord_user: &Member) { async fn add_user_to_game(
ctx: &Context,
guild: &Guild,
global_data: &mut GlobalData,
discord_user: &Member,
) -> CommandResult {
let mut codename = generate_codename(&global_data.cfg); let mut codename = generate_codename(&global_data.cfg);
while global_data.game_state.codename_exists(&mut codename) { while global_data.game_state.codename_exists(&codename) {
codename = generate_codename(&global_data.cfg); codename = generate_codename(&global_data.cfg);
} }
let channel = guild
.create_channel(&ctx.http, |c| {
c.category(&ChannelId::from(global_data.cfg.category))
.name(format!("{}'s Channel", discord_user.display_name()))
})
.await?;
let channel = guild.create_channel(&ctx.http, |c| { let allow = Permissions::SEND_MESSAGES
c | Permissions::READ_MESSAGE_HISTORY
.category(&ChannelId::from(global_data.cfg.category)) | Permissions::READ_MESSAGE_HISTORY;
.name(format!("{}'s Channel", discord_user.display_name()))
}).await.unwrap();
let allow = Permissions::SEND_MESSAGES | Permissions::READ_MESSAGE_HISTORY | Permissions::READ_MESSAGE_HISTORY;
let overwrite = PermissionOverwrite { let overwrite = PermissionOverwrite {
allow, allow,
@ -46,7 +54,7 @@ async fn add_user_to_game(ctx: &Context, guild: &Guild, global_data: &mut Global
kind: PermissionOverwriteType::Member(discord_user.user.id), kind: PermissionOverwriteType::Member(discord_user.user.id),
}; };
channel.create_permission(&ctx.http, &overwrite).await.unwrap(); channel.create_permission(&ctx.http, &overwrite).await?;
let msg = channel.send_message(&ctx.http, |m| { let msg = channel.send_message(&ctx.http, |m| {
m.content(MessageBuilder::new() m.content(MessageBuilder::new()
@ -59,18 +67,20 @@ async fn add_user_to_game(ctx: &Context, guild: &Guild, global_data: &mut Global
.push("SUBJECT CODENAME: ") .push("SUBJECT CODENAME: ")
.push_line(&codename) .push_line(&codename)
) )
}).await.unwrap(); }).await?;
channel.pin(&ctx.http, msg.id).await.unwrap(); channel.pin(&ctx.http, msg.id).await?;
let player_data = PlayerData { let player_data = PlayerData {
channel: channel.id.0,
discord_id: discord_user.user.id.0, discord_id: discord_user.user.id.0,
codename: codename codename,
}; };
global_data.game_state.player_channels.insert(channel.id.0, player_data); global_data.game_state.player_data.push(player_data);
}
Ok(())
}
#[command] #[command]
#[only_in(guilds)] #[only_in(guilds)]
@ -78,33 +88,99 @@ async fn add_user_to_game(ctx: &Context, guild: &Guild, global_data: &mut Global
async fn start(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { async fn start(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
msg.channel_id.say(&ctx.http, "Starting game").await?; msg.channel_id.say(&ctx.http, "Starting game").await?;
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).await.unwrap();
let mut global_data = global_data.lock().await; let mut global_data = global_data.lock().await;
global_data.game_state.clear(); clear_game_state(&mut global_data).unwrap();
for player in args.iter::<u64>() { for player in args.iter::<u64>().flatten() {
if let Ok(player) = player { if let Some(discord_user) = guild.members.get(&UserId::from(player)) {
if let Some(discord_user) = guild.members.get(&UserId::from(player)) { add_user_to_game(ctx, &guild, &mut global_data, discord_user).await?;
add_user_to_game(ctx, &guild, &mut global_data, &discord_user).await; } else {
} msg.reply(
else { &ctx.http,
msg.reply(&ctx.http,format!("User {} is invalid or not in this server!", player)).await.unwrap(); format!("User {} is invalid or not in this server!", player),
break; )
} .await?;
break;
} }
} }
save_game_state(&global_data).unwrap();
Ok(())
}
#[command]
#[only_in(guilds)]
#[allowed_roles("wolfx host")]
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 mut global_data = global_data.lock().await;
for player_data in &global_data.game_state.player_data {
let channel = guild
.channels
.get(&ChannelId::from(player_data.channel))
.unwrap();
channel.delete(&ctx.http).await?;
}
clear_game_state(&mut global_data).unwrap();
Ok(())
}
#[command]
#[only_in(guilds)]
#[allowed_roles("wolfx host")]
async fn say(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
let data = ctx.data.read().await;
let global_data = data.get::<GlobalData>().unwrap();
let guild = msg.guild(&ctx.cache).await.unwrap();
let global_data = global_data.lock().await;
let msg = format!("**wOxlf **> {}", args.rest());
send_msg_to_player_channels(ctx, &guild, &global_data, MessageSource::Host, &msg, false).await;
Ok(())
}
#[command]
#[only_in(guilds)]
#[allowed_roles("wolfx host")]
async fn broadcast(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
let data = ctx.data.read().await;
let global_data = data.get::<GlobalData>().unwrap();
let guild = msg.guild(&ctx.cache).await.unwrap();
let global_data = global_data.lock().await;
let msg = MessageBuilder::new()
.push_bold_line("\\*\\*IMPORTANT wOxlf SYSTEM MESSAGE\\*\\*")
.push_line("")
.push_line(args.rest())
.push_line("")
.push_bold_line("\\*\\*END OF SYSTEM MESSAGE\\*\\*")
.build();
send_msg_to_player_channels(ctx, &guild, &global_data, MessageSource::Host, &msg, true).await;
Ok(()) Ok(())
} }
pub fn command_framework() -> StandardFramework { pub fn command_framework() -> StandardFramework {
StandardFramework::new() StandardFramework::new()
.configure(|c| { .configure(|c| c.prefix("!"))
c.prefix("!")
})
.group(&HOST_GROUP) .group(&HOST_GROUP)
} }

View File

@ -1,17 +1,16 @@
use config::{Config, File}; use config::{Config, File};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::path::Path;
use std::collections::HashMap;
use serenity::prelude::TypeMapKey; use serenity::prelude::TypeMapKey;
use std::sync::Arc; use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc;
use structopt::StructOpt; use structopt::StructOpt;
use tokio::sync::Mutex; use tokio::sync::Mutex;
#[derive(Debug, StructOpt)] #[derive(Debug, StructOpt)]
#[structopt(name = "WOXlf", about = "WOXlf discord bot")] #[structopt(name = "WOXlf", about = "WOXlf discord bot")]
pub struct Args { pub struct Args {
pub cfg_path: PathBuf pub cfg_path: PathBuf,
} }
#[derive(Debug, Deserialize, Serialize, Clone)] #[derive(Debug, Deserialize, Serialize, Clone)]
@ -20,8 +19,9 @@ pub struct BotConfig {
pub app_id: u64, pub app_id: u64,
pub host_channel: u64, pub host_channel: u64,
pub category: u64, pub category: u64,
pub game_state_dir: PathBuf,
pub occupation: Vec<String>, pub occupation: Vec<String>,
pub adjective: Vec<String> pub adjective: Vec<String>,
} }
impl BotConfig { impl BotConfig {
@ -32,12 +32,16 @@ impl BotConfig {
cfg.try_deserialize() cfg.try_deserialize()
} }
pub fn get_game_state_path(&self) -> PathBuf {
self.game_state_dir.join("wOxlf_data.toml")
}
} }
#[derive(Debug, Deserialize, Serialize, Clone)] #[derive(Debug, Deserialize, Serialize, Clone)]
pub enum Phase { pub enum Phase {
Day, Day,
Night Night,
} }
impl Default for Phase { impl Default for Phase {
@ -48,41 +52,45 @@ impl Default for Phase {
#[derive(Debug, Deserialize, Serialize, Clone, Default, Hash)] #[derive(Debug, Deserialize, Serialize, Clone, Default, Hash)]
pub struct PlayerData { pub struct PlayerData {
pub channel: u64,
pub discord_id: u64, pub discord_id: u64,
pub codename: String, pub codename: String,
} }
#[derive(Debug, Deserialize, Serialize, Clone, Default)] #[derive(Debug, Deserialize, Serialize, Clone, Default)]
pub struct GameState { pub struct GameState {
pub player_channels: HashMap<u64, PlayerData>,
pub current_phase: Phase, pub current_phase: Phase,
pub player_data: Vec<PlayerData>,
} }
impl GameState { impl GameState {
pub fn codename_exists(&self, codename: &str) -> bool { pub fn codename_exists(&self, codename: &str) -> bool {
self.player_channels.iter().any(|(_, data)| { self.player_data
data.codename.to_lowercase() == codename .iter()
}) .any(|data| data.codename.to_lowercase() == codename)
} }
pub fn clear(&mut self) { pub fn clear(&mut self) {
self.player_channels.clear(); self.player_data.clear();
self.current_phase = Phase::Night; self.current_phase = Phase::Night;
} }
pub fn get_player_from_channel(&self, channel_id: u64) -> Option<&PlayerData> {
self.player_data.iter().find(|p| p.channel == channel_id)
}
} }
#[derive(Debug, Deserialize, Serialize, Clone)] #[derive(Debug, Deserialize, Serialize, Clone)]
pub struct GlobalData { pub struct GlobalData {
pub cfg: BotConfig, pub cfg: BotConfig,
pub game_state: GameState pub game_state: GameState,
} }
impl GlobalData { impl GlobalData {
pub fn new(cfg: BotConfig) -> Self { pub fn new(cfg: BotConfig) -> Self {
Self { Self {
cfg, cfg,
game_state: GameState::default() game_state: GameState::default(),
} }
} }
} }
@ -91,3 +99,9 @@ impl TypeMapKey for GlobalData {
type Value = Arc<Mutex<GlobalData>>; type Value = Arc<Mutex<GlobalData>>;
} }
#[derive(Debug, Deserialize, Serialize, Clone)]
pub enum MessageSource {
Player(u64),
Host,
Automated,
}

99
src/helper.rs 100644
View File

@ -0,0 +1,99 @@
use crate::data::{GlobalData, MessageSource};
use serenity::model::id::UserId;
use serenity::model::prelude::ChannelId;
use serenity::model::prelude::Guild;
use serenity::prelude::Context;
use std::fs::File;
use std::io::prelude::*;
pub async fn send_msg_to_player_channels(
ctx: &Context,
guild: &Guild,
global_data: &GlobalData,
msg_source: MessageSource,
msg: &str,
pin: bool,
) {
for player_data in &global_data.game_state.player_data {
if let MessageSource::Player(channel_id) = msg_source {
if channel_id == player_data.channel {
continue;
}
}
let channel = guild
.channels
.get(&ChannelId::from(player_data.channel))
.unwrap();
let msg = channel
.send_message(&ctx.http, |m| m.content(&msg))
.await
.unwrap();
if pin {
// pin system messages
msg.pin(&ctx.http).await.unwrap();
}
}
let host_channel = guild
.channels
.get(&ChannelId::from(global_data.cfg.host_channel))
.unwrap();
let source = match msg_source {
MessageSource::Player(channel_id) => {
let discord_id = global_data
.game_state
.get_player_from_channel(channel_id)
.unwrap()
.discord_id;
let name = guild
.members
.get(&UserId::from(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)))
.await
.unwrap();
}
pub fn save_game_state(global_data: &GlobalData) -> std::io::Result<()> {
let s = toml::to_string_pretty(&global_data.game_state).unwrap();
let mut file = File::create(global_data.cfg.get_game_state_path())?;
file.write_all(s.as_bytes())
}
pub fn get_game_state(global_data: &mut GlobalData) -> std::io::Result<()> {
let mut file = File::open(global_data.cfg.get_game_state_path())?;
let mut data = String::new();
file.read_to_string(&mut data)?;
global_data.game_state = toml::from_str(&data).unwrap();
Ok(())
}
pub fn clear_game_state(global_data: &mut GlobalData) -> std::io::Result<()> {
global_data.game_state.clear();
let state_path = global_data.cfg.get_game_state_path();
if state_path.exists() {
std::fs::remove_file(global_data.cfg.get_game_state_path())?;
}
Ok(())
}

View File

@ -1,15 +1,16 @@
mod data;
mod commands; mod commands;
mod data;
mod helper;
use serenity::prelude::*;
use serenity::model::prelude::{Message, Ready};
use std::sync::Arc;
use serenity::client::bridge::gateway::GatewayIntents;
use crate::data::{GlobalData, Args, BotConfig};
use serenity::async_trait;
use crate::commands::command_framework; use crate::commands::command_framework;
use crate::data::{Args, BotConfig, GlobalData, MessageSource};
use crate::helper::{get_game_state, send_msg_to_player_channels};
use serenity::async_trait;
use serenity::client::bridge::gateway::GatewayIntents;
use serenity::model::prelude::{Message, Ready};
use serenity::prelude::*;
use std::sync::Arc;
use structopt::StructOpt; use structopt::StructOpt;
use serenity::model::id::ChannelId;
struct Handler {} struct Handler {}
@ -20,36 +21,32 @@ impl EventHandler for Handler {
return; return;
} }
if msg.content.starts_with('!') {
return;
}
let data = ctx.data.read().await; let data = ctx.data.read().await;
let game_data = data.get::<GlobalData>().unwrap(); let game_data = data.get::<GlobalData>().unwrap();
let game_data = game_data.lock().await; let game_data = game_data.lock().await;
if let Some(player_data) = game_data
if let Some(player_data) = game_data.game_state.player_channels.get(&msg.channel_id.0) { .game_state
.get_player_from_channel(msg.channel_id.0)
{
let guild = msg.guild(&ctx.cache).await.unwrap(); let guild = msg.guild(&ctx.cache).await.unwrap();
let user_msg = format!("{} > {}", player_data.codename, msg.content); let user_msg = format!("{} > {}", player_data.codename, msg.content);
for (channel, _) in &game_data.game_state.player_channels { send_msg_to_player_channels(
if *channel == msg.channel_id.0 { &ctx,
continue; &guild,
} &game_data,
MessageSource::Player(msg.channel_id.0),
let channel = guild.channels.get(&ChannelId::from(*channel)).unwrap(); &user_msg,
false,
channel.send_message(&ctx.http, |m| { )
m .await;
.content(&user_msg)
}).await.unwrap();
}
let host_channel = guild.channels.get(&ChannelId::from(game_data.cfg.host_channel)).unwrap();
host_channel.send_message(&ctx.http, |m| {
m
.content(&user_msg)
}).await.unwrap();
} }
} }
@ -64,11 +61,17 @@ async fn main() {
let bot_cfg: BotConfig = BotConfig::new(&args.cfg_path).expect("Unable to parse cfg"); let bot_cfg: BotConfig = BotConfig::new(&args.cfg_path).expect("Unable to parse cfg");
let mut global_data = GlobalData::new(bot_cfg.clone());
if get_game_state(&mut global_data).is_ok() {
println!("Resuming game...")
}
let mut client = Client::builder(&bot_cfg.token) let mut client = Client::builder(&bot_cfg.token)
.event_handler(Handler {}) .event_handler(Handler {})
.framework(command_framework()) .framework(command_framework())
.intents(GatewayIntents::all()) .intents(GatewayIntents::all())
.type_map_insert::<GlobalData>(Arc::new(Mutex::new(GlobalData::new(bot_cfg)))) .type_map_insert::<GlobalData>(Arc::new(Mutex::new(global_data)))
.await .await
.expect("Err creating client"); .expect("Err creating client");