add chat
This commit is contained in:
parent
22ab359c9e
commit
4d3428e56c
@ -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",
|
||||
|
||||
@ -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")
|
||||
}
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {}
|
||||
|
||||
@ -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!
|
||||
|
||||
8
backend/src/module/chat/chat.module.ts
Normal file
8
backend/src/module/chat/chat.module.ts
Normal 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 {}
|
||||
49
backend/src/module/chat/chat.resolver.ts
Normal file
49
backend/src/module/chat/chat.resolver.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
70
backend/src/module/chat/chat.service.ts
Normal file
70
backend/src/module/chat/chat.service.ts
Normal 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 },
|
||||
})
|
||||
}
|
||||
}
|
||||
17
backend/src/module/chat/input/change-chat-settings.input.ts
Normal file
17
backend/src/module/chat/input/change-chat-settings.input.ts
Normal 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
|
||||
}
|
||||
16
backend/src/module/chat/input/send-message.input.ts
Normal file
16
backend/src/module/chat/input/send-message.input.ts
Normal 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']
|
||||
}
|
||||
25
backend/src/module/chat/models/chat-message.model.ts
Normal file
25
backend/src/module/chat/models/chat-message.model.ts
Normal 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
|
||||
}
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user