diff --git a/Cargo.toml b/Cargo.toml index 5d81582..9b7bb92 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ edition = "2018" music = ["serenity/voice", "songbird"] [dependencies] -serenity = "0.10" +serenity = { version = "0.10", features = ["unstable_discord_api"] } toml = "0.5" serde = { version = "1.0", features = ["derive"] } reqwest = "0.11" diff --git a/src/commands/general.rs b/src/commands/general.rs index c6a6933..991961c 100644 --- a/src/commands/general.rs +++ b/src/commands/general.rs @@ -23,7 +23,8 @@ use serenity::{ older, ping, random_mute, - uptime + uptime, + button )] pub struct General; @@ -283,6 +284,24 @@ async fn _image(ctx: &Context, msg: &Message, mut args: Args) -> crate::commands Ok(()) } +#[command] +async fn button(ctx: &Context, msg: &Message) -> CommandResult { + msg.channel_id + .send_message(ctx, |msg| { + msg.content("An innofensive button").components(|f| { + f.create_action_row(|row| { + row.create_button(|b| { + b.style(ButtonStyle::Danger) + .label("Big Red Fucking Button") + .custom_id("gulag_button") + }) + }) + }) + }) + .await?; + Ok(()) +} + #[command] #[required_permissions(ADMINISTRATOR)] async fn random_mute(ctx: &Context, msg: &Message) -> CommandResult { @@ -336,19 +355,22 @@ async fn uptime(ctx: &Context, msg: &Message) -> CommandResult { let min = (seconds - (hours * 60 * 60)) / 60; let seconds = seconds - (hours * 3600) - (min * 60); - msg.channel_id.send_message(&ctx.http, |m| { - m.embed(|e| { - e.field( - "Uptime", - if hours > 0 { - format!("{}h {:02}min {:02}s", hours, min, seconds) - } else { - format!("{}min {:02}s", min, seconds) - }, - false, - ).colour((247, 76, 0)) + msg.channel_id + .send_message(&ctx.http, |m| { + m.embed(|e| { + e.field( + "Uptime", + if hours > 0 { + format!("{}h {:02}min {:02}s", hours, min, seconds) + } else { + format!("{}min {:02}s", min, seconds) + }, + false, + ) + .colour((247, 76, 0)) + }) }) - }).await?; + .await?; Ok(()) } diff --git a/src/commands/interaction.rs b/src/commands/interaction.rs index d1eefdd..2ff6040 100644 --- a/src/commands/interaction.rs +++ b/src/commands/interaction.rs @@ -1,91 +1,85 @@ -use serde::Serialize; -use serde_json::Value; use serenity::{ - builder::CreateEmbed, client::Context, framework::standard::CommandResult, - model::{channel::Embed, guild::Member, Permissions}, + model::{ + guild::Member, + interactions::{ + ApplicationCommandInteractionData, Interaction, + InteractionApplicationCommandCallbackDataFlags, InteractionData, + InteractionResponseType, MessageComponent, + }, + Permissions, + }, }; use crate::{data::GuildOptionsKey, utils::message::embed_author}; -pub(crate) async fn handle_interaction(ctx: &Context, object: Value) -> CommandResult { - let data = object.get("data").ok_or("Failed to get data")?; +pub(crate) async fn handle_interaction(ctx: &Context, interaction: &Interaction) -> CommandResult { + let data = interaction.data.as_ref().ok_or("Failed to get data")?; - let id = object - .get("id") - .and_then(|i| i.as_str()) - .ok_or("Failed to get ID")?; - - let token = object - .get("token") - .and_then(|t| t.as_str()) - .ok_or("Failed to get token")?; - - let name = data - .get("name") - .and_then(|v| v.as_str()) - .ok_or("Failed to get name")?; - - let guild_id = object - .get("guild_id") - .and_then(|v| v.as_str()) - .and_then(|v| v.parse::().ok()) - .ok_or("Failed to get guild id")?; - - let author = object.get("member").ok_or("Failed to get author")?; - - let author_id = author - .get("user") - .and_then(|v| v.get("id")) - .and_then(|v| v.as_str()) - .and_then(|v| v.parse::().ok()) - .ok_or("Failed to get permission")?; - - let permissions = Permissions::from_bits_truncate( - author - .get("permissions") - .and_then(|v| v.as_str()) - .and_then(|v| v.parse::().ok()) - .ok_or("Failed to get permission")?, - ); - - let author_member = ctx - .cache - .guild(guild_id) - .await - .ok_or("Failed to get guild")? - .member(&ctx.http, author_id) - .await?; - - match name { - "goulag" => { - if permissions.administrator() { - goulag(ctx, data, guild_id, &author_member, id, token).await? - } else { - Response::new_with_embed(4, |e| { - embed_author(e, Some(&author_member)) - .title("Error") - .description("You don't have the right to do that") - .colour((247, 76, 0)) - }) - .send(token, id) - .await?; - } + match data { + InteractionData::ApplicationCommand(app) => { + command(ctx, &interaction, &app).await?; } - _ => (), + InteractionData::MessageComponent(msg) => message_interact(ctx, interaction, msg).await?, } Ok(()) } +async fn command( + ctx: &Context, + interaction: &Interaction, + data: &ApplicationCommandInteractionData, +) -> CommandResult { + let name = &data.name; + + let guild_id = interaction.guild_id.ok_or("Failed to get guild id")?; + + let author = interaction + .member + .as_ref() + .ok_or("Failed to get permission")?; + + let permissions = Permissions::from_bits_truncate( + author + .permissions + .map(|v| v.bits) + .ok_or("Failed to get permission")?, + ); + + match name.as_str() { + "goulag" => { + if permissions.administrator() { + goulag(ctx, interaction, data, guild_id.0, &author).await? + } else { + interaction + .create_interaction_response(&ctx.http, |response| { + response + .kind(InteractionResponseType::ChannelMessageWithSource) + .interaction_response_data(|message| { + message.create_embed(|e| { + embed_author(e, Some(author)) + .title("Error") + .description("You don't have the right to do that") + .colour((247, 76, 0)) + }) + }) + }) + .await?; + } + } + _ => (), + } + Ok(()) +} + async fn goulag( ctx: &Context, - data: &Value, + interaction: &Interaction, + data: &ApplicationCommandInteractionData, guild_id: u64, author: &Member, - id: &str, - token: &str, ) -> CommandResult { let ctx_data = ctx.data.read().await; let ctx_data = ctx_data @@ -96,24 +90,16 @@ async fn goulag( if let Some(guild_options) = guild_options { if let Some(mute_role) = guild_options.get_mute_role() { - let options = data - .get("options") - .and_then(|v| v.as_array()) - .ok_or("Failed to get options")?; + let options = &data.options; let user = options .iter() - .find(|f| { - if let Some(name) = f.get("name").map(|n| n.as_str()).and_then(|f| f) { - name == "user" - } else { - false - } - }) + .find(|f| f.name == "user") .ok_or("Failed to get user")?; let user_id = user - .get("value") + .value + .as_ref() .and_then(|v| v.as_str()) .and_then(|v| v.parse::().ok()) .ok_or("Failed to get user id")?; @@ -128,143 +114,83 @@ async fn goulag( member.add_role(&ctx.http, mute_role).await?; - Response::new_with_embed(4, |e| { - embed_author(e, Some(author)) - .title("Mute") - .description(format!("{} was muted", member.display_name())) - .colour((247, 76, 0)) - }) - .send(token, id) - .await?; - + interaction + .create_interaction_response(&ctx.http, |response| { + response + .kind(InteractionResponseType::ChannelMessageWithSource) + .interaction_response_data(|message| { + message.create_embed(|e| { + embed_author(e, Some(author)) + .title("Mute") + .description(format!("{} was muted", member.display_name())) + .colour((247, 76, 0)) + }) + }) + }) + .await?; return Ok(()); } } - Response::new_with_embed(4, |e| { - embed_author(e, Some(author)) - .title("Error") - .description("Unknown mute role") - .colour((247, 76, 0)) - }) - .send(token, id) - .await?; + interaction + .create_interaction_response(&ctx.http, |response| { + response + .kind(InteractionResponseType::ChannelMessageWithSource) + .interaction_response_data(|message| { + message.create_embed(|e| { + embed_author(e, Some(author)) + .title("Error") + .description("Unknown mute role") + .colour((247, 76, 0)) + }) + }) + }) + .await?; Ok(()) } -#[derive(Debug, Serialize)] -pub(crate) struct ResponseData { - tts: bool, - content: Option, - embeds: Option>, - flags: u64, -} +async fn message_interact( + ctx: &Context, + interaction: &Interaction, + msg_interaction: &MessageComponent, +) -> CommandResult { + match msg_interaction.custom_id.as_str() { + "gulag_button" => { + let guild_id = interaction + .guild_id + .as_ref() + .ok_or("Failed to get guild id")?; -#[allow(dead_code)] -impl ResponseData { - pub(crate) fn embed(mut self, f: F) -> Self - where - F: FnOnce(&mut CreateEmbed) -> &mut CreateEmbed, - { - let embeds = self.embeds.get_or_insert(Default::default()); + let mut member = interaction.member.clone().ok_or("Failed to get user")?; + let role = match ctx + .cache + .guild(guild_id) + .await + .unwrap() + .role_by_name("mute") + { + Some(role) => Ok(role.id), + None => Err(tokio::io::Error::new( + tokio::io::ErrorKind::Other, + "Unkown role", + )), + }?; - embeds.push(Embed::fake(f)); - - self - } - - /// Get a reference to the response data's tts. - pub(crate) fn tts(&self) -> &bool { - &self.tts - } - - /// Get a reference to the response data's content. - pub(crate) fn content(&self) -> &Option { - &self.content - } - - /// Get a reference to the response data's flags. - pub(crate) fn flags(&self) -> &u64 { - &self.flags - } - - /// Set the response data's content. - pub(crate) fn set_content(mut self, content: Option) -> Self { - self.content = content; - self - } - - /// Set the response data's tts. - pub(crate) fn set_tts(mut self, tts: bool) -> Self { - self.tts = tts; - self - } - - /// Set the response data's flags. - pub(crate) fn set_flags(mut self, flags: u64) -> Self { - self.flags = flags; - self - } - - /// Get a reference to the response data's embeds. - pub(crate) fn embeds(&self) -> &Option> { - &self.embeds - } - - /// Get a mutable reference to the response data's embeds. - pub(crate) fn embeds_mut(&mut self) -> &mut Option> { - &mut self.embeds - } - - /// Set the response data's embeds. - pub(crate) fn set_embeds(mut self, embeds: Option>) -> Self { - self.embeds = embeds; - self - } -} - -impl Default for ResponseData { - fn default() -> Self { - Self { - tts: false, - content: None, - embeds: None, - flags: 0, + member.add_role(ctx, role).await?; + interaction + .create_interaction_response(&ctx.http, |response| { + response + .kind(InteractionResponseType::ChannelMessageWithSource) + .interaction_response_data(|message| { + message + .content("<:cheh:780736245675982909>") + .flags(InteractionApplicationCommandCallbackDataFlags::EPHEMERAL) + }) + }) + .await?; } + _ => {} } -} - -#[derive(Debug, Serialize)] -pub(crate) struct Response { - #[serde(rename(serialize = "type"))] - response_type: u64, - data: Option, -} - -impl Response { - pub(crate) fn new_with_embed(response_type: u64, f: F) -> Self - where - F: FnOnce(&mut CreateEmbed) -> &mut CreateEmbed, - { - Response { - response_type, - data: Some(ResponseData::default().embed(f)), - } - } - - pub(crate) async fn send(&self, token: &str, id: &str) -> CommandResult { - reqwest::Client::new() - .post( - format!( - "https://discord.com/api/v8/interactions/{}/{}/callback", - id, token - ) - .as_str(), - ) - .json(self) - .send() - .await?; - Ok(()) - } + Ok(()) } diff --git a/src/commands/music/mod.rs b/src/commands/music/mod.rs index 67c6143..acd81df 100644 --- a/src/commands/music/mod.rs +++ b/src/commands/music/mod.rs @@ -305,6 +305,7 @@ async fn play(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { }) .await?; return Ok(()); + /* msg.channel_id .send_message(&ctx.http, |m| { embed_response( @@ -361,6 +362,7 @@ async fn play(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { }; handler.enqueue_source(s); + } let track = handler.queue().current(); @@ -385,6 +387,7 @@ async fn play(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { .await?; } return Ok(()); + */ } else { match songbird::ytdl(&url).await { Ok(source) => source, diff --git a/src/commands/music/youtube.rs b/src/commands/music/youtube.rs index 22b7990..a85ae8f 100644 --- a/src/commands/music/youtube.rs +++ b/src/commands/music/youtube.rs @@ -1,8 +1,11 @@ +/* use serde::Deserialize; use tokio::{ io::{Error, ErrorKind, Result}, process::Command, }; + + pub(crate) async fn get_list_of_urls(url: &str) -> Result> { let output = Command::new("youtube-dl") .args(&["-j", "--flat-playlist", &url]) @@ -30,3 +33,5 @@ pub(crate) async fn get_list_of_urls(url: &str) -> Result> { struct YtdlResponse { url: String, } + +*/ diff --git a/src/config.rs b/src/config.rs index 3938d26..027ffa3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -8,6 +8,7 @@ pub(crate) struct Conf { #[derive(Debug, Deserialize)] pub(crate) struct Bot { pub(crate) token: String, + pub(crate) application_id: u64, pub(crate) log_attachments: Option, pub(crate) invite_url: Option, } diff --git a/src/data/mod.rs b/src/data/mod.rs index 32ab9eb..d5ae97f 100644 --- a/src/data/mod.rs +++ b/src/data/mod.rs @@ -19,8 +19,7 @@ impl TypeMapKey for BulletsContainer { type Value = HashMap; } - pub(crate) struct Uptime; impl TypeMapKey for Uptime { type Value = std::time::Instant; -} \ No newline at end of file +} diff --git a/src/main.rs b/src/main.rs index 6c7e1d7..b8c4177 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,6 @@ use crate::{ use async_trait::async_trait; use commands::interaction; use log::{debug, error, info}; -use serde_json::Value; use serenity::{ framework::standard::{ help_commands, @@ -27,7 +26,7 @@ use std::{ path::Path, sync::Arc, time::Duration, - time::Instant + time::Instant, }; use tokio::{fs::File, io::AsyncWriteExt}; @@ -44,7 +43,7 @@ mod data; mod presence; mod utils; -const MINIMUM_MENTIONS: usize = 10; +const MINIMUM_MENTIONS: usize = 20; const PREFIX: &str = "?"; static mut LOG_ATTACHMENTS: bool = false; @@ -140,6 +139,7 @@ async fn main() -> IoResult<()> { } let mut client = Client::builder(&token) + .application_id(conf.bot.application_id) .event_handler(Messages {}) .framework(framework); @@ -224,41 +224,85 @@ impl EventHandler for Messages { } } - async fn unknown(&self, ctx: Context, name: String, raw: Value) { - match name.as_str() { - "INTERACTION_CREATE" => { - if let Err(e) = interaction::handle_interaction(&ctx, raw).await { - error!("While handling interaction : {}", e); - } - } - _ => debug!("Unknown event : {}, {:?}", name, raw), - }; + async fn interaction_create(&self, ctx: Context, interaction: Interaction) { + if let Err(e) = interaction::handle_interaction(&ctx, &interaction).await { + error!("While handling interaction : {}", e); + } } } async fn log_mentions(ctx: Context, new_message: &Message) -> CommandResult { + if !new_message.mention_everyone + && new_message.mention_roles.is_empty() + && new_message.mentions.is_empty() + { + return Ok(()); + } + let data = ctx.data.read().await; let guilds_options = data .get::() .expect("Expected NonKickGuildsContainer in TypeMap."); - if new_message.mention_everyone { + let mute = if new_message.mention_everyone { + true + } else { + let mut user_mentioned: HashSet = HashSet::with_capacity(MINIMUM_MENTIONS); // TODO IN GUILD OPTIONS + + let guild = new_message.guild(&ctx).await.ok_or("Failed to get guild")?; + + let mut iter_users = new_message.mentions.iter(); + while user_mentioned.len() < MINIMUM_MENTIONS { + if let Some(u) = iter_users.next() { + user_mentioned.insert(u.id.0); + } else { + break; + } + } + + let mut iter_roles = new_message.mention_roles.iter(); + + let mut continue_getting_members = true; + let mut after = None; + + while continue_getting_members { + let members = guild.members(&ctx, Some(1000), after).await?; + + while user_mentioned.len() < MINIMUM_MENTIONS { + if let Some(r) = iter_roles.next() { + for member in members.iter() { + if let Some(roles) = member.roles(&ctx).await { + if roles.iter().any(|role| role.id.0 == r.0) { + log::debug!("{:?}", member); + user_mentioned.insert(member.user.id.0); + } + } + } + } else { + break; + } + } + + if members.len() == 1000 { + continue_getting_members = true; + after = Some(members.last().unwrap().user.id); + } else { + continue_getting_members = false; + } + } + + user_mentioned.len() >= MINIMUM_MENTIONS + }; + + if mute { 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?; } - } - } - } - 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| {