16 Commits

Author SHA1 Message Date
54d466fb27 Quick-fix
All checks were successful
Publish to NPM / build-and-publish (release) Successful in 34s
Setup testing environment and test the code / build (push) Successful in 1m30s
2026-04-08 16:37:32 +02:00
07d30f7e83 Quick-fix
All checks were successful
Setup testing environment and test the code / build (push) Successful in 1m28s
2026-04-08 14:22:58 +02:00
cd806b7d17 Update package.json
Some checks failed
Setup testing environment and test the code / build (push) Has been cancelled
2026-04-08 13:59:00 +02:00
7daf542961 Update .gitea/workflows/publish.yml
Some checks failed
Setup testing environment and test the code / build (push) Has been cancelled
2026-04-08 13:55:29 +02:00
f911224847 Update .gitea/workflows/publish.yml
Some checks failed
Setup testing environment and test the code / build (push) Has been cancelled
2026-04-08 13:53:45 +02:00
c8d2de9f9d Merge remote-tracking branch 'origin/main'
Some checks failed
Setup testing environment and test the code / build (push) Has been cancelled
2026-04-08 13:51:47 +02:00
f8de78f3ab Type fixes 2026-04-08 13:51:43 +02:00
e07114b1c0 Update .gitea/workflows/publish.yml
Some checks failed
Setup testing environment and test the code / build (push) Has been cancelled
2026-04-08 13:50:17 +02:00
e8c6b9c920 Update .gitea/workflows/publish.yml
Some checks failed
Setup testing environment and test the code / build (push) Has been cancelled
2026-04-08 13:47:51 +02:00
0a97a2cc48 Add .gitea/workflows/publish
Some checks failed
Setup testing environment and test the code / build (push) Has been cancelled
2026-04-08 13:47:38 +02:00
0b38b002df Implemented SessionManager and TextChannelService + several improvements and fixes
All checks were successful
Setup testing environment and test the code / build (push) Successful in 1m13s
2026-04-08 13:21:11 +02:00
a9322e3454 Implemented BroadcastChannelService and FileTransferService
All checks were successful
Setup testing environment and test the code / build (push) Successful in 1m17s
2026-04-08 08:46:16 +02:00
e6798b4be8 Mock database hotfix
All checks were successful
Setup testing environment and test the code / build (push) Successful in 1m6s
2026-04-07 11:37:29 +02:00
55e4aad0a9 Merge remote-tracking branch 'origin/main'
Some checks failed
Setup testing environment and test the code / build (push) Failing after 1m24s
2026-04-07 11:19:42 +02:00
4f96b1d687 Added caching and implemented UserService 2026-04-07 11:19:38 +02:00
aa451e4786 Fix grammar 2026-04-07 10:15:05 +02:00
40 changed files with 1654 additions and 32 deletions

View File

@@ -0,0 +1,30 @@
name: Publish to NPM
on:
workflow_dispatch:
release:
types: [published]
jobs:
build-and-publish:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "24"
- name: Install dependencies
run: npm ci
- name: Build TypeScript
run: npm run build
- name: Add NPM token
run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc
- name: Publish to NPM
run: npm publish --access public

3
.gitignore vendored
View File

@@ -13,4 +13,5 @@ yarn-error.log*
.DS_Store
.env
.env.*
!.env.example
!.env.example
/dist

6
README.md Normal file
View File

@@ -0,0 +1,6 @@
# Chatenium SDK For TypeScript
A library for interacting with the Chatenium API.
## Quick Start
```aiignore
npm install @chatenium/chatenium-sdk
```

View File

@@ -1,8 +1,25 @@
{
"name": "chatenium-sdk",
"version": "1.0.0",
"description": "",
"name": "@chatenium/chatenium-sdk",
"version": "1.0.2",
"description": "A library for interacting with the Chatenium API",
"type": "module",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
},
"./core/*": "./dist/core/*.js",
"./services/*": "./dist/services/*.js"
},
"files": [
"dist",
"README.md",
"LICENSE"
],
"scripts": {
"build": "tsc",
"test": "vitest run",
@@ -15,7 +32,7 @@
"typescript": "^5.5.3",
"vitest": "^4.1.2"
},
"private": true,
"private": false,
"dependencies": {
"@faker-js/faker": "^10.4.0",
"axios": "^1.14.0",

View File

@@ -4,6 +4,9 @@ export interface SDKConfig {
wsUrl: string;
}
declare const process: any;
declare const require: any;
const isNode =
typeof process !== 'undefined' &&
typeof process.versions !== 'undefined' &&

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -0,0 +1,19 @@
import {PublicUserData} from "./common.schema";
import {GIF, PersonalUserData} 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
}

View File

@@ -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
}

View File

@@ -0,0 +1,114 @@
import {TimeStamp} from "./common.schema";
export interface ChangeUsernameReq {
newUsername: string;
userid: string;
}
export interface ChangeDisplayNameReq {
newDisplayName: string;
userid: string;
}
export interface ChangePasswordReq {
newPassword: string;
currentPassword: string;
userid: string;
}
export interface ChangeEmailReq {
currentPassword: string;
newMail: string;
userid: string;
}
export interface VerifyMailChangeReq {
userid: string;
vCodeCurrent: number;
vCodeNew: number;
newAddress: string;
}
export interface ChangePhoneReq {
currentPassword: string;
newPhone: string;
userid: string;
}
export interface VerifyPhoneChange {
userid: string;
vCodeCurrent: number;
vCodeNew: number;
newPhone: string;
}
export interface UploadNewPfpReq {
userid: string;
pfpId: string;
}
export interface UploadNewPfpCdnReq {
userid: string;
data: string | null;
isImage: boolean;
monogramLetter: string | null;
monogramColors: string | null;
}
export interface DeleteReq {
userid: string;
password: string;
}
export interface RegisterFCMTokenReq {
userid: string;
token: string;
language: string;
}
export interface GetSessionsReq {
userid: string;
}
export interface UpdateUserDataReq {
userid: string;
}
export interface ToggleGifSaveReq {
userid: string;
url: string;
}
export interface UploadNewPfpCdnResp {
pfpId: string;
}
export interface Session {
token: string;
os: string;
language: string;
login_at: TimeStamp | string;
}
export interface GIF {
gifId: string;
url: string;
path: string;
}
export interface CurrNewCodeTestingResp {
codeCurr: number|null;
codeNew: number|null;
}
export interface PersonalUserData {
userid: string;
username: string;
displayName: string;
pfp: string;
pictureDiscovery: boolean;
gifs: GIF[];
passwordSet: boolean;
emailSet: boolean;
phoneSet: boolean;
}

View File

@@ -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(<StreamRegistry>{
status: "broadcasting_starting"
})
}),
]

View File

@@ -0,0 +1,11 @@
import {http, HttpResponse} from "msw";
import {GetResp} from "../../domain/pictureService.schema";
import {Session} from "../../domain/userService.schema";
export const userHandler = [
http.post('*/user/getSessions', () => {
return HttpResponse.json(<Session[]>[{
token: "sessionToken"
}])
}),
]

View File

@@ -5,6 +5,8 @@ import {callHandlers} from "./handlers/call.http";
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,
@@ -13,5 +15,7 @@ export const allHandlers = [
...callHandlers,
...fileUploadHandlers,
...chatHandlers,
...dmHandlers
...dmHandlers,
...userHandler,
...brcChanHandlers
]

View File

@@ -4,6 +4,9 @@ export class DatabaseMock implements DatabaseAPI {
database: { [collection: string]: { [key: string]: string } } = {};
set(collection: string, key: string, value: any) {
if (!this.database[collection]) {
this.database[collection] = {};
}
this.database[collection][key] = JSON.stringify(value);
}

View File

@@ -11,11 +11,11 @@ export class KeyringMock implements KeyringAPI {
return this.ring[key];
}
getAll(): string[] {
return Object.keys(this.ring);
}
delete(key: string) {
delete this.ring[key];
}
flush() {
this.ring = {};
}
}

View File

@@ -164,7 +164,6 @@ export class AuthService {
});
return resp.data.authCode
} catch (e) {
console.log(e)
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
}

View File

@@ -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")
})
})

View File

@@ -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<StreamRegistry> {
try {
const resp = await this.client.get<StreamRegistry>(`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<GenericErrorBody>(e)) {
throw e;
}
throw new Error("Unexpected error")
}
}
async createServer(): Promise<StreamRegistry> {
try {
const resp = await this.client.post<StreamRegistry>("network/channel/createServer", <CreateServerReq>{
userid: this.userid,
channelId: this.channelId,
networkId: this.networkId,
type: "rtmp",
categoryId: this.categoryId,
});
return resp.data
} catch (e) {
console.log(e)
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
}
throw new Error("Unexpected error")
}
}
async joinWebSocketRoom(): Promise<void> {
try {
await this.client.post("v2/network/channel/joinWebSocketRoom", <JoinWebsocketRoomReq>{
userid: this.userid,
channelId: this.channelId,
networkId: this.networkId,
connId: WebSocketHandler.getInstance().connId,
categoryId: this.categoryId,
});
return
} catch (e) {
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
}
throw new Error("Unexpected error")
}
}
}

View File

@@ -12,6 +12,7 @@ import {
StartNewReq,
ToggleChatMuteReq
} from "../domain/chatService.schema";
import {Message} from "../domain/dmService.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
@@ -47,9 +48,9 @@ export class ChatService {
async get(): Promise<Chat[]> {
try {
const resp = await this.client.get<Chat[]>(`chat/get?userid=${this.userid}`);
this.database.set("chats", this.userid, JSON.stringify(resp.data))
return resp.data
} catch (e) {
console.log(e)
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
}
@@ -57,6 +58,15 @@ export class ChatService {
}
}
getQuick(): Message[] {
const chats = this.database.get("chats", this.userid)
if (chats) {
return JSON.parse(chats)
} else {
throw new Error("No chats in database")
}
}
/**
* Gets the availability of the specified user
* @param userid

View File

@@ -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 {
@@ -53,6 +53,9 @@ export class DMService {
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}`);
if (from == 0) {
this.database.set("messages", this.chatid, JSON.stringify(resp.data))
}
return resp.data
} catch (e) {
if (isAxiosError<GenericErrorBody>(e)) {
@@ -62,6 +65,15 @@ export class DMService {
}
}
getQuick(): Message[] {
const messages = this.database.get("messages", this.chatid)
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
@@ -121,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<Message> {
async sendMessage(message: string, replyTo: string | null = null, replyToMessage: string | null = null, attachments: FileData[] | null = null, progressListener: FileUploadProgressListener | null = null): Promise<Message> {
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<Message>("chat/dm/finishMessage", <FinishMessageReq>{

View File

@@ -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<string> {
try {
const resp = await this.client.post<StartNewFileTransferResp>("v2/chat/dm/startNewFileTransfer", <StartNewFileTransferReq>{
userid: this.userid,
targetUserId: this.peerId,
metadata: transferableFiles
});
this.transferId = resp.data.transferId
return resp.data.transferId
} catch (e) {
if (isAxiosError<GenericErrorBody>(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<RTCConfiguration> {
try {
const resp = await this.client.post<RTCConfiguration>("v2/chat/dm/acceptFileTransfer", <AcceptFileTransferReq>{
userid: this.userid,
senderId: this.peerId,
transferId: transferId
});
this.transferId = transferId
return resp.data
} catch (e) {
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
}
throw new Error("Unexpected error")
}
}
/**
* Declines the file transfer with the specified transferId.
* @param transferId
*/
async decline(transferId: string): Promise<void> {
try {
await this.client.post("v2/chat/dm/declineFileTransfer", <DeclineFileTransferReq>{
userid: this.userid,
senderId: this.peerId,
transferId: transferId
});
return
} catch (e) {
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
}
throw new Error("Unexpected error")
}
}
/**
* Forwards your RTC offer to the specified user.
* @param offer
*/
async sendRtcOffer(offer: string): Promise<void> {
try {
await this.client.post("v2/chat/dm/sendRtcOfferFileTransfer", <FileTransferSendOfferRTCReq>{
userid: this.userid,
peerId: this.peerId,
transferId: this.transferId,
offer: offer,
});
return
} catch (e) {
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
}
throw new Error("Unexpected error")
}
}
/**
* Forwards your RTC answer to the specified user.
* @param answer
*/
async sendRtcAnswer(answer: string): Promise<void> {
try {
await this.client.post("v2/chat/dm/sendRtcAnswerFileTransfer", <FileTransferSendAnswerRTCReq>{
userid: this.userid,
peerId: this.peerId,
transferId: this.transferId,
answer: answer,
});
return
} catch (e) {
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
}
throw new Error("Unexpected error")
}
}
/**
* Forwards your RTC ICE candidate to the specified user.
* @param candidate
*/
async sendRtcICE(candidate: string): Promise<void> {
try {
await this.client.post("v2/chat/dm/sendRtcICEFileTransfer", <FileTransferSendICERTCReq>{
userid: this.userid,
peerId: this.peerId,
transferId: this.transferId,
candidate: candidate,
});
return
} catch (e) {
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
}
throw new Error("Unexpected error")
}
}
}

View File

@@ -4,7 +4,9 @@ import {FileUploadService} from "./fileUploadService";
describe("fileUploadService", () => {
it('should upload files', async () => {
const service = new FileUploadService("");
const uploadId = await service.uploadFiles("", "", [])
const uploadId = await service.uploadFiles("", "", [], {
fileProgressUpdate: () => {}
})
expect(uploadId).toBe("MockUploadId")
});
})

View File

@@ -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<string> {
async uploadFiles(roomId: string, userid: string, files: FileData[], listener: FileUploadProgressListener): Promise<string> {
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<void> {
private async uploadFile(uploadId: string, roomId: string, userid: string, file: FileData, registration: FileUploadRegistration, listener: FileUploadProgressListener): Promise<void> {
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)
}
}

View File

@@ -19,6 +19,7 @@ import {
import {PublicUserData, RGB} from "../domain/common.schema";
import {WebSocketHandler} from "../core/webSocketHandler";
import {MessageListener} from "../domain/websocket.schema";
import {Message} from "../domain/dmService.schema";
export class NetworkService {
userid: string;
@@ -97,6 +98,7 @@ export class NetworkService {
const resp = await this.client.post<Network[]>("network/get", <GetNetworksReq>{
userid: this.userid,
});
this.database.set("networks", this.userid, JSON.stringify(resp.data))
return resp.data
} catch (e) {
if (isAxiosError<GenericErrorBody>(e)) {
@@ -106,6 +108,15 @@ export class NetworkService {
}
}
getQuick(): Message[] {
const networks = this.database.get("networks", this.userid)
if (networks) {
return JSON.parse(networks)
} else {
throw new Error("No networks in database")
}
}
/**
* Accepts the invite and joins the network
* @param inviteId
@@ -863,7 +874,7 @@ export class NetworkService {
}
/**
* Fetches network data from an invite
* Fetches network data from an invitation
* @param inviteId
*/
async getFromInvite(inviteId: string): Promise<Network> {

View File

@@ -12,6 +12,7 @@ import {
GetResp, PostCommentReq, ToggleFollowReq, TogglePictureLikeReq, UploadImageReq
} from "../domain/pictureService.schema";
import {environment} from "../core/environment";
import {Message} from "../domain/dmService.schema";
export class PictureService {
userid: string;
@@ -42,6 +43,7 @@ export class PictureService {
async get(): Promise<GetResp> {
try {
const resp = await this.client.get<GetResp>(`picture/pictures?userid=${this.userid}&target=${this.uploaderId}`);
this.database.set("pictures", this.uploaderId, JSON.stringify(resp.data))
return resp.data
} catch (e) {
console.log(e)
@@ -52,6 +54,15 @@ export class PictureService {
}
}
getQuick(): Message[] {
const pictures = this.database.get("pictures", this.uploaderId)
if (pictures) {
return JSON.parse(pictures)
} else {
throw new Error("No pictures in database")
}
}
/**
* Fetches the top 10 most liked and newest pictures
*/

View File

@@ -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<boolean> {
try {
const resp = await this.client.post<ValidateSessionResp>("v2/user/validateSession", <ValidateSessionReq>{
token: token,
})
return resp.data.validationOk
} catch (e) {
return true
}
}
private async updateUserData(session: Session): Promise<Session> {
const authenticatedClient = this.client.create({
headers: {
"Authorization": session.token,
}
})
try {
const resp = await authenticatedClient.get<PersonalUserData>(`user/byUseridPersonal?userid=${session.userData.userid}`)
session.userData = resp.data
return session
} catch (e) {
throw new Error("Session update error")
}
}
}

View File

@@ -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<Message[]> {
try {
const resp = await this.client.get<Message[]>(`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<GenericErrorBody>(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<number> {
try {
const resp = await this.client.get<GetMessagePosResp>(`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<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[]>(`network/channel/pinnedMessages?networkId=${this.networkId}&channelId=${this.channelId}&categoryId=${this.categoryId}&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("network/channel/editMessage", <EditMessageReq>{
messageId: messageId,
networkId: this.networkId,
channelId: this.channelId,
categoryId: this.categoryId,
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
* @param progressListener
*/
async sendMessage(message: string, replyTo: string | null = null, replyToMessage: string | null = null, attachments: FileData[] | null = null, progressListener: FileUploadProgressListener | null = null): Promise<Message> {
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<Message>("network/channel/finishMessage", <FinishMessageReq>{
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<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("network/channel/readMessages", <ReadMessagesReq>{
networkId: this.networkId,
channelId: this.channelId,
categoryId: this.categoryId,
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("network/channel/pinMessage", <PinMessageReq>{
messageId: messageId,
networkId: this.networkId,
channelId: this.channelId,
categoryId: this.categoryId,
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("network/channel/unpinMessage", <UnpinMessageReq>{
messageId: messageId,
networkId: this.networkId,
channelId: this.channelId,
categoryId: this.categoryId,
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("network/channel/deleteMessages", <DeleteMessagesReq>{
networkId: this.networkId,
channelId: this.channelId,
categoryId: this.categoryId,
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("network/channel/joinWebSocketRoom", <JoinWsRoomReq>{
networkId: this.networkId,
channelId: this.channelId,
categoryId: this.categoryId,
userid: this.userid,
connId: WebSocketHandler.getInstance().connId,
});
return
} catch (e) {
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
}
throw new Error("Unexpected error")
}
}
}

View File

@@ -0,0 +1,12 @@
import {describe, expect, it} from "vitest";
import {UserService} from "./userService";
import {DatabaseMock} from "../mocks/storage/database";
describe("UserService", () => {
const service = new UserService("", "", new DatabaseMock())
it('should get all sessions', async () => {
const sessions = await service.getSessions()
expect(sessions[0].token).toBe("sessionToken")
});
})

253
src/services/userService.ts Normal file
View File

@@ -0,0 +1,253 @@
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 {DeleteCategoryReq} from "../domain/networkService.schema";
import {GenericErrorBody} from "../domain/http.schema";
import {
ChangeDisplayNameReq,
ChangeEmailReq,
ChangePasswordReq, ChangePhoneReq,
ChangeUsernameReq, CurrNewCodeTestingResp, DeleteReq, GetSessionsReq, GIF, RegisterFCMTokenReq, Session,
ToggleGifSaveReq, UploadNewPfpCdnReq, UploadNewPfpCdnResp,
UploadNewPfpReq, VerifyMailChangeReq, VerifyPhoneChange
} from "../domain/userService.schema";
import {RGB} from "../domain/common.schema";
import {OtpPleCodeSendTestingResp} from "../domain/authService.schema";
export class UserService {
userid: string;
database: DatabaseAPI;
client: AxiosInstance
cdnClient: AxiosInstance
constructor(userid: string, token: string, database: DatabaseAPI) {
this.userid = userid;
this.database = database;
this.client = getClient(false).create({
headers: {
"Authorization": token,
}
})
this.cdnClient = getClient(true).create({
headers: {
"Authorization": token,
}
})
}
async changeUsername(username: string): Promise<void> {
try {
await this.client.patch("user/changeUsername", <ChangeUsernameReq>{
userid: this.userid,
newUsername: username,
});
return
} catch (e) {
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
}
throw new Error("Unexpected error")
}
}
async changeDisplayName(displayName: string): Promise<void> {
try {
await this.client.patch("user/changeDisplayName", <ChangeDisplayNameReq>{
userid: this.userid,
newDisplayName: displayName
});
return
} catch (e) {
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
}
throw new Error("Unexpected error")
}
}
async changePassword(newPassword: string, currentPassword: string): Promise<void> {
try {
await this.client.patch("user/changePassword", <ChangePasswordReq>{
userid: this.userid,
currentPassword: currentPassword,
newPassword: newPassword,
});
return
} catch (e) {
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
}
throw new Error("Unexpected error")
}
}
async changeEmail(newMail: string, currentPassword: string): Promise<void|CurrNewCodeTestingResp> {
try {
const resp = await this.client.patch<CurrNewCodeTestingResp>("user/changeEmail", <ChangeEmailReq>{
userid: this.userid,
currentPassword: currentPassword,
newMail: newMail,
});
if (resp.data.codeCurr != null) {
return resp.data
} else {
return
}
} catch (e) {
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
}
throw new Error("Unexpected error")
}
}
async uploadNewPfp(pfpId: string): Promise<void> {
try {
await this.client.patch("user/uploadNewPfp", <UploadNewPfpReq>{
userid: this.userid,
pfpId: pfpId,
});
return
} catch (e) {
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
}
throw new Error("Unexpected error")
}
}
async uploadNewPfpCdn(image: string | null, monogramLetter: string | null, monogramColors: RGB): Promise<string> {
try {
const resp = await this.cdnClient.post<UploadNewPfpCdnResp>("pfp", <UploadNewPfpCdnReq>{
userid: this.userid,
data: image,
monogramColors: JSON.stringify(monogramColors),
isImage: image !== null,
monogramLetter: monogramLetter,
});
console.log(resp.data.pfpId, "PFPID")
return resp.data.pfpId
} catch (e) {
console.log(e)
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
}
throw new Error("Unexpected error")
}
}
async verifyEmailChange(vCodeCurrent: number, vCodeNew: number, newAddress: string): Promise<void> {
try {
await this.client.patch("user/verifyMailChange", <VerifyMailChangeReq>{
userid: this.userid,
newAddress: newAddress,
vCodeCurrent: vCodeCurrent,
vCodeNew: vCodeNew,
});
return
} catch (e) {
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
}
throw new Error("Unexpected error")
}
}
async delete(password: string): Promise<void> {
try {
await this.client.post("user/deleteAccount", <DeleteReq>{
userid: this.userid,
password: password
});
return
} catch (e) {
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
}
throw new Error("Unexpected error")
}
}
async registerFirebaseToken(fcmToken: string): Promise<void> {
try {
await this.client.post("user/registerFcmToken", <RegisterFCMTokenReq>{
userid: this.userid,
token: fcmToken,
});
return
} catch (e) {
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
}
throw new Error("Unexpected error")
}
}
async getSessions(): Promise<Session[]> {
try {
const resp = await this.client.post<Session[]>("user/getSessions", <GetSessionsReq>{
userid: this.userid,
});
return resp.data
} catch (e) {
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
}
throw new Error("Unexpected error")
}
}
async toggleGIFSave(url: string): Promise<GIF> {
try {
const resp = await this.client.patch<GIF>("user/toggleGIFSave", <ToggleGifSaveReq>{
userid: this.userid,
url: url
});
return resp.data
} catch (e) {
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
}
throw new Error("Unexpected error")
}
}
async changePhoneNumber(currentPassword: string, newPhone: string): Promise<CurrNewCodeTestingResp|void> {
try {
const resp = await this.client.patch<CurrNewCodeTestingResp>("user/changePhone", <ChangePhoneReq>{
userid: this.userid,
newPhone: newPhone,
currentPassword: currentPassword,
});
if (resp.data.codeCurr != null) {
return resp.data
} else {
return
}
} catch (e) {
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
}
throw new Error("Unexpected error")
}
}
async verifyPhoneNumberChange(newPhone: string, vCodeCurrent: number, vCodeNew: number): Promise<void> {
try {
const resp = await this.client.patch("user/verifyPhoneChange", <VerifyPhoneChange>{
userid: this.userid,
newPhone: newPhone,
vCodeCurrent: vCodeCurrent,
vCodeNew: vCodeNew,
});
return resp.data
} catch (e) {
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
}
throw new Error("Unexpected error")
}
}
}

View File

@@ -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;
}

6
src/storage/keyvalue.ts Normal file
View File

@@ -0,0 +1,6 @@
export interface KeyValueAPI {
set(key: string, value: any): void;
get(key: string): string;
delete(key: string): void;
flush(): void;
}

View File

@@ -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()
});
})

View File

@@ -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: () => {},
}
)
});
})

View File

@@ -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)
@@ -74,7 +68,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, <RGB>{r: 0, g: 0, b: 0}, null)
expect(rank.name).toBe(rankName)
});

View File

@@ -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)

View File

@@ -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)
});
})

50
tests/userService.test.ts Normal file
View File

@@ -0,0 +1,50 @@
import {describe, expect, it} from "vitest";
import {UserService} from "../src/services/userService";
import {DatabaseMock} from "../src/mocks/storage/database";
import {RGB} from "../src/domain/common.schema"
const USER_SERVICE_TESTING_USER_ID = "000000000000000000000000"
const USER_SERVICE_TESTING_TOKEN = "testingToken"
describe("UserService Integration Testing", () => {
const service = new UserService(USER_SERVICE_TESTING_USER_ID, USER_SERVICE_TESTING_TOKEN, new DatabaseMock())
it('should not throw on username change', async () => {
await service.changeUsername("bob2")
});
it('should not throw on displayName change', async () => {
await service.changeDisplayName("New Display Name")
});
it('should set new password', async () => {
await service.changePassword("newPasswd", "") // The filler user doesn't have a password set yet
});
it('should set new e-mail', async () => {
const code = await service.changeEmail("bob2@example.com", "")
expect(code).not.toBeNull()
if (code != null) {
await service.verifyEmailChange(code.codeCurr??0, code.codeNew??0, "bob2@example.com")
}
});
it('should upload a new pfp', async () => {
const pfpId = await service.uploadNewPfpCdn(null, "A", <RGB>{r: 255, g: 255, b: 255})
await service.uploadNewPfp(pfpId)
});
it('should get sessions', async () => {
const sessions = await service.getSessions()
expect(sessions[0].token).toBe(USER_SERVICE_TESTING_TOKEN)
});
it('should set new phone', async () => {
const code = await service.changePhoneNumber("", "+36201234567")
expect(code).not.toBeNull()
if (code != null) {
await service.verifyPhoneNumberChange("+36201234567", code.codeCurr??0, code.codeNew??0)
}
});
})

View File

@@ -4,6 +4,7 @@
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"strict": true,
"skipLibCheck": true,
"outDir": "dist"