api.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
  2. import { WSConnectionStatus } from '../services/api/api.service.js'
  3. import { maybeShowChatNotification } from '../services/chat_utils/chat_utils.js'
  4. import { Socket } from 'phoenix'
  5. const retryTimeout = (multiplier) => 1000 * multiplier
  6. const api = {
  7. state: {
  8. retryMultiplier: 1,
  9. backendInteractor: backendInteractorService(),
  10. fetchers: {},
  11. socket: null,
  12. mastoUserSocket: null,
  13. mastoUserSocketStatus: null,
  14. followRequests: []
  15. },
  16. mutations: {
  17. setBackendInteractor (state, backendInteractor) {
  18. state.backendInteractor = backendInteractor
  19. },
  20. addFetcher (state, { fetcherName, fetcher }) {
  21. state.fetchers[fetcherName] = fetcher
  22. },
  23. removeFetcher (state, { fetcherName, fetcher }) {
  24. state.fetchers[fetcherName].stop()
  25. delete state.fetchers[fetcherName]
  26. },
  27. setWsToken (state, token) {
  28. state.wsToken = token
  29. },
  30. setSocket (state, socket) {
  31. state.socket = socket
  32. },
  33. setFollowRequests (state, value) {
  34. state.followRequests = value
  35. },
  36. setMastoUserSocketStatus (state, value) {
  37. state.mastoUserSocketStatus = value
  38. },
  39. incrementRetryMultiplier (state) {
  40. state.retryMultiplier = Math.max(++state.retryMultiplier, 3)
  41. },
  42. resetRetryMultiplier (state) {
  43. state.retryMultiplier = 1
  44. }
  45. },
  46. actions: {
  47. /**
  48. * Global MastoAPI socket control, in future should disable ALL sockets/(re)start relevant sockets
  49. *
  50. * @param {Boolean} [initial] - whether this enabling happened at boot time or not
  51. */
  52. enableMastoSockets (store, initial) {
  53. const { state, dispatch, commit } = store
  54. // Do not initialize unless nonexistent or closed
  55. if (
  56. state.mastoUserSocket &&
  57. ![
  58. WebSocket.CLOSED,
  59. WebSocket.CLOSING
  60. ].includes(state.mastoUserSocket.getState())
  61. ) {
  62. return
  63. }
  64. if (initial) {
  65. commit('setMastoUserSocketStatus', WSConnectionStatus.STARTING_INITIAL)
  66. } else {
  67. commit('setMastoUserSocketStatus', WSConnectionStatus.STARTING)
  68. }
  69. return dispatch('startMastoUserSocket')
  70. },
  71. disableMastoSockets (store) {
  72. const { state, dispatch, commit } = store
  73. if (!state.mastoUserSocket) return
  74. commit('setMastoUserSocketStatus', WSConnectionStatus.DISABLED)
  75. return dispatch('stopMastoUserSocket')
  76. },
  77. // MastoAPI 'User' sockets
  78. startMastoUserSocket (store) {
  79. return new Promise((resolve, reject) => {
  80. try {
  81. const { state, commit, dispatch, rootState } = store
  82. const timelineData = rootState.statuses.timelines.friends
  83. state.mastoUserSocket = state.backendInteractor.startUserSocket({ store })
  84. state.mastoUserSocket.addEventListener(
  85. 'message',
  86. ({ detail: message }) => {
  87. if (!message) return // pings
  88. if (message.event === 'notification') {
  89. dispatch('addNewNotifications', {
  90. notifications: [message.notification],
  91. older: false
  92. })
  93. } else if (message.event === 'update') {
  94. dispatch('addNewStatuses', {
  95. statuses: [message.status],
  96. userId: false,
  97. showImmediately: timelineData.visibleStatuses.length === 0,
  98. timeline: 'friends'
  99. })
  100. } else if (message.event === 'delete') {
  101. dispatch('deleteStatusById', message.id)
  102. } else if (message.event === 'pleroma:chat_update') {
  103. // The setTimeout wrapper is a temporary band-aid to avoid duplicates for the user's own messages when doing optimistic sending.
  104. // The cause of the duplicates is the WS event arriving earlier than the HTTP response.
  105. // This setTimeout wrapper can be removed once the commit `8e41baff` is in the stable Pleroma release.
  106. // (`8e41baff` adds the idempotency key to the chat message entity, which PleromaFE uses when it's available, and it makes this artificial delay unnecessary).
  107. setTimeout(() => {
  108. dispatch('addChatMessages', {
  109. chatId: message.chatUpdate.id,
  110. messages: [message.chatUpdate.lastMessage]
  111. })
  112. dispatch('updateChat', { chat: message.chatUpdate })
  113. maybeShowChatNotification(store, message.chatUpdate)
  114. }, 100)
  115. }
  116. }
  117. )
  118. state.mastoUserSocket.addEventListener('open', () => {
  119. // Do not show notification when we just opened up the page
  120. if (state.mastoUserSocketStatus !== WSConnectionStatus.STARTING_INITIAL) {
  121. dispatch('pushGlobalNotice', {
  122. level: 'success',
  123. messageKey: 'timeline.socket_reconnected',
  124. timeout: 5000
  125. })
  126. }
  127. // Stop polling if we were errored or disabled
  128. if (new Set([
  129. WSConnectionStatus.ERROR,
  130. WSConnectionStatus.DISABLED
  131. ]).has(state.mastoUserSocketStatus)) {
  132. dispatch('stopFetchingTimeline', { timeline: 'friends' })
  133. dispatch('stopFetchingNotifications')
  134. dispatch('stopFetchingChats')
  135. }
  136. commit('resetRetryMultiplier')
  137. commit('setMastoUserSocketStatus', WSConnectionStatus.JOINED)
  138. })
  139. state.mastoUserSocket.addEventListener('error', ({ detail: error }) => {
  140. console.error('Error in MastoAPI websocket:', error)
  141. // TODO is this needed?
  142. dispatch('clearOpenedChats')
  143. })
  144. state.mastoUserSocket.addEventListener('close', ({ detail: closeEvent }) => {
  145. const ignoreCodes = new Set([
  146. 1000, // Normal (intended) closure
  147. 1001 // Going away
  148. ])
  149. const { code } = closeEvent
  150. if (ignoreCodes.has(code)) {
  151. console.debug(`Not restarting socket becasue of closure code ${code} is in ignore list`)
  152. commit('setMastoUserSocketStatus', WSConnectionStatus.CLOSED)
  153. } else {
  154. console.warn(`MastoAPI websocket disconnected, restarting. CloseEvent code: ${code}`)
  155. setTimeout(() => {
  156. dispatch('startMastoUserSocket')
  157. }, retryTimeout(state.retryMultiplier))
  158. commit('incrementRetryMultiplier')
  159. if (state.mastoUserSocketStatus !== WSConnectionStatus.ERROR) {
  160. dispatch('startFetchingTimeline', { timeline: 'friends' })
  161. dispatch('startFetchingNotifications')
  162. dispatch('startFetchingChats')
  163. dispatch('pushGlobalNotice', {
  164. level: 'error',
  165. messageKey: 'timeline.socket_broke',
  166. messageArgs: [code],
  167. timeout: 5000
  168. })
  169. }
  170. commit('setMastoUserSocketStatus', WSConnectionStatus.ERROR)
  171. }
  172. dispatch('clearOpenedChats')
  173. })
  174. resolve()
  175. } catch (e) {
  176. reject(e)
  177. }
  178. })
  179. },
  180. stopMastoUserSocket ({ state, dispatch }) {
  181. dispatch('startFetchingTimeline', { timeline: 'friends' })
  182. dispatch('startFetchingNotifications')
  183. dispatch('startFetchingChats')
  184. state.mastoUserSocket.close()
  185. },
  186. // Timelines
  187. startFetchingTimeline (store, {
  188. timeline = 'friends',
  189. tag = false,
  190. userId = false
  191. }) {
  192. if (store.state.fetchers[timeline]) return
  193. const fetcher = store.state.backendInteractor.startFetchingTimeline({
  194. timeline, store, userId, tag
  195. })
  196. store.commit('addFetcher', { fetcherName: timeline, fetcher })
  197. },
  198. stopFetchingTimeline (store, timeline) {
  199. const fetcher = store.state.fetchers[timeline]
  200. if (!fetcher) return
  201. store.commit('removeFetcher', { fetcherName: timeline, fetcher })
  202. },
  203. fetchTimeline (store, timeline, { ...rest }) {
  204. store.state.backendInteractor.fetchTimeline({
  205. store,
  206. timeline,
  207. ...rest
  208. })
  209. },
  210. // Notifications
  211. startFetchingNotifications (store) {
  212. if (store.state.fetchers.notifications) return
  213. const fetcher = store.state.backendInteractor.startFetchingNotifications({ store })
  214. store.commit('addFetcher', { fetcherName: 'notifications', fetcher })
  215. },
  216. stopFetchingNotifications (store) {
  217. const fetcher = store.state.fetchers.notifications
  218. if (!fetcher) return
  219. store.commit('removeFetcher', { fetcherName: 'notifications', fetcher })
  220. },
  221. fetchNotifications (store, { ...rest }) {
  222. store.state.backendInteractor.fetchNotifications({
  223. store,
  224. ...rest
  225. })
  226. },
  227. // Follow requests
  228. startFetchingFollowRequests (store) {
  229. if (store.state.fetchers['followRequests']) return
  230. const fetcher = store.state.backendInteractor.startFetchingFollowRequests({ store })
  231. store.commit('addFetcher', { fetcherName: 'followRequests', fetcher })
  232. },
  233. stopFetchingFollowRequests (store) {
  234. const fetcher = store.state.fetchers.followRequests
  235. if (!fetcher) return
  236. store.commit('removeFetcher', { fetcherName: 'followRequests', fetcher })
  237. },
  238. removeFollowRequest (store, request) {
  239. let requests = store.state.followRequests.filter((it) => it !== request)
  240. store.commit('setFollowRequests', requests)
  241. },
  242. // Pleroma websocket
  243. setWsToken (store, token) {
  244. store.commit('setWsToken', token)
  245. },
  246. initializeSocket ({ dispatch, commit, state, rootState }) {
  247. // Set up websocket connection
  248. const token = state.wsToken
  249. if (rootState.instance.shoutAvailable && typeof token !== 'undefined' && state.socket === null) {
  250. const socket = new Socket('/socket', { params: { token } })
  251. socket.connect()
  252. commit('setSocket', socket)
  253. dispatch('initializeShout', socket)
  254. }
  255. },
  256. disconnectFromSocket ({ commit, state }) {
  257. state.socket && state.socket.disconnect()
  258. commit('setSocket', null)
  259. }
  260. }
  261. }
  262. export default api