Skip to content

Commit

Permalink
feat(discorddocs): display route and first section of body, if available
Browse files Browse the repository at this point in the history
  • Loading branch information
almostSouji committed Apr 6, 2024
1 parent 1490bce commit 4955609
Show file tree
Hide file tree
Showing 3 changed files with 158 additions and 12 deletions.
26 changes: 14 additions & 12 deletions src/functions/algoliaResponse.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { hideLinkEmbed, hyperlink, userMention, italic, bold } from '@discordjs/builders';
import { hideLinkEmbed, hyperlink, userMention, italic, bold, inlineCode } from '@discordjs/builders';
import pkg from 'he';
import type { Response } from 'polka';
import { fetch } from 'undici';
import type { AlgoliaHit } from '../types/algolia.js';
import { expandAlgoliaObjectId } from '../util/compactAlgoliaId.js';
import { API_BASE_ALGOLIA } from '../util/constants.js';
import { fetchDocsBody } from '../util/discordDocs.js';
import { prepareResponse, prepareErrorResponse } from '../util/respond.js';
import { truncate } from '../util/truncate.js';
import { resolveHitToNamestring } from './autocomplete/algoliaAutoComplete.js';
Expand Down Expand Up @@ -35,17 +36,18 @@ export async function algoliaResponse(
},
}).then(async (res) => res.json())) as AlgoliaHit;

prepareResponse(
res,
`${target ? `${italic(`Suggestion for ${userMention(target)}:`)}\n` : ''}<:${emojiName}:${emojiId}> ${bold(
resolveHitToNamestring(hit),
)}${hit.content?.length ? `\n${truncate(decode(hit.content), 300)}` : ''}\n${hyperlink(
'read more',
hideLinkEmbed(hit.url),
)}`,
ephemeral ?? false,
target ? [target] : [],
);
const docsBody = hit.url.includes('discord.com') ? await fetchDocsBody(hit.url) : null;
const headlineSuffix = docsBody?.heading ? inlineCode(`${docsBody.heading.verb} ${docsBody.heading.route}`) : null;

const contentParts = [
target ? `${italic(`Suggestion for ${userMention(target)}:`)}` : null,
`<:${emojiName}:${emojiId}> ${bold(resolveHitToNamestring(hit))}${headlineSuffix ? ` ${headlineSuffix}` : ''}`,
hit.content?.length ? `${truncate(decode(hit.content), 300)}` : null,
docsBody?.lines.length ? docsBody.lines.at(0) : null,
`${hyperlink('read more', hideLinkEmbed(hit.url))}`,
].filter(Boolean) as string[];

prepareResponse(res, contentParts.join('\n'), ephemeral ?? false, target ? [target] : []);
} catch {
prepareErrorResponse(res, 'Invalid result. Make sure to select an entry from the autocomplete.');
}
Expand Down
129 changes: 129 additions & 0 deletions src/util/discordDocs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { urlOption } from './url.js';

export function toMdFilename(name: string) {
return name
.split('-')
.map((part) => `${part.at(0)?.toUpperCase()}${part.slice(1).toLowerCase()}`)
.join('');
}

export function resolveResourceFromDocsURL(link: string) {
const url = urlOption(link);
if (!url) {
return null;
}

const pathParts = url.pathname.split('/').slice(2);
if (!pathParts.length) {
return null;
}

return {
docsAnchor: url.hash,
githubUrl: `https://raw.githubusercontent.com/discord/discord-api-docs/main/${pathParts
.slice(0, -1)
.join('/')}/${toMdFilename(pathParts.at(-1)!)}.md`,
};
}

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

function parseHeadline(text: string): Heading | null {
const match = /#{1,7} (?<label>.*) % (?<verb>\w{3,6}) (?<route>.*)/g.exec(text);
if (!match) {
return null;
}

const { groups } = match;
return {
docs_anchor: `#${groups!.label.replaceAll(' ', '-').toLowerCase()}`,
label: groups!.label,
verb: groups!.verb,
route: groups!.route,
};
}

// https://raw.githubusercontent.com/discord/discord-api-docs/main/docs/resources/user/User.md
// https://raw.githubusercontent.com/discord/discord-api-docs/main/docs/resources/User.md

type ParsedSection = {
heading: Heading | null;
headline: string;
lines: string[];
};

function cleanLine(line: string) {
return line
.replaceAll(/\[(.*?)]\(.*?\)/g, '$1')
.replaceAll(/{(.*?)#.*?}/g, '$1')
.trim();
}

export function parseSections(content: string): ParsedSection[] {
const res = [];
const section: ParsedSection = {
heading: null,
lines: [],
headline: '',
};

for (const line of content.split('\n')) {
const cleanedLine = cleanLine(line);

if (line.startsWith('>')) {
continue;
}

if (line.startsWith('#')) {
if (section.headline.length) {
res.push({ ...section });

section.lines = [];
section.heading = null;
section.headline = '';
}

section.headline = cleanedLine;
const parsedHeading = parseHeadline(cleanedLine);
if (parsedHeading) {
section.heading = parsedHeading;
}

continue;
}

if (cleanedLine.length) {
section.lines.push(cleanedLine);
}
}

return res;
}

export function findRelevantDocsSection(query: string, docsMd: string) {
const sections = parseSections(docsMd);
for (const section of sections) {
if (section.heading?.docs_anchor === query) {
return section;
}
}
}

export async function fetchDocsBody(link: string) {
const githubResource = resolveResourceFromDocsURL(link);
if (!githubResource) {
return null;
}

const docsMd = await fetch(githubResource.githubUrl).then(async (res) => res.text());
const section = findRelevantDocsSection(githubResource.docsAnchor, docsMd);

if (section) {
return section;
}
}
15 changes: 15 additions & 0 deletions src/util/url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { URL } from 'node:url';

/**
* Transform a link into an URL or null, if invalid
*
* @param url - The link to transform
* @returns The URL instance, if valid
*/
export function urlOption(url: string) {
try {
return new URL(url);
} catch {
return null;
}
}

0 comments on commit 4955609

Please sign in to comment.