@@ -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"] | |||
hex = "0.4.2" |
@@ -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::<Interaction>(&string_buf[..string_len]).err().unwrap()); | |||
println!("Invalid json payload {}\n({})", &string_buf[..string_len], serde_json::from_str::<InteractionRaw>(&string_buf[..string_len]).err().unwrap()); | |||
return Outcome::Failure((Status::Unauthorized, ())) | |||
} | |||
println!("Invalid body"); | |||
@@ -1,13 +1,27 @@ | |||
use crate::discord; | |||
use crate::discord::{Interaction, InteractionResponse, InteractionApplicationCommandCallbackData}; | |||
// fn foo() -> (definition, private?) | |||
pub fn hello_world() -> (discord::ApplicationCommand, Option<String>) { | |||
// hello-world data for telling Discord about it | |||
pub fn def_hello_world() -> (discord::ApplicationCommand, Option<String>) { | |||
(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, | |||
}), | |||
} | |||
} |
@@ -5,9 +5,12 @@ pub struct InteractionRaw { | |||
#[serde(rename = "type")] | |||
pub type_: usize, | |||
pub token: Option<String>, | |||
pub member: Option<GuildMember>, | |||
pub id: Option<String>, | |||
pub guild_id: Option<String>, | |||
pub data: Option<ApplicationCommandInteractionData>, | |||
pub channel_id: Option<String>, | |||
pub version: Option<usize>, | |||
} | |||
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<InteractionCommand> { | |||
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<User>, | |||
pub nick: Option<String>, | |||
pub role: Vec<String>, | |||
pub joined_at: String, | |||
pub premium_since: Option<String>, | |||
pub deaf: bool, | |||
pub mute: bool, | |||
pub pending: Option<bool>, | |||
pub permissions: Option<String>, | |||
} | |||
#[derive(Serialize, Deserialize, Clone)] | |||
pub struct User { | |||
pub id: String, | |||
pub username: String, | |||
pub discriminator: String, | |||
pub avatar: Option<String>, | |||
pub bot: Option<bool>, | |||
pub system: Option<bool>, | |||
pub mfa_enabled: Option<bool>, | |||
pub locale: Option<String>, | |||
pub verified: Option<String>, | |||
pub email: Option<String>, | |||
pub flags: Option<usize>, | |||
pub premium_type: Option<usize>, | |||
pub public_flags: Option<usize>, | |||
} | |||
#[derive(Serialize, Deserialize, Clone)] | |||
pub struct ApplicationCommandInteractionData { | |||
pub id: String, | |||
pub name: String, | |||
pub options: Option<Vec<ApplicationCommandInteractionData>>, | |||
} | |||
pub enum InteractionResponse { | |||
@@ -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 = "<interaction>")] | |||
fn root_post(interaction: AuthenticatedInteraction) -> Json<InteractionResponseRaw> { | |||
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::<String, HashSet<String>>::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<String>), | |||
client: &reqwest::blocking::Client, | |||
registered: &mut HashMap<String, HashSet<String>>) { | |||
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<ApplicationCommand> { | |||
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(); | |||
} | |||
*/ |