работаем бля работаем

This commit is contained in:
2026-05-09 03:21:44 +06:00
parent f845777bac
commit 0b148c6a7d
169 changed files with 15816 additions and 1005 deletions

54
server/.zed/settings.json Normal file
View File

@@ -0,0 +1,54 @@
{
// Use ESLint's --fix:
"code_actions_on_format": {
"source.fixAll.eslint": true,
},
"formatter": [],
// Enable eslint for all supported languages
// Defaults only include https://github.com/search?q=repo%3Azed-industries%2Fzed%20eslint_languages&type=code
"languages": {
"HTML": {
"language_servers": ["...", "eslint"],
},
"Markdown": {
"language_servers": ["...", "eslint"],
},
"Markdown-Inline": {
"language_servers": ["...", "eslint"],
},
"JSON": {
"language_servers": ["...", "eslint"],
},
"JSONC": {
"language_servers": ["...", "eslint"],
},
"YAML": {
"language_servers": ["...", "eslint"],
},
"CSS": {
"language_servers": ["...", "eslint"],
},
// Add other languages as needed
},
"lsp": {
"eslint": {
"settings": {
"workingDirectories": ["./"],
// Silent the stylistic rules in your IDE, but still auto fix them
"rulesCustomizations": [
{ "rule": "style/*", "severity": "off", "fixable": true },
{ "rule": "format/*", "severity": "off", "fixable": true },
{ "rule": "*-indent", "severity": "off", "fixable": true },
{ "rule": "*-spacing", "severity": "off", "fixable": true },
{ "rule": "*-spaces", "severity": "off", "fixable": true },
{ "rule": "*-order", "severity": "off", "fixable": true },
{ "rule": "*-dangle", "severity": "off", "fixable": true },
{ "rule": "*-newline", "severity": "off", "fixable": true },
{ "rule": "*quotes", "severity": "off", "fixable": true },
{ "rule": "*semi", "severity": "off", "fixable": true },
],
},
},
},
}

View File

@@ -10,6 +10,167 @@
* ---------------------------------------------------------------
*/
/**
* Attachment
* Attachment
*/
export interface Attachment {
id: string;
name: string;
mimetype: string;
/** @min 0 */
size: number;
/** @format date-time */
createdAt: string;
}
/**
* Channel
* Channel
*/
export interface Channel {
id: string;
ownerId: string | null;
name: string;
persistent: boolean;
}
/**
* ChatMessage
* ChatMessage
*/
export interface ChatMessage {
/** @format uuid */
id: string;
/** @format uuid */
senderId: string;
/** @minLength 1 */
text: string;
/** @format date-time */
createdAt: string;
/** @format date-time */
updatedAt: string;
attachments: string[];
}
/**
* CreateChannelPayload
* CreateChannelPayload
*/
export interface CreateChannelPayload {
name: string;
persistent: boolean;
}
/**
* CreateUser
* CreateUser
*/
export interface CreateUser {
/** @minLength 1 */
username: string;
/** @minLength 6 */
password: string;
}
/**
* GetAttachmentParams
* GetAttachmentParams
*/
export interface GetAttachmentParams {
/** @format uuid */
id: string;
}
/**
* GetUserQuery
* GetUserQuery
*/
export interface GetUserQuery {
username?: string;
}
/**
* Login
* Login
*/
export interface Login {
/** @minLength 1 */
username: string;
/** @minLength 1 */
password: string;
}
/**
* NewChatMessagePayload
* NewChatMessagePayload
*/
export interface NewChatMessagePayload {
/** @minLength 1 */
text: string;
attachments?: string[];
}
/**
* Reply
* Reply
*/
export interface Reply {
/** @format uuid */
messageId: string;
/** @format uuid */
senderId: string;
text: string;
}
/**
* ResponseError
* ResponseError
*/
export interface ResponseError {
statusCode: number;
error: string;
message: string;
}
/**
* UpdateUserPayload
* UpdateUserPayload
*/
export interface UpdateUserPayload {
displayName: string;
}
/**
* UpdateUserPreferencesPayload
* UpdateUserPreferencesPayload
*/
export interface UpdateUserPreferencesPayload {
toggleInputHotkey?: string;
toggleOutputHotkey?: string;
}
/**
* UserPreferences
* UserPreferences
*/
export interface UserPreferences {
toggleInputHotkey: string;
toggleOutputHotkey: string;
}
/**
* User
* User
*/
export interface User {
id: string;
username: string;
displayName: string;
/** @format date-time */
createdAt: string;
}
export type QueryParamsType = Record<string | number, any>;
export type ResponseFormat = keyof Omit<Body, "body" | "bodyUsed">;
@@ -282,14 +443,7 @@ export class Api<
* @request POST:/chad/attachment/upload
*/
attachmentUpload: (params: RequestParams = {}) =>
this.request<
string,
{
statusCode: number;
error: string;
message: string;
}
>({
this.request<string, ResponseError>({
path: `/chad/attachment/upload`,
method: "POST",
format: "json",
@@ -305,14 +459,7 @@ export class Api<
* @request GET:/chad/attachment/{id}
*/
attachmentGet: (id: string, params: RequestParams = {}) =>
this.request<
any,
{
statusCode: number;
error: string;
message: string;
}
>({
this.request<any, ResponseError>({
path: `/chad/attachment/${id}`,
method: "GET",
format: "json",
@@ -327,29 +474,8 @@ export class Api<
* @summary Register
* @request POST:/chad/auth/register
*/
authRegister: (
data: {
/** @minLength 1 */
username: string;
/** @minLength 6 */
password: string;
},
params: RequestParams = {},
) =>
this.request<
{
id: string;
username: string;
displayName: string;
/** @format date-time */
createdAt: string;
},
{
statusCode: number;
error: string;
message: string;
}
>({
authRegister: (data: CreateUser, params: RequestParams = {}) =>
this.request<User, ResponseError>({
path: `/chad/auth/register`,
method: "POST",
body: data,
@@ -366,29 +492,8 @@ export class Api<
* @summary Login
* @request POST:/chad/auth/login
*/
authLogin: (
data: {
/** @minLength 1 */
username: string;
/** @minLength 1 */
password: string;
},
params: RequestParams = {},
) =>
this.request<
{
id: string;
username: string;
displayName: string;
/** @format date-time */
createdAt: string;
},
{
statusCode: number;
error: string;
message: string;
}
>({
authLogin: (data: Login, params: RequestParams = {}) =>
this.request<User, ResponseError>({
path: `/chad/auth/login`,
method: "POST",
body: data,
@@ -406,20 +511,7 @@ export class Api<
* @request GET:/chad/auth/me
*/
authMe: (params: RequestParams = {}) =>
this.request<
{
id: string;
username: string;
displayName: string;
/** @format date-time */
createdAt: string;
},
{
statusCode: number;
error: string;
message: string;
}
>({
this.request<User, ResponseError>({
path: `/chad/auth/me`,
method: "GET",
format: "json",
@@ -435,19 +527,61 @@ export class Api<
* @request POST:/chad/auth/logout
*/
authLogout: (params: RequestParams = {}) =>
this.request<
any,
{
statusCode: number;
error: string;
message: string;
}
>({
this.request<any, ResponseError>({
path: `/chad/auth/logout`,
method: "POST",
...params,
}),
/**
* No description
*
* @tags Channel
* @name ChannelList
* @summary Get channel list
* @request GET:/chad/channels
*/
channelList: (params: RequestParams = {}) =>
this.request<Channel[], ResponseError>({
path: `/chad/channels`,
method: "GET",
format: "json",
...params,
}),
/**
* No description
*
* @tags Channel
* @name ChannelCreate
* @summary Create channel
* @request POST:/chad/channels
*/
channelCreate: (data: CreateChannelPayload, params: RequestParams = {}) =>
this.request<Channel, ResponseError>({
path: `/chad/channels`,
method: "POST",
body: data,
type: ContentType.Json,
format: "json",
...params,
}),
/**
* No description
*
* @tags Channel
* @name ChannelDelete
* @summary Delete channel
* @request DELETE:/chad/channels/{id}
*/
channelDelete: (id: string, params: RequestParams = {}) =>
this.request<any, ResponseError>({
path: `/chad/channels/${id}`,
method: "DELETE",
...params,
}),
/**
* No description
*
@@ -460,6 +594,7 @@ export class Api<
data: {
/** @minLength 1 */
text: string;
attachments?: string[];
},
params: RequestParams = {},
) =>
@@ -475,12 +610,9 @@ export class Api<
createdAt: string;
/** @format date-time */
updatedAt: string;
attachments: string[];
},
{
statusCode: number;
error: string;
message: string;
}
ResponseError
>({
path: `/chad/chat/send`,
method: "POST",
@@ -508,7 +640,7 @@ export class Api<
/**
* @min 1
* @max 100
* @default 2
* @default 10
*/
limit: number;
},
@@ -527,6 +659,7 @@ export class Api<
createdAt: string;
/** @format date-time */
updatedAt: string;
attachments: string[];
}[];
/**
* Cursor to last message
@@ -534,11 +667,7 @@ export class Api<
*/
nextCursor?: string;
},
{
statusCode: number;
error: string;
message: string;
}
ResponseError
>({
path: `/chad/chat`,
method: "GET",
@@ -561,20 +690,7 @@ export class Api<
},
params: RequestParams = {},
) =>
this.request<
{
id: string;
username: string;
displayName: string;
/** @format date-time */
createdAt: string;
},
{
statusCode: number;
error: string;
message: string;
}
>({
this.request<User, ResponseError>({
path: `/chad/user`,
method: "GET",
query: query,
@@ -591,17 +707,7 @@ export class Api<
* @request GET:/chad/user/preferences
*/
userGetPreferences: (params: RequestParams = {}) =>
this.request<
{
toggleInputHotkey: string;
toggleOutputHotkey: string;
},
{
statusCode: number;
error: string;
message: string;
}
>({
this.request<UserPreferences, ResponseError>({
path: `/chad/user/preferences`,
method: "GET",
format: "json",
@@ -617,20 +723,10 @@ export class Api<
* @request PATCH:/chad/user/preferences
*/
userUpdatePreferences: (
data: {
toggleInputHotkey?: string;
toggleOutputHotkey?: string;
},
data: UpdateUserPreferencesPayload,
params: RequestParams = {},
) =>
this.request<
any,
{
statusCode: number;
error: string;
message: string;
}
>({
this.request<any, ResponseError>({
path: `/chad/user/preferences`,
method: "PATCH",
body: data,
@@ -646,26 +742,8 @@ export class Api<
* @summary Update profile
* @request PATCH:/chad/profile
*/
userUpdateProfile: (
data: {
displayName: string;
},
params: RequestParams = {},
) =>
this.request<
{
id: string;
username: string;
displayName: string;
/** @format date-time */
createdAt: string;
},
{
statusCode: number;
error: string;
message: string;
}
>({
userUpdateProfile: (data: UpdateUserPayload, params: RequestParams = {}) =>
this.request<User, ResponseError>({
path: `/chad/profile`,
method: "PATCH",
body: data,

View File

@@ -2,5 +2,5 @@
"watch": ["."],
"ext": ".ts,.js",
"ignore": ["node_modules", ".idea", "dist"],
"exec": "ts-node --transpile-only server.ts"
"exec": ""
}

View File

@@ -1,12 +1,15 @@
{
"name": "server",
"type": "module",
"packageManager": "yarn@4.10.3",
"engines": {
"node": ">=22.18.0"
},
"scripts": {
"dev": "nodemon",
"start": "ts-node --transpile-only server.ts",
"db:deploy": "npx prisma migrate deploy && npx prisma generate"
},
"type": "module",
"packageManager": "yarn@4.10.3",
"dependencies": {
"@fastify/autoload": "^6.3.1",
"@fastify/cookie": "^11.0.2",
@@ -25,7 +28,7 @@
"fastify": "^5.6.1",
"fastify-plugin": "^5.1.0",
"lucia": "^3.2.2",
"mediasoup": "^3.19.3",
"mediasoup": "^3.19.21",
"prisma": "7",
"socket.io": "^4.8.1",
"typebox": "^1.1.27",
@@ -42,8 +45,5 @@
"nodemon": "^3.1.14",
"ts-node": "^10.9.2",
"typescript": "^5.9.3"
},
"engines": {
"node": ">=22.18.0"
}
}

View File

@@ -1,7 +1,8 @@
import type { FastifyPluginAsync } from 'fastify'
import type { Type } from 'typebox'
import type { UserSchema } from '../schemas/auth.ts'
import type { ChatMessageSchema } from '../schemas/chat.ts'
import type { Channel } from '../prisma/generated-client/client.ts'
import type { UserSchema } from './schemas/auth.ts'
import type { ChatMessageSchema } from './schemas/chat.ts'
import { EventEmitter } from 'node:events'
import fp from 'fastify-plugin'
@@ -14,6 +15,8 @@ declare module 'fastify' {
interface EventMap {
'chat:new-message': [Type.Static<typeof ChatMessageSchema>]
'user:profile-updated': [Type.Static<typeof UserSchema>]
'channel:created': [Channel]
'channel:removed': [Channel]
}
const plugin: FastifyPluginAsync = fp(async (fastify) => {

View File

@@ -27,20 +27,9 @@ export const autoConfig: mediasoup.types.RouterOptions = {
},
{
kind: 'video',
mimeType: 'video/VP8',
mimeType: 'video/AV1',
clockRate: 90000,
parameters: {
'x-google-start-bitrate': 1000,
},
},
{
kind: 'video',
mimeType: 'video/VP9',
clockRate: 90000,
parameters: {
'profile-id': 2,
'x-google-start-bitrate': 1000,
},
parameters: {},
},
{
kind: 'video',
@@ -66,9 +55,20 @@ export const autoConfig: mediasoup.types.RouterOptions = {
},
{
kind: 'video',
mimeType: 'video/AV1',
mimeType: 'video/VP8',
clockRate: 90000,
parameters: {},
parameters: {
'x-google-start-bitrate': 1000,
},
},
{
kind: 'video',
mimeType: 'video/VP9',
clockRate: 90000,
parameters: {
'profile-id': 2,
'x-google-start-bitrate': 1000,
},
},
],
}

View File

@@ -11,6 +11,7 @@ declare module 'fastify' {
export default fp(
async (fastify) => {
const worker = await mediasoup.createWorker()
worker.on('died', () => {
consola.error('[Mediasoup]', 'Worker died, exiting...')

View File

@@ -11,7 +11,7 @@ declare module 'fastify' {
const plugin: FastifyPluginAsync = fp(async (fastify) => {
const prisma = new PrismaClient({
log: ['query', 'error', 'warn'],
log: ['error'],
adapter: new PrismaBetterSqlite3({
url: process.env.DATABASE_URL!,
}),

11
server/plugins/schemas.ts Normal file
View File

@@ -0,0 +1,11 @@
import type { FastifyPluginAsync } from 'fastify'
import fp from 'fastify-plugin'
import * as schemas from './schemas/index.ts'
const plugin: FastifyPluginAsync = fp(async (fastify) => {
for (const schema of Object.values(schemas)) {
fastify.addSchema(schema)
}
})
export default plugin

View File

@@ -6,6 +6,8 @@ export const AttachmentSchema = Type.Object({
mimetype: Type.String(),
size: Type.Number({ minimum: 0 }),
createdAt: Type.String({ format: 'date-time' }),
}, { $id: 'Attachment' })
// message: Type.MessageAttachment(),
}, { title: 'Attachment', description: 'Attachment' })
export const GetAttachmentParamsSchema = Type.Object({
id: Type.String({ format: 'uuid' }),
}, { $id: 'GetAttachmentParams' })

View File

@@ -5,9 +5,14 @@ export const UserSchema = Type.Object({
username: Type.String(),
displayName: Type.String(),
createdAt: Type.String({ format: 'date-time' }),
}, { title: 'User', description: 'User' })
}, { $id: 'User' })
export const CreateUserSchema = Type.Object({
export const CreateUserPayloadSchema = Type.Object({
username: Type.String({ minLength: 1 }),
password: Type.String({ minLength: 6 }),
}, { title: 'CreateUser' })
}, { $id: 'CreateUser' })
export const LoginPayloadSchema = Type.Object({
username: Type.String({ minLength: 1 }),
password: Type.String({ minLength: 1 }),
}, { $id: 'Login' })

View File

@@ -0,0 +1,13 @@
import { Type } from 'typebox'
export const ChannelSchema = Type.Object({
id: Type.String(),
ownerId: Type.Union([Type.String(), Type.Null()]),
name: Type.String(),
persistent: Type.Boolean(),
}, { $id: 'Channel' })
export const CreateChannelPayloadSchema = Type.Object({
name: Type.String(),
persistent: Type.Boolean(),
}, { $id: 'CreateChannelPayload' })

View File

@@ -4,7 +4,7 @@ export const ReplySchema = Type.Object({
messageId: Type.String({ format: 'uuid' }),
senderId: Type.String({ format: 'uuid' }),
text: Type.String(),
}, { title: 'Reply', description: 'Reply' })
}, { $id: 'Reply' })
export const ChatMessageSchema = Type.Object({
id: Type.String({ format: 'uuid' }),
@@ -14,13 +14,12 @@ export const ChatMessageSchema = Type.Object({
updatedAt: Type.String({ format: 'date-time' }),
attachments: Type.Array(Type.String({ format: 'uuid' })),
// replyTo: ReplySchema,
}, { title: 'ChatMessage', description: 'ChatMessage' })
}, { $id: 'ChatMessage' })
export const NewChatMessageSchema = Type.Object({
export const NewChatMessagePayloadSchema = Type.Object({
text: Type.String({ minLength: 1 }),
attachments: Type.Optional(Type.Array(Type.String({ format: 'uuid' }))),
// replyTo: Type.Object({
// messageId: Type.String({ format: 'uuid' }),
// }),
}, { title: 'NewChatMessage', description: 'NewChatMessage' })
}, { $id: 'NewChatMessagePayload' })

View File

@@ -1,7 +1,7 @@
import { Type } from 'typebox'
export const ErrorReplySchema = Type.Object({
export const ResponseErrorSchema = Type.Object({
statusCode: Type.Number(),
error: Type.String(),
message: Type.String(),
}, { title: 'ResponseError', description: 'Response Error' })
}, { $id: 'ResponseError' })

View File

@@ -0,0 +1,6 @@
export * from './attachment.ts'
export * from './auth.ts'
export * from './channel.ts'
export * from './chat.ts'
export * from './common.ts'
export * from './user.ts'

View File

@@ -0,0 +1,19 @@
import { Type } from 'typebox'
export const GetUserQuerySchema = Type.Partial(Type.Object({
username: Type.String(),
}), { $id: 'GetUserQuery' })
export const UserPreferencesSchema = Type.Object({
toggleInputHotkey: Type.String(),
toggleOutputHotkey: Type.String(),
}, { $id: 'UserPreferences' })
export const UpdateUserPreferencesPayloadSchema = Type.Partial(
UserPreferencesSchema,
{ $id: 'UpdateUserPreferencesPayload' },
)
export const UpdateUserPayloadSchema = Type.Object({
displayName: Type.String(),
}, { $id: 'UpdateUserPayload' })

View File

@@ -1,10 +1,9 @@
import type { FastifyInstance } from 'fastify'
import type { ServerOptions } from 'socket.io'
import type { MessageSelect } from '../prisma/generated-client/models/Message.ts'
import fp from 'fastify-plugin'
import { Server } from 'socket.io'
import registerChatSocket from '../socket/chat.ts'
import registerWebrtcSocket from '../socket/webrtc.ts'
import registerChatSocket from './socket/chat/index.ts'
import registerWebrtcSocket from './socket/webrtc/index.ts'
declare module 'fastify' {
interface FastifyInstance {
@@ -24,12 +23,26 @@ export default fp<Partial<ServerOptions>>(
await fastify.io.close()
})
await registerWebrtcSocket(fastify.io, fastify.mediasoupRouter, fastify.prisma)
await registerChatSocket(fastify.io)
fastify.io.use(async (socket, next) => {
const sessionId = fastify.lucia.readSessionCookie(socket.handshake.headers.cookie ?? '')
fastify.bus.on('chat:new-message', async (message: MessageSelect) => {
fastify.io.emit('chat:new-message', message)
if (!sessionId) {
return next(fastify.httpErrors.unauthorized())
}
const { user } = await fastify.lucia.validateSession(sessionId)
if (!user) {
return next(fastify.httpErrors.unauthorized())
}
socket.data.user = user
next()
})
await registerWebrtcSocket(fastify)
await registerChatSocket(fastify)
},
{ name: 'socket-io', dependencies: ['mediasoup-worker', 'mediasoup-router', 'prisma', 'event-bus'] },
)

View File

@@ -0,0 +1,10 @@
import type { FastifyInstance } from 'fastify'
import type { MessageSelect } from '../../../prisma/generated-client/models.ts'
export default async function (fastify: FastifyInstance) {
const { io, bus } = fastify
bus.on('chat:new-message', async (message: MessageSelect) => {
io.emit('chat:new-message', message)
})
}

View File

@@ -0,0 +1,136 @@
import type { types } from 'mediasoup'
import type { Server, Socket } from 'socket.io'
import type { Channel, User } from '../../prisma/generated-client/client.ts'
export interface SerializedClient {
socketId: string
userId: User['id']
channelId: Channel['id']
inputMuted: boolean
outputMuted: boolean
streaming: boolean
}
export interface ProducerAppData extends types.AppData {
source: 'mic-video' | 'share'
}
export interface ErrorCallbackResult {
error: string
}
export interface SuccessCallbackResult {
ok: true
}
export type EventCallback<T = SuccessCallbackResult> = (result: T | ErrorCallbackResult) => void
export interface ClientToServerEvents {
'join-channel': (
options: { channelId: string },
cb?: EventCallback
) => void
'create-transport': (
options: {
producing: boolean
consuming: boolean
},
cb: EventCallback<Pick<types.WebRtcTransport, 'id' | 'iceParameters' | 'iceCandidates' | 'dtlsParameters'>>
) => void
'connect-transport': (
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
'close-producer': (
options: {
producerId: types.Producer['id']
},
cb: EventCallback
) => void
'pause-producer': (
options: {
producerId: types.Producer['id']
},
cb: EventCallback
) => void
'resume-producer': (
options: {
producerId: types.Producer['id']
},
cb: EventCallback
) => void
'pause-consumer': (
options: {
consumerId: types.Consumer['id']
},
cb: EventCallback
) => void
'resume-consumer': (
options: {
consumerId: types.Consumer['id']
},
cb: EventCallback
) => void
'update-client': (
options: Partial<Pick<SerializedClient, 'inputMuted' | 'outputMuted'>>,
cb: EventCallback<SerializedClient>
) => void
}
export interface ServerToClientEvents {
'initialized': (arg: {
rtpCapabilities: types.RtpCapabilities
channelId: string
clients: SerializedClient[]
}) => void
'new-client': (arg: SerializedClient) => void
'client-updated': (arg: SerializedClient) => void
'client-switched-channel': (arg: SerializedClient) => void
'client-disconnected': (arg: string) => void
'producers': (arg: {
producerId: types.Producer['id']
kind: types.MediaKind
}[]) => void
'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
'consumer-closed': (arg: { consumerId: string }) => void
'consumer-paused': (arg: { consumerId: string }) => void
'consumer-resumed': (arg: { consumerId: string }) => void
'speaking-clients': (arg: { clientId: SerializedClient['socketId'], volume: types.AudioLevelObserverVolume['volume'] }[]) => void
'active-speaker': (arg?: SerializedClient['socketId']) => void
'channel-created': (arg: Channel) => void
'channel-removed': (arg: Channel['id']) => void
'channel-updated': (arg: Channel) => void
}
export interface InterServerEvent {}
export interface SocketData {
user: User
}
export type ChadSocket = Socket<ClientToServerEvents, ServerToClientEvents, InterServerEvent, SocketData>
export type ChadSocketServer = Server<ClientToServerEvents, ServerToClientEvents, InterServerEvent, SocketData>

View File

@@ -0,0 +1,119 @@
import type { types } from 'mediasoup'
import type { ActiveSpeakerObserverDominantSpeaker } from 'mediasoup/types'
import type { Client } from './Client.ts'
import { EventEmitter } from 'node:events'
interface ChannelEvents {
'speaking-peers': [{
socketId: string
volume: number
}[]]
'silence': []
'active-speaker': [socketId: string]
'empty': []
}
export class Channel extends EventEmitter<ChannelEvents> {
readonly id: string
readonly persistent: boolean
readonly #audioLevelObserver: types.AudioLevelObserver
readonly #activeSpeakerObserver: types.ActiveSpeakerObserver
readonly #clients = new Map<string, Client>()
private constructor(
id: string,
persistent: boolean,
audioLevelObserver: types.AudioLevelObserver,
activeSpeakerObserver: types.ActiveSpeakerObserver,
) {
super()
this.id = id
this.persistent = persistent
this.#audioLevelObserver = audioLevelObserver
this.#activeSpeakerObserver = activeSpeakerObserver
this.#audioLevelObserver.on('volumes', (volumes: types.AudioLevelObserverVolume[]) => {
this.emit('speaking-peers', volumes.map(({ producer, volume }) => {
const { socketId } = producer.appData as { socketId: string }
return { socketId, volume }
}))
})
this.#audioLevelObserver.on('silence', () => {
this.emit('silence')
})
this.#activeSpeakerObserver.on('dominantspeaker', ({ producer }: ActiveSpeakerObserverDominantSpeaker) => {
const { socketId } = producer.appData as { socketId: string }
this.emit('active-speaker', socketId)
})
}
static async create(id: string, persistent: boolean, router: types.Router): Promise<Channel> {
const audioLevelObserver = await router.createAudioLevelObserver({
maxEntries: 10,
threshold: -80,
interval: 800,
})
const activeSpeakerObserver = await router.createActiveSpeakerObserver()
return new Channel(id, persistent, audioLevelObserver, activeSpeakerObserver)
}
get clients(): Client[] {
return Array.from(this.#clients.values())
}
get size(): number {
return this.#clients.size
}
getClient(socketId: string): Client | undefined {
return this.#clients.get(socketId)
}
addClient(client: Client): void {
client.channelId = this.id
this.#clients.set(client.socketId, client)
}
kickClient(client: Client): void {
this.#clients.delete(client.socketId)
if (this.#clients.size === 0)
this.emit('empty')
}
async addAudioProducer(producer: types.Producer): Promise<void> {
if (producer.kind !== 'audio')
return
await this.#audioLevelObserver.addProducer({ producerId: producer.id })
await this.#activeSpeakerObserver.addProducer({ producerId: producer.id })
}
async wireClient(client: Client): Promise<void> {
for (const otherClient of this.#clients.values()) {
if (otherClient.socketId === client.socketId)
continue
for (const producer of otherClient.producers.values()) {
await client.createConsumerFor(producer, otherClient.socketId)
}
for (const producer of client.producers.values()) {
await otherClient.createConsumerFor(producer, client.socketId)
}
}
}
unwireClient(client: Client): void {
for (const otherClient of this.#clients.values()) {
for (const producerId of client.producers.keys()) {
otherClient.removeConsumersOf(producerId)
}
}
}
}

View File

@@ -0,0 +1,33 @@
import type { Router } from 'mediasoup/types'
import type { Channel as DbChannel } from '../../../prisma/generated-client/client.ts'
import { Channel } from './Channel.ts'
export class ChannelManager {
private channels = new Map<string, Channel>()
private mediasoupRouter: Router
constructor(mediasoupRouter: Router) {
this.mediasoupRouter = mediasoupRouter
}
async create(newChannel: Channel | DbChannel) {
if (newChannel instanceof Channel) {
this.channels.set(newChannel.id, newChannel)
}
else {
this.channels.set(newChannel.id, await Channel.create(newChannel.id, newChannel.persistent, this.mediasoupRouter))
}
}
get(id: string) {
return this.channels.get(id)
}
delete(id: string) {
this.channels.delete(id)
}
get all() {
return Array.from(this.channels.values())
}
}

View File

@@ -0,0 +1,288 @@
import type { types } from 'mediasoup'
import type { SerializedClient } from '../types.ts'
import { EventEmitter } from 'node:events'
import { consola } from 'consola'
export interface NewConsumerSignal {
socketId: string
producerId: string
id: string
kind: types.MediaKind
rtpParameters: types.RtpParameters
type: types.ConsumerType
appData: types.Producer['appData']
producerPaused: boolean
}
interface ClientEvents {
'signal:new-consumer': [data: NewConsumerSignal, onAcked: () => Promise<void>]
'consumer:closed': [consumerId: string]
'consumer:paused': [consumerId: string]
'consumer:resumed': [consumerId: string]
'transport:closed': []
'closed': []
'updated': []
}
export class Client extends EventEmitter<ClientEvents> {
readonly socketId: string
readonly userId: string
channelId: string = ''
#inputMuted = false
#outputMuted = false
readonly #router: types.Router
readonly #transports = new Map<string, types.WebRtcTransport>()
readonly #producers = new Map<string, types.Producer>()
readonly #consumers = new Map<string, types.Consumer>()
constructor(socketId: string, userId: string, router: types.Router) {
super()
this.socketId = socketId
this.userId = userId
this.#router = router
}
get producers(): ReadonlyMap<string, types.Producer> { return this.#producers }
get consumers(): ReadonlyMap<string, types.Consumer> { return this.#consumers }
get transports(): ReadonlyMap<string, types.WebRtcTransport> { return this.#transports }
get inputMuted(): boolean { return this.#inputMuted }
get outputMuted(): boolean { return this.#outputMuted }
get streaming(): boolean {
return Array.from(this.#producers.values()).some(
producer => producer.kind === 'video' && producer.appData.source === 'share',
)
}
async createTransport(options: { producing: boolean, consuming: boolean }) {
const transport = await this.#router.createWebRtcTransport({
listenInfos: [{
protocol: 'udp',
ip: '0.0.0.0',
announcedAddress: process.env.ANNOUNCED_ADDRESS || '127.0.0.1',
portRange: { min: 40000, max: 40100 },
}],
enableUdp: true,
preferUdp: true,
appData: options,
})
this.#transports.set(transport.id, transport)
transport.on('icestatechange', (iceState: types.IceState) => {
if (iceState === 'disconnected' || iceState === 'closed') {
consola.info('[Client]', `[${this.socketId}]`, `iceState=${iceState}`)
this.emit('transport:closed')
}
})
transport.on('dtlsstatechange', (dtlsState: types.DtlsState) => {
if (dtlsState === 'failed' || dtlsState === 'closed') {
consola.warn('[Client]', `[${this.socketId}]`, `dtlsState=${dtlsState}`)
this.emit('transport:closed')
}
})
return {
id: transport.id,
iceParameters: transport.iceParameters,
iceCandidates: transport.iceCandidates,
dtlsParameters: transport.dtlsParameters,
}
}
async connectTransport(transportId: string, dtlsParameters: types.DtlsParameters): Promise<void> {
const transport = this.#transports.get(transportId)
if (!transport)
throw new Error(`Transport not found: ${transportId}`)
await transport.connect({ dtlsParameters })
}
async produce(
transportId: string,
kind: types.MediaKind,
rtpParameters: types.RtpParameters,
appData: object,
): Promise<types.Producer> {
const transport = this.#transports.get(transportId)
if (!transport)
throw new Error(`Transport not found: ${transportId}`)
const streamingBefore = this.streaming
const producer = await transport.produce({
kind,
rtpParameters,
appData: { ...appData, socketId: this.socketId },
})
this.#producers.set(producer.id, producer)
if (this.streaming !== streamingBefore)
this.emit('updated')
return producer
}
closeProducer(producerId: string): void {
const producer = this.#producers.get(producerId)
if (!producer)
throw new Error(`Producer not found: ${producerId}`)
const streamingBefore = this.streaming
producer.close()
this.#producers.delete(producerId)
if (this.streaming !== streamingBefore)
this.emit('updated')
}
async pauseProducer(producerId: string): Promise<void> {
const producer = this.#producers.get(producerId)
if (!producer)
throw new Error(`Producer not found: ${producerId}`)
if (!producer.paused)
await producer.pause()
}
async resumeProducer(producerId: string): Promise<void> {
const producer = this.#producers.get(producerId)
if (!producer)
throw new Error(`Producer not found: ${producerId}`)
await producer.resume()
}
async createConsumerFor(producer: types.Producer, producerSocketId: string): Promise<types.Consumer | null> {
const transport = Array.from(this.#transports.values()).find(t => t.appData.consuming)
if (!transport) {
consola.warn('[Client]', `[${this.socketId}]`, 'No consuming transport, skipping consumer creation')
return null
}
try {
const consumer = await transport.consume({
producerId: producer.id,
rtpCapabilities: this.#router.rtpCapabilities,
enableRtx: true,
paused: true,
ignoreDtx: true,
})
this.#consumers.set(consumer.id, consumer)
consumer.observer.on('close', () => {
this.#consumers.delete(consumer.id)
this.emit('consumer:closed', consumer.id)
})
consumer.on('transportclose', () => {
consumer.close()
})
consumer.on('producerclose', () => {
consumer.close()
})
consumer.on('producerpause', () => {
this.emit('consumer:paused', consumer.id)
})
consumer.on('producerresume', () => {
this.emit('consumer:resumed', consumer.id)
})
await new Promise<void>((resolve) => {
this.emit('signal:new-consumer', {
socketId: producerSocketId,
producerId: producer.id,
id: consumer.id,
kind: consumer.kind,
rtpParameters: consumer.rtpParameters,
type: consumer.type,
appData: producer.appData,
producerPaused: consumer.producerPaused,
}, async () => { resolve() })
})
await consumer.resume()
return consumer
}
catch (error) {
consola.error('[Client]', `[${this.socketId}]`, 'createConsumerFor() failed:', error)
return null
}
}
removeConsumersOf(producerId: string): void {
for (const consumer of this.#consumers.values()) {
if (consumer.producerId === producerId)
consumer.close()
}
}
clearConsumers(): void {
for (const consumer of this.#consumers.values()) {
consumer.close()
}
this.#consumers.clear()
}
async pauseConsumer(consumerId: string): Promise<void> {
const consumer = this.#consumers.get(consumerId)
if (!consumer)
throw new Error(`Consumer not found: ${consumerId}`)
await consumer.pause()
}
async resumeConsumer(consumerId: string): Promise<void> {
const consumer = this.#consumers.get(consumerId)
if (!consumer)
throw new Error(`Consumer not found: ${consumerId}`)
await consumer.resume()
}
update(patch: { inputMuted?: boolean, outputMuted?: boolean }): void {
if (typeof patch.inputMuted === 'boolean')
this.#inputMuted = patch.inputMuted
if (typeof patch.outputMuted === 'boolean')
this.#outputMuted = patch.outputMuted
this.emit('updated')
}
close(): void {
for (const transport of this.#transports.values()) {
transport.close()
}
this.emit('closed')
}
serialize(): SerializedClient {
return {
socketId: this.socketId,
userId: this.userId,
channelId: this.channelId,
inputMuted: this.#inputMuted,
outputMuted: this.#outputMuted,
streaming: this.streaming,
}
}
}

View File

@@ -0,0 +1,254 @@
import type { types } from 'mediasoup'
import type { ChadSocket, ChadSocketServer } from '../types.ts'
import type { Channel } from './Channel.ts'
import type { ChannelManager } from './ChannelManager.ts'
import type { Client } from './Client.ts'
import { consola } from 'consola'
export class WebRtcGateway {
readonly #io: ChadSocketServer
readonly #socket: ChadSocket
readonly #client: Client
readonly #channels: ChannelManager
constructor(
io: ChadSocketServer,
socket: ChadSocket,
client: Client,
channels: ChannelManager,
) {
this.#io = io
this.#socket = socket
this.#client = client
this.#channels = channels
this.register()
}
register(): void {
this.#client.on('signal:new-consumer', async (data, onAcked) => {
await this.#socket.emitWithAck('new-consumer', data)
await onAcked()
})
this.#client.on('consumer:closed', consumerId => this.#socket.emit('consumer-closed', { consumerId }))
this.#client.on('consumer:paused', consumerId => this.#socket.emit('consumer-paused', { consumerId }))
this.#client.on('consumer:resumed', consumerId => this.#socket.emit('consumer-resumed', { consumerId }))
this.#client.on('transport:closed', () => this.#socket.disconnect())
this.#client.on('updated', () => this.#io.emit('client-updated', this.#client.serialize()))
this.#socket.on('join-channel', this.#onJoinChannel.bind(this))
this.#socket.on('create-transport', this.#onCreateTransport.bind(this))
this.#socket.on('connect-transport', this.#onConnectTransport.bind(this))
this.#socket.on('produce', this.#onProduce.bind(this))
this.#socket.on('close-producer', this.#onCloseProducer.bind(this))
this.#socket.on('pause-producer', this.#onPauseProducer.bind(this))
this.#socket.on('resume-producer', this.#onResumeProducer.bind(this))
this.#socket.on('pause-consumer', this.#onPauseConsumer.bind(this))
this.#socket.on('resume-consumer', this.#onResumeConsumer.bind(this))
this.#socket.on('update-client', this.#onUpdateClient.bind(this))
this.#socket.on('disconnect', this.#onDisconnect.bind(this))
}
async #onJoinChannel({ channelId }: { channelId: string }): Promise<void> {
if (this.#client.channelId === channelId)
return
const newChannel = this.#channels.get(channelId)
if (!newChannel) {
consola.error('[Gateway]', `Channel not found: ${channelId}`)
return
}
const oldChannel = this.#channels.get(this.#client.channelId)
if (oldChannel)
this.#leaveChannel(oldChannel)
this.#client.clearConsumers()
this.#socket.join(newChannel.id)
newChannel.addClient(this.#client)
await newChannel.wireClient(this.#client)
this.#io.emit('client-switched-channel', this.#client.serialize())
}
async #onCreateTransport(
{ producing, consuming }: { producing: boolean, consuming: boolean },
cb: (result: any) => void,
): Promise<void> {
try {
const transportData = await this.#client.createTransport({ producing, consuming })
cb(transportData)
if (consuming) {
const channel = this.#channels.get(this.#client.channelId)
if (channel)
await channel.wireClient(this.#client)
}
}
catch (error) {
if (error instanceof Error) {
consola.error('[Gateway]', '[createTransport]', error.message)
cb({ error: error.message })
}
}
}
async #onConnectTransport(
{ transportId, dtlsParameters }: { transportId: string, dtlsParameters: types.DtlsParameters },
cb: (result: any) => void,
): Promise<void> {
try {
await this.#client.connectTransport(transportId, dtlsParameters)
cb({ ok: true })
}
catch (error) {
if (error instanceof Error) {
consola.error('[Gateway]', '[connectTransport]', error.message)
cb({ error: error.message })
}
}
}
async #onProduce(
{ transportId, kind, rtpParameters, appData }: {
transportId: string
kind: types.MediaKind
rtpParameters: types.RtpParameters
appData: { source: string }
},
cb: (result: any) => void,
): Promise<void> {
try {
const producer = await this.#client.produce(transportId, kind, rtpParameters, appData)
cb({ id: producer.id })
const channel = this.#channels.get(this.#client.channelId)
if (channel) {
for (const peer of channel.clients) {
if (peer.socketId !== this.#client.socketId)
await peer.createConsumerFor(producer, this.#client.socketId)
}
if (kind === 'audio')
await channel.addAudioProducer(producer)
}
}
catch (error) {
if (error instanceof Error) {
consola.error('[Gateway]', '[produce]', error.message)
cb({ error: error.message })
}
}
}
#onCloseProducer(
{ producerId }: { producerId: string },
cb: (result: any) => void,
): void {
try {
this.#client.closeProducer(producerId)
cb({ ok: true })
}
catch (error) {
if (error instanceof Error) {
consola.error('[Gateway]', '[closeProducer]', error.message)
cb({ error: error.message })
}
}
}
async #onPauseProducer(
{ producerId }: { producerId: string },
cb: (result: any) => void,
): Promise<void> {
try {
await this.#client.pauseProducer(producerId)
cb({ ok: true })
}
catch (error) {
if (error instanceof Error) {
consola.error('[Gateway]', '[pauseProducer]', error.message)
cb({ error: error.message })
}
}
}
async #onResumeProducer(
{ producerId }: { producerId: string },
cb: (result: any) => void,
): Promise<void> {
try {
await this.#client.resumeProducer(producerId)
cb({ ok: true })
}
catch (error) {
if (error instanceof Error) {
consola.error('[Gateway]', '[resumeProducer]', error.message)
cb({ error: error.message })
}
}
}
async #onPauseConsumer(
{ consumerId }: { consumerId: string },
cb: (result: any) => void,
): Promise<void> {
try {
await this.#client.pauseConsumer(consumerId)
cb({ ok: true })
}
catch (error) {
if (error instanceof Error) {
consola.error('[Gateway]', '[pauseConsumer]', error.message)
cb({ error: error.message })
}
}
}
async #onResumeConsumer(
{ consumerId }: { consumerId: string },
cb: (result: any) => void,
): Promise<void> {
try {
await this.#client.resumeConsumer(consumerId)
cb({ ok: true })
}
catch (error) {
if (error instanceof Error) {
consola.error('[Gateway]', '[resumeConsumer]', error.message)
cb({ error: error.message })
}
}
}
#onUpdateClient(
patch: { inputMuted?: boolean, outputMuted?: boolean },
cb: (result: any) => void,
): void {
this.#client.update(patch)
cb(this.#client.serialize())
}
#leaveChannel(channel: Channel): void {
channel.unwireClient(this.#client)
channel.kickClient(this.#client)
this.#socket.leave(channel.id)
}
#onDisconnect(): void {
consola.info('[Gateway]', 'Client disconnected:', this.#client.socketId)
this.#socket.broadcast.emit('client-disconnected', this.#client.socketId)
const channel = this.#channels.get(this.#client.channelId)
if (channel)
this.#leaveChannel(channel)
this.#client.close()
}
}

View File

@@ -0,0 +1,14 @@
import type { Channel } from '../../../prisma/generated-client/browser.ts'
import type { ChannelManager } from './ChannelManager.ts'
import type { Client } from './Client.ts'
export class MessagingService {
private channels: ChannelManager
constructor(channels: ChannelManager) {
this.channels = channels
}
joinChannel(client: Client, channel: Channel) {
}
}

View File

@@ -0,0 +1,96 @@
import type { FastifyInstance } from 'fastify'
import { consola } from 'consola'
import { Channel } from './Channel.ts'
import { ChannelManager } from './ChannelManager.ts'
import { Client } from './Client.ts'
import { WebRtcGateway } from './Gateway.ts'
export default async function (fastify: FastifyInstance) {
const { io, bus, mediasoupRouter, prisma } = fastify
const channels = new ChannelManager(mediasoupRouter)
const dbChannels = await prisma.channel.findMany()
for (const dbChannel of dbChannels) {
const channel = await Channel.create(dbChannel.id, dbChannel.persistent, mediasoupRouter)
channels.create(channel)
setupChannelEvents(channel)
}
const defaultChannel = channels.get('default')!
io.on('connection', async (socket) => {
consola.info('[WebRtc]', 'Client connected', socket.id)
const client = new Client(socket.id, socket.data.user.id, mediasoupRouter)
defaultChannel.addClient(client)
socket.join(defaultChannel.id)
const _gateway = new WebRtcGateway(io, socket, client, channels)
socket.emit('initialized', {
rtpCapabilities: mediasoupRouter.rtpCapabilities,
channelId: client.channelId,
clients: channels.all.flatMap(c => c.clients).map(c => c.serialize()),
})
socket.broadcast.emit('new-client', client.serialize())
})
bus.on('channel:created', async (dbChannel) => {
io.emit('channel-created', dbChannel)
const channel = await Channel.create(dbChannel.id, dbChannel.persistent, mediasoupRouter)
channels.create(channel)
setupChannelEvents(channel)
})
bus.on('channel:removed', async (dbChannel) => {
io.emit('channel-removed', dbChannel.id)
const channel = channels.get(dbChannel.id)
if (!channel)
return
for (const client of channel.clients) {
channel.unwireClient(client)
client.clearConsumers()
const socket = io.sockets.sockets.get(client.socketId)
if (socket) {
socket.leave(dbChannel.id)
defaultChannel.addClient(client)
socket.join(defaultChannel.id)
await defaultChannel.wireClient(client)
io.emit('client-switched-channel', client.serialize())
}
}
channels.delete(dbChannel.id)
})
function setupChannelEvents(channel: Channel): void {
channel.on('speaking-peers', peers => io.to(channel.id).emit('speaking-clients', peers))
channel.on('silence', () => {
io.to(channel.id).emit('speaking-clients', [])
io.to(channel.id).emit('active-speaker', undefined)
})
channel.on('active-speaker', socketId => io.to(channel.id).emit('active-speaker', socketId))
channel.on('empty', async () => {
if (channel.persistent)
return
channels.delete(channel.id)
await prisma.channel.delete({ where: { id: channel.id } })
consola.info('[WebRtc]', `Non-persistent channel "${channel.id}" deleted`)
io.emit('channel-removed', channel.id)
})
}
}

View File

@@ -1,4 +1,3 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
@@ -8,7 +7,7 @@
* 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.
* See `Client.ts` for the standard, server-side entry point.
*
* 🟢 You can import this file directly.
*/

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,3 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
@@ -8,7 +7,7 @@
*
* 🛑 Under no circumstances should you import this file directly! 🛑
*
* All exports from this file are wrapped under a `Prisma` namespace object in the client.ts file.
* All exports from this file are wrapped under a `Prisma` namespace object in the Client.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
@@ -1026,6 +1025,7 @@ export type MessageAttachmentScalarFieldEnum = (typeof MessageAttachmentScalarFi
export const ChannelScalarFieldEnum = {
id: 'id',
ownerId: 'ownerId',
name: 'name',
persistent: 'persistent'
} as const

View File

@@ -135,6 +135,7 @@ export type MessageAttachmentScalarFieldEnum = (typeof MessageAttachmentScalarFi
export const ChannelScalarFieldEnum = {
id: 'id',
ownerId: 'ownerId',
name: 'name',
persistent: 'persistent'
} as const

View File

@@ -26,18 +26,21 @@ export type AggregateChannel = {
export type ChannelMinAggregateOutputType = {
id: string | null
ownerId: string | null
name: string | null
persistent: boolean | null
}
export type ChannelMaxAggregateOutputType = {
id: string | null
ownerId: string | null
name: string | null
persistent: boolean | null
}
export type ChannelCountAggregateOutputType = {
id: number
ownerId: number
name: number
persistent: number
_all: number
@@ -46,18 +49,21 @@ export type ChannelCountAggregateOutputType = {
export type ChannelMinAggregateInputType = {
id?: true
ownerId?: true
name?: true
persistent?: true
}
export type ChannelMaxAggregateInputType = {
id?: true
ownerId?: true
name?: true
persistent?: true
}
export type ChannelCountAggregateInputType = {
id?: true
ownerId?: true
name?: true
persistent?: true
_all?: true
@@ -137,6 +143,7 @@ export type ChannelGroupByArgs<ExtArgs extends runtime.Types.Extensions.Internal
export type ChannelGroupByOutputType = {
id: string
ownerId: string | null
name: string
persistent: boolean
_count: ChannelCountAggregateOutputType | null
@@ -164,14 +171,18 @@ export type ChannelWhereInput = {
OR?: Prisma.ChannelWhereInput[]
NOT?: Prisma.ChannelWhereInput | Prisma.ChannelWhereInput[]
id?: Prisma.StringFilter<"Channel"> | string
ownerId?: Prisma.StringNullableFilter<"Channel"> | string | null
name?: Prisma.StringFilter<"Channel"> | string
persistent?: Prisma.BoolFilter<"Channel"> | boolean
owner?: Prisma.XOR<Prisma.UserNullableScalarRelationFilter, Prisma.UserWhereInput> | null
}
export type ChannelOrderByWithRelationInput = {
id?: Prisma.SortOrder
ownerId?: Prisma.SortOrderInput | Prisma.SortOrder
name?: Prisma.SortOrder
persistent?: Prisma.SortOrder
owner?: Prisma.UserOrderByWithRelationInput
}
export type ChannelWhereUniqueInput = Prisma.AtLeast<{
@@ -179,12 +190,15 @@ export type ChannelWhereUniqueInput = Prisma.AtLeast<{
AND?: Prisma.ChannelWhereInput | Prisma.ChannelWhereInput[]
OR?: Prisma.ChannelWhereInput[]
NOT?: Prisma.ChannelWhereInput | Prisma.ChannelWhereInput[]
ownerId?: Prisma.StringNullableFilter<"Channel"> | string | null
name?: Prisma.StringFilter<"Channel"> | string
persistent?: Prisma.BoolFilter<"Channel"> | boolean
owner?: Prisma.XOR<Prisma.UserNullableScalarRelationFilter, Prisma.UserWhereInput> | null
}, "id">
export type ChannelOrderByWithAggregationInput = {
id?: Prisma.SortOrder
ownerId?: Prisma.SortOrderInput | Prisma.SortOrder
name?: Prisma.SortOrder
persistent?: Prisma.SortOrder
_count?: Prisma.ChannelCountOrderByAggregateInput
@@ -197,6 +211,7 @@ export type ChannelScalarWhereWithAggregatesInput = {
OR?: Prisma.ChannelScalarWhereWithAggregatesInput[]
NOT?: Prisma.ChannelScalarWhereWithAggregatesInput | Prisma.ChannelScalarWhereWithAggregatesInput[]
id?: Prisma.StringWithAggregatesFilter<"Channel"> | string
ownerId?: Prisma.StringNullableWithAggregatesFilter<"Channel"> | string | null
name?: Prisma.StringWithAggregatesFilter<"Channel"> | string
persistent?: Prisma.BoolWithAggregatesFilter<"Channel"> | boolean
}
@@ -205,10 +220,12 @@ export type ChannelCreateInput = {
id?: string
name: string
persistent: boolean
owner?: Prisma.UserCreateNestedOneWithoutChannelsInput
}
export type ChannelUncheckedCreateInput = {
id?: string
ownerId?: string | null
name: string
persistent: boolean
}
@@ -217,16 +234,19 @@ export type ChannelUpdateInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
persistent?: Prisma.BoolFieldUpdateOperationsInput | boolean
owner?: Prisma.UserUpdateOneWithoutChannelsNestedInput
}
export type ChannelUncheckedUpdateInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
ownerId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
name?: Prisma.StringFieldUpdateOperationsInput | string
persistent?: Prisma.BoolFieldUpdateOperationsInput | boolean
}
export type ChannelCreateManyInput = {
id?: string
ownerId?: string | null
name: string
persistent: boolean
}
@@ -239,65 +259,211 @@ export type ChannelUpdateManyMutationInput = {
export type ChannelUncheckedUpdateManyInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
ownerId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
name?: Prisma.StringFieldUpdateOperationsInput | string
persistent?: Prisma.BoolFieldUpdateOperationsInput | boolean
}
export type ChannelListRelationFilter = {
every?: Prisma.ChannelWhereInput
some?: Prisma.ChannelWhereInput
none?: Prisma.ChannelWhereInput
}
export type ChannelOrderByRelationAggregateInput = {
_count?: Prisma.SortOrder
}
export type ChannelCountOrderByAggregateInput = {
id?: Prisma.SortOrder
ownerId?: Prisma.SortOrder
name?: Prisma.SortOrder
persistent?: Prisma.SortOrder
}
export type ChannelMaxOrderByAggregateInput = {
id?: Prisma.SortOrder
ownerId?: Prisma.SortOrder
name?: Prisma.SortOrder
persistent?: Prisma.SortOrder
}
export type ChannelMinOrderByAggregateInput = {
id?: Prisma.SortOrder
ownerId?: Prisma.SortOrder
name?: Prisma.SortOrder
persistent?: Prisma.SortOrder
}
export type ChannelCreateNestedManyWithoutOwnerInput = {
create?: Prisma.XOR<Prisma.ChannelCreateWithoutOwnerInput, Prisma.ChannelUncheckedCreateWithoutOwnerInput> | Prisma.ChannelCreateWithoutOwnerInput[] | Prisma.ChannelUncheckedCreateWithoutOwnerInput[]
connectOrCreate?: Prisma.ChannelCreateOrConnectWithoutOwnerInput | Prisma.ChannelCreateOrConnectWithoutOwnerInput[]
createMany?: Prisma.ChannelCreateManyOwnerInputEnvelope
connect?: Prisma.ChannelWhereUniqueInput | Prisma.ChannelWhereUniqueInput[]
}
export type ChannelUncheckedCreateNestedManyWithoutOwnerInput = {
create?: Prisma.XOR<Prisma.ChannelCreateWithoutOwnerInput, Prisma.ChannelUncheckedCreateWithoutOwnerInput> | Prisma.ChannelCreateWithoutOwnerInput[] | Prisma.ChannelUncheckedCreateWithoutOwnerInput[]
connectOrCreate?: Prisma.ChannelCreateOrConnectWithoutOwnerInput | Prisma.ChannelCreateOrConnectWithoutOwnerInput[]
createMany?: Prisma.ChannelCreateManyOwnerInputEnvelope
connect?: Prisma.ChannelWhereUniqueInput | Prisma.ChannelWhereUniqueInput[]
}
export type ChannelUpdateManyWithoutOwnerNestedInput = {
create?: Prisma.XOR<Prisma.ChannelCreateWithoutOwnerInput, Prisma.ChannelUncheckedCreateWithoutOwnerInput> | Prisma.ChannelCreateWithoutOwnerInput[] | Prisma.ChannelUncheckedCreateWithoutOwnerInput[]
connectOrCreate?: Prisma.ChannelCreateOrConnectWithoutOwnerInput | Prisma.ChannelCreateOrConnectWithoutOwnerInput[]
upsert?: Prisma.ChannelUpsertWithWhereUniqueWithoutOwnerInput | Prisma.ChannelUpsertWithWhereUniqueWithoutOwnerInput[]
createMany?: Prisma.ChannelCreateManyOwnerInputEnvelope
set?: Prisma.ChannelWhereUniqueInput | Prisma.ChannelWhereUniqueInput[]
disconnect?: Prisma.ChannelWhereUniqueInput | Prisma.ChannelWhereUniqueInput[]
delete?: Prisma.ChannelWhereUniqueInput | Prisma.ChannelWhereUniqueInput[]
connect?: Prisma.ChannelWhereUniqueInput | Prisma.ChannelWhereUniqueInput[]
update?: Prisma.ChannelUpdateWithWhereUniqueWithoutOwnerInput | Prisma.ChannelUpdateWithWhereUniqueWithoutOwnerInput[]
updateMany?: Prisma.ChannelUpdateManyWithWhereWithoutOwnerInput | Prisma.ChannelUpdateManyWithWhereWithoutOwnerInput[]
deleteMany?: Prisma.ChannelScalarWhereInput | Prisma.ChannelScalarWhereInput[]
}
export type ChannelUncheckedUpdateManyWithoutOwnerNestedInput = {
create?: Prisma.XOR<Prisma.ChannelCreateWithoutOwnerInput, Prisma.ChannelUncheckedCreateWithoutOwnerInput> | Prisma.ChannelCreateWithoutOwnerInput[] | Prisma.ChannelUncheckedCreateWithoutOwnerInput[]
connectOrCreate?: Prisma.ChannelCreateOrConnectWithoutOwnerInput | Prisma.ChannelCreateOrConnectWithoutOwnerInput[]
upsert?: Prisma.ChannelUpsertWithWhereUniqueWithoutOwnerInput | Prisma.ChannelUpsertWithWhereUniqueWithoutOwnerInput[]
createMany?: Prisma.ChannelCreateManyOwnerInputEnvelope
set?: Prisma.ChannelWhereUniqueInput | Prisma.ChannelWhereUniqueInput[]
disconnect?: Prisma.ChannelWhereUniqueInput | Prisma.ChannelWhereUniqueInput[]
delete?: Prisma.ChannelWhereUniqueInput | Prisma.ChannelWhereUniqueInput[]
connect?: Prisma.ChannelWhereUniqueInput | Prisma.ChannelWhereUniqueInput[]
update?: Prisma.ChannelUpdateWithWhereUniqueWithoutOwnerInput | Prisma.ChannelUpdateWithWhereUniqueWithoutOwnerInput[]
updateMany?: Prisma.ChannelUpdateManyWithWhereWithoutOwnerInput | Prisma.ChannelUpdateManyWithWhereWithoutOwnerInput[]
deleteMany?: Prisma.ChannelScalarWhereInput | Prisma.ChannelScalarWhereInput[]
}
export type BoolFieldUpdateOperationsInput = {
set?: boolean
}
export type ChannelCreateWithoutOwnerInput = {
id?: string
name: string
persistent: boolean
}
export type ChannelUncheckedCreateWithoutOwnerInput = {
id?: string
name: string
persistent: boolean
}
export type ChannelCreateOrConnectWithoutOwnerInput = {
where: Prisma.ChannelWhereUniqueInput
create: Prisma.XOR<Prisma.ChannelCreateWithoutOwnerInput, Prisma.ChannelUncheckedCreateWithoutOwnerInput>
}
export type ChannelCreateManyOwnerInputEnvelope = {
data: Prisma.ChannelCreateManyOwnerInput | Prisma.ChannelCreateManyOwnerInput[]
}
export type ChannelUpsertWithWhereUniqueWithoutOwnerInput = {
where: Prisma.ChannelWhereUniqueInput
update: Prisma.XOR<Prisma.ChannelUpdateWithoutOwnerInput, Prisma.ChannelUncheckedUpdateWithoutOwnerInput>
create: Prisma.XOR<Prisma.ChannelCreateWithoutOwnerInput, Prisma.ChannelUncheckedCreateWithoutOwnerInput>
}
export type ChannelUpdateWithWhereUniqueWithoutOwnerInput = {
where: Prisma.ChannelWhereUniqueInput
data: Prisma.XOR<Prisma.ChannelUpdateWithoutOwnerInput, Prisma.ChannelUncheckedUpdateWithoutOwnerInput>
}
export type ChannelUpdateManyWithWhereWithoutOwnerInput = {
where: Prisma.ChannelScalarWhereInput
data: Prisma.XOR<Prisma.ChannelUpdateManyMutationInput, Prisma.ChannelUncheckedUpdateManyWithoutOwnerInput>
}
export type ChannelScalarWhereInput = {
AND?: Prisma.ChannelScalarWhereInput | Prisma.ChannelScalarWhereInput[]
OR?: Prisma.ChannelScalarWhereInput[]
NOT?: Prisma.ChannelScalarWhereInput | Prisma.ChannelScalarWhereInput[]
id?: Prisma.StringFilter<"Channel"> | string
ownerId?: Prisma.StringNullableFilter<"Channel"> | string | null
name?: Prisma.StringFilter<"Channel"> | string
persistent?: Prisma.BoolFilter<"Channel"> | boolean
}
export type ChannelCreateManyOwnerInput = {
id?: string
name: string
persistent: boolean
}
export type ChannelUpdateWithoutOwnerInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
persistent?: Prisma.BoolFieldUpdateOperationsInput | boolean
}
export type ChannelUncheckedUpdateWithoutOwnerInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
persistent?: Prisma.BoolFieldUpdateOperationsInput | boolean
}
export type ChannelUncheckedUpdateManyWithoutOwnerInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
persistent?: Prisma.BoolFieldUpdateOperationsInput | boolean
}
export type ChannelSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
ownerId?: boolean
name?: boolean
persistent?: boolean
owner?: boolean | Prisma.Channel$ownerArgs<ExtArgs>
}, ExtArgs["result"]["channel"]>
export type ChannelSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
ownerId?: boolean
name?: boolean
persistent?: boolean
owner?: boolean | Prisma.Channel$ownerArgs<ExtArgs>
}, ExtArgs["result"]["channel"]>
export type ChannelSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
ownerId?: boolean
name?: boolean
persistent?: boolean
owner?: boolean | Prisma.Channel$ownerArgs<ExtArgs>
}, ExtArgs["result"]["channel"]>
export type ChannelSelectScalar = {
id?: boolean
ownerId?: boolean
name?: boolean
persistent?: boolean
}
export type ChannelOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "name" | "persistent", ExtArgs["result"]["channel"]>
export type ChannelOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "ownerId" | "name" | "persistent", ExtArgs["result"]["channel"]>
export type ChannelInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
owner?: boolean | Prisma.Channel$ownerArgs<ExtArgs>
}
export type ChannelIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
owner?: boolean | Prisma.Channel$ownerArgs<ExtArgs>
}
export type ChannelIncludeUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
owner?: boolean | Prisma.Channel$ownerArgs<ExtArgs>
}
export type $ChannelPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
name: "Channel"
objects: {}
objects: {
owner: Prisma.$UserPayload<ExtArgs> | null
}
scalars: runtime.Types.Extensions.GetPayloadResult<{
id: string
ownerId: string | null
name: string
persistent: boolean
}, ExtArgs["result"]["channel"]>
@@ -694,6 +860,7 @@ readonly fields: ChannelFieldRefs;
*/
export interface Prisma__ChannelClient<T, Null = never, ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs, GlobalOmitOptions = {}> extends Prisma.PrismaPromise<T> {
readonly [Symbol.toStringTag]: "PrismaPromise"
owner<T extends Prisma.Channel$ownerArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Channel$ownerArgs<ExtArgs>>): Prisma.Prisma__UserClient<runtime.Types.Result.GetResult<Prisma.$UserPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
/**
* Attaches callbacks for the resolution and/or rejection of the Promise.
* @param onfulfilled The callback to execute when the Promise is resolved.
@@ -724,6 +891,7 @@ export interface Prisma__ChannelClient<T, Null = never, ExtArgs extends runtime.
*/
export interface ChannelFieldRefs {
readonly id: Prisma.FieldRef<"Channel", 'String'>
readonly ownerId: Prisma.FieldRef<"Channel", 'String'>
readonly name: Prisma.FieldRef<"Channel", 'String'>
readonly persistent: Prisma.FieldRef<"Channel", 'Boolean'>
}
@@ -742,6 +910,10 @@ export type ChannelFindUniqueArgs<ExtArgs extends runtime.Types.Extensions.Inter
* Omit specific fields from the Channel
*/
omit?: Prisma.ChannelOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.ChannelInclude<ExtArgs> | null
/**
* Filter, which Channel to fetch.
*/
@@ -760,6 +932,10 @@ export type ChannelFindUniqueOrThrowArgs<ExtArgs extends runtime.Types.Extension
* Omit specific fields from the Channel
*/
omit?: Prisma.ChannelOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.ChannelInclude<ExtArgs> | null
/**
* Filter, which Channel to fetch.
*/
@@ -778,6 +954,10 @@ export type ChannelFindFirstArgs<ExtArgs extends runtime.Types.Extensions.Intern
* Omit specific fields from the Channel
*/
omit?: Prisma.ChannelOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.ChannelInclude<ExtArgs> | null
/**
* Filter, which Channel to fetch.
*/
@@ -826,6 +1006,10 @@ export type ChannelFindFirstOrThrowArgs<ExtArgs extends runtime.Types.Extensions
* Omit specific fields from the Channel
*/
omit?: Prisma.ChannelOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.ChannelInclude<ExtArgs> | null
/**
* Filter, which Channel to fetch.
*/
@@ -874,6 +1058,10 @@ export type ChannelFindManyArgs<ExtArgs extends runtime.Types.Extensions.Interna
* Omit specific fields from the Channel
*/
omit?: Prisma.ChannelOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.ChannelInclude<ExtArgs> | null
/**
* Filter, which Channels to fetch.
*/
@@ -922,6 +1110,10 @@ export type ChannelCreateArgs<ExtArgs extends runtime.Types.Extensions.InternalA
* Omit specific fields from the Channel
*/
omit?: Prisma.ChannelOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.ChannelInclude<ExtArgs> | null
/**
* The data needed to create a Channel.
*/
@@ -954,6 +1146,10 @@ export type ChannelCreateManyAndReturnArgs<ExtArgs extends runtime.Types.Extensi
* The data used to create many Channels.
*/
data: Prisma.ChannelCreateManyInput | Prisma.ChannelCreateManyInput[]
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.ChannelIncludeCreateManyAndReturn<ExtArgs> | null
}
/**
@@ -968,6 +1164,10 @@ export type ChannelUpdateArgs<ExtArgs extends runtime.Types.Extensions.InternalA
* Omit specific fields from the Channel
*/
omit?: Prisma.ChannelOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.ChannelInclude<ExtArgs> | null
/**
* The data needed to update a Channel.
*/
@@ -1020,6 +1220,10 @@ export type ChannelUpdateManyAndReturnArgs<ExtArgs extends runtime.Types.Extensi
* Limit how many Channels to update.
*/
limit?: number
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.ChannelIncludeUpdateManyAndReturn<ExtArgs> | null
}
/**
@@ -1034,6 +1238,10 @@ export type ChannelUpsertArgs<ExtArgs extends runtime.Types.Extensions.InternalA
* Omit specific fields from the Channel
*/
omit?: Prisma.ChannelOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.ChannelInclude<ExtArgs> | null
/**
* The filter to search for the Channel to update in case it exists.
*/
@@ -1060,6 +1268,10 @@ export type ChannelDeleteArgs<ExtArgs extends runtime.Types.Extensions.InternalA
* Omit specific fields from the Channel
*/
omit?: Prisma.ChannelOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.ChannelInclude<ExtArgs> | null
/**
* Filter which Channel to delete.
*/
@@ -1080,6 +1292,25 @@ export type ChannelDeleteManyArgs<ExtArgs extends runtime.Types.Extensions.Inter
limit?: number
}
/**
* Channel.owner
*/
export type Channel$ownerArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the User
*/
select?: Prisma.UserSelect<ExtArgs> | null
/**
* Omit specific fields from the User
*/
omit?: Prisma.UserOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.UserInclude<ExtArgs> | null
where?: Prisma.UserWhereInput
}
/**
* Channel without action
*/
@@ -1092,4 +1323,8 @@ export type ChannelDefaultArgs<ExtArgs extends runtime.Types.Extensions.Internal
* Omit specific fields from the Channel
*/
omit?: Prisma.ChannelOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.ChannelInclude<ExtArgs> | null
}

View File

@@ -193,6 +193,7 @@ export type UserWhereInput = {
Session?: Prisma.SessionListRelationFilter
UserPreferences?: Prisma.XOR<Prisma.UserPreferencesNullableScalarRelationFilter, Prisma.UserPreferencesWhereInput> | null
Messages?: Prisma.MessageListRelationFilter
Channels?: Prisma.ChannelListRelationFilter
}
export type UserOrderByWithRelationInput = {
@@ -205,6 +206,7 @@ export type UserOrderByWithRelationInput = {
Session?: Prisma.SessionOrderByRelationAggregateInput
UserPreferences?: Prisma.UserPreferencesOrderByWithRelationInput
Messages?: Prisma.MessageOrderByRelationAggregateInput
Channels?: Prisma.ChannelOrderByRelationAggregateInput
}
export type UserWhereUniqueInput = Prisma.AtLeast<{
@@ -220,6 +222,7 @@ export type UserWhereUniqueInput = Prisma.AtLeast<{
Session?: Prisma.SessionListRelationFilter
UserPreferences?: Prisma.XOR<Prisma.UserPreferencesNullableScalarRelationFilter, Prisma.UserPreferencesWhereInput> | null
Messages?: Prisma.MessageListRelationFilter
Channels?: Prisma.ChannelListRelationFilter
}, "id" | "username">
export type UserOrderByWithAggregationInput = {
@@ -256,6 +259,7 @@ export type UserCreateInput = {
Session?: Prisma.SessionCreateNestedManyWithoutUserInput
UserPreferences?: Prisma.UserPreferencesCreateNestedOneWithoutUserInput
Messages?: Prisma.MessageCreateNestedManyWithoutSenderInput
Channels?: Prisma.ChannelCreateNestedManyWithoutOwnerInput
}
export type UserUncheckedCreateInput = {
@@ -268,6 +272,7 @@ export type UserUncheckedCreateInput = {
Session?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput
UserPreferences?: Prisma.UserPreferencesUncheckedCreateNestedOneWithoutUserInput
Messages?: Prisma.MessageUncheckedCreateNestedManyWithoutSenderInput
Channels?: Prisma.ChannelUncheckedCreateNestedManyWithoutOwnerInput
}
export type UserUpdateInput = {
@@ -280,6 +285,7 @@ export type UserUpdateInput = {
Session?: Prisma.SessionUpdateManyWithoutUserNestedInput
UserPreferences?: Prisma.UserPreferencesUpdateOneWithoutUserNestedInput
Messages?: Prisma.MessageUpdateManyWithoutSenderNestedInput
Channels?: Prisma.ChannelUpdateManyWithoutOwnerNestedInput
}
export type UserUncheckedUpdateInput = {
@@ -292,6 +298,7 @@ export type UserUncheckedUpdateInput = {
Session?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput
UserPreferences?: Prisma.UserPreferencesUncheckedUpdateOneWithoutUserNestedInput
Messages?: Prisma.MessageUncheckedUpdateManyWithoutSenderNestedInput
Channels?: Prisma.ChannelUncheckedUpdateManyWithoutOwnerNestedInput
}
export type UserCreateManyInput = {
@@ -410,6 +417,22 @@ export type UserUpdateOneWithoutMessagesNestedInput = {
update?: Prisma.XOR<Prisma.XOR<Prisma.UserUpdateToOneWithWhereWithoutMessagesInput, Prisma.UserUpdateWithoutMessagesInput>, Prisma.UserUncheckedUpdateWithoutMessagesInput>
}
export type UserCreateNestedOneWithoutChannelsInput = {
create?: Prisma.XOR<Prisma.UserCreateWithoutChannelsInput, Prisma.UserUncheckedCreateWithoutChannelsInput>
connectOrCreate?: Prisma.UserCreateOrConnectWithoutChannelsInput
connect?: Prisma.UserWhereUniqueInput
}
export type UserUpdateOneWithoutChannelsNestedInput = {
create?: Prisma.XOR<Prisma.UserCreateWithoutChannelsInput, Prisma.UserUncheckedCreateWithoutChannelsInput>
connectOrCreate?: Prisma.UserCreateOrConnectWithoutChannelsInput
upsert?: Prisma.UserUpsertWithoutChannelsInput
disconnect?: Prisma.UserWhereInput | boolean
delete?: Prisma.UserWhereInput | boolean
connect?: Prisma.UserWhereUniqueInput
update?: Prisma.XOR<Prisma.XOR<Prisma.UserUpdateToOneWithWhereWithoutChannelsInput, Prisma.UserUpdateWithoutChannelsInput>, Prisma.UserUncheckedUpdateWithoutChannelsInput>
}
export type UserCreateWithoutSessionInput = {
id?: string
username: string
@@ -419,6 +442,7 @@ export type UserCreateWithoutSessionInput = {
updatedAt?: Date | string
UserPreferences?: Prisma.UserPreferencesCreateNestedOneWithoutUserInput
Messages?: Prisma.MessageCreateNestedManyWithoutSenderInput
Channels?: Prisma.ChannelCreateNestedManyWithoutOwnerInput
}
export type UserUncheckedCreateWithoutSessionInput = {
@@ -430,6 +454,7 @@ export type UserUncheckedCreateWithoutSessionInput = {
updatedAt?: Date | string
UserPreferences?: Prisma.UserPreferencesUncheckedCreateNestedOneWithoutUserInput
Messages?: Prisma.MessageUncheckedCreateNestedManyWithoutSenderInput
Channels?: Prisma.ChannelUncheckedCreateNestedManyWithoutOwnerInput
}
export type UserCreateOrConnectWithoutSessionInput = {
@@ -457,6 +482,7 @@ export type UserUpdateWithoutSessionInput = {
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
UserPreferences?: Prisma.UserPreferencesUpdateOneWithoutUserNestedInput
Messages?: Prisma.MessageUpdateManyWithoutSenderNestedInput
Channels?: Prisma.ChannelUpdateManyWithoutOwnerNestedInput
}
export type UserUncheckedUpdateWithoutSessionInput = {
@@ -468,6 +494,7 @@ export type UserUncheckedUpdateWithoutSessionInput = {
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
UserPreferences?: Prisma.UserPreferencesUncheckedUpdateOneWithoutUserNestedInput
Messages?: Prisma.MessageUncheckedUpdateManyWithoutSenderNestedInput
Channels?: Prisma.ChannelUncheckedUpdateManyWithoutOwnerNestedInput
}
export type UserCreateWithoutUserPreferencesInput = {
@@ -479,6 +506,7 @@ export type UserCreateWithoutUserPreferencesInput = {
updatedAt?: Date | string
Session?: Prisma.SessionCreateNestedManyWithoutUserInput
Messages?: Prisma.MessageCreateNestedManyWithoutSenderInput
Channels?: Prisma.ChannelCreateNestedManyWithoutOwnerInput
}
export type UserUncheckedCreateWithoutUserPreferencesInput = {
@@ -490,6 +518,7 @@ export type UserUncheckedCreateWithoutUserPreferencesInput = {
updatedAt?: Date | string
Session?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput
Messages?: Prisma.MessageUncheckedCreateNestedManyWithoutSenderInput
Channels?: Prisma.ChannelUncheckedCreateNestedManyWithoutOwnerInput
}
export type UserCreateOrConnectWithoutUserPreferencesInput = {
@@ -517,6 +546,7 @@ export type UserUpdateWithoutUserPreferencesInput = {
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
Session?: Prisma.SessionUpdateManyWithoutUserNestedInput
Messages?: Prisma.MessageUpdateManyWithoutSenderNestedInput
Channels?: Prisma.ChannelUpdateManyWithoutOwnerNestedInput
}
export type UserUncheckedUpdateWithoutUserPreferencesInput = {
@@ -528,6 +558,7 @@ export type UserUncheckedUpdateWithoutUserPreferencesInput = {
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
Session?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput
Messages?: Prisma.MessageUncheckedUpdateManyWithoutSenderNestedInput
Channels?: Prisma.ChannelUncheckedUpdateManyWithoutOwnerNestedInput
}
export type UserCreateWithoutMessagesInput = {
@@ -539,6 +570,7 @@ export type UserCreateWithoutMessagesInput = {
updatedAt?: Date | string
Session?: Prisma.SessionCreateNestedManyWithoutUserInput
UserPreferences?: Prisma.UserPreferencesCreateNestedOneWithoutUserInput
Channels?: Prisma.ChannelCreateNestedManyWithoutOwnerInput
}
export type UserUncheckedCreateWithoutMessagesInput = {
@@ -550,6 +582,7 @@ export type UserUncheckedCreateWithoutMessagesInput = {
updatedAt?: Date | string
Session?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput
UserPreferences?: Prisma.UserPreferencesUncheckedCreateNestedOneWithoutUserInput
Channels?: Prisma.ChannelUncheckedCreateNestedManyWithoutOwnerInput
}
export type UserCreateOrConnectWithoutMessagesInput = {
@@ -577,6 +610,7 @@ export type UserUpdateWithoutMessagesInput = {
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
Session?: Prisma.SessionUpdateManyWithoutUserNestedInput
UserPreferences?: Prisma.UserPreferencesUpdateOneWithoutUserNestedInput
Channels?: Prisma.ChannelUpdateManyWithoutOwnerNestedInput
}
export type UserUncheckedUpdateWithoutMessagesInput = {
@@ -588,6 +622,71 @@ export type UserUncheckedUpdateWithoutMessagesInput = {
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
Session?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput
UserPreferences?: Prisma.UserPreferencesUncheckedUpdateOneWithoutUserNestedInput
Channels?: Prisma.ChannelUncheckedUpdateManyWithoutOwnerNestedInput
}
export type UserCreateWithoutChannelsInput = {
id?: string
username: string
password: string
displayName: string
createdAt?: Date | string
updatedAt?: Date | string
Session?: Prisma.SessionCreateNestedManyWithoutUserInput
UserPreferences?: Prisma.UserPreferencesCreateNestedOneWithoutUserInput
Messages?: Prisma.MessageCreateNestedManyWithoutSenderInput
}
export type UserUncheckedCreateWithoutChannelsInput = {
id?: string
username: string
password: string
displayName: string
createdAt?: Date | string
updatedAt?: Date | string
Session?: Prisma.SessionUncheckedCreateNestedManyWithoutUserInput
UserPreferences?: Prisma.UserPreferencesUncheckedCreateNestedOneWithoutUserInput
Messages?: Prisma.MessageUncheckedCreateNestedManyWithoutSenderInput
}
export type UserCreateOrConnectWithoutChannelsInput = {
where: Prisma.UserWhereUniqueInput
create: Prisma.XOR<Prisma.UserCreateWithoutChannelsInput, Prisma.UserUncheckedCreateWithoutChannelsInput>
}
export type UserUpsertWithoutChannelsInput = {
update: Prisma.XOR<Prisma.UserUpdateWithoutChannelsInput, Prisma.UserUncheckedUpdateWithoutChannelsInput>
create: Prisma.XOR<Prisma.UserCreateWithoutChannelsInput, Prisma.UserUncheckedCreateWithoutChannelsInput>
where?: Prisma.UserWhereInput
}
export type UserUpdateToOneWithWhereWithoutChannelsInput = {
where?: Prisma.UserWhereInput
data: Prisma.XOR<Prisma.UserUpdateWithoutChannelsInput, Prisma.UserUncheckedUpdateWithoutChannelsInput>
}
export type UserUpdateWithoutChannelsInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
username?: Prisma.StringFieldUpdateOperationsInput | string
password?: Prisma.StringFieldUpdateOperationsInput | string
displayName?: Prisma.StringFieldUpdateOperationsInput | string
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
Session?: Prisma.SessionUpdateManyWithoutUserNestedInput
UserPreferences?: Prisma.UserPreferencesUpdateOneWithoutUserNestedInput
Messages?: Prisma.MessageUpdateManyWithoutSenderNestedInput
}
export type UserUncheckedUpdateWithoutChannelsInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
username?: Prisma.StringFieldUpdateOperationsInput | string
password?: Prisma.StringFieldUpdateOperationsInput | string
displayName?: Prisma.StringFieldUpdateOperationsInput | string
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
Session?: Prisma.SessionUncheckedUpdateManyWithoutUserNestedInput
UserPreferences?: Prisma.UserPreferencesUncheckedUpdateOneWithoutUserNestedInput
Messages?: Prisma.MessageUncheckedUpdateManyWithoutSenderNestedInput
}
@@ -598,11 +697,13 @@ export type UserUncheckedUpdateWithoutMessagesInput = {
export type UserCountOutputType = {
Session: number
Messages: number
Channels: number
}
export type UserCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
Session?: boolean | UserCountOutputTypeCountSessionArgs
Messages?: boolean | UserCountOutputTypeCountMessagesArgs
Channels?: boolean | UserCountOutputTypeCountChannelsArgs
}
/**
@@ -629,6 +730,13 @@ export type UserCountOutputTypeCountMessagesArgs<ExtArgs extends runtime.Types.E
where?: Prisma.MessageWhereInput
}
/**
* UserCountOutputType without action
*/
export type UserCountOutputTypeCountChannelsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
where?: Prisma.ChannelWhereInput
}
export type UserSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
@@ -640,6 +748,7 @@ export type UserSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = r
Session?: boolean | Prisma.User$SessionArgs<ExtArgs>
UserPreferences?: boolean | Prisma.User$UserPreferencesArgs<ExtArgs>
Messages?: boolean | Prisma.User$MessagesArgs<ExtArgs>
Channels?: boolean | Prisma.User$ChannelsArgs<ExtArgs>
_count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<ExtArgs>
}, ExtArgs["result"]["user"]>
@@ -675,6 +784,7 @@ export type UserInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs =
Session?: boolean | Prisma.User$SessionArgs<ExtArgs>
UserPreferences?: boolean | Prisma.User$UserPreferencesArgs<ExtArgs>
Messages?: boolean | Prisma.User$MessagesArgs<ExtArgs>
Channels?: boolean | Prisma.User$ChannelsArgs<ExtArgs>
_count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<ExtArgs>
}
export type UserIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {}
@@ -686,6 +796,7 @@ export type $UserPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs =
Session: Prisma.$SessionPayload<ExtArgs>[]
UserPreferences: Prisma.$UserPreferencesPayload<ExtArgs> | null
Messages: Prisma.$MessagePayload<ExtArgs>[]
Channels: Prisma.$ChannelPayload<ExtArgs>[]
}
scalars: runtime.Types.Extensions.GetPayloadResult<{
id: string
@@ -1091,6 +1202,7 @@ export interface Prisma__UserClient<T, Null = never, ExtArgs extends runtime.Typ
Session<T extends Prisma.User$SessionArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$SessionArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$SessionPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
UserPreferences<T extends Prisma.User$UserPreferencesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$UserPreferencesArgs<ExtArgs>>): Prisma.Prisma__UserPreferencesClient<runtime.Types.Result.GetResult<Prisma.$UserPreferencesPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
Messages<T extends Prisma.User$MessagesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$MessagesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$MessagePayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
Channels<T extends Prisma.User$ChannelsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$ChannelsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$ChannelPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
/**
* Attaches callbacks for the resolution and/or rejection of the Promise.
* @param onfulfilled The callback to execute when the Promise is resolved.
@@ -1583,6 +1695,30 @@ export type User$MessagesArgs<ExtArgs extends runtime.Types.Extensions.InternalA
distinct?: Prisma.MessageScalarFieldEnum | Prisma.MessageScalarFieldEnum[]
}
/**
* User.Channels
*/
export type User$ChannelsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the Channel
*/
select?: Prisma.ChannelSelect<ExtArgs> | null
/**
* Omit specific fields from the Channel
*/
omit?: Prisma.ChannelOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.ChannelInclude<ExtArgs> | null
where?: Prisma.ChannelWhereInput
orderBy?: Prisma.ChannelOrderByWithRelationInput | Prisma.ChannelOrderByWithRelationInput[]
cursor?: Prisma.ChannelWhereUniqueInput
take?: number
skip?: number
distinct?: Prisma.ChannelScalarFieldEnum | Prisma.ChannelScalarFieldEnum[]
}
/**
* User without action
*/

View File

@@ -0,0 +1,20 @@
/*
Warnings:
- Added the required column `ownerId` to the `Channel` table without a default value. This is not possible if the table is not empty.
*/
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Channel" (
"id" TEXT NOT NULL PRIMARY KEY,
"ownerId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"persistent" BOOLEAN NOT NULL
);
INSERT INTO "new_Channel" ("id", "name", "persistent") SELECT "id", "name", "persistent" FROM "Channel";
DROP TABLE "Channel";
ALTER TABLE "new_Channel" RENAME TO "Channel";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@@ -0,0 +1,15 @@
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Channel" (
"id" TEXT NOT NULL PRIMARY KEY,
"ownerId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"persistent" BOOLEAN NOT NULL,
CONSTRAINT "Channel_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
INSERT INTO "new_Channel" ("id", "name", "ownerId", "persistent") SELECT "id", "name", "ownerId", "persistent" FROM "Channel";
DROP TABLE "Channel";
ALTER TABLE "new_Channel" RENAME TO "Channel";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@@ -0,0 +1,15 @@
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Channel" (
"id" TEXT NOT NULL PRIMARY KEY,
"ownerId" TEXT,
"name" TEXT NOT NULL,
"persistent" BOOLEAN NOT NULL,
CONSTRAINT "Channel_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
INSERT INTO "new_Channel" ("id", "name", "ownerId", "persistent") SELECT "id", "name", "ownerId", "persistent" FROM "Channel";
DROP TABLE "Channel";
ALTER TABLE "new_Channel" RENAME TO "Channel";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@@ -18,6 +18,7 @@ model User {
Session Session[]
UserPreferences UserPreferences?
Messages Message[]
Channels Channel[]
}
model Session {
@@ -70,7 +71,10 @@ model MessageAttachment {
}
model Channel {
id String @id @default(uuid())
id String @id @default(uuid())
ownerId String?
name String
persistent Boolean
owner User? @relation(references: [id], fields: [ownerId], onDelete: Cascade)
}

View File

@@ -2,6 +2,8 @@ import type { FastifyPluginAsyncTypebox } from '@fastify/type-provider-typebox'
import * as fs from 'node:fs'
import * as path from 'node:path'
import { Type } from 'typebox'
import { GetAttachmentParamsSchema } from '../plugins/schemas/attachment.ts'
import { TypeboxRef } from '../utils/typebox-ref.ts'
const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
const uploadDir = path.join(process.cwd(), 'uploads')
@@ -18,6 +20,7 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
tags: ['Attachment'],
operationId: 'attachment.upload',
description: 'Pass file to multipart/form-data',
consumes: ['multipart/form-data'],
response: {
200: Type.String({ format: 'uuid', description: 'Attachment UUID' }),
},
@@ -62,9 +65,7 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
summary: 'Get attachment',
tags: ['Attachment'],
operationId: 'attachment.get',
params: Type.Object({
id: Type.String({ format: 'uuid' }),
}),
params: TypeboxRef(GetAttachmentParamsSchema),
response: {
200: Type.Any({ description: 'Attachment content' }),
},

View File

@@ -1,7 +1,7 @@
import type { FastifyPluginAsyncTypebox } from '@fastify/type-provider-typebox'
import bcrypt from 'bcrypt'
import { Type } from 'typebox'
import { CreateUserSchema, UserSchema } from '../schemas/auth.ts'
import { CreateUserPayloadSchema, LoginPayloadSchema, UserSchema } from '../plugins/schemas/auth.ts'
import { TypeboxRef } from '../utils/typebox-ref.ts'
const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
fastify.post(
@@ -11,9 +11,9 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
summary: 'Register',
tags: ['Auth'],
operationId: 'auth.register',
body: CreateUserSchema,
body: TypeboxRef(CreateUserPayloadSchema),
response: {
200: UserSchema,
200: TypeboxRef(UserSchema),
},
},
config: {
@@ -54,12 +54,9 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
summary: 'Login',
tags: ['Auth'],
operationId: 'auth.login',
body: Type.Object({
username: Type.String({ minLength: 1 }),
password: Type.String({ minLength: 1 }),
}),
body: TypeboxRef(LoginPayloadSchema),
response: {
200: UserSchema,
200: TypeboxRef(UserSchema),
},
},
config: {
@@ -107,7 +104,7 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
tags: ['Auth'],
operationId: 'auth.me',
response: {
200: UserSchema,
200: TypeboxRef(UserSchema),
},
},
},

90
server/routes/channel.ts Normal file
View File

@@ -0,0 +1,90 @@
import type { FastifyPluginAsyncTypebox } from '@fastify/type-provider-typebox'
import { Type } from 'typebox'
import { ChannelSchema, CreateChannelPayloadSchema } from '../plugins/schemas/channel.ts'
import { PrismaClientKnownRequestError } from '../prisma/generated-client/internal/prismaNamespace.ts'
import { TypeboxRef } from '../utils/typebox-ref.ts'
const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
fastify.get(
'/channels',
{
schema: {
summary: 'Get channel list',
tags: ['Channel'],
operationId: 'channel.list',
response: {
200: Type.Array(TypeboxRef(ChannelSchema)),
},
},
},
async () => {
return await fastify.prisma.channel.findMany()
},
)
fastify.post(
'/channels',
{
schema: {
summary: 'Create channel',
tags: ['Channel'],
operationId: 'channel.create',
body: TypeboxRef(CreateChannelPayloadSchema),
response: {
200: TypeboxRef(ChannelSchema),
},
},
},
async (req, reply) => {
const user = req.user!
const channel = await fastify.prisma.channel.create({
data: {
name: req.body.name,
ownerId: user.id,
persistent: req.body.persistent,
},
})
if (!channel) {
return reply.unprocessableEntity()
}
fastify.bus.emit('channel:created', channel)
return channel
},
)
fastify.delete(
'/channels/:id',
{
schema: {
summary: 'Delete channel',
tags: ['Channel'],
operationId: 'channel.delete',
params: Type.Object({
id: Type.String(),
}),
},
},
async (req, reply) => {
const user = req.user!
try {
const channel = await fastify.prisma.channel.delete({
where: { id: req.params.id, ownerId: user.id },
})
fastify.bus.emit('channel:removed', channel)
}
catch (e) {
if (e instanceof PrismaClientKnownRequestError && e.code === 'P2025') {
return reply.notFound()
}
}
},
)
}
export default plugin

View File

@@ -1,6 +1,6 @@
import type { FastifyPluginAsyncTypebox } from '@fastify/type-provider-typebox'
import { Type } from 'typebox'
import { ChatMessageSchema, NewChatMessageSchema } from '../schemas/chat.ts'
import { ChatMessageSchema, NewChatMessagePayloadSchema } from '../plugins/schemas/chat.ts'
const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
fastify.post(
@@ -10,7 +10,7 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
summary: 'Send message',
tags: ['Chat'],
operationId: 'chat.send',
body: NewChatMessageSchema,
body: NewChatMessagePayloadSchema,
response: {
200: ChatMessageSchema,
},

View File

@@ -1,7 +1,7 @@
import type { FastifyPluginAsyncTypebox } from '@fastify/type-provider-typebox'
import { Type } from 'typebox'
import { UserSchema } from '../schemas/auth.ts'
import { UpdateUserPreferencesSchema, UserPreferencesSchema } from '../schemas/user.ts'
import { UserSchema } from '../plugins/schemas/auth.ts'
import { GetUserQuerySchema, UpdateUserPayloadSchema, UpdateUserPreferencesPayloadSchema, UserPreferencesSchema } from '../plugins/schemas/user.ts'
import { TypeboxRef } from '../utils/typebox-ref.ts'
const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
fastify.get(
@@ -11,11 +11,9 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
summary: 'Get user',
tags: ['User'],
operationId: 'user.get',
querystring: Type.Partial(Type.Object({
username: Type.String(),
})),
querystring: TypeboxRef(GetUserQuerySchema),
response: {
200: UserSchema,
200: TypeboxRef(UserSchema),
},
},
},
@@ -49,11 +47,11 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
tags: ['User'],
operationId: 'user.getPreferences',
response: {
200: UserPreferencesSchema,
200: TypeboxRef(UserPreferencesSchema),
},
},
},
async (req, reply) => {
async (req) => {
const user = req.user!
const preferences = await fastify.prisma.userPreferences.upsert({
@@ -62,10 +60,6 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
update: {},
})
if (!preferences) {
return reply.notFound('User preferences not found')
}
return {
toggleInputHotkey: preferences.toggleInputHotkey || '',
toggleOutputHotkey: preferences.toggleOutputHotkey || '',
@@ -80,7 +74,7 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
summary: 'Update preferences',
tags: ['User'],
operationId: 'user.updatePreferences',
body: UpdateUserPreferencesSchema,
body: TypeboxRef(UpdateUserPreferencesPayloadSchema),
},
},
async (req) => {
@@ -104,11 +98,9 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
summary: 'Update profile',
tags: ['User'],
operationId: 'user.updateProfile',
body: Type.Object({
displayName: Type.String(),
}),
body: TypeboxRef(UpdateUserPayloadSchema),
response: {
200: UserSchema,
200: TypeboxRef(UserSchema),
},
},
},

View File

@@ -1,11 +0,0 @@
import { Type } from 'typebox'
export const UserPreferencesSchema = Type.Object({
toggleInputHotkey: Type.String(),
toggleOutputHotkey: Type.String(),
}, { title: 'UserPreferences', description: 'UserPreferences' })
export const UpdateUserPreferencesSchema = Type.Partial(
UserPreferencesSchema,
{ title: 'UpdateUserPreferences', description: 'UpdateUserPreferences' },
)

View File

@@ -10,7 +10,6 @@ import FastifySwagger from '@fastify/swagger'
import FastifyApiReference from '@scalar/fastify-api-reference'
import Fastify from 'fastify'
import { Prisma } from './prisma/generated-client/client.ts'
import { ErrorReplySchema } from './schemas/common.ts'
import 'dotenv/config'
const __filename = fileURLToPath(import.meta.url)
@@ -52,13 +51,30 @@ fastify.register(FastifySwagger, {
const transformedSchema: typeof schema = schema ?? {}
const responseSchema: any = transformedSchema.response ?? {}
responseSchema['4xx'] ??= ErrorReplySchema
responseSchema['5xx'] ??= ErrorReplySchema
responseSchema['4xx'] ??= { $ref: 'ResponseError' }
responseSchema['5xx'] ??= { $ref: 'ResponseError' }
transformedSchema.response = responseSchema
return { schema: transformedSchema, url }
},
refResolver: {
buildLocalReference(json, _baseUri, _fragment, i) {
if (!json.title && json.$id) {
json.title = json.$id
}
if (!json.description) {
json.description = json.title
}
if (!json.$id) {
return `def-${i}`
}
return json.$id.toString()
},
},
})
fastify.register(FastifyApiReference, {
@@ -68,10 +84,10 @@ fastify.register(FastifyApiReference, {
showDeveloperTools: 'never',
pageTitle: 'Chad API',
customCss: `
.scalar-mcp-layer,
.agent-button-container,
.t-doc__sidebar > div > button:last-child {
display: none !important;
.scalar-mcp-layer,
.agent-button-container,
.t-doc__sidebar > div > button:last-child {
display: none !important;
}
`,
},
@@ -93,6 +109,7 @@ fastify.register(FastifyMultipart)
fastify.register(FastifyAutoLoad, {
dir: join(__dirname, 'plugins'),
maxDepth: 1,
})
fastify.register(FastifyAutoLoad, {

View File

@@ -1,7 +0,0 @@
import type { Server as SocketServer } from 'socket.io'
export default async function (io: SocketServer) {
io.on('connection', async (socket) => {
})
}

View File

@@ -1,471 +0,0 @@
import type { types } from 'mediasoup'
import type { Server as SocketServer } from 'socket.io'
import type { PrismaClient } from '../prisma/generated-client/client.ts'
import type {
ChadClient,
SomeSocket,
} from '../types/webrtc.ts'
import { consola } from 'consola'
import { socketToClient } from '../utils/socket-to-client.ts'
export default async function (io: SocketServer, router: types.Router, prisma: PrismaClient) {
const audioLevelObserver = await router.createAudioLevelObserver({
maxEntries: 10,
threshold: -80,
interval: 800,
})
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) => {
consola.info('[WebRtc]', 'Client connected', socket.id)
socket.data.joined = false
socket.data.inputMuted = false
socket.data.outputMuted = false
socket.data.transports = new Map()
socket.data.producers = new Map()
socket.data.consumers = new Map()
const { id, username, displayName } = await prisma.user.findUnique({
where: {
id: socket.handshake.auth.userId,
},
select: {
id: true,
username: true,
displayName: true,
},
})
socket.data.userId = id
socket.data.username = username
socket.data.displayName = displayName
socket.emit('authenticated')
socket.on('join', async ({ rtpCapabilities }, cb) => {
if (socket.data.joined) {
consola.error('[WebRtc]', 'Already joined')
cb({ error: 'Already joined' })
}
socket.data.joined = true
socket.data.rtpCapabilities = rtpCapabilities
const joinedSockets = await getJoinedSockets()
cb(joinedSockets.map(socketToClient))
for (const joinedSocket of joinedSockets.filter(joinedSocket => joinedSocket.id !== socket.id)) {
for (const producer of joinedSocket.data.producers.values()) {
createConsumer(
socket,
joinedSocket,
producer,
)
}
}
socket.broadcast.emit('newPeer', socketToClient(socket))
})
socket.on('getRtpCapabilities', (cb) => {
cb(router.rtpCapabilities)
})
socket.on('createTransport', async ({ producing, consuming }, cb) => {
try {
const transport = await router.createWebRtcTransport({
listenInfos: [
{
protocol: 'udp',
ip: '0.0.0.0',
announcedAddress: process.env.ANNOUNCED_ADDRESS || '127.0.0.1',
portRange: {
min: 40000,
max: 40100,
},
},
],
enableUdp: true,
preferUdp: true,
appData: {
producing,
consuming,
},
})
socket.data.transports.set(transport.id, transport)
cb({
id: transport.id,
iceParameters: transport.iceParameters,
iceCandidates: transport.iceCandidates,
dtlsParameters: transport.dtlsParameters,
})
transport.on('icestatechange', (iceState: types.IceState) => {
if (iceState === 'disconnected' || iceState === 'closed') {
consola.info('[WebRtc]', '[WebRtcTransport]', `"icestatechange" event [iceState:${iceState}], closing peer`, transport.id)
socket.disconnect()
}
})
transport.on('dtlsstatechange', (dtlsState: types.DtlsState) => {
if (dtlsState === 'failed' || dtlsState === 'closed') {
consola.warn('WebRtcTransport "dtlsstatechange" event [dtlsState:%s], closing peer', dtlsState)
socket.disconnect()
}
})
}
catch (error) {
if (error instanceof Error) {
consola.error('[WebRtc]', '[createTransport]', error.message)
cb({ error: error.message })
}
}
})
socket.on('connectTransport', async ({ transportId, dtlsParameters }, cb) => {
const transport = socket.data.transports.get(transportId)
if (!transport) {
consola.error('[WebRtc]', '[connectTransport]', `Transport with id ${transportId} not found`)
cb({ error: 'Transport not found' })
return
}
try {
await transport.connect({ dtlsParameters })
cb({ ok: true })
}
catch (error) {
if (error instanceof Error) {
consola.error('[WebRtc]', '[connectTransport]', error.message)
cb({ error: error.message })
}
}
})
socket.on('produce', async ({ transportId, kind, rtpParameters, appData }, cb) => {
if (!socket.data.joined) {
consola.error('Peer not joined yet')
cb({ error: 'Peer not joined yet' })
return
}
const transport = socket.data.transports.get(transportId)
if (!transport) {
consola.error('[WebRtc]', '[produce]', `Transport with id ${transportId} not found`)
cb({ error: 'Transport not found' })
return
}
try {
const producer = await transport.produce({ kind, rtpParameters, appData: { ...appData, socketId: socket.id } })
socket.data.producers.set(producer.id, producer)
cb({ id: producer.id })
const otherSockets = await getJoinedSockets(socket.id)
for (const otherSocket of otherSockets) {
createConsumer(
otherSocket,
socket,
producer,
)
}
await audioLevelObserver.addProducer({ producerId: producer.id })
await activeSpeakerObserver.addProducer({ producerId: producer.id })
}
catch (error) {
if (error instanceof Error) {
consola.error('[WebRtc]', '[produce]', error.message)
cb({ error: error.message })
}
}
})
socket.on('closeProducer', async ({ producerId }, cb) => {
if (!socket.data.joined) {
consola.error('Peer not joined yet')
cb({ error: 'Peer not joined yet' })
return
}
const producer = socket.data.producers.get(producerId)
if (!producer) {
consola.error(`producer with id "${producerId}" not found`)
cb({ error: `producer with id "${producerId}" not found` })
return
}
producer.close()
socket.data.producers.delete(producerId)
cb({ ok: true })
})
socket.on('pauseProducer', async ({ producerId }, cb) => {
if (!socket.data.joined) {
consola.error('Peer not joined yet')
cb({ error: 'Peer not joined yet' })
return
}
const producer = socket.data.producers.get(producerId)
if (!producer) {
consola.error(`producer with id "${producerId}" not found`)
cb({ error: `producer with id "${producerId}" not found` })
return
}
if (producer.paused)
return
await producer.pause()
cb({ ok: true })
})
socket.on('resumeProducer', async ({ producerId }, cb) => {
if (!socket.data.joined) {
consola.error('Peer not joined yet')
cb({ error: 'Peer not joined yet' })
return
}
const producer = socket.data.producers.get(producerId)
if (!producer) {
consola.error(`producer with id "${producerId}" not found`)
cb({ error: `producer with id "${producerId}" not found` })
return
}
await producer.resume()
cb({ ok: true })
})
socket.on('pauseConsumer', async ({ consumerId }, cb) => {
if (!socket.data.joined) {
consola.error('Peer not joined yet')
cb({ error: 'Peer not joined yet' })
return
}
const consumer = socket.data.consumers.get(consumerId)
if (!consumer) {
consola.error(`consumer with id "${consumerId}" not found`)
cb({ error: `consumer with id "${consumerId}" not found` })
return
}
await consumer.pause()
cb({ ok: true })
})
socket.on('resumeConsumer', async ({ consumerId }, cb) => {
if (!socket.data.joined) {
consola.error('Peer not joined yet')
cb({ error: 'Peer not joined yet' })
return
}
const consumer = socket.data.consumers.get(consumerId)
if (!consumer) {
consola.error(`consumer with id "${consumerId}" not found`)
cb({ error: `consumer with id "${consumerId}" not found` })
return
}
await consumer.resume()
cb({ ok: true })
})
socket.on('updateClient', async (updatedClient, cb) => {
if (typeof updatedClient.inputMuted === 'boolean') {
socket.data.inputMuted = updatedClient.inputMuted
}
if (typeof updatedClient.outputMuted === 'boolean') {
socket.data.outputMuted = updatedClient.outputMuted
}
cb(socketToClient(socket))
io.emit('clientChanged', socket.id, socketToClient(socket))
})
socket.on('disconnect', () => {
consola.info('Client disconnected:', socket.id)
if (socket.data.joined) {
socket.broadcast.emit('peerClosed', socket.id)
}
for (const transport of socket.data.transports.values()) {
transport.close()
}
})
})
async function getJoinedSockets(excludeId?: string) {
const sockets = await io.fetchSockets()
return sockets.filter(socket => socket.data.joined && (excludeId ? excludeId !== socket.id : true))
}
async function createConsumer(
consumerSocket: SomeSocket,
producerSocket: SomeSocket,
producer: types.Producer,
) {
if (
!consumerSocket.data.rtpCapabilities
|| !router.canConsume(
{
producerId: producer.id,
rtpCapabilities: consumerSocket.data.rtpCapabilities,
},
)
) {
return
}
const transport = Array.from(consumerSocket.data.transports.values())
.find(t => t.appData.consuming)
if (!transport) {
consola.error('createConsumer() | Transport for consuming not found')
return
}
let consumer: types.Consumer
try {
consumer = await transport.consume(
{
producerId: producer.id,
rtpCapabilities: consumerSocket.data.rtpCapabilities,
// Enable NACK for OPUS.
enableRtx: true,
paused: true,
ignoreDtx: true,
},
)
}
catch (error) {
consola.error('_createConsumer() | transport.consume():%o', error)
return
}
consumerSocket.data.consumers.set(consumer.id, consumer)
consumer.on('transportclose', () => {
consumerSocket.data.consumers.delete(consumer.id)
})
consumer.on('producerclose', () => {
consumerSocket.data.consumers.delete(consumer.id)
consumerSocket.emit('consumerClosed', { consumerId: consumer.id })
})
consumer.on('producerpause', () => {
consumerSocket.emit('consumerPaused', { consumerId: consumer.id })
})
consumer.on('producerresume', () => {
consumerSocket.emit('consumerResumed', { consumerId: consumer.id })
})
consumer.on('score', (score: types.ConsumerScore) => {
consumerSocket.emit('consumerScore', { consumerId: consumer.id, score })
})
try {
await consumerSocket.emitWithAck(
'newConsumer',
{
socketId: producerSocket.id,
producerId: producer.id,
id: consumer.id,
kind: consumer.kind,
rtpParameters: consumer.rtpParameters,
type: consumer.type,
appData: producer.appData,
producerPaused: consumer.producerPaused,
},
)
await consumer.resume()
consumerSocket.emit(
'consumerScore',
{
consumerId: consumer.id,
score: consumer.score,
},
)
}
catch (error) {
consola.error('_createConsumer() | failed:%o', error)
}
}
}

View File

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

View File

@@ -1,18 +0,0 @@
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,142 +0,0 @@
import type { types } from 'mediasoup'
import type { RemoteSocket, Socket, Namespace as SocketNamespace } from 'socket.io'
import type { User } from '../prisma/client'
export interface ChadClient {
socketId: string
userId: User['id']
username: User['username']
displayName: User['displayName']
inputMuted: boolean
outputMuted: boolean
}
export interface ProducerShort {
producerId: types.Producer['id']
kind: types.MediaKind
}
export interface ErrorCallbackResult {
error: string
}
export interface SuccessCallbackResult {
ok: true
}
export type EventCallback<T = SuccessCallbackResult> = (result: T | ErrorCallbackResult) => void
export interface ClientToServerEvents {
join: (
options: {
rtpCapabilities: types.RtpCapabilities
},
cb: EventCallback<ChadClient[]>
) => void
getRtpCapabilities: (
cb: EventCallback<types.RtpCapabilities>
) => void
createTransport: (
options: {
producing: boolean
consuming: boolean
},
cb: EventCallback<Pick<types.WebRtcTransport, 'id' | 'iceParameters' | 'iceCandidates' | 'dtlsParameters'>>
) => void
connectTransport: (
options: {
transportId: types.WebRtcTransport['id']
dtlsParameters: types.WebRtcTransport['dtlsParameters']
},
cb: EventCallback
) => void
produce: (
options: {
transportId: types.WebRtcTransport['id']
kind: types.MediaKind
rtpParameters: types.RtpParameters
appData: { source: 'share' | string }
},
cb: EventCallback<{ id: types.Producer['id'] }>
) => void
closeProducer: (
options: {
producerId: types.Producer['id']
},
cb: EventCallback
) => void
pauseProducer: (
options: {
producerId: types.Producer['id']
},
cb: EventCallback
) => void
resumeProducer: (
options: {
producerId: types.Producer['id']
},
cb: EventCallback
) => void
pauseConsumer: (
options: {
consumerId: types.Consumer['id']
},
cb: EventCallback
) => void
resumeConsumer: (
options: {
consumerId: types.Consumer['id']
},
cb: EventCallback
) => void
updateClient: (
options: Partial<Pick<ChadClient, 'inputMuted' | 'outputMuted'>>,
cb: EventCallback<ChadClient>
) => void
}
export interface ServerToClientEvents {
authenticated: () => void
newPeer: (arg: ChadClient) => void
producers: (arg: ProducerShort[]) => void
newConsumer: (
arg: {
socketId: string
producerId: types.Producer['id']
id: types.Consumer['id']
kind: types.MediaKind
rtpParameters: types.RtpParameters
type: types.ConsumerType
appData: types.Producer['appData']
producerPaused: types.Consumer['producerPaused']
},
cb: EventCallback
) => void
peerClosed: (arg: string) => void
consumerClosed: (arg: { consumerId: string }) => void
consumerPaused: (arg: { consumerId: string }) => void
consumerResumed: (arg: { consumerId: string }) => void
consumerScore: (arg: { consumerId: string, score: types.ConsumerScore }) => void
clientChanged: (clientId: ChadClient['socketId'], client: ChadClient) => void
speakingPeers: (arg: { clientId: ChadClient['socketId'], volume: types.AudioLevelObserverVolume['volume'] }[]) => void
activeSpeaker: (clientId?: ChadClient['socketId']) => void
}
export interface InterServerEvent {}
export interface SocketData {
joined: boolean
userId: User['id']
username: User['username']
displayName: User['displayName']
inputMuted: boolean
outputMuted: boolean
rtpCapabilities: types.RtpCapabilities
transports: Map<types.WebRtcTransport['id'], types.WebRtcTransport>
producers: Map<types.Producer['id'], types.Producer>
consumers: Map<types.Consumer['id'], types.Consumer>
}
export type SomeSocket = Socket<ClientToServerEvents, ServerToClientEvents, InterServerEvent, SocketData> | RemoteSocket<ServerToClientEvents, SocketData>
export type Namespace = SocketNamespace<ClientToServerEvents, ServerToClientEvents, InterServerEvent, SocketData>

View File

@@ -1,12 +0,0 @@
import type { ChadClient, SomeSocket } from '../types/webrtc.ts'
export function socketToClient(socket: SomeSocket): ChadClient {
return {
socketId: socket.id,
userId: socket.data.userId,
username: socket.data.username,
displayName: socket.data.displayName,
inputMuted: socket.data.inputMuted,
outputMuted: socket.data.outputMuted,
}
}

View File

@@ -0,0 +1,12 @@
import type { Static, TSchema, TSchemaOptions, TUnsafe } from 'typebox'
import { Unsafe } from 'typebox'
export function TypeboxRef<T extends TSchema>(t: T, options: TSchemaOptions = {}): TUnsafe<Static<T>> {
const id = (t as unknown as Record<string, string | undefined>).$id
if (!id) {
throw new Error('missing ID on schema')
}
return Unsafe<Static<T>>({ ...t, $ref: id, $id: undefined, ...options })
}

View File

@@ -1172,13 +1172,6 @@ __metadata:
languageName: node
linkType: hard
"@types/ini@npm:^4.1.1":
version: 4.1.1
resolution: "@types/ini@npm:4.1.1"
checksum: 10c0/a060753a39f8bd73b615186018f7aded0eeb5698c0cb00e2f92ae495aa44b6351260e27f938891eeb304e28c2d42036bac5793a4e2031eff1df1a47de8cc8a97
languageName: node
linkType: hard
"@types/json-schema@npm:^7.0.15":
version: 7.0.15
resolution: "@types/json-schema@npm:7.0.15"
@@ -3042,7 +3035,7 @@ __metadata:
languageName: node
linkType: hard
"flatbuffers@npm:^25.2.10":
"flatbuffers@npm:^25.9.23":
version: 25.9.23
resolution: "flatbuffers@npm:25.9.23"
checksum: 10c0/957c4ae2a02be1703c98b36b4dc8ceb81613cf8e2333026afc95a7b68b088bed5458056dc29d0ab7ce8bc403b8c003732b0968d24aba46f5e7c8f71789a6bd9e
@@ -3269,12 +3262,12 @@ __metadata:
languageName: node
linkType: hard
"h264-profile-level-id@npm:^2.2.3":
version: 2.3.1
resolution: "h264-profile-level-id@npm:2.3.1"
"h264-profile-level-id@npm:^2.3.2":
version: 2.3.2
resolution: "h264-profile-level-id@npm:2.3.2"
dependencies:
debug: "npm:^4.4.3"
checksum: 10c0/c3459549bb28e456db62428c79885cffd4958ce282099c4181b09576f8e5ad90b42395a77209fff4f20a7cb920aaeb660f73902f08343daead0f5527faeb4015
checksum: 10c0/75bd12ff36707ffacf379c31c403d4508f3116ef2065e375deadcfafd4f7d163521cf0c70ae5385ebac970fa0acc07f9dd497c4248cfc1ee5623b4533707731d
languageName: node
linkType: hard
@@ -3423,13 +3416,6 @@ __metadata:
languageName: node
linkType: hard
"ini@npm:^5.0.0":
version: 5.0.0
resolution: "ini@npm:5.0.0"
checksum: 10c0/657491ce766cbb4b335ab221ee8f72b9654d9f0e35c32fe5ff2eb7ab8c5ce72237ff6456555b50cde88e6507a719a70e28e327b450782b4fc20c90326ec8c1a8
languageName: node
linkType: hard
"ini@npm:~1.3.0":
version: 1.3.8
resolution: "ini@npm:1.3.8"
@@ -3967,19 +3953,17 @@ __metadata:
languageName: node
linkType: hard
"mediasoup@npm:^3.19.3":
version: 3.19.3
resolution: "mediasoup@npm:3.19.3"
"mediasoup@npm:^3.19.21":
version: 3.19.21
resolution: "mediasoup@npm:3.19.21"
dependencies:
"@types/ini": "npm:^4.1.1"
debug: "npm:^4.4.3"
flatbuffers: "npm:^25.2.10"
h264-profile-level-id: "npm:^2.2.3"
ini: "npm:^5.0.0"
flatbuffers: "npm:^25.9.23"
h264-profile-level-id: "npm:^2.3.2"
node-fetch: "npm:^3.3.2"
supports-color: "npm:^10.2.2"
tar: "npm:^7.4.4"
checksum: 10c0/957ab3de4e7419eff3e76767511505f007fcd348b431acc2aa5ffcc2d9917cb83aa22f21f1f0004cb10ad65b46219b58fcf009b3b850719c09d5578aa6545f78
tar: "npm:^7.5.13"
checksum: 10c0/da37e478540002bae8350dda138eb9c54bb4b67f04c45386fd13205401e1bcd054043189664f35b03b466f8a4dc7df2d9b9a9fa022d0377d5602da4be6db4091
languageName: node
linkType: hard
@@ -5426,7 +5410,7 @@ __metadata:
fastify: "npm:^5.6.1"
fastify-plugin: "npm:^5.1.0"
lucia: "npm:^3.2.2"
mediasoup: "npm:^3.19.3"
mediasoup: "npm:^3.19.21"
nodemon: "npm:^3.1.14"
prisma: "npm:7"
socket.io: "npm:^4.8.1"
@@ -5800,7 +5784,7 @@ __metadata:
languageName: node
linkType: hard
"tar@npm:^7.4.3, tar@npm:^7.4.4":
"tar@npm:^7.4.3":
version: 7.5.1
resolution: "tar@npm:7.5.1"
dependencies:
@@ -5813,6 +5797,19 @@ __metadata:
languageName: node
linkType: hard
"tar@npm:^7.5.13":
version: 7.5.13
resolution: "tar@npm:7.5.13"
dependencies:
"@isaacs/fs-minipass": "npm:^4.0.0"
chownr: "npm:^3.0.0"
minipass: "npm:^7.1.2"
minizlib: "npm:^3.1.0"
yallist: "npm:^5.0.0"
checksum: 10c0/5c65b8084799bde7a791593a1c1a45d3d6ee98182e3700b24c247b7b8f8654df4191642abbdb07ff25043d45dcff35620827c3997b88ae6c12040f64bed5076b
languageName: node
linkType: hard
"thread-stream@npm:^3.0.0":
version: 3.1.0
resolution: "thread-stream@npm:3.1.0"