all repos — stealth-developers @ 1b5781040ce72dcd1506a4f4058e2ad8fe61abdf

src/interactions/menus/highlight.ts (view raw)

 1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
import { GuildModel } from "@/database/schemas";
import lily from "@/utils/logging";
import {
	ApplicationCommandType,
	type Attachment,
	type Client,
	ContextMenuCommandBuilder,
	type GuildTextBasedChannel,
	type MessageContextMenuCommandInteraction,
} from "discord.js";

const logger = lily.child("highlightMenu");

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: GuildTextBasedChannel | null = null;
	try {
		highlightsChannel = (await client.channels.fetch(
			guildConfig.highlights_channel,
		)) as GuildTextBasedChannel | null;
	} 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;
	}

	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,
};