diff options
Diffstat (limited to 'server/models')
-rw-r--r-- | server/models/abuse/abuse-query-builder.ts | 154 | ||||
-rw-r--r-- | server/models/abuse/abuse.ts | 515 | ||||
-rw-r--r-- | server/models/abuse/video-abuse.ts | 63 | ||||
-rw-r--r-- | server/models/abuse/video-comment-abuse.ts | 47 | ||||
-rw-r--r-- | server/models/account/account-blocklist.ts | 10 | ||||
-rw-r--r-- | server/models/account/account.ts | 9 | ||||
-rw-r--r-- | server/models/account/user-notification-setting.ts | 8 | ||||
-rw-r--r-- | server/models/account/user-notification.ts | 102 | ||||
-rw-r--r-- | server/models/account/user.ts | 38 | ||||
-rw-r--r-- | server/models/server/server-blocklist.ts | 10 | ||||
-rw-r--r-- | server/models/video/video-abuse.ts | 479 | ||||
-rw-r--r-- | server/models/video/video-channel.ts | 3 | ||||
-rw-r--r-- | server/models/video/video-comment.ts | 27 | ||||
-rw-r--r-- | server/models/video/video-query-builder.ts | 4 | ||||
-rw-r--r-- | server/models/video/video.ts | 88 |
15 files changed, 969 insertions, 588 deletions
diff --git a/server/models/abuse/abuse-query-builder.ts b/server/models/abuse/abuse-query-builder.ts new file mode 100644 index 000000000..5fddcf3c4 --- /dev/null +++ b/server/models/abuse/abuse-query-builder.ts | |||
@@ -0,0 +1,154 @@ | |||
1 | |||
2 | import { exists } from '@server/helpers/custom-validators/misc' | ||
3 | import { AbuseFilter, AbuseState, AbuseVideoIs } from '@shared/models' | ||
4 | import { buildBlockedAccountSQL, buildDirectionAndField } from '../utils' | ||
5 | |||
6 | export type BuildAbusesQueryOptions = { | ||
7 | start: number | ||
8 | count: number | ||
9 | sort: string | ||
10 | |||
11 | // search | ||
12 | search?: string | ||
13 | searchReporter?: string | ||
14 | searchReportee?: string | ||
15 | |||
16 | // video releated | ||
17 | searchVideo?: string | ||
18 | searchVideoChannel?: string | ||
19 | videoIs?: AbuseVideoIs | ||
20 | |||
21 | // filters | ||
22 | id?: number | ||
23 | predefinedReasonId?: number | ||
24 | filter?: AbuseFilter | ||
25 | |||
26 | state?: AbuseState | ||
27 | |||
28 | // accountIds | ||
29 | serverAccountId: number | ||
30 | userAccountId: number | ||
31 | } | ||
32 | |||
33 | function buildAbuseListQuery (options: BuildAbusesQueryOptions, type: 'count' | 'id') { | ||
34 | const whereAnd: string[] = [] | ||
35 | const replacements: any = {} | ||
36 | |||
37 | const joins = [ | ||
38 | 'LEFT JOIN "videoAbuse" ON "videoAbuse"."abuseId" = "abuse"."id"', | ||
39 | 'LEFT JOIN "video" ON "videoAbuse"."videoId" = "video"."id"', | ||
40 | 'LEFT JOIN "videoBlacklist" ON "videoBlacklist"."videoId" = "video"."id"', | ||
41 | 'LEFT JOIN "videoChannel" ON "video"."channelId" = "videoChannel"."id"', | ||
42 | 'LEFT JOIN "account" "reporterAccount" ON "reporterAccount"."id" = "abuse"."reporterAccountId"', | ||
43 | 'LEFT JOIN "account" "flaggedAccount" ON "flaggedAccount"."id" = "abuse"."reporterAccountId"', | ||
44 | 'LEFT JOIN "commentAbuse" ON "commentAbuse"."abuseId" = "abuse"."id"', | ||
45 | 'LEFT JOIN "videoComment" ON "commentAbuse"."videoCommentId" = "videoComment"."id"' | ||
46 | ] | ||
47 | |||
48 | whereAnd.push('"abuse"."reporterAccountId" NOT IN (' + buildBlockedAccountSQL([ options.serverAccountId, options.userAccountId ]) + ')') | ||
49 | |||
50 | if (options.search) { | ||
51 | const searchWhereOr = [ | ||
52 | '"video"."name" ILIKE :search', | ||
53 | '"videoChannel"."name" ILIKE :search', | ||
54 | `"videoAbuse"."deletedVideo"->>'name' ILIKE :search`, | ||
55 | `"videoAbuse"."deletedVideo"->'channel'->>'displayName' ILIKE :search`, | ||
56 | '"reporterAccount"."name" ILIKE :search', | ||
57 | '"flaggedAccount"."name" ILIKE :search' | ||
58 | ] | ||
59 | |||
60 | replacements.search = `%${options.search}%` | ||
61 | whereAnd.push('(' + searchWhereOr.join(' OR ') + ')') | ||
62 | } | ||
63 | |||
64 | if (options.searchVideo) { | ||
65 | whereAnd.push('"video"."name" ILIKE :searchVideo') | ||
66 | replacements.searchVideo = `%${options.searchVideo}%` | ||
67 | } | ||
68 | |||
69 | if (options.searchVideoChannel) { | ||
70 | whereAnd.push('"videoChannel"."name" ILIKE :searchVideoChannel') | ||
71 | replacements.searchVideoChannel = `%${options.searchVideoChannel}%` | ||
72 | } | ||
73 | |||
74 | if (options.id) { | ||
75 | whereAnd.push('"abuse"."id" = :id') | ||
76 | replacements.id = options.id | ||
77 | } | ||
78 | |||
79 | if (options.state) { | ||
80 | whereAnd.push('"abuse"."state" = :state') | ||
81 | replacements.state = options.state | ||
82 | } | ||
83 | |||
84 | if (options.videoIs === 'deleted') { | ||
85 | whereAnd.push('"videoAbuse"."deletedVideo" IS NOT NULL') | ||
86 | } else if (options.videoIs === 'blacklisted') { | ||
87 | whereAnd.push('"videoBlacklist"."id" IS NOT NULL') | ||
88 | } | ||
89 | |||
90 | if (options.predefinedReasonId) { | ||
91 | whereAnd.push(':predefinedReasonId = ANY("abuse"."predefinedReasons")') | ||
92 | replacements.predefinedReasonId = options.predefinedReasonId | ||
93 | } | ||
94 | |||
95 | if (options.filter === 'video') { | ||
96 | whereAnd.push('"videoAbuse"."id" IS NOT NULL') | ||
97 | } else if (options.filter === 'comment') { | ||
98 | whereAnd.push('"commentAbuse"."id" IS NOT NULL') | ||
99 | } else if (options.filter === 'account') { | ||
100 | whereAnd.push('"videoAbuse"."id" IS NULL AND "commentAbuse"."id" IS NULL') | ||
101 | } | ||
102 | |||
103 | if (options.searchReporter) { | ||
104 | whereAnd.push('"reporterAccount"."name" ILIKE :searchReporter') | ||
105 | replacements.searchReporter = `%${options.searchReporter}%` | ||
106 | } | ||
107 | |||
108 | if (options.searchReportee) { | ||
109 | whereAnd.push('"flaggedAccount"."name" ILIKE :searchReportee') | ||
110 | replacements.searchReportee = `%${options.searchReportee}%` | ||
111 | } | ||
112 | |||
113 | const prefix = type === 'count' | ||
114 | ? 'SELECT COUNT("abuse"."id") AS "total"' | ||
115 | : 'SELECT "abuse"."id" ' | ||
116 | |||
117 | let suffix = '' | ||
118 | if (type !== 'count') { | ||
119 | |||
120 | if (options.sort) { | ||
121 | const order = buildAbuseOrder(options.sort) | ||
122 | suffix += `${order} ` | ||
123 | } | ||
124 | |||
125 | if (exists(options.count)) { | ||
126 | const count = parseInt(options.count + '', 10) | ||
127 | suffix += `LIMIT ${count} ` | ||
128 | } | ||
129 | |||
130 | if (exists(options.start)) { | ||
131 | const start = parseInt(options.start + '', 10) | ||
132 | suffix += `OFFSET ${start} ` | ||
133 | } | ||
134 | } | ||
135 | |||
136 | const where = whereAnd.length !== 0 | ||
137 | ? `WHERE ${whereAnd.join(' AND ')}` | ||
138 | : '' | ||
139 | |||
140 | return { | ||
141 | query: `${prefix} FROM "abuse" ${joins.join(' ')} ${where} ${suffix}`, | ||
142 | replacements | ||
143 | } | ||
144 | } | ||
145 | |||
146 | function buildAbuseOrder (value: string) { | ||
147 | const { direction, field } = buildDirectionAndField(value) | ||
148 | |||
149 | return `ORDER BY "abuse"."${field}" ${direction}` | ||
150 | } | ||
151 | |||
152 | export { | ||
153 | buildAbuseListQuery | ||
154 | } | ||
diff --git a/server/models/abuse/abuse.ts b/server/models/abuse/abuse.ts new file mode 100644 index 000000000..bd96cf79c --- /dev/null +++ b/server/models/abuse/abuse.ts | |||
@@ -0,0 +1,515 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
2 | import { invert } from 'lodash' | ||
3 | import { literal, Op, QueryTypes, WhereOptions } from 'sequelize' | ||
4 | import { | ||
5 | AllowNull, | ||
6 | BelongsTo, | ||
7 | Column, | ||
8 | CreatedAt, | ||
9 | DataType, | ||
10 | Default, | ||
11 | ForeignKey, | ||
12 | HasOne, | ||
13 | Is, | ||
14 | Model, | ||
15 | Scopes, | ||
16 | Table, | ||
17 | UpdatedAt | ||
18 | } from 'sequelize-typescript' | ||
19 | import { isAbuseModerationCommentValid, isAbuseReasonValid, isAbuseStateValid } from '@server/helpers/custom-validators/abuses' | ||
20 | import { | ||
21 | Abuse, | ||
22 | AbuseFilter, | ||
23 | AbuseObject, | ||
24 | AbusePredefinedReasons, | ||
25 | abusePredefinedReasonsMap, | ||
26 | AbusePredefinedReasonsString, | ||
27 | AbuseState, | ||
28 | AbuseVideoIs, | ||
29 | VideoAbuse, | ||
30 | VideoCommentAbuse | ||
31 | } from '@shared/models' | ||
32 | import { ABUSE_STATES, CONSTRAINTS_FIELDS } from '../../initializers/constants' | ||
33 | import { MAbuse, MAbuseAP, MAbuseFormattable, MUserAccountId } from '../../types/models' | ||
34 | import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account' | ||
35 | import { getSort, throwIfNotValid } from '../utils' | ||
36 | import { ThumbnailModel } from '../video/thumbnail' | ||
37 | import { VideoModel } from '../video/video' | ||
38 | import { VideoBlacklistModel } from '../video/video-blacklist' | ||
39 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions as ChannelSummaryOptions, VideoChannelModel } from '../video/video-channel' | ||
40 | import { VideoCommentModel } from '../video/video-comment' | ||
41 | import { buildAbuseListQuery, BuildAbusesQueryOptions } from './abuse-query-builder' | ||
42 | import { VideoAbuseModel } from './video-abuse' | ||
43 | import { VideoCommentAbuseModel } from './video-comment-abuse' | ||
44 | |||
45 | export enum ScopeNames { | ||
46 | FOR_API = 'FOR_API' | ||
47 | } | ||
48 | |||
49 | @Scopes(() => ({ | ||
50 | [ScopeNames.FOR_API]: () => { | ||
51 | return { | ||
52 | attributes: { | ||
53 | include: [ | ||
54 | [ | ||
55 | // we don't care about this count for deleted videos, so there are not included | ||
56 | literal( | ||
57 | '(' + | ||
58 | 'SELECT count(*) ' + | ||
59 | 'FROM "videoAbuse" ' + | ||
60 | 'WHERE "videoId" = "VideoAbuse"."videoId" AND "videoId" IS NOT NULL' + | ||
61 | ')' | ||
62 | ), | ||
63 | 'countReportsForVideo' | ||
64 | ], | ||
65 | [ | ||
66 | // we don't care about this count for deleted videos, so there are not included | ||
67 | literal( | ||
68 | '(' + | ||
69 | 'SELECT t.nth ' + | ||
70 | 'FROM ( ' + | ||
71 | 'SELECT id, ' + | ||
72 | 'row_number() OVER (PARTITION BY "videoId" ORDER BY "createdAt") AS nth ' + | ||
73 | 'FROM "videoAbuse" ' + | ||
74 | ') t ' + | ||
75 | 'WHERE t.id = "VideoAbuse".id AND t.id IS NOT NULL' + | ||
76 | ')' | ||
77 | ), | ||
78 | 'nthReportForVideo' | ||
79 | ], | ||
80 | [ | ||
81 | literal( | ||
82 | '(' + | ||
83 | 'SELECT count("abuse"."id") ' + | ||
84 | 'FROM "abuse" ' + | ||
85 | 'WHERE "abuse"."reporterAccountId" = "AbuseModel"."reporterAccountId"' + | ||
86 | ')' | ||
87 | ), | ||
88 | 'countReportsForReporter' | ||
89 | ], | ||
90 | [ | ||
91 | literal( | ||
92 | '(' + | ||
93 | 'SELECT count("abuse"."id") ' + | ||
94 | 'FROM "abuse" ' + | ||
95 | 'WHERE "abuse"."flaggedAccountId" = "AbuseModel"."flaggedAccountId"' + | ||
96 | ')' | ||
97 | ), | ||
98 | 'countReportsForReportee' | ||
99 | ] | ||
100 | ] | ||
101 | }, | ||
102 | include: [ | ||
103 | { | ||
104 | model: AccountModel.scope({ | ||
105 | method: [ | ||
106 | AccountScopeNames.SUMMARY, | ||
107 | { actorRequired: false } as AccountSummaryOptions | ||
108 | ] | ||
109 | }), | ||
110 | as: 'ReporterAccount' | ||
111 | }, | ||
112 | { | ||
113 | model: AccountModel.scope({ | ||
114 | method: [ | ||
115 | AccountScopeNames.SUMMARY, | ||
116 | { actorRequired: false } as AccountSummaryOptions | ||
117 | ] | ||
118 | }), | ||
119 | as: 'FlaggedAccount' | ||
120 | }, | ||
121 | { | ||
122 | model: VideoCommentAbuseModel.unscoped(), | ||
123 | include: [ | ||
124 | { | ||
125 | model: VideoCommentModel.unscoped(), | ||
126 | include: [ | ||
127 | { | ||
128 | model: VideoModel.unscoped(), | ||
129 | attributes: [ 'name', 'id', 'uuid' ] | ||
130 | } | ||
131 | ] | ||
132 | } | ||
133 | ] | ||
134 | }, | ||
135 | { | ||
136 | model: VideoAbuseModel.unscoped(), | ||
137 | include: [ | ||
138 | { | ||
139 | attributes: [ 'id', 'uuid', 'name', 'nsfw' ], | ||
140 | model: VideoModel.unscoped(), | ||
141 | include: [ | ||
142 | { | ||
143 | attributes: [ 'filename', 'fileUrl', 'type' ], | ||
144 | model: ThumbnailModel | ||
145 | }, | ||
146 | { | ||
147 | model: VideoChannelModel.scope({ | ||
148 | method: [ | ||
149 | VideoChannelScopeNames.SUMMARY, | ||
150 | { withAccount: false, actorRequired: false } as ChannelSummaryOptions | ||
151 | ] | ||
152 | }), | ||
153 | required: false | ||
154 | }, | ||
155 | { | ||
156 | attributes: [ 'id', 'reason', 'unfederated' ], | ||
157 | required: false, | ||
158 | model: VideoBlacklistModel | ||
159 | } | ||
160 | ] | ||
161 | } | ||
162 | ] | ||
163 | } | ||
164 | ] | ||
165 | } | ||
166 | } | ||
167 | })) | ||
168 | @Table({ | ||
169 | tableName: 'abuse', | ||
170 | indexes: [ | ||
171 | { | ||
172 | fields: [ 'reporterAccountId' ] | ||
173 | }, | ||
174 | { | ||
175 | fields: [ 'flaggedAccountId' ] | ||
176 | } | ||
177 | ] | ||
178 | }) | ||
179 | export class AbuseModel extends Model<AbuseModel> { | ||
180 | |||
181 | @AllowNull(false) | ||
182 | @Default(null) | ||
183 | @Is('AbuseReason', value => throwIfNotValid(value, isAbuseReasonValid, 'reason')) | ||
184 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.ABUSES.REASON.max)) | ||
185 | reason: string | ||
186 | |||
187 | @AllowNull(false) | ||
188 | @Default(null) | ||
189 | @Is('AbuseState', value => throwIfNotValid(value, isAbuseStateValid, 'state')) | ||
190 | @Column | ||
191 | state: AbuseState | ||
192 | |||
193 | @AllowNull(true) | ||
194 | @Default(null) | ||
195 | @Is('AbuseModerationComment', value => throwIfNotValid(value, isAbuseModerationCommentValid, 'moderationComment', true)) | ||
196 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.ABUSES.MODERATION_COMMENT.max)) | ||
197 | moderationComment: string | ||
198 | |||
199 | @AllowNull(true) | ||
200 | @Default(null) | ||
201 | @Column(DataType.ARRAY(DataType.INTEGER)) | ||
202 | predefinedReasons: AbusePredefinedReasons[] | ||
203 | |||
204 | @CreatedAt | ||
205 | createdAt: Date | ||
206 | |||
207 | @UpdatedAt | ||
208 | updatedAt: Date | ||
209 | |||
210 | @ForeignKey(() => AccountModel) | ||
211 | @Column | ||
212 | reporterAccountId: number | ||
213 | |||
214 | @BelongsTo(() => AccountModel, { | ||
215 | foreignKey: { | ||
216 | name: 'reporterAccountId', | ||
217 | allowNull: true | ||
218 | }, | ||
219 | as: 'ReporterAccount', | ||
220 | onDelete: 'set null' | ||
221 | }) | ||
222 | ReporterAccount: AccountModel | ||
223 | |||
224 | @ForeignKey(() => AccountModel) | ||
225 | @Column | ||
226 | flaggedAccountId: number | ||
227 | |||
228 | @BelongsTo(() => AccountModel, { | ||
229 | foreignKey: { | ||
230 | name: 'flaggedAccountId', | ||
231 | allowNull: true | ||
232 | }, | ||
233 | as: 'FlaggedAccount', | ||
234 | onDelete: 'set null' | ||
235 | }) | ||
236 | FlaggedAccount: AccountModel | ||
237 | |||
238 | @HasOne(() => VideoCommentAbuseModel, { | ||
239 | foreignKey: { | ||
240 | name: 'abuseId', | ||
241 | allowNull: false | ||
242 | }, | ||
243 | onDelete: 'cascade' | ||
244 | }) | ||
245 | VideoCommentAbuse: VideoCommentAbuseModel | ||
246 | |||
247 | @HasOne(() => VideoAbuseModel, { | ||
248 | foreignKey: { | ||
249 | name: 'abuseId', | ||
250 | allowNull: false | ||
251 | }, | ||
252 | onDelete: 'cascade' | ||
253 | }) | ||
254 | VideoAbuse: VideoAbuseModel | ||
255 | |||
256 | // FIXME: deprecated in 2.3. Remove these validators | ||
257 | static loadByIdAndVideoId (id: number, videoId?: number, uuid?: string): Bluebird<MAbuse> { | ||
258 | const videoWhere: WhereOptions = {} | ||
259 | |||
260 | if (videoId) videoWhere.videoId = videoId | ||
261 | if (uuid) videoWhere.deletedVideo = { uuid } | ||
262 | |||
263 | const query = { | ||
264 | include: [ | ||
265 | { | ||
266 | model: VideoAbuseModel, | ||
267 | required: true, | ||
268 | where: videoWhere | ||
269 | } | ||
270 | ], | ||
271 | where: { | ||
272 | id | ||
273 | } | ||
274 | } | ||
275 | return AbuseModel.findOne(query) | ||
276 | } | ||
277 | |||
278 | static loadById (id: number): Bluebird<MAbuse> { | ||
279 | const query = { | ||
280 | where: { | ||
281 | id | ||
282 | } | ||
283 | } | ||
284 | |||
285 | return AbuseModel.findOne(query) | ||
286 | } | ||
287 | |||
288 | static async listForApi (parameters: { | ||
289 | start: number | ||
290 | count: number | ||
291 | sort: string | ||
292 | |||
293 | filter?: AbuseFilter | ||
294 | |||
295 | serverAccountId: number | ||
296 | user?: MUserAccountId | ||
297 | |||
298 | id?: number | ||
299 | predefinedReason?: AbusePredefinedReasonsString | ||
300 | state?: AbuseState | ||
301 | videoIs?: AbuseVideoIs | ||
302 | |||
303 | search?: string | ||
304 | searchReporter?: string | ||
305 | searchReportee?: string | ||
306 | searchVideo?: string | ||
307 | searchVideoChannel?: string | ||
308 | }) { | ||
309 | const { | ||
310 | start, | ||
311 | count, | ||
312 | sort, | ||
313 | search, | ||
314 | user, | ||
315 | serverAccountId, | ||
316 | state, | ||
317 | videoIs, | ||
318 | predefinedReason, | ||
319 | searchReportee, | ||
320 | searchVideo, | ||
321 | filter, | ||
322 | searchVideoChannel, | ||
323 | searchReporter, | ||
324 | id | ||
325 | } = parameters | ||
326 | |||
327 | const userAccountId = user ? user.Account.id : undefined | ||
328 | const predefinedReasonId = predefinedReason ? abusePredefinedReasonsMap[predefinedReason] : undefined | ||
329 | |||
330 | const queryOptions: BuildAbusesQueryOptions = { | ||
331 | start, | ||
332 | count, | ||
333 | sort, | ||
334 | id, | ||
335 | filter, | ||
336 | predefinedReasonId, | ||
337 | search, | ||
338 | state, | ||
339 | videoIs, | ||
340 | searchReportee, | ||
341 | searchVideo, | ||
342 | searchVideoChannel, | ||
343 | searchReporter, | ||
344 | serverAccountId, | ||
345 | userAccountId | ||
346 | } | ||
347 | |||
348 | const [ total, data ] = await Promise.all([ | ||
349 | AbuseModel.internalCountForApi(queryOptions), | ||
350 | AbuseModel.internalListForApi(queryOptions) | ||
351 | ]) | ||
352 | |||
353 | return { total, data } | ||
354 | } | ||
355 | |||
356 | toFormattedJSON (this: MAbuseFormattable): Abuse { | ||
357 | const predefinedReasons = AbuseModel.getPredefinedReasonsStrings(this.predefinedReasons) | ||
358 | |||
359 | const countReportsForVideo = this.get('countReportsForVideo') as number | ||
360 | const nthReportForVideo = this.get('nthReportForVideo') as number | ||
361 | |||
362 | const countReportsForReporter = this.get('countReportsForReporter') as number | ||
363 | const countReportsForReportee = this.get('countReportsForReportee') as number | ||
364 | |||
365 | let video: VideoAbuse = null | ||
366 | let comment: VideoCommentAbuse = null | ||
367 | |||
368 | if (this.VideoAbuse) { | ||
369 | const abuseModel = this.VideoAbuse | ||
370 | const entity = abuseModel.Video || abuseModel.deletedVideo | ||
371 | |||
372 | video = { | ||
373 | id: entity.id, | ||
374 | uuid: entity.uuid, | ||
375 | name: entity.name, | ||
376 | nsfw: entity.nsfw, | ||
377 | |||
378 | startAt: abuseModel.startAt, | ||
379 | endAt: abuseModel.endAt, | ||
380 | |||
381 | deleted: !abuseModel.Video, | ||
382 | blacklisted: abuseModel.Video?.isBlacklisted() || false, | ||
383 | thumbnailPath: abuseModel.Video?.getMiniatureStaticPath(), | ||
384 | |||
385 | channel: abuseModel.Video?.VideoChannel.toFormattedJSON() || abuseModel.deletedVideo?.channel, | ||
386 | |||
387 | countReports: countReportsForVideo, | ||
388 | nthReport: nthReportForVideo | ||
389 | } | ||
390 | } | ||
391 | |||
392 | if (this.VideoCommentAbuse) { | ||
393 | const abuseModel = this.VideoCommentAbuse | ||
394 | const entity = abuseModel.VideoComment | ||
395 | |||
396 | comment = { | ||
397 | id: entity.id, | ||
398 | threadId: entity.getThreadId(), | ||
399 | |||
400 | text: entity.text ?? '', | ||
401 | |||
402 | deleted: entity.isDeleted(), | ||
403 | |||
404 | video: { | ||
405 | id: entity.Video.id, | ||
406 | name: entity.Video.name, | ||
407 | uuid: entity.Video.uuid | ||
408 | } | ||
409 | } | ||
410 | } | ||
411 | |||
412 | return { | ||
413 | id: this.id, | ||
414 | reason: this.reason, | ||
415 | predefinedReasons, | ||
416 | |||
417 | reporterAccount: this.ReporterAccount | ||
418 | ? this.ReporterAccount.toFormattedJSON() | ||
419 | : null, | ||
420 | |||
421 | flaggedAccount: this.FlaggedAccount | ||
422 | ? this.FlaggedAccount.toFormattedJSON() | ||
423 | : null, | ||
424 | |||
425 | state: { | ||
426 | id: this.state, | ||
427 | label: AbuseModel.getStateLabel(this.state) | ||
428 | }, | ||
429 | |||
430 | moderationComment: this.moderationComment, | ||
431 | |||
432 | video, | ||
433 | comment, | ||
434 | |||
435 | createdAt: this.createdAt, | ||
436 | updatedAt: this.updatedAt, | ||
437 | |||
438 | countReportsForReporter: (countReportsForReporter || 0), | ||
439 | countReportsForReportee: (countReportsForReportee || 0), | ||
440 | |||
441 | // FIXME: deprecated in 2.3, remove this | ||
442 | startAt: null, | ||
443 | endAt: null, | ||
444 | count: countReportsForVideo || 0, | ||
445 | nth: nthReportForVideo || 0 | ||
446 | } | ||
447 | } | ||
448 | |||
449 | toActivityPubObject (this: MAbuseAP): AbuseObject { | ||
450 | const predefinedReasons = AbuseModel.getPredefinedReasonsStrings(this.predefinedReasons) | ||
451 | |||
452 | const object = this.VideoAbuse?.Video?.url || this.VideoCommentAbuse?.VideoComment?.url || this.FlaggedAccount.Actor.url | ||
453 | |||
454 | const startAt = this.VideoAbuse?.startAt | ||
455 | const endAt = this.VideoAbuse?.endAt | ||
456 | |||
457 | return { | ||
458 | type: 'Flag' as 'Flag', | ||
459 | content: this.reason, | ||
460 | object, | ||
461 | tag: predefinedReasons.map(r => ({ | ||
462 | type: 'Hashtag' as 'Hashtag', | ||
463 | name: r | ||
464 | })), | ||
465 | startAt, | ||
466 | endAt | ||
467 | } | ||
468 | } | ||
469 | |||
470 | private static async internalCountForApi (parameters: BuildAbusesQueryOptions) { | ||
471 | const { query, replacements } = buildAbuseListQuery(parameters, 'count') | ||
472 | const options = { | ||
473 | type: QueryTypes.SELECT as QueryTypes.SELECT, | ||
474 | replacements | ||
475 | } | ||
476 | |||
477 | const [ { total } ] = await AbuseModel.sequelize.query<{ total: string }>(query, options) | ||
478 | if (total === null) return 0 | ||
479 | |||
480 | return parseInt(total, 10) | ||
481 | } | ||
482 | |||
483 | private static async internalListForApi (parameters: BuildAbusesQueryOptions) { | ||
484 | const { query, replacements } = buildAbuseListQuery(parameters, 'id') | ||
485 | const options = { | ||
486 | type: QueryTypes.SELECT as QueryTypes.SELECT, | ||
487 | replacements | ||
488 | } | ||
489 | |||
490 | const rows = await AbuseModel.sequelize.query<{ id: string }>(query, options) | ||
491 | const ids = rows.map(r => r.id) | ||
492 | |||
493 | if (ids.length === 0) return [] | ||
494 | |||
495 | return AbuseModel.scope(ScopeNames.FOR_API) | ||
496 | .findAll({ | ||
497 | order: getSort(parameters.sort), | ||
498 | where: { | ||
499 | id: { | ||
500 | [Op.in]: ids | ||
501 | } | ||
502 | } | ||
503 | }) | ||
504 | } | ||
505 | |||
506 | private static getStateLabel (id: number) { | ||
507 | return ABUSE_STATES[id] || 'Unknown' | ||
508 | } | ||
509 | |||
510 | private static getPredefinedReasonsStrings (predefinedReasons: AbusePredefinedReasons[]): AbusePredefinedReasonsString[] { | ||
511 | return (predefinedReasons || []) | ||
512 | .filter(r => r in AbusePredefinedReasons) | ||
513 | .map(r => invert(abusePredefinedReasonsMap)[r] as AbusePredefinedReasonsString) | ||
514 | } | ||
515 | } | ||
diff --git a/server/models/abuse/video-abuse.ts b/server/models/abuse/video-abuse.ts new file mode 100644 index 000000000..d92bcf19f --- /dev/null +++ b/server/models/abuse/video-abuse.ts | |||
@@ -0,0 +1,63 @@ | |||
1 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' | ||
2 | import { VideoDetails } from '@shared/models' | ||
3 | import { VideoModel } from '../video/video' | ||
4 | import { AbuseModel } from './abuse' | ||
5 | |||
6 | @Table({ | ||
7 | tableName: 'videoAbuse', | ||
8 | indexes: [ | ||
9 | { | ||
10 | fields: [ 'abuseId' ] | ||
11 | }, | ||
12 | { | ||
13 | fields: [ 'videoId' ] | ||
14 | } | ||
15 | ] | ||
16 | }) | ||
17 | export class VideoAbuseModel extends Model<VideoAbuseModel> { | ||
18 | |||
19 | @CreatedAt | ||
20 | createdAt: Date | ||
21 | |||
22 | @UpdatedAt | ||
23 | updatedAt: Date | ||
24 | |||
25 | @AllowNull(true) | ||
26 | @Default(null) | ||
27 | @Column | ||
28 | startAt: number | ||
29 | |||
30 | @AllowNull(true) | ||
31 | @Default(null) | ||
32 | @Column | ||
33 | endAt: number | ||
34 | |||
35 | @AllowNull(true) | ||
36 | @Default(null) | ||
37 | @Column(DataType.JSONB) | ||
38 | deletedVideo: VideoDetails | ||
39 | |||
40 | @ForeignKey(() => AbuseModel) | ||
41 | @Column | ||
42 | abuseId: number | ||
43 | |||
44 | @BelongsTo(() => AbuseModel, { | ||
45 | foreignKey: { | ||
46 | allowNull: false | ||
47 | }, | ||
48 | onDelete: 'cascade' | ||
49 | }) | ||
50 | Abuse: AbuseModel | ||
51 | |||
52 | @ForeignKey(() => VideoModel) | ||
53 | @Column | ||
54 | videoId: number | ||
55 | |||
56 | @BelongsTo(() => VideoModel, { | ||
57 | foreignKey: { | ||
58 | allowNull: true | ||
59 | }, | ||
60 | onDelete: 'set null' | ||
61 | }) | ||
62 | Video: VideoModel | ||
63 | } | ||
diff --git a/server/models/abuse/video-comment-abuse.ts b/server/models/abuse/video-comment-abuse.ts new file mode 100644 index 000000000..8b34009b4 --- /dev/null +++ b/server/models/abuse/video-comment-abuse.ts | |||
@@ -0,0 +1,47 @@ | |||
1 | import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' | ||
2 | import { VideoCommentModel } from '../video/video-comment' | ||
3 | import { AbuseModel } from './abuse' | ||
4 | |||
5 | @Table({ | ||
6 | tableName: 'commentAbuse', | ||
7 | indexes: [ | ||
8 | { | ||
9 | fields: [ 'abuseId' ] | ||
10 | }, | ||
11 | { | ||
12 | fields: [ 'videoCommentId' ] | ||
13 | } | ||
14 | ] | ||
15 | }) | ||
16 | export class VideoCommentAbuseModel extends Model<VideoCommentAbuseModel> { | ||
17 | |||
18 | @CreatedAt | ||
19 | createdAt: Date | ||
20 | |||
21 | @UpdatedAt | ||
22 | updatedAt: Date | ||
23 | |||
24 | @ForeignKey(() => AbuseModel) | ||
25 | @Column | ||
26 | abuseId: number | ||
27 | |||
28 | @BelongsTo(() => AbuseModel, { | ||
29 | foreignKey: { | ||
30 | allowNull: false | ||
31 | }, | ||
32 | onDelete: 'cascade' | ||
33 | }) | ||
34 | Abuse: AbuseModel | ||
35 | |||
36 | @ForeignKey(() => VideoCommentModel) | ||
37 | @Column | ||
38 | videoCommentId: number | ||
39 | |||
40 | @BelongsTo(() => VideoCommentModel, { | ||
41 | foreignKey: { | ||
42 | allowNull: true | ||
43 | }, | ||
44 | onDelete: 'set null' | ||
45 | }) | ||
46 | VideoComment: VideoCommentModel | ||
47 | } | ||
diff --git a/server/models/account/account-blocklist.ts b/server/models/account/account-blocklist.ts index cf8872fd5..577b7dc19 100644 --- a/server/models/account/account-blocklist.ts +++ b/server/models/account/account-blocklist.ts | |||
@@ -1,12 +1,12 @@ | |||
1 | import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' | ||
2 | import { AccountModel } from './account' | ||
3 | import { getSort, searchAttribute } from '../utils' | ||
4 | import { AccountBlock } from '../../../shared/models/blocklist' | ||
5 | import { Op } from 'sequelize' | ||
6 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
2 | import { Op } from 'sequelize' | ||
3 | import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' | ||
7 | import { MAccountBlocklist, MAccountBlocklistAccounts, MAccountBlocklistFormattable } from '@server/types/models' | 4 | import { MAccountBlocklist, MAccountBlocklistAccounts, MAccountBlocklistFormattable } from '@server/types/models' |
5 | import { AccountBlock } from '../../../shared/models' | ||
8 | import { ActorModel } from '../activitypub/actor' | 6 | import { ActorModel } from '../activitypub/actor' |
9 | import { ServerModel } from '../server/server' | 7 | import { ServerModel } from '../server/server' |
8 | import { getSort, searchAttribute } from '../utils' | ||
9 | import { AccountModel } from './account' | ||
10 | 10 | ||
11 | enum ScopeNames { | 11 | enum ScopeNames { |
12 | WITH_ACCOUNTS = 'WITH_ACCOUNTS' | 12 | WITH_ACCOUNTS = 'WITH_ACCOUNTS' |
diff --git a/server/models/account/account.ts b/server/models/account/account.ts index 4395d179a..f97519b14 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts | |||
@@ -42,6 +42,7 @@ export enum ScopeNames { | |||
42 | } | 42 | } |
43 | 43 | ||
44 | export type SummaryOptions = { | 44 | export type SummaryOptions = { |
45 | actorRequired?: boolean // Default: true | ||
45 | whereActor?: WhereOptions | 46 | whereActor?: WhereOptions |
46 | withAccountBlockerIds?: number[] | 47 | withAccountBlockerIds?: number[] |
47 | } | 48 | } |
@@ -65,12 +66,12 @@ export type SummaryOptions = { | |||
65 | } | 66 | } |
66 | 67 | ||
67 | const query: FindOptions = { | 68 | const query: FindOptions = { |
68 | attributes: [ 'id', 'name' ], | 69 | attributes: [ 'id', 'name', 'actorId' ], |
69 | include: [ | 70 | include: [ |
70 | { | 71 | { |
71 | attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ], | 72 | attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ], |
72 | model: ActorModel.unscoped(), | 73 | model: ActorModel.unscoped(), |
73 | required: true, | 74 | required: options.actorRequired ?? true, |
74 | where: whereActor, | 75 | where: whereActor, |
75 | include: [ | 76 | include: [ |
76 | serverInclude, | 77 | serverInclude, |
@@ -388,6 +389,10 @@ export class AccountModel extends Model<AccountModel> { | |||
388 | .findAll(query) | 389 | .findAll(query) |
389 | } | 390 | } |
390 | 391 | ||
392 | getClientUrl () { | ||
393 | return WEBSERVER.URL + '/accounts/' + this.Actor.getIdentifier() | ||
394 | } | ||
395 | |||
391 | toFormattedJSON (this: MAccountFormattable): Account { | 396 | toFormattedJSON (this: MAccountFormattable): Account { |
392 | const actor = this.Actor.toFormattedJSON() | 397 | const actor = this.Actor.toFormattedJSON() |
393 | const account = { | 398 | const account = { |
diff --git a/server/models/account/user-notification-setting.ts b/server/models/account/user-notification-setting.ts index b69b47265..d8f3f13da 100644 --- a/server/models/account/user-notification-setting.ts +++ b/server/models/account/user-notification-setting.ts | |||
@@ -51,11 +51,11 @@ export class UserNotificationSettingModel extends Model<UserNotificationSettingM | |||
51 | @AllowNull(false) | 51 | @AllowNull(false) |
52 | @Default(null) | 52 | @Default(null) |
53 | @Is( | 53 | @Is( |
54 | 'UserNotificationSettingVideoAbuseAsModerator', | 54 | 'UserNotificationSettingAbuseAsModerator', |
55 | value => throwIfNotValid(value, isUserNotificationSettingValid, 'videoAbuseAsModerator') | 55 | value => throwIfNotValid(value, isUserNotificationSettingValid, 'abuseAsModerator') |
56 | ) | 56 | ) |
57 | @Column | 57 | @Column |
58 | videoAbuseAsModerator: UserNotificationSettingValue | 58 | abuseAsModerator: UserNotificationSettingValue |
59 | 59 | ||
60 | @AllowNull(false) | 60 | @AllowNull(false) |
61 | @Default(null) | 61 | @Default(null) |
@@ -166,7 +166,7 @@ export class UserNotificationSettingModel extends Model<UserNotificationSettingM | |||
166 | return { | 166 | return { |
167 | newCommentOnMyVideo: this.newCommentOnMyVideo, | 167 | newCommentOnMyVideo: this.newCommentOnMyVideo, |
168 | newVideoFromSubscription: this.newVideoFromSubscription, | 168 | newVideoFromSubscription: this.newVideoFromSubscription, |
169 | videoAbuseAsModerator: this.videoAbuseAsModerator, | 169 | abuseAsModerator: this.abuseAsModerator, |
170 | videoAutoBlacklistAsModerator: this.videoAutoBlacklistAsModerator, | 170 | videoAutoBlacklistAsModerator: this.videoAutoBlacklistAsModerator, |
171 | blacklistOnMyVideo: this.blacklistOnMyVideo, | 171 | blacklistOnMyVideo: this.blacklistOnMyVideo, |
172 | myVideoPublished: this.myVideoPublished, | 172 | myVideoPublished: this.myVideoPublished, |
diff --git a/server/models/account/user-notification.ts b/server/models/account/user-notification.ts index 30985bb0f..2945bf709 100644 --- a/server/models/account/user-notification.ts +++ b/server/models/account/user-notification.ts | |||
@@ -1,22 +1,24 @@ | |||
1 | import { FindOptions, ModelIndexesOptions, Op, WhereOptions } from 'sequelize' | ||
1 | import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' | 2 | import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' |
3 | import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/types/models/user' | ||
2 | import { UserNotification, UserNotificationType } from '../../../shared' | 4 | import { UserNotification, UserNotificationType } from '../../../shared' |
3 | import { getSort, throwIfNotValid } from '../utils' | ||
4 | import { isBooleanValid } from '../../helpers/custom-validators/misc' | 5 | import { isBooleanValid } from '../../helpers/custom-validators/misc' |
5 | import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications' | 6 | import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications' |
6 | import { UserModel } from './user' | 7 | import { AbuseModel } from '../abuse/abuse' |
7 | import { VideoModel } from '../video/video' | 8 | import { VideoAbuseModel } from '../abuse/video-abuse' |
8 | import { VideoCommentModel } from '../video/video-comment' | 9 | import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse' |
9 | import { FindOptions, ModelIndexesOptions, Op, WhereOptions } from 'sequelize' | ||
10 | import { VideoChannelModel } from '../video/video-channel' | ||
11 | import { AccountModel } from './account' | ||
12 | import { VideoAbuseModel } from '../video/video-abuse' | ||
13 | import { VideoBlacklistModel } from '../video/video-blacklist' | ||
14 | import { VideoImportModel } from '../video/video-import' | ||
15 | import { ActorModel } from '../activitypub/actor' | 10 | import { ActorModel } from '../activitypub/actor' |
16 | import { ActorFollowModel } from '../activitypub/actor-follow' | 11 | import { ActorFollowModel } from '../activitypub/actor-follow' |
17 | import { AvatarModel } from '../avatar/avatar' | 12 | import { AvatarModel } from '../avatar/avatar' |
18 | import { ServerModel } from '../server/server' | 13 | import { ServerModel } from '../server/server' |
19 | import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/types/models/user' | 14 | import { getSort, throwIfNotValid } from '../utils' |
15 | import { VideoModel } from '../video/video' | ||
16 | import { VideoBlacklistModel } from '../video/video-blacklist' | ||
17 | import { VideoChannelModel } from '../video/video-channel' | ||
18 | import { VideoCommentModel } from '../video/video-comment' | ||
19 | import { VideoImportModel } from '../video/video-import' | ||
20 | import { AccountModel } from './account' | ||
21 | import { UserModel } from './user' | ||
20 | 22 | ||
21 | enum ScopeNames { | 23 | enum ScopeNames { |
22 | WITH_ALL = 'WITH_ALL' | 24 | WITH_ALL = 'WITH_ALL' |
@@ -87,9 +89,41 @@ function buildAccountInclude (required: boolean, withActor = false) { | |||
87 | 89 | ||
88 | { | 90 | { |
89 | attributes: [ 'id' ], | 91 | attributes: [ 'id' ], |
90 | model: VideoAbuseModel.unscoped(), | 92 | model: AbuseModel.unscoped(), |
91 | required: false, | 93 | required: false, |
92 | include: [ buildVideoInclude(true) ] | 94 | include: [ |
95 | { | ||
96 | attributes: [ 'id' ], | ||
97 | model: VideoAbuseModel.unscoped(), | ||
98 | required: false, | ||
99 | include: [ buildVideoInclude(true) ] | ||
100 | }, | ||
101 | { | ||
102 | attributes: [ 'id' ], | ||
103 | model: VideoCommentAbuseModel.unscoped(), | ||
104 | required: false, | ||
105 | include: [ | ||
106 | { | ||
107 | attributes: [ 'id', 'originCommentId' ], | ||
108 | model: VideoCommentModel, | ||
109 | required: true, | ||
110 | include: [ | ||
111 | { | ||
112 | attributes: [ 'id', 'name', 'uuid' ], | ||
113 | model: VideoModel.unscoped(), | ||
114 | required: true | ||
115 | } | ||
116 | ] | ||
117 | } | ||
118 | ] | ||
119 | }, | ||
120 | { | ||
121 | model: AccountModel, | ||
122 | as: 'FlaggedAccount', | ||
123 | required: true, | ||
124 | include: [ buildActorWithAvatarInclude() ] | ||
125 | } | ||
126 | ] | ||
93 | }, | 127 | }, |
94 | 128 | ||
95 | { | 129 | { |
@@ -179,9 +213,9 @@ function buildAccountInclude (required: boolean, withActor = false) { | |||
179 | } | 213 | } |
180 | }, | 214 | }, |
181 | { | 215 | { |
182 | fields: [ 'videoAbuseId' ], | 216 | fields: [ 'abuseId' ], |
183 | where: { | 217 | where: { |
184 | videoAbuseId: { | 218 | abuseId: { |
185 | [Op.ne]: null | 219 | [Op.ne]: null |
186 | } | 220 | } |
187 | } | 221 | } |
@@ -276,17 +310,17 @@ export class UserNotificationModel extends Model<UserNotificationModel> { | |||
276 | }) | 310 | }) |
277 | Comment: VideoCommentModel | 311 | Comment: VideoCommentModel |
278 | 312 | ||
279 | @ForeignKey(() => VideoAbuseModel) | 313 | @ForeignKey(() => AbuseModel) |
280 | @Column | 314 | @Column |
281 | videoAbuseId: number | 315 | abuseId: number |
282 | 316 | ||
283 | @BelongsTo(() => VideoAbuseModel, { | 317 | @BelongsTo(() => AbuseModel, { |
284 | foreignKey: { | 318 | foreignKey: { |
285 | allowNull: true | 319 | allowNull: true |
286 | }, | 320 | }, |
287 | onDelete: 'cascade' | 321 | onDelete: 'cascade' |
288 | }) | 322 | }) |
289 | VideoAbuse: VideoAbuseModel | 323 | Abuse: AbuseModel |
290 | 324 | ||
291 | @ForeignKey(() => VideoBlacklistModel) | 325 | @ForeignKey(() => VideoBlacklistModel) |
292 | @Column | 326 | @Column |
@@ -397,10 +431,7 @@ export class UserNotificationModel extends Model<UserNotificationModel> { | |||
397 | video: this.formatVideo(this.Comment.Video) | 431 | video: this.formatVideo(this.Comment.Video) |
398 | } : undefined | 432 | } : undefined |
399 | 433 | ||
400 | const videoAbuse = this.VideoAbuse ? { | 434 | const abuse = this.Abuse ? this.formatAbuse(this.Abuse) : undefined |
401 | id: this.VideoAbuse.id, | ||
402 | video: this.formatVideo(this.VideoAbuse.Video) | ||
403 | } : undefined | ||
404 | 435 | ||
405 | const videoBlacklist = this.VideoBlacklist ? { | 436 | const videoBlacklist = this.VideoBlacklist ? { |
406 | id: this.VideoBlacklist.id, | 437 | id: this.VideoBlacklist.id, |
@@ -439,7 +470,7 @@ export class UserNotificationModel extends Model<UserNotificationModel> { | |||
439 | video, | 470 | video, |
440 | videoImport, | 471 | videoImport, |
441 | comment, | 472 | comment, |
442 | videoAbuse, | 473 | abuse, |
443 | videoBlacklist, | 474 | videoBlacklist, |
444 | account, | 475 | account, |
445 | actorFollow, | 476 | actorFollow, |
@@ -456,6 +487,29 @@ export class UserNotificationModel extends Model<UserNotificationModel> { | |||
456 | } | 487 | } |
457 | } | 488 | } |
458 | 489 | ||
490 | formatAbuse (this: UserNotificationModelForApi, abuse: UserNotificationIncludes.AbuseInclude) { | ||
491 | const commentAbuse = abuse.VideoCommentAbuse?.VideoComment ? { | ||
492 | threadId: abuse.VideoCommentAbuse.VideoComment.getThreadId(), | ||
493 | |||
494 | video: { | ||
495 | id: abuse.VideoCommentAbuse.VideoComment.Video.id, | ||
496 | name: abuse.VideoCommentAbuse.VideoComment.Video.name, | ||
497 | uuid: abuse.VideoCommentAbuse.VideoComment.Video.uuid | ||
498 | } | ||
499 | } : undefined | ||
500 | |||
501 | const videoAbuse = abuse.VideoAbuse?.Video ? this.formatVideo(abuse.VideoAbuse.Video) : undefined | ||
502 | |||
503 | const accountAbuse = (!commentAbuse && !videoAbuse) ? this.formatActor(abuse.FlaggedAccount) : undefined | ||
504 | |||
505 | return { | ||
506 | id: abuse.id, | ||
507 | video: videoAbuse, | ||
508 | comment: commentAbuse, | ||
509 | account: accountAbuse | ||
510 | } | ||
511 | } | ||
512 | |||
459 | formatActor ( | 513 | formatActor ( |
460 | this: UserNotificationModelForApi, | 514 | this: UserNotificationModelForApi, |
461 | accountOrChannel: UserNotificationIncludes.AccountIncludeActor | UserNotificationIncludes.VideoChannelIncludeActor | 515 | accountOrChannel: UserNotificationIncludes.AccountIncludeActor | UserNotificationIncludes.VideoChannelIncludeActor |
diff --git a/server/models/account/user.ts b/server/models/account/user.ts index de193131a..5f45f8e7c 100644 --- a/server/models/account/user.ts +++ b/server/models/account/user.ts | |||
@@ -19,7 +19,7 @@ import { | |||
19 | Table, | 19 | Table, |
20 | UpdatedAt | 20 | UpdatedAt |
21 | } from 'sequelize-typescript' | 21 | } from 'sequelize-typescript' |
22 | import { hasUserRight, MyUser, USER_ROLE_LABELS, UserRight, VideoAbuseState, VideoPlaylistType, VideoPrivacy } from '../../../shared' | 22 | import { hasUserRight, MyUser, USER_ROLE_LABELS, UserRight, AbuseState, VideoPlaylistType, VideoPrivacy } from '../../../shared' |
23 | import { User, UserRole } from '../../../shared/models/users' | 23 | import { User, UserRole } from '../../../shared/models/users' |
24 | import { | 24 | import { |
25 | isNoInstanceConfigWarningModal, | 25 | isNoInstanceConfigWarningModal, |
@@ -168,28 +168,26 @@ enum ScopeNames { | |||
168 | '(' + | 168 | '(' + |
169 | `SELECT concat_ws(':', "abuses", "acceptedAbuses") ` + | 169 | `SELECT concat_ws(':', "abuses", "acceptedAbuses") ` + |
170 | 'FROM (' + | 170 | 'FROM (' + |
171 | 'SELECT COUNT("videoAbuse"."id") AS "abuses", ' + | 171 | 'SELECT COUNT("abuse"."id") AS "abuses", ' + |
172 | `COUNT("videoAbuse"."id") FILTER (WHERE "videoAbuse"."state" = ${VideoAbuseState.ACCEPTED}) AS "acceptedAbuses" ` + | 172 | `COUNT("abuse"."id") FILTER (WHERE "abuse"."state" = ${AbuseState.ACCEPTED}) AS "acceptedAbuses" ` + |
173 | 'FROM "videoAbuse" ' + | 173 | 'FROM "abuse" ' + |
174 | 'INNER JOIN "video" ON "videoAbuse"."videoId" = "video"."id" ' + | 174 | 'INNER JOIN "account" ON "account"."id" = "abuse"."flaggedAccountId" ' + |
175 | 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + | ||
176 | 'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId" ' + | ||
177 | 'WHERE "account"."userId" = "UserModel"."id"' + | 175 | 'WHERE "account"."userId" = "UserModel"."id"' + |
178 | ') t' + | 176 | ') t' + |
179 | ')' | 177 | ')' |
180 | ), | 178 | ), |
181 | 'videoAbusesCount' | 179 | 'abusesCount' |
182 | ], | 180 | ], |
183 | [ | 181 | [ |
184 | literal( | 182 | literal( |
185 | '(' + | 183 | '(' + |
186 | 'SELECT COUNT("videoAbuse"."id") ' + | 184 | 'SELECT COUNT("abuse"."id") ' + |
187 | 'FROM "videoAbuse" ' + | 185 | 'FROM "abuse" ' + |
188 | 'INNER JOIN "account" ON "account"."id" = "videoAbuse"."reporterAccountId" ' + | 186 | 'INNER JOIN "account" ON "account"."id" = "abuse"."reporterAccountId" ' + |
189 | 'WHERE "account"."userId" = "UserModel"."id"' + | 187 | 'WHERE "account"."userId" = "UserModel"."id"' + |
190 | ')' | 188 | ')' |
191 | ), | 189 | ), |
192 | 'videoAbusesCreatedCount' | 190 | 'abusesCreatedCount' |
193 | ], | 191 | ], |
194 | [ | 192 | [ |
195 | literal( | 193 | literal( |
@@ -780,8 +778,8 @@ export class UserModel extends Model<UserModel> { | |||
780 | const videoQuotaUsed = this.get('videoQuotaUsed') | 778 | const videoQuotaUsed = this.get('videoQuotaUsed') |
781 | const videoQuotaUsedDaily = this.get('videoQuotaUsedDaily') | 779 | const videoQuotaUsedDaily = this.get('videoQuotaUsedDaily') |
782 | const videosCount = this.get('videosCount') | 780 | const videosCount = this.get('videosCount') |
783 | const [ videoAbusesCount, videoAbusesAcceptedCount ] = (this.get('videoAbusesCount') as string || ':').split(':') | 781 | const [ abusesCount, abusesAcceptedCount ] = (this.get('abusesCount') as string || ':').split(':') |
784 | const videoAbusesCreatedCount = this.get('videoAbusesCreatedCount') | 782 | const abusesCreatedCount = this.get('abusesCreatedCount') |
785 | const videoCommentsCount = this.get('videoCommentsCount') | 783 | const videoCommentsCount = this.get('videoCommentsCount') |
786 | 784 | ||
787 | const json: User = { | 785 | const json: User = { |
@@ -815,14 +813,14 @@ export class UserModel extends Model<UserModel> { | |||
815 | videosCount: videosCount !== undefined | 813 | videosCount: videosCount !== undefined |
816 | ? parseInt(videosCount + '', 10) | 814 | ? parseInt(videosCount + '', 10) |
817 | : undefined, | 815 | : undefined, |
818 | videoAbusesCount: videoAbusesCount | 816 | abusesCount: abusesCount |
819 | ? parseInt(videoAbusesCount, 10) | 817 | ? parseInt(abusesCount, 10) |
820 | : undefined, | 818 | : undefined, |
821 | videoAbusesAcceptedCount: videoAbusesAcceptedCount | 819 | abusesAcceptedCount: abusesAcceptedCount |
822 | ? parseInt(videoAbusesAcceptedCount, 10) | 820 | ? parseInt(abusesAcceptedCount, 10) |
823 | : undefined, | 821 | : undefined, |
824 | videoAbusesCreatedCount: videoAbusesCreatedCount !== undefined | 822 | abusesCreatedCount: abusesCreatedCount !== undefined |
825 | ? parseInt(videoAbusesCreatedCount + '', 10) | 823 | ? parseInt(abusesCreatedCount + '', 10) |
826 | : undefined, | 824 | : undefined, |
827 | videoCommentsCount: videoCommentsCount !== undefined | 825 | videoCommentsCount: videoCommentsCount !== undefined |
828 | ? parseInt(videoCommentsCount + '', 10) | 826 | ? parseInt(videoCommentsCount + '', 10) |
diff --git a/server/models/server/server-blocklist.ts b/server/models/server/server-blocklist.ts index 30f0525e5..68cd72ee7 100644 --- a/server/models/server/server-blocklist.ts +++ b/server/models/server/server-blocklist.ts | |||
@@ -1,11 +1,11 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
2 | import { Op } from 'sequelize' | ||
1 | import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' | 3 | import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' |
4 | import { MServerBlocklist, MServerBlocklistAccountServer, MServerBlocklistFormattable } from '@server/types/models' | ||
5 | import { ServerBlock } from '@shared/models' | ||
2 | import { AccountModel } from '../account/account' | 6 | import { AccountModel } from '../account/account' |
3 | import { ServerModel } from './server' | ||
4 | import { ServerBlock } from '../../../shared/models/blocklist' | ||
5 | import { getSort, searchAttribute } from '../utils' | 7 | import { getSort, searchAttribute } from '../utils' |
6 | import * as Bluebird from 'bluebird' | 8 | import { ServerModel } from './server' |
7 | import { MServerBlocklist, MServerBlocklistAccountServer, MServerBlocklistFormattable } from '@server/types/models' | ||
8 | import { Op } from 'sequelize' | ||
9 | 9 | ||
10 | enum ScopeNames { | 10 | enum ScopeNames { |
11 | WITH_ACCOUNT = 'WITH_ACCOUNT', | 11 | WITH_ACCOUNT = 'WITH_ACCOUNT', |
diff --git a/server/models/video/video-abuse.ts b/server/models/video/video-abuse.ts deleted file mode 100644 index 1319332f0..000000000 --- a/server/models/video/video-abuse.ts +++ /dev/null | |||
@@ -1,479 +0,0 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
2 | import { literal, Op } from 'sequelize' | ||
3 | import { | ||
4 | AllowNull, | ||
5 | BelongsTo, | ||
6 | Column, | ||
7 | CreatedAt, | ||
8 | DataType, | ||
9 | Default, | ||
10 | ForeignKey, | ||
11 | Is, | ||
12 | Model, | ||
13 | Scopes, | ||
14 | Table, | ||
15 | UpdatedAt | ||
16 | } from 'sequelize-typescript' | ||
17 | import { VideoAbuseVideoIs } from '@shared/models/videos/abuse/video-abuse-video-is.type' | ||
18 | import { | ||
19 | VideoAbuseState, | ||
20 | VideoDetails, | ||
21 | VideoAbusePredefinedReasons, | ||
22 | VideoAbusePredefinedReasonsString, | ||
23 | videoAbusePredefinedReasonsMap | ||
24 | } from '../../../shared' | ||
25 | import { VideoAbuseObject } from '../../../shared/models/activitypub/objects' | ||
26 | import { VideoAbuse } from '../../../shared/models/videos' | ||
27 | import { | ||
28 | isVideoAbuseModerationCommentValid, | ||
29 | isVideoAbuseReasonValid, | ||
30 | isVideoAbuseStateValid | ||
31 | } from '../../helpers/custom-validators/video-abuses' | ||
32 | import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants' | ||
33 | import { MUserAccountId, MVideoAbuse, MVideoAbuseFormattable, MVideoAbuseVideo } from '../../types/models' | ||
34 | import { AccountModel } from '../account/account' | ||
35 | import { buildBlockedAccountSQL, getSort, searchAttribute, throwIfNotValid } from '../utils' | ||
36 | import { ThumbnailModel } from './thumbnail' | ||
37 | import { VideoModel } from './video' | ||
38 | import { VideoBlacklistModel } from './video-blacklist' | ||
39 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel' | ||
40 | import { invert } from 'lodash' | ||
41 | |||
42 | export enum ScopeNames { | ||
43 | FOR_API = 'FOR_API' | ||
44 | } | ||
45 | |||
46 | @Scopes(() => ({ | ||
47 | [ScopeNames.FOR_API]: (options: { | ||
48 | // search | ||
49 | search?: string | ||
50 | searchReporter?: string | ||
51 | searchReportee?: string | ||
52 | searchVideo?: string | ||
53 | searchVideoChannel?: string | ||
54 | |||
55 | // filters | ||
56 | id?: number | ||
57 | predefinedReasonId?: number | ||
58 | |||
59 | state?: VideoAbuseState | ||
60 | videoIs?: VideoAbuseVideoIs | ||
61 | |||
62 | // accountIds | ||
63 | serverAccountId: number | ||
64 | userAccountId: number | ||
65 | }) => { | ||
66 | const where = { | ||
67 | reporterAccountId: { | ||
68 | [Op.notIn]: literal('(' + buildBlockedAccountSQL([ options.serverAccountId, options.userAccountId ]) + ')') | ||
69 | } | ||
70 | } | ||
71 | |||
72 | if (options.search) { | ||
73 | Object.assign(where, { | ||
74 | [Op.or]: [ | ||
75 | { | ||
76 | [Op.and]: [ | ||
77 | { videoId: { [Op.not]: null } }, | ||
78 | searchAttribute(options.search, '$Video.name$') | ||
79 | ] | ||
80 | }, | ||
81 | { | ||
82 | [Op.and]: [ | ||
83 | { videoId: { [Op.not]: null } }, | ||
84 | searchAttribute(options.search, '$Video.VideoChannel.name$') | ||
85 | ] | ||
86 | }, | ||
87 | { | ||
88 | [Op.and]: [ | ||
89 | { deletedVideo: { [Op.not]: null } }, | ||
90 | { deletedVideo: searchAttribute(options.search, 'name') } | ||
91 | ] | ||
92 | }, | ||
93 | { | ||
94 | [Op.and]: [ | ||
95 | { deletedVideo: { [Op.not]: null } }, | ||
96 | { deletedVideo: { channel: searchAttribute(options.search, 'displayName') } } | ||
97 | ] | ||
98 | }, | ||
99 | searchAttribute(options.search, '$Account.name$') | ||
100 | ] | ||
101 | }) | ||
102 | } | ||
103 | |||
104 | if (options.id) Object.assign(where, { id: options.id }) | ||
105 | if (options.state) Object.assign(where, { state: options.state }) | ||
106 | |||
107 | if (options.videoIs === 'deleted') { | ||
108 | Object.assign(where, { | ||
109 | deletedVideo: { | ||
110 | [Op.not]: null | ||
111 | } | ||
112 | }) | ||
113 | } | ||
114 | |||
115 | if (options.predefinedReasonId) { | ||
116 | Object.assign(where, { | ||
117 | predefinedReasons: { | ||
118 | [Op.contains]: [ options.predefinedReasonId ] | ||
119 | } | ||
120 | }) | ||
121 | } | ||
122 | |||
123 | const onlyBlacklisted = options.videoIs === 'blacklisted' | ||
124 | |||
125 | return { | ||
126 | attributes: { | ||
127 | include: [ | ||
128 | [ | ||
129 | // we don't care about this count for deleted videos, so there are not included | ||
130 | literal( | ||
131 | '(' + | ||
132 | 'SELECT count(*) ' + | ||
133 | 'FROM "videoAbuse" ' + | ||
134 | 'WHERE "videoId" = "VideoAbuseModel"."videoId" ' + | ||
135 | ')' | ||
136 | ), | ||
137 | 'countReportsForVideo' | ||
138 | ], | ||
139 | [ | ||
140 | // we don't care about this count for deleted videos, so there are not included | ||
141 | literal( | ||
142 | '(' + | ||
143 | 'SELECT t.nth ' + | ||
144 | 'FROM ( ' + | ||
145 | 'SELECT id, ' + | ||
146 | 'row_number() OVER (PARTITION BY "videoId" ORDER BY "createdAt") AS nth ' + | ||
147 | 'FROM "videoAbuse" ' + | ||
148 | ') t ' + | ||
149 | 'WHERE t.id = "VideoAbuseModel".id ' + | ||
150 | ')' | ||
151 | ), | ||
152 | 'nthReportForVideo' | ||
153 | ], | ||
154 | [ | ||
155 | literal( | ||
156 | '(' + | ||
157 | 'SELECT count("videoAbuse"."id") ' + | ||
158 | 'FROM "videoAbuse" ' + | ||
159 | 'INNER JOIN "video" ON "video"."id" = "videoAbuse"."videoId" ' + | ||
160 | 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + | ||
161 | 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' + | ||
162 | 'WHERE "account"."id" = "VideoAbuseModel"."reporterAccountId" ' + | ||
163 | ')' | ||
164 | ), | ||
165 | 'countReportsForReporter__video' | ||
166 | ], | ||
167 | [ | ||
168 | literal( | ||
169 | '(' + | ||
170 | 'SELECT count(DISTINCT "videoAbuse"."id") ' + | ||
171 | 'FROM "videoAbuse" ' + | ||
172 | `WHERE CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = "VideoAbuseModel"."reporterAccountId" ` + | ||
173 | ')' | ||
174 | ), | ||
175 | 'countReportsForReporter__deletedVideo' | ||
176 | ], | ||
177 | [ | ||
178 | literal( | ||
179 | '(' + | ||
180 | 'SELECT count(DISTINCT "videoAbuse"."id") ' + | ||
181 | 'FROM "videoAbuse" ' + | ||
182 | 'INNER JOIN "video" ON "video"."id" = "videoAbuse"."videoId" ' + | ||
183 | 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + | ||
184 | 'INNER JOIN "account" ON ' + | ||
185 | '"videoChannel"."accountId" = "Video->VideoChannel"."accountId" ' + | ||
186 | `OR "videoChannel"."accountId" = CAST("VideoAbuseModel"."deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) ` + | ||
187 | ')' | ||
188 | ), | ||
189 | 'countReportsForReportee__video' | ||
190 | ], | ||
191 | [ | ||
192 | literal( | ||
193 | '(' + | ||
194 | 'SELECT count(DISTINCT "videoAbuse"."id") ' + | ||
195 | 'FROM "videoAbuse" ' + | ||
196 | `WHERE CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = "Video->VideoChannel"."accountId" ` + | ||
197 | `OR CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = ` + | ||
198 | `CAST("VideoAbuseModel"."deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) ` + | ||
199 | ')' | ||
200 | ), | ||
201 | 'countReportsForReportee__deletedVideo' | ||
202 | ] | ||
203 | ] | ||
204 | }, | ||
205 | include: [ | ||
206 | { | ||
207 | model: AccountModel, | ||
208 | required: true, | ||
209 | where: searchAttribute(options.searchReporter, 'name') | ||
210 | }, | ||
211 | { | ||
212 | model: VideoModel, | ||
213 | required: !!(onlyBlacklisted || options.searchVideo || options.searchReportee || options.searchVideoChannel), | ||
214 | where: searchAttribute(options.searchVideo, 'name'), | ||
215 | include: [ | ||
216 | { | ||
217 | model: ThumbnailModel | ||
218 | }, | ||
219 | { | ||
220 | model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, { withAccount: true } as SummaryOptions ] }), | ||
221 | where: searchAttribute(options.searchVideoChannel, 'name'), | ||
222 | include: [ | ||
223 | { | ||
224 | model: AccountModel, | ||
225 | where: searchAttribute(options.searchReportee, 'name') | ||
226 | } | ||
227 | ] | ||
228 | }, | ||
229 | { | ||
230 | attributes: [ 'id', 'reason', 'unfederated' ], | ||
231 | model: VideoBlacklistModel, | ||
232 | required: onlyBlacklisted | ||
233 | } | ||
234 | ] | ||
235 | } | ||
236 | ], | ||
237 | where | ||
238 | } | ||
239 | } | ||
240 | })) | ||
241 | @Table({ | ||
242 | tableName: 'videoAbuse', | ||
243 | indexes: [ | ||
244 | { | ||
245 | fields: [ 'videoId' ] | ||
246 | }, | ||
247 | { | ||
248 | fields: [ 'reporterAccountId' ] | ||
249 | } | ||
250 | ] | ||
251 | }) | ||
252 | export class VideoAbuseModel extends Model<VideoAbuseModel> { | ||
253 | |||
254 | @AllowNull(false) | ||
255 | @Default(null) | ||
256 | @Is('VideoAbuseReason', value => throwIfNotValid(value, isVideoAbuseReasonValid, 'reason')) | ||
257 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.REASON.max)) | ||
258 | reason: string | ||
259 | |||
260 | @AllowNull(false) | ||
261 | @Default(null) | ||
262 | @Is('VideoAbuseState', value => throwIfNotValid(value, isVideoAbuseStateValid, 'state')) | ||
263 | @Column | ||
264 | state: VideoAbuseState | ||
265 | |||
266 | @AllowNull(true) | ||
267 | @Default(null) | ||
268 | @Is('VideoAbuseModerationComment', value => throwIfNotValid(value, isVideoAbuseModerationCommentValid, 'moderationComment', true)) | ||
269 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.MODERATION_COMMENT.max)) | ||
270 | moderationComment: string | ||
271 | |||
272 | @AllowNull(true) | ||
273 | @Default(null) | ||
274 | @Column(DataType.JSONB) | ||
275 | deletedVideo: VideoDetails | ||
276 | |||
277 | @AllowNull(true) | ||
278 | @Default(null) | ||
279 | @Column(DataType.ARRAY(DataType.INTEGER)) | ||
280 | predefinedReasons: VideoAbusePredefinedReasons[] | ||
281 | |||
282 | @AllowNull(true) | ||
283 | @Default(null) | ||
284 | @Column | ||
285 | startAt: number | ||
286 | |||
287 | @AllowNull(true) | ||
288 | @Default(null) | ||
289 | @Column | ||
290 | endAt: number | ||
291 | |||
292 | @CreatedAt | ||
293 | createdAt: Date | ||
294 | |||
295 | @UpdatedAt | ||
296 | updatedAt: Date | ||
297 | |||
298 | @ForeignKey(() => AccountModel) | ||
299 | @Column | ||
300 | reporterAccountId: number | ||
301 | |||
302 | @BelongsTo(() => AccountModel, { | ||
303 | foreignKey: { | ||
304 | allowNull: true | ||
305 | }, | ||
306 | onDelete: 'set null' | ||
307 | }) | ||
308 | Account: AccountModel | ||
309 | |||
310 | @ForeignKey(() => VideoModel) | ||
311 | @Column | ||
312 | videoId: number | ||
313 | |||
314 | @BelongsTo(() => VideoModel, { | ||
315 | foreignKey: { | ||
316 | allowNull: true | ||
317 | }, | ||
318 | onDelete: 'set null' | ||
319 | }) | ||
320 | Video: VideoModel | ||
321 | |||
322 | static loadByIdAndVideoId (id: number, videoId?: number, uuid?: string): Bluebird<MVideoAbuse> { | ||
323 | const videoAttributes = {} | ||
324 | if (videoId) videoAttributes['videoId'] = videoId | ||
325 | if (uuid) videoAttributes['deletedVideo'] = { uuid } | ||
326 | |||
327 | const query = { | ||
328 | where: { | ||
329 | id, | ||
330 | ...videoAttributes | ||
331 | } | ||
332 | } | ||
333 | return VideoAbuseModel.findOne(query) | ||
334 | } | ||
335 | |||
336 | static listForApi (parameters: { | ||
337 | start: number | ||
338 | count: number | ||
339 | sort: string | ||
340 | |||
341 | serverAccountId: number | ||
342 | user?: MUserAccountId | ||
343 | |||
344 | id?: number | ||
345 | predefinedReason?: VideoAbusePredefinedReasonsString | ||
346 | state?: VideoAbuseState | ||
347 | videoIs?: VideoAbuseVideoIs | ||
348 | |||
349 | search?: string | ||
350 | searchReporter?: string | ||
351 | searchReportee?: string | ||
352 | searchVideo?: string | ||
353 | searchVideoChannel?: string | ||
354 | }) { | ||
355 | const { | ||
356 | start, | ||
357 | count, | ||
358 | sort, | ||
359 | search, | ||
360 | user, | ||
361 | serverAccountId, | ||
362 | state, | ||
363 | videoIs, | ||
364 | predefinedReason, | ||
365 | searchReportee, | ||
366 | searchVideo, | ||
367 | searchVideoChannel, | ||
368 | searchReporter, | ||
369 | id | ||
370 | } = parameters | ||
371 | |||
372 | const userAccountId = user ? user.Account.id : undefined | ||
373 | const predefinedReasonId = predefinedReason ? videoAbusePredefinedReasonsMap[predefinedReason] : undefined | ||
374 | |||
375 | const query = { | ||
376 | offset: start, | ||
377 | limit: count, | ||
378 | order: getSort(sort), | ||
379 | col: 'VideoAbuseModel.id', | ||
380 | distinct: true | ||
381 | } | ||
382 | |||
383 | const filters = { | ||
384 | id, | ||
385 | predefinedReasonId, | ||
386 | search, | ||
387 | state, | ||
388 | videoIs, | ||
389 | searchReportee, | ||
390 | searchVideo, | ||
391 | searchVideoChannel, | ||
392 | searchReporter, | ||
393 | serverAccountId, | ||
394 | userAccountId | ||
395 | } | ||
396 | |||
397 | return VideoAbuseModel | ||
398 | .scope([ | ||
399 | { method: [ ScopeNames.FOR_API, filters ] } | ||
400 | ]) | ||
401 | .findAndCountAll(query) | ||
402 | .then(({ rows, count }) => { | ||
403 | return { total: count, data: rows } | ||
404 | }) | ||
405 | } | ||
406 | |||
407 | toFormattedJSON (this: MVideoAbuseFormattable): VideoAbuse { | ||
408 | const predefinedReasons = VideoAbuseModel.getPredefinedReasonsStrings(this.predefinedReasons) | ||
409 | const countReportsForVideo = this.get('countReportsForVideo') as number | ||
410 | const nthReportForVideo = this.get('nthReportForVideo') as number | ||
411 | const countReportsForReporterVideo = this.get('countReportsForReporter__video') as number | ||
412 | const countReportsForReporterDeletedVideo = this.get('countReportsForReporter__deletedVideo') as number | ||
413 | const countReportsForReporteeVideo = this.get('countReportsForReportee__video') as number | ||
414 | const countReportsForReporteeDeletedVideo = this.get('countReportsForReportee__deletedVideo') as number | ||
415 | |||
416 | const video = this.Video | ||
417 | ? this.Video | ||
418 | : this.deletedVideo | ||
419 | |||
420 | return { | ||
421 | id: this.id, | ||
422 | reason: this.reason, | ||
423 | predefinedReasons, | ||
424 | reporterAccount: this.Account.toFormattedJSON(), | ||
425 | state: { | ||
426 | id: this.state, | ||
427 | label: VideoAbuseModel.getStateLabel(this.state) | ||
428 | }, | ||
429 | moderationComment: this.moderationComment, | ||
430 | video: { | ||
431 | id: video.id, | ||
432 | uuid: video.uuid, | ||
433 | name: video.name, | ||
434 | nsfw: video.nsfw, | ||
435 | deleted: !this.Video, | ||
436 | blacklisted: this.Video?.isBlacklisted() || false, | ||
437 | thumbnailPath: this.Video?.getMiniatureStaticPath(), | ||
438 | channel: this.Video?.VideoChannel.toFormattedJSON() || this.deletedVideo?.channel | ||
439 | }, | ||
440 | createdAt: this.createdAt, | ||
441 | updatedAt: this.updatedAt, | ||
442 | startAt: this.startAt, | ||
443 | endAt: this.endAt, | ||
444 | count: countReportsForVideo || 0, | ||
445 | nth: nthReportForVideo || 0, | ||
446 | countReportsForReporter: (countReportsForReporterVideo || 0) + (countReportsForReporterDeletedVideo || 0), | ||
447 | countReportsForReportee: (countReportsForReporteeVideo || 0) + (countReportsForReporteeDeletedVideo || 0) | ||
448 | } | ||
449 | } | ||
450 | |||
451 | toActivityPubObject (this: MVideoAbuseVideo): VideoAbuseObject { | ||
452 | const predefinedReasons = VideoAbuseModel.getPredefinedReasonsStrings(this.predefinedReasons) | ||
453 | |||
454 | const startAt = this.startAt | ||
455 | const endAt = this.endAt | ||
456 | |||
457 | return { | ||
458 | type: 'Flag' as 'Flag', | ||
459 | content: this.reason, | ||
460 | object: this.Video.url, | ||
461 | tag: predefinedReasons.map(r => ({ | ||
462 | type: 'Hashtag' as 'Hashtag', | ||
463 | name: r | ||
464 | })), | ||
465 | startAt, | ||
466 | endAt | ||
467 | } | ||
468 | } | ||
469 | |||
470 | private static getStateLabel (id: number) { | ||
471 | return VIDEO_ABUSE_STATES[id] || 'Unknown' | ||
472 | } | ||
473 | |||
474 | private static getPredefinedReasonsStrings (predefinedReasons: VideoAbusePredefinedReasons[]): VideoAbusePredefinedReasonsString[] { | ||
475 | return (predefinedReasons || []) | ||
476 | .filter(r => r in VideoAbusePredefinedReasons) | ||
477 | .map(r => invert(videoAbusePredefinedReasonsMap)[r] as VideoAbusePredefinedReasonsString) | ||
478 | } | ||
479 | } | ||
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index 9cee64229..03a3cdf81 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts | |||
@@ -61,6 +61,7 @@ type AvailableWithStatsOptions = { | |||
61 | } | 61 | } |
62 | 62 | ||
63 | export type SummaryOptions = { | 63 | export type SummaryOptions = { |
64 | actorRequired?: boolean // Default: true | ||
64 | withAccount?: boolean // Default: false | 65 | withAccount?: boolean // Default: false |
65 | withAccountBlockerIds?: number[] | 66 | withAccountBlockerIds?: number[] |
66 | } | 67 | } |
@@ -121,7 +122,7 @@ export type SummaryOptions = { | |||
121 | { | 122 | { |
122 | attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ], | 123 | attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ], |
123 | model: ActorModel.unscoped(), | 124 | model: ActorModel.unscoped(), |
124 | required: true, | 125 | required: options.actorRequired ?? true, |
125 | include: [ | 126 | include: [ |
126 | { | 127 | { |
127 | attributes: [ 'host' ], | 128 | attributes: [ 'host' ], |
diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts index 90625d987..75b914b8c 100644 --- a/server/models/video/video-comment.ts +++ b/server/models/video/video-comment.ts | |||
@@ -1,7 +1,20 @@ | |||
1 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
2 | import { uniq } from 'lodash' | 2 | import { uniq } from 'lodash' |
3 | import { FindOptions, Op, Order, ScopeOptions, Sequelize, Transaction } from 'sequelize' | 3 | import { FindOptions, Op, Order, ScopeOptions, Sequelize, Transaction } from 'sequelize' |
4 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' | 4 | import { |
5 | AllowNull, | ||
6 | BelongsTo, | ||
7 | Column, | ||
8 | CreatedAt, | ||
9 | DataType, | ||
10 | ForeignKey, | ||
11 | HasMany, | ||
12 | Is, | ||
13 | Model, | ||
14 | Scopes, | ||
15 | Table, | ||
16 | UpdatedAt | ||
17 | } from 'sequelize-typescript' | ||
5 | import { getServerActor } from '@server/models/application/application' | 18 | import { getServerActor } from '@server/models/application/application' |
6 | import { MAccount, MAccountId, MUserAccountId } from '@server/types/models' | 19 | import { MAccount, MAccountId, MUserAccountId } from '@server/types/models' |
7 | import { VideoPrivacy } from '@shared/models' | 20 | import { VideoPrivacy } from '@shared/models' |
@@ -24,6 +37,7 @@ import { | |||
24 | MCommentOwnerVideoReply, | 37 | MCommentOwnerVideoReply, |
25 | MVideoImmutable | 38 | MVideoImmutable |
26 | } from '../../types/models/video' | 39 | } from '../../types/models/video' |
40 | import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse' | ||
27 | import { AccountModel } from '../account/account' | 41 | import { AccountModel } from '../account/account' |
28 | import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor' | 42 | import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor' |
29 | import { buildBlockedAccountSQL, buildLocalAccountIdsIn, getCommentSort, throwIfNotValid } from '../utils' | 43 | import { buildBlockedAccountSQL, buildLocalAccountIdsIn, getCommentSort, throwIfNotValid } from '../utils' |
@@ -224,6 +238,15 @@ export class VideoCommentModel extends Model<VideoCommentModel> { | |||
224 | }) | 238 | }) |
225 | Account: AccountModel | 239 | Account: AccountModel |
226 | 240 | ||
241 | @HasMany(() => VideoCommentAbuseModel, { | ||
242 | foreignKey: { | ||
243 | name: 'videoCommentId', | ||
244 | allowNull: true | ||
245 | }, | ||
246 | onDelete: 'set null' | ||
247 | }) | ||
248 | CommentAbuses: VideoCommentAbuseModel[] | ||
249 | |||
227 | static loadById (id: number, t?: Transaction): Bluebird<MComment> { | 250 | static loadById (id: number, t?: Transaction): Bluebird<MComment> { |
228 | const query: FindOptions = { | 251 | const query: FindOptions = { |
229 | where: { | 252 | where: { |
@@ -632,7 +655,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> { | |||
632 | id: this.id, | 655 | id: this.id, |
633 | url: this.url, | 656 | url: this.url, |
634 | text: this.text, | 657 | text: this.text, |
635 | threadId: this.originCommentId || this.id, | 658 | threadId: this.getThreadId(), |
636 | inReplyToCommentId: this.inReplyToCommentId || null, | 659 | inReplyToCommentId: this.inReplyToCommentId || null, |
637 | videoId: this.videoId, | 660 | videoId: this.videoId, |
638 | createdAt: this.createdAt, | 661 | createdAt: this.createdAt, |
diff --git a/server/models/video/video-query-builder.ts b/server/models/video/video-query-builder.ts index 984b0e6af..466890364 100644 --- a/server/models/video/video-query-builder.ts +++ b/server/models/video/video-query-builder.ts | |||
@@ -327,7 +327,7 @@ function buildListQuery (model: typeof Model, options: BuildVideosQueryOptions) | |||
327 | attributes.push('COALESCE("video"."originallyPublishedAt", "video"."publishedAt") AS "publishedAtForOrder"') | 327 | attributes.push('COALESCE("video"."originallyPublishedAt", "video"."publishedAt") AS "publishedAtForOrder"') |
328 | } | 328 | } |
329 | 329 | ||
330 | order = buildOrder(model, options.sort) | 330 | order = buildOrder(options.sort) |
331 | suffix += `${order} ` | 331 | suffix += `${order} ` |
332 | } | 332 | } |
333 | 333 | ||
@@ -357,7 +357,7 @@ function buildListQuery (model: typeof Model, options: BuildVideosQueryOptions) | |||
357 | return { query, replacements, order } | 357 | return { query, replacements, order } |
358 | } | 358 | } |
359 | 359 | ||
360 | function buildOrder (model: typeof Model, value: string) { | 360 | function buildOrder (value: string) { |
361 | const { direction, field } = buildDirectionAndField(value) | 361 | const { direction, field } = buildDirectionAndField(value) |
362 | if (field.match(/^[a-zA-Z."]+$/) === null) throw new Error('Invalid sort column ' + field) | 362 | if (field.match(/^[a-zA-Z."]+$/) === null) throw new Error('Invalid sort column ' + field) |
363 | 363 | ||
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index e2718300e..43609587c 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
2 | import { remove } from 'fs-extra' | ||
2 | import { maxBy, minBy, pick } from 'lodash' | 3 | import { maxBy, minBy, pick } from 'lodash' |
3 | import { join } from 'path' | 4 | import { join } from 'path' |
4 | import { FindOptions, IncludeOptions, Op, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize' | 5 | import { FindOptions, IncludeOptions, Op, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize' |
@@ -23,10 +24,18 @@ import { | |||
23 | Table, | 24 | Table, |
24 | UpdatedAt | 25 | UpdatedAt |
25 | } from 'sequelize-typescript' | 26 | } from 'sequelize-typescript' |
26 | import { UserRight, VideoPrivacy, VideoState, ResultList } from '../../../shared' | 27 | import { buildNSFWFilter } from '@server/helpers/express-utils' |
28 | import { getPrivaciesForFederation, isPrivacyForFederation } from '@server/helpers/video' | ||
29 | import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths' | ||
30 | import { getServerActor } from '@server/models/application/application' | ||
31 | import { ModelCache } from '@server/models/model-cache' | ||
32 | import { VideoFile } from '@shared/models/videos/video-file.model' | ||
33 | import { ResultList, UserRight, VideoPrivacy, VideoState } from '../../../shared' | ||
27 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' | 34 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' |
28 | import { Video, VideoDetails } from '../../../shared/models/videos' | 35 | import { Video, VideoDetails } from '../../../shared/models/videos' |
36 | import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' | ||
29 | import { VideoFilter } from '../../../shared/models/videos/video-query.type' | 37 | import { VideoFilter } from '../../../shared/models/videos/video-query.type' |
38 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' | ||
30 | import { peertubeTruncate } from '../../helpers/core-utils' | 39 | import { peertubeTruncate } from '../../helpers/core-utils' |
31 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 40 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
32 | import { isBooleanValid } from '../../helpers/custom-validators/misc' | 41 | import { isBooleanValid } from '../../helpers/custom-validators/misc' |
@@ -43,6 +52,7 @@ import { | |||
43 | } from '../../helpers/custom-validators/videos' | 52 | } from '../../helpers/custom-validators/videos' |
44 | import { getVideoFileResolution } from '../../helpers/ffmpeg-utils' | 53 | import { getVideoFileResolution } from '../../helpers/ffmpeg-utils' |
45 | import { logger } from '../../helpers/logger' | 54 | import { logger } from '../../helpers/logger' |
55 | import { CONFIG } from '../../initializers/config' | ||
46 | import { | 56 | import { |
47 | ACTIVITY_PUB, | 57 | ACTIVITY_PUB, |
48 | API_VERSION, | 58 | API_VERSION, |
@@ -59,40 +69,6 @@ import { | |||
59 | WEBSERVER | 69 | WEBSERVER |
60 | } from '../../initializers/constants' | 70 | } from '../../initializers/constants' |
61 | import { sendDeleteVideo } from '../../lib/activitypub/send' | 71 | import { sendDeleteVideo } from '../../lib/activitypub/send' |
62 | import { AccountModel } from '../account/account' | ||
63 | import { AccountVideoRateModel } from '../account/account-video-rate' | ||
64 | import { ActorModel } from '../activitypub/actor' | ||
65 | import { AvatarModel } from '../avatar/avatar' | ||
66 | import { ServerModel } from '../server/server' | ||
67 | import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils' | ||
68 | import { TagModel } from './tag' | ||
69 | import { VideoAbuseModel } from './video-abuse' | ||
70 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel' | ||
71 | import { VideoCommentModel } from './video-comment' | ||
72 | import { VideoFileModel } from './video-file' | ||
73 | import { VideoShareModel } from './video-share' | ||
74 | import { VideoTagModel } from './video-tag' | ||
75 | import { ScheduleVideoUpdateModel } from './schedule-video-update' | ||
76 | import { VideoCaptionModel } from './video-caption' | ||
77 | import { VideoBlacklistModel } from './video-blacklist' | ||
78 | import { remove } from 'fs-extra' | ||
79 | import { VideoViewModel } from './video-view' | ||
80 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' | ||
81 | import { | ||
82 | videoFilesModelToFormattedJSON, | ||
83 | VideoFormattingJSONOptions, | ||
84 | videoModelToActivityPubObject, | ||
85 | videoModelToFormattedDetailsJSON, | ||
86 | videoModelToFormattedJSON | ||
87 | } from './video-format-utils' | ||
88 | import { UserVideoHistoryModel } from '../account/user-video-history' | ||
89 | import { VideoImportModel } from './video-import' | ||
90 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' | ||
91 | import { VideoPlaylistElementModel } from './video-playlist-element' | ||
92 | import { CONFIG } from '../../initializers/config' | ||
93 | import { ThumbnailModel } from './thumbnail' | ||
94 | import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' | ||
95 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' | ||
96 | import { | 72 | import { |
97 | MChannel, | 73 | MChannel, |
98 | MChannelAccountDefault, | 74 | MChannelAccountDefault, |
@@ -118,15 +94,39 @@ import { | |||
118 | MVideoWithFile, | 94 | MVideoWithFile, |
119 | MVideoWithRights | 95 | MVideoWithRights |
120 | } from '../../types/models' | 96 | } from '../../types/models' |
121 | import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../types/models/video/video-file' | ||
122 | import { MThumbnail } from '../../types/models/video/thumbnail' | 97 | import { MThumbnail } from '../../types/models/video/thumbnail' |
123 | import { VideoFile } from '@shared/models/videos/video-file.model' | 98 | import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../types/models/video/video-file' |
124 | import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths' | 99 | import { VideoAbuseModel } from '../abuse/video-abuse' |
125 | import { ModelCache } from '@server/models/model-cache' | 100 | import { AccountModel } from '../account/account' |
101 | import { AccountVideoRateModel } from '../account/account-video-rate' | ||
102 | import { UserVideoHistoryModel } from '../account/user-video-history' | ||
103 | import { ActorModel } from '../activitypub/actor' | ||
104 | import { AvatarModel } from '../avatar/avatar' | ||
105 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' | ||
106 | import { ServerModel } from '../server/server' | ||
107 | import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils' | ||
108 | import { ScheduleVideoUpdateModel } from './schedule-video-update' | ||
109 | import { TagModel } from './tag' | ||
110 | import { ThumbnailModel } from './thumbnail' | ||
111 | import { VideoBlacklistModel } from './video-blacklist' | ||
112 | import { VideoCaptionModel } from './video-caption' | ||
113 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel' | ||
114 | import { VideoCommentModel } from './video-comment' | ||
115 | import { VideoFileModel } from './video-file' | ||
116 | import { | ||
117 | videoFilesModelToFormattedJSON, | ||
118 | VideoFormattingJSONOptions, | ||
119 | videoModelToActivityPubObject, | ||
120 | videoModelToFormattedDetailsJSON, | ||
121 | videoModelToFormattedJSON | ||
122 | } from './video-format-utils' | ||
123 | import { VideoImportModel } from './video-import' | ||
124 | import { VideoPlaylistElementModel } from './video-playlist-element' | ||
126 | import { buildListQuery, BuildVideosQueryOptions, wrapForAPIResults } from './video-query-builder' | 125 | import { buildListQuery, BuildVideosQueryOptions, wrapForAPIResults } from './video-query-builder' |
127 | import { buildNSFWFilter } from '@server/helpers/express-utils' | 126 | import { VideoShareModel } from './video-share' |
128 | import { getServerActor } from '@server/models/application/application' | 127 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' |
129 | import { getPrivaciesForFederation, isPrivacyForFederation } from "@server/helpers/video" | 128 | import { VideoTagModel } from './video-tag' |
129 | import { VideoViewModel } from './video-view' | ||
130 | 130 | ||
131 | export enum ScopeNames { | 131 | export enum ScopeNames { |
132 | AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS', | 132 | AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS', |
@@ -803,14 +803,14 @@ export class VideoModel extends Model<VideoModel> { | |||
803 | static async saveEssentialDataToAbuses (instance: VideoModel, options) { | 803 | static async saveEssentialDataToAbuses (instance: VideoModel, options) { |
804 | const tasks: Promise<any>[] = [] | 804 | const tasks: Promise<any>[] = [] |
805 | 805 | ||
806 | logger.info('Saving video abuses details of video %s.', instance.url) | ||
807 | |||
808 | if (!Array.isArray(instance.VideoAbuses)) { | 806 | if (!Array.isArray(instance.VideoAbuses)) { |
809 | instance.VideoAbuses = await instance.$get('VideoAbuses') | 807 | instance.VideoAbuses = await instance.$get('VideoAbuses') |
810 | 808 | ||
811 | if (instance.VideoAbuses.length === 0) return undefined | 809 | if (instance.VideoAbuses.length === 0) return undefined |
812 | } | 810 | } |
813 | 811 | ||
812 | logger.info('Saving video abuses details of video %s.', instance.url) | ||
813 | |||
814 | const details = instance.toFormattedDetailsJSON() | 814 | const details = instance.toFormattedDetailsJSON() |
815 | 815 | ||
816 | for (const abuse of instance.VideoAbuses) { | 816 | for (const abuse of instance.VideoAbuses) { |