-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: AOC Leaderboard commands (#246)
* feat: add aoc key and leaderboard id field * feat: add command to set aoc settings * feat: create function to fetch AOC data After fetching, the schema will also sort the members before returning the result * feat: add util functions to get and save leaderboard * feat: add command to fetch leaderboard * feat: add db migration for aoc models and settings * fix: remove filtering out 0 points user * test: use fake leaderboard id, guild id and session key in tests
- Loading branch information
Showing
16 changed files
with
772 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
16 changes: 16 additions & 0 deletions
16
prisma/migrations/20241214153245_aoc_models_and_settings/migration.sql
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
-- AlterTable | ||
ALTER TABLE "ServerChannelsSettings" ADD COLUMN "aocKey" TEXT, | ||
ADD COLUMN "aocLeaderboardId" TEXT; | ||
|
||
-- CreateTable | ||
CREATE TABLE "AocLeaderboard" ( | ||
"guildId" TEXT NOT NULL, | ||
"result" JSONB NOT NULL, | ||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP | ||
); | ||
|
||
-- CreateIndex | ||
CREATE UNIQUE INDEX "AocLeaderboard_guildId_key" ON "AocLeaderboard"("guildId"); | ||
|
||
-- CreateIndex | ||
CREATE INDEX "AocLeaderboard_guildId_idx" ON "AocLeaderboard"("guildId"); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,18 @@ | ||
import { PrismaClient } from '@prisma/client'; | ||
import type { AocLeaderboard } from '../slash-commands/aoc-leaderboard/schema'; | ||
|
||
const prisma: PrismaClient = new PrismaClient(); | ||
declare global { | ||
namespace PrismaJson { | ||
type AocLeaderboardData = AocLeaderboard; | ||
} | ||
} | ||
|
||
const prisma = new PrismaClient({ | ||
omit: { | ||
serverChannelsSettings: { | ||
aocKey: true, | ||
}, | ||
}, | ||
}); | ||
|
||
export const getDbClient = () => prisma; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import wretch from 'wretch'; | ||
import { logger } from '../../utils/logger'; | ||
import { AocLeaderboard } from './schema'; | ||
|
||
function getAocClient(aocKey: string) { | ||
return wretch('https://adventofcode.com') | ||
.options({ credentials: 'same-origin' }) | ||
.headers({ | ||
Cookie: `session=${aocKey}`, | ||
'User-Agent': 'https://github.com/viet-aus-it/discord-bot by [email protected]', | ||
}); | ||
} | ||
|
||
export async function fetchLeaderboard(aocKey: string, leaderboardId: string, year: number) { | ||
const result = await getAocClient(aocKey).url(`/${year}/leaderboard/private/view/${leaderboardId}.json`).get().json(); | ||
|
||
const parsedResult = AocLeaderboard.safeParse(result); | ||
if (!parsedResult.success) { | ||
logger.error('ERROR: Cannot get leaderboard format.', parsedResult.error); | ||
throw new Error(parsedResult.error.stack); | ||
} | ||
|
||
return parsedResult.data; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
import { faker } from '@faker-js/faker'; | ||
import { subHours } from 'date-fns'; | ||
import type { ChatInputCommandInteraction } from 'discord.js'; | ||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; | ||
import { mockDeep, mockReset } from 'vitest-mock-extended'; | ||
import { execute, formatLeaderboard, getAocYear } from '.'; | ||
import mockAocData from './sample/aoc-data.json'; | ||
import { AocLeaderboard } from './schema'; | ||
import { fetchAndSaveLeaderboard, getAocSettings, getSavedLeaderboard } from './utils'; | ||
|
||
vi.mock('./utils'); | ||
const mockGetSavedLeaderboard = vi.mocked(getSavedLeaderboard); | ||
const mockGetAocSettings = vi.mocked(getAocSettings); | ||
const mockFetchAndSaveLeaderboard = vi.mocked(fetchAndSaveLeaderboard); | ||
const mockInteraction = mockDeep<ChatInputCommandInteraction>(); | ||
const parsedMockData = AocLeaderboard.parse(mockAocData); | ||
const mockKey = faker.string.alphanumeric({ length: 127 }); | ||
const mockLeaderboardId = faker.string.alphanumeric(); | ||
const mockGuildId = faker.string.numeric(); | ||
|
||
const mockSystemTime = new Date(2024, 11, 25, 16, 0, 0); // 25/12/2024 16:00:00 | ||
const oneHourEarlier = subHours(mockSystemTime, 1); // 25/12/2024 15:00:00 | ||
|
||
describe('Get AOC Leaderboard test', () => { | ||
beforeEach(() => { | ||
vi.useFakeTimers(); | ||
vi.setSystemTime(mockSystemTime); | ||
mockReset(mockInteraction); | ||
}); | ||
|
||
afterEach(() => { | ||
vi.useRealTimers(); | ||
}); | ||
|
||
describe('Get AOC Year', () => { | ||
it('Should return this year leaderboard if it is December', () => { | ||
const date = new Date(2024, 11, 1); | ||
vi.setSystemTime(date); | ||
const year = getAocYear(); | ||
expect(year).toEqual(2024); | ||
}); | ||
|
||
it('Should return previous year leaderboard if it is not December', () => { | ||
const date = new Date(2024, 1, 1); | ||
vi.setSystemTime(date); | ||
const year = getAocYear(); | ||
expect(year).toEqual(2023); | ||
}); | ||
}); | ||
|
||
describe('Format leaderboard', () => { | ||
it('Should match the leaderboard format', () => { | ||
const leaderboardMessage = formatLeaderboard({ | ||
result: parsedMockData, | ||
updatedAt: mockSystemTime, | ||
}); | ||
expect(leaderboardMessage).toEqual(`\`\`\` | ||
# name score | ||
1 (anonymous user 4) 474 | ||
2 user2 361 | ||
3 user3 353 | ||
4 user1 0 | ||
Last updated at: 25/12/2024 16:00 | ||
\`\`\``); | ||
}); | ||
}); | ||
|
||
describe('Command tests', () => { | ||
it('Should reply with saved leaderboard if it can get one', async () => { | ||
mockGetSavedLeaderboard.mockResolvedValueOnce({ | ||
result: parsedMockData, | ||
updatedAt: mockSystemTime, | ||
}); | ||
|
||
await execute(mockInteraction); | ||
|
||
expect(mockInteraction.editReply).toHaveBeenCalledWith(`\`\`\` | ||
# name score | ||
1 (anonymous user 4) 474 | ||
2 user2 361 | ||
3 user3 353 | ||
4 user1 0 | ||
Last updated at: 25/12/2024 16:00 | ||
\`\`\``); | ||
}); | ||
|
||
it('Should reply with error if it errors out while finding aoc settings', async () => { | ||
mockGetSavedLeaderboard.mockResolvedValueOnce({ | ||
result: parsedMockData, | ||
updatedAt: oneHourEarlier, | ||
}); | ||
mockGetAocSettings.mockRejectedValueOnce(new Error('Synthetic Error Get Settings')); | ||
|
||
await execute(mockInteraction); | ||
|
||
expect(mockInteraction.editReply).toHaveBeenCalledWith('ERROR: Error: Synthetic Error Get Settings'); | ||
}); | ||
|
||
it('Should reply with error if server is not configured', async () => { | ||
mockGetSavedLeaderboard.mockResolvedValueOnce({ | ||
result: parsedMockData, | ||
updatedAt: oneHourEarlier, | ||
}); | ||
mockGetAocSettings.mockResolvedValueOnce(null); | ||
|
||
await execute(mockInteraction); | ||
|
||
expect(mockInteraction.editReply).toHaveBeenCalledWith('ERROR: Server is not configured to get AOC results! Missing Key and/or Leaderboard ID.'); | ||
}); | ||
|
||
it('Should reply with error if there is one during fetching and saving', async () => { | ||
mockGetSavedLeaderboard.mockResolvedValueOnce({ | ||
result: parsedMockData, | ||
updatedAt: oneHourEarlier, | ||
}); | ||
mockGetAocSettings.mockResolvedValueOnce({ | ||
guildId: mockGuildId, | ||
aocKey: mockKey, | ||
aocLeaderboardId: mockLeaderboardId, | ||
}); | ||
mockFetchAndSaveLeaderboard.mockRejectedValueOnce(new Error('Synthetic error fetch and save')); | ||
|
||
await execute(mockInteraction); | ||
|
||
expect(mockInteraction.editReply).toHaveBeenCalledWith( | ||
'ERROR: Error fetching and/or saving new leaderboard result: Error: Synthetic error fetch and save' | ||
); | ||
}); | ||
|
||
it('Should reply with newly fetched leaderboard after fetching and saving', async () => { | ||
mockGetSavedLeaderboard.mockResolvedValueOnce({ | ||
result: parsedMockData, | ||
updatedAt: oneHourEarlier, | ||
}); | ||
mockGetAocSettings.mockResolvedValueOnce({ | ||
guildId: mockGuildId, | ||
aocKey: mockKey, | ||
aocLeaderboardId: mockLeaderboardId, | ||
}); | ||
mockFetchAndSaveLeaderboard.mockResolvedValueOnce({ | ||
result: parsedMockData, | ||
updatedAt: mockSystemTime, | ||
}); | ||
|
||
await execute(mockInteraction); | ||
|
||
expect(mockInteraction.editReply).toHaveBeenCalledWith(`\`\`\` | ||
# name score | ||
1 (anonymous user 4) 474 | ||
2 user2 361 | ||
3 user3 353 | ||
4 user1 0 | ||
Last updated at: 25/12/2024 16:00 | ||
\`\`\``); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.