add stream
This commit is contained in:
parent
5d347acf88
commit
d0a409abf0
@ -8,6 +8,22 @@ datasource db {
|
||||
url = env("POSTGRES_URI")
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
@@map("stream")
|
||||
}
|
||||
|
||||
model SocialLink {
|
||||
id String @id @default(uuid())
|
||||
title String
|
||||
@ -37,6 +53,7 @@ model User {
|
||||
deactivatedAt DateTime? @map("deactivated_at")
|
||||
socialLink SocialLink[]
|
||||
totpSecret String? @map("totp_secret")
|
||||
stream Stream?
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@ import { VerificationModule } from '@/src/module/auth/verification/verification.
|
||||
import { CronModule } from '@/src/module/cron/cron.module'
|
||||
import { MailModule } from '@/src/module/libs/mail/mail.module'
|
||||
import { StorageModule } from '@/src/module/libs/storage/storage.module'
|
||||
import { StreamModule } from '@/src/module/stream/stream.module'
|
||||
import { IS_DEV } from '@/src/shared/util/is-dev.util'
|
||||
import { ApolloDriver } from '@nestjs/apollo'
|
||||
import { Module } from '@nestjs/common'
|
||||
@ -41,6 +42,7 @@ import { RedisModule } from './redis/redis.module'
|
||||
TotpModule,
|
||||
DeactivateModule,
|
||||
ProfileModule,
|
||||
StreamModule,
|
||||
],
|
||||
})
|
||||
export class CoreModule {}
|
||||
|
||||
@ -22,6 +22,11 @@ input ChangeProfileInfoInput {
|
||||
name: String!
|
||||
}
|
||||
|
||||
input ChangeStreamInfoInput {
|
||||
categoryId: String!
|
||||
title: String!
|
||||
}
|
||||
|
||||
input CreateUserInput {
|
||||
email: String!
|
||||
name: String!
|
||||
@ -50,6 +55,12 @@ input EnableTotpInput {
|
||||
secret: String!
|
||||
}
|
||||
|
||||
input FilterInput {
|
||||
searchTerm: String
|
||||
skip: Float
|
||||
take: Float
|
||||
}
|
||||
|
||||
type LocationModel {
|
||||
city: String!
|
||||
country: String!
|
||||
@ -68,6 +79,8 @@ type Mutation {
|
||||
changePassword(data: ChangePasswordInput!): UserModel!
|
||||
changeProfileAvatar(avatar: Upload!): Boolean!
|
||||
changeProfileInfo(data: ChangeProfileInfoInput!): UserModel!
|
||||
changeStreamInfo(data: ChangeStreamInfoInput!): StreamModel!
|
||||
changeStreamThumbnail(thumbnail: Upload!): Boolean!
|
||||
clearSessionCookie: Boolean!
|
||||
createSocialLink(data: SocialLinkInput!): SocialLinkModel!
|
||||
createUser(data: CreateUserInput!): UserModel!
|
||||
@ -79,11 +92,12 @@ type Mutation {
|
||||
removeProfileAvatar: Boolean!
|
||||
removeSession(id: String!): Boolean!
|
||||
removeSocialLink(id: String!): Boolean!
|
||||
removeStreamThumbnail: Boolean!
|
||||
reorderSocialLink(list: [SocialLinkOrderInput!]!): Boolean!
|
||||
resetPassword(data: ResetPasswordInput!): Boolean!
|
||||
setNewPassword(data: NewPasswordInput!): Boolean!
|
||||
updateSocialLink(data: SocialLinkInput!, id: String!): SocialLinkModel!
|
||||
verifyAccount(data: VerificationInput!): UserModel!
|
||||
verifyAccount(data: VerificationInput!): AuthModel!
|
||||
}
|
||||
|
||||
input NewPasswordInput {
|
||||
@ -93,8 +107,10 @@ input NewPasswordInput {
|
||||
}
|
||||
|
||||
type Query {
|
||||
findAllStreams(filters: FilterInput!): [StreamModel!]!
|
||||
findCurrentSession: SessionModel!
|
||||
findProfile: UserModel!
|
||||
findRandomStreams: [StreamModel!]!
|
||||
findSessionsByUser: [SessionModel!]!
|
||||
findSocialLinks: [SocialLinkModel!]!
|
||||
generateTotpSecret: TotpModel!
|
||||
@ -137,6 +153,20 @@ input SocialLinkOrderInput {
|
||||
position: Float!
|
||||
}
|
||||
|
||||
type StreamModel {
|
||||
createdAt: DateTime!
|
||||
id: ID!
|
||||
ingressId: String
|
||||
isLive: Boolean!
|
||||
key: String
|
||||
serverUrl: String
|
||||
thumbnailUrl: String
|
||||
title: String!
|
||||
updatedAt: DateTime!
|
||||
user: UserModel!
|
||||
userId: ID!
|
||||
}
|
||||
|
||||
type TotpModel {
|
||||
qrcodeUrl: String!
|
||||
secret: String!
|
||||
@ -160,6 +190,7 @@ type UserModel {
|
||||
name: String!
|
||||
password: String!
|
||||
socialLink: [SocialLinkModel!]!
|
||||
stream: StreamModel!
|
||||
totpSecret: String
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
|
||||
@ -51,6 +51,11 @@ export class AccountService {
|
||||
email,
|
||||
password: await hash(password),
|
||||
displayName: name,
|
||||
stream: {
|
||||
create: {
|
||||
title: `Stream by ${name}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { SocialLinkModel } from '@/src/module/auth/profile/inputs/models/social-link.model';
|
||||
import { SocialLinkModel } from '@/src/module/auth/profile/inputs/models/social-link.model'
|
||||
import { StreamModel } from '@/src/module/stream/models/stream.model'
|
||||
import { Field, ID, ObjectType } from '@nestjs/graphql'
|
||||
import { User } from '@prisma/generated'
|
||||
|
||||
@ -46,6 +47,9 @@ export class UserModel implements User {
|
||||
@Field(() => [SocialLinkModel])
|
||||
socialLink: SocialLinkModel[]
|
||||
|
||||
@Field(() => StreamModel)
|
||||
stream: StreamModel
|
||||
|
||||
@Field(() => Date)
|
||||
createdAt: Date
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { UserModel } from '@/src/module/auth/account/models/user.model'
|
||||
import { AuthModel } from '@/src/module/auth/account/models/auth.model'
|
||||
import { VerificationInput } from '@/src/module/auth/verification/inputs/verification.input'
|
||||
import { UserAgent } from '@/src/shared/decorators/user-agent.decorator'
|
||||
import { GqlContext } from '@/src/shared/types/gql-context.types'
|
||||
@ -9,7 +9,7 @@ import { VerificationService } from './verification.service'
|
||||
export class VerificationResolver {
|
||||
constructor(private readonly verificationService: VerificationService) {}
|
||||
|
||||
@Mutation(() => UserModel, { name: 'verifyAccount' })
|
||||
@Mutation(() => AuthModel, { name: 'verifyAccount' })
|
||||
public async verify(
|
||||
@Context() { req }: GqlContext,
|
||||
@Args('data') input: VerificationInput,
|
||||
|
||||
15
backend/src/module/stream/inputs/change-stream-info.input.ts
Normal file
15
backend/src/module/stream/inputs/change-stream-info.input.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { Field, InputType } from '@nestjs/graphql'
|
||||
import { IsNotEmpty, IsString } from 'class-validator'
|
||||
|
||||
@InputType()
|
||||
export class ChangeStreamInfoInput {
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
title: string
|
||||
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
categoryId: string
|
||||
}
|
||||
20
backend/src/module/stream/inputs/filter.input.ts
Normal file
20
backend/src/module/stream/inputs/filter.input.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { Field, InputType } from '@nestjs/graphql'
|
||||
import { IsNumber, IsOptional, IsString } from 'class-validator'
|
||||
|
||||
@InputType()
|
||||
export class FilterInput {
|
||||
@Field(() => Number, { nullable: true })
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
public take?: number
|
||||
|
||||
@Field(() => Number, { nullable: true })
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
public skip?: number
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
public searchTerm?: string
|
||||
}
|
||||
39
backend/src/module/stream/models/stream.model.ts
Normal file
39
backend/src/module/stream/models/stream.model.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { UserModel } from '@/src/module/auth/account/models/user.model'
|
||||
import { Field, ID, ObjectType } from '@nestjs/graphql'
|
||||
import { Stream } from '@prisma/generated'
|
||||
|
||||
@ObjectType()
|
||||
export class StreamModel implements Stream {
|
||||
@Field(() => ID)
|
||||
id: string
|
||||
|
||||
@Field(() => ID)
|
||||
userId: UserModel['id']
|
||||
|
||||
@Field(() => UserModel)
|
||||
user: UserModel
|
||||
|
||||
@Field(() => String)
|
||||
title: string
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
key: string
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
thumbnailUrl: string
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
serverUrl: string
|
||||
|
||||
@Field(() => Boolean)
|
||||
isLive: boolean
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
ingressId: string
|
||||
|
||||
@Field(() => Date)
|
||||
updatedAt: Date
|
||||
|
||||
@Field(() => Date)
|
||||
createdAt: Date
|
||||
}
|
||||
8
backend/src/module/stream/stream.module.ts
Normal file
8
backend/src/module/stream/stream.module.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { Module } from '@nestjs/common'
|
||||
import { StreamService } from './stream.service'
|
||||
import { StreamResolver } from './stream.resolver'
|
||||
|
||||
@Module({
|
||||
providers: [StreamResolver, StreamService],
|
||||
})
|
||||
export class StreamModule {}
|
||||
52
backend/src/module/stream/stream.resolver.ts
Normal file
52
backend/src/module/stream/stream.resolver.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { ChangeStreamInfoInput } from '@/src/module/stream/inputs/change-stream-info.input'
|
||||
import { FilterInput } from '@/src/module/stream/inputs/filter.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 { FileValidationPipe } from '@/src/shared/pipes/file-validation.pipe'
|
||||
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'
|
||||
import { User } from '@prisma/generated'
|
||||
import * as Upload from 'graphql-upload/Upload.js'
|
||||
import * as GraphQLUpload from 'graphql-upload/GraphQLUpload.js'
|
||||
import { StreamService } from './stream.service'
|
||||
|
||||
@Resolver('Stream')
|
||||
export class StreamResolver {
|
||||
constructor(private readonly streamService: StreamService) {}
|
||||
|
||||
@Query(() => [StreamModel], { name: 'findAllStreams' })
|
||||
public async findAll(
|
||||
@Args('filters') input: FilterInput,
|
||||
) {
|
||||
return this.streamService.findAll(input)
|
||||
}
|
||||
|
||||
@Query(() => [StreamModel], { name: 'findRandomStreams' })
|
||||
public async findRandomStreams() {
|
||||
return this.streamService.findRandom()
|
||||
}
|
||||
|
||||
@Authorization()
|
||||
@Mutation(() => StreamModel, { name: 'changeStreamInfo' })
|
||||
public async changeInfo(
|
||||
@Authorized() user: User,
|
||||
@Args('data') input: ChangeStreamInfoInput,
|
||||
) {
|
||||
return this.streamService.changeInfo(user, input)
|
||||
}
|
||||
|
||||
@Authorization()
|
||||
@Mutation(() => Boolean, { name: 'changeStreamThumbnail' })
|
||||
public async changeThumbnail(
|
||||
@Authorized() user: User,
|
||||
@Args('thumbnail', { type: () => GraphQLUpload }, FileValidationPipe) file: Upload,
|
||||
) {
|
||||
return this.streamService.changeThumbnail(user, file)
|
||||
}
|
||||
|
||||
@Authorization()
|
||||
@Mutation(() => Boolean, { name: 'removeStreamThumbnail' })
|
||||
public async removeAvatar(@Authorized() user: User) {
|
||||
return this.streamService.removeThumbnail(user)
|
||||
}
|
||||
}
|
||||
153
backend/src/module/stream/stream.service.ts
Normal file
153
backend/src/module/stream/stream.service.ts
Normal file
@ -0,0 +1,153 @@
|
||||
import { Prisma, User } from '@/prisma/generated'
|
||||
import { PrismaService } from '@/src/core/prisma/prisma.service'
|
||||
import { StorageService } from '@/src/module/libs/storage/storage.service'
|
||||
import { ChangeStreamInfoInput } from '@/src/module/stream/inputs/change-stream-info.input'
|
||||
import { FilterInput } from '@/src/module/stream/inputs/filter.input'
|
||||
import { Injectable, NotFoundException } from '@nestjs/common'
|
||||
import * as Upload from 'graphql-upload/Upload'
|
||||
import sharp from 'sharp'
|
||||
|
||||
@Injectable()
|
||||
export class StreamService {
|
||||
constructor(
|
||||
private readonly prismaService: PrismaService,
|
||||
private readonly storageService: StorageService,
|
||||
) {
|
||||
}
|
||||
|
||||
public async findAll(input: FilterInput = {}) {
|
||||
const { take, skip, searchTerm } = input
|
||||
const whereClause = searchTerm
|
||||
? this.findBySearchTermFilter(searchTerm)
|
||||
: undefined
|
||||
|
||||
return this.prismaService.stream.findMany({
|
||||
take: take ?? 12,
|
||||
skip: skip ?? 0,
|
||||
where: {
|
||||
user: { isDeactivated: false },
|
||||
...whereClause,
|
||||
},
|
||||
include: { user: true },
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
public async findRandom() {
|
||||
const total = await this.prismaService.stream.count({
|
||||
where: {
|
||||
user: {
|
||||
isDeactivated: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const randomIndexes = new Set<number>()
|
||||
while (randomIndexes.size < 2) {
|
||||
const randomIndex = Math.floor(Math.random() * total)
|
||||
randomIndexes.add(randomIndex)
|
||||
}
|
||||
|
||||
const streams = await this.prismaService.stream.findMany({
|
||||
where: {
|
||||
user: {
|
||||
isDeactivated: false,
|
||||
},
|
||||
},
|
||||
take: total,
|
||||
skip: 0,
|
||||
include: { user: true },
|
||||
})
|
||||
|
||||
return Array.from(randomIndexes).map(index => streams[index])
|
||||
}
|
||||
|
||||
public findBySearchTermFilter(searchTerm: NonNullable<FilterInput['searchTerm']>): Prisma.StreamWhereInput {
|
||||
return {
|
||||
OR: [
|
||||
{
|
||||
title: {
|
||||
contains: searchTerm,
|
||||
mode: 'insensitive',
|
||||
},
|
||||
},
|
||||
{
|
||||
user: {
|
||||
name: {
|
||||
contains: searchTerm,
|
||||
mode: 'insensitive',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
public async changeInfo(user: User, input: ChangeStreamInfoInput) {
|
||||
const { categoryId, title } = input
|
||||
return this.prismaService.stream.update({
|
||||
where: { userId: user.id },
|
||||
data: { title },
|
||||
})
|
||||
}
|
||||
|
||||
public async changeThumbnail(user: User, file: Upload) {
|
||||
const stream = await this.findStreamByUser(user)
|
||||
|
||||
if (stream.thumbnailUrl) {
|
||||
await this.storageService.remove(stream.thumbnailUrl)
|
||||
}
|
||||
|
||||
const chunks: Buffer[] = []
|
||||
|
||||
for await (const chunk of file.createReadStream()) {
|
||||
chunks.push(chunk)
|
||||
}
|
||||
|
||||
const buffer = Buffer.concat(chunks)
|
||||
const fileName = `/streams/${user.name}.webp`
|
||||
|
||||
const processedBuffer = await sharp(buffer, { animated: file.filename.endsWith('.gif') })
|
||||
.resize(1280, 720)
|
||||
.webp()
|
||||
.toBuffer()
|
||||
|
||||
await this.storageService.upload(processedBuffer, fileName, 'image/webp')
|
||||
|
||||
return this.prismaService.stream.update({
|
||||
where: { id: stream.id },
|
||||
data: { thumbnailUrl: fileName },
|
||||
})
|
||||
}
|
||||
|
||||
public async removeThumbnail(user: User) {
|
||||
const stream = await this.findStreamByUser(user)
|
||||
|
||||
if (!stream.thumbnailUrl) {
|
||||
return true
|
||||
}
|
||||
|
||||
await this.storageService.remove(stream.thumbnailUrl)
|
||||
|
||||
return this.prismaService.stream.update({
|
||||
where: { id: stream.id },
|
||||
data: { thumbnailUrl: null },
|
||||
})
|
||||
}
|
||||
|
||||
private async findStreamByUser(user: User) {
|
||||
const stream = await this.prismaService.stream.findUnique({
|
||||
where: {
|
||||
userId: user.id,
|
||||
},
|
||||
})
|
||||
|
||||
if (!stream) {
|
||||
throw new NotFoundException('Не найден такой стрим')
|
||||
}
|
||||
|
||||
return stream
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user