Code quality improvements and cleanup + Implemented ChatService
Some checks failed
Setup testing environment and test the code / build (push) Has been cancelled
Some checks failed
Setup testing environment and test the code / build (push) Has been cancelled
This commit is contained in:
86
src/core/webSocketHandler.ts
Normal file
86
src/core/webSocketHandler.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import {
|
||||
WSListenerPipe,
|
||||
WSConnIdPayload,
|
||||
WSMakeTokenReq,
|
||||
WSMakeTokenResp,
|
||||
WSMessagePayload
|
||||
} from "../domain/websocket.schema";
|
||||
import {getClient} from "./http";
|
||||
import {CreateNetworkReq, Network} from "../domain/networkService.schema";
|
||||
import {isAxiosError} from "axios";
|
||||
import {GenericErrorBody} from "../domain/http.schema";
|
||||
import {environment} from "./environment";
|
||||
|
||||
export class WebSocketHandler {
|
||||
private static instance: WebSocketHandler;
|
||||
private listeners: Set<WSListenerPipe> = new Set();
|
||||
private connection: WebSocket | null = null;
|
||||
private connectionId: string | null = null;
|
||||
|
||||
public static getInstance(): WebSocketHandler {
|
||||
if (!WebSocketHandler.instance) {
|
||||
WebSocketHandler.instance = new WebSocketHandler();
|
||||
}
|
||||
return WebSocketHandler.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects to Chatenium WS. The ConnectionId is applied automatically in every service. The onMessage pipe is accessible in every service where needed
|
||||
* @param userid
|
||||
* @param token Session token
|
||||
*/
|
||||
public async connect(userid: string, token: string): Promise<void> {
|
||||
const client = getClient(false).create({
|
||||
headers: {
|
||||
"Authorization": token
|
||||
}
|
||||
})
|
||||
|
||||
try {
|
||||
const resp = await client.post<WSMakeTokenResp>("v2/ws/makeToken", <WSMakeTokenReq>{
|
||||
userid: userid,
|
||||
});
|
||||
this.connection = new WebSocket(`${environment.get().apiUrl}/v2/ws?userid=${userid}&access_token=${resp.data.token}`)
|
||||
console.log("Connected to websocket successfully")
|
||||
this.startListening()
|
||||
return
|
||||
} catch (e) {
|
||||
if (isAxiosError<GenericErrorBody>(e)) {
|
||||
throw e;
|
||||
}
|
||||
throw new Error("Unexpected error")
|
||||
}
|
||||
}
|
||||
|
||||
public get connId(): string | null {
|
||||
return this.connectionId;
|
||||
}
|
||||
|
||||
private startListening() {
|
||||
if (this.connection) {
|
||||
this.connection.onmessage = (event) => {
|
||||
const payl: WSMessagePayload = JSON.parse(event.data);
|
||||
if (payl.action == "connectionId") {
|
||||
console.log("ConnectionID received")
|
||||
const data: WSConnIdPayload = JSON.parse(payl.data);
|
||||
this.listeners.forEach(listener => {
|
||||
listener.onNewConnId(data.connId)
|
||||
this.connectionId = data.connId;
|
||||
})
|
||||
} else {
|
||||
this.listeners.forEach(listener => {
|
||||
listener.onNewMessage(payl.action, payl.data)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public registerService(service: WSListenerPipe) {
|
||||
this.listeners.add(service);
|
||||
}
|
||||
|
||||
public unregisterService(service: WSListenerPipe) {
|
||||
this.listeners.delete(service);
|
||||
}
|
||||
}
|
||||
43
src/domain/chatService.schema.ts
Normal file
43
src/domain/chatService.schema.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
// Request schemas
|
||||
export interface GetChatsReq {
|
||||
userid: string
|
||||
}
|
||||
|
||||
export interface StartNewReq {
|
||||
userid: string
|
||||
peerUsername: string
|
||||
}
|
||||
|
||||
export interface ToggleChatMuteReq {
|
||||
userid: string
|
||||
chatid: string
|
||||
}
|
||||
|
||||
export interface GetAvailabilityReq {
|
||||
targetId: string
|
||||
connId: string
|
||||
}
|
||||
|
||||
export interface GetAvailabilityResp {
|
||||
available: boolean
|
||||
}
|
||||
|
||||
// Types
|
||||
export interface Chat {
|
||||
userid: string
|
||||
chatid: string
|
||||
username: string
|
||||
displayName: string
|
||||
pfp: string
|
||||
status: 0 | 1
|
||||
type: "outgoing" | "incoming"
|
||||
notifications: number
|
||||
muted: boolean
|
||||
latestMessage: LatestMessage
|
||||
}
|
||||
|
||||
export interface LatestMessage {
|
||||
message: string
|
||||
isAuthor: boolean
|
||||
msgid: string
|
||||
}
|
||||
28
src/domain/websocket.schema.ts
Normal file
28
src/domain/websocket.schema.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
export interface WSListenerPipe {
|
||||
identifier: string; // Any identifier that is unique to the service (chatid, networkId, etc...)
|
||||
onNewConnId: (newConnId: string) => void;
|
||||
onNewMessage: MessageListener;
|
||||
}
|
||||
|
||||
export type MessageListener = (action: string, data: string) => void
|
||||
|
||||
export interface WSMakeTokenReq {
|
||||
userid: string
|
||||
}
|
||||
|
||||
export interface WSMakeTokenResp {
|
||||
token: string
|
||||
}
|
||||
|
||||
export interface WSConnIdPayload {
|
||||
connId: string
|
||||
}
|
||||
|
||||
export interface WSConnIdPayload {
|
||||
connId: string
|
||||
}
|
||||
|
||||
export interface WSMessagePayload {
|
||||
action: string,
|
||||
data: string
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import {beforeAll, describe, expect, it} from "vitest";
|
||||
import {jezus} from "./index";
|
||||
import {KeyringAPI} from "./storage/keyring";
|
||||
import {KeyringMock} from "./mocks/storage/keyring";
|
||||
|
||||
describe("Testing", () => {
|
||||
it("should be defined", () => {
|
||||
let keyringMock = new KeyringMock();
|
||||
jezus(keyringMock)
|
||||
|
||||
expect(keyringMock.get("keyring")).toBe("apad");
|
||||
})
|
||||
})
|
||||
@@ -1,5 +0,0 @@
|
||||
import {KeyringAPI} from "./storage/keyring";
|
||||
|
||||
export function jezus(keyring: KeyringAPI) {
|
||||
keyring.set("keyring", "apad");
|
||||
}
|
||||
23
src/mocks/handlers/chat.http.ts
Normal file
23
src/mocks/handlers/chat.http.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import {http, HttpResponse} from "msw";
|
||||
import {Chat, GetAvailabilityResp, StartNewReq} from "../../domain/chatService.schema";
|
||||
|
||||
export const chatHandlers = [
|
||||
http.get('*/chat/get', () => {
|
||||
return HttpResponse.json(<Chat[]>[{
|
||||
userid: "chatPartnerId"
|
||||
}])
|
||||
}),
|
||||
|
||||
http.get('*/chat/availability', () => {
|
||||
return HttpResponse.json(<GetAvailabilityResp>{
|
||||
available: true
|
||||
})
|
||||
}),
|
||||
|
||||
http.post('*/chat/startNew', async ({request}) => {
|
||||
const body = await request.json() as StartNewReq
|
||||
return HttpResponse.json(<Chat>{
|
||||
username: body.peerUsername
|
||||
})
|
||||
}),
|
||||
]
|
||||
@@ -3,11 +3,13 @@ import {authHandlers} from "./handlers/network.http";
|
||||
import {pictureHandlers} from "./handlers/picture.http";
|
||||
import {callHandlers} from "./handlers/call.http";
|
||||
import {fileUploadHandlers} from "./handlers/fUpl.http";
|
||||
import {chatHandlers} from "./handlers/chat.http";
|
||||
|
||||
export const allHandlers = [
|
||||
...authHandlers,
|
||||
...networkHandlers,
|
||||
...pictureHandlers,
|
||||
...callHandlers,
|
||||
...fileUploadHandlers
|
||||
...fileUploadHandlers,
|
||||
...chatHandlers
|
||||
]
|
||||
24
src/services/chatService.test.ts
Normal file
24
src/services/chatService.test.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import {describe, expect, it} from "vitest";
|
||||
import {ChatService} from "./chatService";
|
||||
import {DatabaseMock} from "../mocks/storage/database";
|
||||
import {faker} from "@faker-js/faker/locale/en";
|
||||
|
||||
describe("ChatService", () => {
|
||||
const service = new ChatService("", "", new DatabaseMock(), (action, data) => {})
|
||||
|
||||
it('should get chats', async () => {
|
||||
const chats = await service.get()
|
||||
expect(chats[0].userid).toBe("chatPartnerId")
|
||||
})
|
||||
|
||||
it('should get availability', async () => {
|
||||
const available = await service.getUserAvailability("")
|
||||
expect(available).toBeTruthy()
|
||||
});
|
||||
|
||||
it('should create a new chat', async () => {
|
||||
const chatPartnerName = faker.internet.displayName()
|
||||
const newChat = await service.startNew(chatPartnerName)
|
||||
expect(newChat.username).toBe(chatPartnerName)
|
||||
});
|
||||
})
|
||||
116
src/services/chatService.ts
Normal file
116
src/services/chatService.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import {DatabaseAPI} from "../storage/database";
|
||||
import {MessageListener} from "../domain/websocket.schema";
|
||||
import {getClient} from "../core/http";
|
||||
import {WebSocketHandler} from "../core/webSocketHandler";
|
||||
import {AxiosInstance, isAxiosError} from "axios";
|
||||
import {NetworkInvite} from "../domain/networkService.schema";
|
||||
import {GenericErrorBody} from "../domain/http.schema";
|
||||
import {
|
||||
Chat,
|
||||
GetAvailabilityReq,
|
||||
GetAvailabilityResp,
|
||||
StartNewReq,
|
||||
ToggleChatMuteReq
|
||||
} from "../domain/chatService.schema";
|
||||
|
||||
/**
|
||||
* ChatService is an exception because it's one instance for all chats because it's unnecessary to create a new instance for each chat
|
||||
*/
|
||||
export class ChatService {
|
||||
userid: string;
|
||||
database: DatabaseAPI;
|
||||
client: AxiosInstance;
|
||||
|
||||
constructor(userid: string, token: string, database: DatabaseAPI, wsMessageListener: MessageListener) {
|
||||
this.userid = userid;
|
||||
this.database = database;
|
||||
this.client = getClient(false).create({
|
||||
headers: {
|
||||
"Authorization": token,
|
||||
}
|
||||
})
|
||||
WebSocketHandler.getInstance().registerService({
|
||||
identifier: userid,
|
||||
onNewConnId: this.onNewConnId,
|
||||
onNewMessage: wsMessageListener,
|
||||
})
|
||||
}
|
||||
|
||||
private onNewConnId(newConnId: string) {
|
||||
console.log("ChatService: New connection id")
|
||||
this.client.defaults.headers["X-WS-ID"] = newConnId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all chat partners
|
||||
*/
|
||||
async get(): Promise<Chat[]> {
|
||||
try {
|
||||
const resp = await this.client.get<Chat[]>(`chat/get?userid=${this.userid}`);
|
||||
return resp.data
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
if (isAxiosError<GenericErrorBody>(e)) {
|
||||
throw e;
|
||||
}
|
||||
throw new Error("Unexpected error")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the availability of the specified user
|
||||
* @param userid
|
||||
*/
|
||||
async getUserAvailability(userid: string): Promise<boolean> {
|
||||
try {
|
||||
const resp = await this.client.get<GetAvailabilityResp>(`chat/availability?target=${this.userid}&connId=${WebSocketHandler.getInstance().connId}`);
|
||||
return resp.data.available
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
if (isAxiosError<GenericErrorBody>(e)) {
|
||||
throw e;
|
||||
}
|
||||
throw new Error("Unexpected error")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* (un)mutes the specified chat
|
||||
* @param chatid
|
||||
*/
|
||||
async toggleMute(chatid: string): Promise<void> {
|
||||
try {
|
||||
const resp = await this.client.post("v2/chat/toggleMute", <ToggleChatMuteReq>{
|
||||
userid: this.userid,
|
||||
chatid: chatid,
|
||||
});
|
||||
return
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
if (isAxiosError<GenericErrorBody>(e)) {
|
||||
throw e;
|
||||
}
|
||||
throw new Error("Unexpected error")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a new chat with the specified user
|
||||
* @param peerUsername
|
||||
*/
|
||||
async startNew(peerUsername: string): Promise<Chat> {
|
||||
try {
|
||||
const resp = await this.client.post("chat/startNew", <StartNewReq>{
|
||||
userid: this.userid,
|
||||
peerUsername: peerUsername,
|
||||
});
|
||||
return resp.data
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
if (isAxiosError<GenericErrorBody>(e)) {
|
||||
throw e;
|
||||
}
|
||||
throw new Error("Unexpected error")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import {FileUploadService} from "./fileUploadService";
|
||||
|
||||
describe("fileUploadService", () => {
|
||||
it('should upload files', async () => {
|
||||
const service = new FileUploadService("", null, null);
|
||||
const service = new FileUploadService("");
|
||||
const uploadId = await service.uploadFiles("", "", [])
|
||||
expect(uploadId).toBe("MockUploadId")
|
||||
});
|
||||
|
||||
@@ -15,13 +15,13 @@ export class FileUploadService {
|
||||
client: AxiosInstance;
|
||||
cdnClient: AxiosInstance;
|
||||
|
||||
constructor(token: string, private httpClientOverwrite: AxiosInstance | null, private cdnHttpClientOverwrite: AxiosInstance | null) {
|
||||
this.client = (httpClientOverwrite ?? getClient(false)).create({
|
||||
constructor(token: string) {
|
||||
this.client = getClient(false).create({
|
||||
headers: {
|
||||
"Authorization": token
|
||||
}
|
||||
})
|
||||
this.cdnClient = (cdnHttpClientOverwrite ?? getClient(true)).create({
|
||||
this.cdnClient = getClient(true).create({
|
||||
headers: {
|
||||
"Authorization": token
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {getClient} from "../core/http";
|
||||
import {environment, SDKConfig} from "../core/environment";
|
||||
|
||||
describe("NetworkService", () => {
|
||||
const service = new NetworkService("", "", "", new DatabaseMock(), getClient(false))
|
||||
const service = new NetworkService("", "", "", new DatabaseMock(), (action, data) => {})
|
||||
|
||||
it('should get invites', async () => {
|
||||
const invites = await service.getInvites();
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {DatabaseAPI} from "../storage/database";
|
||||
import {AuthMethods} from "../domain/authService.schema";
|
||||
import {getClient} from "../core/http";
|
||||
import {AxiosInstance, isAxiosError} from "axios";
|
||||
import {GenericErrorBody} from "../domain/http.schema";
|
||||
@@ -18,7 +17,8 @@ import {
|
||||
UnbanMemberReq, UploadNewPictureReq
|
||||
} from "../domain/networkService.schema";
|
||||
import {PublicUserData, RGB} from "../domain/common.schema";
|
||||
import {http} from "msw";
|
||||
import {WebSocketHandler} from "../core/webSocketHandler";
|
||||
import {MessageListener} from "../domain/websocket.schema";
|
||||
|
||||
export class NetworkService {
|
||||
userid: string;
|
||||
@@ -26,15 +26,26 @@ export class NetworkService {
|
||||
database: DatabaseAPI;
|
||||
client: AxiosInstance
|
||||
|
||||
constructor(userid: string, token: string, networkId: string, database: DatabaseAPI, httpClientOverwrite: AxiosInstance | null) {
|
||||
constructor(userid: string, token: string, networkId: string, database: DatabaseAPI, wsMessageListener: MessageListener) {
|
||||
this.userid = userid;
|
||||
this.networkId = networkId;
|
||||
this.database = database;
|
||||
this.client = (httpClientOverwrite ?? getClient(false)).create({
|
||||
this.client = getClient(false).create({
|
||||
headers: {
|
||||
"Authorization": token
|
||||
"Authorization": token,
|
||||
"X-WS-ID": WebSocketHandler.getInstance().connId
|
||||
}
|
||||
})
|
||||
WebSocketHandler.getInstance().registerService({
|
||||
identifier: networkId,
|
||||
onNewConnId: this.onNewConnId,
|
||||
onNewMessage: wsMessageListener,
|
||||
})
|
||||
}
|
||||
|
||||
private onNewConnId(newConnId: string) {
|
||||
console.log("NetworkService: New connection id")
|
||||
this.client.defaults.headers["X-WS-ID"] = newConnId;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -101,7 +112,7 @@ export class NetworkService {
|
||||
*/
|
||||
async acceptInvite(inviteId: string): Promise<void> {
|
||||
try {
|
||||
const resp = await this.client.post("network/acceptInvite", <AcceptInviteReq>{
|
||||
await this.client.post("network/acceptInvite", <AcceptInviteReq>{
|
||||
userid: this.userid,
|
||||
inviteId: inviteId,
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import {DatabaseMock} from "../mocks/storage/database";
|
||||
import {faker} from "@faker-js/faker/locale/en";
|
||||
|
||||
describe("PictureService", () => {
|
||||
const service = new PictureService("", "", "", new DatabaseMock(), null, null)
|
||||
const service = new PictureService("", "", "", new DatabaseMock())
|
||||
|
||||
it('should get pictures', async () => {
|
||||
const uploads = await service.get()
|
||||
|
||||
@@ -20,16 +20,16 @@ export class PictureService {
|
||||
client: AxiosInstance
|
||||
cdnClient: AxiosInstance
|
||||
|
||||
constructor(token: string, uploaderId: string, userid: string, database: DatabaseAPI, httpClientOverwrite: AxiosInstance | null, cdnClientOverwrite: AxiosInstance | null) {
|
||||
constructor(token: string, uploaderId: string, userid: string, database: DatabaseAPI) {
|
||||
this.userid = userid;
|
||||
this.uploaderId = uploaderId;
|
||||
this.database = database;
|
||||
this.client = (httpClientOverwrite ?? getClient(false)).create({
|
||||
this.client = getClient(false).create({
|
||||
headers: {
|
||||
"Authorization": token
|
||||
}
|
||||
})
|
||||
this.cdnClient = (cdnClientOverwrite ?? getClient(true)).create({
|
||||
this.cdnClient = getClient(true).create({
|
||||
headers: {
|
||||
"Authorization": token
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user