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);
         }
       });