]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/models/video/video-comment.ts
Fix actor followers/following counts
[github/Chocobozzz/PeerTube.git] / server / models / video / video-comment.ts
CommitLineData
6d852470
C
1import * as Sequelize from 'sequelize'
2import {
bf1f6508 3 AfterDestroy, AllowNull, 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
bf1f6508 178 @AfterDestroy
4cb6d457
C
179 static async sendDeleteIfOwned (instance: VideoCommentModel) {
180 if (instance.isOwned()) {
181 await sendDeleteVideoComment(instance, undefined)
182 }
bf1f6508
C
183 }
184
185 static loadById (id: number, t?: Sequelize.Transaction) {
186 const query: IFindOptions<VideoCommentModel> = {
187 where: {
188 id
189 }
190 }
191
192 if (t !== undefined) query.transaction = t
193
194 return VideoCommentModel.findOne(query)
195 }
196
da854ddd
C
197 static loadByIdAndPopulateVideoAndAccountAndReply (id: number, t?: Sequelize.Transaction) {
198 const query: IFindOptions<VideoCommentModel> = {
199 where: {
200 id
201 }
202 }
203
204 if (t !== undefined) query.transaction = t
205
206 return VideoCommentModel
207 .scope([ ScopeNames.WITH_VIDEO, ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_IN_REPLY_TO ])
208 .findOne(query)
209 }
210
2ccaeeb3 211 static loadByUrlAndPopulateAccount (url: string, t?: Sequelize.Transaction) {
6d852470
C
212 const query: IFindOptions<VideoCommentModel> = {
213 where: {
214 url
215 }
216 }
217
218 if (t !== undefined) query.transaction = t
219
2ccaeeb3 220 return VideoCommentModel.scope([ ScopeNames.WITH_ACCOUNT ]).findOne(query)
6d852470 221 }
bf1f6508 222
2ccaeeb3 223 static loadByUrlAndPopulateReplyAndVideo (url: string, t?: Sequelize.Transaction) {
4cb6d457
C
224 const query: IFindOptions<VideoCommentModel> = {
225 where: {
226 url
227 }
228 }
229
230 if (t !== undefined) query.transaction = t
231
2ccaeeb3 232 return VideoCommentModel.scope([ ScopeNames.WITH_IN_REPLY_TO, ScopeNames.WITH_VIDEO ]).findOne(query)
4cb6d457
C
233 }
234
bf1f6508
C
235 static listThreadsForApi (videoId: number, start: number, count: number, sort: string) {
236 const query = {
237 offset: start,
238 limit: count,
239 order: [ getSort(sort) ],
240 where: {
d3ea8975
C
241 videoId,
242 inReplyToCommentId: null
bf1f6508
C
243 }
244 }
245
246 return VideoCommentModel
4635f59d 247 .scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.ATTRIBUTES_FOR_API ])
bf1f6508
C
248 .findAndCountAll(query)
249 .then(({ rows, count }) => {
250 return { total: count, data: rows }
251 })
252 }
253
254 static listThreadCommentsForApi (videoId: number, threadId: number) {
255 const query = {
a3fd560d 256 order: [ [ 'createdAt', 'ASC' ], [ 'updatedAt', 'ASC' ] ],
bf1f6508
C
257 where: {
258 videoId,
259 [ Sequelize.Op.or ]: [
260 { id: threadId },
261 { originCommentId: threadId }
262 ]
263 }
264 }
265
266 return VideoCommentModel
4635f59d 267 .scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.ATTRIBUTES_FOR_API ])
bf1f6508
C
268 .findAndCountAll(query)
269 .then(({ rows, count }) => {
270 return { total: count, data: rows }
271 })
272 }
273
2ccaeeb3 274 static listThreadParentComments (comment: VideoCommentModel, t: Sequelize.Transaction, order: 'ASC' | 'DESC' = 'ASC') {
d7e70384 275 const query = {
2ccaeeb3 276 order: [ [ 'createdAt', order ] ],
d7e70384
C
277 where: {
278 [ Sequelize.Op.or ]: [
279 { id: comment.getThreadId() },
280 { originCommentId: comment.getThreadId() }
281 ],
282 id: {
283 [ Sequelize.Op.ne ]: comment.id
2ccaeeb3
C
284 },
285 createdAt: {
286 [ Sequelize.Op.lt ]: comment.createdAt
d7e70384
C
287 }
288 },
289 transaction: t
290 }
291
292 return VideoCommentModel
293 .scope([ ScopeNames.WITH_ACCOUNT ])
294 .findAll(query)
295 }
296
297 getThreadId (): number {
298 return this.originCommentId || this.id
299 }
300
4cb6d457
C
301 isOwned () {
302 return this.Account.isOwned()
303 }
304
bf1f6508
C
305 toFormattedJSON () {
306 return {
307 id: this.id,
308 url: this.url,
309 text: this.text,
310 threadId: this.originCommentId || this.id,
d50acfab 311 inReplyToCommentId: this.inReplyToCommentId || null,
bf1f6508
C
312 videoId: this.videoId,
313 createdAt: this.createdAt,
d3ea8975 314 updatedAt: this.updatedAt,
4635f59d 315 totalReplies: this.get('totalReplies') || 0,
cf117aaa 316 account: this.Account.toFormattedJSON()
bf1f6508
C
317 } as VideoComment
318 }
ea44f375 319
d7e70384 320 toActivityPubObject (threadParentComments: VideoCommentModel[]): VideoCommentObject {
ea44f375
C
321 let inReplyTo: string
322 // New thread, so in AS we reply to the video
323 if (this.inReplyToCommentId === null) {
324 inReplyTo = this.Video.url
325 } else {
326 inReplyTo = this.InReplyToVideoComment.url
327 }
328
d7e70384
C
329 const tag: ActivityTagObject[] = []
330 for (const parentComment of threadParentComments) {
331 const actor = parentComment.Account.Actor
332
333 tag.push({
334 type: 'Mention',
335 href: actor.url,
336 name: `@${actor.preferredUsername}@${actor.getHost()}`
337 })
338 }
339
ea44f375
C
340 return {
341 type: 'Note' as 'Note',
342 id: this.url,
343 content: this.text,
344 inReplyTo,
da854ddd 345 updated: this.updatedAt.toISOString(),
ea44f375 346 published: this.createdAt.toISOString(),
da854ddd 347 url: this.url,
d7e70384
C
348 attributedTo: this.Account.Actor.url,
349 tag
ea44f375
C
350 }
351 }
6d852470 352}