feat: rewrite to be more generic
Some checks failed
Code Analysis / SonarQube (push) Failing after 17s
Node.js CI / Lint and Test (push) Successful in 37s

This commit is contained in:
2025-06-27 17:04:32 -07:00
parent 1061c40721
commit 89e3e92fa6
6 changed files with 593 additions and 320 deletions

View File

@ -14,18 +14,18 @@
"author": "Naomi Carrigan", "author": "Naomi Carrigan",
"license": "See license in LICENSE.md", "license": "See license in LICENSE.md",
"devDependencies": { "devDependencies": {
"@nhcarrigan/eslint-config": "5.1.0", "@nhcarrigan/eslint-config": "5.2.0",
"@nhcarrigan/typescript-config": "4.0.0", "@nhcarrigan/typescript-config": "4.0.0",
"@types/node": "22.13.1", "@types/node": "24.0.6",
"@types/node-schedule": "2.1.7", "@types/node-schedule": "2.1.7",
"@vitest/coverage-istanbul": "3.0.5", "@vitest/coverage-istanbul": "3.2.4",
"eslint": "9.20.0", "eslint": "9.30.0",
"typescript": "5.7.3", "typescript": "5.8.3",
"vitest": "3.0.5" "vitest": "3.2.4"
}, },
"dependencies": { "dependencies": {
"@nhcarrigan/logger": "1.0.0", "@nhcarrigan/logger": "1.0.0",
"discord.js": "14.18.0", "discord.js": "14.21.0",
"node-schedule": "2.1.1" "node-schedule": "2.1.1"
} }
} }

663
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -7,18 +7,109 @@
/** /**
* The channels to post standup reminders in. * The channels to post standup reminders in.
*/ */
export const channels = [ export const channels: Array<{
"1326291362483671202", channelId: string;
"1329625299406684263", roleId: string;
"1326291172896931921", name: string;
"1326291132535013409", }> = [
"1326291241045856286", {
"1326291186863833169", channelId: "1382093555228606484",
"1326291226332496026", name: "red-script",
"1326291379621597304", roleId: "1382091453987029144",
"1326291254421749770", },
"1326291160632655983", {
"1326291146267430912", channelId: "1382093583963656223",
"1326291198666866788", name: "orange-bytes",
"1326291214026408009", roleId: "1382091462707122186",
},
{
channelId: "1382093618017337574",
name: "yellow-packet",
roleId: "1382091466767208538",
},
{
channelId: "1382093654885138439",
name: "green-loop",
roleId: "1382091470877491364",
},
{
channelId: "1382093684925005935",
name: "blue-stack",
roleId: "1382091474560356393",
},
{
channelId: "1382093724200210522",
name: "purple-array",
roleId: "1382091477710012456",
},
{
channelId: "1382093753489162330",
name: "pink-protocol",
roleId: "1382091502255341568",
},
{
channelId: "1382093785973915820",
name: "cyan-branch",
roleId: "1382091520533856337",
},
{
channelId: "1382093811693387867",
name: "teal-cache",
roleId: "1382091525873336400",
},
{
channelId: "1382093843918225520",
name: "mint-syntax",
roleId: "1382091530428219603",
},
{
channelId: "1382093883722174605",
name: "lime-variable",
roleId: "1382091534538641458",
},
{
channelId: "1382093910259798046",
name: "rose-token",
roleId: "1382091538900979762",
},
{
channelId: "1382093973488799886",
name: "lavender-query",
roleId: "1382091542961066014",
},
{
channelId: "1382094002526097489",
name: "indigo-class",
roleId: "1382091547742568559",
},
{
channelId: "1382094051360116837",
name: "golden-index",
roleId: "1382091551982882936",
},
{
channelId: "1382094088739754096",
name: "silver-promise",
roleId: "1382091556613394443",
},
{
channelId: "1382094114467745912",
name: "bronze-thread",
roleId: "1382091560367292536",
},
{
channelId: "1382094146206044181",
name: "maroon-socket",
roleId: "1382091564498681907",
},
{
channelId: "1382094200211771522",
name: "violet-function",
roleId: "1382091568252719205",
},
{
channelId: "1382094311264616670",
name: "grey-database",
roleId: "1382091572602212622",
},
]; ];

View File

@ -5,9 +5,12 @@
*/ */
import { Client, Events, GatewayIntentBits } from "discord.js"; import { Client, Events, GatewayIntentBits } from "discord.js";
import { scheduleJob } from "node-schedule"; import { scheduleJob } from "node-schedule";
import { channels } from "./config/channels.js";
import { standup } from "./modules/standup.js"; import { standup } from "./modules/standup.js";
import { logger } from "./utils/logger.js"; import { logger } from "./utils/logger.js";
const activeIds: Array<string> = [];
process.on("unhandledRejection", (error) => { process.on("unhandledRejection", (error) => {
if (error instanceof Error) { if (error instanceof Error) {
void logger.error("Unhandled Rejection", error); void logger.error("Unhandled Rejection", error);
@ -25,14 +28,35 @@ process.on("uncaughtException", (error) => {
}); });
const client = new Client({ const client = new Client({
intents: [ GatewayIntentBits.Guilds ], intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages ],
}); });
client.on(Events.ClientReady, () => { client.on(Events.ClientReady, () => {
void logger.log("debug", "Bot is ready."); void logger.log("debug", "Bot is ready.");
scheduleJob("reminders", "0 6 * * 1-5", async() => { scheduleJob("reminders", "0 9 * * 1-5", async() => {
await standup(client); await standup(client, activeIds);
}); });
}); });
client.on(Events.MessageCreate, (message) => {
if (!message.channel.isThread() || !message.inGuild()) {
return;
}
if (message.channel.parentId === null) {
return;
}
const isInTeamChannel = channels.some((channel) => {
return channel.channelId === message.channel.parentId;
});
if (!isInTeamChannel) {
return;
}
if (message.author.bot) {
return;
}
if (!activeIds.includes(message.author.id)) {
activeIds.push(message.author.id);
}
});
await client.login(process.env.DISCORD_TOKEN); await client.login(process.env.DISCORD_TOKEN);

View File

@ -3,28 +3,58 @@
* @license Naomi's Public License * @license Naomi's Public License
* @author Naomi Carrigan * @author Naomi Carrigan
*/ */
import { ChannelType, type Client } from "discord.js"; import { AttachmentBuilder, ChannelType, type Client } from "discord.js";
import { channels } from "../config/channels.js"; import { channels } from "../config/channels.js";
import { logger } from "../utils/logger.js"; import { logger } from "../utils/logger.js";
const emptyArray = (array: Array<unknown>): void => {
array.splice(0, array.length);
};
/** /**
* Posts a daily standup reminder in configured channels. * Posts a daily standup reminder in configured channels.
* @param client - The Discord client. * @param client - The Discord client.
* @param activeIds - The list of user IDs who sent a progress update yesterday.
*/ */
export const standup = async(client: Client): Promise<void> => { // eslint-disable-next-line max-lines-per-function -- shut up
export const standup = async(
client: Client,
activeIds: Array<string>,
): Promise<void> => {
try { try {
await client.users.
send("465650873650118659", {
content: `The following people sent a progress update yesterday:`,
files: [
new AttachmentBuilder(Buffer.from(activeIds.join("\n")), {
name: "ids.txt",
}),
],
}).
catch(() => {
void logger.log(
"warn",
"Failed to send progress update message to user.",
);
});
emptyArray(activeIds);
const mapped = await Promise.all( const mapped = await Promise.all(
channels.map(async(channel) => { channels.map(async(channel) => {
const fetched = await client.channels.fetch(channel).catch(() => { const fetched = await client.channels.
void logger.log("warn", `Failed to fetch channel ${channel}.`); fetch(channel.channelId).
catch(() => {
void logger.log("warn", `Failed to fetch channel ${channel.name}.`);
return null; return null;
}); });
if (!fetched) { if (!fetched) {
await logger.log("warn", `Channel ${channel} not found.`); await logger.log("warn", `Channel ${channel.name} not found.`);
return null; return null;
} }
if (fetched.type !== ChannelType.GuildText) { if (fetched.type !== ChannelType.GuildText) {
await logger.log("warn", `Channel ${channel} is not a text channel.`); await logger.log(
"warn",
`Channel ${channel.name} is not a text channel.`,
);
return null; return null;
} }
return fetched; return fetched;
@ -35,12 +65,39 @@ export const standup = async(client: Client): Promise<void> => {
}); });
await Promise.all( await Promise.all(
filtered.map(async(channel) => { filtered.map(async(channel) => {
await channel.send( const sent = await channel.
// eslint-disable-next-line stylistic/max-len -- This message is too long to fit on one line. send(
"Good morning @everyone~! It's time for standup! Please share your goals for today.", `Good morning <@&${
).catch(() => { channels.find((c) => {
void logger.log("warn", `Failed to send standup reminder in channel ${channel.name}.`); return c.channelId === channel.id;
})?.roleId ?? "692816967895220344"
}> It is time for your daily progress update. Please share the following in this thread:
1⃣ What did you accomplish yesterday?
2⃣ What are you working on today?
3⃣ Are there any blockers or issues you need help with?`,
).
catch(() => {
void logger.log(
"warn",
`Failed to send progress reminder in channel ${channel.name}.`,
);
return null;
}); });
if (sent) {
await sent.
startThread({
autoArchiveDuration: 1440,
name: `Daily Progress Update - ${new Date().toLocaleDateString()}`,
}).
catch(() => {
void logger.log(
"warn",
`Failed to start thread in channel ${channel.name}.`,
);
return null;
});
}
}), }),
); );
} catch (error) { } catch (error) {

View File

@ -7,6 +7,6 @@
import { Logger } from "@nhcarrigan/logger"; import { Logger } from "@nhcarrigan/logger";
export const logger = new Logger( export const logger = new Logger(
"Standup Bot", "Maribelle",
process.env.LOG_TOKEN ?? "", process.env.LOG_TOKEN ?? "",
); );