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 { AccountModel } from '../account/account'
11 import { ActorModel } from '../activitypub/actor'
12 import { AvatarModel } from '../avatar/avatar'
13 import { ServerModel } from '../server/server'
14 import { getSort, throwIfNotValid } from '../utils'
15 import { VideoModel } from './video'
18 WITH_ACCOUNT = 'WITH_ACCOUNT',
19 WITH_IN_REPLY_TO = 'WITH_IN_REPLY_TO',
20 WITH_VIDEO = 'WITH_VIDEO',
21 ATTRIBUTES_FOR_API = 'ATTRIBUTES_FOR_API'
25 [ScopeNames.ATTRIBUTES_FOR_API]: {
30 '(SELECT COUNT("replies"."id") ' +
31 'FROM "videoComment" AS "replies" ' +
32 'WHERE "replies"."originCommentId" = "VideoCommentModel"."id")'
39 [ScopeNames.WITH_ACCOUNT]: {
42 model: () => AccountModel,
45 model: () => ActorModel,
48 model: () => ServerModel,
52 model: () => AvatarModel,
61 [ScopeNames.WITH_IN_REPLY_TO]: {
64 model: () => VideoCommentModel,
65 as: 'InReplyToVideoComment'
69 [ScopeNames.WITH_VIDEO]: {
72 model: () => VideoModel,
79 tableName: 'videoComment',
85 fields: [ 'videoId', 'originCommentId' ]
89 export class VideoCommentModel extends Model<VideoCommentModel> {
97 @Is('VideoCommentUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
98 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max))
102 @Column(DataType.TEXT)
105 @ForeignKey(() => VideoCommentModel)
107 originCommentId: number
109 @BelongsTo(() => VideoCommentModel, {
111 name: 'originCommentId',
114 as: 'OriginVideoComment',
117 OriginVideoComment: VideoCommentModel
119 @ForeignKey(() => VideoCommentModel)
121 inReplyToCommentId: number
123 @BelongsTo(() => VideoCommentModel, {
125 name: 'inReplyToCommentId',
128 as: 'InReplyToVideoComment',
131 InReplyToVideoComment: VideoCommentModel
133 @ForeignKey(() => VideoModel)
137 @BelongsTo(() => VideoModel, {
145 @ForeignKey(() => AccountModel)
149 @BelongsTo(() => AccountModel, {
155 Account: AccountModel
158 static sendDeleteIfOwned (instance: VideoCommentModel) {
163 static loadById (id: number, t?: Sequelize.Transaction) {
164 const query: IFindOptions<VideoCommentModel> = {
170 if (t !== undefined) query.transaction = t
172 return VideoCommentModel.findOne(query)
175 static loadByIdAndPopulateVideoAndAccountAndReply (id: number, t?: Sequelize.Transaction) {
176 const query: IFindOptions<VideoCommentModel> = {
182 if (t !== undefined) query.transaction = t
184 return VideoCommentModel
185 .scope([ ScopeNames.WITH_VIDEO, ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_IN_REPLY_TO ])
189 static loadByUrl (url: string, t?: Sequelize.Transaction) {
190 const query: IFindOptions<VideoCommentModel> = {
196 if (t !== undefined) query.transaction = t
198 return VideoCommentModel.findOne(query)
201 static listThreadsForApi (videoId: number, start: number, count: number, sort: string) {
205 order: [ getSort(sort) ],
208 inReplyToCommentId: null
212 return VideoCommentModel
213 .scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.ATTRIBUTES_FOR_API ])
214 .findAndCountAll(query)
215 .then(({ rows, count }) => {
216 return { total: count, data: rows }
220 static listThreadCommentsForApi (videoId: number, threadId: number) {
222 order: [ [ 'createdAt', 'ASC' ] ],
225 [ Sequelize.Op.or ]: [
227 { originCommentId: threadId }
232 return VideoCommentModel
233 .scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.ATTRIBUTES_FOR_API ])
234 .findAndCountAll(query)
235 .then(({ rows, count }) => {
236 return { total: count, data: rows }
245 threadId: this.originCommentId || this.id,
246 inReplyToCommentId: this.inReplyToCommentId || null,
247 videoId: this.videoId,
248 createdAt: this.createdAt,
249 updatedAt: this.updatedAt,
250 totalReplies: this.get('totalReplies') || 0,
251 account: this.Account.toFormattedJSON()
255 toActivityPubObject (): VideoCommentObject {
256 let inReplyTo: string
257 // New thread, so in AS we reply to the video
258 if (this.inReplyToCommentId === null) {
259 inReplyTo = this.Video.url
261 inReplyTo = this.InReplyToVideoComment.url
265 type: 'Note' as 'Note',
269 updated: this.updatedAt.toISOString(),
270 published: this.createdAt.toISOString(),
272 attributedTo: this.Account.Actor.url