diff options
Diffstat (limited to 'server/models/video/video-comment.ts')
-rw-r--r-- | server/models/video/video-comment.ts | 140 |
1 files changed, 117 insertions, 23 deletions
diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts index f84c1880c..cf6278da7 100644 --- a/server/models/video/video-comment.ts +++ b/server/models/video/video-comment.ts | |||
@@ -1,21 +1,37 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import * as Sequelize from 'sequelize' |
2 | import { | 2 | import { |
3 | AllowNull, BeforeDestroy, BelongsTo, Column, CreatedAt, DataType, ForeignKey, IFindOptions, Is, Model, Scopes, Table, | 3 | AllowNull, |
4 | BeforeDestroy, | ||
5 | BelongsTo, | ||
6 | Column, | ||
7 | CreatedAt, | ||
8 | DataType, | ||
9 | ForeignKey, | ||
10 | IFindOptions, | ||
11 | Is, | ||
12 | Model, | ||
13 | Scopes, | ||
14 | Table, | ||
4 | UpdatedAt | 15 | UpdatedAt |
5 | } from 'sequelize-typescript' | 16 | } from 'sequelize-typescript' |
6 | import { ActivityTagObject } from '../../../shared/models/activitypub/objects/common-objects' | 17 | import { ActivityTagObject } from '../../../shared/models/activitypub/objects/common-objects' |
7 | import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' | 18 | import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' |
8 | import { VideoComment } from '../../../shared/models/videos/video-comment.model' | 19 | import { VideoComment } from '../../../shared/models/videos/video-comment.model' |
9 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 20 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
10 | import { CONSTRAINTS_FIELDS } from '../../initializers' | 21 | import { CONFIG, CONSTRAINTS_FIELDS } from '../../initializers' |
11 | import { sendDeleteVideoComment } from '../../lib/activitypub/send' | 22 | import { sendDeleteVideoComment } from '../../lib/activitypub/send' |
12 | import { AccountModel } from '../account/account' | 23 | import { AccountModel } from '../account/account' |
13 | import { ActorModel } from '../activitypub/actor' | 24 | import { ActorModel } from '../activitypub/actor' |
14 | import { AvatarModel } from '../avatar/avatar' | 25 | import { AvatarModel } from '../avatar/avatar' |
15 | import { ServerModel } from '../server/server' | 26 | import { ServerModel } from '../server/server' |
16 | import { getSort, throwIfNotValid } from '../utils' | 27 | import { buildBlockedAccountSQL, getSort, throwIfNotValid } from '../utils' |
17 | import { VideoModel } from './video' | 28 | import { VideoModel } from './video' |
18 | import { VideoChannelModel } from './video-channel' | 29 | import { VideoChannelModel } from './video-channel' |
30 | import { getServerActor } from '../../helpers/utils' | ||
31 | import { UserModel } from '../account/user' | ||
32 | import { actorNameAlphabet } from '../../helpers/custom-validators/activitypub/actor' | ||
33 | import { regexpCapture } from '../../helpers/regexp' | ||
34 | import { uniq } from 'lodash' | ||
19 | 35 | ||
20 | enum ScopeNames { | 36 | enum ScopeNames { |
21 | WITH_ACCOUNT = 'WITH_ACCOUNT', | 37 | WITH_ACCOUNT = 'WITH_ACCOUNT', |
@@ -25,18 +41,29 @@ enum ScopeNames { | |||
25 | } | 41 | } |
26 | 42 | ||
27 | @Scopes({ | 43 | @Scopes({ |
28 | [ScopeNames.ATTRIBUTES_FOR_API]: { | 44 | [ScopeNames.ATTRIBUTES_FOR_API]: (serverAccountId: number, userAccountId?: number) => { |
29 | attributes: { | 45 | return { |
30 | include: [ | 46 | attributes: { |
31 | [ | 47 | include: [ |
32 | Sequelize.literal( | 48 | [ |
33 | '(SELECT COUNT("replies"."id") ' + | 49 | Sequelize.literal( |
34 | 'FROM "videoComment" AS "replies" ' + | 50 | '(' + |
35 | 'WHERE "replies"."originCommentId" = "VideoCommentModel"."id")' | 51 | 'WITH "blocklist" AS (' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')' + |
36 | ), | 52 | 'SELECT COUNT("replies"."id") - (' + |
37 | 'totalReplies' | 53 | 'SELECT COUNT("replies"."id") ' + |
54 | 'FROM "videoComment" AS "replies" ' + | ||
55 | 'WHERE "replies"."originCommentId" = "VideoCommentModel"."id" ' + | ||
56 | 'AND "accountId" IN (SELECT "id" FROM "blocklist")' + | ||
57 | ')' + | ||
58 | 'FROM "videoComment" AS "replies" ' + | ||
59 | 'WHERE "replies"."originCommentId" = "VideoCommentModel"."id" ' + | ||
60 | 'AND "accountId" NOT IN (SELECT "id" FROM "blocklist")' + | ||
61 | ')' | ||
62 | ), | ||
63 | 'totalReplies' | ||
64 | ] | ||
38 | ] | 65 | ] |
39 | ] | 66 | } |
40 | } | 67 | } |
41 | }, | 68 | }, |
42 | [ScopeNames.WITH_ACCOUNT]: { | 69 | [ScopeNames.WITH_ACCOUNT]: { |
@@ -267,26 +294,47 @@ export class VideoCommentModel extends Model<VideoCommentModel> { | |||
267 | return VideoCommentModel.scope([ ScopeNames.WITH_IN_REPLY_TO, ScopeNames.WITH_VIDEO ]).findOne(query) | 294 | return VideoCommentModel.scope([ ScopeNames.WITH_IN_REPLY_TO, ScopeNames.WITH_VIDEO ]).findOne(query) |
268 | } | 295 | } |
269 | 296 | ||
270 | static listThreadsForApi (videoId: number, start: number, count: number, sort: string) { | 297 | static async listThreadsForApi (videoId: number, start: number, count: number, sort: string, user?: UserModel) { |
298 | const serverActor = await getServerActor() | ||
299 | const serverAccountId = serverActor.Account.id | ||
300 | const userAccountId = user ? user.Account.id : undefined | ||
301 | |||
271 | const query = { | 302 | const query = { |
272 | offset: start, | 303 | offset: start, |
273 | limit: count, | 304 | limit: count, |
274 | order: getSort(sort), | 305 | order: getSort(sort), |
275 | where: { | 306 | where: { |
276 | videoId, | 307 | videoId, |
277 | inReplyToCommentId: null | 308 | inReplyToCommentId: null, |
309 | accountId: { | ||
310 | [Sequelize.Op.notIn]: Sequelize.literal( | ||
311 | '(' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')' | ||
312 | ) | ||
313 | } | ||
278 | } | 314 | } |
279 | } | 315 | } |
280 | 316 | ||
317 | // FIXME: typings | ||
318 | const scopes: any[] = [ | ||
319 | ScopeNames.WITH_ACCOUNT, | ||
320 | { | ||
321 | method: [ ScopeNames.ATTRIBUTES_FOR_API, serverAccountId, userAccountId ] | ||
322 | } | ||
323 | ] | ||
324 | |||
281 | return VideoCommentModel | 325 | return VideoCommentModel |
282 | .scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.ATTRIBUTES_FOR_API ]) | 326 | .scope(scopes) |
283 | .findAndCountAll(query) | 327 | .findAndCountAll(query) |
284 | .then(({ rows, count }) => { | 328 | .then(({ rows, count }) => { |
285 | return { total: count, data: rows } | 329 | return { total: count, data: rows } |
286 | }) | 330 | }) |
287 | } | 331 | } |
288 | 332 | ||
289 | static listThreadCommentsForApi (videoId: number, threadId: number) { | 333 | static async listThreadCommentsForApi (videoId: number, threadId: number, user?: UserModel) { |
334 | const serverActor = await getServerActor() | ||
335 | const serverAccountId = serverActor.Account.id | ||
336 | const userAccountId = user ? user.Account.id : undefined | ||
337 | |||
290 | const query = { | 338 | const query = { |
291 | order: [ [ 'createdAt', 'ASC' ], [ 'updatedAt', 'ASC' ] ], | 339 | order: [ [ 'createdAt', 'ASC' ], [ 'updatedAt', 'ASC' ] ], |
292 | where: { | 340 | where: { |
@@ -294,12 +342,24 @@ export class VideoCommentModel extends Model<VideoCommentModel> { | |||
294 | [ Sequelize.Op.or ]: [ | 342 | [ Sequelize.Op.or ]: [ |
295 | { id: threadId }, | 343 | { id: threadId }, |
296 | { originCommentId: threadId } | 344 | { originCommentId: threadId } |
297 | ] | 345 | ], |
346 | accountId: { | ||
347 | [Sequelize.Op.notIn]: Sequelize.literal( | ||
348 | '(' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')' | ||
349 | ) | ||
350 | } | ||
298 | } | 351 | } |
299 | } | 352 | } |
300 | 353 | ||
354 | const scopes: any[] = [ | ||
355 | ScopeNames.WITH_ACCOUNT, | ||
356 | { | ||
357 | method: [ ScopeNames.ATTRIBUTES_FOR_API, serverAccountId, userAccountId ] | ||
358 | } | ||
359 | ] | ||
360 | |||
301 | return VideoCommentModel | 361 | return VideoCommentModel |
302 | .scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.ATTRIBUTES_FOR_API ]) | 362 | .scope(scopes) |
303 | .findAndCountAll(query) | 363 | .findAndCountAll(query) |
304 | .then(({ rows, count }) => { | 364 | .then(({ rows, count }) => { |
305 | return { total: count, data: rows } | 365 | return { total: count, data: rows } |
@@ -313,9 +373,11 @@ export class VideoCommentModel extends Model<VideoCommentModel> { | |||
313 | id: { | 373 | id: { |
314 | [ Sequelize.Op.in ]: Sequelize.literal('(' + | 374 | [ Sequelize.Op.in ]: Sequelize.literal('(' + |
315 | 'WITH RECURSIVE children (id, "inReplyToCommentId") AS ( ' + | 375 | 'WITH RECURSIVE children (id, "inReplyToCommentId") AS ( ' + |
316 | 'SELECT id, "inReplyToCommentId" FROM "videoComment" WHERE id = ' + comment.id + ' UNION ' + | 376 | `SELECT id, "inReplyToCommentId" FROM "videoComment" WHERE id = ${comment.id} ` + |
317 | 'SELECT p.id, p."inReplyToCommentId" from "videoComment" p ' + | 377 | 'UNION ' + |
318 | 'INNER JOIN children c ON c."inReplyToCommentId" = p.id) ' + | 378 | 'SELECT "parent"."id", "parent"."inReplyToCommentId" FROM "videoComment" "parent" ' + |
379 | 'INNER JOIN "children" ON "children"."inReplyToCommentId" = "parent"."id"' + | ||
380 | ') ' + | ||
319 | 'SELECT id FROM children' + | 381 | 'SELECT id FROM children' + |
320 | ')'), | 382 | ')'), |
321 | [ Sequelize.Op.ne ]: comment.id | 383 | [ Sequelize.Op.ne ]: comment.id |
@@ -391,6 +453,10 @@ export class VideoCommentModel extends Model<VideoCommentModel> { | |||
391 | } | 453 | } |
392 | } | 454 | } |
393 | 455 | ||
456 | getCommentStaticPath () { | ||
457 | return this.Video.getWatchStaticPath() + ';threadId=' + this.getThreadId() | ||
458 | } | ||
459 | |||
394 | getThreadId (): number { | 460 | getThreadId (): number { |
395 | return this.originCommentId || this.id | 461 | return this.originCommentId || this.id |
396 | } | 462 | } |
@@ -399,6 +465,34 @@ export class VideoCommentModel extends Model<VideoCommentModel> { | |||
399 | return this.Account.isOwned() | 465 | return this.Account.isOwned() |
400 | } | 466 | } |
401 | 467 | ||
468 | extractMentions () { | ||
469 | if (!this.text) return [] | ||
470 | |||
471 | const localMention = `@(${actorNameAlphabet}+)` | ||
472 | const remoteMention = `${localMention}@${CONFIG.WEBSERVER.HOST}` | ||
473 | |||
474 | const remoteMentionsRegex = new RegExp(' ' + remoteMention + ' ', 'g') | ||
475 | const localMentionsRegex = new RegExp(' ' + localMention + ' ', 'g') | ||
476 | const firstMentionRegex = new RegExp('^(?:(?:' + remoteMention + ')|(?:' + localMention + ')) ', 'g') | ||
477 | const endMentionRegex = new RegExp(' (?:(?:' + remoteMention + ')|(?:' + localMention + '))$', 'g') | ||
478 | |||
479 | return uniq( | ||
480 | [].concat( | ||
481 | regexpCapture(this.text, remoteMentionsRegex) | ||
482 | .map(([ , username ]) => username), | ||
483 | |||
484 | regexpCapture(this.text, localMentionsRegex) | ||
485 | .map(([ , username ]) => username), | ||
486 | |||
487 | regexpCapture(this.text, firstMentionRegex) | ||
488 | .map(([ , username1, username2 ]) => username1 || username2), | ||
489 | |||
490 | regexpCapture(this.text, endMentionRegex) | ||
491 | .map(([ , username1, username2 ]) => username1 || username2) | ||
492 | ) | ||
493 | ) | ||
494 | } | ||
495 | |||
402 | toFormattedJSON () { | 496 | toFormattedJSON () { |
403 | return { | 497 | return { |
404 | id: this.id, | 498 | id: this.id, |