From 4d029ef8ec3d5274eeaa3ee6d808eb7035e7faef Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 20 Jul 2021 14:15:15 +0200 Subject: Add ability for instances to follow any actor --- client/src/app/+admin/admin.component.ts | 4 +- client/src/app/+admin/admin.module.ts | 3 +- .../followers-list/followers-list.component.html | 4 +- .../following-list/follow-modal.component.html | 42 +++++++++ .../following-list/follow-modal.component.scss | 3 + .../following-list/follow-modal.component.ts | 69 ++++++++++++++ .../following-list/following-list.component.html | 21 ++--- .../following-list/following-list.component.ts | 22 ++--- .../src/app/+admin/follows/following-list/index.ts | 1 + client/src/app/+admin/follows/follows.routes.ts | 4 +- .../form-validators/batch-domains-validators.ts | 60 ------------ .../app/shared/form-validators/host-validators.ts | 105 +++++++++++++++++++++ client/src/app/shared/form-validators/host.ts | 8 -- client/src/app/shared/form-validators/index.ts | 1 - .../shared-instance/instance-follow.service.ts | 13 ++- .../batch-domains-modal.component.html | 14 +-- .../batch-domains-modal.component.ts | 8 +- 17 files changed, 260 insertions(+), 122 deletions(-) create mode 100644 client/src/app/+admin/follows/following-list/follow-modal.component.html create mode 100644 client/src/app/+admin/follows/following-list/follow-modal.component.scss create mode 100644 client/src/app/+admin/follows/following-list/follow-modal.component.ts delete mode 100644 client/src/app/shared/form-validators/batch-domains-validators.ts create mode 100644 client/src/app/shared/form-validators/host-validators.ts delete mode 100644 client/src/app/shared/form-validators/host.ts (limited to 'client') diff --git a/client/src/app/+admin/admin.component.ts b/client/src/app/+admin/admin.component.ts index dd92ed2ca..4b6fab6ed 100644 --- a/client/src/app/+admin/admin.component.ts +++ b/client/src/app/+admin/admin.component.ts @@ -26,12 +26,12 @@ export class AdminComponent implements OnInit { label: $localize`Federation`, children: [ { - label: $localize`Instances you follow`, + label: $localize`Following`, routerLink: '/admin/follows/following-list', iconName: 'following' }, { - label: $localize`Instances following you`, + label: $localize`Followers`, routerLink: '/admin/follows/followers-list', iconName: 'follower' }, diff --git a/client/src/app/+admin/admin.module.ts b/client/src/app/+admin/admin.module.ts index a7fe20b07..1ea7b9784 100644 --- a/client/src/app/+admin/admin.module.ts +++ b/client/src/app/+admin/admin.module.ts @@ -25,7 +25,7 @@ import { EditVODTranscodingComponent } from './config' import { ConfigService } from './config/shared/config.service' -import { FollowersListComponent, FollowsComponent, VideoRedundanciesListComponent } from './follows' +import { FollowersListComponent, FollowModalComponent, FollowsComponent, VideoRedundanciesListComponent } from './follows' import { FollowingListComponent } from './follows/following-list/following-list.component' import { RedundancyCheckboxComponent } from './follows/shared/redundancy-checkbox.component' import { VideoRedundancyInformationComponent } from './follows/video-redundancies-list/video-redundancy-information.component' @@ -68,6 +68,7 @@ import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersCom FollowsComponent, FollowersListComponent, FollowingListComponent, + FollowModalComponent, RedundancyCheckboxComponent, VideoRedundanciesListComponent, VideoRedundancyInformationComponent, diff --git a/client/src/app/+admin/follows/followers-list/followers-list.component.html b/client/src/app/+admin/follows/followers-list/followers-list.component.html index c2e9a4df6..08459634d 100644 --- a/client/src/app/+admin/follows/followers-list/followers-list.component.html +++ b/client/src/app/+admin/follows/followers-list/followers-list.component.html @@ -1,6 +1,6 @@

- Instances following you + Followers of your instance

Actions - Follower handle + Follower State Score Created diff --git a/client/src/app/+admin/follows/following-list/follow-modal.component.html b/client/src/app/+admin/follows/following-list/follow-modal.component.html new file mode 100644 index 000000000..d0761b718 --- /dev/null +++ b/client/src/app/+admin/follows/following-list/follow-modal.component.html @@ -0,0 +1,42 @@ + + + + + + diff --git a/client/src/app/+admin/follows/following-list/follow-modal.component.scss b/client/src/app/+admin/follows/following-list/follow-modal.component.scss new file mode 100644 index 000000000..9621a566f --- /dev/null +++ b/client/src/app/+admin/follows/following-list/follow-modal.component.scss @@ -0,0 +1,3 @@ +textarea { + height: 200px; +} diff --git a/client/src/app/+admin/follows/following-list/follow-modal.component.ts b/client/src/app/+admin/follows/following-list/follow-modal.component.ts new file mode 100644 index 000000000..dc6909200 --- /dev/null +++ b/client/src/app/+admin/follows/following-list/follow-modal.component.ts @@ -0,0 +1,69 @@ +import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' +import { Notifier } from '@app/core' +import { splitAndGetNotEmpty, UNIQUE_HOSTS_OR_HANDLE_VALIDATOR } from '@app/shared/form-validators/host-validators' +import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' +import { InstanceFollowService } from '@app/shared/shared-instance' +import { NgbModal } from '@ng-bootstrap/ng-bootstrap' +import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' + +@Component({ + selector: 'my-follow-modal', + templateUrl: './follow-modal.component.html', + styleUrls: [ './follow-modal.component.scss' ] +}) +export class FollowModalComponent extends FormReactive implements OnInit { + @ViewChild('modal', { static: true }) modal: NgbModal + + @Output() newFollow = new EventEmitter() + + placeholder = 'example.com\nchocobozzz@example.com\nchocobozzz_channel@example.com' + + private openedModal: NgbModalRef + + constructor ( + protected formValidatorService: FormValidatorService, + private modalService: NgbModal, + private followService: InstanceFollowService, + private notifier: Notifier + ) { + super() + } + + ngOnInit () { + this.buildForm({ + hostsOrHandles: UNIQUE_HOSTS_OR_HANDLE_VALIDATOR + }) + } + + openModal () { + this.openedModal = this.modalService.open(this.modal, { centered: true }) + } + + hide () { + this.openedModal.close() + } + + submit () { + this.addFollowing() + + this.form.reset() + this.hide() + } + + httpEnabled () { + return window.location.protocol === 'https:' + } + + private async addFollowing () { + const hostsOrHandles = splitAndGetNotEmpty(this.form.value['hostsOrHandles']) + + this.followService.follow(hostsOrHandles).subscribe( + () => { + this.notifier.success($localize`Follow request(s) sent!`) + this.newFollow.emit() + }, + + err => this.notifier.error(err.message) + ) + } +} 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 e7c0c9088..75b0efca8 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 @@ -1,6 +1,6 @@

- Instances you follow + Your instance subscriptions

@@ -28,7 +28,7 @@ Action - Host + Following State Created Redundancy allowed @@ -41,8 +41,8 @@ - - {{ follow.following.host }} + + {{ follow.following.name + '@' + follow.following.host }} @@ -57,6 +57,7 @@ {{ follow.createdAt | date: 'short' }} @@ -75,10 +76,4 @@ - - -
- It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers. -
-
-
+ 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 b63fe08c0..ba62dfa23 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 @@ -4,13 +4,14 @@ import { ConfirmService, Notifier, RestPagination, RestTable } from '@app/core' import { InstanceFollowService } from '@app/shared/shared-instance' import { BatchDomainsModalComponent } from '@app/shared/shared-moderation' import { ActorFollow } from '@shared/models' +import { FollowModalComponent } from './follow-modal.component' @Component({ templateUrl: './following-list.component.html', styleUrls: [ '../follows.component.scss', './following-list.component.scss' ] }) export class FollowingListComponent extends RestTable implements OnInit { - @ViewChild('batchDomainsModal') batchDomainsModal: BatchDomainsModalComponent + @ViewChild('followModal') followModal: FollowModalComponent following: ActorFollow[] = [] totalRecords = 0 @@ -33,23 +34,12 @@ export class FollowingListComponent extends RestTable implements OnInit { return 'FollowingListComponent' } - addDomainsToFollow () { - this.batchDomainsModal.openModal() + openFollowModal () { + this.followModal.openModal() } - httpEnabled () { - return window.location.protocol === 'https:' - } - - async addFollowing (hosts: string[]) { - this.followService.follow(hosts).subscribe( - () => { - this.notifier.success($localize`Follow request(s) sent!`) - this.reloadData() - }, - - err => this.notifier.error(err.message) - ) + isInstanceFollowing (follow: ActorFollow) { + return follow.following.name === 'peertube' } async removeFollowing (follow: ActorFollow) { diff --git a/client/src/app/+admin/follows/following-list/index.ts b/client/src/app/+admin/follows/following-list/index.ts index a70d46a7e..88be0ed4c 100644 --- a/client/src/app/+admin/follows/following-list/index.ts +++ b/client/src/app/+admin/follows/following-list/index.ts @@ -1 +1,2 @@ +export * from './follow-modal.component' export * from './following-list.component' diff --git a/client/src/app/+admin/follows/follows.routes.ts b/client/src/app/+admin/follows/follows.routes.ts index cd70daf77..3843b42b5 100644 --- a/client/src/app/+admin/follows/follows.routes.ts +++ b/client/src/app/+admin/follows/follows.routes.ts @@ -25,7 +25,7 @@ export const FollowsRoutes: Routes = [ component: FollowingListComponent, data: { meta: { - title: $localize`Following list` + title: $localize`Following` } } }, @@ -34,7 +34,7 @@ export const FollowsRoutes: Routes = [ component: FollowersListComponent, data: { meta: { - title: $localize`Followers list` + title: $localize`Followers` } } }, diff --git a/client/src/app/shared/form-validators/batch-domains-validators.ts b/client/src/app/shared/form-validators/batch-domains-validators.ts deleted file mode 100644 index 423d1337f..000000000 --- a/client/src/app/shared/form-validators/batch-domains-validators.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { AbstractControl, FormControl, ValidatorFn, Validators } from '@angular/forms' -import { BuildFormValidator } from './form-validator.model' -import { validateHost } from './host' - -export function getNotEmptyHosts (hosts: string) { - return hosts - .split('\n') - .filter((host: string) => host && host.length !== 0) // Eject empty hosts -} - -const validDomains: ValidatorFn = (control: FormControl) => { - if (!control.value) return null - - const newHostsErrors = [] - const hosts = getNotEmptyHosts(control.value) - - for (const host of hosts) { - if (validateHost(host) === false) { - newHostsErrors.push($localize`${host} is not valid`) - } - } - - /* Is not valid. */ - if (newHostsErrors.length !== 0) { - return { - 'validDomains': { - reason: 'invalid', - value: newHostsErrors.join('. ') + '.' - } - } - } - - /* Is valid. */ - return null -} - -const isHostsUnique: ValidatorFn = (control: AbstractControl) => { - if (!control.value) return null - - const hosts = getNotEmptyHosts(control.value) - - if (hosts.every((host: string) => hosts.indexOf(host) === hosts.lastIndexOf(host))) { - return null - } else { - return { - 'uniqueDomains': { - reason: 'invalid' - } - } - } -} - -export const DOMAINS_VALIDATOR: BuildFormValidator = { - VALIDATORS: [Validators.required, validDomains, isHostsUnique], - MESSAGES: { - 'required': $localize`Domain is required.`, - 'validDomains': $localize`Domains entered are invalid.`, - 'uniqueDomains': $localize`Domains entered contain duplicates.` - } -} diff --git a/client/src/app/shared/form-validators/host-validators.ts b/client/src/app/shared/form-validators/host-validators.ts new file mode 100644 index 000000000..d750113ef --- /dev/null +++ b/client/src/app/shared/form-validators/host-validators.ts @@ -0,0 +1,105 @@ +import { AbstractControl, ValidatorFn, Validators } from '@angular/forms' +import { BuildFormValidator } from './form-validator.model' + +function validateHost (value: string) { + // Thanks to http://stackoverflow.com/a/106223 + const HOST_REGEXP = new RegExp( + '^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$' + ) + + return HOST_REGEXP.test(value) +} + +function validateHandle (value: string) { + if (!value) return false + + return value.includes('@') +} + +const validHosts: ValidatorFn = (control: AbstractControl) => { + if (!control.value) return null + + const errors = [] + const hosts = splitAndGetNotEmpty(control.value) + + for (const host of hosts) { + if (validateHost(host) === false) { + errors.push($localize`${host} is not valid`) + } + } + + // valid + if (errors.length === 0) return null + + return { + 'validHosts': { + reason: 'invalid', + value: errors.join('. ') + '.' + } + } +} + +const validHostsOrHandles: ValidatorFn = (control: AbstractControl) => { + if (!control.value) return null + + const errors = [] + const lines = splitAndGetNotEmpty(control.value) + + for (const line of lines) { + if (validateHost(line) === false && validateHandle(line) === false) { + errors.push($localize`${line} is not valid`) + } + } + + // valid + if (errors.length === 0) return null + + return { + 'validHostsOrHandles': { + reason: 'invalid', + value: errors.join('. ') + '.' + } + } +} + +// --------------------------------------------------------------------------- + +export function splitAndGetNotEmpty (value: string) { + return value + .split('\n') + .filter(line => line && line.length !== 0) // Eject empty hosts +} + +export const unique: ValidatorFn = (control: AbstractControl) => { + if (!control.value) return null + + const hosts = splitAndGetNotEmpty(control.value) + + if (hosts.every((host: string) => hosts.indexOf(host) === hosts.lastIndexOf(host))) { + return null + } + + return { + 'unique': { + reason: 'invalid' + } + } +} + +export const UNIQUE_HOSTS_VALIDATOR: BuildFormValidator = { + VALIDATORS: [ Validators.required, validHosts, unique ], + MESSAGES: { + 'required': $localize`Domain is required.`, + 'validHosts': $localize`Hosts entered are invalid.`, + 'unique': $localize`Hosts entered contain duplicates.` + } +} + +export const UNIQUE_HOSTS_OR_HANDLE_VALIDATOR: BuildFormValidator = { + VALIDATORS: [ Validators.required, validHostsOrHandles, unique ], + MESSAGES: { + 'required': $localize`Domain is required.`, + 'validHostsOrHandles': $localize`Hosts or handles are invalid.`, + 'unique': $localize`Hosts or handles contain duplicates.` + } +} diff --git a/client/src/app/shared/form-validators/host.ts b/client/src/app/shared/form-validators/host.ts deleted file mode 100644 index c18a35f9b..000000000 --- a/client/src/app/shared/form-validators/host.ts +++ /dev/null @@ -1,8 +0,0 @@ -export function validateHost (value: string) { - // Thanks to http://stackoverflow.com/a/106223 - const HOST_REGEXP = new RegExp( - '^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$' - ) - - return HOST_REGEXP.test(value) -} diff --git a/client/src/app/shared/form-validators/index.ts b/client/src/app/shared/form-validators/index.ts index f621f03a4..c14272a2a 100644 --- a/client/src/app/shared/form-validators/index.ts +++ b/client/src/app/shared/form-validators/index.ts @@ -1,5 +1,4 @@ export * from './form-validator.model' -export * from './host' // Don't re export const variables because webpack 4 cannot do tree shaking with them // export * from './abuse-validators' diff --git a/client/src/app/shared/shared-instance/instance-follow.service.ts b/client/src/app/shared/shared-instance/instance-follow.service.ts index e52660140..af44020cf 100644 --- a/client/src/app/shared/shared-instance/instance-follow.service.ts +++ b/client/src/app/shared/shared-instance/instance-follow.service.ts @@ -4,7 +4,7 @@ import { catchError, map } from 'rxjs/operators' import { HttpClient, HttpParams } from '@angular/common/http' import { Injectable } from '@angular/core' import { RestExtractor, RestPagination, RestService } from '@app/core' -import { ActivityPubActorType, ActorFollow, FollowState, ResultList } from '@shared/models' +import { ActivityPubActorType, ActorFollow, FollowState, ResultList, ServerFollowCreate } from '@shared/models' import { environment } from '../../../environments/environment' @Injectable() @@ -64,9 +64,10 @@ export class InstanceFollowService { ) } - follow (notEmptyHosts: string[]) { - const body = { - hosts: notEmptyHosts + follow (hostsOrHandles: string[]) { + const body: ServerFollowCreate = { + handles: hostsOrHandles.filter(v => v.includes('@')), + hosts: hostsOrHandles.filter(v => !v.includes('@')) } return this.authHttp.post(InstanceFollowService.BASE_APPLICATION_URL + '/following', body) @@ -77,7 +78,9 @@ export class InstanceFollowService { } unfollow (follow: ActorFollow) { - return this.authHttp.delete(InstanceFollowService.BASE_APPLICATION_URL + '/following/' + follow.following.host) + const handle = follow.following.name + '@' + follow.following.host + + return this.authHttp.delete(InstanceFollowService.BASE_APPLICATION_URL + '/following/' + handle) .pipe( map(this.restExtractor.extractDataBool), catchError(res => this.restExtractor.handleError(res)) diff --git a/client/src/app/shared/shared-moderation/batch-domains-modal.component.html b/client/src/app/shared/shared-moderation/batch-domains-modal.component.html index 6a3c65721..8306a96bc 100644 --- a/client/src/app/shared/shared-moderation/batch-domains-modal.component.html +++ b/client/src/app/shared/shared-moderation/batch-domains-modal.component.html @@ -1,6 +1,6 @@ @@ -11,15 +11,15 @@ -
- {{ formErrors.domains }} +
+ {{ formErrors.hosts }} -
- {{ form.controls['domains'].errors.validDomains.value }} +
+ {{ form.controls['hosts'].errors.validHosts.value }}
diff --git a/client/src/app/shared/shared-moderation/batch-domains-modal.component.ts b/client/src/app/shared/shared-moderation/batch-domains-modal.component.ts index 6edbb6023..20be728f6 100644 --- a/client/src/app/shared/shared-moderation/batch-domains-modal.component.ts +++ b/client/src/app/shared/shared-moderation/batch-domains-modal.component.ts @@ -2,7 +2,7 @@ import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angu import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' import { NgbModal } from '@ng-bootstrap/ng-bootstrap' import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' -import { DOMAINS_VALIDATOR, getNotEmptyHosts } from '../form-validators/batch-domains-validators' +import { splitAndGetNotEmpty, UNIQUE_HOSTS_VALIDATOR } from '../form-validators/host-validators' @Component({ selector: 'my-batch-domains-modal', @@ -28,7 +28,7 @@ export class BatchDomainsModalComponent extends FormReactive implements OnInit { if (!this.action) this.action = $localize`Process domains` this.buildForm({ - domains: DOMAINS_VALIDATOR + hosts: UNIQUE_HOSTS_VALIDATOR }) } @@ -41,9 +41,7 @@ export class BatchDomainsModalComponent extends FormReactive implements OnInit { } submit () { - this.domains.emit( - getNotEmptyHosts(this.form.controls['domains'].value) - ) + this.domains.emit(splitAndGetNotEmpty(this.form.controls['hosts'].value)) this.form.reset() this.hide() } -- cgit v1.2.3