1 import * as Sequelize from 'sequelize'
3 AfterDestroy, AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, IFindOptions, Is, Model, Scopes, Table,
5 } from 'sequelize-typescript'
6 import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object'
7 import { VideoComment } from '../../../shared/models/videos/video-comment.model'
8 import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
9 import { CONSTRAINTS_FIELDS } from '../../initializers'
10 import { sendDeleteVideoComment } from '../../lib/activitypub/send'
11 import { AccountModel } from '../account/account'
12 import { ActorModel } from '../activitypub/actor'
13 import { AvatarModel } from '../avatar/avatar'
14 import { ServerModel } from '../server/server'
15 import { getSort, throwIfNotValid } from '../utils'
16 import { VideoModel } from './video'
17 import { VideoChannelModel } from './video-channel'
20 WITH_ACCOUNT = 'WITH_ACCOUNT',
21 WITH_IN_REPLY_TO = 'WITH_IN_REPLY_TO',
22 WITH_VIDEO = 'WITH_VIDEO',
23 ATTRIBUTES_FOR_API = 'ATTRIBUTES_FOR_API'
27 [ScopeNames.ATTRIBUTES_FOR_API]: {
32 '(SELECT COUNT("replies"."id") ' +
33 'FROM "videoComment" AS "replies" ' +
34 'WHERE "replies"."originCommentId" = "VideoCommentModel"."id")'
41 [ScopeNames.WITH_ACCOUNT]: {
44 model: () => AccountModel,
47 model: () => ActorModel,
50 model: () => ServerModel,
54 model: () => AvatarModel,
63 [ScopeNames.WITH_IN_REPLY_TO]: {
66 model: () => VideoCommentModel,
67 as: 'InReplyToVideoComment'
71 [ScopeNames.WITH_VIDEO]: {
74 model: () => VideoModel,
78 model: () => VideoChannelModel.unscoped(),
82 model: () => AccountModel,
86 model: () => ActorModel,
99 tableName: 'videoComment',
102 fields: [ 'videoId' ]
105 fields: [ 'videoId', 'originCommentId' ]
109 export class VideoCommentModel extends Model<VideoCommentModel> {
117 @Is('VideoCommentUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
118 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max))
122 @Column(DataType.TEXT)
125 @ForeignKey(() => VideoCommentModel)
127 originCommentId: number
129 @BelongsTo(() => VideoCommentModel, {
131 name: 'originCommentId',
134 as: 'OriginVideoComment',
137 OriginVideoComment: VideoCommentModel
139 @ForeignKey(() => VideoCommentModel)
141 inReplyToCommentId: number
143 @BelongsTo(() => VideoCommentModel, {
145 name: 'inReplyToCommentId',
148 as: 'InReplyToVideoComment',
151 InReplyToVideoComment: VideoCommentModel
153 @ForeignKey(() => VideoModel)
157 @BelongsTo(() => VideoModel, {
165 @ForeignKey(() => AccountModel)
169 @BelongsTo(() => AccountModel, {
175 Account: AccountModel
178 static async sendDeleteIfOwned (instance: VideoCommentModel) {
179 if (instance.isOwned()) {
180 await sendDeleteVideoComment(instance, undefined)
184 static loadById (id: number, t?: Sequelize.Transaction) {
185 const query: IFindOptions<VideoCommentModel> = {
191 if (t !== undefined) query.transaction = t
193 return VideoCommentModel.findOne(query)
196 static loadByIdAndPopulateVideoAndAccountAndReply (id: number, t?: Sequelize.Transaction) {
197 const query: IFindOptions<VideoCommentModel> = {
203 if (t !== undefined) query.transaction = t
205 return VideoCommentModel
206 .scope([ ScopeNames.WITH_VIDEO, ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_IN_REPLY_TO ])
210 static loadByUrl (url: string, t?: Sequelize.Transaction) {
211 const query: IFindOptions<VideoCommentModel> = {
217 if (t !== undefined) query.transaction = t
219 return VideoCommentModel.findOne(query)
222 static loadByUrlAndPopulateAccount (url: string, t?: Sequelize.Transaction) {
223 const query: IFindOptions<VideoCommentModel> = {
229 if (t !== undefined) query.transaction = t
231 return VideoCommentModel.scope([ ScopeNames.WITH_ACCOUNT ]).findOne(query)
234 static listThreadsForApi (videoId: number, start: number, count: number, sort: string) {
238 order: [ getSort(sort) ],
241 inReplyToCommentId: null
245 return VideoCommentModel
246 .scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.ATTRIBUTES_FOR_API ])
247 .findAndCountAll(query)
248 .then(({ rows, count }) => {
249 return { total: count, data: rows }
253 static listThreadCommentsForApi (videoId: number, threadId: number) {
255 order: [ [ 'createdAt', 'ASC' ] ],
258 [ Sequelize.Op.or ]: [
260 { originCommentId: threadId }
265 return VideoCommentModel
266 .scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.ATTRIBUTES_FOR_API ])
267 .findAndCountAll(query)
268 .then(({ rows, count }) => {
269 return { total: count, data: rows }
274 return this.Account.isOwned()
282 threadId: this.originCommentId || this.id,
283 inReplyToCommentId: this.inReplyToCommentId || null,
284 videoId: this.videoId,
285 createdAt: this.createdAt,
286 updatedAt: this.updatedAt,
287 totalReplies: this.get('totalReplies') || 0,
288 account: this.Account.toFormattedJSON()
292 toActivityPubObject (): VideoCommentObject {
293 let inReplyTo: string
294 // New thread, so in AS we reply to the video
295 if (this.inReplyToCommentId === null) {
296 inReplyTo = this.Video.url
298 inReplyTo = this.InReplyToVideoComment.url
302 type: 'Note' as 'Note',
306 updated: this.updatedAt.toISOString(),
307 published: this.createdAt.toISOString(),
309 attributedTo: this.Account.Actor.url