From afab95f45dde1127ff80ef8b458f4a1a63b2eaa2 Mon Sep 17 00:00:00 2001 From: Oupson Date: Wed, 17 Mar 2021 23:26:43 +0100 Subject: [PATCH] songbird --- Cargo.toml | 9 +- src/commands/admin.rs | 40 +++++++- src/commands/general.rs | 42 +++++++- src/commands/music.rs | 192 ++++++++++++++----------------------- src/commands/roulette.rs | 108 ++++----------------- src/data/guilds_options.rs | 51 +++++++++- src/main.rs | 61 ++++++++++-- 7 files changed, 279 insertions(+), 224 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 700d5f5..64fdd7a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,20 +7,21 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -music = ["serenity/voice"] +music = ["serenity/voice", "songbird"] [dependencies] -serenity = "0.9" +serenity = "0.10" toml = "0.5" serde = { version = "1.0", features = ["derive"] } reqwest = "0.10" rand = "0.7" lazy_static = "1.4" async-trait = "0.1" -tokio = { version = "0.2", features = ["full"] } +tokio = { version = "1.0", features = ["full"] } futures = "0.3" chrono = "0.4" serde_json = "1.0" log = "0.4" log4rs = "0.13" -ctrlc = "3.1" \ No newline at end of file +ctrlc = "3.1" +songbird = { version = "0.1.0", features = ["driver"], optional = true } \ No newline at end of file diff --git a/src/commands/admin.rs b/src/commands/admin.rs index 4ae3429..cd45733 100644 --- a/src/commands/admin.rs +++ b/src/commands/admin.rs @@ -1,3 +1,4 @@ +use log::debug; use serenity::{ framework::standard::{ macros::{command, group}, @@ -5,11 +6,13 @@ use serenity::{ }, model::prelude::*, prelude::*, + http::CacheHttp, }; -use log::debug; + +use crate::data::GuildOptionsKey; #[group] -#[commands(ban, kick)] +#[commands(ban, kick, ghost_pings)] pub struct Admin; #[command] @@ -93,5 +96,38 @@ async fn ban(ctx: &Context, msg: &Message) -> CommandResult { } } } + Ok(()) +} + +#[command] +#[description = ""] +#[only_in(guilds)] +#[required_permissions("KICK_MEMBERS")] +async fn ghost_pings(ctx: &Context, msg: &Message) -> CommandResult { + let data = ctx.data.read().await; + let guilds_options = data + .get::() + .expect("Expected NonKickGuildsContainer in TypeMap."); + + if let Some(guild) = guilds_options.get(&msg.guild_id.unwrap()) { + let mut message = String::from("\x60\x60\x60"); + + for ping in &guild.last_ghost_pings { + let sender = ctx + .http() + .get_user(ping.sender) + .await + .map(|u| u.name) + .unwrap_or_else(|_| String::from("Unkown")); + + message += &format!("{} : {:?}\n", sender, ping.roles.iter().map(|r| format!("", r))) + } + message += "\x60\x60\x60"; + + crate::api::send_reply(ctx, msg, message).await?; + } + + + Ok(()) } diff --git a/src/commands/general.rs b/src/commands/general.rs index 7e19f6a..cb73370 100644 --- a/src/commands/general.rs +++ b/src/commands/general.rs @@ -1,18 +1,30 @@ use crate::{api, data::ShardManagerContainer}; use futures::StreamExt; use log::error; +use rand::prelude::IteratorRandom; use serenity::{ client::bridge::gateway::ShardId, framework::standard::{ macros::{command, group}, ArgError, Args, CommandResult, }, + http::CacheHttp, model::prelude::*, prelude::*, }; #[group] -#[commands(error, image, infos, invite, latency, longcode, older, ping)] +#[commands( + error, + image, + infos, + invite, + latency, + longcode, + older, + ping, + random_mute +)] pub struct General; #[command] @@ -271,6 +283,34 @@ async fn _image(ctx: &Context, msg: &Message, mut args: Args) -> crate::commands Ok(()) } +#[command] +#[required_permissions(ADMINISTRATOR)] +async fn random_mute(ctx: &Context, msg: &Message) -> CommandResult { + if let Some(guild) = msg.guild_id { + let mut members = guild.members(&ctx, Some(1000), None).await?; + + let mut m: usize = rand::random::() % members.len(); + while members[m].permissions(&ctx).await.unwrap().administrator() || members[m].user.bot { + m = rand::random::() % members.len(); + } + + let guild = ctx.http().get_guild(msg.guild_id.unwrap().0).await?; + + let role = match guild.role_by_name("mute") { + Some(role) => Ok(role.id), + None => Err(tokio::io::Error::new( + tokio::io::ErrorKind::Other, + "Unkown role", + )), + }?; + + members[m].add_role(&ctx.http, role).await?; + + api::send_reply(ctx, msg, format!("{} was choosen by the roulette of the mute !", members[m].mention())).await?; + } + Ok(()) +} + // TODO JSON FILE enum Image { HackerMan(), diff --git a/src/commands/music.rs b/src/commands/music.rs index 843ed0c..78496b8 100644 --- a/src/commands/music.rs +++ b/src/commands/music.rs @@ -1,22 +1,17 @@ use crate::data::{GuildOptions, GuildOptionsKey}; use log::{error, info}; use serenity::{ - client::{bridge::voice::ClientVoiceManager, Context}, + client::{Context}, framework::standard::{ macros::{command, group}, Args, CommandResult, }, model::{channel::Message, guild::PartialMember, id::GuildId, misc::Mentionable}, prelude::*, - voice, Result as SerenityResult, + Result as SerenityResult, }; use std::sync::Arc; -pub(crate) struct VoiceManager; - -impl TypeMapKey for VoiceManager { - type Value = Arc>; -} #[group] #[commands(join, leave, play, stop)] @@ -28,7 +23,7 @@ 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); + msg.channel_id.say(&ctx.http, "DMs not supported").await; return Ok(()); } @@ -39,11 +34,9 @@ async fn join(ctx: &Context, msg: &Message) -> CommandResult { } else { false } { - check_msg( msg.channel_id .say(&ctx.http, "Error, you cant play music") - .await, - ); + .await; return Ok(()); } @@ -57,33 +50,25 @@ async fn join(ctx: &Context, msg: &Message) -> CommandResult { let connect_to = match channel_id { Some(channel) => channel, None => { - check_msg(msg.reply(ctx, "Not in a voice channel").await); + 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; + let manager = songbird::get(ctx).await + .expect("Songbird Voice client placed in at initialisation.").clone(); - if manager.join(guild_id, connect_to).is_some() { - check_msg( + if manager.join(guild_id, connect_to).await.1.is_ok() { + msg.channel_id .say(&ctx.http, &format!("Joined {}", connect_to.mention())) - .await, - ); + .await; } else { - check_msg( + msg.channel_id .say(&ctx.http, "Error joining the channel") - .await, - ); + .await; } Ok(()) @@ -99,7 +84,7 @@ async fn leave(ctx: &Context, msg: &Message) -> CommandResult { { Some(id) => id, None => { - check_msg(msg.channel_id.say(&ctx.http, "DMs not supported").await); + msg.channel_id.say(&ctx.http, "DMs not supported").await; return Ok(()); } @@ -110,32 +95,26 @@ async fn leave(ctx: &Context, msg: &Message) -> CommandResult { } else { false } { - check_msg( msg.channel_id .say(&ctx.http, "Error, you cant play music") - .await, - ); + .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 manager = songbird::get(ctx).await + .expect("Songbird Voice client placed in at initialisation.").clone(); - if let Some(handler) = manager.get_mut(guild_id) { - handler.stop(); - 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); - } + let has_handler = manager.get(guild_id).is_some(); + if has_handler { + if let Err(e) = manager.remove(guild_id).await { + msg.channel_id.say(&ctx.http, format!("Failed: {:?}", e)).await; + } + + msg.channel_id.say(&ctx.http, "Left voice channel").await; + } else { + msg.reply(ctx, "Not in a voice channel").await; + } Ok(()) } @@ -145,22 +124,20 @@ 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, - ); + .await; return Ok(()); } }; if !url.starts_with("http") { - check_msg( + msg.channel_id .say(&ctx.http, "Must provide a valid URL") - .await, - ); + .await; return Ok(()); } @@ -168,11 +145,10 @@ async fn play(ctx: &Context, msg: &Message, mut args: Args) -> 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, - ); + .await; return Ok(()); } @@ -183,45 +159,36 @@ async fn play(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { } else { false } { - check_msg( msg.channel_id .say(&ctx.http, "Error, you cant play music") - .await, - ); + .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 manager = songbird::get(ctx).await + .expect("Songbird Voice client placed in at initialisation.").clone(); - if let Some(handler) = manager.get_mut(guild_id) { - let source = match voice::ytdl(&url).await { - Ok(source) => source, - Err(why) => { - error!("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, - ); - } + if let Some(handler_lock) = manager.get(guild_id) { + let mut handler = handler_lock.lock().await; + + let source = match songbird::ytdl(&url).await { + Ok(source) => source, + Err(why) => { + error!("Err starting source: {:?}", why); + + msg.channel_id.say(&ctx.http, "Error sourcing ffmpeg").await; + + return Ok(()); + }, + }; + + handler.play_source(source); + + msg.channel_id.say(&ctx.http, "Playing song").await; + } else { + msg.channel_id.say(&ctx.http, "Not in a voice channel to play in").await; + } + Ok(()) } @@ -232,11 +199,10 @@ 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, - ); + .await; return Ok(()); } @@ -247,51 +213,35 @@ async fn stop(ctx: &Context, msg: &Message) -> CommandResult { } else { false } { - check_msg( + msg.channel_id .say(&ctx.http, "Error, you cant play music") - .await, - ); + .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(); + let manager = songbird::get(ctx).await + .expect("Songbird Voice client placed in at initialisation.").clone(); + if let Some(handler) = manager.get(guild_id) { - 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, - ); - } + let mut handler = handler.lock().await; + handler.stop(); + + msg.channel_id.say(&ctx.http, "Stopping").await; + } else { + msg.reply(ctx, "Not in a voice channel").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 { - error!("Error sending message: {:?}", why); - } -} - async fn is_mute( ctx: &Context, member: &PartialMember, guild_id: GuildId, ) -> tokio::io::Result { - let mut data = ctx.data.write().await; let data = data .get_mut::() diff --git a/src/commands/roulette.rs b/src/commands/roulette.rs index b91b473..572e035 100644 --- a/src/commands/roulette.rs +++ b/src/commands/roulette.rs @@ -1,8 +1,8 @@ use crate::{ - api, commands, - data::{BulletsContainer, GuildOptions, GuildOptionsKey}, + api, + data::{GuildOptions, GuildOptionsKey}, }; -use log::{debug, error, trace}; +use log::{debug, error}; use rand::Rng; use serenity::{ framework::standard::{ @@ -16,7 +16,7 @@ use serenity::{ #[group] #[default_command(shot)] #[prefix("roulette")] -#[commands(reload, shot, check, kick, disable_kick)] +#[commands(shot, kick, disable_kick)] struct Roulette; #[command] @@ -30,83 +30,21 @@ async fn shot(ctx: &Context, msg: &Message, args: Args) -> CommandResult { } else { api::send_reply(&ctx, &msg, format!("Click ! Reloading")).await?; } - } else if let Err(e) = api::send_reply( - ctx, - msg, - format!("Error : {} is not a valid argument", args.message()), - ) - .await - { - error!("Error : {:?}", e); - } - Ok(()) -} - -#[command] -#[description = "Reload"] -#[bucket = "roulette"] -async fn reload(ctx: &Context, msg: &Message) -> CommandResult { - if let Err(e) = _reload(ctx, msg).await { - error!("{}", 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, (5, rand::thread_rng().gen_range(0, 6))); - msg.react(ctx, ReactionType::Unicode(String::from("✅"))) + } else { + api::send_reply( + ctx, + msg, + format!("Error : {} is not a valid argument", args.message()), + ) .await?; - Ok(()) -} - -#[command] -#[description = "If you use that, you are a coward"] -async fn check(ctx: &Context, msg: &Message) -> CommandResult { - 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((5, rand::thread_rng().gen_range(0, 6))); - msg.channel_id.say(ctx, format!("Because you are a little shit, you open your barrel and you see that the bullet was at the {} position", bullet_to_str(bullets.1 + 1))).await?; - debug!("Bullets Map : {:?}", bullets_map); - Ok(()) -} - -fn bullet_to_str<'m>(nbr: u8) -> &'m str { - match nbr { - 1 => "first", - 2 => "second", - 3 => "third", - 4 => "fourth", - 5 => "fifth", - 6 => "sixth", - _ => unimplemented!(), } + Ok(()) } #[command] #[description = "DO IT"] #[only_in(guilds)] async fn kick(ctx: &Context, msg: &Message) -> CommandResult { - if msg.author.bot { - if let Err(e) = - api::send_reply(ctx, msg, "Error, this command cannot be used by a bot").await - { - error!("Error in kick : {:?}", e); - } - } else if let Err(e) = _kick(ctx, msg).await { - error!("Error in kick : {:?}", e); - } - Ok(()) -} - -async fn _kick(ctx: &Context, msg: &Message) -> CommandResult { if let Some(guild_id) = &msg.guild_id { let mut data = ctx.data.write().await; let guilds_options = data @@ -122,15 +60,14 @@ async fn _kick(ctx: &Context, msg: &Message) -> CommandResult { ) .await?; } else { - let bullets_map = data - .get_mut::() - .expect("Expected CommandCounter in TypeMap."); - let bullets = bullets_map - .entry(msg.author.id.0) - .or_insert((5, rand::thread_rng().gen_range(0, 6))); - if bullets.0 == bullets.1 { + if rand::thread_rng().gen_range(0, 6) == 0 { api::send_reply(ctx, &msg, "💥").await?; - *bullets = (5, rand::thread_rng().gen_range(0, 6)); + + msg.author + .create_dm_channel(&ctx) + .await? + .send_message(&ctx.http, |m| m.content("<:cheh:780736245675982909>")) + .await?; guild_id .member(&ctx.http, &msg.author) @@ -138,15 +75,8 @@ async fn _kick(ctx: &Context, msg: &Message) -> CommandResult { .kick_with_reason(&ctx.http, "You loose at the roulette") .await?; } else { - *bullets = (bullets.0 - 1, bullets.1); - api::send_reply( - &ctx, - &msg, - format!("Click ! bullets remaining : {}", bullets.0 + 1), - ) - .await?; + api::send_reply(&ctx, &msg, format!("Click ! Reloading")).await?; } - debug!("Bullets Map : {:?}", bullets_map); } } Ok(()) diff --git a/src/data/guilds_options.rs b/src/data/guilds_options.rs index 7c86e88..da0fb8f 100644 --- a/src/data/guilds_options.rs +++ b/src/data/guilds_options.rs @@ -1,7 +1,10 @@ use log::error; use serde::{Deserialize, Serialize}; use serenity::{ - model::prelude::{GuildId, RoleId}, + model::{ + channel::Message, + prelude::{GuildId, RoleId, UserId}, + }, prelude::TypeMapKey, }; use std::{ @@ -22,6 +25,8 @@ pub(crate) struct GuildOptions { #[serde(skip_serializing)] pub(crate) mute_id: Option, pub(crate) roulette_options: RouletteOptions, + pub(crate) last_ghost_pings: Vec, + pub(crate) mutes : Vec<(UserId, u64)>, } impl GuildOptions { @@ -118,6 +123,29 @@ impl GuildOptions { } } } + + pub(crate) fn insert_or_ignore(&mut self, message: Message) { + let time = message.timestamp.timestamp_millis(); + + let mut i = 0; + while i < self.last_ghost_pings.len() { + if time > self.last_ghost_pings[i].time { + break; + } + i += 1; + } + + log::debug!("u {}", i); + + if i != 15 { + log::debug!("u {}", i); + self.last_ghost_pings.insert(i, message.into()); + } + + if self.last_ghost_pings.len() == 16 { + self.last_ghost_pings.remove(15); + } + } } impl Default for GuildOptions { @@ -126,6 +154,8 @@ impl Default for GuildOptions { roulette_options: RouletteOptions::default(), guild_id: None, mute_id: None, + last_ghost_pings: Vec::new(), + mutes: Vec::new() } } } @@ -157,3 +187,22 @@ pub(crate) struct GuildOptionsKey; impl TypeMapKey for GuildOptionsKey { type Value = HashMap; } + +#[derive(Debug, Serialize, Deserialize)] +pub(crate) struct GhostPing { + pub(crate) sender: u64, + pub(crate) channel: u64, + pub(crate) roles: Vec, + pub(crate) time: i64, +} + +impl From for GhostPing { + fn from(message: Message) -> Self { + Self { + sender: *message.author.id.as_u64(), + channel: *message.channel_id.as_u64(), + roles: message.mention_roles, + time: message.timestamp.timestamp_millis(), + } + } +} diff --git a/src/main.rs b/src/main.rs index 11af12c..07d0536 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,7 +14,7 @@ use serenity::{ Args, CommandError, CommandGroup, CommandResult, DispatchError, HelpOptions, StandardFramework, }, - http::Http, + http::{CacheHttp, Http}, model::prelude::*, prelude::*, }; @@ -27,9 +27,10 @@ use std::{ time::Duration, }; use tokio::{fs::File, io::AsyncWriteExt}; +use songbird::SerenityInit; #[cfg(feature = "music")] -use crate::commands::music::{VoiceManager, MUSIC_GROUP}; +use crate::commands::music::{MUSIC_GROUP}; mod api; mod commands; @@ -131,6 +132,7 @@ async fn main() -> IoResult<()> { let mut client = Client::builder(&token) .event_handler(Messages {}) .framework(framework) + .register_songbird() .await .unwrap(); @@ -138,10 +140,10 @@ async fn main() -> IoResult<()> { let mut data = client.data.write().await; data.insert::(HashMap::default()); data.insert::(Arc::clone(&client.shard_manager)); - #[cfg(feature = "music")] + /*#[cfg(feature = "music")] { - data.insert::(std::sync::Arc::clone(&client.voice_manager)); - } + data.insert::(std::sync::Arc::clone(&client.voice_manager.unwrap())); + }*/ data.insert::({ let options = GuildOptions::load_from_dir("./data/guilds_options").unwrap_or_default(); @@ -215,6 +217,10 @@ impl EventHandler for Messages { async fn ready(&self, ctx: Context, ready: Ready) { info!("{} connected to discord", ready.user.name); + if let Some(cache) = ctx.cache() { + cache.set_max_messages(10).await; // TODO CONFIG + } + ctx.set_presence(Some(Activity::listening("?")), OnlineStatus::Online) .await; @@ -225,7 +231,7 @@ impl EventHandler for Messages { ctx_clone .set_presence(Some(act), OnlineStatus::Online) .await; - tokio::time::delay_for(delay).await; + tokio::time::sleep(delay).await; } } @@ -242,6 +248,49 @@ impl EventHandler for Messages { async fn unknown(&self, _ctx: Context, name: String, raw: Value) { debug!("Unknown event : {}, {:?}", name, raw); } + + async fn message_delete( + &self, + ctx: Context, + channel_id: ChannelId, + deleted_message_id: MessageId, + _guild_id: Option + ) { + let message = ctx + .cache() + .unwrap() + .message(channel_id, deleted_message_id) + .await; + log::debug!("Deletted message : {:?}", message.as_ref().map(|m| &m.content)); + + if let Some(message) = message { + if let Some(guild_id) = message.guild_id { + if !message.mention_roles.is_empty() { + let mut data = ctx.data.write().await; + let guilds_options = data.get_mut::().unwrap(); + + + let entry = guilds_options + .entry(guild_id) + .or_insert_with(|| GuildOptions::default().set_guild_id(guild_id)); + + entry.insert_or_ignore(message); + + log::debug!("{:?}", entry.last_ghost_pings); + } + } + } + } + + async fn message_update( + &self, + _ctx: Context, + old_if_available: Option, + new: Option, + _event: MessageUpdateEvent, + ) { + log::debug!("update : {:?} vs {:?}", old_if_available, new); + } } async fn download_to_log(attachment: Attachment) -> commands::Result<()> {