Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove embeds from most messages #227

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# 2023-08-20

- Reworked modules to avoid sending messages in embeds.
- Show up to 5 search results from `!hb`.

# 2022-12-16

- Remove `!close`, update `!helper` to include thread tags.
Expand Down
57 changes: 33 additions & 24 deletions src/modules/handbook.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { EmbedBuilder } from 'discord.js';
import algoliasearch from 'algoliasearch/lite';
import { sendWithMessageOwnership } from '../util/send';
import { TS_BLUE } from '../env';
import { decode } from 'html-entities';
import { Bot } from '../bot';
import { MessageBuilder } from '../util/messageBuilder';

const ALGOLIA_APP_ID = 'BGCDYOIYZ5';
const ALGOLIA_API_KEY = '37ee06fa68db6aef451a490df6df7c60';
Expand All @@ -16,52 +15,62 @@ type AlgoliaResult = {
url: string;
};

const HANDBOOK_EMBED = new EmbedBuilder()
.setColor(TS_BLUE)
const HANDBOOK_HELP = new MessageBuilder()
.setTitle('The TypeScript Handbook')
.setURL('https://www.typescriptlang.org/docs/handbook/intro.html')
.setFooter({ text: 'You can search with `!handbook <query>`' });
.setDescription('You can search with `!handbook <query>`')
.build();

export async function handbookModule(bot: Bot) {
bot.registerCommand({
aliases: ['handbook', 'hb'],
description: 'Search the TypeScript Handbook',
async listener(msg, content) {
if (!content) {
return await sendWithMessageOwnership(msg, {
embeds: [HANDBOOK_EMBED],
});
return await sendWithMessageOwnership(msg, HANDBOOK_HELP);
}

console.log('Searching algolia for', [content]);
const data = await algolia.search<AlgoliaResult>([
{
indexName: ALGOLIA_INDEX_NAME,
query: content,
params: {
offset: 0,
length: 1,
length: 5,
},
},
]);
console.log('Algolia response:', data);
const hit = data.results[0].hits[0];
if (!hit)

if (!data.results[0].hits.length) {
return await sendWithMessageOwnership(
msg,
':x: No results found for that query',
);
const hierarchyParts = [0, 1, 2, 3, 4, 5, 6]
.map(i => hit.hierarchy[`lvl${i}`])
.filter(x => x);
const embed = new EmbedBuilder()
.setColor(TS_BLUE)
.setTitle(decode(hierarchyParts[hierarchyParts.length - 1]))
.setAuthor({
name: decode(hierarchyParts.slice(0, -1).join(' / ')),
})
.setURL(hit.url);
await sendWithMessageOwnership(msg, { embeds: [embed] });
}

const response = new MessageBuilder();

const pages = {} as Record<string, string[]>;

for (const hit of data.results[0].hits) {
const hierarchyParts = [0, 1, 2, 3, 4, 5, 6]
.map(i => hit.hierarchy[`lvl${i}`])
.filter(x => x);

const page = hierarchyParts[0]!;
const path = decode(hierarchyParts.slice(1).join(' / '));
pages[page] ??= [];
pages[page].push(`[${path}](<${hit.url}>)`);
}

for (const [page, entries] of Object.entries(pages)) {
response.addFields({
name: page,
value: `- ${entries.join('\n- ')}`,
});
}

await sendWithMessageOwnership(msg, response.build());
},
});
}
32 changes: 13 additions & 19 deletions src/modules/help.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { EmbedBuilder } from 'discord.js';
import { Bot, CommandRegistration } from '../bot';
import { Snippet } from '../entities/Snippet';
import { sendWithMessageOwnership } from '../util/send';
import { MessageBuilder } from '../util/messageBuilder';

function getCategoryHelp(cat: string, commands: Iterable<CommandRegistration>) {
const out: string[] = [];
Expand Down Expand Up @@ -44,31 +44,25 @@ export function helpModule(bot: Bot) {
if (!msg.guild) return;

if (!cmdTrigger) {
const embed = new EmbedBuilder()
.setAuthor({
name: msg.guild.name,
iconURL: msg.guild.iconURL() || undefined,
})
const response = new MessageBuilder()
.setTitle('Bot Usage')
.setDescription(
`Hello ${msg.author.username}! Here is a list of all commands in me! To get detailed description on any specific command, do \`help <command>\``,
);

for (const cat of getCommandCategories(bot.commands.values())) {
embed.addFields({
name: `**${cat} Commands:**`,
response.addFields({
name: `${cat} Commands:`,
value: getCategoryHelp(cat, bot.commands.values()),
});
}

embed
.setFooter({
text: bot.client.user.username,
iconURL: bot.client.user.displayAvatarURL(),
})
.setTimestamp();
response.addFields({
name: 'Playground Links:',
value: 'I will shorten any [TypeScript Playground](<https://www.typescriptlang.org/play>) links in a message or attachment and display a preview of the code. You can choose specific lines to embed by selecting them before copying the link.',
});

return await sendWithMessageOwnership(msg, { embeds: [embed] });
return await sendWithMessageOwnership(msg, response.build());
}

let cmd: { description?: string; aliases?: string[] } =
Expand All @@ -95,25 +89,25 @@ export function helpModule(bot: Bot) {
`:x: Command not found`,
);

const embed = new EmbedBuilder().setTitle(
const builder = new MessageBuilder().setTitle(
`\`${cmdTrigger}\` Usage`,
);
// Get rid of duplicates, this can happen if someone adds the method name as an alias
const triggers = new Set(cmd.aliases ?? [cmdTrigger]);
if (triggers.size > 1) {
embed.addFields({
builder.addFields({
name: 'Aliases',
value: Array.from(triggers, t => `\`${t}\``).join(', '),
});
}
embed.addFields({
builder.addFields({
name: 'Description',
value: `*${
splitCategoryDescription(cmd.description ?? '')[1]
}*`,
});

await sendWithMessageOwnership(msg, { embeds: [embed] });
await sendWithMessageOwnership(msg, builder.build());
},
});
}
81 changes: 35 additions & 46 deletions src/modules/playground.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { EmbedBuilder, Message, User } from 'discord.js';
import { Message, User } from 'discord.js';
import {
compressToEncodedURIComponent,
decompressFromEncodedURIComponent,
} from 'lz-string';
import { format } from 'prettier';
import { URLSearchParams } from 'url';
import { TS_BLUE } from '../env';
import {
makeCodeBlock,
findCode,
Expand All @@ -16,14 +15,18 @@ import { LimitedSizeMap } from '../util/limitedSizeMap';
import { addMessageOwnership, sendWithMessageOwnership } from '../util/send';
import { fetch } from 'undici';
import { Bot } from '../bot';
import { MessageBuilder } from '../util/messageBuilder';

const PLAYGROUND_BASE = 'https://www.typescriptlang.org/play/#code/';
const LINK_SHORTENER_ENDPOINT = 'https://tsplay.dev/api/short';
const MAX_EMBED_LENGTH = 512;
const DEFAULT_EMBED_LENGTH = 256;
const MAX_PREVIEW_LENGTH = 512;
const DEFAULT_PREVIEW_LENGTH = 256;

export async function playgroundModule(bot: Bot) {
const editedLongLink = new LimitedSizeMap<string, Message>(1000);
const editedLongLink = new LimitedSizeMap<
string,
[Message, MessageBuilder]
>(1000);

bot.registerCommand({
aliases: ['playground', 'pg', 'playg'],
Expand All @@ -41,11 +44,10 @@ export async function playgroundModule(bot: Bot) {
":warning: couldn't find a codeblock!",
);
}
const embed = new EmbedBuilder()
.setURL(PLAYGROUND_BASE + compressToEncodedURIComponent(code))
const builder = new MessageBuilder()
.setTitle('View in Playground')
.setColor(TS_BLUE);
await sendWithMessageOwnership(msg, { embeds: [embed] });
.setURL(PLAYGROUND_BASE + compressToEncodedURIComponent(code));
await sendWithMessageOwnership(msg, builder.build());
},
});

Expand All @@ -54,20 +56,19 @@ export async function playgroundModule(bot: Bot) {
if (msg.content[0] === '!') return;
const exec = matchPlaygroundLink(msg.content);
if (!exec) return;
const embed = createPlaygroundEmbed(msg.author, exec);
const builder = createPlaygroundMessage(msg.author, exec);
if (exec.isWholeMatch) {
// Message only contained the link
await sendWithMessageOwnership(msg, {
embeds: [embed],
});
await sendWithMessageOwnership(msg, builder.build());
await msg.delete();
} else {
// Message also contained other characters
const botMsg = await msg.channel.send({
embeds: [embed],
content: `${msg.author} Here's a shortened URL of your playground link! You can remove the full link from your message.`,
});
editedLongLink.set(msg.id, botMsg);
builder.setFooter(
`${msg.author} Here's a shortened URL of your playground link! You can remove the full link from your message.`,
);
builder.setAllowMentions('users');
const botMsg = await msg.channel.send(builder.build());
editedLongLink.set(msg.id, [botMsg, builder]);
await addMessageOwnership(botMsg, msg.author);
}
});
Expand All @@ -82,41 +83,34 @@ export async function playgroundModule(bot: Bot) {
// put the rest of the message in msg.content
if (!exec?.isWholeMatch) return;
const shortenedUrl = await shortenPlaygroundLink(exec.url);
const embed = createPlaygroundEmbed(msg.author, exec, shortenedUrl);
await sendWithMessageOwnership(msg, {
embeds: [embed],
});
const builder = createPlaygroundMessage(msg.author, exec, shortenedUrl);
await sendWithMessageOwnership(msg, builder.build());
if (!msg.content) await msg.delete();
});

bot.client.on('messageUpdate', async (_oldMsg, msg) => {
if (msg.partial) msg = await msg.fetch();
const exec = matchPlaygroundLink(msg.content);
if (msg.author.bot || !editedLongLink.has(msg.id) || exec) return;
const botMsg = editedLongLink.get(msg.id);
// Edit the message to only have the embed and not the "please edit your message" message
await botMsg?.edit({
content: '',
embeds: [botMsg.embeds[0]],
});
const [botMsg, builder] = editedLongLink.get(msg.id)!;
// Edit the message to only have the preview and not the "please edit your message" message
await botMsg?.edit(builder.setFooter('').setAllowMentions().build());
editedLongLink.delete(msg.id);
});
}

// Take care when messing with the truncation, it's extremely finnicky
function createPlaygroundEmbed(
function createPlaygroundMessage(
author: User,
{ url: _url, query, code, isEscaped }: PlaygroundLinkMatch,
url: string = _url,
) {
const embed = new EmbedBuilder()
.setColor(TS_BLUE)
.setTitle('Playground Link')
.setAuthor({ name: author.tag, iconURL: author.displayAvatarURL() })
.setURL(url);
const builder = new MessageBuilder().setAuthor(
`From ${author}: [View in Playground](<${url}>)`,
);

const unzipped = decompressFromEncodedURIComponent(code);
if (!unzipped) return embed;
if (!unzipped) return builder;

// Without 'normalized' you can't get consistent lengths across platforms
// Matters because the playground uses the line breaks of whoever created it
Expand All @@ -135,19 +129,19 @@ function createPlaygroundEmbed(

const startChar = startLine ? lineIndices[startLine - 1] : 0;
const cutoff = endLine
? Math.min(lineIndices[endLine], startChar + MAX_EMBED_LENGTH)
: startChar + DEFAULT_EMBED_LENGTH;
? Math.min(lineIndices[endLine], startChar + MAX_PREVIEW_LENGTH)
: startChar + DEFAULT_PREVIEW_LENGTH;
// End of the line containing the cutoff
const endChar = lineIndices.find(len => len >= cutoff) ?? normalized.length;

let pretty;
try {
// Make lines as short as reasonably possible, so they fit in the embed.
// Make lines as short as reasonably possible, so they fit in the preview.
// We pass prettier the full string, but only format part of it, so we can
// calculate where the endChar is post-formatting.
pretty = format(normalized, {
parser: 'typescript',
printWidth: 55,
printWidth: 72,
tabWidth: 2,
semi: false,
bracketSpacing: false,
Expand All @@ -167,15 +161,10 @@ function createPlaygroundEmbed(
(prettyEndChar === pretty.length ? '' : '\n...');

if (!isEscaped) {
embed.setDescription('**Preview:**' + makeCodeBlock(content));
if (!startLine && !endLine) {
embed.setFooter({
text: 'You can choose specific lines to embed by selecting them before copying the link.',
});
}
builder.addFields({ name: 'Preview:', value: makeCodeBlock(content) });
}

return embed;
return builder;
}

async function shortenPlaygroundLink(url: string) {
Expand Down
Loading