Skip to content

Commit

Permalink
✨ Discord bot improvements (#1738)
Browse files Browse the repository at this point in the history
  • Loading branch information
SPGoding authored Jan 30, 2025
1 parent a472715 commit 0edada7
Showing 1 changed file with 63 additions and 11 deletions.
74 changes: 63 additions & 11 deletions packages/discord-bot/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
#!/usr/bin/env node

import type { ColorToken, ColorTokenType, LanguageError } from '@spyglassmc/core'
import {
CompletionItem,
ErrorSeverity,
FileNode,
fileUtil,
Expand All @@ -12,6 +11,7 @@ import {
import { NodeJsExternals } from '@spyglassmc/core/lib/nodejs.js'
import * as je from '@spyglassmc/java-edition'
import * as mcdoc from '@spyglassmc/mcdoc'
import { webcrypto } from 'crypto'
import {
ActionRowBuilder,
ButtonBuilder,
Expand All @@ -26,12 +26,18 @@ import {
SlashCommandBuilder,
SlashCommandStringOption,
} from 'discord.js'
import type { APIActionRowComponent, APIMessageActionRowComponent } from 'discord.js'
import type {
APIActionRowComponent,
APIEmbed,
APIMessageActionRowComponent,
ApplicationCommandOptionChoiceData,
} from 'discord.js'
import { join } from 'path'
import { env } from 'process'
import { pathToFileURL } from 'url'

export declare const __dirname: undefined // Not defined in ES module scope
const MaxCommandLength = 1000
const MaxContentLength = 2000

const ProfilerId = 'discord-bot#startup'
Expand Down Expand Up @@ -62,7 +68,6 @@ const service = new Service({
projectRoots: [fileUtil.ensureEndingSlash(pathToFileURL(projectPath).toString())],
},
})
const DocumentUri = pathToFileURL(`${rootPath}/virtual/file.mcfunction`).toString()

interface InteractionInfo {
content: string
Expand All @@ -89,12 +94,16 @@ client.on('interactionCreate', async (i) => {
const reply = await i.reply(getReplyOptions(info))
const collector = reply.createMessageComponentCollector({
componentType: ComponentType.Button,
time: 3_600_000, // 1 hour
time: 600_000, // 10 minutes
})
collector
.on('collect', async (bi) => {
if (bi.user.id !== i.user.id) {
// Only allow creator of the interaction to interact.
await bi.reply({
content:
'Only the original initiator of this interaction may switch to other diagnostics.',
ephemeral: true,
})
return
}

Expand All @@ -110,6 +119,10 @@ client.on('interactionCreate', async (i) => {
})
break
}
} else if (i.isAutocomplete()) {
const content = i.options.getFocused()
const options = await getAutocomplete(content)
await i.respond(options)
}
} catch (e) {
console.error('[interactionCreate]', e)
Expand Down Expand Up @@ -155,11 +168,11 @@ async function registerCommands(): Promise<unknown> {
'Ping the Spyglass Bot',
).toJSON()
const spyCommand = new SlashCommandBuilder().setName('spy').setDescription(
'Renders a mcfunction command. Error reporting coming soon™',
'Renders a mcfunction command.',
).addStringOption(
new SlashCommandStringOption().setName('command').setDescription(
'Put a single mcfunction command here',
).setRequired(true),
).setAutocomplete(true).setMaxLength(MaxCommandLength).setRequired(true),
).addBooleanOption(
new SlashCommandBooleanOption().setName('showraw').setDescription(
'Whether to show the result ANSI code in raw code blocks',
Expand All @@ -171,10 +184,43 @@ async function registerCommands(): Promise<unknown> {
})
}

function generateRandomUri(): string {
const uuid = webcrypto.randomUUID()
return pathToFileURL(`${rootPath}/virtual/file-${uuid}.mcfunction`).toString()
}

async function getAutocomplete(
content: string,
): Promise<readonly ApplicationCommandOptionChoiceData<string>[]> {
const uri = generateRandomUri()
await service.project.onDidOpen(uri, 'mcfunction', 0, content)
const docAndNode = await service.project.ensureClientManagedChecked(uri)
service.project.onDidClose(uri)
if (!docAndNode) {
throw new Error('docAndNode is undefined')
}

const { node, doc } = docAndNode

return service.complete(node, doc, content.length)
.sort((a, b) => (a.sortText ?? a.label).localeCompare(b.sortText ?? b.label))
// Convert autocomplete options into full text options
.map((i) => {
const insertText = i.insertText ?? i.label
return `${content.slice(0, i.range.start)}${insertText}${content.slice(i.range.end)}`
})
// Filter to the texts starting with the user input
.filter((v) => v.startsWith(content))
.map((v) => ({ name: v, value: v }))
// Limit to a maximum of 25 options
.slice(0, 25)
}

async function getInteractionInfo(content: string, showRaw: boolean): Promise<InteractionInfo> {
await service.project.onDidOpen(DocumentUri, 'mcfunction', 0, content)
const docAndNode = await service.project.ensureClientManagedChecked(DocumentUri)
service.project.onDidClose(DocumentUri)
const uri = generateRandomUri()
await service.project.onDidOpen(uri, 'mcfunction', 0, content)
const docAndNode = await service.project.ensureClientManagedChecked(uri)
service.project.onDidClose(uri)
if (!docAndNode) {
throw new Error('docAndNode is undefined')
}
Expand All @@ -193,6 +239,7 @@ function getReplyOptions(
): {
content: string
components: APIActionRowComponent<APIMessageActionRowComponent>[]
embeds: APIEmbed[]
fetchReply: true
} {
const content = getReplyContent(info)
Expand All @@ -212,6 +259,11 @@ function getReplyOptions(
).toJSON(),
]
: [],
embeds: expired
? [
new EmbedBuilder({ description: 'The interaction has expired.' }).data,
]
: [],
fetchReply: true,
}
}
Expand Down

0 comments on commit 0edada7

Please sign in to comment.