You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

middleware.web.ts 8.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. import { v4 as uuidv4 } from 'uuid';
  2. import { IStore } from '../app/types';
  3. import { getCurrentConference } from '../base/conference/functions';
  4. import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
  5. import { getLocalParticipant, getParticipantDisplayName } from '../base/participants/functions';
  6. import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
  7. import StateListenerRegistry from '../base/redux/StateListenerRegistry';
  8. import { showErrorNotification } from '../notifications/actions';
  9. import { NOTIFICATION_TIMEOUT_TYPE, NOTIFICATION_TYPE } from '../notifications/constants';
  10. import { DOWNLOAD_FILE, REMOVE_FILE, UPLOAD_FILES, _FILE_LIST_RECEIVED, _FILE_REMOVED } from './actionTypes';
  11. import { addFile, removeFile, updateFileProgress } from './actions';
  12. import { getFileExtension } from './functions.any';
  13. import logger from './logger';
  14. import { IFileMetadata } from './types';
  15. /**
  16. * Registers a change handler for state['features/base/conference'].conference to
  17. * set the event listeners needed for the file sharing feature to operate.
  18. */
  19. StateListenerRegistry.register(
  20. state => state['features/base/conference'].conference,
  21. (conference, { dispatch }, previousConference) => {
  22. if (conference && !previousConference) {
  23. conference.on(JitsiConferenceEvents.FILE_SHARING_FILE_ADDED, (file: IFileMetadata) => {
  24. dispatch(addFile(file));
  25. });
  26. conference.on(JitsiConferenceEvents.FILE_SHARING_FILE_REMOVED, (fileId: string) => {
  27. dispatch({
  28. type: _FILE_REMOVED,
  29. fileId
  30. });
  31. });
  32. conference.on(JitsiConferenceEvents.FILE_SHARING_FILES_RECEIVED, (files: object) => {
  33. dispatch({
  34. type: _FILE_LIST_RECEIVED,
  35. files
  36. });
  37. });
  38. }
  39. });
  40. /**
  41. * Middleware that handles file sharing actions.
  42. *
  43. * @param {Store} store - The redux store.
  44. * @returns {Function}
  45. */
  46. MiddlewareRegistry.register(store => next => action => {
  47. switch (action.type) {
  48. case UPLOAD_FILES: {
  49. const state = store.getState();
  50. const conference = getCurrentConference(state);
  51. conference?.getShortTermCredentials(conference?.getFileSharing()?.getIdentityType()).then((token: string) => {
  52. for (const file of action.files) {
  53. uploadFile(file, store, token);
  54. }
  55. });
  56. return next(action);
  57. }
  58. case REMOVE_FILE: {
  59. const state = store.getState();
  60. const conference = getCurrentConference(state);
  61. const { files } = state['features/file-sharing'];
  62. const fileId = action.fileId;
  63. const existingMetadata = files.get(fileId);
  64. // ignore remove a file till the file is actually uploaded
  65. if (!conference || (existingMetadata?.progress ?? 100) !== 100) {
  66. return next(action);
  67. }
  68. // First, remove the file metadata so others won't attempt to download it anymore.
  69. conference.getFileSharing().removeFile(fileId);
  70. // remove it from local state
  71. store.dispatch({
  72. type: _FILE_REMOVED,
  73. fileId
  74. });
  75. const { fileSharing } = state['features/base/config'];
  76. const sessionId = conference.getMeetingUniqueId();
  77. // Now delete it from the server.
  78. conference.getShortTermCredentials(conference.getFileSharing().getIdentityType())
  79. .then((token: string) => fetch(`${fileSharing!.apiUrl!}/sessions/${sessionId}/files/${fileId}`, {
  80. method: 'DELETE',
  81. headers: {
  82. 'Authorization': `Bearer ${token}`
  83. }
  84. }))
  85. .then((response: { ok: any; statusText: any; }) => {
  86. if (!response.ok) {
  87. throw new Error(`Failed to delete file: ${response.statusText}`);
  88. }
  89. })
  90. .catch((error: any) => {
  91. logger.warn('Could not delete file:', error);
  92. });
  93. return next(action);
  94. }
  95. case DOWNLOAD_FILE: {
  96. const state = store.getState();
  97. const { fileSharing } = state['features/base/config'];
  98. const conference = getCurrentConference(state);
  99. const sessionId = conference?.getMeetingUniqueId();
  100. conference?.getShortTermCredentials(conference?.getFileSharing()?.getIdentityType())
  101. .then((token: string) => fetch(`${fileSharing!.apiUrl!}/sessions/${sessionId}/files/${action.fileId}`, {
  102. method: 'GET',
  103. headers: {
  104. 'Authorization': `Bearer ${token}`
  105. }
  106. }))
  107. .then((response: any) => response.json())
  108. .then((data: { presignedUrl: any; }) => {
  109. const url = data.presignedUrl;
  110. if (!url) {
  111. throw new Error('No presigned URL found in the response.');
  112. }
  113. window.open(url, '_blank', 'noreferrer,noopener');
  114. })
  115. .catch((error: any) => {
  116. logger.warn('Could not download file:', error);
  117. store.dispatch(showErrorNotification({
  118. titleKey: 'fileSharing.downloadFailedTitle',
  119. descriptionKey: 'fileSharing.downloadFailedDescription',
  120. appearance: NOTIFICATION_TYPE.ERROR
  121. }, NOTIFICATION_TIMEOUT_TYPE.STICKY));
  122. });
  123. return next(action);
  124. }
  125. }
  126. return next(action);
  127. });
  128. /**
  129. * Uploads a file to the server.
  130. *
  131. * @param {File} file - The file to upload.
  132. * @param {IStore} store - The redux store.
  133. * @param {string} token - The token to use for requests.
  134. * @returns {void}
  135. */
  136. function uploadFile(file: File, store: IStore, token: string): void {
  137. const state = store.getState();
  138. const conference = getCurrentConference(state);
  139. const sessionId = conference?.getMeetingUniqueId();
  140. const localParticipant = getLocalParticipant(state);
  141. const { fileSharing } = state['features/base/config'];
  142. const { connection } = state['features/base/connection'];
  143. const roomJid = conference?.room?.roomjid;
  144. const jid = connection!.getJid();
  145. const fileId = uuidv4();
  146. const fileMetadata: IFileMetadata = {
  147. authorParticipantId: localParticipant!.id,
  148. authorParticipantJid: jid,
  149. authorParticipantName: getParticipantDisplayName(state, localParticipant!.id),
  150. conferenceFullName: roomJid ?? '',
  151. fileId,
  152. fileName: file.name,
  153. fileSize: file.size,
  154. fileType: getFileExtension(file.name),
  155. timestamp: Date.now()
  156. };
  157. store.dispatch(addFile(fileMetadata));
  158. store.dispatch(updateFileProgress(fileId, 1));
  159. // Upload file.
  160. const formData = new FormData();
  161. formData.append('metadata', JSON.stringify(fileMetadata));
  162. // @ts-ignore
  163. formData.append('file', file as Blob, file.name);
  164. // Use XMLHttpRequest to track upload
  165. const xhr = new XMLHttpRequest();
  166. const handleError = () => {
  167. logger.warn('Could not upload file:', xhr.statusText);
  168. store.dispatch(removeFile(fileId));
  169. store.dispatch(showErrorNotification({
  170. titleKey: 'fileSharing.uploadFailedTitle',
  171. descriptionKey: 'fileSharing.uploadFailedDescription',
  172. appearance: NOTIFICATION_TYPE.ERROR
  173. }, NOTIFICATION_TIMEOUT_TYPE.STICKY));
  174. };
  175. xhr.open('POST', `${fileSharing!.apiUrl!}/sessions/${sessionId}/files`);
  176. xhr.responseType = 'json';
  177. if (token) {
  178. xhr.setRequestHeader('Authorization', `Bearer ${token}`);
  179. }
  180. xhr.upload.onprogress = event => {
  181. if (event.lengthComputable) {
  182. // We use 99% as the max value to avoid showing 100% before the
  183. // upload is actually finished, that is, when the request is completed.
  184. const percent = Math.min((event.loaded / event.total) * 100, 99);
  185. store.dispatch(updateFileProgress(fileId, percent));
  186. }
  187. };
  188. xhr.onload = () => {
  189. if (xhr.status >= 200 && xhr.status < 300) {
  190. store.dispatch(updateFileProgress(fileId, 100));
  191. const fileSharingHandler = conference?.getFileSharing();
  192. fileSharingHandler.addFile(fileMetadata);
  193. } else {
  194. handleError();
  195. }
  196. };
  197. xhr.onerror = handleError;
  198. xhr.send(formData);
  199. }