diff --git a/src/commands/forwardToOwner.ts b/src/commands/forwardToOwner.ts new file mode 100644 index 0000000..2166985 --- /dev/null +++ b/src/commands/forwardToOwner.ts @@ -0,0 +1,72 @@ +import { + ContextMenuCommandBuilder, + ApplicationCommandType, + type MessageContextMenuCommandInteraction, + DiscordAPIError, + ButtonBuilder, + EmbedBuilder, + ActionRowBuilder, + ButtonStyle, +} from "discord.js"; +import { ids } from "../config/ids.js"; + +export const forwardOwnerDM = { + data: new ContextMenuCommandBuilder() + .setName("Forward to Naomi") + .setType(ApplicationCommandType.Message), + + async execute(interaction: MessageContextMenuCommandInteraction) { + await interaction.deferReply({ ephemeral: true }); + + if (interaction.user.id !== ids.users.naomi) { + await interaction.editReply("❌ Only Naomi can use this command."); + return; + } + + const message = interaction.targetMessage; + + if (message.author.id === ids.users.naomi) { + await interaction.editReply("No need to forward your own message to yourself 😄"); + return; + } + + try { + const naomi = await interaction.client.users.fetch(ids.users.naomi); + + const forwardedEmbed = new EmbedBuilder() + .setColor(0x5865F2) + .setTitle(`Message from ${message.author.tag}!`) + .setDescription( + (message.attachments.size > 0 ? `**Attachments:** ${message.attachments.size} file(s)\n\n` : "\n") + + (message.embeds.length > 0 ? `**Embeds:** ${message.embeds.length}\n\n` : "") + "\n" + + (message.content || "*[No text content]*") + "\n\n"); + +const viewButton = new ButtonBuilder() + .setLabel("View Message") + .setURL(message.url) + .setStyle(ButtonStyle.Link); + +const row = new ActionRowBuilder() + .addComponents(viewButton); + await naomi.send({ + embeds: [forwardedEmbed], + files: message.attachments.map((att) => att.url), + components: [row], + }); + + await interaction.editReply({ + content: "✅ Forwarded to your DMs!", + }); + } catch (error) { + console.error("Failed to forward:", error); + + let replyText = "❌ Failed to forward message."; + + if (error instanceof DiscordAPIError && error.code === 50007) { + replyText += " (Naomi's DMs might be closed)"; + } + + await interaction.editReply(replyText); + } + }, +}; \ No newline at end of file diff --git a/src/config/ids.ts b/src/config/ids.ts index c2c42ce..35fca11 100644 --- a/src/config/ids.ts +++ b/src/config/ids.ts @@ -70,5 +70,6 @@ export const ids = { amari: "1406431359345496255", naomi: "465650873650118659", nhcarrigan: "1382837581649150104", + teklu:"1381735115163570198", }, -}; +}; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 312ffeb..84cac00 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,14 +6,15 @@ import { DiscordAnalytics } from "@nhcarrigan/discord-analytics"; import { - Client, - GatewayIntentBits, - Events, - Partials, - MessageFlags, + Client, + GatewayIntentBits, + Events, + Partials, + MessageFlags, } from "discord.js"; import { scheduleJob } from "node-schedule"; import { App } from "octokit"; +import { forwardOwnerDM } from "./commands/forwardToOwner.js"; import { ids } from "./config/ids.js"; import { handleMessageCreate } from "./events/handleMessageCreate.js"; import { cacheData } from "./modules/cacheData.js"; @@ -31,136 +32,148 @@ import { logger } from "./utils/logger.js"; import type { Amari } from "./interfaces/amari.js"; if ( - process.env.GH_CLIENT_ID === undefined - || process.env.GH_PRIVATE_KEY === undefined + process.env.GH_CLIENT_ID === undefined + || process.env.GH_PRIVATE_KEY === undefined ) { - throw new Error("Cannot initialise GitHub!"); + throw new Error("Cannot initialise GitHub!"); } const githubApp = new App({ - appId: process.env.GH_CLIENT_ID, - privateKey: process.env.GH_PRIVATE_KEY.replaceAll("\\n", "\n"), + appId: process.env.GH_CLIENT_ID, + privateKey: process.env.GH_PRIVATE_KEY.replaceAll("\\n", "\n"), }); + const octokit = await githubApp.getInstallationOctokit(83_119_105); const { data } = await octokit.rest.apps.getAuthenticated(); await logger.log( - "debug", - `Authenticated to GitHub as ${data?.name ?? "unknown"}`, + "debug", + `Authenticated to GitHub as ${data?.name ?? "unknown"}`, ); const amari: Amari = { - discord: new Client({ - intents: [ - GatewayIntentBits.Guilds, - GatewayIntentBits.GuildMessages, - GatewayIntentBits.MessageContent, - GatewayIntentBits.GuildMembers, - GatewayIntentBits.DirectMessages, - ], - partials: [ Partials.Channel ], - }), - github: octokit, - lastRssItems: { - freeCodeCamp: null, - hackerNews: null, - }, - recentlyActiveChannels: new Set(), + discord: new Client({ + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.MessageContent, + GatewayIntentBits.GuildMembers, + GatewayIntentBits.DirectMessages, + ], + partials: [Partials.Channel], + }), + github: octokit, + lastRssItems: { + freeCodeCamp: null, + hackerNews: null, + }, + recentlyActiveChannels: new Set(), }; const analytics = new DiscordAnalytics(amari.discord, logger); amari.discord.once(Events.ClientReady, () => { - void logger.log( - "debug", - `Authenticated to Discord as ${amari.discord.user?.username ?? "unknown"}`, - ); - void cacheData(amari); - analytics.startCron(); - scheduleJob("post news", "0 * * * *", async() => { - await postFreeCodeCampNews(amari); - await postHackerNews(amari); - }); - scheduleJob("check guild tags", "0 0 * * *", async() => { - await logger.log("debug", "Auditing guild tags."); - await cacheData(amari); - }); - scheduleJob("post progress reminders", "0 9 * * 1-5", async() => { - await postProgressReminders(amari); - }); - setInterval(() => { - amari.recentlyActiveChannels = new Set(); - }, 10 * 60 * 1000); - setInterval(() => { - void checkRetroAchievements(amari); - }, 10 * 60 * 1000); + void logger.log( + "debug", + `Authenticated to Discord as ${amari.discord.user?.username ?? "unknown"}`, + ); + void cacheData(amari); + analytics.startCron(); + scheduleJob("post news", "0 * * * *", async () => { + await postFreeCodeCampNews(amari); + await postHackerNews(amari); + }); + scheduleJob("check guild tags", "0 0 * * *", async () => { + await logger.log("debug", "Auditing guild tags."); + await cacheData(amari); + }); + scheduleJob("post progress reminders", "0 9 * * 1-5", async () => { + await postProgressReminders(amari); + }); + setInterval(() => { + amari.recentlyActiveChannels = new Set(); + }, 10 * 60 * 1000); + setInterval(() => { + void checkRetroAchievements(amari); + }, 10 * 60 * 1000); }); amari.discord.on(Events.MessageCreate, (message) => { - if (!message.inGuild()) { - void respondToDm(amari, message); - return; - } - void handleMessageCreate(amari, message); + if (!message.inGuild()) { + void respondToDm(amari, message); + return; + } + void handleMessageCreate(amari, message); }); amari.discord.on(Events.InteractionCreate, (interaction) => { - void analytics.logGatewayEvent(Events.InteractionCreate, { ...interaction }); - if (interaction.isButton() && interaction.customId === "resolve") { - if (interaction.user.id !== ids.users.naomi) { - return void interaction.reply({ - content: "Who are you????", - flags: [ MessageFlags.Ephemeral ], - }); + void analytics.logGatewayEvent(Events.InteractionCreate, { ...interaction }); + + if (interaction.isMessageContextMenuCommand() && interaction.commandName === "Forward to Naomi") { + void forwardOwnerDM.execute(interaction); + return } - return void interaction.message.delete(); - } - if (interaction.isAutocomplete()) { - return void interaction; - } - return void interaction.reply({ - content: "What?", - flags: [ MessageFlags.Ephemeral ], - }); + + if (interaction.isButton() && interaction.customId === "resolve") { + if (interaction.user.id !== ids.users.naomi) { + return void interaction.reply({ + content: "Who are you????", + flags: [MessageFlags.Ephemeral], + }); + } + + return void interaction.message.delete(); + } + if (interaction.isAutocomplete()) { + return void interaction; + } + return void interaction.reply({ + content: "What?", + flags: [MessageFlags.Ephemeral], + }); }); amari.discord.on(Events.ThreadCreate, (thread) => { - if (thread.parent?.isThreadOnly() !== true) { - return; - } - const { bugReports, communityFeedback, featureRequests, policyIdeation } - = ids.channels; - if ( - ![ bugReports, - communityFeedback, - featureRequests, - policyIdeation ].includes( - thread.parent.id, - ) - ) { - return; - } - const tagId = getForumTagId(thread.parent.id); - if (tagId === null) { - return; - } - void thread.setAppliedTags([ tagId ]); + if (thread.parent?.isThreadOnly() !== true) { + return; + } + const { bugReports, communityFeedback, featureRequests, policyIdeation } + = ids.channels; + if ( + ![bugReports, + communityFeedback, + featureRequests, + policyIdeation].includes( + thread.parent.id, + ) + ) { + return; + } + const tagId = getForumTagId(thread.parent.id); + if (tagId === null) { + return; + } + void thread.setAppliedTags([tagId]); }); amari.discord.on(Events.UserUpdate, (_oldUser, updatedUser) => { - void processUserGuildTag(amari, updatedUser); + void processUserGuildTag(amari, updatedUser); }); amari.discord.on(Events.GuildMemberUpdate, (oldMember, updatedMember) => { - void processMentorshipRole(amari, oldMember, updatedMember); + void processMentorshipRole(amari, oldMember, updatedMember); }); amari.discord.on(Events.GuildMemberAdd, (member) => { - void logMenteeJoin(amari, member); + void logMenteeJoin(amari, member); }); amari.discord.on(Events.GuildMemberRemove, (member) => { - void logMenteeLeave(amari, member); + void logMenteeLeave(amari, member); }); await amari.discord.login(process.env.BOT_TOKEN); +amari.discord.on(Events.MessageCreate, (message) => { + const channelType = message.channel.type; + console.log(`📩 [${channelType}] ${message.author.tag}: ${message.content}`); +}); instantiateServer(amari); diff --git a/src/scripts/deploy-global.ts b/src/scripts/deploy-global.ts new file mode 100644 index 0000000..0a4de4e --- /dev/null +++ b/src/scripts/deploy-global.ts @@ -0,0 +1,32 @@ +import { REST, Routes } from "discord.js"; +import { forwardOwnerDM } from "../commands/forwardToOwner.js"; + +const commands = [ + forwardOwnerDM.data.toJSON(), +]; + +const token = process.env.BOT_TOKEN; +const clientId = process.env.GH_CLIENT_ID; + +if (token === undefined) { + throw new Error("BOT_TOKEN is missing from environment variables!"); +} +if (clientId === undefined) { + throw new Error("CLIENT_ID is missing from environment variables!"); +} + +const rest = new REST({ version: "10" }).setToken(token); +(async() => { + try { + console.log("Registering GLOBAL context menu command... wait till appear everywhere."); + + await rest.put( + Routes.applicationCommands(clientId), + { body: commands }, + ); + + console.log("Global registration sent! then check right-click on a message → Apps."); + } catch (error) { + console.error("Registration failed:", error); + } +})();