diff --git a/src/functions/algoliaResponse.ts b/src/functions/algoliaResponse.ts index 77e4c2c..263a158 100644 --- a/src/functions/algoliaResponse.ts +++ b/src/functions/algoliaResponse.ts @@ -20,6 +20,7 @@ export async function algoliaResponse( algoliaObjectId: string, emojiId: string, emojiName: string, + user?: string, ephemeral?: boolean, ): Promise { const full = `http://${algoliaAppId}.${API_BASE_ALGOLIA}/1/indexes/${algoliaIndex}/${encodeURIComponent( @@ -48,7 +49,10 @@ export async function algoliaResponse( `${hyperlink('read more', hideLinkEmbed(hit.url))}`, ].filter(Boolean) as string[]; - prepareResponse(res, contentParts.join('\n'), ephemeral ?? false); + prepareResponse(res, contentParts.join('\n'), { + ephemeral, + suggestion: user ? { userId: user, kind: 'documentation' } : undefined, + }); } catch { prepareErrorResponse(res, 'Invalid result. Make sure to select an entry from the autocomplete.'); } diff --git a/src/functions/autocomplete/docsAutoComplete.ts b/src/functions/autocomplete/docsAutoComplete.ts index b4b96be..9fe1897 100644 --- a/src/functions/autocomplete/docsAutoComplete.ts +++ b/src/functions/autocomplete/docsAutoComplete.ts @@ -85,6 +85,7 @@ export async function djsAutoComplete( type DocsAutoCompleteData = { ephemeral?: boolean; + mention?: string; query: string; source: string; version: string; @@ -107,6 +108,7 @@ export function resolveOptionsToDocsAutoComplete( let query = 'Client'; let version = versions?.at(1) ?? 'main'; let ephemeral; + let mention; logger.debug( { @@ -115,6 +117,7 @@ export function resolveOptionsToDocsAutoComplete( versions, version, ephemeral, + mention, source, }, }, @@ -132,6 +135,8 @@ export function resolveOptionsToDocsAutoComplete( } } else if (opt.type === ApplicationCommandOptionType.Boolean && opt.name === 'hide') { ephemeral = opt.value; + } else if (opt.type === ApplicationCommandOptionType.User && opt.name === 'mention') { + mention = opt.value; } } @@ -140,5 +145,6 @@ export function resolveOptionsToDocsAutoComplete( source, ephemeral, version, + mention, }; } diff --git a/src/functions/docs.ts b/src/functions/docs.ts index 23d0be0..19dd659 100644 --- a/src/functions/docs.ts +++ b/src/functions/docs.ts @@ -294,7 +294,14 @@ async function resolveDjsDocsQuery(query: string, source: string, branch: string } } -export async function djsDocs(res: Response, branch: string, _query: string, source: string, ephemeral = false) { +export async function djsDocs( + res: Response, + branch: string, + _query: string, + source: string, + user?: string, + ephemeral?: boolean, +) { try { const query = await resolveDjsDocsQuery(_query, source, branch); if (!query) { @@ -309,14 +316,10 @@ export async function djsDocs(res: Response, branch: string, _query: string, sou return res.end(); } - prepareResponse( - res, - truncate(formatItem(item, _package, branch, member), MAX_MESSAGE_LENGTH), + prepareResponse(res, truncate(formatItem(item, _package, branch, member), MAX_MESSAGE_LENGTH), { ephemeral, - [], - [], - InteractionResponseType.ChannelMessageWithSource, - ); + suggestion: user ? { userId: user, kind: 'documentation' } : undefined, + }); return res.end(); } catch (_error) { const error = _error as Error; diff --git a/src/functions/mdn.ts b/src/functions/mdn.ts index 5587f6b..09751af 100644 --- a/src/functions/mdn.ts +++ b/src/functions/mdn.ts @@ -11,7 +11,7 @@ function escape(text: string) { return text.replaceAll('||', '|\u200B|').replaceAll('*', '\\*'); } -export async function mdnSearch(res: Response, query: string, ephemeral?: boolean): Promise { +export async function mdnSearch(res: Response, query: string, user?: string, ephemeral?: boolean): Promise { const trimmedQuery = query.trim(); try { const qString = `${API_BASE_MDN}/${trimmedQuery}/index.json`; @@ -41,7 +41,10 @@ export async function mdnSearch(res: Response, query: string, ephemeral?: boolea intro, ]; - prepareResponse(res, parts.join('\n'), ephemeral ?? false); + prepareResponse(res, parts.join('\n'), { + ephemeral, + suggestion: user ? { userId: user, kind: 'documentation' } : undefined, + }); return res; } catch (error) { diff --git a/src/functions/node.ts b/src/functions/node.ts index e3ae249..dbd312a 100644 --- a/src/functions/node.ts +++ b/src/functions/node.ts @@ -73,11 +73,11 @@ function docsUrl(version: string, source: string, anchorTextRaw: string) { const jsonCache: Map = new Map(); const docsCache: Map = new Map(); -export async function nodeAutoCompleteResolve(res: Response, query: string, ephemeral?: boolean) { +export async function nodeAutoCompleteResolve(res: Response, query: string, user?: string, ephemeral?: boolean) { const url = urlOption(`${API_BASE_NODE}/${query}`); if (!url || !query.startsWith('docs')) { - return nodeSearch(res, query, undefined, ephemeral); + return nodeSearch(res, query, undefined, user, ephemeral); } const key = `${url.origin}${url.pathname}`; @@ -112,7 +112,7 @@ export async function nodeAutoCompleteResolve(res: Response, query: string, ephe `<:node:${EMOJI_ID_NODE}> ${hyperlink(inlineCode(headingCode.length ? headingCode : heading), url.toString())}`, `${fullSentence ?? partSentence ?? `${truncate(text, 20, '')}..`}.`, ].join('\n'), - ephemeral ?? false, + { ephemeral, suggestion: user ? { userId: user, kind: 'documentation' } : undefined }, ); return res; @@ -122,6 +122,7 @@ export async function nodeSearch( res: Response, query: string, version = 'latest-v20.x', + user?: string, ephemeral?: boolean, ): Promise { const trimmedQuery = query.trim(); @@ -161,7 +162,10 @@ export async function nodeSearch( .replaceAll(boldCodeBlockRegex, bold(inlineCode('$1'))), ); - prepareResponse(res, parts.join('\n'), ephemeral ?? false); + prepareResponse(res, parts.join('\n'), { + ephemeral, + suggestion: user ? { userId: user, kind: 'documentation' } : undefined, + }); return res; } catch (error) { diff --git a/src/functions/tag.ts b/src/functions/tag.ts index f410cdd..c421939 100644 --- a/src/functions/tag.ts +++ b/src/functions/tag.ts @@ -66,7 +66,7 @@ export async function reloadTags(res: Response, tagCache: Collection, + user?: string, ephemeral?: boolean, ): Response { const trimmedQuery = query.trim().toLowerCase(); const content = findTag(tagCache, trimmedQuery); + if (content) { - prepareResponse(res, content, ephemeral ?? false); + prepareResponse(res, content, { ephemeral, suggestion: user ? { userId: user, kind: 'tag' } : undefined }); } else { prepareErrorResponse(res, `Could not find a tag with name or alias similar to \`${trimmedQuery}\`.`); } diff --git a/src/handling/handleApplicationCommand.ts b/src/handling/handleApplicationCommand.ts index 2b3227c..8c6639b 100644 --- a/src/handling/handleApplicationCommand.ts +++ b/src/handling/handleApplicationCommand.ts @@ -57,8 +57,8 @@ export async function handleApplicationCommand( break; } - const { query, version, ephemeral, source } = resolved; - await djsDocs(res, version, query, source, ephemeral); + const { query, version, ephemeral, source, mention } = resolved; + await djsDocs(res, version, query, source, mention, ephemeral); break; } @@ -72,6 +72,7 @@ export async function handleApplicationCommand( castArgs.query, EMOJI_ID_CLYDE_BLURPLE, 'discord', + castArgs.mention, castArgs.hide, ); break; @@ -88,6 +89,7 @@ export async function handleApplicationCommand( castArgs.query, EMOJI_ID_DTYPES, 'dtypes', + castArgs.mention, castArgs.hide, ); @@ -104,6 +106,7 @@ export async function handleApplicationCommand( castArgs.query, EMOJI_ID_GUIDE, 'guide', + castArgs.mention, castArgs.hide, ); break; @@ -111,19 +114,19 @@ export async function handleApplicationCommand( case 'mdn': { const castArgs = args as ArgumentsOf; - await mdnSearch(res, castArgs.query, castArgs.hide); + await mdnSearch(res, castArgs.query, castArgs.mention, castArgs.hide); break; } case 'node': { const castArgs = args as ArgumentsOf; - await nodeAutoCompleteResolve(res, castArgs.query, castArgs.hide); + await nodeAutoCompleteResolve(res, castArgs.query, castArgs.mention, castArgs.hide); break; } case 'tag': { const castArgs = args as ArgumentsOf; - showTag(res, castArgs.query, tagCache, castArgs.hide); + showTag(res, castArgs.query, tagCache, castArgs.mention, castArgs.hide); break; } @@ -141,7 +144,7 @@ export async function handleApplicationCommand( case 'reloadversions': { await reloadDjsVersions(); - prepareResponse(res, `Reloaded versions for all ${inlineCode('@discordjs')} packages.`, true); + prepareResponse(res, `Reloaded versions for all ${inlineCode('@discordjs')} packages.`, { ephemeral: true }); break; } } diff --git a/src/index.ts b/src/index.ts index 4aa9edb..9a24c1c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -104,11 +104,13 @@ export async function start() { break; default: - prepareResponse(res, `${PREFIX_TEAPOT} This shouldn't be here...`, true); + prepareResponse(res, `${PREFIX_TEAPOT} This shouldn't be here...`, { ephemeral: true }); } } catch (error) { logger.error(error as Error); - prepareResponse(res, `${PREFIX_BUG} Looks like something went wrong here, please try again later!`, true); + prepareResponse(res, `${PREFIX_BUG} Looks like something went wrong here, please try again later!`, { + ephemeral: true, + }); } res.end(); diff --git a/src/interactions/discorddocs.ts b/src/interactions/discorddocs.ts index 2c313bf..4664a19 100644 --- a/src/interactions/discorddocs.ts +++ b/src/interactions/discorddocs.ts @@ -17,5 +17,11 @@ export const DiscordDocsCommand = { description: 'Hide command output', required: false, }, + { + type: ApplicationCommandOptionType.User, + name: 'mention', + description: 'User to mention', + required: false, + }, ], } as const; diff --git a/src/interactions/discordtypes.ts b/src/interactions/discordtypes.ts index be85205..6aa049a 100644 --- a/src/interactions/discordtypes.ts +++ b/src/interactions/discordtypes.ts @@ -45,5 +45,11 @@ export const DTypesCommand = { description: EPHEMERAL_DESCRIPTION, required: false, }, + { + type: ApplicationCommandOptionType.User, + name: 'mention', + description: 'User to mention', + required: false, + }, ], } as const; diff --git a/src/interactions/docs.ts b/src/interactions/docs.ts index adf6023..dea7b4b 100644 --- a/src/interactions/docs.ts +++ b/src/interactions/docs.ts @@ -39,6 +39,13 @@ function buildSubCommandOptions(name: string) { value: 'main', }, ], + required: false, + }, + { + type: ApplicationCommandOptionType.User, + name: 'mention', + description: 'User to mention', + required: false, }, ] as const; } diff --git a/src/interactions/guide.ts b/src/interactions/guide.ts index 3609c42..e96303b 100644 --- a/src/interactions/guide.ts +++ b/src/interactions/guide.ts @@ -17,5 +17,11 @@ export const GuideCommand = { description: 'Hide command output', required: false, }, + { + type: ApplicationCommandOptionType.User, + name: 'mention', + description: 'User to mention', + required: false, + }, ], } as const; diff --git a/src/interactions/mdn.ts b/src/interactions/mdn.ts index ec227f6..6cc19db 100644 --- a/src/interactions/mdn.ts +++ b/src/interactions/mdn.ts @@ -17,5 +17,11 @@ export const MdnCommand = { description: 'Hide command output', required: false, }, + { + type: ApplicationCommandOptionType.User, + name: 'mention', + description: 'User to mention', + required: false, + }, ], } as const; diff --git a/src/interactions/node.ts b/src/interactions/node.ts index 6180ab9..763e6c6 100644 --- a/src/interactions/node.ts +++ b/src/interactions/node.ts @@ -17,5 +17,11 @@ export const NodeCommand = { description: 'Hide command output', required: false, }, + { + type: ApplicationCommandOptionType.User, + name: 'mention', + description: 'User to mention', + required: false, + }, ], } as const; diff --git a/src/interactions/tag.ts b/src/interactions/tag.ts index ff1005c..bb00d77 100644 --- a/src/interactions/tag.ts +++ b/src/interactions/tag.ts @@ -17,5 +17,11 @@ export const TagCommand = { description: 'Hide command output', required: false, }, + { + type: ApplicationCommandOptionType.User, + name: 'mention', + description: 'User to mention', + required: false, + }, ], } as const; diff --git a/src/util/respond.ts b/src/util/respond.ts index 94aaf3b..67fc967 100644 --- a/src/util/respond.ts +++ b/src/util/respond.ts @@ -1,6 +1,8 @@ +import { italic } from '@discordjs/builders'; import { InteractionResponseType, MessageFlags } from 'discord-api-types/v10'; import type { Response } from 'polka'; import { PREFIX_FAIL } from './constants.js'; +import { truncate } from './truncate.js'; export function prepareHeader(response: Response) { response.setHeader('Content-Type', 'application/json'); @@ -9,21 +11,27 @@ export function prepareHeader(response: Response) { export function prepareResponse( response: Response, content: string, - ephemeral = false, - users: string[] = [], - parse: string[] = [], - type = InteractionResponseType.ChannelMessageWithSource, + options?: { + ephemeral?: boolean; + suggestion?: { + kind: string; + userId: string; + }; + }, ): void { prepareHeader(response); + const prefixedContent = options?.suggestion + ? `${italic(`${options.suggestion.kind} suggestion for <@${options.suggestion.userId}>:`)}\n${content}` + : content; response.write( JSON.stringify({ data: { - content, - flags: ephemeral ? MessageFlags.Ephemeral | MessageFlags.SuppressEmbeds : MessageFlags.SuppressEmbeds, - allowed_mentions: { parse, users }, + content: truncate(prefixedContent, 4_000, ''), + flags: options?.ephemeral ? MessageFlags.Ephemeral | MessageFlags.SuppressEmbeds : MessageFlags.SuppressEmbeds, + allowed_mentions: options?.suggestion ? { users: [options.suggestion.userId] } : { parse: [] }, components: [], }, - type, + type: InteractionResponseType.ChannelMessageWithSource, }), ); } @@ -41,7 +49,7 @@ export function prepareDeferResponse(response: Response, ephemeral = false) { } export function prepareErrorResponse(response: Response, content: string): void { - prepareResponse(response, `${PREFIX_FAIL} ${content}`, true); + prepareResponse(response, `${PREFIX_FAIL} ${content}`, { ephemeral: true }); } export function prepareAck(response: Response) {