8 Commits

Author SHA1 Message Date
92f7bae50d cringe sfx 2026-04-12 22:35:47 +06:00
7b2cfb4b56 Merge branch 'master' into channels
# Conflicts:
#	server/plugins/socket.ts
#	server/socket/webrtc.ts
2026-02-12 18:12:21 +06:00
363f1008c6 update 2026-02-11 07:05:20 +06:00
1bd8aa0fea cringe sfx
All checks were successful
Deploy / publish-web (push) Successful in 51s
2026-02-06 23:14:13 +06:00
626f52c616 cringe sfx
All checks were successful
Deploy / publish-web (push) Successful in 50s
2026-02-06 23:06:50 +06:00
29914d73a0 cringe sfx 2026-02-06 23:06:42 +06:00
dd530266f9 cringe sfx
All checks were successful
Deploy / publish-web (push) Successful in 49s
2026-02-06 22:44:11 +06:00
65583b1564 channels 2026-01-21 22:39:08 +06:00
79 changed files with 20749 additions and 428 deletions

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

View File

@@ -13,18 +13,13 @@ declare module 'vue' {
PrimeButton: typeof import('primevue/button')['default']
PrimeButtonGroup: typeof import('primevue/buttongroup')['default']
PrimeCard: typeof import('primevue/card')['default']
PrimeDivider: typeof import('primevue/divider')['default']
PrimeFloatLabel: typeof import('primevue/floatlabel')['default']
PrimeInputText: typeof import('primevue/inputtext')['default']
PrimePassword: typeof import('primevue/password')['default']
PrimeProgressBar: typeof import('primevue/progressbar')['default']
PrimeScrollPanel: typeof import('primevue/scrollpanel')['default']
PrimeSelect: typeof import('primevue/select')['default']
PrimeSelectButton: typeof import('primevue/selectbutton')['default']
PrimeSlider: typeof import('primevue/slider')['default']
PrimeTag: typeof import('primevue/tag')['default']
PrimeToast: typeof import('primevue/toast')['default']
PrimeToggleSwitch: typeof import('primevue/toggleswitch')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}

View File

@@ -16,11 +16,12 @@
</template>
<script setup lang="ts">
import type { ChadClient } from '#shared/types'
import type { ChadClient, Consumer, Producer } from '#shared/types'
const props = defineProps<{
client: ChadClient
stream: MediaStream
consumer?: Consumer
producer?: Producer
}>()
const { me } = useClients()
@@ -30,8 +31,21 @@ const isMe = computed(() => {
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() {
fullscreenVideo.show(props.stream)
if (stream.value)
fullscreenVideo.show(stream.value)
}
</script>

View File

@@ -7,8 +7,7 @@ export const useApp = createGlobalState(() => {
const { clients } = useClients()
const mediasoup = useMediasoup()
const signaling = useSignaling()
const toast = useToast()
const sfx = useSfx()
const { emit } = useEventBus()
const ready = ref(false)
const isTauri = computed(() => '__TAURI_INTERNALS__' in window)
@@ -53,8 +52,7 @@ export const useApp = createGlobalState(() => {
await mediasoup.pauseProducer(mediasoup.micProducer.value)
sfx.play('/sfx/off_micr.ogg').then()
toast.add({ severity: 'info', summary: 'Microphone muted', closable: false, life: 1000 })
emit('audio:muted')
}
async function unmuteInput() {
@@ -67,8 +65,7 @@ export const useApp = createGlobalState(() => {
await mediasoup.resumeProducer(mediasoup.micProducer.value)
sfx.play('/sfx/on_micr.ogg').then()
toast.add({ severity: 'info', summary: 'Microphone activated', closable: false, life: 1000 })
emit('audio:unmuted')
}
async function toggleInput() {
@@ -92,7 +89,7 @@ export const useApp = createGlobalState(() => {
outputMuted: true,
})
toast.add({ severity: 'info', summary: 'Sound muted', closable: false, life: 1000 })
emit('output:muted')
}
async function unmuteOutput() {
@@ -105,7 +102,7 @@ export const useApp = createGlobalState(() => {
outputMuted: false,
})
toast.add({ severity: 'info', summary: 'Sound resumed', closable: false, life: 1000 })
emit('output:unmuted')
}
async function toggleOutput() {
@@ -118,22 +115,22 @@ export const useApp = createGlobalState(() => {
async function toggleVideo() {
if (!mediasoup.videoProducer.value) {
await mediasoup.enableVideo()
await sfx.play('/sfx/on_trans.ogg', 0.03)
emit('video:enabled')
}
else {
await mediasoup.disableProducer(mediasoup.videoProducer.value)
await sfx.play('/sfx/off_trans.ogg', 0.03)
emit('video:disabled')
}
}
async function toggleShare() {
if (!mediasoup.shareProducer.value) {
await mediasoup.enableShare()
await sfx.play('/sfx/on_trans.ogg', 0.03)
emit('share:enabled')
}
else {
await mediasoup.disableProducer(mediasoup.shareProducer.value)
await sfx.play('/sfx/off_trans.ogg', 0.03)
emit('share:disabled')
}
}

View File

@@ -4,7 +4,7 @@ import { createGlobalState } from '@vueuse/core'
export const useClients = createGlobalState(() => {
const auth = useAuth()
const signaling = useSignaling()
const toast = useToast()
const { emit } = useEventBus()
const clients = shallowRef<ChadClient[]>([])
@@ -16,10 +16,17 @@ export const useClients = createGlobalState(() => {
socket.on('clientChanged', (clientId: ChadClient['socketId'], updatedClient: UpdatedClient) => {
const client = getClient(clientId)
if (!client)
return
updateClient(clientId, updatedClient)
if (client && client.displayName !== updatedClient.displayName)
toast.add({ severity: 'info', summary: `${client.displayName} is now ${updatedClient.displayName}`, closable: false, life: 1000 })
emit('client:updated', {
socketId: clientId,
oldClient: client,
updatedClient,
})
})
socket.on('disconnect', () => {

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

View File

@@ -7,8 +7,6 @@ import { useDevices } from '~/composables/use-devices'
import { usePreferences } from '~/composables/use-preferences'
import { useSignaling } from '~/composables/use-signaling'
type ProducerType = 'microphone' | 'video' | 'share'
interface SpeakingClient {
clientId: ChadClient['socketId']
volume: number
@@ -28,11 +26,10 @@ const ICE_SERVERS: RTCIceServer[] = [
]
export const useMediasoup = createSharedComposable(() => {
const toast = useToast()
const sfx = useSfx()
const { emit } = useEventBus()
const signaling = useSignaling()
const { addClient, removeClient } = useClients()
const { addClient, removeClient, me } = useClients()
const preferences = usePreferences()
const { getShareStream } = useDevices()
@@ -170,18 +167,25 @@ export const useMediasoup = createSharedComposable(() => {
addClient(...joinedClients)
toast.add({ severity: 'success', summary: 'Joined', closable: false, life: 1000 })
if (me.value)
emit('socket:authenticated', { socketId: me.value.socketId })
await enableMic()
})
socket.on('newPeer', (client) => {
sfx.playRandomConnectionSound(client.socketId).then()
addClient(client)
emit('client:added', client)
})
socket.on('peerClosed', (id) => {
const { getClient } = useClients()
const client = getClient(id)
removeClient(id)
if (client)
emit('client:removed', client)
})
socket.on(
@@ -202,9 +206,6 @@ export const useMediasoup = createSharedComposable(() => {
appData: { ...appData, socketId },
})
if (kind === 'video')
sfx.play('/sfx/on_trans.ogg', 0.03).then()
if (producerPaused)
consumer.pause()
@@ -215,16 +216,27 @@ export const useMediasoup = createSharedComposable(() => {
raw: markRaw(consumer),
}
emit('consumer:added', consumers.value[consumer.id]!)
consumer.observer.on('resume', () => {
consumers.value[consumer.id]!.paused = false
emit('consumer:resumed', consumers.value[consumer.id]!)
})
consumer.observer.on('pause', () => {
consumers.value[consumer.id]!.paused = true
emit('consumer:paused', consumers.value[consumer.id]!)
})
consumer.observer.on('close', () => {
const consumerData = consumers.value[consumer.id]
delete consumers.value[consumer.id]
if (consumerData)
emit('consumer:removed', consumerData)
})
consumer.on('trackended', () => {
@@ -305,16 +317,27 @@ export const useMediasoup = createSharedComposable(() => {
raw: markRaw(producer),
}
emit('producer:added', producers.value[producer.id]!)
producer.observer.on('pause', () => {
producers.value[producer.id]!.paused = true
emit('producer:paused', producers.value[producer.id]!)
})
producer.observer.on('resume', () => {
producers.value[producer.id]!.paused = false
emit('producer:resumed', producers.value[producer.id]!)
})
producer.observer.on('close', () => {
const producerData = producers.value[producer.id]
delete producers.value[producer.id]
if (producerData)
emit('producer:removed', producerData)
})
producer.on('trackended', () => {

View File

@@ -10,7 +10,7 @@ export interface SyncedPreferences {
export const usePreferences = createGlobalState(() => {
const { videoInputs, audioInputs, audioOutputs } = useDevices()
const synced = ref(false)
const fetched = ref(false)
const inputDeviceId = useLocalStorage<MediaDeviceInfo['deviceId']>('INPUT_DEVICE_ID', 'default')
const outputDeviceId = useLocalStorage<MediaDeviceInfo['deviceId']>('OUTPUT_DEVICE_ID', 'default')
@@ -58,7 +58,7 @@ export const usePreferences = createGlobalState(() => {
)
return {
synced,
fetched,
inputDeviceId,
outputDeviceId,
videoDeviceId,

View File

@@ -1,5 +1,5 @@
import { createSharedComposable } from '@vueuse/core'
import { Howl, Howler } from 'howler'
import { Howl } from 'howler'
const CONNECTION_SOUNDS = Object.keys(import.meta.glob('@/../public/sfx/connection/*.ogg')).map(path => path.replace('../public', ''))
@@ -13,10 +13,23 @@ function hashStringToNumber(str: string, cap: number): number {
return Math.abs(hash) % cap
}
export const useSfx = createSharedComposable(() => {
async function play(src: string, volume = 0.2): Promise<void> {
Howler.stop()
const oneShots: Howl[] = []
type SfxEvent = 'mic-on' | 'mic-off' | 'stream-on' | 'stream-off' | 'connection'
const EVENT_VOLUME: Record<SfxEvent, number> = {
'mic-on': 0.2,
'mic-off': 0.2,
'stream-on': 0.03,
'stream-off': 0.03,
'connection': 0.1,
}
// TODO: refactor this shit
export const useSfx = createSharedComposable(() => {
const { outputMuted } = useApp()
async function play(src: string, volume = 0.2): Promise<void> {
return new Promise((resolve) => {
const howl = new Howl({
src,
@@ -31,13 +44,50 @@ export const useSfx = createSharedComposable(() => {
})
}
async function playOneShot(src: string, volume = 0.2): Promise<void> {
for (const oneShot of oneShots) {
oneShot.stop()
}
oneShots.length = 0
return new Promise((resolve) => {
const howl = new Howl({
src,
autoplay: true,
loop: false,
volume,
})
oneShots.push(howl)
howl.on('end', () => {
resolve()
})
})
}
async function playEvent(event: SfxEvent) {
switch (event) {
default:
await playOneShot(`/sfx/${event}.ogg`, EVENT_VOLUME[event])
break
}
}
async function playRandomConnectionSound(seed: string) {
await play('/sfx/on_trans.ogg', 0.03)
await play(CONNECTION_SOUNDS[hashStringToNumber(seed, CONNECTION_SOUNDS.length + 1)]!, 0.1)
await playEvent('stream-on')
if (outputMuted.value)
return
await play(CONNECTION_SOUNDS[hashStringToNumber(seed, CONNECTION_SOUNDS.length)]!, 0.1)
}
return {
playOneShot,
play,
playRandomConnectionSound,
playEvent,
}
})

View File

@@ -4,7 +4,7 @@ import { io } from 'socket.io-client'
import { parseURL } from 'ufo'
export const useSignaling = createSharedComposable(() => {
const toast = useToast()
const { emit } = useEventBus()
const { me } = useAuth()
const socket = shallowRef<Socket>()
@@ -41,9 +41,9 @@ export const useSignaling = createSharedComposable(() => {
watch(connected, (connected) => {
if (connected)
toast.add({ severity: 'success', summary: 'Connected', closable: false, life: 1000 })
emit('socket:connected')
else
toast.add({ severity: 'error', summary: 'Disconnected', closable: false, life: 1000 })
emit('socket:disconnected')
}, { immediate: true })
watch(me, (me) => {
@@ -66,7 +66,7 @@ export const useSignaling = createSharedComposable(() => {
const uri = host ? `${protocol}//${host}` : ``
socket.value = io(`${uri}/webrtc`, {
socket.value = io(`${uri}`, {
path: `${pathname}/ws`,
transports: ['websocket'],
withCredentials: true,

View File

@@ -4,7 +4,7 @@
<div v-auto-animate class="w-1/2 m-auto">
<div class="text-center">
<PrimeSelectButton v-model="tab" class="mb-6" :options="options" option-label="label" :allow-empty="false" />
<PrimeSelectButton :model-value="tab" class="mb-6" :options="options" option-label="label" :allow-empty="false" @update:model-value="onTabChanged" />
</div>
<slot />
@@ -15,28 +15,35 @@
</template>
<script setup lang="ts">
import type { RouteLocationRaw } from 'vue-router'
const route = useRoute()
const router = useRouter()
const { version } = useApp()
interface Tab {
label: string
route: RouteLocationRaw
}
const options = computed(() => {
return [
{
label: 'Login',
routeName: 'Login',
route: { name: 'Login' },
},
{
label: 'Register',
routeName: 'Register',
route: { name: 'Register' },
},
]
] as Tab[]
})
const tab = shallowRef(options.value.find(option => route.name === option.routeName))
watch(tab, (tab) => {
if (!tab)
return
navigateTo({ name: tab.routeName })
const tab = computed(() => {
return options.value.find(option => route.name === router.resolve(option.route)?.name)
})
function onTabChanged(tab: Tab) {
navigateTo(tab.route)
}
</script>

View File

@@ -7,9 +7,9 @@ export default defineNuxtRouteMiddleware(async () => {
if (!me.value)
return
const { synced, toggleInputHotkey, toggleOutputHotkey } = usePreferences()
const { fetched, toggleInputHotkey, toggleOutputHotkey } = usePreferences()
if (synced.value)
if (fetched.value)
return
try {
@@ -20,7 +20,7 @@ export default defineNuxtRouteMiddleware(async () => {
toggleInputHotkey.value = preferences.toggleInputHotkey ?? toggleInputHotkey.value
toggleOutputHotkey.value = preferences.toggleOutputHotkey ?? toggleOutputHotkey.value
synced.value = true
fetched.value = true
}
catch {}
})

View File

@@ -1,21 +1,23 @@
<template>
<div class="grid grid-cols-[1fr_1fr] gap-2">
<GalleryCard
v-for="item in gallery"
:key="item.client.socketId"
:client="item.client"
:stream="item.stream"
v-for="producer in producers"
:key="`producer-${producer.id}`"
:client="me!"
:producer="producer"
/>
<GalleryCard
v-for="consumer in consumers"
:key="`consumer-${consumer.consumer.id}`"
:client="consumer.client"
:consumer="consumer.consumer"
/>
</div>
</template>
<script setup lang="ts">
import type { ChadClient } from '#shared/types'
interface GalleryItem {
client: ChadClient
stream: MediaStream
}
import type { ChadClient, Consumer, Producer } from '#shared/types'
definePageMeta({
name: 'Gallery',
@@ -24,39 +26,29 @@ definePageMeta({
const { videoProducer, shareProducer } = useMediasoup()
const { clients, me } = useClients()
const gallery = computed(() => {
return clients.value.reduce<GalleryItem[]>(
(acc, client) => {
const { streaming, videoConsumers, shareConsumers } = useClient(client.socketId)
const producers = computed(() => {
return [videoProducer.value, shareProducer.value].filter(p => !!p && !!p.raw.track) as Producer[]
})
const consumers = computed(() => {
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 [...videoConsumers.value, ...shareConsumers.value]) {
acc.push({
client,
stream: new MediaStream([consumer.raw.track]),
})
for (const consumer of [...clientVideoConsumers.value, ...clientShareConsumers.value]) {
acc.push({ client, consumer })
}
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
}, []),
)
})
const hasItems = computed(() => producers.value.length > 0 || consumers.value.length > 0)
watch(gallery, (gallery) => {
if (gallery.length > 0)
watch(hasItems, (hasItems) => {
if (hasItems)
return
navigateTo({ name: 'Index' })

View File

@@ -81,6 +81,7 @@
size="small"
option-label="label"
option-value="value"
:allow-empty="false"
/>
</div>

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

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

View File

View File

@@ -22,6 +22,7 @@
"howler": "^2.2.4",
"lucide-vue-next": "^0.562.0",
"mediasoup-client": "^3.18.6",
"mitt": "^3.0.1",
"nuxt": "^4.2.2",
"postcss": "^8.5.6",
"primeicons": "^7.0.0",

View File

@@ -1,7 +1,7 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "Chad",
"version": "0.2.30",
"version": "0.2.33",
"identifier": "xyz.koptilnya.chad",
"build": {
"frontendDist": "../.output/public",

View File

@@ -4070,6 +4070,7 @@ __metadata:
howler: "npm:^2.2.4"
lucide-vue-next: "npm:^0.562.0"
mediasoup-client: "npm:^3.18.6"
mitt: "npm:^3.0.1"
nuxt: "npm:^4.2.2"
postcss: "npm:^8.5.6"
primeicons: "npm:^7.0.0"

73
server/CLAUDE.md Normal file
View 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
View File

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

View File

@@ -2,7 +2,7 @@
"name": "server",
"scripts": {
"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",
"packageManager": "yarn@4.10.3",
@@ -11,7 +11,8 @@
"@fastify/cookie": "^11.0.2",
"@fastify/cors": "^11.1.0",
"@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",
"consola": "^3.4.2",
"dotenv": "^17.2.3",
@@ -19,7 +20,7 @@
"fastify-plugin": "^5.1.0",
"lucia": "^3.2.2",
"mediasoup": "^3.19.3",
"prisma": "^6.17.0",
"prisma": "7",
"socket.io": "^4.8.1",
"ws": "^8.18.3",
"zod": "^4.1.12"

View File

@@ -1,8 +1,12 @@
import type { FastifyInstance } from 'fastify'
import type { ServerOptions } from 'socket.io'
import type { ChadClient } from '../types/socket.ts'
import { consola } from 'consola'
import fp from 'fastify-plugin'
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' {
interface FastifyInstance {
@@ -22,8 +26,80 @@ export default fp<Partial<ServerOptions>>(
await fastify.io.close()
})
fastify.ready(async () => {
await registerWebrtcSocket(fastify.io, fastify.mediasoupRouter)
fastify.ready(() => {
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'] },

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

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

View File

@@ -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({
adapter: new PrismaBetterSqlite3({
url: env('DATABASE_URL'),
}, {
timestampFormat: 'unixepoch-ms',
}),
log: ['query', 'error', 'warn'],
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

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

View File

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

File diff suppressed because one or more lines are too long

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

View 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?module')

View 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')

View File

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

View File

@@ -1,11 +1,11 @@
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
// url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
// output = "./generated/client"
provider = "prisma-client"
output = "./generated/client"
}
model User {
@@ -15,16 +15,15 @@ model User {
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])
@@ -34,7 +33,14 @@ model UserPreferences {
userId String @id
toggleInputHotkey String? @default("")
toggleOutputHotkey String? @default("")
volumes Json? @default("{}")
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
View 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
View 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 })
}
}
})
}

View File

@@ -1,5 +1,5 @@
import type { FastifyInstance } from 'fastify'
import type { Namespace } from '../types/webrtc.ts'
import type { Namespace } from '../types/socket.ts'
import { z } from 'zod'
import prisma from '../prisma/client.ts'
import { socketToClient } from '../utils/socket-to-client.ts'
@@ -77,7 +77,7 @@ export default function (fastify: FastifyInstance) {
if (found) {
found.data.displayName = input.displayName
namespace.emit('clientChanged', found.id, socketToClient(found))
namespace.emit('webrtc:client-changed', found.id, socketToClient(found))
}
return updatedUser

150
server/socket/channel.ts Normal file
View 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,
}
}

View File

@@ -1,52 +1,21 @@
import type { types } from 'mediasoup'
import type { ActiveSpeakerObserver, AudioLevelObserver } from 'mediasoup/types'
import type { Server as SocketServer } from 'socket.io'
import type {
ChadClient,
Namespace,
ChadSocket,
SomeSocket,
} from '../types/webrtc.ts'
} from '../types/socket.ts'
import { consola } from 'consola'
import prisma from '../prisma/client.ts'
import { socketToClient } from '../utils/socket-to-client.ts'
export default async function (io: SocketServer, router: types.Router) {
const namespace: Namespace = io.of('/webrtc')
const audioLevelObserver = await router.createAudioLevelObserver({
maxEntries: 10,
threshold: -80,
interval: 800,
})
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
export default async function (
io: SocketServer,
socket: ChadSocket,
router: types.Router,
audioLevelObserver: AudioLevelObserver,
activeSpeakerObserver: ActiveSpeakerObserver,
) {
io.on('connection', async (socket) => {
socket.data.inputMuted = 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.consumers = new Map()
const { id, username, displayName } = await prisma.user.findUnique({
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) => {
socket.on('webrtc:get-rtp-capabilities', (cb) => {
cb(router.rtpCapabilities)
})
socket.on('createTransport', async ({ producing, consuming }, cb) => {
socket.on('webrtc:create-transport', async ({ producing, consuming }, cb) => {
try {
const transport = await router.createWebRtcTransport({
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)
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) {
consola.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
}
// 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)
if (!transport) {
@@ -203,7 +137,8 @@ export default async function (io: SocketServer, router: types.Router) {
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) {
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) {
consola.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 })
})
socket.on('pauseProducer', async ({ producerId }, cb) => {
socket.on('webrtc:pause-producer', async ({ producerId }, cb) => {
if (!socket.data.joined) {
consola.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 })
})
socket.on('resumeProducer', async ({ producerId }, cb) => {
socket.on('webrtc:resume-producer', async ({ producerId }, cb) => {
if (!socket.data.joined) {
consola.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 })
})
socket.on('pauseConsumer', async ({ consumerId }, cb) => {
socket.on('webrtc:pause-consumer', async ({ consumerId }, cb) => {
if (!socket.data.joined) {
consola.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 })
})
socket.on('resumeConsumer', async ({ consumerId }, cb) => {
socket.on('webrtc:resume-consumer', async ({ consumerId }, cb) => {
if (!socket.data.joined) {
consola.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 })
})
socket.on('updateClient', async (updatedClient, cb) => {
socket.on('webrtc:update-client', async (updatedClient, cb) => {
if (typeof updatedClient.inputMuted === 'boolean') {
socket.data.inputMuted = updatedClient.inputMuted
}
@@ -350,14 +285,21 @@ export default async function (io: SocketServer, router: types.Router) {
cb(socketToClient(socket))
namespace.emit('clientChanged', socket.id, socketToClient(socket))
io.emit('webrtc:client-changed', socket.id, socketToClient(socket))
})
socket.on('disconnect', () => {
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) {
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()) {
@@ -366,10 +308,21 @@ export default async function (io: SocketServer, router: types.Router) {
})
})
async function getJoinedSockets(excludeId?: string) {
const sockets = await namespace.fetchSockets()
async function getJoinedSockets(excludeId?: string, channelId?: string) {
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(
@@ -427,24 +380,24 @@ export default async function (io: SocketServer, router: types.Router) {
consumer.on('producerclose', () => {
consumerSocket.data.consumers.delete(consumer.id)
consumerSocket.emit('consumerClosed', { consumerId: consumer.id })
consumerSocket.emit('webrtc:consumer-closed', { consumerId: consumer.id })
})
consumer.on('producerpause', () => {
consumerSocket.emit('consumerPaused', { consumerId: consumer.id })
consumerSocket.emit('webrtc:consumer-paused', { consumerId: consumer.id })
})
consumer.on('producerresume', () => {
consumerSocket.emit('consumerResumed', { consumerId: consumer.id })
consumerSocket.emit('webrtc:consumer-resumed', { consumerId: consumer.id })
})
consumer.on('score', (score: types.ConsumerScore) => {
consumerSocket.emit('consumerScore', { consumerId: consumer.id, score })
consumerSocket.emit('webrtc:consumer-score', { consumerId: consumer.id, score })
})
try {
await consumerSocket.emitWithAck(
'newConsumer',
'webrtc:new-consumer',
{
socketId: producerSocket.id,
producerId: producer.id,
@@ -460,7 +413,7 @@ export default async function (io: SocketServer, router: types.Router) {
await consumer.resume()
consumerSocket.emit(
'consumerScore',
'webrtc:consumer-score',
{
consumerId: consumer.id,
score: consumer.score,

View File

@@ -1,12 +1,12 @@
{
"compilerOptions": {
"target": "es2016",
"module": "ESNext",
"moduleResolution": "nodenext",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"allowImportingTsExtensions": true
"allowImportingTsExtensions": true,
"module": "ESNext",
"moduleResolution": "node",
"target": "ES2023",
"strict": true,
"esModuleInterop": true
}
}

173
server/types/socket.ts Normal file
View 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
View File

@@ -0,0 +1,4 @@
export type LastArgument<T>
= T extends (...args: [...any[], infer Last]) => any
? Last
: never

View File

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

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

View File

@@ -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 {
// 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 {
socketId: socket.id,
userId: socket.data.userId,
@@ -8,5 +11,6 @@ export function socketToClient(socket: SomeSocket): ChadClient {
displayName: socket.data.displayName,
inputMuted: socket.data.inputMuted,
outputMuted: socket.data.outputMuted,
currentChannelId: channelId,
}
}

File diff suppressed because it is too large Load Diff