From 6c9942a98451de4cfc27e67bd94dc5ef1d1b6d44 Mon Sep 17 00:00:00 2001 From: PxlLoewe <72106766+PxlLoewe@users.noreply.github.com> Date: Thu, 5 Jun 2025 01:03:13 -0700 Subject: [PATCH] added discord container for renaming and role-management --- .env.prod | 5 + apps/discord-server/.d.ts | 19 ++ apps/discord-server/.dockerignore | 6 + apps/discord-server/.env.example | 7 + apps/discord-server/Dockerfile | 50 ++++ apps/discord-server/index.ts | 19 ++ apps/discord-server/modules/chron.ts | 62 +++++ apps/discord-server/modules/discord.ts | 19 ++ apps/discord-server/nodemon.json | 5 + apps/discord-server/package.json | 35 +++ apps/discord-server/routes/member.ts | 69 ++++++ apps/discord-server/routes/router.ts | 8 + apps/discord-server/tsconfig.json | 11 + apps/dispatch-server/.env.example | 1 + apps/dispatch-server/modules/discord.ts | 38 +++ .../socket-events/connect-dispatch.ts | 26 +++ .../socket-events/connect-pilot.ts | 27 ++- apps/hub-server/modules/chron.ts | 89 ++++++- apps/hub-server/modules/discord.ts | 38 +++ apps/hub-server/modules/event.ts | 7 +- apps/hub/app/api/discord-redirect/route.ts | 4 +- apps/hub/helper/discord.ts | 39 ++++ docker-compose.prod.yml | 41 ++-- packages/database/prisma/json/User.ts | 7 + packages/database/prisma/schema/event.prisma | 1 + pnpm-lock.yaml | 219 ++++++++++++++++++ 26 files changed, 824 insertions(+), 28 deletions(-) create mode 100644 apps/discord-server/.d.ts create mode 100644 apps/discord-server/.dockerignore create mode 100644 apps/discord-server/.env.example create mode 100644 apps/discord-server/Dockerfile create mode 100644 apps/discord-server/index.ts create mode 100644 apps/discord-server/modules/chron.ts create mode 100644 apps/discord-server/modules/discord.ts create mode 100644 apps/discord-server/nodemon.json create mode 100644 apps/discord-server/package.json create mode 100644 apps/discord-server/routes/member.ts create mode 100644 apps/discord-server/routes/router.ts create mode 100644 apps/discord-server/tsconfig.json create mode 100644 apps/dispatch-server/modules/discord.ts create mode 100644 apps/hub-server/modules/discord.ts create mode 100644 apps/hub/helper/discord.ts diff --git a/.env.prod b/.env.prod index 9360d8d0..95155f1f 100644 --- a/.env.prod +++ b/.env.prod @@ -19,6 +19,7 @@ NEXT_PUBLIC_HUB_URL=https://hub.premiumag.de NEXT_PUBLIC_HUB_SERVER_URL=https://api.hub.premiumag.de NEXT_PUBLIC_DISPATCH_URL=https://dispatch.premiumag.de NEXT_PUBLIC_DISPATCH_SERVER_URL=https://api.dispatch.premiumag.de +DISCORD_SERVER_URL=http://discord-server NEXT_PUBLIC_ESRI_ACCESS_TOKEN= @@ -50,6 +51,7 @@ REDIS_PORT=6379 # ─────────────────────────────────────────────── HUB_SERVER_PORT=3000 +DISCORD_SERVER_PORT=3005 # ─────────────────────────────────────────────── # 📚 Moodle # ─────────────────────────────────────────────── @@ -58,6 +60,7 @@ MOODLE_API_TOKEN=ac346f0324647b68488d13fd52a9bbe8 MOODLE_USER_PASSWORD=var-api-user-P1 NEXT_PUBLIC_MOODLE_URL=https://02.premiumag.de:8081 + # ─────────────────────────────────────────────── # 📧 E-Mail Einstellungen (nur HUB Server) # ─────────────────────────────────────────────── @@ -70,6 +73,8 @@ MAIL_PASSWORD=b7316PB8aDPCC%-& # 🕹️ Discord OAuth (optional) # ─────────────────────────────────────────────── +DISCORD_GUILD_ID=1077269395019141140 + DISCORD_OAUTH_CLIENT_ID=930384053344034846 DISCORD_OAUTH_SECRET=96aSvmIePqFTbGc54mad0QsZfDnYwhl1 DISCORD_BOT_TOKEN=OTMwMzg0MDUzMzQ0MDM0ODQ2.G7zIy-._hE3dTbtUv6sd7nIP2PUn3d8s-2MFk0x3nYMg8 diff --git a/apps/discord-server/.d.ts b/apps/discord-server/.d.ts new file mode 100644 index 00000000..93743eea --- /dev/null +++ b/apps/discord-server/.d.ts @@ -0,0 +1,19 @@ +declare module "next-auth/jwt" { + interface JWT { + uid: string; + firstname: string; + lastname: string; + email: string; + } +} +declare module "cookie-parser"; + +import type { User } from "@repo/db"; + +declare global { + namespace Express { + interface Request { + user?: User | null; + } + } +} diff --git a/apps/discord-server/.dockerignore b/apps/discord-server/.dockerignore new file mode 100644 index 00000000..7a533e48 --- /dev/null +++ b/apps/discord-server/.dockerignore @@ -0,0 +1,6 @@ +node_modules +Dockerfile +.dockerignore +nodemon.json +.env +.env.example \ No newline at end of file diff --git a/apps/discord-server/.env.example b/apps/discord-server/.env.example new file mode 100644 index 00000000..e53b08af --- /dev/null +++ b/apps/discord-server/.env.example @@ -0,0 +1,7 @@ +DISCORD_SERVER_PORT=3005 +DISCORD_GUILD_ID=1077269395019141140 +DISCORD_OAUTH_CLIENT_ID=930384053344034846 +DISCORD_OAUTH_SECRET=96aSvmIePqFTbGc54mad0QsZfDnYwhl1 +DISCORD_BOT_TOKEN=OTMwMzg0MDUzMzQ0MDM0ODQ2.G7zIy-._hE3dTbtUv6sd7nIP2PUn3d8s-2MFk0x3nYMg8 +DISCORD_REDIRECT_URL=https://hub.premiumag.de/api/discord-redirect +NEXT_PUBLIC_DISCORD_URL=https://discord.com/oauth2/authorize?client_id=930384053344034846&response_type=code&redirect_uri=https%3A%2F%2Fhub.premiumag.de%2Fapi%2Fdiscord-redirect&scope=identify+guilds+email \ No newline at end of file diff --git a/apps/discord-server/Dockerfile b/apps/discord-server/Dockerfile new file mode 100644 index 00000000..c8f26d91 --- /dev/null +++ b/apps/discord-server/Dockerfile @@ -0,0 +1,50 @@ +FROM node:22-alpine AS base + +ENV PNPM_HOME="/usr/local/pnpm" +ENV PATH="${PNPM_HOME}:${PATH}" +RUN corepack enable && corepack prepare pnpm@latest --activate + +RUN pnpm add -g turbo@^2.5 + +FROM base AS builder +RUN apk update +RUN apk add --no-cache libc6-compat + +WORKDIR /usr/app + +COPY . . + +RUN ls -lh + +RUN turbo prune discord-server --docker + +FROM base AS installer +RUN apk update +RUN apk add --no-cache libc6-compat + +WORKDIR /usr/app + +COPY --from=builder /usr/app/out/json/ . +RUN pnpm install + +# Build the project +COPY --from=builder /usr/app/out/full/ . + +RUN turbo run build + +FROM base AS runner +WORKDIR /usr/app + +# Don't run production as root +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs +USER nextjs + +# Automatically leverage output traces to reduce image size +# https://nextjs.org/docs/advanced-features/output-file-tracing +COPY --from=installer --chown=nextjs:nodejs /usr/app/ ./ + +# Expose the application port +EXPOSE 3003 + +CMD ["pnpm", "--dir", "apps/discord-server", "run", "start"] \ No newline at end of file diff --git a/apps/discord-server/index.ts b/apps/discord-server/index.ts new file mode 100644 index 00000000..3c10d455 --- /dev/null +++ b/apps/discord-server/index.ts @@ -0,0 +1,19 @@ +import "dotenv/config"; +import express from "express"; +import { createServer } from "http"; +import router from "routes/router"; +import cookieParser from "cookie-parser"; +import cors from "cors"; +import "modules/chron"; + +const app = express(); +const server = createServer(app); + +app.use(cors()); +app.use(express.json()); +app.use(cookieParser()); +app.use(router); + +server.listen(process.env.DISCORD_SERVER_PORT, () => { + console.log(`Server running on port ${process.env.DISCORD_SERVER_PORT}`); +}); diff --git a/apps/discord-server/modules/chron.ts b/apps/discord-server/modules/chron.ts new file mode 100644 index 00000000..f7747c0b --- /dev/null +++ b/apps/discord-server/modules/chron.ts @@ -0,0 +1,62 @@ +import { MissionLog, prisma } from "@repo/db"; +import cron from "node-cron"; + +const removeClosedMissions = async () => { + const oldMissions = await prisma.mission.findMany({ + where: { + state: "running", + }, + }); + oldMissions.forEach(async (mission) => { + const lastAlert = (mission.missionLog as unknown as MissionLog[]).find((l) => { + return l.type === "alert-log"; + }); + const lastAlertTime = lastAlert ? new Date(lastAlert.timeStamp) : null; + + const aircraftsInMission = await prisma.connectedAircraft.findMany({ + where: { + stationId: { + in: mission.missionStationIds, + }, + }, + }); + + if ( + !aircraftsInMission || + !aircraftsInMission.some((a) => ["1", "2", "6"].includes(a.fmsStatus)) + ) + return; + + const now = new Date(); + if (!lastAlertTime) return; + // change State to closed if last alert was more than 180 minutes ago + if (now.getTime() - lastAlertTime.getTime() < 30 * 60 * 1000) return; + const log: MissionLog = { + type: "completed-log", + auto: true, + timeStamp: new Date().toISOString(), + data: {}, + }; + + await prisma.mission.update({ + where: { + id: mission.id, + }, + data: { + state: "finished", + missionLog: { + push: log as any, + }, + }, + }); + console.log(`Mission ${mission.id} closed due to inactivity.`); + }); +}; + +cron.schedule("*/5 * * * *", async () => { + try { + await removeClosedMissions(); + } catch (error) { + console.error("Error removing closed missions:", error); + } +}); diff --git a/apps/discord-server/modules/discord.ts b/apps/discord-server/modules/discord.ts new file mode 100644 index 00000000..2445a20f --- /dev/null +++ b/apps/discord-server/modules/discord.ts @@ -0,0 +1,19 @@ +import { Client, GatewayIntentBits } from "discord.js"; + +const client = new Client({ + intents: [GatewayIntentBits.Guilds], +}); + +const token = process.env.DISCORD_BOT_TOKEN; + +if (!token) { + throw new Error("DISCORD_BOT_TOKEN environment variable is not set."); +} + +client.login(token); + +client.on("ready", () => { + console.log(`Logged in as ${client.user?.tag}`); +}); + +export default client; diff --git a/apps/discord-server/nodemon.json b/apps/discord-server/nodemon.json new file mode 100644 index 00000000..8bb27568 --- /dev/null +++ b/apps/discord-server/nodemon.json @@ -0,0 +1,5 @@ +{ + "watch": ["."], + "ext": "ts", + "exec": "tsx index.ts" +} \ No newline at end of file diff --git a/apps/discord-server/package.json b/apps/discord-server/package.json new file mode 100644 index 00000000..ff85adaf --- /dev/null +++ b/apps/discord-server/package.json @@ -0,0 +1,35 @@ +{ + "name": "discord-server", + "exports": { + "helpers": "./helper" + }, + "scripts": { + "dev": "nodemon --signal SIGINT", + "start": "tsx index.ts --transpile-only", + "build": "tsc" + }, + "packageManager": "pnpm@10.11.0", + "devDependencies": { + "@repo/db": "workspace:*", + "@repo/typescript-config": "workspace:*", + "@types/cookie-parser": "^1.4.8", + "@types/cors": "^2.8.18", + "@types/express": "^5.0.2", + "@types/node": "^22.15.29", + "@types/nodemailer": "^6.4.17", + "concurrently": "^9.1.2", + "typescript": "latest" + }, + "dependencies": { + "axios": "^1.9.0", + "cors": "^2.8.5", + "cron": "^4.3.1", + "discord.js": "^14.19.3", + "dotenv": "^16.5.0", + "express": "^5.1.0", + "node-cron": "^4.1.0", + "nodemon": "^3.1.10", + "react": "^19.1.0", + "tsx": "^4.19.4" + } +} diff --git a/apps/discord-server/routes/member.ts b/apps/discord-server/routes/member.ts new file mode 100644 index 00000000..4ad269b5 --- /dev/null +++ b/apps/discord-server/routes/member.ts @@ -0,0 +1,69 @@ +import { Router, Request, Response } from "express"; +import client from "modules/discord"; + +const GUILD_ID = process.env.DISCORD_GUILD_ID; +if (!GUILD_ID) { + throw new Error("DISCORD_GUILD_ID environment variable is not set."); +} + +const router: Router = Router(); + +const getMember = async (memberId: string) => { + const guild = client.guilds.cache.get(GUILD_ID); + if (!guild) throw new Error("Guild not found"); + try { + return guild.members.cache.get(memberId) ?? (await guild.members.fetch(memberId)); + } catch (error) { + console.error("Error fetching member:", error); + throw new Error("Member not found"); + } +}; + +router.post("/rename", async (req: Request, res: Response) => { + const { newName, memberId } = req.body; + if (typeof newName !== "string" || !memberId) { + res.status(400).json({ error: "Invalid or missing newName or memberId" }); + return; + } + try { + const member = await getMember(memberId); + await member.setNickname(newName); + console.log(`Member ${member.id} renamed to ${newName}`); + res.status(200).json({ message: "Member renamed successfully" }); + } catch (error) { + console.error("Error renaming member:", error); + res.status(500).json({ error: "Failed to rename member" }); + } +}); + +const handleRoleChange = (action: "add" | "remove") => async (req: Request, res: Response) => { + const { roleIds, memberId } = req.body; + if (!Array.isArray(roleIds) || !memberId) { + res.status(400).json({ error: "Invalid or missing roleIds or memberId" }); + return; + } + try { + const member = await getMember(memberId); + + const currentRoleIds = member.roles.cache.map((role) => role.id); + const filteredRoleIds = + action === "add" + ? roleIds.filter((id: string) => !currentRoleIds.includes(id)) + : roleIds.filter((id: string) => currentRoleIds.includes(id)); + if (filteredRoleIds.length === 0) { + res.status(200).json({ message: `No roles to ${action}` }); + return; + } + + await member.roles[action](roleIds); + res.status(200).json({ message: `Roles ${action}ed successfully` }); + } catch (error) { + console.error(`Error ${action}ing roles:`, error); + res.status(500).json({ error: `Failed to ${action} roles` }); + } +}; + +router.post("/add-role", handleRoleChange("add")); +router.post("/remove-role", handleRoleChange("remove")); + +export default router; diff --git a/apps/discord-server/routes/router.ts b/apps/discord-server/routes/router.ts new file mode 100644 index 00000000..f65c1b0a --- /dev/null +++ b/apps/discord-server/routes/router.ts @@ -0,0 +1,8 @@ +import { Router } from "express"; +import memberRouter from "./member"; + +const router: Router = Router(); + +router.use("/member", memberRouter); + +export default router; diff --git a/apps/discord-server/tsconfig.json b/apps/discord-server/tsconfig.json new file mode 100644 index 00000000..f3c1bbd3 --- /dev/null +++ b/apps/discord-server/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "@repo/typescript-config/base.json", + "compilerOptions": { + "outDir": "dist", + "allowImportingTsExtensions": false, + "baseUrl": ".", + "jsx": "react" + }, + "include": ["**/*.ts", "./index.ts", "**/*.d.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/apps/dispatch-server/.env.example b/apps/dispatch-server/.env.example index 2fed12b1..d117b33b 100644 --- a/apps/dispatch-server/.env.example +++ b/apps/dispatch-server/.env.example @@ -1,6 +1,7 @@ DISPATCH_SERVER_PORT=3002 REDIS_HOST=localhost REDIS_PORT=6379 +DISCORD_SERVER_URL=http://discord-server DISPATCH_APP_TOKEN=dispatch LIVEKIT_API_KEY=APIAnsGdtdYp2Ho LIVEKIT_API_SECRET=tdPjVsYUx8ddC7K9NvdmVAeLRF9GeADD6Fedm1x63fWC \ No newline at end of file diff --git a/apps/dispatch-server/modules/discord.ts b/apps/dispatch-server/modules/discord.ts new file mode 100644 index 00000000..94317358 --- /dev/null +++ b/apps/dispatch-server/modules/discord.ts @@ -0,0 +1,38 @@ +import axios from "axios"; + +const discordAxiosClient = axios.create({ + baseURL: process.env.DISCORD_SERVER_URL || "https://discord.com/api/v10", +}); + +export const renameMember = async (memberId: string, newName: string) => { + discordAxiosClient + .post("/member/rename", { + memberId, + newName, + }) + .catch((error) => { + console.error("Error renaming member:", error); + }); +}; + +export const addRolesToMember = async (memberId: string, roleIds: string[]) => { + discordAxiosClient + .post("/member/add-role", { + memberId, + roleIds, + }) + .catch((error) => { + console.error("Error adding roles to member:", error); + }); +}; + +export const removeRolesFromMember = async (memberId: string, roleIds: string[]) => { + discordAxiosClient + .post("/member/remove-role", { + memberId, + roleIds, + }) + .catch((error) => { + console.error("Error removing roles from member:", error); + }); +}; diff --git a/apps/dispatch-server/socket-events/connect-dispatch.ts b/apps/dispatch-server/socket-events/connect-dispatch.ts index 4a4248df..c6494c22 100644 --- a/apps/dispatch-server/socket-events/connect-dispatch.ts +++ b/apps/dispatch-server/socket-events/connect-dispatch.ts @@ -1,4 +1,6 @@ import { getPublicUser, prisma, User } from "@repo/db"; +import { addRolesToMember, removeRolesFromMember, renameMember } from "modules/discord"; +import { DISCORD_ROLES } from "@repo/db"; import { Server, Socket } from "socket.io"; export const handleConnectDispatch = @@ -67,6 +69,21 @@ export const handleConnectDispatch = }, }); + const discordAccount = await prisma.discordAccount.findFirst({ + where: { + userId: user.id, + }, + }); + if (discordAccount?.id) { + await renameMember( + discordAccount.discordId.toString(), + `${getPublicUser(user).fullName} • ${selectedZone}`, + ); + await addRolesToMember(discordAccount.discordId.toString(), [ + DISCORD_ROLES.ONLINE_DISPATCHER, + ]); + } + socket.join("dispatchers"); // Dem Dispatcher-Raum beitreten socket.join(`user:${user.id}`); // Dem User-Raum beitreten @@ -85,6 +102,15 @@ export const handleConnectDispatch = }); io.to("dispatchers").emit("dispatchers-update"); io.to("pilots").emit("dispatchers-update"); + if (discordAccount?.id) { + await renameMember( + discordAccount.discordId.toString(), + `${getPublicUser(user).fullName} - ${user.publicId}`, + ); + await removeRolesFromMember(discordAccount.discordId.toString(), [ + DISCORD_ROLES.ONLINE_DISPATCHER, + ]); + } }); } catch (error) { console.error("Error connecting to dispatch server:", error); diff --git a/apps/dispatch-server/socket-events/connect-pilot.ts b/apps/dispatch-server/socket-events/connect-pilot.ts index cb077970..ca327b1e 100644 --- a/apps/dispatch-server/socket-events/connect-pilot.ts +++ b/apps/dispatch-server/socket-events/connect-pilot.ts @@ -1,5 +1,6 @@ import { getPublicUser, prisma, User } from "@repo/db"; -import { channel } from "diagnostics_channel"; +import { addRolesToMember, removeRolesFromMember, renameMember } from "modules/discord"; +import { DISCORD_ROLES } from "@repo/db"; import { Server, Socket } from "socket.io"; export const handleConnectPilot = @@ -18,7 +19,6 @@ export const handleConnectPilot = if (!user) return Error("User not found"); - console.log("Pilot connected:", user.publicId); const existingConnection = await prisma.connectedAircraft.findFirst({ where: { userId: user.id, @@ -65,6 +65,20 @@ export const handleConnectPilot = stationId: parseInt(stationId), }, }); + + const discordAccount = await prisma.discordAccount.findFirst({ + where: { + userId: user.id, + }, + }); + if (discordAccount?.id) { + await renameMember( + discordAccount.discordId.toString(), + `${getPublicUser(user).fullName} • ${Station?.bosCallsignShort}`, + ); + await addRolesToMember(discordAccount.discordId.toString(), [DISCORD_ROLES.ONLINE_PILOT]); + } + socket.join("dispatchers"); // Join the dispatchers room socket.join(`user:${userId}`); // Join the user-specific room socket.join(`station:${stationId}`); // Join the station-specific room @@ -106,6 +120,15 @@ export const handleConnectPilot = .catch(console.error); io.to("dispatchers").emit("update-connectedAircraft"); io.to("pilots").emit("pilots-update"); + if (discordAccount?.id) { + await renameMember( + discordAccount.discordId.toString(), + `${getPublicUser(user).fullName} - ${user.publicId}`, + ); + await removeRolesFromMember(discordAccount.discordId.toString(), [ + DISCORD_ROLES.ONLINE_PILOT, + ]); + } }); } catch (error) { console.error("Error connecting to dispatch server:", error); diff --git a/apps/hub-server/modules/chron.ts b/apps/hub-server/modules/chron.ts index 6bce78f8..da56e3e8 100644 --- a/apps/hub-server/modules/chron.ts +++ b/apps/hub-server/modules/chron.ts @@ -1,9 +1,10 @@ import { getMoodleCourseCompletionStatus, getMoodleUserById } from "./moodle"; import { CronJob } from "cron"; -import { prisma } from "@repo/db"; +import { DISCORD_ROLES, prisma } from "@repo/db"; import { sendCourseCompletedEmail } from "modules/mail"; import { handleParticipantFinished } from "modules/event"; import { eventCompleted } from "helper/events"; +import { addRolesToMember, removeRolesFromMember } from "modules/discord"; const syncMoodleIds = async () => { try { @@ -101,19 +102,91 @@ export const checkFinishedParticipants = async () => { }); }; +const checkUnfinishedParticipants = async () => { + const participantsPending = await prisma.participant.findMany({ + where: { + completetionWorkflowFinished: false, + }, + include: { + Event: true, + User: { + include: { + discordAccounts: true, + }, + }, + }, + }); + participantsPending.forEach(async (p) => { + if (!p.User) return; + const completed = eventCompleted(p.Event, p); + + if (completed) return; + + if (p.User.discordAccounts[0] && p.Event.discordRoleId) { + await addRolesToMember(p.User.discordAccounts[0].discordId, [p.Event.discordRoleId]); + prisma.participant.update({ + where: { + id: p.id, + }, + data: { + inscriptionWorkflowCompleted: true, + statusLog: { + push: { + event: "Discord-Rolle hinzugefügt", + timestamp: new Date(), + user: "system", + }, + }, + }, + }); + } + }); +}; + +const checkDiscordRoles = async () => { + const user = await prisma.user.findMany({ + where: { + discordAccounts: { + some: {}, + }, + }, + include: { + discordAccounts: true, + }, + }); + + for (const u of user) { + // Here ony member Roles regarding their rights are checked + if (!u.discordAccounts[0]) continue; + const discordAccount = u.discordAccounts[0]; + + // For Pilot + if (u.permissions.includes("PILOT")) { + await addRolesToMember(discordAccount.discordId, [DISCORD_ROLES.PILOT]); // ONLINE_PILOT + } else { + await removeRolesFromMember(discordAccount.discordId, [DISCORD_ROLES.PILOT]); // ONLINE_PILOT + } + // for Dispatcher + if (u.permissions.includes("DISPO")) { + await addRolesToMember(discordAccount.discordId, [DISCORD_ROLES.DISPATCHER]); // ONLINE_DISPATCHER + } else { + await removeRolesFromMember(discordAccount.discordId, [DISCORD_ROLES.DISPATCHER]); // ONLINE_DISPATCHER + } + } +}; + CronJob.from({ cronTime: "0 * * * *", onTick: syncMoodleIds, start: true }); CronJob.from({ cronTime: "*/1 * * * *", onTick: async () => { await updateParticipantMoodleResults(); await checkFinishedParticipants(); + await checkUnfinishedParticipants(); }, start: true, }); - -const debug = async () => { - await updateParticipantMoodleResults(); - await checkFinishedParticipants(); -}; - -debug(); +CronJob.from({ + cronTime: "0 * * * *", + onTick: checkDiscordRoles, + start: true, +}); diff --git a/apps/hub-server/modules/discord.ts b/apps/hub-server/modules/discord.ts new file mode 100644 index 00000000..94317358 --- /dev/null +++ b/apps/hub-server/modules/discord.ts @@ -0,0 +1,38 @@ +import axios from "axios"; + +const discordAxiosClient = axios.create({ + baseURL: process.env.DISCORD_SERVER_URL || "https://discord.com/api/v10", +}); + +export const renameMember = async (memberId: string, newName: string) => { + discordAxiosClient + .post("/member/rename", { + memberId, + newName, + }) + .catch((error) => { + console.error("Error renaming member:", error); + }); +}; + +export const addRolesToMember = async (memberId: string, roleIds: string[]) => { + discordAxiosClient + .post("/member/add-role", { + memberId, + roleIds, + }) + .catch((error) => { + console.error("Error adding roles to member:", error); + }); +}; + +export const removeRolesFromMember = async (memberId: string, roleIds: string[]) => { + discordAxiosClient + .post("/member/remove-role", { + memberId, + roleIds, + }) + .catch((error) => { + console.error("Error removing roles from member:", error); + }); +}; diff --git a/apps/hub-server/modules/event.ts b/apps/hub-server/modules/event.ts index 87a14525..fe8d11f2 100644 --- a/apps/hub-server/modules/event.ts +++ b/apps/hub-server/modules/event.ts @@ -1,4 +1,5 @@ import { Event, Participant, prisma, User } from "@repo/db"; +import { removeRolesFromMember } from "modules/discord"; import { sendCourseCompletedEmail } from "modules/mail"; export const handleParticipantFinished = async ( @@ -6,7 +7,7 @@ export const handleParticipantFinished = async ( participant: Participant, user: User, ) => { - const discordID = await prisma.discordAccount.findFirst({ + const discordAccount = await prisma.discordAccount.findFirst({ where: { userId: user.id, }, @@ -33,7 +34,9 @@ export const handleParticipantFinished = async ( }, }); - //TODO: Send Discord Message + if (event.discordRoleId && discordAccount) { + await removeRolesFromMember(discordAccount.discordId, [event.discordRoleId]); + } await sendCourseCompletedEmail(user.email, user, event); await prisma.participant.update({ diff --git a/apps/hub/app/api/discord-redirect/route.ts b/apps/hub/app/api/discord-redirect/route.ts index 464fb560..ba0e604d 100644 --- a/apps/hub/app/api/discord-redirect/route.ts +++ b/apps/hub/app/api/discord-redirect/route.ts @@ -14,7 +14,7 @@ export const GET = async (req: NextRequest) => { if ( !process.env.DISCORD_OAUTH_CLIENT_ID || !process.env.DISCORD_OAUTH_SECRET || - !process.env.DISCORD_REDIRECT || + !process.env.DISCORD_REDIRECT_URL || !code ) { return NextResponse.json( @@ -30,7 +30,7 @@ export const GET = async (req: NextRequest) => { const params = new URLSearchParams({ client_id: process.env.DISCORD_OAUTH_CLIENT_ID, client_secret: process.env.DISCORD_OAUTH_SECRET, - redirect_uri: process.env.DISCORD_REDIRECT, + redirect_uri: process.env.DISCORD_REDIRECT_URL, grant_type: "authorization_code", code, }); diff --git a/apps/hub/helper/discord.ts b/apps/hub/helper/discord.ts new file mode 100644 index 00000000..546f5d9a --- /dev/null +++ b/apps/hub/helper/discord.ts @@ -0,0 +1,39 @@ +"use server"; +import axios from "axios"; + +const discordAxiosClient = axios.create({ + baseURL: process.env.DISCORD_SERVER_URL || "https://discord.com/api/v10", +}); + +export const renameMember = async (memberId: string, newName: string) => { + discordAxiosClient + .post("/member/rename", { + memberId, + newName, + }) + .catch((error) => { + console.error("Error renaming member:", error); + }); +}; + +export const addRolesToMember = async (memberId: string, roleIds: string[]) => { + discordAxiosClient + .post("/member/add-role", { + memberId, + roleIds, + }) + .catch((error) => { + console.error("Error adding roles to member:", error); + }); +}; + +export const removeRolesFromMember = async (memberId: string, roleIds: string[]) => { + discordAxiosClient + .post("/member/remove-role", { + memberId, + roleIds, + }) + .catch((error) => { + console.error("Error removing roles from member:", error); + }); +}; diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 710d921e..9df78041 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -2,8 +2,8 @@ services: traefik: image: traefik:v3.4 command: - - "--api.dashboard=true" # Dashboard aktivieren (nicht für Produktion) - - "--api.insecure=true" # Unsicheres Dashboard (nur für Entwicklung) + - "--api.dashboard=true" # Dashboard aktivieren (nicht für Produktion) + - "--api.insecure=true" # Unsicheres Dashboard (nur für Entwicklung) - "--providers.docker=true" - "--providers.docker.exposedbydefault=false" - "--providers.docker.useBindPortIP=true" @@ -17,15 +17,15 @@ services: - "--certificatesresolvers.le.acme.email=johannesambre@gmail.com" - "--certificatesresolvers.le.acme.storage=/letsencrypt/acme.json" ports: - - "443:443" # HTTPS-Zugang - - "80:80" # HTTP-Zugang - - "8080:8080" # Traefik Dashboard + - "443:443" # HTTPS-Zugang + - "80:80" # HTTP-Zugang + - "8080:8080" # Traefik Dashboard volumes: - "/var/run/docker.sock:/var/run/docker.sock:ro" - "./letsencrypt:/letsencrypt" networks: - traefik_network - + portainer: image: portainer/portainer-ce:latest volumes: @@ -33,7 +33,7 @@ services: - /var/run/docker.sock:/var/run/docker.sock restart: unless-stopped labels: - # Frontend + # Frontend - "traefik.enable=true" - "traefik.http.routers.portainer-frontend.rule=Host(`portainer.premiumag.de`)" - "traefik.http.routers.portainer-frontend.entrypoints=websecure" @@ -96,6 +96,7 @@ services: - "traefik.http.services.dispatch-server.loadbalancer.server.port=3000" - "traefik.docker.network=var-monorepo_traefik_network" networks: + - discord_network - postgres_network - redis_network - traefik_network @@ -104,10 +105,20 @@ services: condition: service_healthy redis: condition: service_healthy + discord-server: + build: + context: . + dockerfile: ./apps/discord-server/Dockerfile + env_file: + - .env.prod + deploy: + replicas: 1 + labels: + - "traefik.enable=false" + networks: + - discord_network - - - # Hub Service + # Hub Service hub: build: context: . @@ -147,6 +158,7 @@ services: env_file: - .env.prod networks: + - discord_network - postgres_network - traefik_network depends_on: @@ -216,7 +228,7 @@ services: # - MOODLE_DATABASE_PORT_NUMBER=3306 # - MOODLE_DATABASE_USER=bn_moodle # - MOODLE_DATABASE_NAME=bitnami_moodle -# + # # - MOODLE_USERNAME=admin # - MOODLE_PASSWORD=admin123 # - MOODLE_EMAIL=admin@example.com @@ -225,7 +237,7 @@ services: # - ALLOW_EMPTY_PASSWORD=yes # depends_on: # - moodle_database - # labels: + # labels: # - "traefik.enable=true" # - "traefik.http.routers.moodle.rule=Host(`moodle.premiumag.de`)" # - "traefik.http.routers.moodle.entrypoints=websecure" @@ -255,7 +267,7 @@ services: - redis volumes: - ./livekit.yaml:/etc/livekit.yaml - labels: + labels: - "traefik.enable=true" - "traefik.http.routers.livekit.rule=Host(`livekit.premiumag.de`)" - "traefik.http.routers.livekit.entrypoints=websecure" @@ -264,12 +276,13 @@ services: - "traefik.http.routers.livekit.service=livekit" - "traefik.http.services.livekit.loadbalancer.server.port=7880" - networks: default: driver: bridge postgres_network: driver: bridge + discord_network: + driver: bridge redis_network: driver: bridge traefik_network: diff --git a/packages/database/prisma/json/User.ts b/packages/database/prisma/json/User.ts index a72f3408..4c364b20 100644 --- a/packages/database/prisma/json/User.ts +++ b/packages/database/prisma/json/User.ts @@ -8,6 +8,13 @@ export interface PublicUser { fullName: string; } +export const DISCORD_ROLES = { + ONLINE_DISPATCHER: "1287399540390891571", // Replace with actual role ID + ONLINE_PILOT: "1287399540390891571", // Replace with actual role ID + DISPATCHER: "1081247459994501222", + PILOT: "1081247405304975390", +}; + export const getPublicUser = ( user: User, options = { diff --git a/packages/database/prisma/schema/event.prisma b/packages/database/prisma/schema/event.prisma index 03bf2824..2d9e5130 100644 --- a/packages/database/prisma/schema/event.prisma +++ b/packages/database/prisma/schema/event.prisma @@ -23,6 +23,7 @@ model Participant { attended Boolean @default(false) appointmentCancelled Boolean @default(false) completetionWorkflowFinished Boolean @default(false) + inscriptionWorkflowCompleted Boolean @default(false) eventAppointmentId Int? enscriptionDate DateTime @default(now()) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5b01b4f5..f3926b44 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,6 +18,67 @@ importers: specifier: ^5.8.3 version: 5.8.3 + apps/discord-server: + dependencies: + axios: + specifier: ^1.9.0 + version: 1.9.0 + cors: + specifier: ^2.8.5 + version: 2.8.5 + cron: + specifier: ^4.3.1 + version: 4.3.1 + discord.js: + specifier: ^14.19.3 + version: 14.19.3 + dotenv: + specifier: ^16.5.0 + version: 16.5.0 + express: + specifier: ^5.1.0 + version: 5.1.0 + node-cron: + specifier: ^4.1.0 + version: 4.1.0 + nodemon: + specifier: ^3.1.10 + version: 3.1.10 + react: + specifier: ^19.1.0 + version: 19.1.0 + tsx: + specifier: ^4.19.4 + version: 4.19.4 + devDependencies: + '@repo/db': + specifier: workspace:* + version: link:../../packages/database + '@repo/typescript-config': + specifier: workspace:* + version: link:../../packages/typescript-config + '@types/cookie-parser': + specifier: ^1.4.8 + version: 1.4.8(@types/express@5.0.2) + '@types/cors': + specifier: ^2.8.18 + version: 2.8.18 + '@types/express': + specifier: ^5.0.2 + version: 5.0.2 + '@types/node': + specifier: ^22.15.29 + version: 22.15.29 + '@types/nodemailer': + specifier: ^6.4.17 + version: 6.4.17 + concurrently: + specifier: ^9.1.2 + version: 9.1.2 + typescript: + specifier: latest + version: 5.8.3 + apps/dispatch: dependencies: '@hookform/resolvers': @@ -577,6 +638,34 @@ packages: '@date-fns/tz@1.2.0': resolution: {integrity: sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg==} + '@discordjs/builders@1.11.2': + resolution: {integrity: sha512-F1WTABdd8/R9D1icJzajC4IuLyyS8f3rTOz66JsSI3pKvpCAtsMBweu8cyNYsIyvcrKAVn9EPK+Psoymq+XC0A==} + engines: {node: '>=16.11.0'} + + '@discordjs/collection@1.5.3': + resolution: {integrity: sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==} + engines: {node: '>=16.11.0'} + + '@discordjs/collection@2.1.1': + resolution: {integrity: sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==} + engines: {node: '>=18'} + + '@discordjs/formatters@0.6.1': + resolution: {integrity: sha512-5cnX+tASiPCqCWtFcFslxBVUaCetB0thvM/JyavhbXInP1HJIEU+Qv/zMrnuwSsX3yWH2lVXNJZeDK3EiP4HHg==} + engines: {node: '>=16.11.0'} + + '@discordjs/rest@2.5.0': + resolution: {integrity: sha512-PWhchxTzpn9EV3vvPRpwS0EE2rNYB9pvzDU/eLLW3mByJl0ZHZjHI2/wA8EbH2gRMQV7nu+0FoDF84oiPl8VAQ==} + engines: {node: '>=18'} + + '@discordjs/util@1.1.1': + resolution: {integrity: sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==} + engines: {node: '>=18'} + + '@discordjs/ws@1.2.2': + resolution: {integrity: sha512-dyfq7yn0wO0IYeYOs3z79I6/HumhmKISzFL0Z+007zQJMtAFGtt3AEoq1nuLXtcunUE5YYYQqgKvybXukAK8/w==} + engines: {node: '>=16.11.0'} + '@emnapi/core@1.4.3': resolution: {integrity: sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==} @@ -1339,6 +1428,18 @@ packages: '@rushstack/eslint-patch@1.11.0': resolution: {integrity: sha512-zxnHvoMQVqewTJr/W4pKjF0bMGiKJv1WX7bSrkl46Hg0QjESbzBROWK0Wg4RphzSOS5Jiy7eFimmM3UgMrMZbQ==} + '@sapphire/async-queue@1.5.5': + resolution: {integrity: sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + + '@sapphire/shapeshift@4.0.0': + resolution: {integrity: sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==} + engines: {node: '>=v16'} + + '@sapphire/snowflake@3.5.3': + resolution: {integrity: sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + '@selderee/plugin-htmlparser2@0.11.0': resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} @@ -1597,6 +1698,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + '@typescript-eslint/eslint-plugin@8.33.1': resolution: {integrity: sha512-TDCXj+YxLgtvxvFlAvpoRv9MAncDLBV2oT9Bd7YBGC/b/sEURoOYuIwLI99rjWOfY3QtDzO+mk0n4AmdFExW8A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1759,6 +1863,10 @@ packages: cpu: [x64] os: [win32] + '@vladfrangu/async_event_emitter@2.4.6': + resolution: {integrity: sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + '@webassemblyjs/ast@1.14.1': resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} @@ -2249,6 +2357,13 @@ packages: resolution: {integrity: sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA==} hasBin: true + discord-api-types@0.38.11: + resolution: {integrity: sha512-XN0qhcQpetkyb/49hcDHuoeUPsQqOkb17wbV/t48gUkoEDi4ajhsxqugGcxvcN17BBtI9FPPWEgzv6IhQmCwyw==} + + discord.js@14.19.3: + resolution: {integrity: sha512-lncTRk0k+8Q5D3nThnODBR8fR8x2fM798o8Vsr40Krx0DjPwpZCuxxTcFMrXMQVOqM1QB9wqWgaXPg3TbmlHqA==} + engines: {node: '>=18'} + doctrine@2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} @@ -3204,6 +3319,9 @@ packages: lodash.once@4.1.1: resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + lodash.snakecase@4.1.1: + resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} + lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} @@ -3238,6 +3356,9 @@ packages: resolution: {integrity: sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==} engines: {node: '>=12'} + magic-bytes.js@1.12.1: + resolution: {integrity: sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA==} + magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} @@ -4337,6 +4458,9 @@ packages: ts-debounce@4.0.0: resolution: {integrity: sha512-+1iDGY6NmOGidq7i7xZGA4cm8DAa6fqdYcvO5Z6yBevH++Bdo9Qt/mN0TzHUgcCcKv1gmh9+W5dHqz8pMWbCbg==} + ts-mixer@6.0.4: + resolution: {integrity: sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==} + tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} @@ -4443,6 +4567,10 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici@6.21.1: + resolution: {integrity: sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==} + engines: {node: '>=18.17'} + unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} @@ -4768,6 +4896,53 @@ snapshots: '@date-fns/tz@1.2.0': {} + '@discordjs/builders@1.11.2': + dependencies: + '@discordjs/formatters': 0.6.1 + '@discordjs/util': 1.1.1 + '@sapphire/shapeshift': 4.0.0 + discord-api-types: 0.38.11 + fast-deep-equal: 3.1.3 + ts-mixer: 6.0.4 + tslib: 2.8.1 + + '@discordjs/collection@1.5.3': {} + + '@discordjs/collection@2.1.1': {} + + '@discordjs/formatters@0.6.1': + dependencies: + discord-api-types: 0.38.11 + + '@discordjs/rest@2.5.0': + dependencies: + '@discordjs/collection': 2.1.1 + '@discordjs/util': 1.1.1 + '@sapphire/async-queue': 1.5.5 + '@sapphire/snowflake': 3.5.3 + '@vladfrangu/async_event_emitter': 2.4.6 + discord-api-types: 0.38.11 + magic-bytes.js: 1.12.1 + tslib: 2.8.1 + undici: 6.21.1 + + '@discordjs/util@1.1.1': {} + + '@discordjs/ws@1.2.2': + dependencies: + '@discordjs/collection': 2.1.1 + '@discordjs/rest': 2.5.0 + '@discordjs/util': 1.1.1 + '@sapphire/async-queue': 1.5.5 + '@types/ws': 8.18.1 + '@vladfrangu/async_event_emitter': 2.4.6 + discord-api-types: 0.38.11 + tslib: 2.8.1 + ws: 8.17.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@emnapi/core@1.4.3': dependencies: '@emnapi/wasi-threads': 1.0.2 @@ -5404,6 +5579,15 @@ snapshots: '@rushstack/eslint-patch@1.11.0': {} + '@sapphire/async-queue@1.5.5': {} + + '@sapphire/shapeshift@4.0.0': + dependencies: + fast-deep-equal: 3.1.3 + lodash: 4.17.21 + + '@sapphire/snowflake@3.5.3': {} + '@selderee/plugin-htmlparser2@0.11.0': dependencies: domhandler: 5.0.3 @@ -5660,6 +5844,10 @@ snapshots: '@types/unist@3.0.3': {} + '@types/ws@8.18.1': + dependencies: + '@types/node': 22.15.29 + '@typescript-eslint/eslint-plugin@8.33.1(@typescript-eslint/parser@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: '@eslint-community/regexpp': 4.12.1 @@ -5842,6 +6030,8 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.7.9': optional: true + '@vladfrangu/async_event_emitter@2.4.6': {} + '@webassemblyjs/ast@1.14.1': dependencies: '@webassemblyjs/helper-numbers': 1.13.2 @@ -6378,6 +6568,27 @@ snapshots: direction@2.0.1: {} + discord-api-types@0.38.11: {} + + discord.js@14.19.3: + dependencies: + '@discordjs/builders': 1.11.2 + '@discordjs/collection': 1.5.3 + '@discordjs/formatters': 0.6.1 + '@discordjs/rest': 2.5.0 + '@discordjs/util': 1.1.1 + '@discordjs/ws': 1.2.2 + '@sapphire/snowflake': 3.5.3 + discord-api-types: 0.38.11 + fast-deep-equal: 3.1.3 + lodash.snakecase: 4.1.1 + magic-bytes.js: 1.12.1 + tslib: 2.8.1 + undici: 6.21.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + doctrine@2.1.0: dependencies: esutils: 2.0.3 @@ -7603,6 +7814,8 @@ snapshots: lodash.once@4.1.1: {} + lodash.snakecase@4.1.1: {} + lodash@4.17.21: {} loglevel@1.9.1: {} @@ -7629,6 +7842,8 @@ snapshots: luxon@3.6.1: {} + magic-bytes.js@1.12.1: {} + magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 @@ -9039,6 +9254,8 @@ snapshots: ts-debounce@4.0.0: {} + ts-mixer@6.0.4: {} + tsconfig-paths@3.15.0: dependencies: '@types/json5': 0.0.29 @@ -9162,6 +9379,8 @@ snapshots: undici-types@6.21.0: {} + undici@6.21.1: {} + unified@11.0.5: dependencies: '@types/unist': 3.0.3