From 6c476e2f98e916479f47d56c23fed045a5622717 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Mon, 22 Feb 2021 19:56:22 -0500 Subject: [PATCH] Create response framework --- Cargo.toml | 8 +-- src/auth_tools.rs | 5 +- src/command_definitions.rs | 18 ++++++- src/discord.rs | 97 ++++++++++++++++++++++++++++++++++--- src/main.rs | 99 ++++++++++++++------------------------ 5 files changed, 149 insertions(+), 78 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8e5d3f8..d43987f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,14 +8,10 @@ edition = "2018" [dependencies] rocket = "0.4.7" +rocket_contrib = { version = "0.4.7", default-features = false, features = ["json"]} reqwest = { version = "0.11.1", features = ["json", "blocking"]} ed25519-dalek = "1.0.1" lazy_static = "1.4.0" serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0" } -hex = "0.4.2" -# futures = "0.3.12" - -[dependencies.rocket_contrib] -default-features = false -features = ["json"] \ No newline at end of file +hex = "0.4.2" \ No newline at end of file diff --git a/src/auth_tools.rs b/src/auth_tools.rs index 37abffb..cb74424 100644 --- a/src/auth_tools.rs +++ b/src/auth_tools.rs @@ -56,9 +56,12 @@ impl FromDataSimple for AuthenticatedInteraction { timestamp: timestamp.clone(), interaction: payload.interaction(), }; + if auth.interaction.is_invalid() { + return Outcome::Failure((Status::BadRequest, ())); + } return Outcome::Success(auth); } - println!("Invalid json payload {}\n({})", &string_buf[..string_len], serde_json::from_str::(&string_buf[..string_len]).err().unwrap()); + println!("Invalid json payload {}\n({})", &string_buf[..string_len], serde_json::from_str::(&string_buf[..string_len]).err().unwrap()); return Outcome::Failure((Status::Unauthorized, ())) } println!("Invalid body"); diff --git a/src/command_definitions.rs b/src/command_definitions.rs index 8f47200..0aaf1d1 100644 --- a/src/command_definitions.rs +++ b/src/command_definitions.rs @@ -1,13 +1,27 @@ use crate::discord; +use crate::discord::{Interaction, InteractionResponse, InteractionApplicationCommandCallbackData}; // fn foo() -> (definition, private?) -pub fn hello_world() -> (discord::ApplicationCommand, Option) { +// hello-world data for telling Discord about it +pub fn def_hello_world() -> (discord::ApplicationCommand, Option) { (discord::ApplicationCommand { id: None, application_id: None, name: "hello-world".to_string(), description: "Hello World".to_string(), options: None, - }, None)//Some("616329232389505055".to_string())) + }, Some("616329232389505055".to_string())) +} + +// hello-world action when someone uses the command on Discord +pub fn hello_world(interaction: &Interaction) -> InteractionResponse { + let cmd = interaction.cmd().unwrap(); + InteractionResponse::ChannelMessage { + data: Some(InteractionApplicationCommandCallbackData { + tts: false, + content: format!("Hello {}!", cmd.member.nick.unwrap_or(cmd.member.user.unwrap().username)), + allowed_mentions: None, + }), + } } \ No newline at end of file diff --git a/src/discord.rs b/src/discord.rs index 4435383..11dd451 100644 --- a/src/discord.rs +++ b/src/discord.rs @@ -5,9 +5,12 @@ pub struct InteractionRaw { #[serde(rename = "type")] pub type_: usize, pub token: Option, + pub member: Option, pub id: Option, pub guild_id: Option, + pub data: Option, pub channel_id: Option, + pub version: Option, } impl InteractionRaw { @@ -16,29 +19,30 @@ impl InteractionRaw { 1 => Interaction::Ping {}, 2 => Interaction::Command { token: self.token.clone().unwrap(), + member: self.member.clone().unwrap(), id: self.id.clone().unwrap(), guild_id: self.guild_id.clone().unwrap(), + data: self.data.clone().unwrap(), channel_id: self.channel_id.clone().unwrap(), + version: self.version.unwrap_or(1), }, _ => Interaction::Invalid {}, } } } -#[derive(Serialize, Deserialize, Clone)] -#[serde(tag = "type")] pub enum Interaction { - #[serde(rename = "1")] + //#[serde(rename = "1")] Ping {}, - #[serde(rename = "2")] + //#[serde(rename = "2")] Command { token: String, - // member: Member, + member: GuildMember, id: String, guild_id: String, - // data: Command, + data: ApplicationCommandInteractionData, channel_id: String, - //version: usize, + version: usize, }, Invalid {}, } @@ -51,6 +55,85 @@ impl Interaction { Self::Invalid {..} => false, } } + + pub fn is_command(&self) -> bool { + match self { + Self::Ping {..} => false, + Self::Command {..} => true, + Self::Invalid {..} => false, + } + } + + pub fn is_invalid(&self) -> bool { + match self { + Self::Ping {..} => false, + Self::Command {..} => false, + Self::Invalid {..} => true, + } + } + + pub fn cmd(&self) -> Option { + match self { + Self::Ping {..} => None, + Self::Command { token, member, id, guild_id, data, channel_id, version } => Some(InteractionCommand { + token: token.to_string(), + member: member.clone(), + id: id.to_string(), + guild_id: guild_id.to_string(), + data: data.clone(), + channel_id: channel_id.to_string(), + version: *version, + }), + Self::Invalid {..} => None, + } + } +} + +pub struct InteractionCommand { + pub token: String, + pub member: GuildMember, + pub id: String, + pub guild_id: String, + pub data: ApplicationCommandInteractionData, + pub channel_id: String, + pub version: usize, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct GuildMember { + pub user: Option, + pub nick: Option, + pub role: Vec, + pub joined_at: String, + pub premium_since: Option, + pub deaf: bool, + pub mute: bool, + pub pending: Option, + pub permissions: Option, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct User { + pub id: String, + pub username: String, + pub discriminator: String, + pub avatar: Option, + pub bot: Option, + pub system: Option, + pub mfa_enabled: Option, + pub locale: Option, + pub verified: Option, + pub email: Option, + pub flags: Option, + pub premium_type: Option, + pub public_flags: Option, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct ApplicationCommandInteractionData { + pub id: String, + pub name: String, + pub options: Option>, } pub enum InteractionResponse { diff --git a/src/main.rs b/src/main.rs index 333df96..ecf19c4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,9 +10,8 @@ use lazy_static::lazy_static; use rocket_contrib::json::Json; use std::sync::RwLock; use crate::auth_tools::{AuthenticatedInteraction}; -use crate::discord::{InteractionResponse, InteractionApplicationCommandCallbackData, ApplicationCommand, InteractionResponseRaw}; -use crate::command_definitions::hello_world; -use std::collections::{HashMap, HashSet}; +use crate::discord::{Interaction, InteractionResponse, /*ApplicationCommand, */InteractionResponseRaw}; +use crate::command_definitions::{hello_world, def_hello_world}; static GLOBAL_COMMAND_KEY: &str = "GLOBAL command KEY"; @@ -24,17 +23,18 @@ lazy_static! { #[post("/", data = "")] fn root_post(interaction: AuthenticatedInteraction) -> Json { - if interaction.interaction.is_ping() { - return Json(InteractionResponse::Pong{}.raw()) - } - let resp = InteractionResponse::ChannelMessage { - data: Some(InteractionApplicationCommandCallbackData { - tts: false, - content: "Hello".to_string(), - allowed_mentions: None, - }), - }; - Json(resp.raw()) + Json( + match &interaction.interaction { + Interaction::Ping {} => InteractionResponse::Pong {}, + Interaction::Command {data, ..} => { + match &data.name as &str { + "hello-world" => hello_world(&interaction.interaction), + _ => InteractionResponse::AcknowledgeWithSource {}, + } + }, + _ => InteractionResponse::AcknowledgeWithSource {}, + }.raw() + ) } #[get("/")] @@ -63,67 +63,41 @@ fn main() { // send API requests to bootstrap commands let req_client = reqwest::blocking::Client::new(); - let mut seen_cmds = HashMap::>::new(); - // TODO - register_command(&hello_world, &req_client, &mut seen_cmds); + // TODO add more commands + register_command(&def_hello_world, &req_client); // start web server rocket::ignite().mount("/", routes![root_post, hello_get]).launch(); } fn register_command(f: &dyn Fn() -> (discord::ApplicationCommand, Option), - client: &reqwest::blocking::Client, - registered: &mut HashMap>) { + client: &reqwest::blocking::Client) { let (payload, guild_opt) = f(); if let Some(guild_id) = guild_opt { - if !registered.contains_key(&guild_id) { - let mut seen_guild_cmds = HashSet::new(); - let cmds = get_guild_commands(client, &guild_id); - for c in cmds { - println!("Found app command {} ID:{} App:{}", &c.name, &c.id.unwrap(), guild_id); - seen_guild_cmds.insert(c.name); - } - registered.insert(guild_id.clone(), seen_guild_cmds); - } - if !registered.get(&guild_id).unwrap().contains(&payload.name) { - // create new command - let url = format!("https://discord.com/api/v8/applications/{}/guilds/{}/commands", APPLICATION_ID.read().unwrap().as_ref().unwrap().clone(), &guild_id); - let res = client.post(&url) - .header("Authorization", format!("Bot {}", BOT_TOKEN.read().unwrap().as_ref().unwrap().clone())) - .json(&payload) - .send(); - if let Ok(d) = res { - println!("`{}` status {}", &payload.name, &d.status().as_str()); - println!("{}", &d.text().unwrap()); - registered.get_mut(&guild_id).unwrap().insert(payload.name); - } + // create/upsert command + let url = format!("https://discord.com/api/v8/applications/{}/guilds/{}/commands", APPLICATION_ID.read().unwrap().as_ref().unwrap().clone(), &guild_id); + let res = client.post(&url) + .header("Authorization", format!("Bot {}", BOT_TOKEN.read().unwrap().as_ref().unwrap().clone())) + .json(&payload) + .send(); + if let Ok(d) = res { + println!("`{}` status {}", &payload.name, &d.status().as_str()); + println!("{}", &d.text().unwrap()); } } else { - if !registered.contains_key(GLOBAL_COMMAND_KEY) { - let mut seen_cmds = HashSet::new(); - let cmds = get_commands(client); - for c in cmds { - println!("Found global command {} ID:{}", &c.name, &c.id.unwrap()); - seen_cmds.insert(c.name); - } - registered.insert(GLOBAL_COMMAND_KEY.to_string(), seen_cmds); + // create/upsert command + let url = format!("https://discord.com/api/v8/applications/{}/commands", APPLICATION_ID.read().unwrap().as_ref().unwrap().clone()); + let res = client.post(&url) + .header("Authorization", format!("Bot {}", BOT_TOKEN.read().unwrap().as_ref().unwrap().clone())) + .json(&payload) + .send(); + if let Ok(d) = res { + println!("`{}` status {}", &payload.name, &d.status().as_str()); + println!("{}", &d.text().unwrap()); } - if !registered.get(GLOBAL_COMMAND_KEY).unwrap().contains(&payload.name) { - // create new command - let url = format!("https://discord.com/api/v8/applications/{}/commands", APPLICATION_ID.read().unwrap().as_ref().unwrap().clone()); - let res = client.post(&url) - .header("Authorization", format!("Bot {}", BOT_TOKEN.read().unwrap().as_ref().unwrap().clone())) - .json(&payload) - .send(); - if let Ok(d) = res { - println!("`{}` status {}", &payload.name, &d.status().as_str()); - println!("{}", &d.text().unwrap()); - registered.get_mut(GLOBAL_COMMAND_KEY.clone()).unwrap().insert(payload.name); - } - } - } } +/* fn get_commands(client: &reqwest::blocking::Client) -> Vec { let url = format!("https://discord.com/api/v8/applications/{}/commands", APPLICATION_ID.read().unwrap().as_ref().unwrap().clone()); let res = client.get(&url) @@ -147,3 +121,4 @@ fn get_guild_commands(client: &reqwest::blocking::Client, guild_id: &str) -> Vec } return Vec::new(); } +*/