diff options
author | Chocobozzz <me@florianbigard.com> | 2018-01-04 11:19:16 +0100 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2018-01-04 11:19:16 +0100 |
commit | 4cb6d4578893db310297d7e118ce2fb7ecb952a3 (patch) | |
tree | a89a2e2062ba7bb91e922f07a7950ee51e090ccf /server | |
parent | cf117aaafc1e9ae1ab4c388fc5d2e5ba9349efee (diff) | |
download | PeerTube-4cb6d4578893db310297d7e118ce2fb7ecb952a3.tar.gz PeerTube-4cb6d4578893db310297d7e118ce2fb7ecb952a3.tar.zst PeerTube-4cb6d4578893db310297d7e118ce2fb7ecb952a3.zip |
Add ability to delete comments
Diffstat (limited to 'server')
-rw-r--r-- | server/controllers/api/videos/comment.ts | 31 | ||||
-rw-r--r-- | server/helpers/custom-validators/activitypub/activity.ts | 5 | ||||
-rw-r--r-- | server/helpers/custom-validators/activitypub/video-comments.ts | 7 | ||||
-rw-r--r-- | server/lib/activitypub/process/process-delete.ts | 33 | ||||
-rw-r--r-- | server/lib/activitypub/send/send-delete.ts | 16 | ||||
-rw-r--r-- | server/middlewares/validators/video-comments.ts | 35 | ||||
-rw-r--r-- | server/middlewares/validators/videos.ts | 2 | ||||
-rw-r--r-- | server/models/activitypub/actor.ts | 1 | ||||
-rw-r--r-- | server/models/video/video-comment.ts | 45 | ||||
-rw-r--r-- | server/models/video/video.ts | 46 | ||||
-rw-r--r-- | server/tests/api/check-params/video-comments.ts | 39 | ||||
-rw-r--r-- | server/tests/api/videos/multiple-servers.ts | 33 | ||||
-rw-r--r-- | server/tests/api/videos/video-comments.ts | 38 | ||||
-rw-r--r-- | server/tests/utils/videos/video-comments.ts | 21 |
14 files changed, 325 insertions, 27 deletions
diff --git a/server/controllers/api/videos/comment.ts b/server/controllers/api/videos/comment.ts index e09b242ed..65fcf6b35 100644 --- a/server/controllers/api/videos/comment.ts +++ b/server/controllers/api/videos/comment.ts | |||
@@ -2,14 +2,15 @@ import * as express from 'express' | |||
2 | import { ResultList } from '../../../../shared/models' | 2 | import { ResultList } from '../../../../shared/models' |
3 | import { VideoCommentCreate } from '../../../../shared/models/videos/video-comment.model' | 3 | import { VideoCommentCreate } from '../../../../shared/models/videos/video-comment.model' |
4 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | 4 | import { retryTransactionWrapper } from '../../../helpers/database-utils' |
5 | import { logger } from '../../../helpers/logger' | ||
5 | import { getFormattedObjects } from '../../../helpers/utils' | 6 | import { getFormattedObjects } from '../../../helpers/utils' |
6 | import { sequelizeTypescript } from '../../../initializers' | 7 | import { sequelizeTypescript } from '../../../initializers' |
7 | import { buildFormattedCommentTree, createVideoComment } from '../../../lib/video-comment' | 8 | import { buildFormattedCommentTree, createVideoComment } from '../../../lib/video-comment' |
8 | import { asyncMiddleware, authenticate, paginationValidator, setPagination, setVideoCommentThreadsSort } from '../../../middlewares' | 9 | import { asyncMiddleware, authenticate, paginationValidator, setPagination, setVideoCommentThreadsSort } from '../../../middlewares' |
9 | import { videoCommentThreadsSortValidator } from '../../../middlewares/validators' | 10 | import { videoCommentThreadsSortValidator } from '../../../middlewares/validators' |
10 | import { | 11 | import { |
11 | addVideoCommentReplyValidator, addVideoCommentThreadValidator, listVideoCommentThreadsValidator, | 12 | addVideoCommentReplyValidator, addVideoCommentThreadValidator, listVideoCommentThreadsValidator, listVideoThreadCommentsValidator, |
12 | listVideoThreadCommentsValidator | 13 | removeVideoCommentValidator |
13 | } from '../../../middlewares/validators/video-comments' | 14 | } from '../../../middlewares/validators/video-comments' |
14 | import { VideoModel } from '../../../models/video/video' | 15 | import { VideoModel } from '../../../models/video/video' |
15 | import { VideoCommentModel } from '../../../models/video/video-comment' | 16 | import { VideoCommentModel } from '../../../models/video/video-comment' |
@@ -39,6 +40,11 @@ videoCommentRouter.post('/:videoId/comments/:commentId', | |||
39 | asyncMiddleware(addVideoCommentReplyValidator), | 40 | asyncMiddleware(addVideoCommentReplyValidator), |
40 | asyncMiddleware(addVideoCommentReplyRetryWrapper) | 41 | asyncMiddleware(addVideoCommentReplyRetryWrapper) |
41 | ) | 42 | ) |
43 | videoCommentRouter.delete('/:videoId/comments/:commentId', | ||
44 | authenticate, | ||
45 | asyncMiddleware(removeVideoCommentValidator), | ||
46 | asyncMiddleware(removeVideoCommentRetryWrapper) | ||
47 | ) | ||
42 | 48 | ||
43 | // --------------------------------------------------------------------------- | 49 | // --------------------------------------------------------------------------- |
44 | 50 | ||
@@ -131,3 +137,24 @@ function addVideoCommentReply (req: express.Request, res: express.Response, next | |||
131 | }, t) | 137 | }, t) |
132 | }) | 138 | }) |
133 | } | 139 | } |
140 | |||
141 | async function removeVideoCommentRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
142 | const options = { | ||
143 | arguments: [ req, res ], | ||
144 | errorMessage: 'Cannot remove the video comment with many retries.' | ||
145 | } | ||
146 | |||
147 | await retryTransactionWrapper(removeVideoComment, options) | ||
148 | |||
149 | return res.type('json').status(204).end() | ||
150 | } | ||
151 | |||
152 | async function removeVideoComment (req: express.Request, res: express.Response) { | ||
153 | const videoCommentInstance: VideoCommentModel = res.locals.videoComment | ||
154 | |||
155 | await sequelizeTypescript.transaction(async t => { | ||
156 | await videoCommentInstance.destroy({ transaction: t }) | ||
157 | }) | ||
158 | |||
159 | logger.info('Video comment %d deleted.', videoCommentInstance.id) | ||
160 | } | ||
diff --git a/server/helpers/custom-validators/activitypub/activity.ts b/server/helpers/custom-validators/activitypub/activity.ts index 856c87f2c..577cf4b52 100644 --- a/server/helpers/custom-validators/activitypub/activity.ts +++ b/server/helpers/custom-validators/activitypub/activity.ts | |||
@@ -5,7 +5,7 @@ import { isAnnounceActivityValid } from './announce' | |||
5 | import { isActivityPubUrlValid } from './misc' | 5 | import { isActivityPubUrlValid } from './misc' |
6 | import { isDislikeActivityValid, isLikeActivityValid } from './rate' | 6 | import { isDislikeActivityValid, isLikeActivityValid } from './rate' |
7 | import { isUndoActivityValid } from './undo' | 7 | import { isUndoActivityValid } from './undo' |
8 | import { isVideoCommentCreateActivityValid } from './video-comments' | 8 | import { isVideoCommentCreateActivityValid, isVideoCommentDeleteActivityValid } from './video-comments' |
9 | import { | 9 | import { |
10 | isVideoFlagValid, | 10 | isVideoFlagValid, |
11 | isVideoTorrentCreateActivityValid, | 11 | isVideoTorrentCreateActivityValid, |
@@ -70,7 +70,8 @@ function checkUpdateActivity (activity: any) { | |||
70 | 70 | ||
71 | function checkDeleteActivity (activity: any) { | 71 | function checkDeleteActivity (activity: any) { |
72 | return isVideoTorrentDeleteActivityValid(activity) || | 72 | return isVideoTorrentDeleteActivityValid(activity) || |
73 | isActorDeleteActivityValid(activity) | 73 | isActorDeleteActivityValid(activity) || |
74 | isVideoCommentDeleteActivityValid(activity) | ||
74 | } | 75 | } |
75 | 76 | ||
76 | function checkFollowActivity (activity: any) { | 77 | function checkFollowActivity (activity: any) { |
diff --git a/server/helpers/custom-validators/activitypub/video-comments.ts b/server/helpers/custom-validators/activitypub/video-comments.ts index 489ff27de..6928aced3 100644 --- a/server/helpers/custom-validators/activitypub/video-comments.ts +++ b/server/helpers/custom-validators/activitypub/video-comments.ts | |||
@@ -18,10 +18,15 @@ function isVideoCommentObjectValid (comment: any) { | |||
18 | isActivityPubUrlValid(comment.url) | 18 | isActivityPubUrlValid(comment.url) |
19 | } | 19 | } |
20 | 20 | ||
21 | function isVideoCommentDeleteActivityValid (activity: any) { | ||
22 | return isBaseActivityValid(activity, 'Delete') | ||
23 | } | ||
24 | |||
21 | // --------------------------------------------------------------------------- | 25 | // --------------------------------------------------------------------------- |
22 | 26 | ||
23 | export { | 27 | export { |
24 | isVideoCommentCreateActivityValid | 28 | isVideoCommentCreateActivityValid, |
29 | isVideoCommentDeleteActivityValid | ||
25 | } | 30 | } |
26 | 31 | ||
27 | // --------------------------------------------------------------------------- | 32 | // --------------------------------------------------------------------------- |
diff --git a/server/lib/activitypub/process/process-delete.ts b/server/lib/activitypub/process/process-delete.ts index 523a31822..604570e74 100644 --- a/server/lib/activitypub/process/process-delete.ts +++ b/server/lib/activitypub/process/process-delete.ts | |||
@@ -6,6 +6,7 @@ import { AccountModel } from '../../../models/account/account' | |||
6 | import { ActorModel } from '../../../models/activitypub/actor' | 6 | import { ActorModel } from '../../../models/activitypub/actor' |
7 | import { VideoModel } from '../../../models/video/video' | 7 | import { VideoModel } from '../../../models/video/video' |
8 | import { VideoChannelModel } from '../../../models/video/video-channel' | 8 | import { VideoChannelModel } from '../../../models/video/video-channel' |
9 | import { VideoCommentModel } from '../../../models/video/video-comment' | ||
9 | import { getOrCreateActorAndServerAndModel } from '../actor' | 10 | import { getOrCreateActorAndServerAndModel } from '../actor' |
10 | 11 | ||
11 | async function processDeleteActivity (activity: ActivityDelete) { | 12 | async function processDeleteActivity (activity: ActivityDelete) { |
@@ -24,9 +25,16 @@ async function processDeleteActivity (activity: ActivityDelete) { | |||
24 | } | 25 | } |
25 | 26 | ||
26 | { | 27 | { |
27 | let videoObject = await VideoModel.loadByUrlAndPopulateAccount(activity.id) | 28 | const videoCommentInstance = await VideoCommentModel.loadByUrlAndPopulateAccount(activity.id) |
28 | if (videoObject !== undefined) { | 29 | if (videoCommentInstance) { |
29 | return processDeleteVideo(actor, videoObject) | 30 | return processDeleteVideoComment(actor, videoCommentInstance) |
31 | } | ||
32 | } | ||
33 | |||
34 | { | ||
35 | const videoInstance = await VideoModel.loadByUrlAndPopulateAccount(activity.id) | ||
36 | if (videoInstance) { | ||
37 | return processDeleteVideo(actor, videoInstance) | ||
30 | } | 38 | } |
31 | } | 39 | } |
32 | 40 | ||
@@ -101,3 +109,22 @@ async function deleteRemoteVideoChannel (videoChannelToRemove: VideoChannelModel | |||
101 | 109 | ||
102 | logger.info('Remote video channel with uuid %s removed.', videoChannelToRemove.Actor.uuid) | 110 | logger.info('Remote video channel with uuid %s removed.', videoChannelToRemove.Actor.uuid) |
103 | } | 111 | } |
112 | |||
113 | async function processDeleteVideoComment (actor: ActorModel, videoComment: VideoCommentModel) { | ||
114 | const options = { | ||
115 | arguments: [ actor, videoComment ], | ||
116 | errorMessage: 'Cannot remove the remote video comment with many retries.' | ||
117 | } | ||
118 | |||
119 | await retryTransactionWrapper(deleteRemoteVideoComment, options) | ||
120 | } | ||
121 | |||
122 | function deleteRemoteVideoComment (actor: ActorModel, videoComment: VideoCommentModel) { | ||
123 | logger.debug('Removing remote video comment "%s".', videoComment.url) | ||
124 | |||
125 | return sequelizeTypescript.transaction(async t => { | ||
126 | await videoComment.destroy({ transaction: t }) | ||
127 | |||
128 | logger.info('Remote video comment %s removed.', videoComment.url) | ||
129 | }) | ||
130 | } | ||
diff --git a/server/lib/activitypub/send/send-delete.ts b/server/lib/activitypub/send/send-delete.ts index 4bc5db77e..1ca031898 100644 --- a/server/lib/activitypub/send/send-delete.ts +++ b/server/lib/activitypub/send/send-delete.ts | |||
@@ -2,6 +2,7 @@ import { Transaction } from 'sequelize' | |||
2 | import { ActivityDelete } from '../../../../shared/models/activitypub' | 2 | import { ActivityDelete } from '../../../../shared/models/activitypub' |
3 | import { ActorModel } from '../../../models/activitypub/actor' | 3 | import { ActorModel } from '../../../models/activitypub/actor' |
4 | import { VideoModel } from '../../../models/video/video' | 4 | import { VideoModel } from '../../../models/video/video' |
5 | import { VideoCommentModel } from '../../../models/video/video-comment' | ||
5 | import { VideoShareModel } from '../../../models/video/video-share' | 6 | import { VideoShareModel } from '../../../models/video/video-share' |
6 | import { broadcastToFollowers } from './misc' | 7 | import { broadcastToFollowers } from './misc' |
7 | 8 | ||
@@ -22,11 +23,24 @@ async function sendDeleteActor (byActor: ActorModel, t: Transaction) { | |||
22 | return broadcastToFollowers(data, byActor, [ byActor ], t) | 23 | return broadcastToFollowers(data, byActor, [ byActor ], t) |
23 | } | 24 | } |
24 | 25 | ||
26 | async function sendDeleteVideoComment (videoComment: VideoCommentModel, t: Transaction) { | ||
27 | const byActor = videoComment.Account.Actor | ||
28 | |||
29 | const data = deleteActivityData(videoComment.url, byActor) | ||
30 | |||
31 | const actorsInvolved = await VideoShareModel.loadActorsByShare(videoComment.Video.id, t) | ||
32 | actorsInvolved.push(videoComment.Video.VideoChannel.Account.Actor) | ||
33 | actorsInvolved.push(byActor) | ||
34 | |||
35 | return broadcastToFollowers(data, byActor, actorsInvolved, t) | ||
36 | } | ||
37 | |||
25 | // --------------------------------------------------------------------------- | 38 | // --------------------------------------------------------------------------- |
26 | 39 | ||
27 | export { | 40 | export { |
28 | sendDeleteVideo, | 41 | sendDeleteVideo, |
29 | sendDeleteActor | 42 | sendDeleteActor, |
43 | sendDeleteVideoComment | ||
30 | } | 44 | } |
31 | 45 | ||
32 | // --------------------------------------------------------------------------- | 46 | // --------------------------------------------------------------------------- |
diff --git a/server/middlewares/validators/video-comments.ts b/server/middlewares/validators/video-comments.ts index ade0b7b9f..63804da30 100644 --- a/server/middlewares/validators/video-comments.ts +++ b/server/middlewares/validators/video-comments.ts | |||
@@ -1,9 +1,11 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { body, param } from 'express-validator/check' | 2 | import { body, param } from 'express-validator/check' |
3 | import { UserRight } from '../../../shared' | ||
3 | import { isIdOrUUIDValid, isIdValid } from '../../helpers/custom-validators/misc' | 4 | import { isIdOrUUIDValid, isIdValid } from '../../helpers/custom-validators/misc' |
4 | import { isValidVideoCommentText } from '../../helpers/custom-validators/video-comments' | 5 | import { isValidVideoCommentText } from '../../helpers/custom-validators/video-comments' |
5 | import { isVideoExist } from '../../helpers/custom-validators/videos' | 6 | import { isVideoExist } from '../../helpers/custom-validators/videos' |
6 | import { logger } from '../../helpers/logger' | 7 | import { logger } from '../../helpers/logger' |
8 | import { UserModel } from '../../models/account/user' | ||
7 | import { VideoModel } from '../../models/video/video' | 9 | import { VideoModel } from '../../models/video/video' |
8 | import { VideoCommentModel } from '../../models/video/video-comment' | 10 | import { VideoCommentModel } from '../../models/video/video-comment' |
9 | import { areValidationErrors } from './utils' | 11 | import { areValidationErrors } from './utils' |
@@ -83,6 +85,24 @@ const videoCommentGetValidator = [ | |||
83 | } | 85 | } |
84 | ] | 86 | ] |
85 | 87 | ||
88 | const removeVideoCommentValidator = [ | ||
89 | param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), | ||
90 | param('commentId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid commentId'), | ||
91 | |||
92 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
93 | logger.debug('Checking removeVideoCommentValidator parameters.', { parameters: req.params }) | ||
94 | |||
95 | if (areValidationErrors(req, res)) return | ||
96 | if (!await isVideoExist(req.params.videoId, res)) return | ||
97 | if (!await isVideoCommentExist(req.params.commentId, res.locals.video, res)) return | ||
98 | |||
99 | // Check if the user who did the request is able to delete the video | ||
100 | if (!checkUserCanDeleteVideoComment(res.locals.oauth.token.User, res.locals.videoComment, res)) return | ||
101 | |||
102 | return next() | ||
103 | } | ||
104 | ] | ||
105 | |||
86 | // --------------------------------------------------------------------------- | 106 | // --------------------------------------------------------------------------- |
87 | 107 | ||
88 | export { | 108 | export { |
@@ -90,7 +110,8 @@ export { | |||
90 | listVideoThreadCommentsValidator, | 110 | listVideoThreadCommentsValidator, |
91 | addVideoCommentThreadValidator, | 111 | addVideoCommentThreadValidator, |
92 | addVideoCommentReplyValidator, | 112 | addVideoCommentReplyValidator, |
93 | videoCommentGetValidator | 113 | videoCommentGetValidator, |
114 | removeVideoCommentValidator | ||
94 | } | 115 | } |
95 | 116 | ||
96 | // --------------------------------------------------------------------------- | 117 | // --------------------------------------------------------------------------- |
@@ -160,3 +181,15 @@ function isVideoCommentsEnabled (video: VideoModel, res: express.Response) { | |||
160 | 181 | ||
161 | return true | 182 | return true |
162 | } | 183 | } |
184 | |||
185 | function checkUserCanDeleteVideoComment (user: UserModel, videoComment: VideoCommentModel, res: express.Response) { | ||
186 | const account = videoComment.Account | ||
187 | if (user.hasRight(UserRight.REMOVE_ANY_VIDEO_COMMENT) === false && account.userId !== user.id) { | ||
188 | res.status(403) | ||
189 | .json({ error: 'Cannot remove video comment of another user' }) | ||
190 | .end() | ||
191 | return false | ||
192 | } | ||
193 | |||
194 | return true | ||
195 | } | ||
diff --git a/server/middlewares/validators/videos.ts b/server/middlewares/validators/videos.ts index e8cb2ae03..1acb306c0 100644 --- a/server/middlewares/validators/videos.ts +++ b/server/middlewares/validators/videos.ts | |||
@@ -253,7 +253,7 @@ function checkUserCanDeleteVideo (user: UserModel, video: VideoModel, res: expre | |||
253 | } | 253 | } |
254 | 254 | ||
255 | // Check if the user can delete the video | 255 | // Check if the user can delete the video |
256 | // The user can delete it if s/he is an admin | 256 | // The user can delete it if he has the right |
257 | // Or if s/he is the video's account | 257 | // Or if s/he is the video's account |
258 | const account = video.VideoChannel.Account | 258 | const account = video.VideoChannel.Account |
259 | if (user.hasRight(UserRight.REMOVE_ANY_VIDEO) === false && account.userId !== user.id) { | 259 | if (user.hasRight(UserRight.REMOVE_ANY_VIDEO) === false && account.userId !== user.id) { |
diff --git a/server/models/activitypub/actor.ts b/server/models/activitypub/actor.ts index 2ef7c77a2..ed7fcfe27 100644 --- a/server/models/activitypub/actor.ts +++ b/server/models/activitypub/actor.ts | |||
@@ -271,6 +271,7 @@ export class ActorModel extends Model<ActorModel> { | |||
271 | 271 | ||
272 | return { | 272 | return { |
273 | id: this.id, | 273 | id: this.id, |
274 | url: this.url, | ||
274 | uuid: this.uuid, | 275 | uuid: this.uuid, |
275 | host: this.getHost(), | 276 | host: this.getHost(), |
276 | score, | 277 | score, |
diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts index d2d8945c3..66fca2484 100644 --- a/server/models/video/video-comment.ts +++ b/server/models/video/video-comment.ts | |||
@@ -7,12 +7,14 @@ import { VideoCommentObject } from '../../../shared/models/activitypub/objects/v | |||
7 | import { VideoComment } from '../../../shared/models/videos/video-comment.model' | 7 | import { VideoComment } from '../../../shared/models/videos/video-comment.model' |
8 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 8 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
9 | import { CONSTRAINTS_FIELDS } from '../../initializers' | 9 | import { CONSTRAINTS_FIELDS } from '../../initializers' |
10 | import { sendDeleteVideoComment } from '../../lib/activitypub/send' | ||
10 | import { AccountModel } from '../account/account' | 11 | import { AccountModel } from '../account/account' |
11 | import { ActorModel } from '../activitypub/actor' | 12 | import { ActorModel } from '../activitypub/actor' |
12 | import { AvatarModel } from '../avatar/avatar' | 13 | import { AvatarModel } from '../avatar/avatar' |
13 | import { ServerModel } from '../server/server' | 14 | import { ServerModel } from '../server/server' |
14 | import { getSort, throwIfNotValid } from '../utils' | 15 | import { getSort, throwIfNotValid } from '../utils' |
15 | import { VideoModel } from './video' | 16 | import { VideoModel } from './video' |
17 | import { VideoChannelModel } from './video-channel' | ||
16 | 18 | ||
17 | enum ScopeNames { | 19 | enum ScopeNames { |
18 | WITH_ACCOUNT = 'WITH_ACCOUNT', | 20 | WITH_ACCOUNT = 'WITH_ACCOUNT', |
@@ -70,7 +72,25 @@ enum ScopeNames { | |||
70 | include: [ | 72 | include: [ |
71 | { | 73 | { |
72 | model: () => VideoModel, | 74 | model: () => VideoModel, |
73 | required: false | 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 | ] | ||
74 | } | 94 | } |
75 | ] | 95 | ] |
76 | } | 96 | } |
@@ -155,9 +175,10 @@ export class VideoCommentModel extends Model<VideoCommentModel> { | |||
155 | Account: AccountModel | 175 | Account: AccountModel |
156 | 176 | ||
157 | @AfterDestroy | 177 | @AfterDestroy |
158 | static sendDeleteIfOwned (instance: VideoCommentModel) { | 178 | static async sendDeleteIfOwned (instance: VideoCommentModel) { |
159 | // TODO | 179 | if (instance.isOwned()) { |
160 | return undefined | 180 | await sendDeleteVideoComment(instance, undefined) |
181 | } | ||
161 | } | 182 | } |
162 | 183 | ||
163 | static loadById (id: number, t?: Sequelize.Transaction) { | 184 | static loadById (id: number, t?: Sequelize.Transaction) { |
@@ -198,6 +219,18 @@ export class VideoCommentModel extends Model<VideoCommentModel> { | |||
198 | return VideoCommentModel.findOne(query) | 219 | return VideoCommentModel.findOne(query) |
199 | } | 220 | } |
200 | 221 | ||
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 | |||
201 | static listThreadsForApi (videoId: number, start: number, count: number, sort: string) { | 234 | static listThreadsForApi (videoId: number, start: number, count: number, sort: string) { |
202 | const query = { | 235 | const query = { |
203 | offset: start, | 236 | offset: start, |
@@ -237,6 +270,10 @@ export class VideoCommentModel extends Model<VideoCommentModel> { | |||
237 | }) | 270 | }) |
238 | } | 271 | } |
239 | 272 | ||
273 | isOwned () { | ||
274 | return this.Account.isOwned() | ||
275 | } | ||
276 | |||
240 | toFormattedJSON () { | 277 | toFormattedJSON () { |
241 | return { | 278 | return { |
242 | id: this.id, | 279 | id: this.id, |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index c4b716cd2..4d15c2a50 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -43,7 +43,8 @@ import { VideoTagModel } from './video-tag' | |||
43 | 43 | ||
44 | enum ScopeNames { | 44 | enum ScopeNames { |
45 | AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST', | 45 | AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST', |
46 | WITH_ACCOUNT = 'WITH_ACCOUNT', | 46 | WITH_ACCOUNT_API = 'WITH_ACCOUNT_API', |
47 | WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS', | ||
47 | WITH_TAGS = 'WITH_TAGS', | 48 | WITH_TAGS = 'WITH_TAGS', |
48 | WITH_FILES = 'WITH_FILES', | 49 | WITH_FILES = 'WITH_FILES', |
49 | WITH_SHARES = 'WITH_SHARES', | 50 | WITH_SHARES = 'WITH_SHARES', |
@@ -62,7 +63,35 @@ enum ScopeNames { | |||
62 | privacy: VideoPrivacy.PUBLIC | 63 | privacy: VideoPrivacy.PUBLIC |
63 | } | 64 | } |
64 | }, | 65 | }, |
65 | [ScopeNames.WITH_ACCOUNT]: { | 66 | [ScopeNames.WITH_ACCOUNT_API]: { |
67 | include: [ | ||
68 | { | ||
69 | model: () => VideoChannelModel.unscoped(), | ||
70 | required: true, | ||
71 | include: [ | ||
72 | { | ||
73 | attributes: [ 'name' ], | ||
74 | model: () => AccountModel.unscoped(), | ||
75 | required: true, | ||
76 | include: [ | ||
77 | { | ||
78 | attributes: [ 'serverId' ], | ||
79 | model: () => ActorModel.unscoped(), | ||
80 | required: true, | ||
81 | include: [ | ||
82 | { | ||
83 | model: () => ServerModel.unscoped(), | ||
84 | required: false | ||
85 | } | ||
86 | ] | ||
87 | } | ||
88 | ] | ||
89 | } | ||
90 | ] | ||
91 | } | ||
92 | ] | ||
93 | }, | ||
94 | [ScopeNames.WITH_ACCOUNT_DETAILS]: { | ||
66 | include: [ | 95 | include: [ |
67 | { | 96 | { |
68 | model: () => VideoChannelModel, | 97 | model: () => VideoChannelModel, |
@@ -146,6 +175,9 @@ enum ScopeNames { | |||
146 | }, | 175 | }, |
147 | { | 176 | { |
148 | fields: [ 'channelId' ] | 177 | fields: [ 'channelId' ] |
178 | }, | ||
179 | { | ||
180 | fields: [ 'id', 'privacy' ] | ||
149 | } | 181 | } |
150 | ] | 182 | ] |
151 | }) | 183 | }) |
@@ -461,7 +493,7 @@ export class VideoModel extends Model<VideoModel> { | |||
461 | order: [ getSort(sort) ] | 493 | order: [ getSort(sort) ] |
462 | } | 494 | } |
463 | 495 | ||
464 | return VideoModel.scope([ ScopeNames.AVAILABLE_FOR_LIST, ScopeNames.WITH_ACCOUNT ]) | 496 | return VideoModel.scope([ ScopeNames.AVAILABLE_FOR_LIST, ScopeNames.WITH_ACCOUNT_API ]) |
465 | .findAndCountAll(query) | 497 | .findAndCountAll(query) |
466 | .then(({ rows, count }) => { | 498 | .then(({ rows, count }) => { |
467 | return { | 499 | return { |
@@ -496,7 +528,7 @@ export class VideoModel extends Model<VideoModel> { | |||
496 | 528 | ||
497 | if (t !== undefined) query.transaction = t | 529 | if (t !== undefined) query.transaction = t |
498 | 530 | ||
499 | return VideoModel.scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_FILES ]).findOne(query) | 531 | return VideoModel.scope([ ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_FILES ]).findOne(query) |
500 | } | 532 | } |
501 | 533 | ||
502 | static loadByUUIDOrURL (uuid: string, url: string, t?: Sequelize.Transaction) { | 534 | static loadByUUIDOrURL (uuid: string, url: string, t?: Sequelize.Transaction) { |
@@ -520,7 +552,7 @@ export class VideoModel extends Model<VideoModel> { | |||
520 | } | 552 | } |
521 | 553 | ||
522 | return VideoModel | 554 | return VideoModel |
523 | .scope([ ScopeNames.WITH_TAGS, ScopeNames.WITH_FILES, ScopeNames.WITH_ACCOUNT ]) | 555 | .scope([ ScopeNames.WITH_TAGS, ScopeNames.WITH_FILES, ScopeNames.WITH_ACCOUNT_DETAILS ]) |
524 | .findById(id, options) | 556 | .findById(id, options) |
525 | } | 557 | } |
526 | 558 | ||
@@ -545,7 +577,7 @@ export class VideoModel extends Model<VideoModel> { | |||
545 | } | 577 | } |
546 | 578 | ||
547 | return VideoModel | 579 | return VideoModel |
548 | .scope([ ScopeNames.WITH_TAGS, ScopeNames.WITH_FILES, ScopeNames.WITH_ACCOUNT ]) | 580 | .scope([ ScopeNames.WITH_TAGS, ScopeNames.WITH_FILES, ScopeNames.WITH_ACCOUNT_DETAILS ]) |
549 | .findOne(options) | 581 | .findOne(options) |
550 | } | 582 | } |
551 | 583 | ||
@@ -563,7 +595,7 @@ export class VideoModel extends Model<VideoModel> { | |||
563 | ScopeNames.WITH_SHARES, | 595 | ScopeNames.WITH_SHARES, |
564 | ScopeNames.WITH_TAGS, | 596 | ScopeNames.WITH_TAGS, |
565 | ScopeNames.WITH_FILES, | 597 | ScopeNames.WITH_FILES, |
566 | ScopeNames.WITH_ACCOUNT, | 598 | ScopeNames.WITH_ACCOUNT_DETAILS, |
567 | ScopeNames.WITH_COMMENTS | 599 | ScopeNames.WITH_COMMENTS |
568 | ]) | 600 | ]) |
569 | .findOne(options) | 601 | .findOne(options) |
diff --git a/server/tests/api/check-params/video-comments.ts b/server/tests/api/check-params/video-comments.ts index c11660d07..9190054da 100644 --- a/server/tests/api/check-params/video-comments.ts +++ b/server/tests/api/check-params/video-comments.ts | |||
@@ -3,8 +3,9 @@ | |||
3 | import * as chai from 'chai' | 3 | import * as chai from 'chai' |
4 | import 'mocha' | 4 | import 'mocha' |
5 | import { | 5 | import { |
6 | flushTests, killallServers, makeGetRequest, makePostBodyRequest, runServer, ServerInfo, setAccessTokensToServers, | 6 | createUser, |
7 | uploadVideo | 7 | flushTests, killallServers, makeDeleteRequest, makeGetRequest, makePostBodyRequest, runServer, ServerInfo, setAccessTokensToServers, |
8 | uploadVideo, userLogin | ||
8 | } from '../../utils' | 9 | } from '../../utils' |
9 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' | 10 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' |
10 | import { addVideoCommentThread } from '../../utils/videos/video-comments' | 11 | import { addVideoCommentThread } from '../../utils/videos/video-comments' |
@@ -16,6 +17,7 @@ describe('Test video comments API validator', function () { | |||
16 | let pathComment: string | 17 | let pathComment: string |
17 | let server: ServerInfo | 18 | let server: ServerInfo |
18 | let videoUUID: string | 19 | let videoUUID: string |
20 | let userAccessToken: string | ||
19 | let commentId: number | 21 | let commentId: number |
20 | 22 | ||
21 | // --------------------------------------------------------------- | 23 | // --------------------------------------------------------------- |
@@ -40,6 +42,15 @@ describe('Test video comments API validator', function () { | |||
40 | commentId = res.body.comment.id | 42 | commentId = res.body.comment.id |
41 | pathComment = '/api/v1/videos/' + videoUUID + '/comments/' + commentId | 43 | pathComment = '/api/v1/videos/' + videoUUID + '/comments/' + commentId |
42 | } | 44 | } |
45 | |||
46 | { | ||
47 | const user = { | ||
48 | username: 'user1', | ||
49 | password: 'my super password' | ||
50 | } | ||
51 | await createUser(server.url, server.accessToken, user.username, user.password) | ||
52 | userAccessToken = await userLogin(server, user) | ||
53 | } | ||
43 | }) | 54 | }) |
44 | 55 | ||
45 | describe('When listing video comment threads', function () { | 56 | describe('When listing video comment threads', function () { |
@@ -185,6 +196,30 @@ describe('Test video comments API validator', function () { | |||
185 | }) | 196 | }) |
186 | }) | 197 | }) |
187 | 198 | ||
199 | describe('When removing video comments', function () { | ||
200 | it('Should fail with a non authenticated user', async function () { | ||
201 | await makeDeleteRequest({ url: server.url, path: pathComment, token: 'none', statusCodeExpected: 401 }) | ||
202 | }) | ||
203 | |||
204 | it('Should fail with another user', async function () { | ||
205 | await makeDeleteRequest({ url: server.url, path: pathComment, token: userAccessToken, statusCodeExpected: 403 }) | ||
206 | }) | ||
207 | |||
208 | it('Should fail with an incorrect video', async function () { | ||
209 | const path = '/api/v1/videos/ba708d62-e3d7-45d9-9d73-41b9097cc02d/comments/' + commentId | ||
210 | await makeDeleteRequest({ url: server.url, path, token: server.accessToken, statusCodeExpected: 404 }) | ||
211 | }) | ||
212 | |||
213 | it('Should fail with an incorrect comment', async function () { | ||
214 | const path = '/api/v1/videos/' + videoUUID + '/comments/124' | ||
215 | await makeDeleteRequest({ url: server.url, path, token: server.accessToken, statusCodeExpected: 404 }) | ||
216 | }) | ||
217 | |||
218 | it('Should succeed with the correct parameters', async function () { | ||
219 | await makeDeleteRequest({ url: server.url, path: pathComment, token: server.accessToken, statusCodeExpected: 204 }) | ||
220 | }) | ||
221 | }) | ||
222 | |||
188 | describe('When a video has comments disabled', function () { | 223 | describe('When a video has comments disabled', function () { |
189 | before(async function () { | 224 | before(async function () { |
190 | const res = await uploadVideo(server.url, server.accessToken, { commentsEnabled: false }) | 225 | const res = await uploadVideo(server.url, server.accessToken, { commentsEnabled: false }) |
diff --git a/server/tests/api/videos/multiple-servers.ts b/server/tests/api/videos/multiple-servers.ts index b6dfe0d1b..6712829d4 100644 --- a/server/tests/api/videos/multiple-servers.ts +++ b/server/tests/api/videos/multiple-servers.ts | |||
@@ -13,7 +13,7 @@ import { | |||
13 | updateVideo, uploadVideo, userLogin, viewVideo, wait, webtorrentAdd | 13 | updateVideo, uploadVideo, userLogin, viewVideo, wait, webtorrentAdd |
14 | } from '../../utils' | 14 | } from '../../utils' |
15 | import { | 15 | import { |
16 | addVideoCommentReply, addVideoCommentThread, getVideoCommentThreads, | 16 | addVideoCommentReply, addVideoCommentThread, deleteVideoComment, getVideoCommentThreads, |
17 | getVideoThreadComments | 17 | getVideoThreadComments |
18 | } from '../../utils/videos/video-comments' | 18 | } from '../../utils/videos/video-comments' |
19 | 19 | ||
@@ -738,6 +738,37 @@ describe('Test multiple servers', function () { | |||
738 | } | 738 | } |
739 | }) | 739 | }) |
740 | 740 | ||
741 | it('Should delete the thread comments', async function () { | ||
742 | this.timeout(10000) | ||
743 | |||
744 | const res1 = await getVideoCommentThreads(servers[0].url, videoUUID, 0, 5) | ||
745 | const threadId = res1.body.data.find(c => c.text === 'my super first comment').id | ||
746 | await deleteVideoComment(servers[0].url, servers[0].accessToken, videoUUID, threadId) | ||
747 | |||
748 | await wait(5000) | ||
749 | }) | ||
750 | |||
751 | it('Should have the thread comments deleted on other servers too', async function () { | ||
752 | for (const server of servers) { | ||
753 | const res = await getVideoCommentThreads(server.url, videoUUID, 0, 5) | ||
754 | |||
755 | expect(res.body.total).to.equal(1) | ||
756 | expect(res.body.data).to.be.an('array') | ||
757 | expect(res.body.data).to.have.lengthOf(1) | ||
758 | |||
759 | { | ||
760 | const comment: VideoComment = res.body.data[0] | ||
761 | expect(comment).to.not.be.undefined | ||
762 | expect(comment.inReplyToCommentId).to.be.null | ||
763 | expect(comment.account.name).to.equal('root') | ||
764 | expect(comment.account.host).to.equal('localhost:9003') | ||
765 | expect(comment.totalReplies).to.equal(0) | ||
766 | expect(dateIsValid(comment.createdAt as string)).to.be.true | ||
767 | expect(dateIsValid(comment.updatedAt as string)).to.be.true | ||
768 | } | ||
769 | } | ||
770 | }) | ||
771 | |||
741 | it('Should disable comments', async function () { | 772 | it('Should disable comments', async function () { |
742 | this.timeout(20000) | 773 | this.timeout(20000) |
743 | 774 | ||
diff --git a/server/tests/api/videos/video-comments.ts b/server/tests/api/videos/video-comments.ts index 604a3027d..18d484ccf 100644 --- a/server/tests/api/videos/video-comments.ts +++ b/server/tests/api/videos/video-comments.ts | |||
@@ -9,7 +9,7 @@ import { | |||
9 | uploadVideo | 9 | uploadVideo |
10 | } from '../../utils/index' | 10 | } from '../../utils/index' |
11 | import { | 11 | import { |
12 | addVideoCommentReply, addVideoCommentThread, getVideoCommentThreads, | 12 | addVideoCommentReply, addVideoCommentThread, deleteVideoComment, getVideoCommentThreads, |
13 | getVideoThreadComments | 13 | getVideoThreadComments |
14 | } from '../../utils/videos/video-comments' | 14 | } from '../../utils/videos/video-comments' |
15 | 15 | ||
@@ -20,6 +20,7 @@ describe('Test video comments', function () { | |||
20 | let videoId | 20 | let videoId |
21 | let videoUUID | 21 | let videoUUID |
22 | let threadId | 22 | let threadId |
23 | let replyToDeleteId: number | ||
23 | 24 | ||
24 | before(async function () { | 25 | before(async function () { |
25 | this.timeout(10000) | 26 | this.timeout(10000) |
@@ -61,6 +62,7 @@ describe('Test video comments', function () { | |||
61 | expect(comment.id).to.equal(comment.threadId) | 62 | expect(comment.id).to.equal(comment.threadId) |
62 | expect(comment.account.name).to.equal('root') | 63 | expect(comment.account.name).to.equal('root') |
63 | expect(comment.account.host).to.equal('localhost:9001') | 64 | expect(comment.account.host).to.equal('localhost:9001') |
65 | expect(comment.account.url).to.equal('http://localhost:9001/accounts/root') | ||
64 | expect(comment.totalReplies).to.equal(0) | 66 | expect(comment.totalReplies).to.equal(0) |
65 | expect(dateIsValid(comment.createdAt as string)).to.be.true | 67 | expect(dateIsValid(comment.createdAt as string)).to.be.true |
66 | expect(dateIsValid(comment.updatedAt as string)).to.be.true | 68 | expect(dateIsValid(comment.updatedAt as string)).to.be.true |
@@ -132,6 +134,8 @@ describe('Test video comments', function () { | |||
132 | const secondChild = tree.children[1] | 134 | const secondChild = tree.children[1] |
133 | expect(secondChild.comment.text).to.equal('my second answer to thread 1') | 135 | expect(secondChild.comment.text).to.equal('my second answer to thread 1') |
134 | expect(secondChild.children).to.have.lengthOf(0) | 136 | expect(secondChild.children).to.have.lengthOf(0) |
137 | |||
138 | replyToDeleteId = secondChild.comment.id | ||
135 | }) | 139 | }) |
136 | 140 | ||
137 | it('Should create other threads', async function () { | 141 | it('Should create other threads', async function () { |
@@ -157,6 +161,38 @@ describe('Test video comments', function () { | |||
157 | expect(res.body.data[2].totalReplies).to.equal(0) | 161 | expect(res.body.data[2].totalReplies).to.equal(0) |
158 | }) | 162 | }) |
159 | 163 | ||
164 | it('Should delete a reply', async function () { | ||
165 | await deleteVideoComment(server.url, server.accessToken, videoId, replyToDeleteId) | ||
166 | |||
167 | const res = await getVideoThreadComments(server.url, videoUUID, threadId) | ||
168 | |||
169 | const tree: VideoCommentThreadTree = res.body | ||
170 | expect(tree.comment.text).equal('my super first comment') | ||
171 | expect(tree.children).to.have.lengthOf(1) | ||
172 | |||
173 | const firstChild = tree.children[0] | ||
174 | expect(firstChild.comment.text).to.equal('my super answer to thread 1') | ||
175 | expect(firstChild.children).to.have.lengthOf(1) | ||
176 | |||
177 | const childOfFirstChild = firstChild.children[0] | ||
178 | expect(childOfFirstChild.comment.text).to.equal('my super answer to answer of thread 1') | ||
179 | expect(childOfFirstChild.children).to.have.lengthOf(0) | ||
180 | }) | ||
181 | |||
182 | it('Should delete a complete thread', async function () { | ||
183 | await deleteVideoComment(server.url, server.accessToken, videoId, threadId) | ||
184 | |||
185 | const res = await getVideoCommentThreads(server.url, videoUUID, 0, 5, 'createdAt') | ||
186 | expect(res.body.total).to.equal(2) | ||
187 | expect(res.body.data).to.be.an('array') | ||
188 | expect(res.body.data).to.have.lengthOf(2) | ||
189 | |||
190 | expect(res.body.data[0].text).to.equal('super thread 2') | ||
191 | expect(res.body.data[0].totalReplies).to.equal(0) | ||
192 | expect(res.body.data[1].text).to.equal('super thread 3') | ||
193 | expect(res.body.data[1].totalReplies).to.equal(0) | ||
194 | }) | ||
195 | |||
160 | after(async function () { | 196 | after(async function () { |
161 | killallServers([ server ]) | 197 | killallServers([ server ]) |
162 | 198 | ||
diff --git a/server/tests/utils/videos/video-comments.ts b/server/tests/utils/videos/video-comments.ts index 878147049..1b9ee452e 100644 --- a/server/tests/utils/videos/video-comments.ts +++ b/server/tests/utils/videos/video-comments.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | import * as request from 'supertest' | 1 | import * as request from 'supertest' |
2 | import { makeDeleteRequest } from '../' | ||
2 | 3 | ||
3 | function getVideoCommentThreads (url: string, videoId: number | string, start: number, count: number, sort?: string) { | 4 | function getVideoCommentThreads (url: string, videoId: number | string, start: number, count: number, sort?: string) { |
4 | const path = '/api/v1/videos/' + videoId + '/comment-threads' | 5 | const path = '/api/v1/videos/' + videoId + '/comment-threads' |
@@ -54,11 +55,29 @@ function addVideoCommentReply ( | |||
54 | .expect(expectedStatus) | 55 | .expect(expectedStatus) |
55 | } | 56 | } |
56 | 57 | ||
58 | function deleteVideoComment ( | ||
59 | url: string, | ||
60 | token: string, | ||
61 | videoId: number | string, | ||
62 | commentId: number, | ||
63 | statusCodeExpected = 204 | ||
64 | ) { | ||
65 | const path = '/api/v1/videos/' + videoId + '/comments/' + commentId | ||
66 | |||
67 | return makeDeleteRequest({ | ||
68 | url, | ||
69 | path, | ||
70 | token, | ||
71 | statusCodeExpected | ||
72 | }) | ||
73 | } | ||
74 | |||
57 | // --------------------------------------------------------------------------- | 75 | // --------------------------------------------------------------------------- |
58 | 76 | ||
59 | export { | 77 | export { |
60 | getVideoCommentThreads, | 78 | getVideoCommentThreads, |
61 | getVideoThreadComments, | 79 | getVideoThreadComments, |
62 | addVideoCommentThread, | 80 | addVideoCommentThread, |
63 | addVideoCommentReply | 81 | addVideoCommentReply, |
82 | deleteVideoComment | ||
64 | } | 83 | } |