Skip to content

Commit

Permalink
feat(amaroquest): slash commands
Browse files Browse the repository at this point in the history
  • Loading branch information
tyrone-sudeium committed Jan 16, 2024
1 parent 6c6572a commit 1113f1c
Show file tree
Hide file tree
Showing 2 changed files with 172 additions and 25 deletions.
153 changes: 129 additions & 24 deletions src/features/amaroquest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ interface LeaderboardData {
avatarURL: string
position: string
prevExp: number | null
characterId: number
}

interface History {
Expand Down Expand Up @@ -231,25 +232,130 @@ export class AmaroQuestFeature extends GlobalFeature {
return false
}

private async getHistory(context: MessageContext<this>): Promise<History> {
if (context.message.channel.type === Discord.ChannelType.DM || !context.message.guild) {
return {}
public async handleInteraction(interaction: Discord.Interaction<Discord.CacheType>): Promise<void> {
if (!interaction.isChatInputCommand()) {
return
}
if (interaction.options.getSubcommandGroup() !== "amaroquest") {
return
}
if (interaction.channel?.isDMBased()) {
interaction.reply("⚠️ amaroquest is unavailable in DMs.")
return
}
if (!interaction.guildId) {
interaction.reply("⚠️ amaroquest only available in a server text channel.")
return
}
const guildId = interaction.guildId
const amaroQuestersStr = await this.bot.brain.get(`aq:${guildId}`) ?? "[]"
const amaroQuesters: number[] = JSON.parse(amaroQuestersStr)
if (interaction.options.getSubcommand() === "show") {
const history = await this.getHistory(guildId)
interaction.deferReply()
let leaderboard: LeaderboardData[] = []
try {
leaderboard = await this.generateLeaderboard(amaroQuesters, history)
} catch (error) {
log(`amaroquest error: ${error}`, "always")
interaction.reply({
content: "⚠️ internal error: probably the lodestone is down, or the bot needs an update.",
ephemeral: true,
})
return
}
for (const entry of leaderboard) {
history[entry.characterId] = history.cumulativeExp
}
await this.setHistory(history, guildId)
const embeds = leaderboard.map(embedForLeaderboardData)
interaction.editReply({embeds})
return
}

const characterId = interaction.options.getInteger("id", true)

if (interaction.options.getSubcommand() === "add") {
if (amaroQuesters.length >= 5) {
interaction.reply({
content: "⚠️ I can only track a maximum of 5 amaroquesters per discord server",
ephemeral: true,
})
return
}
const idx = amaroQuesters.indexOf(characterId)
if (idx < 0) {
// Sanity check the character ID on the lodestone
try {
await getCharacterExpData(characterId)
} catch (error) {
interaction.reply({
content: `⚠️ Lodestone failed to verify character with id \`${characterId}\`: ` +
"double check that this is a valid character.",
ephemeral: true,
})
return
}
amaroQuesters.push(characterId)
await this.bot.brain.set(`aq:${guildId}`, JSON.stringify(amaroQuesters))
}
interaction.reply({
content: "ok",
ephemeral: true,
})
return
}

if (interaction.options.getSubcommand() === "remove") {
const idx = amaroQuesters.indexOf(characterId)
if (idx < 0) {
interaction.reply({
content: `⚠️ character with id \`${characterId}\`` +
"is not on the amaroquest leaderboard in this channel",
ephemeral: true,
})
return
}
amaroQuesters.splice(idx, 1)
await this.bot.brain.set(`aq:${guildId}`, JSON.stringify(amaroQuesters))
interaction.reply({
content: "ok",
ephemeral: true,
})
}
const redisKey = `aq:${context.message.guild.id}:history`
}

private async getHistory(guildId: string): Promise<History> {
const redisKey = `aq:${guildId}:history`
const historyStr = await this.bot.brain.get(redisKey) ?? "{}"
const history: History = JSON.parse(historyStr)
return history
}

private async setHistory(history: History, context: MessageContext<this>): Promise<void> {
if (context.message.channel.type === Discord.ChannelType.DM || !context.message.guild) {
return
}
const redisKey = `aq:${context.message.guild.id}:history`
private async setHistory(history: History, guildId: string): Promise<void> {
const redisKey = `aq:${guildId}:history`
const historyStr = JSON.stringify(history)
await this.bot.brain.set(redisKey, historyStr)
}

private async generateLeaderboard(amaroQuesters: number[], history: History): Promise<LeaderboardData[]> {
let leaderboard: LeaderboardData[] = []
const dataPromises: Promise<XIVCharacter>[] = amaroQuesters
.map(id => getCharacterExpData(id))
const data = await Promise.all(dataPromises)
for (const charData of data) {
const name = charData.character.name
const cumulativeExp = totalExpForToon(charData)
const url = `https://na.finalfantasyxiv.com/lodestone/character/${charData.character.id}/`
const avatarURL = charData.character.avatar
const prevExp = history[charData.character.id] ?? null
const characterId = charData.character.id
leaderboard.push({name, cumulativeExp, url, avatarURL, position: "", prevExp, characterId})
}
leaderboard = sortLeaderboard(leaderboard)
return leaderboard
}

private async handleMessageAsync(context: MessageContext<this>): Promise<void> {
const tokens = this.commandTokens(context)
if (tokens.length < 1) {
Expand All @@ -269,34 +375,25 @@ export class AmaroQuestFeature extends GlobalFeature {

const amaroQuestersStr = await this.bot.brain.get(`aq:${context.message.guild.id}`) ?? "[]"
const amaroQuesters: number[] = JSON.parse(amaroQuestersStr)
const history = await this.getHistory(context)
const history = await this.getHistory(context.message.guild.id)

if (tokens.length < 2) {
if (amaroQuesters.length === 0) {
context.sendNegativeReply("nobody on the leaderboard. use `amaroquest add [character-id]`")
return
}
let leaderboard: LeaderboardData[] = []
const dataPromises: Promise<XIVCharacter>[] = amaroQuesters
.map(id => getCharacterExpData(id))
try {
const data = await Promise.all(dataPromises)
for (const charData of data) {
const name = charData.character.name
const cumulativeExp = totalExpForToon(charData)
const url = `https://na.finalfantasyxiv.com/lodestone/character/${charData.character.id}/`
const avatarURL = charData.character.avatar
const prevExp = history[charData.character.id] ?? null
history[charData.character.id] = cumulativeExp
leaderboard.push({name, cumulativeExp, url, avatarURL, position: "", prevExp})
}
leaderboard = await this.generateLeaderboard(amaroQuesters, history)
} catch (error) {
log(`amaroquest error: ${error}`, "always")
context.sendReply("oops something's cooked. check the logs")
return
}
await this.setHistory(history, context)
leaderboard = sortLeaderboard(leaderboard)
for (const entry of leaderboard) {
history[entry.characterId] = history.cumulativeExp
}
await this.setHistory(history, context.message.guild.id)
const embeds = leaderboard.map(embedForLeaderboardData)
await context.sendReply("", embeds)
return
Expand All @@ -320,6 +417,14 @@ export class AmaroQuestFeature extends GlobalFeature {
}
const idx = amaroQuesters.indexOf(charId)
if (idx < 0) {
// Sanity check the character ID on the lodestone
try {
getCharacterExpData(charId)
} catch (error) {
context.sendNegativeReply(`⚠️ Lodestone failed to verify character with id \`${charId}\`: ` +
"double check that this is a valid character.")
return
}
amaroQuesters.push(charId)
await this.bot.brain.set(`aq:${context.message.guild.id}`, JSON.stringify(amaroQuesters))
}
Expand Down
44 changes: 43 additions & 1 deletion src/features/ffxiv_slash_commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import * as Discord from "discord.js"
import { DATA_CENTERS } from "../model/ffxiv-datacenters"
import { stupidTitleCase } from "../util/string_stuff"
import { AmaroQuestFeature } from "./amaroquest"
import { GlobalFeature, SlashCommand } from "./feature"
import { FFXIVCertificateFeature } from "./ffxiv_certificate_helper"

Expand All @@ -36,11 +37,52 @@ export class FFXIVSlashCommandsFeature extends GlobalFeature {
.setDescription("Should I post the results publicly?")
.setRequired(false)
),
)
.addSubcommandGroup(group =>
group.setName("amaroquest")
.setDescription("Levelling leaderboard")
.addSubcommand(subcommand =>
subcommand.setName("show")
.setDescription("Shows the levelling leaderboard")
)
.addSubcommand(subcommand =>
subcommand.setName("add")
.setDescription("Add a character to the leaderboard")
.addIntegerOption(option =>
option.setName("id")
.setDescription("Lodestone ID of character")
.setMaxValue(4294967295)
.setMinValue(0)
.setRequired(true)
)
)
.addSubcommand(subcommand =>
subcommand.setName("remove")
.setDescription("Remove a character from the levelling leaderboard")
.addIntegerOption(option =>
option.setName("id")
.setDescription("Lodestone ID of character")
.setMaxValue(4294967295)
.setMinValue(0)
.setRequired(true)
)
)
),
]

public async handleInteraction(interaction: Discord.Interaction<Discord.CacheType>): Promise<void> {
if (interaction.isChatInputCommand() && interaction.options.getSubcommand() === "certificates") {
if (interaction.isChatInputCommand() && interaction.options.getSubcommandGroup() === "amaroquest") {
const feature = this.bot.loadedFeatureForName<AmaroQuestFeature>("AmaroQuestFeature")
if (!feature) {
await interaction.reply({
content: "⚠️ amaroquest feature not loaded in this bot.",
ephemeral: true,
})
return
}
feature.handleInteraction(interaction)
return
} else if (interaction.isChatInputCommand() && interaction.options.getSubcommand() === "certificates") {
const feature = this.bot.loadedFeatureForName<FFXIVCertificateFeature>("FFXIVCertificateFeature")
if (!feature) {
await interaction.reply({
Expand Down

0 comments on commit 1113f1c

Please sign in to comment.