ConnectedUsersManager.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. /* eslint-disable
  2. camelcase,
  3. */
  4. const async = require('async')
  5. const Settings = require('settings-sharelatex')
  6. const logger = require('logger-sharelatex')
  7. const redis = require('redis-sharelatex')
  8. const rclient = redis.createClient(Settings.redis.realtime)
  9. const Keys = Settings.redis.realtime.key_schema
  10. const ONE_HOUR_IN_S = 60 * 60
  11. const ONE_DAY_IN_S = ONE_HOUR_IN_S * 24
  12. const FOUR_DAYS_IN_S = ONE_DAY_IN_S * 4
  13. const USER_TIMEOUT_IN_S = ONE_HOUR_IN_S / 4
  14. const REFRESH_TIMEOUT_IN_S = 10 // only show clients which have responded to a refresh request in the last 10 seconds
  15. module.exports = {
  16. // Use the same method for when a user connects, and when a user sends a cursor
  17. // update. This way we don't care if the connected_user key has expired when
  18. // we receive a cursor update.
  19. updateUserPosition(project_id, client_id, user, cursorData, callback) {
  20. logger.log({ project_id, client_id }, 'marking user as joined or connected')
  21. const multi = rclient.multi()
  22. multi.sadd(Keys.clientsInProject({ project_id }), client_id)
  23. multi.expire(Keys.clientsInProject({ project_id }), FOUR_DAYS_IN_S)
  24. multi.hset(
  25. Keys.connectedUser({ project_id, client_id }),
  26. 'last_updated_at',
  27. Date.now()
  28. )
  29. multi.hset(
  30. Keys.connectedUser({ project_id, client_id }),
  31. 'user_id',
  32. user._id
  33. )
  34. multi.hset(
  35. Keys.connectedUser({ project_id, client_id }),
  36. 'first_name',
  37. user.first_name || ''
  38. )
  39. multi.hset(
  40. Keys.connectedUser({ project_id, client_id }),
  41. 'last_name',
  42. user.last_name || ''
  43. )
  44. multi.hset(
  45. Keys.connectedUser({ project_id, client_id }),
  46. 'email',
  47. user.email || ''
  48. )
  49. if (cursorData) {
  50. multi.hset(
  51. Keys.connectedUser({ project_id, client_id }),
  52. 'cursorData',
  53. JSON.stringify(cursorData)
  54. )
  55. }
  56. multi.expire(
  57. Keys.connectedUser({ project_id, client_id }),
  58. USER_TIMEOUT_IN_S
  59. )
  60. multi.exec(function (err) {
  61. if (err) {
  62. logger.err(
  63. { err, project_id, client_id },
  64. 'problem marking user as connected'
  65. )
  66. }
  67. callback(err)
  68. })
  69. },
  70. refreshClient(project_id, client_id) {
  71. logger.log({ project_id, client_id }, 'refreshing connected client')
  72. const multi = rclient.multi()
  73. multi.hset(
  74. Keys.connectedUser({ project_id, client_id }),
  75. 'last_updated_at',
  76. Date.now()
  77. )
  78. multi.expire(
  79. Keys.connectedUser({ project_id, client_id }),
  80. USER_TIMEOUT_IN_S
  81. )
  82. multi.exec(function (err) {
  83. if (err) {
  84. logger.err(
  85. { err, project_id, client_id },
  86. 'problem refreshing connected client'
  87. )
  88. }
  89. })
  90. },
  91. markUserAsDisconnected(project_id, client_id, callback) {
  92. logger.log({ project_id, client_id }, 'marking user as disconnected')
  93. const multi = rclient.multi()
  94. multi.srem(Keys.clientsInProject({ project_id }), client_id)
  95. multi.expire(Keys.clientsInProject({ project_id }), FOUR_DAYS_IN_S)
  96. multi.del(Keys.connectedUser({ project_id, client_id }))
  97. multi.exec(callback)
  98. },
  99. _getConnectedUser(project_id, client_id, callback) {
  100. rclient.hgetall(Keys.connectedUser({ project_id, client_id }), function (
  101. err,
  102. result
  103. ) {
  104. if (!(result && result.user_id)) {
  105. result = {
  106. connected: false,
  107. client_id
  108. }
  109. } else {
  110. result.connected = true
  111. result.client_id = client_id
  112. result.client_age =
  113. (Date.now() - parseInt(result.last_updated_at, 10)) / 1000
  114. if (result.cursorData) {
  115. try {
  116. result.cursorData = JSON.parse(result.cursorData)
  117. } catch (e) {
  118. logger.error(
  119. {
  120. err: e,
  121. project_id,
  122. client_id,
  123. cursorData: result.cursorData
  124. },
  125. 'error parsing cursorData JSON'
  126. )
  127. return callback(e)
  128. }
  129. }
  130. }
  131. callback(err, result)
  132. })
  133. },
  134. getConnectedUsers(project_id, callback) {
  135. const self = this
  136. rclient.smembers(Keys.clientsInProject({ project_id }), function (
  137. err,
  138. results
  139. ) {
  140. if (err) {
  141. return callback(err)
  142. }
  143. const jobs = results.map((client_id) => (cb) =>
  144. self._getConnectedUser(project_id, client_id, cb)
  145. )
  146. async.series(jobs, function (err, users) {
  147. if (err) {
  148. return callback(err)
  149. }
  150. users = users.filter(
  151. (user) =>
  152. user && user.connected && user.client_age < REFRESH_TIMEOUT_IN_S
  153. )
  154. callback(null, users)
  155. })
  156. })
  157. }
  158. }