From 8055cbb78fe8ad66868a2ffaa6a32f30af9d7107 Mon Sep 17 00:00:00 2001 From: Oupson Date: Mon, 24 Aug 2020 17:11:06 +0200 Subject: [PATCH] Initial Commit --- .gitignore | 6 + .vs/settings.json | 5 + Cargo.toml | 23 +++ src/api.rs | 34 +++++ src/commands/admin.rs | 1 + src/commands/general.rs | 269 ++++++++++++++++++++++++++++++++++ src/commands/mod.rs | 8 ++ src/commands/music.rs | 233 ++++++++++++++++++++++++++++++ src/commands/roulette.rs | 87 +++++++++++ src/config.rs | 13 ++ src/macros.rs | 8 ++ src/main.rs | 303 +++++++++++++++++++++++++++++++++++++++ src/presence.rs | 27 ++++ 13 files changed, 1017 insertions(+) create mode 100644 .gitignore create mode 100644 .vs/settings.json create mode 100644 Cargo.toml create mode 100644 src/api.rs create mode 100644 src/commands/admin.rs create mode 100644 src/commands/general.rs create mode 100644 src/commands/mod.rs create mode 100644 src/commands/music.rs create mode 100644 src/commands/roulette.rs create mode 100644 src/config.rs create mode 100644 src/macros.rs create mode 100644 src/main.rs create mode 100644 src/presence.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dbd8938 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/target +/.vs/* +!/.vs/settings.json +/logging/ +Cargo.lock +Conf.toml \ No newline at end of file diff --git a/.vs/settings.json b/.vs/settings.json new file mode 100644 index 0000000..d210276 --- /dev/null +++ b/.vs/settings.json @@ -0,0 +1,5 @@ +{ + "rust.features": [ + "music" + ] +} \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ab0ad6e --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "rusty_bot" +version = "0.1.0" +authors = ["oupson"] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[features] +music = ["serenity/voice"] + +[dependencies] +serenity = { version = "0.9.0-rc.0" } +toml = "0.5.6" +serde = { version = "1.0", features = ["derive"] } +reqwest = "0.10.7" +rand = "0.7.3" +lazy_static = "1.4.0" +async-trait = "0.1.36" +tokio = { version = "0.2", features = ["full"] } +futures = "0.3" +chrono = "0.4.15" +serde_json = "1.0" \ No newline at end of file diff --git a/src/api.rs b/src/api.rs new file mode 100644 index 0000000..3145c17 --- /dev/null +++ b/src/api.rs @@ -0,0 +1,34 @@ +use crate::commands::Result; +use serenity::{model::channel::Message, prelude::Context}; + +/// Send a reply to the channel the message was received on. +pub(crate) async fn send_reply<'m, S: std::string::ToString>( + ctx: &Context, + msg: &Message, + message: S, +) -> Result { + Ok(msg.channel_id.say(ctx, message.to_string()).await?) +} + +pub(crate) async fn send_splitted_by_lines_in_card( + ctx: &Context, + msg: &Message, + message: S, +) -> Result<()> { + let mut buffer = String::from("\x60\x60\x60\n"); + for line in message.to_string().lines() { + if buffer.len() + 4 + line.len() >= 2000 { + buffer += "\n\x60\x60\x60"; + send_reply(ctx, msg, &buffer).await?; + buffer = String::from("\x60\x60\x60\n"); + buffer += line; + buffer += "\n"; + } else { + buffer += line; + buffer += "\n"; + } + } + buffer += "\n\x60\x60\x60"; + send_reply(ctx, msg, &buffer).await?; + Ok(()) +} diff --git a/src/commands/admin.rs b/src/commands/admin.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/commands/admin.rs @@ -0,0 +1 @@ + diff --git a/src/commands/general.rs b/src/commands/general.rs new file mode 100644 index 0000000..7cdeb61 --- /dev/null +++ b/src/commands/general.rs @@ -0,0 +1,269 @@ +use crate::{api, debugln}; +use futures::StreamExt; +use serenity::{ + framework::standard::{ + macros::{command, group}, + ArgError, Args, CommandResult, + }, + model::prelude::*, + prelude::*, +}; + +#[group] +#[commands(longcode, image, older, ping, invite, infos, error)] +pub struct General; + +#[command] +pub async fn error(_ctx: &Context, _msg: &Message) -> CommandResult { + std::fs::File::open("foobar.txt")?; + Ok(()) +} + +#[command] +#[description = "Split a huge code"] +#[bucket = "longcode"] +pub async fn longcode(ctx: &Context, msg: &Message, args: Args) -> CommandResult { + if let Err(e) = _longcode(ctx, msg, args).await { + eprintln!("Error in longcode : {:?}", e); + } + Ok(()) +} + +async fn _longcode(ctx: &Context, msg: &Message, mut args: Args) -> crate::commands::Result<()> { + debugln!("_longcode : {:?}", args); + if let Ok(language) = args.single::() { + if !msg.attachments.is_empty() { + let att = msg.attachments[0].clone(); + + let text = reqwest::get(&att.url).await?.text().await?; + let header = format!("\x60\x60\x60{}\n", language); + let mut buf = header.clone(); + for line in text.lines() { + if buf.len() + 4 + line.len() >= 2000 { + buf += "\n\x60\x60\x60"; + api::send_reply(ctx, msg, buf).await?; + buf = header.clone(); + buf += line; + buf += "\n"; + } else { + buf += line; + buf += "\n"; + } + } + buf += "\n\x60\x60\x60"; + api::send_reply(ctx, msg, buf).await?; + } else { + api::send_reply(ctx, msg, "Error, missing code attachement").await?; + } + } else { + api::send_reply( + ctx, + msg, + "Error : Missing language (needed), Example : \n\t\x60?longcode rust\x60", + ) + .await?; + } + Ok(()) +} + +#[command] +#[description("Print the bot invite link")] +async fn invite(ctx: &Context, msg: &Message) -> CommandResult { + let invite = unsafe { &crate::INVITE_URL }; + if let Some(invite_url) = invite { + if let Err(e) = api::send_reply(ctx, msg, invite_url).await { + eprintln!("Error when sending invite : {:?}", e); + } + } else if let Err(e) = + api::send_reply(ctx, msg, "Error : Invite URL is not specified in config").await + { + eprintln!("Error in invite : {:?}", e); + } + Ok(()) +} + +#[command] +#[description("Pong !")] +async fn ping(ctx: &Context, msg: &Message) -> CommandResult { + let now = std::time::Instant::now(); + msg.channel_id.say(&ctx.http, "Pong!").await?; + let elapsed = now.elapsed(); + msg.channel_id + .say( + &ctx.http, + format!("Time elapsed : {}ms", elapsed.as_millis()), + ) + .await?; + + Ok(()) +} + +#[command] +#[description = "Find who is the older"] +pub async fn older(ctx: &Context, msg: &Message, args: Args) -> CommandResult { + if let Err(e) = _older(ctx, msg, args).await { + eprintln!("Error in older : {:?}", e); + } + Ok(()) +} + +async fn _older(ctx: &Context, msg: &Message, mut args: Args) -> crate::commands::Result<()> { + let mut number = 10; + if !args.is_empty() { + number = args.single::()?; + } + + if let Some(guild) = msg.guild_id { + let mut members = guild.members_iter(&ctx).boxed(); + let mut m: Vec = Vec::with_capacity(number + 1); + while let Some(member_result) = members.next().await { + match member_result { + Ok(member) => { + if m.is_empty() { + m.push(member.user) + } else { + let mut added = false; + let user = member.user; + for i in 0..m.len() { + if m[i].created_at() > user.created_at() { + m.insert(i, user.clone()); + if m.len() == number + 1 { + m.remove(number); + } + added = true; + break; + } + } + if !added && m.len() < number { + m.push(user); + } + } + } + Err(error) => eprintln!("Uh oh! Error: {}", error), + } + } + + let mut res = String::new(); + for (i, u) in m.iter().enumerate() { + res += &format!("{}. {} ({})\n", i + 1, u.name, u.created_at()); + } + api::send_splitted_by_lines_in_card(&ctx, msg, res).await?; + } + Ok(()) +} + +#[command] +#[description = "Print bot infos"] +async fn infos(ctx: &Context, msg: &Message) -> CommandResult { + let res = format!( + "{} v{}, by {}\nFeatures : {:?}\nMode : {}", + option_env!("CARGO_PKG_NAME").unwrap_or("unknown"), + option_env!("CARGO_PKG_VERSION").unwrap_or("unknown"), + option_env!("CARGO_PKG_AUTHORS").unwrap_or("unknows"), + get_features(), + { + if cfg!(debug_assertions) { + "debug" + } else { + "release" + } + } + ); + if let Err(e) = api::send_splitted_by_lines_in_card(ctx, msg, res).await { + eprintln!("Error when sending bot infos : {:?}", e); + } + Ok(()) +} + +fn get_features<'m>() -> Vec<&'m str> { + let mut res = Vec::new(); + if cfg!(feature = "music") { + res.push("music"); + } + res +} + +#[command] +#[description = "Image"] +#[bucket = "image"] +pub async fn image(ctx: &Context, msg: &Message, args: Args) -> CommandResult { + if let Err(e) = _image(ctx, msg, args).await { + eprintln!("Error in image : {:?}", e); + } + Ok(()) +} + +async fn _image(ctx: &Context, msg: &Message, mut args: Args) -> crate::commands::Result<()> { + match args.single::() { + Ok(image) => { + msg.channel_id + .send_message(&ctx.http, |m| { + m.embed(|e| { + image.embed(e); + e + }); + m + }) + .await?; + } + Err(e) => { + if let ArgError::Parse(e) = e { + msg.channel_id.say(ctx, e).await?; + } else { + return Err(Box::new(e)); + }; + } + }; + Ok(()) +} + +// TODO JSON FILE +enum Image { + HackerMan(), + Koding(), + BorrowCheckFailled(), + SafetyCheck(), + FerisBurn(), + EmploiStable(), +} + +impl Image { + fn embed(&self, embed: &mut serenity::builder::CreateEmbed) { + match self { + Image::HackerMan() => { + embed.image("https://images-wixmp-ed30a86b8c4ca887773594c2.wixmp.com/f/d8328f77-2bec-4a20-9483-ea76dd62985e/dan31sc-80f18518-0ef0-4bdc-9c0a-11c9e62b769f.png?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ1cm46YXBwOjdlMGQxODg5ODIyNjQzNzNhNWYwZDQxNWVhMGQyNmUwIiwic3ViIjoidXJuOmFwcDo3ZTBkMTg4OTgyMjY0MzczYTVmMGQ0MTVlYTBkMjZlMCIsImF1ZCI6WyJ1cm46c2VydmljZTpmaWxlLmRvd25sb2FkIl0sIm9iaiI6W1t7InBhdGgiOiIvZi9kODMyOGY3Ny0yYmVjLTRhMjAtOTQ4My1lYTc2ZGQ2Mjk4NWUvZGFuMzFzYy04MGYxODUxOC0wZWYwLTRiZGMtOWMwYS0xMWM5ZTYyYjc2OWYucG5nIn1dXX0.upSIXFazVoJxWpPPle4gmgwfJgx7Gvc603Sbbe-KJB0"); + } + Image::Koding() => { + embed.image("https://i.kym-cdn.com/photos/images/original/001/739/593/45d.jpg"); + } + Image::BorrowCheckFailled() => { + embed.image("https://cdn.discordapp.com/attachments/592452150517301248/721104919058448434/brrowchk.jpg"); + } + Image::SafetyCheck() => { + embed.image("https://cdn.discordapp.com/attachments/592452150517301248/695056442704527451/safety-check.png"); + } + Image::FerisBurn() => { + embed.image("https://oupson.fr/rusty_bot/static_files/ferrisburn.gif"); + } + Image::EmploiStable() => { + embed.image("https://i.imgur.com/q5scLGs.jpg"); + } + }; + } +} + +impl std::str::FromStr for Image { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "hackerman" => Ok(Self::HackerMan()), + "koding" => Ok(Self::Koding()), + "borrowcheckfailled" => Ok(Self::BorrowCheckFailled()), + "safetycheck" => Ok(Self::SafetyCheck()), + "ferrisburn" => Ok(Self::FerisBurn()), + "emploistable" => Ok(Self::EmploiStable()), + _ => Err(format!("{} is not an image", s)), + } + } +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs new file mode 100644 index 0000000..5cd84a6 --- /dev/null +++ b/src/commands/mod.rs @@ -0,0 +1,8 @@ +pub(crate) mod admin; +pub(crate) mod general; +pub(crate) mod roulette; + +#[cfg(feature = "music")] +pub(crate) mod music; + +pub(crate) type Result = std::result::Result>; diff --git a/src/commands/music.rs b/src/commands/music.rs new file mode 100644 index 0000000..c39fa0b --- /dev/null +++ b/src/commands/music.rs @@ -0,0 +1,233 @@ +use serenity::{ + client::{bridge::voice::ClientVoiceManager, Context}, + framework::standard::{ + macros::{command, group}, + Args, CommandResult, + }, + model::{channel::Message, misc::Mentionable}, + prelude::*, + voice, Result as SerenityResult, +}; +use std::sync::Arc; + +pub(crate) struct VoiceManager; + +impl TypeMapKey for VoiceManager { + type Value = Arc>; +} + +#[group] +#[commands(join, leave, play, stop)] +struct Music; + +#[command] +#[description("Join the channel you are connected")] +async fn join(ctx: &Context, msg: &Message) -> CommandResult { + let guild = match msg.guild(&ctx.cache).await { + Some(guild) => guild, + None => { + check_msg(msg.channel_id.say(&ctx.http, "DMs not supported").await); + + return Ok(()); + } + }; + + let guild_id = guild.id; + + let channel_id = guild + .voice_states + .get(&msg.author.id) + .and_then(|voice_state| voice_state.channel_id); + + let connect_to = match channel_id { + Some(channel) => channel, + None => { + check_msg(msg.reply(ctx, "Not in a voice channel").await); + + return Ok(()); + } + }; + + let manager_lock = ctx + .data + .read() + .await + .get::() + .cloned() + .expect("Expected VoiceManager in TypeMap."); + let mut manager = manager_lock.lock().await; + + if manager.join(guild_id, connect_to).is_some() { + check_msg( + msg.channel_id + .say(&ctx.http, &format!("Joined {}", connect_to.mention())) + .await, + ); + } else { + check_msg( + msg.channel_id + .say(&ctx.http, "Error joining the channel") + .await, + ); + } + + Ok(()) +} + +#[command] +#[description("Leave the channel you are connected")] +async fn leave(ctx: &Context, msg: &Message) -> CommandResult { + let guild_id = match ctx + .cache + .guild_channel_field(msg.channel_id, |channel| channel.guild_id) + .await + { + Some(id) => id, + None => { + check_msg(msg.channel_id.say(&ctx.http, "DMs not supported").await); + + return Ok(()); + } + }; + + let manager_lock = ctx + .data + .read() + .await + .get::() + .cloned() + .expect("Expected VoiceManager in TypeMap."); + let mut manager = manager_lock.lock().await; + let has_handler = manager.get(guild_id).is_some(); + + if has_handler { + manager.remove(guild_id); + + check_msg(msg.channel_id.say(&ctx.http, "Left voice channel").await); + } else { + check_msg(msg.reply(ctx, "Not in a voice channel").await); + } + + Ok(()) +} + +#[command] +#[description("Play a music (require an url)")] +async fn play(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { + let url = match args.single::() { + Ok(url) => url, + Err(_) => { + check_msg( + msg.channel_id + .say(&ctx.http, "Must provide a URL to a video or audio") + .await, + ); + + return Ok(()); + } + }; + + if !url.starts_with("http") { + check_msg( + msg.channel_id + .say(&ctx.http, "Must provide a valid URL") + .await, + ); + + return Ok(()); + } + + let guild_id = match ctx.cache.guild_channel(msg.channel_id).await { + Some(channel) => channel.guild_id, + None => { + check_msg( + msg.channel_id + .say(&ctx.http, "Error finding channel info") + .await, + ); + + return Ok(()); + } + }; + + let manager_lock = ctx + .data + .read() + .await + .get::() + .cloned() + .expect("Expected VoiceManager in TypeMap."); + let mut manager = manager_lock.lock().await; + + if let Some(handler) = manager.get_mut(guild_id) { + let source = match voice::ytdl(&url).await { + Ok(source) => source, + Err(why) => { + println!("Err starting source: {:?}", why); + + check_msg(msg.channel_id.say(&ctx.http, "Error sourcing ffmpeg").await); + + return Ok(()); + } + }; + handler.stop(); + handler.play(source); + + check_msg(msg.channel_id.say(&ctx.http, "Playing song").await); + } else { + check_msg( + msg.channel_id + .say(&ctx.http, "Not in a voice channel to play in") + .await, + ); + } + + Ok(()) +} + +#[command] +#[description("Stop the music")] +async fn stop(ctx: &Context, msg: &Message) -> CommandResult { + let guild_id = match ctx.cache.guild_channel(msg.channel_id).await { + Some(channel) => channel.guild_id, + None => { + check_msg( + msg.channel_id + .say(&ctx.http, "Error finding channel info") + .await, + ); + + return Ok(()); + } + }; + + let manager_lock = ctx + .data + .read() + .await + .get::() + .cloned() + .expect("Expected VoiceManager in TypeMap."); + let mut manager = manager_lock.lock().await; + + if let Some(handler) = manager.get_mut(guild_id) { + handler.stop(); + + check_msg(msg.channel_id.say(&ctx.http, "Stopping song").await); + } else { + check_msg( + msg.channel_id + .say(&ctx.http, "Not in a voice channel to play in") + .await, + ); + } + + Ok(()) +} + +/// Checks that a message successfully sent; if not, then logs why to stderr. +fn check_msg(result: SerenityResult) { + if let Err(why) = result { + eprintln!("Error sending message: {:?}", why); + } +} diff --git a/src/commands/roulette.rs b/src/commands/roulette.rs new file mode 100644 index 0000000..56162e3 --- /dev/null +++ b/src/commands/roulette.rs @@ -0,0 +1,87 @@ +use crate::{api, commands, debugln}; +use rand::Rng; +use serenity::{ + framework::standard::{ + macros::{command, group}, + Args, CommandResult, + }, + model::prelude::*, + prelude::*, +}; +use std::collections::HashMap; + +pub struct BulletsContainer; + +impl TypeMapKey for BulletsContainer { + type Value = HashMap; +} + +#[group] +#[default_command(shot)] +#[prefix("roulette")] +#[commands(reload, shot)] +struct Roulette; + +#[command] +#[description = "PRESS THE TRIGGER !"] +#[bucket = "roulette"] +async fn shot(ctx: &Context, msg: &Message, args: Args) -> CommandResult { + let _message = args.message().trim_end(); + if _message == "shot" || _message == "" { + if let Err(e) = _shot(ctx, msg).await { + eprintln!("{}", e); + } + } else if let Err(e) = api::send_reply( + ctx, + msg, + format!("Error : {} is not a valid argument", args.message()), + ) + .await + { + eprintln!("Error : {:?}", e); + } + Ok(()) +} + +async fn _shot(ctx: &Context, msg: &Message) -> commands::Result<()> { + let mut data = ctx.data.write().await; + let bullets_map = data + .get_mut::() + .expect("Expected CommandCounter in TypeMap."); + let bullets = bullets_map.entry(msg.author.id.0).or_insert(6); + if rand::thread_rng().gen_range(0, *bullets) == 0 { + api::send_reply(ctx, &msg, "BOOM !").await?; + *bullets = 6; + } else { + *bullets -= 1; + api::send_reply( + &ctx, + &msg, + format!("Click ! bullets remaining : {}", bullets), + ) + .await?; + } + debugln!("{:?}", bullets_map); + Ok(()) +} + +#[command] +#[description = "Reload"] +#[bucket = "roulette"] +async fn reload(ctx: &Context, msg: &Message) -> CommandResult { + if let Err(e) = _reload(ctx, msg).await { + eprintln!("{}", e); + } + Ok(()) +} + +async fn _reload(ctx: &Context, msg: &Message) -> commands::Result<()> { + let mut data = ctx.data.write().await; + let bullets_map = data + .get_mut::() + .expect("Expected CommandCounter in TypeMap."); + bullets_map.insert(msg.author.id.0, 6); + msg.react(ctx, ReactionType::Unicode(String::from("✅"))) + .await?; + Ok(()) +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..3938d26 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,13 @@ +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub(crate) struct Conf { + pub(crate) bot: Bot, +} + +#[derive(Debug, Deserialize)] +pub(crate) struct Bot { + pub(crate) token: String, + pub(crate) log_attachments: Option, + pub(crate) invite_url: Option, +} diff --git a/src/macros.rs b/src/macros.rs new file mode 100644 index 0000000..d7eebbe --- /dev/null +++ b/src/macros.rs @@ -0,0 +1,8 @@ +#[macro_export] +macro_rules! debugln { + ($($arg:tt)*) => { + if cfg!(debug_assertions) { + eprintln!($($arg)*); + } + }; +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..c294ba6 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,303 @@ +use crate::commands::{ + general::GENERAL_GROUP, + roulette::{BulletsContainer, ROULETTE_GROUP}, +}; +use async_trait::async_trait; +use serenity::{ + framework::standard::{ + help_commands, + macros::{help, hook}, + Args, CommandGroup, CommandResult, DispatchError, HelpOptions, StandardFramework, CommandError, + }, + http::Http, + model::prelude::*, + prelude::*, +}; +use std::{ + collections::{HashMap, HashSet}, + fs::{self}, + io::Result as IoResult, + path::Path, + time::Duration, +}; +use tokio::{fs::File, io::AsyncWriteExt}; +use serde_json::Value; + +#[cfg(feature = "music")] +use crate::commands::music::{VoiceManager, MUSIC_GROUP}; + +mod api; +mod commands; +mod config; +mod macros; +mod presence; + +const PREFIX: &str = "?"; +static mut LOG_ATTACHMENTS: bool = false; +pub(crate) static mut INVITE_URL: Option = None; + +//TODO CLAP FOR CLI +#[tokio::main] +async fn main() -> IoResult<()> { + let conf: config::Conf = toml::from_str(&std::fs::read_to_string("Conf.toml")?)?; + debugln!("conf : {:?}", conf); + + let token = conf.bot.token; + + if conf.bot.log_attachments.unwrap_or(false) { + unsafe { + LOG_ATTACHMENTS = true; + } + } + + if let Some(url) = conf.bot.invite_url { + unsafe { + INVITE_URL = Some(url); + } + } + + debugln!("Log attachments : {}", unsafe { LOG_ATTACHMENTS }); + + let dir = Path::new("logging"); + if !dir.exists() { + fs::create_dir(dir)?; + } + + let http = Http::new_with_token(&token); + + // We will fetch your bot's owners and id + let (owners, bot_id) = match http.get_current_application_info().await { + Ok(info) => { + let mut owners = HashSet::new(); + owners.insert(info.owner.id); + + (owners, info.id) + } + Err(why) => panic!("Could not access application info: {:?}", why), + }; + + debugln!("Owners : {:?}", owners); + + let mut framework = StandardFramework::new() + .configure(|c| { + c.with_whitespace(true) + .on_mention(Some(bot_id)) + .prefix(PREFIX) + // In this case, if "," would be first, a message would never + // be delimited at ", ", forcing you to trim your arguments if you + // want to avoid whitespaces at the start of each. + .delimiters(vec![", ", ","]) + // Sets the bot's owners. These will be used for commands that + // are owners only. + .owners(owners) + }) + // Set a function that's called whenever an attempted command-call's + // command could not be found. + .unrecognised_command(unknown_command) + // Set a function that's called whenever a command's execution didn't complete for one + // reason or another. For example, when a user has exceeded a rate-limit or a command + // can only be performed by the bot owner. + .on_dispatch_error(dispatch_error) + .after(after_hook) + .help(&MY_HELP) + .group(&GENERAL_GROUP) + .group(&ROULETTE_GROUP); + + #[cfg(feature = "music")] + { + framework = framework.group(&MUSIC_GROUP); + } + + if cfg!(debug_assertions) { + // Set a function that's called whenever a message is not a command. + framework = framework.normal_message(normal_message) + } + + let mut client = Client::new(&token) + .event_handler(Messages {}) + .framework(framework) + .await + .unwrap(); + + { + let mut data = client.data.write().await; + data.insert::(HashMap::default()); + #[cfg(feature = "music")] + { + data.insert::(std::sync::Arc::clone(&client.voice_manager)); + } + } + + client.start().await.unwrap(); + Ok(()) +} + +struct Messages {} + +impl Messages { + async fn _reaction_add( + &self, + ctx: Context, + reaction: Reaction, + ) -> Result<(), Box> { + // TODO + let message: Message = reaction.message(&ctx.http).await?; + if message.is_own(&ctx).await && message.content.starts_with("Do you want to take the gun") + { + debugln!("My own garbage !"); + let user = reaction.user(&ctx.http).await?; + if user == message.mentions[1] { + message.delete(&ctx.http).await?; + if reaction.emoji + == serenity::model::channel::ReactionType::Unicode(String::from("✅")) + { + let mut data = ctx.data.write().await; + let bullets_map = data + .get_mut::() + .expect("Expected CommandCounter in TypeMap."); + if bullets_map.contains_key(&message.mentions[0].id.0) { + let bullet_count = + bullets_map.remove(&message.mentions[0].id.0).unwrap_or(6); + bullets_map.insert(message.mentions[1].id.0, bullet_count); + message.channel_id.say(&ctx, "Done").await?; + } else { + message + .channel_id + .say(&ctx, "Error : Your gun is empty") + .await?; + } + } + } + } + Ok(()) + } +} + +#[async_trait] +impl EventHandler for Messages { + async fn reaction_add(&self, _ctx: Context, reaction: Reaction) { + debugln!("Reaction added : {:?}", reaction); + /* + if let Err(e) = self._reaction_add(ctx, reaction).await { + eprintln!("{}", e); + }*/ + } + + async fn ready(&self, ctx: Context, ready: Ready) { + println!("{} connected to discord", ready.user.name); + + ctx.set_presence(Some(Activity::listening("?")), OnlineStatus::Online) + .await; + + let ctx_clone = ctx.clone(); + tokio::spawn(async move { + let delay = Duration::from_secs(5); + let mut presence_generator = presence::Presences::new(); + while let Some(act) = presence_generator.next(&ctx_clone).await { + ctx_clone + .set_presence(Some(act), OnlineStatus::Online) + .await; + tokio::time::delay_for(delay).await; + } + }); + } + + async fn message(&self, _ctx: Context, new_message: Message) { + if unsafe { LOG_ATTACHMENTS } && !new_message.attachments.is_empty() { + for att in new_message.attachments { + if let Err(e) = download_to_log(att).await { + eprintln!("Error while downloading to log : {:?}", e); + } + } + } + } + + async fn unknown(&self, _ctx: Context, name: String, raw: Value) { + println!("Unknown event : {}, {:?}", name, raw); + } +} + +async fn download_to_log(attachment: Attachment) -> commands::Result<()> { + debugln!("Download_to_log : {:?}", attachment); + let path = Path::new("logging").join(format!("{}-{}", attachment.id, attachment.filename)); + let content = reqwest::get(&attachment.url).await?.bytes().await?; + let mut file = File::create(path).await?; + file.write_all(&content).await?; + Ok(()) +} + +#[hook] +async fn unknown_command(_ctx: &Context, _msg: &Message, unknown_command_name: &str) { + println!("Could not find command named '{}'", unknown_command_name); +} + +#[hook] +async fn normal_message(_ctx: &Context, msg: &Message) { + println!("Message is not a command '{}'", msg.content); +} + +#[hook] +async fn dispatch_error(ctx: &Context, msg: &Message, error: DispatchError) { + debugln!("Dispatch error : {:?}", error); + if let DispatchError::Ratelimited(seconds) = error { + let _ = msg + .channel_id + .say( + &ctx.http, + &format!("Try this again in {} seconds.", seconds), + ) + .await; + } +} + +#[hook] +async fn after_hook(_: &Context, _: &Message, cmd_name: &str, error: Result<(), CommandError>) { + // Print out an error if it happened + if let Err(why) = error { + println!("Error in {}: {:?}", cmd_name, why); + } +} + +// The framework provides two built-in help commands for you to use. +// But you can also make your own customized help command that forwards +// to the behaviour of either of them. +#[help] +// This replaces the information that a user can pass +// a command-name as argument to gain specific information about it. +#[individual_command_tip = "Hello!\n\ +If you want more information about a specific command, just pass the command as argument."] +// Some arguments require a `{}` in order to replace it with contextual information. +// In this case our `{}` refers to a command's name. +#[command_not_found_text = "Could not find: \x60{}\x60."] +// Define the maximum Levenshtein-distance between a searched command-name +// and commands. If the distance is lower than or equal the set distance, +// it will be displayed as a suggestion. +// Setting the distance to 0 will disable suggestions. +#[max_levenshtein_distance(3)] +// When you use sub-groups, Serenity will use the `indention_prefix` to indicate +// how deeply an item is indented. +// The default value is "-", it will be changed to "+". +#[indention_prefix = "+"] +// On another note, you can set up the help-menu-filter-behaviour. +// Here are all possible settings shown on all possible options. +// First case is if a user lacks permissions for a command, we can hide the command. +#[lacking_permissions = "Hide"] +// If the user is nothing but lacking a certain role, we just display it hence our variant is `Nothing`. +#[lacking_role = "Nothing"] +// The last `enum`-variant is `Strike`, which ~~strikes~~ a command. +#[wrong_channel = "Strike"] +// Serenity will automatically analyse and generate a hint/tip explaining the possible +// cases of ~~strikethrough-commands~~, but only if +// `strikethrough_commands_tip_{dm, guild}` aren't specified. +// If you pass in a value, it will be displayed instead. +async fn my_help( + context: &Context, + msg: &Message, + args: Args, + help_options: &'static HelpOptions, + groups: &[&'static CommandGroup], + owners: HashSet, +) -> CommandResult { + let _ = help_commands::with_embeds(context, msg, args, help_options, groups, owners).await; + Ok(()) +} diff --git a/src/presence.rs b/src/presence.rs new file mode 100644 index 0000000..3b20d0a --- /dev/null +++ b/src/presence.rs @@ -0,0 +1,27 @@ +use serenity::{model::prelude::*, prelude::*}; + +const PRESENCE_COUNT: usize = 3; + +pub(crate) struct Presences { + index: usize, +} + +impl Presences { + pub(crate) fn new() -> Self { + Self { index: 0 } + } + + pub(crate) async fn next(&mut self, ctx: &Context) -> Option { + let res: Activity = match self.index { + 0 => Activity::listening("?"), + 1 => Activity::listening(&format!("{} servers", ctx.cache.guild_count().await)), + 2 => Activity::playing("praising the borrow checker"), + _ => unimplemented!(), + }; + self.index += 1; + if self.index >= PRESENCE_COUNT { + self.index = 0; + } + Some(res) + } +}