71 Commits

Author SHA1 Message Date
d3a295d598 Hotfixes for updateSession in sessionManager.ts
All checks were successful
Setup testing environment and test the code / build (push) Successful in 26s
Publish to NPM / build-and-publish (release) Successful in 17s
2026-04-18 13:10:50 +02:00
d97abc00e2 Fix version number
All checks were successful
Setup testing environment and test the code / build (push) Successful in 24s
Publish to NPM / build-and-publish (release) Successful in 26s
2026-04-17 16:27:31 +02:00
cada5487bf Merge remote-tracking branch 'origin/main'
Some checks failed
Setup testing environment and test the code / build (push) Successful in 33s
Publish to NPM / build-and-publish (release) Failing after 15s
# Conflicts:
#	package.json
2026-04-17 16:25:28 +02:00
2f9c65512b Added network permissions as constants and a permission calculator in permissions.ts 2026-04-17 16:25:03 +02:00
edd87375c3 Update package.json
Some checks failed
Setup testing environment and test the code / build (push) Has been cancelled
Publish to NPM / build-and-publish (release) Successful in 17s
2026-04-16 18:15:30 +02:00
b2d5b84435 Update package.json
Some checks failed
Setup testing environment and test the code / build (push) Has been cancelled
2026-04-16 18:13:06 +02:00
c460dc5385 Updated networkService.ts to use new /v2/ endpoint for uploading new network pictures
All checks were successful
Setup testing environment and test the code / build (push) Successful in 26s
Publish to NPM / build-and-publish (release) Successful in 37s
2026-04-16 16:55:48 +02:00
f54e76ab72 Updated extraMetadata to allow any type of data
All checks were successful
Setup testing environment and test the code / build (push) Successful in 26s
Publish to NPM / build-and-publish (release) Successful in 18s
2026-04-15 16:11:01 +02:00
fb1555338d Update WS
Some checks failed
Publish to NPM / build-and-publish (release) Successful in 27s
Setup testing environment and test the code / build (push) Has been cancelled
2026-04-14 16:44:27 +02:00
c98c917594 Update WS 2026-04-14 16:44:19 +02:00
cfb72d1772 Fixed types in getQuick functions
All checks were successful
Setup testing environment and test the code / build (push) Successful in 1m48s
Publish to NPM / build-and-publish (release) Successful in 27s
2026-04-14 16:05:09 +02:00
01d07d65d1 WebSocket Hotfix
All checks were successful
Setup testing environment and test the code / build (push) Successful in 1m9s
Publish to NPM / build-and-publish (release) Successful in 32s
2026-04-11 17:25:10 +02:00
c6ad01b710 Fix textChannelService.ts websocket
All checks were successful
Setup testing environment and test the code / build (push) Successful in 1m1s
Publish to NPM / build-and-publish (release) Successful in 30s
2026-04-11 17:14:39 +02:00
113cff5512 Fix textChannelService.ts websocket
Some checks failed
Setup testing environment and test the code / build (push) Failing after 1s
Publish to NPM / build-and-publish (release) Successful in 40s
2026-04-11 17:03:19 +02:00
2c91b73a60 Fix textChannelService.ts websocket
Some checks failed
Setup testing environment and test the code / build (push) Has been cancelled
2026-04-11 17:02:53 +02:00
866c8a1838 Removed dotenv
All checks were successful
Setup testing environment and test the code / build (push) Successful in 1m22s
Publish to NPM / build-and-publish (release) Successful in 29s
2026-04-10 21:17:16 +02:00
926a28b7f9 SessionManager hotfix
All checks were successful
Setup testing environment and test the code / build (push) Successful in 2m37s
Publish to NPM / build-and-publish (release) Successful in 1m44s
2026-04-10 19:18:13 +02:00
9d6a18dda4 Updated Attachment type
All checks were successful
Setup testing environment and test the code / build (push) Successful in 2m42s
Publish to NPM / build-and-publish (release) Successful in 2m47s
2026-04-10 09:18:16 +02:00
76f573023f WebSocket onNewConnId scope hotfix
All checks were successful
Setup testing environment and test the code / build (push) Successful in 1m10s
Publish to NPM / build-and-publish (release) Successful in 32s
2026-04-10 08:14:47 +02:00
b217123b99 Fix tests
All checks were successful
Setup testing environment and test the code / build (push) Successful in 1m9s
Publish to NPM / build-and-publish (release) Successful in 32s
2026-04-10 08:01:11 +02:00
59f7e10dd7 Race-condition fix
Some checks failed
Setup testing environment and test the code / build (push) Failing after 1m9s
2026-04-10 07:59:45 +02:00
56a0167120 WebSocket update
Some checks failed
Setup testing environment and test the code / build (push) Failing after 1m26s
Publish to NPM / build-and-publish (release) Successful in 29s
2026-04-10 07:38:52 +02:00
dc782003b0 Improve DX for message sending
Some checks failed
Setup testing environment and test the code / build (push) Failing after 0s
Publish to NPM / build-and-publish (release) Successful in 35s
2026-04-09 16:56:11 +02:00
2af9142d6c Improve DX for message sending
Some checks failed
Setup testing environment and test the code / build (push) Failing after 1m39s
Publish to NPM / build-and-publish (release) Failing after 33s
2026-04-09 16:53:08 +02:00
40905b225c Improve DX for message sending
Some checks failed
Setup testing environment and test the code / build (push) Has been cancelled
2026-04-09 16:52:55 +02:00
77e032fdb2 HotFix
All checks were successful
Setup testing environment and test the code / build (push) Successful in 59s
Publish to NPM / build-and-publish (release) Successful in 31s
2026-04-09 12:04:42 +02:00
96a5e5896b HotFix
All checks were successful
Setup testing environment and test the code / build (push) Successful in 1m13s
Publish to NPM / build-and-publish (release) Successful in 33s
2026-04-09 11:56:40 +02:00
14fe7ef41d HotFix 2026-04-09 11:56:29 +02:00
d04bd6a437 HotFix
All checks were successful
Setup testing environment and test the code / build (push) Successful in 57s
Publish to NPM / build-and-publish (release) Successful in 33s
2026-04-09 11:18:47 +02:00
0e553767b6 HotFix
Some checks failed
Publish to NPM / build-and-publish (release) Has been cancelled
Setup testing environment and test the code / build (push) Failing after 1m1s
2026-04-09 11:16:45 +02:00
b7af5497a4 Fix DM WebSocket
Some checks failed
Setup testing environment and test the code / build (push) Failing after 1m22s
Publish to NPM / build-and-publish (release) Successful in 30s
2026-04-09 11:12:41 +02:00
1cd629e3c1 loadSessions hotfix
All checks were successful
Setup testing environment and test the code / build (push) Successful in 1m20s
Publish to NPM / build-and-publish (release) Successful in 27s
2026-04-08 19:18:06 +02:00
7d50692ece Make storage async
All checks were successful
Setup testing environment and test the code / build (push) Successful in 1m4s
Publish to NPM / build-and-publish (release) Successful in 41s
2026-04-08 18:10:46 +02:00
d7422efcf0 Quick-fix
All checks were successful
Setup testing environment and test the code / build (push) Successful in 1m2s
2026-04-08 16:56:09 +02:00
1ccf534e04 Quick-fix
Some checks failed
Setup testing environment and test the code / build (push) Successful in 1m1s
Publish to NPM / build-and-publish (release) Failing after 29s
2026-04-08 16:48:11 +02:00
7ba341203d Quick-fix
All checks were successful
Setup testing environment and test the code / build (push) Successful in 1m6s
Publish to NPM / build-and-publish (release) Successful in 36s
2026-04-08 16:44:41 +02:00
54d466fb27 Quick-fix
All checks were successful
Setup testing environment and test the code / build (push) Successful in 1m30s
Publish to NPM / build-and-publish (release) Successful in 34s
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
e9eb95e9b7 Add LICENSE
All checks were successful
Setup testing environment and test the code / build (push) Successful in 1m29s
2026-04-07 10:13:30 +02:00
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
13708bcee8 Update .gitea/workflows/test.yml
All checks were successful
Setup testing environment and test the code / build (push) Successful in 1m24s
2026-04-05 16:04:53 +02:00
7069cc0b0f Update .gitea/workflows/test.yml
Some checks failed
Setup testing environment and test the code / build (push) Failing after 53s
2026-04-05 16:02:20 +02:00
0abd377f81 Add debug
Some checks failed
Setup testing environment and test the code / build (push) Failing after 58s
2026-04-05 15:59:31 +02:00
f4bb1d91c2 WebSocket update
Some checks failed
Setup testing environment and test the code / build (push) Failing after 1m6s
2026-04-05 15:57:22 +02:00
f6b5d12914 WebSocket update
Some checks failed
Setup testing environment and test the code / build (push) Failing after 1m4s
2026-04-05 15:52:39 +02:00
52eb73374b Update .gitea/workflows/test.yml
Some checks failed
Setup testing environment and test the code / build (push) Failing after 1m11s
2026-04-05 15:49:38 +02:00
4a34d73c2f Code quality improvements and cleanup + Implemented ChatService
Some checks failed
Setup testing environment and test the code / build (push) Has been cancelled
2026-04-05 15:19:55 +02:00
ec418ca7c9 Remove env overwrites (Not needed anymore)
Some checks failed
Setup testing environment and test the code / build (push) Failing after 1m3s
2026-04-05 12:33:01 +02:00
71788140c0 Update .gitea/workflows/test.yml
Some checks failed
Setup testing environment and test the code / build (push) Failing after 1m19s
2026-04-05 12:31:02 +02:00
afedc9debc Remove env overwrites (Not needed anymore)
Some checks failed
Setup testing environment and test the code / build (push) Failing after 54s
2026-04-05 12:25:54 +02:00
431b33a2b9 Updated environment.ts and workflow
Some checks failed
Setup testing environment and test the code / build (push) Has been cancelled
2026-04-05 12:21:38 +02:00
6c1161b827 Update .gitea/workflows/test.yml
Some checks failed
Setup testing environment and test the code / build (push) Failing after 2m26s
2026-04-05 12:10:01 +02:00
49e0d856d5 Update .gitea/workflows/test.yml
Some checks failed
Setup testing environment and test the code / build (push) Failing after 22s
2026-04-05 11:58:59 +02:00
ebe95f6e30 Update .gitea/workflows/test.yml
Some checks failed
Setup testing environment and test the code / build (push) Failing after 1s
2026-04-05 11:57:27 +02:00
e00de3dd0f Finished implementing FileUploadService + added test workflow
Some checks failed
Setup testing environment and test the code / build (push) Failing after 0s
2026-04-05 11:53:44 +02:00
e390012b1f Finished implementing FileUploadService + added test workflow 2026-04-05 11:50:44 +02:00
77 changed files with 3053 additions and 168 deletions

2
.env.example Normal file
View File

@@ -0,0 +1,2 @@
API_URL=https://api.chatenium.hu
CDN_URL=https://cdn.chatenium.hu

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

55
.gitea/workflows/test.yml Normal file
View File

@@ -0,0 +1,55 @@
name: Setup testing environment and test the code
on:
workflow_dispatch:
push:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
services:
mongodb:
image: mongo:6.0
redis:
image: redis:7-alpine
api:
image: git.chatenium.hu/chatenium/api:latest
credentials:
username: ${{ secrets.REPO_USER }}
password: ${{ secrets.REPO_TOKEN }}
env:
ENVIRONMENT_TYPE: "testing"
MONGODB_URL: mongodb://mongodb:27017
REDIS_SERVER_ADDR: redis:6379
cdn:
image: git.chatenium.hu/chatenium/cdn:latest
credentials:
username: ${{ secrets.REPO_USER }}
password: ${{ secrets.REPO_TOKEN }}
env:
ENVIRONMENT: "testing"
REDIS_SERVER_ADDR: redis:6379
MAIN_API_URL: http://api:3000
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '24'
- name: Run Vitest
env:
API_URL: http://api:3000
CDN_URL: http://cdn:4000
WS_URL: ws://api:3000
run: |
npm install
npm test --experimental-websocket

7
.gitignore vendored
View File

@@ -9,4 +9,9 @@ yarn-error.log*
.pnp.js
.vscode/*
.idea/*
.idea/*
.DS_Store
.env
.env.*
!.env.example
/dist

23
LICENSE
View File

@@ -631,8 +631,8 @@ to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
Chatenium SDK TypeScript
Copyright (C) 2026 Chatenium
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -647,25 +647,6 @@ the "copyright" line and a pointer to where the full notice is found.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
Chatenium Copyright (C) 2026 Chatenium
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with

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
```

55
package-lock.json generated
View File

@@ -1,18 +1,21 @@
{
"name": "chatenium-sdk",
"version": "1.0.0",
"name": "@chatenium/chatenium-sdk",
"version": "1.0.8",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "chatenium-sdk",
"version": "1.0.0",
"name": "@chatenium/chatenium-sdk",
"version": "1.0.8",
"dependencies": {
"@faker-js/faker": "^10.4.0",
"axios": "^1.14.0",
"msw": "^2.12.14"
"dotenv": "^17.4.0",
"msw": "^2.12.14",
"uuid": "^13.0.0"
},
"devDependencies": {
"@types/node": "^25.5.2",
"typescript": "^5.5.3",
"vitest": "^4.1.2"
}
@@ -533,6 +536,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": {
"version": "25.5.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.2.tgz",
"integrity": "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.18.0"
}
},
"node_modules/@types/statuses": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz",
@@ -835,6 +848,18 @@
"node": ">=8"
}
},
"node_modules/dotenv": {
"version": "17.4.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.0.tgz",
"integrity": "sha512-kCKF62fwtzwYm0IGBNjRUjtJgMfGapII+FslMHIjMR5KTnwEmBmWLDRSnc3XSNP8bNy34tekgQyDT0hr7pERRQ==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -1857,6 +1882,13 @@
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "7.18.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
"devOptional": true,
"license": "MIT"
},
"node_modules/until-async": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/until-async/-/until-async-3.0.2.tgz",
@@ -1866,6 +1898,19 @@
"url": "https://github.com/sponsors/kettanaito"
}
},
"node_modules/uuid": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
"integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist-node/bin/uuid"
}
},
"node_modules/vite": {
"version": "8.0.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.3.tgz",

View File

@@ -1,22 +1,45 @@
{
"name": "chatenium-sdk",
"version": "1.0.0",
"description": "",
"name": "@chatenium/chatenium-sdk",
"version": "1.2.1",
"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",
"./domain/*": "./dist/domain/*.js",
"./mocks/*": "./dist/mocks/*.js",
"./storage/*": "./dist/storage/*.js"
},
"files": [
"dist",
"README.md",
"LICENSE"
],
"scripts": {
"build": "tsc",
"test": "vitest run --project unit",
"test": "vitest run",
"test-unit": "vitest run --project unit",
"test-integration": "vitest run --project integration",
"test:watch": "vitest"
},
"devDependencies": {
"@types/node": "^25.5.2",
"typescript": "^5.5.3",
"vitest": "^4.1.2"
},
"private": true,
"private": false,
"dependencies": {
"@faker-js/faker": "^10.4.0",
"axios": "^1.14.0",
"msw": "^2.12.14"
"msw": "^2.12.14",
"uuid": "^13.0.0"
}
}

View File

@@ -1,21 +1,40 @@
export interface SDKConfig {
apiUrl: string;
cdnUrl: string;
wsUrl: string;
}
declare const process: any;
const isNode =
typeof process !== 'undefined' &&
typeof process.versions !== 'undefined' &&
typeof process.versions.node !== 'undefined';
const getEnv = (key: string): string | undefined => {
if (!isNode) return undefined;
return process.env?.[key];
};
const DefaultEnvironment: SDKConfig = {
apiUrl: "https://api.chatenium.hu",
cdnUrl: "https://cdn.chatenium.hu",
}
apiUrl: getEnv('API_URL') ?? "https://api.chatenium.hu",
cdnUrl: getEnv('CDN_URL') ?? "https://cdn.chatenium.hu",
wsUrl: getEnv('WS_URL') ?? "wss://api.chatenium.hu",
};
let currentConfig = {...DefaultEnvironment}
let currentConfig: SDKConfig = { ...DefaultEnvironment };
export const environment = {
get: () => currentConfig,
overwrite: (newConfig: Partial<SDKConfig>) => {
get(): SDKConfig {
return { ...currentConfig };
},
overwrite(newConfig: Partial<SDKConfig>): void {
currentConfig = {
...currentConfig,
...newConfig,
};
},
}
reset(): void {
currentConfig = { ...DefaultEnvironment };
}
};

View File

@@ -1,5 +1,5 @@
import axios, {AxiosInstance} from 'axios';
import {environment} from "./environment";
import {environment} from './environment.js';
export const getClient = (cdn: boolean) => {
const env = environment.get();

32
src/core/permissions.ts Normal file
View File

@@ -0,0 +1,32 @@
export const permissions = {
createAndEditCategories: 2,
deleteCategories: 4,
createAndEditChannels: 8,
deleteChannels: 16,
deleteAnyMessage: 32,
pinMessages: 64,
createAndEditRanks: 128,
deleteRanks: 256,
changeNetworkNamePictureAndVisibility: 512,
createEmojis: 1024,
deleteEmojis: 2048,
manageEmbed: 4096,
createWebhooks: 8192,
deleteWebhooks: 16384,
createInvites: 32768,
deleteInvites: 65536,
sendMessages: 131072,
seeChannels: 262144,
banMembers: 524288,
kickMembers: 1048576,
unAndAssignRanksToMember: 2097152,
}
/**
* Determines whether the set of permissions includes the permission. Note that network owners have regular permissions just like the other members, so if the userid matches with the network's creator ID, then the permission is automatically granted. This logic is not included in this function.
* @param permissions
* @param permission
*/
export function permissionGranted(permissions: number, permission: number): Boolean {
return (permissions & permission) === permission;
}

View File

@@ -0,0 +1,99 @@
import {
WSListenerPipe,
WSConnIdPayload,
WSMakeTokenReq,
WSMakeTokenResp,
WSMessagePayload
} from '../domain/websocket.schema.js';
import {getClient} from './http.js';
import {CreateNetworkReq, Network} from '../domain/networkService.schema.js';
import {isAxiosError} from "axios";
import {GenericErrorBody} from '../domain/http.schema.js';
import {environment} from './environment.js';
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().wsUrl}/v2/ws?userid=${userid}&access_token=${resp.data.token}`)
console.log("Connected to websocket successfully")
this.startListening()
this.connection.onclose = () => {
console.error("The WebSocket connection was closed unexpectedly. Reconnecting...")
setTimeout(() => {
this.connect(userid, token)
}, 3000)
}
return
} catch (e) {
console.log(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.connectionId = data.connId;
this.listeners.forEach(listener => {
console.log(data.connId, listener)
listener.onNewConnId(data.connId)
})
} else {
this.listeners.forEach(listener => {
listener.onNewMessage(payl.action, payl.data)
})
}
}
}
}
public registerService(service: WSListenerPipe) {
console.log("Registering service", service)
if (this.connId) {
service.onNewConnId(this.connId)
}
this.listeners.add(service);
}
public unregisterService(service: WSListenerPipe) {
this.listeners.delete(service);
}
}

View File

@@ -0,0 +1,29 @@
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
disableAutoRemove: boolean
}
export interface StreamRegistry {
streamKey: string
status: "idling" | "broadcasting" | "broadcasting_starting"
type: "rtmp"
streamURL: string
channelId: string
}

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

View File

@@ -14,4 +14,15 @@ 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
extraMetaData: Record<string, any> // Used by clients
}

View File

@@ -0,0 +1,113 @@
import {Attachment, TimeStamp} from './common.schema.js';
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
disableAutoRemove: boolean
}
// 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,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

@@ -0,0 +1,45 @@
// Request schemas
export interface RegisterUploadReq {
roomId: string
userid: string
files: FileUploadRegistration[]
}
export interface ChunkUploadReq {
uploadId: string
fileId: string
chunk: string
roomId: string
userid: string
}
export interface FinishUploadReq {
uploadId: string
roomId: string
userid: string
}
// Response schemas
export interface RegisterUploadResp {
uploadId: string
}
// Types
export interface FileUploadRegistration {
size: number
type: string
name: string
fileId: string
}
export interface FileData {
fileId: string
name: string
extension: string
type: string
data: File
}
export interface FileUploadProgressListener {
fileProgressUpdate: (tempMsgId: string, fileId: string, allChunks: number, chunksDone: number) => void
}

View File

@@ -1,5 +1,5 @@
// Request schemas
import {PublicUserData, RGB, TimeStamp} from "./common.schema";
import {PublicUserData, RGB, TimeStamp} from './common.schema.js';
export interface GetInvitesReq {
networkId: string
@@ -208,9 +208,11 @@ export interface GetMembersReq {
}
export interface UploadNewPictureReq {
picId: string
userid: string
networkId: string
data: string
isImage: boolean
monogramColors: RGB | null
}
export interface ChangeVisibilityReq {
@@ -255,6 +257,7 @@ export interface JoinWebSocketRoomReq {
userid: string
connId: string
networkId: string
disableAutoRemove: boolean
}
export interface GetFromInviteReq {

View File

@@ -1,4 +1,4 @@
import {PublicUserData, TimeStamp} from "./common.schema";
import {PublicUserData, TimeStamp} from './common.schema.js';
// Request schemas
export interface GetReq {

View File

@@ -0,0 +1,19 @@
import {PublicUserData} from './common.schema.js';
import {GIF, PersonalUserData} from './userService.schema.js';
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,139 @@
import {Attachment, PublicUserData, TimeStamp} from './common.schema.js';
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
disableAutoRemove: boolean
}
// 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.js';
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,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
}

View File

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

View File

@@ -1,5 +0,0 @@
import {KeyringAPI} from "./storage/keyring";
export function jezus(keyring: KeyringAPI) {
keyring.set("keyring", "apad");
}

View File

@@ -5,8 +5,8 @@ import {
OtpSendCodeReq,
OtpVerifyCodeReq, PleVerifyCodeReq, PleVerifyCodeResp, RegisterReq, SignInSuccessResp,
UserDataValidationResp, VerifyPasswordResetReq
} from "../../domain/authService.schema";
import {GenericSuccessBody} from "../../domain/http.schema";
} from '../../domain/authService.schema.js';
import {GenericSuccessBody} from '../../domain/http.schema.js';
export const networkHandlers = [
http.get('*/user/authOptions', () => {

View File

@@ -0,0 +1,11 @@
import {http, HttpResponse} from "msw";
import {GetRTCAccessResp} from '../../domain/callService.schema.js';
import {StreamRegistry} from '../../domain/broadcastChannelService.schema.js';
export const brcChanHandlers = [
http.get('*/network/channel/rtmpData', () => {
return HttpResponse.json(<StreamRegistry>{
status: "broadcasting_starting"
})
}),
]

View File

@@ -1,5 +1,5 @@
import {http, HttpResponse} from "msw";
import {GetRTCAccessResp} from "../../domain/callService.schema";
import {GetRTCAccessResp} from '../../domain/callService.schema.js';
export const callHandlers = [
http.post('*/v2/chat/getRTCAccess', () => {

View File

@@ -0,0 +1,23 @@
import {http, HttpResponse} from "msw";
import {Chat, GetAvailabilityResp, StartNewReq} from '../../domain/chatService.schema.js';
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
})
}),
]

View File

@@ -0,0 +1,35 @@
import {http, HttpResponse} from "msw";
import {Chat} from '../../domain/chatService.schema.js';
import {FinishMessageReq, GetMessagePosResp, Message, PinnedMessage} from '../../domain/dmService.schema.js';
import {CreateNetworkReq, Network} from '../../domain/networkService.schema.js';
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,
})
}),
http.post('*/v2/chat/dm/joinWebSocketRoom', async () => {
return HttpResponse.json()
}),
]

View File

@@ -0,0 +1,19 @@
import {http, HttpResponse} from "msw";
import {GetRTCAccessResp} from '../../domain/callService.schema.js';
import {RegisterUploadResp} from '../../domain/fileUploadService.schema.js';
export const fileUploadHandlers = [
http.post('*/chat/cdnRegisterUpload', () => {
return HttpResponse.json(<RegisterUploadResp>{
uploadId: "MockUploadId"
})
}),
http.post('*/chat/uploadChunk', () => {
return HttpResponse.json()
}),
http.post('*/chat/finishUpload', () => {
return HttpResponse.json()
}),
]

View File

@@ -5,8 +5,8 @@ import {
Network,
NetworkCategory,
NetworkInvite, POW
} from "../../domain/networkService.schema";
import {PublicUserData} from "../../domain/common.schema";
} from '../../domain/networkService.schema.js';
import {PublicUserData} from '../../domain/common.schema.js';
export const authHandlers = [
http.get('*/network/invites', () => {

View File

@@ -1,6 +1,6 @@
import {http, HttpResponse} from "msw";
import {CreateNetworkReq, Network, NetworkInvite} from "../../domain/networkService.schema";
import {Album, Comment, CreateAlbumReq, GetResp} from "../../domain/pictureService.schema";
import {CreateNetworkReq, Network, NetworkInvite} from '../../domain/networkService.schema.js';
import {Album, Comment, CreateAlbumReq, GetResp} from '../../domain/pictureService.schema.js';
export const pictureHandlers = [
http.get('*/picture/pictures', () => {

View File

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

View File

@@ -1,11 +1,21 @@
import {networkHandlers} from "./handlers/auth.http";
import {authHandlers} from "./handlers/network.http";
import {pictureHandlers} from "./handlers/picture.http";
import {callHandlers} from "./handlers/call.http";
import {networkHandlers} from './handlers/auth.http.js';
import {authHandlers} from './handlers/network.http.js';
import {pictureHandlers} from './handlers/picture.http.js';
import {callHandlers} from './handlers/call.http.js';
import {fileUploadHandlers} from './handlers/fUpl.http.js';
import {chatHandlers} from './handlers/chat.http.js';
import {dmHandlers} from './handlers/dm.http.js';
import {userHandler} from './handlers/user.http.js';
import {brcChanHandlers} from './handlers/brcChan.http.js';
export const allHandlers = [
...authHandlers,
...networkHandlers,
...pictureHandlers,
...callHandlers
...callHandlers,
...fileUploadHandlers,
...chatHandlers,
...dmHandlers,
...userHandler,
...brcChanHandlers
]

View File

@@ -1,4 +1,4 @@
import {setupServer} from "msw/node";
import {allHandlers} from "./index";
import {allHandlers} from './index.js';
export const mockServer = setupServer(...allHandlers)

View File

@@ -1,9 +1,12 @@
import {DatabaseAPI} from "../../storage/database";
import {DatabaseAPI} from '../../storage/database.js';
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

@@ -1,4 +1,4 @@
import {KeyringAPI} from "../../storage/keyring";
import {KeyringAPI} from '../../storage/keyring.js';
export class KeyringMock implements KeyringAPI {
ring: { [key: string]: string } = {};
@@ -7,15 +7,15 @@ export class KeyringMock implements KeyringAPI {
this.ring[key] = value;
}
get(key: string): string {
return this.ring[key];
get(key: string): Promise<string> {
return Promise.resolve(this.ring[key]);
}
getAll(): Promise<string[]> {
return Promise.resolve(Object.keys(this.ring));
}
delete(key: string) {
delete this.ring[key];
}
flush() {
this.ring = {};
}
}

View File

@@ -1,6 +1,6 @@
import {describe, expect, it} from "vitest";
import {AuthService} from "./authService";
import {VerificationTypeEmail} from "../domain/authService.schema";
import {AuthService} from './authService.js';
import {VerificationTypeEmail} from '../domain/authService.schema.js';
import {faker} from "@faker-js/faker/locale/en";
describe("AuthService", () => {

View File

@@ -1,4 +1,4 @@
import {getClient} from "../core/http";
import {getClient} from '../core/http.js';
import {
AuthMethods, FinishPleAccountReq,
LoginPasswordAuthReq, LoginWithApple, LoginWithGoogleReq, OtpPleCodeSendTestingResp,
@@ -6,9 +6,9 @@ import {
OtpVerifyCodeReq, PleSendCodeReq,
PleVerifyCodeReq, PleVerifyCodeResp, RegisterReq, ResetPasswordReq, ResetPasswordResp,
SignInSuccessResp, UserDataValidationResp, VerifyPasswordResetReq
} from "../domain/authService.schema";
} from '../domain/authService.schema.js';
import {isAxiosError} from "axios";
import {GenericErrorBody, GenericSuccessBody} from "../domain/http.schema";
import {GenericErrorBody, GenericSuccessBody} from '../domain/http.schema.js';
export class AuthService {
/**
@@ -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.js';
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,94 @@
import {AxiosInstance, isAxiosError} from "axios";
import {getClient} from '../core/http.js';
import {MessageListener} from '../domain/websocket.schema.js';
import {WebSocketHandler} from '../core/webSocketHandler.js';
import {AcceptInviteReq} from '../domain/networkService.schema.js';
import {GenericErrorBody} from '../domain/http.schema.js';
import {
CreateServerReq,
GetRTMPDataReq,
JoinWebsocketRoomReq,
StreamRegistry
} from '../domain/broadcastChannelService.schema.js';
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: newConnId => this.onNewConnId(newConnId),
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,
disableAutoRemove: true
});
return
} catch (e) {
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
}
throw new Error("Unexpected error")
}
}
}

View File

@@ -1,5 +1,5 @@
import {describe, expect, it} from "vitest";
import {CallService} from "./callService";
import {CallService} from './callService.js';
describe("CallService", () => {
const handler = new CallService("", "")

View File

@@ -1,8 +1,8 @@
import {getClient} from "../core/http";
import {OtpPleCodeSendTestingResp, OtpSendCodeReq} from "../domain/authService.schema";
import {getClient} from '../core/http.js';
import {OtpPleCodeSendTestingResp, OtpSendCodeReq} from '../domain/authService.schema.js';
import {isAxiosError} from "axios";
import {GenericErrorBody} from "../domain/http.schema";
import {GetRTCAccessReq, GetRTCAccessResp, InviteToCallReq} from "../domain/callService.schema";
import {GenericErrorBody} from '../domain/http.schema.js';
import {GetRTCAccessReq, GetRTCAccessResp, InviteToCallReq} from '../domain/callService.schema.js';
export class CallService {
userid: string;

View File

@@ -0,0 +1,24 @@
import {describe, expect, it} from "vitest";
import {ChatService} from './chatService.js';
import {DatabaseMock} from '../mocks/storage/database.js';
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)
});
})

126
src/services/chatService.ts Normal file
View File

@@ -0,0 +1,126 @@
import {DatabaseAPI} from '../storage/database.js';
import {MessageListener} from '../domain/websocket.schema.js';
import {getClient} from '../core/http.js';
import {WebSocketHandler} from '../core/webSocketHandler.js';
import {AxiosInstance, isAxiosError} from "axios";
import {NetworkInvite} from '../domain/networkService.schema.js';
import {GenericErrorBody} from '../domain/http.schema.js';
import {
Chat,
GetAvailabilityReq,
GetAvailabilityResp,
StartNewReq,
ToggleChatMuteReq
} from '../domain/chatService.schema.js';
import {Message} from '../domain/dmService.schema.js';
/**
* 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: newConnId => this.onNewConnId(newConnId),
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}`);
this.database.set("chats", this.userid, JSON.stringify(resp.data))
return resp.data
} catch (e) {
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
}
throw new Error("Unexpected error")
}
}
async getQuick(): Promise<Chat[]> {
const chats = await 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
*/
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")
}
}
}

View File

@@ -0,0 +1,29 @@
import {describe, expect, it} from "vitest";
import {DMService} from './dmService.js';
import {DatabaseMock} from '../mocks/storage/database.js';
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)
});
})

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

@@ -0,0 +1,263 @@
import {DatabaseAPI} from '../storage/database.js';
import {AxiosInstance, isAxiosError} from "axios";
import {MessageListener} from '../domain/websocket.schema.js';
import {getClient} from '../core/http.js';
import {WebSocketHandler} from '../core/webSocketHandler.js';
import {
DeleteMessagesReq,
EditMessageReq,
FinishMessageReq,
GetMessagePosResp, JoinWsRoomReq,
Message, PinMessageReq,
PinnedMessage, ReadMessagesReq, UnpinMessageReq
} from '../domain/dmService.schema.js';
import {NetworkInvite} from '../domain/networkService.schema.js';
import {GenericErrorBody} from '../domain/http.schema.js';
import {FileData, FileUploadProgressListener} from '../domain/fileUploadService.schema.js';
import {FileUploadService} from './fileUploadService.js';
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: newConnId => this.onNewConnId(newConnId),
onNewMessage: wsMessageListener,
})
}
private onNewConnId(newConnId: string) {
console.log("DmService: New connection id")
this.client.defaults.headers["X-WS-ID"] = newConnId;
this.joinWebSocketRoom().then()
}
/**
* 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}`);
if (from == 0) {
this.database.set("messages", this.chatid, JSON.stringify(resp.data))
}
return resp.data
} catch (e) {
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
}
throw new Error("Unexpected error")
}
}
async getQuick(): Promise<Message[]> {
const messages = await 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
*/
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 tempMsgId
* @param message
* @param replyTo
* @param replyToMessage
* @param attachments
* @param progressListener
*/
async sendMessage(tempMsgId: string, 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(tempMsgId, this.chatid, this.userid, attachments, progressListener!)
}
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 {
await this.client.post("v2/chat/dm/joinWebSocketRoom", <JoinWsRoomReq>{
chatid: this.chatid,
userid: this.userid,
connId: WebSocketHandler.getInstance().connId,
disableAutoRemove: true
});
return
} catch (e) {
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
}
throw new Error("Unexpected error")
}
}
}

View File

@@ -0,0 +1,156 @@
import {DatabaseAPI} from '../storage/database.js';
import {AxiosInstance, isAxiosError} from "axios";
import {MessageListener} from '../domain/websocket.schema.js';
import {getClient} from '../core/http.js';
import {WebSocketHandler} from '../core/webSocketHandler.js';
import {CreateNetworkReq, Network} from '../domain/networkService.schema.js';
import {GenericErrorBody} from '../domain/http.schema.js';
import {
AcceptFileTransferReq, DeclineFileTransferReq, FileTransferSendAnswerRTCReq,
FileTransferSendICERTCReq, FileTransferSendOfferRTCReq,
StartNewFileTransferReq,
StartNewFileTransferResp,
TransferableFileMetadata
} from '../domain/fileTransferService.schema.js';
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

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

View File

@@ -0,0 +1,138 @@
import {
ChunkUploadReq,
FileData, FileUploadProgressListener,
FileUploadRegistration, FinishUploadReq,
RegisterUploadReq,
RegisterUploadResp
} from '../domain/fileUploadService.schema.js';
import {AxiosInstance, isAxiosError} from "axios";
import {getClient} from '../core/http.js';
import {InviteToCallReq} from '../domain/callService.schema.js';
import {GenericErrorBody} from '../domain/http.schema.js';
import {v4 as uuidv4} from 'uuid';
export class FileUploadService {
client: AxiosInstance;
cdnClient: AxiosInstance;
constructor(token: string) {
this.client = getClient(false).create({
headers: {
"Authorization": token
}
})
this.cdnClient = getClient(true).create({
headers: {
"Authorization": token
}
})
}
private MIN_CHUNK_SIZE = 25000000
private calculateChunkSize(totalSize: number) {
let chunks = Math.floor(totalSize / this.MIN_CHUNK_SIZE);
if (chunks === 0) return totalSize;
const chunkSize = Math.ceil(totalSize / chunks);
return Math.max(chunkSize, this.MIN_CHUNK_SIZE);
}
/**
* Automatically registers the upload, calculates chunksize and uploads each chunk and returns with an uploadId that must be provided when sending the message
* @param tempMsgId
* @param roomId chatid or channelId
* @param userid
* @param files
* @param listener
*/
async uploadFiles(tempMsgId: string, roomId: string, userid: string, files: FileData[], listener: FileUploadProgressListener): Promise<string> {
let registrations: FileUploadRegistration[] = [];
files.forEach(file => {
registrations.push({
fileId: file.fileId,
name: file.name,
type: file.type,
size: file.data.size,
})
})
try {
const resp = await this.client.post<RegisterUploadResp>("chat/cdnRegisterUpload", <RegisterUploadReq>{
roomId: roomId,
userid: userid,
files: registrations,
});
for (let filesUploaded = 0; filesUploaded < files.length; filesUploaded++) {
await this.uploadFile(tempMsgId, resp.data.uploadId, roomId, userid, files[filesUploaded], registrations[filesUploaded], listener)
}
await this.finishUpload(roomId, userid, resp.data.uploadId)
return resp.data.uploadId
} catch (e) {
console.log(e)
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
}
throw new Error("Unexpected error")
}
}
private async finishUpload(roomId: string, userid: string, uploadId: string): Promise<void> {
try {
await this.cdnClient.post("chat/finishUpload", <FinishUploadReq>{
roomId: roomId,
userid: userid,
uploadId: uploadId
});
return
} catch (e) {
console.log(e)
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
}
throw new Error("Unexpected error")
}
}
private async uploadFile(tempMsgId: string, 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);
const arrayBuffer = await file.data.arrayBuffer();
for (let i = 0; i < totalChunks; i++) {
const start = i * chunkSize;
const end = Math.min(start + chunkSize, file.data.size);
const chunk = new Uint8Array(arrayBuffer.slice(start, end));
const base64 = this.uint8ToBase64(chunk);
await this.uploadChunk(uploadId, roomId, userid, registration.fileId, base64);
listener.fileProgressUpdate(tempMsgId, file.fileId, totalChunks, i)
}
}
private uint8ToBase64(uint8: Uint8Array): string {
let binString = "";
for (let i = 0; i < uint8.length; i++) {
binString += String.fromCharCode(uint8[i]);
}
return btoa(binString);
}
private async uploadChunk(uploadId: string, roomId: string, userid: string, fileId: string, chunk: string): Promise<void> {
try {
await this.cdnClient.post<RegisterUploadResp>("chat/uploadChunk", <ChunkUploadReq>{
roomId: roomId,
userid: userid,
fileId: fileId,
chunk: chunk,
uploadId: uploadId,
});
return
} catch (e) {
if (isAxiosError<GenericErrorBody>(e)) {
throw e;
}
throw new Error("Unexpected error")
}
}
}

View File

@@ -1,12 +1,12 @@
import {describe, expect, it} from "vitest";
import {NetworkService} from "./networkService";
import {DatabaseMock} from "../mocks/storage/database";
import {NetworkService} from './networkService.js';
import {DatabaseMock} from '../mocks/storage/database.js';
import {faker} from "@faker-js/faker/locale/en";
import {getClient} from "../core/http";
import {environment, SDKConfig} from "../core/environment";
import {getClient} from '../core/http.js';
import {environment, SDKConfig} from '../core/environment.js';
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();

View File

@@ -1,8 +1,7 @@
import {DatabaseAPI} from "../storage/database";
import {AuthMethods} from "../domain/authService.schema";
import {getClient} from "../core/http";
import {DatabaseAPI} from '../storage/database.js';
import {getClient} from '../core/http.js';
import {AxiosInstance, isAxiosError} from "axios";
import {GenericErrorBody} from "../domain/http.schema";
import {GenericErrorBody} from '../domain/http.schema.js';
import {
AcceptInviteReq, AssignRankToMemberReq, BanMemberReq, ChangeVisibilityReq, CreateCategoryReq, CreateChannelReq,
CreateInviteReq,
@@ -16,9 +15,11 @@ import {
NetworkInvite, NetworkRank, OverwriteChannelPermissionReq, OverwritePermissionReq, PermissionUpdate, POW,
RemoveRankFromMemberReq, ToggleCategoryMuteReq, ToggleChannelNetworkMuteReq, ToggleNetworkMuteReq,
UnbanMemberReq, UploadNewPictureReq
} from "../domain/networkService.schema";
import {PublicUserData, RGB} from "../domain/common.schema";
import {http} from "msw";
} from '../domain/networkService.schema.js';
import {PublicUserData, RGB} from '../domain/common.schema.js';
import {WebSocketHandler} from '../core/webSocketHandler.js';
import {MessageListener} from '../domain/websocket.schema.js';
import {Message} from '../domain/dmService.schema.js';
export class NetworkService {
userid: string;
@@ -26,15 +27,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: newConnId => this.onNewConnId(newConnId),
onNewMessage: wsMessageListener,
})
}
private onNewConnId(newConnId: string) {
console.log("NetworkService: New connection id")
this.client.defaults.headers["X-WS-ID"] = newConnId;
}
/**
@@ -86,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)) {
@@ -95,13 +108,22 @@ export class NetworkService {
}
}
async getQuick(): Promise<Network[]> {
const networks = await 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
*/
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,
});
@@ -689,14 +711,18 @@ export class NetworkService {
/**
* Uploads a new network picture
* @param picId
* @param isImage
* @param image
* @param colors
*/
async uploadNewPic(picId: string): Promise<void> {
async uploadNewPic(isImage: boolean, image: string | null, colors: RGB | null): Promise<void> {
try {
await this.client.patch<PublicUserData[]>("network/uploadNewPic", <UploadNewPictureReq>{
await this.client.patch<PublicUserData[]>("v2/network/uploadNewPic", <UploadNewPictureReq>{
userid: this.userid,
networkId: this.networkId,
picId: picId,
data: image,
isImage: isImage,
monogramColors: colors
});
return
} catch (e) {
@@ -841,6 +867,7 @@ export class NetworkService {
userid: this.userid,
networkId: this.networkId,
connId: connId,
disableAutoRemove: true
});
return
} catch (e) {
@@ -852,7 +879,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

@@ -1,10 +1,10 @@
import {describe, expect, it} from "vitest";
import {PictureService} from "./pictureService";
import {DatabaseMock} from "../mocks/storage/database";
import {PictureService} from './pictureService.js';
import {DatabaseMock} from '../mocks/storage/database.js';
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()

View File

@@ -1,8 +1,8 @@
import {DatabaseAPI} from "../storage/database";
import {DatabaseAPI} from '../storage/database.js';
import {AxiosInstance, isAxiosError} from "axios";
import {getClient} from "../core/http";
import {NetworkInvite} from "../domain/networkService.schema";
import {GenericErrorBody} from "../domain/http.schema";
import {getClient} from '../core/http.js';
import {NetworkInvite} from '../domain/networkService.schema.js';
import {GenericErrorBody} from '../domain/http.schema.js';
import {
Album, ChangePictureVisibilityReq, Comment,
CreateAlbumReq,
@@ -10,8 +10,9 @@ import {
DiscoveryResp, EditPictureTitleReq,
FinalizeUploadReq,
GetResp, PostCommentReq, ToggleFollowReq, TogglePictureLikeReq, UploadImageReq
} from "../domain/pictureService.schema";
import {environment} from "../core/environment";
} from '../domain/pictureService.schema.js';
import {environment} from '../core/environment.js';
import {Message} from '../domain/dmService.schema.js';
export class PictureService {
userid: string;
@@ -20,16 +21,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
}
@@ -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 {
}
}
async getQuick(): Promise<Message[]> {
const pictures = await 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,148 @@
import {PublicUserData} from '../domain/common.schema.js';
import {DatabaseAPI} from '../storage/database.js';
import {KeyringAPI} from '../storage/keyring.js';
import {KeyValueAPI} from '../storage/keyvalue.js';
import {Session, ValidateSessionReq, ValidateSessionResp} from '../domain/sessionManager.schema.js';
import {AxiosInstance} from "axios";
import {getClient} from '../core/http.js';
import {PersonalUserData} from '../domain/userService.schema.js';
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
*/
async loadSessions(): Promise<Session[]> {
const tokens = await this.keyring.getAll()
const sessions: Session[] = []
for (const tokenKey of tokens) {
try {
const token = await this.keyring.get(tokenKey)
const userData = await this.database.get("sessions", tokenKey)
if (userData && userData.trim().length > 0) {
sessions.push({
token: token,
userData: JSON.parse(userData)
})
}
} catch (e) {
console.error(`Failed to parse session for ${tokenKey}:`, e)
continue
}
}
return sessions
}
/**
* Gets the preferred user set by the client
*/
async getPreferredUser(): Promise<string> {
return await 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
*/
async loadPreferredSession() {
const sessions = await this.loadSessions()
if (sessions.length == 0) {
throw new Error("No sessions found")
}
let preferredUser = await 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
*/
async updateSessions(sessions: Session[]): Promise<Session[]> {
const activeSessions: Session[] = [];
for (const session of sessions) {
if (!await this.validateSession(session.token)) {
this.database.delete("sessions", session.userData.userid);
this.keyring.delete(session.userData.userid);
console.warn(`Validating session for user ${session.userData.userid} failed. Deleting session...`)
continue;
}
const updatedUserData = await this.updateUserData(session);
this.database.set("sessions", session.userData.userid, updatedUserData);
activeSessions.push(updatedUserData);
}
return activeSessions;
}
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,283 @@
import {DatabaseAPI} from '../storage/database.js';
import {AxiosInstance, isAxiosError} from "axios";
import {MessageListener} from '../domain/websocket.schema.js';
import {getClient} from '../core/http.js';
import {WebSocketHandler} from '../core/webSocketHandler.js';
import {
DeleteMessagesReq,
EditMessageReq,
FinishMessageReq,
GetMessagePosResp, JoinWsRoomReq,
Message, PinMessageReq,
PinnedMessage, ReadMessagesReq, UnpinMessageReq
} from '../domain/textChannelService.schema.js';
import {NetworkInvite} from '../domain/networkService.schema.js';
import {GenericErrorBody} from '../domain/http.schema.js';
import {FileData, FileUploadProgressListener} from '../domain/fileUploadService.schema.js';
import {FileUploadService} from './fileUploadService.js';
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: newConnId => this.onNewConnId(newConnId),
onNewMessage: wsMessageListener,
})
}
private onNewConnId(newConnId: string) {
console.log("NetworkService: New connection id")
this.client.defaults.headers["X-WS-ID"] = newConnId;
this.joinWebSocketRoom().then()
}
/**
* 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")
}
}
async getQuick(): Promise<Message[]> {
const messages = await 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 tempMsgId
* @param message
* @param replyTo
* @param replyToMessage
* @param attachments
* @param progressListener
*/
async sendMessage(tempMsgId: string, 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(tempMsgId, 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.post("v2/network/channel/joinWebSocketRoom", <JoinWsRoomReq>{
networkId: this.networkId,
channelId: this.channelId,
categoryId: this.categoryId,
userid: this.userid,
connId: WebSocketHandler.getInstance().connId,
disableAutoRemove: true
});
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.js';
import {DatabaseMock} from '../mocks/storage/database.js';
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.js';
import {AxiosInstance, isAxiosError} from "axios";
import {MessageListener} from '../domain/websocket.schema.js';
import {getClient} from '../core/http.js';
import {WebSocketHandler} from '../core/webSocketHandler.js';
import {DeleteCategoryReq} from '../domain/networkService.schema.js';
import {GenericErrorBody} from '../domain/http.schema.js';
import {
ChangeDisplayNameReq,
ChangeEmailReq,
ChangePasswordReq, ChangePhoneReq,
ChangeUsernameReq, CurrNewCodeTestingResp, DeleteReq, GetSessionsReq, GIF, RegisterFCMTokenReq, Session,
ToggleGifSaveReq, UploadNewPfpCdnReq, UploadNewPfpCdnResp,
UploadNewPfpReq, VerifyMailChangeReq, VerifyPhoneChange
} from '../domain/userService.schema.js';
import {RGB} from '../domain/common.schema.js';
import {OtpPleCodeSendTestingResp} from '../domain/authService.schema.js';
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 DatabaseAPI {
set(collection: string, key: string, value: any): void;
get(collection: string, key: string): string;
get(collection: string, key: string): Promise<string>;
delete(collection: string, key: string): void;
flush(): void;
}

View File

@@ -1,6 +1,6 @@
export interface KeyringAPI {
set(key: string, value: any): void;
get(key: string): string;
get(key: string): Promise<string>;
getAll(): Promise<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): Promise<string>;
delete(key: string): void;
flush(): void;
}

View File

@@ -1,5 +1,5 @@
import {beforeAll, afterEach, afterAll, vi} from 'vitest';
import {mockServer} from "./mocks/node";
import {mockServer} from './mocks/node.js';
beforeAll(() => mockServer.listen({onUnhandledRequest: 'error'}));
afterEach(() => mockServer.resetHandlers());

View File

@@ -1,6 +1,6 @@
import {describe, expect, it} from "vitest";
import {AuthService} from "../src/services/authService";
import {VerificationTypeEmail} from "../src/domain/authService.schema";
import {AuthService} from '../src/services/authService.js';
import {VerificationTypeEmail} from '../src/domain/authService.schema.js';
describe("AuthService", () => {
it("should return authMethods", async () => {

View File

@@ -0,0 +1,29 @@
import {describe, expect, it} from "vitest";
import {BroadcastChannelService} from '../src/services/broadcastChannelService.js';
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

@@ -0,0 +1,14 @@
import {describe, expect, it} from "vitest";
import {DatabaseMock} from '../src/mocks/storage/database.js';
import {ChatService} from '../src/services/chatService.js';
describe("ChatService Integration Testing", () => {
const CHAT_SERVICE_TESTING_USER_ID = "000000000000000000000000"
const CHAT_SERVICE_TESTING_TOKEN = "testingToken"
const service = new ChatService(CHAT_SERVICE_TESTING_USER_ID, CHAT_SERVICE_TESTING_TOKEN, new DatabaseMock(), () => {})
it('should get chats', async () => {
const chats = await service.get()
expect(chats[0].username).toBe("bob")
})
})

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.js';
import {ChatService} from '../src/services/chatService.js';
import {DatabaseMock} from '../src/mocks/storage/database.js';
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)
});
})

View File

@@ -0,0 +1,38 @@
import {describe, it} from "vitest";
import {FileUploadService} from '../src/services/fileUploadService.js';
import {environment, SDKConfig} from '../src/core/environment.js';
import {getClient} from '../src/core/http.js';
import {FileData} from '../src/domain/fileUploadService.schema.js';
import axios from "axios";
import {v4 as uuidv4} from 'uuid';
describe("FileUploadService Integration Testing", () => {
const FILE_UPL_SERVICE_TESTING_USER_ID = "000000000000000000000000"
const FILE_UPL_SERVICE_TESTING_CHAT_ID = "000000000000000000000000"
const FILE_UPL_SERVICE_TESTING_TOKEN = "testingToken"
it('should upload all files', async () => {
const response = await axios.get("https://picsum.photos/500", {
responseType: 'blob'
});
const service = new FileUploadService(FILE_UPL_SERVICE_TESTING_TOKEN);
await service.uploadFiles(
"",
FILE_UPL_SERVICE_TESTING_CHAT_ID,
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

@@ -1,12 +1,12 @@
import {describe, expect, it} from "vitest";
import {NetworkService} from "../src/services/networkService";
import {DatabaseMock} from "../src/mocks/storage/database";
import {environment, SDKConfig} from "../src/core/environment";
import {getClient} from "../src/core/http";
import {NetworkService} from '../src/services/networkService.js';
import {DatabaseMock} from '../src/mocks/storage/database.js';
import {environment, SDKConfig} from '../src/core/environment.js';
import {getClient} from '../src/core/http.js';
import {faker} from "@faker-js/faker/locale/en";
import {AuthService} from "../src/services/authService";
import {RGB} from "../src/domain/common.schema";
import {NetworkPermissions, PermissionUpdate} from "../src/domain/networkService.schema";
import {AuthService} from '../src/services/authService.js';
import {RGB} from '../src/domain/common.schema.js';
import {NetworkPermissions, PermissionUpdate} from '../src/domain/networkService.schema.js';
const NETWORK_SERVICE_TESTING_NETWORK_ID = "000000000000000000000000"
const NETWORK_SERVICE_TESTING_USER_ID = "000000000000000000000000"
@@ -18,13 +18,12 @@ const NETWORK_SERVICE_TESTING_INVITE_ID = "444444444444444444444444"
const NETWORK_SERVICE_TESTING_TOKEN = "testingToken"
describe("NetworkService Integration Testing", () => {
environment.overwrite(<SDKConfig>{apiUrl: "http://localhost:3000"})
const service = new NetworkService(
NETWORK_SERVICE_TESTING_USER_ID,
NETWORK_SERVICE_TESTING_TOKEN,
NETWORK_SERVICE_TESTING_NETWORK_ID,
new DatabaseMock(),
getClient(false),
(action, data) => {}
)
it("should get invites", async () => {
@@ -57,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)
@@ -75,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

@@ -1,9 +1,9 @@
import {describe, expect, it} from "vitest";
import {environment, SDKConfig} from "../src/core/environment";
import {NetworkService} from "../src/services/networkService";
import {DatabaseMock} from "../src/mocks/storage/database";
import {getClient} from "../src/core/http";
import {PictureService} from "../src/services/pictureService";
import {environment, SDKConfig} from '../src/core/environment.js';
import {NetworkService} from '../src/services/networkService.js';
import {DatabaseMock} from '../src/mocks/storage/database.js';
import {getClient} from '../src/core/http.js';
import {PictureService} from '../src/services/pictureService.js';
import {faker} from "@faker-js/faker/locale/en";
const PICTURE_SERVICE_TESTING_TOKEN = "testingToken"
@@ -11,14 +11,11 @@ const PICTURE_SERVICE_TESTING_USER_ID = "000000000000000000000000"
const PICTURE_SERVICE_TESTING_IMAGE_ID = "111111111111111111111111"
describe("PictureService Integration Test", () => {
environment.overwrite(<SDKConfig>{apiUrl: "http://localhost:3000", cdnUrl: "http://localhost:4000"})
const service = new PictureService(
PICTURE_SERVICE_TESTING_TOKEN,
PICTURE_SERVICE_TESTING_USER_ID,
PICTURE_SERVICE_TESTING_USER_ID,
new DatabaseMock(),
getClient(false),
getClient(true),
new DatabaseMock()
)
it('should get uploads', async () => {
@@ -27,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.js';
import {ChatService} from '../src/services/chatService.js';
import {DatabaseMock} from '../src/mocks/storage/database.js';
import {faker} from "@faker-js/faker/locale/en";
import {TextChannelServiceService} from '../src/services/textChannelService.js';
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.js';
import {DatabaseMock} from '../src/mocks/storage/database.js';
import {RGB} from '../src/domain/common.schema.js'
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

@@ -1,11 +1,7 @@
import {beforeAll, beforeEach} from 'vitest';
import {environment, SDKConfig} from "../src/core/environment";
import {getClient} from "../src/core/http";
import {environment, SDKConfig} from '../src/core/environment.js';
import {getClient} from '../src/core/http.js';
beforeEach(async () => {
environment.overwrite(<SDKConfig>{
apiUrl: "http://localhost:3000"
})
await getClient().post("v2/reset")
await getClient(false).post("v2/reset")
})

View File

@@ -0,0 +1,14 @@
import {describe, it} from "vitest";
import {WebSocketHandler} from '../src/core/webSocketHandler.js';
const WEBSOCKET_HANDLER_TESTING_USER_ID = "000000000000000000000000"
const WEBSOCKET_HANDLER_TESTING_USER_TOKEN = "testingToken"
describe("WebSocketHandler", () => {
it("should connect", async () => {
await WebSocketHandler.getInstance().connect(
WEBSOCKET_HANDLER_TESTING_USER_ID,
WEBSOCKET_HANDLER_TESTING_USER_TOKEN
)
})
})

View File

@@ -1,12 +1,15 @@
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"module": "NodeNext",
"moduleResolution": "nodenext",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"strict": true,
"skipLibCheck": true,
"outDir": "dist"
"outDir": "dist",
"verbatimModuleSyntax": false,
},
"include": ["src"]
}

View File

@@ -2,6 +2,8 @@ import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
maxWorkers: 1,
minWorkers: 1,
// Top-level configuration (global only)
coverage: { provider: 'v8' },