feat: Added notifications outbox to admin panel
This commit is contained in:
107
src/notif.ts
107
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 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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -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
18
src/schemas/Inbox.ts
Normal 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)
|
||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user