From 54b215d1b110114c913476b04b7581572a5d9351 Mon Sep 17 00:00:00 2001 From: Arnab Chatterjee <60937304+arn4b@users.noreply.github.com> Date: Thu, 14 Sep 2023 16:58:54 +0530 Subject: [PATCH] feat: add accept request (#707) * feat: add accept request func * refactor: refactor acceptInvite * refactor: comments rft * refactor: rft * fix: stream array logic fix * fix: comments fic * refactor: rm video --- packages/restapi/src/lib/spaceV2/SpaceV2.ts | 19 +- .../restapi/src/lib/spaceV2/acceptInvite.ts | 260 ++++++++++++++++++ .../spaceV2/helpers/sendSpaceNotification.ts | 156 +++++------ .../restapi/src/lib/spaceV2/inviteToJoin.ts | 15 +- 4 files changed, 351 insertions(+), 99 deletions(-) create mode 100644 packages/restapi/src/lib/spaceV2/acceptInvite.ts diff --git a/packages/restapi/src/lib/spaceV2/SpaceV2.ts b/packages/restapi/src/lib/spaceV2/SpaceV2.ts index 6fd41c99c..c892ef33c 100644 --- a/packages/restapi/src/lib/spaceV2/SpaceV2.ts +++ b/packages/restapi/src/lib/spaceV2/SpaceV2.ts @@ -1,6 +1,7 @@ import { produce } from "immer"; import { join } from "./join"; +import { acceptInvite } from "./acceptInvite"; import { ISpaceInviteInputOptions, inviteToJoin } from "./inviteToJoin"; import Constants, { ENV } from "../constants"; @@ -25,14 +26,14 @@ export const initSpaceInfo: SpaceDTO = { scheduleEnd: null, status: null, inviteeDetails: {} - }; +}; export const initSpaceV2Data: SpaceV2Data = { spaceInfo: initSpaceInfo, meta: { initiator: { - address: '', - signal: null, + address: '', + signal: null, }, }, local: { @@ -78,7 +79,7 @@ export class SpaceV2 { protected pgpPrivateKey: string; protected env: ENV; - private peerConnections: Map = new Map(); + private peerConnections: Map = new Map(); protected data!: SpaceV2Data; @@ -93,7 +94,7 @@ export class SpaceV2 { env = Constants.ENV.PROD, setSpaceV2Data, // to update the 'spaceData' state maintained by the developer } = options || {}; - + this.signer = signer; this.chainId = chainId; this.pgpPrivateKey = pgpPrivateKey; @@ -118,10 +119,10 @@ export class SpaceV2 { draft.local.address = pCAIP10ToWallet(address); }); }); - + // init the state maintained by the developer setSpaceV2Data(() => initSpaceV2Data); - + // init the spaceSpecificData class variable this.data = initSpaceV2Data; } @@ -150,7 +151,7 @@ export class SpaceV2 { } // Set a connected peer's peer connection by their ID - setPeerConnection(peerId: string, peerConnection: RTCPeerConnection) { + setPeerConnection(peerId: string, peerConnection: RTCPeerConnection | undefined) { this.peerConnections.set(peerId, peerConnection); } @@ -185,4 +186,6 @@ export class SpaceV2 { } public join = join; + + public acceptInvite = acceptInvite; } diff --git a/packages/restapi/src/lib/spaceV2/acceptInvite.ts b/packages/restapi/src/lib/spaceV2/acceptInvite.ts new file mode 100644 index 000000000..2b95eac81 --- /dev/null +++ b/packages/restapi/src/lib/spaceV2/acceptInvite.ts @@ -0,0 +1,260 @@ +/** + * @file acceptInvite + * This file defines functions related to accepting invites + * and managing connections within the SpaceV2 class. + * It includes the `acceptInvite` function, + * which handles incoming invitations and manages peer connections, + * as well as related utility functions. + */ + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +import * as Peer from 'simple-peer'; +import { produce } from 'immer'; + +import { initSpaceV2Data, type SpaceV2 } from './SpaceV2'; +import sendSpaceNotification from './helpers/sendSpaceNotification'; + +import { SPACE_ACCEPT_REQUEST_TYPE, SPACE_DISCONNECT_TYPE } from '../payloads/constants'; +import { VideoCallStatus } from '../types'; + +// imports from Video +import getIncomingIndexFromAddress from '../video/helpers/getIncomingIndexFromAddress'; +import isJSON from '../video/helpers/isJSON'; +import { endStream } from '../video/helpers/mediaToggle'; +import { getIceServerConfig } from '../video/helpers/getIceServerConfig'; + +export interface IAcceptInvite { + signalData: any; + senderAddress: string; + recipientAddress: string; + spaceId: string; + onReceiveMessage?: (message: string) => void; + retry?: boolean; + details?: { + type: SPACE_ACCEPT_REQUEST_TYPE; + data: Record; + }; +} + +export async function acceptInvite( + this: SpaceV2, + options: IAcceptInvite +) { + const { + signalData, + senderAddress, + recipientAddress, + spaceId, + onReceiveMessage = (message: string) => { + console.log('received a meesage', message); + }, + retry = false, + details, + } = options || {}; + + try { + console.log('ACCEPT INVITE', options); + + // if getPeerConnection is not null -> acceptRequest/request was called before + if (this.getPeerConnection(recipientAddress)) { + // to prevent connection error we stop the exec of acceptRequest + return Promise.resolve(); + } + + // fetching the iceServers config + const iceServerConfig = await getIceServerConfig(this.env); + + let peerConnection = new Peer({ + initiator: true, + trickle: false, + stream: this.data.local.stream, + config: { + iceServers: iceServerConfig, + }, + }); + + this.setPeerConnection(recipientAddress, peerConnection); + + peerConnection.on('error', (err: any) => { + if (this.data.incomingPeerStreams[0].retryCount >= 5) { + console.log('Max retries exceeded, please try again.'); + this.disconnect({ peerAddress: recipientAddress }); + } + + // retrying in case of connection error + sendSpaceNotification( + { + signer: this.signer, + chainId: this.chainId, + pgpPrivateKey: this.pgpPrivateKey, + }, + { + senderAddress, + recipientAddress, + status: VideoCallStatus.RETRY_INITIALIZED, + spaceId: spaceId, + signalData: null, + env: this.env, + } + ); + }) + + peerConnection.signal(signalData); + + peerConnection.on('signal', (data: any) => { + this.setSpaceV2Data((oldData) => { + return produce(oldData, (draft) => { + draft.meta.initiator.signal = data; + }); + }); + + sendSpaceNotification( + { + signer: this.signer, + chainId: this.chainId, + pgpPrivateKey: this.pgpPrivateKey, + }, + { + senderAddress, + recipientAddress, + status: retry + ? VideoCallStatus.RETRY_RECEIVED + : VideoCallStatus.INITIALIZED, + spaceId, + signalData: data, + env: this.env, + callDetails: details, + } + ); + }); + + peerConnection.on('connect', () => { + peerConnection.send( + JSON.stringify({ + type: 'isAudioOn', + value: this.data.local.audio, + }) + ); + + // set videoCallInfo state with status connected for the receiver's end + this.setSpaceV2Data((oldData) => { + return produce(oldData, (draft) => { + const pendingIndex = getIncomingIndexFromAddress( + oldData.pendingPeerStreams, + recipientAddress + ); + draft.pendingPeerStreams[pendingIndex].status = VideoCallStatus.CONNECTED; + }); + }); + }); + + peerConnection.on('data', (data: any) => { + if (isJSON(data)) { + const parsedData = JSON.parse(data); + + if (parsedData.type === 'isAudioOn') { + console.log('IS AUDIO ON', parsedData.value) + + this.setSpaceV2Data((oldData) => { + return produce(oldData, (draft) => { + let arrayToUpdate = null; + + // Check if the peer is in pendingPeerStreams + const indexInPending = draft.pendingPeerStreams.findIndex(peer => peer.address === recipientAddress); + if (indexInPending !== -1) { + arrayToUpdate = draft.pendingPeerStreams; + } + + // Check if the peer is in incomingPeerStreams + const indexInIncoming = draft.incomingPeerStreams.findIndex(peer => peer.address === recipientAddress); + if (indexInIncoming !== -1) { + arrayToUpdate = draft.incomingPeerStreams; + } + + // If the peer is found in either array, update the property + if (arrayToUpdate) { + arrayToUpdate[indexInIncoming !== -1 ? indexInIncoming : indexInPending].audio = parsedData.value; + } + }); + }); + } + + if (parsedData.type === 'endCall') { + console.log('END CALL'); + + if ( + parsedData?.details?.type === SPACE_DISCONNECT_TYPE.LEAVE + ) { + // destroy connection to only the current peer + peerConnection?.destroy(); + peerConnection = null; + this.setSpaceV2Data((oldData) => { + return produce(oldData, (draft) => { + const incomingIndex = getIncomingIndexFromAddress( + oldData.incomingPeerStreams, + recipientAddress + ); + draft.incomingPeerStreams.splice(incomingIndex, 1); + }); + }); + } + if ( + parsedData?.details?.type === SPACE_DISCONNECT_TYPE.STOP + ) { + // destroy connection to all the peers + for (const connectedAddress in this.getConnectedPeerIds()) { + // TODO: refactor + // this.getPeerConnection(connectedAddress)?.destroy(); + this.setPeerConnection(connectedAddress, undefined); + } + } + + if ( + parsedData?.details?.type === SPACE_DISCONNECT_TYPE.STOP + ) { + // destroy the local stream + if (this.data.local.stream) { + endStream(this.data.local.stream); + } + + // reset the state + this.setSpaceV2Data(() => initSpaceV2Data); + } + } + } else { + onReceiveMessage(data); + } + }); + + peerConnection.on( + 'stream', + (currentStream: MediaStream) => { + console.log('received incoming stream', currentStream); + const pendingStreamIndex = getIncomingIndexFromAddress( + this.data.pendingPeerStreams, + recipientAddress + ); + + // Here, we can handle if we want to merge stream or anything + // this.onReceiveStream( + // currentStream, + // recipientAddress, + // this.data.pendingPeerStreams[pendingIndex].audio + // ); + + // remove stream from pendingPeerStreams and add it to incomingPeerStreams + this.setSpaceV2Data((oldData) => { + return produce(oldData, (draft) => { + const peerStream = draft.pendingPeerStreams[pendingStreamIndex]; + peerStream.stream = currentStream; + draft.incomingPeerStreams.push(peerStream); + draft.pendingPeerStreams.splice(pendingStreamIndex, 1); + }); + }); + } + ); + } catch (error) { + console.log('error in acceptInvite', error); + } +} diff --git a/packages/restapi/src/lib/spaceV2/helpers/sendSpaceNotification.ts b/packages/restapi/src/lib/spaceV2/helpers/sendSpaceNotification.ts index daea8ca5e..751b6b216 100644 --- a/packages/restapi/src/lib/spaceV2/helpers/sendSpaceNotification.ts +++ b/packages/restapi/src/lib/spaceV2/helpers/sendSpaceNotification.ts @@ -2,106 +2,106 @@ import Constants, { ENV } from '../../constants'; import { getCAIPWithChainId } from '../../helpers'; import { sendNotification } from '../../payloads'; import { - NOTIFICATION_TYPE, - SPACE_ACCEPT_REQUEST_TYPE, - SPACE_DISCONNECT_TYPE, - SPACE_REQUEST_TYPE, - VIDEO_CALL_TYPE, + NOTIFICATION_TYPE, + SPACE_ACCEPT_REQUEST_TYPE, + SPACE_DISCONNECT_TYPE, + SPACE_REQUEST_TYPE, + VIDEO_CALL_TYPE, } from '../../payloads/constants'; import { SignerType, VideoCallStatus } from '../../types'; interface CallDetailsType { - type: SPACE_REQUEST_TYPE | SPACE_ACCEPT_REQUEST_TYPE | SPACE_DISCONNECT_TYPE; - data: Record; + type: SPACE_REQUEST_TYPE | SPACE_ACCEPT_REQUEST_TYPE | SPACE_DISCONNECT_TYPE; + data: Record; }; interface SpaceInfoType { - recipientAddress: string; - senderAddress: string; - spaceId: string; - signalData: any; - status: VideoCallStatus; - env?: ENV; - callDetails?: CallDetailsType; + recipientAddress: string; + senderAddress: string; + spaceId: string; + signalData: any; + status: VideoCallStatus; + env?: ENV; + callDetails?: CallDetailsType; } interface UserInfoType { - signer: SignerType; - chainId: number; - pgpPrivateKey: string; + signer: SignerType; + chainId: number; + pgpPrivateKey: string; } export interface SpaceDataType { - recipientAddress: string; - senderAddress: string; - spaceId: string; - signalData?: any; - status: VideoCallStatus; - callDetails?: CallDetailsType; + recipientAddress: string; + senderAddress: string; + spaceId: string; + signalData?: any; + status: VideoCallStatus; + callDetails?: CallDetailsType; } const sendSpaceNotification = async ( - { signer, chainId, pgpPrivateKey }: UserInfoType, - { - recipientAddress, - senderAddress, - spaceId, - status, - signalData = null, - env = Constants.ENV.PROD, - callDetails - }: SpaceInfoType + { signer, chainId, pgpPrivateKey }: UserInfoType, + { + recipientAddress, + senderAddress, + spaceId, + status, + signalData = null, + env = Constants.ENV.PROD, + callDetails + }: SpaceInfoType ) => { - try { - const spaceData: SpaceDataType = { - recipientAddress, - senderAddress, - spaceId, - signalData, - status, - callDetails - }; + try { + const spaceData: SpaceDataType = { + recipientAddress, + senderAddress, + spaceId, + signalData, + status, + callDetails + }; - console.log('sendSpacelNotification', 'spaceData', spaceData); + console.log('sendSpacelNotification', 'spaceData', spaceData); - const senderAddressInCaip = getCAIPWithChainId(senderAddress, chainId); - const recipientAddressInCaip = getCAIPWithChainId( - recipientAddress, - chainId - ); + const senderAddressInCaip = getCAIPWithChainId(senderAddress, chainId); + const recipientAddressInCaip = getCAIPWithChainId( + recipientAddress, + chainId + ); - const notificationText = `Space connection from ${senderAddress}`; + const notificationText = `Space connection from ${senderAddress}`; - const notificationType = NOTIFICATION_TYPE.TARGETTED; + const notificationType = NOTIFICATION_TYPE.TARGETTED; - await sendNotification({ - senderType: 1, // for chat notification - signer, - pgpPrivateKey, - chatId: spaceId, - type: notificationType, - identityType: 2, - notification: { - title: notificationText, - body: notificationText, - }, - payload: { - title: 'SpaceConnection', - body: 'SpaceConnection', - cta: '', - img: '', - additionalMeta: { - type: `${VIDEO_CALL_TYPE.PUSH_SPACE}+1`, - data: JSON.stringify(spaceData), - }, - }, - recipients: recipientAddressInCaip, - channel: senderAddressInCaip, - env, - }); - } catch (err) { - console.log('Error occured while sending notification for spaces connection', err); - } + await sendNotification({ + senderType: 1, // for chat notification + signer, + pgpPrivateKey, + chatId: spaceId, + type: notificationType, + identityType: 2, + notification: { + title: notificationText, + body: notificationText, + }, + payload: { + title: 'SpaceConnection', + body: 'SpaceConnection', + cta: '', + img: '', + additionalMeta: { + type: `${VIDEO_CALL_TYPE.PUSH_SPACE}+1`, + data: JSON.stringify(spaceData), + }, + }, + recipients: recipientAddressInCaip, + channel: senderAddressInCaip, + env, + }); + } catch (err) { + console.log('Error occured while sending notification for spaces connection', err); + } }; export default sendSpaceNotification; diff --git a/packages/restapi/src/lib/spaceV2/inviteToJoin.ts b/packages/restapi/src/lib/spaceV2/inviteToJoin.ts index b187759c9..0f02a44b1 100644 --- a/packages/restapi/src/lib/spaceV2/inviteToJoin.ts +++ b/packages/restapi/src/lib/spaceV2/inviteToJoin.ts @@ -128,12 +128,6 @@ export async function inviteToJoin( peerConnection.send( `initial message from ${senderAddress}` ); - peerConnection.send( - JSON.stringify({ - type: 'isVideoOn', - value: this.data.local.video, - }) - ); peerConnection.send( JSON.stringify({ type: 'isAudioOn', @@ -146,7 +140,7 @@ export async function inviteToJoin( if (isJSON(data)) { const parsedData = JSON.parse(data); - if (parsedData.type === 'isVideoOn' || parsedData.type === 'isAudioOn') { + if (parsedData.type === 'isAudioOn') { console.log(`IS ${parsedData.type.toUpperCase()}`, parsedData.value); this.setSpaceV2Data((oldData) => { @@ -167,12 +161,7 @@ export async function inviteToJoin( // If the peer is found in either array, update the property if (arrayToUpdate) { - if (parsedData.type === 'isVideoOn') { - arrayToUpdate[indexInIncoming !== -1 ? indexInIncoming : indexInPending].video = parsedData.value; - } - if (parsedData.type === 'isAudioOn') { - arrayToUpdate[indexInIncoming !== -1 ? indexInIncoming : indexInPending].audio = parsedData.value; - } + arrayToUpdate[indexInIncoming !== -1 ? indexInIncoming : indexInPending].audio = parsedData.value; } }); });