Prechádzať zdrojové kódy

Add channel reports (#39)

Bartosz Dryl 2 rokov pred
rodič
commit
bc568a95ff

+ 35 - 11
schema.graphql

@@ -17,6 +17,26 @@ type ChannelFollowsInfo {
   id: ID!
 }
 
+type ChannelReportInfo {
+  channelId: ID!
+  createdAt: DateTime!
+  id: ID!
+  rationale: String!
+  reporterIp: String!
+}
+
+enum ChannelReportOrderByInput {
+  createdAt_ASC
+  createdAt_DESC
+}
+
+input ChannelReportsWhereInput {
+  channelId: ID
+  createdAt_gt: DateTime
+  createdAt_lt: DateTime
+  reporterIp: String
+}
+
 """
 The javascript `Date` as string. Type represents date and time as the ISO Date string.
 """
@@ -44,8 +64,11 @@ type Mutation {
   """Add a single follow to the target channel"""
   followChannel(channelId: ID!): ChannelFollowsInfo!
 
+  """Report a channel"""
+  reportChannel(channelId: ID!, rationale: String!): ChannelReportInfo!
+
   """Report a video"""
-  reportVideo(rationale: String!, videoId: ID!): ReportVideoInfo!
+  reportVideo(rationale: String!, videoId: ID!): VideoReportInfo!
   setCategoryFeaturedVideos(categoryId: ID!, videos: [FeaturedVideoInput!]!): [FeaturedVideo!]!
   setKillSwitch(isKilled: Boolean!): Admin!
   setVideoHero(newVideoHero: VideoHeroInput!): VideoHero!
@@ -74,20 +97,13 @@ type Query {
 
   """Get list of most viewed categories of all time"""
   mostViewedCategoriesAllTime(limit: Int!): [EntityViewsInfo!]
-  reportedVideos(limit: Int = 30, orderBy: VideoReportOrderByInput = createdAt_DESC, skip: Int, where: VideoReportWhereInput): [ReportVideoInfo!]!
+  reportedChannels(limit: Int = 30, orderBy: ChannelReportOrderByInput = createdAt_DESC, skip: Int, where: ChannelReportsWhereInput): [ChannelReportInfo!]!
+  reportedVideos(limit: Int = 30, orderBy: VideoReportOrderByInput = createdAt_DESC, skip: Int, where: VideoReportsWhereInput): [VideoReportInfo!]!
 
   """Get current video hero"""
   videoHero: VideoHero!
 }
 
-type ReportVideoInfo {
-  createdAt: DateTime!
-  id: ID!
-  rationale: String!
-  reporterIp: String!
-  videoId: ID!
-}
-
 type VideoHero {
   heroPosterUrl: String!
   heroTitle: String!
@@ -102,12 +118,20 @@ input VideoHeroInput {
   videoId: ID!
 }
 
+type VideoReportInfo {
+  createdAt: DateTime!
+  id: ID!
+  rationale: String!
+  reporterIp: String!
+  videoId: ID!
+}
+
 enum VideoReportOrderByInput {
   createdAt_ASC
   createdAt_DESC
 }
 
-input VideoReportWhereInput {
+input VideoReportsWhereInput {
   createdAt_gt: DateTime
   createdAt_lt: DateTime
   reporterIp: String

+ 13 - 4
src/entities/ReportVideoInfo.ts → src/entities/EntityReportsInfo.ts

@@ -1,13 +1,10 @@
 import { Field, ID, ObjectType } from 'type-graphql'
 
 @ObjectType()
-export class ReportVideoInfo {
+export class EntityReportInfo {
   @Field(() => ID)
   id: string
 
-  @Field(() => ID)
-  videoId: string
-
   @Field()
   rationale: string
 
@@ -17,3 +14,15 @@ export class ReportVideoInfo {
   @Field()
   reporterIp: string
 }
+
+@ObjectType()
+export class VideoReportInfo extends EntityReportInfo {
+  @Field(() => ID)
+  videoId: string
+}
+
+@ObjectType()
+export class ChannelReportInfo extends EntityReportInfo {
+  @Field(() => ID)
+  channelId: string
+}

+ 19 - 4
src/models/ReportedContent.ts

@@ -1,12 +1,9 @@
 import { getModelForClass, prop } from '@typegoose/typegoose'
 
-export class ReportedVideo {
+export class ReportedEntity {
   @prop({ required: true })
   reporterIp: string
 
-  @prop({ required: true, index: true })
-  videoId: string
-
   @prop({ required: true })
   timestamp: Date
 
@@ -14,8 +11,26 @@ export class ReportedVideo {
   rationale: string
 }
 
+export class ReportedVideo extends ReportedEntity {
+  @prop({ required: true, index: true })
+  videoId: string
+}
+
 export const ReportedVideoModel = getModelForClass(ReportedVideo, { schemaOptions: { collection: 'reportedVideos' } })
 
 export const saveReportedVideo = (reportedVideo: ReportedVideo) => {
   return ReportedVideoModel.create(reportedVideo)
 }
+
+export class ReportedChannel extends ReportedEntity {
+  @prop({ required: true, index: true })
+  channelId: string
+}
+
+export const ReportedChannelModel = getModelForClass(ReportedChannel, {
+  schemaOptions: { collection: 'reportedChannels' },
+})
+
+export const saveReportedChannel = (reportedChannel: ReportedChannel) => {
+  return ReportedChannelModel.create(reportedChannel)
+}

+ 112 - 18
src/resolvers/reportsInfo.ts

@@ -14,8 +14,14 @@ import {
   Resolver,
   UseMiddleware,
 } from 'type-graphql'
-import { ReportVideoInfo } from '../entities/ReportVideoInfo'
-import { ReportedVideoModel, saveReportedVideo } from '../models/ReportedContent'
+import { ChannelReportInfo, VideoReportInfo } from '../entities/EntityReportsInfo'
+import { MaxLength } from 'class-validator'
+import {
+  ReportedChannelModel,
+  ReportedVideoModel,
+  saveReportedChannel,
+  saveReportedVideo,
+} from '../models/ReportedContent'
 import { OrionContext } from '../types'
 
 const ONE_HOUR = 60 * 60 * 1000
@@ -43,6 +49,17 @@ class ReportVideoArgs {
   videoId: string
 
   @Field()
+  @MaxLength(400, { message: 'Rationale cannot be longer than 400 characters' })
+  rationale: string
+}
+
+@ArgsType()
+class ReportChannelArgs {
+  @Field(() => ID)
+  channelId: string
+
+  @Field()
+  @MaxLength(400, { message: 'Rationale cannot be longer than 400 characters' })
   rationale: string
 }
 
@@ -50,14 +67,16 @@ enum VideoReportOrderByInput {
   createdAt_ASC = 'createdAt_ASC',
   createdAt_DESC = 'createdAt_DESC',
 }
-
 registerEnumType(VideoReportOrderByInput, { name: 'VideoReportOrderByInput' })
 
-@InputType()
-class VideoReportWhereInput {
-  @Field(() => ID, { nullable: true })
-  videoId?: string
+enum ChannelReportOrderByInput {
+  createdAt_ASC = 'createdAt_ASC',
+  createdAt_DESC = 'createdAt_DESC',
+}
+registerEnumType(ChannelReportOrderByInput, { name: 'ChannelReportOrderByInput' })
 
+@InputType()
+class ReportsWhereInput {
   @Field(() => Date, { nullable: true })
   createdAt_lt?: Date
 
@@ -69,31 +88,57 @@ class VideoReportWhereInput {
 }
 
 @ArgsType()
-class VideoReportsArgs {
-  @Field(() => VideoReportWhereInput, { nullable: true })
-  where: VideoReportWhereInput
+class ReportsArgs {
+  @Field(() => Int, { nullable: true, defaultValue: 30 })
+  limit: number
+
+  @Field(() => Int, { nullable: true })
+  skip: number
+}
+
+@InputType()
+class VideoReportsWhereInput extends ReportsWhereInput {
+  @Field(() => ID, { nullable: true })
+  videoId?: string
+}
+@ArgsType()
+class VideoReportsArgs extends ReportsArgs {
+  @Field(() => VideoReportsWhereInput, { nullable: true })
+  where: VideoReportsWhereInput
 
   @Field(() => VideoReportOrderByInput, {
     nullable: true,
     defaultValue: VideoReportOrderByInput.createdAt_DESC,
   })
   orderBy: VideoReportOrderByInput
+}
 
-  @Field(() => Int, { nullable: true, defaultValue: 30 })
-  limit: number
+@InputType()
+class ChannelReportsWhereInput extends ReportsWhereInput {
+  @Field(() => ID, { nullable: true })
+  channelId?: string
+}
 
-  @Field(() => Int, { nullable: true })
-  skip: number
+@ArgsType()
+class ChannelReportsArgs extends ReportsArgs {
+  @Field(() => ChannelReportsWhereInput, { nullable: true })
+  where: ChannelReportsWhereInput
+
+  @Field(() => ChannelReportOrderByInput, {
+    nullable: true,
+    defaultValue: ChannelReportOrderByInput.createdAt_DESC,
+  })
+  orderBy: ChannelReportOrderByInput
 }
 
 @Resolver()
 export class ReportsInfosResolver {
-  @Mutation(() => ReportVideoInfo, { description: 'Report a video' })
+  @Mutation(() => VideoReportInfo, { description: 'Report a video' })
   @UseMiddleware(rateLimit(MAX_REPORTS_PER_HOUR))
   async reportVideo(
     @Args() { videoId, rationale }: ReportVideoArgs,
     @Ctx() ctx: OrionContext
-  ): Promise<ReportVideoInfo> {
+  ): Promise<VideoReportInfo> {
     const createdAt = new Date()
     const reportedVideo = await saveReportedVideo({
       rationale,
@@ -111,12 +156,35 @@ export class ReportsInfosResolver {
     }
   }
 
-  @Query(() => [ReportVideoInfo])
+  @Mutation(() => ChannelReportInfo, { description: 'Report a channel' })
+  @UseMiddleware(rateLimit(MAX_REPORTS_PER_HOUR))
+  async reportChannel(
+    @Args() { channelId, rationale }: ReportChannelArgs,
+    @Ctx() ctx: OrionContext
+  ): Promise<ChannelReportInfo> {
+    const createdAt = new Date()
+    const reportedVideo = await saveReportedChannel({
+      rationale,
+      channelId,
+      reporterIp: ctx.remoteHost || '',
+      timestamp: createdAt,
+    })
+
+    return {
+      rationale,
+      channelId,
+      createdAt,
+      id: reportedVideo.id,
+      reporterIp: ctx.remoteHost || '',
+    }
+  }
+
+  @Query(() => [VideoReportInfo])
   @Authorized()
   async reportedVideos(
     @Args() { orderBy, where, limit, skip }: VideoReportsArgs,
     @Ctx() ctx: OrionContext
-  ): Promise<ReportVideoInfo[]> {
+  ): Promise<VideoReportInfo[]> {
     const reportedVideosDocument = await ReportedVideoModel.find({
       ...(where?.videoId ? { videoId: where?.videoId } : {}),
       ...(where?.createdAt_gt ? { timestamp: { $gte: where.createdAt_gt } } : {}),
@@ -137,4 +205,30 @@ export class ReportsInfosResolver {
       })) || []
     )
   }
+  @Query(() => [ChannelReportInfo])
+  @Authorized()
+  async reportedChannels(
+    @Args() { orderBy, where, limit, skip }: ChannelReportsArgs,
+    @Ctx() ctx: OrionContext
+  ): Promise<ChannelReportInfo[]> {
+    const reportedChannelsDocument = await ReportedChannelModel.find({
+      ...(where?.channelId ? { channelId: where?.channelId } : {}),
+      ...(where?.createdAt_gt ? { timestamp: { $gte: where.createdAt_gt } } : {}),
+      ...(where?.createdAt_lt ? { timestamp: { $lte: where.createdAt_lt } } : {}),
+      ...(where?.reporterIp ? { reporterIp: where?.reporterIp } : {}),
+    })
+      .skip(skip)
+      .limit(limit)
+      .sort({ timestamp: orderBy.split('_').at(-1) })
+
+    return (
+      reportedChannelsDocument.map((reportedChannel) => ({
+        rationale: reportedChannel.rationale,
+        channelId: reportedChannel.channelId,
+        createdAt: reportedChannel.timestamp,
+        id: reportedChannel.id,
+        reporterIp: ctx.remoteHost || '',
+      })) || []
+    )
+  }
 }