FeaturesUpdater.mjs 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. import _ from 'lodash'
  2. import { callbackify } from 'node:util'
  3. import { callbackifyMultiResult } from '@overleaf/promise-utils'
  4. import PlansLocator from './PlansLocator.mjs'
  5. import SubscriptionLocator from './SubscriptionLocator.mjs'
  6. import SubscriptionHelper from './SubscriptionHelper.mjs'
  7. import UserFeaturesUpdater from './UserFeaturesUpdater.mjs'
  8. import FeaturesHelper from './FeaturesHelper.mjs'
  9. import Settings from '@overleaf/settings'
  10. import logger from '@overleaf/logger'
  11. import ReferalFeatures from '../Referal/ReferalFeatures.mjs'
  12. import V1SubscriptionManager from './V1SubscriptionManager.mjs'
  13. import InstitutionsFeatures from '../Institutions/InstitutionsFeatures.mjs'
  14. import UserGetter from '../User/UserGetter.mjs'
  15. import AnalyticsManager from '../Analytics/AnalyticsManager.mjs'
  16. import Queues from '../../infrastructure/Queues.mjs'
  17. import Modules from '../../infrastructure/Modules.js'
  18. import { AI_ADD_ON_CODE } from './AiHelper.mjs'
  19. // import { fetchNothing } from '@overleaf/fetch-utils'
  20. import metrics from '@overleaf/metrics'
  21. /**
  22. * Enqueue a job for refreshing features for the given user
  23. */
  24. async function scheduleRefreshFeatures(userId, reason) {
  25. const queue = Queues.getQueue('refresh-features')
  26. await queue.add({ userId, reason })
  27. }
  28. /* Check if user features refresh if needed, based on the global featuresEpoch setting */
  29. function featuresEpochIsCurrent(user) {
  30. return Settings.featuresEpoch
  31. ? user.featuresEpoch === Settings.featuresEpoch
  32. : true
  33. }
  34. /**
  35. * Refresh features for the given user
  36. */
  37. async function refreshFeatures(userId, reason) {
  38. const user = await UserGetter.promises.getUser(userId, {
  39. _id: 1,
  40. features: 1,
  41. })
  42. const oldFeatures = _.clone(user.features)
  43. const features = await computeFeatures(userId)
  44. logger.debug({ userId, features, reason }, 'updating user features')
  45. const matchedFeatureSet = FeaturesHelper.getMatchedFeatureSet(features)
  46. AnalyticsManager.setUserPropertyForUserInBackground(
  47. userId,
  48. 'feature-set',
  49. matchedFeatureSet
  50. )
  51. const { features: newFeatures, featuresChanged } =
  52. await UserFeaturesUpdater.promises.updateFeatures(userId, features)
  53. if (oldFeatures.dropbox === true && features.dropbox === false) {
  54. logger.debug({ userId }, '[FeaturesUpdater] must unlink dropbox')
  55. try {
  56. await Modules.promises.hooks.fire('removeDropbox', userId, reason)
  57. } catch (err) {
  58. logger.error({ err, userId }, 'removeDropbox hook failed')
  59. }
  60. }
  61. if (oldFeatures.github === true && features.github === false) {
  62. logger.debug({ userId }, '[FeaturesUpdater] must unlink github')
  63. try {
  64. await Modules.promises.hooks.fire('removeGithub', userId, reason)
  65. } catch (err) {
  66. logger.error({ err, userId }, 'removeGithub hook failed')
  67. }
  68. }
  69. // only update Writefull if the user's features have changed,
  70. // skip if they are the reason we are refreshing features (they'd already be up to date)
  71. if (featuresChanged && reason !== 'writefullEntitlementSynced') {
  72. try {
  73. // update WF with the current feature set for the user
  74. // await fetchNothing(
  75. // `${Settings.writefull.overleafApiUrl}/api/user/status/update-overleaf-status`,
  76. // {
  77. // headers: {
  78. // 'x-api-key': Settings.writefull.overleafApiKey,
  79. // },
  80. // json: {
  81. // userOverleafId: userId,
  82. // hasAiAssist: newFeatures.aiErrorAssistant,
  83. // },
  84. // method: 'POST',
  85. // }
  86. // )
  87. // increment a metric instead of calling WF so we cna give them an idea of the # of requests they will recieve
  88. metrics.inc('feature_sync_called_to_wf')
  89. } catch (err) {
  90. logger.warn(
  91. { userId, reason },
  92. 'failed to sync entitlement to Writefull after a feature refresh'
  93. )
  94. }
  95. }
  96. return { features: newFeatures, featuresChanged }
  97. }
  98. /**
  99. * Return the features that the given user should have.
  100. */
  101. async function computeFeatures(userId) {
  102. const individualFeatures = await _getIndividualFeatures(userId)
  103. const groupFeatureSets = await _getGroupFeatureSets(userId)
  104. const institutionFeatures =
  105. await InstitutionsFeatures.promises.getInstitutionsFeatures(userId)
  106. const user = await UserGetter.promises.getUser(userId, {
  107. featuresOverrides: 1,
  108. 'overleaf.id': 1,
  109. })
  110. const v1Features = await _getV1Features(user)
  111. const bonusFeatures = await ReferalFeatures.promises.getBonusFeatures(userId)
  112. const featuresOverrides = await _getFeaturesOverrides(user)
  113. logger.debug(
  114. {
  115. userId,
  116. individualFeatures,
  117. groupFeatureSets,
  118. institutionFeatures,
  119. v1Features,
  120. bonusFeatures,
  121. featuresOverrides,
  122. },
  123. 'merging user features'
  124. )
  125. const featureSets = groupFeatureSets.concat([
  126. individualFeatures,
  127. institutionFeatures,
  128. v1Features,
  129. bonusFeatures,
  130. featuresOverrides,
  131. ])
  132. return _.reduce(
  133. featureSets,
  134. FeaturesHelper.mergeFeatures,
  135. Settings.defaultFeatures
  136. )
  137. }
  138. async function _getIndividualFeatures(userId) {
  139. const subscription =
  140. await SubscriptionLocator.promises.getUsersSubscription(userId)
  141. if (
  142. subscription == null ||
  143. SubscriptionHelper.getPaidSubscriptionState(subscription) === 'paused' ||
  144. subscription.userFeaturesDisabled
  145. ) {
  146. return {}
  147. }
  148. const featureSets = []
  149. // The plan doesn't apply to the group admin when the subscription
  150. // is a group subscription
  151. if (!subscription.groupPlan) {
  152. featureSets.push(_subscriptionToFeatures(subscription))
  153. }
  154. featureSets.push(_aiAddOnFeatures(subscription))
  155. return _.reduce(featureSets, FeaturesHelper.mergeFeatures, {})
  156. }
  157. async function _getGroupFeatureSets(userId) {
  158. const subs =
  159. await SubscriptionLocator.promises.getGroupSubscriptionsMemberOf(userId)
  160. return (subs || [])
  161. .filter(sub => sub.userFeaturesDisabled !== true)
  162. .map(_subscriptionToFeatures)
  163. }
  164. async function _getFeaturesOverrides(user) {
  165. if (!user || !user.featuresOverrides || user.featuresOverrides.length === 0) {
  166. return {}
  167. }
  168. const activeFeaturesOverrides = []
  169. for (const featuresOverride of user.featuresOverrides) {
  170. if (
  171. !featuresOverride.expiresAt ||
  172. featuresOverride.expiresAt > new Date()
  173. ) {
  174. activeFeaturesOverrides.push(featuresOverride.features)
  175. }
  176. }
  177. return _.reduce(activeFeaturesOverrides, FeaturesHelper.mergeFeatures, {})
  178. }
  179. async function _getV1Features(user) {
  180. const v1Id = user?.overleaf?.id
  181. return V1SubscriptionManager.getGrandfatheredFeaturesForV1User(v1Id) || {}
  182. }
  183. function _subscriptionToFeatures(subscription) {
  184. if (!subscription?.planCode) {
  185. return {}
  186. }
  187. const plan = PlansLocator.findLocalPlanInSettings(subscription.planCode)
  188. if (!plan) {
  189. return {}
  190. } else {
  191. return plan.features
  192. }
  193. }
  194. function _aiAddOnFeatures(subscription) {
  195. if (subscription?.addOns?.some(addOn => addOn.addOnCode === AI_ADD_ON_CODE)) {
  196. return { aiErrorAssistant: true }
  197. } else {
  198. return {}
  199. }
  200. }
  201. async function doSyncFromV1(v1UserId) {
  202. logger.debug({ v1UserId }, '[AccountSync] starting account sync')
  203. const user = await UserGetter.promises.getUser(
  204. { 'overleaf.id': v1UserId },
  205. { _id: 1 }
  206. )
  207. if (user == null) {
  208. logger.warn({ v1UserId }, '[AccountSync] no user found for v1 id')
  209. return
  210. }
  211. logger.debug(
  212. { v1UserId, userId: user._id },
  213. '[AccountSync] updating user subscription and features'
  214. )
  215. return refreshFeatures(user._id, 'sync-v1')
  216. }
  217. export default {
  218. featuresEpochIsCurrent,
  219. computeFeatures: callbackify(computeFeatures),
  220. refreshFeatures: callbackifyMultiResult(refreshFeatures, [
  221. 'features',
  222. 'featuresChanged',
  223. ]),
  224. doSyncFromV1: callbackifyMultiResult(doSyncFromV1, [
  225. 'features',
  226. 'featuresChanged',
  227. ]),
  228. scheduleRefreshFeatures: callbackify(scheduleRefreshFeatures),
  229. promises: {
  230. computeFeatures,
  231. refreshFeatures,
  232. scheduleRefreshFeatures,
  233. doSyncFromV1,
  234. },
  235. }