From dc80bce41c686fdb291d2be4ef12eb1d3476f332 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann Date: Wed, 20 Oct 2021 12:18:14 +0200 Subject: [PATCH] Merge pull request #5306 from overleaf/jpa-clear-institution-notifications [web] add a new script for clearing institution notifications GitOrigin-RevId: d102b484c4fb9816832b48c2dfa66953bc996667 --- services/notifications/app.js | 2 + .../notifications/app/js/Notifications.js | 16 +++++++ .../app/js/NotificationsController.js | 25 ++++++++++ .../Institutions/InstitutionsManager.js | 34 +++++++++++++- .../Notifications/NotificationsHandler.js | 34 ++++++++++++++ .../clear_institution_notifications.js | 47 +++++++++++++++++++ 6 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 services/web/scripts/clear_institution_notifications.js diff --git a/services/notifications/app.js b/services/notifications/app.js index 2e04ed4829..7c8639270b 100644 --- a/services/notifications/app.js +++ b/services/notifications/app.js @@ -35,6 +35,8 @@ app.delete( ) app.delete('/user/:user_id', controller.removeNotificationKey) app.delete('/key/:key', controller.removeNotificationByKeyOnly) +app.get('/key/:key/count', controller.countNotificationsByKeyOnly) +app.delete('/key/:key/bulk', controller.deleteUnreadNotificationsByKeyOnlyBulk) app.get('/status', (req, res) => res.send('notifications sharelatex up')) diff --git a/services/notifications/app/js/Notifications.js b/services/notifications/app/js/Notifications.js index d6d18868aa..a804a3de12 100644 --- a/services/notifications/app/js/Notifications.js +++ b/services/notifications/app/js/Notifications.js @@ -112,6 +112,22 @@ module.exports = Notifications = { db.notifications.updateOne(searchOps, updateOperation, callback) }, + countNotificationsByKeyOnly(notificationKey, callback) { + const searchOps = { key: notificationKey, templateKey: { $exists: true } } + db.notifications.count(searchOps, callback) + }, + + deleteUnreadNotificationsByKeyOnlyBulk(notificationKey, callback) { + if (typeof notificationKey !== 'string') { + throw new Error('refusing to bulk delete arbitrary notifications') + } + const searchOps = { key: notificationKey, templateKey: { $exists: true } } + db.notifications.deleteMany(searchOps, (err, result) => { + if (err) return callback(err) + callback(null, result.deletedCount) + }) + }, + // hard delete of doc, rather than removing the templateKey deleteNotificationByKeyOnly(notification_key, callback) { const searchOps = { key: notification_key } diff --git a/services/notifications/app/js/NotificationsController.js b/services/notifications/app/js/NotificationsController.js index 5cd55c252c..934676a96f 100644 --- a/services/notifications/app/js/NotificationsController.js +++ b/services/notifications/app/js/NotificationsController.js @@ -84,4 +84,29 @@ module.exports = { (err, notifications) => res.sendStatus(200) ) }, + + countNotificationsByKeyOnly(req, res) { + const notificationKey = req.params.key + Notifications.countNotificationsByKeyOnly(notificationKey, (err, count) => { + if (err) { + logger.err({ err, notificationKey }, 'cannot count by key') + return res.sendStatus(500) + } + res.json({ count }) + }) + }, + + deleteUnreadNotificationsByKeyOnlyBulk(req, res) { + const notificationKey = req.params.key + Notifications.deleteUnreadNotificationsByKeyOnlyBulk( + notificationKey, + (err, count) => { + if (err) { + logger.err({ err, notificationKey }, 'cannot bulk remove by key') + return res.sendStatus(500) + } + res.json({ count }) + } + ) + }, } diff --git a/services/web/app/src/Features/Institutions/InstitutionsManager.js b/services/web/app/src/Features/Institutions/InstitutionsManager.js index bdd6a790b4..e346057e26 100644 --- a/services/web/app/src/Features/Institutions/InstitutionsManager.js +++ b/services/web/app/src/Features/Institutions/InstitutionsManager.js @@ -1,5 +1,5 @@ const async = require('async') -const { callbackify } = require('util') +const { callbackify, promisify } = require('util') const { ObjectId } = require('mongodb') const Settings = require('@overleaf/settings') const { @@ -10,6 +10,7 @@ const FeaturesUpdater = require('../Subscription/FeaturesUpdater') const FeaturesHelper = require('../Subscription/FeaturesHelper') const UserGetter = require('../User/UserGetter') const NotificationsBuilder = require('../Notifications/NotificationsBuilder') +const NotificationsHandler = require('../Notifications/NotificationsHandler') const SubscriptionLocator = require('../Subscription/SubscriptionLocator') const { Institution } = require('../../models/Institution') const { Subscription } = require('../../models/Subscription') @@ -186,6 +187,34 @@ async function checkInstitutionUsers(institutionId, emitNonProUserIds) { } const InstitutionsManager = { + clearInstitutionNotifications(institutionId, dryRun, callback) { + function clear(key, cb) { + const run = dryRun + ? NotificationsHandler.previewMarkAsReadByKeyOnlyBulk + : NotificationsHandler.markAsReadByKeyOnlyBulk + + run(key, cb) + } + + async.series( + { + ipMatcherAffiliation(cb) { + const key = `ip-matched-affiliation-${institutionId}` + clear(key, cb) + }, + featuresUpgradedByAffiliation(cb) { + const key = `features-updated-by=${institutionId}` + clear(key, cb) + }, + redundantPersonalSubscription(cb) { + const key = `redundant-personal-subscription-${institutionId}` + clear(key, cb) + }, + }, + callback + ) + }, + refreshInstitutionUsers(institutionId, notify, callback) { const refreshFunction = notify ? refreshFeaturesAndNotify : refreshFeatures async.waterfall( @@ -313,6 +342,9 @@ var notifyUser = (user, affiliation, subscription, featuresChanged, callback) => InstitutionsManager.promises = { checkInstitutionUsers, + clearInstitutionNotifications: promisify( + InstitutionsManager.clearInstitutionNotifications + ), } module.exports = InstitutionsManager diff --git a/services/web/app/src/Features/Notifications/NotificationsHandler.js b/services/web/app/src/Features/Notifications/NotificationsHandler.js index 788e14b375..45282b7269 100644 --- a/services/web/app/src/Features/Notifications/NotificationsHandler.js +++ b/services/web/app/src/Features/Notifications/NotificationsHandler.js @@ -101,4 +101,38 @@ module.exports = { } makeRequest(opts, callback) }, + + previewMarkAsReadByKeyOnlyBulk(key, callback) { + const opts = { + uri: `${notificationsApi}/key/${key}/count`, + method: 'GET', + timeout: 10 * oneSecond, + json: true, + } + makeRequest(opts, (err, res, body) => { + if (err) return callback(err) + if (res.statusCode !== 200) { + return callback( + new Error('cannot preview bulk delete notification: ' + key) + ) + } + callback(null, (body && body.count) || 0) + }) + }, + + markAsReadByKeyOnlyBulk(key, callback) { + const opts = { + uri: `${notificationsApi}/key/${key}/bulk`, + method: 'DELETE', + timeout: 10 * oneSecond, + json: true, + } + makeRequest(opts, (err, res, body) => { + if (err) return callback(err) + if (res.statusCode !== 200) { + return callback(new Error('cannot bulk delete notification: ' + key)) + } + callback(null, (body && body.count) || 0) + }) + }, } diff --git a/services/web/scripts/clear_institution_notifications.js b/services/web/scripts/clear_institution_notifications.js new file mode 100644 index 0000000000..052e3624a4 --- /dev/null +++ b/services/web/scripts/clear_institution_notifications.js @@ -0,0 +1,47 @@ +const { promisify } = require('util') +const InstitutionsManager = require('../app/src/Features/Institutions/InstitutionsManager') +const sleep = promisify(setTimeout) + +async function main() { + const institutionId = parseInt(process.argv[2]) + if (isNaN(institutionId)) throw new Error('No institution id') + const dryRun = process.argv.includes('--dry-run') + + console.log('Deleting notifications of institution', institutionId) + + const preview = await InstitutionsManager.promises.clearInstitutionNotifications( + institutionId, + true + ) + console.log('--- Preview ---') + console.log(JSON.stringify(preview, null, 4)) + console.log('---------------') + + if (dryRun) { + console.log('Exiting early due to --dry-run flag') + return + } + + console.log('Exit in the next 10s in case these numbers are off.') + await sleep(10 * 1000) + + const cleared = await InstitutionsManager.promises.clearInstitutionNotifications( + institutionId, + false + ) + console.log('--- Cleared ---') + console.log(JSON.stringify(cleared, null, 4)) + console.log('---------------') +} + +if (require.main === module) { + main() + .then(() => { + console.log('Done.') + process.exit(0) + }) + .catch(err => { + console.error(err) + process.exit(1) + }) +}