From a9322e34547574dbcf67cbf4433a984b8b2d6a68 Mon Sep 17 00:00:00 2001 From: chatenium Date: Wed, 8 Apr 2026 08:46:16 +0200 Subject: [PATCH] Implemented BroadcastChannelService and FileTransferService --- src/domain/broadcastChannelService.schema.ts | 28 ++++ src/domain/fileTransferService.schema.ts | 95 +++++++++++ src/mocks/handlers/brcChan.http.ts | 11 ++ src/mocks/index.ts | 4 +- src/services/broadcastChannelService.test.ts | 11 ++ src/services/broadcastChannelService.ts | 93 +++++++++++ src/services/fileTransferService.ts | 156 +++++++++++++++++++ tests/broadcastChannelService.test.ts | 29 ++++ tests/networkService.test.ts | 2 +- tests/pictureService.test.ts | 2 +- 10 files changed, 428 insertions(+), 3 deletions(-) create mode 100644 src/domain/broadcastChannelService.schema.ts create mode 100644 src/domain/fileTransferService.schema.ts create mode 100644 src/mocks/handlers/brcChan.http.ts create mode 100644 src/services/broadcastChannelService.test.ts create mode 100644 src/services/broadcastChannelService.ts create mode 100644 src/services/fileTransferService.ts create mode 100644 tests/broadcastChannelService.test.ts diff --git a/src/domain/broadcastChannelService.schema.ts b/src/domain/broadcastChannelService.schema.ts new file mode 100644 index 0000000..5bd2693 --- /dev/null +++ b/src/domain/broadcastChannelService.schema.ts @@ -0,0 +1,28 @@ +export interface CreateServerReq { + type: "rtmp", + channelId: string + categoryId: string + networkId: string +} + +export interface GetRTMPDataReq { + channelId: string + networkId: string + categoryId: string +} + +export interface JoinWebsocketRoomReq { + userid: string + connId: string + channelId: string + networkId: string + categoryId: string +} + +export interface StreamRegistry { + streamKey: string + status: "idling" | "broadcasting" | "broadcasting_starting" + type: "rtmp" + streamURL: string + channelId: string +} \ No newline at end of file diff --git a/src/domain/fileTransferService.schema.ts b/src/domain/fileTransferService.schema.ts new file mode 100644 index 0000000..ad2148b --- /dev/null +++ b/src/domain/fileTransferService.schema.ts @@ -0,0 +1,95 @@ +export interface StartNewFileTransferReq { + userid: string + targetUserId: string + metadata: TransferableFileMetadata[] +} + +export interface AcceptFileTransferReq { + userid: string + senderId: string + transferId: string +} + +export interface DeclineFileTransferReq { + userid: string + senderId: string + transferId: string +} + +export interface FileTransferSendOfferRTCReq { + userid: string + peerId: string + transferId: string + offer: string +} + +export interface FileTransferSendAnswerRTCReq { + userid: string + peerId: string + transferId: string + answer: string +} + +export interface FileTransferSendICERTCReq { + userid: string + peerId: string + transferId: string + candidate: string +} + +// Response schemas +export interface StartNewFileTransferResp { + transferId: string +} + +// WebSocket payloads +export interface WSNewFileTransferPayload { + from: string + transferId: string + metadata: TransferableFileMetadata[] +} + +export interface WSFileTransferAcceptedPayload { + transferId: string + rtcConfig: RTCConfiguration +} + +export interface WSFileTransferDeclinedPayload { + transferId: string +} + +export interface WSFileTransferRTCOfferPayload { + transferId: string + offer: string +} + +export interface WSFileTransferRTCAnswerPayload { + transferId: string + answer: string +} + +export interface WSFileTransferRTCIcePayload { + transferId: string + candidate: string +} + +// DataChannel payloads +export interface DCStartNewFilePayload { + fileId: string + fileIndex: number + fileName: string + totalChunks: number +} + +export interface DCTransferFilePayload { + fileId: string + fileIndex: string + chunk: string +} + +// Types +export interface TransferableFileMetadata { + fileId: string + name: string + size: number +} \ No newline at end of file diff --git a/src/mocks/handlers/brcChan.http.ts b/src/mocks/handlers/brcChan.http.ts new file mode 100644 index 0000000..92d5d2d --- /dev/null +++ b/src/mocks/handlers/brcChan.http.ts @@ -0,0 +1,11 @@ +import {http, HttpResponse} from "msw"; +import {GetRTCAccessResp} from "../../domain/callService.schema"; +import {StreamRegistry} from "../../domain/broadcastChannelService.schema"; + +export const brcChanHandlers = [ + http.get('*/network/channel/rtmpData', () => { + return HttpResponse.json({ + status: "broadcasting_starting" + }) + }), +] \ No newline at end of file diff --git a/src/mocks/index.ts b/src/mocks/index.ts index 3734b4c..3cf79df 100644 --- a/src/mocks/index.ts +++ b/src/mocks/index.ts @@ -6,6 +6,7 @@ import {fileUploadHandlers} from "./handlers/fUpl.http"; import {chatHandlers} from "./handlers/chat.http"; import {dmHandlers} from "./handlers/dm.http"; import {userHandler} from "./handlers/user.http"; +import {brcChanHandlers} from "./handlers/brcChan.http"; export const allHandlers = [ ...authHandlers, @@ -15,5 +16,6 @@ export const allHandlers = [ ...fileUploadHandlers, ...chatHandlers, ...dmHandlers, - ...userHandler + ...userHandler, + ...brcChanHandlers ] \ No newline at end of file diff --git a/src/services/broadcastChannelService.test.ts b/src/services/broadcastChannelService.test.ts new file mode 100644 index 0000000..5bea8b1 --- /dev/null +++ b/src/services/broadcastChannelService.test.ts @@ -0,0 +1,11 @@ +import {describe, expect, it} from "vitest"; +import {BroadcastChannelService} from "./broadcastChannelService"; + +describe("BroadcastChannelService", () => { + const service = new BroadcastChannelService("", "", "", "", "", () => {}) + + it("should get stream registry data", async () => { + const data = await service.getData() + expect(data.status).toBe("broadcasting_starting") + }) +}) \ No newline at end of file diff --git a/src/services/broadcastChannelService.ts b/src/services/broadcastChannelService.ts new file mode 100644 index 0000000..ddc04e4 --- /dev/null +++ b/src/services/broadcastChannelService.ts @@ -0,0 +1,93 @@ +import {AxiosInstance, isAxiosError} from "axios"; +import {getClient} from "../core/http"; +import {MessageListener} from "../domain/websocket.schema"; +import {WebSocketHandler} from "../core/webSocketHandler"; +import {AcceptInviteReq} from "../domain/networkService.schema"; +import {GenericErrorBody} from "../domain/http.schema"; +import { + CreateServerReq, + GetRTMPDataReq, + JoinWebsocketRoomReq, + StreamRegistry +} from "../domain/broadcastChannelService.schema"; + +export class BroadcastChannelService { + userid: string + channelId: string + networkId: string + categoryId: string + client: AxiosInstance; + + constructor(token: string, userid: string, networkId: string, categoryId: string, channelId: string, wsMessageListener: MessageListener) { + this.userid = userid; + this.channelId = channelId; + this.categoryId = categoryId; + this.networkId = networkId; + this.client = getClient(false).create({ + headers: { + "Authorization": token, + "X-WS-ID": WebSocketHandler.getInstance().connId + } + }) + WebSocketHandler.getInstance().registerService({ + identifier: channelId, + onNewConnId: this.onNewConnId, + onNewMessage: wsMessageListener, + }) + } + + private onNewConnId(newConnId: string) { + console.log("NetworkService: New connection id") + this.client.defaults.headers["X-WS-ID"] = newConnId; + } + + async getData(): Promise { + try { + const resp = await this.client.get(`network/channel/rtmpData?userid=${this.userid}&channelId=${this.channelId}&networkId=${this.networkId}&categoryId=${this.categoryId}`); + return resp.data + } catch (e) { + console.log(e) + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + async createServer(): Promise { + try { + const resp = await this.client.post("network/channel/createServer", { + userid: this.userid, + channelId: this.channelId, + networkId: this.networkId, + type: "rtmp", + categoryId: this.categoryId, + }); + return resp.data + } catch (e) { + console.log(e) + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + async joinWebSocketRoom(): Promise { + try { + await this.client.post("v2/network/channel/joinWebSocketRoom", { + userid: this.userid, + channelId: this.channelId, + networkId: this.networkId, + connId: WebSocketHandler.getInstance().connId, + categoryId: this.categoryId, + }); + return + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } +} \ No newline at end of file diff --git a/src/services/fileTransferService.ts b/src/services/fileTransferService.ts new file mode 100644 index 0000000..9950605 --- /dev/null +++ b/src/services/fileTransferService.ts @@ -0,0 +1,156 @@ +import {DatabaseAPI} from "../storage/database"; +import {AxiosInstance, isAxiosError} from "axios"; +import {MessageListener} from "../domain/websocket.schema"; +import {getClient} from "../core/http"; +import {WebSocketHandler} from "../core/webSocketHandler"; +import {CreateNetworkReq, Network} from "../domain/networkService.schema"; +import {GenericErrorBody} from "../domain/http.schema"; +import { + AcceptFileTransferReq, DeclineFileTransferReq, FileTransferSendAnswerRTCReq, + FileTransferSendICERTCReq, FileTransferSendOfferRTCReq, + StartNewFileTransferReq, + StartNewFileTransferResp, + TransferableFileMetadata +} from "../domain/fileTransferService.schema"; + +export class FileTransferService { + userid: string; + peerId: string; + transferId: string = ""; + client: AxiosInstance + + constructor(userid: string, token: string, peerId: string) { + this.userid = userid; + this.peerId = peerId; + this.client = getClient(false).create({ + headers: { + "Authorization": token, + } + }) + } + + /** + * Starts a new file transfer with the specified user. The new transferId will be saved automatically and not required to be provided later. + * @param transferableFiles + */ + async startNew(transferableFiles: TransferableFileMetadata[]): Promise { + try { + const resp = await this.client.post("v2/chat/dm/startNewFileTransfer", { + userid: this.userid, + targetUserId: this.peerId, + metadata: transferableFiles + }); + this.transferId = resp.data.transferId + return resp.data.transferId + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * Accepts the file transfer with the specified transferId. The transferId will be saved automatically and not required to be provided later. + * @param transferId + */ + async accept(transferId: string): Promise { + try { + const resp = await this.client.post("v2/chat/dm/acceptFileTransfer", { + userid: this.userid, + senderId: this.peerId, + transferId: transferId + }); + this.transferId = transferId + return resp.data + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * Declines the file transfer with the specified transferId. + * @param transferId + */ + async decline(transferId: string): Promise { + try { + await this.client.post("v2/chat/dm/declineFileTransfer", { + userid: this.userid, + senderId: this.peerId, + transferId: transferId + }); + return + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * Forwards your RTC offer to the specified user. + * @param offer + */ + async sendRtcOffer(offer: string): Promise { + try { + await this.client.post("v2/chat/dm/sendRtcOfferFileTransfer", { + userid: this.userid, + peerId: this.peerId, + transferId: this.transferId, + offer: offer, + }); + return + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * Forwards your RTC answer to the specified user. + * @param answer + */ + async sendRtcAnswer(answer: string): Promise { + try { + await this.client.post("v2/chat/dm/sendRtcAnswerFileTransfer", { + userid: this.userid, + peerId: this.peerId, + transferId: this.transferId, + answer: answer, + }); + return + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * Forwards your RTC ICE candidate to the specified user. + * @param candidate + */ + async sendRtcICE(candidate: string): Promise { + try { + await this.client.post("v2/chat/dm/sendRtcICEFileTransfer", { + userid: this.userid, + peerId: this.peerId, + transferId: this.transferId, + candidate: candidate, + }); + return + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } +} \ No newline at end of file diff --git a/tests/broadcastChannelService.test.ts b/tests/broadcastChannelService.test.ts new file mode 100644 index 0000000..3a9e43a --- /dev/null +++ b/tests/broadcastChannelService.test.ts @@ -0,0 +1,29 @@ +import {describe, expect, it} from "vitest"; +import {BroadcastChannelService} from "../src/services/broadcastChannelService"; + +const BRC_CHAN_SERVICE_TESTING_NETWORK_ID = "000000000000000000000000" +const BRC_CHAN_SERVICE_TESTING_USER_ID = "000000000000000000000000" +const BRC_CHAN_SERVICE_TESTING_CHANNEL_ID = "333333333333333333333333" +const BRC_CHAN_SERVICE_TESTING_TOKEN = "testingToken" +const BRC_CHAN_SERVICE_TESTING_CATEGORY_ID = "111111111111111111111111" + +describe("BroadcastChannelService Integration Testing", () => { + const service = new BroadcastChannelService( + BRC_CHAN_SERVICE_TESTING_TOKEN, + BRC_CHAN_SERVICE_TESTING_USER_ID, + BRC_CHAN_SERVICE_TESTING_NETWORK_ID, + BRC_CHAN_SERVICE_TESTING_CATEGORY_ID, + BRC_CHAN_SERVICE_TESTING_CHANNEL_ID, + (action, data) => {} + ) + + it('should create a new server and fetch it', async () => { + await service.createServer() + const registry = await service.getData() + expect(registry.status).toBe("idling") + }); + + it('should join ws room', async () => { + await service.joinWebSocketRoom() + }); +}) \ No newline at end of file diff --git a/tests/networkService.test.ts b/tests/networkService.test.ts index 503311d..26e29bf 100644 --- a/tests/networkService.test.ts +++ b/tests/networkService.test.ts @@ -74,7 +74,7 @@ describe("NetworkService Integration Testing", () => { }); it('should create rank', async () => { - const rankName = faker.internet.displayName() + const rankName = faker.internet.displayName().substring(0, 10) const rank = await service.createRank(rankName, {r: 0, g: 0, b: 0}, null) expect(rank.name).toBe(rankName) }); diff --git a/tests/pictureService.test.ts b/tests/pictureService.test.ts index 0739712..f844d68 100644 --- a/tests/pictureService.test.ts +++ b/tests/pictureService.test.ts @@ -24,7 +24,7 @@ describe("PictureService Integration Test", () => { }); it('should create an album', async () => { - const albumName = faker.internet.displayName() + const albumName = faker.internet.displayName().substring(0, 10) await service.createAlbum(albumName) const uploads = await service.get() expect(uploads.pictures[1].name).toBe(albumName)