Skip to content

Commit

Permalink
feat: #310 - webrtc call added for audio (#376)
Browse files Browse the repository at this point in the history
* feat: #310 -  webrtc call added for audio

* feat: #310 -e2e encrypted  oneway audio call

* feat: #310 -e2e encrypted  oneway audio call added

* fix: react hook warning

* fix: few code smell
  • Loading branch information
muke1908 committed Sep 19, 2024
1 parent 26a7c18 commit efae545
Show file tree
Hide file tree
Showing 23 changed files with 704 additions and 81 deletions.
43 changes: 43 additions & 0 deletions backend/api/call/session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import express, { Request, Response } from 'express';
import asyncHandler from '../../middleware/asyncHandler';
import { WebrtcSessionResponse } from '../messaging/types';
import channelValid from '../chatLink/utils/validateChannel';
import getClientInstance from '../../socket.io/clients';
import { SOCKET_TOPIC, socketEmit } from '../../socket.io';
const router = express.Router({ mergeParams: true });

const clients = getClientInstance();

router.post(
"/",
asyncHandler(async (req: Request, res: Response): Promise<Response<WebrtcSessionResponse>> => {
const { description, sender, channel } = req.body;

if (!description) {
return res.send(400);
}

const { valid } = await channelValid(channel);

if (!valid) {
return res.sendStatus(404);
}

if (!clients.isSenderInChannel(channel, sender)) {
console.error('Sender is not in channel');
return res.status(401).send({ error: "Permission denied" });
}

const receiver = clients.getReceiverIDBySenderID(sender, channel);
if(!receiver) {
console.error('No receiver is in the channel');
return res.status(406).send({ error: "No user available to accept call" });
}

const receiverSid = clients.getSIDByIDs(receiver, channel).sid;
socketEmit<SOCKET_TOPIC.WEBRTC_SESSION_DESCRIPTION>(SOCKET_TOPIC.WEBRTC_SESSION_DESCRIPTION, receiverSid, description);
return res.send({ status: "ok" });
})
);

export default router;
2 changes: 2 additions & 0 deletions backend/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import express, { Request, Response } from 'express';

import chatLinkController from './chatLink';
import chatController from './messaging';
import sessionController from './call/session';

const router = express.Router({ mergeParams: true });

Expand All @@ -11,5 +12,6 @@ router.get("/", async (req: Request, res: Response) => {

router.use("/chat", chatController);
router.use("/chat-link", chatLinkController);
router.use("/session", sessionController);

export default router;
20 changes: 8 additions & 12 deletions backend/api/messaging/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import express, { Request, Response } from 'express';

import db from '../../db';
import { PUBLIC_KEY_COLLECTION } from '../../db/const';
import uploadImage from '../../external/uploadImage';
import asyncHandler from '../../middleware/asyncHandler';
import { SOCKET_TOPIC, socketEmit } from '../../socket.io';
import getClientInstance from '../../socket.io/clients';
Expand All @@ -27,16 +26,13 @@ router.post(
if (!valid) {
return res.sendStatus(404);
}
const usersInChannel = clients.getClientsByChannel(channel);
const usersInChannelArr = Object.keys(usersInChannel);
const ifSenderIsInChannel = usersInChannelArr.find((u) => u === sender);

if (!ifSenderIsInChannel) {
if (!clients.isSenderInChannel(channel, sender)) {
console.error('Sender is not in channel');
return res.status(401).send({ error: "Permission denied" });
}

const receiver = usersInChannelArr.find((u) => u !== sender);
const receiver = clients.getReceiverIDBySenderID(sender, channel);
if(!receiver) {
console.error('No receiver is in the channel');
return;
Expand All @@ -53,10 +49,9 @@ router.post(
};

if (image) {
const { imageurl } = await uploadImage(image);
dataToPublish.image = imageurl;
return res.status(400).send({ message: "Image not supported" });
}
const receiverSid = usersInChannel[receiver].sid;
const receiverSid = clients.getSIDByIDs(receiver, channel).sid;
socketEmit<SOCKET_TOPIC.CHAT_MESSAGE>(SOCKET_TOPIC.CHAT_MESSAGE, receiverSid, dataToPublish);
return res.send({ message: "message sent", id, timestamp });
})
Expand All @@ -65,14 +60,14 @@ router.post(
router.post(
"/share-public-key",
asyncHandler(async (req: Request, res: Response): Promise<Response<SharePublicKeyResponse>> => {
const { publicKey, sender, channel } = req.body;
const { aesKey, publicKey, sender, channel } = req.body;

const { valid } = await channelValid(channel);
if (!valid) {
return res.sendStatus(404);
}
// TODO: do not store if already exists
await db.insertInDb({ publicKey, user: sender, channel }, PUBLIC_KEY_COLLECTION);
await db.insertInDb({ aesKey, publicKey, user: sender, channel }, PUBLIC_KEY_COLLECTION);
return res.send({ status: "ok" });
})
);
Expand All @@ -90,7 +85,8 @@ router.get(
const receiverID = clients.getReceiverIDBySenderID(userId as string, channel as string);
const data = await db.findOneFromDB<GetPublicKeyResponse>({ channel, user: receiverID }, PUBLIC_KEY_COLLECTION);
return res.send(data || {
public_key: null
publicKey: null,
aesKey: null
});
})
);
Expand Down
1 change: 1 addition & 0 deletions backend/api/messaging/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// router.response
export type MessageResponse = { message: string, id: string, timestamp: number }
export type SharePublicKeyResponse = { status: string }
export type WebrtcSessionResponse = { status: string }
export type GetPublicKeyResponse = { public_key: string }
export type UsersInChannelResponse = { uuid: string }[]

Expand Down
2 changes: 1 addition & 1 deletion backend/db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const findOneFromDB = async<T>(findCondition, collectionName: string): Promise<T
return _findOneFromDB(findCondition, collectionName);
}

return db.collection(collectionName).findOne(findCondition) as T;
return db.collection(collectionName).findOne(findCondition, { sort: { _id: -1 } }) as T;
}

const updateOneFromDb = async<T>(condition, data, collectionName: string): Promise<T> => {
Expand Down
6 changes: 6 additions & 0 deletions backend/socket.io/clients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ class Clients implements ClientRecordInterface{
delete this.clientRecord[channelID][userID];
}

isSenderInChannel(channel: string, sender: string): boolean {
const usersInChannel = this.getClientsByChannel(channel);
const usersInChannelArr = Object.keys(usersInChannel);
return !!usersInChannelArr.find((u) => u === sender);
}

}

const clientInstance = new Clients();
Expand Down
1 change: 1 addition & 0 deletions backend/socket.io/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export enum SOCKET_TOPIC {
DELIVERED = 'delivered',
ON_ALICE_DISCONNECTED = 'on-alice-disconnect',
MESSAGE = 'message',
WEBRTC_SESSION_DESCRIPTION = 'webrtc-session-description'
}

type emitDataTypes = {
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/Button/Style.module.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.button {
padding: 10px 20px;
padding: 6px 20px;
display: inline-block;
border-radius: 3px;
border-color: transparent;
Expand Down
20 changes: 5 additions & 15 deletions client/src/components/DeleteChatLink/index.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,15 @@
import React, { useContext } from "react";
import { ThemeContext } from "../../ThemeContext";
import styles from "./Style.module.css";
import React from "react";
import Button from "../Button";

const DeleteChatLink = ({ handleDeleteLink }: any) => {
const [darkMode] = useContext(ThemeContext);

const deleteHandler = async () => {
if (window.confirm("Delete chat link forever?")) await handleDeleteLink();
};

return (
<div>
<div
className={`${styles.deleteButton} ${!darkMode && styles.lightModeDelete}`}
role="button"
onClick={deleteHandler}
>
Delete
</div>
<Button onClick={deleteHandler} label = "Delete" type="secondary"/>
</div>
);
};
)
}

export default DeleteChatLink;
109 changes: 86 additions & 23 deletions client/src/components/Messaging/UserStatusInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,56 @@
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import styles from "./styles/UserStatusInfo.module.css";
import ThemeToggle from "../ThemeToggle/index";
import imageRetryIcon from "./assets/image-retry.png";
import DeleteChatLink from "../DeleteChatLink";
import Button from "../Button";
import { IChatE2EE } from "@chat-e2ee/service";

export const UserStatusInfo = ({
online,
getSetUsers,
channelID,
handleDeleteLink
handleDeleteLink,
chate2ee
}: {
online: any;
getSetUsers: any;
channelID: any;
handleDeleteLink: any;
chate2ee: IChatE2EE
}) => {
const [ call, setCall ] = useState(null);
const [loading, setLoading] = useState(false);
const [ callState, setCallState ] = useState(undefined);

useEffect(() => {
chate2ee.onCallAdded((call) => {
setCall(call);
});

chate2ee.onCallRemoved(() => {
setCall(null);
});

chate2ee.onPCStateChanged((state) => {
setCallState(state);
});
}, [chate2ee]);

const makeCall = async () => {
if(call) {
console.error('call is already active');
return;
}

const newCall = await chate2ee.startCall();
setCall(newCall);
}

const stopCall = async() => {
chate2ee.endCall();
setCall(null);
}

const fetchKeyAgain = async () => {
if (loading) return;
Expand All @@ -26,26 +61,54 @@ export const UserStatusInfo = ({
};

return (
<div className={styles.userInfo}>
{online ? (
<span className={styles.userInfoOnline}>
Alice {"<"}Online{">"}
</span>
) : (
<div className={styles.userOnlineWaiting}>
Waiting for Alice to join...
<img
className={
loading ? `${styles.retryImageIcon} ${styles.loading}` : `${styles.retryImageIcon}`
}
src={imageRetryIcon}
onClick={fetchKeyAgain}
alt="retry-icon"
/>
</div>
)}
<DeleteChatLink handleDeleteLink={handleDeleteLink} />
<ThemeToggle />
</div>
<>
{ call && (<CallStatus state={callState}/>) }
<div className={styles.userInfo}>
{online ? (
<span className={styles.userInfoOnline}>
{"<"}Online{">"}
</span>
) : (
<div className={styles.userOnlineWaiting}>
Waiting for Alice to join...
<img
className={
loading ? `${styles.retryImageIcon} ${styles.loading}` : `${styles.retryImageIcon}`
}
src={imageRetryIcon}
onClick={fetchKeyAgain}
alt="retry-icon"
/>
</div>
)}
{
online && <CallButton makeCall={makeCall} stopCall={stopCall} call={call}/>
}
<DeleteChatLink handleDeleteLink={handleDeleteLink} />
<ThemeToggle />
</div>
</>
);
};


const CallStatus = ({state}: {state:any}) => {
return(
<div className={styles.callStatusBar}>Call Status: {state}</div>
)
}

const CallButton = ({ makeCall, stopCall, call }: { makeCall: any, stopCall: any, call: any }) => {
const callButtonHandler = () => {
if(call) {
stopCall();
}else {
makeCall();
}
}
return (
<div>
<Button onClick={callButtonHandler} label = { call ? 'Stop' : 'Call' } type="primary"/>
</div>
)
}
17 changes: 14 additions & 3 deletions client/src/components/Messaging/styles/UserStatusInfo.module.css
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
.callStatusBar {
position: fixed;
background: #4caf50;
width: 100%;
left: 0;
padding: 2px;
font-size: 12px;
color: white;
bottom: 0;
}
.userInfo {
padding: 15px 0px 15px 0px;
font-weight: 400;
display: grid;
grid-template-columns: 2fr auto auto;
display: flex;
flex-direction: row;
grid-column-gap: 15px;
align-content: center;
align-items: center;
}
.userInfoOnline {
color: #4caf50;
flex: 1;
}

@media screen and (max-width: 640px) {
Expand All @@ -29,6 +39,7 @@
.userOnlineWaiting {
display: flex;
align-items: center;
flex: 1;
}

.retryImageIcon {
Expand Down
2 changes: 1 addition & 1 deletion client/src/pages/messaging/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ setConfig({
}

const chate2ee = createChatInstance();

type messageObj = {
body?: string;
image?: string;
Expand Down Expand Up @@ -266,6 +265,7 @@ const Chat = () => {
getSetUsers={getSetUsers}
channelID={channelID}
handleDeleteLink={handleDeleteLink}
chate2ee={chate2ee}
/>

<div className={styles.messageContainer}>
Expand Down
Loading

0 comments on commit efae545

Please sign in to comment.