Skip to content

Commit 511eff9

Browse files
committed
Clean up slash commands
1 parent 740b7b2 commit 511eff9

10 files changed

Lines changed: 492 additions & 416 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
.build/
22
.history
3+
result
4+

shell.nix

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,11 @@ let
3131
cargo run $@
3232
'';
3333

34-
utils = [
34+
utils = with pkgs; [
3535
build # cargo build
3636
run # cargo run
37+
cachix
38+
jq
3739
];
3840

3941
in pkgs.mkShell rec {

src/commands/mod.rs

Lines changed: 183 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,202 @@
1-
use core::panic;
21
use serenity::builder::CreateApplicationCommand;
3-
use serenity::Error;
2+
use serenity::builder::CreateApplicationCommandOption;
3+
use serenity::builder::CreateEmbed;
44
use serenity::http::Http;
55
use serenity::model::prelude::command::Command;
6+
use serenity::model::prelude::command::CommandOptionType;
67
use serenity::model::prelude::interaction::application_command::ApplicationCommandInteraction;
8+
use serenity::model::prelude::interaction::application_command::CommandDataOptionValue;
79
use serenity::prelude::Context;
8-
use std::sync::Arc;
9-
use crate::commands::snippets::*;
10-
use crate::commands::types::*;
10+
use serenity::prelude::TypeMapKey;
11+
use serenity::utils::Colour;
12+
use std::collections::HashMap;
13+
use crate::structures::State;
1114

12-
pub mod snippets;
13-
pub mod types;
15+
mod snippets;
1416

15-
pub async fn get_commands(ctx: &Context) -> Vec<CreateApplicationCommand> {
16-
vec![
17-
SnippetCommand::register(ctx).await,
18-
SetSnippetCommand::register(ctx).await,
19-
RemoveSnippetCommand::register(ctx).await,
20-
ExportSnippetCommand::register(ctx).await
21-
]
17+
pub async fn register(ctx: &Context) -> ApplicationCommandMap {
18+
println!("Registering slash commands...");
19+
20+
let mut data = ctx.data.write().await;
21+
let state = data.get_mut::<State>()
22+
.expect("Failed to get state");
23+
24+
let commands = ApplicationCommandMap::new(state);
25+
26+
match commands.register(ctx).await {
27+
Ok(c) => println!("Registered {} slash commands", c.len()),
28+
Err(e) => println!("Failed to register slash commands: {}", e)
29+
}
30+
31+
commands
32+
}
33+
34+
pub async fn interact(ctx: &Context, interaction: &ApplicationCommandInteraction) {
35+
match interaction.data.name.as_str() {
36+
"snippet" => snippets::snippet(ctx, interaction).await,
37+
"create-snippet" => snippets::create_snippet(ctx, interaction).await,
38+
"edit-snippet" => snippets::edit_snippet(ctx, interaction).await,
39+
"remove-snippet" => snippets::remove_snippet(ctx, interaction).await,
40+
"export-snippet" => snippets::export_snippet(ctx, interaction).await,
41+
_ => println!("WARNING: Received invalid application command interaction!: {}", interaction.data.name)
42+
}
2243
}
2344

24-
pub async fn register(ctx: &Context) -> Result<Vec<Command>, Error> {
25-
let commands = get_commands(ctx).await;
45+
pub(crate) const ACCENT_COLOUR: Colour = Colour(0x8957e5);
46+
pub(crate) const OK_COLOUR: Colour = Colour(0x2ecc71);
47+
pub(crate) const ERROR_COLOUR: Colour = Colour(0xe74c3c);
48+
49+
type CommandHashMap = HashMap<&'static str, CreateApplicationCommand>;
2650

27-
Command::set_global_application_commands(&ctx.http, |c|
28-
c.set_application_commands(commands)
29-
).await
51+
#[derive(Clone)]
52+
pub struct ApplicationCommandMap(pub CommandHashMap);
53+
54+
impl TypeMapKey for ApplicationCommandMap {
55+
type Value = ApplicationCommandMap;
3056
}
3157

32-
pub async fn unregister(http: &Arc<Http>) -> Result<(), Error> {
33-
if let Ok(commands) = Command::get_global_application_commands(http).await {
34-
for command in commands {
35-
if let Err(e) = Command::delete_global_application_command(http, command.id).await {
36-
return Err(e);
58+
impl ApplicationCommandMap {
59+
pub fn new(state: &State) -> ApplicationCommandMap {
60+
let mut id_opt = CreateApplicationCommandOption::default();
61+
id_opt.name("id")
62+
.description("The snippet's id")
63+
.kind(CommandOptionType::String)
64+
.required(true);
65+
66+
let mut title_opt = CreateApplicationCommandOption::default();
67+
title_opt.name("title")
68+
.description("The snippet's title")
69+
.kind(CommandOptionType::String);
70+
71+
let mut content_opt = CreateApplicationCommandOption::default();
72+
content_opt.name("content")
73+
.description("The snippet's content")
74+
.kind(CommandOptionType::String);
75+
76+
let snippet = CreateApplicationCommand::default()
77+
.description("Shows a snippet")
78+
.clone();
79+
80+
let create_snippet = CreateApplicationCommand::default()
81+
.description("Creates a snippet")
82+
.add_option(id_opt)
83+
.add_option(title_opt.required(true).clone())
84+
.add_option(content_opt.required(true).clone())
85+
.clone();
86+
87+
let edit_snippet = CreateApplicationCommand::default()
88+
.description("Edits a snippet")
89+
.add_option(title_opt.required(false).clone())
90+
.add_option(content_opt.required(false).clone())
91+
.clone();
92+
93+
let remove_snippet = CreateApplicationCommand::default()
94+
.description("Removes a snippet")
95+
.clone();
96+
97+
let export_snippet = CreateApplicationCommand::default()
98+
.description("Exports a snippet for user editing")
99+
.clone();
100+
101+
let mut commands = ApplicationCommandMap(CommandHashMap::new());
102+
103+
commands.insert("snippet", snippet);
104+
commands.insert("create-snippet", create_snippet);
105+
commands.insert("edit-snippet", edit_snippet);
106+
commands.insert("remove-snippet", remove_snippet);
107+
commands.insert("export-snippet", export_snippet);
108+
109+
for (name, command) in commands.0.iter_mut() {
110+
match *name {
111+
"snippet" => snippets::sync_snippets(state, command),
112+
"remove-snippet" => snippets::sync_snippets(state, command),
113+
"export-snippet" => snippets::sync_snippets(state, command),
114+
"edit-snippet" => snippets::sync_snippets(state, command),
115+
_ => ()
37116
}
38117
}
118+
119+
commands
120+
}
121+
122+
fn insert(&mut self, k: &'static str, v: CreateApplicationCommand) -> Option<CreateApplicationCommand> {
123+
self.0.insert(k, v)
39124
}
40125

41-
Ok(())
126+
fn builders(&self) -> Vec<CreateApplicationCommand> {
127+
self.0.iter()
128+
.map(|p| {
129+
let mut builder = p.1.clone();
130+
builder.name(p.0);
131+
builder
132+
})
133+
.collect::<Vec<CreateApplicationCommand>>()
134+
}
135+
136+
pub async fn register(&self, http: impl AsRef<Http>) -> Result<Vec<Command>, serenity::Error> {
137+
Command::set_global_application_commands(http, |commands| {
138+
commands.set_application_commands(self.builders())
139+
}).await
140+
}
141+
}
142+
143+
pub fn arg(interaction: &ApplicationCommandInteraction, name: &'static str) -> CommandDataOptionValue {
144+
arg_opt(interaction, name).expect(&format!("No '{name}' argument provided")).clone()
145+
}
146+
147+
pub fn arg_opt(interaction: &ApplicationCommandInteraction, name: &'static str) -> Option<CommandDataOptionValue> {
148+
let opt = interaction.data.options.iter()
149+
.find(|o| o.name == name);
150+
151+
if let Some(opt) = opt {
152+
opt.resolved.as_ref().cloned()
153+
} else {
154+
None
155+
}
156+
}
157+
158+
pub async fn respond_embed(ctx: &Context, interaction: &ApplicationCommandInteraction, embed: &CreateEmbed, ephemeral: bool) {
159+
let result = interaction.create_followup_message(ctx, |r| r
160+
.add_embed(embed.clone())
161+
.ephemeral(ephemeral)
162+
).await;
163+
164+
if let Err(e) = result {
165+
println!("Failed to respond to interaction: {} {}", interaction.data.name, e)
166+
}
167+
}
168+
169+
pub async fn respond_ok(ctx: &Context, interaction: &ApplicationCommandInteraction, title: &str, content: &str) {
170+
let mut embed = CreateEmbed::default();
171+
let embed = embed
172+
.title(title)
173+
.description(content)
174+
.colour(OK_COLOUR);
175+
176+
respond_embed(ctx, interaction, embed, false).await;
42177
}
43178

44-
pub async fn interact(ctx: &Context, command: &ApplicationCommandInteraction) {
45-
match command.data.name.as_str() {
46-
SNIPPET_NAME => SnippetCommand::invoke(ctx, command),
47-
SET_SNIPPET_NAME => SetSnippetCommand::invoke(ctx, command),
48-
REMOVE_SNIPPET_NAME => RemoveSnippetCommand::invoke(ctx, command),
49-
EXPORT_SNIPPET_NAME => ExportSnippetCommand::invoke(ctx, command),
50-
_ => panic!("Invalid interaction command: {}", command.data.name)
51-
}.await
179+
pub async fn respond_err(ctx: &Context, interaction: &ApplicationCommandInteraction, title: &str, content: &str) {
180+
let mut embed = CreateEmbed::default();
181+
let embed = embed
182+
.title(title)
183+
.description(content)
184+
.colour(ERROR_COLOUR);
185+
186+
respond_embed(ctx, interaction, embed, false).await;
187+
}
188+
189+
pub async fn update_commands(ctx: &Context) {
190+
let mut data = ctx.data.write().await;
191+
let state = data.get_mut::<State>()
192+
.expect("Failed to get state");
193+
194+
println!("Updating commands...");
195+
let command_map = ApplicationCommandMap::new(state);
196+
197+
println!("Registering newly updated commands");
198+
match command_map.register(ctx).await {
199+
Ok(commands) => println!("Successfully updated {} commands", commands.len()),
200+
Err(e) => println!("Failed to update commands: {e}")
201+
}
52202
}

0 commit comments

Comments
 (0)