diff --git a/.github/workflows/terraform-apply.yml b/.github/workflows/terraform-apply.yml index ffc834b44..5993ff334 100644 --- a/.github/workflows/terraform-apply.yml +++ b/.github/workflows/terraform-apply.yml @@ -65,7 +65,6 @@ jobs: run: terraform apply tfplan working-directory: terraform env: - TF_VAR_email_function_parameters: ${{ secrets.EMAIL_FUNCTION_PARAMETERS }} - TF_VAR_scrape_discord_events_function_environment_variables: ${{ secrets.SCRAPE_DISCORD_EVENTS_FUNCTION_ENVIRONMENT_VARIABLES }} - TF_VAR_midnight_automations_environment_variables: ${{ secrets.MIDNIGHT_AUTOMATIONS_ENVIRONMENT_VARIABLES }} - TF_VAR_notification_router_environment_variables: ${{ secrets.NOTIFICATION_ROUTER_ENVIRONMENT_VARIABLES }} + TF_VAR_scrape_discord_events_function_environment_variables: ${{ vars.SCRAPE_DISCORD_EVENTS_FUNCTION_ENVIRONMENT_VARIABLES }} + TF_VAR_midnight_automations_environment_variables: ${{ vars.MIDNIGHT_AUTOMATIONS_ENVIRONMENT_VARIABLES }} + TF_VAR_notification_router_environment_variables: ${{ vars.NOTIFICATION_ROUTER_ENVIRONMENT_VARIABLES }} diff --git a/.github/workflows/terraform-plan.yml b/.github/workflows/terraform-plan.yml index d2bdbb1de..422951e77 100644 --- a/.github/workflows/terraform-plan.yml +++ b/.github/workflows/terraform-plan.yml @@ -70,10 +70,9 @@ jobs: id: plan continue-on-error: true env: - TF_VAR_email_function_parameters: ${{ secrets.EMAIL_FUNCTION_PARAMETERS }} - TF_VAR_scrape_discord_events_function_environment_variables: ${{ secrets.SCRAPE_DISCORD_EVENTS_FUNCTION_ENVIRONMENT_VARIABLES }} - TF_VAR_midnight_automations_environment_variables: ${{ secrets.MIDNIGHT_AUTOMATIONS_ENVIRONMENT_VARIABLES }} - TF_VAR_notification_router_environment_variables: ${{ secrets.NOTIFICATION_ROUTER_ENVIRONMENT_VARIABLES }} + TF_VAR_scrape_discord_events_function_environment_variables: ${{ vars.SCRAPE_DISCORD_EVENTS_FUNCTION_ENVIRONMENT_VARIABLES }} + TF_VAR_midnight_automations_environment_variables: ${{ vars.MIDNIGHT_AUTOMATIONS_ENVIRONMENT_VARIABLES }} + TF_VAR_notification_router_environment_variables: ${{ vars.NOTIFICATION_ROUTER_ENVIRONMENT_VARIABLES }} - uses: actions/github-script@v8.0.0 if: github.event_name == 'pull_request' diff --git a/bun-packages/packages/email-function/src/index.ts b/bun-packages/packages/email-function/src/index.ts index f51750835..aa887d723 100644 --- a/bun-packages/packages/email-function/src/index.ts +++ b/bun-packages/packages/email-function/src/index.ts @@ -30,8 +30,7 @@ export const handler: SQSHandler = async (event) => { } const parameters = await fetchParameters({ - // deepcode ignore HardcodedNonCryptoSecret: This is not the actual secret but a reference to the secret in the parameters store - mailgunApiKey: "/email-function/mailgun-api-key", + mailgunApiKey: "/mailgun/api_key", }); await main({ diff --git a/docs/setup-test-and-production.md b/docs/setup-test-and-production.md index 7b38c5ec1..76fed5ac0 100644 --- a/docs/setup-test-and-production.md +++ b/docs/setup-test-and-production.md @@ -16,12 +16,9 @@ 1. Create environments 1. `terraform-test` 2. `terraform-prod` -2. Create environment secrets - - `EMAIL_FUNCTION_PARAMETERS` - - `[{"name":"...","value":"..."}]` -3. Create environment variables +2. Create environment variables - `IAM_ROLE` -4. Enable "Allow GitHub Actions to create and approve pull requests" in Settings/Actions/General/Workflow permissions +3. Enable "Allow GitHub Actions to create and approve pull requests" in Settings/Actions/General/Workflow permissions ## 5. Set up AWS diff --git a/pnpm-monorepo/apps/lambda/.env.example b/pnpm-monorepo/apps/lambda/.env.example index d75ccd743..121979e69 100644 --- a/pnpm-monorepo/apps/lambda/.env.example +++ b/pnpm-monorepo/apps/lambda/.env.example @@ -3,7 +3,7 @@ DATABASE_URL="postgresql://postgres:admin@localhost:5432/db" # Discord DISCORD_GUILD_ID="" -DISCORD_TOKEN="" +DISCORD_BOT_TOKEN="" # Web Push PUBLIC_VAPID_KEY="" diff --git a/pnpm-monorepo/apps/lambda/package.json b/pnpm-monorepo/apps/lambda/package.json index 2f2f65a63..8f44c93bb 100644 --- a/pnpm-monorepo/apps/lambda/package.json +++ b/pnpm-monorepo/apps/lambda/package.json @@ -2,17 +2,14 @@ "name": "@sam-monorepo/lambda", "private": true, "scripts": { - "dev": "tsx watch src/local-api.ts", "build": "./scripts/build.sh" }, "dependencies": { "@aws-sdk/client-dynamodb": "3.939.0", "@aws-sdk/client-eventbridge": "3.939.0", - "@hono/node-server": "1.19.6", "@paralleldrive/cuid2": "3.0.4", "@sam-monorepo/database": "workspace:*", "aws-xray-sdk-core": "3.12.0", - "hono": "4.10.6", "lodash": "4.17.21", "serialize-error": "12.0.0", "web-push": "3.6.7", @@ -24,7 +21,6 @@ "@types/node": "22.19.1", "@types/web-push": "3.6.4", "esbuild": "0.27.0", - "tsx": "4.20.6", "typescript": "5.9.3" } } diff --git a/pnpm-monorepo/apps/lambda/scripts/build.sh b/pnpm-monorepo/apps/lambda/scripts/build.sh index 82a5f0836..3bedd9297 100755 --- a/pnpm-monorepo/apps/lambda/scripts/build.sh +++ b/pnpm-monorepo/apps/lambda/scripts/build.sh @@ -19,9 +19,9 @@ echo "Cleaning up old build..." rm -rf $OUTPUT_DIRECTORY $OUTPUT_DIRECTORY.zip # Get all functions -FUNCTION_FILES=$(find src/functions -maxdepth 1 -type f -name "*.ts") +FUNCTION_FILES=$(find src -maxdepth 1 -type f -name "*.ts") -for file in "src/functions"/*.ts; do +for file in "src"/*.ts; do FUNCTION_FILENAME=$(basename "$file") FUNCTION_NAME="${FUNCTION_FILENAME%.ts}" @@ -31,7 +31,7 @@ for file in "src/functions"/*.ts; do # - `--external:@aws-sdk` excludes any imported AWS SDKs from the bundle since they are already provided by the AWS Lambda runtime. # - The banner is needed to allow usage of `require` in ESM modules (see https://github.com/aws/aws-sam-cli/issues/4827) echo "Bundling $FUNCTION_NAME..." - esbuild src/functions/$FUNCTION_NAME.ts \ + esbuild src/$FUNCTION_NAME.ts \ --bundle \ --outfile=$OUTPUT_DIRECTORY/$FUNCTION_NAME/index.mjs \ --format=esm \ diff --git a/pnpm-monorepo/apps/lambda/src/common/parameters.ts b/pnpm-monorepo/apps/lambda/src/common/parameters.ts new file mode 100644 index 000000000..0442d2587 --- /dev/null +++ b/pnpm-monorepo/apps/lambda/src/common/parameters.ts @@ -0,0 +1,47 @@ +import { z } from "zod"; +import { log } from "./logger"; + +export const fetchParameters = async >( + parameters: T, +): Promise => { + if (!process.env.AWS_SESSION_TOKEN) + throw new Error("Missing AWS_SESSION_TOKEN for fetching parameter store"); + + const responses = await Promise.all( + Object.entries(parameters).map(async ([key, name]) => { + const uriEncodedParameterName = encodeURIComponent(name); + + const response = await fetch( + `http://localhost:2773/systemsmanager/parameters/get?name=${uriEncodedParameterName}&withDecryption=true`, + { + headers: { + // biome-ignore lint/style/noNonNullAssertion: + "X-Aws-Parameters-Secrets-Token": process.env.AWS_SESSION_TOKEN!, + }, + }, + ); + + if (!response.ok) { + void log.error("Failed to fetch parameter", { + name, + status: response.status, + body: await response.text(), + }); + throw new Error("Failed to fetch parameter"); + } + + const body = await response.json(); + const parameter = parameterSchema.parse(body); + + return [key, parameter.Parameter.Value]; + }), + ); + + return Object.fromEntries(responses); +}; + +const parameterSchema = z.object({ + Parameter: z.object({ + Value: z.string(), + }), +}); diff --git a/pnpm-monorepo/apps/lambda/src/functions/midnight-automations.ts b/pnpm-monorepo/apps/lambda/src/functions/midnight-automations.ts deleted file mode 100644 index 9aefb3916..000000000 --- a/pnpm-monorepo/apps/lambda/src/functions/midnight-automations.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { ScheduledHandler } from "aws-lambda"; -import { initializeRequestContext } from "../common/requestContext"; -import { midnightAutomationsHandler } from "./midnight-automations/handler"; - -export const handler: ScheduledHandler = async (event, context) => { - return initializeRequestContext(context.awsRequestId, () => - midnightAutomationsHandler(), - ); -}; diff --git a/pnpm-monorepo/apps/lambda/src/functions/midnight-automations/env.ts b/pnpm-monorepo/apps/lambda/src/functions/midnight-automations/env.ts deleted file mode 100644 index 206383927..000000000 --- a/pnpm-monorepo/apps/lambda/src/functions/midnight-automations/env.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { z } from "zod"; - -const schema = z.object({ - NODE_ENV: z.enum(["development", "production"]).default("development"), - DATABASE_URL: z - .url() - .default("postgresql://postgres:admin@localhost:5432/db"), -}); - -export const env = schema.parse(process.env); diff --git a/pnpm-monorepo/apps/lambda/src/functions/midnight-automations/handler.ts b/pnpm-monorepo/apps/lambda/src/functions/midnight-automations/handler.ts deleted file mode 100644 index 22abccd74..000000000 --- a/pnpm-monorepo/apps/lambda/src/functions/midnight-automations/handler.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { countCitizensPerRole } from "./countCitizensPerRole"; -import { countShips } from "./countShips"; -import { countUniqueLogins } from "./countUniqueLogins"; -import { disburseRoleSalaries } from "./disburseRoleSalaries"; -import "./env"; -import { removeExpiredRoles } from "./removeExpiredRoles"; - -export const midnightAutomationsHandler = async () => { - // TODO: Add profit distribution cycle automation here - - await removeExpiredRoles(); - await countCitizensPerRole(); - await disburseRoleSalaries(); - await countShips(); - await countUniqueLogins(); -}; diff --git a/pnpm-monorepo/apps/lambda/src/functions/notification-router/env.ts b/pnpm-monorepo/apps/lambda/src/functions/notification-router/env.ts deleted file mode 100644 index ecdf358b7..000000000 --- a/pnpm-monorepo/apps/lambda/src/functions/notification-router/env.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { z } from "zod"; - -const schema = z.object({ - NODE_ENV: z.enum(["development", "production"]).default("development"), - DATABASE_URL: z - .url() - .default("postgresql://postgres:admin@localhost:5432/db"), - BASE_URL: z.url().default("http://localhost:3000"), - PUBLIC_VAPID_KEY: z.string(), - PRIVATE_VAPID_KEY: z.string(), -}); - -export const env = schema.parse(process.env); diff --git a/pnpm-monorepo/apps/lambda/src/functions/scrape-discord-events.ts b/pnpm-monorepo/apps/lambda/src/functions/scrape-discord-events.ts deleted file mode 100644 index 13ea8b075..000000000 --- a/pnpm-monorepo/apps/lambda/src/functions/scrape-discord-events.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { ScheduledHandler } from "aws-lambda"; -import { initializeRequestContext } from "../common/requestContext"; -import { scrapeDiscordEventsHandler } from "./scrape-discord-events/handler"; - -export const handler: ScheduledHandler = async (event, context) => { - return initializeRequestContext(context.awsRequestId, () => - scrapeDiscordEventsHandler(), - ); -}; diff --git a/pnpm-monorepo/apps/lambda/src/functions/scrape-discord-events/env.ts b/pnpm-monorepo/apps/lambda/src/functions/scrape-discord-events/env.ts deleted file mode 100644 index 5343783c7..000000000 --- a/pnpm-monorepo/apps/lambda/src/functions/scrape-discord-events/env.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { z } from "zod"; - -const schema = z.object({ - NODE_ENV: z.enum(["development", "production"]).default("development"), - DATABASE_URL: z - .url() - .default("postgresql://postgres:admin@localhost:5432/db"), - DISCORD_GUILD_ID: z.string(), - DISCORD_TOKEN: z.string(), - AWS_EVENT_BUS_ARN: z.string().nullish(), -}); - -export const env = schema.parse(process.env); diff --git a/pnpm-monorepo/apps/lambda/src/functions/scrape-discord-events/handler.ts b/pnpm-monorepo/apps/lambda/src/functions/scrape-discord-events/handler.ts deleted file mode 100644 index 926256973..000000000 --- a/pnpm-monorepo/apps/lambda/src/functions/scrape-discord-events/handler.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { prisma } from "@sam-monorepo/database"; -import shuffle from "lodash/shuffle"; -import { log } from "../../common/logger"; -import { deleteCancelledEvents } from "./deleteCancelledEvents"; -import { getEvents } from "./discord/utils/getEvents"; -import { triggerNotifications } from "./notifications"; -import { updateParticipants } from "./updateParticipants"; - -export const scrapeDiscordEventsHandler = async () => { - try { - const { data: _futureEventsFromDiscord } = await getEvents(); - - // Shuffle array so rate limits not always hitting the same events - const futureEventsFromDiscord = shuffle(_futureEventsFromDiscord); - // // Limit to 5 events to avoid rate limits - // futureEventsFromDiscord = futureEventsFromDiscord.slice(0, 5); - - await deleteCancelledEvents(futureEventsFromDiscord); - - for (const futureEventFromDiscord of futureEventsFromDiscord) { - const existingEventFromDatabase = await prisma.event.findUnique({ - where: { - discordId: futureEventFromDiscord.id, - }, - }); - - if (existingEventFromDatabase) { - const hasAnyChanges = - existingEventFromDatabase.name !== futureEventFromDiscord.name || - existingEventFromDatabase.startTime.getTime() !== - futureEventFromDiscord.scheduled_start_time.getTime() || - // biome-ignore lint/suspicious/noDoubleEquals: - existingEventFromDatabase.endTime?.getTime() != - futureEventFromDiscord.scheduled_end_time?.getTime() || - // biome-ignore lint/suspicious/noDoubleEquals: - existingEventFromDatabase.description != - futureEventFromDiscord.description || - // biome-ignore lint/suspicious/noDoubleEquals: - existingEventFromDatabase.location != - futureEventFromDiscord.entity_metadata.location || - // biome-ignore lint/suspicious/noDoubleEquals: - existingEventFromDatabase.discordImage != - futureEventFromDiscord.image; - - if (hasAnyChanges) { - await prisma.event.update({ - where: { - id: existingEventFromDatabase.id, - }, - data: { - name: futureEventFromDiscord.name, - startTime: futureEventFromDiscord.scheduled_start_time, - endTime: futureEventFromDiscord.scheduled_end_time, - description: futureEventFromDiscord.description, - location: futureEventFromDiscord.entity_metadata.location || null, - discordImage: futureEventFromDiscord.image, - }, - }); - } - - const hasChangesForNotification = - existingEventFromDatabase.name !== futureEventFromDiscord.name || - existingEventFromDatabase.startTime.getTime() !== - futureEventFromDiscord.scheduled_start_time.getTime() || - // biome-ignore lint/suspicious/noDoubleEquals: - existingEventFromDatabase.endTime?.getTime() != - futureEventFromDiscord.scheduled_end_time?.getTime() || - // biome-ignore lint/suspicious/noDoubleEquals: - existingEventFromDatabase.description != - futureEventFromDiscord.description || - // biome-ignore lint/suspicious/noDoubleEquals: - existingEventFromDatabase.location != - futureEventFromDiscord.entity_metadata.location; - - if (hasChangesForNotification) { - await triggerNotifications([ - { - type: "EventUpdated", - payload: { - eventId: existingEventFromDatabase.id, - }, - }, - ]); - } - } else { - const newEvent = await prisma.event.create({ - data: { - discordId: futureEventFromDiscord.id, - discordCreatorId: futureEventFromDiscord.creator_id, - name: futureEventFromDiscord.name, - startTime: futureEventFromDiscord.scheduled_start_time, - endTime: futureEventFromDiscord.scheduled_end_time, - description: futureEventFromDiscord.description, - location: futureEventFromDiscord.entity_metadata.location || null, - discordImage: futureEventFromDiscord.image, - discordGuildId: futureEventFromDiscord.guild_id, - }, - select: { - id: true, - }, - }); - - await triggerNotifications([ - { - type: "EventCreated", - payload: { - eventId: newEvent.id, - }, - }, - ]); - } - - await updateParticipants(futureEventFromDiscord); - } - } catch (error) { - // @ts-expect-error - log.error("Failed to scrape Discord events", error); - throw error; - } -}; diff --git a/pnpm-monorepo/apps/lambda/src/local-api.ts b/pnpm-monorepo/apps/lambda/src/local-api.ts deleted file mode 100644 index 2fd013e63..000000000 --- a/pnpm-monorepo/apps/lambda/src/local-api.ts +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Simple web server to locally simulate AWS Lambda. For now we omitted input validation - * and error handling to keep it simple. On AWS these would be provided by the API - * Gateway and respective Lambda handlers. - */ - -import { serve } from "@hono/node-server"; -import { Hono } from "hono"; -import { cors } from "hono/cors"; -import { log } from "./common/logger"; -import { initializeRequestContext } from "./common/requestContext"; -import { midnightAutomationsHandler } from "./functions/midnight-automations/handler"; -import { scrapeDiscordEventsHandler } from "./functions/scrape-discord-events/handler"; - -const app = new Hono(); - -app.use(async (c, next) => initializeRequestContext(null, async () => next())); - -app.use("/*", cors()); - -app.post("/midnight-automations", async (c) => { - try { - await midnightAutomationsHandler(); - return c.json({}, 200); - } catch (error) { - log.error("Internal Server Error", { error }); - return c.json( - { - error: { - message: "Internal Server Error", - }, - }, - 500, - ); - } -}); - -app.post("/scrape-discord-events", async (c) => { - try { - await scrapeDiscordEventsHandler(); - return c.json({}, 200); - } catch (error) { - log.error("Internal Server Error", { error }); - return c.json( - { - error: { - message: "Internal Server Error", - }, - }, - 500, - ); - } -}); - -const port = 3001; - -log.info("Local API started", { port }); - -serve({ - fetch: app.fetch, - port, -}); diff --git a/pnpm-monorepo/apps/lambda/src/midnight-automations.ts b/pnpm-monorepo/apps/lambda/src/midnight-automations.ts new file mode 100644 index 000000000..9203f7442 --- /dev/null +++ b/pnpm-monorepo/apps/lambda/src/midnight-automations.ts @@ -0,0 +1,21 @@ +import "./midnight-automations/setup"; // must be first + +import type { ScheduledHandler } from "aws-lambda"; +import { initializeRequestContext } from "./common/requestContext"; +import { countCitizensPerRole } from "./midnight-automations/countCitizensPerRole"; +import { countShips } from "./midnight-automations/countShips"; +import { countUniqueLogins } from "./midnight-automations/countUniqueLogins"; +import { disburseRoleSalaries } from "./midnight-automations/disburseRoleSalaries"; +import { removeExpiredRoles } from "./midnight-automations/removeExpiredRoles"; + +export const handler: ScheduledHandler = async (event, context) => { + return initializeRequestContext(context.awsRequestId, async () => { + // TODO: Add profit distribution cycle automation here + + await removeExpiredRoles(); + await countCitizensPerRole(); + await disburseRoleSalaries(); + await countShips(); + await countUniqueLogins(); + }); +}; diff --git a/pnpm-monorepo/apps/lambda/src/functions/midnight-automations/countCitizensPerRole.ts b/pnpm-monorepo/apps/lambda/src/midnight-automations/countCitizensPerRole.ts similarity index 91% rename from pnpm-monorepo/apps/lambda/src/functions/midnight-automations/countCitizensPerRole.ts rename to pnpm-monorepo/apps/lambda/src/midnight-automations/countCitizensPerRole.ts index d9b4ce800..f9f4d067c 100644 --- a/pnpm-monorepo/apps/lambda/src/functions/midnight-automations/countCitizensPerRole.ts +++ b/pnpm-monorepo/apps/lambda/src/midnight-automations/countCitizensPerRole.ts @@ -1,6 +1,6 @@ import { prisma } from "@sam-monorepo/database"; -import { log } from "../../common/logger"; -import { captureAsyncFunc } from "../../common/xray"; +import { log } from "../common/logger"; +import { captureAsyncFunc } from "../common/xray"; export const countCitizensPerRole = async () => { await captureAsyncFunc("countCitizensPerRole", async () => { diff --git a/pnpm-monorepo/apps/lambda/src/functions/midnight-automations/countShips.ts b/pnpm-monorepo/apps/lambda/src/midnight-automations/countShips.ts similarity index 96% rename from pnpm-monorepo/apps/lambda/src/functions/midnight-automations/countShips.ts rename to pnpm-monorepo/apps/lambda/src/midnight-automations/countShips.ts index dcdca6e0f..c9aa4bea6 100644 --- a/pnpm-monorepo/apps/lambda/src/functions/midnight-automations/countShips.ts +++ b/pnpm-monorepo/apps/lambda/src/midnight-automations/countShips.ts @@ -1,6 +1,6 @@ import { prisma, type Organization } from "@sam-monorepo/database"; -import { log } from "../../common/logger"; -import { captureAsyncFunc } from "../../common/xray"; +import { log } from "../common/logger"; +import { captureAsyncFunc } from "../common/xray"; const ORG_ID = "cm4wm57sw0001opxo2c8oq0o0"; // TODO: Implement UI for configuring org ID diff --git a/pnpm-monorepo/apps/lambda/src/functions/midnight-automations/countUniqueLogins.ts b/pnpm-monorepo/apps/lambda/src/midnight-automations/countUniqueLogins.ts similarity index 75% rename from pnpm-monorepo/apps/lambda/src/functions/midnight-automations/countUniqueLogins.ts rename to pnpm-monorepo/apps/lambda/src/midnight-automations/countUniqueLogins.ts index c615a5f9d..94ab89708 100644 --- a/pnpm-monorepo/apps/lambda/src/functions/midnight-automations/countUniqueLogins.ts +++ b/pnpm-monorepo/apps/lambda/src/midnight-automations/countUniqueLogins.ts @@ -1,6 +1,6 @@ import { prisma } from "@sam-monorepo/database"; -import { log } from "../../common/logger"; -import { captureAsyncFunc } from "../../common/xray"; +import { log } from "../common/logger"; +import { captureAsyncFunc } from "../common/xray"; export const countUniqueLogins = async () => { await captureAsyncFunc("countUniqueLogins", async () => { @@ -17,17 +17,15 @@ export const countUniqueLogins = async () => { const endOfDay = new Date(previousDay); endOfDay.setHours(23, 59, 59, 999); - const uniqueLoginCount = await captureAsyncFunc( - "count unique logins", - () => - prisma.user.count({ - where: { - lastSeenAt: { - gte: startOfDay, - lte: endOfDay, - }, + const uniqueLoginCount = await captureAsyncFunc("count unique logins", () => + prisma.user.count({ + where: { + lastSeenAt: { + gte: startOfDay, + lte: endOfDay, }, - }), + }, + }), ); await captureAsyncFunc("save daily login count", () => diff --git a/pnpm-monorepo/apps/lambda/src/functions/midnight-automations/disburseRoleSalaries.ts b/pnpm-monorepo/apps/lambda/src/midnight-automations/disburseRoleSalaries.ts similarity index 95% rename from pnpm-monorepo/apps/lambda/src/functions/midnight-automations/disburseRoleSalaries.ts rename to pnpm-monorepo/apps/lambda/src/midnight-automations/disburseRoleSalaries.ts index 7fc3fec1c..7c9093401 100644 --- a/pnpm-monorepo/apps/lambda/src/functions/midnight-automations/disburseRoleSalaries.ts +++ b/pnpm-monorepo/apps/lambda/src/midnight-automations/disburseRoleSalaries.ts @@ -1,6 +1,6 @@ import { prisma, type Entity, type Role } from "@sam-monorepo/database"; -import { log } from "../../common/logger"; -import { captureAsyncFunc } from "../../common/xray"; +import { log } from "../common/logger"; +import { captureAsyncFunc } from "../common/xray"; import { getRoleSalaries } from "./getRoleSalaries"; import { updateCitizensSilcBalances } from "./updateCitizensSilcBalances"; diff --git a/pnpm-monorepo/apps/lambda/src/functions/midnight-automations/getRoleSalaries.ts b/pnpm-monorepo/apps/lambda/src/midnight-automations/getRoleSalaries.ts similarity index 79% rename from pnpm-monorepo/apps/lambda/src/functions/midnight-automations/getRoleSalaries.ts rename to pnpm-monorepo/apps/lambda/src/midnight-automations/getRoleSalaries.ts index fd56d620d..955860449 100644 --- a/pnpm-monorepo/apps/lambda/src/functions/midnight-automations/getRoleSalaries.ts +++ b/pnpm-monorepo/apps/lambda/src/midnight-automations/getRoleSalaries.ts @@ -1,5 +1,5 @@ import { prisma } from "@sam-monorepo/database"; -import { captureAsyncFunc } from "../../common/xray"; +import { captureAsyncFunc } from "../common/xray"; export const getRoleSalaries = async () => { return captureAsyncFunc("getRoleSalaries", async () => { diff --git a/pnpm-monorepo/apps/lambda/src/functions/midnight-automations/getSilcTransactionsOfAllCitizensWithoutAuthorization.ts b/pnpm-monorepo/apps/lambda/src/midnight-automations/getSilcTransactionsOfAllCitizensWithoutAuthorization.ts similarity index 94% rename from pnpm-monorepo/apps/lambda/src/functions/midnight-automations/getSilcTransactionsOfAllCitizensWithoutAuthorization.ts rename to pnpm-monorepo/apps/lambda/src/midnight-automations/getSilcTransactionsOfAllCitizensWithoutAuthorization.ts index 10266804c..4ec25c5e3 100644 --- a/pnpm-monorepo/apps/lambda/src/functions/midnight-automations/getSilcTransactionsOfAllCitizensWithoutAuthorization.ts +++ b/pnpm-monorepo/apps/lambda/src/midnight-automations/getSilcTransactionsOfAllCitizensWithoutAuthorization.ts @@ -1,5 +1,5 @@ import { prisma } from "@sam-monorepo/database"; -import { captureAsyncFunc } from "../../common/xray"; +import { captureAsyncFunc } from "../common/xray"; export const getSilcTransactionsOfAllCitizensWithoutAuthorization = async () => { diff --git a/pnpm-monorepo/apps/lambda/src/functions/midnight-automations/removeExpiredRoles.ts b/pnpm-monorepo/apps/lambda/src/midnight-automations/removeExpiredRoles.ts similarity index 97% rename from pnpm-monorepo/apps/lambda/src/functions/midnight-automations/removeExpiredRoles.ts rename to pnpm-monorepo/apps/lambda/src/midnight-automations/removeExpiredRoles.ts index f906ddf5d..a8bf75315 100644 --- a/pnpm-monorepo/apps/lambda/src/functions/midnight-automations/removeExpiredRoles.ts +++ b/pnpm-monorepo/apps/lambda/src/midnight-automations/removeExpiredRoles.ts @@ -1,6 +1,6 @@ import { prisma, RoleAssignmentChangeType } from "@sam-monorepo/database"; -import { log } from "../../common/logger"; -import { captureAsyncFunc } from "../../common/xray"; +import { log } from "../common/logger"; +import { captureAsyncFunc } from "../common/xray"; export const removeExpiredRoles = async () => { await captureAsyncFunc("removeExpiredRoles", async () => { diff --git a/pnpm-monorepo/apps/lambda/src/midnight-automations/setup.ts b/pnpm-monorepo/apps/lambda/src/midnight-automations/setup.ts new file mode 100644 index 000000000..448d685a5 --- /dev/null +++ b/pnpm-monorepo/apps/lambda/src/midnight-automations/setup.ts @@ -0,0 +1,26 @@ +import { z } from "zod"; +import { fetchParameters } from "../common/parameters"; + +const parameterMap = { + DATABASE_URL: "/database/connection_string", +}; + +const environmentSchema = z.object({ + NODE_ENV: z.enum(["development", "production"]).default("development"), + DATABASE_URL: z + .url() + .default("postgresql://postgres:admin@localhost:5432/db"), +}); + +export const setup = async () => { + const parameters = await fetchParameters(parameterMap); + + process.env = { + ...process.env, + ...parameters, + }; + + environmentSchema.parse(process.env); +}; + +await setup(); diff --git a/pnpm-monorepo/apps/lambda/src/functions/midnight-automations/updateCitizensSilcBalances.ts b/pnpm-monorepo/apps/lambda/src/midnight-automations/updateCitizensSilcBalances.ts similarity index 96% rename from pnpm-monorepo/apps/lambda/src/functions/midnight-automations/updateCitizensSilcBalances.ts rename to pnpm-monorepo/apps/lambda/src/midnight-automations/updateCitizensSilcBalances.ts index 3e3ce1ea1..7cfeea701 100644 --- a/pnpm-monorepo/apps/lambda/src/functions/midnight-automations/updateCitizensSilcBalances.ts +++ b/pnpm-monorepo/apps/lambda/src/midnight-automations/updateCitizensSilcBalances.ts @@ -1,5 +1,5 @@ import { prisma, type Entity } from "@sam-monorepo/database"; -import { captureAsyncFunc } from "../../common/xray"; +import { captureAsyncFunc } from "../common/xray"; import { getSilcTransactionsOfAllCitizensWithoutAuthorization } from "./getSilcTransactionsOfAllCitizensWithoutAuthorization"; export const updateCitizensSilcBalances = async ( diff --git a/pnpm-monorepo/apps/lambda/src/functions/notification-router.ts b/pnpm-monorepo/apps/lambda/src/notification-router.ts similarity index 87% rename from pnpm-monorepo/apps/lambda/src/functions/notification-router.ts rename to pnpm-monorepo/apps/lambda/src/notification-router.ts index b830cfd66..ee0395cd7 100644 --- a/pnpm-monorepo/apps/lambda/src/functions/notification-router.ts +++ b/pnpm-monorepo/apps/lambda/src/notification-router.ts @@ -1,6 +1,8 @@ +import "./notification-router/setup"; // must be first + import type { SQSBatchItemFailure, SQSHandler } from "aws-lambda"; -import { log } from "../common/logger"; -import { initializeRequestContext } from "../common/requestContext"; +import { log } from "./common/logger"; +import { initializeRequestContext } from "./common/requestContext"; import { isRequestProcessed, setRequestProcessed, @@ -17,7 +19,7 @@ export const handler: SQSHandler = async (event, context) => { log.info("Processing SQS messages", { count: event.Records.length, - }) + }); for (const record of event.Records) { try { const body = bodySchema.parse(JSON.parse(record.body)); diff --git a/pnpm-monorepo/apps/lambda/src/functions/notification-router/dynamodb.ts b/pnpm-monorepo/apps/lambda/src/notification-router/dynamodb.ts similarity index 100% rename from pnpm-monorepo/apps/lambda/src/functions/notification-router/dynamodb.ts rename to pnpm-monorepo/apps/lambda/src/notification-router/dynamodb.ts diff --git a/pnpm-monorepo/apps/lambda/src/common/profit-distribution/utils/getAuecPerSilc.ts b/pnpm-monorepo/apps/lambda/src/notification-router/getAuecPerSilc.ts similarity index 100% rename from pnpm-monorepo/apps/lambda/src/common/profit-distribution/utils/getAuecPerSilc.ts rename to pnpm-monorepo/apps/lambda/src/notification-router/getAuecPerSilc.ts diff --git a/pnpm-monorepo/apps/lambda/src/common/profit-distribution/utils/getTotalSilc.ts b/pnpm-monorepo/apps/lambda/src/notification-router/getTotalSilc.ts similarity index 100% rename from pnpm-monorepo/apps/lambda/src/common/profit-distribution/utils/getTotalSilc.ts rename to pnpm-monorepo/apps/lambda/src/notification-router/getTotalSilc.ts diff --git a/pnpm-monorepo/apps/lambda/src/functions/notification-router/handler.ts b/pnpm-monorepo/apps/lambda/src/notification-router/handler.ts similarity index 98% rename from pnpm-monorepo/apps/lambda/src/functions/notification-router/handler.ts rename to pnpm-monorepo/apps/lambda/src/notification-router/handler.ts index daedab8c1..59ff1b1d3 100644 --- a/pnpm-monorepo/apps/lambda/src/functions/notification-router/handler.ts +++ b/pnpm-monorepo/apps/lambda/src/notification-router/handler.ts @@ -1,6 +1,5 @@ import { z } from "zod"; -import { log } from "../../common/logger"; -import "./env"; +import { log } from "../common/logger"; import { EventCreatedHandler } from "./type-handlers/EventCreated"; import { EventDeletedHandler } from "./type-handlers/EventDeleted"; import { EventLineupEnabledHandler } from "./type-handlers/EventLineupEnabled"; diff --git a/pnpm-monorepo/apps/lambda/src/notification-router/setup.ts b/pnpm-monorepo/apps/lambda/src/notification-router/setup.ts new file mode 100644 index 000000000..6428798db --- /dev/null +++ b/pnpm-monorepo/apps/lambda/src/notification-router/setup.ts @@ -0,0 +1,31 @@ +import { z } from "zod"; +import { fetchParameters } from "../common/parameters"; + +const parameterMap = { + DATABASE_URL: "/database/connection_string", + PUBLIC_VAPID_KEY: "/web_push/public_vapid_key", + PRIVATE_VAPID_KEY: "/web_push/private_vapid_key", +}; + +const environmentSchema = z.object({ + NODE_ENV: z.enum(["development", "production"]).default("development"), + DATABASE_URL: z + .url() + .default("postgresql://postgres:admin@localhost:5432/db"), + BASE_URL: z.url().default("http://localhost:3000"), + PUBLIC_VAPID_KEY: z.string(), + PRIVATE_VAPID_KEY: z.string(), +}); + +export const setup = async () => { + const parameters = await fetchParameters(parameterMap); + + process.env = { + ...process.env, + ...parameters, + }; + + environmentSchema.parse(process.env); +}; + +await setup(); diff --git a/pnpm-monorepo/apps/lambda/src/functions/notification-router/type-handlers/EventCreated.ts b/pnpm-monorepo/apps/lambda/src/notification-router/type-handlers/EventCreated.ts similarity index 100% rename from pnpm-monorepo/apps/lambda/src/functions/notification-router/type-handlers/EventCreated.ts rename to pnpm-monorepo/apps/lambda/src/notification-router/type-handlers/EventCreated.ts diff --git a/pnpm-monorepo/apps/lambda/src/functions/notification-router/type-handlers/EventDeleted.ts b/pnpm-monorepo/apps/lambda/src/notification-router/type-handlers/EventDeleted.ts similarity index 100% rename from pnpm-monorepo/apps/lambda/src/functions/notification-router/type-handlers/EventDeleted.ts rename to pnpm-monorepo/apps/lambda/src/notification-router/type-handlers/EventDeleted.ts diff --git a/pnpm-monorepo/apps/lambda/src/functions/notification-router/type-handlers/EventLineupEnabled.ts b/pnpm-monorepo/apps/lambda/src/notification-router/type-handlers/EventLineupEnabled.ts similarity index 100% rename from pnpm-monorepo/apps/lambda/src/functions/notification-router/type-handlers/EventLineupEnabled.ts rename to pnpm-monorepo/apps/lambda/src/notification-router/type-handlers/EventLineupEnabled.ts diff --git a/pnpm-monorepo/apps/lambda/src/functions/notification-router/type-handlers/EventUpdated.ts b/pnpm-monorepo/apps/lambda/src/notification-router/type-handlers/EventUpdated.ts similarity index 100% rename from pnpm-monorepo/apps/lambda/src/functions/notification-router/type-handlers/EventUpdated.ts rename to pnpm-monorepo/apps/lambda/src/notification-router/type-handlers/EventUpdated.ts diff --git a/pnpm-monorepo/apps/lambda/src/functions/notification-router/type-handlers/PenaltyEntryCreated.ts b/pnpm-monorepo/apps/lambda/src/notification-router/type-handlers/PenaltyEntryCreated.ts similarity index 100% rename from pnpm-monorepo/apps/lambda/src/functions/notification-router/type-handlers/PenaltyEntryCreated.ts rename to pnpm-monorepo/apps/lambda/src/notification-router/type-handlers/PenaltyEntryCreated.ts diff --git a/pnpm-monorepo/apps/lambda/src/functions/notification-router/type-handlers/ProfitDistributionPayoutDisbursed.ts b/pnpm-monorepo/apps/lambda/src/notification-router/type-handlers/ProfitDistributionPayoutDisbursed.ts similarity index 91% rename from pnpm-monorepo/apps/lambda/src/functions/notification-router/type-handlers/ProfitDistributionPayoutDisbursed.ts rename to pnpm-monorepo/apps/lambda/src/notification-router/type-handlers/ProfitDistributionPayoutDisbursed.ts index c7e0c31e7..6769e8b7c 100644 --- a/pnpm-monorepo/apps/lambda/src/functions/notification-router/type-handlers/ProfitDistributionPayoutDisbursed.ts +++ b/pnpm-monorepo/apps/lambda/src/notification-router/type-handlers/ProfitDistributionPayoutDisbursed.ts @@ -1,6 +1,6 @@ import { prisma } from "@sam-monorepo/database"; -import { getAuecPerSilc } from "../../../common/profit-distribution/utils/getAuecPerSilc"; -import { getTotalSilc } from "../../../common/profit-distribution/utils/getTotalSilc"; +import { getAuecPerSilc } from "../getAuecPerSilc"; +import { getTotalSilc } from "../getTotalSilc"; import { publishWebPushNotifications } from "../web-push"; interface Change { diff --git a/pnpm-monorepo/apps/lambda/src/functions/notification-router/type-handlers/ProfitDistributionPayoutStarted.ts b/pnpm-monorepo/apps/lambda/src/notification-router/type-handlers/ProfitDistributionPayoutStarted.ts similarity index 100% rename from pnpm-monorepo/apps/lambda/src/functions/notification-router/type-handlers/ProfitDistributionPayoutStarted.ts rename to pnpm-monorepo/apps/lambda/src/notification-router/type-handlers/ProfitDistributionPayoutStarted.ts diff --git a/pnpm-monorepo/apps/lambda/src/functions/notification-router/type-handlers/RoleAdded.ts b/pnpm-monorepo/apps/lambda/src/notification-router/type-handlers/RoleAdded.ts similarity index 100% rename from pnpm-monorepo/apps/lambda/src/functions/notification-router/type-handlers/RoleAdded.ts rename to pnpm-monorepo/apps/lambda/src/notification-router/type-handlers/RoleAdded.ts diff --git a/pnpm-monorepo/apps/lambda/src/functions/notification-router/type-handlers/SilcTransactionsCreated.ts b/pnpm-monorepo/apps/lambda/src/notification-router/type-handlers/SilcTransactionsCreated.ts similarity index 100% rename from pnpm-monorepo/apps/lambda/src/functions/notification-router/type-handlers/SilcTransactionsCreated.ts rename to pnpm-monorepo/apps/lambda/src/notification-router/type-handlers/SilcTransactionsCreated.ts diff --git a/pnpm-monorepo/apps/lambda/src/functions/notification-router/type-handlers/TaskAssignmentUpdated.ts b/pnpm-monorepo/apps/lambda/src/notification-router/type-handlers/TaskAssignmentUpdated.ts similarity index 100% rename from pnpm-monorepo/apps/lambda/src/functions/notification-router/type-handlers/TaskAssignmentUpdated.ts rename to pnpm-monorepo/apps/lambda/src/notification-router/type-handlers/TaskAssignmentUpdated.ts diff --git a/pnpm-monorepo/apps/lambda/src/functions/notification-router/type-handlers/TaskCreated.ts b/pnpm-monorepo/apps/lambda/src/notification-router/type-handlers/TaskCreated.ts similarity index 100% rename from pnpm-monorepo/apps/lambda/src/functions/notification-router/type-handlers/TaskCreated.ts rename to pnpm-monorepo/apps/lambda/src/notification-router/type-handlers/TaskCreated.ts diff --git a/pnpm-monorepo/apps/lambda/src/functions/notification-router/type-handlers/WebPushSubscribed.ts b/pnpm-monorepo/apps/lambda/src/notification-router/type-handlers/WebPushSubscribed.ts similarity index 93% rename from pnpm-monorepo/apps/lambda/src/functions/notification-router/type-handlers/WebPushSubscribed.ts rename to pnpm-monorepo/apps/lambda/src/notification-router/type-handlers/WebPushSubscribed.ts index 4684ec8db..f38e41452 100644 --- a/pnpm-monorepo/apps/lambda/src/functions/notification-router/type-handlers/WebPushSubscribed.ts +++ b/pnpm-monorepo/apps/lambda/src/notification-router/type-handlers/WebPushSubscribed.ts @@ -1,9 +1,12 @@ import { prisma, type WebPushSubscription } from "@sam-monorepo/database"; import { sendNotification, setVapidDetails, WebPushError } from "web-push"; -import { log } from "../../../common/logger"; -import { env } from "../env"; +import { log } from "../../common/logger"; -setVapidDetails(env.BASE_URL, env.PUBLIC_VAPID_KEY, env.PRIVATE_VAPID_KEY); +setVapidDetails( + process.env.BASE_URL, + process.env.PUBLIC_VAPID_KEY, + process.env.PRIVATE_VAPID_KEY, +); interface Payload { subscriptionId: WebPushSubscription["id"]; diff --git a/pnpm-monorepo/apps/lambda/src/functions/notification-router/web-push.ts b/pnpm-monorepo/apps/lambda/src/notification-router/web-push.ts similarity index 96% rename from pnpm-monorepo/apps/lambda/src/functions/notification-router/web-push.ts rename to pnpm-monorepo/apps/lambda/src/notification-router/web-push.ts index 137672ea5..960c5bb29 100644 --- a/pnpm-monorepo/apps/lambda/src/functions/notification-router/web-push.ts +++ b/pnpm-monorepo/apps/lambda/src/notification-router/web-push.ts @@ -4,10 +4,13 @@ import { type Entity, } from "@sam-monorepo/database"; import { sendNotification, setVapidDetails, WebPushError } from "web-push"; -import { log } from "../../common/logger"; -import { env } from "./env"; +import { log } from "../common/logger"; -setVapidDetails(env.BASE_URL, env.PUBLIC_VAPID_KEY, env.PRIVATE_VAPID_KEY); +setVapidDetails( + process.env.BASE_URL, + process.env.PUBLIC_VAPID_KEY, + process.env.PRIVATE_VAPID_KEY, +); interface Notification { readonly receiverId: Entity["id"]; @@ -161,7 +164,7 @@ export const publishWebPushNotifications = async ( if (subscriptionsToRemove.length <= 0) return; log.info("Deleting Web Push subscriptions", { count: subscriptionsToRemove.length, - }) + }); await prisma.webPushSubscription.deleteMany({ where: { endpoint: { diff --git a/pnpm-monorepo/apps/lambda/src/scrape-discord-events.ts b/pnpm-monorepo/apps/lambda/src/scrape-discord-events.ts new file mode 100644 index 000000000..3279a587d --- /dev/null +++ b/pnpm-monorepo/apps/lambda/src/scrape-discord-events.ts @@ -0,0 +1,127 @@ +import "./scrape-discord-events/setup"; // must be first + +import { prisma } from "@sam-monorepo/database"; +import type { ScheduledHandler } from "aws-lambda"; +import { shuffle } from "lodash"; +import { log } from "./common/logger"; +import { initializeRequestContext } from "./common/requestContext"; +import { deleteCancelledEvents } from "./scrape-discord-events/deleteCancelledEvents"; +import { getEvents } from "./scrape-discord-events/discord/utils/getEvents"; +import { triggerNotifications } from "./scrape-discord-events/notifications"; +import { updateParticipants } from "./scrape-discord-events/updateParticipants"; + +export const handler: ScheduledHandler = async (event, context) => { + return initializeRequestContext(context.awsRequestId, async () => { + try { + const { data: _futureEventsFromDiscord } = await getEvents(); + + // Shuffle array so rate limits not always hitting the same events + const futureEventsFromDiscord = shuffle(_futureEventsFromDiscord); + // // Limit to 5 events to avoid rate limits + // futureEventsFromDiscord = futureEventsFromDiscord.slice(0, 5); + + await deleteCancelledEvents(futureEventsFromDiscord); + + for (const futureEventFromDiscord of futureEventsFromDiscord) { + const existingEventFromDatabase = await prisma.event.findUnique({ + where: { + discordId: futureEventFromDiscord.id, + }, + }); + + if (existingEventFromDatabase) { + const hasAnyChanges = + existingEventFromDatabase.name !== futureEventFromDiscord.name || + existingEventFromDatabase.startTime.getTime() !== + futureEventFromDiscord.scheduled_start_time.getTime() || + // biome-ignore lint/suspicious/noDoubleEquals: + existingEventFromDatabase.endTime?.getTime() != + futureEventFromDiscord.scheduled_end_time?.getTime() || + // biome-ignore lint/suspicious/noDoubleEquals: + existingEventFromDatabase.description != + futureEventFromDiscord.description || + // biome-ignore lint/suspicious/noDoubleEquals: + existingEventFromDatabase.location != + futureEventFromDiscord.entity_metadata.location || + // biome-ignore lint/suspicious/noDoubleEquals: + existingEventFromDatabase.discordImage != + futureEventFromDiscord.image; + + if (hasAnyChanges) { + await prisma.event.update({ + where: { + id: existingEventFromDatabase.id, + }, + data: { + name: futureEventFromDiscord.name, + startTime: futureEventFromDiscord.scheduled_start_time, + endTime: futureEventFromDiscord.scheduled_end_time, + description: futureEventFromDiscord.description, + location: + futureEventFromDiscord.entity_metadata.location || null, + discordImage: futureEventFromDiscord.image, + }, + }); + } + + const hasChangesForNotification = + existingEventFromDatabase.name !== futureEventFromDiscord.name || + existingEventFromDatabase.startTime.getTime() !== + futureEventFromDiscord.scheduled_start_time.getTime() || + // biome-ignore lint/suspicious/noDoubleEquals: + existingEventFromDatabase.endTime?.getTime() != + futureEventFromDiscord.scheduled_end_time?.getTime() || + // biome-ignore lint/suspicious/noDoubleEquals: + existingEventFromDatabase.description != + futureEventFromDiscord.description || + // biome-ignore lint/suspicious/noDoubleEquals: + existingEventFromDatabase.location != + futureEventFromDiscord.entity_metadata.location; + + if (hasChangesForNotification) { + await triggerNotifications([ + { + type: "EventUpdated", + payload: { + eventId: existingEventFromDatabase.id, + }, + }, + ]); + } + } else { + const newEvent = await prisma.event.create({ + data: { + discordId: futureEventFromDiscord.id, + discordCreatorId: futureEventFromDiscord.creator_id, + name: futureEventFromDiscord.name, + startTime: futureEventFromDiscord.scheduled_start_time, + endTime: futureEventFromDiscord.scheduled_end_time, + description: futureEventFromDiscord.description, + location: futureEventFromDiscord.entity_metadata.location || null, + discordImage: futureEventFromDiscord.image, + discordGuildId: futureEventFromDiscord.guild_id, + }, + select: { + id: true, + }, + }); + + await triggerNotifications([ + { + type: "EventCreated", + payload: { + eventId: newEvent.id, + }, + }, + ]); + } + + await updateParticipants(futureEventFromDiscord); + } + } catch (error) { + // @ts-expect-error + void log.error("Failed to scrape Discord events", error); + throw error; + } + }); +}; diff --git a/pnpm-monorepo/apps/lambda/src/functions/scrape-discord-events/deleteCancelledEvents.ts b/pnpm-monorepo/apps/lambda/src/scrape-discord-events/deleteCancelledEvents.ts similarity index 100% rename from pnpm-monorepo/apps/lambda/src/functions/scrape-discord-events/deleteCancelledEvents.ts rename to pnpm-monorepo/apps/lambda/src/scrape-discord-events/deleteCancelledEvents.ts diff --git a/pnpm-monorepo/apps/lambda/src/functions/scrape-discord-events/discord/utils/checkResponseForError.ts b/pnpm-monorepo/apps/lambda/src/scrape-discord-events/discord/utils/checkResponseForError.ts similarity index 61% rename from pnpm-monorepo/apps/lambda/src/functions/scrape-discord-events/discord/utils/checkResponseForError.ts rename to pnpm-monorepo/apps/lambda/src/scrape-discord-events/discord/utils/checkResponseForError.ts index 7ec9c4fa8..82a3fcfb7 100644 --- a/pnpm-monorepo/apps/lambda/src/functions/scrape-discord-events/discord/utils/checkResponseForError.ts +++ b/pnpm-monorepo/apps/lambda/src/scrape-discord-events/discord/utils/checkResponseForError.ts @@ -1,23 +1,21 @@ -import { env } from "../../env"; - export const checkResponseForError = ( data: Record | unknown[], ) => { if (Array.isArray(data) || typeof data.message !== "string") return; if (data.message === "You are being rate limited.") { - throw new Error("Rate Limiting der Discord API"); + throw new Error("Rate limiting by the Discord API"); } if (data.message === "Unknown Guild") { throw new Error( - `Der Discord Server "${env.DISCORD_GUILD_ID}" existiert nicht.`, + `The Discord server "${process.env.DISCORD_GUILD_ID}" does not exist.`, ); } if (data.message === "Missing Access") { throw new Error( - `Diese Anwendung hat keinen Zugriff auf den Discord Server "${env.DISCORD_GUILD_ID}".`, + `This application does not have access to the Discord server "${process.env.DISCORD_GUILD_ID}".`, ); } diff --git a/pnpm-monorepo/apps/lambda/src/functions/scrape-discord-events/discord/utils/getEventUsers.ts b/pnpm-monorepo/apps/lambda/src/scrape-discord-events/discord/utils/getEventUsers.ts similarity index 83% rename from pnpm-monorepo/apps/lambda/src/functions/scrape-discord-events/discord/utils/getEventUsers.ts rename to pnpm-monorepo/apps/lambda/src/scrape-discord-events/discord/utils/getEventUsers.ts index 3ac027f91..f34837576 100644 --- a/pnpm-monorepo/apps/lambda/src/functions/scrape-discord-events/discord/utils/getEventUsers.ts +++ b/pnpm-monorepo/apps/lambda/src/scrape-discord-events/discord/utils/getEventUsers.ts @@ -1,7 +1,6 @@ import { setTimeout } from "node:timers/promises"; import { z } from "zod"; -import { log } from "../../../../common/logger"; -import { env } from "../../env"; +import { log } from "../../../common/logger"; import { checkResponseForError } from "./checkResponseForError"; import { memberSchema, userSchema } from "./schemas"; @@ -10,10 +9,10 @@ export const getEventUsers = async (discordId: string) => { for (let attempt = 0; attempt < 5; attempt++) { response = await fetch( - `https://discord.com/api/v10/guilds/${env.DISCORD_GUILD_ID}/scheduled-events/${discordId}/users?with_member=true`, + `https://discord.com/api/v10/guilds/${process.env.DISCORD_GUILD_ID}/scheduled-events/${discordId}/users?with_member=true`, { headers: new Headers({ - Authorization: `Bot ${env.DISCORD_TOKEN}`, + Authorization: `Bot ${process.env.DISCORD_BOT_TOKEN}`, }), }, ); diff --git a/pnpm-monorepo/apps/lambda/src/functions/scrape-discord-events/discord/utils/getEvents.ts b/pnpm-monorepo/apps/lambda/src/scrape-discord-events/discord/utils/getEvents.ts similarity index 80% rename from pnpm-monorepo/apps/lambda/src/functions/scrape-discord-events/discord/utils/getEvents.ts rename to pnpm-monorepo/apps/lambda/src/scrape-discord-events/discord/utils/getEvents.ts index 67b1ee1bc..fe56232b7 100644 --- a/pnpm-monorepo/apps/lambda/src/functions/scrape-discord-events/discord/utils/getEvents.ts +++ b/pnpm-monorepo/apps/lambda/src/scrape-discord-events/discord/utils/getEvents.ts @@ -1,15 +1,14 @@ import { z } from "zod"; -import { env } from "../../env"; import { checkResponseForError } from "./checkResponseForError"; import { eventSchema } from "./schemas"; export const getEvents = async () => { // https://discord.com/developers/docs/resources/guild-scheduled-event#list-scheduled-events-for-guild const response = await fetch( - `https://discord.com/api/v10/guilds/${env.DISCORD_GUILD_ID}/scheduled-events?with_user_count=true`, + `https://discord.com/api/v10/guilds/${process.env.DISCORD_GUILD_ID}/scheduled-events?with_user_count=true`, { headers: new Headers({ - Authorization: `Bot ${env.DISCORD_TOKEN}`, + Authorization: `Bot ${process.env.DISCORD_BOT_TOKEN}`, }), }, ); diff --git a/pnpm-monorepo/apps/lambda/src/functions/scrape-discord-events/discord/utils/schemas.ts b/pnpm-monorepo/apps/lambda/src/scrape-discord-events/discord/utils/schemas.ts similarity index 100% rename from pnpm-monorepo/apps/lambda/src/functions/scrape-discord-events/discord/utils/schemas.ts rename to pnpm-monorepo/apps/lambda/src/scrape-discord-events/discord/utils/schemas.ts diff --git a/pnpm-monorepo/apps/lambda/src/functions/scrape-discord-events/eventbridge.ts b/pnpm-monorepo/apps/lambda/src/scrape-discord-events/eventbridge.ts similarity index 82% rename from pnpm-monorepo/apps/lambda/src/functions/scrape-discord-events/eventbridge.ts rename to pnpm-monorepo/apps/lambda/src/scrape-discord-events/eventbridge.ts index e80331d21..cc1e6b388 100644 --- a/pnpm-monorepo/apps/lambda/src/functions/scrape-discord-events/eventbridge.ts +++ b/pnpm-monorepo/apps/lambda/src/scrape-discord-events/eventbridge.ts @@ -3,9 +3,8 @@ import { PutEventsCommand, type PutEventsCommandInput, } from "@aws-sdk/client-eventbridge"; -import { log } from "../../common/logger"; -import { captureAsyncFunc } from "../../common/xray"; -import { env } from "./env"; +import { log } from "../common/logger"; +import { captureAsyncFunc } from "../common/xray"; const client = new EventBridgeClient({ region: "eu-central-1", @@ -18,7 +17,7 @@ export const emitEvents = async ( >[], ) => { await captureAsyncFunc("emitEvents", async () => { - if (!env.AWS_EVENT_BUS_ARN) { + if (!process.env.AWS_EVENT_BUS_ARN) { void log.info("Event bus ARN not configured, skipping emitting events."); return; } @@ -31,7 +30,7 @@ export const emitEvents = async ( const input: PutEventsCommandInput = { Entries: entries.map((entry) => ({ ...entry, - EventBusName: env.AWS_EVENT_BUS_ARN, + EventBusName: process.env.AWS_EVENT_BUS_ARN, })), }; diff --git a/pnpm-monorepo/apps/lambda/src/functions/scrape-discord-events/notifications.ts b/pnpm-monorepo/apps/lambda/src/scrape-discord-events/notifications.ts similarity index 100% rename from pnpm-monorepo/apps/lambda/src/functions/scrape-discord-events/notifications.ts rename to pnpm-monorepo/apps/lambda/src/scrape-discord-events/notifications.ts diff --git a/pnpm-monorepo/apps/lambda/src/scrape-discord-events/setup.ts b/pnpm-monorepo/apps/lambda/src/scrape-discord-events/setup.ts new file mode 100644 index 000000000..0fc7efeec --- /dev/null +++ b/pnpm-monorepo/apps/lambda/src/scrape-discord-events/setup.ts @@ -0,0 +1,30 @@ +import { z } from "zod"; +import { fetchParameters } from "../common/parameters"; + +const parameterMap = { + DATABASE_URL: "/database/connection_string", + DISCORD_BOT_TOKEN: "/discord/bot_token", +}; + +const environmentSchema = z.object({ + NODE_ENV: z.enum(["development", "production"]).default("development"), + DATABASE_URL: z + .url() + .default("postgresql://postgres:admin@localhost:5432/db"), + DISCORD_GUILD_ID: z.string(), + DISCORD_BOT_TOKEN: z.string(), + AWS_EVENT_BUS_ARN: z.string().nullish(), +}); + +export const setup = async () => { + const parameters = await fetchParameters(parameterMap); + + process.env = { + ...process.env, + ...parameters, + }; + + environmentSchema.parse(process.env); +}; + +await setup(); diff --git a/pnpm-monorepo/apps/lambda/src/functions/scrape-discord-events/updateParticipants.ts b/pnpm-monorepo/apps/lambda/src/scrape-discord-events/updateParticipants.ts similarity index 100% rename from pnpm-monorepo/apps/lambda/src/functions/scrape-discord-events/updateParticipants.ts rename to pnpm-monorepo/apps/lambda/src/scrape-discord-events/updateParticipants.ts diff --git a/terraform/email-function.tf b/terraform/email-function.tf index 71120b19f..5410e4790 100644 --- a/terraform/email-function.tf +++ b/terraform/email-function.tf @@ -9,7 +9,9 @@ module "email_function" { event_bus_detail_type = "EmailRequested" dynamodb = aws_dynamodb_table.sqs_processed_requests runtime = "nodejs20.x" - parameters = var.email_function_parameters batch_size = 10 batch_window = 30 + parameters = [ + "/mailgun/api_key", + ] } diff --git a/terraform/midnight-automations.tf b/terraform/midnight-automations.tf index db2261d65..e791161c5 100644 --- a/terraform/midnight-automations.tf +++ b/terraform/midnight-automations.tf @@ -8,4 +8,8 @@ module "midnight_automations" { schedule_expression = "cron(0 0 * * ? *)" scheduler_state = "ENABLED" event_bus = data.aws_cloudwatch_event_bus.default + runtime = "nodejs22.x" + parameters = [ + "/database/connection_string", + ] } diff --git a/terraform/modules/eventbridge-sqs-lambda/function.tf b/terraform/modules/eventbridge-sqs-lambda/function.tf index 5cd08563c..202097328 100644 --- a/terraform/modules/eventbridge-sqs-lambda/function.tf +++ b/terraform/modules/eventbridge-sqs-lambda/function.tf @@ -18,7 +18,7 @@ resource "aws_lambda_function" "main" { } layers = [ - "arn:aws:lambda:eu-central-1:187925254637:layer:AWS-Parameters-and-Secrets-Lambda-Extension-Arm64:20", # https://docs.aws.amazon.com/systems-manager/latest/userguide/ps-integration-lambda-extensions.html#ps-integration-lambda-extensions-add + "arn:aws:lambda:eu-central-1:187925254637:layer:AWS-Parameters-and-Secrets-Lambda-Extension-Arm64:21", # https://docs.aws.amazon.com/systems-manager/latest/userguide/ps-integration-lambda-extensions.html#ps-integration-lambda-extensions-add "arn:aws:lambda:eu-central-1:580247275435:layer:LambdaInsightsExtension-Arm64:25" # https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Lambda-Insights-extension-versionsARM.html ] @@ -31,7 +31,13 @@ resource "aws_lambda_function" "main" { } environment { - variables = var.environment_variables + variables = merge( + { + TZ = "Europe/Berlin", + NODE_ENV = "production" + }, + var.environment_variables + ) } } diff --git a/terraform/modules/eventbridge-sqs-lambda/iam.tf b/terraform/modules/eventbridge-sqs-lambda/iam.tf index 0ce2d8d03..7df954578 100644 --- a/terraform/modules/eventbridge-sqs-lambda/iam.tf +++ b/terraform/modules/eventbridge-sqs-lambda/iam.tf @@ -50,9 +50,20 @@ resource "aws_iam_role_policy" "main_parameter_store" { [ data.aws_kms_alias.ssm.target_key_arn, ], - tolist(aws_ssm_parameter.custom[*].arn), + [for param in var.parameters : "arn:aws:ssm:eu-central-1:${var.account_id}:parameter${param}"] ) - }, + } + ] + }) +} + +resource "aws_iam_role_policy" "main_sqs" { + role = aws_iam_role.main.id + name = "sqs" + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ { # https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-configure-lambda-function-trigger.html#configure-lambda-function-trigger-prerequisites Action = [ @@ -64,7 +75,18 @@ resource "aws_iam_role_policy" "main_parameter_store" { Resource = [ aws_sqs_queue.main.arn ] - }, + } + ] + }) +} + +resource "aws_iam_role_policy" "main_dynamodb" { + role = aws_iam_role.main.id + name = "dynamodb" + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ { Action = [ "dynamodb:GetItem", @@ -102,10 +124,3 @@ resource "aws_iam_role_policy" "main_eventbridge" { data "aws_kms_alias" "ssm" { name = "alias/aws/ssm" } - -resource "aws_ssm_parameter" "custom" { - count = length(var.parameters) - type = "SecureString" - name = "/${var.function_name}/${var.parameters[count.index].name}" - value = var.parameters[count.index].value -} diff --git a/terraform/modules/eventbridge-sqs-lambda/variables.tf b/terraform/modules/eventbridge-sqs-lambda/variables.tf index 2fb6a6cd3..f6c3d48aa 100644 --- a/terraform/modules/eventbridge-sqs-lambda/variables.tf +++ b/terraform/modules/eventbridge-sqs-lambda/variables.tf @@ -18,12 +18,8 @@ variable "provisioned_concurrent_executions" { } variable "parameters" { - type = list(object({ - name = string - value = string - })) - default = [] - sensitive = true + type = list(string) + default = [] } variable "account_id" { diff --git a/terraform/modules/scheduled-lambda/function.tf b/terraform/modules/scheduled-lambda/function.tf index e1c7be088..0b7c9eb0a 100644 --- a/terraform/modules/scheduled-lambda/function.tf +++ b/terraform/modules/scheduled-lambda/function.tf @@ -4,7 +4,7 @@ resource "aws_lambda_function" "main" { role = aws_iam_role.main.arn handler = "index.handler" source_code_hash = filebase64sha256("${path.module}/placeholder.zip") - runtime = "nodejs22.x" + runtime = var.runtime timeout = var.timeout memory_size = var.memory_size architectures = ["arm64"] @@ -18,8 +18,9 @@ resource "aws_lambda_function" "main" { } layers = [ - "arn:aws:lambda:eu-central-1:580247275435:layer:LambdaInsightsExtension-Arm64:25" # https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Lambda-Insights-extension-versionsARM.html - ] + "arn:aws:lambda:eu-central-1:187925254637:layer:AWS-Parameters-and-Secrets-Lambda-Extension-Arm64:21", # https://docs.aws.amazon.com/systems-manager/latest/userguide/ps-integration-lambda-extensions.html#ps-integration-lambda-extensions-add + "arn:aws:lambda:eu-central-1:580247275435:layer:LambdaInsightsExtension-Arm64:25" # https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Lambda-Insights-extension-versionsARM.html + ] lifecycle { # Changes to the function's source code are deployed using `.github/workflows/deploy-email-function.yml` @@ -30,6 +31,12 @@ resource "aws_lambda_function" "main" { } environment { - variables = var.environment_variables + variables = merge( + { + TZ = "Europe/Berlin", + NODE_ENV = "production" + }, + var.environment_variables + ) } } diff --git a/terraform/modules/scheduled-lambda/iam.tf b/terraform/modules/scheduled-lambda/iam.tf index b5136624a..b6ec246de 100644 --- a/terraform/modules/scheduled-lambda/iam.tf +++ b/terraform/modules/scheduled-lambda/iam.tf @@ -51,6 +51,30 @@ resource "aws_iam_role_policy_attachment" "main_aws_lambda_role" { policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaRole" } +resource "aws_iam_role_policy" "main_parameter_store" { + role = aws_iam_role.main.id + name = "parameter-store" + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = [ + "kms:Decrypt", + "ssm:GetParameter", + ] + Effect = "Allow" + Resource = concat( + [ + data.aws_kms_alias.ssm.target_key_arn, + ], + [for param in var.parameters : "arn:aws:ssm:eu-central-1:${var.account_id}:parameter${param}"] + ) + } + ] + }) +} + resource "aws_iam_role_policy" "main_eventbridge" { role = aws_iam_role.main.id name = "eventbridge" @@ -70,3 +94,7 @@ resource "aws_iam_role_policy" "main_eventbridge" { ] }) } + +data "aws_kms_alias" "ssm" { + name = "alias/aws/ssm" +} diff --git a/terraform/modules/scheduled-lambda/variables.tf b/terraform/modules/scheduled-lambda/variables.tf index 467f1a1f3..7429dc04e 100644 --- a/terraform/modules/scheduled-lambda/variables.tf +++ b/terraform/modules/scheduled-lambda/variables.tf @@ -2,6 +2,10 @@ variable "function_name" { type = string } +variable "runtime" { + type = string +} + variable "environment_variables" { type = map(string) sensitive = true @@ -36,3 +40,8 @@ variable "event_bus" { arn = string }) } + +variable "parameters" { + type = list(string) + default = [] +} diff --git a/terraform/notification-router.tf b/terraform/notification-router.tf index 2a750363f..fe3ae32fd 100644 --- a/terraform/notification-router.tf +++ b/terraform/notification-router.tf @@ -12,4 +12,9 @@ module "notification_router" { environment_variables = var.notification_router_environment_variables batch_size = 10 batch_window = 30 + parameters = [ + "/database/connection_string", + "/web_push/private_vapid_key", + "/web_push/public_vapid_key", + ] } diff --git a/terraform/scrape-discord-events-function.tf b/terraform/scrape-discord-events-function.tf index 676adf938..581b76abf 100644 --- a/terraform/scrape-discord-events-function.tf +++ b/terraform/scrape-discord-events-function.tf @@ -5,7 +5,17 @@ module "scrape_discord_events_function" { account_id = data.aws_caller_identity.current.account_id timeout = 180 memory_size = 256 - environment_variables = var.scrape_discord_events_function_environment_variables + environment_variables = merge( + { + AWS_EVENT_BUS_ARN = data.aws_cloudwatch_event_bus.default.arn + }, + var.scrape_discord_events_function_environment_variables + ) schedule_expression = "rate(4 minutes)" event_bus = data.aws_cloudwatch_event_bus.default + runtime = "nodejs22.x" + parameters = [ + "/database/connection_string", + "/discord/bot_token", + ] } diff --git a/terraform/variables.tf b/terraform/variables.tf index f1901e4d4..65d3e8f10 100644 --- a/terraform/variables.tf +++ b/terraform/variables.tf @@ -1,11 +1,3 @@ -variable "email_function_parameters" { - type = list(object({ - name = string - value = string - })) - sensitive = true -} - # TF_VAR_scrape_discord_events_function_environment_variables = {"VAR_1":"value","VAR_2":"value"} variable "scrape_discord_events_function_environment_variables" { type = map(string)