diff options
6 files changed, 171 insertions, 56 deletions
diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html index 2204bb371..c1ce093d7 100644 --- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html +++ b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html | |||
@@ -4,6 +4,17 @@ | |||
4 | [showCurrentPageReport]="true" i18n-currentPageReportTemplate | 4 | [showCurrentPageReport]="true" i18n-currentPageReportTemplate |
5 | currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} reports" | 5 | currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} reports" |
6 | > | 6 | > |
7 | <ng-template pTemplate="caption"> | ||
8 | <div class="caption"> | ||
9 | <div class="ml-auto"> | ||
10 | <input | ||
11 | type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..." | ||
12 | (keyup)="onSearch($event)" | ||
13 | > | ||
14 | </div> | ||
15 | </div> | ||
16 | </ng-template> | ||
17 | |||
7 | <ng-template pTemplate="header"> | 18 | <ng-template pTemplate="header"> |
8 | <tr> <!-- header --> | 19 | <tr> <!-- header --> |
9 | <th style="width: 40px;"></th> | 20 | <th style="width: 40px;"></th> |
@@ -40,18 +51,14 @@ | |||
40 | </a> | 51 | </a> |
41 | </td> | 52 | </td> |
42 | 53 | ||
43 | <td> | 54 | <td *ngIf="!videoAbuse.video.deleted"> |
44 | <a [href]="getVideoUrl(videoAbuse)" class="video-abuse-video-link" i18n-title title="Open video in a new tab" target="_blank" rel="noopener noreferrer"> | 55 | <a [href]="getVideoUrl(videoAbuse)" class="video-abuse-video-link" i18n-title title="Open video in a new tab" target="_blank" rel="noopener noreferrer"> |
45 | <div class="video-abuse-video"> | 56 | <div class="video-abuse-video"> |
46 | <div class="video-abuse-video-image"> | 57 | <div class="video-abuse-video-image"><img [src]="videoAbuse.video.thumbnailPath"></div> |
47 | <img *ngIf="!videoAbuse.video.deleted" [src]="videoAbuse.video.thumbnailPath"> | ||
48 | <span *ngIf="videoAbuse.video.deleted" i18n>Deleted</span> | ||
49 | </div> | ||
50 | <div class="video-abuse-video-text"> | 58 | <div class="video-abuse-video-text"> |
51 | <div> | 59 | <div> |
52 | {{ videoAbuse.video.name }} | 60 | {{ videoAbuse.video.name }} |
53 | <span *ngIf="!videoAbuse.video.deleted && !videoAbuse.video.blacklisted" class="glyphicon glyphicon-new-window"></span> | 61 | <span *ngIf="!videoAbuse.video.blacklisted" class="glyphicon glyphicon-new-window"></span> |
54 | <span *ngIf="videoAbuse.video.deleted" i18n-title title="Video was deleted" class="glyphicon glyphicon-trash"></span> | ||
55 | <span *ngIf="videoAbuse.video.blacklisted" i18n-title title="Video was blacklisted" class="glyphicon glyphicon-ban-circle"></span> | 62 | <span *ngIf="videoAbuse.video.blacklisted" i18n-title title="Video was blacklisted" class="glyphicon glyphicon-ban-circle"></span> |
56 | </div> | 63 | </div> |
57 | <div class="text-muted">by {{ videoAbuse.video.channel?.displayName }} on {{ videoAbuse.video.channel?.host }} </div> | 64 | <div class="text-muted">by {{ videoAbuse.video.channel?.displayName }} on {{ videoAbuse.video.channel?.host }} </div> |
@@ -60,7 +67,20 @@ | |||
60 | </a> | 67 | </a> |
61 | </td> | 68 | </td> |
62 | 69 | ||
63 | <td>{{ videoAbuse.createdAt }}</td> | 70 | <td *ngIf="videoAbuse.video.deleted" class="c-hand" [pRowToggler]="videoAbuse"> |
71 | <div class="video-abuse-video" i18n-title title="Video was deleted"> | ||
72 | <div class="video-abuse-video-image"><span i18n>Deleted</span></div> | ||
73 | <div class="video-abuse-video-text"> | ||
74 | <div> | ||
75 | {{ videoAbuse.video.name }} | ||
76 | <span class="glyphicon glyphicon-trash"></span> | ||
77 | </div> | ||
78 | <div class="text-muted">by {{ videoAbuse.video.channel?.displayName }} on {{ videoAbuse.video.channel?.host }} </div> | ||
79 | </div> | ||
80 | </div> | ||
81 | </td> | ||
82 | |||
83 | <td class="c-hand" [pRowToggler]="videoAbuse">{{ videoAbuse.createdAt }}</td> | ||
64 | 84 | ||
65 | <td class="c-hand video-abuse-states" [pRowToggler]="videoAbuse"> | 85 | <td class="c-hand video-abuse-states" [pRowToggler]="videoAbuse"> |
66 | <span *ngIf="isVideoAbuseAccepted(videoAbuse)" [title]="videoAbuse.state.label" class="glyphicon glyphicon-ok"></span> | 86 | <span *ngIf="isVideoAbuseAccepted(videoAbuse)" [title]="videoAbuse.state.label" class="glyphicon glyphicon-ok"></span> |
diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.scss b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.scss index 09402fda7..9b60c39dc 100644 --- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.scss +++ b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.scss | |||
@@ -1,6 +1,14 @@ | |||
1 | @import 'mixins'; | 1 | @import 'mixins'; |
2 | @import 'miniature'; | 2 | @import 'miniature'; |
3 | 3 | ||
4 | .caption { | ||
5 | justify-content: flex-end; | ||
6 | |||
7 | input { | ||
8 | @include peertube-input-text(250px); | ||
9 | } | ||
10 | } | ||
11 | |||
4 | .video-abuse-video-link { | 12 | .video-abuse-video-link { |
5 | @include disable-outline; | 13 | @include disable-outline; |
6 | position: relative; | 14 | position: relative; |
diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts index cc5014ae8..6dcf96ccf 100644 --- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts +++ b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts | |||
@@ -16,6 +16,8 @@ import { getAbsoluteAPIUrl } from '@app/shared/misc/utils' | |||
16 | import { DomSanitizer } from '@angular/platform-browser' | 16 | import { DomSanitizer } from '@angular/platform-browser' |
17 | import { BlocklistService } from '@app/shared/blocklist' | 17 | import { BlocklistService } from '@app/shared/blocklist' |
18 | import { VideoService } from '@app/shared/video/video.service' | 18 | import { VideoService } from '@app/shared/video/video.service' |
19 | import { ActivatedRoute } from '@angular/router' | ||
20 | import { first } from 'rxjs/operators' | ||
19 | 21 | ||
20 | @Component({ | 22 | @Component({ |
21 | selector: 'my-video-abuse-list', | 23 | selector: 'my-video-abuse-list', |
@@ -43,7 +45,8 @@ export class VideoAbuseListComponent extends RestTable implements OnInit { | |||
43 | private confirmService: ConfirmService, | 45 | private confirmService: ConfirmService, |
44 | private i18n: I18n, | 46 | private i18n: I18n, |
45 | private markdownRenderer: MarkdownService, | 47 | private markdownRenderer: MarkdownService, |
46 | private sanitizer: DomSanitizer | 48 | private sanitizer: DomSanitizer, |
49 | private route: ActivatedRoute, | ||
47 | ) { | 50 | ) { |
48 | super() | 51 | super() |
49 | 52 | ||
@@ -185,6 +188,10 @@ export class VideoAbuseListComponent extends RestTable implements OnInit { | |||
185 | 188 | ||
186 | ngOnInit () { | 189 | ngOnInit () { |
187 | this.initialize() | 190 | this.initialize() |
191 | |||
192 | this.route.queryParams | ||
193 | .pipe(first(params => params.search !== undefined && params.search !== null)) | ||
194 | .subscribe(params => this.search = params.search) | ||
188 | } | 195 | } |
189 | 196 | ||
190 | getIdentifier () { | 197 | getIdentifier () { |
@@ -253,26 +260,29 @@ export class VideoAbuseListComponent extends RestTable implements OnInit { | |||
253 | } | 260 | } |
254 | 261 | ||
255 | protected loadData () { | 262 | protected loadData () { |
256 | return this.videoAbuseService.getVideoAbuses(this.pagination, this.sort) | 263 | return this.videoAbuseService.getVideoAbuses({ |
257 | .subscribe( | 264 | pagination: this.pagination, |
258 | async resultList => { | 265 | sort: this.sort, |
259 | this.totalRecords = resultList.total | 266 | search: this.search |
260 | 267 | }).subscribe( | |
261 | this.videoAbuses = resultList.data | 268 | async resultList => { |
262 | 269 | this.totalRecords = resultList.total | |
263 | for (const abuse of this.videoAbuses) { | 270 | |
264 | Object.assign(abuse, { | 271 | this.videoAbuses = resultList.data |
265 | reasonHtml: await this.toHtml(abuse.reason), | 272 | |
266 | moderationCommentHtml: await this.toHtml(abuse.moderationComment), | 273 | for (const abuse of this.videoAbuses) { |
267 | embedHtml: this.sanitizer.bypassSecurityTrustHtml(this.getVideoEmbed(abuse)), | 274 | Object.assign(abuse, { |
268 | reporterAccount: new Account(abuse.reporterAccount) | 275 | reasonHtml: await this.toHtml(abuse.reason), |
269 | }) | 276 | moderationCommentHtml: await this.toHtml(abuse.moderationComment), |
270 | } | 277 | embedHtml: this.sanitizer.bypassSecurityTrustHtml(this.getVideoEmbed(abuse)), |
271 | 278 | reporterAccount: new Account(abuse.reporterAccount) | |
272 | }, | 279 | }) |
273 | 280 | } | |
274 | err => this.notifier.error(err.message) | 281 | |
275 | ) | 282 | }, |
283 | |||
284 | err => this.notifier.error(err.message) | ||
285 | ) | ||
276 | } | 286 | } |
277 | 287 | ||
278 | private toHtml (text: string) { | 288 | private toHtml (text: string) { |
diff --git a/client/src/app/shared/video-abuse/video-abuse.service.ts b/client/src/app/shared/video-abuse/video-abuse.service.ts index 61a328575..a39ad31d4 100644 --- a/client/src/app/shared/video-abuse/video-abuse.service.ts +++ b/client/src/app/shared/video-abuse/video-abuse.service.ts | |||
@@ -17,12 +17,19 @@ export class VideoAbuseService { | |||
17 | private restExtractor: RestExtractor | 17 | private restExtractor: RestExtractor |
18 | ) {} | 18 | ) {} |
19 | 19 | ||
20 | getVideoAbuses (pagination: RestPagination, sort: SortMeta): Observable<ResultList<VideoAbuse>> { | 20 | getVideoAbuses (options: { |
21 | pagination: RestPagination, | ||
22 | sort: SortMeta, | ||
23 | search?: string | ||
24 | }): Observable<ResultList<VideoAbuse>> { | ||
25 | const { pagination, sort, search } = options | ||
21 | const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + 'abuse' | 26 | const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + 'abuse' |
22 | 27 | ||
23 | let params = new HttpParams() | 28 | let params = new HttpParams() |
24 | params = this.restService.addRestGetParams(params, pagination, sort) | 29 | params = this.restService.addRestGetParams(params, pagination, sort) |
25 | 30 | ||
31 | if (search) params = params.append('search', search) | ||
32 | |||
26 | return this.authHttp.get<ResultList<VideoAbuse>>(url, { params }) | 33 | return this.authHttp.get<ResultList<VideoAbuse>>(url, { params }) |
27 | .pipe( | 34 | .pipe( |
28 | map(res => this.restExtractor.convertResultListDateToHuman(res)), | 35 | map(res => this.restExtractor.convertResultListDateToHuman(res)), |
diff --git a/server/controllers/api/videos/abuse.ts b/server/controllers/api/videos/abuse.ts index 4ae899b7e..f37d90896 100644 --- a/server/controllers/api/videos/abuse.ts +++ b/server/controllers/api/videos/abuse.ts | |||
@@ -69,6 +69,7 @@ async function listVideoAbuses (req: express.Request, res: express.Response) { | |||
69 | start: req.query.start, | 69 | start: req.query.start, |
70 | count: req.query.count, | 70 | count: req.query.count, |
71 | sort: req.query.sort, | 71 | sort: req.query.sort, |
72 | search: req.query.search, | ||
72 | serverAccountId: serverActor.Account.id, | 73 | serverAccountId: serverActor.Account.id, |
73 | user | 74 | user |
74 | }) | 75 | }) |
diff --git a/server/models/video/video-abuse.ts b/server/models/video/video-abuse.ts index ea943ffdf..5ead02eca 100644 --- a/server/models/video/video-abuse.ts +++ b/server/models/video/video-abuse.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { | 1 | import { |
2 | AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Is, Model, Table, UpdatedAt, DefaultScope | 2 | AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Is, Model, Table, UpdatedAt, Scopes |
3 | } from 'sequelize-typescript' | 3 | } from 'sequelize-typescript' |
4 | import { VideoAbuseObject } from '../../../shared/models/activitypub/objects' | 4 | import { VideoAbuseObject } from '../../../shared/models/activitypub/objects' |
5 | import { VideoAbuse } from '../../../shared/models/videos' | 5 | import { VideoAbuse } from '../../../shared/models/videos' |
@@ -21,34 +21,99 @@ import { VideoChannelModel } from './video-channel' | |||
21 | import { ActorModel } from '../activitypub/actor' | 21 | import { ActorModel } from '../activitypub/actor' |
22 | import { VideoBlacklistModel } from './video-blacklist' | 22 | import { VideoBlacklistModel } from './video-blacklist' |
23 | 23 | ||
24 | @DefaultScope(() => ({ | 24 | export enum ScopeNames { |
25 | include: [ | 25 | FOR_API = 'FOR_API' |
26 | { | 26 | } |
27 | model: AccountModel, | 27 | |
28 | required: true | 28 | @Scopes(() => ({ |
29 | }, | 29 | [ScopeNames.FOR_API]: (options: { |
30 | { | 30 | search?: string |
31 | model: VideoModel, | 31 | searchReporter?: string |
32 | required: false, | 32 | searchVideo?: string |
33 | searchVideoChannel?: string | ||
34 | serverAccountId: number | ||
35 | userAccountId: any | ||
36 | }) => { | ||
37 | const search = (sourceField, targetField) => sourceField ? ({ | ||
38 | [targetField]: { | ||
39 | [Op.iLike]: `%${sourceField}%` | ||
40 | } | ||
41 | }) : {} | ||
42 | |||
43 | let where = { | ||
44 | reporterAccountId: { | ||
45 | [Op.notIn]: literal('(' + buildBlockedAccountSQL(options.serverAccountId, options.userAccountId) + ')') | ||
46 | } | ||
47 | } | ||
48 | |||
49 | if (options.search) { | ||
50 | where = Object.assign(where, { | ||
51 | [Op.or]: [ | ||
52 | { | ||
53 | [Op.and]: [ | ||
54 | { videoId: { [Op.not]: null } }, | ||
55 | { '$Video.name$': { [Op.iLike]: `%${options.search}%` } } | ||
56 | ] | ||
57 | }, | ||
58 | { | ||
59 | [Op.and]: [ | ||
60 | { videoId: { [Op.not]: null } }, | ||
61 | { '$Video.VideoChannel.name$': { [Op.iLike]: `%${options.search}%` } } | ||
62 | ] | ||
63 | }, | ||
64 | { | ||
65 | [Op.and]: [ | ||
66 | { deletedVideo: { [Op.not]: null } }, | ||
67 | { deletedVideo: { name: { [Op.iLike]: `%${options.search}%` } } } | ||
68 | ] | ||
69 | }, | ||
70 | { | ||
71 | [Op.and]: [ | ||
72 | { deletedVideo: { [Op.not]: null } }, | ||
73 | { deletedVideo: { channel: { displayName: { [Op.iLike]: `%${options.search}%` } } } } | ||
74 | ] | ||
75 | }, | ||
76 | { '$Account.name$': { [Op.iLike]: `%${options.search}%` } } | ||
77 | ] | ||
78 | }) | ||
79 | } | ||
80 | |||
81 | console.log(where) | ||
82 | |||
83 | return { | ||
33 | include: [ | 84 | include: [ |
34 | { | 85 | { |
35 | model: ThumbnailModel | 86 | model: AccountModel, |
87 | required: true, | ||
88 | where: { ...search(options.searchReporter, 'name') } | ||
36 | }, | 89 | }, |
37 | { | 90 | { |
38 | model: VideoChannelModel.unscoped(), | 91 | model: VideoModel, |
92 | required: false, | ||
93 | where: { ...search(options.searchVideo, 'name') }, | ||
39 | include: [ | 94 | include: [ |
40 | { | 95 | { |
41 | model: ActorModel | 96 | model: ThumbnailModel |
97 | }, | ||
98 | { | ||
99 | model: VideoChannelModel.unscoped(), | ||
100 | where: { ...search(options.searchVideoChannel, 'name') }, | ||
101 | include: [ | ||
102 | { | ||
103 | model: ActorModel | ||
104 | } | ||
105 | ] | ||
106 | }, | ||
107 | { | ||
108 | attributes: [ 'id', 'reason', 'unfederated' ], | ||
109 | model: VideoBlacklistModel | ||
42 | } | 110 | } |
43 | ] | 111 | ] |
44 | }, | ||
45 | { | ||
46 | attributes: [ 'id', 'reason', 'unfederated' ], | ||
47 | model: VideoBlacklistModel | ||
48 | } | 112 | } |
49 | ] | 113 | ], |
114 | where | ||
50 | } | 115 | } |
51 | ] | 116 | } |
52 | })) | 117 | })) |
53 | @Table({ | 118 | @Table({ |
54 | tableName: 'videoAbuse', | 119 | tableName: 'videoAbuse', |
@@ -134,26 +199,30 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> { | |||
134 | start: number | 199 | start: number |
135 | count: number | 200 | count: number |
136 | sort: string | 201 | sort: string |
202 | search?: string | ||
137 | serverAccountId: number | 203 | serverAccountId: number |
138 | user?: MUserAccountId | 204 | user?: MUserAccountId |
139 | }) { | 205 | }) { |
140 | const { start, count, sort, user, serverAccountId } = parameters | 206 | const { start, count, sort, search, user, serverAccountId } = parameters |
141 | const userAccountId = user ? user.Account.id : undefined | 207 | const userAccountId = user ? user.Account.id : undefined |
142 | 208 | ||
143 | const query = { | 209 | const query = { |
144 | offset: start, | 210 | offset: start, |
145 | limit: count, | 211 | limit: count, |
146 | order: getSort(sort), | 212 | order: getSort(sort), |
147 | where: { | ||
148 | reporterAccountId: { | ||
149 | [Op.notIn]: literal('(' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')') | ||
150 | } | ||
151 | }, | ||
152 | col: 'VideoAbuseModel.id', | 213 | col: 'VideoAbuseModel.id', |
153 | distinct: true | 214 | distinct: true |
154 | } | 215 | } |
155 | 216 | ||
156 | return VideoAbuseModel.findAndCountAll(query) | 217 | const filters = { |
218 | search, | ||
219 | serverAccountId, | ||
220 | userAccountId | ||
221 | } | ||
222 | |||
223 | return VideoAbuseModel | ||
224 | .scope({ method: [ ScopeNames.FOR_API, filters ] }) | ||
225 | .findAndCountAll(query) | ||
157 | .then(({ rows, count }) => { | 226 | .then(({ rows, count }) => { |
158 | return { total: count, data: rows } | 227 | return { total: count, data: rows } |
159 | }) | 228 | }) |