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.ts 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  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. return dispatch(connect());
  107. });
  108. }
  109. } else if (msg.action === 'promotion-request') {
  110. const request = {
  111. from: msg.from,
  112. nick: msg.nick
  113. };
  114. if (msg.on) {
  115. dispatch(promotionRequestReceived(request));
  116. } else {
  117. dispatch(clearPromotionRequest(request));
  118. }
  119. _handlePromotionNotification({
  120. dispatch,
  121. getState
  122. });
  123. } else {
  124. logger.error('Unknown action:', msg.action);
  125. }
  126. });
  127. conference.on(JitsiConferenceEvents.VISITORS_REJECTION, () => {
  128. dispatch(raiseHand(false));
  129. });
  130. break;
  131. }
  132. case ENDPOINT_MESSAGE_RECEIVED: {
  133. const { data } = action;
  134. if (data?.action === 'promotion-response' && data.approved) {
  135. const request = getPromotionRequests(getState())
  136. .find((r: any) => r.from === data.id);
  137. request && dispatch(clearPromotionRequest(request));
  138. }
  139. break;
  140. }
  141. case CONNECTION_FAILED: {
  142. const { error } = action;
  143. if (error?.name !== JitsiConnectionErrors.NOT_LIVE_ERROR) {
  144. break;
  145. }
  146. const { hosts, visitors: visitorsConfig } = getState()['features/base/config'];
  147. const { locationURL, preferVisitor } = getState()['features/base/connection'];
  148. if (!visitorsConfig?.queueService || !locationURL || !preferVisitor) {
  149. break;
  150. }
  151. // let's subscribe for visitor waiting queue
  152. const { room } = getState()['features/base/conference'];
  153. const conferenceJid = `${room}@${hosts?.muc}`;
  154. WebsocketClient.getInstance()
  155. .connect(`wss://${visitorsConfig?.queueService}/visitor/websocket`,
  156. `/secured/conference/visitor/topic.${conferenceJid}`,
  157. msg => {
  158. if ('status' in msg && msg.status === 'live') {
  159. logger.info('The conference is now live!');
  160. WebsocketClient.getInstance().disconnect()
  161. .then(() => {
  162. let delay = 0;
  163. // now let's connect to meeting
  164. if ('randomDelayMs' in msg) {
  165. delay = msg.randomDelayMs;
  166. }
  167. if (WebsocketClient.getInstance().connectCount > 1) {
  168. // if we keep connecting/disconnecting, let's slow it down
  169. delay = 30 * 1000;
  170. }
  171. setTimeout(() => {
  172. dispatch(joinConference());
  173. dispatch(setInVisitorsQueue(false));
  174. }, Math.random() * delay);
  175. });
  176. }
  177. },
  178. getState()['features/base/jwt'].jwt,
  179. () => {
  180. dispatch(setInVisitorsQueue(true));
  181. });
  182. break;
  183. }
  184. case PARTICIPANT_UPDATED: {
  185. const { visitors: visitorsConfig } = toState(getState)['features/base/config'];
  186. if (visitorsConfig?.queueService && isLocalParticipantModerator(getState)) {
  187. const { metadata } = getState()['features/base/conference'];
  188. if (metadata?.visitors?.live === false && !WebsocketClient.getInstance().isActive()) {
  189. // when go live is available and false, we should subscribe
  190. // to the service if available to listen for waiting visitors
  191. _subscribeQueueStats(getState(), dispatch);
  192. }
  193. }
  194. break;
  195. }
  196. case SET_CONFIG: {
  197. const result = next(action);
  198. const { preferVisitor } = action.config;
  199. if (preferVisitor !== undefined) {
  200. setPreferVisitor(preferVisitor);
  201. }
  202. return result;
  203. }
  204. case UPDATE_CONFERENCE_METADATA: {
  205. const { metadata } = action;
  206. const { visitors: visitorsConfig } = toState(getState)['features/base/config'];
  207. if (!visitorsConfig?.queueService) {
  208. break;
  209. }
  210. if (isLocalParticipantModerator(getState)) {
  211. if (metadata?.visitors?.live === false) {
  212. if (!WebsocketClient.getInstance().isActive()) {
  213. // if metadata go live changes to goLive false and local is moderator
  214. // we should subscribe to the service if available to listen for waiting visitors
  215. _subscribeQueueStats(getState(), dispatch);
  216. }
  217. _showNotLiveNotification(dispatch, getVisitorsInQueueCount(getState));
  218. } else if (metadata?.visitors?.live) {
  219. dispatch(hideNotification(VISITORS_NOT_LIVE_NOTIFICATION_ID));
  220. WebsocketClient.getInstance().disconnect();
  221. }
  222. }
  223. break;
  224. }
  225. case UPDATE_VISITORS_IN_QUEUE_COUNT: {
  226. _showNotLiveNotification(dispatch, action.count);
  227. break;
  228. }
  229. }
  230. return next(action);
  231. });
  232. /**
  233. * Shows a notification that the meeting is not live.
  234. *
  235. * @param {Dispatch} dispatch - The Redux dispatch function.
  236. * @param {number} count - The count of visitors waiting.
  237. * @returns {void}
  238. */
  239. function _showNotLiveNotification(dispatch: IStore['dispatch'], count: number): void {
  240. // let's show notification
  241. dispatch(showNotification({
  242. titleKey: 'notify.waitingVisitorsTitle',
  243. descriptionKey: 'notify.waitingVisitors',
  244. descriptionArguments: {
  245. waitingVisitors: count
  246. },
  247. disableClosing: true,
  248. uid: VISITORS_NOT_LIVE_NOTIFICATION_ID,
  249. customActionNameKey: [ 'participantsPane.actions.goLive' ],
  250. customActionType: [ BUTTON_TYPES.PRIMARY ],
  251. customActionHandler: [ () => batch(() => {
  252. dispatch(hideNotification(VISITORS_NOT_LIVE_NOTIFICATION_ID));
  253. dispatch(goLive());
  254. }) ],
  255. icon: NOTIFICATION_ICON.PARTICIPANTS
  256. }, NOTIFICATION_TIMEOUT_TYPE.STICKY));
  257. }
  258. /**
  259. * Subscribe for moderator stats.
  260. *
  261. * @param {Function|Object} stateful - The redux store or {@code getState}
  262. * function.
  263. * @param {Dispatch} dispatch - The Redux dispatch function.
  264. * @returns {void}
  265. */
  266. function _subscribeQueueStats(stateful: IStateful, dispatch: IStore['dispatch']) {
  267. const { hosts } = toState(stateful)['features/base/config'];
  268. const { room } = toState(stateful)['features/base/conference'];
  269. const conferenceJid = `${room}@${hosts?.muc}`;
  270. const { visitors: visitorsConfig } = toState(stateful)['features/base/config'];
  271. WebsocketClient.getInstance()
  272. .connect(`wss://${visitorsConfig?.queueService}/visitor/websocket`,
  273. `/secured/conference/state/topic.${conferenceJid}`,
  274. msg => {
  275. if ('visitorsWaiting' in msg) {
  276. dispatch(updateVisitorsInQueueCount(msg.visitorsWaiting));
  277. }
  278. },
  279. toState(stateful)['features/base/jwt'].jwt);
  280. }
  281. /**
  282. * Function to handle the promotion notification.
  283. *
  284. * @param {Object} store - The Redux store.
  285. * @returns {void}
  286. */
  287. function _handlePromotionNotification(
  288. { dispatch, getState }: { dispatch: IStore['dispatch']; getState: IStore['getState']; }) {
  289. const requests = getPromotionRequests(getState());
  290. if (requests.length === 0) {
  291. dispatch(hideNotification(VISITORS_PROMOTION_NOTIFICATION_ID));
  292. return;
  293. }
  294. let notificationTitle;
  295. let customActionNameKey;
  296. let customActionHandler;
  297. let customActionType;
  298. let descriptionKey;
  299. let icon;
  300. if (requests.length === 1) {
  301. const firstRequest = requests[0];
  302. descriptionKey = 'notify.participantWantsToJoin';
  303. notificationTitle = firstRequest.nick;
  304. icon = NOTIFICATION_ICON.PARTICIPANT;
  305. customActionNameKey = [ 'participantsPane.actions.admit', 'participantsPane.actions.reject' ];
  306. customActionType = [ BUTTON_TYPES.PRIMARY, BUTTON_TYPES.DESTRUCTIVE ];
  307. customActionHandler = [ () => batch(() => {
  308. dispatch(hideNotification(VISITORS_PROMOTION_NOTIFICATION_ID));
  309. dispatch(approveRequest(firstRequest));
  310. }),
  311. () => batch(() => {
  312. dispatch(hideNotification(VISITORS_PROMOTION_NOTIFICATION_ID));
  313. dispatch(denyRequest(firstRequest));
  314. }) ];
  315. } else {
  316. descriptionKey = 'notify.participantsWantToJoin';
  317. notificationTitle = i18n.t('notify.waitingParticipants', {
  318. waitingParticipants: requests.length
  319. });
  320. icon = NOTIFICATION_ICON.PARTICIPANTS;
  321. customActionNameKey = [ 'notify.viewVisitors' ];
  322. customActionType = [ BUTTON_TYPES.PRIMARY ];
  323. customActionHandler = [ () => batch(() => {
  324. dispatch(hideNotification(VISITORS_PROMOTION_NOTIFICATION_ID));
  325. dispatch(openParticipantsPane());
  326. }) ];
  327. }
  328. dispatch(showNotification({
  329. title: notificationTitle,
  330. descriptionKey,
  331. uid: VISITORS_PROMOTION_NOTIFICATION_ID,
  332. customActionNameKey,
  333. customActionType,
  334. customActionHandler,
  335. icon
  336. }, NOTIFICATION_TIMEOUT_TYPE.STICKY));
  337. }