]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/models/video/video-comment.ts
Add background placeholder for thumbnails
[github/Chocobozzz/PeerTube.git] / server / models / video / video-comment.ts
CommitLineData
6d852470
C
1import * as Sequelize from 'sequelize'
2import {
f05a1c30 3 AllowNull, BeforeDestroy, BelongsTo, Column, CreatedAt, DataType, ForeignKey, IFindOptions, Is, Model, Scopes, Table,
6d852470
C
4 UpdatedAt
5} from 'sequelize-typescript'
d7e70384 6import { ActivityTagObject } from '../../../shared/models/activitypub/objects/common-objects'
ea44f375 7import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object'
bf1f6508 8import { VideoComment } from '../../../shared/models/videos/video-comment.model'
da854ddd 9import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
6d852470 10import { CONSTRAINTS_FIELDS } from '../../initializers'
4cb6d457 11import { sendDeleteVideoComment } from '../../lib/activitypub/send'
d3ea8975 12import { AccountModel } from '../account/account'
4635f59d 13import { ActorModel } from '../activitypub/actor'
cf117aaa 14import { AvatarModel } from '../avatar/avatar'
4635f59d 15import { ServerModel } from '../server/server'
bf1f6508 16import { getSort, throwIfNotValid } from '../utils'
6d852470 17import { VideoModel } from './video'
4cb6d457 18import { VideoChannelModel } from './video-channel'
6d852470 19
bf1f6508 20enum ScopeNames {
ea44f375 21 WITH_ACCOUNT = 'WITH_ACCOUNT',
4635f59d 22 WITH_IN_REPLY_TO = 'WITH_IN_REPLY_TO',
da854ddd 23 WITH_VIDEO = 'WITH_VIDEO',
4635f59d 24 ATTRIBUTES_FOR_API = 'ATTRIBUTES_FOR_API'
bf1f6508
C
25}
26
27@Scopes({
4635f59d
C
28 [ScopeNames.ATTRIBUTES_FOR_API]: {
29 attributes: {
30 include: [
31 [
32 Sequelize.literal(
33 '(SELECT COUNT("replies"."id") ' +
34 'FROM "videoComment" AS "replies" ' +
35 'WHERE "replies"."originCommentId" = "VideoCommentModel"."id")'
36 ),
37 'totalReplies'
38 ]
39 ]
40 }
41 },
d3ea8975 42 [ScopeNames.WITH_ACCOUNT]: {
bf1f6508 43 include: [
4635f59d
C
44 {
45 model: () => AccountModel,
46 include: [
47 {
48 model: () => ActorModel,
49 include: [
50 {
51 model: () => ServerModel,
52 required: false
cf117aaa
C
53 },
54 {
55 model: () => AvatarModel,
56 required: false
4635f59d
C
57 }
58 ]
59 }
60 ]
61 }
bf1f6508 62 ]
ea44f375
C
63 },
64 [ScopeNames.WITH_IN_REPLY_TO]: {
65 include: [
66 {
67 model: () => VideoCommentModel,
da854ddd
C
68 as: 'InReplyToVideoComment'
69 }
70 ]
71 },
72 [ScopeNames.WITH_VIDEO]: {
73 include: [
74 {
75 model: () => VideoModel,
4cb6d457
C
76 required: true,
77 include: [
78 {
79 model: () => VideoChannelModel.unscoped(),
80 required: true,
81 include: [
82 {
83 model: () => AccountModel,
84 required: true,
85 include: [
86 {
87 model: () => ActorModel,
88 required: true
89 }
90 ]
91 }
92 ]
93 }
94 ]
ea44f375
C
95 }
96 ]
bf1f6508
C
97 }
98})
6d852470
C
99@Table({
100 tableName: 'videoComment',
101 indexes: [
102 {
103 fields: [ 'videoId' ]
bf1f6508
C
104 },
105 {
106 fields: [ 'videoId', 'originCommentId' ]
0776d83f
C
107 },
108 {
109 fields: [ 'url' ],
110 unique: true
6d852470
C
111 }
112 ]
113})
114export class VideoCommentModel extends Model<VideoCommentModel> {
115 @CreatedAt
116 createdAt: Date
117
118 @UpdatedAt
119 updatedAt: Date
120
121 @AllowNull(false)
122 @Is('VideoCommentUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
123 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max))
124 url: string
125
126 @AllowNull(false)
127 @Column(DataType.TEXT)
128 text: string
129
130 @ForeignKey(() => VideoCommentModel)
131 @Column
132 originCommentId: number
133
134 @BelongsTo(() => VideoCommentModel, {
135 foreignKey: {
db799da3 136 name: 'originCommentId',
6d852470
C
137 allowNull: true
138 },
db799da3 139 as: 'OriginVideoComment',
6d852470
C
140 onDelete: 'CASCADE'
141 })
142 OriginVideoComment: VideoCommentModel
143
144 @ForeignKey(() => VideoCommentModel)
145 @Column
146 inReplyToCommentId: number
147
148 @BelongsTo(() => VideoCommentModel, {
149 foreignKey: {
db799da3 150 name: 'inReplyToCommentId',
6d852470
C
151 allowNull: true
152 },
da854ddd 153 as: 'InReplyToVideoComment',
6d852470
C
154 onDelete: 'CASCADE'
155 })
156 InReplyToVideoComment: VideoCommentModel
157
158 @ForeignKey(() => VideoModel)
159 @Column
160 videoId: number
161
162 @BelongsTo(() => VideoModel, {
163 foreignKey: {
164 allowNull: false
165 },
166 onDelete: 'CASCADE'
167 })
168 Video: VideoModel
169
d3ea8975 170 @ForeignKey(() => AccountModel)
6d852470 171 @Column
d3ea8975 172 accountId: number
6d852470 173
d3ea8975 174 @BelongsTo(() => AccountModel, {
6d852470
C
175 foreignKey: {
176 allowNull: false
177 },
178 onDelete: 'CASCADE'
179 })
d3ea8975 180 Account: AccountModel
6d852470 181
f05a1c30
C
182 @BeforeDestroy
183 static async sendDeleteIfOwned (instance: VideoCommentModel, options) {
184 if (!instance.Account || !instance.Account.Actor) {
185 instance.Account = await instance.$get('Account', {
186 include: [ ActorModel ],
187 transaction: options.transaction
188 }) as AccountModel
189 }
190
4cb6d457 191 if (instance.isOwned()) {
f05a1c30 192 await sendDeleteVideoComment(instance, options.transaction)
4cb6d457 193 }
bf1f6508
C
194 }
195
196 static loadById (id: number, t?: Sequelize.Transaction) {
197 const query: IFindOptions<VideoCommentModel> = {
198 where: {
199 id
200 }
201 }
202
203 if (t !== undefined) query.transaction = t
204
205 return VideoCommentModel.findOne(query)
206 }
207
da854ddd
C
208 static loadByIdAndPopulateVideoAndAccountAndReply (id: number, t?: Sequelize.Transaction) {
209 const query: IFindOptions<VideoCommentModel> = {
210 where: {
211 id
212 }
213 }
214
215 if (t !== undefined) query.transaction = t
216
217 return VideoCommentModel
218 .scope([ ScopeNames.WITH_VIDEO, ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_IN_REPLY_TO ])
219 .findOne(query)
220 }
221
2ccaeeb3 222 static loadByUrlAndPopulateAccount (url: string, t?: Sequelize.Transaction) {
6d852470
C
223 const query: IFindOptions<VideoCommentModel> = {
224 where: {
225 url
226 }
227 }
228
229 if (t !== undefined) query.transaction = t
230
2ccaeeb3 231 return VideoCommentModel.scope([ ScopeNames.WITH_ACCOUNT ]).findOne(query)
6d852470 232 }
bf1f6508 233
2ccaeeb3 234 static loadByUrlAndPopulateReplyAndVideo (url: string, t?: Sequelize.Transaction) {
4cb6d457
C
235 const query: IFindOptions<VideoCommentModel> = {
236 where: {
237 url
238 }
239 }
240
241 if (t !== undefined) query.transaction = t
242
2ccaeeb3 243 return VideoCommentModel.scope([ ScopeNames.WITH_IN_REPLY_TO, ScopeNames.WITH_VIDEO ]).findOne(query)
4cb6d457
C
244 }
245
bf1f6508
C
246 static listThreadsForApi (videoId: number, start: number, count: number, sort: string) {
247 const query = {
248 offset: start,
249 limit: count,
250 order: [ getSort(sort) ],
251 where: {
d3ea8975
C
252 videoId,
253 inReplyToCommentId: null
bf1f6508
C
254 }
255 }
256
257 return VideoCommentModel
4635f59d 258 .scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.ATTRIBUTES_FOR_API ])
bf1f6508
C
259 .findAndCountAll(query)
260 .then(({ rows, count }) => {
261 return { total: count, data: rows }
262 })
263 }
264
265 static listThreadCommentsForApi (videoId: number, threadId: number) {
266 const query = {
a3fd560d 267 order: [ [ 'createdAt', 'ASC' ], [ 'updatedAt', 'ASC' ] ],
bf1f6508
C
268 where: {
269 videoId,
270 [ Sequelize.Op.or ]: [
271 { id: threadId },
272 { originCommentId: threadId }
273 ]
274 }
275 }
276
277 return VideoCommentModel
4635f59d 278 .scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.ATTRIBUTES_FOR_API ])
bf1f6508
C
279 .findAndCountAll(query)
280 .then(({ rows, count }) => {
281 return { total: count, data: rows }
282 })
283 }
284
2ccaeeb3 285 static listThreadParentComments (comment: VideoCommentModel, t: Sequelize.Transaction, order: 'ASC' | 'DESC' = 'ASC') {
d7e70384 286 const query = {
2ccaeeb3 287 order: [ [ 'createdAt', order ] ],
d7e70384
C
288 where: {
289 [ Sequelize.Op.or ]: [
290 { id: comment.getThreadId() },
291 { originCommentId: comment.getThreadId() }
292 ],
293 id: {
294 [ Sequelize.Op.ne ]: comment.id
2ccaeeb3
C
295 },
296 createdAt: {
297 [ Sequelize.Op.lt ]: comment.createdAt
d7e70384
C
298 }
299 },
300 transaction: t
301 }
302
303 return VideoCommentModel
304 .scope([ ScopeNames.WITH_ACCOUNT ])
305 .findAll(query)
306 }
307
308 getThreadId (): number {
309 return this.originCommentId || this.id
310 }
311
4cb6d457
C
312 isOwned () {
313 return this.Account.isOwned()
314 }
315
bf1f6508
C
316 toFormattedJSON () {
317 return {
318 id: this.id,
319 url: this.url,
320 text: this.text,
321 threadId: this.originCommentId || this.id,
d50acfab 322 inReplyToCommentId: this.inReplyToCommentId || null,
bf1f6508
C
323 videoId: this.videoId,
324 createdAt: this.createdAt,
d3ea8975 325 updatedAt: this.updatedAt,
4635f59d 326 totalReplies: this.get('totalReplies') || 0,
cf117aaa 327 account: this.Account.toFormattedJSON()
bf1f6508
C
328 } as VideoComment
329 }
ea44f375 330
d7e70384 331 toActivityPubObject (threadParentComments: VideoCommentModel[]): VideoCommentObject {
ea44f375
C
332 let inReplyTo: string
333 // New thread, so in AS we reply to the video
334 if (this.inReplyToCommentId === null) {
335 inReplyTo = this.Video.url
336 } else {
337 inReplyTo = this.InReplyToVideoComment.url
338 }
339
d7e70384
C
340 const tag: ActivityTagObject[] = []
341 for (const parentComment of threadParentComments) {
342 const actor = parentComment.Account.Actor
343
344 tag.push({
345 type: 'Mention',
346 href: actor.url,
347 name: `@${actor.preferredUsername}@${actor.getHost()}`
348 })
349 }
350
ea44f375
C
351 return {
352 type: 'Note' as 'Note',
353 id: this.url,
354 content: this.text,
355 inReplyTo,
da854ddd 356 updated: this.updatedAt.toISOString(),
ea44f375 357 published: this.createdAt.toISOString(),
da854ddd 358 url: this.url,
d7e70384
C
359 attributedTo: this.Account.Actor.url,
360 tag
ea44f375
C
361 }
362 }
6d852470 363}