This commit is contained in:
Sergey Krylov 2025-07-14 06:11:54 +03:00
parent 22ab359c9e
commit 4d3428e56c
13 changed files with 282 additions and 26 deletions

View File

@ -48,6 +48,7 @@
"express-session": "^1.18.1",
"geoip-lite": "^1.4.10",
"graphql": "^16.11.0",
"graphql-subscriptions": "^3.0.0",
"graphql-upload": "14",
"hi-base32": "^0.5.1",
"i18n-iso-countries": "^7.14.0",

View File

@ -9,23 +9,40 @@ datasource db {
}
model Stream {
id String @id @default(uuid())
title String
thumbnailUrl String? @map("thumbnail_url")
ingressId String? @unique @map("ingress_id")
serverUrl String? @map("server_url")
key String? @map("key")
isLive Boolean @default(false) @map("is_live")
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
userId String? @unique @map("user_id")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
category Category? @relation(fields: [categoryId], references: [id], onDelete: Cascade)
categoryId String? @map("category_id")
id String @id @default(uuid())
title String
thumbnailUrl String? @map("thumbnail_url")
ingressId String? @unique @map("ingress_id")
serverUrl String? @map("server_url")
key String? @map("key")
isLive Boolean @default(false) @map("is_live")
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
userId String? @unique @map("user_id")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
category Category? @relation(fields: [categoryId], references: [id], onDelete: Cascade)
categoryId String? @map("category_id")
chatMessages ChatMessage[]
isChatEnable Boolean @default(true) @map("is_chat_enable")
isChatFollowersOnly Boolean @default(false) @map("is_chat_followers_onlys")
isChatPremiumFollowersOnly Boolean @default(false) @map("is_chat_premium_followers_onlys")
@@map("stream")
}
model ChatMessage {
id String @id @default(uuid())
text String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId String @map("user_id")
stream Stream @relation(fields: [streamId], references: [id], onDelete: Cascade)
streamId String @map("stream_id")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("chat_messages ")
}
model SocialLink {
id String @id @default(uuid())
title String
@ -40,24 +57,25 @@ model SocialLink {
}
model User {
id String @id @default(uuid())
email String @unique
id String @id @default(uuid())
email String @unique
password String
name String @unique
displayName String @map("display_name")
name String @unique
displayName String @map("display_name")
avatar String?
bio String?
token Token[]
isVerified Boolean @default(false) @map("is_verified")
isEmailVerified Boolean @default(false) @map("is_email_verified")
isTotpEnabled Boolean @default(false) @map("is_totp_enabled")
isDeactivated Boolean @default(false) @map("is_deactivated")
deactivatedAt DateTime? @map("deactivated_at")
isVerified Boolean @default(false) @map("is_verified")
isEmailVerified Boolean @default(false) @map("is_email_verified")
isTotpEnabled Boolean @default(false) @map("is_totp_enabled")
isDeactivated Boolean @default(false) @map("is_deactivated")
deactivatedAt DateTime? @map("deactivated_at")
socialLink SocialLink[]
totpSecret String? @map("totp_secret")
totpSecret String? @map("totp_secret")
stream Stream?
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
chatMessages ChatMessage[]
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("users")
}

View File

@ -16,5 +16,6 @@ export function getGraphQLConfig(configService: ConfigService): ApolloDriverConf
autoSchemaFile: path.join(process.cwd(), 'src', 'core', 'graphql', 'schema.gql'),
sortSchema: true,
context: ({ req, res }: ContextType) => ({ req, res }),
installSubscriptionHandlers: true,
}
}

View File

@ -8,6 +8,7 @@ import { SessionModule } from '@/src/module/auth/session/session.module'
import { TotpModule } from '@/src/module/auth/totp/totp.module'
import { VerificationModule } from '@/src/module/auth/verification/verification.module'
import { CategoryModule } from '@/src/module/category/category.module'
import { ChatModule } from '@/src/module/chat/chat.module'
import { CronModule } from '@/src/module/cron/cron.module'
import { LiveKitModule } from '@/src/module/libs/livekit/livekit.module'
import { MailModule } from '@/src/module/libs/mail/mail.module'
@ -56,6 +57,7 @@ import { RedisModule } from './redis/redis.module'
IngressModule,
WebhookModule,
CategoryModule,
ChatModule,
],
})
export class CoreModule {}

View File

@ -18,6 +18,12 @@ type CategoryModel {
updatedAt: DateTime!
}
input ChangeChatSettingsInput {
isChatEnable: Boolean!
isChatFollowersOnly: Boolean!
isChatPremiumFollowersOnly: Boolean!
}
input ChangeEmailInput {
email: String!
}
@ -38,6 +44,15 @@ input ChangeStreamInfoInput {
title: String!
}
type ChatMessageModel {
createdAt: DateTime!
id: ID!
streamId: ID!
text: String!
updatedAt: DateTime!
userId: ID!
}
input CreateUserInput {
email: String!
name: String!
@ -95,6 +110,7 @@ input LoginInput {
}
type Mutation {
changeChatSettings(data: ChangeChatSettingsInput!): StreamModel!
changeEmail(data: ChangeEmailInput!): UserModel!
changePassword(data: ChangePasswordInput!): UserModel!
changeProfileAvatar(avatar: Upload!): Boolean!
@ -117,6 +133,7 @@ type Mutation {
removeStreamThumbnail: Boolean!
reorderSocialLink(list: [SocialLinkOrderInput!]!): Boolean!
resetPassword(data: ResetPasswordInput!): Boolean!
sendChatMessage(data: SendMessageInput!): ChatMessageModel!
setNewPassword(data: NewPasswordInput!): Boolean!
updateSocialLink(data: SocialLinkInput!, id: String!): SocialLinkModel!
verifyAccount(data: VerificationInput!): AuthModel!
@ -133,6 +150,7 @@ type Query {
findAllStreams(filters: FilterInput!): [StreamModel!]!
findCategoryBySlug(slug: String!): CategoryModel!
findCurrentSession: SessionModel!
findMessagesByStream(streamId: String!): [ChatMessageModel!]!
findProfile: UserModel!
findRandomCategories: [CategoryModel!]!
findRandomStreams: [StreamModel!]!
@ -145,6 +163,11 @@ input ResetPasswordInput {
email: String!
}
input SendMessageInput {
streamId: String!
text: String!
}
type SessionMetadataModel {
device: DeviceModel!
ip: String!
@ -181,9 +204,13 @@ input SocialLinkOrderInput {
type StreamModel {
category: CategoryModel!
categoryId: ID!
chatMessages: [ChatMessageModel!]!
createdAt: DateTime!
id: ID!
ingressId: String
isChatEnable: Boolean!
isChatFollowersOnly: Boolean!
isChatPremiumFollowersOnly: Boolean!
isLive: Boolean!
key: String
serverUrl: String
@ -194,6 +221,10 @@ type StreamModel {
userId: ID!
}
type Subscription {
chatMessageAdded(streamId: String!): ChatMessageModel!
}
type TotpModel {
qrcodeUrl: String!
secret: String!

View File

@ -0,0 +1,8 @@
import { Module } from '@nestjs/common'
import { ChatService } from './chat.service'
import { ChatResolver } from './chat.resolver'
@Module({
providers: [ChatResolver, ChatService],
})
export class ChatModule {}

View File

@ -0,0 +1,49 @@
import { ChangeChatSettingsInput } from '@/src/module/chat/input/change-chat-settings.input'
import { SendMessageInput } from '@/src/module/chat/input/send-message.input'
import { StreamModel } from '@/src/module/stream/models/stream.model'
import { Authorization } from '@/src/shared/decorators/auth.decorator'
import { Authorized } from '@/src/shared/decorators/authorized.decorator'
import { Args, Mutation, Query, Resolver, Subscription } from '@nestjs/graphql'
import { ChatMessage, User } from '@prisma/generated'
import { PubSub } from 'graphql-subscriptions'
import { ChatService } from './chat.service'
import { ChatMessageModel } from './models/chat-message.model'
@Resolver('Chat')
export class ChatResolver {
public pubSub: PubSub
constructor(private readonly chatService: ChatService) {
this.pubSub = new PubSub()
}
@Query(() => [ChatMessageModel], { name: 'findMessagesByStream' })
public async findMessagesByStream(@Args('streamId') streamId: string) {
return this.chatService.findMessagesByStream(streamId)
}
@Authorization()
@Mutation(() => ChatMessageModel, { name: 'sendChatMessage' })
public async sendMessage(
@Authorized('id') userId: User['id'],
@Args('data') input: SendMessageInput,
) {
const message = this.chatService.sendMessage(userId, input)
void this.pubSub.publish('CHAT_MESSAGE_ADDED', { message })
return message
}
@Subscription(() => ChatMessageModel, { name: 'chatMessageAdded', filter: (payload: { message: ChatMessage }, variables: ChatMessageModel) => payload.message.streamId === variables.streamId })
public async chatMessageAdded(@Args('streamId') streamId: string) {
return this.pubSub.asyncIterableIterator('CHAT_MESSAGE_ADDED')
}
@Authorization()
@Mutation(() => StreamModel, { name: 'changeChatSettings' })
public async changeSettings(
@Args('data') input: ChangeChatSettingsInput,
@Authorized() user: User,
) {
return this.chatService.changeSettings(user, input)
}
}

View File

@ -0,0 +1,70 @@
import { PrismaService } from '@/src/core/prisma/prisma.service'
import { ChangeChatSettingsInput } from '@/src/module/chat/input/change-chat-settings.input'
import { SendMessageInput } from '@/src/module/chat/input/send-message.input'
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'
import { Stream, User } from '@prisma/generated'
@Injectable()
export class ChatService {
constructor(
public readonly prismaService: PrismaService,
) {
}
public async findMessagesByStream(streamId: Stream['id']) {
return this.prismaService.chatMessage.findMany({
where: { streamId },
orderBy: { createdAt: 'desc' },
include: { user: true },
})
}
public async sendMessage(userId: User['id'], input: SendMessageInput) {
const { text, streamId } = input
const stream = await this.prismaService.stream.findUnique({
where: { id: streamId },
})
if (!stream) {
throw new NotFoundException('Стрим не найден')
}
if (!stream.isLive) {
throw new BadRequestException('Стрим не запущен')
}
const user = await this.prismaService.user.findUnique({
where: { id: userId },
})
if (!user) {
throw new NotFoundException('Пользователь не найден')
}
return this.prismaService.chatMessage.create({
data: {
user: {
connect: {
id: user.id,
},
},
stream: {
connect: {
id: stream.id,
},
},
text,
},
include: {
stream: true,
},
})
}
public async changeSettings(user: User, input: ChangeChatSettingsInput) {
const { isChatEnable, isChatFollowersOnly, isChatPremiumFollowersOnly } = input
return this.prismaService.stream.update({
where: { userId: user.id },
data: { isChatEnable, isChatFollowersOnly, isChatPremiumFollowersOnly },
})
}
}

View File

@ -0,0 +1,17 @@
import { Field, InputType } from '@nestjs/graphql'
import { IsBoolean } from 'class-validator'
@InputType()
export class ChangeChatSettingsInput {
@Field(() => Boolean)
@IsBoolean()
public isChatEnable: boolean
@Field(() => Boolean)
@IsBoolean()
public isChatFollowersOnly: boolean
@Field(() => Boolean)
@IsBoolean()
public isChatPremiumFollowersOnly: boolean
}

View File

@ -0,0 +1,16 @@
import { Field, InputType } from '@nestjs/graphql'
import { Stream } from '@prisma/generated'
import { IsNotEmpty, IsString } from 'class-validator'
@InputType()
export class SendMessageInput {
@Field(() => String)
@IsString()
@IsNotEmpty()
public text: string
@Field(() => String)
@IsString()
@IsNotEmpty()
streamId: Stream['id']
}

View File

@ -0,0 +1,25 @@
import { UserModel } from '@/src/module/auth/account/models/user.model'
import { StreamModel } from '@/src/module/stream/models/stream.model'
import { Field, ID, ObjectType } from '@nestjs/graphql'
import { ChatMessage } from '@prisma/generated'
@ObjectType()
export class ChatMessageModel implements ChatMessage {
@Field(() => ID)
public id: string
@Field(() => String)
text: string
@Field(() => ID)
streamId: StreamModel['id']
@Field(() => ID)
userId: UserModel['id']
@Field(() => Date)
public createdAt: Date
@Field(() => Date)
public updatedAt: Date
}

View File

@ -1,5 +1,6 @@
import { UserModel } from '@/src/module/auth/account/models/user.model'
import { CategoryModel } from '@/src/module/category/models/category.model';
import { CategoryModel } from '@/src/module/category/models/category.model'
import { ChatMessageModel } from '@/src/module/chat/models/chat-message.model';
import { Field, ID, ObjectType } from '@nestjs/graphql'
import { Stream } from '@prisma/generated'
@ -38,6 +39,18 @@ export class StreamModel implements Stream {
@Field(() => String, { nullable: true })
ingressId: string
@Field(() => Boolean)
isChatEnable: boolean
@Field(() => Boolean)
isChatPremiumFollowersOnly: boolean
@Field(() => Boolean)
isChatFollowersOnly: boolean
@Field(() => [ChatMessageModel])
chatMessages: ChatMessageModel[]
@Field(() => Date)
updatedAt: Date

View File

@ -6013,6 +6013,11 @@ graphemer@^1.4.0:
resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6"
integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==
graphql-subscriptions@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/graphql-subscriptions/-/graphql-subscriptions-3.0.0.tgz#820c846ef271414c08f64827b5c9a192801e1b6f"
integrity sha512-kZCdevgmzDjGAOqH7GlDmQXYAkuHoKpMlJrqF40HMPhUhM5ZWSFSxCwD/nSi6AkaijmMfsFhoJRGJ27UseCvRA==
graphql-tag@2.12.6:
version "2.12.6"
resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.12.6.tgz#d441a569c1d2537ef10ca3d1633b48725329b5f1"