Refactor WebSocket message interfaces
Changes:
- Replace AccountTextMessage with an extendable ContactMessage interface
- Add accountId parameter to server-side WebSocket callbacks
- Set the accountId for WebRTC messages on server-side for security
- Rename all WebRTC and SDP variables to proper camelCase or PascalCase
GitLab: #147
Change-Id: I125b5431821b03ef4d46b751eb1c13830017ccff
diff --git a/client/src/contexts/CallProvider.tsx b/client/src/contexts/CallProvider.tsx
index 0cc667a..a17e738 100644
--- a/client/src/contexts/CallProvider.tsx
+++ b/client/src/contexts/CallProvider.tsx
@@ -15,16 +15,15 @@
* License along with this program. If not, see
* <https://www.gnu.org/licenses/>.
*/
-import { AccountTextMessage, WebSocketMessageType } from 'jami-web-common';
-import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
+import { CallAction, WebSocketMessageType } from 'jami-web-common';
+import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { Navigate } from 'react-router-dom';
import { useUrlParams } from '../hooks/useUrlParams';
import { CallRouteParams } from '../router';
import { WithChildren } from '../utils/utils';
-import { useAuthContext } from './AuthProvider';
import { ConversationContext } from './ConversationProvider';
-import { WebRTCContext } from './WebRTCProvider';
+import { WebRtcContext } from './WebRtcProvider';
import { WebSocketContext } from './WebSocketProvider';
export type CallRole = 'caller' | 'receiver';
@@ -77,10 +76,9 @@
const {
queryParams: { role: callRole },
} = useUrlParams<CallRouteParams>();
- const { account } = useAuthContext();
const webSocket = useContext(WebSocketContext);
- const { webRTCConnection, remoteStreams, sendWebRTCOffer, isConnected } = useContext(WebRTCContext);
- const { conversation } = useContext(ConversationContext);
+ const { webRtcConnection, remoteStreams, sendWebRtcOffer, isConnected } = useContext(WebRtcContext);
+ const { conversationId, conversation } = useContext(ConversationContext);
const [mediaDevices, setMediaDevices] = useState<Record<MediaDeviceKind, MediaDeviceInfo[]>>(
defaultCallContext.mediaDevices
@@ -135,12 +133,12 @@
}, [setLocalStream]);
useEffect(() => {
- if (localStream && webRTCConnection) {
+ if (localStream && webRtcConnection) {
for (const track of localStream.getTracks()) {
- webRTCConnection.addTrack(track, localStream);
+ webRtcConnection.addTrack(track, localStream);
}
}
- }, [localStream, webRTCConnection]);
+ }, [localStream, webRtcConnection]);
const setAudioStatus = useCallback(
(isOn: boolean) => {
@@ -173,22 +171,22 @@
);
useEffect(() => {
- if (!webSocket || !webRTCConnection) {
+ if (!webSocket || !webRtcConnection) {
return;
}
if (callRole === 'caller' && callStatus === CallStatus.Ringing) {
- const callAcceptListener = (data: AccountTextMessage<undefined>) => {
- console.info('Received event on CallAccept', data);
+ const callAcceptListener = (_data: CallAction) => {
+ console.info('Received event on CallAccept');
setCallStatus(CallStatus.Connecting);
- webRTCConnection
+ webRtcConnection
.createOffer({
offerToReceiveAudio: true,
offerToReceiveVideo: true,
})
- .then((offerSDP) => {
- sendWebRTCOffer(offerSDP);
+ .then((sdp) => {
+ sendWebRtcOffer(sdp);
});
};
@@ -198,7 +196,7 @@
webSocket.unbind(WebSocketMessageType.CallAccept, callAcceptListener);
};
}
- }, [callRole, webSocket, webRTCConnection, sendWebRTCOffer, callStatus]);
+ }, [callRole, webSocket, webRtcConnection, sendWebRtcOffer, callStatus]);
useEffect(() => {
if (callStatus === CallStatus.Connecting && isConnected) {
@@ -212,16 +210,15 @@
throw new Error('Could not accept call');
}
- const callAccept = {
- from: account.getId(),
- to: contactUri,
- message: undefined,
+ const callAccept: CallAction = {
+ contactId: contactUri,
+ conversationId,
};
console.info('Sending CallAccept', callAccept);
webSocket.send(WebSocketMessageType.CallAccept, callAccept);
setCallStatus(CallStatus.Connecting);
- }, [webSocket, account, contactUri]);
+ }, [webSocket, contactUri, conversationId]);
if (!callRole) {
console.error('Call role not defined. Redirecting...');
diff --git a/client/src/contexts/ConversationProvider.tsx b/client/src/contexts/ConversationProvider.tsx
index 89fb450..a282664 100644
--- a/client/src/contexts/ConversationProvider.tsx
+++ b/client/src/contexts/ConversationProvider.tsx
@@ -15,7 +15,7 @@
* License along with this program. If not, see
* <https://www.gnu.org/licenses/>.
*/
-import { Conversation, WebSocketMessageType } from 'jami-web-common';
+import { CallAction, Conversation, ConversationView, WebSocketMessageType } from 'jami-web-common';
import { createContext, useCallback, useContext, useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
@@ -40,7 +40,7 @@
const {
urlParams: { conversationId },
} = useUrlParams<ConversationRouteParams>();
- const { account, accountId } = useAuthContext();
+ const { accountId } = useAuthContext();
const webSocket = useContext(WebSocketContext);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
@@ -69,14 +69,12 @@
throw new Error('Could not begin call');
}
- // TODO: Could we move this logic to the server? The client could make a single request with the conversationId, and the server is tasked with sending all the individual requests to the members of the conversation
+ // TODO: Could we move this logic to the server? The client could make a single request with the conversationId,
+ // and the server is tasked with sending all the individual requests to the members of the conversation
for (const member of conversation.getMembers()) {
- const callBegin = {
- from: account.getId(),
- to: member.contact.getUri(),
- message: {
- conversationId,
- },
+ const callBegin: CallAction = {
+ contactId: member.contact.getUri(),
+ conversationId,
};
console.info('Sending CallBegin', callBegin);
@@ -84,13 +82,18 @@
}
navigate(`/conversation/${conversationId}/call?role=caller`);
- }, [conversationId, webSocket, conversation, account, navigate]);
+ }, [conversationId, webSocket, conversation, navigate]);
useEffect(() => {
if (!conversation || !webSocket) {
return;
}
- webSocket.send(WebSocketMessageType.ConversationView, { accountId, conversationId });
+
+ const conversationView: ConversationView = {
+ conversationId,
+ };
+
+ webSocket.send(WebSocketMessageType.ConversationView, conversationView);
}, [accountId, conversation, conversationId, webSocket]);
if (isLoading) {
diff --git a/client/src/contexts/WebRTCProvider.tsx b/client/src/contexts/WebRTCProvider.tsx
deleted file mode 100644
index 04cc61a..0000000
--- a/client/src/contexts/WebRTCProvider.tsx
+++ /dev/null
@@ -1,193 +0,0 @@
-/*
- * Copyright (C) 2022 Savoir-faire Linux Inc.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation; either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public
- * License along with this program. If not, see
- * <https://www.gnu.org/licenses/>.
- */
-
-import { AccountTextMessage, WebRTCIceCandidate, WebRtcSdp, WebSocketMessageType } from 'jami-web-common';
-import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
-
-import { WithChildren } from '../utils/utils';
-import { useAuthContext } from './AuthProvider';
-import { ConversationContext } from './ConversationProvider';
-import { WebSocketContext } from './WebSocketProvider';
-
-interface IWebRTCContext {
- isConnected: boolean;
-
- remoteStreams: readonly MediaStream[] | undefined;
- webRTCConnection: RTCPeerConnection | undefined;
-
- sendWebRTCOffer: (sdp: RTCSessionDescriptionInit) => Promise<void>;
-}
-
-const defaultWebRTCContext: IWebRTCContext = {
- isConnected: false,
- remoteStreams: undefined,
- webRTCConnection: undefined,
- sendWebRTCOffer: async () => {},
-};
-
-export const WebRTCContext = createContext<IWebRTCContext>(defaultWebRTCContext);
-
-export default ({ children }: WithChildren) => {
- const { accountId } = useAuthContext();
- const webSocket = useContext(WebSocketContext);
- const { conversation } = useContext(ConversationContext);
- const [webRTCConnection, setWebRTCConnection] = useState<RTCPeerConnection | undefined>();
- const [remoteStreams, setRemoteStreams] = useState<readonly MediaStream[]>();
- const [isConnected, setIsConnected] = useState(false);
-
- // TODO: This logic will have to change to support multiple people in a call
- const contactUri = useMemo(() => conversation.getFirstMember().contact.getUri(), [conversation]);
-
- useEffect(() => {
- if (!webRTCConnection) {
- // TODO use SFL iceServers
- const iceConfig = { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] };
- setWebRTCConnection(new RTCPeerConnection(iceConfig));
- }
- }, [webRTCConnection]);
-
- const sendWebRTCOffer = useCallback(
- async (sdp: RTCSessionDescriptionInit) => {
- if (!webRTCConnection || !webSocket) {
- throw new Error('Could not send WebRTC offer');
- }
- const webRTCOffer: AccountTextMessage<WebRtcSdp> = {
- from: accountId,
- to: contactUri,
- message: {
- sdp,
- },
- };
-
- console.info('Sending WebRTCOffer', webRTCOffer);
- webSocket.send(WebSocketMessageType.WebRTCOffer, webRTCOffer);
- await webRTCConnection.setLocalDescription(new RTCSessionDescription(sdp));
- },
- [accountId, webRTCConnection, webSocket, contactUri]
- );
-
- const sendWebRTCAnswer = useCallback(
- (sdp: RTCSessionDescriptionInit) => {
- if (!webRTCConnection || !webSocket) {
- throw new Error('Could not send WebRTC answer');
- }
-
- const webRTCAnswer: AccountTextMessage<WebRtcSdp> = {
- from: accountId,
- to: contactUri,
- message: {
- sdp,
- },
- };
-
- console.info('Sending WebRTCAnswer', webRTCAnswer);
- webSocket.send(WebSocketMessageType.WebRTCAnswer, webRTCAnswer);
- },
- [accountId, contactUri, webRTCConnection, webSocket]
- );
-
- useEffect(() => {
- if (!webSocket || !webRTCConnection) {
- return;
- }
-
- const webRTCOfferListener = async (data: AccountTextMessage<WebRtcSdp>) => {
- console.info('Received event on WebRTCOffer', data);
- await webRTCConnection.setRemoteDescription(new RTCSessionDescription(data.message.sdp));
-
- const sdp = await webRTCConnection.createAnswer({
- offerToReceiveAudio: true,
- offerToReceiveVideo: true,
- });
- sendWebRTCAnswer(sdp);
- await webRTCConnection.setLocalDescription(new RTCSessionDescription(sdp));
- setIsConnected(true);
- };
-
- const webRTCAnswerListener = async (data: AccountTextMessage<WebRtcSdp>) => {
- console.info('Received event on WebRTCAnswer', data);
- await webRTCConnection.setRemoteDescription(new RTCSessionDescription(data.message.sdp));
- setIsConnected(true);
- };
-
- const webRTCIceCandidateListener = async (data: AccountTextMessage<WebRTCIceCandidate>) => {
- console.info('Received event on WebRTCIceCandidate', data);
- await webRTCConnection.addIceCandidate(data.message.candidate);
- };
-
- webSocket.bind(WebSocketMessageType.WebRTCOffer, webRTCOfferListener);
- webSocket.bind(WebSocketMessageType.WebRTCAnswer, webRTCAnswerListener);
- webSocket.bind(WebSocketMessageType.IceCandidate, webRTCIceCandidateListener);
-
- return () => {
- webSocket.unbind(WebSocketMessageType.WebRTCOffer, webRTCOfferListener);
- webSocket.unbind(WebSocketMessageType.WebRTCAnswer, webRTCAnswerListener);
- webSocket.unbind(WebSocketMessageType.IceCandidate, webRTCIceCandidateListener);
- };
- }, [webSocket, webRTCConnection, sendWebRTCAnswer]);
-
- useEffect(() => {
- if (!webRTCConnection || !webSocket) {
- return;
- }
-
- const icecandidateEventListener = (event: RTCPeerConnectionIceEvent) => {
- console.info('Received WebRTC event on icecandidate', event);
- if (!contactUri) {
- throw new Error('Could not handle WebRTC event on icecandidate: contactUri is not defined');
- }
- if (event.candidate) {
- const iceCandidateMessageData: AccountTextMessage<WebRTCIceCandidate> = {
- from: accountId,
- to: contactUri,
- message: {
- candidate: event.candidate,
- },
- };
-
- console.info('Sending IceCandidate', iceCandidateMessageData);
- webSocket.send(WebSocketMessageType.IceCandidate, iceCandidateMessageData);
- }
- };
- const trackEventListener = (event: RTCTrackEvent) => {
- console.info('Received WebRTC event on track', event);
- setRemoteStreams(event.streams);
- };
-
- webRTCConnection.addEventListener('icecandidate', icecandidateEventListener);
- webRTCConnection.addEventListener('track', trackEventListener);
-
- return () => {
- webRTCConnection.removeEventListener('icecandidate', icecandidateEventListener);
- webRTCConnection.removeEventListener('track', trackEventListener);
- };
- }, [accountId, webRTCConnection, webSocket, contactUri]);
-
- return (
- <WebRTCContext.Provider
- value={{
- isConnected,
- remoteStreams,
- webRTCConnection,
- sendWebRTCOffer,
- }}
- >
- {children}
- </WebRTCContext.Provider>
- );
-};
diff --git a/client/src/contexts/WebRtcProvider.tsx b/client/src/contexts/WebRtcProvider.tsx
new file mode 100644
index 0000000..3999259
--- /dev/null
+++ b/client/src/contexts/WebRtcProvider.tsx
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2022 Savoir-faire Linux Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program. If not, see
+ * <https://www.gnu.org/licenses/>.
+ */
+
+import { WebRtcIceCandidate, WebRtcSdp, WebSocketMessageType } from 'jami-web-common';
+import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
+
+import { WithChildren } from '../utils/utils';
+import { ConversationContext } from './ConversationProvider';
+import { WebSocketContext } from './WebSocketProvider';
+
+interface IWebRtcContext {
+ isConnected: boolean;
+
+ remoteStreams: readonly MediaStream[] | undefined;
+ webRtcConnection: RTCPeerConnection | undefined;
+
+ sendWebRtcOffer: (sdp: RTCSessionDescriptionInit) => Promise<void>;
+}
+
+const defaultWebRtcContext: IWebRtcContext = {
+ isConnected: false,
+ remoteStreams: undefined,
+ webRtcConnection: undefined,
+ sendWebRtcOffer: async () => {},
+};
+
+export const WebRtcContext = createContext<IWebRtcContext>(defaultWebRtcContext);
+
+export default ({ children }: WithChildren) => {
+ const webSocket = useContext(WebSocketContext);
+ const { conversation } = useContext(ConversationContext);
+ const [webRtcConnection, setWebRtcConnection] = useState<RTCPeerConnection | undefined>();
+ const [remoteStreams, setRemoteStreams] = useState<readonly MediaStream[]>();
+ const [isConnected, setIsConnected] = useState(false);
+
+ // TODO: This logic will have to change to support multiple people in a call
+ const contactUri = useMemo(() => conversation.getFirstMember().contact.getUri(), [conversation]);
+
+ useEffect(() => {
+ if (!webRtcConnection) {
+ // TODO: Use SFL iceServers
+ const iceConfig = { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] };
+ setWebRtcConnection(new RTCPeerConnection(iceConfig));
+ }
+ }, [webRtcConnection]);
+
+ const sendWebRtcOffer = useCallback(
+ async (sdp: RTCSessionDescriptionInit) => {
+ if (!webRtcConnection || !webSocket) {
+ throw new Error('Could not send WebRTC offer');
+ }
+
+ const webRtcOffer: WebRtcSdp = {
+ contactId: contactUri,
+ sdp,
+ };
+
+ console.info('Sending WebRtcOffer', webRtcOffer);
+ webSocket.send(WebSocketMessageType.WebRtcOffer, webRtcOffer);
+ await webRtcConnection.setLocalDescription(new RTCSessionDescription(sdp));
+ },
+ [webRtcConnection, webSocket, contactUri]
+ );
+
+ const sendWebRtcAnswer = useCallback(
+ (sdp: RTCSessionDescriptionInit) => {
+ if (!webRtcConnection || !webSocket) {
+ throw new Error('Could not send WebRTC answer');
+ }
+
+ const webRtcAnswer: WebRtcSdp = {
+ contactId: contactUri,
+ sdp,
+ };
+
+ console.info('Sending WebRtcAnswer', webRtcAnswer);
+ webSocket.send(WebSocketMessageType.WebRtcAnswer, webRtcAnswer);
+ },
+ [contactUri, webRtcConnection, webSocket]
+ );
+
+ useEffect(() => {
+ if (!webSocket || !webRtcConnection) {
+ return;
+ }
+
+ const webRtcOfferListener = async (data: WebRtcSdp) => {
+ console.info('Received event on WebRtcOffer', data);
+ await webRtcConnection.setRemoteDescription(new RTCSessionDescription(data.sdp));
+
+ const sdp = await webRtcConnection.createAnswer({
+ offerToReceiveAudio: true,
+ offerToReceiveVideo: true,
+ });
+ sendWebRtcAnswer(sdp);
+ await webRtcConnection.setLocalDescription(new RTCSessionDescription(sdp));
+ setIsConnected(true);
+ };
+
+ const webRtcAnswerListener = async (data: WebRtcSdp) => {
+ console.info('Received event on WebRtcAnswer', data);
+ await webRtcConnection.setRemoteDescription(new RTCSessionDescription(data.sdp));
+ setIsConnected(true);
+ };
+
+ const webRtcIceCandidateListener = async (data: WebRtcIceCandidate) => {
+ console.info('Received event on WebRtcIceCandidate', data);
+ await webRtcConnection.addIceCandidate(data.candidate);
+ };
+
+ webSocket.bind(WebSocketMessageType.WebRtcOffer, webRtcOfferListener);
+ webSocket.bind(WebSocketMessageType.WebRtcAnswer, webRtcAnswerListener);
+ webSocket.bind(WebSocketMessageType.WebRtcIceCandidate, webRtcIceCandidateListener);
+
+ return () => {
+ webSocket.unbind(WebSocketMessageType.WebRtcOffer, webRtcOfferListener);
+ webSocket.unbind(WebSocketMessageType.WebRtcAnswer, webRtcAnswerListener);
+ webSocket.unbind(WebSocketMessageType.WebRtcIceCandidate, webRtcIceCandidateListener);
+ };
+ }, [webSocket, webRtcConnection, sendWebRtcAnswer]);
+
+ useEffect(() => {
+ if (!webRtcConnection || !webSocket) {
+ return;
+ }
+
+ const iceCandidateEventListener = (event: RTCPeerConnectionIceEvent) => {
+ console.info('Received WebRTC event on icecandidate', event);
+ if (!contactUri) {
+ throw new Error('Could not handle WebRTC event on icecandidate: contactUri is not defined');
+ }
+
+ if (event.candidate) {
+ const webRtcIceCandidate: WebRtcIceCandidate = {
+ contactId: contactUri,
+ candidate: event.candidate,
+ };
+
+ console.info('Sending WebRtcIceCandidate', webRtcIceCandidate);
+ webSocket.send(WebSocketMessageType.WebRtcIceCandidate, webRtcIceCandidate);
+ }
+ };
+
+ const trackEventListener = (event: RTCTrackEvent) => {
+ console.info('Received WebRTC event on track', event);
+ setRemoteStreams(event.streams);
+ };
+
+ webRtcConnection.addEventListener('icecandidate', iceCandidateEventListener);
+ webRtcConnection.addEventListener('track', trackEventListener);
+
+ return () => {
+ webRtcConnection.removeEventListener('icecandidate', iceCandidateEventListener);
+ webRtcConnection.removeEventListener('track', trackEventListener);
+ };
+ }, [webRtcConnection, webSocket, contactUri]);
+
+ return (
+ <WebRtcContext.Provider
+ value={{
+ isConnected,
+ remoteStreams,
+ webRtcConnection,
+ sendWebRtcOffer,
+ }}
+ >
+ {children}
+ </WebRtcContext.Provider>
+ );
+};
diff --git a/client/src/contexts/WebSocketProvider.tsx b/client/src/contexts/WebSocketProvider.tsx
index 917dc22..8c530c6 100644
--- a/client/src/contexts/WebSocketProvider.tsx
+++ b/client/src/contexts/WebSocketProvider.tsx
@@ -15,19 +15,27 @@
* License along with this program. If not, see
* <https://www.gnu.org/licenses/>.
*/
-import {
- buildWebSocketCallbacks,
- WebSocketCallbacks,
- WebSocketMessage,
- WebSocketMessageTable,
- WebSocketMessageType,
-} from 'jami-web-common';
+import { WebSocketMessage, WebSocketMessageTable, WebSocketMessageType } from 'jami-web-common';
import { createContext, useCallback, useEffect, useRef, useState } from 'react';
import { apiUrl } from '../utils/constants';
import { WithChildren } from '../utils/utils';
import { useAuthContext } from './AuthProvider';
+type WebSocketCallback<T extends WebSocketMessageType> = (data: WebSocketMessageTable[T]) => void;
+
+type WebSocketCallbacks = {
+ [key in WebSocketMessageType]: Set<WebSocketCallback<key>>;
+};
+
+const buildWebSocketCallbacks = (): WebSocketCallbacks => {
+ const webSocketCallback = {} as WebSocketCallbacks;
+ for (const messageType of Object.values(WebSocketMessageType)) {
+ webSocketCallback[messageType] = new Set<WebSocketCallback<typeof messageType>>();
+ }
+ return webSocketCallback;
+};
+
type BindFunction = <T extends WebSocketMessageType>(
type: T,
callback: (data: WebSocketMessageTable[T]) => void
diff --git a/client/src/managers/NotificationManager.tsx b/client/src/managers/NotificationManager.tsx
index 29a7f75..4154c33 100644
--- a/client/src/managers/NotificationManager.tsx
+++ b/client/src/managers/NotificationManager.tsx
@@ -15,7 +15,7 @@
* License along with this program. If not, see
* <https://www.gnu.org/licenses/>.
*/
-import { AccountTextMessage, CallBegin, WebSocketMessageType } from 'jami-web-common';
+import { CallAction, WebSocketMessageType } from 'jami-web-common';
import { useContext, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
@@ -36,9 +36,9 @@
return;
}
- const callBeginListener = (data: AccountTextMessage<CallBegin>) => {
+ const callBeginListener = (data: CallAction) => {
console.info('Received event on CallBegin', data);
- navigate(`/conversation/${data.message.conversationId}/call?role=receiver`);
+ navigate(`/conversation/${data.conversationId}/call?role=receiver`);
};
webSocket.bind(WebSocketMessageType.CallBegin, callBeginListener);
diff --git a/client/src/pages/ChatInterface.tsx b/client/src/pages/ChatInterface.tsx
index f226fec..f30c15a 100644
--- a/client/src/pages/ChatInterface.tsx
+++ b/client/src/pages/ChatInterface.tsx
@@ -88,12 +88,13 @@
useEffect(() => {
if (webSocket) {
- const conversationMessageListener = ({ message }: ConversationMessage) => {
+ const conversationMessageListener = (data: ConversationMessage) => {
console.log('newMessage');
- setMessages((messages) => addMessage(messages, message));
+ setMessages((messages) => addMessage(messages, data.message));
};
webSocket.bind(WebSocketMessageType.ConversationMessage, conversationMessageListener);
+
return () => {
webSocket.unbind(WebSocketMessageType.ConversationMessage, conversationMessageListener);
};
diff --git a/client/src/pages/Messenger.tsx b/client/src/pages/Messenger.tsx
index 9b28a28..c2bc1e7 100644
--- a/client/src/pages/Messenger.tsx
+++ b/client/src/pages/Messenger.tsx
@@ -16,7 +16,7 @@
* <https://www.gnu.org/licenses/>.
*/
import { Box, Stack } from '@mui/material';
-import { Contact, Conversation, WebSocketMessageType } from 'jami-web-common';
+import { Contact, Conversation, ConversationMessage, WebSocketMessageType } from 'jami-web-common';
import { useContext, useEffect, useState } from 'react';
import { Outlet } from 'react-router-dom';
@@ -66,9 +66,15 @@
return;
}
- const conversationMessageListener = () => dispatch(setRefreshFromSlice());
+ const conversationMessageListener = (_data: ConversationMessage) => {
+ dispatch(setRefreshFromSlice());
+ };
+
webSocket.bind(WebSocketMessageType.ConversationMessage, conversationMessageListener);
- return () => webSocket.unbind(WebSocketMessageType.ConversationMessage, conversationMessageListener);
+
+ return () => {
+ webSocket.unbind(WebSocketMessageType.ConversationMessage, conversationMessageListener);
+ };
}, [webSocket, dispatch]);
useEffect(() => {
diff --git a/client/src/router.tsx b/client/src/router.tsx
index 4b053aa..8e53188 100644
--- a/client/src/router.tsx
+++ b/client/src/router.tsx
@@ -23,7 +23,7 @@
import AuthProvider from './contexts/AuthProvider';
import CallProvider, { CallRole } from './contexts/CallProvider';
import ConversationProvider from './contexts/ConversationProvider';
-import WebRTCProvider from './contexts/WebRTCProvider';
+import WebRtcProvider from './contexts/WebRtcProvider';
import WebSocketProvider from './contexts/WebSocketProvider';
import { RouteParams } from './hooks/useUrlParams';
import NotificationManager from './managers/NotificationManager';
@@ -77,11 +77,11 @@
<Route
path="call"
element={
- <WebRTCProvider>
+ <WebRtcProvider>
<CallProvider>
<CallInterface />
</CallProvider>
- </WebRTCProvider>
+ </WebRtcProvider>
}
/>
</Route>
diff --git a/common/src/enums/websocket-message-type.ts b/common/src/enums/websocket-message-type.ts
index 1152101..04a1f90 100644
--- a/common/src/enums/websocket-message-type.ts
+++ b/common/src/enums/websocket-message-type.ts
@@ -18,11 +18,11 @@
export enum WebSocketMessageType {
ConversationMessage = 'conversation-message',
ConversationView = 'conversation-view',
- WebRTCOffer = 'webrtc-offer',
- WebRTCAnswer = 'webrtc-answer',
- IceCandidate = 'ice-candidate',
CallBegin = 'call-begin',
CallAccept = 'call-accept',
CallRefuse = 'call-refuse',
CallEnd = 'call-end',
+ WebRtcOffer = 'webrtc-offer',
+ WebRtcAnswer = 'webrtc-answer',
+ WebRtcIceCandidate = 'webrtc-ice-candidate',
}
diff --git a/common/src/index.ts b/common/src/index.ts
index 3fb02d1..9c36a37 100644
--- a/common/src/index.ts
+++ b/common/src/index.ts
@@ -21,8 +21,6 @@
export * from './Conversation.js';
export * from './enums/http-status-code.js';
export * from './enums/websocket-message-type.js';
-export * from './interfaces/account-text-message.js';
export * from './interfaces/websocket-interfaces.js';
export * from './interfaces/websocket-message.js';
-export * from './types/websocket-callbacks.js';
export * from './util.js';
diff --git a/common/src/interfaces/account-text-message.ts b/common/src/interfaces/account-text-message.ts
deleted file mode 100644
index 68eea24..0000000
--- a/common/src/interfaces/account-text-message.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * Copyright (C) 2022 Savoir-faire Linux Inc.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation; either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public
- * License along with this program. If not, see
- * <https://www.gnu.org/licenses/>.
- */
-export interface AccountTextMessage<T> {
- from: string;
- to: string;
- message: T;
-}
diff --git a/common/src/interfaces/websocket-interfaces.ts b/common/src/interfaces/websocket-interfaces.ts
index 2c6a109..c4d6b0b 100644
--- a/common/src/interfaces/websocket-interfaces.ts
+++ b/common/src/interfaces/websocket-interfaces.ts
@@ -17,24 +17,27 @@
*/
import { Message } from '../Conversation.js';
+export interface ContactMessage {
+ contactId: string;
+}
+
export interface ConversationMessage {
conversationId: string;
message: Message;
}
export interface ConversationView {
- accountId: string;
conversationId: string;
}
-export interface WebRtcSdp {
+export interface CallAction extends ContactMessage {
+ conversationId: string;
+}
+
+export interface WebRtcSdp extends ContactMessage {
sdp: RTCSessionDescriptionInit;
}
-export interface WebRTCIceCandidate {
+export interface WebRtcIceCandidate extends ContactMessage {
candidate: RTCIceCandidate;
}
-
-export interface CallBegin {
- conversationId: string;
-}
diff --git a/common/src/interfaces/websocket-message.ts b/common/src/interfaces/websocket-message.ts
index ff1de98..a431fcc 100644
--- a/common/src/interfaces/websocket-message.ts
+++ b/common/src/interfaces/websocket-message.ts
@@ -16,25 +16,24 @@
* <https://www.gnu.org/licenses/>.
*/
import { WebSocketMessageType } from '../enums/websocket-message-type.js';
-import { AccountTextMessage } from './account-text-message.js';
import {
- CallBegin,
+ CallAction,
ConversationMessage,
ConversationView,
- WebRTCIceCandidate,
+ WebRtcIceCandidate,
WebRtcSdp,
} from './websocket-interfaces.js';
export interface WebSocketMessageTable {
[WebSocketMessageType.ConversationMessage]: ConversationMessage;
[WebSocketMessageType.ConversationView]: ConversationView;
- [WebSocketMessageType.WebRTCOffer]: AccountTextMessage<WebRtcSdp>;
- [WebSocketMessageType.WebRTCAnswer]: AccountTextMessage<WebRtcSdp>;
- [WebSocketMessageType.IceCandidate]: AccountTextMessage<WebRTCIceCandidate>;
- [WebSocketMessageType.CallBegin]: AccountTextMessage<CallBegin>;
- [WebSocketMessageType.CallAccept]: AccountTextMessage<undefined>;
- [WebSocketMessageType.CallRefuse]: AccountTextMessage<undefined>;
- [WebSocketMessageType.CallEnd]: AccountTextMessage<undefined>;
+ [WebSocketMessageType.CallBegin]: CallAction;
+ [WebSocketMessageType.CallAccept]: CallAction;
+ [WebSocketMessageType.CallRefuse]: CallAction;
+ [WebSocketMessageType.CallEnd]: CallAction;
+ [WebSocketMessageType.WebRtcOffer]: WebRtcSdp;
+ [WebSocketMessageType.WebRtcAnswer]: WebRtcSdp;
+ [WebSocketMessageType.WebRtcIceCandidate]: WebRtcIceCandidate;
}
export interface WebSocketMessage<T extends WebSocketMessageType> {
diff --git a/common/src/types/websocket-callbacks.ts b/common/src/types/websocket-callbacks.ts
deleted file mode 100644
index 366bfee..0000000
--- a/common/src/types/websocket-callbacks.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright (C) 2022 Savoir-faire Linux Inc.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation; either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public
- * License along with this program. If not, see
- * <https://www.gnu.org/licenses/>.
- */
-
-import { WebSocketMessageType } from '../enums/websocket-message-type.js';
-import { WebSocketMessageTable } from '../interfaces/websocket-message.js';
-
-export type WebSocketCallback<T extends WebSocketMessageType> = (data: WebSocketMessageTable[T]) => void;
-
-export type WebSocketCallbacks = {
- [key in WebSocketMessageType]: Set<WebSocketCallback<key>>;
-};
-
-export const buildWebSocketCallbacks = (): WebSocketCallbacks => {
- const webSocketCallback = {} as WebSocketCallbacks;
- for (const type of Object.values(WebSocketMessageType)) {
- webSocketCallback[type] = new Set<WebSocketCallback<typeof type>>();
- }
-
- return webSocketCallback;
-};
diff --git a/server/src/app.ts b/server/src/app.ts
index 27e6e99..3e27a88 100644
--- a/server/src/app.ts
+++ b/server/src/app.ts
@@ -31,7 +31,7 @@
import { defaultModeratorsRouter } from './routers/default-moderators-router.js';
import { nameserverRouter } from './routers/nameserver-router.js';
import { setupRouter } from './routers/setup-router.js';
-import { bindWebRTCCallbacks } from './websocket/webrtc-handler.js';
+import { bindWebRtcCallbacks } from './websocket/webrtc-handler.js';
@Service()
export class App {
@@ -57,7 +57,7 @@
this.app.use('/ns', nameserverRouter);
// Setup WebSocket callbacks
- bindWebRTCCallbacks();
+ bindWebRtcCallbacks();
// Setup 404 error handling
this.app.use((_req, res) => {
diff --git a/server/src/jamid/jamid.ts b/server/src/jamid/jamid.ts
index e7107b6..30439ef 100644
--- a/server/src/jamid/jamid.ts
+++ b/server/src/jamid/jamid.ts
@@ -19,7 +19,6 @@
import {
AccountDetails,
- AccountTextMessage,
ConversationMessage,
Message,
VolatileDetails,
@@ -227,10 +226,10 @@
return stringVectToArray(this.jamiSwig.getAccountList());
}
- sendAccountTextMessage(from: string, to: string, message: string): void {
+ sendAccountTextMessage(accountId: string, contactId: string, message: string): void {
const messageStringMap: StringMap = new this.jamiSwig.StringMap();
messageStringMap.set('application/json', message);
- this.jamiSwig.sendAccountTextMessage(from, to, messageStringMap);
+ this.jamiSwig.sendAccountTextMessage(accountId, contactId, messageStringMap);
}
// TODO: Add interface for returned type
@@ -379,6 +378,7 @@
`Received VolatileDetailsChanged: {"accountId":"${accountId}",` +
`"details":{"Account.registeredName":"${username}", ...}}`
);
+
if (username) {
// Keep map of usernames to account IDs
this.usernamesToAccountIds.set(username, accountId);
@@ -401,9 +401,10 @@
log.debug(`Received KnownDevicesChanged: {"accountId":"${accountId}", ...}`);
});
- this.events.onIncomingAccountMessage.subscribe((signal) => {
+ this.events.onIncomingAccountMessage.subscribe(<T extends WebSocketMessageType>(signal: IncomingAccountMessage) => {
log.debug('Received IncomingAccountMessage:', JSON.stringify(signal));
- const message: WebSocketMessage<any> = JSON.parse(signal.payload['application/json']);
+
+ const message: WebSocketMessage<T> = JSON.parse(signal.payload['application/json']);
if (message === undefined) {
log.warn('Undefined account message');
@@ -420,13 +421,7 @@
return;
}
- const data: AccountTextMessage<unknown> = {
- from: signal.from,
- to: signal.accountId,
- message: message.data.message,
- };
-
- this.webSocketServer.send(signal.accountId, message.type, data);
+ this.webSocketServer.send(signal.accountId, message.type, message.data);
});
this.events.onAccountMessageStatusChanged.subscribe((signal) => {
diff --git a/server/src/websocket/webrtc-handler.ts b/server/src/websocket/webrtc-handler.ts
index abef6e7..da4a374 100644
--- a/server/src/websocket/webrtc-handler.ts
+++ b/server/src/websocket/webrtc-handler.ts
@@ -15,51 +15,39 @@
* License along with this program. If not, see
* <https://www.gnu.org/licenses/>.
*/
-import { WebSocketMessageTable, WebSocketMessageType } from 'jami-web-common';
+import { ContactMessage, WebSocketMessageType } from 'jami-web-common';
import log from 'loglevel';
import { Container } from 'typedi';
import { Jamid } from '../jamid/jamid.js';
import { WebSocketServer } from './websocket-server.js';
-const jamid = Container.get(Jamid);
-const webSocketServer = Container.get(WebSocketServer);
-
-const webRTCWebSocketMessageTypes = [
- WebSocketMessageType.IceCandidate,
- WebSocketMessageType.WebRTCOffer,
- WebSocketMessageType.WebRTCAnswer,
+const webRtcWebSocketMessageTypes = [
WebSocketMessageType.CallBegin,
WebSocketMessageType.CallAccept,
WebSocketMessageType.CallRefuse,
WebSocketMessageType.CallEnd,
+ WebSocketMessageType.WebRtcOffer,
+ WebSocketMessageType.WebRtcAnswer,
+ WebSocketMessageType.WebRtcIceCandidate,
] as const;
-type WebRTCWebSocketMessageType = typeof webRTCWebSocketMessageTypes[number];
+const jamid = Container.get(Jamid);
+const webSocketServer = Container.get(WebSocketServer);
-function sendWebRTCData<T extends WebRTCWebSocketMessageType>(
- type: WebRTCWebSocketMessageType,
- data: Partial<WebSocketMessageTable[T]>
-) {
- if (data.from === undefined || data.to === undefined) {
- log.warn('Message is not a valid AccountTextMessage (missing from or to fields)');
- return;
- }
- log.info('Handling WebRTC message of type:', type);
- jamid.sendAccountTextMessage(
- data.from,
- data.to,
- JSON.stringify({
- type,
- data,
- })
- );
-}
-
-export function bindWebRTCCallbacks() {
- for (const messageType of webRTCWebSocketMessageTypes) {
- webSocketServer.bind(messageType, (data) => {
- sendWebRTCData(messageType, data);
+export function bindWebRtcCallbacks() {
+ for (const messageType of webRtcWebSocketMessageTypes) {
+ webSocketServer.bind(messageType, (accountId, data) => {
+ sendWebRtcData(messageType, accountId, data);
});
}
}
+
+function sendWebRtcData(type: WebSocketMessageType, accountId: string, data: Partial<ContactMessage>) {
+ if (data.contactId === undefined) {
+ log.warn('Message is not a valid ContactMessage (missing contactId field)');
+ return;
+ }
+
+ jamid.sendAccountTextMessage(accountId, data.contactId, JSON.stringify({ type, data }));
+}
diff --git a/server/src/websocket/websocket-server.ts b/server/src/websocket/websocket-server.ts
index ff28bb9..c1b9904 100644
--- a/server/src/websocket/websocket-server.ts
+++ b/server/src/websocket/websocket-server.ts
@@ -18,14 +18,7 @@
import { IncomingMessage } from 'node:http';
import { Duplex } from 'node:stream';
-import {
- buildWebSocketCallbacks,
- WebSocketCallback,
- WebSocketCallbacks,
- WebSocketMessage,
- WebSocketMessageTable,
- WebSocketMessageType,
-} from 'jami-web-common';
+import { WebSocketMessage, WebSocketMessageTable, WebSocketMessageType } from 'jami-web-common';
import log from 'loglevel';
import { Service } from 'typedi';
import { URL } from 'whatwg-url';
@@ -33,13 +26,24 @@
import { verifyJwt } from '../utils/jwt.js';
+type WebSocketCallback<T extends WebSocketMessageType> = (accountId: string, data: WebSocketMessageTable[T]) => void;
+
+type WebSocketCallbacks = {
+ [key in WebSocketMessageType]: Set<WebSocketCallback<key>>;
+};
+
@Service()
export class WebSocketServer {
private wss = new WebSocket.WebSocketServer({ noServer: true });
private sockets = new Map<string, WebSocket.WebSocket[]>();
- private callbacks: WebSocketCallbacks = buildWebSocketCallbacks();
+ private callbacks: WebSocketCallbacks;
constructor() {
+ this.callbacks = {} as WebSocketCallbacks;
+ for (const messageType of Object.values(WebSocketMessageType)) {
+ this.callbacks[messageType] = new Set<WebSocketCallback<typeof messageType>>();
+ }
+
this.wss.on('connection', (ws: WebSocket.WebSocket, _request: IncomingMessage, accountId: string) => {
log.info('New connection for account', accountId);
const accountSockets = this.sockets.get(accountId);
@@ -63,7 +67,7 @@
const callbacks = this.callbacks[message.type];
for (const callback of callbacks) {
- callback(message.data);
+ callback(accountId, message.data);
}
});