FeaturesUpdater.js 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. const _ = require('lodash')
  2. const { callbackify } = require('util')
  3. const { callbackifyMultiResult } = require('@overleaf/promise-utils')
  4. const PlansLocator = require('./PlansLocator')
  5. const SubscriptionLocator = require('./SubscriptionLocator')
  6. const UserFeaturesUpdater = require('./UserFeaturesUpdater')
  7. const FeaturesHelper = require('./FeaturesHelper')
  8. const Settings = require('@overleaf/settings')
  9. const logger = require('@overleaf/logger')
  10. const ReferalFeatures = require('../Referal/ReferalFeatures')
  11. const V1SubscriptionManager = require('./V1SubscriptionManager')
  12. const InstitutionsFeatures = require('../Institutions/InstitutionsFeatures')
  13. const UserGetter = require('../User/UserGetter')
  14. const AnalyticsManager = require('../Analytics/AnalyticsManager')
  15. const Queues = require('../../infrastructure/Queues')
  16. const Modules = require('../../infrastructure/Modules')
  17. /**
  18. * Enqueue a job for refreshing features for the given user
  19. */
  20. async function scheduleRefreshFeatures(userId, reason) {
  21. const queue = Queues.getQueue('refresh-features')
  22. await queue.add({ userId, reason })
  23. }
  24. /* Check if user features refresh if needed, based on the global featuresEpoch setting */
  25. function featuresEpochIsCurrent(user) {
  26. return Settings.featuresEpoch
  27. ? user.featuresEpoch === Settings.featuresEpoch
  28. : true
  29. }
  30. /**
  31. * Refresh features for the given user
  32. */
  33. async function refreshFeatures(userId, reason) {
  34. const user = await UserGetter.promises.getUser(userId, {
  35. _id: 1,
  36. features: 1,
  37. })
  38. const oldFeatures = _.clone(user.features)
  39. const features = await computeFeatures(userId)
  40. logger.debug({ userId, features }, 'updating user features')
  41. const matchedFeatureSet = FeaturesHelper.getMatchedFeatureSet(features)
  42. AnalyticsManager.setUserPropertyForUserInBackground(
  43. userId,
  44. 'feature-set',
  45. matchedFeatureSet
  46. )
  47. const { features: newFeatures, featuresChanged } =
  48. await UserFeaturesUpdater.promises.updateFeatures(userId, features)
  49. if (oldFeatures.dropbox === true && features.dropbox === false) {
  50. logger.debug({ userId }, '[FeaturesUpdater] must unlink dropbox')
  51. try {
  52. await Modules.promises.hooks.fire('removeDropbox', userId, reason)
  53. } catch (err) {
  54. logger.error(err)
  55. }
  56. }
  57. if (oldFeatures.github === true && features.github === false) {
  58. logger.debug({ userId }, '[FeaturesUpdater] must unlink github')
  59. try {
  60. await Modules.promises.hooks.fire('removeGithub', userId, reason)
  61. } catch (err) {
  62. logger.error(err)
  63. }
  64. }
  65. return { features: newFeatures, featuresChanged }
  66. }
  67. /**
  68. * Return the features that the given user should have.
  69. */
  70. async function computeFeatures(userId) {
  71. const individualFeatures = await _getIndividualFeatures(userId)
  72. const groupFeatureSets = await _getGroupFeatureSets(userId)
  73. const institutionFeatures =
  74. await InstitutionsFeatures.promises.getInstitutionsFeatures(userId)
  75. const user = await UserGetter.promises.getUser(userId, {
  76. featuresOverrides: 1,
  77. 'overleaf.id': 1,
  78. })
  79. const v1Features = await _getV1Features(user)
  80. const bonusFeatures = await ReferalFeatures.promises.getBonusFeatures(userId)
  81. const featuresOverrides = await _getFeaturesOverrides(user)
  82. logger.debug(
  83. {
  84. userId,
  85. individualFeatures,
  86. groupFeatureSets,
  87. institutionFeatures,
  88. v1Features,
  89. bonusFeatures,
  90. featuresOverrides,
  91. },
  92. 'merging user features'
  93. )
  94. const featureSets = groupFeatureSets.concat([
  95. individualFeatures,
  96. institutionFeatures,
  97. v1Features,
  98. bonusFeatures,
  99. featuresOverrides,
  100. ])
  101. return _.reduce(
  102. featureSets,
  103. FeaturesHelper.mergeFeatures,
  104. Settings.defaultFeatures
  105. )
  106. }
  107. async function _getIndividualFeatures(userId) {
  108. const sub =
  109. await SubscriptionLocator.promises.getUserIndividualSubscription(userId)
  110. return _subscriptionToFeatures(sub)
  111. }
  112. async function _getGroupFeatureSets(userId) {
  113. const subs =
  114. await SubscriptionLocator.promises.getGroupSubscriptionsMemberOf(userId)
  115. return (subs || []).map(_subscriptionToFeatures)
  116. }
  117. async function _getFeaturesOverrides(user) {
  118. if (!user || !user.featuresOverrides || user.featuresOverrides.length === 0) {
  119. return {}
  120. }
  121. const activeFeaturesOverrides = []
  122. for (const featuresOverride of user.featuresOverrides) {
  123. if (
  124. !featuresOverride.expiresAt ||
  125. featuresOverride.expiresAt > new Date()
  126. ) {
  127. activeFeaturesOverrides.push(featuresOverride.features)
  128. }
  129. }
  130. return _.reduce(activeFeaturesOverrides, FeaturesHelper.mergeFeatures, {})
  131. }
  132. async function _getV1Features(user) {
  133. const v1Id = user?.overleaf?.id
  134. return V1SubscriptionManager.getGrandfatheredFeaturesForV1User(v1Id) || {}
  135. }
  136. function _subscriptionToFeatures(subscription) {
  137. return _planCodeToFeatures(subscription && subscription.planCode)
  138. }
  139. function _planCodeToFeatures(planCode) {
  140. if (!planCode) {
  141. return {}
  142. }
  143. const plan = PlansLocator.findLocalPlanInSettings(planCode)
  144. if (!plan) {
  145. return {}
  146. } else {
  147. return plan.features
  148. }
  149. }
  150. async function doSyncFromV1(v1UserId) {
  151. logger.debug({ v1UserId }, '[AccountSync] starting account sync')
  152. const user = await UserGetter.promises.getUser(
  153. { 'overleaf.id': v1UserId },
  154. { _id: 1 }
  155. )
  156. if (user == null) {
  157. logger.warn({ v1UserId }, '[AccountSync] no user found for v1 id')
  158. return
  159. }
  160. logger.debug(
  161. { v1UserId, userId: user._id },
  162. '[AccountSync] updating user subscription and features'
  163. )
  164. return refreshFeatures(user._id, 'sync-v1')
  165. }
  166. module.exports = {
  167. featuresEpochIsCurrent,
  168. computeFeatures: callbackify(computeFeatures),
  169. refreshFeatures: callbackifyMultiResult(refreshFeatures, [
  170. 'features',
  171. 'featuresChanged',
  172. ]),
  173. doSyncFromV1: callbackifyMultiResult(doSyncFromV1, [
  174. 'features',
  175. 'featuresChanged',
  176. ]),
  177. scheduleRefreshFeatures: callbackify(scheduleRefreshFeatures),
  178. promises: {
  179. computeFeatures,
  180. refreshFeatures,
  181. scheduleRefreshFeatures,
  182. doSyncFromV1,
  183. },
  184. }