2 Commits

Author SHA1 Message Date
f787ec56a8 Merge remote-tracking branch 'origin/main'
All checks were successful
Setup testing environment and test the code / build (push) Successful in 1m17s
2026-04-06 08:00:21 +02:00
91bb2a5d9c Implemented DMService 2026-04-06 08:00:16 +02:00
7 changed files with 493 additions and 1 deletions

View File

@@ -14,4 +14,14 @@ export interface PublicUserData {
displayName: string
username: string
userid: string
}
export interface Attachment {
fileId: string
fileName: string
format: string
type: string
path: string
height: number
width: number
}

View File

@@ -0,0 +1,112 @@
import {Attachment, TimeStamp} from "./common.schema";
export interface GetMessageReq {
from: number
chatid: string
}
export interface GetMessagePosReq {
messageId: string
chatid: string
}
export interface GetPinnedMessagesReq {
chatid: string
}
export interface EditMessageReq {
message: string
messageId: string
chatid: string
userid: string
}
export interface FinishMessageReq {
uploadId: string | null
message: string
replyTo: string
replyToMessage: string
chatid: string
userid: string
}
export interface ReadMessagesReq {
chatid: string
userid: string
}
export interface PinMessageReq {
chatid: string
messageId: string
userid: string
message: string
}
export interface UnpinMessageReq {
chatid: string,
messageId: string,
userid: string
}
export interface DeleteMessagesReq {
messageIds: string[]
chatid: string
userid: string
}
export interface JoinWsRoomReq {
connId: string
chatid: string
userid: string
}
// Response schemas
export interface GetMessagePosResp {
messagePos: number
}
// Types
export interface Message {
msgid: string
author: string
message: string
sent_at: TimeStamp
isEdited: boolean
chatid: string
files: Attachment[]
seen: boolean
replyTo: string
replyToId: string
forwardedFrom: string
forwardedFromName: string
}
export interface PinnedMessage {
message: string
messageId: string
}
// WebSocket payloads
export interface WSMessageDeletedPayload {
messageId: string
}
export interface WSMessageEditedPayload {
messageId: string
message: string
}
export interface WSMessagePinnedPayload {
chatid: string
messageId: string
message: string
}
export interface WSMessagesReadPayload {
userid: string
}
export interface WSMessageUnpinnedPayload {
chatid: string
messageId: string
}

View File

@@ -0,0 +1,31 @@
import {http, HttpResponse} from "msw";
import {Chat} from "../../domain/chatService.schema";
import {FinishMessageReq, GetMessagePosResp, Message, PinnedMessage} from "../../domain/dmService.schema";
import {CreateNetworkReq, Network} from "../../domain/networkService.schema";
export const dmHandlers = [
http.get('*/chat/dm/messages', () => {
return HttpResponse.json(<Message[]>[{
message: "This is a message",
}])
}),
http.get('*/chat/dm/getMessagePosition', () => {
return HttpResponse.json(<GetMessagePosResp>{
messagePos: 5000
})
}),
http.get('*/chat/dm/pinnedMessages', () => {
return HttpResponse.json(<PinnedMessage[]>[{
message: "This is a pinned message",
}])
}),
http.post('*/chat/dm/finishMessage', async ({request}) => {
const body = await request.json() as FinishMessageReq
return HttpResponse.json(<Message>{
message: body.message,
})
}),
]

View File

@@ -4,6 +4,7 @@ import {pictureHandlers} from "./handlers/picture.http";
import {callHandlers} from "./handlers/call.http";
import {fileUploadHandlers} from "./handlers/fUpl.http";
import {chatHandlers} from "./handlers/chat.http";
import {dmHandlers} from "./handlers/dm.http";
export const allHandlers = [
...authHandlers,
@@ -11,5 +12,6 @@ export const allHandlers = [
...pictureHandlers,
...callHandlers,
...fileUploadHandlers,
...chatHandlers
...chatHandlers,
...dmHandlers
]

View File

@@ -0,0 +1,29 @@
import {describe, expect, it} from "vitest";
import {DMService} from "./dmService";
import {DatabaseMock} from "../mocks/storage/database";
import {faker} from "@faker-js/faker/locale/en";
describe("DmService", () => {
const service = new DMService("", "", "", new DatabaseMock(), () => {})
it("should get messages", async () => {
const messages = await service.get()
expect(messages[0].message).toBe("This is a message")
})
it('should get message position', async () => {
const pos = await service.getMessagePos("")
expect(pos).toBe(5000)
});
it('should get pinned messages', async () => {
const pinnedMessages = await service.getPinnedMessages()
expect(pinnedMessages[0].message).toBe("This is a pinned message")
});
it('should send a new message', async () => {
const message = faker.internet.displayName()
const newMessage = await service.sendMessage(message)
expect(newMessage.message).toBe(message)
});
})

247
src/services/dmService.ts Normal file
View File

@@ -0,0 +1,247 @@
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 {
DeleteMessagesReq,
EditMessageReq,
FinishMessageReq,
GetMessagePosResp, JoinWsRoomReq,
Message, PinMessageReq,
PinnedMessage, ReadMessagesReq, UnpinMessageReq
} from "../domain/dmService.schema";
import {NetworkInvite} from "../domain/networkService.schema";
import {GenericErrorBody} from "../domain/http.schema";
import {FileData} from "../domain/fileUploadService.schema";
import {FileUploadService} from "./fileUploadService";
export class DMService {
userid: string;
chatid: string;
token: string;
database: DatabaseAPI;
client: AxiosInstance
constructor(userid: string, token: string, chatid: string, database: DatabaseAPI, wsMessageListener: MessageListener) {
this.userid = userid;
this.chatid = chatid;
this.database = database;
this.token = token;
this.client = getClient(false).create({
headers: {
"Authorization": token,
"X-WS-ID": WebSocketHandler.getInstance().connId
}
})
WebSocketHandler.getInstance().registerService({
identifier: chatid,
onNewConnId: this.onNewConnId,
onNewMessage: wsMessageListener,
})
}
private onNewConnId(newConnId: string) {
console.log("NetworkService: New connection id")
this.client.defaults.headers["X-WS-ID"] = newConnId;
}
/**
* Fetches all messages in the chat
* @param from
*/
async get(from: number = 0): Promise<Message[]> {
try {
const resp = await this.client.get<Message[]>(`chat/dm/messages?chatid=${this.chatid}&userid=${this.userid}&from=${from}`);
return resp.data
} catch (e) {
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
}
throw new Error("Unexpected error")
}
}
/**
* Fetches the position of the specified message which can be used in get() to fetch an old message with it's surrounding messages
* @param messageId
*/
async getMessagePos(messageId: string): Promise<number> {
try {
const resp = await this.client.get<GetMessagePosResp>(`chat/dm/getMessagePosition?chatid=${this.chatid}&userid=${this.userid}&messageId=${messageId}`);
return resp.data.messagePos
} catch (e) {
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
}
throw new Error("Unexpected error")
}
}
/**
* Gets all messages pinned in the chat
*/
async getPinnedMessages(): Promise<PinnedMessage[]> {
try {
const resp = await this.client.get<PinnedMessage[]>(`chat/dm/pinnedMessages?chatid=${this.chatid}&userid=${this.userid}`);
return resp.data
} catch (e) {
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
}
throw new Error("Unexpected error")
}
}
/**
* Edits the specified message
* @param messageId
* @param newMessage
*/
async editMessage(messageId: string, newMessage: string): Promise<void> {
try {
const resp = await this.client.patch("chat/dm/editMessage", <EditMessageReq>{
messageId: messageId,
chatid: this.chatid,
userid: this.userid,
message: newMessage
});
return resp.data
} catch (e) {
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
}
throw new Error("Unexpected error")
}
}
/**
* Sends a new message to the chat
* @param message
* @param replyTo
* @param replyToMessage
* @param attachments
*/
async sendMessage(message: string, replyTo: string | null = null, replyToMessage: string | null = null, attachments: FileData[] | null = null): Promise<Message> {
let uploadId = ""
if (attachments) {
const uploader = new FileUploadService(this.token)
uploadId = await uploader.uploadFiles(this.chatid, this.userid, attachments)
}
try {
const resp = await this.client.post<Message>("chat/dm/finishMessage", <FinishMessageReq>{
message: message,
chatid: this.chatid,
replyTo: replyTo,
replyToMessage: replyToMessage,
userid: this.userid,
uploadId: uploadId,
});
return resp.data
} catch (e) {
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
}
throw new Error("Unexpected error")
}
}
/**
* Reads all messages sent to you in the chat
*/
async readMessages(): Promise<void> {
try {
await this.client.patch("chat/dm/readMessages", <ReadMessagesReq>{
chatid: this.chatid,
userid: this.userid,
});
return
} catch (e) {
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
}
throw new Error("Unexpected error")
}
}
/**
* Pins the specified message
* @param messageId
* @param message
*/
async pinMessage(messageId: string, message: string): Promise<void> {
try {
const resp = await this.client.patch("chat/dm/pinMessage", <PinMessageReq>{
messageId: messageId,
chatid: this.chatid,
userid: this.userid,
message: message
});
return
} catch (e) {
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
}
throw new Error("Unexpected error")
}
}
/**
* Unpins the specified message
* @param messageId
*/
async unpinMessage(messageId: string): Promise<void> {
try {
const resp = await this.client.patch("chat/dm/unpinMessage", <UnpinMessageReq>{
messageId: messageId,
chatid: this.chatid,
userid: this.userid,
});
return
} catch (e) {
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
}
throw new Error("Unexpected error")
}
}
/**
* Deletes the message(s)
* @param messageIds
*/
async deleteMessages(messageIds: string[]): Promise<void> {
try {
const resp = await this.client.patch("chat/dm/deleteMessages", <DeleteMessagesReq>{
chatid: this.chatid,
userid: this.userid,
messageIds: messageIds
});
return
} catch (e) {
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
}
throw new Error("Unexpected error")
}
}
/**
* Joins the WebSocket room to start receiving realtime messages
*/
async joinWebSocketRoom(): Promise<void> {
try {
const resp = await this.client.patch("chat/dm/joinWebSocketRoom", <JoinWsRoomReq>{
chatid: this.chatid,
userid: this.userid,
connId: WebSocketHandler.getInstance().connId,
});
return
} catch (e) {
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
}
throw new Error("Unexpected error")
}
}
}

61
tests/dmService.test.ts Normal file
View File

@@ -0,0 +1,61 @@
import {describe, expect, it} from "vitest";
import {DMService} from "../src/services/dmService";
import {ChatService} from "../src/services/chatService";
import {DatabaseMock} from "../src/mocks/storage/database";
import {faker} from "@faker-js/faker/locale/en";
describe("DmService Integration Testing", () => {
const DM_SERVICE_TESTING_CHAT_ID = "000000000000000000000000"
const DM_SERVICE_TESTING_USER_ID = "000000000000000000000000"
const DM_SERVICE_TESTING_MESSAGE_ID = "111111111111111111111111"
const DM_SERVICE_TESTING_TOKEN = "testingToken"
const service = new DMService(
DM_SERVICE_TESTING_USER_ID,
DM_SERVICE_TESTING_TOKEN,
DM_SERVICE_TESTING_CHAT_ID,
new DatabaseMock(),
() => {}
)
it('should get messages', async () => {
const messages = await service.get()
expect(messages[0].message).toBe("This is a message")
expect(messages[0].isEdited).toBeTruthy()
});
it('should get message position', async () => {
const pos = await service.getMessagePos(DM_SERVICE_TESTING_MESSAGE_ID)
expect(pos).toBe(0)
});
it('should edit message', async () => {
const newMessage = faker.lorem.paragraph()
await service.editMessage(DM_SERVICE_TESTING_MESSAGE_ID, newMessage)
const messages = await service.get()
expect(messages[0].message).toBe(newMessage)
});
it('should send a message', async () => {
const message = faker.lorem.paragraph()
const newMessage = await service.sendMessage(message)
expect(newMessage.message).toBe(message)
});
it('should read messages', async () => {
await service.readMessages()
});
it('should pin and unpin messages', async () => {
let pinnedMessages = await service.getPinnedMessages()
expect(pinnedMessages.length).toBe(0)
await service.pinMessage(DM_SERVICE_TESTING_MESSAGE_ID, "message")
pinnedMessages = await service.getPinnedMessages()
expect(pinnedMessages.length).toBe(1)
await service.unpinMessage(DM_SERVICE_TESTING_MESSAGE_ID)
pinnedMessages = await service.getPinnedMessages()
expect(pinnedMessages.length).toBe(0)
});
})