Initial commit
commit
fd878c3721
|
@ -0,0 +1,3 @@
|
||||||
|
/target
|
||||||
|
config.toml
|
||||||
|
.idea
|
File diff suppressed because it is too large
Load Diff
|
@ -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"]
|
|
@ -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.
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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>>;
|
||||||
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue