Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions src/functions/autocomplete/oramaAutoComplete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { InteractionResponseType } from 'discord-api-types/v10';
import type { Response } from 'polka';
import type { OramaSearchResult, OramaSearchResults } from '../../types/orama';
import { AUTOCOMPLETE_MAX_ITEMS, AUTOCOMPLETE_MAX_NAME_LENGTH, DJS_GUIDE_BASE } from '../../util/constants.js';
import { prepareHeader } from '../../util/respond.js';
import { truncate } from '../../util/truncate.js';

function resolveAutocompleteName(element: OramaSearchResult) {
if (element.type === 'page') {
return element.content;
}

if (element.type === 'heading') {
return `# ${element.content}`;
}

return `[...] ${element.content}`;
}

function autocompleteMap(elements: OramaSearchResults) {
return elements
.filter((element) => element.url.length < AUTOCOMPLETE_MAX_NAME_LENGTH)
.map((element) => {
return {
name: truncate(resolveAutocompleteName(element), AUTOCOMPLETE_MAX_NAME_LENGTH),
value: element.url,
};
});
}

export async function oramaAutocomplete(res: Response, query: string) {
const queryUrl = `${DJS_GUIDE_BASE}/api/search?query=${query}`;
const result = (await fetch(queryUrl, {
headers: {
'Content-Type': 'application/json',
},
}).then(async (res) => res.json())) as OramaSearchResults;

prepareHeader(res);

const choices = autocompleteMap(result);

res.write(
JSON.stringify({
data: {
choices: choices.slice(0, AUTOCOMPLETE_MAX_ITEMS - 1),
},
type: InteractionResponseType.ApplicationCommandAutocompleteResult,
}),
);

return res;
}
53 changes: 53 additions & 0 deletions src/functions/oramaResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { bold, hyperlink } from '@discordjs/builders';
import type { Response } from 'polka';
import { EMOJI_ID_GUIDE } from '../util/constants.js';
import { findRelevantDocsSection } from '../util/discordDocs.js';
import { noCodeLines, resolveResourceFromGuideUrl } from '../util/djsguide.js';
import { prepareResponse } from '../util/respond.js';
import { truncate } from '../util/truncate.js';

type GuideCacheEntry = {
page: string;
timestamp: number;
};

const cache = new Map<string, GuideCacheEntry>();

async function getPage(url: string) {
const cacheEntry = cache.get(url);

if (cacheEntry && cacheEntry.timestamp < Date.now() - 1_000 * 60 * 60) {
return cacheEntry.page;
}

const page = await fetch(url).then(async (res) => res.text());
cache.set(url, { page, timestamp: Date.now() });

return page;
}

export async function oramaResponse(res: Response, resultUrl: string, user?: string, ephemeral?: boolean) {
const parsed = resolveResourceFromGuideUrl(resultUrl);
const contentParts: string[] = [];

const docsContents = await getPage(parsed.githubUrl);
const section = findRelevantDocsSection(parsed.anchor ? `#${parsed.anchor}` : parsed.endpoint ?? '', docsContents);

if (section) {
const title = section.heading?.label ?? parsed.endpoint ?? 'No Title';
contentParts.push(`<:guide:${EMOJI_ID_GUIDE}> ${bold(title)}`);
}

const relevantLines = noCodeLines(section?.lines ?? []);
if (relevantLines.length) {
contentParts.push(truncate(relevantLines.join(' '), 300));
}

contentParts.push(hyperlink('read more', parsed.guideUrl));

prepareResponse(res, contentParts.join('\n'), {
ephemeral,
suggestion: user ? { userId: user, kind: 'guide' } : undefined,
});
return res;
}
24 changes: 3 additions & 21 deletions src/handling/handleApplicationCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { resolveOptionsToDocsAutoComplete } from '../functions/autocomplete/docs
import { djsDocs } from '../functions/docs.js';
import { mdnSearch } from '../functions/mdn.js';
import { nodeAutoCompleteResolve } from '../functions/node.js';
import { oramaResponse } from '../functions/oramaResponse.js';
import type { Tag } from '../functions/tag.js';
import { showTag, reloadTags } from '../functions/tag.js';
import { testTag } from '../functions/testtag.js';
Expand Down Expand Up @@ -107,27 +108,8 @@ export async function handleApplicationCommand(
}

case 'guide': {
// const castArgs = args as ArgumentsOf<typeof GuideCommand>;
prepareResponse(
res,
'The guide command is currently unavailable while we rework it to use the new guide page.',
{
ephemeral: true,
},
);

// await algoliaResponse(
// res,
// process.env.DJS_GUIDE_ALGOLIA_APP!,
// process.env.DJS_GUIDE_ALGOLIA_KEY!,
// 'discordjs',
// castArgs.query,
// EMOJI_ID_GUIDE,
// 'guide',
// castArgs.mention,
// castArgs.hide,
// 'guide',
// );
const castArgs = args as ArgumentsOf<typeof GuideCommand>;
await oramaResponse(res, castArgs.query, castArgs.mention, castArgs.hide);
break;
}

Expand Down
9 changes: 2 additions & 7 deletions src/handling/handleApplicationCommandAutocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { algoliaAutoComplete } from '../functions/autocomplete/algoliaAutoComple
import { djsAutoComplete } from '../functions/autocomplete/docsAutoComplete.js';
import { mdnAutoComplete } from '../functions/autocomplete/mdnAutoComplete.js';
import { nodeAutoComplete } from '../functions/autocomplete/nodeAutoComplete.js';
import { oramaAutocomplete } from '../functions/autocomplete/oramaAutoComplete.js';
import { tagAutoComplete } from '../functions/autocomplete/tagAutoComplete.js';
import type { Tag } from '../functions/tag.js';
import type { DTypesCommand } from '../interactions/discordtypes.js';
Expand Down Expand Up @@ -43,13 +44,7 @@ export async function handleApplicationCommandAutocomplete(

case 'guide': {
const args = transformInteraction<typeof GuideCommand>(data.options);
await algoliaAutoComplete(
res,
args.query,
process.env.DJS_GUIDE_ALGOLIA_APP!,
process.env.DJS_GUIDE_ALGOLIA_KEY!,
'discordjs',
);
await oramaAutocomplete(res, args.query);
break;
}

Expand Down
8 changes: 8 additions & 0 deletions src/types/orama.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export type OramaSearchResult = {
content: string;
id: string;
type: 'heading' | 'page' | 'text';
url: string;
};

export type OramaSearchResults = OramaSearchResult[];
1 change: 1 addition & 0 deletions src/util/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const MAX_MESSAGE_LENGTH = 4_000;
export const REMOTE_TAG_URL = 'https://raw.githubusercontent.com/discordjs/discord-utils-bot/main/tags' as const;
export const WEBSITE_URL_ROOT = 'https://discordjs.dev';
export const DJS_DOCS_BASE = 'https://discord.js.org/docs';
export const DJS_GUIDE_BASE = 'https://discordjs.guide' as const;
export const DEFAULT_DOCS_BRANCH = 'stable' as const;
export const VALIDATION_FAIL_COLOR = 0xed4245 as const;
export const VALIDATION_SUCCESS_COLOR = 0x3ba55d as const;
Expand Down
22 changes: 16 additions & 6 deletions src/util/discordDocs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ export function resolveResourceFromDocsURL(link: string) {
}

type Heading = {
docs_anchor: string;
docs_anchor?: string;
label: string;
route: string;
verb: string;
route?: string;
verb?: string;
};

function parseHeadline(text: string): Heading | null {
Expand Down Expand Up @@ -66,7 +66,7 @@ function cleanLine(line: string) {
.trim();
}

const IGNORE_LINE_PREFIXES = ['>', '---', '|'];
const IGNORE_LINE_PREFIXES = ['>', '---', '|', '!'];

export function parseSections(content: string): ParsedSection[] {
const res = [];
Expand All @@ -89,6 +89,12 @@ export function parseSections(content: string): ParsedSection[] {
}
}

if (withinPreamble && line.startsWith('title:')) {
const titleName = line.replace('title: ', '');
section.headline = titleName;
section.heading = { label: titleName };
}

index++;

const startsWithIgnorePrefix = IGNORE_LINE_PREFIXES.some((prefix) => line.startsWith(prefix));
Expand Down Expand Up @@ -119,6 +125,10 @@ export function parseSections(content: string): ParsedSection[] {
}
}

if (section.heading) {
res.push({ ...section });
}

return res;
}

Expand All @@ -140,8 +150,8 @@ function anchorsCompressedEqual(one?: string, other?: string) {
export function findRelevantDocsSection(query: string, docsMd: string) {
const sections = parseSections(docsMd);
for (const section of sections) {
const anchor = section.heading?.docs_anchor;
if (anchor?.startsWith(query) || anchorsCompressedEqual(anchor, query)) {
const anchor = section.heading?.docs_anchor ?? section.headline.toLowerCase();
if (anchor.startsWith(query) || anchorsCompressedEqual(anchor, query)) {
return section;
}
}
Expand Down
39 changes: 39 additions & 0 deletions src/util/djsguide.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { DJS_GUIDE_BASE } from '../util/constants.js';

export function resolveResourceFromGuideUrl(url: string) {
const anchorSplit = url.split('#');
const withoutAnchor = anchorSplit[0];
const pathParts = withoutAnchor.split('/').slice(1);
const path = pathParts.join('/');
const githubUrl = `https://raw.githubusercontent.com/discordjs/discord.js/main/apps/guide/content/docs/${withoutAnchor}.mdx`;
const guideUrl = `${DJS_GUIDE_BASE}${url}`;
const anchor = anchorSplit.length > 1 ? anchorSplit.slice(1).join('#') : undefined;

return {
githubUrl,
path,
guideUrl,
anchor,
endpoint: pathParts.at(-1),
};
}

export function noCodeLines(lines: string[]) {
const res: string[] = [];

let withinCodeBlock = false;
for (const line of lines) {
if (line.startsWith('```')) {
withinCodeBlock = !withinCodeBlock;
continue;
}

if (withinCodeBlock || line.startsWith('<')) {
continue;
}

res.push(line);
}

return res;
}