From 2f35f2bb65744ec7de1415f918515e81dcd13291 Mon Sep 17 00:00:00 2001 From: Antoine Clausse Date: Thu, 2 Apr 2026 11:26:56 +0200 Subject: [PATCH] [web] Re-add remove_unwanted_ieee_collabratec_users script (#32603) * Revert "Merge pull request #19398 from overleaf/rh-rm-ieee-notification" This reverts commit 14ec3e50ed4913b815620f5215df59b17fc03054, reversing changes made to 326352c7c459063bfddf98937e830565c5422ce2. * Convert remove_unwanted_ieee_collabratec_users to ESM * Use scriptRunner * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> GitOrigin-RevId: f7d9a4c74173e38789b053792597f761d91efa4f --- ...remove_unwanted_ieee_collabratec_users.mjs | 195 ++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 services/web/scripts/remove_unwanted_ieee_collabratec_users.mjs diff --git a/services/web/scripts/remove_unwanted_ieee_collabratec_users.mjs b/services/web/scripts/remove_unwanted_ieee_collabratec_users.mjs new file mode 100644 index 0000000000..5b35576d79 --- /dev/null +++ b/services/web/scripts/remove_unwanted_ieee_collabratec_users.mjs @@ -0,0 +1,195 @@ +import path from 'node:path' +import fs from 'node:fs' +import AnalyticsManager from '../app/src/Features/Analytics/AnalyticsManager.mjs' +import { waitForDb, db } from '../app/src/infrastructure/mongodb.mjs' +import { Subscription } from '../app/src/models/Subscription.mjs' +import minimist from 'minimist' +import { promiseMapWithLimit } from '@overleaf/promise-utils' +import _ from 'lodash' +import { scriptRunner } from './lib/ScriptRunner.mjs' + +/** + * This script is used to remove some users from the IEEEPublications group. + * + * Parameters: + * --filename: the filename of the JSON file containing emails of users that + * should **not** be removed + * --commit: if present, the script will commit the changes to the database. + * + * Usage: + * - dry run: + * node scripts/remove_unwanted_ieee_collabratec_users.mjs --filename=emails-to-keep.json + * - commit: + * node scripts/remove_unwanted_ieee_collabratec_users.mjs --filename=emails-to-keep.json --commit + */ + +let COMMIT = false +let EMAILS_FILENAME + +/** + * The IEEE have provided us with a list of active users that should not be removed + * This method retrieves those users. + */ +function getActiveUserEmails(filename) { + const data = fs.readFileSync(path.join(import.meta.dirname, filename), 'utf8') + const emailsArray = JSON.parse(data) + const emailsSet = new Set(emailsArray) + console.log( + `Read ${emailsSet.size} (${emailsArray.length} in array) emails from ${filename}` + ) + return emailsSet +} + +async function getIEEEUsers() { + const results = await db.subscriptions + .aggregate([ + { $match: { teamName: 'IEEEPublications' } }, + { $unwind: '$member_ids' }, + { + $lookup: { + from: 'users', + localField: 'member_ids', + foreignField: '_id', + as: 'member_details', + }, + }, + { + $project: { + _id: 1, + teamName: 1, + 'member_details._id': 1, + 'member_details.email': 1, + 'member_details.emails.email': 1, + }, + }, + ]) + .toArray() + + return results + .map(subscription => subscription.member_details[0]) + .filter(Boolean) +} + +async function main() { + const start = performance.now() + + if (!EMAILS_FILENAME) { + throw new Error('No email filename provided') + } + + await waitForDb() + const subscription = await Subscription.findOne({ + teamName: 'IEEEPublications', + }) + + if (!subscription) { + console.error(`No IEEEPublications group subscription found so quitting`) + return + } + + /** + * @type {string[]} + */ + const oldMemberIds = subscription.member_ids.map(id => id.toString()) + + console.log( + `Found ${oldMemberIds.length} members_ids in IEEEPublications group` + ) + + const usersArray = await getIEEEUsers() + console.log( + `Found ${usersArray.length} users in IEEEPublications group. (${oldMemberIds.length - usersArray.length} missing)` + ) + + const activeUsers = getActiveUserEmails(EMAILS_FILENAME) + + const activeUsersFound = new Set() + + /** + * @type {string[]} + */ + const memberIdsToRemove = [] + + let index = 0 + + // Then go through each IEEEPublications group member to see if we need to remove them + await promiseMapWithLimit(10, usersArray, async userDetails => { + if (index % 1000 === 0) + console.log( + `progress: ${index} / ${usersArray.length} (${memberIdsToRemove.length} to remove)` + ) + + index = index + 1 + + if (COMMIT) { + await AnalyticsManager.setUserPropertyForUser( + userDetails._id.toString(), + 'ieee-retirement', + true + ) + } + + for (const email of userDetails.emails) { + if (activeUsers.has(email.email)) { + activeUsersFound.add(email.email) + return + } + } + + memberIdsToRemove.push(userDetails._id.toString()) + }) + + console.log(`Found ${memberIdsToRemove.length} users to remove`) + + /** + * @type {string[]} + */ + const memberIdsToKeep = _.difference(oldMemberIds, memberIdsToRemove) + + console.log(`Keeping ${memberIdsToKeep.length} users`) + + if (COMMIT) { + await Subscription.updateOne( + { teamName: 'IEEEPublications' }, + { member_ids: memberIdsToKeep } + ) + } + + console.log(`Found ${activeUsersFound.size} active users`) + + const activeUsersNotFound = Array.from(activeUsers).filter( + user => !activeUsersFound.has(user) + ) + + console.log(`${activeUsersNotFound.length} IEEE active users not found:`) + console.log(activeUsersNotFound) + + const subscriptionAfter = await Subscription.findOne({ + teamName: 'IEEEPublications', + }) + console.log( + `There are now ${subscriptionAfter?.member_ids?.length} member_ids in IEEEPublications group` + ) + + const end = performance.now() + console.log(`Took ${end - start} ms`) +} + +const setup = () => { + const argv = minimist(process.argv.slice(2)) + COMMIT = argv.commit !== undefined + EMAILS_FILENAME = argv.filename + if (!COMMIT) { + console.warn('Doing dry run. Add --commit to commit changes') + } +} + +setup() + +try { + await scriptRunner(main) + process.exit(0) +} catch (error) { + console.error(error) + process.exit(1) +}