142 Commits

Author SHA1 Message Date
65583b1564 channels 2026-01-21 22:39:08 +06:00
595354b7f0 Merge pull request 'shareFps' (#9) from shareFps into master
Reviewed-on: #9
2026-01-12 07:23:51 +00:00
Nadar
d08b011596 shareFps 2026-01-12 10:22:56 +03:00
12ce381abd minor update
All checks were successful
Deploy / publish-web (push) Successful in 46s
2025-12-27 02:52:17 +06:00
2d30ac2863 minor update
All checks were successful
Deploy / publish-web (push) Successful in 44s
2025-12-27 02:49:39 +06:00
0f218c1519 button colors 2025-12-27 01:58:01 +06:00
4b1a563850 screen sharing
All checks were successful
Deploy / publish-web (push) Successful in 1m25s
2025-12-27 01:49:25 +06:00
169d43f0db screen sharing 2025-12-27 01:48:49 +06:00
47a464f08f screen sharing
All checks were successful
Deploy / publish-web (push) Successful in 48s
2025-12-26 18:22:22 +06:00
4d5db12e1b screen sharing
Some checks failed
Deploy / deploy (push) Successful in 35s
Deploy / publish-web (push) Failing after 22s
2025-12-26 17:36:30 +06:00
4f59cbcf65 screen sharing
All checks were successful
Deploy / deploy (push) Successful in 35s
2025-12-26 17:21:59 +06:00
3b3f6b6e40 update 2025-12-26 01:44:16 +06:00
461cbc6f83 profile rest
All checks were successful
Deploy / publish-web (push) Successful in 46s
2025-12-26 01:25:14 +06:00
a5cda8828f refactor
All checks were successful
Deploy / deploy (push) Successful in 34s
2025-12-26 01:22:34 +06:00
778f0a5687 refactor
All checks were successful
Deploy / deploy (push) Successful in 35s
2025-12-26 01:18:54 +06:00
2aca9bca08 refactor
All checks were successful
Deploy / deploy (push) Successful in 40s
2025-12-26 01:13:21 +06:00
7ed23df3e9 refactor
All checks were successful
Deploy / deploy (push) Successful in 39s
2025-12-26 01:08:44 +06:00
2ac88f1010 user preferences
All checks were successful
Deploy / publish-web (push) Successful in 2m14s
2025-12-25 22:53:12 +06:00
c2cffd18de user preferences 2025-12-25 22:51:02 +06:00
bf38267c37 user preferences
All checks were successful
Deploy / deploy (push) Successful in 36s
2025-12-25 22:50:56 +06:00
22c5fafb11 user preferences
All checks were successful
Deploy / deploy (push) Successful in 36s
2025-12-25 21:30:30 +06:00
37683c42a9 user preferences
All checks were successful
Deploy / deploy (push) Successful in 35s
2025-12-25 21:27:54 +06:00
2cbc75d7e3 user preferences
All checks were successful
Deploy / deploy (push) Successful in 37s
2025-12-25 21:24:41 +06:00
6721f63d22 user preferences
All checks were successful
Deploy / deploy (push) Successful in 36s
2025-12-25 21:22:43 +06:00
b47643552f user preferences
All checks were successful
Deploy / deploy (push) Successful in 36s
2025-12-25 21:08:14 +06:00
0ab3e15784 cors origins
All checks were successful
Deploy / deploy (push) Successful in 37s
2025-12-25 07:38:07 +06:00
28c64edaf8 update 2025-12-25 07:33:18 +06:00
67a8dc7782 update 2025-12-25 07:30:57 +06:00
43a8b98a6a update 2025-12-25 07:21:45 +06:00
0f9a7e39ce update 2025-12-25 07:21:30 +06:00
8265e2d719 update 2025-12-25 03:51:29 +06:00
4f91309f7f update 2025-12-24 06:29:44 +06:00
bcd457e2d6 update 2025-12-24 06:20:11 +06:00
8eef4fc477 update 2025-12-24 04:39:46 +06:00
614867bd12 update 2025-12-24 04:35:41 +06:00
cdf2bf5952 update 2025-12-24 04:35:13 +06:00
a4bd6705b6 Merge branch 'master' of git.koptilnya.xyz:opti1337/chad
Some checks failed
Deploy / publish-windows (push) Has been cancelled
2025-12-24 04:03:07 +06:00
723048c72a update 2025-12-24 04:02:53 +06:00
06ea0cd488 Обновить .gitea/workflows/deploy-client.yml
Some checks failed
Deploy / publish-windows (push) Failing after 6m3s
2025-12-23 19:52:47 +00:00
007d3ddda7 Update Dockerfile.windows
Some checks failed
Deploy / publish-windows (push) Has been cancelled
2025-12-24 00:42:07 +06:00
33cdaebada Update Dockerfile.windows
Some checks failed
Deploy / publish-windows (push) Failing after 3m0s
2025-12-24 00:25:30 +06:00
9650ea63fc update
Some checks failed
Deploy / publish-windows (push) Failing after 18s
2025-12-24 00:23:29 +06:00
db59b85bd2 udpate
All checks were successful
Deploy / publish-windows (push) Successful in 3m22s
2025-12-24 00:11:03 +06:00
3f6f3b739e Update Dockerfile.windows
Some checks failed
Deploy / publish-windows (push) Failing after 9m54s
2025-12-23 23:43:14 +06:00
b33a896117 Update Dockerfile.windows
Some checks failed
Deploy / publish-windows (push) Failing after 4m17s
2025-12-23 23:14:44 +06:00
01ae1b5011 Update Dockerfile.windows
Some checks failed
Deploy / publish-windows (push) Failing after 24s
2025-12-23 22:53:46 +06:00
0ac69610f2 Update Dockerfile.windows
Some checks failed
Deploy / publish-windows (push) Failing after 26s
2025-12-23 22:52:38 +06:00
c4489b58c9 Update Dockerfile.windows
Some checks failed
Deploy / publish-windows (push) Failing after 23s
2025-12-23 22:50:37 +06:00
c19bef73e0 Update Dockerfile.windows
Some checks failed
Deploy / publish-windows (push) Has been cancelled
2025-12-23 22:44:24 +06:00
c573d2277a update
Some checks failed
Deploy / publish-windows (push) Failing after 23s
2025-12-23 22:42:04 +06:00
3006a82a0f update
Some checks failed
Deploy / publish-windows (push) Failing after 19m11s
2025-12-23 22:03:09 +06:00
a68aa78ae1 update
Some checks failed
Deploy / publish-windows (push) Failing after 16m16s
2025-12-23 21:45:21 +06:00
ba9f51bd5e Update Dockerfile.windows
Some checks failed
Deploy / publish-windows (push) Failing after 18m59s
2025-12-23 21:21:44 +06:00
89a3eac2b9 Update deploy-client.yml
Some checks failed
Deploy / publish-windows (push) Failing after 18s
2025-12-23 20:55:14 +06:00
01de23a036 Merge pull request 'release test' (#7) from release into master
Reviewed-on: #7
2025-12-23 14:51:49 +00:00
aef93ef821 update 2025-12-23 20:50:13 +06:00
72f46df4d1 update
All checks were successful
Deploy / publish-windows (push) Successful in 19s
2025-12-23 00:59:01 +06:00
50c56e87ff Update Dockerfile.windows
All checks were successful
Deploy / publish-windows (push) Successful in 6m11s
2025-12-23 00:37:41 +06:00
a7d65f0e3c Update deploy-client.yml
Some checks failed
Deploy / publish-windows (push) Failing after 5m37s
2025-12-23 00:27:43 +06:00
649b49a732 update
Some checks failed
Deploy / publish-windows (push) Failing after 9s
2025-12-23 00:27:01 +06:00
b887b69997 Update Dockerfile.windows
Some checks failed
Deploy / publish-windows (push) Failing after 2m45s
2025-12-23 00:07:40 +06:00
687a2958c0 Update Dockerfile.windows
Some checks failed
Deploy / publish-windows (push) Failing after 1m52s
2025-12-22 23:35:59 +06:00
a7fe94abec Update Dockerfile.windows
Some checks failed
Deploy / publish-windows (push) Failing after 3m13s
2025-12-22 22:40:13 +06:00
adb539350f Update Dockerfile.windows
Some checks failed
Deploy / publish-windows (push) Failing after 27s
2025-12-22 22:37:41 +06:00
d870b7c1f1 Update Dockerfile.windows
Some checks failed
Deploy / publish-windows (push) Failing after 9m31s
2025-12-22 22:27:02 +06:00
edd5a69cd4 Update Dockerfile.windows
Some checks failed
Deploy / publish-windows (push) Failing after 35s
2025-12-22 22:24:34 +06:00
6343f1de4d Update Dockerfile.windows
Some checks failed
Deploy / publish-windows (push) Failing after 2m21s
2025-12-22 22:19:14 +06:00
3f581ea8e9 Update deploy-client.yml
Some checks failed
Deploy / publish-windows (push) Failing after 2m34s
2025-12-22 22:15:37 +06:00
10bfcf0727 Update Dockerfile.windows
Some checks failed
Deploy / publish-windows (push) Failing after 21s
2025-12-22 22:14:24 +06:00
6b5383ba24 Update Dockerfile.windows
Some checks failed
Deploy / publish-windows (push) Failing after 19s
2025-12-22 22:01:30 +06:00
6b5d669f64 update
Some checks failed
Deploy / publish-windows (push) Failing after 27s
2025-12-22 21:55:12 +06:00
40d66d356b update
Some checks failed
Deploy / publish-tauri (push) Failing after 15s
Deploy / publish-web (push) Successful in 40s
2025-12-22 21:50:33 +06:00
c665c19cf3 update
Some checks failed
Deploy / publish-web (push) Failing after 33s
Deploy / publish-tauri (push) Failing after 50s
2025-12-22 21:47:24 +06:00
b05a7324d6 update
Some checks failed
Deploy / publish-web (push) Successful in 41s
Deploy / publish-tauri (push) Failing after 52s
2025-12-22 21:45:17 +06:00
e8cbf6e146 update
Some checks failed
Deploy / publish-tauri (push) Has been cancelled
Deploy / publish-web (push) Has been cancelled
2025-12-22 21:44:36 +06:00
472fa8d907 update
Some checks failed
Deploy / publish-web (push) Failing after 20s
Deploy / publish-tauri (push) Failing after 18s
2025-12-22 21:40:38 +06:00
ddc43e4b42 update
Some checks failed
Deploy / publish-web (push) Failing after 41s
Deploy / publish-tauri (push) Failing after 25s
2025-12-22 21:36:12 +06:00
8d02eb380d update
Some checks failed
Deploy / publish-web (push) Failing after 13s
Deploy / publish-tauri (push) Failing after 12s
2025-12-22 21:17:29 +06:00
4c70dce568 build
Some checks failed
Deploy / publish-web (push) Failing after 25s
Deploy / publish-tauri (push) Failing after 10s
2025-12-22 21:07:20 +06:00
76f0ec74b5 test publish
Some checks failed
Deploy / deploy (push) Has been cancelled
Deploy / publish-web (push) Successful in 1m43s
Deploy / publish-tauri (push) Has been cancelled
2025-12-22 19:23:06 +06:00
Никита Круглицкий
e3d0106d8f куча говна
All checks were successful
Deploy / deploy (push) Successful in 44s
2025-10-20 06:44:21 +06:00
Никита Круглицкий
e2068dd89a куча говна
All checks were successful
Deploy / deploy (push) Successful in 45s
2025-10-20 06:14:18 +06:00
Никита Круглицкий
a2f845f228 куча говна
All checks were successful
Deploy / deploy (push) Successful in 45s
2025-10-20 06:01:29 +06:00
Никита Круглицкий
1a497d402d куча говна
All checks were successful
Deploy / deploy (push) Successful in 46s
2025-10-20 05:59:06 +06:00
Никита Круглицкий
924bbd4285 куча говна
All checks were successful
Deploy / deploy (push) Successful in 43s
2025-10-20 05:57:08 +06:00
Никита Круглицкий
58d37ee02b куча говна
All checks were successful
Deploy / deploy (push) Successful in 45s
2025-10-20 05:50:27 +06:00
Никита Круглицкий
ba12d413dc куча говна
All checks were successful
Deploy / deploy (push) Successful in 44s
2025-10-20 03:53:29 +06:00
Никита Круглицкий
f525d1afe5 куча говна
All checks were successful
Deploy / deploy (push) Successful in 45s
2025-10-20 03:36:30 +06:00
Никита Круглицкий
75fe5b0b8c куча говна
All checks were successful
Deploy / deploy (push) Successful in 44s
2025-10-20 03:33:07 +06:00
Никита Круглицкий
39e877bbe9 куча говна
All checks were successful
Deploy / deploy (push) Successful in 47s
2025-10-20 03:23:34 +06:00
Никита Круглицкий
fe205531e0 куча говна
All checks were successful
Deploy / deploy (push) Successful in 44s
2025-10-20 03:17:54 +06:00
Никита Круглицкий
1ef0df7afb куча говна
All checks were successful
Deploy / deploy (push) Successful in 44s
2025-10-20 00:20:09 +06:00
Никита Круглицкий
a0bd6f92e9 куча говна
Some checks failed
Deploy / deploy (push) Failing after 25s
2025-10-20 00:19:25 +06:00
Никита Круглицкий
511cb76888 куча говна
Some checks failed
Deploy / deploy (push) Failing after 1m27s
2025-10-20 00:16:59 +06:00
Никита Круглицкий
a433e1982b куча говна
Some checks failed
Deploy / deploy (push) Failing after 9s
2025-10-20 00:11:03 +06:00
Никита Круглицкий
ec67be8aa6 куча говна
All checks were successful
Deploy / deploy (push) Successful in 4m32s
2025-10-20 00:10:13 +06:00
Никита Круглицкий
31460598ba server
All checks were successful
Deploy / deploy (push) Successful in 24s
2025-10-16 22:23:30 +06:00
Никита Круглицкий
4c368458a9 server
All checks were successful
Deploy / deploy (push) Successful in 44s
2025-10-09 23:56:49 +06:00
Никита Круглицкий
aa31e7ad0c server
Some checks failed
Deploy / deploy (push) Failing after 8s
2025-10-09 23:52:08 +06:00
Никита Круглицкий
7b4be60099 server
Some checks failed
Deploy / deploy (push) Failing after 9s
2025-10-09 23:51:17 +06:00
Никита Круглицкий
d5915a9d40 fix migrations
All checks were successful
Deploy / deploy (push) Successful in 45s
2025-10-09 23:19:26 +06:00
Никита Круглицкий
7d4fdfb2e6 add migrations
All checks were successful
Deploy / deploy (push) Successful in 40s
2025-10-09 22:58:48 +06:00
Никита Круглицкий
ad71b15f81 update
All checks were successful
Deploy / deploy (push) Successful in 20s
2025-10-09 22:54:34 +06:00
Никита Круглицкий
a49fe5fc53 update
Some checks failed
Deploy / deploy (push) Failing after 41s
2025-10-09 22:53:20 +06:00
Никита Круглицкий
8f2b9861c2 update
All checks were successful
Deploy / deploy (push) Successful in 49s
2025-10-09 22:47:51 +06:00
Никита Круглицкий
adcc5b65a6 update
All checks were successful
Deploy / deploy (push) Successful in 19s
2025-10-09 22:40:28 +06:00
Никита Круглицкий
b8a21a1451 update
Some checks failed
Deploy / deploy (push) Failing after 13s
2025-10-09 22:37:02 +06:00
Никита Круглицкий
c0969c4494 update
Some checks failed
Deploy / deploy (push) Failing after 15s
2025-10-09 22:35:05 +06:00
Никита Круглицкий
bb48e52a99 update
Some checks failed
Deploy / deploy (push) Failing after 4m2s
2025-10-09 22:29:42 +06:00
Никита Круглицкий
6ada2c3fbd update
All checks were successful
Deploy / deploy (push) Successful in 43s
2025-10-09 06:23:13 +06:00
Никита Круглицкий
e2064dba6c front update
All checks were successful
Deploy / deploy (push) Successful in 38s
2025-10-09 06:14:35 +06:00
Никита Круглицкий
fc43c8df96 server update
Some checks failed
Deploy / deploy (push) Failing after 15s
2025-10-09 06:14:24 +06:00
Никита Круглицкий
6b6a66eef4 server update
All checks were successful
Deploy / deploy (push) Successful in 43s
2025-10-09 06:09:28 +06:00
Никита Круглицкий
5ca3a47d40 front update
All checks were successful
Deploy / deploy (push) Successful in 39s
2025-10-09 04:48:02 +06:00
Никита Круглицкий
484e61f337 front update
All checks were successful
Deploy / deploy (push) Successful in 40s
2025-10-09 04:45:09 +06:00
Никита Круглицкий
196aa36970 front update
All checks were successful
Deploy / deploy (push) Successful in 43s
2025-10-09 04:42:34 +06:00
Никита Круглицкий
7cd4ff72d4 #4
All checks were successful
Deploy / deploy (push) Successful in 45s
2025-10-07 06:13:54 +06:00
Никита Круглицкий
1339a48da0 #4 2025-10-07 06:13:42 +06:00
Никита Круглицкий
ea55b99116 #4
Some checks failed
Deploy / deploy (push) Failing after 7s
2025-10-07 05:39:40 +06:00
Никита Круглицкий
fbbb2b9b8e #4
All checks were successful
Deploy / deploy (push) Successful in 44s
2025-10-06 23:14:32 +06:00
Никита Круглицкий
87b4c1489e #4
All checks were successful
Deploy / deploy (push) Successful in 43s
2025-10-06 23:03:06 +06:00
Никита Круглицкий
d224ccc52e #4
All checks were successful
Deploy / deploy (push) Successful in 45s
2025-10-06 22:52:47 +06:00
Никита Круглицкий
fb5b42e9db front update
All checks were successful
Deploy / deploy (push) Successful in 1m38s
2025-10-05 21:43:28 +06:00
Никита Круглицкий
1ea3b2d376 #4
All checks were successful
Deploy / deploy (push) Successful in 44s
2025-10-05 21:36:28 +06:00
Никита Круглицкий
c370a455bd #4
All checks were successful
Deploy / deploy (push) Successful in 42s
2025-10-05 07:29:41 +06:00
Никита Круглицкий
189404e22c #4
All checks were successful
Deploy / deploy (push) Successful in 44s
2025-10-04 23:59:48 +06:00
Никита Круглицкий
80c53a079b #4
All checks were successful
Deploy / deploy (push) Successful in 44s
2025-10-04 22:44:55 +06:00
Никита Круглицкий
e6a274cc9d #6
All checks were successful
Deploy / deploy (push) Successful in 1m6s
2025-10-04 02:53:42 +06:00
Никита Круглицкий
c365f3e885 #4
All checks were successful
Deploy / deploy (push) Successful in 44s
2025-10-03 02:36:38 +06:00
Никита Круглицкий
7cecf0641f #4
All checks were successful
Deploy / deploy (push) Successful in 29s
2025-10-03 02:05:50 +06:00
Никита Круглицкий
423fa3b62a #4
All checks were successful
Deploy / deploy (push) Successful in 33s
2025-10-03 02:01:12 +06:00
Никита Круглицкий
0a8015528d #4
All checks were successful
Deploy / deploy (push) Successful in 30s
2025-10-03 01:53:36 +06:00
Никита Круглицкий
c9feff7224 №4
All checks were successful
Deploy / deploy (push) Successful in 30s
2025-10-02 23:57:54 +06:00
Никита Круглицкий
7294e56778 №4
All checks were successful
Deploy / deploy (push) Successful in 22s
2025-10-02 23:55:04 +06:00
Никита Круглицкий
8e19f55dc0 №4
All checks were successful
Deploy / deploy (push) Successful in 3m59s
2025-10-02 23:39:59 +06:00
Никита Круглицкий
a01f8857ab #2 update
All checks were successful
Deploy / deploy (push) Successful in 49s
2025-10-02 04:38:33 +06:00
Никита Круглицкий
e66493b733 #2 update
All checks were successful
Deploy / deploy (push) Successful in 45s
2025-10-02 04:33:29 +06:00
Никита Круглицкий
496e2ec4ef #2 update
All checks were successful
Deploy / deploy (push) Successful in 44s
2025-10-02 04:29:43 +06:00
Никита Круглицкий
c442e1aa47 #2 #3 update
All checks were successful
Deploy / deploy (push) Successful in 48s
2025-10-02 04:22:10 +06:00
Никита Круглицкий
6d4076969e #2 update
All checks were successful
Deploy / deploy (push) Successful in 23s
2025-10-02 03:54:35 +06:00
Никита Круглицкий
b24a30314f #2 update
All checks were successful
Deploy / deploy (push) Successful in 13s
2025-10-02 03:47:50 +06:00
Никита Круглицкий
aa257565fb #2 update 2025-10-02 03:47:30 +06:00
125 changed files with 32565 additions and 1991 deletions

View File

@@ -1,15 +1,55 @@
name: Deploy
on:
workflow_dispatch:
push:
branches:
- master
paths:
- '.gitea/workflows/deploy-client.yml'
- 'client/**'
tags:
- "v[0-9]+.[0-9]+.[0-9]+"
# paths:
# - ".gitea/workflows/deploy-client.yml"
# - "client/**"
jobs:
deploy:
# publish-windows:
# runs-on: ubuntu-latest
#
# steps:
# - name: Keyscan
# run: |
# ssh-keyscan git.koptilnya.xyz >> ~/.ssh/known_hosts
#
# - name: Checkout
# uses: actions/checkout@v4
# with:
# ssh-key: ${{ secrets.SSH_PRIVATE_KEY }}
# ssh-strict: false
# persist-credentials: false
#
# - name: Build
# run: |
# docker build \
# -t chad-client-windows-builder \
# -f ./client/Dockerfile.windows \
# ./client \
# --build-arg COMMIT_SHA=${{ gitea.sha }} \
# --build-arg API_BASE_URL=${{ vars.API_BASE_URL }} \
# --build-arg TAURI_SIGNING_PRIVATE_KEY=${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
#
# docker create --name chad-client-windows-container chad-client-windows-builder
# mkdir -p artifacts
# docker cp chad-client-windows-container:/artifacts artifacts/
# docker rm chad-client-windows-container
# ls -la artifacts
#
# - name: Publish
# uses: akkuman/gitea-release-action@v1
# with:
# files: |
# artifacts/**
# draft: true
publish-web:
runs-on: ubuntu-latest
steps:
@@ -25,7 +65,7 @@ jobs:
persist-credentials: false
- name: Build
run: docker build -t chad-client ./client --build-arg COMMIT_SHA=${{ gitea.sha }}
run: docker build -t chad-client -f ./client/Dockerfile.web ./client --build-arg COMMIT_SHA=${{ gitea.sha }} --build-arg API_BASE_URL=${{ vars.API_BASE_URL }}
- name: Stop old container
run: docker rm -f chad-client || true

View File

@@ -24,6 +24,9 @@ jobs:
ssh-strict: false
persist-credentials: false
# - name: Backup DB
# run: cp /home/koptilnya/services/chad/data/database.db /home/koptilnya/services/chad/database-$(date+"%d-%m-%Y").db
- name: Build
run: docker build -t chad-server ./server
@@ -35,7 +38,8 @@ jobs:
docker run -d \
--name chad-server \
--network traefik \
-p 20000-40000:20000-40000/udp \
--volume /home/koptilnya/services/chad/data:/app/data \
-p 40000-40100:40000-40100/udp \
--label "traefik.enable=true" \
--label "traefik.http.routers.chad-server.rule=Host(\`api.koptilnya.xyz\`) && PathPrefix(\`/chad\`)" \
--label "traefik.http.routers.chad-server.entrypoints=websecure" \

2
client/.gitattributes vendored Normal file
View File

@@ -0,0 +1,2 @@
/.yarn/releases/** binary
/.yarn/plugins/** binary

6
client/.gitignore vendored
View File

@@ -23,9 +23,15 @@ logs
.env.*
!.env.example
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
scripts/release.ps1
.tauri
updater.json

Binary file not shown.

942
client/.yarn/releases/yarn-4.12.0.cjs vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1 +1,3 @@
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.12.0.cjs

View File

@@ -2,16 +2,17 @@ FROM node:lts-alpine AS builder
WORKDIR /app
RUN corepack enable
RUN yarn set version stable
COPY package.json yarn.lock .yarnrc.yml ./
COPY .yarn ./.yarn
RUN yarn install
RUN yarn install --immutable
COPY . .
ARG COMMIT_SHA=unknown
ENV COMMIT_SHA=$COMMIT_SHA
ARG API_BASE_URL
ENV COMMIT_SHA=$COMMIT_SHA \
API_BASE_URL=$API_BASE_URL
RUN yarn generate

46
client/Dockerfile.windows Normal file
View File

@@ -0,0 +1,46 @@
# === Build ===
FROM node:lts AS builder
WORKDIR /app
COPY package.json yarn.lock .yarnrc.yml ./
COPY .yarn ./.yarn
RUN yarn install --immutable
COPY . .
ARG COMMIT_SHA=unknown
ARG API_BASE_URL
ARG TAURI_SIGNING_PRIVATE_KEY
ENV COMMIT_SHA=$COMMIT_SHA \
API_BASE_URL=$API_BASE_URL \
TAURI_SIGNING_PRIVATE_KEY=$TAURI_SIGNING_PRIVATE_KEY \
TAURI_SIGNING_PRIVATE_KEY_PASSWORD=
RUN apt update && apt install -y --no-install-recommends \
nsis \
clang \
lld \
llvm \
&& rm -rf /var/lib/apt/lists/*
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
ENV PATH=/root/.cargo/bin:$PATH
RUN rustup target add x86_64-pc-windows-msvc
RUN cargo install --locked cargo-xwin
RUN yarn tauri build --runner cargo-xwin --target x86_64-pc-windows-msvc
RUN node scripts/generate-updater.mjs
# === Artifacts ===
FROM scratch AS artifacts
COPY --from=builder /app/updater.json ./artifacts
COPY --from=builder /app/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/ ./artifacts
CMD ["true"]

View File

@@ -1,13 +1,11 @@
<template>
<h1>yo</h1>
<pre>{{ connected }}</pre>
<audio v-for="(s, i) in streams" :key="i" :ref="(el) => onAudioRef(el, s)" autoplay controls />
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
<PrimeToast position="bottom-center" />
</template>
<script lang="ts" setup>
const { connected, streams } = useMediasoup()
function onAudioRef(ref: Element, stream: MediaStream) {
(ref as HTMLAudioElement).srcObject = stream
}
<script setup lang="ts">
const route = useRoute()
</script>

View File

@@ -0,0 +1,47 @@
html,
body {
font-family: 'Inter', sans-serif;
font-size: 16px;
height: 100%;
overflow: hidden;
background-image: radial-gradient(var(--p-surface-700), var(--p-surface-800));
background-size: 200% 200%;
background-position: left -100% top -100%;
}
#__nuxt {
--p-scrollpanel-bar-size: 5px;
--p-scrollpanel-bar-background: var(--p-surface-950);
--p-divider-horizontal-margin: 2rem 0 1rem;
height: 100%;
}
.p-divider {
&:first-child {
--p-divider-horizontal-margin: 1rem 0 1rem;
}
}
.p-scrollpanel-bar-y {
translate: -0.25rem;
}
.p-select-overlay {
/* Force dropdown width to match computed min-width from PrimeVue internals. */
width: 0 !important;
}
.p-select-label {
width: 0 !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
}
.p-select-option-label {
min-width: 0 !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
}

View File

@@ -0,0 +1 @@
@import 'primeicons/primeicons.css';

View File

@@ -0,0 +1,2 @@
@import "tailwindcss";
@import "tailwindcss-primeui";

32
client/app/components.d.ts vendored Normal file
View File

@@ -0,0 +1,32 @@
/* eslint-disable */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
// biome-ignore lint: disable
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
PrimeAvatar: typeof import('primevue/avatar')['default']
PrimeBadge: typeof import('primevue/badge')['default']
PrimeButton: typeof import('primevue/button')['default']
PrimeButtonGroup: typeof import('primevue/buttongroup')['default']
PrimeCard: typeof import('primevue/card')['default']
PrimeDivider: typeof import('primevue/divider')['default']
PrimeFloatLabel: typeof import('primevue/floatlabel')['default']
PrimeInputText: typeof import('primevue/inputtext')['default']
PrimePassword: typeof import('primevue/password')['default']
PrimeScrollPanel: typeof import('primevue/scrollpanel')['default']
PrimeSelect: typeof import('primevue/select')['default']
PrimeSelectButton: typeof import('primevue/selectbutton')['default']
PrimeSlider: typeof import('primevue/slider')['default']
PrimeToast: typeof import('primevue/toast')['default']
PrimeToggleSwitch: typeof import('primevue/toggleswitch')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
export interface ComponentCustomProperties {
Tooltip: typeof import('primevue/tooltip')['default']
}
}

View File

@@ -0,0 +1,129 @@
<template>
<div
class="overflow-hidden rounded-xl transition-[background-color]"
:class="{
'hover:bg-surface-800 cursor-pointer': !isMe,
'bg-surface-800': expanded,
}"
>
<div class="p-3 flex items-center gap-3" @click="toggleExpand">
<PrimeAvatar size="small">
<template #icon>
<User :size="20" />
</template>
</PrimeAvatar>
<div class="flex-1 overflow-hidden text-sm leading-5 font-medium text-color whitespace-nowrap overflow-ellipsis">
{{ client.displayName || client.username }}
</div>
<div class="flex align-center gap-1">
<PrimeBadge v-if="!!shareConsumer" v-tooltip.top="'Watch'" severity="success" value="Streaming" size="small" @click.stop="watchStream" />
<PrimeBadge v-if="premuted" severity="danger" value="Muted" size="small" />
<PrimeBadge v-else-if="client.outputMuted" severity="info" value="No sound" size="small" />
<PrimeBadge v-else-if="inputMuted" severity="info" value="Muted" size="small" />
<PrimeBadge v-if="isMe" severity="secondary" value="You" size="small" class="shrink-0" />
</div>
<Component :is="expanded ? ChevronUp : ChevronDown" v-if="!isMe" :size="20" class="text-muted-color" />
</div>
<CollapseTransition v-if="!isMe">
<div v-if="expanded">
<div class="px-3 pb-3">
<div class="flex justify-between text-sm mb-3">
<span>Volume</span>
<span>{{ volume }}</span>
</div>
<PrimeSlider v-model="volume" :min="0" :max="1000" :step="volume < 200 ? 5 : 25" />
<div class="mt-3 flex gap-1 justify-end">
<PrimeButton size="small" variant="text" @click="premuted = !premuted">
{{ premuted ? 'Unmute' : 'Mute' }}
</PrimeButton>
</div>
</div>
</div>
</CollapseTransition>
</div>
</template>
<script setup lang="ts">
import type { ChadClient } from '#shared/types'
import { useLocalStorage } from '@vueuse/core'
import { ChevronDown, ChevronUp, User } from 'lucide-vue-next'
import CollapseTransition from '~/components/CollapseTransition.vue'
const props = defineProps<{
client: ChadClient
}>()
const { outputMuted } = useApp()
const { consumers: allConsumers, micProducer } = useMediasoup()
const { me } = useClients()
const { show } = useFullscreenVideo()
const expanded = ref(false)
const volume = useLocalStorage<number>(computed(() => `CLIENT_VOLUME_${props.client.userId}`), 100, { writeDefaults: false })
const premuted = useLocalStorage<boolean>(computed(() => `CLIENT_PREMUTED_${props.client.userId}`), false, { writeDefaults: false })
const isMe = computed(() => {
return me.value && props.client.userId === me.value.userId
})
const consumers = computed(() => {
return allConsumers.value.values().filter(consumer => consumer.appData.socketId === props.client.socketId).toArray()
})
const audioConsumer = computed(() => {
return consumers.value.find(consumer => consumer.track.kind === 'audio')
})
const videoConsumers = computed(() => {
return consumers.value.filter(consumer => consumer.track.kind === 'video')
})
const shareConsumer = computed(() => {
return videoConsumers.value.find(consumer => consumer.appData.source === 'share')
})
const audioTrack = computed(() => {
return audioConsumer.value?.track
})
const audioConsumerPaused = ref(false)
const inputMuted = computed(() => {
if (isMe.value)
return micProducer.value?.paused ?? false
return premuted.value || audioConsumerPaused.value
})
watch(allConsumers, () => {
audioConsumerPaused.value = audioConsumer.value?.paused ?? false
})
const { setGain } = useAudioContext(audioTrack)
watchEffect(() => {
setGain((outputMuted.value || premuted.value) ? 0 : (volume.value * 0.01))
})
function toggleExpand() {
if (isMe.value)
return
expanded.value = !expanded.value
}
function watchStream() {
if (!shareConsumer.value)
return
show(new MediaStream([shareConsumer.value.track]))
}
</script>

View File

@@ -0,0 +1,118 @@
<template>
<Transition name="collapse-transition" v-on="bindings">
<slot />
</Transition>
</template>
<script lang="ts" setup>
import type { RendererElement } from 'vue'
defineOptions({
name: 'CollapseTransition',
})
const emit = defineEmits<{
expanded: []
collapsed: []
}>()
const bindings = {
beforeEnter(el: RendererElement) {
if (!el.dataset)
el.dataset = {}
el.dataset.oldPaddingTop = el.style.paddingTop
el.dataset.oldPaddingBottom = el.style.paddingBottom
el.dataset.elExistsHeight = el.style.height ?? undefined
el.style.maxHeight = 0
el.style.paddingTop = 0
el.style.paddingBottom = 0
},
enter(el: RendererElement) {
requestAnimationFrame(() => {
el.dataset.oldOverflow = el.style.overflow
if (el.dataset.elExistsHeight) {
el.style.maxHeight = el.dataset.elExistsHeight
}
else if (el.scrollHeight !== 0) {
el.style.maxHeight = `${el.scrollHeight}px`
}
else {
el.style.maxHeight = 0
}
el.style.paddingTop = el.dataset.oldPaddingTop
el.style.paddingBottom = el.dataset.oldPaddingBottom
el.style.overflow = 'hidden'
})
},
afterEnter(el: RendererElement) {
el.style.maxHeight = ''
el.style.overflow = el.dataset.oldOverflow
emit('expanded')
},
enterCancelled(el: RendererElement) {
reset(el)
},
beforeLeave(el: RendererElement) {
if (!el.dataset)
el.dataset = {}
el.dataset.oldPaddingTop = el.style.paddingTop
el.dataset.oldPaddingBottom = el.style.paddingBottom
el.dataset.oldOverflow = el.style.overflow
el.style.maxHeight = `${el.scrollHeight}px`
el.style.overflow = 'hidden'
},
leave(el: RendererElement) {
if (el.scrollHeight !== 0) {
el.style.maxHeight = 0
el.style.paddingTop = 0
el.style.paddingBottom = 0
}
},
afterLeave(el: RendererElement) {
reset(el)
emit('collapsed')
},
leaveCancelled(el: RendererElement) {
reset(el)
},
}
function reset(el: RendererElement) {
el.style.maxHeight = ''
el.style.overflow = el.dataset.oldOverflow
el.style.paddingTop = el.dataset.oldPaddingTop
el.style.paddingBottom = el.dataset.oldPaddingBottom
}
</script>
<style lang="scss">
.collapse-transition {
transition-property: height, padding-top, padding-bottom;
transition-timing-function: var(--default-transition-timing-function, cubic-bezier(0.4, 0, 0.2, 1));
transition-duration: var(--default-transition-duration, 150ms);
}
.collapse-transition-leave-active,
.collapse-transition-enter-active {
transition-property: opacity, max-height, padding-top, padding-bottom;
transition-timing-function: var(--default-transition-timing-function, cubic-bezier(0.4, 0, 0.2, 1));
transition-duration: var(--default-transition-duration, 150ms);
}
.collapse-transition-leave-to,
.collapse-transition-enter-from {
opacity: 0;
}
</style>

View File

@@ -0,0 +1,130 @@
import { getVersion } from '@tauri-apps/api/app'
import { computedAsync, createGlobalState } from '@vueuse/core'
import { useClients } from '~/composables/use-clients'
export const useApp = createGlobalState(() => {
const { clients } = useClients()
const mediasoup = useMediasoup()
const signaling = useSignaling()
const toast = useToast()
const ready = ref(false)
const isTauri = computed(() => '__TAURI_INTERNALS__' in window)
const commitSha = __COMMIT_SHA__
const version = computedAsync(() => {
if (import.meta.dev) {
return 'dev'
}
else if (isTauri.value) {
return getVersion()
}
else {
return 'web'
}
}, '-')
const inputMuted = computed(() => {
return !!mediasoup.micProducer.value?.paused
})
const previousInputMuted = ref(inputMuted.value)
const outputMuted = ref(false)
const sharingEnabled = computed(() => {
return !!mediasoup.shareProducer.value
})
async function muteInput() {
if (inputMuted.value)
return
await mediasoup.pauseProducer('microphone')
toast.add({ severity: 'info', summary: 'Microphone muted', closable: false, life: 1000 })
}
async function unmuteInput() {
if (!inputMuted.value)
return
if (outputMuted.value) {
await unmuteOutput()
}
await mediasoup.resumeProducer('microphone')
toast.add({ severity: 'info', summary: 'Microphone activated', closable: false, life: 1000 })
}
async function toggleInput() {
if (inputMuted.value)
await unmuteInput()
else
await muteInput()
}
async function muteOutput() {
if (outputMuted.value)
return
outputMuted.value = true
previousInputMuted.value = inputMuted.value
await muteInput()
await signaling.socket.value?.emitWithAck('updateClient', {
outputMuted: true,
})
toast.add({ severity: 'info', summary: 'Sound muted', closable: false, life: 1000 })
}
async function unmuteOutput() {
outputMuted.value = false
if (!previousInputMuted.value)
await unmuteInput()
await signaling.socket.value?.emitWithAck('updateClient', {
outputMuted: false,
})
toast.add({ severity: 'info', summary: 'Sound resumed', closable: false, life: 1000 })
}
async function toggleOutput() {
if (outputMuted.value)
await unmuteOutput()
else
await muteOutput()
}
async function toggleShare() {
if (!mediasoup.shareProducer.value) {
await mediasoup.enableShare()
}
else {
await mediasoup.disableProducer('share')
}
}
return {
ready,
clients,
inputMuted,
muteInput,
unmuteInput,
toggleInput,
outputMuted,
muteOutput,
unmuteOutput,
toggleOutput,
version,
isTauri,
commitSha,
toggleShare,
sharingEnabled,
}
})

View File

@@ -0,0 +1,62 @@
import { useEventListener } from '@vueuse/core'
export default function useAudioContext(audioTrack: Ref<MediaStreamTrack | undefined>) {
const ctx = new (window.AudioContext || window.webkitAudioContext)()
const stream = new MediaStream()
const audioEl = new Audio()
const sourceNode = shallowRef<MediaStreamAudioSourceNode>()
const gainNode = ctx.createGain()
watch(audioTrack, async (track, prevTrack) => {
if (prevTrack)
stream.removeTrack(prevTrack)
if (!track)
return
stream.addTrack(track)
if (!audioEl.srcObject) {
audioEl.srcObject = stream
audioEl.muted = true
}
sourceNode.value = ctx.createMediaStreamSource(stream)
await connect()
}, { immediate: true })
useEventListener(document, 'click', async () => {
if (ctx.state === 'suspended') {
await ctx.resume()
}
await connect()
}, { once: true })
onScopeDispose(() => {
audioEl.pause()
audioEl.srcObject = null
ctx.close()
})
async function connect() {
if (!sourceNode.value || ctx.state === 'suspended')
return
sourceNode.value.connect(gainNode)
gainNode.connect(ctx.destination)
ctx.resume()
}
function setGain(value: number) {
gainNode.gain.value = value
}
return {
setGain,
}
}

View File

@@ -0,0 +1,69 @@
import chadApi from '#shared/chad-api'
import { createGlobalState } from '@vueuse/core'
interface Me {
id: string
username: string
displayName: string
}
export const useAuth = createGlobalState(() => {
const me = shallowRef<Me>()
function setMe(value: Me | undefined): void {
me.value = value
}
async function login(username: string, password: string): Promise<void> {
try {
const result = await chadApi<Me>('/login', {
method: 'POST',
body: {
username,
password,
},
})
setMe(result)
await navigateTo('/')
}
catch {}
}
async function register(username: string, password: string): Promise<void> {
try {
const result = await chadApi<Me>('/register', {
method: 'POST',
body: {
username,
password,
},
})
setMe(result)
await navigateTo('/')
}
catch {}
}
async function logout(): Promise<void> {
try {
await chadApi('/logout', { method: 'POST' })
setMe(undefined)
await navigateTo({ name: 'Login' })
}
catch {}
}
return {
me: readonly(me),
setMe,
login,
register,
logout,
}
})

View File

@@ -0,0 +1,66 @@
import type { ChadClient, UpdatedClient } from '#shared/types'
import { createGlobalState } from '@vueuse/core'
export const useClients = createGlobalState(() => {
const auth = useAuth()
const signaling = useSignaling()
const toast = useToast()
const clients = shallowRef<ChadClient[]>([])
const me = computed(() => clients.value.find(client => client.userId === auth.me.value?.id))
watch(signaling.socket, (socket) => {
if (!socket)
return
socket.on('clientChanged', (clientId: ChadClient['socketId'], updatedClient: UpdatedClient) => {
const client = getClient(clientId)
updateClient(clientId, updatedClient)
if (client && client.displayName !== updatedClient.displayName)
toast.add({ severity: 'info', summary: `${client.displayName} is now ${updatedClient.displayName}`, closable: false, life: 1000 })
})
socket.on('disconnect', () => {
clients.value = []
})
})
function getClient(clientId: ChadClient['socketId']) {
return clients.value.find(client => client.socketId === clientId)
}
function addClient(...client: ChadClient[]) {
clients.value.push(...client)
triggerRef(clients)
}
function removeClient(clientId: ChadClient['socketId']) {
clients.value = clients.value.filter(client => client.socketId !== clientId)
}
function updateClient(clientId: ChadClient['socketId'], updatedClient: UpdatedClient) {
const clientIdx = clients.value.findIndex(client => client.socketId === clientId)
if (clientIdx === -1)
return
clients.value[clientIdx] = {
...clients.value[clientIdx],
...updatedClient,
} as ChadClient
triggerRef(clients)
}
return {
me,
clients,
getClient,
addClient,
removeClient,
updateClient,
}
})

View File

@@ -0,0 +1,28 @@
import { createGlobalState, useDevicesList } from '@vueuse/core'
export const useDevices = createGlobalState(() => {
const {
ensurePermissions,
permissionGranted,
videoInputs,
audioInputs,
audioOutputs,
} = useDevicesList()
async function getShareStream(fps = 30) {
return navigator.mediaDevices.getDisplayMedia({
audio: false,
video: {
displaySurface: 'monitor',
frameRate: { max: fps },
},
})
}
return {
videoInputs: computed<MediaDeviceInfo[]>(() => JSON.parse(JSON.stringify(videoInputs.value))),
audioInputs: computed<MediaDeviceInfo[]>(() => JSON.parse(JSON.stringify(audioInputs.value))),
audioOutputs: computed<MediaDeviceInfo[]>(() => JSON.parse(JSON.stringify(audioOutputs.value))),
getShareStream,
}
})

View File

@@ -0,0 +1,65 @@
import { createGlobalState, useEventListener } from '@vueuse/core'
export const useFullscreenVideo = createGlobalState(() => {
const videoEl = shallowRef<HTMLVideoElement>()
const visible = computed(() => !!videoEl.value)
async function show(stream: MediaStream) {
if (videoEl.value) {
videoEl.value.srcObject = stream
}
else {
const el = document.createElement('video')
el.srcObject = stream
el.autoplay = true
el.playsInline = true
el.controls = false
el.muted = true
// el.style.position = 'fixed'
// el.style.top = '0'
// el.style.left = '0'
// el.style.width = '1px'
// el.style.height = '1px'
// el.style.opacity = '0'
// el.style.pointerEvents = 'none'
document.body.appendChild(el)
videoEl.value = el
}
stream.getTracks().forEach(t =>
t.addEventListener('ended', hide),
)
videoEl.value.addEventListener('ended', hide)
await videoEl.value.requestFullscreen()
}
function hide() {
if (!videoEl.value)
return
(videoEl.value.srcObject as MediaStream).getTracks().forEach(t =>
t.removeEventListener('ended', hide),
)
videoEl.value.removeEventListener('ended', hide)
videoEl.value?.remove()
videoEl.value = undefined
}
useEventListener(document, 'fullscreenchange', () => {
if (!document.fullscreenElement && videoEl.value) {
videoEl.value?.remove()
videoEl.value = undefined
}
})
return {
visible,
show,
hide,
}
})

View File

@@ -1,161 +1,439 @@
import type { Socket } from 'socket.io-client'
import { createGlobalState } from '@vueuse/core'
import type { MediaKind, ProducerOptions } from 'mediasoup-client/types'
import { createSharedComposable } from '@vueuse/core'
import * as mediasoupClient from 'mediasoup-client'
import { io } from 'socket.io-client'
import { useDevices } from '~/composables/use-devices'
import { usePreferences } from '~/composables/use-preferences'
import { useSignaling } from '~/composables/use-signaling'
type ProducerType = 'microphone' | 'camera' | 'share'
const ICE_SERVERS: RTCIceServer[] = [
// {
// urls: 'stun:stun.relay.metered.ca:80',
// },
// {
// urls: 'turn:global.relay.metered.ca:80',
// username: '4cad09c0111f423f5283814c',
// credential: 'B1dO8AGehex4o3pt',
// },
// {
// urls: 'turn:global.relay.metered.ca:80?transport=tcp',
// username: '4cad09c0111f423f5283814c',
// credential: 'B1dO8AGehex4o3pt',
// },
// {
// urls: 'turn:global.relay.metered.ca:443',
// username: '4cad09c0111f423f5283814c',
// credential: 'B1dO8AGehex4o3pt',
// },
{
urls: 'turns:global.relay.metered.ca:443?transport=tcp',
username: '4cad09c0111f423f5283814c',
credential: 'B1dO8AGehex4o3pt',
},
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun.l.google.com:5349' },
{ urls: 'stun:stun1.l.google.com:3478' },
{ urls: 'stun:stun1.l.google.com:5349' },
{ urls: 'stun:stun2.l.google.com:19302' },
{ urls: 'stun:stun2.l.google.com:5349' },
{ urls: 'stun:stun3.l.google.com:3478' },
{ urls: 'stun:stun3.l.google.com:5349' },
{ urls: 'stun:stun4.l.google.com:19302' },
{ urls: 'stun:stun4.l.google.com:5349' },
]
export const useMediasoup = createGlobalState(() => {
const socket: Socket = io('https://api.koptilnya.xyz', {
path: '/chad/ws/',
transports: ['websocket'],
})
export const useMediasoup = createSharedComposable(() => {
const toast = useToast()
const initializing = ref(false)
const connected = ref(false)
const streams = shallowRef<MediaStream[]>([])
let device: mediasoupClient.Device
let sendTransport: mediasoupClient.types.Transport
let recvTransport: mediasoupClient.types.Transport
const signaling = useSignaling()
const { addClient, removeClient } = useClients()
const preferences = usePreferences()
const { getShareStream } = useDevices()
socket.on('newProducer', async ({ producerId }) => {
const params = await socket.emitWithAck('consume', {
producerId,
transportId: recvTransport.id,
rtpCapabilities: device.rtpCapabilities,
})
const device = shallowRef<mediasoupClient.Device>()
const rtpCapabilities = shallowRef<mediasoupClient.types.RtpCapabilities>()
const sendTransport = shallowRef<mediasoupClient.types.Transport>()
const recvTransport = shallowRef<mediasoupClient.types.Transport>()
if (params?.error) {
console.error('consume error:', params.error)
const micProducer = shallowRef<mediasoupClient.types.Producer>()
const cameraProducer = shallowRef<mediasoupClient.types.Producer>()
const shareProducer = shallowRef<mediasoupClient.types.Producer>()
const consumers = shallowRef<Map<string, mediasoupClient.types.Consumer>>(new Map())
const producers = shallowRef<Map<string, mediasoupClient.types.Producer>>(new Map())
watch(signaling.socket, (socket) => {
if (!socket)
return
}
const consumer = await recvTransport.consume(params)
const stream = new MediaStream([consumer.track])
socket.on('authenticated', async () => {
if (!signaling.socket.value)
return
streams.value.push(stream)
triggerRef(streams)
})
device.value = new mediasoupClient.Device()
rtpCapabilities.value = await signaling.socket.value.emitWithAck('getRtpCapabilities')
async function loadDevice() {
device = new mediasoupClient.Device()
const rtpCapabilities = await socket.emitWithAck('getRtpCapabilities')
await device.load({ routerRtpCapabilities: rtpCapabilities })
}
await device.value.load({ routerRtpCapabilities: rtpCapabilities.value! })
async function createSendTransport() {
const params = await socket.emitWithAck('createTransport')
sendTransport = device.createSendTransport({
...params,
// Send transport
{
const transportInfo = await signaling.socket.value.emitWithAck('createTransport', { producing: true, consuming: false })
sendTransport.value = device.value.createSendTransport({
...transportInfo,
iceServers: [
...ICE_SERVERS,
...(params.iceServers ?? []),
...(transportInfo.iceServers ?? []),
],
})
sendTransport.on('connect', async ({ dtlsParameters }, callback, errback) => {
sendTransport.value.on('connect', async ({ dtlsParameters }, callback, errback) => {
try {
await socket.emitWithAck('connectTransport', {
transportId: sendTransport.id,
await signaling.socket.value!.emitWithAck('connectTransport', {
transportId: sendTransport.value!.id,
dtlsParameters,
})
callback()
}
catch (err) {
errback(err)
catch (error) {
if (error instanceof Error) {
errback(error)
}
}
})
sendTransport.on('produce', async ({ kind, rtpParameters }, callback, errback) => {
sendTransport.value.on('produce', async ({ kind, rtpParameters, appData }, callback, errback) => {
try {
const { id } = await socket.emitWithAck('produce', {
transportId: sendTransport.id,
const { id } = await signaling.socket.value!.emitWithAck('produce', {
transportId: sendTransport.value!.id,
kind,
rtpParameters,
appData,
})
callback({ id })
}
catch (err) {
errback(err)
catch (error) {
if (error instanceof Error) {
errback(error)
}
}
})
}
async function publishMic() {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
const track = stream.getAudioTracks()[0]
await sendTransport.produce({ track })
}
async function createRecvTransport() {
const params = await socket.emitWithAck('createTransport')
recvTransport = device.createRecvTransport({
...params,
// Recv Transport
{
const transportInfo = await signaling.socket.value.emitWithAck('createTransport', { producing: false, consuming: true })
recvTransport.value = device.value.createRecvTransport({
...transportInfo,
iceServers: [
...ICE_SERVERS,
...(params.iceServers ?? []),
...(transportInfo.iceServers ?? []),
],
})
recvTransport.on('connect', async ({ dtlsParameters }, callback, errback) => {
recvTransport.value.on('connect', async ({ dtlsParameters }, callback, errback) => {
try {
await socket.emitWithAck('connectTransport', {
transportId: recvTransport.id,
await signaling.socket.value!.emitWithAck('connectTransport', {
transportId: recvTransport.value!.id,
dtlsParameters,
})
callback()
}
catch (err) {
errback(err)
catch (error) {
if (error instanceof Error) {
errback(error)
}
}
})
}
(async () => {
if (initializing.value || connected.value)
const joinedClients = (await signaling.socket.value.emitWithAck('join', {
rtpCapabilities: rtpCapabilities.value,
}))
addClient(...joinedClients)
toast.add({ severity: 'success', summary: 'Joined', closable: false, life: 1000 })
await enableMic()
})
socket.on('newPeer', (client) => {
addClient(client)
})
socket.on('peerClosed', (id) => {
removeClient(id)
})
socket.on(
'newConsumer',
async (
{ id, producerId, kind, rtpParameters, socketId, appData, producerPaused },
cb,
) => {
if (!recvTransport.value)
return
initializing.value = true
connected.value = false
const consumer = await recvTransport.value.consume({
id,
producerId,
kind,
rtpParameters,
streamId: `${socketId}-${appData.source === 'share' ? 'share' : 'mic-webcam'}`,
appData: { ...appData, socketId },
})
await loadDevice()
await createSendTransport()
await createRecvTransport()
await publishMic()
if (producerPaused)
consumer.pause()
initializing.value = false
connected.value = true
})()
consumer.on('transportclose', () => {
if (consumers.value.delete(consumer.id))
triggerRef(consumers)
})
consumers.value.set(consumer.id, consumer)
triggerRef(consumers)
cb()
},
)
socket.on(
'consumerClosed',
async (
{ consumerId },
) => {
if (consumers.value.delete(consumerId))
triggerRef(consumers)
},
)
socket.on('disconnect', () => {
device.value = undefined
rtpCapabilities.value = undefined
sendTransport.value?.close()
sendTransport.value = undefined
recvTransport.value?.close()
recvTransport.value = undefined
micProducer.value = undefined
cameraProducer.value = undefined
shareProducer.value = undefined
consumers.value = new Map()
producers.value = new Map()
})
socket.on('consumerPaused', ({ consumerId }) => {
const consumer = consumers.value.get(consumerId)
if (!consumer)
return
consumer.pause()
triggerRef(consumers)
})
socket.on('consumerResumed', ({ consumerId }) => {
const consumer = consumers.value.get(consumerId)
if (!consumer)
return
consumer.resume()
triggerRef(consumers)
})
}, { immediate: true, flush: 'sync' })
async function enableProducer(type: ProducerType, options: ProducerOptions) {
const producer = getProducerByType(type)
if (producer.value)
return
if (!device.value || !sendTransport.value)
return
if (!options.track)
return
if (!device.value.canProduce(options.track.kind as MediaKind))
return
producer.value = await sendTransport.value.produce(options)
producers.value.set(producer.value.id, producer.value)
triggerRef(producers)
triggerRef(producer)
producer.value.on('transportclose', () => {
micProducer.value = undefined
})
producer.value.on('trackended', () => {
disableProducer(type)
})
}
async function disableProducer(type: ProducerType) {
const producer = getProducerByType(type)
if (!signaling.socket.value || !producer.value)
return
producers.value.delete(producer.value.id)
try {
producer.value.close()
await signaling.socket.value.emitWithAck('closeProducer', {
producerId: producer.value.id,
})
}
catch {
}
finally {
triggerRef(producers)
triggerRef(producer)
}
producer.value = undefined
}
async function enableMic() {
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
deviceId: { exact: preferences.inputDeviceId.value },
autoGainControl: { exact: preferences.autoGainControl.value },
echoCancellation: { exact: preferences.echoCancellation.value },
noiseSuppression: { exact: preferences.noiseSuppression.value },
},
})
const track = stream.getAudioTracks()[0]
if (!track)
return
await enableProducer('microphone', {
track,
codecOptions: {
opusStereo: true,
opusDtx: true, // Меньше пакетов летит когда тишина
opusFec: false, // Фиксит пакет лос
},
})
}
async function disableMic() {
await disableProducer('microphone')
}
async function enableShare() {
if (!device.value)
return
const stream = await getShareStream(preferences.shareFps.value)
const track = stream.getVideoTracks()[0]
if (!track)
return
await enableProducer('share', {
track,
codec: device.value.rtpCapabilities.codecs?.find(
c => c.mimeType.toLowerCase() === 'video/h264',
),
codecOptions: {
videoGoogleStartBitrate: 1000,
},
appData: {
source: 'share',
},
})
}
async function pauseProducer(type: ProducerType) {
if (!signaling.socket.value)
return
const producer = getProducerByType(type)
if (!producer.value)
return
if (producer.value.paused)
return
try {
producer.value.pause()
await signaling.socket.value.emitWithAck('pauseProducer', {
producerId: producer.value.id,
})
}
catch {
producer.value.resume()
}
finally {
triggerRef(producers)
triggerRef(producer)
}
}
async function resumeProducer(type: ProducerType) {
if (!signaling.socket.value)
return
const producer = getProducerByType(type)
if (!producer.value)
return
try {
producer.value.resume()
await signaling.socket.value.emitWithAck('resumeProducer', {
producerId: producer.value.id,
})
}
catch {
producer.value.pause()
}
finally {
triggerRef(producers)
triggerRef(producer)
}
}
async function init() {
signaling.connect()
}
function getProducerByType(type: ProducerType) {
switch (type) {
case 'microphone':
return micProducer
case 'camera':
return cameraProducer
case 'share':
return shareProducer
}
}
watch([
preferences.inputDeviceId,
preferences.echoCancellation,
preferences.autoGainControl,
preferences.noiseSuppression,
], async ([inputDeviceId]) => {
await disableMic()
if (!inputDeviceId)
return
await enableMic()
})
return {
connected,
streams,
init,
consumers,
producers,
sendTransport,
recvTransport,
rtpCapabilities,
device,
micProducer,
cameraProducer,
shareProducer,
pauseProducer,
resumeProducer,
enableShare,
disableProducer,
}
})

View File

@@ -0,0 +1,68 @@
import chadApi from '#shared/chad-api'
import { createGlobalState, useLocalStorage, watchDebounced } from '@vueuse/core'
export interface SyncedPreferences {
toggleInputHotkey: string
toggleOutputHotkey: string
volumes: Record<Client['id'], number>
}
export const usePreferences = createGlobalState(() => {
const { videoInputs, audioInputs, audioOutputs } = useDevices()
const fetched = ref(false)
const inputDeviceId = useLocalStorage<MediaDeviceInfo['deviceId']>('INPUT_DEVICE_ID', 'default')
const outputDeviceId = useLocalStorage<MediaDeviceInfo['deviceId']>('OUTPUT_DEVICE_ID', 'default')
const autoGainControl = useLocalStorage('AUTO_GAIN_CONTROL', false)
const noiseSuppression = useLocalStorage('NOISE_SUPPRESSION', true)
const echoCancellation = useLocalStorage('ECHO_CANCELLATION', true)
const shareFps = useLocalStorage('SHARE_FPS', 30)
const toggleInputHotkey = ref<SyncedPreferences['toggleInputHotkey']>('')
const toggleOutputHotkey = ref<SyncedPreferences['toggleOutputHotkey']>('')
const inputDeviceExist = computed(() => {
return audioInputs.value.some(device => device.deviceId === inputDeviceId.value)
})
const outputDeviceExist = computed(() => {
return audioOutputs.value.some(device => device.deviceId === outputDeviceId.value)
})
watchDebounced(
[toggleInputHotkey, toggleOutputHotkey],
async ([toggleInputHotkey, toggleOutputHotkey]) => {
try {
await chadApi(
'/preferences',
{
method: 'PATCH',
body: {
toggleInputHotkey,
toggleOutputHotkey,
},
},
)
}
catch {}
},
{ debounce: 1000 },
)
return {
fetched,
inputDeviceId,
outputDeviceId,
autoGainControl,
noiseSuppression,
echoCancellation,
shareFps,
toggleInputHotkey,
toggleOutputHotkey,
inputDeviceExist,
outputDeviceExist,
}
})

View File

@@ -0,0 +1,84 @@
import type { Socket } from 'socket.io-client'
import { createSharedComposable } from '@vueuse/core'
import { io } from 'socket.io-client'
import { parseURL } from 'ufo'
export const useSignaling = createSharedComposable(() => {
const toast = useToast()
const { me } = useAuth()
const socket = shallowRef<Socket>()
const connected = ref(false)
watch(socket, (socket, prevSocket) => {
if (prevSocket) {
prevSocket.close()
}
if (!socket) {
return
}
if (import.meta.dev) {
socket.onAny((event, ...args) => {
console.info('[onAny]', event, args)
})
socket.onAnyOutgoing((event, ...args) => {
console.info('[onAnyOutgoing]', event, args)
})
}
socket.on('connect', async () => {
connected.value = true
})
socket.on('disconnect', async () => {
connected.value = false
})
}, { immediate: true, flush: 'sync' })
watch(connected, (connected) => {
if (connected)
toast.add({ severity: 'success', summary: 'Connected', closable: false, life: 1000 })
else
toast.add({ severity: 'error', summary: 'Disconnected', closable: false, life: 1000 })
}, { immediate: true })
watch(me, (me) => {
if (!me) {
socket.value?.close()
socket.value = undefined
}
})
onScopeDispose(() => {
socket.value?.close()
socket.value = undefined
})
function connect() {
if (socket.value || !me.value)
return
const { protocol, host, pathname } = parseURL(__API_BASE_URL__)
const uri = host ? `${protocol}//${host}` : ``
socket.value = io(`${uri}/webrtc`, {
path: `${pathname}/ws`,
transports: ['websocket'],
withCredentials: true,
auth: {
userId: me.value.id,
},
})
}
return {
socket,
connected,
connect,
}
})

View File

@@ -0,0 +1,28 @@
import type { Update } from '@tauri-apps/plugin-updater'
import { check } from '@tauri-apps/plugin-updater'
import { createGlobalState } from '@vueuse/core'
export const useUpdater = createGlobalState(() => {
const lastUpdate = shallowRef<Update>()
const checking = ref(false)
async function checkForUpdates() {
try {
checking.value = true
lastUpdate.value = (await check()) ?? undefined
}
finally {
checking.value = false
}
return lastUpdate.value
}
return {
lastUpdate,
checking,
checkForUpdates,
}
})

7
client/app/index.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
declare module '#app' {
interface PageMeta {
auth?: boolean | 'guest'
}
}
export {}

View File

@@ -0,0 +1,42 @@
<template>
<div class="w-full h-full flex p-3">
<img src="/chad-bg.webp" alt="Background" draggable="false" class="pointer-events-none absolute opacity-3 -z-1 block inset-0 object-contain w-full h-full">
<div v-auto-animate class="w-1/2 m-auto">
<div class="text-center">
<PrimeSelectButton v-model="tab" class="mb-6" :options="options" option-label="label" :allow-empty="false" />
</div>
<slot />
</div>
</div>
<PrimeBadge class="fixed top-3 right-3 opacity-50" severity="secondary" :value="version" size="small" />
</template>
<script setup lang="ts">
const route = useRoute()
const { version } = useApp()
const options = computed(() => {
return [
{
label: 'Login',
routeName: 'Login',
},
{
label: 'Register',
routeName: 'Register',
},
]
})
const tab = shallowRef(options.value.find(option => route.name === option.routeName))
watch(tab, (tab) => {
if (!tab)
return
navigateTo({ name: tab.routeName })
})
</script>

View File

@@ -0,0 +1,3 @@
<template>
<slot />
</template>

View File

@@ -0,0 +1,125 @@
<template>
<div class="grid grid-cols-[1fr_1.25fr] gap-2 p-2 h-screen grid-rows-[auto_1fr]">
<div
class="flex items-center justify-between gap-2 rounded-xl p-3 bg-surface-950"
>
<div class="inline-flex items-center gap-3">
<PrimeBadge class="opacity-50" severity="secondary" :value="version" size="small" />
<PrimeBadge :severity="connected ? 'success' : 'danger' " />
</div>
<PrimeButtonGroup class="ml-auto">
<PrimeButton :severity="inputMuted ? 'info' : undefined" outlined @click="toggleInput">
<template #icon>
<Component :is="inputMuted ? MicOff : Mic" />
</template>
</PrimeButton>
<PrimeButton :severity="outputMuted ? 'info' : undefined" outlined @click="toggleOutput">
<template #icon>
<Component :is="outputMuted ? VolumeOff : Volume2" />
</template>
</PrimeButton>
</PrimeButtonGroup>
<PrimeButton :severity="sharingEnabled ? 'success' : undefined" outlined @click="toggleShare">
<template #icon>
<Component :is="sharingEnabled ? ScreenShareOff : ScreenShare" />
</template>
</PrimeButton>
</div>
<div
class="flex items-center justify-center rounded-xl p-3 bg-surface-950"
>
<PrimeSelectButton
v-model="activeTab"
:options="tabs"
data-key="id"
:allow-empty="false"
style="--p-togglebutton-content-padding: 0.25rem 0.5rem"
>
<template #option="{ option }">
<Component :is="option.icon" size="24" />
</template>
</PrimeSelectButton>
</div>
<PrimeScrollPanel class="bg-surface-900 rounded-xl" style="min-height: 0">
<div v-auto-animate class="p-3 space-y-1">
<ClientRow v-for="client of clients" :key="client.userId" :client="client" />
</div>
</PrimeScrollPanel>
<PrimeScrollPanel class="bg-surface-900 rounded-xl" style="min-height: 0">
<div class="p-3">
<slot />
</div>
</PrimeScrollPanel>
</div>
</template>
<script setup lang="ts">
import {
MessageCircle,
Mic,
MicOff,
ScreenShare,
ScreenShareOff,
Settings,
UserPen,
Volume2,
VolumeOff,
} from 'lucide-vue-next'
const {
version,
clients,
inputMuted,
outputMuted,
sharingEnabled,
toggleInput,
toggleOutput,
toggleShare,
} = useApp()
const { connect, connected } = useSignaling()
interface Tab {
id: string
icon: Component
onClick: () => void | Promise<void>
}
const route = useRoute()
const tabs: Tab[] = [
{
id: 'Index',
icon: MessageCircle,
onClick: () => {
navigateTo({ name: 'Index' })
},
},
{
id: 'Profile',
icon: UserPen,
onClick: () => {
navigateTo({ name: 'Profile' })
},
},
{
id: 'Preferences',
icon: Settings,
onClick: () => {
navigateTo({ name: 'Preferences' })
},
},
]
const activeTab = ref<Tab>(tabs.find(tab => tab.id === route.name) ?? tabs[0]!)
watch(activeTab, (activeTab) => {
activeTab.onClick()
})
connect()
</script>

View File

@@ -0,0 +1,20 @@
export default defineNuxtRouteMiddleware(async (to, from) => {
if (import.meta.dev || import.meta.server)
return
const { isTauri } = useApp()
if (!isTauri.value)
return
if (from?.name || !!to.redirectedFrom)
return
const { checkForUpdates } = useUpdater()
const update = await checkForUpdates()
if (update) {
return navigateTo({ name: 'Updater' })
}
})

View File

@@ -0,0 +1,23 @@
import chadApi from '#shared/chad-api'
export default defineNuxtRouteMiddleware(async (to, from) => {
const { me, setMe } = useAuth()
if (!me.value) {
try {
setMe(await chadApi('/me', { method: 'GET' }))
if (to.meta.auth !== false)
return navigateTo({ name: 'Index' })
}
catch {
if (to.meta.auth !== 'guest') {
return navigateTo({ name: 'Login' })
}
}
}
if (me.value && to.meta.auth === 'guest') {
return navigateTo({ name: 'Index' })
}
})

View File

@@ -0,0 +1,26 @@
import type { SyncedPreferences } from '~/composables/use-preferences'
import chadApi from '#shared/chad-api'
export default defineNuxtRouteMiddleware(async () => {
const { me } = useAuth()
if (!me.value)
return
const { fetched, toggleInputHotkey, toggleOutputHotkey } = usePreferences()
if (fetched.value)
return
try {
const preferences = await chadApi<SyncedPreferences>('/preferences', { method: 'GET' })
if (!preferences)
return
toggleInputHotkey.value = preferences.toggleInputHotkey ?? toggleInputHotkey.value
toggleOutputHotkey.value = preferences.toggleOutputHotkey ?? toggleOutputHotkey.value
fetched.value = true
}
catch {}
})

View File

@@ -1,5 +0,0 @@
export default defineNuxtRouteMiddleware((to, from) => {
console.group('Build Info')
console.log(`COMMIT_SHA: ${__COMMIT_SHA__}`)
console.groupEnd()
})

View File

@@ -0,0 +1,17 @@
<template>
<div>
<div class="flex items-center justify-center">
<PrimeCard>
<template #content>
The chat is under development.
</template>
</PrimeCard>
</div>
</div>
</template>
<script setup lang="ts">
definePageMeta({
name: 'Index',
})
</script>

View File

@@ -0,0 +1,67 @@
<template>
<PrimeCard>
<template #content>
<form @submit.prevent="submit">
<div class="flex flex-col gap-3">
<PrimeFloatLabel variant="on">
<PrimeInputText id="username" v-model="username" size="large" autocomplete="off" fluid autofocus />
<label for="username">Username</label>
</PrimeFloatLabel>
<PrimeFloatLabel variant="on">
<PrimePassword id="password" v-model="password" size="large" autocomplete="off" toggle-mask fluid :feedback="false" />
<label for="password">Password</label>
</PrimeFloatLabel>
</div>
<PrimeButton
class="mt-6"
size="large"
icon="pi pi-arrow-right"
icon-pos="right"
label="Let's go"
:loading="submitting"
:disabled="!valid"
type="submit"
fluid
/>
</form>
</template>
</PrimeCard>
</template>
<script lang="ts" setup>
definePageMeta({
name: 'Login',
layout: 'auth',
auth: 'guest',
})
const { login } = useAuth()
const submitting = ref(false)
const username = ref<string>()
const password = ref<string>()
const valid = computed(() => {
if (!username.value)
return false
if (!password.value)
return false
return true
})
async function submit() {
if (!valid.value)
return
submitting.value = true
await login(username.value!, password.value!)
submitting.value = false
}
</script>

View File

@@ -0,0 +1,174 @@
<template>
<div>
<PrimeDivider align="left">
Audio
</PrimeDivider>
<PrimeFloatLabel variant="on">
<PrimeSelect
v-model="inputDeviceId"
:options="audioInputs"
option-label="label"
option-value="deviceId"
input-id="inputDevice"
fluid
:invalid="!inputDeviceExist"
/>
<label for="inputDevice">Input device</label>
</PrimeFloatLabel>
<div class="flex items-center gap-2 mt-3">
<PrimeToggleSwitch v-model="autoGainControl" input-id="autoGainControl" />
<label for="autoGainControl">Auto Gain Control</label>
</div>
<div class="flex items-center gap-2 mt-3">
<PrimeToggleSwitch v-model="echoCancellation" input-id="echoCancellation" />
<label for="echoCancellation">Echo Cancellation</label>
</div>
<div class="flex items-center gap-2 mt-3">
<PrimeToggleSwitch v-model="noiseSuppression" input-id="noiseSuppression" />
<label for="noiseSuppression">Noise Suppression</label>
</div>
<!-- <PrimeFloatLabel variant="on"> -->
<!-- <PrimeSelect -->
<!-- v-model="outputDeviceId" -->
<!-- :options="audioOutputs" -->
<!-- option-label="label" -->
<!-- option-value="deviceId" -->
<!-- fluid -->
<!-- class="mt-6" -->
<!-- input-id="outputDevice" -->
<!-- :invalid="!outputDeviceExist" -->
<!-- -->
<!-- /> -->
<!-- <label for="outputDevice">Output device</label> -->
<!-- </PrimeFloatLabel> -->
<PrimeDivider align="left">
Video
</PrimeDivider>
<div>
<div class="flex justify-between text-sm mb-3">
<span>FPS</span>
<span>{{ shareFps }}</span>
</div>
<PrimeSlider v-model="shareFps" class="mx-[10px]" :min="30" :max="60" :step="5" />
</div>
<template v-if="isTauri">
<PrimeDivider align="left">
Hotkeys
</PrimeDivider>
<PrimeFloatLabel variant="on">
<PrimeInputText id="microphoneToggle" :model-value="toggleInputHotkey" fluid @keydown="setupToggleInputHotkey" />
<label for="microphoneToggle">Toggle microphone</label>
</PrimeFloatLabel>
<PrimeFloatLabel variant="on" class="mt-3">
<PrimeInputText id="soundToggle" :model-value="toggleOutputHotkey" fluid @keydown="setupToggleOutputHotkey" />
<label for="soundToggle">Toggle sound</label>
</PrimeFloatLabel>
</template>
<PrimeDivider align="left">
About
</PrimeDivider>
<p v-if="version" class="text-muted-color text-sm">
VERSION: {{ version }}
</p>
<p class="text-muted-color text-sm mt-2">
COMMIT_SHA: {{ commitSha }}
</p>
<template v-if="isTauri">
<PrimeButton
v-if="lastUpdate"
class="mt-3"
size="small"
label="Install new version"
fluid
severity="success"
@click="navigateTo({ name: 'Updater' })"
/>
<PrimeButton
v-else
class="mt-3"
size="small"
label="Check for Updates"
fluid
severity="info"
:loading="checking"
@click="checkForUpdates"
/>
</template>
</div>
</template>
<script setup lang="ts">
import type { RemovableRef } from '@vueuse/core'
definePageMeta({
name: 'Preferences',
})
const { isTauri, version, commitSha } = useApp()
const { checking, checkForUpdates, lastUpdate } = useUpdater()
const { audioInputs, audioOutputs } = useDevices()
const {
inputDeviceId,
outputDeviceId,
autoGainControl,
noiseSuppression,
echoCancellation,
toggleInputHotkey,
toggleOutputHotkey,
inputDeviceExist,
outputDeviceExist,
shareFps,
} = usePreferences()
const setupToggleInputHotkey = (event: KeyboardEvent) => setupHotkey(event, toggleInputHotkey)
const setupToggleOutputHotkey = (event: KeyboardEvent) => setupHotkey(event, toggleOutputHotkey)
function setupHotkey(event: KeyboardEvent, model: RemovableRef<string>) {
if (event.key === 'Tab' || event.key === 'Enter') {
return
}
event.preventDefault()
const hotkey = []
if (event.ctrlKey || event.metaKey)
hotkey.push('CommandOrControl')
if (event.altKey)
hotkey.push('Alt')
if (event.shiftKey)
hotkey.push('Shift')
const modifierApplied = hotkey.length > 0
if (!modifierApplied && ['Escape', 'Backspace', 'Delete'].includes(event.key)) {
model.value = ''
return
}
if (!['Control', 'Shift', 'Alt'].includes(event.key)) {
hotkey.push(event.key.toUpperCase())
}
if (modifierApplied && hotkey.length === 1) {
model.value = ''
return
}
model.value = hotkey.join('+')
}
</script>

View File

@@ -0,0 +1,68 @@
<template>
<form @submit.prevent="save()">
<PrimeDivider align="left">
General
</PrimeDivider>
<PrimeFloatLabel variant="on">
<PrimeInputText id="displayName" v-model="displayName" fluid autocomplete="off" />
<label for="displayName">Display name</label>
</PrimeFloatLabel>
<div class="flex items-center gap-3 mt-6">
<PrimeButton label="Save" :disabled="!valid" :loading="saving" fluid type="submit" />
<PrimeButton severity="danger" class="shrink-0" @click="logout()">
<template #icon>
<LogOut />
</template>
</PrimeButton>
</div>
</form>
</template>
<script setup lang="ts">
import chadApi from '#shared/chad-api'
import { LogOut } from 'lucide-vue-next'
definePageMeta({
name: 'Profile',
})
const { me, setMe, logout } = useAuth()
const signaling = useSignaling()
const toast = useToast()
const displayName = ref(me.value?.displayName || '')
const saving = ref(false)
const valid = computed(() => {
if (!me.value)
return false
if (displayName.value === me.value.displayName)
return false
return true
})
async function save() {
if (!valid.value)
return
saving.value = true
const updatedMe = await chadApi('/profile', {
method: 'PATCH',
body: {
displayName: displayName.value,
},
})
setMe({ ...me.value!, displayName: (updatedMe.displayName as string) })
toast.add({ severity: 'success', summary: 'Saved', life: 1000, closable: false })
saving.value = false
}
</script>

View File

@@ -0,0 +1,74 @@
<template>
<PrimeCard>
<template #content>
<form @submit.prevent="submit">
<div class="flex flex-col gap-3">
<PrimeFloatLabel variant="on">
<PrimeInputText id="username" v-model="username" size="large" autocomplete="off" fluid autofocus />
<label for="username">Username</label>
</PrimeFloatLabel>
<PrimeFloatLabel variant="on">
<PrimePassword id="password" v-model="password" size="large" autocomplete="off" toggle-mask fluid :feedback="false" />
<label for="password">Password</label>
</PrimeFloatLabel>
<PrimeFloatLabel variant="on">
<PrimePassword id="repeatPassword" v-model="repeatPassword" size="large" autocomplete="off" toggle-mask fluid :feedback="false" />
<label for="repeatPassword">Repeat password</label>
</PrimeFloatLabel>
</div>
<PrimeButton
class="mt-6"
size="large"
label="Register"
:loading="submitting"
:disabled="!valid"
type="submit"
fluid
/>
</form>
</template>
</PrimeCard>
</template>
<script lang="ts" setup>
definePageMeta({
name: 'Register',
layout: 'auth',
auth: 'guest',
})
const { register } = useAuth()
const submitting = ref(false)
const username = ref<string>()
const password = ref<string>()
const repeatPassword = ref<string>()
const valid = computed(() => {
if (!username.value)
return false
if (!password.value)
return false
if (repeatPassword.value !== password.value)
return false
return true
})
async function submit() {
if (!valid.value)
return
submitting.value = true
await register(username.value!, password.value!)
submitting.value = false
}
</script>

View File

@@ -0,0 +1,41 @@
<template>
<div class="flex h-full">
<div class="w-80 m-auto">
<p class="text-center text-muted-color mb-4">
Updating...
</p>
<PrimeProgressBar mode="indeterminate" style="height: 8px;" />
<div class="flex items-center justify-center gap-2 mt-8">
<PrimeBadge :value="lastUpdate.currentVersion" size="small" severity="secondary" />
<i class="pi pi-arrow-right" style="font-size: 0.75rem;" />
<PrimeBadge :value="lastUpdate.version" size="small" severity="contrast" />
</div>
</div>
</div>
</template>
<script lang="ts" setup>
definePageMeta({
name: 'Updater',
layout: 'blank',
auth: false,
middleware: () => {
const { lastUpdate } = useUpdater()
if (!lastUpdate.value)
return navigateTo('/')
},
})
const lastUpdate = useUpdater().lastUpdate.value!
;(async () => {
try {
await lastUpdate.downloadAndInstall()
}
catch {
await navigateTo('/')
}
})()
</script>

View File

@@ -0,0 +1,7 @@
export default defineNuxtPlugin({
setup() {
console.group('Build Info')
console.log(`COMMIT_SHA: ${__COMMIT_SHA__}`)
console.groupEnd()
},
})

View File

@@ -0,0 +1,45 @@
import { isRegistered, register, unregister, unregisterAll } from '@tauri-apps/plugin-global-shortcut'
export default defineNuxtPlugin({
async setup() {
const { isTauri, toggleInput, toggleOutput } = useApp()
const preferences = usePreferences()
if (!isTauri.value)
return
await unregisterAll()
watch(preferences.toggleInputHotkey, async (shortcut, prevShortcut) => {
await registerHotkey(shortcut, prevShortcut, toggleInput)
}, {
immediate: true,
})
watch(preferences.toggleOutputHotkey, async (shortcut, prevShortcut) => {
await registerHotkey(shortcut, prevShortcut, toggleOutput)
}, {
immediate: true,
})
async function registerHotkey(shortcut: string, prevShortcut: string | undefined, cb: () => void) {
if (!!prevShortcut && await isRegistered(prevShortcut)) {
await unregister(prevShortcut)
}
if (!shortcut)
return
if (await isRegistered(shortcut)) {
await unregister(shortcut)
}
await register(shortcut, ({ state }) => {
if (state !== 'Released')
return
cb()
})
}
},
})

View File

@@ -5,6 +5,9 @@ export default antfu({
css: true,
},
overrides: {
typescript: {
'no-console': 'off',
},
vue: {
'vue/block-order': ['error', {
order: ['template', 'script', 'style'],

1
client/globals.d.ts vendored
View File

@@ -1 +1,2 @@
declare const __API_BASE_URL__: string
declare const __COMMIT_SHA__: string

View File

@@ -1,18 +1,104 @@
import { definePreset } from '@primeuix/themes'
import Aura from '@primeuix/themes/aura'
import tailwindcss from '@tailwindcss/vite'
export default defineNuxtConfig({
compatibilityDate: '2025-09-29',
devtools: { enabled: true },
devtools: { enabled: true, vueDevTools: true },
ssr: false,
devServer: {
// host: '0',
modules: [
'@nuxt/fonts',
'@primevue/nuxt-module',
'@formkit/auto-animate/nuxt',
],
fonts: {
provider: 'google',
},
primevue: {
options: {
theme: {
preset: definePreset(Aura, {
semantic: {
transitionDuration: '150ms',
primary: {
50: '{zinc.50}',
100: '{zinc.100}',
200: '{zinc.200}',
300: '{zinc.300}',
400: '{zinc.400}',
500: '{zinc.500}',
600: '{zinc.600}',
700: '{zinc.700}',
800: '{zinc.800}',
900: '{zinc.900}',
950: '{zinc.950}',
},
},
colorScheme: {
light: {
primary: {
color: '{zinc.950}',
inverseColor: '#ffffff',
hoverColor: '{zinc.900}',
activeColor: '{zinc.800}',
},
highlight: {
background: '{zinc.950}',
focusBackground: '{zinc.700}',
color: '#ffffff',
focusColor: '#ffffff',
},
},
dark: {
primary: {
color: '{zinc.50}',
inverseColor: '{zinc.950}',
hoverColor: '{zinc.100}',
activeColor: '{zinc.200}',
},
highlight: {
background: 'rgba(250, 250, 250, .16)',
focusBackground: 'rgba(250, 250, 250, .24)',
color: 'rgba(255,255,255,.87)',
focusColor: 'rgba(255,255,255,.87)',
},
},
},
}),
},
},
components: {
prefix: 'Prime',
},
},
css: [
'@/assets/styles/tailwind.css',
'@/assets/styles/primeicons.css',
'@/assets/styles/main.scss',
],
vite: {
plugins: [
tailwindcss(),
],
clearScreen: false,
envPrefix: ['VITE_', 'TAURI_'],
server: {
strictPort: true,
proxy: {
'/api': {
target: 'http://localhost:4000/chad',
// target: 'https://api.koptilnya.xyz/chad',
ws: true,
changeOrigin: true,
rewrite: (path) => {
return path.replace(/^\/api/, '')
},
},
},
},
define: {
__COMMIT_SHA__: JSON.stringify(process.env.COMMIT_SHA || 'local'),
__API_BASE_URL__: JSON.stringify(import.meta.env.API_BASE_URL || 'http://localhost:4000/chad'),
__COMMIT_SHA__: JSON.stringify(import.meta.env.COMMIT_SHA || 'local'),
},
},
ignore: ['**/src-tauri/**'],

View File

@@ -10,19 +10,36 @@
"postinstall": "nuxt prepare"
},
"dependencies": {
"@formkit/auto-animate": "^0.9.0",
"@nuxt/fonts": "^0.11.4",
"@primeuix/themes": "^1.2.5",
"@tailwindcss/vite": "^4.1.14",
"@tauri-apps/plugin-global-shortcut": "~2",
"@tauri-apps/plugin-process": "~2",
"@tauri-apps/plugin-updater": "~2",
"@vueuse/core": "^13.9.0",
"hotkeys-js": "^4.0.0",
"lucide-vue-next": "^0.562.0",
"mediasoup-client": "^3.16.7",
"nuxt": "^4.1.2",
"nuxt": "^4.2.2",
"postcss": "^8.5.6",
"primeicons": "^7.0.0",
"primevue": "^4.4.0",
"socket.io-client": "^4.8.1",
"typescript": "^5.9.3",
"tailwindcss": "^4.1.14",
"tailwindcss-primeui": "^0.6.1",
"ufo": "^1.6.1",
"vue": "^3.5.22",
"vue-router": "^4.5.1"
},
"packageManager": "yarn@4.10.3",
"packageManager": "yarn@4.12.0",
"devDependencies": {
"@antfu/eslint-config": "^5.4.1",
"@primevue/nuxt-module": "^4.4.0",
"@tauri-apps/cli": "^2.8.4",
"eslint": "^9.36.0",
"eslint-plugin-format": "^1.0.2"
"eslint-plugin-format": "^1.0.2",
"sass-embedded": "^1.93.2",
"typescript": "^5.9.3"
}
}

BIN
client/public/chad-bg.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

View File

@@ -0,0 +1,41 @@
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const TAURI_CONF = path.resolve(__dirname, '../src-tauri/tauri.conf.json')
const OUTPUT = path.resolve(__dirname, '../updater.json')
const PLATFORM = 'windows-x86_64'
const BASE_URL = 'https://git.koptilnya.xyz/opti1337/chad/releases/download/latest'
const tauriConfRaw = fs.readFileSync(TAURI_CONF, 'utf8')
const tauriConf = JSON.parse(tauriConfRaw)
const version = tauriConf.version
const SIG_FILE = path.resolve(
__dirname,
`../src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/chad_${version}_x64-setup.exe.sig`,
)
const signature = fs.readFileSync(SIG_FILE, 'utf8').trim()
const installerName = `chad_${version}_x64-setup.exe`
const updater = {
pub_date: new Date().toISOString(),
version,
platforms: {
[PLATFORM]: {
url: `${BASE_URL}/${installerName}`,
signature,
},
},
notes: '',
}
fs.writeFileSync(OUTPUT, JSON.stringify(updater, null, 2), 'utf8')
console.log('updater.json generated')

View File

@@ -0,0 +1,4 @@
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('render:html', (html, { event }) => {
})
})

64
client/shared/chad-api.ts Normal file
View File

@@ -0,0 +1,64 @@
import { ToastEventBus } from 'primevue'
const instance = $fetch.create({
baseURL: __API_BASE_URL__,
credentials: 'include',
retry: false,
onResponseError({ request, response }) {
if (!import.meta.client)
return
if (typeof request === 'string' && request.endsWith('/me'))
return
const message = response._data.error || 'Something went wrong'
ToastEventBus.emit('add', { severity: 'error', summary: 'Error', detail: message, closable: false, life: 3000 })
},
})
// function instance(url: string, request: Request) {
// console.log(url)
// return fetch({
// url: `https://api.koptilnya.xyz${url}`,
// credentials: 'include',
// method: 'GET',
// headers: {
// 'Accept': 'application/json',
// 'Content-Type': 'application/json',
// ...request.headers,
// },
// ...request,
// body: request.body ? JSON.stringify(request.body) : undefined,
// })
// }
// async function instance(url: string, options: Partial<Request> = {}) {
// const response = await fetch(`https://api.koptilnya.xyz/chad${url}`, {
// method: options.method || 'GET',
// headers: {
// 'Content-Type': 'application/json',
// ...(options.headers || {}),
// },
// body: JSON.stringify(options.body ?? {}),
// responseType: 2,
// credentials: 'include',
// })
//
// const data = await response.json()
//
// if (response.status >= 400) {
// if (!import.meta.client)
// return
//
// const message = data?.error || 'Something went wrong'
//
// ToastEventBus.emit('add', { severity: 'error', summary: 'Error', detail: message, closable: false, life: 3000 })
//
// throw new Error(`HTTP ${response.status}: ${data?.message || 'Ошибка'}`)
// }
//
// return data
// }
export default instance

10
client/shared/types.ts Normal file
View File

@@ -0,0 +1,10 @@
export interface ChadClient {
socketId: string
userId: string
username: string
displayName: string
inputMuted?: boolean
outputMuted?: boolean
}
export type UpdatedClient = Omit<ChadClient, 'socketId' | 'userId' | 'isMe'>

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
[package]
name = "app"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
description = "WW додепчик"
authors = ["KPTL"]
license = ""
repository = ""
edition = "2021"
@@ -23,3 +23,9 @@ serde = { version = "1.0", features = ["derive"] }
log = "0.4"
tauri = { version = "2.8.5", features = [] }
tauri-plugin-log = "2"
tauri-plugin-process = "2"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-global-shortcut = "2"
tauri-plugin-single-instance = "2"
tauri-plugin-updater = "2"

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSCameraUsageDescription</key>
<string>Request camera access for WebRTC</string>
<key>NSMicrophoneUsageDescription</key>
<string>Request microphone access for WebRTC</string>
</dict>
</plist>

View File

@@ -0,0 +1,18 @@
{
"identifier": "desktop-capability",
"platforms": [
"macOS",
"windows",
"linux"
],
"windows": [
"main"
],
"permissions": [
"updater:default",
"global-shortcut:allow-is-registered",
"global-shortcut:allow-register",
"global-shortcut:allow-unregister",
"global-shortcut:allow-unregister-all"
]
}

View File

@@ -1,6 +1,10 @@
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_single_instance::init(|_, _, _| {}))
.setup(|app| {
if cfg!(debug_assertions) {
app.handle().plugin(

View File

@@ -1,31 +1,39 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "chad",
"version": "0.1.0",
"identifier": "com.tauri.dev",
"version": "0.2.19",
"identifier": "xyz.koptilnya.chad",
"build": {
"frontendDist": "../dist",
"frontendDist": "../.output/public",
"devUrl": "http://localhost:3000",
"beforeDevCommand": "yarn dev",
"beforeBuildCommand": "yarn build"
"beforeBuildCommand": "yarn generate"
},
"app": {
"windows": [
{
"maximizable": false,
"label": "main",
"title": "Chad",
"width": 800,
"height": 600,
"resizable": false,
"fullscreen": false
"fullscreen": false,
"center": true,
"theme": "Dark",
"additionalBrowserArgs": "--disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection --autoplay-policy=no-user-gesture-required",
"incognito": false
}
],
"security": {
"csp": null
"csp": null,
"capabilities": []
}
},
"bundle": {
"createUpdaterArtifacts": true,
"active": true,
"targets": "all",
"targets": ["nsis"],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
@@ -33,5 +41,13 @@
"icons/icon.icns",
"icons/icon.ico"
]
},
"plugins": {
"updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEU3MzkxMzM3RkQ3NTg4QUQKUldTdGlIWDlOeE01NStIak9VbmZTTm9HY2NyNUQrVXB5ZEdIN1BkK2lhYW9zWkNCQnZQSjRmelIK",
"endpoints": [
"https://git.koptilnya.xyz/opti1337/chad/releases/download/latest/updater.json"
]
}
}
}

File diff suppressed because it is too large Load Diff

3
server/.gitignore vendored
View File

@@ -15,3 +15,6 @@ node_modules
#!.yarn/cache
.pnp.*
.env*
*.db

View File

@@ -8,7 +8,10 @@ RUN yarn install --frozen-lockfile
COPY . .
ENV PORT=80
ENV DEBUG=mediasoup*
ENV CORS_ORIGIN=chad.koptilnya.xyz
ENV ANNOUNCED_ADDRESS=91.144.171.182
ENV DATABASE_URL=file:../data/database.db
EXPOSE 80
RUN ls -la
CMD ["yarn", "node", "index.mjs"]
CMD yarn db:deploy && yarn start

34
server/auth/lucia.ts Normal file
View File

@@ -0,0 +1,34 @@
import { PrismaAdapter } from '@lucia-auth/adapter-prisma'
import { Lucia } from 'lucia'
import prisma from '../prisma/client.ts'
declare module 'lucia' {
interface Register {
Lucia: typeof Lucia
UserId: string
DatabaseUserAttributes: DatabaseUserAttributes
}
}
interface DatabaseUserAttributes {
id: string
displayName: string
username: string
}
export const auth = new Lucia(new PrismaAdapter(prisma.session, prisma.user), {
sessionCookie: {
attributes: {
sameSite: 'none',
},
},
getUserAttributes: ({ id, displayName, username }) => {
return {
id,
displayName,
username,
}
},
})
export type Auth = typeof auth

14
server/dto/channel.dto.ts Normal file
View File

@@ -0,0 +1,14 @@
// dto/channel.dto.ts
import type { Prisma } from '../prisma/generated/client'
export const channelPublicSelect = {
id: true,
name: true,
owner_id: true,
persistent: true,
maxClients: true,
} satisfies Prisma.ChannelSelect
export type ChannelPublicDTO = Prisma.ChannelGetPayload<{
select: typeof channelPublicSelect
}>

10
server/eslint.config.mjs Normal file
View File

@@ -0,0 +1,10 @@
import antfu from '@antfu/eslint-config'
export default antfu({
typescript: {
overrides: {
'no-console': 'off',
'n/prefer-global/process': 'off',
},
},
})

View File

@@ -1,146 +0,0 @@
import express from "express";
import http from "http";
import cors from "cors";
import { Server } from "socket.io";
import * as mediasoup from "mediasoup";
const app = express();
app.use(cors());
const server = http.createServer(app);
const io = new Server(server, {
path: '/chad/ws',
cors: {
origin: '*'
}
});
let worker;
let router;
const transports = new Map(); // socketId -> [transports]
const producers = new Map(); // socketId -> [producers]
const consumers = new Map(); // socketId -> [consumers]
async function createWorker() {
worker = await mediasoup.createWorker();
worker.on("died", () => {
console.error("mediasoup worker died, exiting...");
process.exit(1);
});
router = await worker.createRouter({
mediaCodecs: [
{
kind: "audio",
mimeType: "audio/opus",
clockRate: 48000,
channels: 2,
}
],
});
console.log("Mediasoup worker & router created");
}
createWorker();
io.on("connection", (socket) => {
console.log("Client connected:", socket.id);
socket.on("getRtpCapabilities", (cb) => {
cb(router.rtpCapabilities);
});
socket.on("createTransport", async (cb) => {
try {
const transport = await router.createWebRtcTransport({
listenIps: [{ ip: "0.0.0.0", announcedIp: "91.144.171.182" }],
enableUdp: true,
enableTcp: true,
preferUdp: true,
});
transports.set(socket.id, [...(transports.get(socket.id) || []), transport]);
cb({
id: transport.id,
iceParameters: transport.iceParameters,
iceCandidates: transport.iceCandidates,
dtlsParameters: transport.dtlsParameters,
});
transport.observer.on("close", () => {
console.log("transport closed", transport.id);
});
} catch (err) {
console.error("createTransport error:", err);
cb({ error: err.message });
}
});
socket.on("connectTransport", async ({ transportId, dtlsParameters }, cb) => {
const transport = transports.get(socket.id)?.find((t) => t.id === transportId);
if (!transport) return cb({ error: "transport not found" });
await transport.connect({ dtlsParameters });
cb({ connected: true });
});
socket.on("produce", async ({ transportId, kind, rtpParameters }, cb) => {
const transport = transports.get(socket.id)?.find((t) => t.id === transportId);
if (!transport) return cb({ error: "transport not found" });
const producer = await transport.produce({ kind, rtpParameters });
producers.set(socket.id, [...(producers.get(socket.id) || []), producer]);
cb({ id: producer.id });
socket.broadcast.emit("newProducer", { producerId: producer.id });
producer.observer.on("close", () => {
console.log("producer closed", producer.id);
});
});
socket.on("consume", async ({ producerId, transportId, rtpCapabilities }, cb) => {
try {
if (!router.canConsume({ producerId, rtpCapabilities })) {
return cb({ error: "cannot consume" });
}
const transport = transports.get(socket.id)?.find((t) => t.id === transportId);
if (!transport) return cb({ error: "transport not found" });
const consumer = await transport.consume({
producerId,
rtpCapabilities,
paused: false,
});
consumers.set(socket.id, [...(consumers.get(socket.id) || []), consumer]);
cb({
id: consumer.id,
producerId,
kind: consumer.kind,
rtpParameters: consumer.rtpParameters,
});
} catch (err) {
console.error("consume error:", err);
cb({ error: err.message });
}
});
socket.on("disconnect", () => {
console.log("Client disconnected:", socket.id);
transports.get(socket.id)?.forEach((t) => t.close());
producers.get(socket.id)?.forEach((p) => p.close());
consumers.get(socket.id)?.forEach((c) => c.close());
transports.delete(socket.id);
producers.delete(socket.id);
consumers.delete(socket.id);
});
});
server.listen(process.env.PORT || 3000);

View File

@@ -1,13 +1,39 @@
{
"name": "server",
"scripts": {
"start": "node index.mjs"
"start": "ts-node --transpile-only server.ts",
"db:deploy": "npx prisma migrate deploy && npx prisma db seed && npx prisma generate"
},
"type": "module",
"packageManager": "yarn@4.10.3",
"dependencies": {
"cors": "^2.8.5",
"express": "^5.1.0",
"@fastify/autoload": "^6.3.1",
"@fastify/cookie": "^11.0.2",
"@fastify/cors": "^11.1.0",
"@lucia-auth/adapter-prisma": "^4.0.1",
"@prisma/adapter-better-sqlite3": "^7.2.0",
"@prisma/client": "7",
"bcrypt": "^6.0.0",
"consola": "^3.4.2",
"dotenv": "^17.2.3",
"fastify": "^5.6.1",
"fastify-plugin": "^5.1.0",
"lucia": "^3.2.2",
"mediasoup": "^3.19.3",
"socket.io": "^4.8.1"
"prisma": "7",
"socket.io": "^4.8.1",
"ws": "^8.18.3",
"zod": "^4.1.12"
},
"devDependencies": {
"@antfu/eslint-config": "^5.4.1",
"@types/bcrypt": "^6",
"@types/ws": "^8",
"eslint": "^9.36.0",
"ts-node": "^10.9.2",
"typescript": "^5.9.3"
},
"engines": {
"node": ">=22.18.0"
}
}

45
server/plugins/auth.ts Normal file
View File

@@ -0,0 +1,45 @@
import type { Session, User } from 'lucia'
import fp from 'fastify-plugin'
import { auth } from '../auth/lucia.ts'
declare module 'fastify' {
interface FastifyRequest {
user: User | null
session: Session | null
}
}
export default fp(async (fastify) => {
fastify.decorateRequest('user', null)
fastify.decorateRequest('session', null)
fastify.addHook('preHandler', async (req, reply) => {
try {
const sessionId = auth.readSessionCookie(req.headers.cookie ?? '')
if (!sessionId)
return
const { session, user } = await auth.validateSession(sessionId ?? '')
if (session && session.fresh) {
const cookie = auth.createSessionCookie(session.id)
reply.setCookie(cookie.name, cookie.value, cookie.attributes)
}
if (!session) {
const blank = auth.createBlankSessionCookie()
reply.setCookie(blank.name, blank.value, blank.attributes)
}
req.user = user
req.session = session
}
catch {
req.user = null
req.session = null
}
})
})

View File

@@ -0,0 +1,74 @@
import type * as mediasoup from 'mediasoup'
import fp from 'fastify-plugin'
declare module 'fastify' {
interface FastifyInstance {
mediasoupRouter: mediasoup.types.Router
}
}
export default fp<mediasoup.types.RouterOptions>(
async (fastify, opts) => {
const router = await fastify.mediasoupWorker.createRouter(opts)
fastify.decorate('mediasoupRouter', router)
},
{ name: 'mediasoup-router', dependencies: ['mediasoup-worker'] },
)
export const autoConfig: mediasoup.types.RouterOptions = {
mediaCodecs: [
{
kind: 'audio',
mimeType: 'audio/opus',
clockRate: 48000,
channels: 2,
parameters: { useinbandfec: 1, stereo: 1 },
},
{
kind: 'video',
mimeType: 'video/VP8',
clockRate: 90000,
parameters: {
'x-google-start-bitrate': 1000,
},
},
{
kind: 'video',
mimeType: 'video/VP9',
clockRate: 90000,
parameters: {
'profile-id': 2,
'x-google-start-bitrate': 1000,
},
},
{
kind: 'video',
mimeType: 'video/h264',
clockRate: 90000,
parameters: {
'packetization-mode': 1,
'profile-level-id': '4d0032',
'level-asymmetry-allowed': 1,
'x-google-start-bitrate': 1000,
},
},
{
kind: 'video',
mimeType: 'video/h264',
clockRate: 90000,
parameters: {
'packetization-mode': 1,
'profile-level-id': '42e01f',
'level-asymmetry-allowed': 1,
'x-google-start-bitrate': 1000,
},
},
{
kind: 'video',
mimeType: 'video/AV1',
clockRate: 90000,
parameters: {},
},
],
}

View File

@@ -0,0 +1,23 @@
import { consola } from 'consola'
import fp from 'fastify-plugin'
import * as mediasoup from 'mediasoup'
declare module 'fastify' {
interface FastifyInstance {
mediasoupWorker: mediasoup.types.Worker
}
}
export default fp(
async (fastify) => {
const worker = await mediasoup.createWorker()
worker.on('died', () => {
consola.error('[Mediasoup]', 'Worker died, exiting...')
process.exit(1)
})
fastify.decorate('mediasoupWorker', worker)
},
{ name: 'mediasoup-worker' },
)

40
server/plugins/socket.ts Normal file
View File

@@ -0,0 +1,40 @@
import type { FastifyInstance } from 'fastify'
import type { ServerOptions } from 'socket.io'
import fp from 'fastify-plugin'
import { Server } from 'socket.io'
import registerWebrtcSocket from '../socket/webrtc.ts'
declare module 'fastify' {
interface FastifyInstance {
io: Server
}
}
export default fp<Partial<ServerOptions>>(
async (fastify, opts) => {
fastify.decorate('io', new Server(fastify.server, opts))
fastify.addHook('preClose', () => {
fastify.io.disconnectSockets(true)
})
fastify.addHook('onClose', async (fastify: FastifyInstance) => {
await fastify.io.close()
})
fastify.ready(() => {
registerWebrtcSocket(fastify.io, fastify.mediasoupRouter)
registerChannelSocket(fastify.io, fastify.mediasoupRouter)
})
},
{ name: 'socket-io', dependencies: ['mediasoup-worker', 'mediasoup-router'] },
)
export const autoConfig: Partial<ServerOptions> = {
path: '/chad/ws',
cors: {
origin: process.env.CORS_ORIGIN || '*',
methods: ['GET', 'POST'],
credentials: true,
},
}

13
server/prisma.config.ts Normal file
View File

@@ -0,0 +1,13 @@
import { defineConfig, env } from 'prisma/config'
import 'dotenv/config'
export default defineConfig({
schema: './prisma/schema.prisma',
migrations: {
path: './prisma/migrations',
seed: 'ts-node ./prisma/seed.ts',
},
datasource: {
url: env('DATABASE_URL'),
},
})

15
server/prisma/client.ts Normal file
View File

@@ -0,0 +1,15 @@
import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3'
import { env } from 'prisma/config'
import { PrismaClient } from './generated/client/index.js'
import 'dotenv/config'
const client = new PrismaClient({
adapter: new PrismaBetterSqlite3({
url: env('DATABASE_URL'),
}, {
timestampFormat: 'unixepoch-ms',
}),
log: ['query', 'error', 'warn'],
})
export default client

View File

@@ -0,0 +1,39 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* This file should be your main import to use Prisma-related types and utilities in a browser.
* Use it to get access to models, enums, and input types.
*
* This file does not contain a `PrismaClient` class, nor several other helpers that are intended as server-side only.
* See `client.ts` for the standard, server-side entry point.
*
* 🟢 You can import this file directly.
*/
import * as Prisma from './internal/prismaNamespaceBrowser.ts'
export { Prisma }
export * as $Enums from './enums.ts'
export * from './enums.ts';
/**
* Model User
*
*/
export type User = Prisma.UserModel
/**
* Model Session
*
*/
export type Session = Prisma.SessionModel
/**
* Model UserPreferences
*
*/
export type UserPreferences = Prisma.UserPreferencesModel
/**
* Model Channel
*
*/
export type Channel = Prisma.ChannelModel

View File

@@ -0,0 +1 @@
export * from "./index"

View File

@@ -0,0 +1,5 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
module.exports = { ...require('.') }

View File

@@ -0,0 +1,61 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* This file should be your main import to use Prisma. Through it you get access to all the models, enums, and input types.
* If you're looking for something you can import in the client-side of your application, please refer to the `browser.ts` file instead.
*
* 🟢 You can import this file directly.
*/
import * as process from 'node:process'
import * as path from 'node:path'
import { fileURLToPath } from 'node:url'
globalThis['__dirname'] = path.dirname(fileURLToPath(import.meta.url))
import * as runtime from "@prisma/client/runtime/client"
import * as $Enums from "./enums.ts"
import * as $Class from "./internal/class.ts"
import * as Prisma from "./internal/prismaNamespace.ts"
export * as $Enums from './enums.ts'
export * from "./enums.ts"
/**
* ## Prisma Client
*
* Type-safe database client for TypeScript
* @example
* ```
* const prisma = new PrismaClient()
* // Fetch zero or more Users
* const users = await prisma.user.findMany()
* ```
*
* Read more in our [docs](https://pris.ly/d/client).
*/
export const PrismaClient = $Class.getPrismaClientClass()
export type PrismaClient<LogOpts extends Prisma.LogLevel = never, OmitOpts extends Prisma.PrismaClientOptions["omit"] = Prisma.PrismaClientOptions["omit"], ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = $Class.PrismaClient<LogOpts, OmitOpts, ExtArgs>
export { Prisma }
/**
* Model User
*
*/
export type User = Prisma.UserModel
/**
* Model Session
*
*/
export type Session = Prisma.SessionModel
/**
* Model UserPreferences
*
*/
export type UserPreferences = Prisma.UserPreferencesModel
/**
* Model Channel
*
*/
export type Channel = Prisma.ChannelModel

View File

@@ -0,0 +1,298 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* This file exports various common sort, input & filter types that are not directly linked to a particular model.
*
* 🟢 You can import this file directly.
*/
import type * as runtime from "@prisma/client/runtime/client"
import * as $Enums from "./enums.ts"
import type * as Prisma from "./internal/prismaNamespace.ts"
export type StringFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[]
notIn?: string[]
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedStringFilter<$PrismaModel> | string
}
export type DateTimeFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
in?: Date[] | string[]
notIn?: Date[] | string[]
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string
}
export type StringWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[]
notIn?: string[]
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedStringFilter<$PrismaModel>
_max?: Prisma.NestedStringFilter<$PrismaModel>
}
export type DateTimeWithAggregatesFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
in?: Date[] | string[]
notIn?: Date[] | string[]
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
}
export type StringNullableFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | null
notIn?: string[] | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
}
export type SortOrderInput = {
sort: Prisma.SortOrder
nulls?: Prisma.NullsOrder
}
export type StringNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | null
notIn?: string[] | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
}
export type IntNullableFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
in?: number[] | null
notIn?: number[] | null
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
}
export type BoolFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean
}
export type IntNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
in?: number[] | null
notIn?: number[] | null
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntNullableWithAggregatesFilter<$PrismaModel> | number | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>
_sum?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedIntNullableFilter<$PrismaModel>
_max?: Prisma.NestedIntNullableFilter<$PrismaModel>
}
export type BoolWithAggregatesFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedBoolFilter<$PrismaModel>
_max?: Prisma.NestedBoolFilter<$PrismaModel>
}
export type NestedStringFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[]
notIn?: string[]
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedStringFilter<$PrismaModel> | string
}
export type NestedDateTimeFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
in?: Date[] | string[]
notIn?: Date[] | string[]
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string
}
export type NestedStringWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[]
notIn?: string[]
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedStringFilter<$PrismaModel>
_max?: Prisma.NestedStringFilter<$PrismaModel>
}
export type NestedIntFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
in?: number[]
notIn?: number[]
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntFilter<$PrismaModel> | number
}
export type NestedDateTimeWithAggregatesFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
in?: Date[] | string[]
notIn?: Date[] | string[]
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
}
export type NestedStringNullableFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | null
notIn?: string[] | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
}
export type NestedStringNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | null
notIn?: string[] | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
}
export type NestedIntNullableFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
in?: number[] | null
notIn?: number[] | null
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
}
export type NestedBoolFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean
}
export type NestedIntNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
in?: number[] | null
notIn?: number[] | null
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntNullableWithAggregatesFilter<$PrismaModel> | number | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>
_sum?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedIntNullableFilter<$PrismaModel>
_max?: Prisma.NestedIntNullableFilter<$PrismaModel>
}
export type NestedFloatNullableFilter<$PrismaModel = never> = {
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> | null
in?: number[] | null
notIn?: number[] | null
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
not?: Prisma.NestedFloatNullableFilter<$PrismaModel> | number | null
}
export type NestedBoolWithAggregatesFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedBoolFilter<$PrismaModel>
_max?: Prisma.NestedBoolFilter<$PrismaModel>
}

View File

@@ -0,0 +1 @@
export * from "./index"

View File

@@ -0,0 +1,5 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
module.exports = { ...require('#main-entry-point') }

View File

@@ -0,0 +1 @@
export * from "./default"

View File

@@ -0,0 +1,165 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
Object.defineProperty(exports, "__esModule", { value: true });
const {
PrismaClientKnownRequestError,
PrismaClientUnknownRequestError,
PrismaClientRustPanicError,
PrismaClientInitializationError,
PrismaClientValidationError,
getPrismaClient,
sqltag,
empty,
join,
raw,
skip,
Decimal,
Debug,
DbNull,
JsonNull,
AnyNull,
NullTypes,
makeStrictEnum,
Extensions,
warnOnce,
defineDmmfProperty,
Public,
getRuntime,
createParam,
} = require('./runtime/wasm-compiler-edge.js')
const Prisma = {}
exports.Prisma = Prisma
exports.$Enums = {}
/**
* Prisma Client JS version: 7.2.0
* Query Engine version: 0c8ef2ce45c83248ab3df073180d5eda9e8be7a3
*/
Prisma.prismaVersion = {
client: "7.2.0",
engine: "0c8ef2ce45c83248ab3df073180d5eda9e8be7a3"
}
Prisma.PrismaClientKnownRequestError = PrismaClientKnownRequestError;
Prisma.PrismaClientUnknownRequestError = PrismaClientUnknownRequestError
Prisma.PrismaClientRustPanicError = PrismaClientRustPanicError
Prisma.PrismaClientInitializationError = PrismaClientInitializationError
Prisma.PrismaClientValidationError = PrismaClientValidationError
Prisma.Decimal = Decimal
/**
* Re-export of sql-template-tag
*/
Prisma.sql = sqltag
Prisma.empty = empty
Prisma.join = join
Prisma.raw = raw
Prisma.validator = Public.validator
/**
* Extensions
*/
Prisma.getExtensionContext = Extensions.getExtensionContext
Prisma.defineExtension = Extensions.defineExtension
/**
* Shorthand utilities for JSON filtering
*/
Prisma.DbNull = DbNull
Prisma.JsonNull = JsonNull
Prisma.AnyNull = AnyNull
Prisma.NullTypes = NullTypes
/**
* Enums
*/
exports.Prisma.TransactionIsolationLevel = makeStrictEnum({
Serializable: 'Serializable'
});
exports.Prisma.UserScalarFieldEnum = {
id: 'id',
username: 'username',
password: 'password',
displayName: 'displayName',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
exports.Prisma.SessionScalarFieldEnum = {
id: 'id',
userId: 'userId',
expiresAt: 'expiresAt'
};
exports.Prisma.UserPreferencesScalarFieldEnum = {
userId: 'userId',
toggleInputHotkey: 'toggleInputHotkey',
toggleOutputHotkey: 'toggleOutputHotkey'
};
exports.Prisma.ChannelScalarFieldEnum = {
id: 'id',
name: 'name',
maxClients: 'maxClients',
persistent: 'persistent',
owner_id: 'owner_id'
};
exports.Prisma.SortOrder = {
asc: 'asc',
desc: 'desc'
};
exports.Prisma.NullsOrder = {
first: 'first',
last: 'last'
};
exports.Prisma.ModelName = {
User: 'User',
Session: 'Session',
UserPreferences: 'UserPreferences',
Channel: 'Channel'
};
/**
* Create the Client
*/
const config = {
"previewFeatures": [],
"clientVersion": "7.2.0",
"engineVersion": "0c8ef2ce45c83248ab3df073180d5eda9e8be7a3",
"activeProvider": "sqlite",
"inlineSchema": "datasource db {\n provider = \"sqlite\"\n}\n\ngenerator client {\n provider = \"prisma-client-js\"\n output = \"./generated/client\"\n}\n\nmodel User {\n id String @id @default(cuid())\n username String @unique\n password String\n displayName String\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n Session Session[]\n UserPreferences UserPreferences?\n channels Channel[]\n}\n\nmodel Session {\n id String @id\n userId String\n expiresAt DateTime\n user User @relation(references: [id], fields: [userId], onDelete: Cascade)\n\n @@index([userId])\n}\n\nmodel UserPreferences {\n userId String @id\n toggleInputHotkey String? @default(\"\")\n toggleOutputHotkey String? @default(\"\")\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n}\n\nmodel Channel {\n id String @id\n name String\n maxClients Int?\n persistent Boolean @default(false)\n owner_id String?\n owner User? @relation(fields: [owner_id], references: [id], onDelete: Cascade)\n}\n"
}
config.runtimeDataModel = JSON.parse("{\"models\":{\"User\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"username\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"password\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"displayName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"Session\",\"kind\":\"object\",\"type\":\"Session\",\"relationName\":\"SessionToUser\"},{\"name\":\"UserPreferences\",\"kind\":\"object\",\"type\":\"UserPreferences\",\"relationName\":\"UserToUserPreferences\"},{\"name\":\"channels\",\"kind\":\"object\",\"type\":\"Channel\",\"relationName\":\"ChannelToUser\"}],\"dbName\":null},\"Session\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"expiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"SessionToUser\"}],\"dbName\":null},\"UserPreferences\":{\"fields\":[{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"toggleInputHotkey\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"toggleOutputHotkey\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"UserToUserPreferences\"}],\"dbName\":null},\"Channel\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"maxClients\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"persistent\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"owner_id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"owner\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"ChannelToUser\"}],\"dbName\":null}},\"enums\":{},\"types\":{}}")
defineDmmfProperty(exports.Prisma, config.runtimeDataModel)
config.compilerWasm = {
getRuntime: async () => require('./query_compiler_bg.js'),
getQueryCompilerWasmModule: async () => {
const loader = (await import('#wasm-compiler-loader')).default
const compiler = (await loader).default
return compiler
}
}
if (typeof globalThis !== 'undefined' && globalThis['DEBUG'] || (typeof process !== 'undefined' && process.env && process.env.DEBUG) || undefined) {
Debug.enable(typeof globalThis !== 'undefined' && globalThis['DEBUG'] || (typeof process !== 'undefined' && process.env && process.env.DEBUG) || undefined)
}
const PrismaClient = getPrismaClient(config)
exports.PrismaClient = PrismaClient
Object.assign(exports, Prisma)

View File

@@ -0,0 +1,15 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* This file exports all enum related types from the schema.
*
* 🟢 You can import this file directly.
*/
// This file is empty because there are no enums in the schema.
export {}

View File

@@ -0,0 +1,196 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
Object.defineProperty(exports, "__esModule", { value: true });
const {
Decimal,
DbNull,
JsonNull,
AnyNull,
NullTypes,
makeStrictEnum,
Public,
getRuntime,
skip
} = require('./runtime/index-browser.js')
const Prisma = {}
exports.Prisma = Prisma
exports.$Enums = {}
/**
* Prisma Client JS version: 7.2.0
* Query Engine version: 0c8ef2ce45c83248ab3df073180d5eda9e8be7a3
*/
Prisma.prismaVersion = {
client: "7.2.0",
engine: "0c8ef2ce45c83248ab3df073180d5eda9e8be7a3"
}
Prisma.PrismaClientKnownRequestError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientKnownRequestError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)};
Prisma.PrismaClientUnknownRequestError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientUnknownRequestError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.PrismaClientRustPanicError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientRustPanicError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.PrismaClientInitializationError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientInitializationError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.PrismaClientValidationError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientValidationError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.Decimal = Decimal
/**
* Re-export of sql-template-tag
*/
Prisma.sql = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`sqltag is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.empty = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`empty is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.join = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`join is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.raw = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`raw is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.validator = Public.validator
/**
* Extensions
*/
Prisma.getExtensionContext = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`Extensions.getExtensionContext is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.defineExtension = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`Extensions.defineExtension is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
/**
* Shorthand utilities for JSON filtering
*/
Prisma.DbNull = DbNull
Prisma.JsonNull = JsonNull
Prisma.AnyNull = AnyNull
Prisma.NullTypes = NullTypes
/**
* Enums
*/
exports.Prisma.TransactionIsolationLevel = makeStrictEnum({
Serializable: 'Serializable'
});
exports.Prisma.UserScalarFieldEnum = {
id: 'id',
username: 'username',
password: 'password',
displayName: 'displayName',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
exports.Prisma.SessionScalarFieldEnum = {
id: 'id',
userId: 'userId',
expiresAt: 'expiresAt'
};
exports.Prisma.UserPreferencesScalarFieldEnum = {
userId: 'userId',
toggleInputHotkey: 'toggleInputHotkey',
toggleOutputHotkey: 'toggleOutputHotkey'
};
exports.Prisma.ChannelScalarFieldEnum = {
id: 'id',
name: 'name',
maxClients: 'maxClients',
persistent: 'persistent',
owner_id: 'owner_id'
};
exports.Prisma.SortOrder = {
asc: 'asc',
desc: 'desc'
};
exports.Prisma.NullsOrder = {
first: 'first',
last: 'last'
};
exports.Prisma.ModelName = {
User: 'User',
Session: 'Session',
UserPreferences: 'UserPreferences',
Channel: 'Channel'
};
/**
* This is a stub Prisma Client that will error at runtime if called.
*/
class PrismaClient {
constructor() {
return new Proxy(this, {
get(target, prop) {
let message
const runtime = getRuntime()
if (runtime.isEdge) {
message = `PrismaClient is not configured to run in ${runtime.prettyName}. In order to run Prisma Client on edge runtime, either:
- Use Prisma Accelerate: https://pris.ly/d/accelerate
- Use Driver Adapters: https://pris.ly/d/driver-adapters
`;
} else {
message = 'PrismaClient is unable to run in this browser environment, or has been bundled for the browser (running in `' + runtime.prettyName + '`).'
}
message += `
If this is unexpected, please open an issue: https://pris.ly/prisma-prisma-bug-report`
throw new Error(message)
}
})
}
}
exports.PrismaClient = PrismaClient
Object.assign(exports, Prisma)

6981
server/prisma/generated/client/index.d.ts vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,165 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
Object.defineProperty(exports, "__esModule", { value: true });
const {
PrismaClientKnownRequestError,
PrismaClientUnknownRequestError,
PrismaClientRustPanicError,
PrismaClientInitializationError,
PrismaClientValidationError,
getPrismaClient,
sqltag,
empty,
join,
raw,
skip,
Decimal,
Debug,
DbNull,
JsonNull,
AnyNull,
NullTypes,
makeStrictEnum,
Extensions,
warnOnce,
defineDmmfProperty,
Public,
getRuntime,
createParam,
} = require('./runtime/client.js')
const Prisma = {}
exports.Prisma = Prisma
exports.$Enums = {}
/**
* Prisma Client JS version: 7.2.0
* Query Engine version: 0c8ef2ce45c83248ab3df073180d5eda9e8be7a3
*/
Prisma.prismaVersion = {
client: "7.2.0",
engine: "0c8ef2ce45c83248ab3df073180d5eda9e8be7a3"
}
Prisma.PrismaClientKnownRequestError = PrismaClientKnownRequestError;
Prisma.PrismaClientUnknownRequestError = PrismaClientUnknownRequestError
Prisma.PrismaClientRustPanicError = PrismaClientRustPanicError
Prisma.PrismaClientInitializationError = PrismaClientInitializationError
Prisma.PrismaClientValidationError = PrismaClientValidationError
Prisma.Decimal = Decimal
/**
* Re-export of sql-template-tag
*/
Prisma.sql = sqltag
Prisma.empty = empty
Prisma.join = join
Prisma.raw = raw
Prisma.validator = Public.validator
/**
* Extensions
*/
Prisma.getExtensionContext = Extensions.getExtensionContext
Prisma.defineExtension = Extensions.defineExtension
/**
* Shorthand utilities for JSON filtering
*/
Prisma.DbNull = DbNull
Prisma.JsonNull = JsonNull
Prisma.AnyNull = AnyNull
Prisma.NullTypes = NullTypes
const path = require('path')
/**
* Enums
*/
exports.Prisma.TransactionIsolationLevel = makeStrictEnum({
Serializable: 'Serializable'
});
exports.Prisma.UserScalarFieldEnum = {
id: 'id',
username: 'username',
password: 'password',
displayName: 'displayName',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
exports.Prisma.SessionScalarFieldEnum = {
id: 'id',
userId: 'userId',
expiresAt: 'expiresAt'
};
exports.Prisma.UserPreferencesScalarFieldEnum = {
userId: 'userId',
toggleInputHotkey: 'toggleInputHotkey',
toggleOutputHotkey: 'toggleOutputHotkey'
};
exports.Prisma.ChannelScalarFieldEnum = {
id: 'id',
name: 'name',
maxClients: 'maxClients',
persistent: 'persistent',
owner_id: 'owner_id'
};
exports.Prisma.SortOrder = {
asc: 'asc',
desc: 'desc'
};
exports.Prisma.NullsOrder = {
first: 'first',
last: 'last'
};
exports.Prisma.ModelName = {
User: 'User',
Session: 'Session',
UserPreferences: 'UserPreferences',
Channel: 'Channel'
};
/**
* Create the Client
*/
const config = {
"previewFeatures": [],
"clientVersion": "7.2.0",
"engineVersion": "0c8ef2ce45c83248ab3df073180d5eda9e8be7a3",
"activeProvider": "sqlite",
"inlineSchema": "datasource db {\n provider = \"sqlite\"\n}\n\ngenerator client {\n provider = \"prisma-client-js\"\n output = \"./generated/client\"\n}\n\nmodel User {\n id String @id @default(cuid())\n username String @unique\n password String\n displayName String\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n Session Session[]\n UserPreferences UserPreferences?\n channels Channel[]\n}\n\nmodel Session {\n id String @id\n userId String\n expiresAt DateTime\n user User @relation(references: [id], fields: [userId], onDelete: Cascade)\n\n @@index([userId])\n}\n\nmodel UserPreferences {\n userId String @id\n toggleInputHotkey String? @default(\"\")\n toggleOutputHotkey String? @default(\"\")\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n}\n\nmodel Channel {\n id String @id\n name String\n maxClients Int?\n persistent Boolean @default(false)\n owner_id String?\n owner User? @relation(fields: [owner_id], references: [id], onDelete: Cascade)\n}\n"
}
config.runtimeDataModel = JSON.parse("{\"models\":{\"User\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"username\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"password\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"displayName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"Session\",\"kind\":\"object\",\"type\":\"Session\",\"relationName\":\"SessionToUser\"},{\"name\":\"UserPreferences\",\"kind\":\"object\",\"type\":\"UserPreferences\",\"relationName\":\"UserToUserPreferences\"},{\"name\":\"channels\",\"kind\":\"object\",\"type\":\"Channel\",\"relationName\":\"ChannelToUser\"}],\"dbName\":null},\"Session\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"expiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"SessionToUser\"}],\"dbName\":null},\"UserPreferences\":{\"fields\":[{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"toggleInputHotkey\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"toggleOutputHotkey\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"UserToUserPreferences\"}],\"dbName\":null},\"Channel\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"maxClients\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"persistent\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"owner_id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"owner\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"ChannelToUser\"}],\"dbName\":null}},\"enums\":{},\"types\":{}}")
defineDmmfProperty(exports.Prisma, config.runtimeDataModel)
config.compilerWasm = {
getRuntime: async () => require('./query_compiler_bg.js'),
getQueryCompilerWasmModule: async () => {
const { Buffer } = require('node:buffer')
const { wasm } = require('./query_compiler_bg.wasm-base64.js')
const queryCompilerWasmFileBytes = Buffer.from(wasm, 'base64')
return new WebAssembly.Module(queryCompilerWasmFileBytes)
}
}
const PrismaClient = getPrismaClient(config)
exports.PrismaClient = PrismaClient
Object.assign(exports, Prisma)

View File

@@ -0,0 +1,220 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* WARNING: This is an internal file that is subject to change!
*
* 🛑 Under no circumstances should you import this file directly! 🛑
*
* Please import the `PrismaClient` class from the `client.ts` file instead.
*/
import * as runtime from "@prisma/client/runtime/client"
import type * as Prisma from "./prismaNamespace.ts"
const config: runtime.GetPrismaClientConfig = {
"previewFeatures": [],
"clientVersion": "7.2.0",
"engineVersion": "0c8ef2ce45c83248ab3df073180d5eda9e8be7a3",
"activeProvider": "sqlite",
"inlineSchema": "datasource db {\n provider = \"sqlite\"\n // url = env(\"DATABASE_URL\")\n}\n\ngenerator client {\n provider = \"prisma-client\"\n output = \"./generated/client\"\n}\n\nmodel User {\n id String @id @default(cuid())\n username String @unique\n password String\n displayName String\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n Session Session[]\n UserPreferences UserPreferences?\n channels Channel[]\n}\n\nmodel Session {\n id String @id\n userId String\n expiresAt DateTime\n user User @relation(references: [id], fields: [userId], onDelete: Cascade)\n\n @@index([userId])\n}\n\nmodel UserPreferences {\n userId String @id\n toggleInputHotkey String? @default(\"\")\n toggleOutputHotkey String? @default(\"\")\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n}\n\nmodel Channel {\n id String @id @default(cuid())\n name String\n maxClients Int?\n persistent Boolean @default(false)\n owner_id String?\n owner User? @relation(fields: [owner_id], references: [id], onDelete: Cascade)\n}\n",
"runtimeDataModel": {
"models": {},
"enums": {},
"types": {}
}
}
config.runtimeDataModel = JSON.parse("{\"models\":{\"User\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"username\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"password\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"displayName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"Session\",\"kind\":\"object\",\"type\":\"Session\",\"relationName\":\"SessionToUser\"},{\"name\":\"UserPreferences\",\"kind\":\"object\",\"type\":\"UserPreferences\",\"relationName\":\"UserToUserPreferences\"},{\"name\":\"channels\",\"kind\":\"object\",\"type\":\"Channel\",\"relationName\":\"ChannelToUser\"}],\"dbName\":null},\"Session\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"expiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"SessionToUser\"}],\"dbName\":null},\"UserPreferences\":{\"fields\":[{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"toggleInputHotkey\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"toggleOutputHotkey\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"UserToUserPreferences\"}],\"dbName\":null},\"Channel\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"maxClients\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"persistent\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"owner_id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"owner\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"ChannelToUser\"}],\"dbName\":null}},\"enums\":{},\"types\":{}}")
async function decodeBase64AsWasm(wasmBase64: string): Promise<WebAssembly.Module> {
const { Buffer } = await import('node:buffer')
const wasmArray = Buffer.from(wasmBase64, 'base64')
return new WebAssembly.Module(wasmArray)
}
config.compilerWasm = {
getRuntime: async () => await import("@prisma/client/runtime/query_compiler_bg.sqlite.mjs"),
getQueryCompilerWasmModule: async () => {
const { wasm } = await import("@prisma/client/runtime/query_compiler_bg.sqlite.wasm-base64.mjs")
return await decodeBase64AsWasm(wasm)
}
}
export type LogOptions<ClientOptions extends Prisma.PrismaClientOptions> =
'log' extends keyof ClientOptions ? ClientOptions['log'] extends Array<Prisma.LogLevel | Prisma.LogDefinition> ? Prisma.GetEvents<ClientOptions['log']> : never : never
export interface PrismaClientConstructor {
/**
* ## Prisma Client
*
* Type-safe database client for TypeScript
* @example
* ```
* const prisma = new PrismaClient()
* // Fetch zero or more Users
* const users = await prisma.user.findMany()
* ```
*
* Read more in our [docs](https://pris.ly/d/client).
*/
new <
Options extends Prisma.PrismaClientOptions = Prisma.PrismaClientOptions,
LogOpts extends LogOptions<Options> = LogOptions<Options>,
OmitOpts extends Prisma.PrismaClientOptions['omit'] = Options extends { omit: infer U } ? U : Prisma.PrismaClientOptions['omit'],
ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs
>(options: Prisma.Subset<Options, Prisma.PrismaClientOptions> ): PrismaClient<LogOpts, OmitOpts, ExtArgs>
}
/**
* ## Prisma Client
*
* Type-safe database client for TypeScript
* @example
* ```
* const prisma = new PrismaClient()
* // Fetch zero or more Users
* const users = await prisma.user.findMany()
* ```
*
* Read more in our [docs](https://pris.ly/d/client).
*/
export interface PrismaClient<
in LogOpts extends Prisma.LogLevel = never,
in out OmitOpts extends Prisma.PrismaClientOptions['omit'] = undefined,
in out ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs
> {
[K: symbol]: { types: Prisma.TypeMap<ExtArgs>['other'] }
$on<V extends LogOpts>(eventType: V, callback: (event: V extends 'query' ? Prisma.QueryEvent : Prisma.LogEvent) => void): PrismaClient;
/**
* Connect with the database
*/
$connect(): runtime.Types.Utils.JsPromise<void>;
/**
* Disconnect from the database
*/
$disconnect(): runtime.Types.Utils.JsPromise<void>;
/**
* Executes a prepared raw query and returns the number of affected rows.
* @example
* ```
* const result = await prisma.$executeRaw`UPDATE User SET cool = ${true} WHERE email = ${'user@email.com'};`
* ```
*
* Read more in our [docs](https://pris.ly/d/raw-queries).
*/
$executeRaw<T = unknown>(query: TemplateStringsArray | Prisma.Sql, ...values: any[]): Prisma.PrismaPromise<number>;
/**
* Executes a raw query and returns the number of affected rows.
* Susceptible to SQL injections, see documentation.
* @example
* ```
* const result = await prisma.$executeRawUnsafe('UPDATE User SET cool = $1 WHERE email = $2 ;', true, 'user@email.com')
* ```
*
* Read more in our [docs](https://pris.ly/d/raw-queries).
*/
$executeRawUnsafe<T = unknown>(query: string, ...values: any[]): Prisma.PrismaPromise<number>;
/**
* Performs a prepared raw query and returns the `SELECT` data.
* @example
* ```
* const result = await prisma.$queryRaw`SELECT * FROM User WHERE id = ${1} OR email = ${'user@email.com'};`
* ```
*
* Read more in our [docs](https://pris.ly/d/raw-queries).
*/
$queryRaw<T = unknown>(query: TemplateStringsArray | Prisma.Sql, ...values: any[]): Prisma.PrismaPromise<T>;
/**
* Performs a raw query and returns the `SELECT` data.
* Susceptible to SQL injections, see documentation.
* @example
* ```
* const result = await prisma.$queryRawUnsafe('SELECT * FROM User WHERE id = $1 OR email = $2;', 1, 'user@email.com')
* ```
*
* Read more in our [docs](https://pris.ly/d/raw-queries).
*/
$queryRawUnsafe<T = unknown>(query: string, ...values: any[]): Prisma.PrismaPromise<T>;
/**
* Allows the running of a sequence of read/write operations that are guaranteed to either succeed or fail as a whole.
* @example
* ```
* const [george, bob, alice] = await prisma.$transaction([
* prisma.user.create({ data: { name: 'George' } }),
* prisma.user.create({ data: { name: 'Bob' } }),
* prisma.user.create({ data: { name: 'Alice' } }),
* ])
* ```
*
* Read more in our [docs](https://www.prisma.io/docs/concepts/components/prisma-client/transactions).
*/
$transaction<P extends Prisma.PrismaPromise<any>[]>(arg: [...P], options?: { isolationLevel?: Prisma.TransactionIsolationLevel }): runtime.Types.Utils.JsPromise<runtime.Types.Utils.UnwrapTuple<P>>
$transaction<R>(fn: (prisma: Omit<PrismaClient, runtime.ITXClientDenyList>) => runtime.Types.Utils.JsPromise<R>, options?: { maxWait?: number, timeout?: number, isolationLevel?: Prisma.TransactionIsolationLevel }): runtime.Types.Utils.JsPromise<R>
$extends: runtime.Types.Extensions.ExtendsHook<"extends", Prisma.TypeMapCb<OmitOpts>, ExtArgs, runtime.Types.Utils.Call<Prisma.TypeMapCb<OmitOpts>, {
extArgs: ExtArgs
}>>
/**
* `prisma.user`: Exposes CRUD operations for the **User** model.
* Example usage:
* ```ts
* // Fetch zero or more Users
* const users = await prisma.user.findMany()
* ```
*/
get user(): Prisma.UserDelegate<ExtArgs, { omit: OmitOpts }>;
/**
* `prisma.session`: Exposes CRUD operations for the **Session** model.
* Example usage:
* ```ts
* // Fetch zero or more Sessions
* const sessions = await prisma.session.findMany()
* ```
*/
get session(): Prisma.SessionDelegate<ExtArgs, { omit: OmitOpts }>;
/**
* `prisma.userPreferences`: Exposes CRUD operations for the **UserPreferences** model.
* Example usage:
* ```ts
* // Fetch zero or more UserPreferences
* const userPreferences = await prisma.userPreferences.findMany()
* ```
*/
get userPreferences(): Prisma.UserPreferencesDelegate<ExtArgs, { omit: OmitOpts }>;
/**
* `prisma.channel`: Exposes CRUD operations for the **Channel** model.
* Example usage:
* ```ts
* // Fetch zero or more Channels
* const channels = await prisma.channel.findMany()
* ```
*/
get channel(): Prisma.ChannelDelegate<ExtArgs, { omit: OmitOpts }>;
}
export function getPrismaClientClass(): PrismaClientConstructor {
return runtime.getPrismaClient(config) as unknown as PrismaClientConstructor
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,128 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* WARNING: This is an internal file that is subject to change!
*
* 🛑 Under no circumstances should you import this file directly! 🛑
*
* All exports from this file are wrapped under a `Prisma` namespace object in the browser.ts file.
* While this enables partial backward compatibility, it is not part of the stable public API.
*
* If you are looking for your Models, Enums, and Input Types, please import them from the respective
* model files in the `model` directory!
*/
import * as runtime from "@prisma/client/runtime/index-browser"
export type * from '../models.ts'
export type * from './prismaNamespace.ts'
export const Decimal = runtime.Decimal
export const NullTypes = {
DbNull: runtime.NullTypes.DbNull as (new (secret: never) => typeof runtime.DbNull),
JsonNull: runtime.NullTypes.JsonNull as (new (secret: never) => typeof runtime.JsonNull),
AnyNull: runtime.NullTypes.AnyNull as (new (secret: never) => typeof runtime.AnyNull),
}
/**
* Helper for filtering JSON entries that have `null` on the database (empty on the db)
*
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
*/
export const DbNull = runtime.DbNull
/**
* Helper for filtering JSON entries that have JSON `null` values (not empty on the db)
*
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
*/
export const JsonNull = runtime.JsonNull
/**
* Helper for filtering JSON entries that are `Prisma.DbNull` or `Prisma.JsonNull`
*
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
*/
export const AnyNull = runtime.AnyNull
export const ModelName = {
User: 'User',
Session: 'Session',
UserPreferences: 'UserPreferences',
Channel: 'Channel'
} as const
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
/*
* Enums
*/
export const TransactionIsolationLevel = {
Serializable: 'Serializable'
} as const
export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel]
export const UserScalarFieldEnum = {
id: 'id',
username: 'username',
password: 'password',
displayName: 'displayName',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type UserScalarFieldEnum = (typeof UserScalarFieldEnum)[keyof typeof UserScalarFieldEnum]
export const SessionScalarFieldEnum = {
id: 'id',
userId: 'userId',
expiresAt: 'expiresAt'
} as const
export type SessionScalarFieldEnum = (typeof SessionScalarFieldEnum)[keyof typeof SessionScalarFieldEnum]
export const UserPreferencesScalarFieldEnum = {
userId: 'userId',
toggleInputHotkey: 'toggleInputHotkey',
toggleOutputHotkey: 'toggleOutputHotkey'
} as const
export type UserPreferencesScalarFieldEnum = (typeof UserPreferencesScalarFieldEnum)[keyof typeof UserPreferencesScalarFieldEnum]
export const ChannelScalarFieldEnum = {
id: 'id',
name: 'name',
maxClients: 'maxClients',
persistent: 'persistent',
owner_id: 'owner_id'
} as const
export type ChannelScalarFieldEnum = (typeof ChannelScalarFieldEnum)[keyof typeof ChannelScalarFieldEnum]
export const SortOrder = {
asc: 'asc',
desc: 'desc'
} as const
export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder]
export const NullsOrder = {
first: 'first',
last: 'last'
} as const
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder]

View File

@@ -0,0 +1,15 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* This is a barrel export file for all models and their related types.
*
* 🟢 You can import this file directly.
*/
export type * from './models/User.ts'
export type * from './models/Session.ts'
export type * from './models/UserPreferences.ts'
export type * from './models/Channel.ts'
export type * from './commonInputTypes.ts'

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,144 @@
{
"name": "prisma-client-bc59e606b3f744a8b15c18a26f89a0eb5328cd0b7e5ee2c8265028eef68b0317",
"main": "index.js",
"types": "index.d.ts",
"browser": "default.js",
"exports": {
"./client": {
"require": {
"node": "./index.js",
"edge-light": "./edge.js",
"workerd": "./edge.js",
"worker": "./edge.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"import": {
"node": "./index.js",
"edge-light": "./edge.js",
"workerd": "./edge.js",
"worker": "./edge.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"default": "./index.js"
},
"./package.json": "./package.json",
".": {
"require": {
"node": "./index.js",
"edge-light": "./edge.js",
"workerd": "./edge.js",
"worker": "./edge.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"import": {
"node": "./index.js",
"edge-light": "./edge.js",
"workerd": "./edge.js",
"worker": "./edge.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"default": "./index.js"
},
"./extension": {
"types": "./extension.d.ts",
"require": "./extension.js",
"import": "./extension.js",
"default": "./extension.js"
},
"./index-browser": {
"types": "./index.d.ts",
"require": "./index-browser.js",
"import": "./index-browser.js",
"default": "./index-browser.js"
},
"./index": {
"types": "./index.d.ts",
"require": "./index.js",
"import": "./index.js",
"default": "./index.js"
},
"./edge": {
"types": "./edge.d.ts",
"require": "./edge.js",
"import": "./edge.js",
"default": "./edge.js"
},
"./runtime/client": {
"types": "./runtime/client.d.ts",
"node": {
"require": "./runtime/client.js",
"default": "./runtime/client.js"
},
"require": "./runtime/client.js",
"import": "./runtime/client.mjs",
"default": "./runtime/client.mjs"
},
"./runtime/wasm-compiler-edge": {
"types": "./runtime/wasm-compiler-edge.d.ts",
"require": "./runtime/wasm-compiler-edge.js",
"import": "./runtime/wasm-compiler-edge.mjs",
"default": "./runtime/wasm-compiler-edge.mjs"
},
"./runtime/index-browser": {
"types": "./runtime/index-browser.d.ts",
"require": "./runtime/index-browser.js",
"import": "./runtime/index-browser.mjs",
"default": "./runtime/index-browser.mjs"
},
"./generator-build": {
"require": "./generator-build/index.js",
"import": "./generator-build/index.js",
"default": "./generator-build/index.js"
},
"./sql": {
"require": {
"types": "./sql.d.ts",
"node": "./sql.js",
"default": "./sql.js"
},
"import": {
"types": "./sql.d.ts",
"node": "./sql.mjs",
"default": "./sql.mjs"
},
"default": "./sql.js"
},
"./*": "./*"
},
"version": "7.2.0",
"sideEffects": false,
"dependencies": {
"@prisma/client-runtime-utils": "7.2.0"
},
"imports": {
"#wasm-compiler-loader": {
"edge-light": "./wasm-edge-light-loader.mjs",
"workerd": "./wasm-worker-loader.mjs",
"worker": "./wasm-worker-loader.mjs",
"default": "./wasm-worker-loader.mjs"
},
"#main-entry-point": {
"require": {
"node": "./index.js",
"edge-light": "./edge.js",
"workerd": "./edge.js",
"worker": "./edge.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"import": {
"node": "./index.js",
"edge-light": "./edge.js",
"workerd": "./edge.js",
"worker": "./edge.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"default": "./index.js"
}
}
}

View File

@@ -0,0 +1,2 @@
"use strict";var h=Object.defineProperty;var T=Object.getOwnPropertyDescriptor;var M=Object.getOwnPropertyNames;var j=Object.prototype.hasOwnProperty;var D=(e,t)=>{for(var n in t)h(e,n,{get:t[n],enumerable:!0})},O=(e,t,n,_)=>{if(t&&typeof t=="object"||typeof t=="function")for(let r of M(t))!j.call(e,r)&&r!==n&&h(e,r,{get:()=>t[r],enumerable:!(_=T(t,r))||_.enumerable});return e};var B=e=>O(h({},"__esModule",{value:!0}),e);var xe={};D(xe,{QueryCompiler:()=>F,__wbg_Error_e83987f665cf5504:()=>q,__wbg_Number_bb48ca12f395cd08:()=>C,__wbg_String_8f0eb39a4a4c2f66:()=>k,__wbg___wbindgen_boolean_get_6d5a1ee65bab5f68:()=>W,__wbg___wbindgen_debug_string_df47ffb5e35e6763:()=>V,__wbg___wbindgen_in_bb933bd9e1b3bc0f:()=>z,__wbg___wbindgen_is_object_c818261d21f283a4:()=>L,__wbg___wbindgen_is_string_fbb76cb2940daafd:()=>P,__wbg___wbindgen_is_undefined_2d472862bd29a478:()=>Q,__wbg___wbindgen_jsval_loose_eq_b664b38a2f582147:()=>Y,__wbg___wbindgen_number_get_a20bf9b85341449d:()=>G,__wbg___wbindgen_string_get_e4f06c90489ad01b:()=>J,__wbg___wbindgen_throw_b855445ff6a94295:()=>X,__wbg_entries_e171b586f8f6bdbf:()=>H,__wbg_getTime_14776bfb48a1bff9:()=>K,__wbg_get_7bed016f185add81:()=>Z,__wbg_get_with_ref_key_1dc361bd10053bfe:()=>v,__wbg_instanceof_ArrayBuffer_70beb1189ca63b38:()=>ee,__wbg_instanceof_Uint8Array_20c8e73002f7af98:()=>te,__wbg_isSafeInteger_d216eda7911dde36:()=>ne,__wbg_length_69bca3cb64fc8748:()=>re,__wbg_length_cdd215e10d9dd507:()=>_e,__wbg_new_0_f9740686d739025c:()=>oe,__wbg_new_1acc0b6eea89d040:()=>ce,__wbg_new_5a79be3ab53b8aa5:()=>ie,__wbg_new_68651c719dcda04e:()=>se,__wbg_new_e17d9f43105b08be:()=>ue,__wbg_prototypesetcall_2a6620b6922694b2:()=>fe,__wbg_set_3f1d0b984ed272ed:()=>be,__wbg_set_907fb406c34a251d:()=>de,__wbg_set_c213c871859d6500:()=>ae,__wbg_set_message_82ae475bb413aa5c:()=>ge,__wbg_set_wasm:()=>N,__wbindgen_cast_2241b6af4c4b2941:()=>le,__wbindgen_cast_4625c577ab2ec9ee:()=>we,__wbindgen_cast_9ae0607507abb057:()=>pe,__wbindgen_cast_d6cd19b81560fd6e:()=>ye,__wbindgen_init_externref_table:()=>me});module.exports=B(xe);var A=()=>{};A.prototype=A;let o;function N(e){o=e}let p=null;function a(){return(p===null||p.byteLength===0)&&(p=new Uint8Array(o.memory.buffer)),p}let y=new TextDecoder("utf-8",{ignoreBOM:!0,fatal:!0});y.decode();const U=2146435072;let S=0;function R(e,t){return S+=t,S>=U&&(y=new TextDecoder("utf-8",{ignoreBOM:!0,fatal:!0}),y.decode(),S=t),y.decode(a().subarray(e,e+t))}function m(e,t){return e=e>>>0,R(e,t)}let f=0;const g=new TextEncoder;"encodeInto"in g||(g.encodeInto=function(e,t){const n=g.encode(e);return t.set(n),{read:e.length,written:n.length}});function l(e,t,n){if(n===void 0){const i=g.encode(e),d=t(i.length,1)>>>0;return a().subarray(d,d+i.length).set(i),f=i.length,d}let _=e.length,r=t(_,1)>>>0;const s=a();let c=0;for(;c<_;c++){const i=e.charCodeAt(c);if(i>127)break;s[r+c]=i}if(c!==_){c!==0&&(e=e.slice(c)),r=n(r,_,_=c+e.length*3,1)>>>0;const i=a().subarray(r+c,r+_),d=g.encodeInto(e,i);c+=d.written,r=n(r,_,c,1)>>>0}return f=c,r}let b=null;function u(){return(b===null||b.buffer.detached===!0||b.buffer.detached===void 0&&b.buffer!==o.memory.buffer)&&(b=new DataView(o.memory.buffer)),b}function x(e){return e==null}function I(e){const t=typeof e;if(t=="number"||t=="boolean"||e==null)return`${e}`;if(t=="string")return`"${e}"`;if(t=="symbol"){const r=e.description;return r==null?"Symbol":`Symbol(${r})`}if(t=="function"){const r=e.name;return typeof r=="string"&&r.length>0?`Function(${r})`:"Function"}if(Array.isArray(e)){const r=e.length;let s="[";r>0&&(s+=I(e[0]));for(let c=1;c<r;c++)s+=", "+I(e[c]);return s+="]",s}const n=/\[object ([^\]]+)\]/.exec(toString.call(e));let _;if(n&&n.length>1)_=n[1];else return toString.call(e);if(_=="Object")try{return"Object("+JSON.stringify(e)+")"}catch{return"Object"}return e instanceof Error?`${e.name}: ${e.message}
${e.stack}`:_}function $(e,t){return e=e>>>0,a().subarray(e/1,e/1+t)}function w(e){const t=o.__wbindgen_externrefs.get(e);return o.__externref_table_dealloc(e),t}const E=typeof FinalizationRegistry>"u"?{register:()=>{},unregister:()=>{}}:new FinalizationRegistry(e=>o.__wbg_querycompiler_free(e>>>0,1));class F{__destroy_into_raw(){const t=this.__wbg_ptr;return this.__wbg_ptr=0,E.unregister(this),t}free(){const t=this.__destroy_into_raw();o.__wbg_querycompiler_free(t,0)}compileBatch(t){const n=l(t,o.__wbindgen_malloc,o.__wbindgen_realloc),_=f,r=o.querycompiler_compileBatch(this.__wbg_ptr,n,_);if(r[2])throw w(r[1]);return w(r[0])}constructor(t){const n=o.querycompiler_new(t);if(n[2])throw w(n[1]);return this.__wbg_ptr=n[0]>>>0,E.register(this,this.__wbg_ptr,this),this}compile(t){const n=l(t,o.__wbindgen_malloc,o.__wbindgen_realloc),_=f,r=o.querycompiler_compile(this.__wbg_ptr,n,_);if(r[2])throw w(r[1]);return w(r[0])}}Symbol.dispose&&(F.prototype[Symbol.dispose]=F.prototype.free);function q(e,t){return Error(m(e,t))}function C(e){return Number(e)}function k(e,t){const n=String(t),_=l(n,o.__wbindgen_malloc,o.__wbindgen_realloc),r=f;u().setInt32(e+4*1,r,!0),u().setInt32(e+4*0,_,!0)}function W(e){const t=e,n=typeof t=="boolean"?t:void 0;return x(n)?16777215:n?1:0}function V(e,t){const n=I(t),_=l(n,o.__wbindgen_malloc,o.__wbindgen_realloc),r=f;u().setInt32(e+4*1,r,!0),u().setInt32(e+4*0,_,!0)}function z(e,t){return e in t}function L(e){const t=e;return typeof t=="object"&&t!==null}function P(e){return typeof e=="string"}function Q(e){return e===void 0}function Y(e,t){return e==t}function G(e,t){const n=t,_=typeof n=="number"?n:void 0;u().setFloat64(e+8*1,x(_)?0:_,!0),u().setInt32(e+4*0,!x(_),!0)}function J(e,t){const n=t,_=typeof n=="string"?n:void 0;var r=x(_)?0:l(_,o.__wbindgen_malloc,o.__wbindgen_realloc),s=f;u().setInt32(e+4*1,s,!0),u().setInt32(e+4*0,r,!0)}function X(e,t){throw new Error(m(e,t))}function H(e){return Object.entries(e)}function K(e){return e.getTime()}function Z(e,t){return e[t>>>0]}function v(e,t){return e[t]}function ee(e){let t;try{t=e instanceof ArrayBuffer}catch{t=!1}return t}function te(e){let t;try{t=e instanceof Uint8Array}catch{t=!1}return t}function ne(e){return Number.isSafeInteger(e)}function re(e){return e.length}function _e(e){return e.length}function oe(){return new Date}function ce(){return new Object}function ie(e){return new Uint8Array(e)}function se(){return new Map}function ue(){return new Array}function fe(e,t,n){Uint8Array.prototype.set.call($(e,t),n)}function be(e,t,n){e[t]=n}function de(e,t,n){return e.set(t,n)}function ae(e,t,n){e[t>>>0]=n}function ge(e,t){global.PRISMA_WASM_PANIC_REGISTRY.set_message(m(e,t))}function le(e,t){return m(e,t)}function we(e){return BigInt.asUintN(64,e)}function pe(e){return e}function ye(e){return e}function me(){const e=o.__wbindgen_externrefs,t=e.grow(4);e.set(0,void 0),e.set(t+0,void 0),e.set(t+1,null),e.set(t+2,!0),e.set(t+3,!1)}0&&(module.exports={QueryCompiler,__wbg_Error_e83987f665cf5504,__wbg_Number_bb48ca12f395cd08,__wbg_String_8f0eb39a4a4c2f66,__wbg___wbindgen_boolean_get_6d5a1ee65bab5f68,__wbg___wbindgen_debug_string_df47ffb5e35e6763,__wbg___wbindgen_in_bb933bd9e1b3bc0f,__wbg___wbindgen_is_object_c818261d21f283a4,__wbg___wbindgen_is_string_fbb76cb2940daafd,__wbg___wbindgen_is_undefined_2d472862bd29a478,__wbg___wbindgen_jsval_loose_eq_b664b38a2f582147,__wbg___wbindgen_number_get_a20bf9b85341449d,__wbg___wbindgen_string_get_e4f06c90489ad01b,__wbg___wbindgen_throw_b855445ff6a94295,__wbg_entries_e171b586f8f6bdbf,__wbg_getTime_14776bfb48a1bff9,__wbg_get_7bed016f185add81,__wbg_get_with_ref_key_1dc361bd10053bfe,__wbg_instanceof_ArrayBuffer_70beb1189ca63b38,__wbg_instanceof_Uint8Array_20c8e73002f7af98,__wbg_isSafeInteger_d216eda7911dde36,__wbg_length_69bca3cb64fc8748,__wbg_length_cdd215e10d9dd507,__wbg_new_0_f9740686d739025c,__wbg_new_1acc0b6eea89d040,__wbg_new_5a79be3ab53b8aa5,__wbg_new_68651c719dcda04e,__wbg_new_e17d9f43105b08be,__wbg_prototypesetcall_2a6620b6922694b2,__wbg_set_3f1d0b984ed272ed,__wbg_set_907fb406c34a251d,__wbg_set_c213c871859d6500,__wbg_set_message_82ae475bb413aa5c,__wbg_set_wasm,__wbindgen_cast_2241b6af4c4b2941,__wbindgen_cast_4625c577ab2ec9ee,__wbindgen_cast_9ae0607507abb057,__wbindgen_cast_d6cd19b81560fd6e,__wbindgen_init_externref_table});

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,87 @@
import { AnyNull } from '@prisma/client-runtime-utils';
import { DbNull } from '@prisma/client-runtime-utils';
import { Decimal } from '@prisma/client-runtime-utils';
import { isAnyNull } from '@prisma/client-runtime-utils';
import { isDbNull } from '@prisma/client-runtime-utils';
import { isJsonNull } from '@prisma/client-runtime-utils';
import { JsonNull } from '@prisma/client-runtime-utils';
import { NullTypes } from '@prisma/client-runtime-utils';
export { AnyNull }
declare type Args<T, F extends Operation> = T extends {
[K: symbol]: {
types: {
operations: {
[K in F]: {
args: any;
};
};
};
};
} ? T[symbol]['types']['operations'][F]['args'] : any;
export { DbNull }
export { Decimal }
declare type Exact<A, W> = (A extends unknown ? (W extends A ? {
[K in keyof A]: Exact<A[K], W[K]>;
} : W) : never) | (A extends Narrowable ? A : never);
export declare function getRuntime(): GetRuntimeOutput;
declare type GetRuntimeOutput = {
id: RuntimeName;
prettyName: string;
isEdge: boolean;
};
export { isAnyNull }
export { isDbNull }
export { isJsonNull }
export { JsonNull }
/**
* Generates more strict variant of an enum which, unlike regular enum,
* throws on non-existing property access. This can be useful in following situations:
* - we have an API, that accepts both `undefined` and `SomeEnumType` as an input
* - enum values are generated dynamically from DMMF.
*
* In that case, if using normal enums and no compile-time typechecking, using non-existing property
* will result in `undefined` value being used, which will be accepted. Using strict enum
* in this case will help to have a runtime exception, telling you that you are probably doing something wrong.
*
* Note: if you need to check for existence of a value in the enum you can still use either
* `in` operator or `hasOwnProperty` function.
*
* @param definition
* @returns
*/
export declare function makeStrictEnum<T extends Record<PropertyKey, string | number>>(definition: T): T;
declare type Narrowable = string | number | bigint | boolean | [];
export { NullTypes }
declare type Operation = 'findFirst' | 'findFirstOrThrow' | 'findUnique' | 'findUniqueOrThrow' | 'findMany' | 'create' | 'createMany' | 'createManyAndReturn' | 'update' | 'updateMany' | 'updateManyAndReturn' | 'upsert' | 'delete' | 'deleteMany' | 'aggregate' | 'count' | 'groupBy' | '$queryRaw' | '$executeRaw' | '$queryRawUnsafe' | '$executeRawUnsafe' | 'findRaw' | 'aggregateRaw' | '$runCommandRaw';
declare namespace Public {
export {
validator
}
}
export { Public }
declare type RuntimeName = 'workerd' | 'deno' | 'netlify' | 'node' | 'bun' | 'edge-light' | '';
declare function validator<V>(): <S>(select: Exact<S, V>) => S;
declare function validator<C, M extends Exclude<keyof C, `$${string}`>, O extends keyof C[M] & Operation>(client: C, model: M, operation: O): <S>(select: Exact<S, Args<C[M], O>>) => S;
declare function validator<C, M extends Exclude<keyof C, `$${string}`>, O extends keyof C[M] & Operation, P extends keyof Args<C[M], O>>(client: C, model: M, operation: O, prop: P): <S>(select: Exact<S, Args<C[M], O>[P]>) => S;
export { }

View File

@@ -0,0 +1,6 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
"use strict";var s=Object.defineProperty;var g=Object.getOwnPropertyDescriptor;var p=Object.getOwnPropertyNames;var f=Object.prototype.hasOwnProperty;var a=(e,t)=>{for(var n in t)s(e,n,{get:t[n],enumerable:!0})},y=(e,t,n,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of p(t))!f.call(e,i)&&i!==n&&s(e,i,{get:()=>t[i],enumerable:!(r=g(t,i))||r.enumerable});return e};var x=e=>y(s({},"__esModule",{value:!0}),e);var O={};a(O,{AnyNull:()=>o.AnyNull,DbNull:()=>o.DbNull,Decimal:()=>m.Decimal,JsonNull:()=>o.JsonNull,NullTypes:()=>o.NullTypes,Public:()=>l,getRuntime:()=>c,isAnyNull:()=>o.isAnyNull,isDbNull:()=>o.isDbNull,isJsonNull:()=>o.isJsonNull,makeStrictEnum:()=>u});module.exports=x(O);var l={};a(l,{validator:()=>d});function d(...e){return t=>t}var b=new Set(["toJSON","$$typeof","asymmetricMatch",Symbol.iterator,Symbol.toStringTag,Symbol.isConcatSpreadable,Symbol.toPrimitive]);function u(e){return new Proxy(e,{get(t,n){if(n in t)return t[n];if(!b.has(n))throw new TypeError("Invalid enum value: ".concat(String(n)))}})}var N=()=>{var e,t;return((t=(e=globalThis.process)==null?void 0:e.release)==null?void 0:t.name)==="node"},S=()=>{var e,t;return!!globalThis.Bun||!!((t=(e=globalThis.process)==null?void 0:e.versions)!=null&&t.bun)},E=()=>!!globalThis.Deno,R=()=>typeof globalThis.Netlify=="object",h=()=>typeof globalThis.EdgeRuntime=="object",C=()=>{var e;return((e=globalThis.navigator)==null?void 0:e.userAgent)==="Cloudflare-Workers"};function k(){var n;return(n=[[R,"netlify"],[h,"edge-light"],[C,"workerd"],[E,"deno"],[S,"bun"],[N,"node"]].flatMap(r=>r[0]()?[r[1]]:[]).at(0))!=null?n:""}var M={node:"Node.js",workerd:"Cloudflare Workers",deno:"Deno and Deno Deploy",netlify:"Netlify Edge Functions","edge-light":"Edge Runtime (Vercel Edge Functions, Vercel Edge Middleware, Next.js (Pages Router) Edge API Routes, Next.js (App Router) Edge Route Handlers or Next.js Middleware)"};function c(){let e=k();return{id:e,prettyName:M[e]||e,isEdge:["workerd","deno","netlify","edge-light"].includes(e)}}var o=require("@prisma/client-runtime-utils"),m=require("@prisma/client-runtime-utils");0&&(module.exports={AnyNull,DbNull,Decimal,JsonNull,NullTypes,Public,getRuntime,isAnyNull,isDbNull,isJsonNull,makeStrictEnum});
//# sourceMappingURL=index-browser.js.map

Some files were not shown because too many files have changed in this diff Show More