Add integrations + better mentions handling + big red button

This commit is contained in:
oupson 2021-06-20 12:21:21 +02:00
parent d1edef3623
commit 644bd66ec3
8 changed files with 246 additions and 246 deletions

View File

@ -10,7 +10,7 @@ edition = "2018"
music = ["serenity/voice", "songbird"] music = ["serenity/voice", "songbird"]
[dependencies] [dependencies]
serenity = "0.10" serenity = { version = "0.10", features = ["unstable_discord_api"] }
toml = "0.5" toml = "0.5"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
reqwest = "0.11" reqwest = "0.11"

View File

@ -23,7 +23,8 @@ use serenity::{
older, older,
ping, ping,
random_mute, random_mute,
uptime uptime,
button
)] )]
pub struct General; pub struct General;
@ -283,6 +284,24 @@ async fn _image(ctx: &Context, msg: &Message, mut args: Args) -> crate::commands
Ok(()) 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] #[command]
#[required_permissions(ADMINISTRATOR)] #[required_permissions(ADMINISTRATOR)]
async fn random_mute(ctx: &Context, msg: &Message) -> CommandResult { 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 min = (seconds - (hours * 60 * 60)) / 60;
let seconds = seconds - (hours * 3600) - (min * 60); let seconds = seconds - (hours * 3600) - (min * 60);
msg.channel_id.send_message(&ctx.http, |m| { msg.channel_id
m.embed(|e| { .send_message(&ctx.http, |m| {
e.field( m.embed(|e| {
"Uptime", e.field(
if hours > 0 { "Uptime",
format!("{}h {:02}min {:02}s", hours, min, seconds) if hours > 0 {
} else { format!("{}h {:02}min {:02}s", hours, min, seconds)
format!("{}min {:02}s", min, seconds) } else {
}, format!("{}min {:02}s", min, seconds)
false, },
).colour((247, 76, 0)) false,
)
.colour((247, 76, 0))
})
}) })
}).await?; .await?;
Ok(()) Ok(())
} }

View File

@ -1,91 +1,85 @@
use serde::Serialize;
use serde_json::Value;
use serenity::{ use serenity::{
builder::CreateEmbed,
client::Context, client::Context,
framework::standard::CommandResult, 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}; use crate::{data::GuildOptionsKey, utils::message::embed_author};
pub(crate) async fn handle_interaction(ctx: &Context, object: Value) -> CommandResult { pub(crate) async fn handle_interaction(ctx: &Context, interaction: &Interaction) -> CommandResult {
let data = object.get("data").ok_or("Failed to get data")?; let data = interaction.data.as_ref().ok_or("Failed to get data")?;
let id = object match data {
.get("id") InteractionData::ApplicationCommand(app) => {
.and_then(|i| i.as_str()) command(ctx, &interaction, &app).await?;
.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::<u64>().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::<u64>().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::<u64>().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?;
}
} }
_ => (), InteractionData::MessageComponent(msg) => message_interact(ctx, interaction, msg).await?,
} }
Ok(()) 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( async fn goulag(
ctx: &Context, ctx: &Context,
data: &Value, interaction: &Interaction,
data: &ApplicationCommandInteractionData,
guild_id: u64, guild_id: u64,
author: &Member, author: &Member,
id: &str,
token: &str,
) -> CommandResult { ) -> CommandResult {
let ctx_data = ctx.data.read().await; let ctx_data = ctx.data.read().await;
let ctx_data = ctx_data let ctx_data = ctx_data
@ -96,24 +90,16 @@ async fn goulag(
if let Some(guild_options) = guild_options { if let Some(guild_options) = guild_options {
if let Some(mute_role) = guild_options.get_mute_role() { if let Some(mute_role) = guild_options.get_mute_role() {
let options = data let options = &data.options;
.get("options")
.and_then(|v| v.as_array())
.ok_or("Failed to get options")?;
let user = options let user = options
.iter() .iter()
.find(|f| { .find(|f| f.name == "user")
if let Some(name) = f.get("name").map(|n| n.as_str()).and_then(|f| f) {
name == "user"
} else {
false
}
})
.ok_or("Failed to get user")?; .ok_or("Failed to get user")?;
let user_id = user let user_id = user
.get("value") .value
.as_ref()
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.and_then(|v| v.parse::<u64>().ok()) .and_then(|v| v.parse::<u64>().ok())
.ok_or("Failed to get user id")?; .ok_or("Failed to get user id")?;
@ -128,143 +114,83 @@ async fn goulag(
member.add_role(&ctx.http, mute_role).await?; member.add_role(&ctx.http, mute_role).await?;
Response::new_with_embed(4, |e| { interaction
embed_author(e, Some(author)) .create_interaction_response(&ctx.http, |response| {
.title("Mute") response
.description(format!("{} was muted", member.display_name())) .kind(InteractionResponseType::ChannelMessageWithSource)
.colour((247, 76, 0)) .interaction_response_data(|message| {
}) message.create_embed(|e| {
.send(token, id) embed_author(e, Some(author))
.await?; .title("Mute")
.description(format!("{} was muted", member.display_name()))
.colour((247, 76, 0))
})
})
})
.await?;
return Ok(()); return Ok(());
} }
} }
Response::new_with_embed(4, |e| { interaction
embed_author(e, Some(author)) .create_interaction_response(&ctx.http, |response| {
.title("Error") response
.description("Unknown mute role") .kind(InteractionResponseType::ChannelMessageWithSource)
.colour((247, 76, 0)) .interaction_response_data(|message| {
}) message.create_embed(|e| {
.send(token, id) embed_author(e, Some(author))
.await?; .title("Error")
.description("Unknown mute role")
.colour((247, 76, 0))
})
})
})
.await?;
Ok(()) Ok(())
} }
#[derive(Debug, Serialize)] async fn message_interact(
pub(crate) struct ResponseData { ctx: &Context,
tts: bool, interaction: &Interaction,
content: Option<String>, msg_interaction: &MessageComponent,
embeds: Option<Vec<Value>>, ) -> CommandResult {
flags: u64, 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)] let mut member = interaction.member.clone().ok_or("Failed to get user")?;
impl ResponseData { let role = match ctx
pub(crate) fn embed<F>(mut self, f: F) -> Self .cache
where .guild(guild_id)
F: FnOnce(&mut CreateEmbed) -> &mut CreateEmbed, .await
{ .unwrap()
let embeds = self.embeds.get_or_insert(Default::default()); .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)); member.add_role(ctx, role).await?;
interaction
self .create_interaction_response(&ctx.http, |response| {
} response
.kind(InteractionResponseType::ChannelMessageWithSource)
/// Get a reference to the response data's tts. .interaction_response_data(|message| {
pub(crate) fn tts(&self) -> &bool { message
&self.tts .content("<:cheh:780736245675982909>")
} .flags(InteractionApplicationCommandCallbackDataFlags::EPHEMERAL)
})
/// Get a reference to the response data's content. })
pub(crate) fn content(&self) -> &Option<String> { .await?;
&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<String>) -> 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<Vec<Value>> {
&self.embeds
}
/// Get a mutable reference to the response data's embeds.
pub(crate) fn embeds_mut(&mut self) -> &mut Option<Vec<Value>> {
&mut self.embeds
}
/// Set the response data's embeds.
pub(crate) fn set_embeds(mut self, embeds: Option<Vec<Value>>) -> Self {
self.embeds = embeds;
self
}
}
impl Default for ResponseData {
fn default() -> Self {
Self {
tts: false,
content: None,
embeds: None,
flags: 0,
} }
_ => {}
} }
} Ok(())
#[derive(Debug, Serialize)]
pub(crate) struct Response {
#[serde(rename(serialize = "type"))]
response_type: u64,
data: Option<ResponseData>,
}
impl Response {
pub(crate) fn new_with_embed<F>(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(())
}
} }

View File

@ -305,6 +305,7 @@ async fn play(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
}) })
.await?; .await?;
return Ok(()); return Ok(());
/*
msg.channel_id msg.channel_id
.send_message(&ctx.http, |m| { .send_message(&ctx.http, |m| {
embed_response( embed_response(
@ -361,6 +362,7 @@ async fn play(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
}; };
handler.enqueue_source(s); handler.enqueue_source(s);
} }
let track = handler.queue().current(); let track = handler.queue().current();
@ -385,6 +387,7 @@ async fn play(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
.await?; .await?;
} }
return Ok(()); return Ok(());
*/
} else { } else {
match songbird::ytdl(&url).await { match songbird::ytdl(&url).await {
Ok(source) => source, Ok(source) => source,

View File

@ -1,8 +1,11 @@
/*
use serde::Deserialize; use serde::Deserialize;
use tokio::{ use tokio::{
io::{Error, ErrorKind, Result}, io::{Error, ErrorKind, Result},
process::Command, process::Command,
}; };
pub(crate) async fn get_list_of_urls(url: &str) -> Result<Vec<String>> { pub(crate) async fn get_list_of_urls(url: &str) -> Result<Vec<String>> {
let output = Command::new("youtube-dl") let output = Command::new("youtube-dl")
.args(&["-j", "--flat-playlist", &url]) .args(&["-j", "--flat-playlist", &url])
@ -30,3 +33,5 @@ pub(crate) async fn get_list_of_urls(url: &str) -> Result<Vec<String>> {
struct YtdlResponse { struct YtdlResponse {
url: String, url: String,
} }
*/

View File

@ -8,6 +8,7 @@ pub(crate) struct Conf {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub(crate) struct Bot { pub(crate) struct Bot {
pub(crate) token: String, pub(crate) token: String,
pub(crate) application_id: u64,
pub(crate) log_attachments: Option<bool>, pub(crate) log_attachments: Option<bool>,
pub(crate) invite_url: Option<String>, pub(crate) invite_url: Option<String>,
} }

View File

@ -19,8 +19,7 @@ impl TypeMapKey for BulletsContainer {
type Value = HashMap<u64, (u8, u8)>; type Value = HashMap<u64, (u8, u8)>;
} }
pub(crate) struct Uptime; pub(crate) struct Uptime;
impl TypeMapKey for Uptime { impl TypeMapKey for Uptime {
type Value = std::time::Instant; type Value = std::time::Instant;
} }

View File

@ -8,7 +8,6 @@ use crate::{
use async_trait::async_trait; use async_trait::async_trait;
use commands::interaction; use commands::interaction;
use log::{debug, error, info}; use log::{debug, error, info};
use serde_json::Value;
use serenity::{ use serenity::{
framework::standard::{ framework::standard::{
help_commands, help_commands,
@ -27,7 +26,7 @@ use std::{
path::Path, path::Path,
sync::Arc, sync::Arc,
time::Duration, time::Duration,
time::Instant time::Instant,
}; };
use tokio::{fs::File, io::AsyncWriteExt}; use tokio::{fs::File, io::AsyncWriteExt};
@ -44,7 +43,7 @@ mod data;
mod presence; mod presence;
mod utils; mod utils;
const MINIMUM_MENTIONS: usize = 10; const MINIMUM_MENTIONS: usize = 20;
const PREFIX: &str = "?"; const PREFIX: &str = "?";
static mut LOG_ATTACHMENTS: bool = false; static mut LOG_ATTACHMENTS: bool = false;
@ -140,6 +139,7 @@ async fn main() -> IoResult<()> {
} }
let mut client = Client::builder(&token) let mut client = Client::builder(&token)
.application_id(conf.bot.application_id)
.event_handler(Messages {}) .event_handler(Messages {})
.framework(framework); .framework(framework);
@ -224,41 +224,85 @@ impl EventHandler for Messages {
} }
} }
async fn unknown(&self, ctx: Context, name: String, raw: Value) { async fn interaction_create(&self, ctx: Context, interaction: Interaction) {
match name.as_str() { if let Err(e) = interaction::handle_interaction(&ctx, &interaction).await {
"INTERACTION_CREATE" => { error!("While handling interaction : {}", e);
if let Err(e) = interaction::handle_interaction(&ctx, raw).await { }
error!("While handling interaction : {}", e);
}
}
_ => debug!("Unknown event : {}, {:?}", name, raw),
};
} }
} }
async fn log_mentions(ctx: Context, new_message: &Message) -> CommandResult { 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 data = ctx.data.read().await;
let guilds_options = data let guilds_options = data
.get::<GuildOptionsKey>() .get::<GuildOptionsKey>()
.expect("Expected NonKickGuildsContainer in TypeMap."); .expect("Expected NonKickGuildsContainer in TypeMap.");
if new_message.mention_everyone { let mute = if new_message.mention_everyone {
true
} else {
let mut user_mentioned: HashSet<u64> = 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(guild_id) = new_message.guild_id {
if let Some(options) = guilds_options.get(&guild_id) { if let Some(options) = guilds_options.get(&guild_id) {
if let Some(role_id) = options.mute_id { if let Some(role_id) = options.mute_id {
let mut member = new_message.member(&ctx).await?; let mut member = new_message.member(&ctx).await?;
member.add_role(&ctx.http, role_id).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 { if let Some(channel_id) = options.mention_log_channel {
channel_id channel_id
.send_message(&ctx.http, |m| { .send_message(&ctx.http, |m| {