diff options
author | Chocobozzz <me@florianbigard.com> | 2017-12-22 10:50:07 +0100 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2017-12-22 11:29:12 +0100 |
commit | bf1f650817dadfd5eeee9e5e0b6b6938c136e25d (patch) | |
tree | 0f1dc95d87089be177ebe60740a55dd0c96b2414 | |
parent | 6d8524702874120a4667269a81a61e3c7c5e300d (diff) | |
download | PeerTube-bf1f650817dadfd5eeee9e5e0b6b6938c136e25d.tar.gz PeerTube-bf1f650817dadfd5eeee9e5e0b6b6938c136e25d.tar.zst PeerTube-bf1f650817dadfd5eeee9e5e0b6b6938c136e25d.zip |
Add comments controller
-rw-r--r-- | server/controllers/api/videos/comment.ts | 202 | ||||
-rw-r--r-- | server/controllers/api/videos/index.ts | 2 | ||||
-rw-r--r-- | server/helpers/custom-validators/video-comments.ts | 16 | ||||
-rw-r--r-- | server/initializers/constants.ts | 4 | ||||
-rw-r--r-- | server/lib/activitypub/url.ts | 13 | ||||
-rw-r--r-- | server/lib/video-comment.ts | 74 | ||||
-rw-r--r-- | server/middlewares/sort.ts | 9 | ||||
-rw-r--r-- | server/middlewares/validators/sort.ts | 5 | ||||
-rw-r--r-- | server/middlewares/validators/video-comments.ts | 131 | ||||
-rw-r--r-- | server/models/video/video-comment.ts | 88 | ||||
-rw-r--r-- | shared/models/videos/video-comment.model.ts | 19 |
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' | 1 | import * as express from 'express' |
2 | // import { logger, getFormattedObjects } from '../../../helpers' | 2 | import { VideoCommentCreate } from '../../../../shared/models/videos/video-comment.model' |
3 | // import { | 3 | import { getFormattedObjects, retryTransactionWrapper } from '../../../helpers' |
4 | // authenticate, | 4 | import { sequelizeTypescript } from '../../../initializers' |
5 | // ensureUserHasRight, | 5 | import { buildFormattedCommentTree, createVideoComment } from '../../../lib/video-comment' |
6 | // videosBlacklistAddValidator, | 6 | import { asyncMiddleware, authenticate, paginationValidator, setPagination, setVideoCommentThreadsSort } from '../../../middlewares' |
7 | // videosBlacklistRemoveValidator, | 7 | import { videoCommentThreadsSortValidator } from '../../../middlewares/validators' |
8 | // paginationValidator, | 8 | import { |
9 | // blacklistSortValidator, | 9 | addVideoCommentReplyValidator, addVideoCommentThreadValidator, listVideoCommentThreadsValidator, |
10 | // setBlacklistSort, | 10 | listVideoThreadCommentsValidator |
11 | // setPagination, | 11 | } from '../../../middlewares/validators/video-comments' |
12 | // asyncMiddleware | 12 | import { VideoCommentModel } from '../../../models/video/video-comment' |
13 | // } from '../../../middlewares' | 13 | |
14 | // import { BlacklistedVideo, UserRight } from '../../../../shared' | 14 | const videoCommentRouter = express.Router() |
15 | // import { VideoBlacklistModel } from '../../../models/video/video-blacklist' | 15 | |
16 | // | 16 | videoCommentRouter.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 | // ) | 24 | videoCommentRouter.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), | 29 | videoCommentRouter.post('/:videoId/comment-threads', |
30 | // asyncMiddleware(addVideoToBlacklist) | 30 | authenticate, |
31 | // ) | 31 | asyncMiddleware(addVideoCommentThreadValidator), |
32 | // | 32 | asyncMiddleware(addVideoCommentThreadRetryWrapper) |
33 | // videoCommentRouter.get('/blacklist', | 33 | ) |
34 | // authenticate, | 34 | videoCommentRouter.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 | // | 42 | export { |
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 | // ) | 48 | async 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 | // } | 54 | async 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 | // | 60 | async 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) { | 75 | function 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 | // } | 88 | async 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 | |||
103 | function 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' | |||
47 | import { abuseVideoRouter } from './abuse' | 47 | import { abuseVideoRouter } from './abuse' |
48 | import { blacklistRouter } from './blacklist' | 48 | import { blacklistRouter } from './blacklist' |
49 | import { videoChannelRouter } from './channel' | 49 | import { videoChannelRouter } from './channel' |
50 | import { videoCommentRouter } from './comment' | ||
50 | import { rateVideoRouter } from './rate' | 51 | import { rateVideoRouter } from './rate' |
51 | 52 | ||
52 | const videosRouter = express.Router() | 53 | const videosRouter = express.Router() |
@@ -78,6 +79,7 @@ videosRouter.use('/', abuseVideoRouter) | |||
78 | videosRouter.use('/', blacklistRouter) | 79 | videosRouter.use('/', blacklistRouter) |
79 | videosRouter.use('/', rateVideoRouter) | 80 | videosRouter.use('/', rateVideoRouter) |
80 | videosRouter.use('/', videoChannelRouter) | 81 | videosRouter.use('/', videoChannelRouter) |
82 | videosRouter.use('/', videoCommentRouter) | ||
81 | 83 | ||
82 | videosRouter.get('/categories', listVideoCategories) | 84 | videosRouter.get('/categories', listVideoCategories) |
83 | videosRouter.get('/licences', listVideoLicences) | 85 | videosRouter.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 @@ | |||
1 | import 'express-validator' | ||
2 | import 'multer' | ||
3 | import * as validator from 'validator' | ||
4 | import { CONSTRAINTS_FIELDS } from '../../initializers' | ||
5 | |||
6 | const VIDEO_COMMENTS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_COMMENTS | ||
7 | |||
8 | function isValidVideoCommentText (value: string) { | ||
9 | return value === null || validator.isLength(value, VIDEO_COMMENTS_CONSTRAINTS_FIELDS.TEXT) | ||
10 | } | ||
11 | |||
12 | // --------------------------------------------------------------------------- | ||
13 | |||
14 | export { | ||
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' | |||
3 | import { ActorFollowModel } from '../../models/activitypub/actor-follow' | 3 | import { ActorFollowModel } from '../../models/activitypub/actor-follow' |
4 | import { VideoModel } from '../../models/video/video' | 4 | import { VideoModel } from '../../models/video/video' |
5 | import { VideoAbuseModel } from '../../models/video/video-abuse' | 5 | import { VideoAbuseModel } from '../../models/video/video-abuse' |
6 | import { VideoCommentModel } from '../../models/video/video-comment' | ||
6 | 7 | ||
7 | function getVideoActivityPubUrl (video: VideoModel) { | 8 | function 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 | ||
11 | function getVideoChannelActivityPubUrl (videoChannelUUID: string) { | 12 | function 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 | ||
15 | function getApplicationActivityPubUrl () { | 16 | function getVideoChannelActivityPubUrl (videoChannelUUID: string) { |
16 | return CONFIG.WEBSERVER.URL + '/application/peertube' | 17 | return CONFIG.WEBSERVER.URL + '/video-channels/' + videoChannelUUID |
17 | } | 18 | } |
18 | 19 | ||
19 | function getAccountActivityPubUrl (accountName: string) { | 20 | function getAccountActivityPubUrl (accountName: string) { |
@@ -63,7 +64,6 @@ function getUndoActivityPubUrl (originalUrl: string) { | |||
63 | } | 64 | } |
64 | 65 | ||
65 | export { | 66 | export { |
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 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | import { ResultList } from '../../shared/models' | ||
3 | import { VideoCommentThread } from '../../shared/models/videos/video-comment.model' | ||
4 | import { VideoModel } from '../models/video/video' | ||
5 | import { VideoCommentModel } from '../models/video/video-comment' | ||
6 | import { getVideoCommentActivityPubUrl } from './activitypub' | ||
7 | |||
8 | async 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 | |||
35 | function 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 | |||
71 | export { | ||
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 | ||
35 | function 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 | |||
35 | function setFollowersSort (req: express.Request, res: express.Response, next: express.NextFunction) { | 41 | function 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) | |||
9 | const SORTABLE_JOBS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.JOBS) | 9 | const SORTABLE_JOBS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.JOBS) |
10 | const SORTABLE_VIDEO_ABUSES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_ABUSES) | 10 | const SORTABLE_VIDEO_ABUSES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_ABUSES) |
11 | const SORTABLE_VIDEOS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS) | 11 | const SORTABLE_VIDEOS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS) |
12 | const SORTABLE_VIDEO_COMMENT_THREADS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_COMMENT_THREADS) | ||
12 | const SORTABLE_BLACKLISTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.BLACKLISTS) | 13 | const SORTABLE_BLACKLISTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.BLACKLISTS) |
13 | const SORTABLE_VIDEO_CHANNELS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_CHANNELS) | 14 | const SORTABLE_VIDEO_CHANNELS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_CHANNELS) |
14 | const SORTABLE_FOLLOWERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWERS) | 15 | const SORTABLE_FOLLOWERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWERS) |
@@ -18,6 +19,7 @@ const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS) | |||
18 | const jobsSortValidator = checkSort(SORTABLE_JOBS_COLUMNS) | 19 | const jobsSortValidator = checkSort(SORTABLE_JOBS_COLUMNS) |
19 | const videoAbusesSortValidator = checkSort(SORTABLE_VIDEO_ABUSES_COLUMNS) | 20 | const videoAbusesSortValidator = checkSort(SORTABLE_VIDEO_ABUSES_COLUMNS) |
20 | const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS) | 21 | const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS) |
22 | const videoCommentThreadsSortValidator = checkSort(SORTABLE_VIDEO_COMMENT_THREADS_COLUMNS) | ||
21 | const blacklistSortValidator = checkSort(SORTABLE_BLACKLISTS_COLUMNS) | 23 | const blacklistSortValidator = checkSort(SORTABLE_BLACKLISTS_COLUMNS) |
22 | const videoChannelsSortValidator = checkSort(SORTABLE_VIDEO_CHANNELS_COLUMNS) | 24 | const videoChannelsSortValidator = checkSort(SORTABLE_VIDEO_CHANNELS_COLUMNS) |
23 | const followersSortValidator = checkSort(SORTABLE_FOLLOWERS_COLUMNS) | 25 | const 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 @@ | |||
1 | import * as express from 'express' | ||
2 | import { body, param } from 'express-validator/check' | ||
3 | import { logger } from '../../helpers' | ||
4 | import { isIdOrUUIDValid, isIdValid } from '../../helpers/custom-validators/misc' | ||
5 | import { isValidVideoCommentText } from '../../helpers/custom-validators/video-comments' | ||
6 | import { isVideoExist } from '../../helpers/custom-validators/videos' | ||
7 | import { VideoCommentModel } from '../../models/video/video-comment' | ||
8 | import { areValidationErrors } from './utils' | ||
9 | |||
10 | const 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 | |||
23 | const 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 | |||
38 | const 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 | |||
52 | const 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 | |||
70 | export { | ||
71 | listVideoCommentThreadsValidator, | ||
72 | listVideoThreadCommentsValidator, | ||
73 | addVideoCommentThreadValidator, | ||
74 | addVideoCommentReplyValidator | ||
75 | } | ||
76 | |||
77 | // --------------------------------------------------------------------------- | ||
78 | |||
79 | async 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 | |||
110 | async 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 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import * as Sequelize from 'sequelize' |
2 | import { | 2 | import { |
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' |
6 | import { VideoComment } from '../../../shared/models/videos/video-comment.model' | ||
6 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub' | 7 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub' |
7 | import { CONSTRAINTS_FIELDS } from '../../initializers' | 8 | import { CONSTRAINTS_FIELDS } from '../../initializers' |
8 | import { ActorModel } from '../activitypub/actor' | 9 | import { ActorModel } from '../activitypub/actor' |
9 | import { throwIfNotValid } from '../utils' | 10 | import { getSort, throwIfNotValid } from '../utils' |
10 | import { VideoModel } from './video' | 11 | import { VideoModel } from './video' |
11 | 12 | ||
13 | enum 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 @@ | |||
1 | export 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 | |||
12 | export interface VideoCommentThread { | ||
13 | comment: VideoComment | ||
14 | children: VideoCommentThread[] | ||
15 | } | ||
16 | |||
17 | export interface VideoCommentCreate { | ||
18 | text: string | ||
19 | } | ||