FeaturesUpdater.js 5.9 KB

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