1 import * as Sequelize from 'sequelize'
16 } from 'sequelize-typescript'
17 import { ActivityTagObject } from '../../../shared/models/activitypub/objects/common-objects'
18 import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object'
19 import { VideoComment } from '../../../shared/models/videos/video-comment.model'
20 import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
21 import { CONSTRAINTS_FIELDS } from '../../initializers'
22 import { sendDeleteVideoComment } from '../../lib/activitypub/send'
23 import { AccountModel } from '../account/account'
24 import { ActorModel } from '../activitypub/actor'
25 import { AvatarModel } from '../avatar/avatar'
26 import { ServerModel } from '../server/server'
27 import { buildBlockedAccountSQL, getSort, throwIfNotValid } from '../utils'
28 import { VideoModel } from './video'
29 import { VideoChannelModel } from './video-channel'
30 import { getServerActor } from '../../helpers/utils'
31 import { UserModel } from '../account/user'
34 WITH_ACCOUNT = 'WITH_ACCOUNT',
35 WITH_IN_REPLY_TO = 'WITH_IN_REPLY_TO',
36 WITH_VIDEO = 'WITH_VIDEO',
37 ATTRIBUTES_FOR_API = 'ATTRIBUTES_FOR_API'
41 [ScopeNames.ATTRIBUTES_FOR_API]: (serverAccountId: number, userAccountId?: number) => {
48 'WITH "blocklist" AS (' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')' +
49 'SELECT COUNT("replies"."id") - (' +
50 'SELECT COUNT("replies"."id") ' +
51 'FROM "videoComment" AS "replies" ' +
52 'WHERE "replies"."originCommentId" = "VideoCommentModel"."id" ' +
53 'AND "accountId" IN (SELECT "id" FROM "blocklist")' +
55 'FROM "videoComment" AS "replies" ' +
56 'WHERE "replies"."originCommentId" = "VideoCommentModel"."id" ' +
57 'AND "accountId" NOT IN (SELECT "id" FROM "blocklist")' +
66 [ScopeNames.WITH_ACCOUNT]: {
69 model: () => AccountModel,
72 model: () => ActorModel,
75 model: () => ServerModel,
79 model: () => AvatarModel,
88 [ScopeNames.WITH_IN_REPLY_TO]: {
91 model: () => VideoCommentModel,
92 as: 'InReplyToVideoComment'
96 [ScopeNames.WITH_VIDEO]: {
99 model: () => VideoModel,
103 model: () => VideoChannelModel.unscoped(),
107 model: () => AccountModel,
111 model: () => ActorModel,
124 tableName: 'videoComment',
127 fields: [ 'videoId' ]
130 fields: [ 'videoId', 'originCommentId' ]
137 fields: [ 'accountId' ]
141 export class VideoCommentModel extends Model<VideoCommentModel> {
149 @Is('VideoCommentUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
150 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max))
154 @Column(DataType.TEXT)
157 @ForeignKey(() => VideoCommentModel)
159 originCommentId: number
161 @BelongsTo(() => VideoCommentModel, {
163 name: 'originCommentId',
166 as: 'OriginVideoComment',
169 OriginVideoComment: VideoCommentModel
171 @ForeignKey(() => VideoCommentModel)
173 inReplyToCommentId: number
175 @BelongsTo(() => VideoCommentModel, {
177 name: 'inReplyToCommentId',
180 as: 'InReplyToVideoComment',
183 InReplyToVideoComment: VideoCommentModel | null
185 @ForeignKey(() => VideoModel)
189 @BelongsTo(() => VideoModel, {
197 @ForeignKey(() => AccountModel)
201 @BelongsTo(() => AccountModel, {
207 Account: AccountModel
210 static async sendDeleteIfOwned (instance: VideoCommentModel, options) {
211 if (!instance.Account || !instance.Account.Actor) {
212 instance.Account = await instance.$get('Account', {
213 include: [ ActorModel ],
214 transaction: options.transaction
218 if (!instance.Video) {
219 instance.Video = await instance.$get('Video', {
222 model: VideoChannelModel,
235 transaction: options.transaction
239 if (instance.isOwned()) {
240 await sendDeleteVideoComment(instance, options.transaction)
244 static loadById (id: number, t?: Sequelize.Transaction) {
245 const query: IFindOptions<VideoCommentModel> = {
251 if (t !== undefined) query.transaction = t
253 return VideoCommentModel.findOne(query)
256 static loadByIdAndPopulateVideoAndAccountAndReply (id: number, t?: Sequelize.Transaction) {
257 const query: IFindOptions<VideoCommentModel> = {
263 if (t !== undefined) query.transaction = t
265 return VideoCommentModel
266 .scope([ ScopeNames.WITH_VIDEO, ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_IN_REPLY_TO ])
270 static loadByUrlAndPopulateAccount (url: string, t?: Sequelize.Transaction) {
271 const query: IFindOptions<VideoCommentModel> = {
277 if (t !== undefined) query.transaction = t
279 return VideoCommentModel.scope([ ScopeNames.WITH_ACCOUNT ]).findOne(query)
282 static loadByUrlAndPopulateReplyAndVideo (url: string, t?: Sequelize.Transaction) {
283 const query: IFindOptions<VideoCommentModel> = {
289 if (t !== undefined) query.transaction = t
291 return VideoCommentModel.scope([ ScopeNames.WITH_IN_REPLY_TO, ScopeNames.WITH_VIDEO ]).findOne(query)
294 static async listThreadsForApi (videoId: number, start: number, count: number, sort: string, user?: UserModel) {
295 const serverActor = await getServerActor()
296 const serverAccountId = serverActor.Account.id
297 const userAccountId = user ? user.Account.id : undefined
302 order: getSort(sort),
305 inReplyToCommentId: null,
307 [Sequelize.Op.notIn]: Sequelize.literal(
308 '(' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')'
315 const scopes: any[] = [
316 ScopeNames.WITH_ACCOUNT,
318 method: [ ScopeNames.ATTRIBUTES_FOR_API, serverAccountId, userAccountId ]
322 return VideoCommentModel
324 .findAndCountAll(query)
325 .then(({ rows, count }) => {
326 return { total: count, data: rows }
330 static async listThreadCommentsForApi (videoId: number, threadId: number, user?: UserModel) {
331 const serverActor = await getServerActor()
332 const serverAccountId = serverActor.Account.id
333 const userAccountId = user ? user.Account.id : undefined
336 order: [ [ 'createdAt', 'ASC' ], [ 'updatedAt', 'ASC' ] ],
339 [ Sequelize.Op.or ]: [
341 { originCommentId: threadId }
344 [Sequelize.Op.notIn]: Sequelize.literal(
345 '(' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')'
351 const scopes: any[] = [
352 ScopeNames.WITH_ACCOUNT,
354 method: [ ScopeNames.ATTRIBUTES_FOR_API, serverAccountId, userAccountId ]
358 return VideoCommentModel
360 .findAndCountAll(query)
361 .then(({ rows, count }) => {
362 return { total: count, data: rows }
366 static listThreadParentComments (comment: VideoCommentModel, t: Sequelize.Transaction, order: 'ASC' | 'DESC' = 'ASC') {
368 order: [ [ 'createdAt', order ] ],
371 [ Sequelize.Op.in ]: Sequelize.literal('(' +
372 'WITH RECURSIVE children (id, "inReplyToCommentId") AS ( ' +
373 'SELECT id, "inReplyToCommentId" FROM "videoComment" WHERE id = ' + comment.id + ' UNION ' +
374 'SELECT p.id, p."inReplyToCommentId" from "videoComment" p ' +
375 'INNER JOIN children c ON c."inReplyToCommentId" = p.id) ' +
376 'SELECT id FROM children' +
378 [ Sequelize.Op.ne ]: comment.id
384 return VideoCommentModel
385 .scope([ ScopeNames.WITH_ACCOUNT ])
389 static listAndCountByVideoId (videoId: number, start: number, count: number, t?: Sequelize.Transaction, order: 'ASC' | 'DESC' = 'ASC') {
391 order: [ [ 'createdAt', order ] ],
400 return VideoCommentModel.findAndCountAll(query)
403 static listForFeed (start: number, count: number, videoId?: number) {
405 order: [ [ 'createdAt', 'DESC' ] ],
411 attributes: [ 'name', 'uuid' ],
412 model: VideoModel.unscoped(),
418 if (videoId) query.where['videoId'] = videoId
420 return VideoCommentModel
421 .scope([ ScopeNames.WITH_ACCOUNT ])
425 static async getStats () {
426 const totalLocalVideoComments = await VideoCommentModel.count({
443 const totalVideoComments = await VideoCommentModel.count()
446 totalLocalVideoComments,
451 getThreadId (): number {
452 return this.originCommentId || this.id
456 return this.Account.isOwned()
464 threadId: this.originCommentId || this.id,
465 inReplyToCommentId: this.inReplyToCommentId || null,
466 videoId: this.videoId,
467 createdAt: this.createdAt,
468 updatedAt: this.updatedAt,
469 totalReplies: this.get('totalReplies') || 0,
470 account: this.Account.toFormattedJSON()
474 toActivityPubObject (threadParentComments: VideoCommentModel[]): VideoCommentObject {
475 let inReplyTo: string
476 // New thread, so in AS we reply to the video
477 if (this.inReplyToCommentId === null) {
478 inReplyTo = this.Video.url
480 inReplyTo = this.InReplyToVideoComment.url
483 const tag: ActivityTagObject[] = []
484 for (const parentComment of threadParentComments) {
485 const actor = parentComment.Account.Actor
490 name: `@${actor.preferredUsername}@${actor.getHost()}`
495 type: 'Note' as 'Note',
499 updated: this.updatedAt.toISOString(),
500 published: this.createdAt.toISOString(),
502 attributedTo: this.Account.Actor.url,