Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
7fe31aa
feat: add Advent of Code channel ID to environment configuration
wiktoriavh Nov 11, 2025
54e8f8b
chore: add node-cron dependency with types
wiktoriavh Nov 11, 2025
cbdad05
feat: implement Advent of Code scheduler to automate daily post creat…
wiktoriavh Nov 11, 2025
f16dabf
feat: integrate Advent of Code scheduler initialization in ready even…
wiktoriavh Nov 11, 2025
297816f
test: add unit tests for Advent scheduler tracker file operations
wiktoriavh Nov 11, 2025
0e59157
chore: update .gitignore to include advent-of-code-tracker.json and r…
wiktoriavh Nov 11, 2025
b741052
feat: add Advent of Code tracker path to environment configuration an…
wiktoriavh Nov 11, 2025
1bf9785
chore: update docker-compose.yml to clarify tracker data volume comment
wiktoriavh Nov 11, 2025
79c128e
chore: update .env.example to clarify required Discord bot credential…
wiktoriavh Nov 11, 2025
fa1dabc
fix: correct month check in Advent scheduler to ensure posts are crea…
wiktoriavh Nov 11, 2025
2224bc1
chore: update test command in package.json to use tsx for TypeScript …
wiktoriavh Nov 12, 2025
a69c0bd
refactor: export functions to be used for testing
wiktoriavh Nov 12, 2025
949c3c1
refactor: convert to ts file
wiktoriavh Nov 12, 2025
f22a94d
chore: update biome.json to include test files in the includes array
wiktoriavh Nov 12, 2025
014be19
refactor: remove redundant comments and improve clarity in Advent sch…
wiktoriavh Nov 12, 2025
e3fe8f1
refactor: update Advent scheduler documentation to reflect time zone …
wiktoriavh Nov 12, 2025
34b9f0a
Merge branch 'main' into feat/daily-aoc-post
wiktoriavh Nov 12, 2025
3ff2ee1
Merge branch 'main' into feat/daily-aoc-post
wiktoriavh Nov 12, 2025
a06c6a8
feat: add Advent of Code channel and tracker path to test environment…
wiktoriavh Nov 12, 2025
ac64ff8
refactor: update file path handling in Advent scheduler tests
wiktoriavh Nov 12, 2025
80a47fd
fix: simplify error handling in loadTracker function by removing unus…
wiktoriavh Nov 12, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 21 additions & 19 deletions .env.example
Original file line number Diff line number Diff line change
@@ -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
# 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
2 changes: 2 additions & 0 deletions .env.production
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ 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
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)
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
17 changes: 17 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(',')
Expand All @@ -35,6 +36,7 @@ export const config = {
channelIds: {
repelLogs: requireEnv('REPEL_LOG_CHANNEL_ID'),
guides: requireEnv('GUIDES_CHANNEL_ID'),
adventOfCode: requireEnv('ADVENT_OF_CODE_CHANNEL_ID'),
},
};

Expand Down
8 changes: 8 additions & 0 deletions src/events/ready.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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);
}
}
);
157 changes: 157 additions & 0 deletions src/util/advent-scheduler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
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 = config.adventOfCodeTrackerPath;

type TrackerData = {
[year: string]: number[];
};

/**
* Load the tracker file to see which days have already been posted
*/
Comment thread
wiktoriavh marked this conversation as resolved.
Outdated
async function loadTracker(): Promise<TrackerData> {
try {
const data = await fs.readFile(TRACKER_FILE, 'utf-8');
return JSON.parse(data);
} catch (_error) {
Comment thread
wiktoriavh marked this conversation as resolved.
Outdated
// If file doesn't exist or can't be read, return empty object
return {};
}
}

/**
* Save the tracker file with updated data
*/
Comment thread
wiktoriavh marked this conversation as resolved.
Outdated
async function saveTracker(data: TrackerData): Promise<void> {
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
*/
Comment thread
wiktoriavh marked this conversation as resolved.
Outdated
async function isDayPosted(year: number, day: number): Promise<boolean> {
const tracker = await loadTracker();
const yearData = tracker[year.toString()];
return yearData ? yearData.includes(day) : false;
}

/**
* Mark a day as posted for a given year
*/
Comment thread
wiktoriavh marked this conversation as resolved.
Outdated
async function markDayAsPosted(year: number, day: number): Promise<void> {
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
*/
Comment thread
wiktoriavh marked this conversation as resolved.
Outdated
async function createAdventPost(
client: Client,
channelId: string,
year: number,
day: number
): Promise<boolean> {
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;
Comment thread
wiktoriavh marked this conversation as resolved.
Outdated
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<void> {
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 !== 11) {
return;
}

// Only run for days 1-25
if (day < 1 || day > 25) {
return;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this specific date range?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because advent of code runs till Christmas.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But isn't it 12 days only?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was always 25 days, this time it is only 12 puzzles. i don't know yet how it will be. i will update the job once i have more information, but for the first day this will run correctly.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's based on the advent calendar (24 or 25 days long), so it being only 12 is very weird. it could be that it only runs from day 1 to day 12, or it will run to day 24, where every other day a puzzle is posted? unclear to me yet.


// 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)
Comment thread
wiktoriavh marked this conversation as resolved.
Outdated
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);
});
});
Comment on lines +125 to +130
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  cron.schedule(
    '0 0 * * *',
    () => {
      console.log('⏰ Running scheduled Advent of Code check...');
      checkAndCreateTodaysPost(client, channelId).catch((error) => {
        console.error('❌ Error in scheduled Advent of Code check:', error);
      });
    },
    {
      timezone: 'America/New_York',
    }
  );

This way the expression is more understandable, it uses 0s (midnight) and has the timezone visible vs having to read in as UTC

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer to specifically have it run on UTC. It's the standard for the time.


console.log('✅ Advent of Code scheduler initialized (runs daily at midnight UTC)');
}
Loading