From d2de03894a5c3ed419a6c74aa37b4aa1602b8dbf Mon Sep 17 00:00:00 2001 From: "oupson1er@gmail.com" Date: Thu, 18 Mar 2021 19:54:30 +0100 Subject: [PATCH] Working on embeds --- src/commands/admin.rs | 42 +--------- src/commands/mod.rs | 3 +- src/commands/music.rs | 157 ++++++++++++++++++++++++++++++------- src/commands/roulette.rs | 6 +- src/commands/settings.rs | 142 +++++++++++++++++++++++++++++++++ src/data/guilds_options.rs | 93 ++-------------------- src/main.rs | 91 +++++++++++---------- 7 files changed, 335 insertions(+), 199 deletions(-) create mode 100644 src/commands/settings.rs diff --git a/src/commands/admin.rs b/src/commands/admin.rs index c707e80..206ef64 100644 --- a/src/commands/admin.rs +++ b/src/commands/admin.rs @@ -4,15 +4,12 @@ use serenity::{ macros::{command, group}, CommandResult, }, - http::CacheHttp, model::prelude::*, prelude::*, }; -use crate::data::GuildOptionsKey; - #[group] -#[commands(ban, kick, ghost_pings)] +#[commands(ban, kick)] pub struct Admin; #[command] @@ -97,39 +94,4 @@ 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(()) -} +} \ No newline at end of file diff --git a/src/commands/mod.rs b/src/commands/mod.rs index bee744f..faba371 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -2,8 +2,9 @@ pub(crate) mod admin; pub(crate) mod general; pub(crate) mod owner; pub(crate) mod roulette; +pub(crate) mod settings; #[cfg(feature = "music")] pub(crate) mod music; -pub(crate) type Result = std::result::Result>; +pub(crate) type Result = std::result::Result>; \ No newline at end of file diff --git a/src/commands/music.rs b/src/commands/music.rs index 21e0b45..7d9aefa 100644 --- a/src/commands/music.rs +++ b/src/commands/music.rs @@ -5,7 +5,7 @@ use tokio::sync::Mutex as TokioMutex; use crate::data::{GuildOptions, GuildOptionsKey}; use log::error; use serenity::{ - builder::CreateMessage, + builder::{CreateEmbed, CreateMessage}, client::Context, framework::standard::{ macros::{command, group}, @@ -14,7 +14,7 @@ use serenity::{ http::Http, model::{ channel::Message, - guild::PartialMember, + guild::{Member, PartialMember}, id::{ChannelId, GuildId}, misc::Mentionable, }, @@ -37,7 +37,7 @@ impl songbird::EventHandler for TrackStartNotifier { if let Err(why) = self .chan_id .send_message(&self.http, |m| { - embed_song(m, metadata); + embed_song(m, metadata, None); m }) .await @@ -55,12 +55,12 @@ impl songbird::EventHandler for TrackStartNotifier { } } -fn embed_song(msg: &mut CreateMessage, metadata: &Metadata) { +fn embed_song(msg: &mut CreateMessage, metadata: &Metadata, author: Option<&Member>) { msg.embed(|e| { e.title("Now playing"); if let Some(title) = &metadata.title { - e.field("title", title, true); + e.field("Title", title, true); } if let Some(url) = &metadata.source_url { @@ -75,7 +75,7 @@ fn embed_song(msg: &mut CreateMessage, metadata: &Metadata) { let seconds = seconds - (min * 60); e.field( - "duration", + "Duration", if hours > 0 { format!("{}:{:02}:{:02}", hours, min, seconds) } else { @@ -88,16 +88,17 @@ fn embed_song(msg: &mut CreateMessage, metadata: &Metadata) { if let Some(img) = &metadata.thumbnail { e.image(img); } - e + + embed_author(e, author).colour((247, 76, 0)) }); } -fn embed_queued(msg: &mut CreateMessage, metadata: &Metadata) { +fn embed_queued(msg: &mut CreateMessage, metadata: &Metadata, author: Option<&Member>) { msg.embed(|e| { e.title("Queued"); if let Some(title) = &metadata.title { - e.field("title", title, true); + e.field("Title", title, true); } if let Some(url) = &metadata.source_url { @@ -112,7 +113,7 @@ fn embed_queued(msg: &mut CreateMessage, metadata: &Metadata) { let seconds = seconds - (min * 60); e.field( - "duration", + "Duration", if hours > 0 { format!("{}:{:02}:{:02}", hours, min, seconds) } else { @@ -125,10 +126,45 @@ fn embed_queued(msg: &mut CreateMessage, metadata: &Metadata) { if let Some(img) = &metadata.thumbnail { e.image(img); } - e + + embed_author(e, author).colour((247, 76, 0)) }); } +fn embed_response<'a, 'b>( + msg: &'a mut CreateMessage<'b>, + title: &str, + content: &str, + author: Option<&Member>, +) -> &'a mut CreateMessage<'b> { + msg.embed(|e| { + e.title(title).description(content); + embed_author(e, author).colour((247, 76, 0)) + }) +} + +fn embed_author<'a>(e: &'a mut CreateEmbed, author: Option<&Member>) -> &'a mut CreateEmbed { + if let Some(author) = author { + e.footer(|f| { + f.text(format!( + "{}", + if let Some(nick) = &author.nick { + nick + } else { + &author.user.name + } + )); + + if let Some(url) = &author.user.avatar_url() { + f.icon_url(url); + } + f + }) + } else { + e + } +} + #[group] #[commands(join, leave, play, stop, next)] struct Music; @@ -145,13 +181,21 @@ async fn join(ctx: &Context, msg: &Message) -> CommandResult { } }; + let member = msg.member(&ctx.http).await?; if if let Some(member) = &msg.member { is_mute(ctx, member, guild.id).await.unwrap_or(false) } else { false } { msg.channel_id - .say(&ctx.http, "Error, you cant play music") + .send_message(&ctx.http, |m| { + embed_response( + m, + "Error", + "You don't have the right to play music", + Some(&member), + ) + }) .await?; return Ok(()); } @@ -166,7 +210,11 @@ async fn join(ctx: &Context, msg: &Message) -> CommandResult { let connect_to = match channel_id { Some(channel) => channel, None => { - msg.reply(ctx, "Not in a voice channel").await?; + msg.channel_id + .send_message(&ctx.http, |m| { + embed_response(m, "Error", "You must be on a voice channel", Some(&member)) + }) + .await?; return Ok(()); } @@ -191,21 +239,35 @@ async fn join(ctx: &Context, msg: &Message) -> CommandResult { handler_lock: handler_lock.clone(), }, ); + msg.channel_id - .say( - &ctx.http, - &format!("Joined channel {}", connect_to.mention()), - ) + .send_message(&ctx.http, |m| { + embed_response( + m, + "Joined Channel", + &format!("Joined channel {}", connect_to.mention()), + Some(&member), + ) + }) .await?; } else { msg.channel_id - .say(&ctx.http, "Error joining the channel") + .send_message(&ctx.http, |m| { + embed_response(m, "Error", "Error joining the channel", Some(&member)) + }) .await?; } } else { // TODO WARN msg.channel_id - .say(&ctx.http, "Error : Already connected on another channel") + .send_message(&ctx.http, |m| { + embed_response( + m, + "Error", + "Already connected on another channel", + Some(&member), + ) + }) .await?; } @@ -228,13 +290,21 @@ async fn leave(ctx: &Context, msg: &Message) -> CommandResult { } }; + let member = msg.member(&ctx.http).await?; if if let Some(member) = &msg.member { is_mute(ctx, member, guild_id).await.unwrap_or(false) } else { false } { msg.channel_id - .say(&ctx.http, "Error, you cant play music") + .send_message(&ctx.http, |m| { + embed_response( + m, + "Error", + "You don't have the right to play music", + Some(&member), + ) + }) .await?; return Ok(()); } @@ -246,14 +316,40 @@ async fn leave(ctx: &Context, msg: &Message) -> CommandResult { if let Some(_handler) = manager.get(guild_id) { if let Err(e) = manager.remove(guild_id).await { + log::error!("Failed to leave : {}", e); msg.channel_id - .say(&ctx.http, format!("Failed: {:?}", e)) + .send_message(&ctx.http, |m| { + embed_response( + m, + "Error", + "Failed to leave", + Some(&member), + ) + }) .await?; } - msg.channel_id.say(&ctx.http, "Left voice channel").await?; + msg.channel_id + .send_message(&ctx.http, |m| { + embed_response( + m, + "Left channel", + "", + Some(&member), + ) + }) + .await?; } else { - msg.reply(ctx, "Not in a voice channel").await?; + msg.channel_id + .send_message(&ctx.http, |m| { + embed_response( + m, + "Error", + "Not in a voice channel", + Some(&member), + ) + }) + .await?; } Ok(()) } @@ -333,12 +429,13 @@ async fn play(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { let meta = &source.metadata.clone(); handler.enqueue_source(source); + let author = msg.member(&ctx).await?; msg.channel_id .send_message(&ctx.http, |m| { if handler.queue().len() == 1 { - embed_song(m, meta); + embed_song(m, meta, Some(&author)); } else { - embed_queued(m, meta); + embed_queued(m, meta, Some(&author)); } m }) @@ -442,7 +539,7 @@ async fn stop(ctx: &Context, msg: &Message) -> CommandResult { .await .expect("Songbird Voice client placed in at initialisation.") .clone(); - + if let Some(handler) = manager.get(guild_id) { let handler = handler.lock().await; handler.queue().stop(); @@ -469,7 +566,9 @@ async fn is_mute( .entry(guild_id) .or_insert_with(|| GuildOptions::default().set_guild_id(guild_id)); - Ok(member - .roles - .contains(&guild_options.get_mute_role(ctx).await?)) + if let Some(mute_role) = guild_options.get_mute_role() { + Ok(member.roles.contains(&mute_role)) + } else { + Ok(false) + } } diff --git a/src/commands/roulette.rs b/src/commands/roulette.rs index 03c0059..614306f 100644 --- a/src/commands/roulette.rs +++ b/src/commands/roulette.rs @@ -2,7 +2,6 @@ use crate::{ api, data::{GuildOptions, GuildOptionsKey}, }; -use log::debug; use rand::Rng; use serenity::{ framework::standard::{ @@ -28,7 +27,7 @@ async fn shot(ctx: &Context, msg: &Message, args: Args) -> CommandResult { if rand::thread_rng().gen_range(0..6) == 0 { api::send_reply(ctx, &msg, "💥").await?; } else { - api::send_reply(&ctx, &msg, format!("Click ! Reloading")).await?; + api::send_reply(&ctx, &msg, "Click ! Reloading").await?; } } else { api::send_reply( @@ -74,7 +73,7 @@ async fn kick(ctx: &Context, msg: &Message) -> CommandResult { .kick_with_reason(&ctx.http, "You loose at the roulette") .await?; } else { - api::send_reply(&ctx, &msg, format!("Click ! Reloading")).await?; + api::send_reply(&ctx, &msg, "Click ! Reloading").await?; } } Ok(()) @@ -108,6 +107,5 @@ async fn disable_kick(ctx: &Context, msg: &Message, mut args: Args) -> CommandRe entry.save_async(guild_id.0).await?; } - debug!("{:?}", guilds_options); Ok(()) } diff --git a/src/commands/settings.rs b/src/commands/settings.rs new file mode 100644 index 0000000..820095f --- /dev/null +++ b/src/commands/settings.rs @@ -0,0 +1,142 @@ +use std::str::FromStr; + +use serenity::{ + client::Context, + framework::standard::{Args, CommandResult}, + model::channel::Message, +}; + +use serenity::{ + framework::standard::macros::{command, group}, + model::prelude::*, +}; + +use crate::data::{GuildOptions, GuildOptionsKey}; + +#[group] +#[prefix("settings")] +#[commands(muterole, logchannel)] +struct Settings; + +#[command] +#[description = "Set mute role"] +#[only_in(guilds)] +#[required_permissions("ADMINISTRATOR")] +async fn muterole(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { + let role_id = match args.len() { + 0 => None, + _ => Some(args.single::()?), + }; + + if let Some(id) = role_id { + let mut data = ctx.data.write().await; + + let guilds_options = data + .get_mut::() + .expect("Expected NonKickGuildsContainer in TypeMap."); + + if let Some(guild_id) = msg.guild_id { + let entry = guilds_options + .entry(guild_id) + .or_insert_with(|| GuildOptions::default().set_guild_id(guild_id)); + + entry.mute_id = match id.as_str() { + "none" => None, + _ => Some(RoleId::from_str(&id)?), + }; + + entry.save_async(guild_id.0).await?; + + msg.channel_id.say(&ctx.http, "Saved").await?; + } + } else { + let data = ctx.data.read().await; + + let guilds_options = data + .get::() + .expect("Expected NonKickGuildsContainer in TypeMap."); + + if let Some(guild_id) = msg.guild_id { + if let Some(options) = guilds_options.get(&guild_id) { + msg.channel_id + .say( + &ctx.http, + if let Some(role_id) = options.mute_id { + format!( + "Mute role is @{}", + role_id.to_role_cached(&ctx).await.unwrap().name + ) + } else { + "Mute role is None".to_string() + }, + ) + .await?; + } else { + msg.channel_id.say(&ctx.http, "Mute role is None").await?; + } + } + } + + Ok(()) +} + +#[command] +#[description = "Set mention logging channel"] +#[only_in(guilds)] +#[required_permissions("ADMINISTRATOR")] +async fn logchannel(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { + let channel_id = match args.len() { + 0 => None, + _ => Some(args.single::()?), + }; + + if let Some(id) = channel_id { + let mut data = ctx.data.write().await; + + let guilds_options = data + .get_mut::() + .expect("Expected NonKickGuildsContainer in TypeMap."); + + if let Some(guild_id) = msg.guild_id { + let entry = guilds_options + .entry(guild_id) + .or_insert_with(|| GuildOptions::default().set_guild_id(guild_id)); + + entry.mention_log_channel = match id.as_str() { + "none" => None, + _ => Some(ChannelId::from_str(&id)?), + }; + + entry.save_async(guild_id.0).await?; + + msg.channel_id.say(&ctx.http, "Saved").await?; + } + } else { + let data = ctx.data.read().await; + + let guilds_options = data + .get::() + .expect("Expected NonKickGuildsContainer in TypeMap."); + + if let Some(guild_id) = msg.guild_id { + if let Some(options) = guilds_options.get(&guild_id) { + msg.channel_id + .say( + &ctx.http, + if let Some(channel_id) = options.mention_log_channel { + format!("Logging channel is {}", channel_id.mention()) + } else { + "Logging channel is None".to_string() + }, + ) + .await?; + } else { + msg.channel_id + .say(&ctx.http, "Logging channel is None") + .await?; + } + } + } + + Ok(()) +} diff --git a/src/data/guilds_options.rs b/src/data/guilds_options.rs index 53eefdb..cf29bd0 100644 --- a/src/data/guilds_options.rs +++ b/src/data/guilds_options.rs @@ -2,8 +2,8 @@ use log::error; use serde::{Deserialize, Serialize}; use serenity::{ model::{ - channel::Message, - prelude::{GuildId, RoleId, UserId}, + id::ChannelId, + prelude::{GuildId, RoleId}, }, prelude::TypeMapKey, }; @@ -15,18 +15,13 @@ use std::{ }; use tokio::io::AsyncWriteExt; -#[cfg(feature = "music")] -use serenity::{http::CacheHttp, model::prelude::PartialGuild}; - #[derive(Debug, Serialize, Deserialize)] pub(crate) struct GuildOptions { #[serde(skip_serializing)] pub(crate) guild_id: Option, - #[serde(skip_serializing)] pub(crate) mute_id: Option, + pub(crate) mention_log_channel: Option, pub(crate) roulette_options: RouletteOptions, - pub(crate) last_ghost_pings: Vec, - pub(crate) mutes: Vec<(UserId, u64)>, } impl GuildOptions { @@ -89,62 +84,10 @@ impl GuildOptions { } #[cfg(feature = "music")] - pub(crate) async fn get_mute_role( - &mut self, - cache: &C, - ) -> tokio::io::Result { - match self.mute_id { - Some(mute_id) => Ok(mute_id), - - None => { - if let Some(guild_id) = self.guild_id { - let guild: PartialGuild = cache - .http() - .get_guild(guild_id.0) - .await - .map_err(|e| tokio::io::Error::new(tokio::io::ErrorKind::Other, e))?; - - match guild.role_by_name("mute") { - Some(role) => { - self.mute_id = Some(role.id); - Ok(role.id) - } - None => Err(tokio::io::Error::new( - tokio::io::ErrorKind::Other, - "Unkown role", - )), - } - } else { - Err(tokio::io::Error::new( - tokio::io::ErrorKind::Other, - "Unkown guild id", - )) - } - } - } - } - - 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); - } + pub(crate) fn get_mute_role( + &self + ) -> Option { + self.mute_id } } @@ -154,8 +97,7 @@ impl Default for GuildOptions { roulette_options: RouletteOptions::default(), guild_id: None, mute_id: None, - last_ghost_pings: Vec::new(), - mutes: Vec::new(), + mention_log_channel: None, } } } @@ -187,22 +129,3 @@ 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 97796ae..e8219f1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ use crate::{ commands::{ admin::ADMIN_GROUP, general::GENERAL_GROUP, owner::OWNER_GROUP, roulette::ROULETTE_GROUP, + settings::SETTINGS_GROUP, }, data::{BulletsContainer, GuildOptions, GuildOptionsKey, ShardManagerContainer}, }; @@ -40,6 +41,8 @@ mod config; mod data; mod presence; +const MINIMUM_MENTIONS: usize = 10; + const PREFIX: &str = "?"; static mut LOG_ATTACHMENTS: bool = false; pub(crate) static mut INVITE_URL: Option = None; @@ -114,7 +117,8 @@ async fn main() -> IoResult<()> { .group(&ADMIN_GROUP) .group(&GENERAL_GROUP) .group(&ROULETTE_GROUP) - .group(&OWNER_GROUP); + .group(&OWNER_GROUP) + .group(&SETTINGS_GROUP); #[cfg(feature = "music")] { @@ -200,67 +204,74 @@ impl EventHandler for Messages { } } - async fn message(&self, _ctx: Context, new_message: Message) { + async fn message(&self, ctx: Context, new_message: Message) { if unsafe { LOG_ATTACHMENTS } && !new_message.attachments.is_empty() { - for att in new_message.attachments { + for att in &new_message.attachments { if let Err(e) = download_to_log(att).await { error!("Error while downloading to log : {:?}", e); } } } + + if let Err(e) = log_mentions(ctx, &new_message).await { + error!("Error while logging mentions: {}", e); + } } 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) - ); +async fn log_mentions(ctx: Context, new_message: &Message) -> CommandResult { + let data = ctx.data.read().await; - 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 guilds_options = data + .get::() + .expect("Expected NonKickGuildsContainer in TypeMap."); - 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); + if new_message.mention_everyone { + if let Some(guild_id) = new_message.guild_id { + if let Some(options) = guilds_options.get(&guild_id) { + if let Some(role_id) = options.mute_id { + let mut member = new_message.member(&ctx).await?; + member.add_role(&ctx.http, role_id).await?; } } } } - async fn message_update( - &self, - _ctx: Context, - old_if_available: Option, - new: Option, - _event: MessageUpdateEvent, - ) { - log::debug!("update : {:?} vs {:?}", old_if_available, new); + if new_message.mention_everyone + || (new_message.mention_roles.len() + new_message.mentions.len()) > MINIMUM_MENTIONS + { + if let Some(guild_id) = new_message.guild_id { + if let Some(options) = guilds_options.get(&guild_id) { + if let Some(channel_id) = options.mention_log_channel { + channel_id + .send_message(&ctx.http, |m| { + m.embed(|e| { + e.title("New message with mentions") + .fields(vec![ + ("Sender", new_message.author.mention().to_string(), false), + ("Content", new_message.content.clone(), false), + ( + "Channel", + new_message.channel_id.mention().to_string(), + false, + ), + ]) + .colour((247, 76, 0)) + }) + }) + .await?; + } + } + } } + Ok(()) } -async fn download_to_log(attachment: Attachment) -> commands::Result<()> { +async fn download_to_log(attachment: &Attachment) -> commands::Result<()> { debug!("Download_to_log : {:?}", attachment); let path = Path::new("logging").join(format!("{}-{}", attachment.id, attachment.filename)); let content = reqwest::get(&attachment.url).await?.bytes().await?;