فهرست منبع

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

[web] add a new script for clearing institution notifications

GitOrigin-RevId: d102b484c4fb9816832b48c2dfa66953bc996667
Jakob Ackermann 5 سال پیش
والد
کامیت
7996f90c7b

+ 2 - 0
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'))
 

+ 16 - 0
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 }

+ 25 - 0
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 })
+      }
+    )
+  },
 }

+ 33 - 1
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

+ 34 - 0
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)
+    })
+  },
 }

+ 47 - 0
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)
+    })
+}