aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/shared/shared-moderation
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app/shared/shared-moderation')
-rw-r--r--client/src/app/shared/shared-moderation/abuse.service.ts154
-rw-r--r--client/src/app/shared/shared-moderation/index.ts5
-rw-r--r--client/src/app/shared/shared-moderation/report-modals/account-report.component.ts94
-rw-r--r--client/src/app/shared/shared-moderation/report-modals/comment-report.component.ts94
-rw-r--r--client/src/app/shared/shared-moderation/report-modals/index.ts3
-rw-r--r--client/src/app/shared/shared-moderation/report-modals/report.component.html62
-rw-r--r--client/src/app/shared/shared-moderation/report-modals/report.component.scss (renamed from client/src/app/shared/shared-moderation/video-report.component.scss)0
-rw-r--r--client/src/app/shared/shared-moderation/report-modals/video-report.component.html (renamed from client/src/app/shared/shared-moderation/video-report.component.html)9
-rw-r--r--client/src/app/shared/shared-moderation/report-modals/video-report.component.ts122
-rw-r--r--client/src/app/shared/shared-moderation/shared-moderation.module.ts18
-rw-r--r--client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts7
-rw-r--r--client/src/app/shared/shared-moderation/video-abuse.service.ts98
-rw-r--r--client/src/app/shared/shared-moderation/video-report.component.ts161
13 files changed, 557 insertions, 270 deletions
diff --git a/client/src/app/shared/shared-moderation/abuse.service.ts b/client/src/app/shared/shared-moderation/abuse.service.ts
new file mode 100644
index 000000000..95ac16955
--- /dev/null
+++ b/client/src/app/shared/shared-moderation/abuse.service.ts
@@ -0,0 +1,154 @@
1import { omit } from 'lodash-es'
2import { SortMeta } from 'primeng/api'
3import { Observable } from 'rxjs'
4import { catchError, map } from 'rxjs/operators'
5import { HttpClient, HttpParams } from '@angular/common/http'
6import { Injectable } from '@angular/core'
7import { RestExtractor, RestPagination, RestService } from '@app/core'
8import { Abuse, AbuseCreate, AbuseFilter, AbusePredefinedReasonsString, AbuseState, AbuseUpdate, ResultList } from '@shared/models'
9import { environment } from '../../../environments/environment'
10import { I18n } from '@ngx-translate/i18n-polyfill'
11
12@Injectable()
13export class AbuseService {
14 private static BASE_ABUSE_URL = environment.apiUrl + '/api/v1/abuses'
15
16 constructor (
17 private i18n: I18n,
18 private authHttp: HttpClient,
19 private restService: RestService,
20 private restExtractor: RestExtractor
21 ) { }
22
23 getAbuses (options: {
24 pagination: RestPagination,
25 sort: SortMeta,
26 search?: string
27 }): Observable<ResultList<Abuse>> {
28 const { pagination, sort, search } = options
29 const url = AbuseService.BASE_ABUSE_URL
30
31 let params = new HttpParams()
32 params = this.restService.addRestGetParams(params, pagination, sort)
33
34 if (search) {
35 const filters = this.restService.parseQueryStringFilter(search, {
36 id: { prefix: '#' },
37 state: {
38 prefix: 'state:',
39 handler: v => {
40 if (v === 'accepted') return AbuseState.ACCEPTED
41 if (v === 'pending') return AbuseState.PENDING
42 if (v === 'rejected') return AbuseState.REJECTED
43
44 return undefined
45 }
46 },
47 videoIs: {
48 prefix: 'videoIs:',
49 handler: v => {
50 if (v === 'deleted') return v
51 if (v === 'blacklisted') return v
52
53 return undefined
54 }
55 },
56 searchReporter: { prefix: 'reporter:' },
57 searchReportee: { prefix: 'reportee:' },
58 predefinedReason: { prefix: 'tag:' }
59 })
60
61 params = this.restService.addObjectParams(params, filters)
62 }
63
64 return this.authHttp.get<ResultList<Abuse>>(url, { params })
65 .pipe(
66 catchError(res => this.restExtractor.handleError(res))
67 )
68 }
69
70 reportVideo (parameters: AbuseCreate) {
71 const url = AbuseService.BASE_ABUSE_URL
72
73 const body = omit(parameters, ['id'])
74
75 return this.authHttp.post(url, body)
76 .pipe(
77 map(this.restExtractor.extractDataBool),
78 catchError(res => this.restExtractor.handleError(res))
79 )
80 }
81
82 updateAbuse (abuse: Abuse, abuseUpdate: AbuseUpdate) {
83 const url = AbuseService.BASE_ABUSE_URL + '/' + abuse.id
84
85 return this.authHttp.put(url, abuseUpdate)
86 .pipe(
87 map(this.restExtractor.extractDataBool),
88 catchError(res => this.restExtractor.handleError(res))
89 )
90 }
91
92 removeAbuse (abuse: Abuse) {
93 const url = AbuseService.BASE_ABUSE_URL + '/' + abuse.id
94
95 return this.authHttp.delete(url)
96 .pipe(
97 map(this.restExtractor.extractDataBool),
98 catchError(res => this.restExtractor.handleError(res))
99 )
100 }
101
102 getPrefefinedReasons (type: AbuseFilter) {
103 let reasons: { id: AbusePredefinedReasonsString, label: string, description?: string, help?: string }[] = [
104 {
105 id: 'violentOrRepulsive',
106 label: this.i18n('Violent or repulsive'),
107 help: this.i18n('Contains offensive, violent, or coarse language or iconography.')
108 },
109 {
110 id: 'hatefulOrAbusive',
111 label: this.i18n('Hateful or abusive'),
112 help: this.i18n('Contains abusive, racist or sexist language or iconography.')
113 },
114 {
115 id: 'spamOrMisleading',
116 label: this.i18n('Spam, ad or false news'),
117 help: this.i18n('Contains marketing, spam, purposefully deceitful news, or otherwise misleading thumbnail/text/tags. Please provide reputable sources to report hoaxes.')
118 },
119 {
120 id: 'privacy',
121 label: this.i18n('Privacy breach or doxxing'),
122 help: this.i18n('Contains personal information that could be used to track, identify, contact or impersonate someone (e.g. name, address, phone number, email, or credit card details).')
123 },
124 {
125 id: 'rights',
126 label: this.i18n('Intellectual property violation'),
127 help: this.i18n('Infringes my intellectual property or copyright, wrt. the regional rules with which the server must comply.')
128 },
129 {
130 id: 'serverRules',
131 label: this.i18n('Breaks server rules'),
132 description: this.i18n('Anything not included in the above that breaks the terms of service, code of conduct, or general rules in place on the server.')
133 }
134 ]
135
136 if (type === 'video') {
137 reasons = reasons.concat([
138 {
139 id: 'thumbnails',
140 label: this.i18n('Thumbnails'),
141 help: this.i18n('The above can only be seen in thumbnails.')
142 },
143 {
144 id: 'captions',
145 label: this.i18n('Captions'),
146 help: this.i18n('The above can only be seen in captions (please describe which).')
147 }
148 ])
149 }
150
151 return reasons
152 }
153
154}
diff --git a/client/src/app/shared/shared-moderation/index.ts b/client/src/app/shared/shared-moderation/index.ts
index 8e74254f6..41c910ffe 100644
--- a/client/src/app/shared/shared-moderation/index.ts
+++ b/client/src/app/shared/shared-moderation/index.ts
@@ -1,3 +1,6 @@
1export * from './report-modals'
2
3export * from './abuse.service'
1export * from './account-block.model' 4export * from './account-block.model'
2export * from './account-blocklist.component' 5export * from './account-blocklist.component'
3export * from './batch-domains-modal.component' 6export * from './batch-domains-modal.component'
@@ -6,8 +9,6 @@ export * from './bulk.service'
6export * from './server-blocklist.component' 9export * from './server-blocklist.component'
7export * from './user-ban-modal.component' 10export * from './user-ban-modal.component'
8export * from './user-moderation-dropdown.component' 11export * from './user-moderation-dropdown.component'
9export * from './video-abuse.service'
10export * from './video-block.component' 12export * from './video-block.component'
11export * from './video-block.service' 13export * from './video-block.service'
12export * from './video-report.component'
13export * from './shared-moderation.module' 14export * from './shared-moderation.module'
diff --git a/client/src/app/shared/shared-moderation/report-modals/account-report.component.ts b/client/src/app/shared/shared-moderation/report-modals/account-report.component.ts
new file mode 100644
index 000000000..78ca934c7
--- /dev/null
+++ b/client/src/app/shared/shared-moderation/report-modals/account-report.component.ts
@@ -0,0 +1,94 @@
1import { mapValues, pickBy } from 'lodash-es'
2import { Component, Input, OnInit, ViewChild } from '@angular/core'
3import { Notifier } from '@app/core'
4import { AbuseValidatorsService, FormReactive, FormValidatorService } from '@app/shared/shared-forms'
5import { Account } from '@app/shared/shared-main'
6import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
7import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
8import { I18n } from '@ngx-translate/i18n-polyfill'
9import { abusePredefinedReasonsMap, AbusePredefinedReasonsString } from '@shared/models'
10import { AbuseService } from '../abuse.service'
11
12@Component({
13 selector: 'my-account-report',
14 templateUrl: './report.component.html',
15 styleUrls: [ './report.component.scss' ]
16})
17export class AccountReportComponent extends FormReactive implements OnInit {
18 @Input() account: Account = null
19
20 @ViewChild('modal', { static: true }) modal: NgbModal
21
22 error: string = null
23 predefinedReasons: { id: AbusePredefinedReasonsString, label: string, description?: string, help?: string }[] = []
24 modalTitle: string
25
26 private openedModal: NgbModalRef
27
28 constructor (
29 protected formValidatorService: FormValidatorService,
30 private modalService: NgbModal,
31 private abuseValidatorsService: AbuseValidatorsService,
32 private abuseService: AbuseService,
33 private notifier: Notifier,
34 private i18n: I18n
35 ) {
36 super()
37 }
38
39 get currentHost () {
40 return window.location.host
41 }
42
43 get originHost () {
44 if (this.isRemote()) {
45 return this.account.host
46 }
47
48 return ''
49 }
50
51 ngOnInit () {
52 this.modalTitle = this.i18n('Report {{displayName}}', { displayName: this.account.displayName })
53
54 this.buildForm({
55 reason: this.abuseValidatorsService.ABUSE_REASON,
56 predefinedReasons: mapValues(abusePredefinedReasonsMap, r => null)
57 })
58
59 this.predefinedReasons = this.abuseService.getPrefefinedReasons('account')
60 }
61
62 show () {
63 this.openedModal = this.modalService.open(this.modal, { centered: true, keyboard: false, size: 'lg' })
64 }
65
66 hide () {
67 this.openedModal.close()
68 this.openedModal = null
69 }
70
71 report () {
72 const reason = this.form.get('reason').value
73 const predefinedReasons = Object.keys(pickBy(this.form.get('predefinedReasons').value)) as AbusePredefinedReasonsString[]
74
75 this.abuseService.reportVideo({
76 reason,
77 predefinedReasons,
78 account: {
79 id: this.account.id
80 }
81 }).subscribe(
82 () => {
83 this.notifier.success(this.i18n('Account reported.'))
84 this.hide()
85 },
86
87 err => this.notifier.error(err.message)
88 )
89 }
90
91 isRemote () {
92 return !this.account.isLocal
93 }
94}
diff --git a/client/src/app/shared/shared-moderation/report-modals/comment-report.component.ts b/client/src/app/shared/shared-moderation/report-modals/comment-report.component.ts
new file mode 100644
index 000000000..00d7b8d34
--- /dev/null
+++ b/client/src/app/shared/shared-moderation/report-modals/comment-report.component.ts
@@ -0,0 +1,94 @@
1import { mapValues, pickBy } from 'lodash-es'
2import { Component, Input, OnInit, ViewChild } from '@angular/core'
3import { Notifier } from '@app/core'
4import { AbuseValidatorsService, FormReactive, FormValidatorService } from '@app/shared/shared-forms'
5import { VideoComment } from '@app/shared/shared-video-comment'
6import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
7import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
8import { I18n } from '@ngx-translate/i18n-polyfill'
9import { abusePredefinedReasonsMap, AbusePredefinedReasonsString } from '@shared/models'
10import { AbuseService } from '../abuse.service'
11
12@Component({
13 selector: 'my-comment-report',
14 templateUrl: './report.component.html',
15 styleUrls: [ './report.component.scss' ]
16})
17export class CommentReportComponent extends FormReactive implements OnInit {
18 @Input() comment: VideoComment = null
19
20 @ViewChild('modal', { static: true }) modal: NgbModal
21
22 modalTitle: string
23 error: string = null
24 predefinedReasons: { id: AbusePredefinedReasonsString, label: string, description?: string, help?: string }[] = []
25
26 private openedModal: NgbModalRef
27
28 constructor (
29 protected formValidatorService: FormValidatorService,
30 private modalService: NgbModal,
31 private abuseValidatorsService: AbuseValidatorsService,
32 private abuseService: AbuseService,
33 private notifier: Notifier,
34 private i18n: I18n
35 ) {
36 super()
37 }
38
39 get currentHost () {
40 return window.location.host
41 }
42
43 get originHost () {
44 if (this.isRemote()) {
45 return this.comment.account.host
46 }
47
48 return ''
49 }
50
51 ngOnInit () {
52 this.modalTitle = this.i18n('Report comment')
53
54 this.buildForm({
55 reason: this.abuseValidatorsService.ABUSE_REASON,
56 predefinedReasons: mapValues(abusePredefinedReasonsMap, r => null)
57 })
58
59 this.predefinedReasons = this.abuseService.getPrefefinedReasons('comment')
60 }
61
62 show () {
63 this.openedModal = this.modalService.open(this.modal, { centered: true, keyboard: false, size: 'lg' })
64 }
65
66 hide () {
67 this.openedModal.close()
68 this.openedModal = null
69 }
70
71 report () {
72 const reason = this.form.get('reason').value
73 const predefinedReasons = Object.keys(pickBy(this.form.get('predefinedReasons').value)) as AbusePredefinedReasonsString[]
74
75 this.abuseService.reportVideo({
76 reason,
77 predefinedReasons,
78 comment: {
79 id: this.comment.id
80 }
81 }).subscribe(
82 () => {
83 this.notifier.success(this.i18n('Comment reported.'))
84 this.hide()
85 },
86
87 err => this.notifier.error(err.message)
88 )
89 }
90
91 isRemote () {
92 return !this.comment.isLocal
93 }
94}
diff --git a/client/src/app/shared/shared-moderation/report-modals/index.ts b/client/src/app/shared/shared-moderation/report-modals/index.ts
new file mode 100644
index 000000000..f3c4058ae
--- /dev/null
+++ b/client/src/app/shared/shared-moderation/report-modals/index.ts
@@ -0,0 +1,3 @@
1export * from './account-report.component'
2export * from './comment-report.component'
3export * from './video-report.component'
diff --git a/client/src/app/shared/shared-moderation/report-modals/report.component.html b/client/src/app/shared/shared-moderation/report-modals/report.component.html
new file mode 100644
index 000000000..bda62312f
--- /dev/null
+++ b/client/src/app/shared/shared-moderation/report-modals/report.component.html
@@ -0,0 +1,62 @@
1<ng-template #modal>
2 <div class="modal-header">
3 <h4 class="modal-title">{{ modalTitle }}</h4>
4 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
5 </div>
6
7 <div class="modal-body">
8 <form novalidate [formGroup]="form" (ngSubmit)="report()">
9
10 <div class="row">
11 <div class="col-5 form-group">
12
13 <label i18n for="reportPredefinedReasons">What is the issue?</label>
14
15 <div class="ml-2 mt-2 d-flex flex-column">
16 <ng-container formGroupName="predefinedReasons">
17
18 <div class="form-group" *ngFor="let reason of predefinedReasons">
19 <my-peertube-checkbox [inputName]="reason.id" [formControlName]="reason.id" [labelText]="reason.label">
20 <ng-template *ngIf="reason.help" ptTemplate="help">
21 <div [innerHTML]="reason.help"></div>
22 </ng-template>
23
24 <ng-container *ngIf="reason.description" ngProjectAs="description">
25 <div [innerHTML]="reason.description"></div>
26 </ng-container>
27 </my-peertube-checkbox>
28 </div>
29
30 </ng-container>
31 </div>
32
33 </div>
34
35 <div class="col-7">
36 <div i18n class="information">
37 Your report will be sent to moderators of {{ currentHost }}<ng-container *ngIf="isRemote()"> and will be forwarded to the comment origin ({{ originHost }}) too</ng-container>.
38 </div>
39
40 <div class="form-group">
41 <textarea
42 i18n-placeholder placeholder="Please describe the issue..." formControlName="reason" ngbAutofocus
43 [ngClass]="{ 'input-error': formErrors['reason'] }" class="form-control"
44 ></textarea>
45 <div *ngIf="formErrors.reason" class="form-error">
46 {{ formErrors.reason }}
47 </div>
48 </div>
49 </div>
50 </div>
51
52 <div class="form-group inputs">
53 <input
54 type="button" role="button" i18n-value value="Cancel" class="action-button action-button-cancel"
55 (click)="hide()" (key.enter)="hide()"
56 >
57 <input type="submit" i18n-value value="Submit" class="action-button-submit" [disabled]="!form.valid">
58 </div>
59
60 </form>
61 </div>
62</ng-template>
diff --git a/client/src/app/shared/shared-moderation/video-report.component.scss b/client/src/app/shared/shared-moderation/report-modals/report.component.scss
index b2606cbd8..b2606cbd8 100644
--- a/client/src/app/shared/shared-moderation/video-report.component.scss
+++ b/client/src/app/shared/shared-moderation/report-modals/report.component.scss
diff --git a/client/src/app/shared/shared-moderation/video-report.component.html b/client/src/app/shared/shared-moderation/report-modals/video-report.component.html
index d6beb6d2a..4947088d1 100644
--- a/client/src/app/shared/shared-moderation/video-report.component.html
+++ b/client/src/app/shared/shared-moderation/report-modals/video-report.component.html
@@ -14,16 +14,19 @@
14 14
15 <div class="ml-2 mt-2 d-flex flex-column"> 15 <div class="ml-2 mt-2 d-flex flex-column">
16 <ng-container formGroupName="predefinedReasons"> 16 <ng-container formGroupName="predefinedReasons">
17
17 <div class="form-group" *ngFor="let reason of predefinedReasons"> 18 <div class="form-group" *ngFor="let reason of predefinedReasons">
18 <my-peertube-checkbox formControlName="{{reason.id}}" labelText="{{reason.label}}"> 19 <my-peertube-checkbox [inputName]="reason.id" [formControlName]="reason.id" [labelText]="reason.label">
19 <ng-template *ngIf="reason.help" ptTemplate="help"> 20 <ng-template *ngIf="reason.help" ptTemplate="help">
20 <div [innerHTML]="reason.help"></div> 21 <div [innerHTML]="reason.help"></div>
21 </ng-template> 22 </ng-template>
23
22 <ng-container *ngIf="reason.description" ngProjectAs="description"> 24 <ng-container *ngIf="reason.description" ngProjectAs="description">
23 <div [innerHTML]="reason.description"></div> 25 <div [innerHTML]="reason.description"></div>
24 </ng-container> 26 </ng-container>
25 </my-peertube-checkbox> 27 </my-peertube-checkbox>
26 </div> 28 </div>
29
27 </ng-container> 30 </ng-container>
28 </div> 31 </div>
29 32
@@ -69,11 +72,11 @@
69 </div> 72 </div>
70 73
71 <div i18n class="information"> 74 <div i18n class="information">
72 Your report will be sent to moderators of {{ currentHost }}<ng-container *ngIf="isRemoteVideo()"> and will be forwarded to the video origin ({{ originHost }}) too</ng-container>. 75 Your report will be sent to moderators of {{ currentHost }}<ng-container *ngIf="isRemote()"> and will be forwarded to the video origin ({{ originHost }}) too</ng-container>.
73 </div> 76 </div>
74 77
75 <div class="form-group"> 78 <div class="form-group">
76 <textarea 79 <textarea
77 i18n-placeholder placeholder="Please describe the issue..." formControlName="reason" ngbAutofocus 80 i18n-placeholder placeholder="Please describe the issue..." formControlName="reason" ngbAutofocus
78 [ngClass]="{ 'input-error': formErrors['reason'] }" class="form-control" 81 [ngClass]="{ 'input-error': formErrors['reason'] }" class="form-control"
79 ></textarea> 82 ></textarea>
diff --git a/client/src/app/shared/shared-moderation/report-modals/video-report.component.ts b/client/src/app/shared/shared-moderation/report-modals/video-report.component.ts
new file mode 100644
index 000000000..7d53ea3c9
--- /dev/null
+++ b/client/src/app/shared/shared-moderation/report-modals/video-report.component.ts
@@ -0,0 +1,122 @@
1import { mapValues, pickBy } from 'lodash-es'
2import { buildVideoEmbed, buildVideoLink } from 'src/assets/player/utils'
3import { Component, Input, OnInit, ViewChild } from '@angular/core'
4import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
5import { Notifier } from '@app/core'
6import { AbuseValidatorsService, FormReactive, FormValidatorService } from '@app/shared/shared-forms'
7import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
8import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
9import { I18n } from '@ngx-translate/i18n-polyfill'
10import { abusePredefinedReasonsMap, AbusePredefinedReasonsString } from '@shared/models'
11import { Video } from '../../shared-main'
12import { AbuseService } from '../abuse.service'
13
14@Component({
15 selector: 'my-video-report',
16 templateUrl: './video-report.component.html',
17 styleUrls: [ './report.component.scss' ]
18})
19export class VideoReportComponent extends FormReactive implements OnInit {
20 @Input() video: Video = null
21
22 @ViewChild('modal', { static: true }) modal: NgbModal
23
24 error: string = null
25 predefinedReasons: { id: AbusePredefinedReasonsString, label: string, description?: string, help?: string }[] = []
26 embedHtml: SafeHtml
27
28 private openedModal: NgbModalRef
29
30 constructor (
31 protected formValidatorService: FormValidatorService,
32 private modalService: NgbModal,
33 private abuseValidatorsService: AbuseValidatorsService,
34 private abuseService: AbuseService,
35 private notifier: Notifier,
36 private sanitizer: DomSanitizer,
37 private i18n: I18n
38 ) {
39 super()
40 }
41
42 get currentHost () {
43 return window.location.host
44 }
45
46 get originHost () {
47 if (this.isRemote()) {
48 return this.video.account.host
49 }
50
51 return ''
52 }
53
54 get timestamp () {
55 return this.form.get('timestamp').value
56 }
57
58 getVideoEmbed () {
59 return this.sanitizer.bypassSecurityTrustHtml(
60 buildVideoEmbed(
61 buildVideoLink({
62 baseUrl: this.video.embedUrl,
63 title: false,
64 warningTitle: false
65 })
66 )
67 )
68 }
69
70 ngOnInit () {
71 this.buildForm({
72 reason: this.abuseValidatorsService.ABUSE_REASON,
73 predefinedReasons: mapValues(abusePredefinedReasonsMap, r => null),
74 timestamp: {
75 hasStart: null,
76 startAt: null,
77 hasEnd: null,
78 endAt: null
79 }
80 })
81
82 this.predefinedReasons = this.abuseService.getPrefefinedReasons('video')
83
84 this.embedHtml = this.getVideoEmbed()
85 }
86
87 show () {
88 this.openedModal = this.modalService.open(this.modal, { centered: true, keyboard: false, size: 'lg' })
89 }
90
91 hide () {
92 this.openedModal.close()
93 this.openedModal = null
94 }
95
96 report () {
97 const reason = this.form.get('reason').value
98 const predefinedReasons = Object.keys(pickBy(this.form.get('predefinedReasons').value)) as AbusePredefinedReasonsString[]
99 const { hasStart, startAt, hasEnd, endAt } = this.form.get('timestamp').value
100
101 this.abuseService.reportVideo({
102 reason,
103 predefinedReasons,
104 video: {
105 id: this.video.id,
106 startAt: hasStart && startAt ? startAt : undefined,
107 endAt: hasEnd && endAt ? endAt : undefined
108 }
109 }).subscribe(
110 () => {
111 this.notifier.success(this.i18n('Video reported.'))
112 this.hide()
113 },
114
115 err => this.notifier.error(err.message)
116 )
117 }
118
119 isRemote () {
120 return !this.video.isLocal
121 }
122}
diff --git a/client/src/app/shared/shared-moderation/shared-moderation.module.ts b/client/src/app/shared/shared-moderation/shared-moderation.module.ts
index f7e64dfa3..8fa9ee794 100644
--- a/client/src/app/shared/shared-moderation/shared-moderation.module.ts
+++ b/client/src/app/shared/shared-moderation/shared-moderation.module.ts
@@ -3,21 +3,23 @@ import { NgModule } from '@angular/core'
3import { SharedFormModule } from '../shared-forms/shared-form.module' 3import { SharedFormModule } from '../shared-forms/shared-form.module'
4import { SharedGlobalIconModule } from '../shared-icons' 4import { SharedGlobalIconModule } from '../shared-icons'
5import { SharedMainModule } from '../shared-main/shared-main.module' 5import { SharedMainModule } from '../shared-main/shared-main.module'
6import { SharedVideoCommentModule } from '../shared-video-comment'
7import { AbuseService } from './abuse.service'
6import { BatchDomainsModalComponent } from './batch-domains-modal.component' 8import { BatchDomainsModalComponent } from './batch-domains-modal.component'
7import { BlocklistService } from './blocklist.service' 9import { BlocklistService } from './blocklist.service'
8import { BulkService } from './bulk.service' 10import { BulkService } from './bulk.service'
9import { UserBanModalComponent } from './user-ban-modal.component' 11import { UserBanModalComponent } from './user-ban-modal.component'
10import { UserModerationDropdownComponent } from './user-moderation-dropdown.component' 12import { UserModerationDropdownComponent } from './user-moderation-dropdown.component'
11import { VideoAbuseService } from './video-abuse.service'
12import { VideoBlockComponent } from './video-block.component' 13import { VideoBlockComponent } from './video-block.component'
13import { VideoBlockService } from './video-block.service' 14import { VideoBlockService } from './video-block.service'
14import { VideoReportComponent } from './video-report.component' 15import { VideoReportComponent, AccountReportComponent, CommentReportComponent } from './report-modals'
15 16
16@NgModule({ 17@NgModule({
17 imports: [ 18 imports: [
18 SharedMainModule, 19 SharedMainModule,
19 SharedFormModule, 20 SharedFormModule,
20 SharedGlobalIconModule 21 SharedGlobalIconModule,
22 SharedVideoCommentModule
21 ], 23 ],
22 24
23 declarations: [ 25 declarations: [
@@ -25,7 +27,9 @@ import { VideoReportComponent } from './video-report.component'
25 UserModerationDropdownComponent, 27 UserModerationDropdownComponent,
26 VideoBlockComponent, 28 VideoBlockComponent,
27 VideoReportComponent, 29 VideoReportComponent,
28 BatchDomainsModalComponent 30 BatchDomainsModalComponent,
31 CommentReportComponent,
32 AccountReportComponent
29 ], 33 ],
30 34
31 exports: [ 35 exports: [
@@ -33,13 +37,15 @@ import { VideoReportComponent } from './video-report.component'
33 UserModerationDropdownComponent, 37 UserModerationDropdownComponent,
34 VideoBlockComponent, 38 VideoBlockComponent,
35 VideoReportComponent, 39 VideoReportComponent,
36 BatchDomainsModalComponent 40 BatchDomainsModalComponent,
41 CommentReportComponent,
42 AccountReportComponent
37 ], 43 ],
38 44
39 providers: [ 45 providers: [
40 BlocklistService, 46 BlocklistService,
41 BulkService, 47 BulkService,
42 VideoAbuseService, 48 AbuseService,
43 VideoBlockService 49 VideoBlockService
44 ] 50 ]
45}) 51})
diff --git a/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts b/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts
index d3c37f082..78c2658df 100644
--- a/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts
+++ b/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts
@@ -16,6 +16,7 @@ export class UserModerationDropdownComponent implements OnInit, OnChanges {
16 16
17 @Input() user: User 17 @Input() user: User
18 @Input() account: Account 18 @Input() account: Account
19 @Input() prependActions: DropdownAction<{ user: User, account: Account }>[]
19 20
20 @Input() buttonSize: 'normal' | 'small' = 'normal' 21 @Input() buttonSize: 'normal' | 'small' = 'normal'
21 @Input() placement = 'left-top left-bottom auto' 22 @Input() placement = 'left-top left-bottom auto'
@@ -250,6 +251,12 @@ export class UserModerationDropdownComponent implements OnInit, OnChanges {
250 private buildActions () { 251 private buildActions () {
251 this.userActions = [] 252 this.userActions = []
252 253
254 if (this.prependActions) {
255 this.userActions = [
256 this.prependActions
257 ]
258 }
259
253 if (this.authService.isLoggedIn()) { 260 if (this.authService.isLoggedIn()) {
254 const authUser = this.authService.getUser() 261 const authUser = this.authService.getUser()
255 262
diff --git a/client/src/app/shared/shared-moderation/video-abuse.service.ts b/client/src/app/shared/shared-moderation/video-abuse.service.ts
deleted file mode 100644
index 44dea44a5..000000000
--- a/client/src/app/shared/shared-moderation/video-abuse.service.ts
+++ /dev/null
@@ -1,98 +0,0 @@
1import { omit } from 'lodash-es'
2import { SortMeta } from 'primeng/api'
3import { Observable } from 'rxjs'
4import { catchError, map } from 'rxjs/operators'
5import { HttpClient, HttpParams } from '@angular/common/http'
6import { Injectable } from '@angular/core'
7import { RestExtractor, RestPagination, RestService } from '@app/core'
8import { ResultList, VideoAbuse, VideoAbuseCreate, VideoAbuseState, VideoAbuseUpdate } from '@shared/models'
9import { environment } from '../../../environments/environment'
10
11@Injectable()
12export class VideoAbuseService {
13 private static BASE_VIDEO_ABUSE_URL = environment.apiUrl + '/api/v1/videos/'
14
15 constructor (
16 private authHttp: HttpClient,
17 private restService: RestService,
18 private restExtractor: RestExtractor
19 ) {}
20
21 getVideoAbuses (options: {
22 pagination: RestPagination,
23 sort: SortMeta,
24 search?: string
25 }): Observable<ResultList<VideoAbuse>> {
26 const { pagination, sort, search } = options
27 const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + 'abuse'
28
29 let params = new HttpParams()
30 params = this.restService.addRestGetParams(params, pagination, sort)
31
32 if (search) {
33 const filters = this.restService.parseQueryStringFilter(search, {
34 id: { prefix: '#' },
35 state: {
36 prefix: 'state:',
37 handler: v => {
38 if (v === 'accepted') return VideoAbuseState.ACCEPTED
39 if (v === 'pending') return VideoAbuseState.PENDING
40 if (v === 'rejected') return VideoAbuseState.REJECTED
41
42 return undefined
43 }
44 },
45 videoIs: {
46 prefix: 'videoIs:',
47 handler: v => {
48 if (v === 'deleted') return v
49 if (v === 'blacklisted') return v
50
51 return undefined
52 }
53 },
54 searchReporter: { prefix: 'reporter:' },
55 searchReportee: { prefix: 'reportee:' },
56 predefinedReason: { prefix: 'tag:' }
57 })
58
59 params = this.restService.addObjectParams(params, filters)
60 }
61
62 return this.authHttp.get<ResultList<VideoAbuse>>(url, { params })
63 .pipe(
64 catchError(res => this.restExtractor.handleError(res))
65 )
66 }
67
68 reportVideo (parameters: { id: number } & VideoAbuseCreate) {
69 const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + parameters.id + '/abuse'
70
71 const body = omit(parameters, [ 'id' ])
72
73 return this.authHttp.post(url, body)
74 .pipe(
75 map(this.restExtractor.extractDataBool),
76 catchError(res => this.restExtractor.handleError(res))
77 )
78 }
79
80 updateVideoAbuse (videoAbuse: VideoAbuse, abuseUpdate: VideoAbuseUpdate) {
81 const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + videoAbuse.video.uuid + '/abuse/' + videoAbuse.id
82
83 return this.authHttp.put(url, abuseUpdate)
84 .pipe(
85 map(this.restExtractor.extractDataBool),
86 catchError(res => this.restExtractor.handleError(res))
87 )
88 }
89
90 removeVideoAbuse (videoAbuse: VideoAbuse) {
91 const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + videoAbuse.video.uuid + '/abuse/' + videoAbuse.id
92
93 return this.authHttp.delete(url)
94 .pipe(
95 map(this.restExtractor.extractDataBool),
96 catchError(res => this.restExtractor.handleError(res))
97 )
98 }}
diff --git a/client/src/app/shared/shared-moderation/video-report.component.ts b/client/src/app/shared/shared-moderation/video-report.component.ts
deleted file mode 100644
index 11c805636..000000000
--- a/client/src/app/shared/shared-moderation/video-report.component.ts
+++ /dev/null
@@ -1,161 +0,0 @@
1import { mapValues, pickBy } from 'lodash-es'
2import { buildVideoEmbed, buildVideoLink } from 'src/assets/player/utils'
3import { Component, Input, OnInit, ViewChild } from '@angular/core'
4import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
5import { Notifier } from '@app/core'
6import { FormReactive, FormValidatorService, VideoAbuseValidatorsService } from '@app/shared/shared-forms'
7import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
8import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
9import { I18n } from '@ngx-translate/i18n-polyfill'
10import { videoAbusePredefinedReasonsMap, VideoAbusePredefinedReasonsString } from '@shared/models/videos/abuse/video-abuse-reason.model'
11import { Video } from '../shared-main'
12import { VideoAbuseService } from './video-abuse.service'
13
14@Component({
15 selector: 'my-video-report',
16 templateUrl: './video-report.component.html',
17 styleUrls: [ './video-report.component.scss' ]
18})
19export class VideoReportComponent extends FormReactive implements OnInit {
20 @Input() video: Video = null
21
22 @ViewChild('modal', { static: true }) modal: NgbModal
23
24 error: string = null
25 predefinedReasons: { id: VideoAbusePredefinedReasonsString, label: string, description?: string, help?: string }[] = []
26 embedHtml: SafeHtml
27
28 private openedModal: NgbModalRef
29
30 constructor (
31 protected formValidatorService: FormValidatorService,
32 private modalService: NgbModal,
33 private videoAbuseValidatorsService: VideoAbuseValidatorsService,
34 private videoAbuseService: VideoAbuseService,
35 private notifier: Notifier,
36 private sanitizer: DomSanitizer,
37 private i18n: I18n
38 ) {
39 super()
40 }
41
42 get currentHost () {
43 return window.location.host
44 }
45
46 get originHost () {
47 if (this.isRemoteVideo()) {
48 return this.video.account.host
49 }
50
51 return ''
52 }
53
54 get timestamp () {
55 return this.form.get('timestamp').value
56 }
57
58 getVideoEmbed () {
59 return this.sanitizer.bypassSecurityTrustHtml(
60 buildVideoEmbed(
61 buildVideoLink({
62 baseUrl: this.video.embedUrl,
63 title: false,
64 warningTitle: false
65 })
66 )
67 )
68 }
69
70 ngOnInit () {
71 this.buildForm({
72 reason: this.videoAbuseValidatorsService.VIDEO_ABUSE_REASON,
73 predefinedReasons: mapValues(videoAbusePredefinedReasonsMap, r => null),
74 timestamp: {
75 hasStart: null,
76 startAt: null,
77 hasEnd: null,
78 endAt: null
79 }
80 })
81
82 this.predefinedReasons = [
83 {
84 id: 'violentOrRepulsive',
85 label: this.i18n('Violent or repulsive'),
86 help: this.i18n('Contains offensive, violent, or coarse language or iconography.')
87 },
88 {
89 id: 'hatefulOrAbusive',
90 label: this.i18n('Hateful or abusive'),
91 help: this.i18n('Contains abusive, racist or sexist language or iconography.')
92 },
93 {
94 id: 'spamOrMisleading',
95 label: this.i18n('Spam, ad or false news'),
96 help: this.i18n('Contains marketing, spam, purposefully deceitful news, or otherwise misleading thumbnail/text/tags. Please provide reputable sources to report hoaxes.')
97 },
98 {
99 id: 'privacy',
100 label: this.i18n('Privacy breach or doxxing'),
101 help: this.i18n('Contains personal information that could be used to track, identify, contact or impersonate someone (e.g. name, address, phone number, email, or credit card details).')
102 },
103 {
104 id: 'rights',
105 label: this.i18n('Intellectual property violation'),
106 help: this.i18n('Infringes my intellectual property or copyright, wrt. the regional rules with which the server must comply.')
107 },
108 {
109 id: 'serverRules',
110 label: this.i18n('Breaks server rules'),
111 description: this.i18n('Anything not included in the above that breaks the terms of service, code of conduct, or general rules in place on the server.')
112 },
113 {
114 id: 'thumbnails',
115 label: this.i18n('Thumbnails'),
116 help: this.i18n('The above can only be seen in thumbnails.')
117 },
118 {
119 id: 'captions',
120 label: this.i18n('Captions'),
121 help: this.i18n('The above can only be seen in captions (please describe which).')
122 }
123 ]
124
125 this.embedHtml = this.getVideoEmbed()
126 }
127
128 show () {
129 this.openedModal = this.modalService.open(this.modal, { centered: true, keyboard: false, size: 'lg' })
130 }
131
132 hide () {
133 this.openedModal.close()
134 this.openedModal = null
135 }
136
137 report () {
138 const reason = this.form.get('reason').value
139 const predefinedReasons = Object.keys(pickBy(this.form.get('predefinedReasons').value)) as VideoAbusePredefinedReasonsString[]
140 const { hasStart, startAt, hasEnd, endAt } = this.form.get('timestamp').value
141
142 this.videoAbuseService.reportVideo({
143 id: this.video.id,
144 reason,
145 predefinedReasons,
146 startAt: hasStart && startAt ? startAt : undefined,
147 endAt: hasEnd && endAt ? endAt : undefined
148 }).subscribe(
149 () => {
150 this.notifier.success(this.i18n('Video reported.'))
151 this.hide()
152 },
153
154 err => this.notifier.error(err.message)
155 )
156 }
157
158 isRemoteVideo () {
159 return !this.video.isLocal
160 }
161}