all repos — stealth-developers @ c5869ac3e5e314f4661c207e5c989f0918da7c18

feat: add highlight ctx menu item
willow hai@wlo.moe
Thu, 05 Jun 2025 22:24:37 +0100
commit

c5869ac3e5e314f4661c207e5c989f0918da7c18

parent

b53151942897546a166182795a5a5894da42d0a0

3 files changed, 191 insertions(+), 3 deletions(-)

jump to
M src/config.tssrc/config.ts

@@ -34,6 +34,7 @@ projects: projectSchema,

bloxlink: bloxlinkSchema.optional(), roblox: robloxSchema.optional(), trelloBoardId: z.string().optional(), + developerId: z.string(), }); function validateConfig() {
M src/interactions/commands/config.tssrc/interactions/commands/config.ts

@@ -1,3 +1,6 @@

+import config from "@/config.ts"; +import { GuildModel, type GuildType } from "@/database/schemas.ts"; +import { Logger } from "@/utils/logging.ts"; import { ChannelType, type ChatInputCommandInteraction,

@@ -5,15 +8,12 @@ type Client,

PermissionFlagsBits, SlashCommandBuilder, } from "discord.js"; -import { GuildModel, type GuildType } from "../../database/schemas.ts"; -import { Logger } from "../../utils/logging.ts"; const logger = new Logger("config-command"); const commandData = new SlashCommandBuilder() .setName("config") .setDescription("configure bot settings for this server") - .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild) .addSubcommand((subcommand) => subcommand .setName("manager-role")

@@ -46,6 +46,17 @@ .setName("channel")

.setDescription("channel for bug reports") .setRequired(true), ), + ) + .addSubcommand((subcommand) => + subcommand + .setName("highlight-channel") + .setDescription("set the channel where highlights are sent") + .addChannelOption((option) => + option + .setName("channel") + .setDescription("channel for highlights") + .setRequired(true), + ), ); async function execute(

@@ -55,6 +66,18 @@ ) {

if (!interaction.guild) { await interaction.reply({ content: "❌ this command can only be used in a server.", + flags: ["Ephemeral"], + }); + return; + } + + if ( + !interaction.memberPermissions || + (!interaction.memberPermissions.has(PermissionFlagsBits.ManageGuild) && + interaction.user.id !== config.data.developerId) + ) { + await interaction.reply({ + content: "❌ you do not have permission to manage this server.", flags: ["Ephemeral"], }); return;

@@ -80,6 +103,13 @@ if (subcommand === "manager-role") {

await handleManagerRole(interaction, guild); } else if (subcommand === "bug-channel") { await handleBugChannel(interaction, guild); + } else if (subcommand === "highlight-channel") { + await handleHighlightChannel(interaction, guild); + } else { + await interaction.reply({ + content: "❌ unknown subcommand.", + flags: ["Ephemeral"], + }); } } catch (error) { logger.error("failed to execute config command:", error);

@@ -180,6 +210,29 @@ await guild.save();

await interaction.reply({ content: `✅ set bug reports channel to ${channel}.`, + flags: ["Ephemeral"], + }); +} + +async function handleHighlightChannel( + interaction: ChatInputCommandInteraction, + guild: GuildType, +) { + const channel = interaction.options.getChannel("channel", true); + + if (channel.type !== ChannelType.GuildText) { + await interaction.reply({ + content: "❌ highlight channel must be a text channel.", + flags: ["Ephemeral"], + }); + return; + } + + guild.highlights_channel = channel.id; + await guild.save(); + + await interaction.reply({ + content: `✅ set highlights channel to ${channel}.`, flags: ["Ephemeral"], }); }
A src/interactions/menus/highlight.ts

@@ -0,0 +1,134 @@

+import { GuildModel } from "@/database/schemas"; +import { Logger } from "@/utils/logging"; +import { + ApplicationCommandType, + type Attachment, + type Channel, + ChannelType, + type Client, + ContextMenuCommandBuilder, + type MessageContextMenuCommandInteraction, +} from "discord.js"; + +const logger = new Logger("highlight-menu"); + +const commandData = new ContextMenuCommandBuilder() + .setName("highlight") + .setType(ApplicationCommandType.Message); + +function extractVideoLinks(message: { content: string }): string[] { + const urlRegex = + /https?:\/\/(?:www\.)?(?:youtube\.com\/watch\?v=[\w-]+|youtu\.be\/[\w-]+|medal\.tv\/(?:games\/[\w-]+\/clips\/[\w-]+|g\/[\w-]+|clips\/[\w-]+))/gi; + return Array.from(message.content.matchAll(urlRegex)).map((m) => m[0]); +} + +function isVideoAttachment(attachment: Attachment): boolean { + return ( + typeof attachment.contentType === "string" && + attachment.contentType.startsWith("video/") + ); +} + +async function execute( + client: Client, + interaction: MessageContextMenuCommandInteraction, +) { + if (!interaction.guild) { + await interaction.reply({ + content: "❌ this command can only be used in a server.", + ephemeral: true, + }); + return; + } + + await interaction.deferReply({ flags: ["Ephemeral"] }); + const guildConfig = await GuildModel.findOne({ + guild_id: interaction.guild.id, + }); + if (!guildConfig?.highlights_channel) { + await interaction.editReply({ + content: + "❌ no highlights channel configured. Use `/config highlight-channel`.", + }); + return; + } + + const targetMessage = interaction.targetMessage; + if (!targetMessage) { + await interaction.editReply({ + content: "❌ couldn't find the target message.", + }); + return; + } + + const videoAttachments = targetMessage.attachments.filter(isVideoAttachment); + + const videoLinks = extractVideoLinks(targetMessage); + + if (videoAttachments.size === 0 && videoLinks.length === 0) { + await interaction.editReply({ + content: + "❌ no video attachments or supported links found in this message.", + }); + return; + } + + let highlightsChannel: Channel | null = null; + try { + highlightsChannel = await client.channels.fetch( + guildConfig.highlights_channel, + ); + } catch (e) { + logger.error("failed to fetch highlights channel:", e); + await interaction.editReply({ + content: "❌ could not access the highlights channel.", + }); + return; + } + + if (!highlightsChannel) { + await interaction.editReply({ + content: "❌ highlights channel not found.", + }); + return; + } + + if (highlightsChannel.type !== ChannelType.GuildText) { + await interaction.editReply({ + content: "❌ the highlights channel is not a text channel.", + }); + return; + } + + const files = Array.from(videoAttachments.values()).map((a) => a.url); + const author = `<@${targetMessage.author.id}>`; + const jumpUrl = targetMessage.url; + + const videoLinksText = videoLinks + .map((link) => `• [video Link](${link})`) + .join(" "); + const description = `:star: new highlight from ${author}!\n-# [jump to message](${jumpUrl}) ${videoLinksText}`; + + try { + const msg = await highlightsChannel.send({ + content: description, + files: files.length > 0 ? files : undefined, + allowedMentions: { users: [targetMessage.author.id] }, + }); + + await msg.react("⭐"); + await interaction.editReply({ + content: "✅ highlight sent!", + }); + } catch (error) { + logger.error("failed to send highlight:", error); + await interaction.editReply({ + content: "❌ failed to send highlight.", + }); + } +} + +export default { + data: commandData, + execute, +};