Add video comment components
[oweals/peertube.git] / server / models / video / video-comment.ts
1 import * as Sequelize from 'sequelize'
2 import {
3   AfterDestroy, AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, IFindOptions, Is, Model, Scopes, Table,
4   UpdatedAt
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'
9 import { CONSTRAINTS_FIELDS } from '../../initializers'
10 import { AccountModel } from '../account/account'
11 import { ActorModel } from '../activitypub/actor'
12 import { ServerModel } from '../server/server'
13 import { getSort, throwIfNotValid } from '../utils'
14 import { VideoModel } from './video'
15
16 enum ScopeNames {
17   WITH_ACCOUNT = 'WITH_ACCOUNT',
18   WITH_IN_REPLY_TO = 'WITH_IN_REPLY_TO',
19   ATTRIBUTES_FOR_API = 'ATTRIBUTES_FOR_API'
20 }
21
22 @Scopes({
23   [ScopeNames.ATTRIBUTES_FOR_API]: {
24     attributes: {
25       include: [
26         [
27           Sequelize.literal(
28             '(SELECT COUNT("replies"."id") ' +
29             'FROM "videoComment" AS "replies" ' +
30             'WHERE "replies"."originCommentId" = "VideoCommentModel"."id")'
31           ),
32           'totalReplies'
33         ]
34       ]
35     }
36   },
37   [ScopeNames.WITH_ACCOUNT]: {
38     include: [
39       {
40         model: () => AccountModel,
41         include: [
42           {
43             model: () => ActorModel,
44             include: [
45               {
46                 model: () => ServerModel,
47                 required: false
48               }
49             ]
50           }
51         ]
52       }
53     ]
54   },
55   [ScopeNames.WITH_IN_REPLY_TO]: {
56     include: [
57       {
58         model: () => VideoCommentModel,
59         as: 'InReplyTo'
60       }
61     ]
62   }
63 })
64 @Table({
65   tableName: 'videoComment',
66   indexes: [
67     {
68       fields: [ 'videoId' ]
69     },
70     {
71       fields: [ 'videoId', 'originCommentId' ]
72     }
73   ]
74 })
75 export class VideoCommentModel extends Model<VideoCommentModel> {
76   @CreatedAt
77   createdAt: Date
78
79   @UpdatedAt
80   updatedAt: Date
81
82   @AllowNull(false)
83   @Is('VideoCommentUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
84   @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max))
85   url: string
86
87   @AllowNull(false)
88   @Column(DataType.TEXT)
89   text: string
90
91   @ForeignKey(() => VideoCommentModel)
92   @Column
93   originCommentId: number
94
95   @BelongsTo(() => VideoCommentModel, {
96     foreignKey: {
97       allowNull: true
98     },
99     onDelete: 'CASCADE'
100   })
101   OriginVideoComment: VideoCommentModel
102
103   @ForeignKey(() => VideoCommentModel)
104   @Column
105   inReplyToCommentId: number
106
107   @BelongsTo(() => VideoCommentModel, {
108     foreignKey: {
109       allowNull: true
110     },
111     as: 'InReplyTo',
112     onDelete: 'CASCADE'
113   })
114   InReplyToVideoComment: VideoCommentModel
115
116   @ForeignKey(() => VideoModel)
117   @Column
118   videoId: number
119
120   @BelongsTo(() => VideoModel, {
121     foreignKey: {
122       allowNull: false
123     },
124     onDelete: 'CASCADE'
125   })
126   Video: VideoModel
127
128   @ForeignKey(() => AccountModel)
129   @Column
130   accountId: number
131
132   @BelongsTo(() => AccountModel, {
133     foreignKey: {
134       allowNull: false
135     },
136     onDelete: 'CASCADE'
137   })
138   Account: AccountModel
139
140   @AfterDestroy
141   static sendDeleteIfOwned (instance: VideoCommentModel) {
142     // TODO
143     return undefined
144   }
145
146   static loadById (id: number, t?: Sequelize.Transaction) {
147     const query: IFindOptions<VideoCommentModel> = {
148       where: {
149         id
150       }
151     }
152
153     if (t !== undefined) query.transaction = t
154
155     return VideoCommentModel.findOne(query)
156   }
157
158   static loadByUrl (url: string, t?: Sequelize.Transaction) {
159     const query: IFindOptions<VideoCommentModel> = {
160       where: {
161         url
162       }
163     }
164
165     if (t !== undefined) query.transaction = t
166
167     return VideoCommentModel.findOne(query)
168   }
169
170   static listThreadsForApi (videoId: number, start: number, count: number, sort: string) {
171     const query = {
172       offset: start,
173       limit: count,
174       order: [ getSort(sort) ],
175       where: {
176         videoId,
177         inReplyToCommentId: null
178       }
179     }
180
181     return VideoCommentModel
182       .scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.ATTRIBUTES_FOR_API ])
183       .findAndCountAll(query)
184       .then(({ rows, count }) => {
185         return { total: count, data: rows }
186       })
187   }
188
189   static listThreadCommentsForApi (videoId: number, threadId: number) {
190     const query = {
191       order: [ [ 'id', 'ASC' ] ],
192       where: {
193         videoId,
194         [ Sequelize.Op.or ]: [
195           { id: threadId },
196           { originCommentId: threadId }
197         ]
198       }
199     }
200
201     return VideoCommentModel
202       .scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.ATTRIBUTES_FOR_API ])
203       .findAndCountAll(query)
204       .then(({ rows, count }) => {
205         return { total: count, data: rows }
206       })
207   }
208
209   toFormattedJSON () {
210     return {
211       id: this.id,
212       url: this.url,
213       text: this.text,
214       threadId: this.originCommentId || this.id,
215       inReplyToCommentId: this.inReplyToCommentId,
216       videoId: this.videoId,
217       createdAt: this.createdAt,
218       updatedAt: this.updatedAt,
219       totalReplies: this.get('totalReplies') || 0,
220       account: {
221         name: this.Account.name,
222         host: this.Account.Actor.getHost()
223       }
224     } as VideoComment
225   }
226
227   toActivityPubObject (): VideoCommentObject {
228     let inReplyTo: string
229     // New thread, so in AS we reply to the video
230     if (this.inReplyToCommentId === null) {
231       inReplyTo = this.Video.url
232     } else {
233       inReplyTo = this.InReplyToVideoComment.url
234     }
235
236     return {
237       type: 'Note' as 'Note',
238       id: this.url,
239       content: this.text,
240       inReplyTo,
241       published: this.createdAt.toISOString(),
242       url: this.url
243     }
244   }
245 }