Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

middleware.ts 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. import i18n from 'i18next';
  2. import { batch } from 'react-redux';
  3. import { IStore } from '../app/types';
  4. import { IStateful } from '../base/app/types';
  5. import {
  6. CONFERENCE_JOINED,
  7. CONFERENCE_JOIN_IN_PROGRESS,
  8. ENDPOINT_MESSAGE_RECEIVED,
  9. UPDATE_CONFERENCE_METADATA
  10. } from '../base/conference/actionTypes';
  11. import { SET_CONFIG } from '../base/config/actionTypes';
  12. import { CONNECTION_FAILED } from '../base/connection/actionTypes';
  13. import { connect, setPreferVisitor } from '../base/connection/actions';
  14. import { disconnect } from '../base/connection/actions.any';
  15. import { openDialog } from '../base/dialog/actions';
  16. import { JitsiConferenceEvents, JitsiConnectionErrors } from '../base/lib-jitsi-meet';
  17. import { PARTICIPANT_UPDATED } from '../base/participants/actionTypes';
  18. import { raiseHand } from '../base/participants/actions';
  19. import {
  20. getLocalParticipant,
  21. getParticipantById,
  22. isLocalParticipantModerator
  23. } from '../base/participants/functions';
  24. import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
  25. import { toState } from '../base/redux/functions';
  26. import { BUTTON_TYPES } from '../base/ui/constants.any';
  27. import { hideNotification, showNotification } from '../notifications/actions';
  28. import {
  29. NOTIFICATION_ICON,
  30. NOTIFICATION_TIMEOUT_TYPE,
  31. VISITORS_NOT_LIVE_NOTIFICATION_ID,
  32. VISITORS_PROMOTION_NOTIFICATION_ID
  33. } from '../notifications/constants';
  34. import { INotificationProps } from '../notifications/types';
  35. import { open as openParticipantsPane } from '../participants-pane/actions';
  36. import { joinConference } from '../prejoin/actions';
  37. import { UPDATE_VISITORS_IN_QUEUE_COUNT } from './actionTypes';
  38. import {
  39. approveRequest,
  40. clearPromotionRequest,
  41. denyRequest,
  42. goLive,
  43. promotionRequestReceived,
  44. setInVisitorsQueue,
  45. setVisitorDemoteActor,
  46. setVisitorsSupported,
  47. updateVisitorsCount,
  48. updateVisitorsInQueueCount
  49. } from './actions';
  50. import { JoinMeetingDialog } from './components';
  51. import { getPromotionRequests, getVisitorsCount, getVisitorsInQueueCount } from './functions';
  52. import logger from './logger';
  53. import { WebsocketClient } from './websocket-client';
  54. MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
  55. switch (action.type) {
  56. case CONFERENCE_JOIN_IN_PROGRESS: {
  57. const { conference } = action;
  58. conference.on(JitsiConferenceEvents.PROPERTIES_CHANGED, (properties: { 'visitor-count': number; }) => {
  59. const visitorCount = Number(properties?.['visitor-count']);
  60. if (!isNaN(visitorCount) && getVisitorsCount(getState) !== visitorCount) {
  61. dispatch(updateVisitorsCount(visitorCount));
  62. }
  63. });
  64. break;
  65. }
  66. case CONFERENCE_JOINED: {
  67. const { conference } = action;
  68. if (getState()['features/visitors'].iAmVisitor) {
  69. const { demoteActorDisplayName } = getState()['features/visitors'];
  70. if (demoteActorDisplayName) {
  71. const notificationParams: INotificationProps = {
  72. titleKey: 'visitors.notification.title',
  73. descriptionKey: 'visitors.notification.demoteDescription',
  74. descriptionArguments: {
  75. actor: demoteActorDisplayName
  76. }
  77. };
  78. batch(() => {
  79. dispatch(showNotification(notificationParams, NOTIFICATION_TIMEOUT_TYPE.STICKY));
  80. dispatch(setVisitorDemoteActor(undefined));
  81. });
  82. } else {
  83. dispatch(openDialog(JoinMeetingDialog));
  84. }
  85. } else {
  86. dispatch(setVisitorsSupported(conference.isVisitorsSupported()));
  87. conference.on(JitsiConferenceEvents.VISITORS_SUPPORTED_CHANGED, (value: boolean) => {
  88. dispatch(setVisitorsSupported(value));
  89. });
  90. }
  91. conference.on(JitsiConferenceEvents.VISITORS_MESSAGE, (
  92. msg: { action: string; actor: string; from: string; id: string; nick: string; on: boolean; }) => {
  93. if (msg.action === 'demote-request') {
  94. // we need it before the disconnect
  95. const participantById = getParticipantById(getState, msg.actor);
  96. const localParticipant = getLocalParticipant(getState);
  97. if (localParticipant && localParticipant.id === msg.id) {
  98. // handle demote
  99. dispatch(disconnect(true))
  100. .then(() => {
  101. dispatch(setPreferVisitor(true));
  102. // we need to set the name, so we can use it later in the notification
  103. if (participantById) {
  104. dispatch(setVisitorDemoteActor(participantById.name));
  105. }
  106. logger.info('Dispatching connect on demote request visitor message for local participant.');
  107. return dispatch(connect());
  108. });
  109. }
  110. } else if (msg.action === 'promotion-request') {
  111. const request = {
  112. from: msg.from,
  113. nick: msg.nick
  114. };
  115. if (msg.on) {
  116. dispatch(promotionRequestReceived(request));
  117. } else {
  118. dispatch(clearPromotionRequest(request));
  119. }
  120. _handlePromotionNotification({
  121. dispatch,
  122. getState
  123. });
  124. } else {
  125. logger.error('Unknown action:', msg.action);
  126. }
  127. });
  128. conference.on(JitsiConferenceEvents.VISITORS_REJECTION, () => {
  129. dispatch(raiseHand(false));
  130. });
  131. break;
  132. }
  133. case ENDPOINT_MESSAGE_RECEIVED: {
  134. const { data } = action;
  135. if (data?.action === 'promotion-response' && data.approved) {
  136. const request = getPromotionRequests(getState())
  137. .find((r: any) => r.from === data.id);
  138. request && dispatch(clearPromotionRequest(request));
  139. }
  140. break;
  141. }
  142. case CONNECTION_FAILED: {
  143. const { error } = action;
  144. if (error?.name !== JitsiConnectionErrors.NOT_LIVE_ERROR) {
  145. break;
  146. }
  147. const { hosts, visitors: visitorsConfig } = getState()['features/base/config'];
  148. const { locationURL, preferVisitor } = getState()['features/base/connection'];
  149. if (!visitorsConfig?.queueService || !locationURL || !preferVisitor) {
  150. break;
  151. }
  152. // let's subscribe for visitor waiting queue
  153. const { room } = getState()['features/base/conference'];
  154. const conferenceJid = `${room}@${hosts?.muc}`;
  155. WebsocketClient.getInstance()
  156. .connect(`wss://${visitorsConfig?.queueService}/visitor/websocket`,
  157. `/secured/conference/visitor/topic.${conferenceJid}`,
  158. msg => {
  159. if ('status' in msg && msg.status === 'live') {
  160. logger.info('The conference is now live!');
  161. WebsocketClient.getInstance().disconnect()
  162. .then(() => {
  163. let delay = 0;
  164. // now let's connect to meeting
  165. if ('randomDelayMs' in msg) {
  166. delay = msg.randomDelayMs;
  167. }
  168. if (WebsocketClient.getInstance().connectCount > 1) {
  169. // if we keep connecting/disconnecting, let's slow it down
  170. delay = 30 * 1000;
  171. }
  172. setTimeout(() => {
  173. logger.info('Dispatching joinConference on conference live event.');
  174. dispatch(joinConference());
  175. dispatch(setInVisitorsQueue(false));
  176. }, Math.random() * delay);
  177. });
  178. }
  179. },
  180. getState()['features/base/jwt'].jwt,
  181. () => {
  182. dispatch(setInVisitorsQueue(true));
  183. });
  184. break;
  185. }
  186. case PARTICIPANT_UPDATED: {
  187. const { visitors: visitorsConfig } = toState(getState)['features/base/config'];
  188. if (visitorsConfig?.queueService && isLocalParticipantModerator(getState)) {
  189. const { metadata } = getState()['features/base/conference'];
  190. if (metadata?.visitors?.live === false && !WebsocketClient.getInstance().isActive()) {
  191. // when go live is available and false, we should subscribe
  192. // to the service if available to listen for waiting visitors
  193. _subscribeQueueStats(getState(), dispatch);
  194. }
  195. }
  196. break;
  197. }
  198. case SET_CONFIG: {
  199. const result = next(action);
  200. const { preferVisitor } = action.config;
  201. if (preferVisitor !== undefined) {
  202. setPreferVisitor(preferVisitor);
  203. }
  204. return result;
  205. }
  206. case UPDATE_CONFERENCE_METADATA: {
  207. const { metadata } = action;
  208. const { visitors: visitorsConfig } = toState(getState)['features/base/config'];
  209. if (!visitorsConfig?.queueService) {
  210. break;
  211. }
  212. if (isLocalParticipantModerator(getState)) {
  213. if (metadata?.visitors?.live === false) {
  214. if (!WebsocketClient.getInstance().isActive()) {
  215. // if metadata go live changes to goLive false and local is moderator
  216. // we should subscribe to the service if available to listen for waiting visitors
  217. _subscribeQueueStats(getState(), dispatch);
  218. }
  219. _showNotLiveNotification(dispatch, getVisitorsInQueueCount(getState));
  220. } else if (metadata?.visitors?.live) {
  221. dispatch(hideNotification(VISITORS_NOT_LIVE_NOTIFICATION_ID));
  222. WebsocketClient.getInstance().disconnect();
  223. }
  224. }
  225. break;
  226. }
  227. case UPDATE_VISITORS_IN_QUEUE_COUNT: {
  228. _showNotLiveNotification(dispatch, action.count);
  229. break;
  230. }
  231. }
  232. return next(action);
  233. });
  234. /**
  235. * Shows a notification that the meeting is not live.
  236. *
  237. * @param {Dispatch} dispatch - The Redux dispatch function.
  238. * @param {number} count - The count of visitors waiting.
  239. * @returns {void}
  240. */
  241. function _showNotLiveNotification(dispatch: IStore['dispatch'], count: number): void {
  242. // let's show notification
  243. dispatch(showNotification({
  244. titleKey: 'notify.waitingVisitorsTitle',
  245. descriptionKey: 'notify.waitingVisitors',
  246. descriptionArguments: {
  247. waitingVisitors: count
  248. },
  249. disableClosing: true,
  250. uid: VISITORS_NOT_LIVE_NOTIFICATION_ID,
  251. customActionNameKey: [ 'participantsPane.actions.goLive' ],
  252. customActionType: [ BUTTON_TYPES.PRIMARY ],
  253. customActionHandler: [ () => batch(() => {
  254. dispatch(hideNotification(VISITORS_NOT_LIVE_NOTIFICATION_ID));
  255. dispatch(goLive());
  256. }) ],
  257. icon: NOTIFICATION_ICON.PARTICIPANTS
  258. }, NOTIFICATION_TIMEOUT_TYPE.STICKY));
  259. }
  260. /**
  261. * Subscribe for moderator stats.
  262. *
  263. * @param {Function|Object} stateful - The redux store or {@code getState}
  264. * function.
  265. * @param {Dispatch} dispatch - The Redux dispatch function.
  266. * @returns {void}
  267. */
  268. function _subscribeQueueStats(stateful: IStateful, dispatch: IStore['dispatch']) {
  269. const { hosts } = toState(stateful)['features/base/config'];
  270. const { room } = toState(stateful)['features/base/conference'];
  271. const conferenceJid = `${room}@${hosts?.muc}`;
  272. const { visitors: visitorsConfig } = toState(stateful)['features/base/config'];
  273. WebsocketClient.getInstance()
  274. .connect(`wss://${visitorsConfig?.queueService}/visitor/websocket`,
  275. `/secured/conference/state/topic.${conferenceJid}`,
  276. msg => {
  277. if ('visitorsWaiting' in msg) {
  278. dispatch(updateVisitorsInQueueCount(msg.visitorsWaiting));
  279. }
  280. },
  281. toState(stateful)['features/base/jwt'].jwt);
  282. }
  283. /**
  284. * Function to handle the promotion notification.
  285. *
  286. * @param {Object} store - The Redux store.
  287. * @returns {void}
  288. */
  289. function _handlePromotionNotification(
  290. { dispatch, getState }: { dispatch: IStore['dispatch']; getState: IStore['getState']; }) {
  291. const requests = getPromotionRequests(getState());
  292. if (requests.length === 0) {
  293. dispatch(hideNotification(VISITORS_PROMOTION_NOTIFICATION_ID));
  294. return;
  295. }
  296. let notificationTitle;
  297. let customActionNameKey;
  298. let customActionHandler;
  299. let customActionType;
  300. let descriptionKey;
  301. let icon;
  302. if (requests.length === 1) {
  303. const firstRequest = requests[0];
  304. descriptionKey = 'notify.participantWantsToJoin';
  305. notificationTitle = firstRequest.nick;
  306. icon = NOTIFICATION_ICON.PARTICIPANT;
  307. customActionNameKey = [ 'participantsPane.actions.admit', 'participantsPane.actions.reject' ];
  308. customActionType = [ BUTTON_TYPES.PRIMARY, BUTTON_TYPES.DESTRUCTIVE ];
  309. customActionHandler = [ () => batch(() => {
  310. dispatch(hideNotification(VISITORS_PROMOTION_NOTIFICATION_ID));
  311. dispatch(approveRequest(firstRequest));
  312. }),
  313. () => batch(() => {
  314. dispatch(hideNotification(VISITORS_PROMOTION_NOTIFICATION_ID));
  315. dispatch(denyRequest(firstRequest));
  316. }) ];
  317. } else {
  318. descriptionKey = 'notify.participantsWantToJoin';
  319. notificationTitle = i18n.t('notify.waitingParticipants', {
  320. waitingParticipants: requests.length
  321. });
  322. icon = NOTIFICATION_ICON.PARTICIPANTS;
  323. customActionNameKey = [ 'notify.viewVisitors' ];
  324. customActionType = [ BUTTON_TYPES.PRIMARY ];
  325. customActionHandler = [ () => batch(() => {
  326. dispatch(hideNotification(VISITORS_PROMOTION_NOTIFICATION_ID));
  327. dispatch(openParticipantsPane());
  328. }) ];
  329. }
  330. dispatch(showNotification({
  331. title: notificationTitle,
  332. descriptionKey,
  333. uid: VISITORS_PROMOTION_NOTIFICATION_ID,
  334. customActionNameKey,
  335. customActionType,
  336. customActionHandler,
  337. icon
  338. }, NOTIFICATION_TIMEOUT_TYPE.STICKY));
  339. }