Files
drop/server/internal/notifications/index.ts
Husky 1ae051f066 Update Prisma to 6.11 (#133)
* chore: update prisma to 6.11

more prisma future proofing due to experimental features

* chore: update dependencies

twemoji - new unicode update
argon2 - bux fixes
vue3-carousel - improve mobile experiance
vue-tsc - more stable

* fix: incorrect prisma version in docker

Also remove default value for BUILD_DROP_VERSION, that is now handled in nuxt config

* fix: no logging in prod

* chore: optimize docker builds even more

* fix: revert adoption of prisma driverAdapters

see: https://github.com/prisma/prisma/issues/27486

* chore: optimize dockerignore some more

* Fix `pino-pretty` not being included in build (#135)

* Remove `pino` from frontend

* Fix for downloads and removing of library source (#136)

* fix: downloads and removing library source

* fix: linting

* Fix max file size of 4GB (update droplet) (#137)

* Fix manual metadata import (#138)

* chore(deps): bump vue-i18n from 10.0.7 to 10.0.8 (#140)

Bumps [vue-i18n](https://github.com/intlify/vue-i18n/tree/HEAD/packages/vue-i18n) from 10.0.7 to 10.0.8.
- [Release notes](https://github.com/intlify/vue-i18n/releases)
- [Changelog](https://github.com/intlify/vue-i18n/blob/master/CHANGELOG.md)
- [Commits](https://github.com/intlify/vue-i18n/commits/v10.0.8/packages/vue-i18n)

---
updated-dependencies:
- dependency-name: vue-i18n
  dependency-version: 10.0.8
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump @intlify/core from 10.0.7 to 10.0.8 (#139)

---
updated-dependencies:
- dependency-name: "@intlify/core"
  dependency-version: 10.0.8
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Small fixes (#141)

* fix: save task as Json rather than string

* fix: pull objects before creating game in database

* fix: strips relative dirs from version information

* fix: #132

* fix: lint

* fix: news object ids and small tweaks

* fix: notification styling errors

* fix: lint

* fix: build issues by regenerating lockfile

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: DecDuck <declanahofmeyr@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-25 21:28:00 +10:00

155 lines
4.2 KiB
TypeScript

/*
The notification system handles the recieving, creation and sending of notifications in Drop
Design goals:
1. Nonce-based notifications; notifications should only be created once
2. Real-time; use websocket listeners to keep clients up-to-date
*/
import type { NotificationModel } from "~/prisma/client/models";
import prisma from "../db/database";
import type { GlobalACL } from "../acls";
// type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
// TODO: document notification action format
export type NotificationCreateArgs = Pick<
NotificationModel,
"title" | "description" | "actions" | "nonce"
> & { acls: Array<GlobalACL> };
class NotificationSystem {
// userId to acl to listenerId
private listeners = new Map<
string,
Map<
string,
{ callback: (notification: NotificationModel) => void; acls: GlobalACL[] }
>
>();
listen(
userId: string,
acls: Array<GlobalACL>,
id: string,
callback: (notification: NotificationModel) => void,
) {
if (!this.listeners.has(userId)) this.listeners.set(userId, new Map());
// eslint-disable-next-line @typescript-eslint/no-extra-non-null-assertion
this.listeners.get(userId)!!.set(id, { callback, acls });
this.catchupListener(userId, id);
}
unlisten(userId: string, id: string) {
this.listeners.get(userId)?.delete(id);
}
private async catchupListener(userId: string, id: string) {
const listener = this.listeners.get(userId)?.get(id);
if (!listener)
throw new Error("Failed to catch-up listener: callback does not exist");
const notifications = await prisma.notification.findMany({
where: { userId: userId, acls: { hasSome: listener.acls } },
orderBy: {
created: "asc", // Oldest first, because they arrive in reverse order
},
});
for (const notification of notifications) {
await listener.callback(notification);
}
}
private async pushNotification(
userId: string,
notification: NotificationModel,
) {
for (const [_, listener] of this.listeners.get(userId) ?? []) {
const hasSome =
notification.acls.findIndex(
(e) => listener.acls.findIndex((v) => v === e) != -1,
) != -1;
if (hasSome) await listener.callback(notification);
}
}
async push(userId: string, notificationCreateArgs: NotificationCreateArgs) {
if (!notificationCreateArgs.nonce)
throw new Error("No nonce in notificationCreateArgs");
const notification = await prisma.notification.upsert({
where: {
userId_nonce: {
nonce: notificationCreateArgs.nonce,
userId,
},
},
update: {
// we don't need to update the userid right?
// userId: userId,
...notificationCreateArgs,
},
create: {
userId,
...notificationCreateArgs,
},
});
await this.pushNotification(userId, notification);
}
/**
* Internal call to batch push notifications to many users
* @param notificationCreateArgs
* @param users
*/
private async _pushMany(
notificationCreateArgs: NotificationCreateArgs,
users: { id: string }[],
) {
const res: Promise<void>[] = [];
for (const user of users) {
res.push(this.push(user.id, notificationCreateArgs));
}
// wait for all notifications to pass
await Promise.all(res);
}
/**
* Send a notification to all users
* @param notificationCreateArgs
*/
async pushAll(notificationCreateArgs: NotificationCreateArgs) {
const users = await prisma.user.findMany({
where: { id: { not: "system" } },
select: {
id: true,
},
});
await this._pushMany(notificationCreateArgs, users);
}
/**
* Send a notification to all system level users
* @param notificationCreateArgs
* @returns
*/
async systemPush(notificationCreateArgs: NotificationCreateArgs) {
const users = await prisma.user.findMany({
where: {
id: { not: "system" },
// no reason to send to any users other then admins rn
admin: true,
},
select: {
id: true,
},
});
await this._pushMany(notificationCreateArgs, users);
}
}
export const notificationSystem = new NotificationSystem();
export default notificationSystem;