diff --git a/.github/workflows/deploy-sigpwny.yml b/.github/workflows/deploy-sigpwny.yml index 1c2cb6260..8ea4ea532 100644 --- a/.github/workflows/deploy-sigpwny.yml +++ b/.github/workflows/deploy-sigpwny.yml @@ -59,4 +59,10 @@ jobs: working-directory: ${{ env.CI_WORKING_DIR }} env: DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }} - DISCORD_SERVER_ID: ${{ secrets.DISCORD_SERVER_ID }} \ No newline at end of file + DISCORD_SERVER_ID: ${{ secrets.DISCORD_SERVER_ID }} + + - name: (On main) Schedule Discord pings + if: github.ref_name == github.event.repository.default_branch + run: npm run schedule-discord-workflow + shell: bash + working-directory: ${{ env.CI_WORKING_DIR }} \ No newline at end of file diff --git a/sigpwny.com/package-lock.json b/sigpwny.com/package-lock.json index 2ba67e4bc..7a7b5cbd9 100644 --- a/sigpwny.com/package-lock.json +++ b/sigpwny.com/package-lock.json @@ -17,6 +17,7 @@ "@astrojs/tailwind": "^5.1.0", "@floating-ui/dom": "^1.6.8", "@floating-ui/react": "^0.26.20", + "@reteps/github-action-scheduler": "^1.0.0", "@types/react": "^18.2.79", "@types/react-dom": "^18.2.25", "astro": "^4.14.3", @@ -1869,6 +1870,19 @@ "node": ">=14" } }, + "node_modules/@reteps/github-action-scheduler": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@reteps/github-action-scheduler/-/github-action-scheduler-1.0.0.tgz", + "integrity": "sha512-6pJbdDeTPYG4cHXWgmzYq5q/5tHS9misRSDch9mM/tcNghSHB6NGQuXr/o34hDMZDfLsozZJq1GXBni8CLZ/DQ==", + "license": "ISC", + "dependencies": { + "@types/js-yaml": "^4.0.9", + "@types/node": "^22.7.4", + "js-yaml": "^4.1.0", + "slugify": "^1.6.6", + "typescript": "^5.6.2" + } + }, "node_modules/@rollup/pluginutils": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", @@ -2224,6 +2238,12 @@ "@types/unist": "*" } }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "license": "MIT" + }, "node_modules/@types/katex": { "version": "0.16.7", "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.7.tgz", @@ -2267,9 +2287,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.5.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.0.tgz", - "integrity": "sha512-DkFrJOe+rfdHTqqMg0bSNlGlQ85hSoh2TPzZyhHsXnMtligRWpxUySiyw8FY14ITt24HVCiQPWxS3KO/QlGmWg==", + "version": "22.7.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.4.tgz", + "integrity": "sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==", "license": "MIT", "dependencies": { "undici-types": "~6.19.2" @@ -9080,6 +9100,15 @@ "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==", "license": "MIT" }, + "node_modules/slugify": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz", + "integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/source-map": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", @@ -9621,9 +9650,9 @@ "license": "MIT" }, "node_modules/typescript": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", - "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", + "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/sigpwny.com/package.json b/sigpwny.com/package.json index 5c9126f4a..7fb0e3e6c 100644 --- a/sigpwny.com/package.json +++ b/sigpwny.com/package.json @@ -29,6 +29,7 @@ "@astrojs/tailwind": "^5.1.0", "@floating-ui/dom": "^1.6.8", "@floating-ui/react": "^0.26.20", + "@reteps/github-action-scheduler": "^1.0.0", "@types/react": "^18.2.79", "@types/react-dom": "^18.2.25", "astro": "^4.14.3", diff --git a/sigpwny.com/src/scripts/schedule-discord-pings.ts b/sigpwny.com/src/scripts/schedule-discord-pings.ts new file mode 100644 index 000000000..d957d9295 --- /dev/null +++ b/sigpwny.com/src/scripts/schedule-discord-pings.ts @@ -0,0 +1,113 @@ +import { scheduleJobs, Job } from '@reteps/github-action-scheduler'; +import fs from 'fs'; +import path from 'path'; +import { Client, GatewayIntentBits, Events, type GuildScheduledEventCreateOptions, GuildScheduledEventEntityType, GuildScheduledEventPrivacyLevel, GuildScheduledEvent, type GuildScheduledEventEditOptions, GuildScheduledEventStatus } from 'discord.js'; +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; +import timezone from 'dayjs/plugin/timezone'; +import duration from 'dayjs/plugin/duration'; +import advanced from 'dayjs/plugin/advancedFormat'; +import { meetingMetadata } from '../utils/meetingMetadata'; +dayjs.extend(utc); +dayjs.extend(timezone); +dayjs.extend(duration); +dayjs.extend(advanced); + + +const fetchMeetings = async () => { + const meetingsPath = path.join(__dirname, '..', '..', 'dist', 'meetings', 'all.json'); + if (fs.existsSync(meetingsPath)) { + const data = fs.readFileSync(meetingsPath, 'utf8'); + return JSON.parse(data); + } +} + +const makeJob = (meeting: any, beforeDuration) => { + const { data : { title, type, location, card_image, week_number, time_end, time_start, description }, body, filePath, slug } = meeting; + + /* +runs-on: ubuntu-latest +steps: + - uses: actions/checkout@v4 + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: latest + - run: npm ci + working-directory: sigpwny.com + - run: npm run send-discord-ping + working-directory: sigpwny.com + */ + + const url = `https://sigpwny.com${slug}`; + + const formattedDuration = dayjs.duration(beforeDuration).format('D [days] H [hours] m [minutes]'); + const message = `**${title}** is in ${formattedDuration}! + ${url} + `; + + const runAt = time_start.subtract(beforeDuration); + const job: Job = { + date: runAt.toDate(), + name: `helper ping ${title}`, + 'runs-on': 'ubuntu-latest', + steps: [ + { + uses: 'actions/checkout@v4', + }, + { + name: 'Use Node.js', + uses: 'actions/setup-node@v4', + with: { + 'node-version': 'latest', + }, + }, + { + run: 'npm ci', + 'working-directory': 'sigpwny.com', + }, + { + run: 'npm run send-discord-ping', + 'working-directory': 'sigpwny.com', + env: { + DISCORD_TOKEN: '${{ secrets.DISCORD_TOKEN }}', + DISCORD_CHANNEL_ID: '${{ vars.DISCORD_CONTENT_CHANNEL_ID }}', + DISCORD_SERVER_ID: '${{ secrets.DISCORD_SERVER_ID }}', + DISCORD_B64_MESSAGE: Buffer.from(message).toString('base64'), + } + }, + ], + }; + return job; +} + +async function main() { + const meetings = await fetchMeetings(); + const upcomingMeetings = meetings.map((meeting : any) => { + return { + ...meeting, + data: { + ...meeting.data, + time_start: dayjs(meeting.data.time_start).tz(meeting.data.timezone), + time_end: dayjs(meeting.data.time_start).tz(meeting.data.timezone).add(dayjs.duration(meeting.data.duration)), + } + } + }).filter((meeting: any) => meeting.data.time_start > dayjs()); + + const pingNotice = [ + dayjs.duration({ days: 1 }), + dayjs.duration({ hours: 1 }) + ] + + const jobs = upcomingMeetings.flatMap((meeting) => { + return pingNotice.map((notice) => makeJob(meeting, notice)); + }) + + scheduleJobs(jobs, { + path: path.join(__dirname, '..', '..', '..', 'scheduled-pings.yml'), + check: false, + replace: true + }); +} + +main(); \ No newline at end of file