feat: initial prototype
All checks were successful
Node.js CI / Lint and Test (push) Successful in 31s

This commit is contained in:
Naomi Carrigan 2025-02-25 16:37:00 -08:00
parent 2ba0ae631b
commit f3ab9e541f
Signed by: naomi
SSH Key Fingerprint: SHA256:rca1iUI2OhAM6n4FIUaFcZcicmri0jgocqKiTTAfrt8
12 changed files with 4978 additions and 0 deletions

38
.gitea/workflows/ci.yml Normal file
View File

@ -0,0 +1,38 @@
name: Node.js CI
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
lint:
name: Lint and Test
steps:
- name: Checkout Source Files
uses: actions/checkout@v4
- name: Use Node.js v22
uses: actions/setup-node@v4
with:
node-version: 22
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 10
- name: Install Dependencies
run: pnpm install
- name: Lint Source Files
run: pnpm run lint
- name: Verify Build
run: pnpm run build
- name: Run Tests
run: pnpm run test

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules
prod

6
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,6 @@
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"eslint.validate": ["typescript"]
}

5
eslint.config.js Normal file
View File

@ -0,0 +1,5 @@
import NaomisConfig from "@nhcarrigan/eslint-config";
export default [
...NaomisConfig
]

31
package.json Normal file
View File

@ -0,0 +1,31 @@
{
"name": "standup-bot",
"version": "0.0.0",
"description": "Discord bot to post standup reminders for the Commit Your Code cohort.",
"main": "index.js",
"type": "module",
"scripts": {
"build": "rm -rf prod && tsc",
"lint": "eslint src --max-warnings 0",
"start": "op run --env-file=prod.env --no-masking -- node prod/index.js",
"test": "echo \"No tests yet!\" && exit 0"
},
"keywords": [],
"author": "Naomi Carrigan",
"license": "See license in LICENSE.md",
"devDependencies": {
"@nhcarrigan/eslint-config": "5.1.0",
"@nhcarrigan/typescript-config": "4.0.0",
"@types/node": "22.13.1",
"@types/node-schedule": "2.1.7",
"@vitest/coverage-istanbul": "3.0.5",
"eslint": "9.20.0",
"typescript": "5.7.3",
"vitest": "3.0.5"
},
"dependencies": {
"@nhcarrigan/logger": "1.0.0",
"discord.js": "14.18.0",
"node-schedule": "2.1.1"
}
}

4764
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

2
prod.env Normal file
View File

@ -0,0 +1,2 @@
DISCORD_TOKEN="op://Environment Variables - Naomi/Standup Bot/discord_token"
LOG_TOKEN="op://Environment Variables - Naomi/Alert Server/api_auth"

24
src/config/channels.ts Normal file
View File

@ -0,0 +1,24 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
/**
* The channels to post standup reminders in.
*/
export const channels = [
"1326291362483671202",
"1329625299406684263",
"1326291172896931921",
"1326291132535013409",
"1326291241045856286",
"1326291186863833169",
"1326291226332496026",
"1326291379621597304",
"1326291254421749770",
"1326291160632655983",
"1326291146267430912",
"1326291198666866788",
"1326291214026408009",
];

38
src/index.ts Normal file
View File

@ -0,0 +1,38 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { Client, Events, GatewayIntentBits } from "discord.js";
import { scheduleJob } from "node-schedule";
import { standup } from "./modules/standup.js";
import { logger } from "./utils/logger.js";
process.on("unhandledRejection", (error) => {
if (error instanceof Error) {
void logger.error("Unhandled Rejection", error);
return;
}
void logger.error("unhandled rejection", new Error(String(error)));
});
process.on("uncaughtException", (error) => {
if (error instanceof Error) {
void logger.error("Uncaught Exception", error);
return;
}
void logger.error("uncaught exception", new Error(String(error)));
});
const client = new Client({
intents: [ GatewayIntentBits.Guilds ],
});
client.on(Events.ClientReady, () => {
void logger.log("debug", "Bot is ready.");
scheduleJob("reminders", "0 6 * * 1-5", async() => {
await standup(client);
});
});
await client.login(process.env.DISCORD_TOKEN);

48
src/modules/standup.ts Normal file
View File

@ -0,0 +1,48 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { channels } from "../config/channels.js";
import { logger } from "../utils/logger.js";
import type { Client } from "discord.js";
/**
* Posts a daily standup reminder in configured channels.
* @param client - The Discord client.
*/
export const standup = async(client: Client): Promise<void> => {
try {
const mapped = await Promise.all(
channels.map(async(channel) => {
const fetched = await client.channels.fetch(channel).catch(() => {
return null;
});
if (!fetched) {
return null;
}
if (!fetched.isTextBased() || !("send" in fetched)) {
return null;
}
return fetched;
}),
);
const filtered = mapped.filter((channel) => {
return channel !== null;
});
await Promise.all(
filtered.map(async(channel) => {
await channel.send(
// eslint-disable-next-line stylistic/max-len -- This message is too long to fit on one line.
"Good morning @everyone~! It's time for standup! Please share your goals for today.",
);
}),
);
} catch (error) {
if (error instanceof Error) {
await logger.error("standup module", error);
return;
}
await logger.error("standup module", new Error(String(error)));
}
};

12
src/utils/logger.ts Normal file
View File

@ -0,0 +1,12 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { Logger } from "@nhcarrigan/logger";
export const logger = new Logger(
"Standup Bot",
process.env.LOG_TOKEN ?? "",
);

8
tsconfig.json Normal file
View File

@ -0,0 +1,8 @@
{
"extends": "@nhcarrigan/typescript-config",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./prod"
},
"exclude": ["test/**/*.ts", "vitest.config.ts"]
}