diff --git a/src/notif.ts b/src/notif.ts index f330b8f..654d939 100644 --- a/src/notif.ts +++ b/src/notif.ts @@ -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 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 - 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 this.options = { vapidDetails: { @@ -14,27 +30,66 @@ export class NotifcationHelper { 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 & {_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 & {_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 & {_id: Types.ObjectId}}>('user', ['groups', '_id']).exec() + return notif.filter(val => val.user.groups.find(x => x.toString() == groupId)) + } + + public async send(): Promise { + 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 subslen = subscriptions.length for (const v of subscriptions) { - var result + var result: SendResult try { - result = await sendNotification(v, message, this.options) + result = await sendNotification(v, JSON.stringify(this.message), this.options) count++ } catch (error) { if (error instanceof WebPushError) { switch (error.statusCode) { case 410: console.log("GONE") - await Notification.findOneAndDelete({endpoint: v.endpoint, keys: v.keys}) + await Notification.findByIdAndRemove(v._id) subslen-- break; case 404: console.warn("NOT FOUND", error.message) - await Notification.findOneAndDelete(v) + await Notification.findByIdAndRemove(v._id) subslen-- break; default: @@ -44,39 +99,7 @@ export class NotifcationHelper { } } } + 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> { - var notif = await Notification.find().populate<{user: Pick}>('user', 'uname').exec() - return notif.filter(val => val.user.uname == uname) - } - - async findRoomNotif(room: string): Promise> { - var notif = await Notification.find().populate<{user: Pick}>('user', 'room').exec() - return notif.filter(val => val.user.room == room) - } - - async findGroupNotif(groupId: string): Promise> { - var notif = await Notification.find().populate<{user: Pick}>('user', 'groups').exec() - return notif.filter(val => val.user.groups.find(x => x.toString() == groupId)) - } } \ No newline at end of file diff --git a/src/routes/api/admin/notif.ts b/src/routes/api/admin/notif.ts index 80164be..25fc549 100644 --- a/src/routes/api/admin/notif.ts +++ b/src/routes/api/admin/notif.ts @@ -1,38 +1,44 @@ -import { Router } from "express"; +import { Request, Response, Router } from "express"; import { Perms, adminPerm } from "@/utility"; import Group from "@schemas/Group"; -import { NotifcationHelper } from "@/notif"; +import { PushResult, Message } from "@/notif"; import capability, { Features } from "@/helpers/capability"; +import Inbox from "@/schemas/Inbox"; +import { Types } from "mongoose"; +import { IUser } from "@/schemas/User"; const notifRouter = Router() -const nh = new NotifcationHelper() - notifRouter.use(adminPerm(Perms.Notif)) notifRouter.use(capability.mw(Features.Notif)) -notifRouter.post("/send", async (req, res) => { - const message = nh.simpleMessage(req.body.title, req.body.body) +type PushSendBody = {recp: + {type: "uname", uname: string} | + {type: "room", room: string} | + {type: "group", group: string}, + title: string, + body: string +} + +notifRouter.post("/send", async (req: Request, res: Response) => { let recp: string - let result; switch (req.body.recp.type) { case "uname": recp = req.body.recp.uname - result = await message.user(recp); break; case "room": recp = req.body.recp.room - result = await message.room(recp) break; case "group": if (!capability.settings.groups) return res.sendStatus(406).end() recp = req.body.recp.group - result = await message.group(recp) break; default: res.status(400).end() break; } + const message = new Message(req.body.title, req.body.body, req.body.recp.type, recp) + let result: PushResult = await message.send() console.log(` From: ${req.user.uname} (${req.user._id}) To: ${recp} @@ -43,8 +49,19 @@ notifRouter.post("/send", async (req, res) => { res.send(result) }) -notifRouter.get("/groups", async (req,res) => { - res.send(await Group.find({}, {name: 1, _id: 1})) +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) }) -export {notifRouter} \ No newline at end of file +notifRouter.get("/groups", async (req, res) => { + res.send(await Group.find({}, { name: 1, _id: 1 })) +}) + +export { notifRouter } \ No newline at end of file diff --git a/src/schemas/Inbox.ts b/src/schemas/Inbox.ts new file mode 100644 index 0000000..d7f2570 --- /dev/null +++ b/src/schemas/Inbox.ts @@ -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({ + 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) \ No newline at end of file diff --git a/src/schemas/User.ts b/src/schemas/User.ts index 5bb7820..669550c 100644 --- a/src/schemas/User.ts +++ b/src/schemas/User.ts @@ -1,7 +1,5 @@ import mongoose, { Types, Schema } from "mongoose" -// TODO: Unify `fname` and `surename` into single field - export interface IUser { uname: string; pass: string;