From 0b38b002df026e6738b46b91f1afc73785d96b93 Mon Sep 17 00:00:00 2001 From: chatenium Date: Wed, 8 Apr 2026 13:21:11 +0200 Subject: [PATCH] Implemented SessionManager and TextChannelService + several improvements and fixes --- package.json | 2 +- src/domain/fileUploadService.schema.ts | 5 + src/domain/sessionManager.schema.ts | 19 ++ src/domain/textChannelService.schema.ts | 138 +++++++++ .../{dm.http.test.ts => dmService.test.ts} | 0 src/services/dmService.ts | 7 +- src/services/fileUploadService.ts | 12 +- src/services/sessionManager.ts | 133 +++++++++ src/services/textChannelService.ts | 280 ++++++++++++++++++ src/storage/keyring.ts | 2 +- src/storage/keyvalue.ts | 6 + tests/fileUploadService.test.ts | 7 +- tests/networkService.test.ts | 6 - tests/textChannelService.test.ts | 66 +++++ 14 files changed, 666 insertions(+), 17 deletions(-) create mode 100644 src/domain/sessionManager.schema.ts create mode 100644 src/domain/textChannelService.schema.ts rename src/services/{dm.http.test.ts => dmService.test.ts} (100%) create mode 100644 src/services/sessionManager.ts create mode 100644 src/services/textChannelService.ts create mode 100644 src/storage/keyvalue.ts create mode 100644 tests/textChannelService.test.ts diff --git a/package.json b/package.json index 64e1707..e194bae 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "chatenium-sdk", "version": "1.0.0", - "description": "", + "description": "A library for interacting with the Chatenium API", "main": "dist/index.js", "scripts": { "build": "tsc", diff --git a/src/domain/fileUploadService.schema.ts b/src/domain/fileUploadService.schema.ts index 3907675..4575177 100644 --- a/src/domain/fileUploadService.schema.ts +++ b/src/domain/fileUploadService.schema.ts @@ -33,8 +33,13 @@ export interface FileUploadRegistration { } export interface FileData { + fileId: string name: string extension: string type: string data: File +} + +export interface FileUploadProgressListener { + fileProgressUpdate: (fileId: string, allChunks: number, chunksDone: number) => void } \ No newline at end of file diff --git a/src/domain/sessionManager.schema.ts b/src/domain/sessionManager.schema.ts new file mode 100644 index 0000000..a400630 --- /dev/null +++ b/src/domain/sessionManager.schema.ts @@ -0,0 +1,19 @@ +import {PublicUserData} from "./common.schema"; +import {GIF} from "./userService.schema"; + +export interface Session { + userData: PersonalUserData + token: string +} + +export interface ValidateSessionReq { + token: string +} + +export interface ValidateSessionResp { + validationOk: boolean +} + +export interface UpdateUserDataReq { + userid: string +} \ No newline at end of file diff --git a/src/domain/textChannelService.schema.ts b/src/domain/textChannelService.schema.ts new file mode 100644 index 0000000..7cf88a6 --- /dev/null +++ b/src/domain/textChannelService.schema.ts @@ -0,0 +1,138 @@ +import {Attachment, PublicUserData, TimeStamp} from "./common.schema"; + +export interface GetMessageReq { + from: number + channelId: string + networkId: string + categoryId: string +} + +export interface GetMessagePosReq { + messageId: string + channelId: string + networkId: string + categoryId: string +} + +export interface GetPinnedMessagesReq { + channelId: string + networkId: string + categoryId: string +} + +export interface EditMessageReq { + message: string + messageId: string + channelId: string + networkId: string + categoryId: string + userid: string +} + +export interface FinishMessageReq { + uploadId: string | null + message: string + replyTo: string + replyToMessage: string + channelId: string + networkId: string + categoryId: string + userid: string +} + +export interface ReadMessagesReq { + channelId: string + networkId: string + categoryId: string + userid: string +} + +export interface PinMessageReq { + channelId: string + networkId: string + categoryId: string + messageId: string + userid: string + message: string +} + +export interface UnpinMessageReq { + channelId: string + networkId: string + categoryId: string, + messageId: string, + userid: string +} + +export interface DeleteMessagesReq { + messageIds: string[] + channelId: string + networkId: string + categoryId: string + userid: string +} + +export interface JoinWsRoomReq { + connId: string + channelId: string + networkId: string + categoryId: string + userid: string +} + +// Response schemas +export interface GetMessagePosResp { + messagePos: number +} + +// Types +export interface Message { + msgid: string + author: PublicUserData + message: string + sent_at: TimeStamp + isEdited: boolean + channelId: string + networkId: string + categoryId: 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 { + channelId: string + networkId: string + categoryId: string + messageId: string + message: string +} + +export interface WSMessagesReadPayload { + userid: string +} + +export interface WSMessageUnpinnedPayload { + channelId: string + networkId: string + categoryId: string + messageId: string +} \ No newline at end of file diff --git a/src/services/dm.http.test.ts b/src/services/dmService.test.ts similarity index 100% rename from src/services/dm.http.test.ts rename to src/services/dmService.test.ts diff --git a/src/services/dmService.ts b/src/services/dmService.ts index 2413561..1745602 100644 --- a/src/services/dmService.ts +++ b/src/services/dmService.ts @@ -13,7 +13,7 @@ import { } from "../domain/dmService.schema"; import {NetworkInvite} from "../domain/networkService.schema"; import {GenericErrorBody} from "../domain/http.schema"; -import {FileData} from "../domain/fileUploadService.schema"; +import {FileData, FileUploadProgressListener} from "../domain/fileUploadService.schema"; import {FileUploadService} from "./fileUploadService"; export class DMService { @@ -133,12 +133,13 @@ export class DMService { * @param replyTo * @param replyToMessage * @param attachments + * @param progressListener */ - async sendMessage(message: string, replyTo: string | null = null, replyToMessage: string | null = null, attachments: FileData[] | null = null): Promise { + async sendMessage(message: string, replyTo: string | null = null, replyToMessage: string | null = null, attachments: FileData[] | null = null, progressListener: FileUploadProgressListener | null = null): Promise { let uploadId = "" if (attachments) { const uploader = new FileUploadService(this.token) - uploadId = await uploader.uploadFiles(this.chatid, this.userid, attachments) + uploadId = await uploader.uploadFiles(this.chatid, this.userid, attachments, progressListener!) } try { const resp = await this.client.post("chat/dm/finishMessage", { diff --git a/src/services/fileUploadService.ts b/src/services/fileUploadService.ts index c650512..0f502fd 100644 --- a/src/services/fileUploadService.ts +++ b/src/services/fileUploadService.ts @@ -1,6 +1,6 @@ import { ChunkUploadReq, - FileData, + FileData, FileUploadProgressListener, FileUploadRegistration, FinishUploadReq, RegisterUploadReq, RegisterUploadResp @@ -42,13 +42,14 @@ export class FileUploadService { * @param roomId chatid or channelId * @param userid * @param files + * @param listener */ - async uploadFiles(roomId: string, userid: string, files: FileData[]): Promise { + async uploadFiles(roomId: string, userid: string, files: FileData[], listener: FileUploadProgressListener): Promise { let registrations: FileUploadRegistration[] = []; files.forEach(file => { registrations.push({ - fileId: uuidv4(), + fileId: file.fileId, name: file.name, type: file.type, size: file.data.size, @@ -62,7 +63,7 @@ export class FileUploadService { files: registrations, }); for (let filesUploaded = 0; filesUploaded < files.length; filesUploaded++) { - await this.uploadFile(resp.data.uploadId, roomId, userid, files[filesUploaded], registrations[filesUploaded]) + await this.uploadFile(resp.data.uploadId, roomId, userid, files[filesUploaded], registrations[filesUploaded], listener) } await this.finishUpload(roomId, userid, resp.data.uploadId) return resp.data.uploadId @@ -92,7 +93,7 @@ export class FileUploadService { } } - private async uploadFile(uploadId: string, roomId: string, userid: string, file: FileData, registration: FileUploadRegistration): Promise { + private async uploadFile(uploadId: string, roomId: string, userid: string, file: FileData, registration: FileUploadRegistration, listener: FileUploadProgressListener): Promise { const chunkSize = this.calculateChunkSize(file.data.size); const totalChunks = Math.ceil(file.data.size / chunkSize); @@ -104,6 +105,7 @@ export class FileUploadService { const chunk = new Uint8Array(arrayBuffer.slice(start, end)); const base64 = this.uint8ToBase64(chunk); await this.uploadChunk(uploadId, roomId, userid, registration.fileId, base64); + listener.fileProgressUpdate(file.fileId, totalChunks, i) } } diff --git a/src/services/sessionManager.ts b/src/services/sessionManager.ts new file mode 100644 index 0000000..8b49472 --- /dev/null +++ b/src/services/sessionManager.ts @@ -0,0 +1,133 @@ +import {PublicUserData} from "../domain/common.schema"; +import {DatabaseAPI} from "../storage/database"; +import {KeyringAPI} from "../storage/keyring"; +import {KeyValueAPI} from "../storage/keyvalue"; +import {Session, ValidateSessionReq, ValidateSessionResp} from "../domain/sessionManager.schema"; +import {AxiosInstance} from "axios"; +import {getClient} from "../core/http"; +import {PersonalUserData} from "../domain/userService.schema"; + +export class SessionManager { + client: AxiosInstance; + database: DatabaseAPI; + keyring: KeyringAPI; + KeyValue: KeyValueAPI; + + constructor(database: DatabaseAPI, keyring: KeyringAPI, KeyValue: KeyValueAPI) { + this.database = database; + this.keyring = keyring; + this.KeyValue = KeyValue; + this.client = getClient(false) + } + + /** + * Saves the new session to the database and the keyring + * @param userData + * @param token + */ + addSession(userData: PublicUserData, token: string): void { + this.database.set("sessions", userData.userid, JSON.stringify(userData)) + this.keyring.set(userData.userid, token) + } + + /** + * Loads all saved sessions + */ + loadSessions(): Session[] { + const tokens = this.keyring.getAll() + const sessions: Session[] = [] + tokens.forEach(token => { + const userData = this.database.get("sessions", token.split(".")[0]) + if (userData) { + sessions.push({ + token: token, + userData: JSON.parse(userData) + }) + } + }) + + return sessions + } + + /** + * Gets the preferred user set by the client + */ + getPreferredUser(): string { + return this.KeyValue.get("preferredUser") ?? "" + } + + /** + * Sets a new preferred user + * @param userid + */ + setPreferredUser(userid: string): void { + this.KeyValue.set("preferredUser", userid) + } + + /** + * Loads the preferred session by the client + */ + loadPreferredSession() { + const sessions = this.loadSessions() + let preferredUser = this.getPreferredUser() + if (preferredUser == "") { + preferredUser = sessions[0].userData.userid + this.setPreferredUser(sessions[0].userData.userid) + } + + const preferredSession = sessions.find(s => s.userData.userid == preferredUser) + if (preferredSession) { + return preferredSession + } else { + return sessions[0] + } + } + + /** + * Validates and updates all sessions and returns with a new session list + * @param sessions + */ + updateSessions(sessions: Session[]): Session[] { + sessions.forEach(async session => { + const index = sessions.indexOf(session) + if (!await this.validateSession(session.token)) { + this.database.delete("sessions", session.userData.userid) + this.keyring.delete(session.userData.userid) + sessions.splice(index, 1) + } + + const updatedUserData = await this.updateUserData(session) + this.database.set("sessions", session.userData.userid, updatedUserData) + sessions[index] = updatedUserData + }) + + return sessions + } + + private async validateSession(token: string): Promise { + try { + const resp = await this.client.post("v2/user/validateSession", { + token: token, + }) + return resp.data.validationOk + } catch (e) { + return true + } + } + + private async updateUserData(session: Session): Promise { + const authenticatedClient = this.client.create({ + headers: { + "Authorization": session.token, + } + }) + + try { + const resp = await authenticatedClient.get(`user/byUseridPersonal?userid=${session.userData.userid}`) + session.userData = resp.data + return session + } catch (e) { + throw new Error("Session update error") + } + } +} \ No newline at end of file diff --git a/src/services/textChannelService.ts b/src/services/textChannelService.ts new file mode 100644 index 0000000..5964607 --- /dev/null +++ b/src/services/textChannelService.ts @@ -0,0 +1,280 @@ +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/textChannelService.schema"; +import {NetworkInvite} from "../domain/networkService.schema"; +import {GenericErrorBody} from "../domain/http.schema"; +import {FileData, FileUploadProgressListener} from "../domain/fileUploadService.schema"; +import {FileUploadService} from "./fileUploadService"; + +export class TextChannelServiceService { + userid: string; + networkId: string; + categoryId: string; + channelId: string; + token: string; + database: DatabaseAPI; + client: AxiosInstance + + constructor(userid: string, token: string, networkId: string, categoryId: string, channelId: string, database: DatabaseAPI, wsMessageListener: MessageListener) { + this.userid = userid; + this.networkId = networkId; + this.categoryId = categoryId; + this.channelId = channelId; + this.database = database; + this.token = token; + 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; + } + + /** + * Fetches all messages in the chat + * @param from + */ + async get(from: number = 0): Promise { + try { + const resp = await this.client.get(`network/channel/messages?networkId=${this.networkId}&channelId=${this.channelId}&categoryId=${this.categoryId}&userid=${this.userid}&from=${from}`); + if (from == 0) { + this.database.set("networkmessages", this.channelId, JSON.stringify(resp.data)) + } + console.log(resp.data, "ASD") + return resp.data + } catch (e) { + console.log(e) + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + getQuick(): Message[] { + const messages = this.database.get("networkmessages", this.channelId) + if (messages) { + return JSON.parse(messages) + } else { + throw new Error("No messages in database") + } + } + + /** + * 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 { + try { + const resp = await this.client.get(`network/channel/getMessagePosition?networkId=${this.networkId}&channelId=${this.channelId}&categoryId=${this.categoryId}&userid=${this.userid}&messageId=${messageId}`); + return resp.data.messagePos + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * Gets all messages pinned in the chat + */ + async getPinnedMessages(): Promise { + try { + const resp = await this.client.get(`network/channel/pinnedMessages?networkId=${this.networkId}&channelId=${this.channelId}&categoryId=${this.categoryId}&userid=${this.userid}`); + return resp.data + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * Edits the specified message + * @param messageId + * @param newMessage + */ + async editMessage(messageId: string, newMessage: string): Promise { + try { + const resp = await this.client.patch("network/channel/editMessage", { + messageId: messageId, + networkId: this.networkId, + channelId: this.channelId, + categoryId: this.categoryId, + userid: this.userid, + message: newMessage + }); + return resp.data + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * Sends a new message to the chat + * @param message + * @param replyTo + * @param replyToMessage + * @param attachments + * @param progressListener + */ + async sendMessage(message: string, replyTo: string | null = null, replyToMessage: string | null = null, attachments: FileData[] | null = null, progressListener: FileUploadProgressListener | null = null): Promise { + let uploadId = "" + if (attachments) { + const uploader = new FileUploadService(this.token) + uploadId = await uploader.uploadFiles(this.channelId, this.userid, attachments, progressListener!) + } + try { + const resp = await this.client.post("network/channel/finishMessage", { + message: message, + networkId: this.networkId, + channelId: this.channelId, + categoryId: this.categoryId, + replyTo: replyTo, + replyToMessage: replyToMessage, + userid: this.userid, + uploadId: uploadId, + }); + return resp.data + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * Reads all messages sent to you in the chat + */ + async readMessages(): Promise { + try { + await this.client.patch("network/channel/readMessages", { + networkId: this.networkId, + channelId: this.channelId, + categoryId: this.categoryId, + userid: this.userid, + }); + return + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * Pins the specified message + * @param messageId + * @param message + */ + async pinMessage(messageId: string, message: string): Promise { + try { + const resp = await this.client.patch("network/channel/pinMessage", { + messageId: messageId, + networkId: this.networkId, + channelId: this.channelId, + categoryId: this.categoryId, + userid: this.userid, + message: message + }); + return + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * Unpins the specified message + * @param messageId + */ + async unpinMessage(messageId: string): Promise { + try { + const resp = await this.client.patch("network/channel/unpinMessage", { + messageId: messageId, + networkId: this.networkId, + channelId: this.channelId, + categoryId: this.categoryId, + userid: this.userid, + }); + return + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * Deletes the message(s) + * @param messageIds + */ + async deleteMessages(messageIds: string[]): Promise { + try { + const resp = await this.client.patch("network/channel/deleteMessages", { + networkId: this.networkId, + channelId: this.channelId, + categoryId: this.categoryId, + userid: this.userid, + messageIds: messageIds + }); + return + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } + + /** + * Joins the WebSocket room to start receiving realtime messages + */ + async joinWebSocketRoom(): Promise { + try { + const resp = await this.client.patch("network/channel/joinWebSocketRoom", { + networkId: this.networkId, + channelId: this.channelId, + categoryId: this.categoryId, + userid: this.userid, + connId: WebSocketHandler.getInstance().connId, + }); + return + } catch (e) { + if (isAxiosError(e)) { + throw e; + } + throw new Error("Unexpected error") + } + } +} \ No newline at end of file diff --git a/src/storage/keyring.ts b/src/storage/keyring.ts index 3313c3a..17daad9 100644 --- a/src/storage/keyring.ts +++ b/src/storage/keyring.ts @@ -1,6 +1,6 @@ export interface KeyringAPI { set(key: string, value: any): void; get(key: string): string; + getAll(): string[]; delete(key: string): void; - flush(): void; } \ No newline at end of file diff --git a/src/storage/keyvalue.ts b/src/storage/keyvalue.ts new file mode 100644 index 0000000..b52d77b --- /dev/null +++ b/src/storage/keyvalue.ts @@ -0,0 +1,6 @@ +export interface KeyValueAPI { + set(key: string, value: any): void; + get(key: string): string; + delete(key: string): void; + flush(): void; +} \ No newline at end of file diff --git a/tests/fileUploadService.test.ts b/tests/fileUploadService.test.ts index f37d474..98dac0c 100644 --- a/tests/fileUploadService.test.ts +++ b/tests/fileUploadService.test.ts @@ -4,6 +4,7 @@ import {environment, SDKConfig} from "../src/core/environment"; import {getClient} from "../src/core/http"; import {FileData} from "../src/domain/fileUploadService.schema"; import axios from "axios"; +import {v4 as uuidv4} from 'uuid'; describe("FileUploadService Integration Testing", () => { const FILE_UPL_SERVICE_TESTING_USER_ID = "000000000000000000000000" @@ -21,12 +22,16 @@ describe("FileUploadService Integration Testing", () => { FILE_UPL_SERVICE_TESTING_USER_ID, [ { + fileId: uuidv4(), name: "filename", type: "image", extension: "jpeg", data: new File([response.data], "filename", { type: "image/jpeg" }) } - ] + ], + { + fileProgressUpdate: () => {}, + } ) }); }) \ No newline at end of file diff --git a/tests/networkService.test.ts b/tests/networkService.test.ts index 26e29bf..b9f60ba 100644 --- a/tests/networkService.test.ts +++ b/tests/networkService.test.ts @@ -56,12 +56,6 @@ describe("NetworkService Integration Testing", () => { expect(category.name).toBe(catName) }); - it('should delete category', async () => { - await service.deleteCategory(NETWORK_SERVICE_TESTING_CATEGORY_ID) - const networks = await service.get() - expect(networks[0].categories.length).toBe(0) - }); - it('should move category', async () => { await service.createCategory("Test name", "Test desc") await service.moveCategory(0, 1) diff --git a/tests/textChannelService.test.ts b/tests/textChannelService.test.ts new file mode 100644 index 0000000..70c5a07 --- /dev/null +++ b/tests/textChannelService.test.ts @@ -0,0 +1,66 @@ +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"; +import {TextChannelServiceService} from "../src/services/textChannelService"; + +describe("DmService Integration Testing", () => { + const TXT_CHAN_SERVICE_TESTING_CHANNEL_ID = "222222222222222222222222" + const TXT_CHAN_SERVICE_TESTING_USER_ID = "000000000000000000000000" + const TXT_CHAN_SERVICE_TESTING_MESSAGE_ID = "111111111111111111111111" + const TXT_CHAN_SERVICE_TESTING_TOKEN = "testingToken" + const NETWORK_SERVICE_TESTING_NETWORK_ID = "000000000000000000000000" + const NETWORK_SERVICE_TESTING_CATEGORY_ID = "111111111111111111111111" + + const service = new TextChannelServiceService( + TXT_CHAN_SERVICE_TESTING_USER_ID, + TXT_CHAN_SERVICE_TESTING_TOKEN, + NETWORK_SERVICE_TESTING_NETWORK_ID, + NETWORK_SERVICE_TESTING_CATEGORY_ID, + TXT_CHAN_SERVICE_TESTING_CHANNEL_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(TXT_CHAN_SERVICE_TESTING_MESSAGE_ID) + expect(pos).toBe(0) + }); + + it('should edit message', async () => { + const newMessage = faker.lorem.paragraph() + await service.editMessage(TXT_CHAN_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(TXT_CHAN_SERVICE_TESTING_MESSAGE_ID, "message") + pinnedMessages = await service.getPinnedMessages() + expect(pinnedMessages.length).toBe(1) + + await service.unpinMessage(TXT_CHAN_SERVICE_TESTING_MESSAGE_ID) + pinnedMessages = await service.getPinnedMessages() + expect(pinnedMessages.length).toBe(0) + }); +}) \ No newline at end of file