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