diff --git a/package.json b/package.json index a01b23c..2d0e170 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ }, "dependencies": { "@nhcarrigan/logger": "1.0.0", + "@retroachievements/api": "2.6.0", "discord.js": "14.22.0", "fastify": "5.5.0", "node-schedule": "2.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fe89fa1..b742312 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@nhcarrigan/logger': specifier: 1.0.0 version: 1.0.0 + '@retroachievements/api': + specifier: 2.6.0 + version: 2.6.0 discord.js: specifier: 14.22.0 version: 14.22.0 @@ -488,6 +491,10 @@ packages: resolution: {integrity: sha512-fdDH1LSGfZdTH2sxdpVMw31BanV28K/Gry0cVFxaNP77neJSkd82mM8ErPNYs9e+0O7SdHBLTDzDgwUuy18RnQ==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@retroachievements/api@2.6.0': + resolution: {integrity: sha512-ra6tSHYRJ1Mdm25GtlwQUAQLfrad32hOfBwYRHQo+Fv+vjeki3jEtXX/KNqFGZK+5DKH6S1rSgVoGBdXVbNzkA==} + engines: {node: '>=16'} + '@rollup/rollup-android-arm-eabi@4.46.3': resolution: {integrity: sha512-UmTdvXnLlqQNOCJnyksjPs1G4GqXNGW1LrzCe8+8QoaLhhDeTXYBgJ3k6x61WIhlHX2U+VzEJ55TtIjR/HTySA==} cpu: [arm] @@ -2819,6 +2826,8 @@ snapshots: '@pkgr/core@0.1.2': {} + '@retroachievements/api@2.6.0': {} + '@rollup/rollup-android-arm-eabi@4.46.3': optional: true diff --git a/prod.env b/prod.env index 92cb311..80d88a0 100644 --- a/prod.env +++ b/prod.env @@ -5,4 +5,5 @@ GH_CLIENT_SECRET="op://Environment Variables - Naomi/Amari/gh client secret" GH_PRIVATE_KEY="op://Environment Variables - Naomi/Amari/gh private key" GH_WEBHOOK_SECRET="op://Environment Variables - Naomi/Amari/gh webhook secret" BASEROW_SECRET="op://Environment Variables - Naomi/Amari/baserow hook auth" -BASEROW_TOKEN="op://Environment Variables - Naomi/Amari/baserow token" \ No newline at end of file +BASEROW_TOKEN="op://Environment Variables - Naomi/Amari/baserow token" +RA_KEY="op://Environment Variables - Naomi/Amari/retroachievements key" \ No newline at end of file diff --git a/src/config/ids.ts b/src/config/ids.ts index 027a813..d33c742 100644 --- a/src/config/ids.ts +++ b/src/config/ids.ts @@ -7,6 +7,7 @@ export const ids = { channels: { formSubmissions: "1410435042898874471", + gaming: "1385797656307175468", general: "1385797320389431336", menteeChat: "1400589073613062204", mentorshipGoalForum: "1400629118110011526", diff --git a/src/index.ts b/src/index.ts index 1722eba..6fc05ab 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,6 +14,7 @@ import { App } from "octokit"; import { ids } from "./config/ids.js"; import { handleMessageCreate } from "./events/handleMessageCreate.js"; import { cacheData } from "./modules/cacheData.js"; +import { checkRetroAchievements } from "./modules/checkAchievements.js"; import { logMenteeJoin } from "./modules/logMenteeJoin.js"; import { logMenteeLeave } from "./modules/logMenteeLeave.js"; import { @@ -72,6 +73,9 @@ amari.discord.once(Events.ClientReady, () => { setInterval(() => { amari.recentlyActiveChannels = new Set(); }, 10 * 60 * 1000); + setInterval(() => { + void checkRetroAchievements(amari); + }, 10 * 60 * 1000); }); amari.discord.on(Events.MessageCreate, (message) => { diff --git a/src/modules/checkAchievements.ts b/src/modules/checkAchievements.ts new file mode 100644 index 0000000..2819cda --- /dev/null +++ b/src/modules/checkAchievements.ts @@ -0,0 +1,118 @@ +/** + * @copyright NHCarrigan + * @license Naomi's Public License + * @author Naomi Carrigan + */ + +import { + buildAuthorization, + getUserRecentAchievements, + type UserRecentAchievement, +} from "@retroachievements/api"; +import { + MessageFlags, + TextDisplayBuilder, + ContainerBuilder, + SectionBuilder, + ThumbnailBuilder, + SeparatorBuilder, + SeparatorSpacingSize, + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + type MessageActionRowComponentBuilder, +} from "discord.js"; +import { ids } from "../config/ids.js"; +import type { Amari } from "../interfaces/amari.js"; + +const username = "naomilgbt"; +const webApiKey = process.env.RA_KEY; + +const constructComponents = ( + achievement: UserRecentAchievement, +): Array< + TextDisplayBuilder | + ContainerBuilder | + ActionRowBuilder> => { + return [ + new TextDisplayBuilder(). + setContent("Naomi has unlocked a new achievement!"), + new ContainerBuilder(). + setAccentColor(16_758_493). + addSectionComponents( + new SectionBuilder(). + setThumbnailAccessory( + new ThumbnailBuilder(). + setURL(`https://retroachievements.org/${achievement.badgeUrl}`), + ). + addTextDisplayComponents( + new TextDisplayBuilder().setContent(`# ${achievement.title}`), + new TextDisplayBuilder().setContent(achievement.description), + ), + ). + addSeparatorComponents( + new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small). + setDivider(true), + ). + addSectionComponents( + new SectionBuilder(). + setThumbnailAccessory( + new ThumbnailBuilder(). + setURL(`https://retroachievements.org${achievement.gameIcon}`), + ). + addTextDisplayComponents( + new TextDisplayBuilder().setContent( + `-# From ${achievement.gameTitle} on ${achievement.consoleName}`, + ), + new TextDisplayBuilder().setContent( + `-# Worth ${achievement.points.toString()} points!`, + ), + ), + ), + new ActionRowBuilder(). + addComponents( + new ButtonBuilder(). + setStyle(ButtonStyle.Link). + setLabel("See her profile"). + setURL("https://retroachievements.org/user/naomilgbt"), + ), + ]; +}; + +/** + * Checks for RA achievements earned in the last 10 minutes. + * If achievements are found, formats them and sends to Discord. + * @param amari - Amari's instance. + */ +export const checkRetroAchievements = async( + amari: Amari, +): Promise => { + if (webApiKey === undefined) { + return; + } + + const auth = buildAuthorization({ username, webApiKey }); + + const recentAchievements = await getUserRecentAchievements(auth, { + recentMinutes: 10, + username: username, + }); + + if (recentAchievements.length <= 0) { + return; + } + + const channel = amari.discord.channels.cache.get(ids.channels.gaming) + ?? await amari.discord.channels.fetch(ids.channels.gaming); + + if (channel?.isSendable() !== true) { + return; + } + + await Promise.all(recentAchievements.map(async(achievement) => { + await channel.send({ + components: constructComponents(achievement), + flags: [ MessageFlags.IsComponentsV2 ], + }); + })); +};