import { CommandContext, CommandOptionType, ComponentContext, SlashCommand, type SlashCreator } from 'slash-create/web'; import { calculateUnlockedValues, DELETE_ID, DONE_ID, generateErrorMessageFor, generateFieldFor, generateMessageFor, generateValuesFor, getEmbedFrom, loadEmbed, populateLocksFor, REROLL_ID, RollTableNames, SELECT_ID, selectUnlockedFrom } from './generated.js'; import { type DbAccess, DeleteResult, UpdateResult } from './dbAccess.js'; import { isTable, RollTableOrder, ValueAccess } from './rolltable.js'; import { getTimestamp, isSnowflake, type Snowflake } from 'discord-snowflake'; export class ResponseCommand extends SlashCommand { private readonly db: DbAccess private readonly baseUrl: string; constructor(creator: SlashCreator, db: DbAccess, baseUrl: string, forGuilds?: Snowflake|Snowflake[]) { super(creator, { name: "response", description: "Modifies the responses available in the generator.", nsfw: false, guildIDs: forGuilds, dmPermission: true, options: [ { type: CommandOptionType.SUB_COMMAND, name: "add", description: "Adds a new response to the generator.", options: [ { type: CommandOptionType.INTEGER, name: "table", description: "The table to insert the response into.", choices: RollTableOrder.map(v => ({name: RollTableNames[v], value: v})), required: true, }, { type: CommandOptionType.STRING, name: "text", description: "The text to use as the response.", required: true, } ] }, { type: CommandOptionType.SUB_COMMAND, name: "list", description: "Lists responses that will appear in /generate in the current context." }, { type: CommandOptionType.SUB_COMMAND, name: "edit", description: "Modifies a response that was previously created.", options: [ { type: CommandOptionType.INTEGER, name: "table", description: "The table to update the response from.", choices: RollTableOrder.map(v => ({name: RollTableNames[v], value: v})), required: true, }, { type: CommandOptionType.STRING, name: "old_text", description: "The text of the response to edit.", required: true, }, { type: CommandOptionType.STRING, name: "new_text", description: "The text to replace the response with.", required: true, } ] }, { type: CommandOptionType.SUB_COMMAND, name: "delete", description: "Deletes a response that was previously created.", options: [ { type: CommandOptionType.INTEGER, name: "table", description: "The table to delete the response from.", choices: RollTableOrder.map(v => ({name: RollTableNames[v], value: v})), required: true, }, { type: CommandOptionType.STRING, name: "text", description: "The text of the response to delete.", required: true, }, ] }, ] }); this.baseUrl = baseUrl this.db = db } async run(ctx: CommandContext): Promise { switch (ctx.subcommands[0]) { case "add": try { await this.onAdd(ctx) } catch (e) { await ctx.send(generateErrorMessageFor(e, "add a new response")) } break case "list": try { await this.onList(ctx) } catch (e) { await ctx.send(generateErrorMessageFor(e, "get the list URL")) } break case "edit": try { await this.onEdit(ctx) } catch (e) { await ctx.send(generateErrorMessageFor(e, "edit a response")) } break case "delete": try { await this.onDelete(ctx) } catch (e) { await ctx.send(generateErrorMessageFor(e, "delete a response")) } break default: await ctx.send(generateErrorMessageFor(Error("I don't know what command you want"), "manage responses")) break } } private async onAdd(ctx: CommandContext): Promise { const guildId = ctx.guildID ?? null const userId = ctx.user.id const id = ctx.interactionID if (!isSnowflake(id)) { throw Error("the snowflake wasn't a snowflake") } const timestamp = getTimestamp(id) const table = ctx.options['add']['table'] if (!isTable(table)) { throw Error(`there's no table number ${table}`) } const text = ctx.options['add']['text'] const { timestamp: insertedTimestamp, access, inserted } = await this.db.putResponse(timestamp, table, text, userId, guildId, guildId === null ? ValueAccess.CreatorDM : ValueAccess.Server) await ctx.send({ embeds: [{ title: `${inserted ? 'Your new' : 'An existing'}${access === ValueAccess.Global ? " global" : ""} response`, fields: [generateFieldFor(table, text)], timestamp: new Date(insertedTimestamp), }], ephemeral: !inserted, }) } private async onList(ctx: CommandContext) { if (ctx.guildID) { await ctx.send({ embeds: [{ title: `Response list for this server`, description: "Shows all global and server-local responses.", url: `${this.baseUrl}/responses?server=${ctx.guildID}`, }] }) } else { await ctx.send({ embeds: [{ title: `Response list for DMs`, description: "It's not supported right now, so please just hang tight." }] }) } } private async onEdit(ctx: CommandContext): Promise { const guildId = ctx.guildID ?? null const userId = ctx.user.id const id = ctx.interactionID if (!isSnowflake(id)) { throw Error("the snowflake wasn't a snowflake") } const timestamp = getTimestamp(id) const table = ctx.options['edit']['table'] if (!isTable(table)) { throw Error(`there's no table number ${table}`) } const oldText = ctx.options['edit']['old_text'] const newText = ctx.options['edit']['new_text'] const result = await this.db.updateResponse(timestamp, table, oldText, newText, userId, guildId, guildId === null ? ValueAccess.CreatorDM : ValueAccess.Server) switch (result.result) { case UpdateResult.Updated: await ctx.send({ embeds: [{ title: `Your updated response`, fields: [generateFieldFor(table, oldText), generateFieldFor(table, newText)], timestamp: new Date(timestamp).toISOString() }], }) break case UpdateResult.NewConflict: await ctx.send({ embeds: [{ title: `An existing${result.access === ValueAccess.Global ? " global" : ""} response`, fields: [generateFieldFor(table, newText)], timestamp: new Date(result.timestamp).toISOString(), }], ephemeral: true, }) break case UpdateResult.NoOldText: await ctx.send({ embeds: [{ title: `A nonexistent response`, fields: [generateFieldFor(table, oldText)], }], ephemeral: true, }) break case UpdateResult.OldGlobal: await ctx.send({ embeds: [{ title: `An uneditable global response`, fields: [generateFieldFor(table, oldText)], }], ephemeral: true, }) break } } private async onDelete(ctx: CommandContext): Promise { const guildId = ctx.guildID ?? null const userId = ctx.user.id const id = ctx.interactionID if (!isSnowflake(id)) { throw Error("the snowflake wasn't a snowflake") } const timestamp = getTimestamp(id) const table = ctx.options['delete']['table'] if (!isTable(table)) { throw Error(`there's no table number ${table}`) } const text = ctx.options['delete']['text'] const result = await this.db.deleteResponse(table, text, userId, guildId, guildId === null ? ValueAccess.CreatorDM : ValueAccess.Server) switch (result.result) { case DeleteResult.Deleted: await ctx.send({ embeds: [{ title: 'Your deleted response', fields: [generateFieldFor(table, text)], timestamp: new Date(result.timestamp).toISOString(), }] }) break case DeleteResult.OldGlobal: await ctx.send({ embeds: [{ title: 'An undeletable global response', fields: [generateFieldFor(table, text)], timestamp: new Date(result.timestamp).toISOString(), }], ephemeral: true }) break case DeleteResult.NoOldText: await ctx.send({ embeds: [{ title: 'A nonexistent response', fields: [generateFieldFor(table, text)], }], ephemeral: true }) break } } } export class GenerateCommand extends SlashCommand { private readonly db: DbAccess constructor(creator: SlashCreator, db: DbAccess, forGuilds?: Snowflake|Snowflake[]) { super(creator, { name: "generate", description: "Generates a new scenario to play with and sends it to the current channel.", nsfw: false, dmPermission: true, guildIDs: forGuilds, throttling: { duration: 5, usages: 1, } }); this.db = db if (!forGuilds) { creator.registerGlobalComponent(DONE_ID, this.onDone.bind(this)) creator.registerGlobalComponent(REROLL_ID, this.onReroll.bind(this)) creator.registerGlobalComponent(SELECT_ID, this.onSelect.bind(this)) creator.registerGlobalComponent(DELETE_ID, this.onDelete.bind(this)) } } async run(ctx: CommandContext): Promise { try { const tables = calculateUnlockedValues() const responses = await (ctx.guildID ? this.db.getResponsesInServer(ctx.guildID) : this.db.getResponsesInDMWith(ctx.user.id)) const values = generateValuesFor(tables, responses) const locks = populateLocksFor(values) await ctx.send(generateMessageFor(values, locks)) } catch (e) { await ctx.send(generateErrorMessageFor(e, "generate a scenario for you")) } } async onSelect(ctx: ComponentContext): Promise { try { const oldEmbed = getEmbedFrom(ctx.message) const {values, locked: oldLocks} = loadEmbed(oldEmbed) const newLocks = selectUnlockedFrom(ctx.values, oldLocks) await ctx.editParent(generateMessageFor(values, newLocks)) } catch (e) { await ctx.send(generateErrorMessageFor(e, "change the selected components")) } } async onDone(ctx: ComponentContext): Promise { try { const oldEmbed = getEmbedFrom(ctx.message) const { values } = loadEmbed(oldEmbed) await ctx.editParent(generateMessageFor(values, undefined)) } catch (e) { await ctx.send(generateErrorMessageFor(e, "finish this scenario")) } } async onReroll(ctx: ComponentContext): Promise { try { const oldEmbed = getEmbedFrom(ctx.message) const { values: oldValues, locked: locks } = loadEmbed(oldEmbed) const selected = calculateUnlockedValues(oldValues, locks) const responses = await (ctx.guildID ? this.db.getResponsesInServer(ctx.guildID) : this.db.getResponsesInDMWith(ctx.user.id)) const newValues = generateValuesFor(selected, responses, oldValues) await ctx.editParent(generateMessageFor(newValues, locks)) } catch (e) { await ctx.send(generateErrorMessageFor(e, "reroll this scenario")) throw e } } async onDelete(ctx: ComponentContext): Promise { try { await ctx.delete(ctx.messageID) } catch (e) { await ctx.send(generateErrorMessageFor(e, "delete this scenario")) } } }