aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html36
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.scss8
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts52
-rw-r--r--client/src/app/shared/video-abuse/video-abuse.service.ts9
-rw-r--r--server/controllers/api/videos/abuse.ts1
-rw-r--r--server/models/video/video-abuse.ts121
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'
16import { DomSanitizer } from '@angular/platform-browser' 16import { DomSanitizer } from '@angular/platform-browser'
17import { BlocklistService } from '@app/shared/blocklist' 17import { BlocklistService } from '@app/shared/blocklist'
18import { VideoService } from '@app/shared/video/video.service' 18import { VideoService } from '@app/shared/video/video.service'
19import { ActivatedRoute } from '@angular/router'
20import { 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 @@
1import { 1import {
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'
4import { VideoAbuseObject } from '../../../shared/models/activitypub/objects' 4import { VideoAbuseObject } from '../../../shared/models/activitypub/objects'
5import { VideoAbuse } from '../../../shared/models/videos' 5import { VideoAbuse } from '../../../shared/models/videos'
@@ -21,34 +21,99 @@ import { VideoChannelModel } from './video-channel'
21import { ActorModel } from '../activitypub/actor' 21import { ActorModel } from '../activitypub/actor'
22import { VideoBlacklistModel } from './video-blacklist' 22import { VideoBlacklistModel } from './video-blacklist'
23 23
24@DefaultScope(() => ({ 24export 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 })