Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 92f7bae50d | |||
| 7b2cfb4b56 | |||
| 363f1008c6 | |||
| 65583b1564 |
7
client/.claude/settings.local.json
Normal file
7
client/.claude/settings.local.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(printf '%s\\\\n' \"\n================================================================================\nCOMPREHENSIVE ARCHITECTURE REPORT\nTauri 2 + Nuxt 4 + mediasoup-client Voice Chat App\n================================================================================\")"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
118
client/CLAUDE.md
Normal file
118
client/CLAUDE.md
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
Chad is a voice/video chat desktop app built with **Tauri 2 + Nuxt 4 (SPA) + mediasoup-client**. It uses an SFU (Selective Forwarding Unit) architecture for WebRTC media — each peer sends to the server, which forwards selectively to others.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `yarn dev` | Start dev server (binds to all interfaces via `--host`) |
|
||||||
|
| `yarn generate` | Generate static site to `.output/public` |
|
||||||
|
| `yarn build` | Build for production |
|
||||||
|
| `yarn preview` | Preview production build |
|
||||||
|
| `npx eslint .` | Lint the project |
|
||||||
|
| `npx eslint --fix .` | Lint and auto-fix |
|
||||||
|
|
||||||
|
**Tauri desktop build**: `yarn tauri build` (runs `yarn generate` first automatically).
|
||||||
|
|
||||||
|
Package manager is **Yarn 4.12.0**. No test framework is configured.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Composable-Based State (No Pinia/Vuex)
|
||||||
|
|
||||||
|
All state is managed through Vue composables using `@vueuse/core`'s `createGlobalState` and `createSharedComposable`. Nuxt auto-imports all composables from `app/composables/`.
|
||||||
|
|
||||||
|
**Dependency graph** (arrows = "depends on"):
|
||||||
|
|
||||||
|
```
|
||||||
|
useAuth (foundation — user session)
|
||||||
|
└→ useSignaling (Socket.IO connection to /webrtc namespace)
|
||||||
|
└→ useClients (connected peer list)
|
||||||
|
└→ useMediasoup (transports, producers, consumers)
|
||||||
|
├← usePreferences (device IDs, audio effects, hotkeys)
|
||||||
|
│ └← useDevices (enumerates hardware via getUserMedia)
|
||||||
|
└← useSfx (Howler.js sound effects)
|
||||||
|
|
||||||
|
useApp (top-level orchestrator — mute/unmute, video/share toggles)
|
||||||
|
└→ depends on useClients, useMediasoup, useSignaling, useSfx
|
||||||
|
```
|
||||||
|
|
||||||
|
### Composable Tiers
|
||||||
|
|
||||||
|
**Global state** (`createGlobalState` — single instance, persists for app lifetime):
|
||||||
|
- `useApp` — ready state, input/output mute, video/share toggles, version info
|
||||||
|
- `useAuth` — `me` ref, login/register/logout
|
||||||
|
- `useClients` — `clients[]` array, add/remove/update peers
|
||||||
|
- `usePreferences` — device selections, audio effects, hotkeys (localStorage + server sync)
|
||||||
|
- `useUpdater` — Tauri app update checks
|
||||||
|
- `useFullscreenVideo` — fullscreen video element control
|
||||||
|
|
||||||
|
**Shared** (`createSharedComposable` — single instance, disposed when last consumer unmounts):
|
||||||
|
- `useMediasoup` — mediasoup Device, transports, producers, consumers, speaking state
|
||||||
|
- `useSignaling` — Socket.IO client, connect/disconnect
|
||||||
|
- `useSfx` — sound effect playback via Howler.js
|
||||||
|
|
||||||
|
**Per-instance** (new instance per call):
|
||||||
|
- `useClient(socketId)` — per-peer volume/mute (localStorage), filtered consumers/producers
|
||||||
|
- `useAudioContext(audioTrack)` — Web Audio API gain node for per-client volume
|
||||||
|
- `useDevices` — media device enumeration
|
||||||
|
|
||||||
|
### Initialization Flow
|
||||||
|
|
||||||
|
1. **Middleware** (global, ordered by filename prefix):
|
||||||
|
- `00.updater` — Tauri update check, redirects to `/updater` if available
|
||||||
|
- `01.auth` — validates session via `GET /me`, redirects guests to `/login`
|
||||||
|
- `02.user-preferences` — loads server-synced preferences for authenticated users
|
||||||
|
2. **Plugins**: build info logging, Tauri hotkey registration
|
||||||
|
3. **Layout** (`default.vue`): calls `useApp()` which triggers `useSignaling().connect()`
|
||||||
|
4. **Socket authenticated** → mediasoup Device created → transports created → `join()` → mic enabled
|
||||||
|
|
||||||
|
### mediasoup Integration
|
||||||
|
|
||||||
|
**Producer types** (tracked in `appData.source`):
|
||||||
|
- Microphone: `kind='audio'`, `source='mic-video'`
|
||||||
|
- Camera: `kind='video'`, `source='mic-video'`
|
||||||
|
- Screen share: `kind='video'`, `source='share'`
|
||||||
|
|
||||||
|
**Consumer filtering** uses `kind` + `appData.source` to distinguish audio/video/share streams.
|
||||||
|
|
||||||
|
Transports use `emitWithAck()` on the Socket.IO connection for all signaling (create, connect, produce, join, pause/resume/close).
|
||||||
|
|
||||||
|
### API & Networking
|
||||||
|
|
||||||
|
- **REST API**: `shared/chad-api.ts` — `$fetch` instance with `credentials: 'include'`, base URL from `__API_BASE_URL__` build-time define
|
||||||
|
- **WebSocket**: Socket.IO to `/webrtc` namespace for signaling
|
||||||
|
- **Vite proxy**: `/api` → production API server (for dev)
|
||||||
|
|
||||||
|
### Shared Types
|
||||||
|
|
||||||
|
`shared/types.ts` defines `ChadClient`, `Consumer`, `Producer`, `AppData`, `UpdatedClient` — used by both composables and components.
|
||||||
|
|
||||||
|
## Code Conventions
|
||||||
|
|
||||||
|
- **ESLint**: `@antfu/eslint-config` with formatters enabled
|
||||||
|
- **Vue SFC block order**: `<template>`, `<script>`, `<style>` (enforced by ESLint)
|
||||||
|
- **`console.log` allowed** (no-console is off)
|
||||||
|
- **PrimeVue components** are prefixed: `PrimeButton`, `PrimeCard`, etc.
|
||||||
|
- **Icons**: `lucide-vue-next` + `primeicons`
|
||||||
|
- **Styling**: Tailwind CSS 4 + PrimeVue Aura theme (Zinc palette, dark-first)
|
||||||
|
- **Reactivity**: mediasoup objects wrapped with `markRaw()` to prevent deep reactivity; `triggerRef()` used for manual reactivity triggers on the clients array
|
||||||
|
|
||||||
|
## Build-Time Defines
|
||||||
|
|
||||||
|
Set via `.env` or environment variables:
|
||||||
|
- `API_BASE_URL` — backend API base (default: `http://localhost:4000/chad`)
|
||||||
|
- `COMMIT_SHA` — git commit hash (default: `'local'`)
|
||||||
|
|
||||||
|
## Tauri
|
||||||
|
|
||||||
|
- App identifier: `xyz.koptilnya.chad`
|
||||||
|
- Windows-only NSIS installer (`bundle.targets: ["nsis"]`)
|
||||||
|
- Plugins: global-shortcut, process, updater, single-instance, log
|
||||||
|
- Frontend dist: `.output/public` (from `yarn generate`)
|
||||||
|
- `@tauri-apps/plugin-*` packages provide JS bindings; use `window.__TAURI__` check or the `useTauri()` composable for Tauri detection
|
||||||
5
client/app/components.d.ts
vendored
5
client/app/components.d.ts
vendored
@@ -13,18 +13,13 @@ declare module 'vue' {
|
|||||||
PrimeButton: typeof import('primevue/button')['default']
|
PrimeButton: typeof import('primevue/button')['default']
|
||||||
PrimeButtonGroup: typeof import('primevue/buttongroup')['default']
|
PrimeButtonGroup: typeof import('primevue/buttongroup')['default']
|
||||||
PrimeCard: typeof import('primevue/card')['default']
|
PrimeCard: typeof import('primevue/card')['default']
|
||||||
PrimeDivider: typeof import('primevue/divider')['default']
|
|
||||||
PrimeFloatLabel: typeof import('primevue/floatlabel')['default']
|
PrimeFloatLabel: typeof import('primevue/floatlabel')['default']
|
||||||
PrimeInputText: typeof import('primevue/inputtext')['default']
|
PrimeInputText: typeof import('primevue/inputtext')['default']
|
||||||
PrimePassword: typeof import('primevue/password')['default']
|
PrimePassword: typeof import('primevue/password')['default']
|
||||||
PrimeProgressBar: typeof import('primevue/progressbar')['default']
|
|
||||||
PrimeScrollPanel: typeof import('primevue/scrollpanel')['default']
|
PrimeScrollPanel: typeof import('primevue/scrollpanel')['default']
|
||||||
PrimeSelect: typeof import('primevue/select')['default']
|
|
||||||
PrimeSelectButton: typeof import('primevue/selectbutton')['default']
|
PrimeSelectButton: typeof import('primevue/selectbutton')['default']
|
||||||
PrimeSlider: typeof import('primevue/slider')['default']
|
PrimeSlider: typeof import('primevue/slider')['default']
|
||||||
PrimeTag: typeof import('primevue/tag')['default']
|
|
||||||
PrimeToast: typeof import('primevue/toast')['default']
|
PrimeToast: typeof import('primevue/toast')['default']
|
||||||
PrimeToggleSwitch: typeof import('primevue/toggleswitch')['default']
|
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,11 +16,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { ChadClient } from '#shared/types'
|
import type { ChadClient, Consumer, Producer } from '#shared/types'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
client: ChadClient
|
client: ChadClient
|
||||||
stream: MediaStream
|
consumer?: Consumer
|
||||||
|
producer?: Producer
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const { me } = useClients()
|
const { me } = useClients()
|
||||||
@@ -30,8 +31,21 @@ const isMe = computed(() => {
|
|||||||
return props.client.socketId === me.value?.socketId
|
return props.client.socketId === me.value?.socketId
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const track = computed(() => {
|
||||||
|
return props.consumer?.raw.track ?? props.producer?.raw.track
|
||||||
|
})
|
||||||
|
|
||||||
|
const stream = computed<MediaStream | undefined>((previousStream) => {
|
||||||
|
if (previousStream?.getTracks()[0] === track.value) {
|
||||||
|
return previousStream
|
||||||
|
}
|
||||||
|
|
||||||
|
return track.value ? new MediaStream([track.value]) : undefined
|
||||||
|
})
|
||||||
|
|
||||||
function watch() {
|
function watch() {
|
||||||
fullscreenVideo.show(props.stream)
|
if (stream.value)
|
||||||
|
fullscreenVideo.show(stream.value)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ export const useApp = createGlobalState(() => {
|
|||||||
const { clients } = useClients()
|
const { clients } = useClients()
|
||||||
const mediasoup = useMediasoup()
|
const mediasoup = useMediasoup()
|
||||||
const signaling = useSignaling()
|
const signaling = useSignaling()
|
||||||
const toast = useToast()
|
const { emit } = useEventBus()
|
||||||
const sfx = useSfx()
|
|
||||||
|
|
||||||
const ready = ref(false)
|
const ready = ref(false)
|
||||||
const isTauri = computed(() => '__TAURI_INTERNALS__' in window)
|
const isTauri = computed(() => '__TAURI_INTERNALS__' in window)
|
||||||
@@ -53,8 +52,7 @@ export const useApp = createGlobalState(() => {
|
|||||||
|
|
||||||
await mediasoup.pauseProducer(mediasoup.micProducer.value)
|
await mediasoup.pauseProducer(mediasoup.micProducer.value)
|
||||||
|
|
||||||
sfx.playEvent('mic-off').then()
|
emit('audio:muted')
|
||||||
toast.add({ severity: 'info', summary: 'Microphone muted', closable: false, life: 1000 })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function unmuteInput() {
|
async function unmuteInput() {
|
||||||
@@ -67,8 +65,7 @@ export const useApp = createGlobalState(() => {
|
|||||||
|
|
||||||
await mediasoup.resumeProducer(mediasoup.micProducer.value)
|
await mediasoup.resumeProducer(mediasoup.micProducer.value)
|
||||||
|
|
||||||
sfx.playEvent('mic-on').then()
|
emit('audio:unmuted')
|
||||||
toast.add({ severity: 'info', summary: 'Microphone activated', closable: false, life: 1000 })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleInput() {
|
async function toggleInput() {
|
||||||
@@ -92,7 +89,7 @@ export const useApp = createGlobalState(() => {
|
|||||||
outputMuted: true,
|
outputMuted: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
toast.add({ severity: 'info', summary: 'Sound muted', closable: false, life: 1000 })
|
emit('output:muted')
|
||||||
}
|
}
|
||||||
|
|
||||||
async function unmuteOutput() {
|
async function unmuteOutput() {
|
||||||
@@ -105,7 +102,7 @@ export const useApp = createGlobalState(() => {
|
|||||||
outputMuted: false,
|
outputMuted: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
toast.add({ severity: 'info', summary: 'Sound resumed', closable: false, life: 1000 })
|
emit('output:unmuted')
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleOutput() {
|
async function toggleOutput() {
|
||||||
@@ -118,22 +115,22 @@ export const useApp = createGlobalState(() => {
|
|||||||
async function toggleVideo() {
|
async function toggleVideo() {
|
||||||
if (!mediasoup.videoProducer.value) {
|
if (!mediasoup.videoProducer.value) {
|
||||||
await mediasoup.enableVideo()
|
await mediasoup.enableVideo()
|
||||||
await sfx.playEvent('stream-on')
|
emit('video:enabled')
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
await mediasoup.disableProducer(mediasoup.videoProducer.value)
|
await mediasoup.disableProducer(mediasoup.videoProducer.value)
|
||||||
await sfx.playEvent('stream-off')
|
emit('video:disabled')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleShare() {
|
async function toggleShare() {
|
||||||
if (!mediasoup.shareProducer.value) {
|
if (!mediasoup.shareProducer.value) {
|
||||||
await mediasoup.enableShare()
|
await mediasoup.enableShare()
|
||||||
await sfx.playEvent('stream-on')
|
emit('share:enabled')
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
await mediasoup.disableProducer(mediasoup.shareProducer.value)
|
await mediasoup.disableProducer(mediasoup.shareProducer.value)
|
||||||
await sfx.playEvent('stream-off')
|
emit('share:disabled')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { createGlobalState } from '@vueuse/core'
|
|||||||
export const useClients = createGlobalState(() => {
|
export const useClients = createGlobalState(() => {
|
||||||
const auth = useAuth()
|
const auth = useAuth()
|
||||||
const signaling = useSignaling()
|
const signaling = useSignaling()
|
||||||
const toast = useToast()
|
const { emit } = useEventBus()
|
||||||
|
|
||||||
const clients = shallowRef<ChadClient[]>([])
|
const clients = shallowRef<ChadClient[]>([])
|
||||||
|
|
||||||
@@ -16,10 +16,17 @@ export const useClients = createGlobalState(() => {
|
|||||||
|
|
||||||
socket.on('clientChanged', (clientId: ChadClient['socketId'], updatedClient: UpdatedClient) => {
|
socket.on('clientChanged', (clientId: ChadClient['socketId'], updatedClient: UpdatedClient) => {
|
||||||
const client = getClient(clientId)
|
const client = getClient(clientId)
|
||||||
|
|
||||||
|
if (!client)
|
||||||
|
return
|
||||||
|
|
||||||
updateClient(clientId, updatedClient)
|
updateClient(clientId, updatedClient)
|
||||||
|
|
||||||
if (client && client.displayName !== updatedClient.displayName)
|
emit('client:updated', {
|
||||||
toast.add({ severity: 'info', summary: `${client.displayName} is now ${updatedClient.displayName}`, closable: false, life: 1000 })
|
socketId: clientId,
|
||||||
|
oldClient: client,
|
||||||
|
updatedClient,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on('disconnect', () => {
|
socket.on('disconnect', () => {
|
||||||
|
|||||||
42
client/app/composables/use-event-bus.ts
Normal file
42
client/app/composables/use-event-bus.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import type { ChadClient, Consumer, Producer } from '#shared/types'
|
||||||
|
import type { EventType } from 'mitt'
|
||||||
|
import mitt from 'mitt'
|
||||||
|
|
||||||
|
export interface AppEvents extends Record<EventType, unknown> {
|
||||||
|
'socket:connected': void
|
||||||
|
'socket:disconnected': void
|
||||||
|
'socket:authenticated': { socketId: string }
|
||||||
|
|
||||||
|
'client:added': ChadClient
|
||||||
|
'client:removed': ChadClient
|
||||||
|
'client:updated': { socketId: string, oldClient: ChadClient, updatedClient: Partial<ChadClient> }
|
||||||
|
|
||||||
|
'consumer:added': Consumer
|
||||||
|
'consumer:removed': Consumer
|
||||||
|
'consumer:paused': Consumer
|
||||||
|
'consumer:resumed': Consumer
|
||||||
|
|
||||||
|
'producer:added': Producer
|
||||||
|
'producer:removed': Producer
|
||||||
|
'producer:paused': Producer
|
||||||
|
'producer:resumed': Producer
|
||||||
|
|
||||||
|
'audio:muted': void
|
||||||
|
'audio:unmuted': void
|
||||||
|
'output:muted': void
|
||||||
|
'output:unmuted': void
|
||||||
|
'video:enabled': void
|
||||||
|
'video:disabled': void
|
||||||
|
'share:enabled': void
|
||||||
|
'share:disabled': void
|
||||||
|
}
|
||||||
|
|
||||||
|
const emitter = mitt<AppEvents>()
|
||||||
|
|
||||||
|
export function useEventBus() {
|
||||||
|
return {
|
||||||
|
emit: emitter.emit,
|
||||||
|
on: emitter.on,
|
||||||
|
off: emitter.off,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,8 +7,6 @@ import { useDevices } from '~/composables/use-devices'
|
|||||||
import { usePreferences } from '~/composables/use-preferences'
|
import { usePreferences } from '~/composables/use-preferences'
|
||||||
import { useSignaling } from '~/composables/use-signaling'
|
import { useSignaling } from '~/composables/use-signaling'
|
||||||
|
|
||||||
type ProducerType = 'microphone' | 'video' | 'share'
|
|
||||||
|
|
||||||
interface SpeakingClient {
|
interface SpeakingClient {
|
||||||
clientId: ChadClient['socketId']
|
clientId: ChadClient['socketId']
|
||||||
volume: number
|
volume: number
|
||||||
@@ -28,8 +26,7 @@ const ICE_SERVERS: RTCIceServer[] = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
export const useMediasoup = createSharedComposable(() => {
|
export const useMediasoup = createSharedComposable(() => {
|
||||||
const toast = useToast()
|
const { emit } = useEventBus()
|
||||||
const sfx = useSfx()
|
|
||||||
|
|
||||||
const signaling = useSignaling()
|
const signaling = useSignaling()
|
||||||
const { addClient, removeClient, me } = useClients()
|
const { addClient, removeClient, me } = useClients()
|
||||||
@@ -171,20 +168,24 @@ export const useMediasoup = createSharedComposable(() => {
|
|||||||
addClient(...joinedClients)
|
addClient(...joinedClients)
|
||||||
|
|
||||||
if (me.value)
|
if (me.value)
|
||||||
sfx.playRandomConnectionSound(me.value.socketId).then()
|
emit('socket:authenticated', { socketId: me.value.socketId })
|
||||||
|
|
||||||
toast.add({ severity: 'success', summary: 'Joined', closable: false, life: 1000 })
|
|
||||||
|
|
||||||
await enableMic()
|
await enableMic()
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on('newPeer', (client) => {
|
socket.on('newPeer', (client) => {
|
||||||
sfx.playRandomConnectionSound(client.socketId).then()
|
|
||||||
addClient(client)
|
addClient(client)
|
||||||
|
emit('client:added', client)
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on('peerClosed', (id) => {
|
socket.on('peerClosed', (id) => {
|
||||||
|
const { getClient } = useClients()
|
||||||
|
const client = getClient(id)
|
||||||
|
|
||||||
removeClient(id)
|
removeClient(id)
|
||||||
|
|
||||||
|
if (client)
|
||||||
|
emit('client:removed', client)
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on(
|
socket.on(
|
||||||
@@ -205,9 +206,6 @@ export const useMediasoup = createSharedComposable(() => {
|
|||||||
appData: { ...appData, socketId },
|
appData: { ...appData, socketId },
|
||||||
})
|
})
|
||||||
|
|
||||||
if (kind === 'video')
|
|
||||||
sfx.playEvent('stream-on').then()
|
|
||||||
|
|
||||||
if (producerPaused)
|
if (producerPaused)
|
||||||
consumer.pause()
|
consumer.pause()
|
||||||
|
|
||||||
@@ -218,19 +216,27 @@ export const useMediasoup = createSharedComposable(() => {
|
|||||||
raw: markRaw(consumer),
|
raw: markRaw(consumer),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
emit('consumer:added', consumers.value[consumer.id]!)
|
||||||
|
|
||||||
consumer.observer.on('resume', () => {
|
consumer.observer.on('resume', () => {
|
||||||
consumers.value[consumer.id]!.paused = false
|
consumers.value[consumer.id]!.paused = false
|
||||||
|
|
||||||
|
emit('consumer:resumed', consumers.value[consumer.id]!)
|
||||||
})
|
})
|
||||||
|
|
||||||
consumer.observer.on('pause', () => {
|
consumer.observer.on('pause', () => {
|
||||||
consumers.value[consumer.id]!.paused = true
|
consumers.value[consumer.id]!.paused = true
|
||||||
|
|
||||||
|
emit('consumer:paused', consumers.value[consumer.id]!)
|
||||||
})
|
})
|
||||||
|
|
||||||
consumer.observer.on('close', () => {
|
consumer.observer.on('close', () => {
|
||||||
if (kind === 'video')
|
const consumerData = consumers.value[consumer.id]
|
||||||
sfx.playEvent('stream-off').then()
|
|
||||||
|
|
||||||
delete consumers.value[consumer.id]
|
delete consumers.value[consumer.id]
|
||||||
|
|
||||||
|
if (consumerData)
|
||||||
|
emit('consumer:removed', consumerData)
|
||||||
})
|
})
|
||||||
|
|
||||||
consumer.on('trackended', () => {
|
consumer.on('trackended', () => {
|
||||||
@@ -311,16 +317,27 @@ export const useMediasoup = createSharedComposable(() => {
|
|||||||
raw: markRaw(producer),
|
raw: markRaw(producer),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
emit('producer:added', producers.value[producer.id]!)
|
||||||
|
|
||||||
producer.observer.on('pause', () => {
|
producer.observer.on('pause', () => {
|
||||||
producers.value[producer.id]!.paused = true
|
producers.value[producer.id]!.paused = true
|
||||||
|
|
||||||
|
emit('producer:paused', producers.value[producer.id]!)
|
||||||
})
|
})
|
||||||
|
|
||||||
producer.observer.on('resume', () => {
|
producer.observer.on('resume', () => {
|
||||||
producers.value[producer.id]!.paused = false
|
producers.value[producer.id]!.paused = false
|
||||||
|
|
||||||
|
emit('producer:resumed', producers.value[producer.id]!)
|
||||||
})
|
})
|
||||||
|
|
||||||
producer.observer.on('close', () => {
|
producer.observer.on('close', () => {
|
||||||
|
const producerData = producers.value[producer.id]
|
||||||
|
|
||||||
delete producers.value[producer.id]
|
delete producers.value[producer.id]
|
||||||
|
|
||||||
|
if (producerData)
|
||||||
|
emit('producer:removed', producerData)
|
||||||
})
|
})
|
||||||
|
|
||||||
producer.on('trackended', () => {
|
producer.on('trackended', () => {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export interface SyncedPreferences {
|
|||||||
export const usePreferences = createGlobalState(() => {
|
export const usePreferences = createGlobalState(() => {
|
||||||
const { videoInputs, audioInputs, audioOutputs } = useDevices()
|
const { videoInputs, audioInputs, audioOutputs } = useDevices()
|
||||||
|
|
||||||
const synced = ref(false)
|
const fetched = ref(false)
|
||||||
|
|
||||||
const inputDeviceId = useLocalStorage<MediaDeviceInfo['deviceId']>('INPUT_DEVICE_ID', 'default')
|
const inputDeviceId = useLocalStorage<MediaDeviceInfo['deviceId']>('INPUT_DEVICE_ID', 'default')
|
||||||
const outputDeviceId = useLocalStorage<MediaDeviceInfo['deviceId']>('OUTPUT_DEVICE_ID', 'default')
|
const outputDeviceId = useLocalStorage<MediaDeviceInfo['deviceId']>('OUTPUT_DEVICE_ID', 'default')
|
||||||
@@ -58,7 +58,7 @@ export const usePreferences = createGlobalState(() => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
synced,
|
fetched,
|
||||||
inputDeviceId,
|
inputDeviceId,
|
||||||
outputDeviceId,
|
outputDeviceId,
|
||||||
videoDeviceId,
|
videoDeviceId,
|
||||||
|
|||||||
@@ -25,7 +25,10 @@ const EVENT_VOLUME: Record<SfxEvent, number> = {
|
|||||||
'connection': 0.1,
|
'connection': 0.1,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: refactor this shit
|
||||||
export const useSfx = createSharedComposable(() => {
|
export const useSfx = createSharedComposable(() => {
|
||||||
|
const { outputMuted } = useApp()
|
||||||
|
|
||||||
async function play(src: string, volume = 0.2): Promise<void> {
|
async function play(src: string, volume = 0.2): Promise<void> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const howl = new Howl({
|
const howl = new Howl({
|
||||||
@@ -74,6 +77,10 @@ export const useSfx = createSharedComposable(() => {
|
|||||||
|
|
||||||
async function playRandomConnectionSound(seed: string) {
|
async function playRandomConnectionSound(seed: string) {
|
||||||
await playEvent('stream-on')
|
await playEvent('stream-on')
|
||||||
|
|
||||||
|
if (outputMuted.value)
|
||||||
|
return
|
||||||
|
|
||||||
await play(CONNECTION_SOUNDS[hashStringToNumber(seed, CONNECTION_SOUNDS.length)]!, 0.1)
|
await play(CONNECTION_SOUNDS[hashStringToNumber(seed, CONNECTION_SOUNDS.length)]!, 0.1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { io } from 'socket.io-client'
|
|||||||
import { parseURL } from 'ufo'
|
import { parseURL } from 'ufo'
|
||||||
|
|
||||||
export const useSignaling = createSharedComposable(() => {
|
export const useSignaling = createSharedComposable(() => {
|
||||||
const toast = useToast()
|
const { emit } = useEventBus()
|
||||||
const { me } = useAuth()
|
const { me } = useAuth()
|
||||||
|
|
||||||
const socket = shallowRef<Socket>()
|
const socket = shallowRef<Socket>()
|
||||||
@@ -41,9 +41,9 @@ export const useSignaling = createSharedComposable(() => {
|
|||||||
|
|
||||||
watch(connected, (connected) => {
|
watch(connected, (connected) => {
|
||||||
if (connected)
|
if (connected)
|
||||||
toast.add({ severity: 'success', summary: 'Connected', closable: false, life: 1000 })
|
emit('socket:connected')
|
||||||
else
|
else
|
||||||
toast.add({ severity: 'error', summary: 'Disconnected', closable: false, life: 1000 })
|
emit('socket:disconnected')
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
||||||
watch(me, (me) => {
|
watch(me, (me) => {
|
||||||
@@ -66,7 +66,7 @@ export const useSignaling = createSharedComposable(() => {
|
|||||||
|
|
||||||
const uri = host ? `${protocol}//${host}` : ``
|
const uri = host ? `${protocol}//${host}` : ``
|
||||||
|
|
||||||
socket.value = io(`${uri}/webrtc`, {
|
socket.value = io(`${uri}`, {
|
||||||
path: `${pathname}/ws`,
|
path: `${pathname}/ws`,
|
||||||
transports: ['websocket'],
|
transports: ['websocket'],
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
<div v-auto-animate class="w-1/2 m-auto">
|
<div v-auto-animate class="w-1/2 m-auto">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<PrimeSelectButton v-model="tab" class="mb-6" :options="options" option-label="label" :allow-empty="false" />
|
<PrimeSelectButton :model-value="tab" class="mb-6" :options="options" option-label="label" :allow-empty="false" @update:model-value="onTabChanged" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<slot />
|
<slot />
|
||||||
@@ -15,28 +15,35 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { RouteLocationRaw } from 'vue-router'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
const { version } = useApp()
|
const { version } = useApp()
|
||||||
|
|
||||||
|
interface Tab {
|
||||||
|
label: string
|
||||||
|
route: RouteLocationRaw
|
||||||
|
}
|
||||||
|
|
||||||
const options = computed(() => {
|
const options = computed(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: 'Login',
|
label: 'Login',
|
||||||
routeName: 'Login',
|
route: { name: 'Login' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Register',
|
label: 'Register',
|
||||||
routeName: 'Register',
|
route: { name: 'Register' },
|
||||||
},
|
},
|
||||||
]
|
] as Tab[]
|
||||||
})
|
})
|
||||||
|
|
||||||
const tab = shallowRef(options.value.find(option => route.name === option.routeName))
|
const tab = computed(() => {
|
||||||
|
return options.value.find(option => route.name === router.resolve(option.route)?.name)
|
||||||
watch(tab, (tab) => {
|
|
||||||
if (!tab)
|
|
||||||
return
|
|
||||||
|
|
||||||
navigateTo({ name: tab.routeName })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function onTabChanged(tab: Tab) {
|
||||||
|
navigateTo(tab.route)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ export default defineNuxtRouteMiddleware(async () => {
|
|||||||
if (!me.value)
|
if (!me.value)
|
||||||
return
|
return
|
||||||
|
|
||||||
const { synced, toggleInputHotkey, toggleOutputHotkey } = usePreferences()
|
const { fetched, toggleInputHotkey, toggleOutputHotkey } = usePreferences()
|
||||||
|
|
||||||
if (synced.value)
|
if (fetched.value)
|
||||||
return
|
return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -20,7 +20,7 @@ export default defineNuxtRouteMiddleware(async () => {
|
|||||||
|
|
||||||
toggleInputHotkey.value = preferences.toggleInputHotkey ?? toggleInputHotkey.value
|
toggleInputHotkey.value = preferences.toggleInputHotkey ?? toggleInputHotkey.value
|
||||||
toggleOutputHotkey.value = preferences.toggleOutputHotkey ?? toggleOutputHotkey.value
|
toggleOutputHotkey.value = preferences.toggleOutputHotkey ?? toggleOutputHotkey.value
|
||||||
synced.value = true
|
fetched.value = true
|
||||||
}
|
}
|
||||||
catch {}
|
catch {}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,21 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="grid grid-cols-[1fr_1fr] gap-2">
|
<div class="grid grid-cols-[1fr_1fr] gap-2">
|
||||||
<GalleryCard
|
<GalleryCard
|
||||||
v-for="item in gallery"
|
v-for="producer in producers"
|
||||||
:key="item.client.socketId"
|
:key="`producer-${producer.id}`"
|
||||||
:client="item.client"
|
:client="me!"
|
||||||
:stream="item.stream"
|
:producer="producer"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<GalleryCard
|
||||||
|
v-for="consumer in consumers"
|
||||||
|
:key="`consumer-${consumer.consumer.id}`"
|
||||||
|
:client="consumer.client"
|
||||||
|
:consumer="consumer.consumer"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { ChadClient } from '#shared/types'
|
import type { ChadClient, Consumer, Producer } from '#shared/types'
|
||||||
|
|
||||||
interface GalleryItem {
|
|
||||||
client: ChadClient
|
|
||||||
stream: MediaStream
|
|
||||||
}
|
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
name: 'Gallery',
|
name: 'Gallery',
|
||||||
@@ -24,39 +26,29 @@ definePageMeta({
|
|||||||
const { videoProducer, shareProducer } = useMediasoup()
|
const { videoProducer, shareProducer } = useMediasoup()
|
||||||
const { clients, me } = useClients()
|
const { clients, me } = useClients()
|
||||||
|
|
||||||
const gallery = computed(() => {
|
const producers = computed(() => {
|
||||||
return clients.value.reduce<GalleryItem[]>(
|
return [videoProducer.value, shareProducer.value].filter(p => !!p && !!p.raw.track) as Producer[]
|
||||||
(acc, client) => {
|
|
||||||
const { streaming, videoConsumers, shareConsumers } = useClient(client.socketId)
|
|
||||||
|
|
||||||
if (!streaming.value)
|
|
||||||
return acc
|
|
||||||
|
|
||||||
for (const consumer of [...videoConsumers.value, ...shareConsumers.value]) {
|
|
||||||
acc.push({
|
|
||||||
client,
|
|
||||||
stream: new MediaStream([consumer.raw.track]),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc
|
|
||||||
},
|
|
||||||
[videoProducer.value, shareProducer.value].reduce<GalleryItem[]>((acc, producer) => {
|
|
||||||
if (!me.value || !producer || !producer.raw.track)
|
|
||||||
return acc
|
|
||||||
|
|
||||||
acc.push({
|
|
||||||
client: me.value,
|
|
||||||
stream: new MediaStream([producer.raw.track]),
|
|
||||||
})
|
|
||||||
|
|
||||||
return acc
|
|
||||||
}, []),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(gallery, (gallery) => {
|
const consumers = computed(() => {
|
||||||
if (gallery.length > 0)
|
return clients.value.reduce<{ client: ChadClient, consumer: Consumer }[]>((acc, client) => {
|
||||||
|
const { streaming, videoConsumers: clientVideoConsumers, shareConsumers: clientShareConsumers } = useClient(client.socketId)
|
||||||
|
|
||||||
|
if (!streaming.value)
|
||||||
|
return acc
|
||||||
|
|
||||||
|
for (const consumer of [...clientVideoConsumers.value, ...clientShareConsumers.value]) {
|
||||||
|
acc.push({ client, consumer })
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, [])
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasItems = computed(() => producers.value.length > 0 || consumers.value.length > 0)
|
||||||
|
|
||||||
|
watch(hasItems, (hasItems) => {
|
||||||
|
if (hasItems)
|
||||||
return
|
return
|
||||||
|
|
||||||
navigateTo({ name: 'Index' })
|
navigateTo({ name: 'Index' })
|
||||||
|
|||||||
@@ -81,6 +81,7 @@
|
|||||||
size="small"
|
size="small"
|
||||||
option-label="label"
|
option-label="label"
|
||||||
option-value="value"
|
option-value="value"
|
||||||
|
:allow-empty="false"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
57
client/app/plugins/sfx-listener.ts
Normal file
57
client/app/plugins/sfx-listener.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
export default defineNuxtPlugin(() => {
|
||||||
|
const { on } = useEventBus()
|
||||||
|
const sfx = useSfx()
|
||||||
|
|
||||||
|
// Connection sounds
|
||||||
|
on('socket:authenticated', ({ socketId }) => {
|
||||||
|
sfx.playRandomConnectionSound(socketId)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Client events
|
||||||
|
on('client:added', (client) => {
|
||||||
|
sfx.playRandomConnectionSound(client.socketId)
|
||||||
|
})
|
||||||
|
|
||||||
|
on('client:removed', () => {
|
||||||
|
sfx.playEvent('stream-off')
|
||||||
|
})
|
||||||
|
|
||||||
|
// Audio mute/unmute
|
||||||
|
on('audio:muted', () => {
|
||||||
|
sfx.playEvent('mic-off')
|
||||||
|
})
|
||||||
|
|
||||||
|
on('audio:unmuted', () => {
|
||||||
|
sfx.playEvent('mic-on')
|
||||||
|
})
|
||||||
|
|
||||||
|
// Video/share toggle
|
||||||
|
on('video:enabled', () => {
|
||||||
|
sfx.playEvent('stream-on')
|
||||||
|
})
|
||||||
|
|
||||||
|
on('video:disabled', () => {
|
||||||
|
sfx.playEvent('stream-off')
|
||||||
|
})
|
||||||
|
|
||||||
|
on('share:enabled', () => {
|
||||||
|
sfx.playEvent('stream-on')
|
||||||
|
})
|
||||||
|
|
||||||
|
on('share:disabled', () => {
|
||||||
|
sfx.playEvent('stream-off')
|
||||||
|
})
|
||||||
|
|
||||||
|
// Consumer video streams
|
||||||
|
on('consumer:added', (consumer) => {
|
||||||
|
if (consumer.raw.kind === 'video') {
|
||||||
|
sfx.playEvent('stream-on')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
on('consumer:removed', (consumer) => {
|
||||||
|
if (consumer.raw.kind === 'video') {
|
||||||
|
sfx.playEvent('stream-off')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
62
client/app/plugins/toast-listener.ts
Normal file
62
client/app/plugins/toast-listener.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
export default defineNuxtPlugin(() => {
|
||||||
|
const { on } = useEventBus()
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
// Socket events
|
||||||
|
on('socket:connected', () => {
|
||||||
|
toast.add({ severity: 'success', summary: 'Connected', life: 1000 })
|
||||||
|
})
|
||||||
|
|
||||||
|
on('socket:disconnected', () => {
|
||||||
|
toast.add({ severity: 'error', summary: 'Disconnected', life: 1000 })
|
||||||
|
})
|
||||||
|
|
||||||
|
on('socket:authenticated', () => {
|
||||||
|
toast.add({ severity: 'success', summary: 'Joined', life: 1000 })
|
||||||
|
})
|
||||||
|
|
||||||
|
// Client events
|
||||||
|
on('client:added', (client) => {
|
||||||
|
toast.add({
|
||||||
|
severity: 'info',
|
||||||
|
summary: `${client.displayName} connected`,
|
||||||
|
life: 1000,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
on('client:removed', (client) => {
|
||||||
|
toast.add({
|
||||||
|
severity: 'info',
|
||||||
|
summary: `${client.displayName} disconnected`,
|
||||||
|
life: 1000,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
on('client:updated', ({ oldClient, updatedClient }) => {
|
||||||
|
if (oldClient.displayName !== updatedClient.displayName) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'info',
|
||||||
|
summary: `${oldClient.displayName} is now ${updatedClient.displayName}`,
|
||||||
|
life: 1000,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Audio control
|
||||||
|
on('audio:muted', () => {
|
||||||
|
toast.add({ severity: 'info', summary: 'Microphone muted', life: 1000 })
|
||||||
|
})
|
||||||
|
|
||||||
|
on('audio:unmuted', () => {
|
||||||
|
toast.add({ severity: 'info', summary: 'Microphone activated', life: 1000 })
|
||||||
|
})
|
||||||
|
|
||||||
|
// Output mute control
|
||||||
|
on('output:muted', () => {
|
||||||
|
toast.add({ severity: 'info', summary: 'Sound muted', life: 1000 })
|
||||||
|
})
|
||||||
|
|
||||||
|
on('output:unmuted', () => {
|
||||||
|
toast.add({ severity: 'info', summary: 'Sound resumed', life: 1000 })
|
||||||
|
})
|
||||||
|
})
|
||||||
0
client/app/types/events.ts
Normal file
0
client/app/types/events.ts
Normal file
@@ -22,6 +22,7 @@
|
|||||||
"howler": "^2.2.4",
|
"howler": "^2.2.4",
|
||||||
"lucide-vue-next": "^0.562.0",
|
"lucide-vue-next": "^0.562.0",
|
||||||
"mediasoup-client": "^3.18.6",
|
"mediasoup-client": "^3.18.6",
|
||||||
|
"mitt": "^3.0.1",
|
||||||
"nuxt": "^4.2.2",
|
"nuxt": "^4.2.2",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"primeicons": "^7.0.0",
|
"primeicons": "^7.0.0",
|
||||||
|
|||||||
@@ -4070,6 +4070,7 @@ __metadata:
|
|||||||
howler: "npm:^2.2.4"
|
howler: "npm:^2.2.4"
|
||||||
lucide-vue-next: "npm:^0.562.0"
|
lucide-vue-next: "npm:^0.562.0"
|
||||||
mediasoup-client: "npm:^3.18.6"
|
mediasoup-client: "npm:^3.18.6"
|
||||||
|
mitt: "npm:^3.0.1"
|
||||||
nuxt: "npm:^4.2.2"
|
nuxt: "npm:^4.2.2"
|
||||||
postcss: "npm:^8.5.6"
|
postcss: "npm:^8.5.6"
|
||||||
primeicons: "npm:^7.0.0"
|
primeicons: "npm:^7.0.0"
|
||||||
|
|||||||
73
server/CLAUDE.md
Normal file
73
server/CLAUDE.md
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
Chad is a real-time voice/video chat server (think Discord-like) built with Fastify, Socket.IO, and mediasoup for WebRTC media handling. It uses SQLite via Prisma ORM and Lucia for session-based authentication. The client is a Tauri desktop app (separate repo).
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
- **Start server:** `yarn start` (runs `ts-node --transpile-only server.ts`)
|
||||||
|
- **Deploy DB (migrate + seed + generate):** `yarn db:deploy`
|
||||||
|
- **Generate Prisma client after schema changes:** `npx prisma generate`
|
||||||
|
- **Create a migration:** `npx prisma migrate dev --name <name>`
|
||||||
|
- **Lint:** `npx eslint .`
|
||||||
|
- **Package manager:** Yarn 4 (corepack). Do not use npm.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Entry Point & Plugin System
|
||||||
|
|
||||||
|
`server.ts` creates a Fastify instance and uses `@fastify/autoload` to auto-register everything in `plugins/` and `routes/` (prefixed under `/chad`). Plugins use `fastify-plugin` (`fp`) with named dependencies to control load order.
|
||||||
|
|
||||||
|
### Plugin Load Order (dependency chain)
|
||||||
|
|
||||||
|
1. `plugins/auth.ts` — Adds `req.user` / `req.session` via Lucia cookie validation on every request (preHandler hook)
|
||||||
|
2. `plugins/mediasoup-worker.ts` — Creates a mediasoup Worker, decorates `fastify.mediasoupWorker`
|
||||||
|
3. `plugins/mediasoup-router.ts` — Creates a mediasoup Router (depends on worker), decorates `fastify.mediasoupRouter`. Configures supported audio/video codecs (Opus, VP8, VP9, H.264, AV1)
|
||||||
|
4. `plugins/socket.ts` — Creates Socket.IO server at `/chad/ws` (depends on worker + router), decorates `fastify.io`. Registers socket handlers on `fastify.ready()`
|
||||||
|
|
||||||
|
### Socket Handlers (`socket/`)
|
||||||
|
|
||||||
|
- `socket/webrtc.ts` — Main WebRTC signaling: join/leave, transport creation, producer/consumer lifecycle, audio level observation, active speaker detection
|
||||||
|
- `socket/channel.ts` — Channel-based socket logic (in development on `channels` branch)
|
||||||
|
|
||||||
|
Both handlers authenticate by looking up `socket.handshake.auth.userId` against the DB.
|
||||||
|
|
||||||
|
### REST Routes (`routes/`)
|
||||||
|
|
||||||
|
All routes are prefixed with `/chad` via autoload config.
|
||||||
|
|
||||||
|
- `routes/auth.ts` — `/register`, `/login`, `/logout`, `/me`
|
||||||
|
- `routes/user.ts` — `/preferences` (GET/PATCH), `/profile` (PATCH). Profile changes broadcast to connected socket peers via `clientChanged` event.
|
||||||
|
|
||||||
|
### Type System (`types/socket.ts`)
|
||||||
|
|
||||||
|
Fully typed Socket.IO events: `ClientToServerEvents`, `ServerToClientEvents`, `SocketData`. The `SomeSocket` union type covers both live `Socket` and `RemoteSocket` (from `fetchSockets()`).
|
||||||
|
|
||||||
|
### Database
|
||||||
|
|
||||||
|
- SQLite with Prisma 7 + `@prisma/adapter-better-sqlite3`
|
||||||
|
- Schema at `prisma/schema.prisma`, generated client at `prisma/generated/client/`
|
||||||
|
- Models: `User`, `Session` (Lucia), `UserPreferences`, `Channel`
|
||||||
|
- Seed creates a persistent "Default" channel
|
||||||
|
- Config in `prisma.config.ts` using `dotenv` for `DATABASE_URL`
|
||||||
|
|
||||||
|
### Key Patterns
|
||||||
|
|
||||||
|
- Request validation uses Zod schemas defined inline in route handlers
|
||||||
|
- DTOs in `dto/` define Prisma select objects with `satisfies Prisma.*Select` for type-safe projections
|
||||||
|
- `utils/socket-to-client.ts` maps socket data to the `ChadClient` interface sent to clients
|
||||||
|
- ESLint uses `@antfu/eslint-config` (flat config) with `no-console` and `n/prefer-global/process` disabled
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
- `PORT` — Server port (default: 4000, Docker: 80)
|
||||||
|
- `DATABASE_URL` — SQLite path (e.g., `file:../data/database.db`)
|
||||||
|
- `ANNOUNCED_ADDRESS` — Public IP for WebRTC ICE candidates (default: `127.0.0.1`)
|
||||||
|
- `CORS_ORIGIN` — Socket.IO CORS origin (default: `*`)
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
`Dockerfile` requires python3 and build-essential for mediasoup native compilation. Runs `yarn db:deploy && yarn start`.
|
||||||
14
server/dto/channel.dto.ts
Normal file
14
server/dto/channel.dto.ts
Normal 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,
|
||||||
|
} as Prisma.ChannelSelect
|
||||||
|
|
||||||
|
export type ChannelPublicDTO = Prisma.ChannelGetPayload<{
|
||||||
|
select: typeof channelPublicSelect
|
||||||
|
}>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "server",
|
"name": "server",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "ts-node --transpile-only server.ts",
|
"start": "ts-node --transpile-only server.ts",
|
||||||
"db:deploy": "npx prisma migrate deploy && npx prisma generate"
|
"db:deploy": "npx prisma migrate deploy && npx prisma db seed && npx prisma generate"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "yarn@4.10.3",
|
"packageManager": "yarn@4.10.3",
|
||||||
@@ -11,7 +11,8 @@
|
|||||||
"@fastify/cookie": "^11.0.2",
|
"@fastify/cookie": "^11.0.2",
|
||||||
"@fastify/cors": "^11.1.0",
|
"@fastify/cors": "^11.1.0",
|
||||||
"@lucia-auth/adapter-prisma": "^4.0.1",
|
"@lucia-auth/adapter-prisma": "^4.0.1",
|
||||||
"@prisma/client": "^6.17.0",
|
"@prisma/adapter-better-sqlite3": "^7.2.0",
|
||||||
|
"@prisma/client": "7",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"consola": "^3.4.2",
|
"consola": "^3.4.2",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
@@ -19,7 +20,7 @@
|
|||||||
"fastify-plugin": "^5.1.0",
|
"fastify-plugin": "^5.1.0",
|
||||||
"lucia": "^3.2.2",
|
"lucia": "^3.2.2",
|
||||||
"mediasoup": "^3.19.3",
|
"mediasoup": "^3.19.3",
|
||||||
"prisma": "^6.17.0",
|
"prisma": "7",
|
||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
"ws": "^8.18.3",
|
"ws": "^8.18.3",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import type { FastifyInstance } from 'fastify'
|
import type { FastifyInstance } from 'fastify'
|
||||||
import type { ServerOptions } from 'socket.io'
|
import type { ServerOptions } from 'socket.io'
|
||||||
|
import type { ChadClient } from '../types/socket.ts'
|
||||||
|
import { consola } from 'consola'
|
||||||
import fp from 'fastify-plugin'
|
import fp from 'fastify-plugin'
|
||||||
import { Server } from 'socket.io'
|
import { Server } from 'socket.io'
|
||||||
import registerWebrtcSocket from '../socket/webrtc.ts'
|
import prisma from '../prisma/client.ts'
|
||||||
|
import registerChannelHandlers from '../socket/channel.ts'
|
||||||
|
import registerWebrtcHandlers from '../socket/webrtc.ts'
|
||||||
|
|
||||||
declare module 'fastify' {
|
declare module 'fastify' {
|
||||||
interface FastifyInstance {
|
interface FastifyInstance {
|
||||||
@@ -22,8 +26,80 @@ export default fp<Partial<ServerOptions>>(
|
|||||||
await fastify.io.close()
|
await fastify.io.close()
|
||||||
})
|
})
|
||||||
|
|
||||||
fastify.ready(async () => {
|
fastify.ready(() => {
|
||||||
await registerWebrtcSocket(fastify.io, fastify.mediasoupRouter)
|
const audioLevelObserver = await fastify.mediasoupRouter.createAudioLevelObserver({
|
||||||
|
maxEntries: 10,
|
||||||
|
threshold: -80,
|
||||||
|
interval: 800,
|
||||||
|
})
|
||||||
|
|
||||||
|
const activeSpeakerObserver = await fastify.mediasoupRouter.createActiveSpeakerObserver()
|
||||||
|
|
||||||
|
audioLevelObserver.on('volumes', async (volumes: types.AudioLevelObserverVolume[]) => {
|
||||||
|
fastify.io.emit('webrtc:speaking-peers', volumes.map(({ producer, volume }) => {
|
||||||
|
const { socketId } = producer.appData as { socketId: ChadClient['socketId'] }
|
||||||
|
|
||||||
|
return {
|
||||||
|
clientId: socketId,
|
||||||
|
volume,
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
audioLevelObserver.on('silence', () => {
|
||||||
|
fastify.io.emit('webrtc:speaking-peers', [])
|
||||||
|
fastify.io.emit('webrtc:active-speaker', undefined)
|
||||||
|
})
|
||||||
|
|
||||||
|
activeSpeakerObserver.on('dominantspeaker', ({ producer }) => {
|
||||||
|
const { socketId } = producer.appData as { socketId: ChadClient['socketId'] }
|
||||||
|
|
||||||
|
fastify.io.emit('webrtc:active-speaker', socketId)
|
||||||
|
})
|
||||||
|
|
||||||
|
fastify.io.on('connection', async (socket) => {
|
||||||
|
consola.info('New connection', socket.id)
|
||||||
|
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: {
|
||||||
|
id: socket.handshake.auth.userId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
displayName: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
socket.disconnect()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id, username, displayName } = user
|
||||||
|
|
||||||
|
socket.data.userId = id
|
||||||
|
socket.data.username = username
|
||||||
|
socket.data.displayName = displayName
|
||||||
|
|
||||||
|
consola.info('User authorized', ...Object.values(user))
|
||||||
|
|
||||||
|
const channel = await registerChannelHandlers(fastify.io, socket)
|
||||||
|
const webrtc = await registerWebrtcHandlers(
|
||||||
|
fastify.io,
|
||||||
|
socket,
|
||||||
|
fastify.mediasoupRouter,
|
||||||
|
audioLevelObserver,
|
||||||
|
activeSpeakerObserver,
|
||||||
|
)
|
||||||
|
|
||||||
|
socket.emit('webrtc:authenticated', {
|
||||||
|
...channel,
|
||||||
|
...webrtc,
|
||||||
|
rtpCapabilities: fastify.mediasoupRouter.rtpCapabilities,
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{ name: 'socket-io', dependencies: ['mediasoup-worker', 'mediasoup-router'] },
|
{ name: 'socket-io', dependencies: ['mediasoup-worker', 'mediasoup-router'] },
|
||||||
|
|||||||
13
server/prisma.config.ts
Normal file
13
server/prisma.config.ts
Normal 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'),
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -1,6 +1,14 @@
|
|||||||
import { PrismaClient } from '@prisma/client'
|
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({
|
const client = new PrismaClient({
|
||||||
|
adapter: new PrismaBetterSqlite3({
|
||||||
|
url: env('DATABASE_URL'),
|
||||||
|
}, {
|
||||||
|
timestampFormat: 'unixepoch-ms',
|
||||||
|
}),
|
||||||
log: ['query', 'error', 'warn'],
|
log: ['query', 'error', 'warn'],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
39
server/prisma/generated/client/browser.ts
Normal file
39
server/prisma/generated/client/browser.ts
Normal 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
|
||||||
1
server/prisma/generated/client/client.d.ts
vendored
Normal file
1
server/prisma/generated/client/client.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./index"
|
||||||
5
server/prisma/generated/client/client.js
Normal file
5
server/prisma/generated/client/client.js
Normal 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('.') }
|
||||||
61
server/prisma/generated/client/client.ts
Normal file
61
server/prisma/generated/client/client.ts
Normal 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
|
||||||
298
server/prisma/generated/client/commonInputTypes.ts
Normal file
298
server/prisma/generated/client/commonInputTypes.ts
Normal 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>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
1
server/prisma/generated/client/default.d.ts
vendored
Normal file
1
server/prisma/generated/client/default.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./index"
|
||||||
5
server/prisma/generated/client/default.js
Normal file
5
server/prisma/generated/client/default.js
Normal 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') }
|
||||||
1
server/prisma/generated/client/edge.d.ts
vendored
Normal file
1
server/prisma/generated/client/edge.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./default"
|
||||||
165
server/prisma/generated/client/edge.js
Normal file
165
server/prisma/generated/client/edge.js
Normal 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)
|
||||||
15
server/prisma/generated/client/enums.ts
Normal file
15
server/prisma/generated/client/enums.ts
Normal 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 {}
|
||||||
196
server/prisma/generated/client/index-browser.js
Normal file
196
server/prisma/generated/client/index-browser.js
Normal 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
6981
server/prisma/generated/client/index.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
165
server/prisma/generated/client/index.js
Normal file
165
server/prisma/generated/client/index.js
Normal 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)
|
||||||
220
server/prisma/generated/client/internal/class.ts
Normal file
220
server/prisma/generated/client/internal/class.ts
Normal 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
|
||||||
|
}
|
||||||
1000
server/prisma/generated/client/internal/prismaNamespace.ts
Normal file
1000
server/prisma/generated/client/internal/prismaNamespace.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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]
|
||||||
|
|
||||||
15
server/prisma/generated/client/models.ts
Normal file
15
server/prisma/generated/client/models.ts
Normal 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'
|
||||||
1412
server/prisma/generated/client/models/Channel.ts
Normal file
1412
server/prisma/generated/client/models/Channel.ts
Normal file
File diff suppressed because it is too large
Load Diff
1267
server/prisma/generated/client/models/Session.ts
Normal file
1267
server/prisma/generated/client/models/Session.ts
Normal file
File diff suppressed because it is too large
Load Diff
1597
server/prisma/generated/client/models/User.ts
Normal file
1597
server/prisma/generated/client/models/User.ts
Normal file
File diff suppressed because it is too large
Load Diff
1228
server/prisma/generated/client/models/UserPreferences.ts
Normal file
1228
server/prisma/generated/client/models/UserPreferences.ts
Normal file
File diff suppressed because it is too large
Load Diff
144
server/prisma/generated/client/package.json
Normal file
144
server/prisma/generated/client/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
server/prisma/generated/client/query_compiler_bg.js
Normal file
2
server/prisma/generated/client/query_compiler_bg.js
Normal 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});
|
||||||
BIN
server/prisma/generated/client/query_compiler_bg.wasm
Normal file
BIN
server/prisma/generated/client/query_compiler_bg.wasm
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
3317
server/prisma/generated/client/runtime/client.d.ts
vendored
Normal file
3317
server/prisma/generated/client/runtime/client.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
86
server/prisma/generated/client/runtime/client.js
Normal file
86
server/prisma/generated/client/runtime/client.js
Normal file
File diff suppressed because one or more lines are too long
87
server/prisma/generated/client/runtime/index-browser.d.ts
vendored
Normal file
87
server/prisma/generated/client/runtime/index-browser.d.ts
vendored
Normal 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 { }
|
||||||
6
server/prisma/generated/client/runtime/index-browser.js
Normal file
6
server/prisma/generated/client/runtime/index-browser.js
Normal 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
|
||||||
76
server/prisma/generated/client/runtime/wasm-compiler-edge.js
Normal file
76
server/prisma/generated/client/runtime/wasm-compiler-edge.js
Normal file
File diff suppressed because one or more lines are too long
45
server/prisma/generated/client/schema.prisma
Normal file
45
server/prisma/generated/client/schema.prisma
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
datasource db {
|
||||||
|
provider = "sqlite"
|
||||||
|
}
|
||||||
|
|
||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
output = "./generated/client"
|
||||||
|
}
|
||||||
|
|
||||||
|
model User {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
username String @unique
|
||||||
|
password String
|
||||||
|
displayName String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
Session Session[]
|
||||||
|
UserPreferences UserPreferences?
|
||||||
|
channels Channel[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model Session {
|
||||||
|
id String @id
|
||||||
|
userId String
|
||||||
|
expiresAt DateTime
|
||||||
|
user User @relation(references: [id], fields: [userId], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@index([userId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model UserPreferences {
|
||||||
|
userId String @id
|
||||||
|
toggleInputHotkey String? @default("")
|
||||||
|
toggleOutputHotkey String? @default("")
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
}
|
||||||
|
|
||||||
|
model Channel {
|
||||||
|
id String @id
|
||||||
|
name String
|
||||||
|
maxClients Int?
|
||||||
|
persistent Boolean @default(false)
|
||||||
|
owner_id String?
|
||||||
|
owner User? @relation(fields: [owner_id], references: [id], onDelete: Cascade)
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
|
||||||
|
/* !!! This is code generated by Prisma. Do not edit directly. !!!
|
||||||
|
/* eslint-disable */
|
||||||
|
// biome-ignore-all lint: generated file
|
||||||
|
export default import('./query_compiler_bg.wasm?module')
|
||||||
5
server/prisma/generated/client/wasm-worker-loader.mjs
Normal file
5
server/prisma/generated/client/wasm-worker-loader.mjs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
|
||||||
|
/* !!! This is code generated by Prisma. Do not edit directly. !!!
|
||||||
|
/* eslint-disable */
|
||||||
|
// biome-ignore-all lint: generated file
|
||||||
|
export default import('./query_compiler_bg.wasm')
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `volumes` on the `UserPreferences` table. All the data in the column will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Channel" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"maxClients" INTEGER,
|
||||||
|
"persistent" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"owner_id" TEXT,
|
||||||
|
CONSTRAINT "Channel_owner_id_fkey" FOREIGN KEY ("owner_id") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- RedefineTables
|
||||||
|
PRAGMA defer_foreign_keys=ON;
|
||||||
|
PRAGMA foreign_keys=OFF;
|
||||||
|
CREATE TABLE "new_UserPreferences" (
|
||||||
|
"userId" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"toggleInputHotkey" TEXT DEFAULT '',
|
||||||
|
"toggleOutputHotkey" TEXT DEFAULT '',
|
||||||
|
CONSTRAINT "UserPreferences_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
INSERT INTO "new_UserPreferences" ("toggleInputHotkey", "toggleOutputHotkey", "userId") SELECT "toggleInputHotkey", "toggleOutputHotkey", "userId" FROM "UserPreferences";
|
||||||
|
DROP TABLE "UserPreferences";
|
||||||
|
ALTER TABLE "new_UserPreferences" RENAME TO "UserPreferences";
|
||||||
|
PRAGMA foreign_keys=ON;
|
||||||
|
PRAGMA defer_foreign_keys=OFF;
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
datasource db {
|
datasource db {
|
||||||
provider = "sqlite"
|
provider = "sqlite"
|
||||||
url = env("DATABASE_URL")
|
// url = env("DATABASE_URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
generator client {
|
generator client {
|
||||||
provider = "prisma-client-js"
|
provider = "prisma-client"
|
||||||
// output = "./generated/client"
|
output = "./generated/client"
|
||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
@@ -15,16 +15,15 @@ model User {
|
|||||||
displayName String
|
displayName String
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
Session Session[]
|
Session Session[]
|
||||||
UserPreferences UserPreferences?
|
UserPreferences UserPreferences?
|
||||||
|
channels Channel[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Session {
|
model Session {
|
||||||
id String @id
|
id String @id
|
||||||
userId String
|
userId String
|
||||||
expiresAt DateTime
|
expiresAt DateTime
|
||||||
|
|
||||||
user User @relation(references: [id], fields: [userId], onDelete: Cascade)
|
user User @relation(references: [id], fields: [userId], onDelete: Cascade)
|
||||||
|
|
||||||
@@index([userId])
|
@@index([userId])
|
||||||
@@ -34,7 +33,14 @@ model UserPreferences {
|
|||||||
userId String @id
|
userId String @id
|
||||||
toggleInputHotkey String? @default("")
|
toggleInputHotkey String? @default("")
|
||||||
toggleOutputHotkey String? @default("")
|
toggleOutputHotkey String? @default("")
|
||||||
volumes Json? @default("{}")
|
|
||||||
|
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
}
|
||||||
|
|
||||||
|
model Channel {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
name String
|
||||||
|
maxClients Int?
|
||||||
|
persistent Boolean @default(false)
|
||||||
|
owner_id String?
|
||||||
|
owner User? @relation(fields: [owner_id], references: [id], onDelete: Cascade)
|
||||||
}
|
}
|
||||||
23
server/prisma/seed.ts
Normal file
23
server/prisma/seed.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import prisma from '../prisma/client.ts'
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
await prisma.channel.upsert({
|
||||||
|
where: { id: 'default' },
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
id: 'default',
|
||||||
|
name: 'Default',
|
||||||
|
persistent: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.then(async () => {
|
||||||
|
await prisma.$disconnect()
|
||||||
|
})
|
||||||
|
.catch(async (e) => {
|
||||||
|
console.error(e)
|
||||||
|
await prisma.$disconnect()
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
255
server/routes/channels.ts
Normal file
255
server/routes/channels.ts
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
import type { FastifyInstance } from 'fastify'
|
||||||
|
import { z } from 'zod'
|
||||||
|
import prisma from '../prisma/client.ts'
|
||||||
|
import { channelPublicSelect } from '../dto/channel.dto.ts'
|
||||||
|
|
||||||
|
export default function (fastify: FastifyInstance) {
|
||||||
|
// GET /chad/channels - List all channels with client counts
|
||||||
|
fastify.get('/channels', async (req, reply) => {
|
||||||
|
if (!req.user) {
|
||||||
|
return reply.code(401).send({ error: 'Unauthorized' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const channels = await prisma.channel.findMany({
|
||||||
|
select: channelPublicSelect,
|
||||||
|
orderBy: { name: 'asc' },
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add client count to each channel using Socket.IO rooms
|
||||||
|
const channelsWithCounts = await Promise.all(
|
||||||
|
channels.map(async channel => {
|
||||||
|
const socketsInChannel = await fastify.io.in(channel.id).fetchSockets()
|
||||||
|
return {
|
||||||
|
...channel,
|
||||||
|
clientCount: socketsInChannel.length,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
return channelsWithCounts
|
||||||
|
})
|
||||||
|
|
||||||
|
// GET /chad/channels/:id - Get specific channel with client list
|
||||||
|
fastify.get('/channels/:id', async (req, reply) => {
|
||||||
|
if (!req.user) {
|
||||||
|
return reply.code(401).send({ error: 'Unauthorized' })
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const paramsSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
})
|
||||||
|
const params = paramsSchema.parse(req.params)
|
||||||
|
|
||||||
|
const channel = await prisma.channel.findUnique({
|
||||||
|
where: { id: params.id },
|
||||||
|
select: channelPublicSelect,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!channel) {
|
||||||
|
return reply.code(404).send({ error: 'Channel not found' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get clients in this channel using Socket.IO rooms
|
||||||
|
const sockets = await fastify.io.in(params.id).fetchSockets()
|
||||||
|
const clients = sockets
|
||||||
|
.filter(s => s.data.joined)
|
||||||
|
.map(s => {
|
||||||
|
const channelId = Array.from(s.rooms).find(room => room !== s.id) || 'default'
|
||||||
|
return {
|
||||||
|
socketId: s.id,
|
||||||
|
userId: s.data.userId,
|
||||||
|
username: s.data.username,
|
||||||
|
displayName: s.data.displayName,
|
||||||
|
inputMuted: s.data.inputMuted,
|
||||||
|
outputMuted: s.data.outputMuted,
|
||||||
|
currentChannelId: channelId,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
...channel,
|
||||||
|
clients,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
fastify.log.error(err)
|
||||||
|
reply.code(400)
|
||||||
|
if (err instanceof z.ZodError) {
|
||||||
|
reply.send({ error: z.prettifyError(err) })
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
reply.send({ error: err.message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// POST /chad/channels - Create channel
|
||||||
|
fastify.post('/channels', async (req, reply) => {
|
||||||
|
if (!req.user) {
|
||||||
|
return reply.code(401).send({ error: 'Unauthorized' })
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const schema = z.object({
|
||||||
|
name: z.string().min(1).max(50),
|
||||||
|
maxClients: z.number().int().positive().optional().nullable(),
|
||||||
|
persistent: z.boolean().default(false),
|
||||||
|
})
|
||||||
|
const input = schema.parse(req.body)
|
||||||
|
|
||||||
|
const channel = await prisma.channel.create({
|
||||||
|
data: {
|
||||||
|
name: input.name,
|
||||||
|
maxClients: input.maxClients,
|
||||||
|
persistent: input.persistent,
|
||||||
|
owner_id: req.user.id,
|
||||||
|
},
|
||||||
|
select: channelPublicSelect,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Notify all connected clients about new channel
|
||||||
|
fastify.io.emit('channelCreated', channel)
|
||||||
|
|
||||||
|
return channel
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
fastify.log.error(err)
|
||||||
|
reply.code(400)
|
||||||
|
if (err instanceof z.ZodError) {
|
||||||
|
reply.send({ error: z.prettifyError(err) })
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
reply.send({ error: err.message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// PATCH /chad/channels/:id - Update channel
|
||||||
|
fastify.patch('/channels/:id', async (req, reply) => {
|
||||||
|
if (!req.user) {
|
||||||
|
return reply.code(401).send({ error: 'Unauthorized' })
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const paramsSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
})
|
||||||
|
const params = paramsSchema.parse(req.params)
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
name: z.string().min(1).max(50).optional(),
|
||||||
|
maxClients: z.number().int().positive().optional().nullable(),
|
||||||
|
})
|
||||||
|
const input = schema.parse(req.body)
|
||||||
|
|
||||||
|
// Cannot update default channel
|
||||||
|
if (params.id === 'default') {
|
||||||
|
return reply.code(403).send({ error: 'Cannot modify default channel' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = await prisma.channel.findUnique({
|
||||||
|
where: { id: params.id },
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
return reply.code(404).send({ error: 'Channel not found' })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existing.owner_id !== req.user.id) {
|
||||||
|
return reply.code(403).send({ error: 'Not channel owner' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const channel = await prisma.channel.update({
|
||||||
|
where: { id: params.id },
|
||||||
|
data: input,
|
||||||
|
select: channelPublicSelect,
|
||||||
|
})
|
||||||
|
|
||||||
|
fastify.io.emit('channelUpdated', channel)
|
||||||
|
|
||||||
|
return channel
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
fastify.log.error(err)
|
||||||
|
reply.code(400)
|
||||||
|
if (err instanceof z.ZodError) {
|
||||||
|
reply.send({ error: z.prettifyError(err) })
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
reply.send({ error: err.message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// DELETE /chad/channels/:id - Delete channel
|
||||||
|
fastify.delete('/channels/:id', async (req, reply) => {
|
||||||
|
if (!req.user) {
|
||||||
|
return reply.code(401).send({ error: 'Unauthorized' })
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const paramsSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
})
|
||||||
|
const params = paramsSchema.parse(req.params)
|
||||||
|
|
||||||
|
if (params.id === 'default') {
|
||||||
|
return reply.code(403).send({ error: 'Cannot delete default channel' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = await prisma.channel.findUnique({
|
||||||
|
where: { id: params.id },
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
return reply.code(404).send({ error: 'Channel not found' })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existing.owner_id !== req.user.id) {
|
||||||
|
return reply.code(403).send({ error: 'Not channel owner' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move all users in this channel back to default using Socket.IO rooms
|
||||||
|
const sockets = await fastify.io.in(params.id).fetchSockets()
|
||||||
|
|
||||||
|
for (const socket of sockets) {
|
||||||
|
// Close all their producers
|
||||||
|
for (const producer of socket.data.producers.values()) {
|
||||||
|
producer.close()
|
||||||
|
}
|
||||||
|
socket.data.producers.clear()
|
||||||
|
|
||||||
|
// Close all their consumers
|
||||||
|
for (const consumer of socket.data.consumers.values()) {
|
||||||
|
consumer.close()
|
||||||
|
}
|
||||||
|
socket.data.consumers.clear()
|
||||||
|
|
||||||
|
// Move to default room
|
||||||
|
await socket.leave(params.id)
|
||||||
|
await socket.join('default')
|
||||||
|
|
||||||
|
socket.emit('forcedChannelSwitch', { channelId: 'default' })
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.channel.delete({
|
||||||
|
where: { id: params.id },
|
||||||
|
})
|
||||||
|
|
||||||
|
fastify.io.emit('channelDeleted', { channelId: params.id })
|
||||||
|
|
||||||
|
return { success: true }
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
fastify.log.error(err)
|
||||||
|
reply.code(400)
|
||||||
|
if (err instanceof z.ZodError) {
|
||||||
|
reply.send({ error: z.prettifyError(err) })
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
reply.send({ error: err.message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { FastifyInstance } from 'fastify'
|
import type { FastifyInstance } from 'fastify'
|
||||||
import type { Namespace } from '../types/webrtc.ts'
|
import type { Namespace } from '../types/socket.ts'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import prisma from '../prisma/client.ts'
|
import prisma from '../prisma/client.ts'
|
||||||
import { socketToClient } from '../utils/socket-to-client.ts'
|
import { socketToClient } from '../utils/socket-to-client.ts'
|
||||||
@@ -77,7 +77,7 @@ export default function (fastify: FastifyInstance) {
|
|||||||
|
|
||||||
if (found) {
|
if (found) {
|
||||||
found.data.displayName = input.displayName
|
found.data.displayName = input.displayName
|
||||||
namespace.emit('clientChanged', found.id, socketToClient(found))
|
namespace.emit('webrtc:client-changed', found.id, socketToClient(found))
|
||||||
}
|
}
|
||||||
|
|
||||||
return updatedUser
|
return updatedUser
|
||||||
|
|||||||
150
server/socket/channel.ts
Normal file
150
server/socket/channel.ts
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import type {
|
||||||
|
ChadSocket,
|
||||||
|
ClientToServerEvents,
|
||||||
|
ExtractCallbackPayload,
|
||||||
|
SocketServer,
|
||||||
|
} from '../types/socket.ts'
|
||||||
|
import { consola } from 'consola'
|
||||||
|
import { channelPublicSelect } from '../dto/channel.dto.ts'
|
||||||
|
import prisma from '../prisma/client.ts'
|
||||||
|
import { socketToClient } from '../utils/socket-to-client.ts'
|
||||||
|
|
||||||
|
export default async function (io: SocketServer, socket: ChadSocket) {
|
||||||
|
// io.on('channel:join', async (cb) => {
|
||||||
|
// if (socket.data.joined) {
|
||||||
|
// consola.error('[WebRtc]', 'Already joined')
|
||||||
|
// cb({ error: 'Already joined' })
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// socket.data.joined = true
|
||||||
|
// socket.data.rtpCapabilities = rtpCapabilities
|
||||||
|
//
|
||||||
|
// // Get current channel from Socket.IO rooms
|
||||||
|
// const currentChannelId = Array.from(socket.rooms).find(room => room !== socket.id) || 'default'
|
||||||
|
// const joinedSockets = await getJoinedSockets(socket.id, currentChannelId)
|
||||||
|
//
|
||||||
|
// cb(joinedSockets.map(socketToClient))
|
||||||
|
//
|
||||||
|
// for (const joinedSocket of joinedSockets) {
|
||||||
|
// for (const producer of joinedSocket.data.producers.values()) {
|
||||||
|
// createConsumer(
|
||||||
|
// socket,
|
||||||
|
// joinedSocket,
|
||||||
|
// producer,
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Broadcast only to same channel using Socket.IO room
|
||||||
|
// socket.to(currentChannelId).emit('newPeer', socketToClient(socket))
|
||||||
|
// })
|
||||||
|
|
||||||
|
socket.on('channel:join', async ({ channelId }, cb) => {
|
||||||
|
try {
|
||||||
|
cb(await handleJoin(channelId))
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
cb(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await handleJoin('default')
|
||||||
|
|
||||||
|
const channels = await prisma.channel.findMany({
|
||||||
|
select: channelPublicSelect,
|
||||||
|
orderBy: { name: 'asc' },
|
||||||
|
})
|
||||||
|
|
||||||
|
type ChannelJoinCallback = ExtractCallbackPayload<ClientToServerEvents['channel:join']>
|
||||||
|
async function handleJoin(channelId: string): Promise<ChannelJoinCallback> {
|
||||||
|
try {
|
||||||
|
const channel = await prisma.channel.findUnique({
|
||||||
|
where: { id: channelId },
|
||||||
|
select: channelPublicSelect,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!channel) {
|
||||||
|
return { error: 'Channel not found' }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channel.maxClients) {
|
||||||
|
const socketsInChannel = await io.in(channelId).fetchSockets()
|
||||||
|
if (socketsInChannel.length >= channel.maxClients) {
|
||||||
|
return { error: 'Channel is full' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldChannelId = Array.from(socket.rooms).find(room => room !== socket.id)
|
||||||
|
|
||||||
|
// for (const producer of socket.data.producers.values()) {
|
||||||
|
// producer.close()
|
||||||
|
// }
|
||||||
|
// socket.data.producers.clear()
|
||||||
|
//
|
||||||
|
// for (const consumer of socket.data.consumers.values()) {
|
||||||
|
// consumer.close()
|
||||||
|
// }
|
||||||
|
// socket.data.consumers.clear()
|
||||||
|
|
||||||
|
if (oldChannelId) {
|
||||||
|
await socket.leave(oldChannelId)
|
||||||
|
io.emit('channel:user-left', { channelId: oldChannelId, clientId: socket.id })
|
||||||
|
|
||||||
|
// // Auto-delete non-persistent empty channels
|
||||||
|
// if (isLeavingNonPersistentChannel) {
|
||||||
|
// const oldChannelSockets = await io.in(oldChannelId).fetchSockets()
|
||||||
|
//
|
||||||
|
// if (oldChannelSockets.length === 0) {
|
||||||
|
// const oldChannel = await prisma.channel.findUnique({
|
||||||
|
// where: { id: oldChannelId },
|
||||||
|
// select: { persistent: true, id: true },
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
// if (oldChannel && !oldChannel.persistent) {
|
||||||
|
// await prisma.channel.delete({ where: { id: oldChannelId } })
|
||||||
|
// io.emit('channelDeleted', { channelId: oldChannelId })
|
||||||
|
// consola.info('[Channel]', `Auto-deleted empty non-persistent channel: ${oldChannelId}`)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
await socket.join(channelId)
|
||||||
|
|
||||||
|
// Get new channel members
|
||||||
|
// const newChannelSockets = await getJoinedSockets(socket.id, channelId)
|
||||||
|
//
|
||||||
|
// // Create consumers for existing producers in new channel
|
||||||
|
// for (const peer of newChannelSockets) {
|
||||||
|
// for (const producer of peer.data.producers.values()) {
|
||||||
|
// createConsumer(socket, peer, producer)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
io.emit('channel:user-joined', {
|
||||||
|
channelId,
|
||||||
|
client: socketToClient(socket),
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
channel,
|
||||||
|
clients: newChannelSockets.map(socketToClient),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
consola.error('[channel:join]', error)
|
||||||
|
|
||||||
|
if (error instanceof Error) {
|
||||||
|
return { error: error.message }
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return { error: 'Something went wrong' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
channels,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,52 +1,21 @@
|
|||||||
import type { types } from 'mediasoup'
|
import type { types } from 'mediasoup'
|
||||||
|
import type { ActiveSpeakerObserver, AudioLevelObserver } from 'mediasoup/types'
|
||||||
import type { Server as SocketServer } from 'socket.io'
|
import type { Server as SocketServer } from 'socket.io'
|
||||||
import type {
|
import type {
|
||||||
ChadClient,
|
ChadSocket,
|
||||||
Namespace,
|
|
||||||
SomeSocket,
|
SomeSocket,
|
||||||
} from '../types/webrtc.ts'
|
} from '../types/socket.ts'
|
||||||
import { consola } from 'consola'
|
import { consola } from 'consola'
|
||||||
import prisma from '../prisma/client.ts'
|
|
||||||
import { socketToClient } from '../utils/socket-to-client.ts'
|
import { socketToClient } from '../utils/socket-to-client.ts'
|
||||||
|
|
||||||
export default async function (io: SocketServer, router: types.Router) {
|
export default async function (
|
||||||
const namespace: Namespace = io.of('/webrtc')
|
io: SocketServer,
|
||||||
|
socket: ChadSocket,
|
||||||
const audioLevelObserver = await router.createAudioLevelObserver({
|
router: types.Router,
|
||||||
maxEntries: 10,
|
audioLevelObserver: AudioLevelObserver,
|
||||||
threshold: -80,
|
activeSpeakerObserver: ActiveSpeakerObserver,
|
||||||
interval: 800,
|
) {
|
||||||
})
|
io.on('connection', async (socket) => {
|
||||||
|
|
||||||
const activeSpeakerObserver = await router.createActiveSpeakerObserver()
|
|
||||||
|
|
||||||
audioLevelObserver.on('volumes', async (volumes: types.AudioLevelObserverVolume[]) => {
|
|
||||||
namespace.emit('speakingPeers', volumes.map(({ producer, volume }) => {
|
|
||||||
const { socketId } = producer.appData as { socketId: ChadClient['socketId'] }
|
|
||||||
|
|
||||||
return {
|
|
||||||
clientId: socketId,
|
|
||||||
volume,
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
|
|
||||||
audioLevelObserver.on('silence', () => {
|
|
||||||
namespace.emit('speakingPeers', [])
|
|
||||||
namespace.emit('activeSpeaker', undefined)
|
|
||||||
})
|
|
||||||
|
|
||||||
activeSpeakerObserver.on('dominantspeaker', ({ producer }) => {
|
|
||||||
const { socketId } = producer.appData as { socketId: ChadClient['socketId'] }
|
|
||||||
|
|
||||||
namespace.emit('activeSpeaker', socketId)
|
|
||||||
})
|
|
||||||
|
|
||||||
namespace.on('connection', async (socket) => {
|
|
||||||
consola.info('[WebRtc]', 'Client connected', socket.id)
|
|
||||||
|
|
||||||
socket.data.joined = false
|
|
||||||
|
|
||||||
socket.data.inputMuted = false
|
socket.data.inputMuted = false
|
||||||
socket.data.outputMuted = false
|
socket.data.outputMuted = false
|
||||||
|
|
||||||
@@ -54,54 +23,11 @@ export default async function (io: SocketServer, router: types.Router) {
|
|||||||
socket.data.producers = new Map()
|
socket.data.producers = new Map()
|
||||||
socket.data.consumers = new Map()
|
socket.data.consumers = new Map()
|
||||||
|
|
||||||
const { id, username, displayName } = await prisma.user.findUnique({
|
socket.on('webrtc:get-rtp-capabilities', (cb) => {
|
||||||
where: {
|
|
||||||
id: socket.handshake.auth.userId,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
username: true,
|
|
||||||
displayName: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
socket.data.userId = id
|
|
||||||
socket.data.username = username
|
|
||||||
socket.data.displayName = displayName
|
|
||||||
|
|
||||||
socket.emit('authenticated')
|
|
||||||
|
|
||||||
socket.on('join', async ({ rtpCapabilities }, cb) => {
|
|
||||||
if (socket.data.joined) {
|
|
||||||
consola.error('[WebRtc]', 'Already joined')
|
|
||||||
cb({ error: 'Already joined' })
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.data.joined = true
|
|
||||||
socket.data.rtpCapabilities = rtpCapabilities
|
|
||||||
|
|
||||||
const joinedSockets = await getJoinedSockets()
|
|
||||||
|
|
||||||
cb(joinedSockets.map(socketToClient))
|
|
||||||
|
|
||||||
for (const joinedSocket of joinedSockets.filter(joinedSocket => joinedSocket.id !== socket.id)) {
|
|
||||||
for (const producer of joinedSocket.data.producers.values()) {
|
|
||||||
createConsumer(
|
|
||||||
socket,
|
|
||||||
joinedSocket,
|
|
||||||
producer,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.broadcast.emit('newPeer', socketToClient(socket))
|
|
||||||
})
|
|
||||||
|
|
||||||
socket.on('getRtpCapabilities', (cb) => {
|
|
||||||
cb(router.rtpCapabilities)
|
cb(router.rtpCapabilities)
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on('createTransport', async ({ producing, consuming }, cb) => {
|
socket.on('webrtc:create-transport', async ({ producing, consuming }, cb) => {
|
||||||
try {
|
try {
|
||||||
const transport = await router.createWebRtcTransport({
|
const transport = await router.createWebRtcTransport({
|
||||||
listenInfos: [
|
listenInfos: [
|
||||||
@@ -156,7 +82,7 @@ export default async function (io: SocketServer, router: types.Router) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on('connectTransport', async ({ transportId, dtlsParameters }, cb) => {
|
socket.on('webrtc:connect-transport', async ({ transportId, dtlsParameters }, cb) => {
|
||||||
const transport = socket.data.transports.get(transportId)
|
const transport = socket.data.transports.get(transportId)
|
||||||
|
|
||||||
if (!transport) {
|
if (!transport) {
|
||||||
@@ -179,7 +105,7 @@ export default async function (io: SocketServer, router: types.Router) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on('produce', async ({ transportId, kind, rtpParameters, appData }, cb) => {
|
socket.on('webrtc:produce', async ({ transportId, kind, rtpParameters, appData }, cb) => {
|
||||||
if (!socket.data.joined) {
|
if (!socket.data.joined) {
|
||||||
consola.error('Peer not joined yet')
|
consola.error('Peer not joined yet')
|
||||||
cb({ error: 'Peer not joined yet' })
|
cb({ error: 'Peer not joined yet' })
|
||||||
@@ -187,6 +113,14 @@ export default async function (io: SocketServer, router: types.Router) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Block production in default channel
|
||||||
|
const currentChannelId = Array.from(socket.rooms).find(room => room !== socket.id) || 'default'
|
||||||
|
if (currentChannelId === 'default') {
|
||||||
|
consola.error('Cannot produce in default channel')
|
||||||
|
cb({ error: 'Cannot produce media in default channel' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const transport = socket.data.transports.get(transportId)
|
const transport = socket.data.transports.get(transportId)
|
||||||
|
|
||||||
if (!transport) {
|
if (!transport) {
|
||||||
@@ -203,7 +137,8 @@ export default async function (io: SocketServer, router: types.Router) {
|
|||||||
|
|
||||||
cb({ id: producer.id })
|
cb({ id: producer.id })
|
||||||
|
|
||||||
const otherSockets = await getJoinedSockets(socket.id)
|
// Filter by channel when creating consumers
|
||||||
|
const otherSockets = await getJoinedSockets(socket.id, currentChannelId)
|
||||||
|
|
||||||
for (const otherSocket of otherSockets) {
|
for (const otherSocket of otherSockets) {
|
||||||
createConsumer(
|
createConsumer(
|
||||||
@@ -224,7 +159,7 @@ export default async function (io: SocketServer, router: types.Router) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on('closeProducer', async ({ producerId }, cb) => {
|
socket.on('webrtc:close-producer', async ({ producerId }, cb) => {
|
||||||
if (!socket.data.joined) {
|
if (!socket.data.joined) {
|
||||||
consola.error('Peer not joined yet')
|
consola.error('Peer not joined yet')
|
||||||
cb({ error: 'Peer not joined yet' })
|
cb({ error: 'Peer not joined yet' })
|
||||||
@@ -248,7 +183,7 @@ export default async function (io: SocketServer, router: types.Router) {
|
|||||||
cb({ ok: true })
|
cb({ ok: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on('pauseProducer', async ({ producerId }, cb) => {
|
socket.on('webrtc:pause-producer', async ({ producerId }, cb) => {
|
||||||
if (!socket.data.joined) {
|
if (!socket.data.joined) {
|
||||||
consola.error('Peer not joined yet')
|
consola.error('Peer not joined yet')
|
||||||
cb({ error: 'Peer not joined yet' })
|
cb({ error: 'Peer not joined yet' })
|
||||||
@@ -273,7 +208,7 @@ export default async function (io: SocketServer, router: types.Router) {
|
|||||||
cb({ ok: true })
|
cb({ ok: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on('resumeProducer', async ({ producerId }, cb) => {
|
socket.on('webrtc:resume-producer', async ({ producerId }, cb) => {
|
||||||
if (!socket.data.joined) {
|
if (!socket.data.joined) {
|
||||||
consola.error('Peer not joined yet')
|
consola.error('Peer not joined yet')
|
||||||
cb({ error: 'Peer not joined yet' })
|
cb({ error: 'Peer not joined yet' })
|
||||||
@@ -295,7 +230,7 @@ export default async function (io: SocketServer, router: types.Router) {
|
|||||||
cb({ ok: true })
|
cb({ ok: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on('pauseConsumer', async ({ consumerId }, cb) => {
|
socket.on('webrtc:pause-consumer', async ({ consumerId }, cb) => {
|
||||||
if (!socket.data.joined) {
|
if (!socket.data.joined) {
|
||||||
consola.error('Peer not joined yet')
|
consola.error('Peer not joined yet')
|
||||||
cb({ error: 'Peer not joined yet' })
|
cb({ error: 'Peer not joined yet' })
|
||||||
@@ -317,7 +252,7 @@ export default async function (io: SocketServer, router: types.Router) {
|
|||||||
cb({ ok: true })
|
cb({ ok: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on('resumeConsumer', async ({ consumerId }, cb) => {
|
socket.on('webrtc:resume-consumer', async ({ consumerId }, cb) => {
|
||||||
if (!socket.data.joined) {
|
if (!socket.data.joined) {
|
||||||
consola.error('Peer not joined yet')
|
consola.error('Peer not joined yet')
|
||||||
cb({ error: 'Peer not joined yet' })
|
cb({ error: 'Peer not joined yet' })
|
||||||
@@ -339,7 +274,7 @@ export default async function (io: SocketServer, router: types.Router) {
|
|||||||
cb({ ok: true })
|
cb({ ok: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on('updateClient', async (updatedClient, cb) => {
|
socket.on('webrtc:update-client', async (updatedClient, cb) => {
|
||||||
if (typeof updatedClient.inputMuted === 'boolean') {
|
if (typeof updatedClient.inputMuted === 'boolean') {
|
||||||
socket.data.inputMuted = updatedClient.inputMuted
|
socket.data.inputMuted = updatedClient.inputMuted
|
||||||
}
|
}
|
||||||
@@ -350,14 +285,21 @@ export default async function (io: SocketServer, router: types.Router) {
|
|||||||
|
|
||||||
cb(socketToClient(socket))
|
cb(socketToClient(socket))
|
||||||
|
|
||||||
namespace.emit('clientChanged', socket.id, socketToClient(socket))
|
io.emit('webrtc:client-changed', socket.id, socketToClient(socket))
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on('disconnect', () => {
|
socket.on('disconnect', () => {
|
||||||
consola.info('Client disconnected:', socket.id)
|
consola.info('Client disconnected:', socket.id)
|
||||||
|
|
||||||
|
// Get current channel from Socket.IO rooms
|
||||||
|
const channelId = Array.from(socket.rooms).find(room => room !== socket.id)
|
||||||
|
|
||||||
if (socket.data.joined) {
|
if (socket.data.joined) {
|
||||||
socket.broadcast.emit('peerClosed', socket.id)
|
// Notify only same channel using Socket.IO room
|
||||||
|
if (channelId) {
|
||||||
|
socket.to(channelId).emit('webrtc:peer-closed', socket.id)
|
||||||
|
io.emit('channelUserLeft', { channelId, clientId: socket.id })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const transport of socket.data.transports.values()) {
|
for (const transport of socket.data.transports.values()) {
|
||||||
@@ -366,10 +308,21 @@ export default async function (io: SocketServer, router: types.Router) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
async function getJoinedSockets(excludeId?: string) {
|
async function getJoinedSockets(excludeId?: string, channelId?: string) {
|
||||||
const sockets = await namespace.fetchSockets()
|
let sockets = await io.fetchSockets()
|
||||||
|
|
||||||
return sockets.filter(socket => socket.data.joined && (excludeId ? excludeId !== socket.id : true))
|
// Filter by channel using Socket.IO rooms
|
||||||
|
if (channelId) {
|
||||||
|
sockets = await io.in(channelId).fetchSockets()
|
||||||
|
}
|
||||||
|
|
||||||
|
return sockets.filter((socket) => {
|
||||||
|
if (!socket.data.joined)
|
||||||
|
return false
|
||||||
|
if (excludeId && socket.id === excludeId)
|
||||||
|
return false
|
||||||
|
return true
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createConsumer(
|
async function createConsumer(
|
||||||
@@ -427,24 +380,24 @@ export default async function (io: SocketServer, router: types.Router) {
|
|||||||
consumer.on('producerclose', () => {
|
consumer.on('producerclose', () => {
|
||||||
consumerSocket.data.consumers.delete(consumer.id)
|
consumerSocket.data.consumers.delete(consumer.id)
|
||||||
|
|
||||||
consumerSocket.emit('consumerClosed', { consumerId: consumer.id })
|
consumerSocket.emit('webrtc:consumer-closed', { consumerId: consumer.id })
|
||||||
})
|
})
|
||||||
|
|
||||||
consumer.on('producerpause', () => {
|
consumer.on('producerpause', () => {
|
||||||
consumerSocket.emit('consumerPaused', { consumerId: consumer.id })
|
consumerSocket.emit('webrtc:consumer-paused', { consumerId: consumer.id })
|
||||||
})
|
})
|
||||||
|
|
||||||
consumer.on('producerresume', () => {
|
consumer.on('producerresume', () => {
|
||||||
consumerSocket.emit('consumerResumed', { consumerId: consumer.id })
|
consumerSocket.emit('webrtc:consumer-resumed', { consumerId: consumer.id })
|
||||||
})
|
})
|
||||||
|
|
||||||
consumer.on('score', (score: types.ConsumerScore) => {
|
consumer.on('score', (score: types.ConsumerScore) => {
|
||||||
consumerSocket.emit('consumerScore', { consumerId: consumer.id, score })
|
consumerSocket.emit('webrtc:consumer-score', { consumerId: consumer.id, score })
|
||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await consumerSocket.emitWithAck(
|
await consumerSocket.emitWithAck(
|
||||||
'newConsumer',
|
'webrtc:new-consumer',
|
||||||
{
|
{
|
||||||
socketId: producerSocket.id,
|
socketId: producerSocket.id,
|
||||||
producerId: producer.id,
|
producerId: producer.id,
|
||||||
@@ -460,7 +413,7 @@ export default async function (io: SocketServer, router: types.Router) {
|
|||||||
await consumer.resume()
|
await consumer.resume()
|
||||||
|
|
||||||
consumerSocket.emit(
|
consumerSocket.emit(
|
||||||
'consumerScore',
|
'webrtc:consumer-score',
|
||||||
{
|
{
|
||||||
consumerId: consumer.id,
|
consumerId: consumer.id,
|
||||||
score: consumer.score,
|
score: consumer.score,
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es2016",
|
|
||||||
"module": "ESNext",
|
|
||||||
"moduleResolution": "nodenext",
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"strict": true,
|
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"allowImportingTsExtensions": true
|
"allowImportingTsExtensions": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"target": "ES2023",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
173
server/types/socket.ts
Normal file
173
server/types/socket.ts
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
import type { types } from 'mediasoup'
|
||||||
|
import type { RemoteSocket, Server, Socket } from 'socket.io'
|
||||||
|
import type { ChannelPublicDTO } from '../dto/channel.dto.ts'
|
||||||
|
import type { ChannelModel, UserModel } from '../prisma/generated/client/models.ts'
|
||||||
|
|
||||||
|
export interface ServerInfo {
|
||||||
|
owner_id: UserModel['id']
|
||||||
|
channels: ChannelPublicDTO[]
|
||||||
|
rtpCapabilities: types.RtpCapabilities
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChadClient {
|
||||||
|
socketId: string
|
||||||
|
userId: UserModel['id']
|
||||||
|
username: UserModel['username']
|
||||||
|
displayName: UserModel['displayName']
|
||||||
|
inputMuted: boolean
|
||||||
|
outputMuted: boolean
|
||||||
|
currentChannelId: ChannelModel['id']
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProducerShort {
|
||||||
|
producerId: types.Producer['id']
|
||||||
|
kind: types.MediaKind
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ErrorCallbackResult {
|
||||||
|
error: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SuccessCallbackResult {
|
||||||
|
ok: true
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EventCallback<T = SuccessCallbackResult> = (result: T | ErrorCallbackResult) => void
|
||||||
|
export type EventCallbackResult<T = SuccessCallbackResult> = Parameters<EventCallback<T>>[0]
|
||||||
|
|
||||||
|
type LastArg<T extends any[]> = T extends [...any[], infer Last] ? Last : T extends [infer Only] ? Only : never
|
||||||
|
|
||||||
|
export type ExtractCallbackPayload<
|
||||||
|
T,
|
||||||
|
Fallback = EventCallbackResult,
|
||||||
|
>
|
||||||
|
= T extends (...args: infer Args) => any
|
||||||
|
? LastArg<Args> extends (...inner: infer CallbackArgs) => any
|
||||||
|
? CallbackArgs extends [infer First, ...any[]]
|
||||||
|
? First
|
||||||
|
: Fallback
|
||||||
|
: Fallback
|
||||||
|
: Fallback
|
||||||
|
|
||||||
|
export interface ClientToServerEvents {
|
||||||
|
'webrtc:get-rtp-capabilities': (
|
||||||
|
cb: EventCallback<types.RtpCapabilities>
|
||||||
|
) => void
|
||||||
|
'webrtc:create-transport': (
|
||||||
|
options: {
|
||||||
|
producing: boolean
|
||||||
|
consuming: boolean
|
||||||
|
},
|
||||||
|
cb: EventCallback<Pick<types.WebRtcTransport, 'id' | 'iceParameters' | 'iceCandidates' | 'dtlsParameters'>>
|
||||||
|
) => void
|
||||||
|
'webrtc:connect-transport': (
|
||||||
|
options: {
|
||||||
|
transportId: types.WebRtcTransport['id']
|
||||||
|
dtlsParameters: types.WebRtcTransport['dtlsParameters']
|
||||||
|
},
|
||||||
|
cb: EventCallback
|
||||||
|
) => void
|
||||||
|
'webrtc:produce': (
|
||||||
|
options: {
|
||||||
|
transportId: types.WebRtcTransport['id']
|
||||||
|
kind: types.MediaKind
|
||||||
|
rtpParameters: types.RtpParameters
|
||||||
|
appData: { source: 'share' | string }
|
||||||
|
},
|
||||||
|
cb: EventCallback<{ id: types.Producer['id'] }>
|
||||||
|
) => void
|
||||||
|
'webrtc:close-producer': (
|
||||||
|
options: {
|
||||||
|
producerId: types.Producer['id']
|
||||||
|
},
|
||||||
|
cb: EventCallback
|
||||||
|
) => void
|
||||||
|
'webrtc:pause-producer': (
|
||||||
|
options: {
|
||||||
|
producerId: types.Producer['id']
|
||||||
|
},
|
||||||
|
cb: EventCallback
|
||||||
|
) => void
|
||||||
|
'webrtc:resume-producer': (
|
||||||
|
options: {
|
||||||
|
producerId: types.Producer['id']
|
||||||
|
},
|
||||||
|
cb: EventCallback
|
||||||
|
) => void
|
||||||
|
'webrtc:pause-consumer': (
|
||||||
|
options: {
|
||||||
|
consumerId: types.Consumer['id']
|
||||||
|
},
|
||||||
|
cb: EventCallback
|
||||||
|
) => void
|
||||||
|
'webrtc:resume-consumer': (
|
||||||
|
options: {
|
||||||
|
consumerId: types.Consumer['id']
|
||||||
|
},
|
||||||
|
cb: EventCallback
|
||||||
|
) => void
|
||||||
|
'webrtc:update-client': (
|
||||||
|
options: Partial<Pick<ChadClient, 'inputMuted' | 'outputMuted'>>,
|
||||||
|
cb: EventCallback<ChadClient>
|
||||||
|
) => void
|
||||||
|
|
||||||
|
'channel:join': (
|
||||||
|
options: { channelId: ChannelModel['id'] },
|
||||||
|
cb: EventCallback<{ channel: ChannelPublicDTO, clients: ChadClient[] }>
|
||||||
|
) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServerToClientEvents {
|
||||||
|
'webrtc:authenticated': (arg: ServerInfo) => void
|
||||||
|
'webrtc:producers': (arg: ProducerShort[]) => void
|
||||||
|
'webrtc:new-consumer': (
|
||||||
|
arg: {
|
||||||
|
socketId: string
|
||||||
|
producerId: types.Producer['id']
|
||||||
|
id: types.Consumer['id']
|
||||||
|
kind: types.MediaKind
|
||||||
|
rtpParameters: types.RtpParameters
|
||||||
|
type: types.ConsumerType
|
||||||
|
appData: types.Producer['appData']
|
||||||
|
producerPaused: types.Consumer['producerPaused']
|
||||||
|
},
|
||||||
|
cb: EventCallback
|
||||||
|
) => void
|
||||||
|
'webrtc:peer-closed': (arg: string) => void
|
||||||
|
'webrtc:consumer-closed': (arg: { consumerId: string }) => void
|
||||||
|
'webrtc:consumer-paused': (arg: { consumerId: string }) => void
|
||||||
|
'webrtc:consumer-resumed': (arg: { consumerId: string }) => void
|
||||||
|
'webrtc:consumer-score': (arg: { consumerId: string, score: types.ConsumerScore }) => void
|
||||||
|
'webrtc:client-changed': (clientId: ChadClient['socketId'], client: ChadClient) => void
|
||||||
|
'webrtc:speaking-peers': (arg: { clientId: ChadClient['socketId'], volume: types.AudioLevelObserverVolume['volume'] }[]) => void
|
||||||
|
'webrtc:active-speaker': (clientId?: ChadClient['socketId']) => void
|
||||||
|
|
||||||
|
'channel:user-joined': (arg: { channelId: string, client: ChadClient }) => void
|
||||||
|
'channel:user-left': (arg: { channelId: string, clientId: string }) => void
|
||||||
|
'channel:deleted': (arg: { channelId: string }) => void
|
||||||
|
'channel:created': (arg: ChannelPublicDTO) => void
|
||||||
|
'channel:updated': (arg: ChannelPublicDTO) => void
|
||||||
|
'channel:force-switch': (arg: { channelId: string }) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InterServerEvent {}
|
||||||
|
|
||||||
|
export interface SocketData {
|
||||||
|
joined: boolean
|
||||||
|
userId: UserModel['id']
|
||||||
|
username: UserModel['username']
|
||||||
|
displayName: UserModel['displayName']
|
||||||
|
inputMuted: boolean
|
||||||
|
outputMuted: boolean
|
||||||
|
rtpCapabilities: types.RtpCapabilities
|
||||||
|
transports: Map<types.WebRtcTransport['id'], types.WebRtcTransport>
|
||||||
|
producers: Map<types.Producer['id'], types.Producer>
|
||||||
|
consumers: Map<types.Consumer['id'], types.Consumer>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ChadSocket = Socket<ClientToServerEvents, ServerToClientEvents, InterServerEvent, SocketData>
|
||||||
|
export type ChadRemoteSocket = RemoteSocket<ServerToClientEvents, SocketData>
|
||||||
|
|
||||||
|
export type SomeSocket = ChadSocket | ChadRemoteSocket
|
||||||
|
|
||||||
|
export type SocketServer = Server<ClientToServerEvents, ServerToClientEvents, InterServerEvent, SocketData>
|
||||||
4
server/types/utils.ts
Normal file
4
server/types/utils.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export type LastArgument<T>
|
||||||
|
= T extends (...args: [...any[], infer Last]) => any
|
||||||
|
? Last
|
||||||
|
: never
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
import type { types } from 'mediasoup'
|
|
||||||
import type { RemoteSocket, Socket, Namespace as SocketNamespace } from 'socket.io'
|
|
||||||
import type { User } from '../prisma/client'
|
|
||||||
|
|
||||||
export interface ChadClient {
|
|
||||||
socketId: string
|
|
||||||
userId: User['id']
|
|
||||||
username: User['username']
|
|
||||||
displayName: User['displayName']
|
|
||||||
inputMuted: boolean
|
|
||||||
outputMuted: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ProducerShort {
|
|
||||||
producerId: types.Producer['id']
|
|
||||||
kind: types.MediaKind
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ErrorCallbackResult {
|
|
||||||
error: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SuccessCallbackResult {
|
|
||||||
ok: true
|
|
||||||
}
|
|
||||||
|
|
||||||
export type EventCallback<T = SuccessCallbackResult> = (result: T | ErrorCallbackResult) => void
|
|
||||||
|
|
||||||
export interface ClientToServerEvents {
|
|
||||||
join: (
|
|
||||||
options: {
|
|
||||||
rtpCapabilities: types.RtpCapabilities
|
|
||||||
},
|
|
||||||
cb: EventCallback<ChadClient[]>
|
|
||||||
) => void
|
|
||||||
getRtpCapabilities: (
|
|
||||||
cb: EventCallback<types.RtpCapabilities>
|
|
||||||
) => void
|
|
||||||
createTransport: (
|
|
||||||
options: {
|
|
||||||
producing: boolean
|
|
||||||
consuming: boolean
|
|
||||||
},
|
|
||||||
cb: EventCallback<Pick<types.WebRtcTransport, 'id' | 'iceParameters' | 'iceCandidates' | 'dtlsParameters'>>
|
|
||||||
) => void
|
|
||||||
connectTransport: (
|
|
||||||
options: {
|
|
||||||
transportId: types.WebRtcTransport['id']
|
|
||||||
dtlsParameters: types.WebRtcTransport['dtlsParameters']
|
|
||||||
},
|
|
||||||
cb: EventCallback
|
|
||||||
) => void
|
|
||||||
produce: (
|
|
||||||
options: {
|
|
||||||
transportId: types.WebRtcTransport['id']
|
|
||||||
kind: types.MediaKind
|
|
||||||
rtpParameters: types.RtpParameters
|
|
||||||
appData: { source: 'share' | string }
|
|
||||||
},
|
|
||||||
cb: EventCallback<{ id: types.Producer['id'] }>
|
|
||||||
) => void
|
|
||||||
closeProducer: (
|
|
||||||
options: {
|
|
||||||
producerId: types.Producer['id']
|
|
||||||
},
|
|
||||||
cb: EventCallback
|
|
||||||
) => void
|
|
||||||
pauseProducer: (
|
|
||||||
options: {
|
|
||||||
producerId: types.Producer['id']
|
|
||||||
},
|
|
||||||
cb: EventCallback
|
|
||||||
) => void
|
|
||||||
resumeProducer: (
|
|
||||||
options: {
|
|
||||||
producerId: types.Producer['id']
|
|
||||||
},
|
|
||||||
cb: EventCallback
|
|
||||||
) => void
|
|
||||||
pauseConsumer: (
|
|
||||||
options: {
|
|
||||||
consumerId: types.Consumer['id']
|
|
||||||
},
|
|
||||||
cb: EventCallback
|
|
||||||
) => void
|
|
||||||
resumeConsumer: (
|
|
||||||
options: {
|
|
||||||
consumerId: types.Consumer['id']
|
|
||||||
},
|
|
||||||
cb: EventCallback
|
|
||||||
) => void
|
|
||||||
updateClient: (
|
|
||||||
options: Partial<Pick<ChadClient, 'inputMuted' | 'outputMuted'>>,
|
|
||||||
cb: EventCallback<ChadClient>
|
|
||||||
) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ServerToClientEvents {
|
|
||||||
authenticated: () => void
|
|
||||||
newPeer: (arg: ChadClient) => void
|
|
||||||
producers: (arg: ProducerShort[]) => void
|
|
||||||
newConsumer: (
|
|
||||||
arg: {
|
|
||||||
socketId: string
|
|
||||||
producerId: types.Producer['id']
|
|
||||||
id: types.Consumer['id']
|
|
||||||
kind: types.MediaKind
|
|
||||||
rtpParameters: types.RtpParameters
|
|
||||||
type: types.ConsumerType
|
|
||||||
appData: types.Producer['appData']
|
|
||||||
producerPaused: types.Consumer['producerPaused']
|
|
||||||
},
|
|
||||||
cb: EventCallback
|
|
||||||
) => void
|
|
||||||
peerClosed: (arg: string) => void
|
|
||||||
consumerClosed: (arg: { consumerId: string }) => void
|
|
||||||
consumerPaused: (arg: { consumerId: string }) => void
|
|
||||||
consumerResumed: (arg: { consumerId: string }) => void
|
|
||||||
consumerScore: (arg: { consumerId: string, score: types.ConsumerScore }) => void
|
|
||||||
clientChanged: (clientId: ChadClient['socketId'], client: ChadClient) => void
|
|
||||||
speakingPeers: (arg: { clientId: ChadClient['socketId'], volume: types.AudioLevelObserverVolume['volume'] }[]) => void
|
|
||||||
activeSpeaker: (clientId?: ChadClient['socketId']) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface InterServerEvent {}
|
|
||||||
|
|
||||||
export interface SocketData {
|
|
||||||
joined: boolean
|
|
||||||
userId: User['id']
|
|
||||||
username: User['username']
|
|
||||||
displayName: User['displayName']
|
|
||||||
inputMuted: boolean
|
|
||||||
outputMuted: boolean
|
|
||||||
rtpCapabilities: types.RtpCapabilities
|
|
||||||
transports: Map<types.WebRtcTransport['id'], types.WebRtcTransport>
|
|
||||||
producers: Map<types.Producer['id'], types.Producer>
|
|
||||||
consumers: Map<types.Consumer['id'], types.Consumer>
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SomeSocket = Socket<ClientToServerEvents, ServerToClientEvents, InterServerEvent, SocketData> | RemoteSocket<ServerToClientEvents, SocketData>
|
|
||||||
|
|
||||||
export type Namespace = SocketNamespace<ClientToServerEvents, ServerToClientEvents, InterServerEvent, SocketData>
|
|
||||||
16
server/utils/fetch-sockets.ts
Normal file
16
server/utils/fetch-sockets.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import type { SocketServer } from '../types/socket.ts'
|
||||||
|
|
||||||
|
export async function fetchSockets(io: SocketServer, excludeId?: string, channelId?: string) {
|
||||||
|
let sockets: Awaited<ReturnType<typeof io.fetchSockets>>
|
||||||
|
|
||||||
|
if (channelId) {
|
||||||
|
sockets = await io.in(channelId).fetchSockets()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
sockets = await io.fetchSockets()
|
||||||
|
}
|
||||||
|
|
||||||
|
return sockets.filter((socket) => {
|
||||||
|
return !(excludeId && socket.id === excludeId)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
import type { ChadClient, SomeSocket } from '../types/webrtc.ts'
|
import type { ChadClient, SomeSocket } from '../types/socket.ts'
|
||||||
|
|
||||||
export function socketToClient(socket: SomeSocket): ChadClient {
|
export function socketToClient(socket: SomeSocket): ChadClient {
|
||||||
|
// Socket.IO rooms: Extract channel room (filter out the socket's own room)
|
||||||
|
const channelId = Array.from(socket.rooms).find(room => room !== socket.id) || 'default'
|
||||||
|
|
||||||
return {
|
return {
|
||||||
socketId: socket.id,
|
socketId: socket.id,
|
||||||
userId: socket.data.userId,
|
userId: socket.data.userId,
|
||||||
@@ -8,5 +11,6 @@ export function socketToClient(socket: SomeSocket): ChadClient {
|
|||||||
displayName: socket.data.displayName,
|
displayName: socket.data.displayName,
|
||||||
inputMuted: socket.data.inputMuted,
|
inputMuted: socket.data.inputMuted,
|
||||||
outputMuted: socket.data.outputMuted,
|
outputMuted: socket.data.outputMuted,
|
||||||
|
currentChannelId: channelId,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
825
server/yarn.lock
825
server/yarn.lock
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user