aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/controllers/api/videos/comment.ts88
-rw-r--r--server/helpers/custom-validators/activitypub/activity.ts4
-rw-r--r--server/helpers/custom-validators/activitypub/video-comments.ts40
-rw-r--r--server/initializers/constants.ts3
-rw-r--r--server/initializers/database.ts4
-rw-r--r--server/lib/activitypub/process/process-create.ts53
-rw-r--r--server/models/video/video-comment.ts95
-rw-r--r--server/models/video/video.ts12
8 files changed, 295 insertions, 4 deletions
diff --git a/server/controllers/api/videos/comment.ts b/server/controllers/api/videos/comment.ts
new file mode 100644
index 000000000..b69aa5d40
--- /dev/null
+++ b/server/controllers/api/videos/comment.ts
@@ -0,0 +1,88 @@
1// import * as express from 'express'
2// import { logger, getFormattedObjects } from '../../../helpers'
3// import {
4// authenticate,
5// ensureUserHasRight,
6// videosBlacklistAddValidator,
7// videosBlacklistRemoveValidator,
8// paginationValidator,
9// blacklistSortValidator,
10// setBlacklistSort,
11// setPagination,
12// asyncMiddleware
13// } from '../../../middlewares'
14// import { BlacklistedVideo, UserRight } from '../../../../shared'
15// import { VideoBlacklistModel } from '../../../models/video/video-blacklist'
16//
17// const videoCommentRouter = express.Router()
18//
19// videoCommentRouter.get('/:videoId/comment',
20// authenticate,
21// ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST),
22// asyncMiddleware(listVideoCommentsThreadsValidator),
23// asyncMiddleware(listVideoCommentsThreads)
24// )
25//
26// videoCommentRouter.post('/:videoId/comment',
27// authenticate,
28// ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST),
29// asyncMiddleware(videosBlacklistAddValidator),
30// asyncMiddleware(addVideoToBlacklist)
31// )
32//
33// videoCommentRouter.get('/blacklist',
34// authenticate,
35// ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST),
36// paginationValidator,
37// blacklistSortValidator,
38// setBlacklistSort,
39// setPagination,
40// asyncMiddleware(listBlacklist)
41// )
42//
43// videoCommentRouter.delete('/:videoId/blacklist',
44// authenticate,
45// ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST),
46// asyncMiddleware(videosBlacklistRemoveValidator),
47// asyncMiddleware(removeVideoFromBlacklistController)
48// )
49//
50// // ---------------------------------------------------------------------------
51//
52// export {
53// videoCommentRouter
54// }
55//
56// // ---------------------------------------------------------------------------
57//
58// async function addVideoToBlacklist (req: express.Request, res: express.Response, next: express.NextFunction) {
59// const videoInstance = res.locals.video
60//
61// const toCreate = {
62// videoId: videoInstance.id
63// }
64//
65// await VideoBlacklistModel.create(toCreate)
66// return res.type('json').status(204).end()
67// }
68//
69// async function listBlacklist (req: express.Request, res: express.Response, next: express.NextFunction) {
70// const resultList = await VideoBlacklistModel.listForApi(req.query.start, req.query.count, req.query.sort)
71//
72// return res.json(getFormattedObjects<BlacklistedVideo, VideoBlacklistModel>(resultList.data, resultList.total))
73// }
74//
75// async function removeVideoFromBlacklistController (req: express.Request, res: express.Response, next: express.NextFunction) {
76// const blacklistedVideo = res.locals.blacklistedVideo as VideoBlacklistModel
77//
78// try {
79// await blacklistedVideo.destroy()
80//
81// logger.info('Video %s removed from blacklist.', res.locals.video.uuid)
82//
83// return res.sendStatus(204)
84// } catch (err) {
85// logger.error('Some error while removing video %s from blacklist.', res.locals.video.uuid, err)
86// throw err
87// }
88// }
diff --git a/server/helpers/custom-validators/activitypub/activity.ts b/server/helpers/custom-validators/activitypub/activity.ts
index c402800a4..f2e137061 100644
--- a/server/helpers/custom-validators/activitypub/activity.ts
+++ b/server/helpers/custom-validators/activitypub/activity.ts
@@ -6,6 +6,7 @@ import { isActivityPubUrlValid } from './misc'
6import { isDislikeActivityValid, isLikeActivityValid } from './rate' 6import { isDislikeActivityValid, isLikeActivityValid } from './rate'
7import { isUndoActivityValid } from './undo' 7import { isUndoActivityValid } from './undo'
8import { isVideoChannelDeleteActivityValid, isVideoChannelUpdateActivityValid } from './video-channels' 8import { isVideoChannelDeleteActivityValid, isVideoChannelUpdateActivityValid } from './video-channels'
9import { isVideoCommentCreateActivityValid } from './video-comments'
9import { 10import {
10 isVideoFlagValid, 11 isVideoFlagValid,
11 isVideoTorrentCreateActivityValid, 12 isVideoTorrentCreateActivityValid,
@@ -59,7 +60,8 @@ function checkCreateActivity (activity: any) {
59 return isViewActivityValid(activity) || 60 return isViewActivityValid(activity) ||
60 isDislikeActivityValid(activity) || 61 isDislikeActivityValid(activity) ||
61 isVideoTorrentCreateActivityValid(activity) || 62 isVideoTorrentCreateActivityValid(activity) ||
62 isVideoFlagValid(activity) 63 isVideoFlagValid(activity) ||
64 isVideoCommentCreateActivityValid(activity)
63} 65}
64 66
65function checkUpdateActivity (activity: any) { 67function checkUpdateActivity (activity: any) {
diff --git a/server/helpers/custom-validators/activitypub/video-comments.ts b/server/helpers/custom-validators/activitypub/video-comments.ts
new file mode 100644
index 000000000..489ff27de
--- /dev/null
+++ b/server/helpers/custom-validators/activitypub/video-comments.ts
@@ -0,0 +1,40 @@
1import * as validator from 'validator'
2import { exists, isDateValid } from '../misc'
3import { isActivityPubUrlValid, isBaseActivityValid } from './misc'
4import * as sanitizeHtml from 'sanitize-html'
5
6function isVideoCommentCreateActivityValid (activity: any) {
7 return isBaseActivityValid(activity, 'Create') &&
8 isVideoCommentObjectValid(activity.object)
9}
10
11function isVideoCommentObjectValid (comment: any) {
12 return comment.type === 'Note' &&
13 isActivityPubUrlValid(comment.id) &&
14 sanitizeCommentHTML(comment) &&
15 isCommentContentValid(comment.content) &&
16 isActivityPubUrlValid(comment.inReplyTo) &&
17 isDateValid(comment.published) &&
18 isActivityPubUrlValid(comment.url)
19}
20
21// ---------------------------------------------------------------------------
22
23export {
24 isVideoCommentCreateActivityValid
25}
26
27// ---------------------------------------------------------------------------
28
29function sanitizeCommentHTML (comment: any) {
30 return sanitizeHtml(comment.content, {
31 allowedTags: [ 'b', 'i', 'em', 'span', 'a' ],
32 allowedAttributes: {
33 'a': [ 'href' ]
34 }
35 })
36}
37
38function isCommentContentValid (content: any) {
39 return exists(content) && validator.isLength('' + content, { min: 1 })
40}
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 100a77622..c8b21d10d 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -175,6 +175,9 @@ const CONSTRAINTS_FIELDS = {
175 }, 175 },
176 VIDEO_EVENTS: { 176 VIDEO_EVENTS: {
177 COUNT: { min: 0 } 177 COUNT: { min: 0 }
178 },
179 COMMENT: {
180 URL: { min: 3, max: 2000 } // Length
178 } 181 }
179} 182}
180 183
diff --git a/server/initializers/database.ts b/server/initializers/database.ts
index 0b3f695f7..852db68a0 100644
--- a/server/initializers/database.ts
+++ b/server/initializers/database.ts
@@ -18,6 +18,7 @@ import { VideoModel } from '../models/video/video'
18import { VideoAbuseModel } from '../models/video/video-abuse' 18import { VideoAbuseModel } from '../models/video/video-abuse'
19import { VideoBlacklistModel } from '../models/video/video-blacklist' 19import { VideoBlacklistModel } from '../models/video/video-blacklist'
20import { VideoChannelModel } from '../models/video/video-channel' 20import { VideoChannelModel } from '../models/video/video-channel'
21import { VideoCommentModel } from '../models/video/video-comment'
21import { VideoFileModel } from '../models/video/video-file' 22import { VideoFileModel } from '../models/video/video-file'
22import { VideoShareModel } from '../models/video/video-share' 23import { VideoShareModel } from '../models/video/video-share'
23import { VideoTagModel } from '../models/video/video-tag' 24import { VideoTagModel } from '../models/video/video-tag'
@@ -73,7 +74,8 @@ async function initDatabaseModels (silent: boolean) {
73 VideoFileModel, 74 VideoFileModel,
74 VideoBlacklistModel, 75 VideoBlacklistModel,
75 VideoTagModel, 76 VideoTagModel,
76 VideoModel 77 VideoModel,
78 VideoCommentModel
77 ]) 79 ])
78 80
79 if (!silent) logger.info('Database %s is ready.', dbname) 81 if (!silent) logger.info('Database %s is ready.', dbname)
diff --git a/server/lib/activitypub/process/process-create.ts b/server/lib/activitypub/process/process-create.ts
index 1ddd817db..102e54b19 100644
--- a/server/lib/activitypub/process/process-create.ts
+++ b/server/lib/activitypub/process/process-create.ts
@@ -1,6 +1,7 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { ActivityCreate, VideoTorrentObject } from '../../../../shared' 2import { ActivityCreate, VideoTorrentObject } from '../../../../shared'
3import { DislikeObject, VideoAbuseObject, ViewObject } from '../../../../shared/models/activitypub/objects' 3import { DislikeObject, VideoAbuseObject, ViewObject } from '../../../../shared/models/activitypub/objects'
4import { VideoCommentObject } from '../../../../shared/models/activitypub/objects/video-comment-object'
4import { VideoRateType } from '../../../../shared/models/videos' 5import { VideoRateType } from '../../../../shared/models/videos'
5import { logger, retryTransactionWrapper } from '../../../helpers' 6import { logger, retryTransactionWrapper } from '../../../helpers'
6import { sequelizeTypescript } from '../../../initializers' 7import { sequelizeTypescript } from '../../../initializers'
@@ -9,6 +10,7 @@ import { ActorModel } from '../../../models/activitypub/actor'
9import { TagModel } from '../../../models/video/tag' 10import { TagModel } from '../../../models/video/tag'
10import { VideoModel } from '../../../models/video/video' 11import { VideoModel } from '../../../models/video/video'
11import { VideoAbuseModel } from '../../../models/video/video-abuse' 12import { VideoAbuseModel } from '../../../models/video/video-abuse'
13import { VideoCommentModel } from '../../../models/video/video-comment'
12import { VideoFileModel } from '../../../models/video/video-file' 14import { VideoFileModel } from '../../../models/video/video-file'
13import { getOrCreateActorAndServerAndModel } from '../actor' 15import { getOrCreateActorAndServerAndModel } from '../actor'
14import { forwardActivity } from '../send/misc' 16import { forwardActivity } from '../send/misc'
@@ -28,6 +30,8 @@ async function processCreateActivity (activity: ActivityCreate) {
28 return processCreateVideo(actor, activity) 30 return processCreateVideo(actor, activity)
29 } else if (activityType === 'Flag') { 31 } else if (activityType === 'Flag') {
30 return processCreateVideoAbuse(actor, activityObject as VideoAbuseObject) 32 return processCreateVideoAbuse(actor, activityObject as VideoAbuseObject)
33 } else if (activityType === 'Note') {
34 return processCreateVideoComment(actor, activity)
31 } 35 }
32 36
33 logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id }) 37 logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id })
@@ -184,7 +188,7 @@ function createVideoDislike (byActor: ActorModel, activity: ActivityCreate) {
184 }) 188 })
185} 189}
186 190
187async function processCreateView (byAccount: ActorModel, activity: ActivityCreate) { 191async function processCreateView (byActor: ActorModel, activity: ActivityCreate) {
188 const view = activity.object as ViewObject 192 const view = activity.object as ViewObject
189 193
190 const video = await VideoModel.loadByUrlAndPopulateAccount(view.object) 194 const video = await VideoModel.loadByUrlAndPopulateAccount(view.object)
@@ -198,7 +202,7 @@ async function processCreateView (byAccount: ActorModel, activity: ActivityCreat
198 202
199 if (video.isOwned()) { 203 if (video.isOwned()) {
200 // Don't resend the activity to the sender 204 // Don't resend the activity to the sender
201 const exceptions = [ byAccount ] 205 const exceptions = [ byActor ]
202 await forwardActivity(activity, undefined, exceptions) 206 await forwardActivity(activity, undefined, exceptions)
203 } 207 }
204} 208}
@@ -236,3 +240,48 @@ function addRemoteVideoAbuse (actor: ActorModel, videoAbuseToCreateData: VideoAb
236 logger.info('Remote abuse for video uuid %s created', videoAbuseToCreateData.object) 240 logger.info('Remote abuse for video uuid %s created', videoAbuseToCreateData.object)
237 }) 241 })
238} 242}
243
244function processCreateVideoComment (byActor: ActorModel, activity: ActivityCreate) {
245 const options = {
246 arguments: [ byActor, activity ],
247 errorMessage: 'Cannot create video comment with many retries.'
248 }
249
250 return retryTransactionWrapper(createVideoComment, options)
251}
252
253function createVideoComment (byActor: ActorModel, activity: ActivityCreate) {
254 const comment = activity.object as VideoCommentObject
255 const byAccount = byActor.Account
256
257 if (!byAccount) throw new Error('Cannot create video comment with the non account actor ' + byActor.url)
258
259 return sequelizeTypescript.transaction(async t => {
260 const video = await VideoModel.loadByUrl(comment.inReplyTo, t)
261
262 // This is a new thread
263 if (video) {
264 return VideoCommentModel.create({
265 url: comment.id,
266 text: comment.content,
267 originCommentId: null,
268 inReplyToComment: null,
269 videoId: video.id,
270 actorId: byActor.id
271 }, { transaction: t })
272 }
273
274 const inReplyToComment = await VideoCommentModel.loadByUrl(comment.inReplyTo, t)
275 if (!inReplyToComment) throw new Error('Unknown replied comment ' + comment.inReplyTo)
276
277 const originCommentId = inReplyToComment.originCommentId || inReplyToComment.id
278 return VideoCommentModel.create({
279 url: comment.id,
280 text: comment.content,
281 originCommentId,
282 inReplyToCommentId: inReplyToComment.id,
283 videoId: inReplyToComment.videoId,
284 actorId: byActor.id
285 }, { transaction: t })
286 })
287}
diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts
new file mode 100644
index 000000000..92c0c6112
--- /dev/null
+++ b/server/models/video/video-comment.ts
@@ -0,0 +1,95 @@
1import * as Sequelize from 'sequelize'
2import {
3 AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, IFindOptions, Is, IsUUID, Model, Table,
4 UpdatedAt
5} from 'sequelize-typescript'
6import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub'
7import { CONSTRAINTS_FIELDS } from '../../initializers'
8import { ActorModel } from '../activitypub/actor'
9import { throwIfNotValid } from '../utils'
10import { VideoModel } from './video'
11
12@Table({
13 tableName: 'videoComment',
14 indexes: [
15 {
16 fields: [ 'videoId' ]
17 }
18 ]
19})
20export class VideoCommentModel extends Model<VideoCommentModel> {
21 @CreatedAt
22 createdAt: Date
23
24 @UpdatedAt
25 updatedAt: Date
26
27 @AllowNull(false)
28 @Is('VideoCommentUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
29 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max))
30 url: string
31
32 @AllowNull(false)
33 @Column(DataType.TEXT)
34 text: string
35
36 @ForeignKey(() => VideoCommentModel)
37 @Column
38 originCommentId: number
39
40 @BelongsTo(() => VideoCommentModel, {
41 foreignKey: {
42 allowNull: true
43 },
44 onDelete: 'CASCADE'
45 })
46 OriginVideoComment: VideoCommentModel
47
48 @ForeignKey(() => VideoCommentModel)
49 @Column
50 inReplyToCommentId: number
51
52 @BelongsTo(() => VideoCommentModel, {
53 foreignKey: {
54 allowNull: true
55 },
56 onDelete: 'CASCADE'
57 })
58 InReplyToVideoComment: VideoCommentModel
59
60 @ForeignKey(() => VideoModel)
61 @Column
62 videoId: number
63
64 @BelongsTo(() => VideoModel, {
65 foreignKey: {
66 allowNull: false
67 },
68 onDelete: 'CASCADE'
69 })
70 Video: VideoModel
71
72 @ForeignKey(() => ActorModel)
73 @Column
74 actorId: number
75
76 @BelongsTo(() => ActorModel, {
77 foreignKey: {
78 allowNull: false
79 },
80 onDelete: 'CASCADE'
81 })
82 Actor: ActorModel
83
84 static loadByUrl (url: string, t?: Sequelize.Transaction) {
85 const query: IFindOptions<VideoCommentModel> = {
86 where: {
87 url
88 }
89 }
90
91 if (t !== undefined) query.transaction = t
92
93 return VideoCommentModel.findOne(query)
94 }
95}
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 8c49bc3af..b6a2ce6b5 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -491,6 +491,18 @@ export class VideoModel extends Model<VideoModel> {
491 return VideoModel.findById(id) 491 return VideoModel.findById(id)
492 } 492 }
493 493
494 static loadByUrl (url: string, t?: Sequelize.Transaction) {
495 const query: IFindOptions<VideoModel> = {
496 where: {
497 url
498 }
499 }
500
501 if (t !== undefined) query.transaction = t
502
503 return VideoModel.findOne(query)
504 }
505
494 static loadByUrlAndPopulateAccount (url: string, t?: Sequelize.Transaction) { 506 static loadByUrlAndPopulateAccount (url: string, t?: Sequelize.Transaction) {
495 const query: IFindOptions<VideoModel> = { 507 const query: IFindOptions<VideoModel> = {
496 where: { 508 where: {