Merge pull request #5306 from overleaf/jpa-clear-institution-notifications

[web] add a new script for clearing institution notifications

GitOrigin-RevId: d102b484c4fb9816832b48c2dfa66953bc996667
This commit is contained in:
Jakob Ackermann
2021-10-20 12:18:14 +02:00
committed by Copybot
parent fae4b96762
commit dc80bce41c
6 changed files with 157 additions and 1 deletions

View File

@@ -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'))

View File

@@ -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 }

View File

@@ -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 })
}
)
},
}

View File

@@ -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

View File

@@ -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)
})
},
}

View File

@@ -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)
})
}