From 7fe31aa5d76c3ba7d43bcd153ef59fd0ba6fb717 Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Tue, 11 Nov 2025 20:26:43 +0100 Subject: [PATCH 01/19] feat: add Advent of Code channel ID to environment configuration --- .env.production | 1 + src/env.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/.env.production b/.env.production index fb3ab4c..88ae3d1 100644 --- a/.env.production +++ b/.env.production @@ -8,6 +8,7 @@ SERVER_ID=434487340535382016 # Channel IDs (from your dev server) GUIDES_CHANNEL_ID=1429492053825290371 REPEL_LOG_CHANNEL_ID=1403558160144531589 +ADVENT_OF_CODE_CHANNEL_ID=1047623689488830495 # Role IDs (from your dev server) REPEL_ROLE_ID=1002411741776461844 diff --git a/src/env.ts b/src/env.ts index e97f373..5ef679d 100644 --- a/src/env.ts +++ b/src/env.ts @@ -35,6 +35,7 @@ export const config = { channelIds: { repelLogs: requireEnv('REPEL_LOG_CHANNEL_ID'), guides: requireEnv('GUIDES_CHANNEL_ID'), + adventOfCode: requireEnv('ADVENT_OF_CODE_CHANNEL_ID'), }, }; From 54e8f8bfb9e653e7042d22613b1db3aab3b75e80 Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Tue, 11 Nov 2025 20:33:27 +0100 Subject: [PATCH 02/19] chore: add node-cron dependency with types --- package.json | 4 +++- pnpm-lock.yaml | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 21dcc6e..435e8f8 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "check": "biome check .", "check:fix": "biome check --write .", "typecheck": "tsc --noEmit", - "test": "pnpm run build:dev && node --test dist/**/*.test.js", + "test": "pnpm run build:dev && node --test test/*.test.mjs", "test:ci": "node --test dist/**/*.test.js", "prepare": "husky", "pre-commit": "lint-staged", @@ -34,12 +34,14 @@ "packageManager": "pnpm@10.17.1", "dependencies": { "discord.js": "^14.22.1", + "node-cron": "^4.2.1", "typescript": "^5.9.3", "web-features": "^3.7.0" }, "devDependencies": { "@biomejs/biome": "2.2.4", "@types/node": "^24.5.2", + "@types/node-cron": "^3.0.11", "husky": "^9.1.7", "lint-staged": "^16.2.1", "tsup": "^8.5.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 637a258..b34e314 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: discord.js: specifier: ^14.22.1 version: 14.23.2 + node-cron: + specifier: ^4.2.1 + version: 4.2.1 typescript: specifier: ^5.9.3 version: 5.9.3 @@ -24,6 +27,9 @@ importers: '@types/node': specifier: ^24.5.2 version: 24.8.1 + '@types/node-cron': + specifier: ^3.0.11 + version: 3.0.11 husky: specifier: ^9.1.7 version: 9.1.7 @@ -422,6 +428,9 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/node-cron@3.0.11': + resolution: {integrity: sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==} + '@types/node@24.8.1': resolution: {integrity: sha512-alv65KGRadQVfVcG69MuB4IzdYVpRwMG/mq8KWOaoOdyY617P5ivaDiMCGOFDWD2sAn5Q0mR3mRtUOgm99hL9Q==} @@ -698,6 +707,10 @@ packages: resolution: {integrity: sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==} engines: {node: '>=20.17'} + node-cron@4.2.1: + resolution: {integrity: sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==} + engines: {node: '>=6.0.0'} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -1223,6 +1236,8 @@ snapshots: '@types/estree@1.0.8': {} + '@types/node-cron@3.0.11': {} + '@types/node@24.8.1': dependencies: undici-types: 7.14.0 @@ -1505,6 +1520,8 @@ snapshots: nano-spawn@2.0.0: {} + node-cron@4.2.1: {} + object-assign@4.1.1: {} onetime@7.0.0: From cbdad055a3704a31be725ad38d56c0bd0c28ecf5 Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Tue, 11 Nov 2025 20:33:38 +0100 Subject: [PATCH 03/19] feat: implement Advent of Code scheduler to automate daily post creation in Discord forum --- src/util/advent-scheduler.ts | 156 +++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 src/util/advent-scheduler.ts diff --git a/src/util/advent-scheduler.ts b/src/util/advent-scheduler.ts new file mode 100644 index 0000000..ba906fe --- /dev/null +++ b/src/util/advent-scheduler.ts @@ -0,0 +1,156 @@ +import { ChannelType, type Client, type ForumChannel } from 'discord.js'; +import * as cron from 'node-cron'; +import { promises as fs } from 'node:fs'; + +const TRACKER_FILE = 'advent-of-code-tracker.json'; + +type TrackerData = { + [year: string]: number[]; +}; + +/** + * Load the tracker file to see which days have already been posted + */ +async function loadTracker(): Promise { + try { + const data = await fs.readFile(TRACKER_FILE, 'utf-8'); + return JSON.parse(data); + } catch (_error) { + // If file doesn't exist or can't be read, return empty object + return {}; + } +} + +/** + * Save the tracker file with updated data + */ +async function saveTracker(data: TrackerData): Promise { + await fs.writeFile(TRACKER_FILE, JSON.stringify(data, null, 2), 'utf-8'); +} + +/** + * Check if a specific day has already been posted for a given year + */ +async function isDayPosted(year: number, day: number): Promise { + const tracker = await loadTracker(); + const yearData = tracker[year.toString()]; + return yearData ? yearData.includes(day) : false; +} + +/** + * Mark a day as posted for a given year + */ +async function markDayAsPosted(year: number, day: number): Promise { + const tracker = await loadTracker(); + const yearKey = year.toString(); + + if (!tracker[yearKey]) { + tracker[yearKey] = []; + } + + if (!tracker[yearKey].includes(day)) { + tracker[yearKey].push(day); + tracker[yearKey].sort((a, b) => a - b); + await saveTracker(tracker); + } +} + +/** + * Create a forum post for a specific Advent of Code day + */ +async function createAdventPost( + client: Client, + channelId: string, + year: number, + day: number +): Promise { + try { + const channel = await client.channels.fetch(channelId); + + if (!channel) { + console.error(`❌ Advent of Code channel not found: ${channelId}`); + return false; + } + + if (channel.type !== ChannelType.GuildForum) { + console.error(`❌ Advent of Code channel is not a forum channel. Type: ${channel.type}`); + return false; + } + + const forumChannel = channel as ForumChannel; + const title = `Day ${day}, ${year}`; + const content = `https://adventofcode.com/${year}/day/${day}`; + + await forumChannel.threads.create({ + name: title, + message: { + content: content, + }, + }); + + console.log(`✅ Created Advent of Code post: ${title}`); + return true; + } catch (error) { + console.error(`❌ Failed to create Advent of Code post for day ${day}:`, error); + return false; + } +} + +/** + * Check if today is during Advent of Code (December 1-25) and create post if needed + */ +async function checkAndCreateTodaysPost(client: Client, channelId: string): Promise { + const now = new Date(); + const month = now.getUTCMonth(); // 0-indexed, so December is 11 + const day = now.getUTCDate(); + const year = now.getUTCFullYear(); + + // Only run during December (month 11) + if (month !== 10) { + return; + } + + // Only run for days 1-25 + if (day < 1 || day > 25) { + return; + } + + // Check if we've already posted for this day this year + const alreadyPosted = await isDayPosted(year, day); + if (alreadyPosted) { + console.log(`ℹ️ Advent of Code post for ${year} day ${day} already exists`); + return; + } + + // Create the post + const success = await createAdventPost(client, channelId, year, day); + + // Mark as posted if successful + if (success) { + await markDayAsPosted(year, day); + } +} + +/** + * Initialize the Advent of Code scheduler + * Runs every day at midnight UTC and checks if we should create a post + */ +export function initializeAdventScheduler(client: Client, channelId: string): void { + console.log('🎄 Initializing Advent of Code scheduler...'); + + // Run immediately on startup to check if we need to post today + checkAndCreateTodaysPost(client, channelId).catch((error) => { + console.error('❌ Error checking for Advent of Code post on startup:', error); + }); + + // Schedule to run every day at midnight UTC + // Cron pattern: '0 5 * * *' = At 05:00 UTC every day (midnight UTC-5) + cron.schedule('0 5 * * *', () => { + console.log('⏰ Running scheduled Advent of Code check...'); + checkAndCreateTodaysPost(client, channelId).catch((error) => { + console.error('❌ Error in scheduled Advent of Code check:', error); + }); + }); + + console.log('✅ Advent of Code scheduler initialized (runs daily at midnight UTC)'); +} From f16dabf0f46b8b5e3af7ea9fdd8adb4c4592658a Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Tue, 11 Nov 2025 20:33:49 +0100 Subject: [PATCH 04/19] feat: integrate Advent of Code scheduler initialization in ready event with error handling --- src/events/ready.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/events/ready.ts b/src/events/ready.ts index a9f45f8..0afa6c7 100644 --- a/src/events/ready.ts +++ b/src/events/ready.ts @@ -1,5 +1,6 @@ import { Events } from 'discord.js'; import { config } from '../env.js'; +import { initializeAdventScheduler } from '../util/advent-scheduler.js'; import { fetchAndCachePublicChannelsMessages } from '../util/cache.js'; import { createEvent } from '../util/events.js'; import { syncGuidesToChannel } from '../util/post-guides.js'; @@ -44,5 +45,12 @@ export const readyEvent = createEvent( } } } + + // Initialize Advent of Code scheduler + try { + initializeAdventScheduler(client, config.channelIds.adventOfCode); + } catch (error) { + console.error('❌ Failed to initialize Advent of Code scheduler:', error); + } } ); From 297816f8e0bc5da007c1bd102905b7f7f3a58f31 Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Tue, 11 Nov 2025 20:33:54 +0100 Subject: [PATCH 05/19] test: add unit tests for Advent scheduler tracker file operations --- test/advent-scheduler.test.mjs | 67 ++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 test/advent-scheduler.test.mjs diff --git a/test/advent-scheduler.test.mjs b/test/advent-scheduler.test.mjs new file mode 100644 index 0000000..2da1401 --- /dev/null +++ b/test/advent-scheduler.test.mjs @@ -0,0 +1,67 @@ +import assert from 'node:assert/strict'; +import { promises as fs } from 'node:fs'; +import test from 'node:test'; + +const TEST_TRACKER_FILE = 'test-advent-tracker.json'; + +/** + * Helper function to load tracker data + */ +async function loadTracker(filename) { + try { + const data = await fs.readFile(filename, 'utf-8'); + return JSON.parse(data); + } catch (error) { + return {}; + } +} + +/** + * Helper function to save tracker data + */ +async function saveTracker(filename, data) { + await fs.writeFile(filename, JSON.stringify(data, null, 2), 'utf-8'); +} + +/** + * Helper function to clean up test tracker file + */ +async function cleanupTestTracker() { + try { + await fs.unlink(TEST_TRACKER_FILE); + } catch (error) { + // File might not exist, that's fine + } +} + +test('advent scheduler: tracker file operations', async (t) => { + await t.test('should create empty tracker if file does not exist', async () => { + await cleanupTestTracker(); + const tracker = await loadTracker(TEST_TRACKER_FILE); + assert.deepEqual(tracker, {}); + }); + + await t.test('should save and load tracker data correctly', async () => { + const testData = { + '2025': [1, 2, 3], + '2026': [1], + }; + await saveTracker(TEST_TRACKER_FILE, testData); + const loaded = await loadTracker(TEST_TRACKER_FILE); + assert.deepEqual(loaded, testData); + }); + + await t.test('should track multiple days per year', async () => { + const tracker = { + '2025': [1, 5, 10, 15, 20, 25], + }; + await saveTracker(TEST_TRACKER_FILE, tracker); + const loaded = await loadTracker(TEST_TRACKER_FILE); + assert.equal(loaded['2025'].length, 6); + assert.ok(loaded['2025'].includes(1)); + assert.ok(loaded['2025'].includes(25)); + }); + + // Cleanup + await cleanupTestTracker(); +}); From 0e591578343885252e43b6b14acb01608190d3f9 Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Tue, 11 Nov 2025 20:34:04 +0100 Subject: [PATCH 06/19] chore: update .gitignore to include advent-of-code-tracker.json and rename guides-tracker.json --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 67b2b83..40c947f 100644 --- a/.gitignore +++ b/.gitignore @@ -19,8 +19,9 @@ yarn-error.log* !.env.production !.env.example -# guides tracker +# tracker guides-tracker.json +advent-of-code-tracker.json # Docker docker-compose.yml \ No newline at end of file From b74105221637138f3c11a725195598559655e84f Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Tue, 11 Nov 2025 20:37:09 +0100 Subject: [PATCH 07/19] feat: add Advent of Code tracker path to environment configuration and update related references --- .env.production | 1 + src/env.ts | 1 + src/util/advent-scheduler.ts | 3 ++- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.env.production b/.env.production index 88ae3d1..6631190 100644 --- a/.env.production +++ b/.env.production @@ -16,5 +16,6 @@ MODERATORS_ROLE_IDS=849481536654803004 # Other GUIDES_TRACKER_PATH=/app/data/guides-tracker.json +ADVENT_OF_CODE_TRACKER_PATH=/app/data/advent-of-code-tracker.json # Note: DISCORD_TOKEN & CLIENT_ID should be in .env.local (not committed) diff --git a/src/env.ts b/src/env.ts index 5ef679d..ace8485 100644 --- a/src/env.ts +++ b/src/env.ts @@ -23,6 +23,7 @@ export const config = { serverId: requireEnv('SERVER_ID'), fetchAndSyncMessages: true, guidesTrackerPath: optionalEnv('GUIDES_TRACKER_PATH'), + adventOfCodeTrackerPath: requireEnv('ADVENT_OF_CODE_TRACKER_PATH'), roleIds: { moderators: requireEnv('MODERATORS_ROLE_IDS') ? requireEnv('MODERATORS_ROLE_IDS').split(',') diff --git a/src/util/advent-scheduler.ts b/src/util/advent-scheduler.ts index ba906fe..836c003 100644 --- a/src/util/advent-scheduler.ts +++ b/src/util/advent-scheduler.ts @@ -1,8 +1,9 @@ import { ChannelType, type Client, type ForumChannel } from 'discord.js'; import * as cron from 'node-cron'; import { promises as fs } from 'node:fs'; +import { config } from '../env.js'; -const TRACKER_FILE = 'advent-of-code-tracker.json'; +const TRACKER_FILE = config.adventOfCodeTrackerPath; type TrackerData = { [year: string]: number[]; From 1bf978516b605f4681d2c78dad0da517bfa98eaf Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Tue, 11 Nov 2025 20:44:28 +0100 Subject: [PATCH 08/19] chore: update docker-compose.yml to clarify tracker data volume comment --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index bd5b017..30bcce6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,7 +15,7 @@ services: volumes: # Mount environment config file - ./.env.production:/app/.env.production:ro - # Persist guides tracker data + # Persist tracker data - guides-data:/app/data profiles: - prod From 79c128e10007c24d87d027d964ab32bc4fd5cb29 Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Tue, 11 Nov 2025 20:44:35 +0100 Subject: [PATCH 09/19] chore: update .env.example to clarify required Discord bot credentials and server configuration --- .env.example | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/.env.example b/.env.example index a5d0f74..be18af9 100644 --- a/.env.example +++ b/.env.example @@ -1,24 +1,26 @@ -# Local Environment Variables (Secrets) -# Copy this file to .env and fill in your actual values -# .env is gitignored and should NEVER be committed +# Discord Bot Credentials (REQUIRED - Secret values, do not commit!) +DISCORD_TOKEN=your_discord_bot_token_here +CLIENT_ID=your_discord_client_id_here -# Discord Bot Token & Application ID (REQUIRED) -# Get this from: https://discord.com/developers/applications -DISCORD_TOKEN=your-bot-token-here -CLIENT_ID=your-bot-application-id +# Server Configuration (REQUIRED) +SERVER_ID=your_server_id_here -# Override any public config values for local testing +# Channel IDs (REQUIRED) +GUIDES_CHANNEL_ID=your_guides_channel_id_here +ADVENT_OF_CODE_CHANNEL_ID=your_advent_of_code_forum_channel_id_here +REPEL_LOG_CHANNEL_ID=your_repel_log_channel_id_here -# Discord Server ID (your dev server) -SERVER_ID=your-server-id +# Role IDs (REQUIRED) +MODERATORS_ROLE_IDS=role_id_1,role_id_2,role_id_3 +REPEL_ROLE_ID=your_repel_role_id_here -# Channel IDs (from your dev server) -GUIDES_CHANNEL_ID=your-guide-channel-id -REPEL_LOG_CHANNEL_ID=your-repel-log-channel-id +# Optional Role IDs +# ROLE_A_ID=optional_role_a_id +# ROLE_B_ID=optional_role_b_id +# ROLE_C_ID=optional_role_c_id -# Role IDs (from your dev server) -REPEL_ROLE_ID=your-repel-role-id -MODERATORS_ROLE_IDS=your-moderator-role-id - -# Other -GUIDES_TRACKER_PATH=guides-tracker.json \ No newline at end of file +# Data Persistence (OPTIONAL) +# Local development defaults to current directory +# Docker deployments should use /app/data for persistence +GUIDES_TRACKER_PATH=guides-tracker.json +ADVENT_OF_CODE_TRACKER_PATH=advent-of-code-tracker.json From fa1dabc1cb6855197b20cb676c762dca1e5c569c Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Tue, 11 Nov 2025 20:45:39 +0100 Subject: [PATCH 10/19] fix: correct month check in Advent scheduler to ensure posts are created only in December --- src/util/advent-scheduler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/advent-scheduler.ts b/src/util/advent-scheduler.ts index 836c003..a93d104 100644 --- a/src/util/advent-scheduler.ts +++ b/src/util/advent-scheduler.ts @@ -107,7 +107,7 @@ async function checkAndCreateTodaysPost(client: Client, channelId: string): Prom const year = now.getUTCFullYear(); // Only run during December (month 11) - if (month !== 10) { + if (month !== 11) { return; } From 2224bc1a29bb69dee9bbdc8935b56417c56a4cb0 Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Wed, 12 Nov 2025 18:47:59 +0100 Subject: [PATCH 11/19] chore: update test command in package.json to use tsx for TypeScript test execution --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 435e8f8..69fb003 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "check": "biome check .", "check:fix": "biome check --write .", "typecheck": "tsc --noEmit", - "test": "pnpm run build:dev && node --test test/*.test.mjs", + "test": "tsx --test '**/*.test.ts'", "test:ci": "node --test dist/**/*.test.js", "prepare": "husky", "pre-commit": "lint-staged", From a69c0bd452728712f3b03d09a2de6c10302d7662 Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Wed, 12 Nov 2025 18:48:35 +0100 Subject: [PATCH 12/19] refactor: export functions to be used for testing --- .../util/advent-scheduler.test.ts | 36 +++++-------------- src/util/advent-scheduler.ts | 23 +++--------- 2 files changed, 13 insertions(+), 46 deletions(-) rename test/advent-scheduler.test.mjs => src/util/advent-scheduler.test.ts (58%) diff --git a/test/advent-scheduler.test.mjs b/src/util/advent-scheduler.test.ts similarity index 58% rename from test/advent-scheduler.test.mjs rename to src/util/advent-scheduler.test.ts index 2da1401..9762396 100644 --- a/test/advent-scheduler.test.mjs +++ b/src/util/advent-scheduler.test.ts @@ -3,33 +3,15 @@ import { promises as fs } from 'node:fs'; import test from 'node:test'; const TEST_TRACKER_FILE = 'test-advent-tracker.json'; +process.env.ADVENT_OF_CODE_TRACKER_PATH = TEST_TRACKER_FILE; -/** - * Helper function to load tracker data - */ -async function loadTracker(filename) { - try { - const data = await fs.readFile(filename, 'utf-8'); - return JSON.parse(data); - } catch (error) { - return {}; - } -} - -/** - * Helper function to save tracker data - */ -async function saveTracker(filename, data) { - await fs.writeFile(filename, JSON.stringify(data, null, 2), 'utf-8'); -} +// Import after setting env var +const { loadTracker, saveTracker } = await import('./advent-scheduler.js'); -/** - * Helper function to clean up test tracker file - */ async function cleanupTestTracker() { try { await fs.unlink(TEST_TRACKER_FILE); - } catch (error) { + } catch (_error) { // File might not exist, that's fine } } @@ -37,7 +19,7 @@ async function cleanupTestTracker() { test('advent scheduler: tracker file operations', async (t) => { await t.test('should create empty tracker if file does not exist', async () => { await cleanupTestTracker(); - const tracker = await loadTracker(TEST_TRACKER_FILE); + const tracker = await loadTracker(); assert.deepEqual(tracker, {}); }); @@ -46,8 +28,8 @@ test('advent scheduler: tracker file operations', async (t) => { '2025': [1, 2, 3], '2026': [1], }; - await saveTracker(TEST_TRACKER_FILE, testData); - const loaded = await loadTracker(TEST_TRACKER_FILE); + await saveTracker(testData); + const loaded = await loadTracker(); assert.deepEqual(loaded, testData); }); @@ -55,8 +37,8 @@ test('advent scheduler: tracker file operations', async (t) => { const tracker = { '2025': [1, 5, 10, 15, 20, 25], }; - await saveTracker(TEST_TRACKER_FILE, tracker); - const loaded = await loadTracker(TEST_TRACKER_FILE); + await saveTracker(tracker); + const loaded = await loadTracker(); assert.equal(loaded['2025'].length, 6); assert.ok(loaded['2025'].includes(1)); assert.ok(loaded['2025'].includes(25)); diff --git a/src/util/advent-scheduler.ts b/src/util/advent-scheduler.ts index a93d104..79dd039 100644 --- a/src/util/advent-scheduler.ts +++ b/src/util/advent-scheduler.ts @@ -1,4 +1,4 @@ -import { ChannelType, type Client, type ForumChannel } from 'discord.js'; +import { ChannelType, type Client } from 'discord.js'; import * as cron from 'node-cron'; import { promises as fs } from 'node:fs'; import { config } from '../env.js'; @@ -9,10 +9,7 @@ type TrackerData = { [year: string]: number[]; }; -/** - * Load the tracker file to see which days have already been posted - */ -async function loadTracker(): Promise { +export async function loadTracker(): Promise { try { const data = await fs.readFile(TRACKER_FILE, 'utf-8'); return JSON.parse(data); @@ -22,25 +19,16 @@ async function loadTracker(): Promise { } } -/** - * Save the tracker file with updated data - */ -async function saveTracker(data: TrackerData): Promise { +export async function saveTracker(data: TrackerData): Promise { await fs.writeFile(TRACKER_FILE, JSON.stringify(data, null, 2), 'utf-8'); } -/** - * Check if a specific day has already been posted for a given year - */ async function isDayPosted(year: number, day: number): Promise { const tracker = await loadTracker(); const yearData = tracker[year.toString()]; return yearData ? yearData.includes(day) : false; } -/** - * Mark a day as posted for a given year - */ async function markDayAsPosted(year: number, day: number): Promise { const tracker = await loadTracker(); const yearKey = year.toString(); @@ -56,9 +44,6 @@ async function markDayAsPosted(year: number, day: number): Promise { } } -/** - * Create a forum post for a specific Advent of Code day - */ async function createAdventPost( client: Client, channelId: string, @@ -78,7 +63,7 @@ async function createAdventPost( return false; } - const forumChannel = channel as ForumChannel; + const forumChannel = channel; const title = `Day ${day}, ${year}`; const content = `https://adventofcode.com/${year}/day/${day}`; From 949c3c1b2d19d794524b8e5df72c272ca1c9f153 Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Wed, 12 Nov 2025 18:53:01 +0100 Subject: [PATCH 13/19] refactor: convert to ts file --- test/{sanity.test.mjs => sanity.test.ts} | 1 - 1 file changed, 1 deletion(-) rename test/{sanity.test.mjs => sanity.test.ts} (99%) diff --git a/test/sanity.test.mjs b/test/sanity.test.ts similarity index 99% rename from test/sanity.test.mjs rename to test/sanity.test.ts index 33a4ced..7130a62 100644 --- a/test/sanity.test.mjs +++ b/test/sanity.test.ts @@ -4,4 +4,3 @@ import test from 'node:test'; test('sanity: 1 + 1 equals 2', () => { assert.equal(1 + 1, 2); }); - From f22a94d03f1b28f74ac3354e26ef4e53ac4e3f70 Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Wed, 12 Nov 2025 18:53:12 +0100 Subject: [PATCH 14/19] chore: update biome.json to include test files in the includes array --- biome.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/biome.json b/biome.json index b7ae8e1..4813a99 100644 --- a/biome.json +++ b/biome.json @@ -33,6 +33,6 @@ } }, "files": { - "includes": ["*.ts", "*.js", "*.json", "src/**/*"] + "includes": ["*.ts", "*.js", "*.json", "src/**/*", "test/**/*"] } } From 014be194438b95a129ed6f0969b9af6d79976ef9 Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Wed, 12 Nov 2025 18:55:49 +0100 Subject: [PATCH 15/19] refactor: remove redundant comments and improve clarity in Advent scheduler --- src/util/advent-scheduler.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/util/advent-scheduler.ts b/src/util/advent-scheduler.ts index 79dd039..a74ec59 100644 --- a/src/util/advent-scheduler.ts +++ b/src/util/advent-scheduler.ts @@ -82,9 +82,6 @@ async function createAdventPost( } } -/** - * Check if today is during Advent of Code (December 1-25) and create post if needed - */ async function checkAndCreateTodaysPost(client: Client, channelId: string): Promise { const now = new Date(); const month = now.getUTCMonth(); // 0-indexed, so December is 11 @@ -101,17 +98,14 @@ async function checkAndCreateTodaysPost(client: Client, channelId: string): Prom return; } - // Check if we've already posted for this day this year const alreadyPosted = await isDayPosted(year, day); if (alreadyPosted) { console.log(`ℹ️ Advent of Code post for ${year} day ${day} already exists`); return; } - // Create the post const success = await createAdventPost(client, channelId, year, day); - // Mark as posted if successful if (success) { await markDayAsPosted(year, day); } @@ -124,13 +118,12 @@ async function checkAndCreateTodaysPost(client: Client, channelId: string): Prom export function initializeAdventScheduler(client: Client, channelId: string): void { console.log('🎄 Initializing Advent of Code scheduler...'); - // Run immediately on startup to check if we need to post today checkAndCreateTodaysPost(client, channelId).catch((error) => { console.error('❌ Error checking for Advent of Code post on startup:', error); }); - // Schedule to run every day at midnight UTC - // Cron pattern: '0 5 * * *' = At 05:00 UTC every day (midnight UTC-5) + // Schedule to run every day at midnight UTC-5 + // https://github.com/node-cron/node-cron?tab=readme-ov-file#cron-syntax cron.schedule('0 5 * * *', () => { console.log('⏰ Running scheduled Advent of Code check...'); checkAndCreateTodaysPost(client, channelId).catch((error) => { From e3fe8f1e2a827a6056bc17334d826588fa5ede20 Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Wed, 12 Nov 2025 18:56:33 +0100 Subject: [PATCH 16/19] refactor: update Advent scheduler documentation to reflect time zone change --- src/util/advent-scheduler.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/util/advent-scheduler.ts b/src/util/advent-scheduler.ts index a74ec59..9aabd65 100644 --- a/src/util/advent-scheduler.ts +++ b/src/util/advent-scheduler.ts @@ -88,12 +88,10 @@ async function checkAndCreateTodaysPost(client: Client, channelId: string): Prom const day = now.getUTCDate(); const year = now.getUTCFullYear(); - // Only run during December (month 11) if (month !== 11) { return; } - // Only run for days 1-25 if (day < 1 || day > 25) { return; } @@ -113,7 +111,7 @@ async function checkAndCreateTodaysPost(client: Client, channelId: string): Prom /** * Initialize the Advent of Code scheduler - * Runs every day at midnight UTC and checks if we should create a post + * Runs every day at midnight UTC-5 and checks if we should create a post */ export function initializeAdventScheduler(client: Client, channelId: string): void { console.log('🎄 Initializing Advent of Code scheduler...'); From a06c6a89bbe385b5ee5abeb836d7c5cf220965fb Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Wed, 12 Nov 2025 20:28:42 +0100 Subject: [PATCH 17/19] feat: add Advent of Code channel and tracker path to test environment configuration --- .env.test | 4 +++- src/util/advent-scheduler.test.ts | 3 --- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.env.test b/.env.test index a5d0f74..d099dce 100644 --- a/.env.test +++ b/.env.test @@ -14,6 +14,7 @@ SERVER_ID=your-server-id # Channel IDs (from your dev server) GUIDES_CHANNEL_ID=your-guide-channel-id +ADVENT_OF_CODE_CHANNEL_ID=your_advent_of_code_forum_channel_id_here REPEL_LOG_CHANNEL_ID=your-repel-log-channel-id # Role IDs (from your dev server) @@ -21,4 +22,5 @@ REPEL_ROLE_ID=your-repel-role-id MODERATORS_ROLE_IDS=your-moderator-role-id # Other -GUIDES_TRACKER_PATH=guides-tracker.json \ No newline at end of file +GUIDES_TRACKER_PATH=guides-tracker.json +ADVENT_OF_CODE_TRACKER_PATH=test-advent-tracker.json \ No newline at end of file diff --git a/src/util/advent-scheduler.test.ts b/src/util/advent-scheduler.test.ts index 9762396..169b6cd 100644 --- a/src/util/advent-scheduler.test.ts +++ b/src/util/advent-scheduler.test.ts @@ -2,9 +2,6 @@ import assert from 'node:assert/strict'; import { promises as fs } from 'node:fs'; import test from 'node:test'; -const TEST_TRACKER_FILE = 'test-advent-tracker.json'; -process.env.ADVENT_OF_CODE_TRACKER_PATH = TEST_TRACKER_FILE; - // Import after setting env var const { loadTracker, saveTracker } = await import('./advent-scheduler.js'); From ac64ff8b514a27f91cbb1c81ae252b777ba10ec4 Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Wed, 12 Nov 2025 20:30:45 +0100 Subject: [PATCH 18/19] refactor: update file path handling in Advent scheduler tests --- src/util/advent-scheduler.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/util/advent-scheduler.test.ts b/src/util/advent-scheduler.test.ts index 169b6cd..2b6001d 100644 --- a/src/util/advent-scheduler.test.ts +++ b/src/util/advent-scheduler.test.ts @@ -1,13 +1,14 @@ import assert from 'node:assert/strict'; import { promises as fs } from 'node:fs'; import test from 'node:test'; +import { config } from '../env.js'; // Import after setting env var const { loadTracker, saveTracker } = await import('./advent-scheduler.js'); async function cleanupTestTracker() { try { - await fs.unlink(TEST_TRACKER_FILE); + await fs.unlink(config.adventOfCodeTrackerPath); } catch (_error) { // File might not exist, that's fine } From 80a47fd8dcf3ce9cda682ae67ac997ea34a20c4e Mon Sep 17 00:00:00 2001 From: Wiktoria Van Harneveldt Date: Wed, 12 Nov 2025 20:30:50 +0100 Subject: [PATCH 19/19] fix: simplify error handling in loadTracker function by removing unused error parameter --- src/util/advent-scheduler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/advent-scheduler.ts b/src/util/advent-scheduler.ts index 9aabd65..48949ad 100644 --- a/src/util/advent-scheduler.ts +++ b/src/util/advent-scheduler.ts @@ -13,7 +13,7 @@ export async function loadTracker(): Promise { try { const data = await fs.readFile(TRACKER_FILE, 'utf-8'); return JSON.parse(data); - } catch (_error) { + } catch { // If file doesn't exist or can't be read, return empty object return {}; }