FeaturesUpdater.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. const async = require('async')
  2. const OError = require('@overleaf/o-error')
  3. const PlansLocator = require('./PlansLocator')
  4. const _ = require('underscore')
  5. const SubscriptionLocator = require('./SubscriptionLocator')
  6. const UserFeaturesUpdater = require('./UserFeaturesUpdater')
  7. const Settings = require('settings-sharelatex')
  8. const logger = require('logger-sharelatex')
  9. const ReferalFeatures = require('../Referal/ReferalFeatures')
  10. const V1SubscriptionManager = require('./V1SubscriptionManager')
  11. const InstitutionsFeatures = require('../Institutions/InstitutionsFeatures')
  12. const UserGetter = require('../User/UserGetter')
  13. const FeaturesUpdater = {
  14. refreshFeatures(userId, callback = () => {}) {
  15. UserGetter.getUser(userId, { _id: 1, features: 1 }, (err, user) => {
  16. if (err) {
  17. return callback(err)
  18. }
  19. const oldFeatures = _.clone(user.features)
  20. FeaturesUpdater._computeFeatures(userId, (error, features) => {
  21. if (error) {
  22. return callback(error)
  23. }
  24. logger.log({ userId, features }, 'updating user features')
  25. UserFeaturesUpdater.updateFeatures(
  26. userId,
  27. features,
  28. (err, newFeatures, featuresChanged) => {
  29. if (err) {
  30. return callback(err)
  31. }
  32. if (oldFeatures.dropbox === true && features.dropbox === false) {
  33. logger.log({ userId }, '[FeaturesUpdater] must unlink dropbox')
  34. const Modules = require('../../infrastructure/Modules')
  35. Modules.hooks.fire('removeDropbox', userId, err => {
  36. if (err) {
  37. logger.error(err)
  38. }
  39. })
  40. }
  41. return callback(null, newFeatures, featuresChanged)
  42. }
  43. )
  44. })
  45. })
  46. },
  47. _computeFeatures(userId, callback) {
  48. const jobs = {
  49. individualFeatures(cb) {
  50. FeaturesUpdater._getIndividualFeatures(userId, cb)
  51. },
  52. groupFeatureSets(cb) {
  53. FeaturesUpdater._getGroupFeatureSets(userId, cb)
  54. },
  55. institutionFeatures(cb) {
  56. InstitutionsFeatures.getInstitutionsFeatures(userId, cb)
  57. },
  58. v1Features(cb) {
  59. FeaturesUpdater._getV1Features(userId, cb)
  60. },
  61. bonusFeatures(cb) {
  62. ReferalFeatures.getBonusFeatures(userId, cb)
  63. },
  64. featuresOverrides(cb) {
  65. FeaturesUpdater._getFeaturesOverrides(userId, cb)
  66. },
  67. }
  68. async.series(jobs, function (err, results) {
  69. if (err) {
  70. OError.tag(
  71. err,
  72. 'error getting subscription or group for refreshFeatures',
  73. {
  74. userId,
  75. }
  76. )
  77. return callback(err)
  78. }
  79. const {
  80. individualFeatures,
  81. groupFeatureSets,
  82. institutionFeatures,
  83. v1Features,
  84. bonusFeatures,
  85. featuresOverrides,
  86. } = results
  87. logger.log(
  88. {
  89. userId,
  90. individualFeatures,
  91. groupFeatureSets,
  92. institutionFeatures,
  93. v1Features,
  94. bonusFeatures,
  95. featuresOverrides,
  96. },
  97. 'merging user features'
  98. )
  99. const featureSets = groupFeatureSets.concat([
  100. individualFeatures,
  101. institutionFeatures,
  102. v1Features,
  103. bonusFeatures,
  104. featuresOverrides,
  105. ])
  106. const features = _.reduce(
  107. featureSets,
  108. FeaturesUpdater._mergeFeatures,
  109. Settings.defaultFeatures
  110. )
  111. callback(null, features)
  112. })
  113. },
  114. _getIndividualFeatures(userId, callback) {
  115. SubscriptionLocator.getUserIndividualSubscription(userId, (err, sub) =>
  116. callback(err, FeaturesUpdater._subscriptionToFeatures(sub))
  117. )
  118. },
  119. _getGroupFeatureSets(userId, callback) {
  120. SubscriptionLocator.getGroupSubscriptionsMemberOf(userId, (err, subs) =>
  121. callback(err, (subs || []).map(FeaturesUpdater._subscriptionToFeatures))
  122. )
  123. },
  124. _getFeaturesOverrides(userId, callback) {
  125. UserGetter.getUser(userId, { featuresOverrides: 1 }, (error, user) => {
  126. if (error) {
  127. return callback(error)
  128. }
  129. if (
  130. !user ||
  131. !user.featuresOverrides ||
  132. user.featuresOverrides.length === 0
  133. ) {
  134. return callback(null, {})
  135. }
  136. const activeFeaturesOverrides = []
  137. for (const featuresOverride of user.featuresOverrides) {
  138. if (
  139. !featuresOverride.expiresAt ||
  140. featuresOverride.expiresAt > new Date()
  141. ) {
  142. activeFeaturesOverrides.push(featuresOverride.features)
  143. }
  144. }
  145. const features = _.reduce(
  146. activeFeaturesOverrides,
  147. FeaturesUpdater._mergeFeatures,
  148. {}
  149. )
  150. callback(null, features)
  151. })
  152. },
  153. _getV1Features(userId, callback) {
  154. V1SubscriptionManager.getPlanCodeFromV1(
  155. userId,
  156. function (err, planCode, v1Id) {
  157. if (err) {
  158. if ((err ? err.name : undefined) === 'NotFoundError') {
  159. return callback(null, [])
  160. }
  161. return callback(err)
  162. }
  163. callback(
  164. err,
  165. FeaturesUpdater._mergeFeatures(
  166. V1SubscriptionManager.getGrandfatheredFeaturesForV1User(v1Id) || {},
  167. FeaturesUpdater._planCodeToFeatures(planCode)
  168. )
  169. )
  170. }
  171. )
  172. },
  173. _mergeFeatures(featuresA, featuresB) {
  174. const features = Object.assign({}, featuresA)
  175. for (const key in featuresB) {
  176. // Special merging logic for non-boolean features
  177. if (key === 'compileGroup') {
  178. if (
  179. features.compileGroup === 'priority' ||
  180. featuresB.compileGroup === 'priority'
  181. ) {
  182. features.compileGroup = 'priority'
  183. } else {
  184. features.compileGroup = 'standard'
  185. }
  186. } else if (key === 'collaborators') {
  187. if (features.collaborators === -1 || featuresB.collaborators === -1) {
  188. features.collaborators = -1
  189. } else {
  190. features.collaborators = Math.max(
  191. features.collaborators || 0,
  192. featuresB.collaborators || 0
  193. )
  194. }
  195. } else if (key === 'compileTimeout') {
  196. features.compileTimeout = Math.max(
  197. features.compileTimeout || 0,
  198. featuresB.compileTimeout || 0
  199. )
  200. } else {
  201. // Boolean keys, true is better
  202. features[key] = features[key] || featuresB[key]
  203. }
  204. }
  205. return features
  206. },
  207. _subscriptionToFeatures(subscription) {
  208. return FeaturesUpdater._planCodeToFeatures(
  209. subscription ? subscription.planCode : undefined
  210. )
  211. },
  212. _planCodeToFeatures(planCode) {
  213. if (!planCode) {
  214. return {}
  215. }
  216. const plan = PlansLocator.findLocalPlanInSettings(planCode)
  217. if (!plan) {
  218. return {}
  219. } else {
  220. return plan.features
  221. }
  222. },
  223. compareFeatures(currentFeatures, expectedFeatures) {
  224. currentFeatures = _.clone(currentFeatures)
  225. expectedFeatures = _.clone(expectedFeatures)
  226. if (_.isEqual(currentFeatures, expectedFeatures)) {
  227. return {}
  228. }
  229. const mismatchReasons = {}
  230. const featureKeys = [
  231. ...new Set([
  232. ...Object.keys(currentFeatures),
  233. ...Object.keys(expectedFeatures),
  234. ]),
  235. ]
  236. featureKeys.sort().forEach(key => {
  237. if (expectedFeatures[key] !== currentFeatures[key]) {
  238. mismatchReasons[key] = expectedFeatures[key]
  239. }
  240. })
  241. if (mismatchReasons.compileTimeout) {
  242. // store the compile timeout difference instead of the new compile timeout
  243. mismatchReasons.compileTimeout =
  244. expectedFeatures.compileTimeout - currentFeatures.compileTimeout
  245. }
  246. if (mismatchReasons.collaborators) {
  247. // store the collaborators difference instead of the new number only
  248. // replace -1 by 100 to make it clearer
  249. if (expectedFeatures.collaborators === -1) {
  250. expectedFeatures.collaborators = 100
  251. }
  252. if (currentFeatures.collaborators === -1) {
  253. currentFeatures.collaborators = 100
  254. }
  255. mismatchReasons.collaborators =
  256. expectedFeatures.collaborators - currentFeatures.collaborators
  257. }
  258. return mismatchReasons
  259. },
  260. doSyncFromV1(v1UserId, callback) {
  261. logger.log({ v1UserId }, '[AccountSync] starting account sync')
  262. return UserGetter.getUser(
  263. { 'overleaf.id': v1UserId },
  264. { _id: 1 },
  265. function (err, user) {
  266. if (err != null) {
  267. OError.tag(err, '[AccountSync] error getting user', {
  268. v1UserId,
  269. })
  270. return callback(err)
  271. }
  272. if ((user != null ? user._id : undefined) == null) {
  273. logger.warn({ v1UserId }, '[AccountSync] no user found for v1 id')
  274. return callback(null)
  275. }
  276. logger.log(
  277. { v1UserId, userId: user._id },
  278. '[AccountSync] updating user subscription and features'
  279. )
  280. return FeaturesUpdater.refreshFeatures(user._id, callback)
  281. }
  282. )
  283. },
  284. }
  285. const refreshFeaturesPromise = userId =>
  286. new Promise(function (resolve, reject) {
  287. FeaturesUpdater.refreshFeatures(
  288. userId,
  289. (error, features, featuresChanged) => {
  290. if (error) {
  291. reject(error)
  292. } else {
  293. resolve({ features, featuresChanged })
  294. }
  295. }
  296. )
  297. })
  298. FeaturesUpdater.promises = {
  299. refreshFeatures: refreshFeaturesPromise,
  300. }
  301. module.exports = FeaturesUpdater