| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321 |
- const async = require('async')
- const OError = require('@overleaf/o-error')
- const PlansLocator = require('./PlansLocator')
- const _ = require('underscore')
- const SubscriptionLocator = require('./SubscriptionLocator')
- const UserFeaturesUpdater = require('./UserFeaturesUpdater')
- const Settings = require('settings-sharelatex')
- const logger = require('logger-sharelatex')
- const ReferalFeatures = require('../Referal/ReferalFeatures')
- const V1SubscriptionManager = require('./V1SubscriptionManager')
- const InstitutionsFeatures = require('../Institutions/InstitutionsFeatures')
- const UserGetter = require('../User/UserGetter')
- const FeaturesUpdater = {
- refreshFeatures(userId, callback = () => {}) {
- UserGetter.getUser(userId, { _id: 1, features: 1 }, (err, user) => {
- if (err) {
- return callback(err)
- }
- const oldFeatures = _.clone(user.features)
- FeaturesUpdater._computeFeatures(userId, (error, features) => {
- if (error) {
- return callback(error)
- }
- logger.log({ userId, features }, 'updating user features')
- UserFeaturesUpdater.updateFeatures(
- userId,
- features,
- (err, newFeatures, featuresChanged) => {
- if (err) {
- return callback(err)
- }
- if (oldFeatures.dropbox === true && features.dropbox === false) {
- logger.log({ userId }, '[FeaturesUpdater] must unlink dropbox')
- const Modules = require('../../infrastructure/Modules')
- Modules.hooks.fire('removeDropbox', userId, err => {
- if (err) {
- logger.error(err)
- }
- })
- }
- return callback(null, newFeatures, featuresChanged)
- }
- )
- })
- })
- },
- _computeFeatures(userId, callback) {
- const jobs = {
- individualFeatures(cb) {
- FeaturesUpdater._getIndividualFeatures(userId, cb)
- },
- groupFeatureSets(cb) {
- FeaturesUpdater._getGroupFeatureSets(userId, cb)
- },
- institutionFeatures(cb) {
- InstitutionsFeatures.getInstitutionsFeatures(userId, cb)
- },
- v1Features(cb) {
- FeaturesUpdater._getV1Features(userId, cb)
- },
- bonusFeatures(cb) {
- ReferalFeatures.getBonusFeatures(userId, cb)
- },
- featuresOverrides(cb) {
- FeaturesUpdater._getFeaturesOverrides(userId, cb)
- },
- }
- async.series(jobs, function (err, results) {
- if (err) {
- OError.tag(
- err,
- 'error getting subscription or group for refreshFeatures',
- {
- userId,
- }
- )
- return callback(err)
- }
- const {
- individualFeatures,
- groupFeatureSets,
- institutionFeatures,
- v1Features,
- bonusFeatures,
- featuresOverrides,
- } = results
- logger.log(
- {
- userId,
- individualFeatures,
- groupFeatureSets,
- institutionFeatures,
- v1Features,
- bonusFeatures,
- featuresOverrides,
- },
- 'merging user features'
- )
- const featureSets = groupFeatureSets.concat([
- individualFeatures,
- institutionFeatures,
- v1Features,
- bonusFeatures,
- featuresOverrides,
- ])
- const features = _.reduce(
- featureSets,
- FeaturesUpdater._mergeFeatures,
- Settings.defaultFeatures
- )
- callback(null, features)
- })
- },
- _getIndividualFeatures(userId, callback) {
- SubscriptionLocator.getUserIndividualSubscription(userId, (err, sub) =>
- callback(err, FeaturesUpdater._subscriptionToFeatures(sub))
- )
- },
- _getGroupFeatureSets(userId, callback) {
- SubscriptionLocator.getGroupSubscriptionsMemberOf(userId, (err, subs) =>
- callback(err, (subs || []).map(FeaturesUpdater._subscriptionToFeatures))
- )
- },
- _getFeaturesOverrides(userId, callback) {
- UserGetter.getUser(userId, { featuresOverrides: 1 }, (error, user) => {
- if (error) {
- return callback(error)
- }
- if (
- !user ||
- !user.featuresOverrides ||
- user.featuresOverrides.length === 0
- ) {
- return callback(null, {})
- }
- const activeFeaturesOverrides = []
- for (const featuresOverride of user.featuresOverrides) {
- if (
- !featuresOverride.expiresAt ||
- featuresOverride.expiresAt > new Date()
- ) {
- activeFeaturesOverrides.push(featuresOverride.features)
- }
- }
- const features = _.reduce(
- activeFeaturesOverrides,
- FeaturesUpdater._mergeFeatures,
- {}
- )
- callback(null, features)
- })
- },
- _getV1Features(userId, callback) {
- V1SubscriptionManager.getPlanCodeFromV1(
- userId,
- function (err, planCode, v1Id) {
- if (err) {
- if ((err ? err.name : undefined) === 'NotFoundError') {
- return callback(null, [])
- }
- return callback(err)
- }
- callback(
- err,
- FeaturesUpdater._mergeFeatures(
- V1SubscriptionManager.getGrandfatheredFeaturesForV1User(v1Id) || {},
- FeaturesUpdater._planCodeToFeatures(planCode)
- )
- )
- }
- )
- },
- _mergeFeatures(featuresA, featuresB) {
- const features = Object.assign({}, featuresA)
- for (const key in featuresB) {
- // Special merging logic for non-boolean features
- if (key === 'compileGroup') {
- if (
- features.compileGroup === 'priority' ||
- featuresB.compileGroup === 'priority'
- ) {
- features.compileGroup = 'priority'
- } else {
- features.compileGroup = 'standard'
- }
- } else if (key === 'collaborators') {
- if (features.collaborators === -1 || featuresB.collaborators === -1) {
- features.collaborators = -1
- } else {
- features.collaborators = Math.max(
- features.collaborators || 0,
- featuresB.collaborators || 0
- )
- }
- } else if (key === 'compileTimeout') {
- features.compileTimeout = Math.max(
- features.compileTimeout || 0,
- featuresB.compileTimeout || 0
- )
- } else {
- // Boolean keys, true is better
- features[key] = features[key] || featuresB[key]
- }
- }
- return features
- },
- _subscriptionToFeatures(subscription) {
- return FeaturesUpdater._planCodeToFeatures(
- subscription ? subscription.planCode : undefined
- )
- },
- _planCodeToFeatures(planCode) {
- if (!planCode) {
- return {}
- }
- const plan = PlansLocator.findLocalPlanInSettings(planCode)
- if (!plan) {
- return {}
- } else {
- return plan.features
- }
- },
- compareFeatures(currentFeatures, expectedFeatures) {
- currentFeatures = _.clone(currentFeatures)
- expectedFeatures = _.clone(expectedFeatures)
- if (_.isEqual(currentFeatures, expectedFeatures)) {
- return {}
- }
- const mismatchReasons = {}
- const featureKeys = [
- ...new Set([
- ...Object.keys(currentFeatures),
- ...Object.keys(expectedFeatures),
- ]),
- ]
- featureKeys.sort().forEach(key => {
- if (expectedFeatures[key] !== currentFeatures[key]) {
- mismatchReasons[key] = expectedFeatures[key]
- }
- })
- if (mismatchReasons.compileTimeout) {
- // store the compile timeout difference instead of the new compile timeout
- mismatchReasons.compileTimeout =
- expectedFeatures.compileTimeout - currentFeatures.compileTimeout
- }
- if (mismatchReasons.collaborators) {
- // store the collaborators difference instead of the new number only
- // replace -1 by 100 to make it clearer
- if (expectedFeatures.collaborators === -1) {
- expectedFeatures.collaborators = 100
- }
- if (currentFeatures.collaborators === -1) {
- currentFeatures.collaborators = 100
- }
- mismatchReasons.collaborators =
- expectedFeatures.collaborators - currentFeatures.collaborators
- }
- return mismatchReasons
- },
- doSyncFromV1(v1UserId, callback) {
- logger.log({ v1UserId }, '[AccountSync] starting account sync')
- return UserGetter.getUser(
- { 'overleaf.id': v1UserId },
- { _id: 1 },
- function (err, user) {
- if (err != null) {
- OError.tag(err, '[AccountSync] error getting user', {
- v1UserId,
- })
- return callback(err)
- }
- if ((user != null ? user._id : undefined) == null) {
- logger.warn({ v1UserId }, '[AccountSync] no user found for v1 id')
- return callback(null)
- }
- logger.log(
- { v1UserId, userId: user._id },
- '[AccountSync] updating user subscription and features'
- )
- return FeaturesUpdater.refreshFeatures(user._id, callback)
- }
- )
- },
- }
- const refreshFeaturesPromise = userId =>
- new Promise(function (resolve, reject) {
- FeaturesUpdater.refreshFeatures(
- userId,
- (error, features, featuresChanged) => {
- if (error) {
- reject(error)
- } else {
- resolve({ features, featuresChanged })
- }
- }
- )
- })
- FeaturesUpdater.promises = {
- refreshFeatures: refreshFeaturesPromise,
- }
- module.exports = FeaturesUpdater
|