2021-07-05 11:15:54 -04:00
|
|
|
const _ = require('lodash')
|
2021-09-20 08:20:14 -04:00
|
|
|
const { callbackify } = require('util')
|
|
|
|
const { callbackifyMultiResult } = require('../../util/promises')
|
|
|
|
const PlansLocator = require('./PlansLocator')
|
2019-05-29 05:21:06 -04:00
|
|
|
const SubscriptionLocator = require('./SubscriptionLocator')
|
|
|
|
const UserFeaturesUpdater = require('./UserFeaturesUpdater')
|
2021-09-20 08:20:14 -04:00
|
|
|
const FeaturesHelper = require('./FeaturesHelper')
|
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')
|
2021-09-20 08:20:33 -04:00
|
|
|
const Queues = require('../../infrastructure/Queues')
|
2019-05-29 05:21:06 -04:00
|
|
|
|
2021-09-20 08:20:33 -04:00
|
|
|
/**
|
|
|
|
* Enqueue a job for refreshing features for the given user
|
|
|
|
*/
|
|
|
|
async function scheduleRefreshFeatures(userId, reason) {
|
2021-10-25 08:07:45 -04:00
|
|
|
const queue = Queues.getQueue('refresh-features')
|
2021-09-20 08:20:33 -04:00
|
|
|
await queue.add({ userId, reason })
|
|
|
|
}
|
|
|
|
|
2021-10-26 09:31:24 -04:00
|
|
|
/* Check if user features refresh if needed, based on the global featuresEpoch setting */
|
|
|
|
function featuresEpochIsCurrent(user) {
|
|
|
|
return Settings.featuresEpoch
|
|
|
|
? user.featuresEpoch === Settings.featuresEpoch
|
|
|
|
: true
|
|
|
|
}
|
|
|
|
|
2021-09-20 08:20:33 -04:00
|
|
|
/**
|
|
|
|
* Refresh features for the given user
|
|
|
|
*/
|
2021-09-20 08:20:14 -04:00
|
|
|
async function refreshFeatures(userId, reason) {
|
|
|
|
const user = await UserGetter.promises.getUser(userId, {
|
|
|
|
_id: 1,
|
|
|
|
features: 1,
|
|
|
|
})
|
|
|
|
const oldFeatures = _.clone(user.features)
|
|
|
|
const features = await computeFeatures(userId)
|
|
|
|
logger.log({ userId, features }, 'updating user features')
|
|
|
|
|
|
|
|
const matchedFeatureSet = _getMatchedFeatureSet(features)
|
|
|
|
AnalyticsManager.setUserPropertyForUser(
|
|
|
|
userId,
|
|
|
|
'feature-set',
|
|
|
|
matchedFeatureSet
|
|
|
|
)
|
|
|
|
|
|
|
|
const {
|
|
|
|
features: newFeatures,
|
|
|
|
featuresChanged,
|
|
|
|
} = await UserFeaturesUpdater.promises.updateFeatures(userId, features)
|
|
|
|
if (oldFeatures.dropbox === true && features.dropbox === false) {
|
|
|
|
logger.log({ userId }, '[FeaturesUpdater] must unlink dropbox')
|
|
|
|
const Modules = require('../../infrastructure/Modules')
|
|
|
|
try {
|
|
|
|
await Modules.promises.hooks.fire('removeDropbox', userId, reason)
|
|
|
|
} catch (err) {
|
|
|
|
logger.error(err)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
2021-09-20 08:20:14 -04:00
|
|
|
}
|
|
|
|
return { features: newFeatures, featuresChanged }
|
|
|
|
}
|
2019-12-02 08:51:57 -05:00
|
|
|
|
2021-09-20 08:20:33 -04:00
|
|
|
/**
|
|
|
|
* Return the features that the given user should have.
|
|
|
|
*/
|
2021-09-20 08:20:14 -04:00
|
|
|
async function computeFeatures(userId) {
|
|
|
|
const individualFeatures = await _getIndividualFeatures(userId)
|
|
|
|
const groupFeatureSets = await _getGroupFeatureSets(userId)
|
|
|
|
const institutionFeatures = await InstitutionsFeatures.promises.getInstitutionsFeatures(
|
|
|
|
userId
|
|
|
|
)
|
|
|
|
const v1Features = await _getV1Features(userId)
|
|
|
|
const bonusFeatures = await ReferalFeatures.promises.getBonusFeatures(userId)
|
|
|
|
const featuresOverrides = await _getFeaturesOverrides(userId)
|
|
|
|
logger.log(
|
|
|
|
{
|
2021-04-14 09:17:21 -04:00
|
|
|
userId,
|
2021-09-20 08:20:14 -04:00
|
|
|
individualFeatures,
|
|
|
|
groupFeatureSets,
|
|
|
|
institutionFeatures,
|
|
|
|
v1Features,
|
|
|
|
bonusFeatures,
|
|
|
|
featuresOverrides,
|
|
|
|
},
|
|
|
|
'merging user features'
|
|
|
|
)
|
|
|
|
const featureSets = groupFeatureSets.concat([
|
|
|
|
individualFeatures,
|
|
|
|
institutionFeatures,
|
|
|
|
v1Features,
|
|
|
|
bonusFeatures,
|
|
|
|
featuresOverrides,
|
|
|
|
])
|
|
|
|
const features = _.reduce(
|
|
|
|
featureSets,
|
|
|
|
FeaturesHelper.mergeFeatures,
|
|
|
|
Settings.defaultFeatures
|
|
|
|
)
|
|
|
|
return features
|
|
|
|
}
|
2019-05-29 05:21:06 -04:00
|
|
|
|
2021-09-20 08:20:14 -04:00
|
|
|
async function _getIndividualFeatures(userId) {
|
|
|
|
const sub = await SubscriptionLocator.promises.getUserIndividualSubscription(
|
|
|
|
userId
|
|
|
|
)
|
|
|
|
return _subscriptionToFeatures(sub)
|
|
|
|
}
|
2021-07-05 11:15:54 -04:00
|
|
|
|
2021-09-20 08:20:14 -04:00
|
|
|
async function _getGroupFeatureSets(userId) {
|
|
|
|
const subs = await SubscriptionLocator.promises.getGroupSubscriptionsMemberOf(
|
|
|
|
userId
|
|
|
|
)
|
|
|
|
return (subs || []).map(_subscriptionToFeatures)
|
|
|
|
}
|
2019-05-29 05:21:06 -04:00
|
|
|
|
2021-09-20 08:20:14 -04:00
|
|
|
async function _getFeaturesOverrides(userId) {
|
|
|
|
const user = await UserGetter.promises.getUser(userId, {
|
|
|
|
featuresOverrides: 1,
|
|
|
|
})
|
|
|
|
if (!user || !user.featuresOverrides || user.featuresOverrides.length === 0) {
|
|
|
|
return {}
|
|
|
|
}
|
|
|
|
const activeFeaturesOverrides = []
|
|
|
|
for (const featuresOverride of user.featuresOverrides) {
|
|
|
|
if (
|
|
|
|
!featuresOverride.expiresAt ||
|
|
|
|
featuresOverride.expiresAt > new Date()
|
|
|
|
) {
|
|
|
|
activeFeaturesOverrides.push(featuresOverride.features)
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
2021-09-20 08:20:14 -04:00
|
|
|
}
|
|
|
|
const features = _.reduce(
|
|
|
|
activeFeaturesOverrides,
|
|
|
|
FeaturesHelper.mergeFeatures,
|
|
|
|
{}
|
|
|
|
)
|
|
|
|
return features
|
|
|
|
}
|
2020-01-07 06:03:14 -05:00
|
|
|
|
2021-09-20 08:20:14 -04:00
|
|
|
async function _getV1Features(userId) {
|
|
|
|
let planCode, v1Id
|
|
|
|
try {
|
|
|
|
;({
|
|
|
|
planCode,
|
|
|
|
v1Id,
|
|
|
|
} = await V1SubscriptionManager.promises.getPlanCodeFromV1(userId))
|
|
|
|
} catch (err) {
|
|
|
|
if (err.name === 'NotFoundError') {
|
2020-01-07 06:03:14 -05:00
|
|
|
return {}
|
2021-09-20 08:20:14 -04:00
|
|
|
} else {
|
|
|
|
throw err
|
2020-01-07 06:03:14 -05:00
|
|
|
}
|
2021-09-20 08:20:14 -04:00
|
|
|
}
|
|
|
|
return FeaturesHelper.mergeFeatures(
|
|
|
|
V1SubscriptionManager.getGrandfatheredFeaturesForV1User(v1Id) || {},
|
|
|
|
_planCodeToFeatures(planCode)
|
|
|
|
)
|
|
|
|
}
|
2020-01-07 06:03:14 -05:00
|
|
|
|
2021-09-20 08:20:14 -04:00
|
|
|
function _subscriptionToFeatures(subscription) {
|
|
|
|
return _planCodeToFeatures(subscription && subscription.planCode)
|
|
|
|
}
|
2020-01-07 06:03:14 -05:00
|
|
|
|
2021-09-20 08:20:14 -04:00
|
|
|
function _planCodeToFeatures(planCode) {
|
|
|
|
if (!planCode) {
|
|
|
|
return {}
|
|
|
|
}
|
|
|
|
const plan = PlansLocator.findLocalPlanInSettings(planCode)
|
|
|
|
if (!plan) {
|
|
|
|
return {}
|
|
|
|
} else {
|
|
|
|
return plan.features
|
|
|
|
}
|
|
|
|
}
|
2020-02-03 09:12:34 -05:00
|
|
|
|
2021-09-20 08:20:14 -04:00
|
|
|
async function doSyncFromV1(v1UserId) {
|
|
|
|
logger.log({ v1UserId }, '[AccountSync] starting account sync')
|
|
|
|
const user = await UserGetter.promises.getUser(
|
|
|
|
{ 'overleaf.id': v1UserId },
|
|
|
|
{ _id: 1 }
|
|
|
|
)
|
|
|
|
if (user == null) {
|
|
|
|
logger.warn({ v1UserId }, '[AccountSync] no user found for v1 id')
|
|
|
|
return
|
|
|
|
}
|
|
|
|
logger.log(
|
|
|
|
{ v1UserId, userId: user._id },
|
|
|
|
'[AccountSync] updating user subscription and features'
|
|
|
|
)
|
|
|
|
return refreshFeatures(user._id, 'sync-v1')
|
|
|
|
}
|
2021-06-16 10:22:15 -04:00
|
|
|
|
2021-09-20 08:20:14 -04:00
|
|
|
function _getMatchedFeatureSet(features) {
|
|
|
|
for (const [name, featureSet] of Object.entries(Settings.features)) {
|
|
|
|
if (_.isEqual(features, featureSet)) {
|
|
|
|
return name
|
2021-06-16 10:22:15 -04:00
|
|
|
}
|
2021-09-20 08:20:14 -04:00
|
|
|
}
|
|
|
|
return 'mixed'
|
2019-05-29 05:21:06 -04:00
|
|
|
}
|
2019-11-25 08:29:40 -05:00
|
|
|
|
2021-09-20 08:20:14 -04:00
|
|
|
module.exports = {
|
2021-10-26 09:31:24 -04:00
|
|
|
featuresEpochIsCurrent,
|
2021-09-20 08:20:14 -04:00
|
|
|
computeFeatures: callbackify(computeFeatures),
|
|
|
|
refreshFeatures: callbackifyMultiResult(refreshFeatures, [
|
|
|
|
'features',
|
|
|
|
'featuresChanged',
|
|
|
|
]),
|
|
|
|
doSyncFromV1: callbackifyMultiResult(doSyncFromV1, [
|
|
|
|
'features',
|
|
|
|
'featuresChanged',
|
|
|
|
]),
|
2021-09-20 08:20:33 -04:00
|
|
|
scheduleRefreshFeatures: callbackify(scheduleRefreshFeatures),
|
2021-09-20 08:20:14 -04:00
|
|
|
promises: {
|
|
|
|
computeFeatures,
|
|
|
|
refreshFeatures,
|
2021-09-20 08:20:33 -04:00
|
|
|
scheduleRefreshFeatures,
|
2021-09-20 08:20:14 -04:00
|
|
|
doSyncFromV1,
|
|
|
|
},
|
2019-11-25 08:29:40 -05:00
|
|
|
}
|