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