Register/delete guild commands on call

As the title already should explain my question but here I go in depth: Is it possible to register/delete guild commands on a specific guild in the runtime?
21 Replies
Favna
Favna•13mo ago
Discord.js
Discord.js is a powerful node.js module that allows you to interact with the Discord API very easily. It takes a much more object-oriented approach than most other JS Discord libraries, making your bot's code significantly tidier and easier to comprehend.
Favna
Favna•13mo ago
Oh and also uh You also have to add it to the command store so it gets recognised Just keep in mind that you're treading in the advanced territory. It's possible yes but you'll have to write a fair bit of code For example you also have to track the commands in an external database or similar to re-register them on next reboot
ShowCast
ShowCast•13mo ago
Oh yeah I did that in the past myself but I thought maybe there is a way for sapphire to do so but it's fine then I'll have to re-use my old code 😄 Thx for the answers <:PES_Ok:493353112501747712>
Favna
Favna•13mo ago
yeah so the only sapphire thing you need to add is something along the lines of
container.stores.get('commands').set('myFreshNewCommand', allTheData)
container.stores.get('commands').set('myFreshNewCommand', allTheData)
Assuming of course it's not there yet. From the top of my head we don't check guildIds when a command interaction comes in so you could also just have a command in the store at boot time (by having a file for it) and only register it in your test guild through guildIds (or don't register it at all) then add additional guilds later. @vladdy what do you think of exposing that function we call in CoreReady to register all commands so people can use the out-of-command way of adding to the registries (by acquiring them) and then calling that function to do registration?
vladdy
vladdy•13mo ago
I would rather first get an example use case haha Realistically speaking, registries are pretty self contained So it shouldn't take too much code I'll think about how to properly expose this when I'm back home as I'd rather we didn't just expose our register methods (esp since this wouldn't be compatible with bulk override)
Favna
Favna•13mo ago
Correct me if I'm wrong here ShowCast but I think it's something like this 1. Some commands are either opt-in on a per-guild basis through a dashboard or there are premium commands so they are selectively made available 2. The command availability is tracked in a database. For this example we will reason this from boot time, so in our own Ready event. dummy code:
export class ReadyListener extends Listener {

public async run() {
const commandsData = dbConnection.get('commands');

for ({ guildId, ...commandData } of commandsData) {
const guild = await this.container.client.guilds.fetch(guildId);
guild.commands.create(commandData);
}
}
}
export class ReadyListener extends Listener {

public async run() {
const commandsData = dbConnection.get('commands');

for ({ guildId, ...commandData } of commandsData) {
const guild = await this.container.client.guilds.fetch(guildId);
guild.commands.create(commandData);
}
}
}
vladdy
vladdy•13mo ago
I feel like instead we can as a method on registries that trigger just that commands registries and handles everything else for the user, but definitely an advanced feature not for most users to use @ShowCast if you can share an example of current code that would be lovely
Favna
Favna•13mo ago
definitely that latter part yes
ShowCast
ShowCast•13mo ago
My code base is an old use case. My use case would be the following: There is a premium system. If a server owner redeems a premium key for his server the command /premium should get registered. My old code was for some other use case where on boot a repository posted the command to every guild but with a configurationable command per server.
const commandsFromApi = await this.rest.get(Routes.applicationGuildCommands(envParseKey('DISCORD_CLIENT_ID'), discordGuild.id))
const lfgCommand = commandsFromApi.find((command: any) => command.name === 'lfg')
try {
await this.rest
.patch(
Routes.applicationGuildCommand(
envParseKey('DISCORD_CLIENT_ID'),
discordGuild.id,
lfgCommand.id
),
{body: commandBuilder.toJSON()}
).catch(console.error)
sapphireContainer.logger.info(`Successfully registered application commands for guild ${discordGuild.name}[${discordGuild.id}]`);
} catch (error: any) {
this.client.sendToErrorChannel(error);
}
const commandsFromApi = await this.rest.get(Routes.applicationGuildCommands(envParseKey('DISCORD_CLIENT_ID'), discordGuild.id))
const lfgCommand = commandsFromApi.find((command: any) => command.name === 'lfg')
try {
await this.rest
.patch(
Routes.applicationGuildCommand(
envParseKey('DISCORD_CLIENT_ID'),
discordGuild.id,
lfgCommand.id
),
{body: commandBuilder.toJSON()}
).catch(console.error)
sapphireContainer.logger.info(`Successfully registered application commands for guild ${discordGuild.name}[${discordGuild.id}]`);
} catch (error: any) {
this.client.sendToErrorChannel(error);
}
I created this earlier before I started to use sapphire so this isn't compatible yet
vladdy
vladdy•13mo ago
Oh i see So question
ShowCast
ShowCast•13mo ago
Would appreciate that as then I would only need to call it on guild base when like the redeemd premium key command is ran on that guild
vladdy
vladdy•13mo ago
Why can't you add this into the register application command method? And then reload just that command Cause then you can fetch this at bootup from a db of ids And whenever you reload Wait shit that won't handle deleted unless you use bulk overwrite
ShowCast
ShowCast•13mo ago
The delete would happen in my case when premium expired or the key on the guild is unlinked with a command So yeah that would be possible aswell
vladdy
vladdy•13mo ago
Hmmmm Let me get home
ShowCast
ShowCast•13mo ago
ofc 😄
vladdy
vladdy•13mo ago
And I'll see what i can concoct
ShowCast
ShowCast•13mo ago
Perfect, I'm curious what your mind will print out kekGasm
vladdy
vladdy•13mo ago
Might not even need any code on our part Do you use bulk overwrite? @ShowCast
ShowCast
ShowCast•13mo ago
Nope
vladdy
vladdy•13mo ago
@ShowCast something like this should be enough
import { Command, container } from '@sapphire/framework';

export async function addGuildToCommand(guildId: string) {
// Store it so its fetchable on the command side
await storeInADb(guildId);

const command = container.stores.get('commands').get('cmd')!;

// Get old guild ids from the registry (.keys() returns the guild ids, .values() returns the guild command ids, entries is obv)
const oldGuildIds = new Map([...command.applicationCommandRegistry.guildCommandIds.entries()]);

// reload the cmd, which will handle new registrations
await command.reload();

// Get new guild ids from the registry
const newGuildIds = [...command.applicationCommandRegistry.guildCommandIds.keys()];

// Delete all new guild ids from the old guild ids
for (const newGuildId of newGuildIds) {
oldGuildIds.delete(newGuildId);
}

// Delete all old guild ids from discord
for (const [oldGuildId, commandId] of oldGuildIds) {
try {
await container.client.application!.commands.delete(commandId, oldGuildId);
} catch (err) {
container.logger.warn(`Failed to delete command ${commandId} from guild ${oldGuildId}`, err);
}
}
}

class MyCommand extends Command {
public override async registerApplicationCommands(registry: Command.Registry) {
const guildIds = await getGuildIdsFromDb();

registry.registerChatInputCommand((cmd) => cmd, { guildIds });
}
}
import { Command, container } from '@sapphire/framework';

export async function addGuildToCommand(guildId: string) {
// Store it so its fetchable on the command side
await storeInADb(guildId);

const command = container.stores.get('commands').get('cmd')!;

// Get old guild ids from the registry (.keys() returns the guild ids, .values() returns the guild command ids, entries is obv)
const oldGuildIds = new Map([...command.applicationCommandRegistry.guildCommandIds.entries()]);

// reload the cmd, which will handle new registrations
await command.reload();

// Get new guild ids from the registry
const newGuildIds = [...command.applicationCommandRegistry.guildCommandIds.keys()];

// Delete all new guild ids from the old guild ids
for (const newGuildId of newGuildIds) {
oldGuildIds.delete(newGuildId);
}

// Delete all old guild ids from discord
for (const [oldGuildId, commandId] of oldGuildIds) {
try {
await container.client.application!.commands.delete(commandId, oldGuildId);
} catch (err) {
container.logger.warn(`Failed to delete command ${commandId} from guild ${oldGuildId}`, err);
}
}
}

class MyCommand extends Command {
public override async registerApplicationCommands(registry: Command.Registry) {
const guildIds = await getGuildIdsFromDb();

registry.registerChatInputCommand((cmd) => cmd, { guildIds });
}
}
Only thing it wont handle is if you register the cmd while the bot is offline but i'm sure you can figure that out in a ready listener :3 hope it helps! (hope it works >~>) technically speaking you can inline newGuildIds with the for loop This way you get to reuse as much work as there already is for command registration, and have to only deal with deletions (if you dont wanna deal with tracking those too, bulk overwrite MIGHT WORK but no guarantees)
ShowCast
ShowCast•13mo ago
I will try that thanks for the code snippet <:PES2_LovePat:513350757333073930>