diff options
29 files changed, 437 insertions, 52 deletions
diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts index b1818c298..ef8597203 100644 --- a/client/src/app/app.component.ts +++ b/client/src/app/app.component.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { Router } from '@angular/router' | 2 | import { Router } from '@angular/router' |
3 | import { AuthService, ServerService } from './core' | 3 | import { AuthService, ServerService } from '@app/core' |
4 | 4 | ||
5 | @Component({ | 5 | @Component({ |
6 | selector: 'my-app', | 6 | selector: 'my-app', |
@@ -50,10 +50,6 @@ export class AppComponent implements OnInit { | |||
50 | } | 50 | } |
51 | } | 51 | } |
52 | 52 | ||
53 | isInAdmin () { | ||
54 | return this.router.url.indexOf('/admin/') !== -1 | ||
55 | } | ||
56 | |||
57 | toggleMenu () { | 53 | toggleMenu () { |
58 | window.scrollTo(0, 0) | 54 | window.scrollTo(0, 0) |
59 | this.isMenuDisplayed = !this.isMenuDisplayed | 55 | this.isMenuDisplayed = !this.isMenuDisplayed |
diff --git a/client/src/app/shared/account/account.model.ts b/client/src/app/shared/account/account.model.ts index cc46dad77..1dce0003c 100644 --- a/client/src/app/shared/account/account.model.ts +++ b/client/src/app/shared/account/account.model.ts | |||
@@ -1,11 +1,11 @@ | |||
1 | import { Account as ServerAccount } from '../../../../../shared/models/actors/account.model' | 1 | import { Account as ServerAccount } from '../../../../../shared/models/actors/account.model' |
2 | import { Avatar } from '../../../../../shared/models/avatars/avatar.model' | 2 | import { Avatar } from '../../../../../shared/models/avatars/avatar.model' |
3 | import { environment } from '../../../environments/environment' | ||
4 | import { getAbsoluteAPIUrl } from '../misc/utils' | 3 | import { getAbsoluteAPIUrl } from '../misc/utils' |
5 | 4 | ||
6 | export class Account implements ServerAccount { | 5 | export class Account implements ServerAccount { |
7 | id: number | 6 | id: number |
8 | uuid: string | 7 | uuid: string |
8 | url: string | ||
9 | name: string | 9 | name: string |
10 | displayName: string | 10 | displayName: string |
11 | host: string | 11 | host: string |
diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.scss b/client/src/app/videos/+video-edit/shared/video-edit.component.scss index 0fefcee28..1df9d4006 100644 --- a/client/src/app/videos/+video-edit/shared/video-edit.component.scss +++ b/client/src/app/videos/+video-edit/shared/video-edit.component.scss | |||
@@ -51,8 +51,6 @@ | |||
51 | 51 | ||
52 | .submit-container { | 52 | .submit-container { |
53 | text-align: right; | 53 | text-align: right; |
54 | position: relative; | ||
55 | bottom: $button-height; | ||
56 | 54 | ||
57 | .message-submit { | 55 | .message-submit { |
58 | display: inline-block; | 56 | display: inline-block; |
diff --git a/client/src/app/videos/+video-watch/comment/video-comment.component.html b/client/src/app/videos/+video-watch/comment/video-comment.component.html index e9c23929c..4f9597607 100644 --- a/client/src/app/videos/+video-watch/comment/video-comment.component.html +++ b/client/src/app/videos/+video-watch/comment/video-comment.component.html | |||
@@ -3,13 +3,14 @@ | |||
3 | 3 | ||
4 | <div class="comment"> | 4 | <div class="comment"> |
5 | <div class="comment-account-date"> | 5 | <div class="comment-account-date"> |
6 | <div class="comment-account">{{ comment.by }}</div> | 6 | <a target="_blank" [href]="comment.account.url" class="comment-account">{{ comment.by }}</a> |
7 | <div class="comment-date">{{ comment.createdAt | myFromNow }}</div> | 7 | <div class="comment-date">{{ comment.createdAt | myFromNow }}</div> |
8 | </div> | 8 | </div> |
9 | <div>{{ comment.text }}</div> | 9 | <div>{{ comment.text }}</div> |
10 | 10 | ||
11 | <div class="comment-actions"> | 11 | <div class="comment-actions"> |
12 | <div *ngIf="isUserLoggedIn()" (click)="onWantToReply()" class="comment-action-reply">Reply</div> | 12 | <div *ngIf="isUserLoggedIn()" (click)="onWantToReply()" class="comment-action-reply">Reply</div> |
13 | <div *ngIf="isRemovableByUser()" (click)="onWantToDelete()" class="comment-action-delete">Delete</div> | ||
13 | </div> | 14 | </div> |
14 | 15 | ||
15 | <my-video-comment-add | 16 | <my-video-comment-add |
@@ -28,7 +29,8 @@ | |||
28 | [video]="video" | 29 | [video]="video" |
29 | [inReplyToCommentId]="inReplyToCommentId" | 30 | [inReplyToCommentId]="inReplyToCommentId" |
30 | [commentTree]="commentChild" | 31 | [commentTree]="commentChild" |
31 | (wantedToReply)="onWantedToReply($event)" | 32 | (wantedToReply)="onWantToReply($event)" |
33 | (wantedToDelete)="onWantToDelete($event)" | ||
32 | (resetReply)="onResetReply()" | 34 | (resetReply)="onResetReply()" |
33 | ></my-video-comment> | 35 | ></my-video-comment> |
34 | </div> | 36 | </div> |
diff --git a/client/src/app/videos/+video-watch/comment/video-comment.component.scss b/client/src/app/videos/+video-watch/comment/video-comment.component.scss index aae03ab6d..a22c5a9fd 100644 --- a/client/src/app/videos/+video-watch/comment/video-comment.component.scss +++ b/client/src/app/videos/+video-watch/comment/video-comment.component.scss | |||
@@ -20,6 +20,9 @@ | |||
20 | margin-bottom: 4px; | 20 | margin-bottom: 4px; |
21 | 21 | ||
22 | .comment-account { | 22 | .comment-account { |
23 | @include disable-default-a-behaviour; | ||
24 | |||
25 | color: #000; | ||
23 | font-weight: $font-bold; | 26 | font-weight: $font-bold; |
24 | } | 27 | } |
25 | 28 | ||
@@ -31,10 +34,16 @@ | |||
31 | 34 | ||
32 | .comment-actions { | 35 | .comment-actions { |
33 | margin: 10px 0; | 36 | margin: 10px 0; |
37 | display: flex; | ||
34 | 38 | ||
35 | .comment-action-reply { | 39 | .comment-action-reply, .comment-action-delete { |
36 | color: #585858; | 40 | color: #585858; |
37 | cursor: pointer; | 41 | cursor: pointer; |
42 | margin-right: 10px; | ||
43 | |||
44 | &:hover { | ||
45 | color: #000; | ||
46 | } | ||
38 | } | 47 | } |
39 | } | 48 | } |
40 | } | 49 | } |
diff --git a/client/src/app/videos/+video-watch/comment/video-comment.component.ts b/client/src/app/videos/+video-watch/comment/video-comment.component.ts index b305c639a..9bc9c8844 100644 --- a/client/src/app/videos/+video-watch/comment/video-comment.component.ts +++ b/client/src/app/videos/+video-watch/comment/video-comment.component.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | import { Component, EventEmitter, Input, Output } from '@angular/core' | 1 | import { Component, EventEmitter, Input, Output } from '@angular/core' |
2 | import { Account as AccountInterface } from '../../../../../../shared/models/actors' | 2 | import { Account as AccountInterface } from '../../../../../../shared/models/actors' |
3 | import { UserRight } from '../../../../../../shared/models/users' | ||
3 | import { VideoCommentThreadTree } from '../../../../../../shared/models/videos/video-comment.model' | 4 | import { VideoCommentThreadTree } from '../../../../../../shared/models/videos/video-comment.model' |
4 | import { AuthService } from '../../../core/auth' | 5 | import { AuthService } from '../../../core/auth' |
5 | import { Account } from '../../../shared/account/account.model' | 6 | import { Account } from '../../../shared/account/account.model' |
@@ -17,7 +18,9 @@ export class VideoCommentComponent { | |||
17 | @Input() commentTree: VideoCommentThreadTree | 18 | @Input() commentTree: VideoCommentThreadTree |
18 | @Input() inReplyToCommentId: number | 19 | @Input() inReplyToCommentId: number |
19 | 20 | ||
21 | @Output() wantedToDelete = new EventEmitter<VideoComment>() | ||
20 | @Output() wantedToReply = new EventEmitter<VideoComment>() | 22 | @Output() wantedToReply = new EventEmitter<VideoComment>() |
23 | @Output() threadCreated = new EventEmitter<VideoCommentThreadTree>() | ||
21 | @Output() resetReply = new EventEmitter() | 24 | @Output() resetReply = new EventEmitter() |
22 | 25 | ||
23 | constructor (private authService: AuthService) {} | 26 | constructor (private authService: AuthService) {} |
@@ -32,6 +35,8 @@ export class VideoCommentComponent { | |||
32 | comment: this.comment, | 35 | comment: this.comment, |
33 | children: [] | 36 | children: [] |
34 | } | 37 | } |
38 | |||
39 | this.threadCreated.emit(this.commentTree) | ||
35 | } | 40 | } |
36 | 41 | ||
37 | this.commentTree.children.push({ | 42 | this.commentTree.children.push({ |
@@ -41,17 +46,16 @@ export class VideoCommentComponent { | |||
41 | this.resetReply.emit() | 46 | this.resetReply.emit() |
42 | } | 47 | } |
43 | 48 | ||
44 | onWantToReply () { | 49 | onWantToReply (comment?: VideoComment) { |
45 | this.wantedToReply.emit(this.comment) | 50 | this.wantedToReply.emit(comment || this.comment) |
46 | } | 51 | } |
47 | 52 | ||
48 | isUserLoggedIn () { | 53 | onWantToDelete (comment?: VideoComment) { |
49 | return this.authService.isLoggedIn() | 54 | this.wantedToDelete.emit(comment || this.comment) |
50 | } | 55 | } |
51 | 56 | ||
52 | // Event from child comment | 57 | isUserLoggedIn () { |
53 | onWantedToReply (comment: VideoComment) { | 58 | return this.authService.isLoggedIn() |
54 | this.wantedToReply.emit(comment) | ||
55 | } | 59 | } |
56 | 60 | ||
57 | onResetReply () { | 61 | onResetReply () { |
@@ -61,4 +65,12 @@ export class VideoCommentComponent { | |||
61 | getAvatarUrl (account: AccountInterface) { | 65 | getAvatarUrl (account: AccountInterface) { |
62 | return Account.GET_ACCOUNT_AVATAR_URL(account) | 66 | return Account.GET_ACCOUNT_AVATAR_URL(account) |
63 | } | 67 | } |
68 | |||
69 | isRemovableByUser () { | ||
70 | return this.isUserLoggedIn() && | ||
71 | ( | ||
72 | this.user.account.id === this.comment.account.id || | ||
73 | this.user.hasRight(UserRight.REMOVE_ANY_VIDEO_COMMENT) | ||
74 | ) | ||
75 | } | ||
64 | } | 76 | } |
diff --git a/client/src/app/videos/+video-watch/comment/video-comment.service.ts b/client/src/app/videos/+video-watch/comment/video-comment.service.ts index 2fe6cc3e9..c42f55496 100644 --- a/client/src/app/videos/+video-watch/comment/video-comment.service.ts +++ b/client/src/app/videos/+video-watch/comment/video-comment.service.ts | |||
@@ -66,6 +66,15 @@ export class VideoCommentService { | |||
66 | .catch((res) => this.restExtractor.handleError(res)) | 66 | .catch((res) => this.restExtractor.handleError(res)) |
67 | } | 67 | } |
68 | 68 | ||
69 | deleteVideoComment (videoId: number | string, commentId: number) { | ||
70 | const url = `${VideoCommentService.BASE_VIDEO_URL + videoId}/comments/${commentId}` | ||
71 | |||
72 | return this.authHttp | ||
73 | .delete(url) | ||
74 | .map(this.restExtractor.extractDataBool) | ||
75 | .catch((res) => this.restExtractor.handleError(res)) | ||
76 | } | ||
77 | |||
69 | private extractVideoComment (videoComment: VideoCommentServerModel) { | 78 | private extractVideoComment (videoComment: VideoCommentServerModel) { |
70 | return new VideoComment(videoComment) | 79 | return new VideoComment(videoComment) |
71 | } | 80 | } |
diff --git a/client/src/app/videos/+video-watch/comment/video-comments.component.html b/client/src/app/videos/+video-watch/comment/video-comments.component.html index 4a4248073..80b200931 100644 --- a/client/src/app/videos/+video-watch/comment/video-comments.component.html +++ b/client/src/app/videos/+video-watch/comment/video-comments.component.html | |||
@@ -27,6 +27,8 @@ | |||
27 | [inReplyToCommentId]="inReplyToCommentId" | 27 | [inReplyToCommentId]="inReplyToCommentId" |
28 | [commentTree]="threadComments[comment.id]" | 28 | [commentTree]="threadComments[comment.id]" |
29 | (wantedToReply)="onWantedToReply($event)" | 29 | (wantedToReply)="onWantedToReply($event)" |
30 | (wantedToDelete)="onWantedToDelete($event)" | ||
31 | (threadCreated)="onThreadCreated($event)" | ||
30 | (resetReply)="onResetReply()" | 32 | (resetReply)="onResetReply()" |
31 | ></my-video-comment> | 33 | ></my-video-comment> |
32 | 34 | ||
diff --git a/client/src/app/videos/+video-watch/comment/video-comments.component.ts b/client/src/app/videos/+video-watch/comment/video-comments.component.ts index 1230725c1..030dee9af 100644 --- a/client/src/app/videos/+video-watch/comment/video-comments.component.ts +++ b/client/src/app/videos/+video-watch/comment/video-comments.component.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import { Component, Input, OnInit } from '@angular/core' | 1 | import { Component, Input, OnInit } from '@angular/core' |
2 | import { ConfirmService } from '@app/core' | ||
2 | import { NotificationsService } from 'angular2-notifications' | 3 | import { NotificationsService } from 'angular2-notifications' |
3 | import { VideoCommentThreadTree } from '../../../../../../shared/models/videos/video-comment.model' | 4 | import { VideoComment as VideoCommentInterface, VideoCommentThreadTree } from '../../../../../../shared/models/videos/video-comment.model' |
4 | import { AuthService } from '../../../core/auth' | 5 | import { AuthService } from '../../../core/auth' |
5 | import { ComponentPagination } from '../../../shared/rest/component-pagination.model' | 6 | import { ComponentPagination } from '../../../shared/rest/component-pagination.model' |
6 | import { User } from '../../../shared/users' | 7 | import { User } from '../../../shared/users' |
@@ -32,6 +33,7 @@ export class VideoCommentsComponent implements OnInit { | |||
32 | constructor ( | 33 | constructor ( |
33 | private authService: AuthService, | 34 | private authService: AuthService, |
34 | private notificationsService: NotificationsService, | 35 | private notificationsService: NotificationsService, |
36 | private confirmService: ConfirmService, | ||
35 | private videoCommentService: VideoCommentService | 37 | private videoCommentService: VideoCommentService |
36 | ) {} | 38 | ) {} |
37 | 39 | ||
@@ -41,7 +43,7 @@ export class VideoCommentsComponent implements OnInit { | |||
41 | } | 43 | } |
42 | } | 44 | } |
43 | 45 | ||
44 | viewReplies (comment: VideoComment) { | 46 | viewReplies (comment: VideoCommentInterface) { |
45 | this.threadLoading[comment.id] = true | 47 | this.threadLoading[comment.id] = true |
46 | 48 | ||
47 | this.videoCommentService.getVideoThreadComments(this.video.id, comment.id) | 49 | this.videoCommentService.getVideoThreadComments(this.video.id, comment.id) |
@@ -79,6 +81,44 @@ export class VideoCommentsComponent implements OnInit { | |||
79 | this.inReplyToCommentId = undefined | 81 | this.inReplyToCommentId = undefined |
80 | } | 82 | } |
81 | 83 | ||
84 | onThreadCreated (commentTree: VideoCommentThreadTree) { | ||
85 | this.viewReplies(commentTree.comment) | ||
86 | } | ||
87 | |||
88 | onWantedToDelete (commentToDelete: VideoComment) { | ||
89 | let message = 'Do you really want to delete this comment?' | ||
90 | if (commentToDelete.totalReplies !== 0) message += `${commentToDelete.totalReplies} would be deleted too.` | ||
91 | |||
92 | this.confirmService.confirm(message, 'Delete').subscribe( | ||
93 | res => { | ||
94 | if (res === false) return | ||
95 | |||
96 | this.videoCommentService.deleteVideoComment(commentToDelete.videoId, commentToDelete.id) | ||
97 | .subscribe( | ||
98 | () => { | ||
99 | // Delete the comment in the tree | ||
100 | if (commentToDelete.inReplyToCommentId) { | ||
101 | const thread = this.threadComments[commentToDelete.threadId] | ||
102 | if (!thread) { | ||
103 | console.error(`Cannot find thread ${commentToDelete.threadId} of the comment to delete ${commentToDelete.id}`) | ||
104 | return | ||
105 | } | ||
106 | |||
107 | this.deleteLocalCommentThread(thread, commentToDelete) | ||
108 | return | ||
109 | } | ||
110 | |||
111 | // Delete the thread | ||
112 | this.comments = this.comments.filter(c => c.id !== commentToDelete.id) | ||
113 | this.componentPagination.totalItems-- | ||
114 | }, | ||
115 | |||
116 | err => this.notificationsService.error('Error', err.message) | ||
117 | ) | ||
118 | } | ||
119 | ) | ||
120 | } | ||
121 | |||
82 | isUserLoggedIn () { | 122 | isUserLoggedIn () { |
83 | return this.authService.isLoggedIn() | 123 | return this.authService.isLoggedIn() |
84 | } | 124 | } |
@@ -91,7 +131,7 @@ export class VideoCommentsComponent implements OnInit { | |||
91 | } | 131 | } |
92 | } | 132 | } |
93 | 133 | ||
94 | protected hasMoreComments () { | 134 | private hasMoreComments () { |
95 | // No results | 135 | // No results |
96 | if (this.componentPagination.totalItems === 0) return false | 136 | if (this.componentPagination.totalItems === 0) return false |
97 | 137 | ||
@@ -101,4 +141,15 @@ export class VideoCommentsComponent implements OnInit { | |||
101 | const maxPage = this.componentPagination.totalItems / this.componentPagination.itemsPerPage | 141 | const maxPage = this.componentPagination.totalItems / this.componentPagination.itemsPerPage |
102 | return maxPage > this.componentPagination.currentPage | 142 | return maxPage > this.componentPagination.currentPage |
103 | } | 143 | } |
144 | |||
145 | private deleteLocalCommentThread (parentComment: VideoCommentThreadTree, commentToDelete: VideoComment) { | ||
146 | for (const commentChild of parentComment.children) { | ||
147 | if (commentChild.comment.id === commentToDelete.id) { | ||
148 | parentComment.children = parentComment.children.filter(c => c.comment.id !== commentToDelete.id) | ||
149 | return | ||
150 | } | ||
151 | |||
152 | this.deleteLocalCommentThread(commentChild, commentToDelete) | ||
153 | } | ||
154 | } | ||
104 | } | 155 | } |
diff --git a/client/src/app/videos/+video-watch/video-watch.component.ts b/client/src/app/videos/+video-watch/video-watch.component.ts index 0f44d3dd7..f1f194764 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.ts +++ b/client/src/app/videos/+video-watch/video-watch.component.ts | |||
@@ -137,7 +137,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
137 | blacklistVideo (event: Event) { | 137 | blacklistVideo (event: Event) { |
138 | event.preventDefault() | 138 | event.preventDefault() |
139 | 139 | ||
140 | this.confirmService.confirm('Do you really want to blacklist this video ?', 'Blacklist').subscribe( | 140 | this.confirmService.confirm('Do you really want to blacklist this video?', 'Blacklist').subscribe( |
141 | res => { | 141 | res => { |
142 | if (res === false) return | 142 | if (res === false) return |
143 | 143 | ||
diff --git a/client/tsconfig.json b/client/tsconfig.json index a6c016bf3..43b27ce8e 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json | |||
@@ -14,6 +14,10 @@ | |||
14 | "lib": [ | 14 | "lib": [ |
15 | "es2017", | 15 | "es2017", |
16 | "dom" | 16 | "dom" |
17 | ] | 17 | ], |
18 | "baseUrl": "src", | ||
19 | "paths": { | ||
20 | "@app/*": [ "app/*" ] | ||
21 | } | ||
18 | } | 22 | } |
19 | } | 23 | } |
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 | } |
diff --git a/shared/models/actors/account.model.ts b/shared/models/actors/account.model.ts index ef6fca539..e4dbc81e5 100644 --- a/shared/models/actors/account.model.ts +++ b/shared/models/actors/account.model.ts | |||
@@ -3,6 +3,7 @@ import { Avatar } from '../avatars/avatar.model' | |||
3 | export interface Account { | 3 | export interface Account { |
4 | id: number | 4 | id: number |
5 | uuid: string | 5 | uuid: string |
6 | url: string | ||
6 | name: string | 7 | name: string |
7 | displayName: string | 8 | displayName: string |
8 | host: string | 9 | host: string |
diff --git a/shared/models/users/user-right.enum.ts b/shared/models/users/user-right.enum.ts index 238e38a36..2e7fa1bcf 100644 --- a/shared/models/users/user-right.enum.ts +++ b/shared/models/users/user-right.enum.ts | |||
@@ -6,5 +6,6 @@ export enum UserRight { | |||
6 | MANAGE_VIDEO_BLACKLIST, | 6 | MANAGE_VIDEO_BLACKLIST, |
7 | MANAGE_JOBS, | 7 | MANAGE_JOBS, |
8 | REMOVE_ANY_VIDEO, | 8 | REMOVE_ANY_VIDEO, |
9 | REMOVE_ANY_VIDEO_CHANNEL | 9 | REMOVE_ANY_VIDEO_CHANNEL, |
10 | REMOVE_ANY_VIDEO_COMMENT | ||
10 | } | 11 | } |
diff --git a/shared/models/users/user-role.ts b/shared/models/users/user-role.ts index cc32c768d..0e75444f8 100644 --- a/shared/models/users/user-role.ts +++ b/shared/models/users/user-role.ts | |||
@@ -23,7 +23,8 @@ const userRoleRights: { [ id: number ]: UserRight[] } = { | |||
23 | UserRight.MANAGE_VIDEO_BLACKLIST, | 23 | UserRight.MANAGE_VIDEO_BLACKLIST, |
24 | UserRight.MANAGE_VIDEO_ABUSES, | 24 | UserRight.MANAGE_VIDEO_ABUSES, |
25 | UserRight.REMOVE_ANY_VIDEO, | 25 | UserRight.REMOVE_ANY_VIDEO, |
26 | UserRight.REMOVE_ANY_VIDEO_CHANNEL | 26 | UserRight.REMOVE_ANY_VIDEO_CHANNEL, |
27 | UserRight.REMOVE_ANY_VIDEO_COMMENT | ||
27 | ], | 28 | ], |
28 | 29 | ||
29 | [UserRole.USER]: [] | 30 | [UserRole.USER]: [] |
diff --git a/shared/models/videos/video-channel.model.ts b/shared/models/videos/video-channel.model.ts index ee56c54b6..d1a952826 100644 --- a/shared/models/videos/video-channel.model.ts +++ b/shared/models/videos/video-channel.model.ts | |||
@@ -3,6 +3,7 @@ import { Video } from './video.model' | |||
3 | export interface VideoChannel { | 3 | export interface VideoChannel { |
4 | id: number | 4 | id: number |
5 | name: string | 5 | name: string |
6 | url: string | ||
6 | description: string | 7 | description: string |
7 | isLocal: boolean | 8 | isLocal: boolean |
8 | createdAt: Date | string | 9 | createdAt: Date | string |