feat: Added notifications outbox to admin panel

This commit is contained in:
2025-05-31 16:57:53 +02:00
parent ec787e2321
commit dd17fd5294
4 changed files with 113 additions and 57 deletions

View File

@@ -1,11 +1,27 @@
import { PushSubscription, RequestOptions, VapidKeys, WebPushError, sendNotification } from "web-push"; import { RequestOptions, SendResult, VapidKeys, WebPushError, sendNotification } from "web-push";
import Notification from "./schemas/Notification"; import Notification from "./schemas/Notification";
import vapidKeys from "./vapidKeys"; import vapidKeys from "./vapidKeys";
import { IUser } from "./schemas/User"; import User, { IUser } from "./schemas/User";
import Inbox from "./schemas/Inbox";
import { Types } from "mongoose";
export class NotifcationHelper { export interface SimpleMessage {
title: string;
body: string;
}
export interface PushResult {
sent: number;
possible: number;
}
export class Message {
private options: RequestOptions private options: RequestOptions
constructor () { private message: { notification: SimpleMessage }
private rcptType: "uname" | "room" | "group"
private rcpt: string
constructor (title: string, body: string, rcptType: "uname" | "room" | "group", rcpt: string) {
let keys: VapidKeys = vapidKeys.keys let keys: VapidKeys = vapidKeys.keys
this.options = { this.options = {
vapidDetails: { vapidDetails: {
@@ -14,27 +30,66 @@ export class NotifcationHelper {
publicKey: keys.publicKey publicKey: keys.publicKey
} }
} }
this.message = { notification: { title: title, body: body } }
this.rcptType = rcptType
this.rcpt = rcpt
} }
private async send(message: string, subscriptions: PushSubscription[]) { async findUserNotif(uname: string) {
var notif = await Notification.find().populate<{user: Pick<IUser, 'uname'> & {_id: Types.ObjectId}}>('user', ['uname', '_id']).exec()
return notif.filter(val => val.user.uname == uname)
}
async findRoomNotif(room: string) {
var notif = await Notification.find().populate<{user: Pick<IUser, 'room'> & {_id: Types.ObjectId}}>('user', ['room', '_id']).exec()
return notif.filter(val => val.user.room == room)
}
async findGroupNotif(groupId: string) {
var notif = await Notification.find().populate<{user: Pick<IUser, 'groups'> & {_id: Types.ObjectId}}>('user', ['groups', '_id']).exec()
return notif.filter(val => val.user.groups.find(x => x.toString() == groupId))
}
public async send(): Promise<PushResult> {
var subscriptions
var rcptIds: Types.ObjectId[]
switch (this.rcptType) {
case "uname":
subscriptions = await this.findUserNotif(this.rcpt)
rcptIds = (await User.find({uname: this.rcpt})).map(v => v._id)
break;
case "room":
subscriptions = await this.findRoomNotif(this.rcpt)
rcptIds = (await User.find({room: this.rcpt})).map(v => v._id)
break;
case "group":
subscriptions = await this.findGroupNotif(this.rcpt)
rcptIds = (await User.find({groups: this.rcpt})).map(v => v._id)
break;
default:
throw new Error(`Wrong recipient type used: ${this.rcptType}`);
}
await Inbox.create({message: this.message.notification, rcpt: rcptIds})
var count = 0; var count = 0;
var subslen = subscriptions.length var subslen = subscriptions.length
for (const v of subscriptions) { for (const v of subscriptions) {
var result var result: SendResult
try { try {
result = await sendNotification(v, message, this.options) result = await sendNotification(v, JSON.stringify(this.message), this.options)
count++ count++
} catch (error) { } catch (error) {
if (error instanceof WebPushError) { if (error instanceof WebPushError) {
switch (error.statusCode) { switch (error.statusCode) {
case 410: case 410:
console.log("GONE") console.log("GONE")
await Notification.findOneAndDelete({endpoint: v.endpoint, keys: v.keys}) await Notification.findByIdAndRemove(v._id)
subslen-- subslen--
break; break;
case 404: case 404:
console.warn("NOT FOUND", error.message) console.warn("NOT FOUND", error.message)
await Notification.findOneAndDelete(v) await Notification.findByIdAndRemove(v._id)
subslen-- subslen--
break; break;
default: default:
@@ -44,39 +99,7 @@ export class NotifcationHelper {
} }
} }
} }
return {sent: count, possible: subslen} return {sent: count, possible: subslen}
} }
private rcpt(message: string) {
return {
user: async (uname: string) => {
return await this.send(message, await this.findUserNotif(uname))
},
room: async (room: string) => {
return await this.send(message, await this.findRoomNotif(room))
},
group: async (group: string) => {
return await this.send(message, await this.findGroupNotif(group))
}
}
}
simpleMessage(title: string, body: string) {
return this.rcpt(JSON.stringify({notification: {title: title, body: body}}))
}
async findUserNotif(uname: string): Promise<Array<any>> {
var notif = await Notification.find().populate<{user: Pick<IUser, 'uname'>}>('user', 'uname').exec()
return notif.filter(val => val.user.uname == uname)
}
async findRoomNotif(room: string): Promise<Array<any>> {
var notif = await Notification.find().populate<{user: Pick<IUser, 'room'>}>('user', 'room').exec()
return notif.filter(val => val.user.room == room)
}
async findGroupNotif(groupId: string): Promise<Array<any>> {
var notif = await Notification.find().populate<{user: Pick<IUser, 'groups'>}>('user', 'groups').exec()
return notif.filter(val => val.user.groups.find(x => x.toString() == groupId))
}
} }

View File

@@ -1,38 +1,44 @@
import { Router } from "express"; import { Request, Response, Router } from "express";
import { Perms, adminPerm } from "@/utility"; import { Perms, adminPerm } from "@/utility";
import Group from "@schemas/Group"; import Group from "@schemas/Group";
import { NotifcationHelper } from "@/notif"; import { PushResult, Message } from "@/notif";
import capability, { Features } from "@/helpers/capability"; import capability, { Features } from "@/helpers/capability";
import Inbox from "@/schemas/Inbox";
import { Types } from "mongoose";
import { IUser } from "@/schemas/User";
const notifRouter = Router() const notifRouter = Router()
const nh = new NotifcationHelper()
notifRouter.use(adminPerm(Perms.Notif)) notifRouter.use(adminPerm(Perms.Notif))
notifRouter.use(capability.mw(Features.Notif)) notifRouter.use(capability.mw(Features.Notif))
notifRouter.post("/send", async (req, res) => { type PushSendBody = {recp:
const message = nh.simpleMessage(req.body.title, req.body.body) {type: "uname", uname: string} |
{type: "room", room: string} |
{type: "group", group: string},
title: string,
body: string
}
notifRouter.post("/send", async (req: Request<undefined, PushResult, PushSendBody>, res: Response<PushResult>) => {
let recp: string let recp: string
let result;
switch (req.body.recp.type) { switch (req.body.recp.type) {
case "uname": case "uname":
recp = req.body.recp.uname recp = req.body.recp.uname
result = await message.user(recp);
break; break;
case "room": case "room":
recp = req.body.recp.room recp = req.body.recp.room
result = await message.room(recp)
break; break;
case "group": case "group":
if (!capability.settings.groups) return res.sendStatus(406).end() if (!capability.settings.groups) return res.sendStatus(406).end()
recp = req.body.recp.group recp = req.body.recp.group
result = await message.group(recp)
break; break;
default: default:
res.status(400).end() res.status(400).end()
break; break;
} }
const message = new Message(req.body.title, req.body.body, req.body.recp.type, recp)
let result: PushResult = await message.send()
console.log(` console.log(`
From: ${req.user.uname} (${req.user._id}) From: ${req.user.uname} (${req.user._id})
To: ${recp} To: ${recp}
@@ -43,6 +49,17 @@ notifRouter.post("/send", async (req, res) => {
res.send(result) res.send(result)
}) })
notifRouter.get("/outbox", async (req, res: Response) => {
var result = await Inbox.find({}, {}, {sort: {sentDate: -1}}).populate<{rcpt: IUser & {_id: Types.ObjectId}}>("rcpt", ['fname', 'surname', 'uname', '_id', 'room']).exec()
var final = result.map(v => {
return {
...v.toJSON(),
ack: v.ack.length
}
})
res.send(final)
})
notifRouter.get("/groups", async (req, res) => { notifRouter.get("/groups", async (req, res) => {
res.send(await Group.find({}, { name: 1, _id: 1 })) res.send(await Group.find({}, { name: 1, _id: 1 }))
}) })

18
src/schemas/Inbox.ts Normal file
View File

@@ -0,0 +1,18 @@
import { SimpleMessage } from "@/notif"
import mongoose, { Types, Schema } from "mongoose"
export interface IInbox {
message: SimpleMessage,
sentDate: Date,
rcpt: Types.ObjectId[],
ack: Types.ObjectId[]
}
const inboxSchema = new Schema<IInbox>({
message: {type: Object, required: true},
sentDate: {type: Date, required: true, default: Date.now()},
rcpt: [{type: Schema.Types.ObjectId, ref: "logins", required: true}],
ack: [{type: Schema.Types.ObjectId, ref: "logins", required: true, default: []}],
})
export default mongoose.model("inbox", inboxSchema)

View File

@@ -1,7 +1,5 @@
import mongoose, { Types, Schema } from "mongoose" import mongoose, { Types, Schema } from "mongoose"
// TODO: Unify `fname` and `surename` into single field
export interface IUser { export interface IUser {
uname: string; uname: string;
pass: string; pass: string;