2019-05-29 05:21:06 -04:00
|
|
|
const async = require('async')
|
2020-08-11 05:35:08 -04:00
|
|
|
const OError = require('@overleaf/o-error')
|
2019-05-29 05:21:06 -04:00
|
|
|
const PlansLocator = require('./PlansLocator')
|
2021-07-05 11:15:54 -04:00
|
|
|
const _ = require('lodash')
|
2019-05-29 05:21:06 -04:00
|
|
|
const SubscriptionLocator = require('./SubscriptionLocator')
|
|
|
|
const UserFeaturesUpdater = require('./UserFeaturesUpdater')
|
2021-07-07 05:38:56 -04:00
|
|
|
const Settings = require('@overleaf/settings')
|
2019-05-29 05:21:06 -04:00
|
|
|
const logger = require('logger-sharelatex')
|
|
|
|
const ReferalFeatures = require('../Referal/ReferalFeatures')
|
|
|
|
const V1SubscriptionManager = require('./V1SubscriptionManager')
|
|
|
|
const InstitutionsFeatures = require('../Institutions/InstitutionsFeatures')
|
2019-10-05 13:43:21 -04:00
|
|
|
const UserGetter = require('../User/UserGetter')
|
2021-06-16 10:22:15 -04:00
|
|
|
const AnalyticsManager = require('../Analytics/AnalyticsManager')
|
2019-05-29 05:21:06 -04:00
|
|
|
|
2019-11-25 08:29:40 -05:00
|
|
|
const FeaturesUpdater = {
|
2021-05-11 10:08:37 -04:00
|
|
|
refreshFeatures(userId, reason, callback = () => {}) {
|
2020-11-04 04:53:06 -05:00
|
|
|
UserGetter.getUser(userId, { _id: 1, features: 1 }, (err, user) => {
|
|
|
|
if (err) {
|
|
|
|
return callback(err)
|
2019-12-05 08:57:58 -05:00
|
|
|
}
|
2020-11-04 04:53:06 -05:00
|
|
|
const oldFeatures = _.clone(user.features)
|
|
|
|
FeaturesUpdater._computeFeatures(userId, (error, features) => {
|
|
|
|
if (error) {
|
|
|
|
return callback(error)
|
|
|
|
}
|
|
|
|
logger.log({ userId, features }, 'updating user features')
|
2021-06-16 10:22:15 -04:00
|
|
|
|
|
|
|
const matchedFeatureSet = FeaturesUpdater._getMatchedFeatureSet(
|
|
|
|
features
|
|
|
|
)
|
2021-09-09 13:03:29 -04:00
|
|
|
AnalyticsManager.setUserProperty(
|
2021-06-16 10:22:15 -04:00
|
|
|
userId,
|
|
|
|
'feature-set',
|
|
|
|
matchedFeatureSet
|
|
|
|
)
|
|
|
|
|
2021-03-10 04:51:50 -05:00
|
|
|
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')
|
2021-05-18 09:40:56 -04:00
|
|
|
Modules.hooks.fire('removeDropbox', userId, reason, err => {
|
2021-03-10 04:51:50 -05:00
|
|
|
if (err) {
|
|
|
|
logger.error(err)
|
|
|
|
}
|
2021-05-18 09:40:56 -04:00
|
|
|
|
|
|
|
return callback(null, newFeatures, featuresChanged)
|
2021-03-10 04:51:50 -05:00
|
|
|
})
|
2021-05-18 09:40:56 -04:00
|
|
|
} else {
|
|
|
|
return callback(null, newFeatures, featuresChanged)
|
2021-03-10 04:51:50 -05:00
|
|
|
}
|
2020-11-04 04:53:06 -05:00
|
|
|
}
|
2021-03-10 04:51:50 -05:00
|
|
|
)
|
2020-11-04 04:53:06 -05:00
|
|
|
})
|
2019-12-05 08:57:58 -05:00
|
|
|
})
|
|
|
|
},
|
2019-05-29 05:21:06 -04:00
|
|
|
|
2020-01-07 06:03:14 -05:00
|
|
|
_computeFeatures(userId, callback) {
|
2019-05-29 05:21:06 -04:00
|
|
|
const jobs = {
|
|
|
|
individualFeatures(cb) {
|
2020-01-07 06:03:14 -05:00
|
|
|
FeaturesUpdater._getIndividualFeatures(userId, cb)
|
2019-05-29 05:21:06 -04:00
|
|
|
},
|
|
|
|
groupFeatureSets(cb) {
|
2020-01-07 06:03:14 -05:00
|
|
|
FeaturesUpdater._getGroupFeatureSets(userId, cb)
|
2019-05-29 05:21:06 -04:00
|
|
|
},
|
|
|
|
institutionFeatures(cb) {
|
2020-01-07 06:03:14 -05:00
|
|
|
InstitutionsFeatures.getInstitutionsFeatures(userId, cb)
|
2019-05-29 05:21:06 -04:00
|
|
|
},
|
|
|
|
v1Features(cb) {
|
2020-01-07 06:03:14 -05:00
|
|
|
FeaturesUpdater._getV1Features(userId, cb)
|
2019-05-29 05:21:06 -04:00
|
|
|
},
|
|
|
|
bonusFeatures(cb) {
|
2020-01-07 06:03:14 -05:00
|
|
|
ReferalFeatures.getBonusFeatures(userId, cb)
|
2019-10-05 13:43:21 -04:00
|
|
|
},
|
2019-12-02 08:51:57 -05:00
|
|
|
featuresOverrides(cb) {
|
2020-01-07 06:03:14 -05:00
|
|
|
FeaturesUpdater._getFeaturesOverrides(userId, cb)
|
2021-04-27 03:52:58 -04:00
|
|
|
},
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
2021-04-14 09:17:21 -04:00
|
|
|
async.series(jobs, function (err, results) {
|
2020-01-07 06:03:14 -05:00
|
|
|
if (err) {
|
2020-08-11 05:35:08 -04:00
|
|
|
OError.tag(
|
|
|
|
err,
|
|
|
|
'error getting subscription or group for refreshFeatures',
|
|
|
|
{
|
2021-04-27 03:52:58 -04:00
|
|
|
userId,
|
2020-08-11 05:35:08 -04:00
|
|
|
}
|
2019-05-29 05:21:06 -04:00
|
|
|
)
|
|
|
|
return callback(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
const {
|
|
|
|
individualFeatures,
|
|
|
|
groupFeatureSets,
|
|
|
|
institutionFeatures,
|
|
|
|
v1Features,
|
2019-10-05 13:43:21 -04:00
|
|
|
bonusFeatures,
|
2021-04-27 03:52:58 -04:00
|
|
|
featuresOverrides,
|
2019-05-29 05:21:06 -04:00
|
|
|
} = results
|
|
|
|
logger.log(
|
|
|
|
{
|
2020-01-07 06:03:14 -05:00
|
|
|
userId,
|
2019-05-29 05:21:06 -04:00
|
|
|
individualFeatures,
|
|
|
|
groupFeatureSets,
|
|
|
|
institutionFeatures,
|
|
|
|
v1Features,
|
2019-10-05 13:43:21 -04:00
|
|
|
bonusFeatures,
|
2021-04-27 03:52:58 -04:00
|
|
|
featuresOverrides,
|
2019-05-29 05:21:06 -04:00
|
|
|
},
|
|
|
|
'merging user features'
|
|
|
|
)
|
|
|
|
const featureSets = groupFeatureSets.concat([
|
|
|
|
individualFeatures,
|
|
|
|
institutionFeatures,
|
|
|
|
v1Features,
|
2019-10-05 13:43:21 -04:00
|
|
|
bonusFeatures,
|
2021-04-27 03:52:58 -04:00
|
|
|
featuresOverrides,
|
2019-05-29 05:21:06 -04:00
|
|
|
])
|
|
|
|
const features = _.reduce(
|
|
|
|
featureSets,
|
|
|
|
FeaturesUpdater._mergeFeatures,
|
|
|
|
Settings.defaultFeatures
|
|
|
|
)
|
2019-12-05 08:57:58 -05:00
|
|
|
callback(null, features)
|
2019-05-29 05:21:06 -04:00
|
|
|
})
|
|
|
|
},
|
|
|
|
|
2020-01-07 06:03:14 -05:00
|
|
|
_getIndividualFeatures(userId, callback) {
|
2020-02-03 09:11:23 -05:00
|
|
|
SubscriptionLocator.getUserIndividualSubscription(userId, (err, sub) =>
|
2019-05-29 05:21:06 -04:00
|
|
|
callback(err, FeaturesUpdater._subscriptionToFeatures(sub))
|
|
|
|
)
|
|
|
|
},
|
|
|
|
|
2020-01-07 06:03:14 -05:00
|
|
|
_getGroupFeatureSets(userId, callback) {
|
|
|
|
SubscriptionLocator.getGroupSubscriptionsMemberOf(userId, (err, subs) =>
|
|
|
|
callback(err, (subs || []).map(FeaturesUpdater._subscriptionToFeatures))
|
2019-05-29 05:21:06 -04:00
|
|
|
)
|
|
|
|
},
|
|
|
|
|
2020-01-07 06:03:14 -05:00
|
|
|
_getFeaturesOverrides(userId, callback) {
|
|
|
|
UserGetter.getUser(userId, { featuresOverrides: 1 }, (error, user) => {
|
2019-12-02 08:51:57 -05:00
|
|
|
if (error) {
|
|
|
|
return callback(error)
|
|
|
|
}
|
|
|
|
if (
|
|
|
|
!user ||
|
|
|
|
!user.featuresOverrides ||
|
|
|
|
user.featuresOverrides.length === 0
|
|
|
|
) {
|
|
|
|
return callback(null, {})
|
|
|
|
}
|
2021-05-05 09:05:04 -04:00
|
|
|
const activeFeaturesOverrides = []
|
|
|
|
for (const featuresOverride of user.featuresOverrides) {
|
2019-12-02 08:51:57 -05:00
|
|
|
if (
|
|
|
|
!featuresOverride.expiresAt ||
|
|
|
|
featuresOverride.expiresAt > new Date()
|
|
|
|
) {
|
|
|
|
activeFeaturesOverrides.push(featuresOverride.features)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const features = _.reduce(
|
|
|
|
activeFeaturesOverrides,
|
|
|
|
FeaturesUpdater._mergeFeatures,
|
|
|
|
{}
|
|
|
|
)
|
2020-01-07 06:03:14 -05:00
|
|
|
callback(null, features)
|
2019-12-02 08:51:57 -05:00
|
|
|
})
|
|
|
|
},
|
|
|
|
|
2020-01-07 06:03:14 -05:00
|
|
|
_getV1Features(userId, callback) {
|
2021-04-14 09:17:21 -04:00
|
|
|
V1SubscriptionManager.getPlanCodeFromV1(
|
|
|
|
userId,
|
|
|
|
function (err, planCode, v1Id) {
|
|
|
|
if (err) {
|
|
|
|
if ((err ? err.name : undefined) === 'NotFoundError') {
|
|
|
|
return callback(null, [])
|
|
|
|
}
|
|
|
|
return callback(err)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
|
2021-04-14 09:17:21 -04:00
|
|
|
callback(
|
|
|
|
err,
|
|
|
|
FeaturesUpdater._mergeFeatures(
|
|
|
|
V1SubscriptionManager.getGrandfatheredFeaturesForV1User(v1Id) || {},
|
2021-07-05 11:15:54 -04:00
|
|
|
FeaturesUpdater.planCodeToFeatures(planCode)
|
2021-04-14 09:17:21 -04:00
|
|
|
)
|
2019-05-29 05:21:06 -04:00
|
|
|
)
|
2021-04-14 09:17:21 -04:00
|
|
|
}
|
|
|
|
)
|
2019-05-29 05:21:06 -04:00
|
|
|
},
|
|
|
|
|
|
|
|
_mergeFeatures(featuresA, featuresB) {
|
|
|
|
const features = Object.assign({}, featuresA)
|
2021-05-05 09:05:04 -04:00
|
|
|
for (const key in featuresB) {
|
2019-05-29 05:21:06 -04:00
|
|
|
// Special merging logic for non-boolean features
|
|
|
|
if (key === 'compileGroup') {
|
|
|
|
if (
|
2020-12-16 05:37:00 -05:00
|
|
|
features.compileGroup === 'priority' ||
|
|
|
|
featuresB.compileGroup === 'priority'
|
2019-05-29 05:21:06 -04:00
|
|
|
) {
|
2020-12-16 05:37:00 -05:00
|
|
|
features.compileGroup = 'priority'
|
2019-05-29 05:21:06 -04:00
|
|
|
} else {
|
2020-12-16 05:37:00 -05:00
|
|
|
features.compileGroup = 'standard'
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
|
|
|
} else if (key === 'collaborators') {
|
2020-12-16 05:37:00 -05:00
|
|
|
if (features.collaborators === -1 || featuresB.collaborators === -1) {
|
|
|
|
features.collaborators = -1
|
2019-05-29 05:21:06 -04:00
|
|
|
} else {
|
2020-12-16 05:37:00 -05:00
|
|
|
features.collaborators = Math.max(
|
|
|
|
features.collaborators || 0,
|
|
|
|
featuresB.collaborators || 0
|
2019-05-29 05:21:06 -04:00
|
|
|
)
|
|
|
|
}
|
|
|
|
} else if (key === 'compileTimeout') {
|
2020-12-16 05:37:00 -05:00
|
|
|
features.compileTimeout = Math.max(
|
|
|
|
features.compileTimeout || 0,
|
|
|
|
featuresB.compileTimeout || 0
|
2019-05-29 05:21:06 -04:00
|
|
|
)
|
|
|
|
} else {
|
|
|
|
// Boolean keys, true is better
|
|
|
|
features[key] = features[key] || featuresB[key]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return features
|
|
|
|
},
|
|
|
|
|
2021-07-05 11:15:54 -04:00
|
|
|
isFeatureSetBetter(featuresA, featuresB) {
|
|
|
|
const mergedFeatures = FeaturesUpdater._mergeFeatures(featuresA, featuresB)
|
|
|
|
return _.isEqual(featuresA, mergedFeatures)
|
|
|
|
},
|
|
|
|
|
2019-05-29 05:21:06 -04:00
|
|
|
_subscriptionToFeatures(subscription) {
|
2021-07-05 11:15:54 -04:00
|
|
|
return FeaturesUpdater.planCodeToFeatures(
|
2020-01-07 06:03:14 -05:00
|
|
|
subscription ? subscription.planCode : undefined
|
2019-05-29 05:21:06 -04:00
|
|
|
)
|
|
|
|
},
|
|
|
|
|
2021-07-05 11:15:54 -04:00
|
|
|
planCodeToFeatures(planCode) {
|
2020-01-07 06:03:14 -05:00
|
|
|
if (!planCode) {
|
2019-05-29 05:21:06 -04:00
|
|
|
return {}
|
|
|
|
}
|
|
|
|
const plan = PlansLocator.findLocalPlanInSettings(planCode)
|
2020-01-07 06:03:14 -05:00
|
|
|
if (!plan) {
|
2019-05-29 05:21:06 -04:00
|
|
|
return {}
|
|
|
|
} else {
|
|
|
|
return plan.features
|
|
|
|
}
|
2020-01-07 06:03:14 -05:00
|
|
|
},
|
|
|
|
|
|
|
|
compareFeatures(currentFeatures, expectedFeatures) {
|
|
|
|
currentFeatures = _.clone(currentFeatures)
|
|
|
|
expectedFeatures = _.clone(expectedFeatures)
|
|
|
|
if (_.isEqual(currentFeatures, expectedFeatures)) {
|
|
|
|
return {}
|
|
|
|
}
|
|
|
|
|
2021-05-05 09:05:04 -04:00
|
|
|
const mismatchReasons = {}
|
2020-01-07 06:03:14 -05:00
|
|
|
const featureKeys = [
|
|
|
|
...new Set([
|
|
|
|
...Object.keys(currentFeatures),
|
2021-04-27 03:52:58 -04:00
|
|
|
...Object.keys(expectedFeatures),
|
|
|
|
]),
|
2020-01-07 06:03:14 -05:00
|
|
|
]
|
|
|
|
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
|
2020-02-03 09:12:34 -05:00
|
|
|
},
|
|
|
|
|
|
|
|
doSyncFromV1(v1UserId, callback) {
|
|
|
|
logger.log({ v1UserId }, '[AccountSync] starting account sync')
|
2021-04-14 09:17:21 -04:00
|
|
|
return UserGetter.getUser(
|
|
|
|
{ 'overleaf.id': v1UserId },
|
|
|
|
{ _id: 1 },
|
|
|
|
function (err, user) {
|
|
|
|
if (err != null) {
|
|
|
|
OError.tag(err, '[AccountSync] error getting user', {
|
2021-04-27 03:52:58 -04:00
|
|
|
v1UserId,
|
2021-04-14 09:17:21 -04:00
|
|
|
})
|
|
|
|
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'
|
|
|
|
)
|
2021-05-11 10:08:37 -04:00
|
|
|
return FeaturesUpdater.refreshFeatures(user._id, 'sync-v1', callback)
|
2020-02-03 09:12:34 -05:00
|
|
|
}
|
2021-04-14 09:17:21 -04:00
|
|
|
)
|
2021-04-27 03:52:58 -04:00
|
|
|
},
|
2021-06-16 10:22:15 -04:00
|
|
|
|
|
|
|
_getMatchedFeatureSet(features) {
|
|
|
|
for (const [name, featureSet] of Object.entries(Settings.features)) {
|
|
|
|
if (_.isEqual(features, featureSet)) {
|
|
|
|
return name
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return 'mixed'
|
|
|
|
},
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
2019-11-25 08:29:40 -05:00
|
|
|
|
2021-05-11 10:08:37 -04:00
|
|
|
const refreshFeaturesPromise = (userId, reason) =>
|
2021-04-14 09:17:21 -04:00
|
|
|
new Promise(function (resolve, reject) {
|
2019-11-25 08:29:40 -05:00
|
|
|
FeaturesUpdater.refreshFeatures(
|
2020-01-07 06:03:14 -05:00
|
|
|
userId,
|
2021-05-11 10:08:37 -04:00
|
|
|
reason,
|
2019-11-25 08:29:40 -05:00
|
|
|
(error, features, featuresChanged) => {
|
|
|
|
if (error) {
|
|
|
|
reject(error)
|
|
|
|
} else {
|
|
|
|
resolve({ features, featuresChanged })
|
|
|
|
}
|
|
|
|
}
|
|
|
|
)
|
|
|
|
})
|
|
|
|
|
|
|
|
FeaturesUpdater.promises = {
|
2021-04-27 03:52:58 -04:00
|
|
|
refreshFeatures: refreshFeaturesPromise,
|
2019-11-25 08:29:40 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = FeaturesUpdater
|