aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorRigel Kent <sendmemail@rigelk.eu>2020-04-14 10:55:34 +0200
committerRigel Kent <par@rigelk.eu>2020-04-14 15:53:37 +0200
commitbb152476c819e4c7487d080433c616f0d523e049 (patch)
tree955b6afa140b6e83eced3cb21c8ec1d62fa6a15e
parent1055eb3a7b89bc50dc611ab933f9dbd603488747 (diff)
downloadPeerTube-bb152476c819e4c7487d080433c616f0d523e049.tar.gz
PeerTube-bb152476c819e4c7487d080433c616f0d523e049.tar.zst
PeerTube-bb152476c819e4c7487d080433c616f0d523e049.zip
Refactor follow/mute as modals in admin, add actions in abuse list
-rw-r--r--client/src/app/+admin/admin.module.ts8
-rw-r--r--client/src/app/+admin/config/shared/batch-domains-modal.component.html43
-rw-r--r--client/src/app/+admin/config/shared/batch-domains-modal.component.scss3
-rw-r--r--client/src/app/+admin/config/shared/batch-domains-modal.component.ts54
-rw-r--r--client/src/app/+admin/config/shared/batch-domains-validators.service.ts68
-rw-r--r--client/src/app/+admin/follows/followers-list/followers-list.component.ts2
-rw-r--r--client/src/app/+admin/follows/following-add/following-add.component.html22
-rw-r--r--client/src/app/+admin/follows/following-add/following-add.component.scss11
-rw-r--r--client/src/app/+admin/follows/following-add/following-add.component.ts85
-rw-r--r--client/src/app/+admin/follows/following-add/index.ts1
-rw-r--r--client/src/app/+admin/follows/following-list/following-list.component.html8
-rw-r--r--client/src/app/+admin/follows/following-list/following-list.component.scss6
-rw-r--r--client/src/app/+admin/follows/following-list/following-list.component.ts22
-rw-r--r--client/src/app/+admin/follows/follows.component.html2
-rw-r--r--client/src/app/+admin/follows/follows.routes.ts8
-rw-r--r--client/src/app/+admin/follows/index.ts1
-rw-r--r--client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html16
-rw-r--r--client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.scss6
-rw-r--r--client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.ts26
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html4
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts84
-rw-r--r--client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.ts2
-rw-r--r--client/src/app/shared/buttons/action-dropdown.component.html16
-rw-r--r--client/src/app/shared/buttons/action-dropdown.component.scss4
-rw-r--r--client/src/app/shared/buttons/action-dropdown.component.ts1
-rw-r--r--client/src/app/shared/shared.module.ts2
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comment-add.component.ts2
-rw-r--r--server/middlewares/validators/blocklist.ts6
-rw-r--r--server/tests/api/check-params/blocklist.ts12
29 files changed, 341 insertions, 184 deletions
diff --git a/client/src/app/+admin/admin.module.ts b/client/src/app/+admin/admin.module.ts
index fdbe70314..16273f6d8 100644
--- a/client/src/app/+admin/admin.module.ts
+++ b/client/src/app/+admin/admin.module.ts
@@ -5,7 +5,7 @@ import { TableModule } from 'primeng/table'
5import { SharedModule } from '../shared' 5import { SharedModule } from '../shared'
6import { AdminRoutingModule } from './admin-routing.module' 6import { AdminRoutingModule } from './admin-routing.module'
7import { AdminComponent } from './admin.component' 7import { AdminComponent } from './admin.component'
8import { FollowersListComponent, FollowingAddComponent, FollowsComponent, VideoRedundanciesListComponent } from './follows' 8import { FollowersListComponent, FollowsComponent, VideoRedundanciesListComponent } from './follows'
9import { FollowingListComponent } from './follows/following-list/following-list.component' 9import { FollowingListComponent } from './follows/following-list/following-list.component'
10import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersComponent, UserUpdateComponent } from './users' 10import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersComponent, UserUpdateComponent } from './users'
11import { 11import {
@@ -28,6 +28,7 @@ import { SelectButtonModule } from 'primeng/selectbutton'
28import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service' 28import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service'
29import { VideoRedundancyInformationComponent } from '@app/+admin/follows/video-redundancies-list/video-redundancy-information.component' 29import { VideoRedundancyInformationComponent } from '@app/+admin/follows/video-redundancies-list/video-redundancy-information.component'
30import { ChartModule } from 'primeng/chart' 30import { ChartModule } from 'primeng/chart'
31import { BatchDomainsModalComponent } from './config/shared/batch-domains-modal.component'
31 32
32@NgModule({ 33@NgModule({
33 imports: [ 34 imports: [
@@ -44,7 +45,6 @@ import { ChartModule } from 'primeng/chart'
44 AdminComponent, 45 AdminComponent,
45 46
46 FollowsComponent, 47 FollowsComponent,
47 FollowingAddComponent,
48 FollowersListComponent, 48 FollowersListComponent,
49 FollowingListComponent, 49 FollowingListComponent,
50 RedundancyCheckboxComponent, 50 RedundancyCheckboxComponent,
@@ -76,7 +76,9 @@ import { ChartModule } from 'primeng/chart'
76 DebugComponent, 76 DebugComponent,
77 77
78 ConfigComponent, 78 ConfigComponent,
79 EditCustomConfigComponent 79 EditCustomConfigComponent,
80
81 BatchDomainsModalComponent
80 ], 82 ],
81 83
82 exports: [ 84 exports: [
diff --git a/client/src/app/+admin/config/shared/batch-domains-modal.component.html b/client/src/app/+admin/config/shared/batch-domains-modal.component.html
new file mode 100644
index 000000000..1b85c8f48
--- /dev/null
+++ b/client/src/app/+admin/config/shared/batch-domains-modal.component.html
@@ -0,0 +1,43 @@
1<ng-template #modal>
2 <div class="modal-header">
3 <h4 i18n class="modal-title">{{ action }}</h4>
4
5 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
6 </div>
7
8 <div class="modal-body">
9 <form novalidate [formGroup]="form" (ngSubmit)="submit()">
10 <div class="form-group">
11 <label i18n for="hosts">1 host (without "http://") per line</label>
12
13 <textarea
14 [placeholder]="placeholder" formControlName="domains" type="text" id="hosts" name="hosts"
15 class="form-control" [ngClass]="{ 'input-error': formErrors['domains'] }" ngbAutofocus
16 ></textarea>
17
18 <div *ngIf="formErrors.domains" class="form-error">
19 {{ formErrors.domains }}
20
21 <div *ngIf="form.controls['domains'].errors.validDomains">
22 {{ form.controls['domains'].errors.validDomains.value }}
23 </div>
24 </div>
25 </div>
26
27 <ng-content select="warning"></ng-content>
28
29 <div class="form-group inputs">
30 <input
31 type="button" role="button" i18n-value value="Cancel" class="action-button action-button-cancel"
32 (click)="hide()" (key.enter)="hide()"
33 >
34
35 <input
36 type="submit" [value]="action" class="action-button-submit"
37 [disabled]="!form.valid"
38 >
39 </div>
40 </form>
41 </div>
42
43</ng-template>
diff --git a/client/src/app/+admin/config/shared/batch-domains-modal.component.scss b/client/src/app/+admin/config/shared/batch-domains-modal.component.scss
new file mode 100644
index 000000000..9621a566f
--- /dev/null
+++ b/client/src/app/+admin/config/shared/batch-domains-modal.component.scss
@@ -0,0 +1,3 @@
1textarea {
2 height: 200px;
3}
diff --git a/client/src/app/+admin/config/shared/batch-domains-modal.component.ts b/client/src/app/+admin/config/shared/batch-domains-modal.component.ts
new file mode 100644
index 000000000..620f2726b
--- /dev/null
+++ b/client/src/app/+admin/config/shared/batch-domains-modal.component.ts
@@ -0,0 +1,54 @@
1import { Component, OnInit, ViewChild, Input, Output, EventEmitter } from '@angular/core'
2import { I18n } from '@ngx-translate/i18n-polyfill'
3import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
4import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
5import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
6import { FormReactive } from '@app/shared/forms'
7import { BatchDomainsValidatorsService } from './batch-domains-validators.service'
8
9@Component({
10 selector: 'my-batch-domains-modal',
11 templateUrl: './batch-domains-modal.component.html',
12 styleUrls: [ './batch-domains-modal.component.scss' ]
13})
14export class BatchDomainsModalComponent extends FormReactive implements OnInit {
15 @ViewChild('modal', { static: true }) modal: NgbModal
16 @Input() placeholder = 'example.com'
17 @Input() action: string
18 @Output() domains = new EventEmitter<string[]>()
19
20 private openedModal: NgbModalRef
21
22 constructor (
23 protected formValidatorService: FormValidatorService,
24 private modalService: NgbModal,
25 private batchDomainsValidatorsService: BatchDomainsValidatorsService,
26 private i18n: I18n
27 ) {
28 super()
29 }
30
31 ngOnInit () {
32 if (!this.action) this.action = this.i18n('Process domains')
33
34 this.buildForm({
35 domains: this.batchDomainsValidatorsService.DOMAINS
36 })
37 }
38
39 openModal () {
40 this.openedModal = this.modalService.open(this.modal, { centered: true })
41 }
42
43 hide () {
44 this.openedModal.close()
45 }
46
47 submit () {
48 this.domains.emit(
49 this.batchDomainsValidatorsService.getNotEmptyHosts(this.form.controls['domains'].value)
50 )
51 this.form.reset()
52 this.hide()
53 }
54}
diff --git a/client/src/app/+admin/config/shared/batch-domains-validators.service.ts b/client/src/app/+admin/config/shared/batch-domains-validators.service.ts
new file mode 100644
index 000000000..154ef3a23
--- /dev/null
+++ b/client/src/app/+admin/config/shared/batch-domains-validators.service.ts
@@ -0,0 +1,68 @@
1import { I18n } from '@ngx-translate/i18n-polyfill'
2import { Validators, ValidatorFn } from '@angular/forms'
3import { Injectable } from '@angular/core'
4import { BuildFormValidator, validateHost } from '@app/shared'
5
6@Injectable()
7export class BatchDomainsValidatorsService {
8 readonly DOMAINS: BuildFormValidator
9
10 constructor (private i18n: I18n) {
11 this.DOMAINS = {
12 VALIDATORS: [ Validators.required, this.validDomains, this.isHostsUnique ],
13 MESSAGES: {
14 'required': this.i18n('Domain is required.'),
15 'validDomains': this.i18n('Domains entered are invalid.'),
16 'uniqueDomains': this.i18n('Domains entered contain duplicates.')
17 }
18 }
19 }
20
21 getNotEmptyHosts (hosts: string) {
22 return hosts
23 .split('\n')
24 .filter((host: string) => host && host.length !== 0) // Eject empty hosts
25 }
26
27 private validDomains: ValidatorFn = (control) => {
28 if (!control.value) return null
29
30 const newHostsErrors = []
31 const hosts = this.getNotEmptyHosts(control.value)
32
33 for (const host of hosts) {
34 if (validateHost(host) === false) {
35 newHostsErrors.push(this.i18n('{{host}} is not valid', { host }))
36 }
37 }
38
39 /* Is not valid. */
40 if (newHostsErrors.length !== 0) {
41 return {
42 'validDomains': {
43 reason: 'invalid',
44 value: newHostsErrors.join('. ') + '.'
45 }
46 }
47 }
48
49 /* Is valid. */
50 return null
51 }
52
53 private isHostsUnique: ValidatorFn = (control) => {
54 if (!control.value) return null
55
56 const hosts = this.getNotEmptyHosts(control.value)
57
58 if (hosts.every((host: string) => hosts.indexOf(host) === hosts.lastIndexOf(host))) {
59 return null
60 } else {
61 return {
62 'uniqueDomains': {
63 reason: 'invalid'
64 }
65 }
66 }
67 }
68}
diff --git a/client/src/app/+admin/follows/followers-list/followers-list.component.ts b/client/src/app/+admin/follows/followers-list/followers-list.component.ts
index aff59a691..585902827 100644
--- a/client/src/app/+admin/follows/followers-list/followers-list.component.ts
+++ b/client/src/app/+admin/follows/followers-list/followers-list.component.ts
@@ -15,7 +15,7 @@ export class FollowersListComponent extends RestTable implements OnInit {
15 followers: ActorFollow[] = [] 15 followers: ActorFollow[] = []
16 totalRecords = 0 16 totalRecords = 0
17 rowsPerPage = 10 17 rowsPerPage = 10
18 sort: SortMeta = { field: 'createdAt', order: 1 } 18 sort: SortMeta = { field: 'createdAt', order: -1 }
19 pagination: RestPagination = { count: this.rowsPerPage, start: 0 } 19 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
20 20
21 constructor ( 21 constructor (
diff --git a/client/src/app/+admin/follows/following-add/following-add.component.html b/client/src/app/+admin/follows/following-add/following-add.component.html
deleted file mode 100644
index e08decb3f..000000000
--- a/client/src/app/+admin/follows/following-add/following-add.component.html
+++ /dev/null
@@ -1,22 +0,0 @@
1<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
2
3<form (ngSubmit)="addFollowing()">
4 <div class="form-group">
5 <label i18n for="hosts">1 host (without "http://") per line</label>
6
7 <textarea
8 type="text" class="form-control" placeholder="example.com" id="hosts" name="hosts"
9 [(ngModel)]="hostsString" (ngModelChange)="onHostsChanged()" [ngClass]="{ 'input-error': hostsError }"
10 ></textarea>
11
12 <div *ngIf="hostsError" class="form-error">
13 {{ hostsError }}
14 </div>
15 </div>
16
17 <div i18n *ngIf="httpEnabled() === false" class="alert alert-warning">
18 It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.
19 </div>
20
21 <input type="submit" i18n-value value="Add following" [disabled]="hostsError || !hostsString" class="btn btn-secondary">
22</form>
diff --git a/client/src/app/+admin/follows/following-add/following-add.component.scss b/client/src/app/+admin/follows/following-add/following-add.component.scss
deleted file mode 100644
index 7594b502c..000000000
--- a/client/src/app/+admin/follows/following-add/following-add.component.scss
+++ /dev/null
@@ -1,11 +0,0 @@
1@import '_variables';
2@import '_mixins';
3
4textarea {
5 height: 250px;
6}
7
8input[type=submit] {
9 @include peertube-button;
10 @include orange-button;
11}
diff --git a/client/src/app/+admin/follows/following-add/following-add.component.ts b/client/src/app/+admin/follows/following-add/following-add.component.ts
deleted file mode 100644
index 308bbb0c5..000000000
--- a/client/src/app/+admin/follows/following-add/following-add.component.ts
+++ /dev/null
@@ -1,85 +0,0 @@
1import { Component } from '@angular/core'
2import { Router } from '@angular/router'
3import { Notifier } from '@app/core'
4import { ConfirmService } from '../../../core'
5import { validateHost } from '../../../shared'
6import { FollowService } from '@app/shared/instance/follow.service'
7import { I18n } from '@ngx-translate/i18n-polyfill'
8
9@Component({
10 selector: 'my-following-add',
11 templateUrl: './following-add.component.html',
12 styleUrls: [ './following-add.component.scss' ]
13})
14export class FollowingAddComponent {
15 hostsString = ''
16 hostsError: string = null
17 error: string = null
18
19 constructor (
20 private router: Router,
21 private notifier: Notifier,
22 private confirmService: ConfirmService,
23 private followService: FollowService,
24 private i18n: I18n
25 ) {}
26
27 httpEnabled () {
28 return window.location.protocol === 'https:'
29 }
30
31 onHostsChanged () {
32 this.hostsError = null
33
34 const newHostsErrors = []
35 const hosts = this.getNotEmptyHosts()
36
37 for (const host of hosts) {
38 if (validateHost(host) === false) {
39 newHostsErrors.push(this.i18n('{{host}} is not valid', { host }))
40 }
41 }
42
43 if (newHostsErrors.length !== 0) {
44 this.hostsError = newHostsErrors.join('. ')
45 }
46 }
47
48 async addFollowing () {
49 this.error = ''
50
51 const hosts = this.getNotEmptyHosts()
52 if (hosts.length === 0) {
53 this.error = this.i18n('You need to specify hosts to follow.')
54 }
55
56 if (!this.isHostsUnique(hosts)) {
57 this.error = this.i18n('Hosts need to be unique.')
58 return
59 }
60
61 const confirmMessage = this.i18n('If you confirm, you will send a follow request to:<br /> - ') + hosts.join('<br /> - ')
62 const res = await this.confirmService.confirm(confirmMessage, this.i18n('Follow new server(s)'))
63 if (res === false) return
64
65 this.followService.follow(hosts).subscribe(
66 () => {
67 this.notifier.success(this.i18n('Follow request(s) sent!'))
68
69 setTimeout(() => this.router.navigate([ '/admin/follows/following-list' ]), 500)
70 },
71
72 err => this.notifier.error(err.message)
73 )
74 }
75
76 private isHostsUnique (hosts: string[]) {
77 return hosts.every(host => hosts.indexOf(host) === hosts.lastIndexOf(host))
78 }
79
80 private getNotEmptyHosts () {
81 return this.hostsString
82 .split('\n')
83 .filter(host => host && host.length !== 0) // Eject empty hosts
84 }
85}
diff --git a/client/src/app/+admin/follows/following-add/index.ts b/client/src/app/+admin/follows/following-add/index.ts
deleted file mode 100644
index 1b1897ffa..000000000
--- a/client/src/app/+admin/follows/following-add/index.ts
+++ /dev/null
@@ -1 +0,0 @@
1export * from './following-add.component'
diff --git a/client/src/app/+admin/follows/following-list/following-list.component.html b/client/src/app/+admin/follows/following-list/following-list.component.html
index 01aba0c11..cb62d52dd 100644
--- a/client/src/app/+admin/follows/following-list/following-list.component.html
+++ b/client/src/app/+admin/follows/following-list/following-list.component.html
@@ -4,12 +4,16 @@
4> 4>
5 <ng-template pTemplate="caption"> 5 <ng-template pTemplate="caption">
6 <div class="caption"> 6 <div class="caption">
7 <div> 7 <div class="ml-auto">
8 <input 8 <input
9 type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..." 9 type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
10 (keyup)="onSearch($event)" 10 (keyup)="onSearch($event)"
11 > 11 >
12 </div> 12 </div>
13 <a class="ml-2 follow-button" (click)="addDomainsToFollow()" (key.enter)="addDomainsToFollow()">
14 <my-global-icon iconName="add"></my-global-icon>
15 <ng-container i18n>Follow domain</ng-container>
16 </a>
13 </div> 17 </div>
14 </ng-template> 18 </ng-template>
15 19
@@ -42,3 +46,5 @@
42 </tr> 46 </tr>
43 </ng-template> 47 </ng-template>
44</p-table> 48</p-table>
49
50<my-batch-domains-modal #batchDomainsModal i18n-action action="Follow domains" (domains)="addFollowing($event)"></my-batch-domains-modal>
diff --git a/client/src/app/+admin/follows/following-list/following-list.component.scss b/client/src/app/+admin/follows/following-list/following-list.component.scss
index a6f0656b8..f4656b88d 100644
--- a/client/src/app/+admin/follows/following-list/following-list.component.scss
+++ b/client/src/app/+admin/follows/following-list/following-list.component.scss
@@ -7,4 +7,8 @@
7 input { 7 input {
8 @include peertube-input-text(250px); 8 @include peertube-input-text(250px);
9 } 9 }
10} \ No newline at end of file 10}
11
12.follow-button {
13 @include create-button;
14}
diff --git a/client/src/app/+admin/follows/following-list/following-list.component.ts b/client/src/app/+admin/follows/following-list/following-list.component.ts
index dd7629ead..477a6c0d7 100644
--- a/client/src/app/+admin/follows/following-list/following-list.component.ts
+++ b/client/src/app/+admin/follows/following-list/following-list.component.ts
@@ -1,4 +1,4 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit, ViewChild } from '@angular/core'
2import { Notifier } from '@app/core' 2import { Notifier } from '@app/core'
3import { SortMeta } from 'primeng/api' 3import { SortMeta } from 'primeng/api'
4import { ActorFollow } from '../../../../../../shared/models/actors/follow.model' 4import { ActorFollow } from '../../../../../../shared/models/actors/follow.model'
@@ -6,6 +6,7 @@ import { ConfirmService } from '../../../core/confirm/confirm.service'
6import { RestPagination, RestTable } from '../../../shared' 6import { RestPagination, RestTable } from '../../../shared'
7import { FollowService } from '@app/shared/instance/follow.service' 7import { FollowService } from '@app/shared/instance/follow.service'
8import { I18n } from '@ngx-translate/i18n-polyfill' 8import { I18n } from '@ngx-translate/i18n-polyfill'
9import { BatchDomainsModalComponent } from '@app/+admin/config/shared/batch-domains-modal.component'
9 10
10@Component({ 11@Component({
11 selector: 'my-followers-list', 12 selector: 'my-followers-list',
@@ -13,10 +14,12 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
13 styleUrls: [ './following-list.component.scss' ] 14 styleUrls: [ './following-list.component.scss' ]
14}) 15})
15export class FollowingListComponent extends RestTable implements OnInit { 16export class FollowingListComponent extends RestTable implements OnInit {
17 @ViewChild('batchDomainsModal') batchDomainsModal: BatchDomainsModalComponent
18
16 following: ActorFollow[] = [] 19 following: ActorFollow[] = []
17 totalRecords = 0 20 totalRecords = 0
18 rowsPerPage = 10 21 rowsPerPage = 10
19 sort: SortMeta = { field: 'createdAt', order: 1 } 22 sort: SortMeta = { field: 'createdAt', order: -1 }
20 pagination: RestPagination = { count: this.rowsPerPage, start: 0 } 23 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
21 24
22 constructor ( 25 constructor (
@@ -36,6 +39,21 @@ export class FollowingListComponent extends RestTable implements OnInit {
36 return 'FollowingListComponent' 39 return 'FollowingListComponent'
37 } 40 }
38 41
42 addDomainsToFollow () {
43 this.batchDomainsModal.openModal()
44 }
45
46 async addFollowing (hosts: string[]) {
47 this.followService.follow(hosts).subscribe(
48 () => {
49 this.notifier.success(this.i18n('Follow request(s) sent!'))
50 this.loadData()
51 },
52
53 err => this.notifier.error(err.message)
54 )
55 }
56
39 async removeFollowing (follow: ActorFollow) { 57 async removeFollowing (follow: ActorFollow) {
40 const res = await this.confirmService.confirm( 58 const res = await this.confirmService.confirm(
41 this.i18n('Do you really want to unfollow {{host}}?', { host: follow.following.host }), 59 this.i18n('Do you really want to unfollow {{host}}?', { host: follow.following.host }),
diff --git a/client/src/app/+admin/follows/follows.component.html b/client/src/app/+admin/follows/follows.component.html
index 46581daf9..7b5bcc2db 100644
--- a/client/src/app/+admin/follows/follows.component.html
+++ b/client/src/app/+admin/follows/follows.component.html
@@ -4,8 +4,6 @@
4 <div class="admin-sub-nav"> 4 <div class="admin-sub-nav">
5 <a i18n routerLink="following-list" routerLinkActive="active">Following</a> 5 <a i18n routerLink="following-list" routerLinkActive="active">Following</a>
6 6
7 <a i18n routerLink="following-add" routerLinkActive="active">Follow</a>
8
9 <a i18n routerLink="followers-list" routerLinkActive="active">Followers</a> 7 <a i18n routerLink="followers-list" routerLinkActive="active">Followers</a>
10 8
11 <a i18n routerLink="video-redundancies-list" routerLinkActive="active">Video redundancies</a> 9 <a i18n routerLink="video-redundancies-list" routerLinkActive="active">Video redundancies</a>
diff --git a/client/src/app/+admin/follows/follows.routes.ts b/client/src/app/+admin/follows/follows.routes.ts
index 298733eb0..8270ae444 100644
--- a/client/src/app/+admin/follows/follows.routes.ts
+++ b/client/src/app/+admin/follows/follows.routes.ts
@@ -2,7 +2,6 @@ import { Routes } from '@angular/router'
2 2
3import { UserRightGuard } from '../../core' 3import { UserRightGuard } from '../../core'
4import { FollowsComponent } from './follows.component' 4import { FollowsComponent } from './follows.component'
5import { FollowingAddComponent } from './following-add'
6import { FollowersListComponent } from './followers-list' 5import { FollowersListComponent } from './followers-list'
7import { UserRight } from '../../../../../shared' 6import { UserRight } from '../../../../../shared'
8import { FollowingListComponent } from './following-list/following-list.component' 7import { FollowingListComponent } from './following-list/following-list.component'
@@ -42,12 +41,7 @@ export const FollowsRoutes: Routes = [
42 }, 41 },
43 { 42 {
44 path: 'following-add', 43 path: 'following-add',
45 component: FollowingAddComponent, 44 redirectTo: 'following-list'
46 data: {
47 meta: {
48 title: 'Add follow'
49 }
50 }
51 }, 45 },
52 { 46 {
53 path: 'video-redundancies-list', 47 path: 'video-redundancies-list',
diff --git a/client/src/app/+admin/follows/index.ts b/client/src/app/+admin/follows/index.ts
index 4fcb35cb1..285955468 100644
--- a/client/src/app/+admin/follows/index.ts
+++ b/client/src/app/+admin/follows/index.ts
@@ -1,4 +1,3 @@
1export * from './following-add'
2export * from './followers-list' 1export * from './followers-list'
3export * from './following-list' 2export * from './following-list'
4export * from './video-redundancies-list' 3export * from './video-redundancies-list'
diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html
index 44c5c2fb8..0e072d84b 100644
--- a/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html
+++ b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html
@@ -4,6 +4,14 @@
4 [showCurrentPageReport]="true" i18n-currentPageReportTemplate 4 [showCurrentPageReport]="true" i18n-currentPageReportTemplate
5 currentPageReportTemplate="Showing {first} to {last} of {totalRecords} muted instances" 5 currentPageReportTemplate="Showing {first} to {last} of {totalRecords} muted instances"
6> 6>
7 <ng-template pTemplate="caption">
8 <div class="caption">
9 <a class="ml-auto block-button" (click)="addServersToBlock()" (key.enter)="addServersToBlock()">
10 <my-global-icon iconName="add"></my-global-icon>
11 <ng-container i18n>Mute domain</ng-container>
12 </a>
13 </div>
14 </ng-template>
7 15
8 <ng-template pTemplate="header"> 16 <ng-template pTemplate="header">
9 <tr> 17 <tr>
@@ -23,3 +31,11 @@
23 </tr> 31 </tr>
24 </ng-template> 32 </ng-template>
25</p-table> 33</p-table>
34
35<my-batch-domains-modal #batchDomainsModal i18n-action action="Mute domains" (domains)="onDomainsToBlock($event)">
36 <ng-container ngProjectAs="warning">
37 <div i18n *ngIf="httpEnabled() === false" class="alert alert-warning">
38 It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.
39 </div>
40 </ng-container>
41</my-batch-domains-modal>
diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.scss b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.scss
index 6028b75ea..9d3bedd80 100644
--- a/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.scss
+++ b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.scss
@@ -4,4 +4,8 @@
4.unblock-button { 4.unblock-button {
5 @include peertube-button; 5 @include peertube-button;
6 @include grey-button; 6 @include grey-button;
7} \ No newline at end of file 7}
8
9.block-button {
10 @include create-button;
11}
diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.ts b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.ts
index 5af6d8f76..431729ef2 100644
--- a/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.ts
+++ b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.ts
@@ -1,10 +1,11 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit, ViewChild } from '@angular/core'
2import { Notifier } from '@app/core' 2import { Notifier } from '@app/core'
3import { I18n } from '@ngx-translate/i18n-polyfill' 3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { RestPagination, RestTable } from '@app/shared' 4import { RestPagination, RestTable } from '@app/shared'
5import { SortMeta } from 'primeng/api' 5import { SortMeta } from 'primeng/api'
6import { BlocklistService } from '@app/shared/blocklist' 6import { BlocklistService } from '@app/shared/blocklist'
7import { ServerBlock } from '../../../../../../shared' 7import { ServerBlock } from '../../../../../../shared'
8import { BatchDomainsModalComponent } from '@app/+admin/config/shared/batch-domains-modal.component'
8 9
9@Component({ 10@Component({
10 selector: 'my-instance-server-blocklist', 11 selector: 'my-instance-server-blocklist',
@@ -12,6 +13,8 @@ import { ServerBlock } from '../../../../../../shared'
12 templateUrl: './instance-server-blocklist.component.html' 13 templateUrl: './instance-server-blocklist.component.html'
13}) 14})
14export class InstanceServerBlocklistComponent extends RestTable implements OnInit { 15export class InstanceServerBlocklistComponent extends RestTable implements OnInit {
16 @ViewChild('batchDomainsModal') batchDomainsModal: BatchDomainsModalComponent
17
15 blockedServers: ServerBlock[] = [] 18 blockedServers: ServerBlock[] = []
16 totalRecords = 0 19 totalRecords = 0
17 rowsPerPage = 10 20 rowsPerPage = 10
@@ -47,6 +50,27 @@ export class InstanceServerBlocklistComponent extends RestTable implements OnIni
47 ) 50 )
48 } 51 }
49 52
53 httpEnabled () {
54 return window.location.protocol === 'https:'
55 }
56
57 addServersToBlock () {
58 this.batchDomainsModal.openModal()
59 }
60
61 onDomainsToBlock (domains: string[]) {
62 domains.forEach(domain => {
63 this.blocklistService.blockServerByInstance(domain)
64 .subscribe(
65 () => {
66 this.notifier.success(this.i18n('Instance {{domain}} muted by your instance.', { domain }))
67
68 this.loadData()
69 }
70 )
71 })
72 }
73
50 protected loadData () { 74 protected loadData () {
51 return this.blocklistService.getInstanceServerBlocklist(this.pagination, this.sort) 75 return this.blocklistService.getInstanceServerBlocklist(this.pagination, this.sort)
52 .subscribe( 76 .subscribe(
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 155d10dda..3899ee07f 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
@@ -48,13 +48,13 @@
48 </a> 48 </a>
49 </td> 49 </td>
50 50
51 <td> 51 <td class="c-hand" [pRowToggler]="videoAbuse">
52 <span *ngIf="isVideoAbuseAccepted(videoAbuse)" [title]="videoAbuse.state.label" class="glyphicon glyphicon-ok"></span> 52 <span *ngIf="isVideoAbuseAccepted(videoAbuse)" [title]="videoAbuse.state.label" class="glyphicon glyphicon-ok"></span>
53 <span *ngIf="isVideoAbuseRejected(videoAbuse)" [title]="videoAbuse.state.label" class="glyphicon glyphicon-remove"></span> 53 <span *ngIf="isVideoAbuseRejected(videoAbuse)" [title]="videoAbuse.state.label" class="glyphicon glyphicon-remove"></span>
54 </td> 54 </td>
55 55
56 <td class="action-cell"> 56 <td class="action-cell">
57 <my-action-dropdown placement="bottom-right" i18n-label label="Actions" [actions]="videoAbuseActions" [entry]="videoAbuse"></my-action-dropdown> 57 <my-action-dropdown placement="bottom-right auto" i18n-label label="Actions" [actions]="videoAbuseActions" [entry]="videoAbuse"></my-action-dropdown>
58 </td> 58 </td>
59 </tr> 59 </tr>
60 </ng-template> 60 </ng-template>
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 b135792a7..5e48cf24f 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
@@ -1,9 +1,9 @@
1import { Component, OnInit, ViewChild } from '@angular/core' 1import { Component, OnInit, ViewChild } from '@angular/core'
2import { Account } from '../../../shared/account/account.model' 2import { Account } from '@app/shared/account/account.model'
3import { Notifier } from '@app/core' 3import { Notifier } from '@app/core'
4import { SortMeta } from 'primeng/api' 4import { SortMeta } from 'primeng/api'
5import { VideoAbuse, VideoAbuseState } from '../../../../../../shared' 5import { VideoAbuse, VideoAbuseState } from '../../../../../../shared'
6import { RestPagination, RestTable, VideoAbuseService } from '../../../shared' 6import { RestPagination, RestTable, VideoAbuseService, VideoBlacklistService } from '../../../shared'
7import { I18n } from '@ngx-translate/i18n-polyfill' 7import { I18n } from '@ngx-translate/i18n-polyfill'
8import { DropdownAction } from '../../../shared/buttons/action-dropdown.component' 8import { DropdownAction } from '../../../shared/buttons/action-dropdown.component'
9import { ConfirmService } from '../../../core/index' 9import { ConfirmService } from '../../../core/index'
@@ -14,6 +14,7 @@ import { Actor } from '@app/shared/actor/actor.model'
14import { buildVideoLink, buildVideoEmbed } from 'src/assets/player/utils' 14import { buildVideoLink, buildVideoEmbed } from 'src/assets/player/utils'
15import { getAbsoluteAPIUrl } from '@app/shared/misc/utils' 15import { getAbsoluteAPIUrl } from '@app/shared/misc/utils'
16import { DomSanitizer } from '@angular/platform-browser' 16import { DomSanitizer } from '@angular/platform-browser'
17import { BlocklistService } from '@app/shared/blocklist'
17 18
18@Component({ 19@Component({
19 selector: 'my-video-abuse-list', 20 selector: 'my-video-abuse-list',
@@ -29,11 +30,13 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
29 sort: SortMeta = { field: 'createdAt', order: 1 } 30 sort: SortMeta = { field: 'createdAt', order: 1 }
30 pagination: RestPagination = { count: this.rowsPerPage, start: 0 } 31 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
31 32
32 videoAbuseActions: DropdownAction<VideoAbuse>[] = [] 33 videoAbuseActions: DropdownAction<VideoAbuse>[][] = []
33 34
34 constructor ( 35 constructor (
35 private notifier: Notifier, 36 private notifier: Notifier,
36 private videoAbuseService: VideoAbuseService, 37 private videoAbuseService: VideoAbuseService,
38 private blocklistService: BlocklistService,
39 private videoBlacklistService: VideoBlacklistService,
37 private confirmService: ConfirmService, 40 private confirmService: ConfirmService,
38 private i18n: I18n, 41 private i18n: I18n,
39 private markdownRenderer: MarkdownService, 42 private markdownRenderer: MarkdownService,
@@ -42,30 +45,57 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
42 super() 45 super()
43 46
44 this.videoAbuseActions = [ 47 this.videoAbuseActions = [
45 { 48 [
46 label: this.i18n('Delete this report'), 49 {
47 handler: videoAbuse => this.removeVideoAbuse(videoAbuse) 50 label: this.i18n('Internal actions'),
48 }, 51 isHeader: true
49 { 52 },
50 label: this.i18n('Add note'), 53 {
51 handler: videoAbuse => this.openModerationCommentModal(videoAbuse), 54 label: this.i18n('Delete report'),
52 isDisplayed: videoAbuse => !videoAbuse.moderationComment 55 handler: videoAbuse => this.removeVideoAbuse(videoAbuse)
53 }, 56 },
54 { 57 {
55 label: this.i18n('Update note'), 58 label: this.i18n('Add note'),
56 handler: videoAbuse => this.openModerationCommentModal(videoAbuse), 59 handler: videoAbuse => this.openModerationCommentModal(videoAbuse),
57 isDisplayed: videoAbuse => !!videoAbuse.moderationComment 60 isDisplayed: videoAbuse => !videoAbuse.moderationComment
58 }, 61 },
59 { 62 {
60 label: this.i18n('Mark as accepted'), 63 label: this.i18n('Update note'),
61 handler: videoAbuse => this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED), 64 handler: videoAbuse => this.openModerationCommentModal(videoAbuse),
62 isDisplayed: videoAbuse => !this.isVideoAbuseAccepted(videoAbuse) 65 isDisplayed: videoAbuse => !!videoAbuse.moderationComment
63 }, 66 },
64 { 67 {
65 label: this.i18n('Mark as rejected'), 68 label: this.i18n('Mark as accepted'),
66 handler: videoAbuse => this.updateVideoAbuseState(videoAbuse, VideoAbuseState.REJECTED), 69 handler: videoAbuse => this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED),
67 isDisplayed: videoAbuse => !this.isVideoAbuseRejected(videoAbuse) 70 isDisplayed: videoAbuse => !this.isVideoAbuseAccepted(videoAbuse)
68 } 71 },
72 {
73 label: this.i18n('Mark as rejected'),
74 handler: videoAbuse => this.updateVideoAbuseState(videoAbuse, VideoAbuseState.REJECTED),
75 isDisplayed: videoAbuse => !this.isVideoAbuseRejected(videoAbuse)
76 }
77 ],
78 [
79 {
80 label: this.i18n('Actions for the video'),
81 isHeader: true
82 },
83 {
84 label: this.i18n('Blacklist video'),
85 handler: videoAbuse => {
86 this.videoBlacklistService.blacklistVideo(videoAbuse.video.id, undefined, true)
87 .subscribe(
88 () => {
89 this.notifier.success(this.i18n('Video blacklisted.'))
90
91 this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED)
92 },
93
94 err => this.notifier.error(err.message)
95 )
96 }
97 }
98 ]
69 ] 99 ]
70 } 100 }
71 101
diff --git a/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.ts b/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.ts
index 43b6863af..4e9965bee 100644
--- a/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.ts
+++ b/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.ts
@@ -18,7 +18,7 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit {
18 blacklist: (VideoBlacklist & { reasonHtml?: string })[] = [] 18 blacklist: (VideoBlacklist & { reasonHtml?: string })[] = []
19 totalRecords = 0 19 totalRecords = 0
20 rowsPerPage = 10 20 rowsPerPage = 10
21 sort: SortMeta = { field: 'createdAt', order: 1 } 21 sort: SortMeta = { field: 'createdAt', order: -1 }
22 pagination: RestPagination = { count: this.rowsPerPage, start: 0 } 22 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
23 listBlacklistTypeFilter: VideoBlacklistType = undefined 23 listBlacklistTypeFilter: VideoBlacklistType = undefined
24 24
diff --git a/client/src/app/shared/buttons/action-dropdown.component.html b/client/src/app/shared/buttons/action-dropdown.component.html
index cd993db9f..14cfe9a22 100644
--- a/client/src/app/shared/buttons/action-dropdown.component.html
+++ b/client/src/app/shared/buttons/action-dropdown.component.html
@@ -24,17 +24,27 @@
24 </div> 24 </div>
25 </ng-template> 25 </ng-template>
26 26
27 <a *ngIf="action.linkBuilder" [ngClass]="{ 'with-icon': !!action.iconName }" class="dropdown-item" [routerLink]="action.linkBuilder(entry)" [title]="action.title || ''"> 27 <a
28 *ngIf="action.linkBuilder && !action.isHeader" [ngClass]="{ 'with-icon': !!action.iconName }"
29 class="dropdown-item" [routerLink]="action.linkBuilder(entry)" [title]="action.title || ''"
30 >
28 <ng-container *ngTemplateOutlet="templateActionLabel; context:{ $implicit: action }"></ng-container> 31 <ng-container *ngTemplateOutlet="templateActionLabel; context:{ $implicit: action }"></ng-container>
29 </a> 32 </a>
30 33
31 <span 34 <span
32 *ngIf="!action.linkBuilder" [ngClass]="{ 'with-icon': !!action.iconName }" (click)="action.handler(entry)" 35 *ngIf="!action.linkBuilder && !action.isHeader" [ngClass]="{ 'with-icon': !!action.iconName }"
33 class="custom-action dropdown-item" role="button" [title]="action.title || ''" 36 class="custom-action dropdown-item" role="button" [title]="action.title || ''" (click)="action.handler(entry)"
34 > 37 >
35 <ng-container *ngTemplateOutlet="templateActionLabel; context:{ $implicit: action }"></ng-container> 38 <ng-container *ngTemplateOutlet="templateActionLabel; context:{ $implicit: action }"></ng-container>
36 </span> 39 </span>
37 40
41 <h6
42 *ngIf="!action.linkBuilder && action.isHeader" [ngClass]="{ 'with-icon': !!action.iconName }"
43 class="dropdown-header" role="button" [title]="action.title || ''" (click)="action.handler(entry)"
44 >
45 <ng-container *ngTemplateOutlet="templateActionLabel; context:{ $implicit: action }"></ng-container>
46 </h6>
47
38 </ng-container> 48 </ng-container>
39 </ng-container> 49 </ng-container>
40 50
diff --git a/client/src/app/shared/buttons/action-dropdown.component.scss b/client/src/app/shared/buttons/action-dropdown.component.scss
index 442c90984..7a030f32c 100644
--- a/client/src/app/shared/buttons/action-dropdown.component.scss
+++ b/client/src/app/shared/buttons/action-dropdown.component.scss
@@ -51,6 +51,10 @@
51} 51}
52 52
53.dropdown-menu { 53.dropdown-menu {
54 .dropdown-header {
55 padding: 0.2rem 1rem;
56 }
57
54 .dropdown-item { 58 .dropdown-item {
55 display: flex; 59 display: flex;
56 cursor: pointer; 60 cursor: pointer;
diff --git a/client/src/app/shared/buttons/action-dropdown.component.ts b/client/src/app/shared/buttons/action-dropdown.component.ts
index 6649b092a..8fcaa38b9 100644
--- a/client/src/app/shared/buttons/action-dropdown.component.ts
+++ b/client/src/app/shared/buttons/action-dropdown.component.ts
@@ -9,6 +9,7 @@ export type DropdownAction<T> = {
9 handler?: (a: T) => any 9 handler?: (a: T) => any
10 linkBuilder?: (a: T) => (string | number)[] 10 linkBuilder?: (a: T) => (string | number)[]
11 isDisplayed?: (a: T) => boolean 11 isDisplayed?: (a: T) => boolean
12 isHeader?: boolean
12} 13}
13 14
14export type DropdownButtonSize = 'normal' | 'small' 15export type DropdownButtonSize = 'normal' | 'small'
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts
index a952880a6..01735c187 100644
--- a/client/src/app/shared/shared.module.ts
+++ b/client/src/app/shared/shared.module.ts
@@ -107,6 +107,7 @@ import { InputSwitchModule } from 'primeng/inputswitch'
107import { MyAccountVideoSettingsComponent } from '@app/+my-account/my-account-settings/my-account-video-settings' 107import { MyAccountVideoSettingsComponent } from '@app/+my-account/my-account-settings/my-account-video-settings'
108import { MyAccountInterfaceSettingsComponent } from '@app/+my-account/my-account-settings/my-account-interface' 108import { MyAccountInterfaceSettingsComponent } from '@app/+my-account/my-account-settings/my-account-interface'
109import { ActorAvatarInfoComponent } from '@app/+my-account/shared/actor-avatar-info.component' 109import { ActorAvatarInfoComponent } from '@app/+my-account/shared/actor-avatar-info.component'
110import { BatchDomainsValidatorsService } from '@app/+admin/config/shared/batch-domains-validators.service'
110 111
111@NgModule({ 112@NgModule({
112 imports: [ 113 imports: [
@@ -297,6 +298,7 @@ import { ActorAvatarInfoComponent } from '@app/+my-account/shared/actor-avatar-i
297 LoginValidatorsService, 298 LoginValidatorsService,
298 ResetPasswordValidatorsService, 299 ResetPasswordValidatorsService,
299 UserValidatorsService, 300 UserValidatorsService,
301 BatchDomainsValidatorsService,
300 VideoPlaylistValidatorsService, 302 VideoPlaylistValidatorsService,
301 VideoAbuseValidatorsService, 303 VideoAbuseValidatorsService,
302 VideoChannelValidatorsService, 304 VideoChannelValidatorsService,
diff --git a/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts b/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts
index 0f7c19765..e1a8f6260 100644
--- a/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts
+++ b/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts
@@ -11,7 +11,6 @@ import { VideoCommentService } from './video-comment.service'
11import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 11import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
12import { VideoCommentValidatorsService } from '@app/shared/forms/form-validators/video-comment-validators.service' 12import { VideoCommentValidatorsService } from '@app/shared/forms/form-validators/video-comment-validators.service'
13import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 13import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
14import { AuthService } from '@app/core/auth'
15 14
16@Component({ 15@Component({
17 selector: 'my-video-comment-add', 16 selector: 'my-video-comment-add',
@@ -38,7 +37,6 @@ export class VideoCommentAddComponent extends FormReactive implements OnInit {
38 private videoCommentValidatorsService: VideoCommentValidatorsService, 37 private videoCommentValidatorsService: VideoCommentValidatorsService,
39 private notifier: Notifier, 38 private notifier: Notifier,
40 private videoCommentService: VideoCommentService, 39 private videoCommentService: VideoCommentService,
41 private authService: AuthService,
42 private modalService: NgbModal, 40 private modalService: NgbModal,
43 private router: Router 41 private router: Router
44 ) { 42 ) {
diff --git a/server/middlewares/validators/blocklist.ts b/server/middlewares/validators/blocklist.ts
index 47a0b1a1c..b2183437c 100644
--- a/server/middlewares/validators/blocklist.ts
+++ b/server/middlewares/validators/blocklist.ts
@@ -84,11 +84,9 @@ const blockServerValidator = [
84 .end() 84 .end()
85 } 85 }
86 86
87 const server = await ServerModel.loadByHost(host) 87 let server = await ServerModel.loadByHost(host)
88 if (!server) { 88 if (!server) {
89 return res.status(404) 89 server = await ServerModel.create({ host })
90 .send({ error: 'Server host not found.' })
91 .end()
92 } 90 }
93 91
94 res.locals.server = server 92 res.locals.server = server
diff --git a/server/tests/api/check-params/blocklist.ts b/server/tests/api/check-params/blocklist.ts
index fb459f756..1219ec9bd 100644
--- a/server/tests/api/check-params/blocklist.ts
+++ b/server/tests/api/check-params/blocklist.ts
@@ -175,13 +175,13 @@ describe('Test blocklist API validators', function () {
175 }) 175 })
176 }) 176 })
177 177
178 it('Should fail with an unknown server', async function () { 178 it('Should succeed with an unknown server', async function () {
179 await makePostBodyRequest({ 179 await makePostBodyRequest({
180 url: server.url, 180 url: server.url,
181 token: server.accessToken, 181 token: server.accessToken,
182 path, 182 path,
183 fields: { host: 'localhost:9003' }, 183 fields: { host: 'localhost:9003' },
184 statusCodeExpected: 404 184 statusCodeExpected: 204
185 }) 185 })
186 }) 186 })
187 187
@@ -218,7 +218,7 @@ describe('Test blocklist API validators', function () {
218 it('Should fail with an unknown server block', async function () { 218 it('Should fail with an unknown server block', async function () {
219 await makeDeleteRequest({ 219 await makeDeleteRequest({
220 url: server.url, 220 url: server.url,
221 path: path + '/localhost:9003', 221 path: path + '/localhost:9004',
222 token: server.accessToken, 222 token: server.accessToken,
223 statusCodeExpected: 404 223 statusCodeExpected: 404
224 }) 224 })
@@ -415,13 +415,13 @@ describe('Test blocklist API validators', function () {
415 }) 415 })
416 }) 416 })
417 417
418 it('Should fail with an unknown server', async function () { 418 it('Should succeed with an unknown server', async function () {
419 await makePostBodyRequest({ 419 await makePostBodyRequest({
420 url: server.url, 420 url: server.url,
421 token: server.accessToken, 421 token: server.accessToken,
422 path, 422 path,
423 fields: { host: 'localhost:9003' }, 423 fields: { host: 'localhost:9003' },
424 statusCodeExpected: 404 424 statusCodeExpected: 204
425 }) 425 })
426 }) 426 })
427 427
@@ -467,7 +467,7 @@ describe('Test blocklist API validators', function () {
467 it('Should fail with an unknown server block', async function () { 467 it('Should fail with an unknown server block', async function () {
468 await makeDeleteRequest({ 468 await makeDeleteRequest({
469 url: server.url, 469 url: server.url,
470 path: path + '/localhost:9003', 470 path: path + '/localhost:9004',
471 token: server.accessToken, 471 token: server.accessToken,
472 statusCodeExpected: 404 472 statusCodeExpected: 404
473 }) 473 })