4 Commits

Author SHA1 Message Date
c966aa9c4b продолжаю чат 2026-04-16 14:02:59 +06:00
0915d3c64d начало чата
All checks were successful
Deploy / deploy (push) Successful in 3m47s
2026-04-16 02:21:54 +06:00
9f39ee6430 убрал из панели задач, скрыл от шаринга
All checks were successful
Deploy / publish-web (push) Successful in 1m28s
2026-04-15 15:43:43 +06:00
3658975b93 cringe sfx
All checks were successful
Deploy / publish-web (push) Successful in 1m33s
2026-04-12 22:38:48 +06:00
80 changed files with 2099 additions and 21400 deletions

Binary file not shown.

View File

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

View File

@@ -1,5 +1,5 @@
import { getVersion } from '@tauri-apps/api/app' import { getVersion } from '@tauri-apps/api/app'
import { openUrl as tauriOpenUrl } from '@tauri-apps/plugin-opener'
import { computedAsync, createGlobalState } from '@vueuse/core' import { computedAsync, createGlobalState } from '@vueuse/core'
import { useClients } from '~/composables/use-clients' import { useClients } from '~/composables/use-clients'
@@ -134,6 +134,15 @@ export const useApp = createGlobalState(() => {
} }
} }
async function openUrl(href: string) {
if (isTauri.value) {
await tauriOpenUrl(href)
}
else {
window.open(href, '_blank', 'noopener noreferrer')
}
}
return { return {
ready, ready,
clients, clients,
@@ -153,5 +162,6 @@ export const useApp = createGlobalState(() => {
videoEnabled, videoEnabled,
sharingEnabled, sharingEnabled,
somebodyStreamingVideo, somebodyStreamingVideo,
openUrl,
} }
}) })

View File

@@ -0,0 +1,60 @@
import { createGlobalState } from '@vueuse/core'
export interface ChatClientMessage {
text: string
replyTo?: {
messageId: string
}
}
export interface ChatMessage {
id: string
sender: string
text: string
createdAt: string
replyTo?: {
messageId: string
sender: string
text: string
}
}
export const useChat = createGlobalState(() => {
const signaling = useSignaling()
const { emit } = useEventBus()
const messages = shallowRef<ChatMessage[]>([])
watch(signaling.socket, (socket) => {
if (!socket)
return
socket.on('chat:new-message', (message: ChatMessage) => {
messages.value.push(message)
triggerRef(messages)
emit('chat:new-message')
})
socket.on('disconnect', () => {
messages.value = []
})
}, { immediate: true })
function sendMessage(message: ChatClientMessage) {
if (!signaling.connected.value)
return
message.text = message.text.trim()
if (!message.text.length)
return
signaling.socket.value!.emit('chat:message', message)
}
return {
messages,
sendMessage,
}
})

View File

@@ -29,6 +29,8 @@ export interface AppEvents extends Record<EventType, unknown> {
'video:disabled': void 'video:disabled': void
'share:enabled': void 'share:enabled': void
'share:disabled': void 'share:disabled': void
'chat:new-message': void
} }
const emitter = mitt<AppEvents>() const emitter = mitt<AppEvents>()

View File

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

View File

@@ -15,7 +15,7 @@ function hashStringToNumber(str: string, cap: number): number {
const oneShots: Howl[] = [] const oneShots: Howl[] = []
type SfxEvent = 'mic-on' | 'mic-off' | 'stream-on' | 'stream-off' | 'connection' type SfxEvent = 'mic-on' | 'mic-off' | 'stream-on' | 'stream-off' | 'connection' | 'message'
const EVENT_VOLUME: Record<SfxEvent, number> = { const EVENT_VOLUME: Record<SfxEvent, number> = {
'mic-on': 0.2, 'mic-on': 0.2,

View File

@@ -66,7 +66,7 @@ export const useSignaling = createSharedComposable(() => {
const uri = host ? `${protocol}//${host}` : `` const uri = host ? `${protocol}//${host}` : ``
socket.value = io(`${uri}`, { socket.value = io(uri, {
path: `${pathname}/ws`, path: `${pathname}/ws`,
transports: ['websocket'], transports: ['websocket'],
withCredentials: true, withCredentials: true,

View File

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

View File

@@ -56,11 +56,9 @@
</div> </div>
</PrimeScrollPanel> </PrimeScrollPanel>
<PrimeScrollPanel class="bg-surface-900 rounded-xl overflow-hidden" style="min-height: 0"> <div class="bg-surface-900 rounded-xl overflow-hidden p-3 flex flex-col min-h-full">
<div class="p-3"> <slot />
<slot /> </div>
</div>
</PrimeScrollPanel>
</div> </div>
<FullscreenGallery /> <FullscreenGallery />

View File

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

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="grid grid-cols-[1fr_1fr] gap-2"> <PrimeScrollPanel class="grid grid-cols-[1fr_1fr] gap-2 min-h-0">
<GalleryCard <GalleryCard
v-for="producer in producers" v-for="producer in producers"
:key="`producer-${producer.id}`" :key="`producer-${producer.id}`"
@@ -13,7 +13,7 @@
:client="consumer.client" :client="consumer.client"
:consumer="consumer.consumer" :consumer="consumer.consumer"
/> />
</div> </PrimeScrollPanel>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">

View File

@@ -1,17 +1,161 @@
<template> <template>
<div> <p v-if="!messages.length" class="text-muted-color text-center m-auto">
<div class="flex items-center justify-center"> Chat is empty
<PrimeCard> </p>
<template #content>
The chat is under development. <PrimeScrollPanel v-else ref="scroll" class="flex-1 min-h-0 overflow-x-hidden">
</template> <div class="flex flex-col gap-3">
</PrimeCard> <div
v-for="message in messages"
:key="message.id"
class="w-fit max-w-[60%]"
:class="{
'ml-auto': message.sender === me?.username,
}"
>
<p
v-if="message.sender !== me?.username"
class="text-sm text-muted-color mb-1"
>
{{ message.sender }}
</p>
<div
class="px-3 py-2 rounded-lg"
:class="{
'bg-surface-800': message.sender !== me?.username,
'bg-surface-700': message.sender === me?.username,
}"
>
<p class="[&>a]:break-all" @click="handleMessageClick" v-html="parseMessageText(message.text)" />
<p class="mt-1 text-right text-sm text-muted-color" :title="formatDate(message.createdAt, 'dd.MM.yyyy, HH:mm')">
{{ formatDate(message.createdAt) }}
</p>
</div>
</div>
</div> </div>
</PrimeScrollPanel>
<div class="mt-3 shrink-0">
<PrimeInputGroup>
<!-- <PrimeInputGroupAddon> -->
<!-- <PrimeButton severity="secondary" class="shrink-0" disabled> -->
<!-- <template #icon> -->
<!-- <Paperclip /> -->
<!-- </template> -->
<!-- </PrimeButton> -->
<!-- </PrimeInputGroupAddon> -->
<PrimeInputText
v-model="text"
placeholder="Write a message..."
fluid
autocomplete="off"
@keydown.enter.exact="sendMessage"
@vue:mounted="onInputMounted"
/>
<PrimeButton class="shrink-0" label="Send" severity="contrast" @click="sendMessage" />
</PrimeInputGroup>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useEventBus } from '#imports'
import { format } from 'date-fns'
import linkifyStr from 'linkify-string'
import { useChat } from '~/composables/use-chat'
definePageMeta({ definePageMeta({
name: 'Index', name: 'Index',
}) })
const { openUrl } = useApp()
const { me } = useClients()
const chat = useChat()
const eventBus = useEventBus()
const { messages } = chat
const scrollRef = useTemplateRef('scroll')
const contentRef = computed(() => scrollRef.value?.$refs.content)
const text = ref('')
eventBus.on('chat:new-message', onNewMessage)
onScopeDispose(() => {
eventBus.off('chat:new-message', onNewMessage)
})
function parseMessageText(text: string) {
return linkifyStr(
text,
{
className: 'underline',
rel: 'noopener noreferrer',
target: '_blank',
},
).replaceAll('\n', '<br>')
}
function formatDate(date: string, formatStr = 'HH:mm') {
return format(date, formatStr)
}
function sendMessage() {
if (!text.value)
return
chat.sendMessage({
text: text.value,
})
text.value = ''
}
function onInputMounted(ref: VNode) {
ref.el?.focus()
}
const ARRIVED_STATE_THRESHOLD_PIXELS = 1
async function onNewMessage() {
if (!contentRef.value)
return
const arrivedBottom = contentRef.value.scrollTop + contentRef.value.clientHeight
>= contentRef.value.scrollHeight - ARRIVED_STATE_THRESHOLD_PIXELS
const scrollable = contentRef.value.scrollHeight > contentRef.value.clientHeight
await nextTick()
if (scrollable && !arrivedBottom)
return
contentRef.value.scrollTo({
behavior: 'smooth',
top: contentRef.value.scrollHeight,
})
}
function handleMessageClick({ target }: MouseEvent) {
if (!target)
return
if (target instanceof HTMLElement) {
if (target.tagName === 'A') {
target.addEventListener('click', onAnchorClick)
}
}
}
function onAnchorClick(event: MouseEvent) {
event.preventDefault()
const target = event.target as HTMLAnchorElement
console.log('yo')
openUrl(target.href)
}
</script> </script>

View File

@@ -1,5 +1,5 @@
<template> <template>
<div> <PrimeScrollPanel class="min-h-0">
<PrimeDivider align="left"> <PrimeDivider align="left">
Audio Audio
</PrimeDivider> </PrimeDivider>
@@ -81,7 +81,6 @@
size="small" size="small"
option-label="label" option-label="label"
option-value="value" option-value="value"
:allow-empty="false"
/> />
</div> </div>
@@ -133,7 +132,7 @@
@click="checkForUpdates" @click="checkForUpdates"
/> />
</template> </template>
</div> </PrimeScrollPanel>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">

View File

@@ -4,12 +4,12 @@ export default defineNuxtPlugin(() => {
// Connection sounds // Connection sounds
on('socket:authenticated', ({ socketId }) => { on('socket:authenticated', ({ socketId }) => {
sfx.playRandomConnectionSound(socketId) sfx.playEvent('stream-on')
}) })
// Client events // Client events
on('client:added', (client) => { on('client:added', (client) => {
sfx.playRandomConnectionSound(client.socketId) sfx.playEvent('stream-on')
}) })
on('client:removed', () => { on('client:removed', () => {
@@ -54,4 +54,8 @@ export default defineNuxtPlugin(() => {
sfx.playEvent('stream-off') sfx.playEvent('stream-off')
} }
}) })
on('chat:new-message', () => {
sfx.playEvent('message')
})
}) })

View File

@@ -14,12 +14,16 @@
"@nuxt/fonts": "^0.11.4", "@nuxt/fonts": "^0.11.4",
"@primeuix/themes": "^1.2.5", "@primeuix/themes": "^1.2.5",
"@tailwindcss/vite": "^4.1.14", "@tailwindcss/vite": "^4.1.14",
"@tauri-apps/plugin-global-shortcut": "~2", "@tauri-apps/plugin-global-shortcut": "^2.3.1",
"@tauri-apps/plugin-process": "~2", "@tauri-apps/plugin-opener": "~2",
"@tauri-apps/plugin-updater": "~2", "@tauri-apps/plugin-process": "^2.3.1",
"@tauri-apps/plugin-updater": "^2.10.1",
"@vueuse/core": "^13.9.0", "@vueuse/core": "^13.9.0",
"date-fns": "^4.1.0",
"hotkeys-js": "^4.0.0", "hotkeys-js": "^4.0.0",
"howler": "^2.2.4", "howler": "^2.2.4",
"linkify-string": "^4.3.2",
"linkifyjs": "^4.3.2",
"lucide-vue-next": "^0.562.0", "lucide-vue-next": "^0.562.0",
"mediasoup-client": "^3.18.6", "mediasoup-client": "^3.18.6",
"mitt": "^3.0.1", "mitt": "^3.0.1",

File diff suppressed because it is too large Load Diff

View File

@@ -24,6 +24,8 @@ log = "0.4"
tauri = { version = "2.8.5", features = [] } tauri = { version = "2.8.5", features = [] }
tauri-plugin-log = "2" tauri-plugin-log = "2"
tauri-plugin-process = "2" tauri-plugin-process = "2"
windows = { version = "0.52", features = ["Win32_UI_Shell"] }
tauri-plugin-opener = "2"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-global-shortcut = "2" tauri-plugin-global-shortcut = "2"

View File

@@ -13,6 +13,8 @@
"global-shortcut:allow-is-registered", "global-shortcut:allow-is-registered",
"global-shortcut:allow-register", "global-shortcut:allow-register",
"global-shortcut:allow-unregister", "global-shortcut:allow-unregister",
"global-shortcut:allow-unregister-all" "global-shortcut:allow-unregister-all",
"opener:allow-default-urls",
"opener:allow-open-url"
] ]
} }

View File

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

View File

@@ -1,6 +1,21 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!! // Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
#[cfg(target_os = "windows")]
fn set_app_user_model_id() {
use windows::core::HSTRING;
use windows::Win32::UI::Shell::SetCurrentProcessExplicitAppUserModelID;
unsafe {
SetCurrentProcessExplicitAppUserModelID(&HSTRING::from("xyz.koptilnya.chad"))
.ok()
.expect("Failed to set AppUserModelID");
}
}
fn main() { fn main() {
#[cfg(target_os = "windows")]
set_app_user_model_id();
app_lib::run(); app_lib::run();
} }

View File

@@ -1,7 +1,7 @@
{ {
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "Chad", "productName": "Chad",
"version": "0.2.33", "version": "0.3.0-rc.2",
"identifier": "xyz.koptilnya.chad", "identifier": "xyz.koptilnya.chad",
"build": { "build": {
"frontendDist": "../.output/public", "frontendDist": "../.output/public",
@@ -23,7 +23,7 @@
"fullscreen": false, "fullscreen": false,
"center": true, "center": true,
"theme": "Dark", "theme": "Dark",
"additionalBrowserArgs": "--disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection --autoplay-policy=no-user-gesture-required", "additionalBrowserArgs": "--disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection --autoplay-policy=no-user-gesture-required --lang=en",
"incognito": false "incognito": false
} }
], ],

View File

@@ -2824,10 +2824,10 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@tauri-apps/api@npm:^2.6.0": "@tauri-apps/api@npm:^2.10.1":
version: 2.8.0 version: 2.10.1
resolution: "@tauri-apps/api@npm:2.8.0" resolution: "@tauri-apps/api@npm:2.10.1"
checksum: 10c0/fb111e4d7572372997b440ebe6879543fa8c4765151878e3fddfbfe809b18da29eed142ce83061d14a9ca6d896b3266dc8a4927c642d71cdc0b4277dc7e3aabf checksum: 10c0/f3c0b2ba67a0b887440a7faa1e0589e847760ee30ec29b964f22573a46b817cb3af2199d6f5f7dfdda54d65b465ebaaa280454c610a5c53d808a0911fa15e45d
languageName: node languageName: node
linkType: hard linkType: hard
@@ -2959,7 +2959,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@tauri-apps/plugin-global-shortcut@npm:~2": "@tauri-apps/plugin-global-shortcut@npm:^2.3.1":
version: 2.3.1 version: 2.3.1
resolution: "@tauri-apps/plugin-global-shortcut@npm:2.3.1" resolution: "@tauri-apps/plugin-global-shortcut@npm:2.3.1"
dependencies: dependencies:
@@ -2968,21 +2968,30 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@tauri-apps/plugin-process@npm:~2": "@tauri-apps/plugin-opener@npm:~2":
version: 2.3.0 version: 2.5.3
resolution: "@tauri-apps/plugin-process@npm:2.3.0" resolution: "@tauri-apps/plugin-opener@npm:2.5.3"
dependencies: dependencies:
"@tauri-apps/api": "npm:^2.6.0" "@tauri-apps/api": "npm:^2.8.0"
checksum: 10c0/ef50344a7436d92278c2ef4526f72daaf3171c4d65743bbc1f7a00fa581644a8583bb8680f637a34af5c7e6a0e8722c22189290e903584fef70ed83b64b6e9c0 checksum: 10c0/9ef2fae01e03f3bb16d8e55bfd921cf7c1d284e6459bd5b45777806304eb70ab0b50cbf03be76fc05e64ef70a37493e0cd90b0acc16eaee4a4fc2cfff7e43b71
languageName: node languageName: node
linkType: hard linkType: hard
"@tauri-apps/plugin-updater@npm:~2": "@tauri-apps/plugin-process@npm:^2.3.1":
version: 2.9.0 version: 2.3.1
resolution: "@tauri-apps/plugin-updater@npm:2.9.0" resolution: "@tauri-apps/plugin-process@npm:2.3.1"
dependencies: dependencies:
"@tauri-apps/api": "npm:^2.6.0" "@tauri-apps/api": "npm:^2.8.0"
checksum: 10c0/72ce83d1c241308a13b9929f0900e4d33453875877009166e3998e3e75a1003ac48c3641086b4d3230f0f18c64f475ad6c3556d1603fc641ca50dc9c18d61866 checksum: 10c0/2e5086898f1c9f25f6426a752404c788727237142bbb7c8f418b97c76c5360874d06203150d136e51114df9e720022e4fa3681fd1d4cb6f777dc83c3553f8670
languageName: node
linkType: hard
"@tauri-apps/plugin-updater@npm:^2.10.1":
version: 2.10.1
resolution: "@tauri-apps/plugin-updater@npm:2.10.1"
dependencies:
"@tauri-apps/api": "npm:^2.10.1"
checksum: 10c0/5d3813851ccbbf90253ad4647dbd97501c2bb75db864175693322fa6eb062b6e1bae03890810ad0bbe7f6da4e020af35e8c787e6999f8c7c645121164587dc29
languageName: node languageName: node
linkType: hard linkType: hard
@@ -4059,15 +4068,19 @@ __metadata:
"@primevue/nuxt-module": "npm:^4.4.0" "@primevue/nuxt-module": "npm:^4.4.0"
"@tailwindcss/vite": "npm:^4.1.14" "@tailwindcss/vite": "npm:^4.1.14"
"@tauri-apps/cli": "npm:^2.8.4" "@tauri-apps/cli": "npm:^2.8.4"
"@tauri-apps/plugin-global-shortcut": "npm:~2" "@tauri-apps/plugin-global-shortcut": "npm:^2.3.1"
"@tauri-apps/plugin-process": "npm:~2" "@tauri-apps/plugin-opener": "npm:~2"
"@tauri-apps/plugin-updater": "npm:~2" "@tauri-apps/plugin-process": "npm:^2.3.1"
"@tauri-apps/plugin-updater": "npm:^2.10.1"
"@types/howler": "npm:^2" "@types/howler": "npm:^2"
"@vueuse/core": "npm:^13.9.0" "@vueuse/core": "npm:^13.9.0"
date-fns: "npm:^4.1.0"
eslint: "npm:^9.36.0" eslint: "npm:^9.36.0"
eslint-plugin-format: "npm:^1.0.2" eslint-plugin-format: "npm:^1.0.2"
hotkeys-js: "npm:^4.0.0" hotkeys-js: "npm:^4.0.0"
howler: "npm:^2.2.4" howler: "npm:^2.2.4"
linkify-string: "npm:^4.3.2"
linkifyjs: "npm:^4.3.2"
lucide-vue-next: "npm:^0.562.0" lucide-vue-next: "npm:^0.562.0"
mediasoup-client: "npm:^3.18.6" mediasoup-client: "npm:^3.18.6"
mitt: "npm:^3.0.1" mitt: "npm:^3.0.1"
@@ -4566,6 +4579,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"date-fns@npm:^4.1.0":
version: 4.1.0
resolution: "date-fns@npm:4.1.0"
checksum: 10c0/b79ff32830e6b7faa009590af6ae0fb8c3fd9ffad46d930548fbb5acf473773b4712ae887e156ba91a7b3dc30591ce0f517d69fd83bd9c38650fdc03b4e0bac8
languageName: node
linkType: hard
"db0@npm:^0.3.4": "db0@npm:^0.3.4":
version: 0.3.4 version: 0.3.4
resolution: "db0@npm:0.3.4" resolution: "db0@npm:0.3.4"
@@ -6953,6 +6973,22 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"linkify-string@npm:^4.3.2":
version: 4.3.2
resolution: "linkify-string@npm:4.3.2"
peerDependencies:
linkifyjs: ^4.0.0
checksum: 10c0/674e908b46aa6da3ee7e5c0749464d8de55f4d44933d7e9dea4d2f9bb5af0137d45142494288cd120335e21d8298c76ec4524ab6e67edede4613adb5f17f7dc6
languageName: node
linkType: hard
"linkifyjs@npm:^4.3.2":
version: 4.3.2
resolution: "linkifyjs@npm:4.3.2"
checksum: 10c0/1a85e6b368304a4417567fe5e38651681e3e82465590836942d1b4f3c834cc35532898eb1e2479f6337d9144b297d418eb708b6be8ed0b3dc3954a3588e07971
languageName: node
linkType: hard
"listhen@npm:^1.9.0": "listhen@npm:^1.9.0":
version: 1.9.0 version: 1.9.0
resolution: "listhen@npm:1.9.0" resolution: "listhen@npm:1.9.0"

View File

@@ -1,73 +0,0 @@
# 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`.

View File

@@ -1,14 +0,0 @@
// 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", "name": "server",
"scripts": { "scripts": {
"start": "ts-node --transpile-only server.ts", "start": "ts-node --transpile-only server.ts",
"db:deploy": "npx prisma migrate deploy && npx prisma db seed && npx prisma generate" "db:deploy": "npx prisma migrate deploy && npx prisma generate"
}, },
"type": "module", "type": "module",
"packageManager": "yarn@4.10.3", "packageManager": "yarn@4.10.3",
@@ -10,9 +10,9 @@
"@fastify/autoload": "^6.3.1", "@fastify/autoload": "^6.3.1",
"@fastify/cookie": "^11.0.2", "@fastify/cookie": "^11.0.2",
"@fastify/cors": "^11.1.0", "@fastify/cors": "^11.1.0",
"@fastify/multipart": "^10.0.0",
"@lucia-auth/adapter-prisma": "^4.0.1", "@lucia-auth/adapter-prisma": "^4.0.1",
"@prisma/adapter-better-sqlite3": "^7.2.0", "@prisma/client": "^6.17.0",
"@prisma/client": "7",
"bcrypt": "^6.0.0", "bcrypt": "^6.0.0",
"consola": "^3.4.2", "consola": "^3.4.2",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
@@ -20,8 +20,9 @@
"fastify-plugin": "^5.1.0", "fastify-plugin": "^5.1.0",
"lucia": "^3.2.2", "lucia": "^3.2.2",
"mediasoup": "^3.19.3", "mediasoup": "^3.19.3",
"prisma": "7", "prisma": "^6.17.0",
"socket.io": "^4.8.1", "socket.io": "^4.8.1",
"uuid": "^13.0.0",
"ws": "^8.18.3", "ws": "^8.18.3",
"zod": "^4.1.12" "zod": "^4.1.12"
}, },

View File

@@ -1,12 +1,9 @@
import type { FastifyInstance } from 'fastify' import type { FastifyInstance } from 'fastify'
import type { ServerOptions } from 'socket.io' import type { ServerOptions } from 'socket.io'
import type { ChadClient } from '../types/socket.ts'
import { consola } from 'consola'
import fp from 'fastify-plugin' import fp from 'fastify-plugin'
import { Server } from 'socket.io' import { Server } from 'socket.io'
import prisma from '../prisma/client.ts' import registerChatSocket from '../socket/chat.ts'
import registerChannelHandlers from '../socket/channel.ts' import registerWebrtcSocket from '../socket/webrtc.ts'
import registerWebrtcHandlers from '../socket/webrtc.ts'
declare module 'fastify' { declare module 'fastify' {
interface FastifyInstance { interface FastifyInstance {
@@ -26,80 +23,9 @@ export default fp<Partial<ServerOptions>>(
await fastify.io.close() await fastify.io.close()
}) })
fastify.ready(() => { fastify.ready(async () => {
const audioLevelObserver = await fastify.mediasoupRouter.createAudioLevelObserver({ await registerWebrtcSocket(fastify.io, fastify.mediasoupRouter)
maxEntries: 10, await registerChatSocket(fastify.io)
threshold: -80,
interval: 800,
})
const activeSpeakerObserver = await fastify.mediasoupRouter.createActiveSpeakerObserver()
audioLevelObserver.on('volumes', async (volumes: types.AudioLevelObserverVolume[]) => {
fastify.io.emit('webrtc:speaking-peers', volumes.map(({ producer, volume }) => {
const { socketId } = producer.appData as { socketId: ChadClient['socketId'] }
return {
clientId: socketId,
volume,
}
}))
})
audioLevelObserver.on('silence', () => {
fastify.io.emit('webrtc:speaking-peers', [])
fastify.io.emit('webrtc:active-speaker', undefined)
})
activeSpeakerObserver.on('dominantspeaker', ({ producer }) => {
const { socketId } = producer.appData as { socketId: ChadClient['socketId'] }
fastify.io.emit('webrtc:active-speaker', socketId)
})
fastify.io.on('connection', async (socket) => {
consola.info('New connection', socket.id)
const user = await prisma.user.findUnique({
where: {
id: socket.handshake.auth.userId,
},
select: {
id: true,
username: true,
displayName: true,
},
})
if (!user) {
socket.disconnect()
return
}
const { id, username, displayName } = user
socket.data.userId = id
socket.data.username = username
socket.data.displayName = displayName
consola.info('User authorized', ...Object.values(user))
const channel = await registerChannelHandlers(fastify.io, socket)
const webrtc = await registerWebrtcHandlers(
fastify.io,
socket,
fastify.mediasoupRouter,
audioLevelObserver,
activeSpeakerObserver,
)
socket.emit('webrtc:authenticated', {
...channel,
...webrtc,
rtpCapabilities: fastify.mediasoupRouter.rtpCapabilities,
})
})
}) })
}, },
{ name: 'socket-io', dependencies: ['mediasoup-worker', 'mediasoup-router'] }, { name: 'socket-io', dependencies: ['mediasoup-worker', 'mediasoup-router'] },

View File

@@ -1,13 +0,0 @@
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,14 +1,6 @@
import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3' import { PrismaClient } from '@prisma/client'
import { env } from 'prisma/config'
import { PrismaClient } from './generated/client/index.js'
import 'dotenv/config'
const client = new PrismaClient({ const client = new PrismaClient({
adapter: new PrismaBetterSqlite3({
url: env('DATABASE_URL'),
}, {
timestampFormat: 'unixepoch-ms',
}),
log: ['query', 'error', 'warn'], log: ['query', 'error', 'warn'],
}) })

View File

@@ -1,39 +0,0 @@
/* !!! 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

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

View File

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

View File

@@ -1,61 +0,0 @@
/* !!! 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

@@ -1,298 +0,0 @@
/* !!! 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

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

View File

@@ -1,5 +0,0 @@
/* !!! 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

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

View File

@@ -1,165 +0,0 @@
/* !!! 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

@@ -1,15 +0,0 @@
/* !!! 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

@@ -1,196 +0,0 @@
/* !!! 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)

File diff suppressed because it is too large Load Diff

View File

@@ -1,165 +0,0 @@
/* !!! 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

@@ -1,220 +0,0 @@
/* !!! 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

@@ -1,128 +0,0 @@
/* !!! 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

@@ -1,15 +0,0 @@
/* !!! 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

@@ -1,144 +0,0 @@
{
"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

@@ -1,2 +0,0 @@
"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});

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

@@ -1,87 +0,0 @@
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

@@ -1,6 +0,0 @@
/* !!! 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

@@ -1,45 +0,0 @@
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

@@ -1,5 +0,0 @@
/* !!! 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

@@ -1,5 +0,0 @@
/* !!! 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

@@ -1,30 +0,0 @@
/*
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 { datasource db {
provider = "sqlite" provider = "sqlite"
// url = env("DATABASE_URL") url = env("DATABASE_URL")
} }
generator client { generator client {
provider = "prisma-client" provider = "prisma-client-js"
output = "./generated/client" // output = "./generated/client"
} }
model User { model User {
@@ -15,15 +15,16 @@ model User {
displayName String displayName String
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
Session Session[] Session Session[]
UserPreferences UserPreferences? UserPreferences UserPreferences?
channels Channel[]
} }
model Session { model Session {
id String @id id String @id
userId String userId String
expiresAt DateTime expiresAt DateTime
user User @relation(references: [id], fields: [userId], onDelete: Cascade) user User @relation(references: [id], fields: [userId], onDelete: Cascade)
@@index([userId]) @@index([userId])
@@ -33,14 +34,7 @@ model UserPreferences {
userId String @id userId String @id
toggleInputHotkey String? @default("") toggleInputHotkey String? @default("")
toggleOutputHotkey String? @default("") toggleOutputHotkey String? @default("")
volumes Json? @default("{}")
user User @relation(fields: [userId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade)
} }
model Channel {
id String @id @default(cuid())
name String
maxClients Int?
persistent Boolean @default(false)
owner_id String?
owner User? @relation(fields: [owner_id], references: [id], onDelete: Cascade)
}

View File

@@ -1,23 +0,0 @@
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)
})

View File

@@ -0,0 +1,33 @@
import type { FastifyInstance } from 'fastify'
import bcrypt from 'bcrypt'
import { z } from 'zod'
export default function (fastify: FastifyInstance) {
fastify.post('/attachments/upload', async (req, reply) => {
try {
const schema = z.object({
file: z.file(),
})
const input = schema.parse(req.body)
// const file = req.file({ limits: { } })
const id = await bcrypt.hash(input.file, 10)
return {
id,
}
}
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,255 +0,0 @@
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 { FastifyInstance } from 'fastify'
import type { Namespace } from '../types/socket.ts' import type { Namespace } from '../types/webrtc.ts'
import { z } from 'zod' import { z } from 'zod'
import prisma from '../prisma/client.ts' import prisma from '../prisma/client.ts'
import { socketToClient } from '../utils/socket-to-client.ts' import { socketToClient } from '../utils/socket-to-client.ts'
@@ -77,7 +77,7 @@ export default function (fastify: FastifyInstance) {
if (found) { if (found) {
found.data.displayName = input.displayName found.data.displayName = input.displayName
namespace.emit('webrtc:client-changed', found.id, socketToClient(found)) namespace.emit('clientChanged', found.id, socketToClient(found))
} }
return updatedUser return updatedUser

View File

@@ -3,9 +3,12 @@ import { fileURLToPath } from 'node:url'
import FastifyAutoLoad from '@fastify/autoload' import FastifyAutoLoad from '@fastify/autoload'
import FastifyCookie from '@fastify/cookie' import FastifyCookie from '@fastify/cookie'
import FastifyCors from '@fastify/cors' import FastifyCors from '@fastify/cors'
import FastifyMultipart from '@fastify/multipart'
import Fastify from 'fastify' import Fastify from 'fastify'
import prisma from './prisma/client.ts' import prisma from './prisma/client.ts'
console.log(process.env.DATABASE_URL)
const __filename = fileURLToPath(import.meta.url) const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename) const __dirname = dirname(__filename)
@@ -25,6 +28,7 @@ fastify.register(FastifyCors, {
}) })
fastify.register(FastifyCookie) fastify.register(FastifyCookie)
fastify.register(FastifyMultipart)
fastify.register(FastifyAutoLoad, { fastify.register(FastifyAutoLoad, {
dir: join(__dirname, 'plugins'), dir: join(__dirname, 'plugins'),

View File

@@ -1,150 +0,0 @@
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,
}
}

28
server/socket/chat.ts Normal file
View File

@@ -0,0 +1,28 @@
import type { Server as SocketServer } from 'socket.io'
import type { ChatClientMessage, ChatMessage } from '../types/chat.ts'
import { v4 as uuidv4 } from 'uuid'
export default async function (io: SocketServer) {
const messages: ChatMessage[] = []
io.on('connection', async (socket) => {
socket.on('chat:message', async (clientMessage: ChatClientMessage, cb) => {
const message: ChatMessage = {
id: uuidv4(),
createdAt: new Date().toISOString(),
sender: socket.data.username,
text: clientMessage.text,
}
console.log(message)
messages.push(message)
if (messages.length > 5000) {
messages.shift()
}
io.emit('chat:new-message', message)
})
})
}

View File

@@ -1,21 +1,49 @@
import type { types } from 'mediasoup' import type { types } from 'mediasoup'
import type { ActiveSpeakerObserver, AudioLevelObserver } from 'mediasoup/types'
import type { Server as SocketServer } from 'socket.io' import type { Server as SocketServer } from 'socket.io'
import type { import type {
ChadSocket, ChadClient,
SomeSocket, SomeSocket,
} from '../types/socket.ts' } from '../types/webrtc.ts'
import { consola } from 'consola' import { consola } from 'consola'
import prisma from '../prisma/client.ts'
import { socketToClient } from '../utils/socket-to-client.ts' import { socketToClient } from '../utils/socket-to-client.ts'
export default async function ( export default async function (io: SocketServer, router: types.Router) {
io: SocketServer, const audioLevelObserver = await router.createAudioLevelObserver({
socket: ChadSocket, maxEntries: 10,
router: types.Router, threshold: -80,
audioLevelObserver: AudioLevelObserver, interval: 800,
activeSpeakerObserver: ActiveSpeakerObserver, })
) {
const activeSpeakerObserver = await router.createActiveSpeakerObserver()
audioLevelObserver.on('volumes', async (volumes: types.AudioLevelObserverVolume[]) => {
io.emit('speakingPeers', volumes.map(({ producer, volume }) => {
const { socketId } = producer.appData as { socketId: ChadClient['socketId'] }
return {
clientId: socketId,
volume,
}
}))
})
audioLevelObserver.on('silence', () => {
io.emit('speakingPeers', [])
io.emit('activeSpeaker', undefined)
})
activeSpeakerObserver.on('dominantspeaker', ({ producer }) => {
const { socketId } = producer.appData as { socketId: ChadClient['socketId'] }
io.emit('activeSpeaker', socketId)
})
io.on('connection', async (socket) => { io.on('connection', async (socket) => {
consola.info('[WebRtc]', 'Client connected', socket.id)
socket.data.joined = false
socket.data.inputMuted = false socket.data.inputMuted = false
socket.data.outputMuted = false socket.data.outputMuted = false
@@ -23,11 +51,54 @@ export default async function (
socket.data.producers = new Map() socket.data.producers = new Map()
socket.data.consumers = new Map() socket.data.consumers = new Map()
socket.on('webrtc:get-rtp-capabilities', (cb) => { 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) => {
cb(router.rtpCapabilities) cb(router.rtpCapabilities)
}) })
socket.on('webrtc:create-transport', async ({ producing, consuming }, cb) => { socket.on('createTransport', async ({ producing, consuming }, cb) => {
try { try {
const transport = await router.createWebRtcTransport({ const transport = await router.createWebRtcTransport({
listenInfos: [ listenInfos: [
@@ -82,7 +153,7 @@ export default async function (
} }
}) })
socket.on('webrtc:connect-transport', async ({ transportId, dtlsParameters }, cb) => { socket.on('connectTransport', async ({ transportId, dtlsParameters }, cb) => {
const transport = socket.data.transports.get(transportId) const transport = socket.data.transports.get(transportId)
if (!transport) { if (!transport) {
@@ -105,7 +176,7 @@ export default async function (
} }
}) })
socket.on('webrtc:produce', async ({ transportId, kind, rtpParameters, appData }, cb) => { socket.on('produce', async ({ transportId, kind, rtpParameters, appData }, cb) => {
if (!socket.data.joined) { if (!socket.data.joined) {
consola.error('Peer not joined yet') consola.error('Peer not joined yet')
cb({ error: 'Peer not joined yet' }) cb({ error: 'Peer not joined yet' })
@@ -113,14 +184,6 @@ export default async function (
return return
} }
// Block production in default channel
const currentChannelId = Array.from(socket.rooms).find(room => room !== socket.id) || 'default'
if (currentChannelId === 'default') {
consola.error('Cannot produce in default channel')
cb({ error: 'Cannot produce media in default channel' })
return
}
const transport = socket.data.transports.get(transportId) const transport = socket.data.transports.get(transportId)
if (!transport) { if (!transport) {
@@ -137,8 +200,7 @@ export default async function (
cb({ id: producer.id }) cb({ id: producer.id })
// Filter by channel when creating consumers const otherSockets = await getJoinedSockets(socket.id)
const otherSockets = await getJoinedSockets(socket.id, currentChannelId)
for (const otherSocket of otherSockets) { for (const otherSocket of otherSockets) {
createConsumer( createConsumer(
@@ -159,7 +221,7 @@ export default async function (
} }
}) })
socket.on('webrtc:close-producer', async ({ producerId }, cb) => { socket.on('closeProducer', async ({ producerId }, cb) => {
if (!socket.data.joined) { if (!socket.data.joined) {
consola.error('Peer not joined yet') consola.error('Peer not joined yet')
cb({ error: 'Peer not joined yet' }) cb({ error: 'Peer not joined yet' })
@@ -183,7 +245,7 @@ export default async function (
cb({ ok: true }) cb({ ok: true })
}) })
socket.on('webrtc:pause-producer', async ({ producerId }, cb) => { socket.on('pauseProducer', async ({ producerId }, cb) => {
if (!socket.data.joined) { if (!socket.data.joined) {
consola.error('Peer not joined yet') consola.error('Peer not joined yet')
cb({ error: 'Peer not joined yet' }) cb({ error: 'Peer not joined yet' })
@@ -208,7 +270,7 @@ export default async function (
cb({ ok: true }) cb({ ok: true })
}) })
socket.on('webrtc:resume-producer', async ({ producerId }, cb) => { socket.on('resumeProducer', async ({ producerId }, cb) => {
if (!socket.data.joined) { if (!socket.data.joined) {
consola.error('Peer not joined yet') consola.error('Peer not joined yet')
cb({ error: 'Peer not joined yet' }) cb({ error: 'Peer not joined yet' })
@@ -230,7 +292,7 @@ export default async function (
cb({ ok: true }) cb({ ok: true })
}) })
socket.on('webrtc:pause-consumer', async ({ consumerId }, cb) => { socket.on('pauseConsumer', async ({ consumerId }, cb) => {
if (!socket.data.joined) { if (!socket.data.joined) {
consola.error('Peer not joined yet') consola.error('Peer not joined yet')
cb({ error: 'Peer not joined yet' }) cb({ error: 'Peer not joined yet' })
@@ -252,7 +314,7 @@ export default async function (
cb({ ok: true }) cb({ ok: true })
}) })
socket.on('webrtc:resume-consumer', async ({ consumerId }, cb) => { socket.on('resumeConsumer', async ({ consumerId }, cb) => {
if (!socket.data.joined) { if (!socket.data.joined) {
consola.error('Peer not joined yet') consola.error('Peer not joined yet')
cb({ error: 'Peer not joined yet' }) cb({ error: 'Peer not joined yet' })
@@ -274,7 +336,7 @@ export default async function (
cb({ ok: true }) cb({ ok: true })
}) })
socket.on('webrtc:update-client', async (updatedClient, cb) => { socket.on('updateClient', async (updatedClient, cb) => {
if (typeof updatedClient.inputMuted === 'boolean') { if (typeof updatedClient.inputMuted === 'boolean') {
socket.data.inputMuted = updatedClient.inputMuted socket.data.inputMuted = updatedClient.inputMuted
} }
@@ -285,21 +347,14 @@ export default async function (
cb(socketToClient(socket)) cb(socketToClient(socket))
io.emit('webrtc:client-changed', socket.id, socketToClient(socket)) io.emit('clientChanged', socket.id, socketToClient(socket))
}) })
socket.on('disconnect', () => { socket.on('disconnect', () => {
consola.info('Client disconnected:', socket.id) consola.info('Client disconnected:', socket.id)
// Get current channel from Socket.IO rooms
const channelId = Array.from(socket.rooms).find(room => room !== socket.id)
if (socket.data.joined) { if (socket.data.joined) {
// Notify only same channel using Socket.IO room socket.broadcast.emit('peerClosed', socket.id)
if (channelId) {
socket.to(channelId).emit('webrtc:peer-closed', socket.id)
io.emit('channelUserLeft', { channelId, clientId: socket.id })
}
} }
for (const transport of socket.data.transports.values()) { for (const transport of socket.data.transports.values()) {
@@ -308,21 +363,10 @@ export default async function (
}) })
}) })
async function getJoinedSockets(excludeId?: string, channelId?: string) { async function getJoinedSockets(excludeId?: string) {
let sockets = await io.fetchSockets() const sockets = await io.fetchSockets()
// Filter by channel using Socket.IO rooms return sockets.filter(socket => socket.data.joined && (excludeId ? excludeId !== socket.id : true))
if (channelId) {
sockets = await io.in(channelId).fetchSockets()
}
return sockets.filter((socket) => {
if (!socket.data.joined)
return false
if (excludeId && socket.id === excludeId)
return false
return true
})
} }
async function createConsumer( async function createConsumer(
@@ -380,24 +424,24 @@ export default async function (
consumer.on('producerclose', () => { consumer.on('producerclose', () => {
consumerSocket.data.consumers.delete(consumer.id) consumerSocket.data.consumers.delete(consumer.id)
consumerSocket.emit('webrtc:consumer-closed', { consumerId: consumer.id }) consumerSocket.emit('consumerClosed', { consumerId: consumer.id })
}) })
consumer.on('producerpause', () => { consumer.on('producerpause', () => {
consumerSocket.emit('webrtc:consumer-paused', { consumerId: consumer.id }) consumerSocket.emit('consumerPaused', { consumerId: consumer.id })
}) })
consumer.on('producerresume', () => { consumer.on('producerresume', () => {
consumerSocket.emit('webrtc:consumer-resumed', { consumerId: consumer.id }) consumerSocket.emit('consumerResumed', { consumerId: consumer.id })
}) })
consumer.on('score', (score: types.ConsumerScore) => { consumer.on('score', (score: types.ConsumerScore) => {
consumerSocket.emit('webrtc:consumer-score', { consumerId: consumer.id, score }) consumerSocket.emit('consumerScore', { consumerId: consumer.id, score })
}) })
try { try {
await consumerSocket.emitWithAck( await consumerSocket.emitWithAck(
'webrtc:new-consumer', 'newConsumer',
{ {
socketId: producerSocket.id, socketId: producerSocket.id,
producerId: producer.id, producerId: producer.id,
@@ -413,7 +457,7 @@ export default async function (
await consumer.resume() await consumer.resume()
consumerSocket.emit( consumerSocket.emit(
'webrtc:consumer-score', 'consumerScore',
{ {
consumerId: consumer.id, consumerId: consumer.id,
score: consumer.score, score: consumer.score,

View File

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

18
server/types/chat.ts Normal file
View File

@@ -0,0 +1,18 @@
export interface ChatClientMessage {
text: string
replyTo?: {
messageId: string
}
}
export interface ChatMessage {
id: string
sender: string
text: string
createdAt: string
replyTo?: {
messageId: string
sender: string
text: string
}
}

View File

@@ -1,173 +0,0 @@
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>

View File

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

142
server/types/webrtc.ts Normal file
View File

@@ -0,0 +1,142 @@
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

@@ -1,16 +0,0 @@
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,9 +1,6 @@
import type { ChadClient, SomeSocket } from '../types/socket.ts' import type { ChadClient, SomeSocket } from '../types/webrtc.ts'
export function socketToClient(socket: SomeSocket): ChadClient { export function socketToClient(socket: SomeSocket): ChadClient {
// Socket.IO rooms: Extract channel room (filter out the socket's own room)
const channelId = Array.from(socket.rooms).find(room => room !== socket.id) || 'default'
return { return {
socketId: socket.id, socketId: socket.id,
userId: socket.data.userId, userId: socket.data.userId,
@@ -11,6 +8,5 @@ export function socketToClient(socket: SomeSocket): ChadClient {
displayName: socket.data.displayName, displayName: socket.data.displayName,
inputMuted: socket.data.inputMuted, inputMuted: socket.data.inputMuted,
outputMuted: socket.data.outputMuted, outputMuted: socket.data.outputMuted,
currentChannelId: channelId,
} }
} }

File diff suppressed because it is too large Load Diff