Skip to content

Commit

Permalink
Add new context menu commands for editing webhook messages
Browse files Browse the repository at this point in the history
  • Loading branch information
marvin-roesch committed Jul 20, 2024
1 parent fb12fc7 commit f25061d
Show file tree
Hide file tree
Showing 7 changed files with 1,040 additions and 1,298 deletions.
2,113 changes: 846 additions & 1,267 deletions package-lock.json

Large diffs are not rendered by default.

14 changes: 7 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@
},
"homepage": "https://github.com/17thshard/harmony#readme",
"dependencies": {
"@discordjs/builders": "^1.2.0",
"discord-api-types": "^0.37.4",
"discord.js": "^14.3.0",
"@discordjs/builders": "^1.8.2",
"discord-api-types": "^0.37.92",
"discord.js": "^14.15.3",
"keyv": "^4.0.3",
"keyv-file": "^0.2.0",
"node-interval-tree": "^1.3.3",
Expand All @@ -45,12 +45,12 @@
"devDependencies": {
"@types/core-js": "^2.5.5",
"@types/keyv": "^3.1.3",
"@typescript-eslint/eslint-plugin": "^5.43.0",
"@typescript-eslint/parser": "^5.34.0",
"@typescript-eslint/eslint-plugin": "^7.16.1",
"@typescript-eslint/parser": "^7.16.1",
"concurrently": "^6.2.2",
"eslint": "^8.18.0",
"eslint": "^8.56.0",
"nodemon": "^2.0.13",
"ts-node": "^10.2.1",
"typescript": "^4.7.4"
"typescript": "^5.5.3"
}
}
43 changes: 28 additions & 15 deletions src/bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import autoThreadInvite from './cmds/auto-thread-invite';
import rawMessage from './cmds/raw-message';
import messageFilter from './cmds/message-filter';
import channelStarboard from './cmds/channel-starboard';
import editWebhook from './cmds/edit-webhook';
import { Command } from './commands';
import logger from './utils/logger';

Expand All @@ -23,16 +24,23 @@ const client = new Client({
'MessageContent',
'GuildMessageReactions',
],

partials: [
Partials.Reaction,
Partials.User,
Partials.Message,
],
});

export interface Module {
interface SingleCommandModule {
command?: Command;
}

interface MultiCommandModule {
commands: Command[];
}

export type Module = (SingleCommandModule | MultiCommandModule) & {
additionalHandlers?: Partial<{ [K in keyof ClientEvents]: (client: Client, ...args: ClientEvents[K]) => Awaitable<void> }>;
}

Expand All @@ -43,19 +51,27 @@ const modules: Module[] = [
rawMessage,
messageFilter,
channelStarboard,
editWebhook,
];
const commands = modules.reduce<{ [name: string]: Command }>(
(acc, module) => {
const commands = modules
.flatMap((module) => {
if ('commands' in module) {
return module.commands;
}
if (module.command === undefined) {
return acc;
return [];
}

acc[module.command.name] = module.command;
return [module.command];
})
.reduce(
(acc, command) => {
acc[command.name] = command;

return acc;
},
{}
);
return acc;
},
{} as Record<string, Command>
);

client.once('ready', async () => {
logger.info('Ready!');
Expand All @@ -73,11 +89,8 @@ async function tryJoinThread (thread: ThreadChannel) {
}

async function joinActiveThreads (guild: Guild) {
let activeThreads: FetchedThreads;
do {
activeThreads = await guild.channels.fetchActiveThreads();
await Promise.all(activeThreads.threads.map(tryJoinThread));
} while (activeThreads.hasMore);
const activeThreads = await guild.channels.fetchActiveThreads();
await Promise.all(activeThreads.threads.map(tryJoinThread));
}

client.on('threadCreate', tryJoinThread);
Expand Down
13 changes: 7 additions & 6 deletions src/cmds/channel-starboard.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ButtonBuilder } from '@discordjs/builders';
import { ButtonStyle, ComponentType } from 'discord-api-types/v10';
import { Channel, ChatInputCommandInteraction, GuildChannel, Snowflake, Webhook, WebhookMessageOptions } from 'discord.js';
import { APIActionRowComponent, APIMessageActionRowComponent, ButtonStyle, ComponentType } from 'discord-api-types/v10';
import { Channel, ChatInputCommandInteraction, GuildChannel, Snowflake, Webhook, WebhookMessageCreateOptions } from 'discord.js';
import { Module } from '../bot';
import { ComplexCommand } from '../commands';
import logger from '../utils/logger';
Expand All @@ -9,6 +9,7 @@ import { guilds as storage } from '../utils/storage';
const threadStorageKey = 'starboard-thread';
const webhookStorageKey = 'starboard-webhook';

type DeepWriteable<T> = { -readonly [P in keyof T]: DeepWriteable<T[P]> };
type WebhookCapableChannel = Extract<Channel, { fetchWebhooks(): unknown }>;
type ThreadsCapableChannel = Extract<Channel, { threads: unknown }>;
type StarboardCapableChannel = WebhookCapableChannel & ThreadsCapableChannel;
Expand Down Expand Up @@ -96,7 +97,7 @@ export default {
// easy to abuse, but this is meant for internal use atm so can assume good faith
reaction = reaction.partial ? await reaction.fetch() : reaction;
if (reaction.count !== 1) return;

const message = reaction.message.partial ? await reaction.message.fetch(true) : reaction.message;
logger.info({
message: `Pinning a message at the behest of ${pinner.tag}...`,
Expand Down Expand Up @@ -130,7 +131,7 @@ export default {
avatarURL = author.displayAvatarURL();
}

const clonedMessage: WebhookMessageOptions = {
const clonedMessage: DeepWriteable<WebhookMessageCreateOptions> = {
username,
avatarURL,
allowedMentions: { roles: [], users: [], },
Expand Down Expand Up @@ -160,8 +161,8 @@ export default {

// assume it's valid because it has to be checked before a thread is stored
const channel = await client.channels.fetch(channelId) as StarboardCapableChannel;
await getUsableWebhook(channel).then(wh => wh.send(clonedMessage));
await getUsableWebhook(channel).then(wh => wh.send(clonedMessage as WebhookMessageCreateOptions));

await notificationMessage.edit({
content: `<@${pinner.id}> pinned this message to this channel's starboard. See all pinned messages: <#${starThreadId}>`,
allowedMentions: { users: [] },
Expand Down
137 changes: 137 additions & 0 deletions src/cmds/edit-webhook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { ActionRowBuilder, ChannelSelectMenuBuilder, ModalActionRowComponentBuilder } from '@discordjs/builders';
import { ChannelType, ComponentType, Snowflake, TextInputStyle } from 'discord-api-types/v10';
import {
Client,
MessageContextMenuCommandInteraction,
ModalBuilder,
ModalSubmitInteraction,
TextInputBuilder,
WebhookClient
} from 'discord.js';
import { Module } from '../bot';
import { SimpleCommand } from '../commands';
import logger from '../utils/logger';

const supportedWebhooks = (process.env.EDITABLE_WEBHOOKS ?? '').split(',').map(hook => hook.trim()).reduce((acc, url) => {
if (url.length === 0) {
return acc;
}

const webhookClient = new WebhookClient({ url });
acc[webhookClient.id] = webhookClient;
return acc;
}, {} as Record<Snowflake, WebhookClient>);

export default {
commands: [
new SimpleCommand(
'Edit Webhook Message',
async (
client: Client,
interaction: MessageContextMenuCommandInteraction<'cached'>
) => {
const message = interaction.targetMessage;
const webhook = supportedWebhooks[message.webhookId];
if (!webhook) {
await interaction.reply({ content: 'Can only edit messages from supported webhooks!', ephemeral: true });
return;
}

const modalId = `edit-webhook-${interaction.user.id}-${interaction.targetId}`;
const modal = new ModalBuilder()
.setCustomId(modalId)
.setTitle('Edit Webhook Message')
.addComponents(
new ActionRowBuilder<ModalActionRowComponentBuilder>()
.addComponents(
new TextInputBuilder()
.setCustomId('content')
.setLabel('Content')
.setStyle(TextInputStyle.Paragraph)
.setValue(message.content)
.setRequired(true)
)
);

await interaction.showModal(modal);

const filter = (interaction: ModalSubmitInteraction) => interaction.customId === modalId;
const answer = await interaction.awaitModalSubmit({
filter,
time: 300_000
});
const newContent = answer.fields.getTextInputValue('content').trim();

try {
await webhook.editMessage(interaction.targetMessage, { content: newContent });
} catch (error) {
logger.error({
message: 'Failed to edit webhook message',
error,
context: {
user: interaction.user.tag,
channel: `#${message.channel.name}`,
guild: interaction.guildId
}
});
await answer.reply({ content: 'Failed to edit webhook message', ephemeral: true });
return;
}

await answer.reply({ content: 'Webhook message succesfully edited', ephemeral: true });
}
),
new SimpleCommand(
'Add Channel Directions',
async (
client: Client,
interaction: MessageContextMenuCommandInteraction<'cached'>
) => {
const message = interaction.targetMessage;
const webhook = supportedWebhooks[message.webhookId];
if (!webhook) {
await interaction.reply({ content: 'Can only edit messages from supported webhooks!', ephemeral: true });
return;
}

await interaction.deferReply({ ephemeral: true });

const selectId = `channel-directions-${interaction.user.id}-${interaction.targetId}`;
const row = new ActionRowBuilder<ChannelSelectMenuBuilder>()
.addComponents(
new ChannelSelectMenuBuilder()
.setCustomId(selectId)
.setChannelTypes(ChannelType.GuildText, ChannelType.PublicThread, ChannelType.PrivateThread, ChannelType.GuildForum)
);
const reply = await interaction.editReply({ content: 'Which channel should conversation be directed to?', components: [row] });
const targetChannel = (await reply.awaitMessageComponent({ componentType: ComponentType.ChannelSelect })).values[0];
let content = message.content;
const cta = `-# Want to talk about this? Go to <#${targetChannel}>!`;
const regex = /^-# Want to talk about this\? Go to <#[0-9]+>!/m;
if (content.match(regex)) {
content = content.replace(regex, cta);
} else {
content = `${content}\n\n${cta}`;
}

try {
await webhook.editMessage(interaction.targetMessage, { content });
} catch (error) {
logger.error({
message: 'Failed to edit webhook message',
error,
context: {
user: interaction.user.tag,
channel: `#${message.channel.name}`,
guild: interaction.guildId
}
});
await interaction.editReply({ content: 'Failed to edit webhook message', components: [] });
return;
}

await interaction.editReply({ content: 'Webhook message succesfully edited', components: [] });
}
)
]
} as Module;
6 changes: 3 additions & 3 deletions src/cmds/spoiler-attachments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
MessageActionRowComponentBuilder,
MessageComponentInteraction,
MessageEditOptions,
MessageOptions,
MessageCreateOptions,
TextChannel,
} from 'discord.js';
import { SimpleCommand } from '../commands';
Expand Down Expand Up @@ -143,7 +143,7 @@ export default {
await promptMessage.edit({ components: [] });

if (result.type === 'cancel') {
const edit: MessageOptions & MessageEditOptions = {
const edit: MessageCreateOptions & MessageEditOptions = {
embeds: [buildEmbed('The attachment process has been canceled.')],
components: []
};
Expand Down Expand Up @@ -228,7 +228,7 @@ export default {
});
}

const content: MessageOptions & MessageEditOptions = {
const content: MessageCreateOptions & MessageEditOptions = {
embeds: [buildEmbed(message, 'Red')],
components: []
};
Expand Down
12 changes: 12 additions & 0 deletions src/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,18 @@ const cmds: ApplicationCommandDataResolvable[] = [
},
]
},
{
type: ApplicationCommandType.Message,
name: 'Edit Webhook Message',
defaultMemberPermissions: PermissionFlagsBits.ManageMessages,
dmPermission: false,
},
{
type: ApplicationCommandType.Message,
name: 'Add Channel Directions',
defaultMemberPermissions: PermissionFlagsBits.ManageMessages,
dmPermission: false,
},
];

const client = new Client({ intents: [] });
Expand Down

0 comments on commit f25061d

Please sign in to comment.