From 4ec6dcb64b875d76000a069b9a60964c5b7b4f95 Mon Sep 17 00:00:00 2001 From: Omran Jamal Date: Fri, 4 Oct 2024 01:46:58 +0600 Subject: [PATCH] functional sms notifications --- apps/jonogon-core/package.json | 2 + apps/jonogon-core/src/api/queues/index.mts | 26 +++ apps/jonogon-core/src/api/trpc/index.mts | 2 + .../src/api/trpc/procedures/comments/crud.mts | 3 +- apps/jonogon-core/src/db/postgres/types.mts | 2 +- apps/jonogon-core/src/index.mts | 11 + .../queues/milestoneDetectionQueue.mts | 57 +++++ .../queues/notificationsSchedulerQueue.mts | 32 +++ .../queues/smsNotificationDispatchQueue.mts | 220 ++++++++++++++++++ ...727773354446_create-notifications-table.ts | 1 + pnpm-lock.yaml | 135 ++++++++++- 11 files changed, 487 insertions(+), 4 deletions(-) create mode 100644 apps/jonogon-core/src/api/queues/index.mts create mode 100644 apps/jonogon-core/src/services/queues/milestoneDetectionQueue.mts create mode 100644 apps/jonogon-core/src/services/queues/notificationsSchedulerQueue.mts create mode 100644 apps/jonogon-core/src/services/queues/smsNotificationDispatchQueue.mts diff --git a/apps/jonogon-core/package.json b/apps/jonogon-core/package.json index 49da15de..3e862782 100644 --- a/apps/jonogon-core/package.json +++ b/apps/jonogon-core/package.json @@ -17,7 +17,9 @@ "@t3-oss/env-core": "0.11.0", "@trpc/server": "10.45.2", "bufferutil": "4.0.8", + "bull": "^4.16.3", "cors": "2.8.5", + "dedent": "^1.5.3", "es-toolkit": "1.15.1", "express": "4.19.2", "firebase-admin": "12.4.0", diff --git a/apps/jonogon-core/src/api/queues/index.mts b/apps/jonogon-core/src/api/queues/index.mts new file mode 100644 index 00000000..113c2b39 --- /dev/null +++ b/apps/jonogon-core/src/api/queues/index.mts @@ -0,0 +1,26 @@ +import {milestoneDetectionQueue} from '../../services/queues/milestoneDetectionQueue.mjs'; +import {notificationsSchedulerQueue} from '../../services/queues/notificationsSchedulerQueue.mjs'; + +export async function initQueues() { + await milestoneDetectionQueue.add( + {}, + { + jobId: 'detect-milestone', + repeat: { + // 6pm every day + cron: '0 18 * * *', + }, + }, + ); + + await notificationsSchedulerQueue.add( + {}, + { + jobId: 'aggregate-notifications', + repeat: { + // 8pm every day + cron: '0 20 * * *', + }, + }, + ); +} diff --git a/apps/jonogon-core/src/api/trpc/index.mts b/apps/jonogon-core/src/api/trpc/index.mts index 4f693b07..d77d1d06 100644 --- a/apps/jonogon-core/src/api/trpc/index.mts +++ b/apps/jonogon-core/src/api/trpc/index.mts @@ -7,3 +7,5 @@ export const router = t.router; export const middleware = t.middleware; export const publicProcedure = t.procedure; + +export const createCallerFactory = t.createCallerFactory; diff --git a/apps/jonogon-core/src/api/trpc/procedures/comments/crud.mts b/apps/jonogon-core/src/api/trpc/procedures/comments/crud.mts index 11460008..66814e36 100644 --- a/apps/jonogon-core/src/api/trpc/procedures/comments/crud.mts +++ b/apps/jonogon-core/src/api/trpc/procedures/comments/crud.mts @@ -450,7 +450,8 @@ export const createComment = protectedProcedure // notify all the other users who commented on the same comment const otherCommenters = await ctx.services.postgresQueryBuilder .selectFrom('comments') - .select(['created_by']) + .select('created_by') + .distinct() .where('parent_id', '=', `${input.parent_id}`) .where('id', '<>', `${created.id}`) .execute(); diff --git a/apps/jonogon-core/src/db/postgres/types.mts b/apps/jonogon-core/src/db/postgres/types.mts index 607ac632..9e0d740c 100644 --- a/apps/jonogon-core/src/db/postgres/types.mts +++ b/apps/jonogon-core/src/db/postgres/types.mts @@ -53,7 +53,7 @@ export interface Notifications { meta: Json | null; petition_id: Int8 | null; reply_comment_id: Int8 | null; - type: string | null; + type: string; user_id: Int8; vote_id: Int8 | null; } diff --git a/apps/jonogon-core/src/index.mts b/apps/jonogon-core/src/index.mts index b717e115..a3d4a514 100644 --- a/apps/jonogon-core/src/index.mts +++ b/apps/jonogon-core/src/index.mts @@ -8,6 +8,10 @@ import {registerWSHandlers} from './api/websocket/index.mjs'; import {logger} from './logger.mjs'; import {createServices} from './services.mjs'; import cors from 'cors'; +import {initQueues} from './api/queues/index.mjs'; +import {processMilestoneDetectionQueue} from './services/queues/milestoneDetectionQueue.mjs'; +import {processNotificationsSchedulerQueue} from './services/queues/notificationsSchedulerQueue.mjs'; +import {processSmsNotificationDispatchQueue} from './services/queues/smsNotificationDispatchQueue.mjs'; const services = await createServices(); @@ -42,4 +46,11 @@ server.listen(env.PORT, '0.0.0.0', () => { }); }); +// QUEUES +await initQueues(); + +processMilestoneDetectionQueue(services); +processNotificationsSchedulerQueue(services); +processSmsNotificationDispatchQueue(services); + export {TAppRouter} from './api/trpc/routers/index.mjs'; diff --git a/apps/jonogon-core/src/services/queues/milestoneDetectionQueue.mts b/apps/jonogon-core/src/services/queues/milestoneDetectionQueue.mts new file mode 100644 index 00000000..4648b5e9 --- /dev/null +++ b/apps/jonogon-core/src/services/queues/milestoneDetectionQueue.mts @@ -0,0 +1,57 @@ +import Queue from 'bull'; +import {env} from '../../env.mjs'; +import {TServices} from '../../services.mjs'; +import {appRouter} from '../../api/trpc/routers/index.mjs'; + +export const milestoneDetectionQueue = new Queue<{}>( + 'milestone_detection_queue', + env.REDIS_CONNECTION_URL, +); + +export function processMilestoneDetectionQueue(services: TServices) { + const caller = appRouter.createCaller({services}); + + milestoneDetectionQueue.process(async (job) => { + const firstPagePetitions = await caller.petitions.list({ + sort: 'votes', + order: 'desc', + filter: 'request', + page: 0, + }); + + const top5 = firstPagePetitions.data.slice(0, 5); + + const topPetitionIDs = top5.map((petition) => `${petition.data.id}`); + const topPetitionSet = new Set(topPetitionIDs); + + const notifications = await services.postgresQueryBuilder + .selectFrom('notifications') + .select(['petition_id']) + .where('type', '=', 'top') + .where('petition_id', 'in', topPetitionIDs) + .execute(); + + notifications.forEach((notification) => { + topPetitionSet.delete(`${notification.petition_id}`); + }); + + const nextNotifications = [...topPetitionSet] + .map((petition_id) => { + return top5.find( + (petition) => petition.data.id === petition_id, + ); + }) + .filter((petition) => !!petition) + .map((petition) => ({ + type: 'top', + petition_id: petition.data.id, + user_id: petition.data.created_by.id, + })); + + await services.postgresQueryBuilder + .insertInto('notifications') + .values(nextNotifications) + .returning(['id']) + .execute(); + }); +} diff --git a/apps/jonogon-core/src/services/queues/notificationsSchedulerQueue.mts b/apps/jonogon-core/src/services/queues/notificationsSchedulerQueue.mts new file mode 100644 index 00000000..3a31c0db --- /dev/null +++ b/apps/jonogon-core/src/services/queues/notificationsSchedulerQueue.mts @@ -0,0 +1,32 @@ +import Queue from 'bull'; +import {env} from '../../env.mjs'; +import {TServices} from '../../services.mjs'; +import {smsNotificationDispatchQueue} from './smsNotificationDispatchQueue.mjs'; + +export const notificationsSchedulerQueue = new Queue<{}>( + 'notification_aggregator_queue', + env.REDIS_CONNECTION_URL, +); + +export function processNotificationsSchedulerQueue(services: TServices) { + notificationsSchedulerQueue.process(async (job) => { + const result = await services.postgresQueryBuilder + .selectFrom('notifications') + .select('user_id') + .distinct() + .where( + 'created_at', + '>=', + new Date(Date.now() - 24 * 60 * 60 * 1000), + ) + .execute(); + + await smsNotificationDispatchQueue.addBulk( + result.map((result) => ({ + data: { + user_id: result.user_id, + }, + })), + ); + }); +} diff --git a/apps/jonogon-core/src/services/queues/smsNotificationDispatchQueue.mts b/apps/jonogon-core/src/services/queues/smsNotificationDispatchQueue.mts new file mode 100644 index 00000000..af98d199 --- /dev/null +++ b/apps/jonogon-core/src/services/queues/smsNotificationDispatchQueue.mts @@ -0,0 +1,220 @@ +import Queue from 'bull'; +import {env} from '../../env.mjs'; +import {TServices} from '../../services.mjs'; +import {groupBy, countBy} from 'es-toolkit'; +import {decrypt} from '../../lib/crypto/encryption.mjs'; +import {deriveKey} from '../../lib/crypto/keys.mjs'; + +export const smsNotificationDispatchQueue = new Queue<{user_id: string}>( + 'sms_notification_dispatch_queue', + { + redis: env.REDIS_CONNECTION_URL, + limiter: { + max: 120, + duration: 60_000, + }, + }, +); + +export function processSmsNotificationDispatchQueue(services: TServices) { + smsNotificationDispatchQueue.process(async (job) => { + const user = await services.postgresQueryBuilder + .selectFrom('users') + .select([ + 'id', + 'phone_number_encryption_iv', + 'phone_number_encryption_key_salt', + 'encrypted_phone_number', + ]) + .where('id', '=', job.data.user_id) + .executeTakeFirst(); + + if (!user) { + return; + } + + const result = await services.postgresQueryBuilder + .selectFrom('notifications') + .where('user_id', '=', job.data.user_id) + .where('actor_user_id', '<>', job.data.user_id) + .selectAll() + .execute(); + + const grouped = groupBy(result, (item) => item.type); + + // PETITION COMMENTS + const postCommentIDs = new Set(); + + for (const notification of grouped.comment ?? []) { + if (notification.comment_id) { + postCommentIDs.add(notification.comment_id); + } + } + + for (const notification of grouped.reply_to_someones_comment ?? []) { + if (notification.reply_comment_id) { + postCommentIDs.add(notification.reply_comment_id); + } + } + + const petitionCommentCount = postCommentIDs.size; + + // COMMENT REPLIES + const commentReplyIDs = new Set(); + + for (const notification of grouped.reply ?? []) { + if (notification.reply_comment_id) { + commentReplyIDs.add(notification.reply_comment_id); + } + } + + const commentReplyCount = commentReplyIDs.size; + + // COMMENT VOTES + const commentVoteIDs = new Set(); + + for (const notification of grouped.reply_vote ?? []) { + if (notification.comment_vote_id) { + commentVoteIDs.add(notification.comment_vote_id); + } + } + + for (const notification of grouped.comment_vote ?? []) { + if (notification.comment_vote_id) { + commentVoteIDs.add(notification.comment_vote_id); + } + } + + const commentVoteCount = commentVoteIDs.size; + + // PETITION VOTES + const petitionVoteIDs = new Set(); + + for (const notification of grouped.vote ?? []) { + if (notification.vote_id) { + petitionVoteIDs.add(notification.vote_id); + } + } + + const petitionVoteCount = petitionVoteIDs.size; + + // PETITION MODERATION + const petitionStatus: { + [petition_id: string]: 'approved' | 'rejected' | 'formalized'; + } = {}; + + for (const notification of grouped.petition_approved ?? []) { + if (notification.petition_id) { + petitionStatus[notification.petition_id] = 'approved'; + } + } + + for (const notification of grouped.petition_rejected ?? []) { + if (notification.petition_id) { + petitionStatus[notification.petition_id] = 'rejected'; + } + } + + for (const notification of grouped.petition_formalized ?? []) { + if (notification.petition_id) { + petitionStatus[notification.petition_id] = 'formalized'; + } + } + + const petitionStatusCounts = countBy( + Object.values(petitionStatus), + (item) => item, + ); + + const approvedPetitionCount = petitionStatusCounts.approved ?? 0; + const rejectedPetitionCount = petitionStatusCounts.rejected ?? 0; + const formalizedPetitionCount = petitionStatusCounts.formalized ?? 0; + + // PETITION MILESTONES + const topPetitionIDs = new Set(); + + for (const notification of grouped.top ?? []) { + if (notification.petition_id) { + topPetitionIDs.add(notification.petition_id); + } + } + + const topPetitionCount = topPetitionIDs.size; + + let message = `Your https://jonogon.org in the last 24 hours:\n`; + + if (topPetitionCount > 0) { + if (topPetitionCount === 1) { + message += '- 1 petition is in top 5\n'; + } else { + message += `- ${topPetitionCount} petitions are top 5\n`; + } + } + + if (petitionVoteCount > 0) { + if (petitionVoteCount === 1) { + message += '- 1 vote on a petition\n'; + } else { + message += `- ${petitionVoteCount} votes on your petitions\n`; + } + } + + if (petitionCommentCount > 0) { + if (petitionCommentCount === 1) { + message += '- 1 new comment on a petition\n'; + } else { + message += `- ${petitionCommentCount} new comments across your petitions\n`; + } + } + + if (commentReplyCount > 0) { + if (commentReplyCount === 1) { + message += '- 1 reply to a comment\n'; + } else { + message += `- ${commentReplyCount} replies to your comments\n`; + } + } + + if (commentVoteCount > 0) { + if (commentVoteCount === 1) { + message += '- 1 vote on a comment\n'; + } else { + message += `- ${commentVoteCount} votes on your comments\n`; + } + } + + if (approvedPetitionCount > 0) { + if (approvedPetitionCount === 1) { + message += '- 1 petition was approved\n'; + } else { + message += `- ${approvedPetitionCount} petitions were approved\n`; + } + } + + if (rejectedPetitionCount > 0) { + if (rejectedPetitionCount === 1) { + message += '- 1 petition was rejected\n'; + } else { + message += `- ${rejectedPetitionCount} petitions were rejected\n`; + } + } + + if (formalizedPetitionCount > 0) { + if (formalizedPetitionCount === 1) { + message += '- 1 petition was formalized\n'; + } else { + message += `- ${formalizedPetitionCount} petitions were formalized\n`; + } + } + + const key = await deriveKey( + env.COMMON_ENCRYPTION_SECRET, + user.phone_number_encryption_key_salt, + ); + + const iv = Buffer.from(user.phone_number_encryption_iv, 'base64'); + const number = decrypt(key, iv, user.encrypted_phone_number); + + await services.smsService.sendSMS(number, message); + }); +} diff --git a/misc/migrator/migrations/1727773354446_create-notifications-table.ts b/misc/migrator/migrations/1727773354446_create-notifications-table.ts index fd8e3eb6..fb1b83eb 100644 --- a/misc/migrator/migrations/1727773354446_create-notifications-table.ts +++ b/misc/migrator/migrations/1727773354446_create-notifications-table.ts @@ -16,6 +16,7 @@ export async function up(pgm: MigrationBuilder): Promise { }, type: { type: 'varchar', + notNull: true, }, actor_user_id: { type: 'bigint', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ead2a5fd..3740be31 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,9 +26,15 @@ importers: bufferutil: specifier: 4.0.8 version: 4.0.8 + bull: + specifier: ^4.16.3 + version: 4.16.3 cors: specifier: 2.8.5 version: 2.8.5 + dedent: + specifier: ^1.5.3 + version: 1.5.3 es-toolkit: specifier: 1.15.1 version: 1.15.1 @@ -1310,6 +1316,36 @@ packages: resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} hasBin: true + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': + resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==} + cpu: [arm64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': + resolution: {integrity: sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==} + cpu: [x64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': + resolution: {integrity: sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==} + cpu: [arm64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': + resolution: {integrity: sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==} + cpu: [arm] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': + resolution: {integrity: sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==} + cpu: [x64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': + resolution: {integrity: sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==} + cpu: [x64] + os: [win32] + '@next/env@14.2.5': resolution: {integrity: sha512-/zZGkrTOsraVfYjGP8uM0p6r0BDT6xWpkjdVbcz66PJVSpwXX3yNiRycxAuDfBKGWBrZBXRuK/YVlkNgxHGwmA==} @@ -2920,6 +2956,10 @@ packages: resolution: {integrity: sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==} engines: {node: '>=6.14.2'} + bull@4.16.3: + resolution: {integrity: sha512-BZbPzNiKXczfZPXBTVhcN73b+CQFHTzVb7yJi1bSYld4/8bDc9oh/j/dYTsQBgOAZIZahFeHO6dPHbVEXXCvCg==} + engines: {node: '>=12'} + busboy@1.6.0: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} @@ -3133,6 +3173,10 @@ packages: crelt@1.0.6: resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} + cron-parser@4.9.0: + resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} + engines: {node: '>=12.0.0'} + cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -3213,6 +3257,14 @@ packages: decode-named-character-reference@1.0.2: resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} + dedent@1.5.3: + resolution: {integrity: sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + deep-equal@2.2.3: resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==} engines: {node: '>= 0.4'} @@ -3986,6 +4038,10 @@ packages: resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} engines: {node: '>=6'} + get-port@5.1.1: + resolution: {integrity: sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==} + engines: {node: '>=8'} + get-source@2.0.12: resolution: {integrity: sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==} @@ -4583,6 +4639,10 @@ packages: peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc + luxon@3.5.0: + resolution: {integrity: sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==} + engines: {node: '>=12'} + magic-string@0.25.9: resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} @@ -4874,6 +4934,13 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + msgpackr-extract@3.0.3: + resolution: {integrity: sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==} + hasBin: true + + msgpackr@1.11.0: + resolution: {integrity: sha512-I8qXuuALqJe5laEBYoFykChhSXLikZmUhccjGsPuSJ/7uPip2TJ7lwdIQwWSAi0jGZDXv4WOP8Qg65QZRuXxXw==} + mustache@4.2.0: resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} hasBin: true @@ -4950,6 +5017,10 @@ packages: resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} engines: {node: '>= 6.13.0'} + node-gyp-build-optional-packages@5.2.2: + resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} + hasBin: true + node-gyp-build@4.8.1: resolution: {integrity: sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==} hasBin: true @@ -7820,6 +7891,24 @@ snapshots: - encoding - supports-color + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': + optional: true + '@next/env@14.2.5': {} '@next/eslint-plugin-next@14.2.5': @@ -9682,6 +9771,18 @@ snapshots: dependencies: node-gyp-build: 4.8.1 + bull@4.16.3: + dependencies: + cron-parser: 4.9.0 + get-port: 5.1.1 + ioredis: 5.4.1 + lodash: 4.17.21 + msgpackr: 1.11.0 + semver: 7.6.3 + uuid: 8.3.2 + transitivePeerDependencies: + - supports-color + busboy@1.6.0: dependencies: streamsearch: 1.1.0 @@ -9888,6 +9989,10 @@ snapshots: crelt@1.0.6: {} + cron-parser@4.9.0: + dependencies: + luxon: 3.5.0 + cross-spawn@7.0.3: dependencies: path-key: 3.1.1 @@ -9952,6 +10057,8 @@ snapshots: dependencies: character-entities: 2.0.2 + dedent@1.5.3: {} + deep-equal@2.2.3: dependencies: array-buffer-byte-length: 1.0.1 @@ -10940,6 +11047,8 @@ snapshots: get-nonce@1.0.1: {} + get-port@5.1.1: {} + get-source@2.0.12: dependencies: data-uri-to-buffer: 2.0.2 @@ -11602,6 +11711,8 @@ snapshots: dependencies: react: 18.3.1 + luxon@3.5.0: {} + magic-string@0.25.9: dependencies: sourcemap-codec: 1.4.8 @@ -12108,6 +12219,22 @@ snapshots: ms@2.1.3: {} + msgpackr-extract@3.0.3: + dependencies: + node-gyp-build-optional-packages: 5.2.2 + optionalDependencies: + '@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.3 + optional: true + + msgpackr@1.11.0: + optionalDependencies: + msgpackr-extract: 3.0.3 + mustache@4.2.0: {} mz@2.7.0: @@ -12166,6 +12293,11 @@ snapshots: node-forge@1.3.1: {} + node-gyp-build-optional-packages@5.2.2: + dependencies: + detect-libc: 2.0.3 + optional: true + node-gyp-build@4.8.1: {} node-pg-migrate@7.6.1(@types/pg@8.11.6)(pg@8.12.0): @@ -13682,8 +13814,7 @@ snapshots: uuid@3.3.2: {} - uuid@8.3.2: - optional: true + uuid@8.3.2: {} uuid@9.0.1: {}