Add video channel and video thumbnail, rework video appearance in row
[oweals/peertube.git] / server / models / video / video-abuse.ts
1 import {
2   AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Is, Model, Table, UpdatedAt, DefaultScope
3 } from 'sequelize-typescript'
4 import { VideoAbuseObject } from '../../../shared/models/activitypub/objects'
5 import { VideoAbuse } from '../../../shared/models/videos'
6 import {
7   isVideoAbuseModerationCommentValid,
8   isVideoAbuseReasonValid,
9   isVideoAbuseStateValid
10 } from '../../helpers/custom-validators/video-abuses'
11 import { AccountModel } from '../account/account'
12 import { buildBlockedAccountSQL, getSort, throwIfNotValid } from '../utils'
13 import { VideoModel } from './video'
14 import { VideoAbuseState, Video } from '../../../shared'
15 import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants'
16 import { MUserAccountId, MVideoAbuse, MVideoAbuseFormattable, MVideoAbuseVideo } from '../../typings/models'
17 import * as Bluebird from 'bluebird'
18 import { literal, Op } from 'sequelize'
19 import { ThumbnailModel } from './thumbnail'
20 import { VideoChannelModel } from './video-channel'
21 import { ActorModel } from '../activitypub/actor'
22 import { VideoBlacklistModel } from './video-blacklist'
23
24 @DefaultScope(() => ({
25   include: [
26     {
27       model: AccountModel,
28       required: true
29     },
30     {
31       model: VideoModel,
32       required: false,
33       include: [
34         {
35           model: ThumbnailModel
36         },
37         {
38           model: VideoChannelModel.unscoped(),
39           include: [
40             {
41               model: ActorModel
42             }
43           ]
44         },
45         {
46           attributes: [ 'id', 'reason', 'unfederated' ],
47           model: VideoBlacklistModel
48         }
49       ]
50     }
51   ]
52 }))
53 @Table({
54   tableName: 'videoAbuse',
55   indexes: [
56     {
57       fields: [ 'videoId' ]
58     },
59     {
60       fields: [ 'reporterAccountId' ]
61     }
62   ]
63 })
64 export class VideoAbuseModel extends Model<VideoAbuseModel> {
65
66   @AllowNull(false)
67   @Default(null)
68   @Is('VideoAbuseReason', value => throwIfNotValid(value, isVideoAbuseReasonValid, 'reason'))
69   @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.REASON.max))
70   reason: string
71
72   @AllowNull(false)
73   @Default(null)
74   @Is('VideoAbuseState', value => throwIfNotValid(value, isVideoAbuseStateValid, 'state'))
75   @Column
76   state: VideoAbuseState
77
78   @AllowNull(true)
79   @Default(null)
80   @Is('VideoAbuseModerationComment', value => throwIfNotValid(value, isVideoAbuseModerationCommentValid, 'moderationComment', true))
81   @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.MODERATION_COMMENT.max))
82   moderationComment: string
83
84   @AllowNull(true)
85   @Default(null)
86   @Column(DataType.JSONB)
87   deletedVideo: Video
88
89   @CreatedAt
90   createdAt: Date
91
92   @UpdatedAt
93   updatedAt: Date
94
95   @ForeignKey(() => AccountModel)
96   @Column
97   reporterAccountId: number
98
99   @BelongsTo(() => AccountModel, {
100     foreignKey: {
101       allowNull: true
102     },
103     onDelete: 'set null'
104   })
105   Account: AccountModel
106
107   @ForeignKey(() => VideoModel)
108   @Column
109   videoId: number
110
111   @BelongsTo(() => VideoModel, {
112     foreignKey: {
113       allowNull: true
114     },
115     onDelete: 'set null'
116   })
117   Video: VideoModel
118
119   static loadByIdAndVideoId (id: number, videoId?: number, uuid?: string): Bluebird<MVideoAbuse> {
120     const videoAttributes = {}
121     if (videoId) videoAttributes['videoId'] = videoId
122     if (uuid) videoAttributes['deletedVideo'] = { uuid }
123
124     const query = {
125       where: {
126         id,
127         ...videoAttributes
128       }
129     }
130     return VideoAbuseModel.findOne(query)
131   }
132
133   static listForApi (parameters: {
134     start: number
135     count: number
136     sort: string
137     serverAccountId: number
138     user?: MUserAccountId
139   }) {
140     const { start, count, sort, user, serverAccountId } = parameters
141     const userAccountId = user ? user.Account.id : undefined
142
143     const query = {
144       offset: start,
145       limit: count,
146       order: getSort(sort),
147       where: {
148         reporterAccountId: {
149           [Op.notIn]: literal('(' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')')
150         }
151       },
152       col: 'VideoAbuseModel.id',
153       distinct: true
154     }
155
156     return VideoAbuseModel.findAndCountAll(query)
157       .then(({ rows, count }) => {
158         return { total: count, data: rows }
159       })
160   }
161
162   toFormattedJSON (this: MVideoAbuseFormattable): VideoAbuse {
163     const video = this.Video
164       ? this.Video
165       : this.deletedVideo
166
167     return {
168       id: this.id,
169       reason: this.reason,
170       reporterAccount: this.Account.toFormattedJSON(),
171       state: {
172         id: this.state,
173         label: VideoAbuseModel.getStateLabel(this.state)
174       },
175       moderationComment: this.moderationComment,
176       video: {
177         id: video.id,
178         uuid: video.uuid,
179         name: video.name,
180         nsfw: video.nsfw,
181         deleted: !this.Video,
182         blacklisted: this.Video && this.Video.isBlacklisted(),
183         thumbnailPath: this.Video?.getMiniatureStaticPath(),
184         channel: this.Video?.VideoChannel.toFormattedSummaryJSON() || this.deletedVideo?.channel
185       },
186       createdAt: this.createdAt
187     }
188   }
189
190   toActivityPubObject (this: MVideoAbuseVideo): VideoAbuseObject {
191     return {
192       type: 'Flag' as 'Flag',
193       content: this.reason,
194       object: this.Video.url
195     }
196   }
197
198   private static getStateLabel (id: number) {
199     return VIDEO_ABUSE_STATES[id] || 'Unknown'
200   }
201 }