From b90c52aa394607b83978707a18ace2f5ce799432 Mon Sep 17 00:00:00 2001 From: Tyrone Trevorrow <819705+tyrone-sudeium@users.noreply.github.com> Date: Thu, 4 Jan 2024 15:59:30 +1100 Subject: [PATCH] feat: slash command for certificate helper --- .vscode/settings.json | 2 +- src/features/ffxiv_certificate_helper.ts | 60 +++++++++++++++--------- src/features/ffxiv_slash_commands.ts | 56 ++++++++++++++++++++++ src/features/index.ts | 2 + src/model/ffxiv-datacenters.ts | 31 ++++++++++++ src/util/string_stuff.ts | 17 +++++++ 6 files changed, 144 insertions(+), 24 deletions(-) create mode 100644 src/features/ffxiv_slash_commands.ts create mode 100644 src/model/ffxiv-datacenters.ts create mode 100644 src/util/string_stuff.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 027e059..366d761 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,7 @@ { "typescript.tsdk": "node_modules/typescript/lib", "editor.codeActionsOnSave": { - "source.fixAll": true + "source.fixAll": "explicit" }, "eslint.alwaysShowStatus": true } diff --git a/src/features/ffxiv_certificate_helper.ts b/src/features/ffxiv_certificate_helper.ts index 21a3d5a..e4a1759 100644 --- a/src/features/ffxiv_certificate_helper.ts +++ b/src/features/ffxiv_certificate_helper.ts @@ -16,6 +16,8 @@ import * as Discord from "discord.js" import { ACCESSORIES, MAJORS, MINORS, ITEM_NAMES } from "../model/ffxiv-items" import { getJSON, queryStringFromObject } from "../util/http" import { log } from "../log" +import { DataCenter, isDataCenter } from "../model/ffxiv-datacenters" +import { stupidTitleCase } from "../util/string_stuff" import { GlobalFeature, MessageContext } from "./feature" @@ -42,25 +44,6 @@ interface PriceInfo { itemId: number } -const DATA_CENTERS = [ - "aether", - "crystal", - "dynamis", - "primal", - "chaos", - "light", - "elemental", - "gaia", - "mana", - "meteor", - "materia", -] as const -type DataCenter = typeof DATA_CENTERS[number] - -function isDataCenter(str: string): str is DataCenter { - return (DATA_CENTERS as readonly string[]).includes(str) -} - function certificateValue(itemId: number): number { if (MAJORS.has(itemId)) { return 17 @@ -87,15 +70,46 @@ function formatPriceInfo(info: PriceInfo): string { return `${price} on ${info.worldName} (${perCert} per cert)` } -function stupidTitleCase(str: string): string { - return str[0].toUpperCase() + str.slice(1) -} - function isPriceInfoEqual(p1: PriceInfo, p2: PriceInfo): boolean { return p1.itemName === p2.itemName && p1.price === p2.price && p1.worldName === p2.worldName } export class FFXIVCertificateFeature extends GlobalFeature { + public async handleInteraction(interaction: Discord.Interaction): Promise { + if (!interaction.isChatInputCommand()) { + return + } + const dc = interaction.options.getString("datacentre") + if (!dc) { + // This is required? lul. + await interaction.reply({content: "⚠️ Missing data centre. Try `/xiv certificates [dc]`", ephemeral: true}) + return + } + if (!isDataCenter(dc)) { + await interaction.reply({content: `⚠️ \`${dc}\` is not a recognised data centre`, ephemeral: true}) + return + } + + try { + const prices = await this.getPricesFromUniversalis(dc) + const embeds = prices.map((priceInfo, index) => { + const embed = new Discord.EmbedBuilder() + const thumb = `https://universalis-ffxiv.github.io/universalis-assets/icon2x/${priceInfo.itemId}.png` + embed.setAuthor({ + name: `#${index+1} ${priceInfo.itemName}`, + iconURL: thumb, + url: `https://universalis.app/market/${priceInfo.itemId}`, + }) + embed.setFooter({text: formatPriceInfo(priceInfo)}) + return embed + }) + interaction.reply({embeds, ephemeral: true}) + } catch(err) { + log(`ffxiv_certificate_helper error: ${err}`, "always") + interaction.reply({content: "oops something's cooked. check the logs", ephemeral: true}) + } + } + public handleMessage(context: MessageContext): boolean { const tokens = this.commandTokens(context) diff --git a/src/features/ffxiv_slash_commands.ts b/src/features/ffxiv_slash_commands.ts new file mode 100644 index 0000000..bdb8cd3 --- /dev/null +++ b/src/features/ffxiv_slash_commands.ts @@ -0,0 +1,56 @@ +/** + * Container for all FFXIV slash commands + */ + +/* +* AetheBot - A Discord Chatbot +* +* Created by Tyrone Trevorrow on 04/01/24. +* Copyright (c) 2024 Tyrone Trevorrow. All rights reserved. +* +* This source code is licensed under the permissive MIT license. +*/ + +import * as Discord from "discord.js" +import { DATA_CENTERS } from "../model/ffxiv-datacenters" +import { stupidTitleCase } from "../util/string_stuff" +import { GlobalFeature, SlashCommand } from "./feature" +import { FFXIVCertificateFeature } from "./ffxiv_certificate_helper" + +export class FFXIVSlashCommandsFeature extends GlobalFeature { + public static slashCommands?: SlashCommand[] | undefined = [ + new Discord.SlashCommandBuilder() + .setName("xiv") + .setDescription("FFXIV-related subcommands") + .addSubcommand(subcommand => + subcommand.setName("certificates") + .setDescription("Find the best market board items to trade for certificates (FFXIV)") + .addStringOption(option => + option.setName("datacentre") + .setDescription("Data centre") + .setRequired(true) + .setChoices(...DATA_CENTERS.map(dc => ({ name: stupidTitleCase(dc), value: dc }))) + ), + ), + ] + + public async handleInteraction(interaction: Discord.Interaction): Promise { + if (interaction.isChatInputCommand() && interaction.options.getSubcommand() === "certificates") { + const feature = this.bot.loadedFeatureForName("FFXIVCertificateFeature") + if (!feature) { + await interaction.reply({ + content: "⚠️ FFXIVCertificates feature not loaded in this bot.", + ephemeral: true, + }) + return + } + feature.handleInteraction(interaction) + return + } + } + + public handleMessage(): boolean { + // Doesn't handle any chat messages directly, only slash commands + return false + } +} diff --git a/src/features/index.ts b/src/features/index.ts index 63ed535..fcc7221 100644 --- a/src/features/index.ts +++ b/src/features/index.ts @@ -38,6 +38,7 @@ import { ReactionRolesFeature } from "./reaction_roles" import { AmaroQuestFeature } from "./amaroquest" import { FFXIVCertificateFeature } from "./ffxiv_certificate_helper" import { MemeFeature } from "./memegen/meme" +import { FFXIVSlashCommandsFeature } from "./ffxiv_slash_commands" export { GlobalFeature } @@ -67,6 +68,7 @@ export const allFeatures: GlobalFeatureConstructor[] = [ AmaroQuestFeature, FFXIVCertificateFeature, MemeFeature, + FFXIVSlashCommandsFeature, ] export const allServerFeatures: ServerFeatureConstructor[] = [ diff --git a/src/model/ffxiv-datacenters.ts b/src/model/ffxiv-datacenters.ts new file mode 100644 index 0000000..c013a44 --- /dev/null +++ b/src/model/ffxiv-datacenters.ts @@ -0,0 +1,31 @@ +/** + * FFXIV Data Centres + */ + +/* +* AetheBot - A Discord Chatbot +* +* Created by Tyrone Trevorrow on 04/01/24. +* Copyright (c) 2024 Tyrone Trevorrow. All rights reserved. +* +* This source code is licensed under the permissive MIT license. +*/ + +export const DATA_CENTERS = [ + "aether", + "crystal", + "dynamis", + "primal", + "chaos", + "light", + "elemental", + "gaia", + "mana", + "meteor", + "materia", +] as const +export type DataCenter = typeof DATA_CENTERS[number] + +export function isDataCenter(str: string): str is DataCenter { + return (DATA_CENTERS as readonly string[]).includes(str) +} diff --git a/src/util/string_stuff.ts b/src/util/string_stuff.ts new file mode 100644 index 0000000..2048be1 --- /dev/null +++ b/src/util/string_stuff.ts @@ -0,0 +1,17 @@ +/** + * Just some string stuff + */ + +/* +* AetheBot - A Discord Chatbot +* +* Created by Tyrone Trevorrow on 04/01/24. +* Copyright (c) 2024 Tyrone Trevorrow. All rights reserved. +* +* This source code is licensed under the permissive MIT license. +*/ + +/** Extremely naïve title case: just upcases first char. No bounds checking. */ +export function stupidTitleCase(str: string): string { + return str[0].toUpperCase() + str.slice(1) +}