]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/models/video/video-comment.ts
Destroy user token when changing its role
[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' ]
6d852470
C
107 }
108 ]
109})
110export class VideoCommentModel extends Model<VideoCommentModel> {
111 @CreatedAt
112 createdAt: Date
113
114 @UpdatedAt
115 updatedAt: Date
116
117 @AllowNull(false)
118 @Is('VideoCommentUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
119 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max))
120 url: string
121
122 @AllowNull(false)
123 @Column(DataType.TEXT)
124 text: string
125
126 @ForeignKey(() => VideoCommentModel)
127 @Column
128 originCommentId: number
129
130 @BelongsTo(() => VideoCommentModel, {
131 foreignKey: {
db799da3 132 name: 'originCommentId',
6d852470
C
133 allowNull: true
134 },
db799da3 135 as: 'OriginVideoComment',
6d852470
C
136 onDelete: 'CASCADE'
137 })
138 OriginVideoComment: VideoCommentModel
139
140 @ForeignKey(() => VideoCommentModel)
141 @Column
142 inReplyToCommentId: number
143
144 @BelongsTo(() => VideoCommentModel, {
145 foreignKey: {
db799da3 146 name: 'inReplyToCommentId',
6d852470
C
147 allowNull: true
148 },
da854ddd 149 as: 'InReplyToVideoComment',
6d852470
C
150 onDelete: 'CASCADE'
151 })
152 InReplyToVideoComment: VideoCommentModel
153
154 @ForeignKey(() => VideoModel)
155 @Column
156 videoId: number
157
158 @BelongsTo(() => VideoModel, {
159 foreignKey: {
160 allowNull: false
161 },
162 onDelete: 'CASCADE'
163 })
164 Video: VideoModel
165
d3ea8975 166 @ForeignKey(() => AccountModel)
6d852470 167 @Column
d3ea8975 168 accountId: number
6d852470 169
d3ea8975 170 @BelongsTo(() => AccountModel, {
6d852470
C
171 foreignKey: {
172 allowNull: false
173 },
174 onDelete: 'CASCADE'
175 })
d3ea8975 176 Account: AccountModel
6d852470 177
f05a1c30
C
178 @BeforeDestroy
179 static async sendDeleteIfOwned (instance: VideoCommentModel, options) {
180 if (!instance.Account || !instance.Account.Actor) {
181 instance.Account = await instance.$get('Account', {
182 include: [ ActorModel ],
183 transaction: options.transaction
184 }) as AccountModel
185 }
186
4cb6d457 187 if (instance.isOwned()) {
f05a1c30 188 await sendDeleteVideoComment(instance, options.transaction)
4cb6d457 189 }
bf1f6508
C
190 }
191
192 static loadById (id: number, t?: Sequelize.Transaction) {
193 const query: IFindOptions<VideoCommentModel> = {
194 where: {
195 id
196 }
197 }
198
199 if (t !== undefined) query.transaction = t
200
201 return VideoCommentModel.findOne(query)
202 }
203
da854ddd
C
204 static loadByIdAndPopulateVideoAndAccountAndReply (id: number, t?: Sequelize.Transaction) {
205 const query: IFindOptions<VideoCommentModel> = {
206 where: {
207 id
208 }
209 }
210
211 if (t !== undefined) query.transaction = t
212
213 return VideoCommentModel
214 .scope([ ScopeNames.WITH_VIDEO, ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_IN_REPLY_TO ])
215 .findOne(query)
216 }
217
2ccaeeb3 218 static loadByUrlAndPopulateAccount (url: string, t?: Sequelize.Transaction) {
6d852470
C
219 const query: IFindOptions<VideoCommentModel> = {
220 where: {
221 url
222 }
223 }
224
225 if (t !== undefined) query.transaction = t
226
2ccaeeb3 227 return VideoCommentModel.scope([ ScopeNames.WITH_ACCOUNT ]).findOne(query)
6d852470 228 }
bf1f6508 229
2ccaeeb3 230 static loadByUrlAndPopulateReplyAndVideo (url: string, t?: Sequelize.Transaction) {
4cb6d457
C
231 const query: IFindOptions<VideoCommentModel> = {
232 where: {
233 url
234 }
235 }
236
237 if (t !== undefined) query.transaction = t
238
2ccaeeb3 239 return VideoCommentModel.scope([ ScopeNames.WITH_IN_REPLY_TO, ScopeNames.WITH_VIDEO ]).findOne(query)
4cb6d457
C
240 }
241
bf1f6508
C
242 static listThreadsForApi (videoId: number, start: number, count: number, sort: string) {
243 const query = {
244 offset: start,
245 limit: count,
246 order: [ getSort(sort) ],
247 where: {
d3ea8975
C
248 videoId,
249 inReplyToCommentId: null
bf1f6508
C
250 }
251 }
252
253 return VideoCommentModel
4635f59d 254 .scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.ATTRIBUTES_FOR_API ])
bf1f6508
C
255 .findAndCountAll(query)
256 .then(({ rows, count }) => {
257 return { total: count, data: rows }
258 })
259 }
260
261 static listThreadCommentsForApi (videoId: number, threadId: number) {
262 const query = {
a3fd560d 263 order: [ [ 'createdAt', 'ASC' ], [ 'updatedAt', 'ASC' ] ],
bf1f6508
C
264 where: {
265 videoId,
266 [ Sequelize.Op.or ]: [
267 { id: threadId },
268 { originCommentId: threadId }
269 ]
270 }
271 }
272
273 return VideoCommentModel
4635f59d 274 .scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.ATTRIBUTES_FOR_API ])
bf1f6508
C
275 .findAndCountAll(query)
276 .then(({ rows, count }) => {
277 return { total: count, data: rows }
278 })
279 }
280
2ccaeeb3 281 static listThreadParentComments (comment: VideoCommentModel, t: Sequelize.Transaction, order: 'ASC' | 'DESC' = 'ASC') {
d7e70384 282 const query = {
2ccaeeb3 283 order: [ [ 'createdAt', order ] ],
d7e70384
C
284 where: {
285 [ Sequelize.Op.or ]: [
286 { id: comment.getThreadId() },
287 { originCommentId: comment.getThreadId() }
288 ],
289 id: {
290 [ Sequelize.Op.ne ]: comment.id
2ccaeeb3
C
291 },
292 createdAt: {
293 [ Sequelize.Op.lt ]: comment.createdAt
d7e70384
C
294 }
295 },
296 transaction: t
297 }
298
299 return VideoCommentModel
300 .scope([ ScopeNames.WITH_ACCOUNT ])
301 .findAll(query)
302 }
303
304 getThreadId (): number {
305 return this.originCommentId || this.id
306 }
307
4cb6d457
C
308 isOwned () {
309 return this.Account.isOwned()
310 }
311
bf1f6508
C
312 toFormattedJSON () {
313 return {
314 id: this.id,
315 url: this.url,
316 text: this.text,
317 threadId: this.originCommentId || this.id,
d50acfab 318 inReplyToCommentId: this.inReplyToCommentId || null,
bf1f6508
C
319 videoId: this.videoId,
320 createdAt: this.createdAt,
d3ea8975 321 updatedAt: this.updatedAt,
4635f59d 322 totalReplies: this.get('totalReplies') || 0,
cf117aaa 323 account: this.Account.toFormattedJSON()
bf1f6508
C
324 } as VideoComment
325 }
ea44f375 326
d7e70384 327 toActivityPubObject (threadParentComments: VideoCommentModel[]): VideoCommentObject {
ea44f375
C
328 let inReplyTo: string
329 // New thread, so in AS we reply to the video
330 if (this.inReplyToCommentId === null) {
331 inReplyTo = this.Video.url
332 } else {
333 inReplyTo = this.InReplyToVideoComment.url
334 }
335
d7e70384
C
336 const tag: ActivityTagObject[] = []
337 for (const parentComment of threadParentComments) {
338 const actor = parentComment.Account.Actor
339
340 tag.push({
341 type: 'Mention',
342 href: actor.url,
343 name: `@${actor.preferredUsername}@${actor.getHost()}`
344 })
345 }
346
ea44f375
C
347 return {
348 type: 'Note' as 'Note',
349 id: this.url,
350 content: this.text,
351 inReplyTo,
da854ddd 352 updated: this.updatedAt.toISOString(),
ea44f375 353 published: this.createdAt.toISOString(),
da854ddd 354 url: this.url,
d7e70384
C
355 attributedTo: this.Account.Actor.url,
356 tag
ea44f375
C
357 }
358 }
6d852470 359}