Skip to content

Commit

Permalink
Batch notifications into groups of 50
Browse files Browse the repository at this point in the history
  • Loading branch information
Timothy-Gonzalez committed Feb 24, 2024
1 parent 42b6570 commit 850fa0d
Show file tree
Hide file tree
Showing 2 changed files with 97 additions and 50 deletions.
1 change: 1 addition & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ const Config = {
/* Limits */
LEADERBOARD_QUERY_LIMIT: 25,
MAX_RESUME_SIZE_BYTES: 2 * 1024 * 1024,
NOTIFICATION_BATCH_SIZE: 50,

/* Misc */
SHOP_BYTES_GEN: 2,
Expand Down
146 changes: 96 additions & 50 deletions src/services/notification/notification-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { hasStaffPerms } from "../auth/auth-lib.js";
import { NotificationSendFormat, isValidNotificationSendFormat } from "./notification-formats.js";
import { StaffShift } from "database/staff-db.js";
import { NotificationsMiddleware } from "../../middleware/fcm.js";
import Config from "../../config.js";
import axios from "axios";

const notificationsRouter: Router = Router();

Expand Down Expand Up @@ -38,90 +40,134 @@ notificationsRouter.post("/", strongJwtVerification, async (req: Request, res: R
return res.status(StatusCode.SuccessOK).send({ status: "Success" });
});

// ADMIN ONLY ENDPOINT
// Send a notification to a set of people
// Internal public route used to batch send. Sends notifications to specified users.
// Only accepts Config.NOTIFICATION_BATCH_SIZE users.
notificationsRouter.post(
"/send/",
"/send/batch",
strongJwtVerification,
NotificationsMiddleware,
async (req: Request, res: Response, next: NextFunction) => {
const startTime = new Date();
const admin = res.locals.fcm;
const payload: JwtPayload = res.locals.payload as JwtPayload;

if (!hasStaffPerms(payload)) {
return next(new RouterError(StatusCode.ClientErrorForbidden, "Forbidden"));
}

const sendRequest = req.body as NotificationSendFormat;
sendRequest.role = sendRequest.role?.toUpperCase();

if (!isValidNotificationSendFormat(sendRequest)) {
return next(new RouterError(StatusCode.ClientErrorBadRequest, "BadSendRequest"));
}

let targetUserIds: string[] = [];

if (sendRequest.eventId) {
const eventFollowers = await Models.EventFollowers.findOne({ eventId: sendRequest.eventId });
const eventUserIds = eventFollowers?.followers ?? [];
targetUserIds = targetUserIds.concat(eventUserIds);
}

if (sendRequest.role) {
const roles = await Models.AuthInfo.find({ roles: { $in: [sendRequest.role] } }, "userId");
const roleUserIds = roles.map((x) => x.userId);
targetUserIds = targetUserIds.concat(roleUserIds);
}

if (sendRequest.staffShift) {
const staffShifts: StaffShift[] = await Models.StaffShift.find({ shifts: { $in: [sendRequest.staffShift] } });
const staffUserIds: string[] = staffShifts.map((x) => x.userId);
targetUserIds = targetUserIds.concat(staffUserIds);
const admin = res.locals.fcm;
const sendRequest = req.body as { title: string; body: string; userIds: string[] };
const targetUserIds = sendRequest.userIds;
if (!targetUserIds || !sendRequest.body || !sendRequest.title) {
return next(new RouterError(StatusCode.ClientErrorBadRequest, "InvalidFormat"));
}

if (sendRequest.foodWave) {
const foodwaves = await Models.AttendeeProfile.find({ foodWave: sendRequest.foodWave });
const foodUserIds = foodwaves.map((x) => x.userId);
targetUserIds = targetUserIds.concat(foodUserIds);
if (targetUserIds.length > Config.NOTIFICATION_BATCH_SIZE) {
return next(new RouterError(StatusCode.ClientErrorBadRequest, "TooManyUsers"));
}

const messageTemplate = {
notification: {
title: sendRequest.title,
body: sendRequest.body,
},
};

const startTime = new Date();
const notifMappings = await Models.NotificationMappings.find({ userId: { $in: targetUserIds } }).exec();
const deviceTokens = notifMappings.map((x) => x?.deviceToken).filter((x): x is string => x != undefined);

let error_ct = 0;
let errors = 0;
const messages = deviceTokens.map((token) =>
admin
.messaging()
.send({ token: token, ...messageTemplate })
.catch(() => {
error_ct++;
errors++;
}),
);

await Promise.all(messages);

await Models.NotificationMessages.create({
sender: payload.id,
title: sendRequest.title,
body: sendRequest.body,
recipientCount: targetUserIds.length,
});

const endTime = new Date();
const timeElapsed = endTime.getTime() - startTime.getTime();

return res
.status(StatusCode.SuccessOK)
.send({ status: "Success", recipients: targetUserIds.length, errors: error_ct, time_ms: timeElapsed });
.send({ status: "Success", recipients: targetUserIds.length, errors: errors, time_ms: timeElapsed });
},
);

// ADMIN ONLY ENDPOINT
// Send a notification to a set of people
notificationsRouter.post("/send/", strongJwtVerification, async (req: Request, res: Response, next: NextFunction) => {
const startTime = new Date();
const payload: JwtPayload = res.locals.payload as JwtPayload;

if (!hasStaffPerms(payload)) {
return next(new RouterError(StatusCode.ClientErrorForbidden, "Forbidden"));
}

const sendRequest = req.body as NotificationSendFormat;
sendRequest.role = sendRequest.role?.toUpperCase();

if (!isValidNotificationSendFormat(sendRequest)) {
return next(new RouterError(StatusCode.ClientErrorBadRequest, "BadSendRequest"));
}

let targetUserIds: string[] = [];

if (sendRequest.eventId) {
const eventFollowers = await Models.EventFollowers.findOne({ eventId: sendRequest.eventId });
const eventUserIds = eventFollowers?.followers ?? [];
targetUserIds = targetUserIds.concat(eventUserIds);
}

if (sendRequest.role) {
const roles = await Models.AuthInfo.find({ roles: { $in: [sendRequest.role] } }, "userId");
const roleUserIds = roles.map((x) => x.userId);
targetUserIds = targetUserIds.concat(roleUserIds);
}

if (sendRequest.staffShift) {
const staffShifts: StaffShift[] = await Models.StaffShift.find({ shifts: { $in: [sendRequest.staffShift] } });
const staffUserIds: string[] = staffShifts.map((x) => x.userId);
targetUserIds = targetUserIds.concat(staffUserIds);
}

if (sendRequest.foodWave) {
const foodwaves = await Models.AttendeeProfile.find({ foodWave: sendRequest.foodWave });
const foodUserIds = foodwaves.map((x) => x.userId);
targetUserIds = targetUserIds.concat(foodUserIds);
}

let recipients = 0;
let errors = 0;
const requests = [];

for (let i = 0; i < targetUserIds.length; i += Config.NOTIFICATION_BATCH_SIZE) {
const thisUserIds = targetUserIds.slice(i, i + Config.NOTIFICATION_BATCH_SIZE);
const baseUrl = `${req.protocol}://${req.get("host")}`;
const reqUrl = `${baseUrl}/notification/send/batch`;
const data = {
title: sendRequest.title,
body: sendRequest.body,
userIds: thisUserIds,
};
const config = {
headers: {
"content-type": req.headers["content-type"],
"user-agent": req.headers["user-agent"],
authorization: req.headers.authorization,
},
};
const request = axios.post(reqUrl, data, config).then((res) => {
recipients += res.data.recipients;
errors += res.data.errors;
});
requests.push(request);
}

await Promise.all(requests);

const endTime = new Date();
const timeElapsed = endTime.getTime() - startTime.getTime();

return res
.status(StatusCode.SuccessOK)
.send({ status: "Success", recipients: recipients, errors: errors, time_ms: timeElapsed });
});

export default notificationsRouter;

0 comments on commit 850fa0d

Please sign in to comment.