| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200 |
- const _ = require('lodash')
- const { callbackify } = require('util')
- const { callbackifyMultiResult } = require('@overleaf/promise-utils')
- const PlansLocator = require('./PlansLocator')
- const SubscriptionLocator = require('./SubscriptionLocator')
- const UserFeaturesUpdater = require('./UserFeaturesUpdater')
- const FeaturesHelper = require('./FeaturesHelper')
- const Settings = require('@overleaf/settings')
- const logger = require('@overleaf/logger')
- const ReferalFeatures = require('../Referal/ReferalFeatures')
- const V1SubscriptionManager = require('./V1SubscriptionManager')
- const InstitutionsFeatures = require('../Institutions/InstitutionsFeatures')
- const UserGetter = require('../User/UserGetter')
- const AnalyticsManager = require('../Analytics/AnalyticsManager')
- const Queues = require('../../infrastructure/Queues')
- const Modules = require('../../infrastructure/Modules')
- /**
- * Enqueue a job for refreshing features for the given user
- */
- async function scheduleRefreshFeatures(userId, reason) {
- const queue = Queues.getQueue('refresh-features')
- await queue.add({ userId, reason })
- }
- /* Check if user features refresh if needed, based on the global featuresEpoch setting */
- function featuresEpochIsCurrent(user) {
- return Settings.featuresEpoch
- ? user.featuresEpoch === Settings.featuresEpoch
- : true
- }
- /**
- * Refresh features for the given user
- */
- 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.debug({ userId, features }, 'updating user features')
- const matchedFeatureSet = FeaturesHelper.getMatchedFeatureSet(features)
- AnalyticsManager.setUserPropertyForUserInBackground(
- userId,
- 'feature-set',
- matchedFeatureSet
- )
- const { features: newFeatures, featuresChanged } =
- await UserFeaturesUpdater.promises.updateFeatures(userId, features)
- if (oldFeatures.dropbox === true && features.dropbox === false) {
- logger.debug({ userId }, '[FeaturesUpdater] must unlink dropbox')
- try {
- await Modules.promises.hooks.fire('removeDropbox', userId, reason)
- } catch (err) {
- logger.error(err)
- }
- }
- if (oldFeatures.github === true && features.github === false) {
- logger.debug({ userId }, '[FeaturesUpdater] must unlink github')
- try {
- await Modules.promises.hooks.fire('removeGithub', userId, reason)
- } catch (err) {
- logger.error(err)
- }
- }
- return { features: newFeatures, featuresChanged }
- }
- /**
- * Return the features that the given user should have.
- */
- async function computeFeatures(userId) {
- const individualFeatures = await _getIndividualFeatures(userId)
- const groupFeatureSets = await _getGroupFeatureSets(userId)
- const institutionFeatures =
- await InstitutionsFeatures.promises.getInstitutionsFeatures(userId)
- const user = await UserGetter.promises.getUser(userId, {
- featuresOverrides: 1,
- 'overleaf.id': 1,
- })
- const v1Features = await _getV1Features(user)
- const bonusFeatures = await ReferalFeatures.promises.getBonusFeatures(userId)
- const featuresOverrides = await _getFeaturesOverrides(user)
- logger.debug(
- {
- userId,
- individualFeatures,
- groupFeatureSets,
- institutionFeatures,
- v1Features,
- bonusFeatures,
- featuresOverrides,
- },
- 'merging user features'
- )
- const featureSets = groupFeatureSets.concat([
- individualFeatures,
- institutionFeatures,
- v1Features,
- bonusFeatures,
- featuresOverrides,
- ])
- return _.reduce(
- featureSets,
- FeaturesHelper.mergeFeatures,
- Settings.defaultFeatures
- )
- }
- async function _getIndividualFeatures(userId) {
- const sub =
- await SubscriptionLocator.promises.getUserIndividualSubscription(userId)
- return _subscriptionToFeatures(sub)
- }
- async function _getGroupFeatureSets(userId) {
- const subs =
- await SubscriptionLocator.promises.getGroupSubscriptionsMemberOf(userId)
- return (subs || []).map(_subscriptionToFeatures)
- }
- async function _getFeaturesOverrides(user) {
- 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)
- }
- }
- return _.reduce(activeFeaturesOverrides, FeaturesHelper.mergeFeatures, {})
- }
- async function _getV1Features(user) {
- const v1Id = user?.overleaf?.id
- return V1SubscriptionManager.getGrandfatheredFeaturesForV1User(v1Id) || {}
- }
- function _subscriptionToFeatures(subscription) {
- return _planCodeToFeatures(subscription && subscription.planCode)
- }
- function _planCodeToFeatures(planCode) {
- if (!planCode) {
- return {}
- }
- const plan = PlansLocator.findLocalPlanInSettings(planCode)
- if (!plan) {
- return {}
- } else {
- return plan.features
- }
- }
- async function doSyncFromV1(v1UserId) {
- logger.debug({ 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.debug(
- { v1UserId, userId: user._id },
- '[AccountSync] updating user subscription and features'
- )
- return refreshFeatures(user._id, 'sync-v1')
- }
- module.exports = {
- featuresEpochIsCurrent,
- computeFeatures: callbackify(computeFeatures),
- refreshFeatures: callbackifyMultiResult(refreshFeatures, [
- 'features',
- 'featuresChanged',
- ]),
- doSyncFromV1: callbackifyMultiResult(doSyncFromV1, [
- 'features',
- 'featuresChanged',
- ]),
- scheduleRefreshFeatures: callbackify(scheduleRefreshFeatures),
- promises: {
- computeFeatures,
- refreshFeatures,
- scheduleRefreshFeatures,
- doSyncFromV1,
- },
- }
|