4 Commits

Author SHA1 Message Date
teklu 10c7d337a5 fix: solve linter error
Node.js CI / CI (pull_request) Successful in 28s
Security Scan and Upload / Security & DefectDojo Upload (pull_request) Successful in 51s
2026-02-23 22:40:53 +09:00
teklu 60fec0f2d8 fix: solve linter error
Node.js CI / CI (pull_request) Failing after 24s
Security Scan and Upload / Security & DefectDojo Upload (pull_request) Successful in 53s
2026-02-21 23:36:20 +09:00
teklu 0a3c000add fix: fix lint erorr
Node.js CI / CI (pull_request) Failing after 23s
Security Scan and Upload / Security & DefectDojo Upload (pull_request) Successful in 50s
2026-02-17 01:13:41 +09:00
teklu 0394d03361 feat: add owner-only message context menu to forward messages to DMs 2026-02-16 19:48:27 +09:00
4 changed files with 159 additions and 10 deletions
+95
View File
@@ -0,0 +1,95 @@
/**
* @copyright NHCarrigan
* @license Naomi's Public License
* @author Teklu Abayneh
*/
import {
ContextMenuCommandBuilder,
ApplicationCommandType,
type MessageContextMenuCommandInteraction,
DiscordAPIError,
ButtonBuilder,
EmbedBuilder,
ActionRowBuilder,
ButtonStyle,
type Message,
} from "discord.js";
import { ids } from "../config/ids.js";
const buildForwardedEmbed = (message: Message): EmbedBuilder => {
const forwardedEmbed = new EmbedBuilder().
setColor(0x58_65_F2).
setTitle(`Message from ${String(message.author.tag)}!`).
setDescription(
`${(message.attachments.size > 0
? `**Attachments:** ${String(message.attachments.size)}
file(s)\n\n`
: "\n")
+ (message.embeds.length > 0
? `**Embeds:** ${String(message.embeds.length)}\n\n`
: "")}
\n${message.content}\n\n`,
);
return forwardedEmbed;
};
const buildViewButtonFunction = (message: Message): ButtonBuilder => {
const viewButton = new ButtonBuilder().
setLabel("View Message").
setURL(message.url).
setStyle(ButtonStyle.Link);
return viewButton;
};
const data = new ContextMenuCommandBuilder().setName("Forward to Naomi").
setType(ApplicationCommandType.Message);
const execute = async(interaction: MessageContextMenuCommandInteraction):
Promise<void> => {
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 = buildForwardedEmbed(message);
const viewButton = buildViewButtonFunction(message);
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(
viewButton,
);
await naomi.send({
components: [ row ],
embeds: [ forwardedEmbed ],
files: message.attachments.map((att) => {
return att.url;
}),
});
await interaction.editReply({
content: "✅ Forwarded to your DMs!",
});
} catch (error) {
let replyText = "❌ Failed to forward message.";
if (error instanceof DiscordAPIError && error.code === 50_007) {
replyText = `${replyText} (Naomi's DMs might be closed)`;
}
await interaction.editReply(replyText);
}
};
export const forwardOwnerDM = {
data,
execute,
};
+1
View File
@@ -70,5 +70,6 @@ export const ids = {
amari: "1406431359345496255", amari: "1406431359345496255",
naomi: "465650873650118659", naomi: "465650873650118659",
nhcarrigan: "1382837581649150104", nhcarrigan: "1382837581649150104",
teklu: "1381735115163570198",
}, },
}; };
+31 -10
View File
@@ -14,6 +14,7 @@ import {
} from "discord.js"; } from "discord.js";
import { scheduleJob } from "node-schedule"; import { scheduleJob } from "node-schedule";
import { App } from "octokit"; import { App } from "octokit";
import { forwardOwnerDM } from "./commands/forwardToOwner.js";
import { ids } from "./config/ids.js"; import { ids } from "./config/ids.js";
import { handleMessageCreate } from "./events/handleMessageCreate.js"; import { handleMessageCreate } from "./events/handleMessageCreate.js";
import { cacheData } from "./modules/cacheData.js"; import { cacheData } from "./modules/cacheData.js";
@@ -41,6 +42,7 @@ const githubApp = new App({
appId: process.env.GH_CLIENT_ID, appId: process.env.GH_CLIENT_ID,
privateKey: process.env.GH_PRIVATE_KEY.replaceAll("\\n", "\n"), privateKey: process.env.GH_PRIVATE_KEY.replaceAll("\\n", "\n"),
}); });
const octokit = await githubApp.getInstallationOctokit(83_119_105); const octokit = await githubApp.getInstallationOctokit(83_119_105);
const { data } = await octokit.rest.apps.getAuthenticated(); const { data } = await octokit.rest.apps.getAuthenticated();
await logger.log( await logger.log(
@@ -87,12 +89,18 @@ amari.discord.once(Events.ClientReady, () => {
scheduleJob("post progress reminders", "0 9 * * 1-5", async() => { scheduleJob("post progress reminders", "0 9 * * 1-5", async() => {
await postProgressReminders(amari); await postProgressReminders(amari);
}); });
setInterval(() => { setInterval(
amari.recentlyActiveChannels = new Set<string>(); () => {
}, 10 * 60 * 1000); amari.recentlyActiveChannels = new Set<string>();
setInterval(() => { },
void checkRetroAchievements(amari); 10 * 60 * 1000,
}, 10 * 60 * 1000); );
setInterval(
() => {
void checkRetroAchievements(amari);
},
10 * 60 * 1000,
);
}); });
amari.discord.on(Events.MessageCreate, (message) => { amari.discord.on(Events.MessageCreate, (message) => {
@@ -105,19 +113,32 @@ amari.discord.on(Events.MessageCreate, (message) => {
amari.discord.on(Events.InteractionCreate, (interaction) => { amari.discord.on(Events.InteractionCreate, (interaction) => {
void analytics.logGatewayEvent(Events.InteractionCreate, { ...interaction }); void analytics.logGatewayEvent(Events.InteractionCreate, { ...interaction });
if (
interaction.isMessageContextMenuCommand()
&& interaction.commandName === "Forward to Naomi"
) {
void forwardOwnerDM.execute(interaction);
return;
}
if (interaction.isButton() && interaction.customId === "resolve") { if (interaction.isButton() && interaction.customId === "resolve") {
if (interaction.user.id !== ids.users.naomi) { if (interaction.user.id !== ids.users.naomi) {
return void interaction.reply({ void interaction.reply({
content: "Who are you????", content: "Who are you????",
flags: [ MessageFlags.Ephemeral ], flags: [ MessageFlags.Ephemeral ],
}); });
return;
} }
return void interaction.message.delete();
void interaction.message.delete();
return;
} }
if (interaction.isAutocomplete()) { if (interaction.isAutocomplete()) {
return void interaction; void interaction;
return;
} }
return void interaction.reply({ void interaction.reply({
content: "What?", content: "What?",
flags: [ MessageFlags.Ephemeral ], flags: [ MessageFlags.Ephemeral ],
}); });
+32
View File
@@ -0,0 +1,32 @@
/**
* @copyright NHCarrigan
* @license Naomi's Public License
* @author Teklu Abayneh
*/
import { REST, Routes } from "discord.js";
import { forwardOwnerDM } from "../commands/forwardToOwner.js";
import { logger } from "../utils/logger.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);
const requestCommand = async(): Promise<void> => {
try {
await rest.put(Routes.applicationCommands(clientId), { body: commands });
} catch (error) {
if (error instanceof Error) {
await logger.error("operation", error);
}
}
};
void requestCommand();