aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2017-12-22 10:50:07 +0100
committerChocobozzz <me@florianbigard.com>2017-12-22 11:29:12 +0100
commitbf1f650817dadfd5eeee9e5e0b6b6938c136e25d (patch)
tree0f1dc95d87089be177ebe60740a55dd0c96b2414
parent6d8524702874120a4667269a81a61e3c7c5e300d (diff)
downloadPeerTube-bf1f650817dadfd5eeee9e5e0b6b6938c136e25d.tar.gz
PeerTube-bf1f650817dadfd5eeee9e5e0b6b6938c136e25d.tar.zst
PeerTube-bf1f650817dadfd5eeee9e5e0b6b6938c136e25d.zip
Add comments controller
-rw-r--r--server/controllers/api/videos/comment.ts202
-rw-r--r--server/controllers/api/videos/index.ts2
-rw-r--r--server/helpers/custom-validators/video-comments.ts16
-rw-r--r--server/initializers/constants.ts4
-rw-r--r--server/lib/activitypub/url.ts13
-rw-r--r--server/lib/video-comment.ts74
-rw-r--r--server/middlewares/sort.ts9
-rw-r--r--server/middlewares/validators/sort.ts5
-rw-r--r--server/middlewares/validators/video-comments.ts131
-rw-r--r--server/models/video/video-comment.ts88
-rw-r--r--shared/models/videos/video-comment.model.ts19
11 files changed, 464 insertions, 99 deletions
diff --git a/server/controllers/api/videos/comment.ts b/server/controllers/api/videos/comment.ts
index b69aa5d40..81c9e7d16 100644
--- a/server/controllers/api/videos/comment.ts
+++ b/server/controllers/api/videos/comment.ts
@@ -1,88 +1,114 @@
1// import * as express from 'express' 1import * as express from 'express'
2// import { logger, getFormattedObjects } from '../../../helpers' 2import { VideoCommentCreate } from '../../../../shared/models/videos/video-comment.model'
3// import { 3import { getFormattedObjects, retryTransactionWrapper } from '../../../helpers'
4// authenticate, 4import { sequelizeTypescript } from '../../../initializers'
5// ensureUserHasRight, 5import { buildFormattedCommentTree, createVideoComment } from '../../../lib/video-comment'
6// videosBlacklistAddValidator, 6import { asyncMiddleware, authenticate, paginationValidator, setPagination, setVideoCommentThreadsSort } from '../../../middlewares'
7// videosBlacklistRemoveValidator, 7import { videoCommentThreadsSortValidator } from '../../../middlewares/validators'
8// paginationValidator, 8import {
9// blacklistSortValidator, 9 addVideoCommentReplyValidator, addVideoCommentThreadValidator, listVideoCommentThreadsValidator,
10// setBlacklistSort, 10 listVideoThreadCommentsValidator
11// setPagination, 11} from '../../../middlewares/validators/video-comments'
12// asyncMiddleware 12import { VideoCommentModel } from '../../../models/video/video-comment'
13// } from '../../../middlewares' 13
14// import { BlacklistedVideo, UserRight } from '../../../../shared' 14const videoCommentRouter = express.Router()
15// import { VideoBlacklistModel } from '../../../models/video/video-blacklist' 15
16// 16videoCommentRouter.get('/:videoId/comment-threads',
17// const videoCommentRouter = express.Router() 17 paginationValidator,
18// 18 videoCommentThreadsSortValidator,
19// videoCommentRouter.get('/:videoId/comment', 19 setVideoCommentThreadsSort,
20// authenticate, 20 setPagination,
21// ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST), 21 asyncMiddleware(listVideoCommentThreadsValidator),
22// asyncMiddleware(listVideoCommentsThreadsValidator), 22 asyncMiddleware(listVideoThreads)
23// asyncMiddleware(listVideoCommentsThreads) 23)
24// ) 24videoCommentRouter.get('/:videoId/comment-threads/:threadId',
25// 25 asyncMiddleware(listVideoThreadCommentsValidator),
26// videoCommentRouter.post('/:videoId/comment', 26 asyncMiddleware(listVideoThreadComments)
27// authenticate, 27)
28// ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST), 28
29// asyncMiddleware(videosBlacklistAddValidator), 29videoCommentRouter.post('/:videoId/comment-threads',
30// asyncMiddleware(addVideoToBlacklist) 30 authenticate,
31// ) 31 asyncMiddleware(addVideoCommentThreadValidator),
32// 32 asyncMiddleware(addVideoCommentThreadRetryWrapper)
33// videoCommentRouter.get('/blacklist', 33)
34// authenticate, 34videoCommentRouter.post('/:videoId/comments/:commentId',
35// ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST), 35 authenticate,
36// paginationValidator, 36 asyncMiddleware(addVideoCommentReplyValidator),
37// blacklistSortValidator, 37 asyncMiddleware(addVideoCommentReplyRetryWrapper)
38// setBlacklistSort, 38)
39// setPagination, 39
40// asyncMiddleware(listBlacklist) 40// ---------------------------------------------------------------------------
41// ) 41
42// 42export {
43// videoCommentRouter.delete('/:videoId/blacklist', 43 videoCommentRouter
44// authenticate, 44}
45// ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST), 45
46// asyncMiddleware(videosBlacklistRemoveValidator), 46// ---------------------------------------------------------------------------
47// asyncMiddleware(removeVideoFromBlacklistController) 47
48// ) 48async function listVideoThreads (req: express.Request, res: express.Response, next: express.NextFunction) {
49// 49 const resultList = await VideoCommentModel.listThreadsForApi(res.locals.video.id, req.query.start, req.query.count, req.query.sort)
50// // --------------------------------------------------------------------------- 50
51// 51 return res.json(getFormattedObjects(resultList.data, resultList.total))
52// export { 52}
53// videoCommentRouter 53
54// } 54async function listVideoThreadComments (req: express.Request, res: express.Response, next: express.NextFunction) {
55// 55 const resultList = await VideoCommentModel.listThreadCommentsForApi(res.locals.video.id, res.locals.videoCommentThread.id)
56// // --------------------------------------------------------------------------- 56
57// 57 return res.json(buildFormattedCommentTree(resultList))
58// async function addVideoToBlacklist (req: express.Request, res: express.Response, next: express.NextFunction) { 58}
59// const videoInstance = res.locals.video 59
60// 60async function addVideoCommentThreadRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) {
61// const toCreate = { 61 const options = {
62// videoId: videoInstance.id 62 arguments: [ req, res ],
63// } 63 errorMessage: 'Cannot insert the video comment thread with many retries.'
64// 64 }
65// await VideoBlacklistModel.create(toCreate) 65
66// return res.type('json').status(204).end() 66 const comment = await retryTransactionWrapper(addVideoCommentThread, options)
67// } 67
68// 68 res.json({
69// async function listBlacklist (req: express.Request, res: express.Response, next: express.NextFunction) { 69 comment: {
70// const resultList = await VideoBlacklistModel.listForApi(req.query.start, req.query.count, req.query.sort) 70 id: comment.id
71// 71 }
72// return res.json(getFormattedObjects<BlacklistedVideo, VideoBlacklistModel>(resultList.data, resultList.total)) 72 }).end()
73// } 73}
74// 74
75// async function removeVideoFromBlacklistController (req: express.Request, res: express.Response, next: express.NextFunction) { 75function addVideoCommentThread (req: express.Request, res: express.Response) {
76// const blacklistedVideo = res.locals.blacklistedVideo as VideoBlacklistModel 76 const videoCommentInfo: VideoCommentCreate = req.body
77// 77
78// try { 78 return sequelizeTypescript.transaction(async t => {
79// await blacklistedVideo.destroy() 79 return createVideoComment({
80// 80 text: videoCommentInfo.text,
81// logger.info('Video %s removed from blacklist.', res.locals.video.uuid) 81 inReplyToComment: null,
82// 82 video: res.locals.video,
83// return res.sendStatus(204) 83 actorId: res.locals.oauth.token.User.Account.Actor.id
84// } catch (err) { 84 }, t)
85// logger.error('Some error while removing video %s from blacklist.', res.locals.video.uuid, err) 85 })
86// throw err 86}
87// } 87
88// } 88async function addVideoCommentReplyRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) {
89 const options = {
90 arguments: [ req, res ],
91 errorMessage: 'Cannot insert the video comment reply with many retries.'
92 }
93
94 const comment = await retryTransactionWrapper(addVideoCommentReply, options)
95
96 res.json({
97 comment: {
98 id: comment.id
99 }
100 }).end()
101}
102
103function addVideoCommentReply (req: express.Request, res: express.Response, next: express.NextFunction) {
104 const videoCommentInfo: VideoCommentCreate = req.body
105
106 return sequelizeTypescript.transaction(async t => {
107 return createVideoComment({
108 text: videoCommentInfo.text,
109 inReplyToComment: res.locals.videoComment.id,
110 video: res.locals.video,
111 actorId: res.locals.oauth.token.User.Account.Actor.id
112 }, t)
113 })
114}
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index 8283f2e4e..8e54d95ab 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -47,6 +47,7 @@ import { VideoFileModel } from '../../../models/video/video-file'
47import { abuseVideoRouter } from './abuse' 47import { abuseVideoRouter } from './abuse'
48import { blacklistRouter } from './blacklist' 48import { blacklistRouter } from './blacklist'
49import { videoChannelRouter } from './channel' 49import { videoChannelRouter } from './channel'
50import { videoCommentRouter } from './comment'
50import { rateVideoRouter } from './rate' 51import { rateVideoRouter } from './rate'
51 52
52const videosRouter = express.Router() 53const videosRouter = express.Router()
@@ -78,6 +79,7 @@ videosRouter.use('/', abuseVideoRouter)
78videosRouter.use('/', blacklistRouter) 79videosRouter.use('/', blacklistRouter)
79videosRouter.use('/', rateVideoRouter) 80videosRouter.use('/', rateVideoRouter)
80videosRouter.use('/', videoChannelRouter) 81videosRouter.use('/', videoChannelRouter)
82videosRouter.use('/', videoCommentRouter)
81 83
82videosRouter.get('/categories', listVideoCategories) 84videosRouter.get('/categories', listVideoCategories)
83videosRouter.get('/licences', listVideoLicences) 85videosRouter.get('/licences', listVideoLicences)
diff --git a/server/helpers/custom-validators/video-comments.ts b/server/helpers/custom-validators/video-comments.ts
new file mode 100644
index 000000000..2b3f66063
--- /dev/null
+++ b/server/helpers/custom-validators/video-comments.ts
@@ -0,0 +1,16 @@
1import 'express-validator'
2import 'multer'
3import * as validator from 'validator'
4import { CONSTRAINTS_FIELDS } from '../../initializers'
5
6const VIDEO_COMMENTS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_COMMENTS
7
8function isValidVideoCommentText (value: string) {
9 return value === null || validator.isLength(value, VIDEO_COMMENTS_CONSTRAINTS_FIELDS.TEXT)
10}
11
12// ---------------------------------------------------------------------------
13
14export {
15 isValidVideoCommentText
16}
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index c8b21d10d..25b2dff84 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -26,6 +26,7 @@ const SORTABLE_COLUMNS = {
26 VIDEO_ABUSES: [ 'id', 'createdAt' ], 26 VIDEO_ABUSES: [ 'id', 'createdAt' ],
27 VIDEO_CHANNELS: [ 'id', 'name', 'updatedAt', 'createdAt' ], 27 VIDEO_CHANNELS: [ 'id', 'name', 'updatedAt', 'createdAt' ],
28 VIDEOS: [ 'name', 'duration', 'createdAt', 'views', 'likes' ], 28 VIDEOS: [ 'name', 'duration', 'createdAt', 'views', 'likes' ],
29 VIDEO_COMMENT_THREADS: [ 'createdAt' ],
29 BLACKLISTS: [ 'id', 'name', 'duration', 'views', 'likes', 'dislikes', 'uuid', 'createdAt' ], 30 BLACKLISTS: [ 'id', 'name', 'duration', 'views', 'likes', 'dislikes', 'uuid', 'createdAt' ],
30 FOLLOWERS: [ 'createdAt' ], 31 FOLLOWERS: [ 'createdAt' ],
31 FOLLOWING: [ 'createdAt' ] 32 FOLLOWING: [ 'createdAt' ]
@@ -176,7 +177,8 @@ const CONSTRAINTS_FIELDS = {
176 VIDEO_EVENTS: { 177 VIDEO_EVENTS: {
177 COUNT: { min: 0 } 178 COUNT: { min: 0 }
178 }, 179 },
179 COMMENT: { 180 VIDEO_COMMENTS: {
181 TEXT: { min: 2, max: 3000 }, // Length
180 URL: { min: 3, max: 2000 } // Length 182 URL: { min: 3, max: 2000 } // Length
181 } 183 }
182} 184}
diff --git a/server/lib/activitypub/url.ts b/server/lib/activitypub/url.ts
index bb2d4d11e..729bb8dda 100644
--- a/server/lib/activitypub/url.ts
+++ b/server/lib/activitypub/url.ts
@@ -3,17 +3,18 @@ import { ActorModel } from '../../models/activitypub/actor'
3import { ActorFollowModel } from '../../models/activitypub/actor-follow' 3import { ActorFollowModel } from '../../models/activitypub/actor-follow'
4import { VideoModel } from '../../models/video/video' 4import { VideoModel } from '../../models/video/video'
5import { VideoAbuseModel } from '../../models/video/video-abuse' 5import { VideoAbuseModel } from '../../models/video/video-abuse'
6import { VideoCommentModel } from '../../models/video/video-comment'
6 7
7function getVideoActivityPubUrl (video: VideoModel) { 8function getVideoActivityPubUrl (video: VideoModel) {
8 return CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid 9 return CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid
9} 10}
10 11
11function getVideoChannelActivityPubUrl (videoChannelUUID: string) { 12function getVideoCommentActivityPubUrl (video: VideoModel, videoComment: VideoCommentModel) {
12 return CONFIG.WEBSERVER.URL + '/video-channels/' + videoChannelUUID 13 return CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid + '#comment-' + videoComment.id
13} 14}
14 15
15function getApplicationActivityPubUrl () { 16function getVideoChannelActivityPubUrl (videoChannelUUID: string) {
16 return CONFIG.WEBSERVER.URL + '/application/peertube' 17 return CONFIG.WEBSERVER.URL + '/video-channels/' + videoChannelUUID
17} 18}
18 19
19function getAccountActivityPubUrl (accountName: string) { 20function getAccountActivityPubUrl (accountName: string) {
@@ -63,7 +64,6 @@ function getUndoActivityPubUrl (originalUrl: string) {
63} 64}
64 65
65export { 66export {
66 getApplicationActivityPubUrl,
67 getVideoActivityPubUrl, 67 getVideoActivityPubUrl,
68 getVideoChannelActivityPubUrl, 68 getVideoChannelActivityPubUrl,
69 getAccountActivityPubUrl, 69 getAccountActivityPubUrl,
@@ -75,5 +75,6 @@ export {
75 getUndoActivityPubUrl, 75 getUndoActivityPubUrl,
76 getVideoViewActivityPubUrl, 76 getVideoViewActivityPubUrl,
77 getVideoLikeActivityPubUrl, 77 getVideoLikeActivityPubUrl,
78 getVideoDislikeActivityPubUrl 78 getVideoDislikeActivityPubUrl,
79 getVideoCommentActivityPubUrl
79} 80}
diff --git a/server/lib/video-comment.ts b/server/lib/video-comment.ts
new file mode 100644
index 000000000..edb72d4e2
--- /dev/null
+++ b/server/lib/video-comment.ts
@@ -0,0 +1,74 @@
1import * as Sequelize from 'sequelize'
2import { ResultList } from '../../shared/models'
3import { VideoCommentThread } from '../../shared/models/videos/video-comment.model'
4import { VideoModel } from '../models/video/video'
5import { VideoCommentModel } from '../models/video/video-comment'
6import { getVideoCommentActivityPubUrl } from './activitypub'
7
8async function createVideoComment (obj: {
9 text: string,
10 inReplyToComment: number,
11 video: VideoModel
12 actorId: number
13}, t: Sequelize.Transaction) {
14 let originCommentId: number = null
15 if (obj.inReplyToComment) {
16 const repliedComment = await VideoCommentModel.loadById(obj.inReplyToComment)
17 if (!repliedComment) throw new Error('Unknown replied comment.')
18
19 originCommentId = repliedComment.originCommentId || repliedComment.id
20 }
21
22 const comment = await VideoCommentModel.create({
23 text: obj.text,
24 originCommentId,
25 inReplyToComment: obj.inReplyToComment,
26 videoId: obj.video.id,
27 actorId: obj.actorId
28 }, { transaction: t })
29
30 comment.set('url', getVideoCommentActivityPubUrl(obj.video, comment))
31
32 return comment.save({ transaction: t })
33}
34
35function buildFormattedCommentTree (resultList: ResultList<VideoCommentModel>): VideoCommentThread {
36 // Comments are sorted by id ASC
37 const comments = resultList.data
38
39 const comment = comments.shift()
40 const thread: VideoCommentThread = {
41 comment: comment.toFormattedJSON(),
42 children: []
43 }
44 const idx = {
45 [comment.id]: thread
46 }
47
48 while (comments.length !== 0) {
49 const childComment = comments.shift()
50
51 const childCommentThread: VideoCommentThread = {
52 comment: childComment.toFormattedJSON(),
53 children: []
54 }
55
56 const parentCommentThread = idx[childComment.inReplyToCommentId]
57 if (!parentCommentThread) {
58 const msg = `Cannot format video thread tree, parent ${childComment.inReplyToCommentId} not found for child ${childComment.id}`
59 throw new Error(msg)
60 }
61
62 parentCommentThread.children.push(childCommentThread)
63 idx[childComment.id] = childCommentThread
64 }
65
66 return thread
67}
68
69// ---------------------------------------------------------------------------
70
71export {
72 createVideoComment,
73 buildFormattedCommentTree
74}
diff --git a/server/middlewares/sort.ts b/server/middlewares/sort.ts
index 5d2a43acc..0eb50db89 100644
--- a/server/middlewares/sort.ts
+++ b/server/middlewares/sort.ts
@@ -32,6 +32,12 @@ function setVideosSort (req: express.Request, res: express.Response, next: expre
32 return next() 32 return next()
33} 33}
34 34
35function setVideoCommentThreadsSort (req: express.Request, res: express.Response, next: express.NextFunction) {
36 if (!req.query.sort) req.query.sort = '-createdAt'
37
38 return next()
39}
40
35function setFollowersSort (req: express.Request, res: express.Response, next: express.NextFunction) { 41function setFollowersSort (req: express.Request, res: express.Response, next: express.NextFunction) {
36 if (!req.query.sort) req.query.sort = '-createdAt' 42 if (!req.query.sort) req.query.sort = '-createdAt'
37 43
@@ -75,5 +81,6 @@ export {
75 setBlacklistSort, 81 setBlacklistSort,
76 setFollowersSort, 82 setFollowersSort,
77 setFollowingSort, 83 setFollowingSort,
78 setJobsSort 84 setJobsSort,
85 setVideoCommentThreadsSort
79} 86}
diff --git a/server/middlewares/validators/sort.ts b/server/middlewares/validators/sort.ts
index 38184fefa..56855bda0 100644
--- a/server/middlewares/validators/sort.ts
+++ b/server/middlewares/validators/sort.ts
@@ -9,6 +9,7 @@ const SORTABLE_USERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USERS)
9const SORTABLE_JOBS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.JOBS) 9const SORTABLE_JOBS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.JOBS)
10const SORTABLE_VIDEO_ABUSES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_ABUSES) 10const SORTABLE_VIDEO_ABUSES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_ABUSES)
11const SORTABLE_VIDEOS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS) 11const SORTABLE_VIDEOS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS)
12const SORTABLE_VIDEO_COMMENT_THREADS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_COMMENT_THREADS)
12const SORTABLE_BLACKLISTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.BLACKLISTS) 13const SORTABLE_BLACKLISTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.BLACKLISTS)
13const SORTABLE_VIDEO_CHANNELS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_CHANNELS) 14const SORTABLE_VIDEO_CHANNELS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_CHANNELS)
14const SORTABLE_FOLLOWERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWERS) 15const SORTABLE_FOLLOWERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWERS)
@@ -18,6 +19,7 @@ const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS)
18const jobsSortValidator = checkSort(SORTABLE_JOBS_COLUMNS) 19const jobsSortValidator = checkSort(SORTABLE_JOBS_COLUMNS)
19const videoAbusesSortValidator = checkSort(SORTABLE_VIDEO_ABUSES_COLUMNS) 20const videoAbusesSortValidator = checkSort(SORTABLE_VIDEO_ABUSES_COLUMNS)
20const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS) 21const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS)
22const videoCommentThreadsSortValidator = checkSort(SORTABLE_VIDEO_COMMENT_THREADS_COLUMNS)
21const blacklistSortValidator = checkSort(SORTABLE_BLACKLISTS_COLUMNS) 23const blacklistSortValidator = checkSort(SORTABLE_BLACKLISTS_COLUMNS)
22const videoChannelsSortValidator = checkSort(SORTABLE_VIDEO_CHANNELS_COLUMNS) 24const videoChannelsSortValidator = checkSort(SORTABLE_VIDEO_CHANNELS_COLUMNS)
23const followersSortValidator = checkSort(SORTABLE_FOLLOWERS_COLUMNS) 25const followersSortValidator = checkSort(SORTABLE_FOLLOWERS_COLUMNS)
@@ -33,7 +35,8 @@ export {
33 blacklistSortValidator, 35 blacklistSortValidator,
34 followersSortValidator, 36 followersSortValidator,
35 followingSortValidator, 37 followingSortValidator,
36 jobsSortValidator 38 jobsSortValidator,
39 videoCommentThreadsSortValidator
37} 40}
38 41
39// --------------------------------------------------------------------------- 42// ---------------------------------------------------------------------------
diff --git a/server/middlewares/validators/video-comments.ts b/server/middlewares/validators/video-comments.ts
new file mode 100644
index 000000000..5e1be00f2
--- /dev/null
+++ b/server/middlewares/validators/video-comments.ts
@@ -0,0 +1,131 @@
1import * as express from 'express'
2import { body, param } from 'express-validator/check'
3import { logger } from '../../helpers'
4import { isIdOrUUIDValid, isIdValid } from '../../helpers/custom-validators/misc'
5import { isValidVideoCommentText } from '../../helpers/custom-validators/video-comments'
6import { isVideoExist } from '../../helpers/custom-validators/videos'
7import { VideoCommentModel } from '../../models/video/video-comment'
8import { areValidationErrors } from './utils'
9
10const listVideoCommentThreadsValidator = [
11 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
12
13 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
14 logger.debug('Checking blacklistRemove parameters.', { parameters: req.params })
15
16 if (areValidationErrors(req, res)) return
17 if (!await isVideoExist(req.params.videoId, res)) return
18
19 return next()
20 }
21]
22
23const listVideoThreadCommentsValidator = [
24 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
25 param('threadId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid threadId'),
26
27 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
28 logger.debug('Checking blacklistRemove parameters.', { parameters: req.params })
29
30 if (areValidationErrors(req, res)) return
31 if (!await isVideoExist(req.params.videoId, res)) return
32 if (!await isVideoCommentThreadExist(req.params.threadId, req.params.videoId, res)) return
33
34 return next()
35 }
36]
37
38const addVideoCommentThreadValidator = [
39 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
40 body('text').custom(isValidVideoCommentText).not().isEmpty().withMessage('Should have a valid comment text'),
41
42 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
43 logger.debug('Checking blacklistRemove parameters.', { parameters: req.params })
44
45 if (areValidationErrors(req, res)) return
46 if (!await isVideoExist(req.params.videoId, res)) return
47
48 return next()
49 }
50]
51
52const addVideoCommentReplyValidator = [
53 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
54 param('commentId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid commentId'),
55 body('text').custom(isValidVideoCommentText).not().isEmpty().withMessage('Should have a valid comment text'),
56
57 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
58 logger.debug('Checking blacklistRemove parameters.', { parameters: req.params })
59
60 if (areValidationErrors(req, res)) return
61 if (!await isVideoExist(req.params.videoId, res)) return
62 if (!await isVideoCommentExist(req.params.commentId, req.params.videoId, res)) return
63
64 return next()
65 }
66]
67
68// ---------------------------------------------------------------------------
69
70export {
71 listVideoCommentThreadsValidator,
72 listVideoThreadCommentsValidator,
73 addVideoCommentThreadValidator,
74 addVideoCommentReplyValidator
75}
76
77// ---------------------------------------------------------------------------
78
79async function isVideoCommentThreadExist (id: number, videoId: number, res: express.Response) {
80 const videoComment = await VideoCommentModel.loadById(id)
81
82 if (!videoComment) {
83 res.status(404)
84 .json({ error: 'Video comment thread not found' })
85 .end()
86
87 return false
88 }
89
90 if (videoComment.videoId !== videoId) {
91 res.status(400)
92 .json({ error: 'Video comment is associated to this video.' })
93 .end()
94
95 return false
96 }
97
98 if (videoComment.inReplyToCommentId !== null) {
99 res.status(400)
100 .json({ error: 'Video comment is not a thread.' })
101 .end()
102
103 return false
104 }
105
106 res.locals.videoCommentThread = videoComment
107 return true
108}
109
110async function isVideoCommentExist (id: number, videoId: number, res: express.Response) {
111 const videoComment = await VideoCommentModel.loadById(id)
112
113 if (!videoComment) {
114 res.status(404)
115 .json({ error: 'Video comment thread not found' })
116 .end()
117
118 return false
119 }
120
121 if (videoComment.videoId !== videoId) {
122 res.status(400)
123 .json({ error: 'Video comment is associated to this video.' })
124 .end()
125
126 return false
127 }
128
129 res.locals.videoComment = videoComment
130 return true
131}
diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts
index 92c0c6112..d66f933ee 100644
--- a/server/models/video/video-comment.ts
+++ b/server/models/video/video-comment.ts
@@ -1,19 +1,34 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import { 2import {
3 AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, IFindOptions, Is, IsUUID, Model, Table, 3 AfterDestroy, AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, IFindOptions, Is, Model, Scopes, Table,
4 UpdatedAt 4 UpdatedAt
5} from 'sequelize-typescript' 5} from 'sequelize-typescript'
6import { VideoComment } from '../../../shared/models/videos/video-comment.model'
6import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub' 7import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub'
7import { CONSTRAINTS_FIELDS } from '../../initializers' 8import { CONSTRAINTS_FIELDS } from '../../initializers'
8import { ActorModel } from '../activitypub/actor' 9import { ActorModel } from '../activitypub/actor'
9import { throwIfNotValid } from '../utils' 10import { getSort, throwIfNotValid } from '../utils'
10import { VideoModel } from './video' 11import { VideoModel } from './video'
11 12
13enum ScopeNames {
14 WITH_ACTOR = 'WITH_ACTOR'
15}
16
17@Scopes({
18 [ScopeNames.WITH_ACTOR]: {
19 include: [
20 () => ActorModel
21 ]
22 }
23})
12@Table({ 24@Table({
13 tableName: 'videoComment', 25 tableName: 'videoComment',
14 indexes: [ 26 indexes: [
15 { 27 {
16 fields: [ 'videoId' ] 28 fields: [ 'videoId' ]
29 },
30 {
31 fields: [ 'videoId', 'originCommentId' ]
17 } 32 }
18 ] 33 ]
19}) 34})
@@ -81,6 +96,24 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
81 }) 96 })
82 Actor: ActorModel 97 Actor: ActorModel
83 98
99 @AfterDestroy
100 static sendDeleteIfOwned (instance: VideoCommentModel) {
101 // TODO
102 return undefined
103 }
104
105 static loadById (id: number, t?: Sequelize.Transaction) {
106 const query: IFindOptions<VideoCommentModel> = {
107 where: {
108 id
109 }
110 }
111
112 if (t !== undefined) query.transaction = t
113
114 return VideoCommentModel.findOne(query)
115 }
116
84 static loadByUrl (url: string, t?: Sequelize.Transaction) { 117 static loadByUrl (url: string, t?: Sequelize.Transaction) {
85 const query: IFindOptions<VideoCommentModel> = { 118 const query: IFindOptions<VideoCommentModel> = {
86 where: { 119 where: {
@@ -92,4 +125,55 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
92 125
93 return VideoCommentModel.findOne(query) 126 return VideoCommentModel.findOne(query)
94 } 127 }
128
129 static listThreadsForApi (videoId: number, start: number, count: number, sort: string) {
130 const query = {
131 offset: start,
132 limit: count,
133 order: [ getSort(sort) ],
134 where: {
135 videoId
136 }
137 }
138
139 return VideoCommentModel
140 .scope([ ScopeNames.WITH_ACTOR ])
141 .findAndCountAll(query)
142 .then(({ rows, count }) => {
143 return { total: count, data: rows }
144 })
145 }
146
147 static listThreadCommentsForApi (videoId: number, threadId: number) {
148 const query = {
149 order: [ 'id', 'ASC' ],
150 where: {
151 videoId,
152 [ Sequelize.Op.or ]: [
153 { id: threadId },
154 { originCommentId: threadId }
155 ]
156 }
157 }
158
159 return VideoCommentModel
160 .scope([ ScopeNames.WITH_ACTOR ])
161 .findAndCountAll(query)
162 .then(({ rows, count }) => {
163 return { total: count, data: rows }
164 })
165 }
166
167 toFormattedJSON () {
168 return {
169 id: this.id,
170 url: this.url,
171 text: this.text,
172 threadId: this.originCommentId || this.id,
173 inReplyToCommentId: this.inReplyToCommentId,
174 videoId: this.videoId,
175 createdAt: this.createdAt,
176 updatedAt: this.updatedAt
177 } as VideoComment
178 }
95} 179}
diff --git a/shared/models/videos/video-comment.model.ts b/shared/models/videos/video-comment.model.ts
new file mode 100644
index 000000000..bdeb30d28
--- /dev/null
+++ b/shared/models/videos/video-comment.model.ts
@@ -0,0 +1,19 @@
1export interface VideoComment {
2 id: number
3 url: string
4 text: string
5 threadId: number
6 inReplyToCommentId: number
7 videoId: number
8 createdAt: Date | string
9 updatedAt: Date | string
10}
11
12export interface VideoCommentThread {
13 comment: VideoComment
14 children: VideoCommentThread[]
15}
16
17export interface VideoCommentCreate {
18 text: string
19}