Initial commit

msg_refactor
Joey Hines 2022-03-05 14:35:02 -07:00
commit fd878c3721
No known key found for this signature in database
GPG Key ID: 80F567B5C968F91B
8 changed files with 2099 additions and 0 deletions

3
.gitignore vendored 100644
View File

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

1741
Cargo.lock generated 100644

File diff suppressed because it is too large Load Diff

21
Cargo.toml 100644
View File

@ -0,0 +1,21 @@
[package]
name = "woxlf"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
config = "0.12.0"
structopt = "0.3.26"
chrono = "0.4.19"
serde = "1.0.136"
rand = "0.8.5"
[dependencies.serenity]
version = "0.10.10"
features = ["framework", "standard_framework"]
[dependencies.tokio]
version = "1.0"
features = ["macros", "rt-multi-thread"]

19
LICENSE 100644
View File

@ -0,0 +1,19 @@
Copyright (c) 2022 Joey Hines
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

34
README.md 100644
View File

@ -0,0 +1,34 @@
# wOXlf
Discord bot for managing an anonymous [Werewolf Game](https://en.wikipedia.org/wiki/Mafia_(party_game)).
## How It Works
A host gets a list of players to play, and then begins the game with the `!start` command.
Each player is assigned a channel where they will view the game through. The player can read and send messages
in this channel normally. When a message is sent, it is forwarded to all other channels. The message's author
is obscured by a codename.
The bot also handles communications from the host, daily votes, and notifying players of deaths.
## Example Config
```toml
# Discord Bot Token
token = ""
# Discord App Id
app_id = 0
# Channel to accept host commands from
host_channel = 1
# Category to crate the player cannels in
category = 2
# Random code names are generated in the format of <Adjective> <Occupation>
occupation = ["Engineer", "Scientist", "Dancer", "Farmer", "Captain", "Janitor", "Author", "Bartender", "Bum",
"Student", "Teacher", "Chef", "Waiter", "Comedian"]
adjective = ["Sleepy", "Drunk", "Smart", "Gifted", "Extreme", "Eccentric", "Amazing", "Bad", "Silly", "Dumb", "Smelly"]
```
## License
[MIT License](./LICENSE)

110
src/commands.rs 100644
View File

@ -0,0 +1,110 @@
use serenity::prelude::Context;
use serenity::model::prelude::{Message, UserId};
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 serenity::model::guild::{Member, Guild};
use serenity::model::id::ChannelId;
use serenity::model::Permissions;
use serenity::model::channel::{PermissionOverwrite, PermissionOverwriteType};
use serenity::utils::MessageBuilder;
#[group]
#[commands(start)]
struct Host;
fn generate_codename(config: &BotConfig) -> String {
let mut rng = rand::thread_rng();
let occupation = &config.occupation[rng.gen_range(0..config.occupation.len())];
let adj = &config.adjective[rng.gen_range(0..config.adjective.len())];
format!("{} {}", adj, occupation)
}
async fn add_user_to_game(ctx: &Context, guild: &Guild, global_data: &mut GlobalData, discord_user: &Member) {
let mut codename = generate_codename(&global_data.cfg);
while global_data.game_state.codename_exists(&mut codename) {
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.unwrap();
let allow = Permissions::SEND_MESSAGES | Permissions::READ_MESSAGE_HISTORY | Permissions::READ_MESSAGE_HISTORY;
let overwrite = PermissionOverwrite {
allow,
deny: Default::default(),
kind: PermissionOverwriteType::Member(discord_user.user.id),
};
channel.create_permission(&ctx.http, &overwrite).await.unwrap();
let msg = channel.send_message(&ctx.http, |m| {
m.content(MessageBuilder::new()
.push("Welcome ")
.mention(discord_user)
.push_line(" to your WOxlf Terminal. You may use this terminal to communicate to other subjects.")
.push_line("You will also use this terminal for choosing one of your fellow subjects for termination.")
.push_line("Happy testing :)")
.push_line("")
.push("SUBJECT CODENAME: ")
.push_line(&codename)
)
}).await.unwrap();
channel.pin(&ctx.http, msg.id).await.unwrap();
let player_data = PlayerData {
discord_id: discord_user.user.id.0,
codename: codename
};
global_data.game_state.player_channels.insert(channel.id.0, player_data);
}
#[command]
#[only_in(guilds)]
#[allowed_roles("wolfx host")]
async fn start(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
msg.channel_id.say(&ctx.http, "Starting game").await?;
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;
global_data.game_state.clear();
for player in args.iter::<u64>() {
if let Ok(player) = player {
if let Some(discord_user) = guild.members.get(&UserId::from(player)) {
add_user_to_game(ctx, &guild, &mut global_data, &discord_user).await;
}
else {
msg.reply(&ctx.http,format!("User {} is invalid or not in this server!", player)).await.unwrap();
break;
}
}
}
Ok(())
}
pub fn command_framework() -> StandardFramework {
StandardFramework::new()
.configure(|c| {
c.prefix("!")
})
.group(&HOST_GROUP)
}

93
src/data.rs 100644
View File

@ -0,0 +1,93 @@
use config::{Config, File};
use serde::{Deserialize, Serialize};
use std::path::Path;
use std::collections::HashMap;
use serenity::prelude::TypeMapKey;
use std::sync::Arc;
use std::path::PathBuf;
use structopt::StructOpt;
use tokio::sync::Mutex;
#[derive(Debug, StructOpt)]
#[structopt(name = "WOXlf", about = "WOXlf discord bot")]
pub struct Args {
pub cfg_path: PathBuf
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct BotConfig {
pub token: String,
pub app_id: u64,
pub host_channel: u64,
pub category: u64,
pub occupation: Vec<String>,
pub adjective: Vec<String>
}
impl BotConfig {
pub fn new(config_path: &Path) -> Result<Self, config::ConfigError> {
let cfg = Config::builder()
.add_source(File::from(config_path))
.build()?;
cfg.try_deserialize()
}
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub enum Phase {
Day,
Night
}
impl Default for Phase {
fn default() -> Self {
Self::Night
}
}
#[derive(Debug, Deserialize, Serialize, Clone, Default, Hash)]
pub struct PlayerData {
pub discord_id: u64,
pub codename: String,
}
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
pub struct GameState {
pub player_channels: HashMap<u64, PlayerData>,
pub current_phase: Phase,
}
impl GameState {
pub fn codename_exists(&self, codename: &str) -> bool {
self.player_channels.iter().any(|(_, data)| {
data.codename.to_lowercase() == codename
})
}
pub fn clear(&mut self) {
self.player_channels.clear();
self.current_phase = Phase::Night;
}
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct GlobalData {
pub cfg: BotConfig,
pub game_state: GameState
}
impl GlobalData {
pub fn new(cfg: BotConfig) -> Self {
Self {
cfg,
game_state: GameState::default()
}
}
}
impl TypeMapKey for GlobalData {
type Value = Arc<Mutex<GlobalData>>;
}

78
src/main.rs 100644
View File

@ -0,0 +1,78 @@
mod data;
mod commands;
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 structopt::StructOpt;
use serenity::model::id::ChannelId;
struct Handler {}
#[async_trait]
impl EventHandler for Handler {
async fn message(&self, ctx: Context, msg: Message) {
if msg.author.bot {
return;
}
let data = ctx.data.read().await;
let game_data = data.get::<GlobalData>().unwrap();
let game_data = game_data.lock().await;
if let Some(player_data) = game_data.game_state.player_channels.get(&msg.channel_id.0) {
let guild = msg.guild(&ctx.cache).await.unwrap();
let user_msg = format!("{} > {}", player_data.codename, msg.content);
for (channel, _) in &game_data.game_state.player_channels {
if *channel == msg.channel_id.0 {
continue;
}
let channel = guild.channels.get(&ChannelId::from(*channel)).unwrap();
channel.send_message(&ctx.http, |m| {
m
.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();
}
}
async fn ready(&self, _ctx: Context, ready: Ready) {
println!("{} is connected!", ready.user.name);
}
}
#[tokio::main]
async fn main() {
let args: Args = Args::from_args();
let bot_cfg: BotConfig = BotConfig::new(&args.cfg_path).expect("Unable to parse cfg");
let mut client = Client::builder(&bot_cfg.token)
.event_handler(Handler {})
.framework(command_framework())
.intents(GatewayIntents::all())
.type_map_insert::<GlobalData>(Arc::new(Mutex::new(GlobalData::new(bot_cfg))))
.await
.expect("Err creating client");
if let Err(why) = client.start().await {
println!("Client error: {:?}", why);
}
}