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'
},
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'
FollowsComponent,
FollowersListComponent,
FollowingListComponent,
+ FollowModalComponent,
RedundancyCheckboxComponent,
VideoRedundanciesListComponent,
VideoRedundancyInformationComponent,
<h1>
<my-global-icon iconName="follower" aria-hidden="true"></my-global-icon>
- <ng-container i18n>Instances following you</ng-container>
+ <ng-container i18n>Followers of your instance</ng-container>
</h1>
<p-table
<ng-template pTemplate="header">
<tr>
<th style="width: 150px;" i18n>Actions</th>
- <th i18n>Follower handle</th>
+ <th i18n>Follower</th>
<th style="width: 100px;" i18n pSortableColumn="state">State <p-sortIcon field="state"></p-sortIcon></th>
<th style="width: 100px;" i18n pSortableColumn="score">Score <p-sortIcon field="score"></p-sortIcon></th>
<th style="width: 150px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
--- /dev/null
+<ng-template #modal>
+ <div class="modal-header">
+ <h4 i18n class="modal-title">Follow</h4>
+
+ <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
+ </div>
+
+ <div class="modal-body">
+ <form novalidate [formGroup]="form" (ngSubmit)="submit()">
+ <div class="form-group">
+ <label i18n for="hostsOrHandles">1 host (without "http://"), account handle or channel handle per line</label>
+
+ <textarea
+ [placeholder]="placeholder" formControlName="hostsOrHandles" type="text" id="hostsOrHandles" name="hostsOrHandles"
+ class="form-control" [ngClass]="{ 'input-error': formErrors['hostsOrHandles'] }" ngbAutofocus
+ ></textarea>
+
+ <div *ngIf="formErrors.hostsOrHandles" class="form-error">
+ {{ formErrors.hostsOrHandles }}
+
+ <div *ngIf="form.controls['hostsOrHandles'].errors.validHostsOrHandles">
+ {{ form.controls['hostsOrHandles'].errors.validHostsOrHandles.value }}
+ </div>
+ </div>
+ </div>
+
+ <div i18n *ngIf="httpEnabled() === false" class="alert alert-warning">
+ It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.
+ </div>
+
+ <div class="form-group inputs">
+ <input
+ type="button" role="button" i18n-value value="Cancel" class="peertube-button grey-button"
+ (click)="hide()" (key.enter)="hide()"
+ >
+
+ <input type="submit" i18n-value value="Follow" class="peertube-button orange-button" [disabled]="!form.valid" />
+ </div>
+ </form>
+ </div>
+
+</ng-template>
--- /dev/null
+textarea {
+ height: 200px;
+}
--- /dev/null
+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<void>()
+
+ 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)
+ )
+ }
+}
<h1>
<my-global-icon iconName="following" aria-hidden="true"></my-global-icon>
- <ng-container i18n>Instances you follow</ng-container>
+ <ng-container i18n>Your instance subscriptions</ng-container>
</h1>
<p-table
<ng-template pTemplate="caption">
<div class="caption">
<div class="left-buttons">
- <a class="follow-button" (click)="addDomainsToFollow()" (key.enter)="addDomainsToFollow()">
+ <a class="follow-button" (click)="openFollowModal()" (key.enter)="openFollowModal()">
<my-global-icon iconName="following" aria-hidden="true"></my-global-icon>
- <ng-container i18n>Follow instances</ng-container>
+ <ng-container i18n>Follow</ng-container>
</a>
</div>
<ng-template pTemplate="header">
<tr>
<th style="width: 150px;" i18n>Action</th>
- <th i18n>Host</th>
+ <th i18n>Following</th>
<th style="width: 100px;" i18n pSortableColumn="state">State <p-sortIcon field="state"></p-sortIcon></th>
<th style="width: 150px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
<th style="width: 160px;" i18n pSortableColumn="redundancyAllowed">Redundancy allowed <p-sortIcon field="redundancyAllowed"></p-sortIcon></th>
<my-delete-button label="Unfollow" i18n-label (click)="removeFollowing(follow)"></my-delete-button>
</td>
<td>
- <a [href]="'https://' + follow.following.host" i18n-title title="Open instance in a new tab" target="_blank" rel="noopener noreferrer">
- {{ follow.following.host }}
+ <a [href]="follow.following.url" i18n-title title="Open instance in a new tab" target="_blank" rel="noopener noreferrer">
+ {{ follow.following.name + '@' + follow.following.host }}
<span class="glyphicon glyphicon-new-window"></span>
</a>
</td>
<td>{{ follow.createdAt | date:Â 'short' }}</td>
<td>
<my-redundancy-checkbox
+ *ngIf="isInstanceFollowing(follow)"
[host]="follow.following.host" [redundancyAllowed]="follow.following.hostRedundancyAllowed"
></my-redundancy-checkbox>
</td>
</ng-template>
</p-table>
-<my-batch-domains-modal #batchDomainsModal i18n-action action="Follow domains" (domains)="addFollowing($event)">
- <ng-container ngProjectAs="warning">
- <div i18n *ngIf="httpEnabled() === false" class="alert alert-warning">
- It seems that you are not on a HTTPS server. Your webserver needs to have TLS activated in order to follow servers.
- </div>
- </ng-container>
-</my-batch-domains-modal>
+<my-follow-modal #followModal></my-follow-modal>
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
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) {
+export * from './follow-modal.component'
export * from './following-list.component'
component: FollowingListComponent,
data: {
meta: {
- title: $localize`Following list`
+ title: $localize`Following`
}
}
},
component: FollowersListComponent,
data: {
meta: {
- title: $localize`Followers list`
+ title: $localize`Followers`
}
}
},
+++ /dev/null
-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.`
- }
-}
--- /dev/null
+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.`
+ }
+}
+++ /dev/null
-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)
-}
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'
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()
)
}
- 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)
}
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))
<ng-template #modal>
<div class="modal-header">
- <h4 i18n class="modal-title">{{ action }}</h4>
+ <h4 class="modal-title">{{ action }}</h4>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
</div>
<label i18n for="hosts">1 host (without "http://") per line</label>
<textarea
- [placeholder]="placeholder" formControlName="domains" type="text" id="hosts" name="hosts"
- class="form-control" [ngClass]="{ 'input-error': formErrors['domains'] }" ngbAutofocus
+ [placeholder]="placeholder" formControlName="hosts" type="text" id="hosts" name="hosts"
+ class="form-control" [ngClass]="{ 'input-error': formErrors['hosts'] }" ngbAutofocus
></textarea>
- <div *ngIf="formErrors.domains" class="form-error">
- {{ formErrors.domains }}
+ <div *ngIf="formErrors.hosts" class="form-error">
+ {{ formErrors.hosts }}
- <div *ngIf="form.controls['domains'].errors.validDomains">
- {{ form.controls['domains'].errors.validDomains.value }}
+ <div *ngIf="form.controls['hosts'].errors.validHosts">
+ {{ form.controls['hosts'].errors.validHosts.value }}
</div>
</div>
</div>
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',
if (!this.action) this.action = $localize`Process domains`
this.buildForm({
- domains: DOMAINS_VALIDATOR
+ hosts: UNIQUE_HOSTS_VALIDATOR
})
}
}
submit () {
- this.domains.emit(
- getNotEmptyHosts(this.form.controls['domains'].value)
- )
+ this.domains.emit(splitAndGetNotEmpty(this.form.controls['hosts'].value))
this.form.reset()
this.hide()
}
removeFollowingValidator
} from '../../../middlewares/validators'
import { ActorFollowModel } from '../../../models/actor/actor-follow'
+import { ServerFollowCreate } from '@shared/models'
const serverFollowsRouter = express.Router()
serverFollowsRouter.get('/following',
ensureUserHasRight(UserRight.MANAGE_SERVER_FOLLOW),
followValidator,
setBodyHostsPort,
- asyncMiddleware(followInstance)
+ asyncMiddleware(addFollow)
)
-serverFollowsRouter.delete('/following/:host',
+serverFollowsRouter.delete('/following/:hostOrHandle',
authenticate,
ensureUserHasRight(UserRight.MANAGE_SERVER_FOLLOW),
asyncMiddleware(removeFollowingValidator),
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
-async function followInstance (req: express.Request, res: express.Response) {
- const hosts = req.body.hosts as string[]
+async function addFollow (req: express.Request, res: express.Response) {
+ const { hosts, handles } = req.body as ServerFollowCreate
const follower = await getServerActor()
for (const host of hosts) {
JobQueue.Instance.createJob({ type: 'activitypub-follow', payload })
}
+ for (const handle of handles) {
+ const [ name, host ] = handle.split('@')
+
+ const payload = {
+ host,
+ name,
+ followerActorId: follower.id
+ }
+
+ JobQueue.Instance.createJob({ type: 'activitypub-follow', payload })
+ }
+
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
-import { exists } from './misc'
+import { exists, isArray } from './misc'
import { FollowState } from '@shared/models'
function isFollowStateValid (value: FollowState) {
return value === 'pending' || value === 'accepted'
}
+function isRemoteHandleValid (value: string) {
+ if (!exists(value)) return false
+ if (typeof value !== 'string') return false
+
+ return value.includes('@')
+}
+
+function isEachUniqueHandleValid (handles: string[]) {
+ return isArray(handles) &&
+ handles.every(handle => {
+ return isRemoteHandleValid(handle) && handles.indexOf(handle) === handles.lastIndexOf(handle)
+ })
+}
+
// ---------------------------------------------------------------------------
export {
- isFollowStateValid
+ isFollowStateValid,
+ isRemoteHandleValid,
+ isEachUniqueHandleValid
}
function isEachUniqueHostValid (hosts: string[]) {
return isArray(hosts) &&
- hosts.length !== 0 &&
hosts.every(host => {
return isHostValid(host) && hosts.indexOf(host) === hosts.lastIndexOf(host)
})
+import { retryTransactionWrapper } from '@server/helpers/database-utils'
import * as Bluebird from 'bluebird'
import { URL } from 'url'
import { ActivityPubOrderedCollection } from '../../../shared/models/activitypub'
}
}
- if (cleaner) await cleaner(startDate)
+ if (cleaner) await retryTransactionWrapper(cleaner, startDate)
}
export {
}
}
+// If we only have an host, use a default account handle
+function getRemoteNameAndHost (handleOrHost: string) {
+ let name = SERVER_ACTOR_NAME
+ let host = handleOrHost
+
+ const splitted = handleOrHost.split('@')
+ if (splitted.length === 2) {
+ name = splitted[0]
+ host = splitted[1]
+ }
+
+ return { name, host }
+}
+
export {
- autoFollowBackIfNeeded
+ autoFollowBackIfNeeded,
+ getRemoteNameAndHost
}
import * as express from 'express'
import { body, param, query } from 'express-validator'
-import { isFollowStateValid } from '@server/helpers/custom-validators/follows'
+import { isEachUniqueHandleValid, isFollowStateValid, isRemoteHandleValid } from '@server/helpers/custom-validators/follows'
import { loadActorUrlOrGetFromWebfinger } from '@server/lib/activitypub/actors'
+import { getRemoteNameAndHost } from '@server/lib/activitypub/follow'
import { getServerActor } from '@server/models/application/application'
import { MActorFollowActorsDefault } from '@server/types/models'
import { HttpStatusCode } from '../../../shared/models/http/http-error-codes'
import { isActorTypeValid, isValidActorHandle } from '../../helpers/custom-validators/activitypub/actor'
import { isEachUniqueHostValid, isHostValid } from '../../helpers/custom-validators/servers'
import { logger } from '../../helpers/logger'
-import { SERVER_ACTOR_NAME, WEBSERVER } from '../../initializers/constants'
+import { WEBSERVER } from '../../initializers/constants'
import { ActorModel } from '../../models/actor/actor'
import { ActorFollowModel } from '../../models/actor/actor-follow'
import { areValidationErrors } from './shared'
+import { ServerFollowCreate } from '@shared/models'
const listFollowsValidator = [
query('state')
]
const followValidator = [
- body('hosts').custom(isEachUniqueHostValid).withMessage('Should have an array of unique hosts'),
+ body('hosts')
+ .toArray()
+ .custom(isEachUniqueHostValid).withMessage('Should have an array of unique hosts'),
+
+ body('handles')
+ .toArray()
+ .custom(isEachUniqueHandleValid).withMessage('Should have an array of handles'),
(req: express.Request, res: express.Response, next: express.NextFunction) => {
- // Force https if the administrator wants to make friends
+ // Force https if the administrator wants to follow remote actors
if (isTestInstance() === false && WEBSERVER.SCHEME === 'http') {
return res
.status(HttpStatusCode.INTERNAL_SERVER_ERROR_500)
.json({
error: 'Cannot follow on a non HTTPS web server.'
})
- .end()
}
logger.debug('Checking follow parameters', { parameters: req.body })
if (areValidationErrors(req, res)) return
+ const body: ServerFollowCreate = req.body
+ if (body.hosts.length === 0 && body.handles.length === 0) {
+
+ return res
+ .status(HttpStatusCode.BAD_REQUEST_400)
+ .json({
+ error: 'You must provide at least one handle or one host.'
+ })
+ }
+
return next()
}
]
const removeFollowingValidator = [
- param('host').custom(isHostValid).withMessage('Should have a valid host'),
+ param('hostOrHandle')
+ .custom(value => isHostValid(value) || isRemoteHandleValid(value))
+ .withMessage('Should have a valid host/handle'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking unfollowing parameters', { parameters: req.params })
if (areValidationErrors(req, res)) return
const serverActor = await getServerActor()
- const follow = await ActorFollowModel.loadByActorAndTargetNameAndHostForAPI(serverActor.id, SERVER_ACTOR_NAME, req.params.host)
+
+ const { name, host } = getRemoteNameAndHost(req.params.hostOrHandle)
+ const follow = await ActorFollowModel.loadByActorAndTargetNameAndHostForAPI(serverActor.id, name, host)
if (!follow) {
return res.fail({
status: HttpStatusCode.NOT_FOUND_404,
- message: `Following ${req.params.host} not found.`
+ message: `Follow ${req.params.hostOrHandle} not found.`
})
}
const followWhere = state ? { state } : {}
const followingWhere: WhereOptions = {}
- const followingServerWhere: WhereOptions = {}
if (search) {
- Object.assign(followingServerWhere, {
- host: {
- [Op.iLike]: '%' + search + '%'
- }
+ Object.assign(followWhere, {
+ [Op.or]: [
+ searchAttribute(options.search, '$ActorFollowing.preferredUsername$'),
+ searchAttribute(options.search, '$ActorFollowing.Server.host$')
+ ]
})
}
include: [
{
model: ServerModel,
- required: true,
- where: followingServerWhere
+ required: true
}
]
}
const followWhere = state ? { state } : {}
const followerWhere: WhereOptions = {}
- const followerServerWhere: WhereOptions = {}
if (search) {
- Object.assign(followerServerWhere, {
- host: {
- [Op.iLike]: '%' + search + '%'
- }
+ Object.assign(followWhere, {
+ [Op.or]: [
+ searchAttribute(search, '$ActorFollower.preferredUsername$'),
+ searchAttribute(search, '$ActorFollower.Server.host$')
+ ]
})
}
include: [
{
model: ServerModel,
- required: true,
- where: followerServerWhere
+ required: true
}
]
},
private whereFollowerActorId (followerActorId: number, includeLocalVideos: boolean) {
let query =
'(' +
- ' EXISTS (' +
+ ' EXISTS (' + // Videos shared by actors we follow
' SELECT 1 FROM "videoShare" ' +
' INNER JOIN "actorFollow" "actorFollowShare" ON "actorFollowShare"."targetActorId" = "videoShare"."actorId" ' +
' AND "actorFollowShare"."actorId" = :followerActorId AND "actorFollowShare"."state" = \'accepted\' ' +
' WHERE "videoShare"."videoId" = "video"."id"' +
' )' +
' OR' +
- ' EXISTS (' +
+ ' EXISTS (' + // Videos published by accounts we follow
' SELECT 1 from "actorFollow" ' +
- ' WHERE "actorFollow"."targetActorId" = "videoChannel"."actorId" AND "actorFollow"."actorId" = :followerActorId ' +
+ ' WHERE "actorFollow"."targetActorId" = "account"."actorId" AND "actorFollow"."actorId" = :followerActorId ' +
' AND "actorFollow"."state" = \'accepted\'' +
' )'
let userAccessToken = null
before(async function () {
- const user = {
- username: 'user1',
- password: 'password'
- }
-
- await server.users.create({ username: user.username, password: user.password })
- userAccessToken = await server.login.getAccessToken(user)
+ userAccessToken = await server.users.generateUserAndToken('user1')
})
describe('When adding follows', function () {
const path = '/api/v1/server/following'
- it('Should fail without hosts', async function () {
+ it('Should fail with nothing', async function () {
await makePostBodyRequest({
url: server.url,
path,
})
})
- it('Should fail if hosts is not an array', async function () {
+ it('Should fail if hosts is not composed by hosts', async function () {
await makePostBodyRequest({
url: server.url,
path,
+ fields: { hosts: [ 'localhost:9002', 'localhost:coucou' ] },
token: server.accessToken,
- fields: { hosts: 'localhost:9002' },
expectedStatus: HttpStatusCode.BAD_REQUEST_400
})
})
- it('Should fail if the array is not composed by hosts', async function () {
+ it('Should fail if hosts is composed with http schemes', async function () {
await makePostBodyRequest({
url: server.url,
path,
- fields: { hosts: [ 'localhost:9002', 'localhost:coucou' ] },
+ fields: { hosts: [ 'localhost:9002', 'http://localhost:9003' ] },
token: server.accessToken,
expectedStatus: HttpStatusCode.BAD_REQUEST_400
})
})
- it('Should fail if the array is composed with http schemes', async function () {
+ it('Should fail if hosts are not unique', async function () {
await makePostBodyRequest({
url: server.url,
path,
- fields: { hosts: [ 'localhost:9002', 'http://localhost:9003' ] },
+ fields: { urls: [ 'localhost:9002', 'localhost:9002' ] },
token: server.accessToken,
expectedStatus: HttpStatusCode.BAD_REQUEST_400
})
})
- it('Should fail if hosts are not unique', async function () {
+ it('Should fail if handles is not composed by handles', async function () {
await makePostBodyRequest({
url: server.url,
path,
- fields: { urls: [ 'localhost:9002', 'localhost:9002' ] },
+ fields: { handles: [ 'hello@example.com', 'localhost:9001' ] },
+ token: server.accessToken,
+ expectedStatus: HttpStatusCode.BAD_REQUEST_400
+ })
+ })
+
+ it('Should fail if handles are not unique', async function () {
+ await makePostBodyRequest({
+ url: server.url,
+ path,
+ fields: { urls: [ 'hello@example.com', 'hello@example.com' ] },
token: server.accessToken,
expectedStatus: HttpStatusCode.BAD_REQUEST_400
})
const now = new Date()
await servers[1].follows.unfollow({ target: servers[0] })
await waitJobs(servers)
- await servers[1].follows.follow({ targets: [ servers[0].host ] })
+ await servers[1].follows.follow({ hosts: [ servers[0].host ] })
await waitJobs(servers)
const now = new Date()
await servers[1].follows.unfollow({ target: servers[0] })
await waitJobs(servers)
- await servers[1].follows.follow({ targets: [ servers[0].host ] })
+ await servers[1].follows.follow({ hosts: [ servers[0].host ] })
await waitJobs(servers)
it('Should send a notification only to admin when there is a new instance follower', async function () {
this.timeout(20000)
- await servers[2].follows.follow({ targets: [ servers[0].url ] })
+ await servers[2].follows.follow({ hosts: [ servers[0].url ] })
await waitJobs(servers)
}
await servers[0].config.updateCustomSubConfig({ newConfig: config })
- await servers[2].follows.follow({ targets: [ servers[0].url ] })
+ await servers[2].follows.follow({ hosts: [ servers[0].url ] })
await waitJobs(servers)
await waitJobs(servers)
// Server 1 and server 2 follow each other
- await remoteServer.follows.follow({ targets: [ localServer.url ] })
+ await remoteServer.follows.follow({ hosts: [ localServer.url ] })
await waitJobs(servers)
await remoteServer.redundancy.updateRedundancy({ host: localServer.host, redundancyAllowed: true })
it('Should have redundancy on server 1 and on server 2 with followings filter now server 2 follows server 1', async function () {
this.timeout(120000)
- await localServer.follows.follow({ targets: [ remoteServer.url ] })
+ await localServer.follows.follow({ hosts: [ remoteServer.url ] })
await waitJobs(servers)
await uploadWrapper('video 4 server 2')
}
async function server1Follows2 (servers: PeerTubeServer[]) {
- await servers[0].follows.follow({ targets: [ servers[1].host ] })
+ await servers[0].follows.follow({ hosts: [ servers[1].host ] })
await waitJobs(servers)
}
it('Should have server 1 following server 2', async function () {
this.timeout(30000)
- await commands[0].follow({ targets: [ servers[1].url ] })
+ await commands[0].follow({ hosts: [ servers[1].url ] })
await waitJobs(servers)
})
await servers[1].config.updateCustomSubConfig({ newConfig: subConfig })
- await commands[0].follow({ targets: [ servers[1].url ] })
+ await commands[0].follow({ hosts: [ servers[1].url ] })
await waitJobs(servers)
await checkNoFollowers(servers)
await servers[1].config.updateCustomSubConfig({ newConfig: subConfig })
- await commands[0].follow({ targets: [ servers[1].url ] })
+ await commands[0].follow({ hosts: [ servers[1].url ] })
await waitJobs(servers)
await checkServer1And2HasFollowers(servers)
await servers[1].config.updateCustomSubConfig({ newConfig: subConfig })
await servers[2].config.updateCustomSubConfig({ newConfig: subConfig })
- await commands[0].follow({ targets: [ servers[1].url ] })
+ await commands[0].follow({ hosts: [ servers[1].url ] })
await waitJobs(servers)
await checkServer1And2HasFollowers(servers, 'pending')
it('Should reject another follower', async function () {
this.timeout(20000)
- await commands[0].follow({ targets: [ servers[2].url ] })
+ await commands[0].follow({ hosts: [ servers[2].url ] })
await waitJobs(servers)
{
createMultipleServers,
dateIsValid,
expectAccountFollows,
- FollowsCommand,
+ expectChannelsFollows,
PeerTubeServer,
setAccessTokensToServers,
testCaptionFile,
waitJobs
} from '@shared/extra-utils'
-import { Video, VideoPrivacy } from '@shared/models'
+import { VideoCreateResult, VideoPrivacy } from '@shared/models'
const expect = chai.expect
describe('Test follows', function () {
let servers: PeerTubeServer[] = []
- let followsCommands: FollowsCommand[]
before(async function () {
this.timeout(30000)
servers = await createMultipleServers(3)
- followsCommands = servers.map(s => s.follows)
// Get the access tokens
await setAccessTokensToServers(servers)
})
- it('Should not have followers', async function () {
- for (const server of servers) {
- const body = await server.follows.getFollowers({ start: 0, count: 5, sort: 'createdAt' })
- expect(body.total).to.equal(0)
-
- const follows = body.data
- expect(follows).to.be.an('array')
- expect(follows.length).to.equal(0)
- }
- })
+ describe('Data propagation after follow', function () {
- it('Should not have following', async function () {
- for (const server of servers) {
- const body = await server.follows.getFollowings({ start: 0, count: 5, sort: 'createdAt' })
- expect(body.total).to.equal(0)
+ it('Should not have followers/followings', async function () {
+ for (const server of servers) {
+ const bodies = await Promise.all([
+ server.follows.getFollowers({ start: 0, count: 5, sort: 'createdAt' }),
+ server.follows.getFollowings({ start: 0, count: 5, sort: 'createdAt' })
+ ])
- const follows = body.data
- expect(follows).to.be.an('array')
- expect(follows.length).to.equal(0)
- }
- })
+ for (const body of bodies) {
+ expect(body.total).to.equal(0)
- it('Should have server 1 following server 2 and 3', async function () {
- this.timeout(30000)
+ const follows = body.data
+ expect(follows).to.be.an('array')
+ expect(follows).to.have.lengthOf(0)
+ }
+ }
+ })
- await followsCommands[0].follow({ targets: [ servers[1].url, servers[2].url ] })
+ it('Should have server 1 following root account of server 2 and server 3', async function () {
+ this.timeout(30000)
- await waitJobs(servers)
- })
+ await servers[0].follows.follow({
+ hosts: [ servers[2].url ],
+ handles: [ 'root@' + servers[1].host ]
+ })
- it('Should have 2 followings on server 1', async function () {
- const body = await followsCommands[0].getFollowings({ start: 0, count: 1, sort: 'createdAt' })
- expect(body.total).to.equal(2)
+ await waitJobs(servers)
+ })
- let follows = body.data
- expect(follows).to.be.an('array')
- expect(follows.length).to.equal(1)
+ it('Should have 2 followings on server 1', async function () {
+ const body = await servers[0].follows.getFollowings({ start: 0, count: 1, sort: 'createdAt' })
+ expect(body.total).to.equal(2)
- const body2 = await followsCommands[0].getFollowings({ start: 1, count: 1, sort: 'createdAt' })
- follows = follows.concat(body2.data)
+ let follows = body.data
+ expect(follows).to.be.an('array')
+ expect(follows).to.have.lengthOf(1)
- const server2Follow = follows.find(f => f.following.host === 'localhost:' + servers[1].port)
- const server3Follow = follows.find(f => f.following.host === 'localhost:' + servers[2].port)
+ const body2 = await servers[0].follows.getFollowings({ start: 1, count: 1, sort: 'createdAt' })
+ follows = follows.concat(body2.data)
- expect(server2Follow).to.not.be.undefined
- expect(server3Follow).to.not.be.undefined
- expect(server2Follow.state).to.equal('accepted')
- expect(server3Follow.state).to.equal('accepted')
- })
+ const server2Follow = follows.find(f => f.following.host === servers[1].host)
+ const server3Follow = follows.find(f => f.following.host === servers[2].host)
- it('Should search/filter followings on server 1', async function () {
- const sort = 'createdAt'
- const start = 0
- const count = 1
+ expect(server2Follow).to.not.be.undefined
+ expect(server2Follow.following.name).to.equal('root')
+ expect(server2Follow.state).to.equal('accepted')
- {
- const search = ':' + servers[1].port
+ expect(server3Follow).to.not.be.undefined
+ expect(server3Follow.following.name).to.equal('peertube')
+ expect(server3Follow.state).to.equal('accepted')
+ })
- {
- const body = await followsCommands[0].getFollowings({ start, count, sort, search })
- expect(body.total).to.equal(1)
+ it('Should have 0 followings on server 2 and 3', async function () {
+ for (const server of [ servers[1], servers[2] ]) {
+ const body = await server.follows.getFollowings({ start: 0, count: 5, sort: 'createdAt' })
+ expect(body.total).to.equal(0)
const follows = body.data
- expect(follows.length).to.equal(1)
- expect(follows[0].following.host).to.equal('localhost:' + servers[1].port)
+ expect(follows).to.be.an('array')
+ expect(follows).to.have.lengthOf(0)
}
+ })
- {
- const body = await followsCommands[0].getFollowings({ start, count, sort, search, state: 'accepted' })
- expect(body.total).to.equal(1)
- expect(body.data).to.have.lengthOf(1)
+ it('Should have 1 followers on server 3', async function () {
+ const body = await servers[2].follows.getFollowers({ start: 0, count: 1, sort: 'createdAt' })
+ expect(body.total).to.equal(1)
+
+ const follows = body.data
+ expect(follows).to.be.an('array')
+ expect(follows).to.have.lengthOf(1)
+ expect(follows[0].follower.host).to.equal('localhost:' + servers[0].port)
+ })
+
+ it('Should have 0 followers on server 1 and 2', async function () {
+ for (const server of [ servers[0], servers[1] ]) {
+ const body = await server.follows.getFollowers({ start: 0, count: 5, sort: 'createdAt' })
+ expect(body.total).to.equal(0)
+
+ const follows = body.data
+ expect(follows).to.be.an('array')
+ expect(follows).to.have.lengthOf(0)
}
+ })
+
+ it('Should search/filter followings on server 1', async function () {
+ const sort = 'createdAt'
+ const start = 0
+ const count = 1
{
- const body = await followsCommands[0].getFollowings({ start, count, sort, search, state: 'accepted', actorType: 'Person' })
- expect(body.total).to.equal(0)
- expect(body.data).to.have.lengthOf(0)
+ const search = ':' + servers[1].port
+
+ {
+ const body = await servers[0].follows.getFollowings({ start, count, sort, search })
+ expect(body.total).to.equal(1)
+
+ const follows = body.data
+ expect(follows).to.have.lengthOf(1)
+ expect(follows[0].following.host).to.equal(servers[1].host)
+ }
+
+ {
+ const body = await servers[0].follows.getFollowings({ start, count, sort, search, state: 'accepted' })
+ expect(body.total).to.equal(1)
+ expect(body.data).to.have.lengthOf(1)
+ }
+
+ {
+ const body = await servers[0].follows.getFollowings({ start, count, sort, search, state: 'accepted', actorType: 'Person' })
+ expect(body.total).to.equal(1)
+ expect(body.data).to.have.lengthOf(1)
+ }
+
+ {
+ const body = await servers[0].follows.getFollowings({
+ start,
+ count,
+ sort,
+ search,
+ state: 'accepted',
+ actorType: 'Application'
+ })
+ expect(body.total).to.equal(0)
+ expect(body.data).to.have.lengthOf(0)
+ }
+
+ {
+ const body = await servers[0].follows.getFollowings({ start, count, sort, search, state: 'pending' })
+ expect(body.total).to.equal(0)
+ expect(body.data).to.have.lengthOf(0)
+ }
}
{
- const body = await followsCommands[0].getFollowings({
- start,
- count,
- sort,
- search,
- state: 'accepted',
- actorType: 'Application'
- })
+ const body = await servers[0].follows.getFollowings({ start, count, sort, search: 'root' })
expect(body.total).to.equal(1)
expect(body.data).to.have.lengthOf(1)
}
{
- const body = await followsCommands[0].getFollowings({ start, count, sort, search, state: 'pending' })
+ const body = await servers[0].follows.getFollowings({ start, count, sort, search: 'bla' })
expect(body.total).to.equal(0)
+
expect(body.data).to.have.lengthOf(0)
}
- }
+ })
- {
- const body = await followsCommands[0].getFollowings({ start, count, sort, search: 'bla' })
- expect(body.total).to.equal(0)
+ it('Should search/filter followers on server 2', async function () {
+ const start = 0
+ const count = 5
+ const sort = 'createdAt'
- expect(body.data.length).to.equal(0)
- }
- })
+ {
+ const search = servers[0].port + ''
- it('Should have 0 followings on server 2 and 3', async function () {
- for (const server of [ servers[1], servers[2] ]) {
- const body = await server.follows.getFollowings({ start: 0, count: 5, sort: 'createdAt' })
- expect(body.total).to.equal(0)
+ {
+ const body = await servers[2].follows.getFollowers({ start, count, sort, search })
+ expect(body.total).to.equal(1)
- const follows = body.data
- expect(follows).to.be.an('array')
- expect(follows.length).to.equal(0)
- }
- })
+ const follows = body.data
+ expect(follows).to.have.lengthOf(1)
+ expect(follows[0].following.host).to.equal(servers[2].host)
+ }
- it('Should have 1 followers on server 2 and 3', async function () {
- for (const server of [ servers[1], servers[2] ]) {
- const body = await server.follows.getFollowers({ start: 0, count: 1, sort: 'createdAt' })
- expect(body.total).to.equal(1)
+ {
+ const body = await servers[2].follows.getFollowers({ start, count, sort, search, state: 'accepted' })
+ expect(body.total).to.equal(1)
+ expect(body.data).to.have.lengthOf(1)
+ }
- const follows = body.data
- expect(follows).to.be.an('array')
- expect(follows.length).to.equal(1)
- expect(follows[0].follower.host).to.equal('localhost:' + servers[0].port)
- }
- })
+ {
+ const body = await servers[2].follows.getFollowers({ start, count, sort, search, state: 'accepted', actorType: 'Person' })
+ expect(body.total).to.equal(0)
+ expect(body.data).to.have.lengthOf(0)
+ }
- it('Should search/filter followers on server 2', async function () {
- const start = 0
- const count = 5
- const sort = 'createdAt'
+ {
+ const body = await servers[2].follows.getFollowers({
+ start,
+ count,
+ sort,
+ search,
+ state: 'accepted',
+ actorType: 'Application'
+ })
+ expect(body.total).to.equal(1)
+ expect(body.data).to.have.lengthOf(1)
+ }
- {
- const search = servers[0].port + ''
+ {
+ const body = await servers[2].follows.getFollowers({ start, count, sort, search, state: 'pending' })
+ expect(body.total).to.equal(0)
+ expect(body.data).to.have.lengthOf(0)
+ }
+ }
{
- const body = await followsCommands[2].getFollowers({ start, count, sort, search })
- expect(body.total).to.equal(1)
+ const body = await servers[2].follows.getFollowers({ start, count, sort, search: 'bla' })
+ expect(body.total).to.equal(0)
const follows = body.data
- expect(follows.length).to.equal(1)
- expect(follows[0].following.host).to.equal('localhost:' + servers[2].port)
+ expect(follows).to.have.lengthOf(0)
}
+ })
- {
- const body = await followsCommands[2].getFollowers({ start, count, sort, search, state: 'accepted' })
- expect(body.total).to.equal(1)
- expect(body.data).to.have.lengthOf(1)
- }
+ it('Should have the correct follows counts', async function () {
+ await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[0].host, followers: 0, following: 2 })
+ await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 1, following: 0 })
+ await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 })
- {
- const body = await followsCommands[2].getFollowers({ start, count, sort, search, state: 'accepted', actorType: 'Person' })
- expect(body.total).to.equal(0)
- expect(body.data).to.have.lengthOf(0)
- }
+ // Server 2 and 3 does not know server 1 follow another server (there was not a refresh)
+ await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 })
+ await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 1, following: 0 })
+ await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[1].host, followers: 0, following: 0 })
- {
- const body = await followsCommands[2].getFollowers({
- start,
- count,
- sort,
- search,
- state: 'accepted',
- actorType: 'Application'
- })
- expect(body.total).to.equal(1)
- expect(body.data).to.have.lengthOf(1)
- }
+ await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 })
+ await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 })
+ })
- {
- const body = await followsCommands[2].getFollowers({ start, count, sort, search, state: 'pending' })
- expect(body.total).to.equal(0)
- expect(body.data).to.have.lengthOf(0)
- }
- }
+ it('Should unfollow server 3 on server 1', async function () {
+ this.timeout(15000)
- {
- const body = await followsCommands[2].getFollowers({ start, count, sort, search: 'bla' })
- expect(body.total).to.equal(0)
+ await servers[0].follows.unfollow({ target: servers[2] })
+
+ await waitJobs(servers)
+ })
+
+ it('Should not follow server 3 on server 1 anymore', async function () {
+ const body = await servers[0].follows.getFollowings({ start: 0, count: 2, sort: 'createdAt' })
+ expect(body.total).to.equal(1)
const follows = body.data
- expect(follows.length).to.equal(0)
- }
- })
+ expect(follows).to.be.an('array')
+ expect(follows).to.have.lengthOf(1)
- it('Should have 0 followers on server 1', async function () {
- const body = await followsCommands[0].getFollowers({ start: 0, count: 5, sort: 'createdAt' })
- expect(body.total).to.equal(0)
+ expect(follows[0].following.host).to.equal(servers[1].host)
+ })
- const follows = body.data
- expect(follows).to.be.an('array')
- expect(follows.length).to.equal(0)
- })
+ it('Should not have server 1 as follower on server 3 anymore', async function () {
+ const body = await servers[2].follows.getFollowers({ start: 0, count: 1, sort: 'createdAt' })
+ expect(body.total).to.equal(0)
- it('Should have the correct follows counts', async function () {
- await expectAccountFollows({ server: servers[0], handle: 'peertube@localhost:' + servers[0].port, followers: 0, following: 2 })
- await expectAccountFollows({ server: servers[0], handle: 'peertube@localhost:' + servers[1].port, followers: 1, following: 0 })
- await expectAccountFollows({ server: servers[0], handle: 'peertube@localhost:' + servers[2].port, followers: 1, following: 0 })
+ const follows = body.data
+ expect(follows).to.be.an('array')
+ expect(follows).to.have.lengthOf(0)
+ })
- // Server 2 and 3 does not know server 1 follow another server (there was not a refresh)
- await expectAccountFollows({ server: servers[1], handle: 'peertube@localhost:' + servers[0].port, followers: 0, following: 1 })
- await expectAccountFollows({ server: servers[1], handle: 'peertube@localhost:' + servers[1].port, followers: 1, following: 0 })
+ it('Should have the correct follows counts after the unfollow', async function () {
+ await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 })
+ await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 1, following: 0 })
+ await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[2].host, followers: 0, following: 0 })
- await expectAccountFollows({ server: servers[2], handle: 'peertube@localhost:' + servers[0].port, followers: 0, following: 1 })
- await expectAccountFollows({ server: servers[2], handle: 'peertube@localhost:' + servers[2].port, followers: 1, following: 0 })
- })
+ await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 })
+ await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 1, following: 0 })
+ await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[1].host, followers: 0, following: 0 })
- it('Should unfollow server 3 on server 1', async function () {
- this.timeout(5000)
+ await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[0].host, followers: 0, following: 0 })
+ await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[2].host, followers: 0, following: 0 })
+ })
- await followsCommands[0].unfollow({ target: servers[2] })
+ it('Should upload a video on server 2 and 3 and propagate only the video of server 2', async function () {
+ this.timeout(60000)
- await waitJobs(servers)
- })
+ await servers[1].videos.upload({ attributes: { name: 'server2' } })
+ await servers[2].videos.upload({ attributes: { name: 'server3' } })
- it('Should not follow server 3 on server 1 anymore', async function () {
- const body = await followsCommands[0].getFollowings({ start: 0, count: 2, sort: 'createdAt' })
- expect(body.total).to.equal(1)
+ await waitJobs(servers)
- const follows = body.data
- expect(follows).to.be.an('array')
- expect(follows.length).to.equal(1)
+ {
+ const { total, data } = await servers[0].videos.list()
+ expect(total).to.equal(1)
+ expect(data[0].name).to.equal('server2')
+ }
- expect(follows[0].following.host).to.equal('localhost:' + servers[1].port)
- })
+ {
+ const { total, data } = await servers[1].videos.list()
+ expect(total).to.equal(1)
+ expect(data[0].name).to.equal('server2')
+ }
- it('Should not have server 1 as follower on server 3 anymore', async function () {
- const body = await followsCommands[2].getFollowers({ start: 0, count: 1, sort: 'createdAt' })
- expect(body.total).to.equal(0)
+ {
+ const { total, data } = await servers[2].videos.list()
+ expect(total).to.equal(1)
+ expect(data[0].name).to.equal('server3')
+ }
+ })
- const follows = body.data
- expect(follows).to.be.an('array')
- expect(follows.length).to.equal(0)
- })
+ it('Should remove account follow', async function () {
+ this.timeout(15000)
- it('Should have the correct follows counts 2', async function () {
- await expectAccountFollows({ server: servers[0], handle: 'peertube@localhost:' + servers[0].port, followers: 0, following: 1 })
- await expectAccountFollows({ server: servers[0], handle: 'peertube@localhost:' + servers[1].port, followers: 1, following: 0 })
+ await servers[0].follows.unfollow({ target: 'root@' + servers[1].host })
- await expectAccountFollows({ server: servers[1], handle: 'peertube@localhost:' + servers[0].port, followers: 0, following: 1 })
- await expectAccountFollows({ server: servers[1], handle: 'peertube@localhost:' + servers[1].port, followers: 1, following: 0 })
+ await waitJobs(servers)
+ })
- await expectAccountFollows({ server: servers[2], handle: 'peertube@localhost:' + servers[0].port, followers: 0, following: 0 })
- await expectAccountFollows({ server: servers[2], handle: 'peertube@localhost:' + servers[2].port, followers: 0, following: 0 })
- })
+ it('Should have removed the account follow', async function () {
+ await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 0, following: 0 })
+ await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 0, following: 0 })
- it('Should upload a video on server 2 and 3 and propagate only the video of server 2', async function () {
- this.timeout(60000)
+ {
+ const { total, data } = await servers[0].follows.getFollowings()
+ expect(total).to.equal(0)
+ expect(data).to.have.lengthOf(0)
+ }
- await servers[1].videos.upload({ attributes: { name: 'server2' } })
- await servers[2].videos.upload({ attributes: { name: 'server3' } })
+ {
+ const { total, data } = await servers[0].videos.list()
+ expect(total).to.equal(0)
+ expect(data).to.have.lengthOf(0)
+ }
+ })
- await waitJobs(servers)
+ it('Should follow a channel', async function () {
+ this.timeout(15000)
- {
- const { total, data } = await servers[0].videos.list()
- expect(total).to.equal(1)
- expect(data[0].name).to.equal('server2')
- }
+ await servers[0].follows.follow({
+ handles: [ 'root_channel@' + servers[1].host ]
+ })
- {
- const { total, data } = await servers[1].videos.list()
- expect(total).to.equal(1)
- expect(data[0].name).to.equal('server2')
- }
+ await waitJobs(servers)
- {
- const { total, data } = await servers[2].videos.list()
- expect(total).to.equal(1)
- expect(data[0].name).to.equal('server3')
- }
+ await expectChannelsFollows({ server: servers[0], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 })
+ await expectChannelsFollows({ server: servers[1], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 })
+
+ {
+ const { total, data } = await servers[0].follows.getFollowings()
+ expect(total).to.equal(1)
+ expect(data).to.have.lengthOf(1)
+ }
+
+ {
+ const { total, data } = await servers[0].videos.list()
+ expect(total).to.equal(1)
+ expect(data).to.have.lengthOf(1)
+ }
+ })
})
- describe('Should propagate data on a new following', function () {
- let video4: Video
+ describe('Should propagate data on a new server follow', function () {
+ let video4: VideoCreateResult
before(async function () {
this.timeout(50000)
await servers[2].videos.upload({ attributes: { name: 'server3-2' } })
await servers[2].videos.upload({ attributes: { name: 'server3-3' } })
- await servers[2].videos.upload({ attributes: video4Attributes })
+ video4 = await servers[2].videos.upload({ attributes: video4Attributes })
await servers[2].videos.upload({ attributes: { name: 'server3-5' } })
await servers[2].videos.upload({ attributes: { name: 'server3-6' } })
{
const userAccessToken = await servers[2].users.generateUserAndToken('captain')
- const { data } = await servers[2].videos.list()
- video4 = data.find(v => v.name === 'server3-4')
-
- {
- await servers[2].videos.rate({ id: video4.id, rating: 'like' })
- await servers[2].videos.rate({ token: userAccessToken, id: video4.id, rating: 'dislike' })
- }
-
- {
- {
- const text = 'my super first comment'
- const created = await servers[2].comments.createThread({ videoId: video4.id, text })
- const threadId = created.id
-
- const text1 = 'my super answer to thread 1'
- const childComment = await servers[2].comments.addReply({ videoId: video4.id, toCommentId: threadId, text: text1 })
-
- const text2 = 'my super answer to answer of thread 1'
- await servers[2].comments.addReply({ videoId: video4.id, toCommentId: childComment.id, text: text2 })
-
- const text3 = 'my second answer to thread 1'
- await servers[2].comments.addReply({ videoId: video4.id, toCommentId: threadId, text: text3 })
- }
+ await servers[2].videos.rate({ id: video4.id, rating: 'like' })
+ await servers[2].videos.rate({ token: userAccessToken, id: video4.id, rating: 'dislike' })
+ }
- {
- const text = 'will be deleted'
- const created = await servers[2].comments.createThread({ videoId: video4.id, text })
- const threadId = created.id
+ {
+ await servers[2].comments.createThread({ videoId: video4.id, text: 'my super first comment' })
- const text1 = 'answer to deleted'
- await servers[2].comments.addReply({ videoId: video4.id, toCommentId: threadId, text: text1 })
+ await servers[2].comments.addReplyToLastThread({ text: 'my super answer to thread 1' })
+ await servers[2].comments.addReplyToLastReply({ text: 'my super answer to answer of thread 1' })
+ await servers[2].comments.addReplyToLastThread({ text: 'my second answer to thread 1' })
+ }
- const text2 = 'will also be deleted'
- const childComment = await servers[2].comments.addReply({ videoId: video4.id, toCommentId: threadId, text: text2 })
+ {
+ const { id: threadId } = await servers[2].comments.createThread({ videoId: video4.id, text: 'will be deleted' })
+ await servers[2].comments.addReplyToLastThread({ text: 'answer to deleted' })
- const text3 = 'my second answer to deleted'
- await servers[2].comments.addReply({ videoId: video4.id, toCommentId: childComment.id, text: text3 })
+ const { id: replyId } = await servers[2].comments.addReplyToLastThread({ text: 'will also be deleted' })
- await servers[2].comments.delete({ videoId: video4.id, commentId: threadId })
- await servers[2].comments.delete({ videoId: video4.id, commentId: childComment.id })
- }
- }
+ await servers[2].comments.addReplyToLastReply({ text: 'my second answer to deleted' })
- {
- await servers[2].captions.createVideoCaption({
- language: 'ar',
- videoId: video4.id,
- fixture: 'subtitle-good2.vtt'
- })
- }
+ await servers[2].comments.delete({ videoId: video4.id, commentId: threadId })
+ await servers[2].comments.delete({ videoId: video4.id, commentId: replyId })
}
+ await servers[2].captions.createVideoCaption({
+ language: 'ar',
+ videoId: video4.id,
+ fixture: 'subtitle-good2.vtt'
+ })
+
await waitJobs(servers)
// Server 1 follows server 3
- await followsCommands[0].follow({ targets: [ servers[2].url ] })
+ await servers[0].follows.follow({ hosts: [ servers[2].url ] })
await waitJobs(servers)
})
- it('Should have the correct follows counts 3', async function () {
- await expectAccountFollows({ server: servers[0], handle: 'peertube@localhost:' + servers[0].port, followers: 0, following: 2 })
- await expectAccountFollows({ server: servers[0], handle: 'peertube@localhost:' + servers[1].port, followers: 1, following: 0 })
- await expectAccountFollows({ server: servers[0], handle: 'peertube@localhost:' + servers[2].port, followers: 1, following: 0 })
+ it('Should have the correct follows counts', async function () {
+ await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[0].host, followers: 0, following: 2 })
+ await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 0, following: 0 })
+ await expectChannelsFollows({ server: servers[0], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 })
+ await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 })
- await expectAccountFollows({ server: servers[1], handle: 'peertube@localhost:' + servers[0].port, followers: 0, following: 1 })
- await expectAccountFollows({ server: servers[1], handle: 'peertube@localhost:' + servers[1].port, followers: 1, following: 0 })
+ await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 })
+ await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[1].host, followers: 0, following: 0 })
+ await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 0, following: 0 })
+ await expectChannelsFollows({ server: servers[1], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 })
- await expectAccountFollows({ server: servers[2], handle: 'peertube@localhost:' + servers[0].port, followers: 0, following: 1 })
- await expectAccountFollows({ server: servers[2], handle: 'peertube@localhost:' + servers[2].port, followers: 1, following: 0 })
+ await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 })
+ await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 })
})
it('Should have propagated videos', async function () {
support: 'my super support text',
account: {
name: 'root',
- host: 'localhost:' + servers[2].port
+ host: servers[2].host
},
isLocal,
commentsEnabled: true,
expect(comment.videoId).to.equal(video4.id)
expect(comment.id).to.equal(comment.threadId)
expect(comment.account.name).to.equal('root')
- expect(comment.account.host).to.equal('localhost:' + servers[2].port)
+ expect(comment.account.host).to.equal(servers[2].host)
expect(comment.totalReplies).to.equal(3)
expect(dateIsValid(comment.createdAt as string)).to.be.true
expect(dateIsValid(comment.updatedAt as string)).to.be.true
it('Should unfollow server 3 on server 1 and does not list server 3 videos', async function () {
this.timeout(5000)
- await followsCommands[0].unfollow({ target: servers[2] })
+ await servers[0].follows.unfollow({ target: servers[2] })
await waitJobs(servers)
const { total } = await servers[0].videos.list()
expect(total).to.equal(1)
})
+ })
+
+ describe('Should propagate data on a new channel follow', function () {
+
+ before(async function () {
+ this.timeout(60000)
+ await servers[2].videos.upload({ attributes: { name: 'server3-7' } })
+
+ await waitJobs(servers)
+
+ const video = await servers[0].videos.find({ name: 'server3-7' })
+ expect(video).to.not.exist
+ })
+
+ it('Should have propagated channel video', async function () {
+ this.timeout(60000)
+
+ await servers[0].follows.follow({ handles: [ 'root_channel@' + servers[2].host ] })
+
+ await waitJobs(servers)
+
+ const video = await servers[0].videos.find({ name: 'server3-7' })
+
+ expect(video).to.exist
+ })
})
after(async function () {
this.timeout(240000)
// Server 2 and 3 follow server 1
- await servers[1].follows.follow({ targets: [ servers[0].url ] })
- await servers[2].follows.follow({ targets: [ servers[0].url ] })
+ await servers[1].follows.follow({ hosts: [ servers[0].url ] })
+ await servers[2].follows.follow({ hosts: [ servers[0].url ] })
await waitJobs(servers)
await servers[1].follows.unfollow({ target: servers[0] })
await waitJobs(servers)
- await servers[1].follows.follow({ targets: [ servers[0].url ] })
+ await servers[1].follows.follow({ hosts: [ servers[0].url ] })
await waitJobs(servers)
// Wait the video views repeatable job
await wait(8000)
- await servers[2].follows.follow({ targets: [ servers[0].url ] })
+ await servers[2].follows.follow({ hosts: [ servers[0].url ] })
await waitJobs(servers)
})
it('Should have server 1 follow server 3 and display server 3 videos', async function () {
this.timeout(60000)
- await servers[0].follows.follow({ targets: [ servers[2].url ] })
+ await servers[0].follows.follow({ hosts: [ servers[2].url ] })
await waitJobs(servers)
token = 'my_super_token'
await server.follows.follow({
- targets: [ 'http://example.com' ],
+ hosts: [ 'http://example.com' ],
token,
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
})
import { pick } from 'lodash'
-import { ActivityPubActorType, ActorFollow, FollowState, HttpStatusCode, ResultList } from '@shared/models'
+import { ActivityPubActorType, ActorFollow, FollowState, HttpStatusCode, ResultList, ServerFollowCreate } from '@shared/models'
import { AbstractCommand, OverrideCommandOptions } from '../shared'
import { PeerTubeServer } from './server'
}
getFollowings (options: OverrideCommandOptions & {
- start: number
- count: number
- sort: string
+ start?: number
+ count?: number
+ sort?: string
search?: string
actorType?: ActivityPubActorType
state?: FollowState
- }) {
+ } = {}) {
const path = '/api/v1/server/following'
const toPick = [ 'start', 'count', 'sort', 'search', 'state', 'actorType' ]
}
follow (options: OverrideCommandOptions & {
- targets: string[]
+ hosts?: string[]
+ handles?: string[]
}) {
const path = '/api/v1/server/following'
- const hosts = options.targets.map(f => f.replace(/^http:\/\//, ''))
+ const fields: ServerFollowCreate = {}
+
+ if (options.hosts) {
+ fields.hosts = options.hosts.map(f => f.replace(/^http:\/\//, ''))
+ }
+
+ if (options.handles) {
+ fields.handles = options.handles
+ }
return this.postBodyRequest({
...options,
path,
- fields: { hosts },
+ fields,
implicitToken: true,
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
})
}
async unfollow (options: OverrideCommandOptions & {
- target: PeerTubeServer
+ target: PeerTubeServer | string
}) {
- const path = '/api/v1/server/following/' + options.target.host
+ const { target } = options
+
+ const handle = typeof target === 'string'
+ ? target
+ : target.host
+
+ const path = '/api/v1/server/following/' + handle
return this.deleteRequest({
...options,
async function doubleFollow (server1: PeerTubeServer, server2: PeerTubeServer) {
await Promise.all([
- server1.follows.follow({ targets: [ server2.url ] }),
- server2.follows.follow({ targets: [ server1.url ] })
+ server1.follows.follow({ hosts: [ server2.url ] }),
+ server2.follows.follow({ hosts: [ server1.url ] })
])
// Wait request propagation
import { pathExists, readdir } from 'fs-extra'
import { join } from 'path'
import { root } from '@server/helpers/core-utils'
+import { Account, VideoChannel } from '@shared/models'
import { PeerTubeServer } from '../server'
-async function expectAccountFollows (options: {
+async function expectChannelsFollows (options: {
server: PeerTubeServer
handle: string
followers: number
following: number
}) {
- const { server, handle, followers, following } = options
+ const { server } = options
+ const { data } = await server.channels.list()
- const body = await server.accounts.list()
- const account = body.data.find(a => a.name + '@' + a.host === handle)
+ return expectActorFollow({ ...options, data })
+}
- const message = `${handle} on ${server.url}`
- expect(account.followersCount).to.equal(followers, message)
- expect(account.followingCount).to.equal(following, message)
+async function expectAccountFollows (options: {
+ server: PeerTubeServer
+ handle: string
+ followers: number
+ following: number
+}) {
+ const { server } = options
+ const { data } = await server.accounts.list()
+
+ return expectActorFollow({ ...options, data })
}
async function checkActorFilesWereRemoved (filename: string, serverNumber: number) {
export {
expectAccountFollows,
+ expectChannelsFollows,
checkActorFilesWereRemoved
}
+
+// ---------------------------------------------------------------------------
+
+function expectActorFollow (options: {
+ server: PeerTubeServer
+ data: (Account | VideoChannel)[]
+ handle: string
+ followers: number
+ following: number
+}) {
+ const { server, data, handle, followers, following } = options
+
+ const actor = data.find(a => a.name + '@' + a.host === handle)
+ const message = `${handle} on ${server.url}`
+
+ expect(actor, message).to.exist
+ expect(actor.followersCount).to.equal(followers, message)
+ expect(actor.followingCount).to.equal(following, message)
+}
export * from './accounts-command'
-export * from './accounts'
+export * from './actors'
export * from './blocklist-command'
export * from './login'
export * from './login-command'
export class CommentsCommand extends AbstractCommand {
+ private lastVideoId: number | string
+ private lastThreadId: number
+ private lastReplyId: number
+
listForAdmin (options: OverrideCommandOptions & {
start?: number
count?: number
defaultExpectedStatus: HttpStatusCode.OK_200
}))
+ this.lastThreadId = body.comment.id
+ this.lastVideoId = videoId
+
return body.comment
}
defaultExpectedStatus: HttpStatusCode.OK_200
}))
+ this.lastReplyId = body.comment.id
+
return body.comment
}
+ async addReplyToLastReply (options: OverrideCommandOptions & {
+ text: string
+ }) {
+ return this.addReply({ ...options, videoId: this.lastVideoId, toCommentId: this.lastReplyId })
+ }
+
+ async addReplyToLastThread (options: OverrideCommandOptions & {
+ text: string
+ }) {
+ return this.addReply({ ...options, videoId: this.lastVideoId, toCommentId: this.lastThreadId })
+ }
+
async findCommentId (options: OverrideCommandOptions & {
videoId: number | string
text: string
// ---------------------------------------------------------------------------
+ async find (options: OverrideCommandOptions & {
+ name: string
+ }) {
+ const { data } = await this.list(options)
+
+ return data.find(v => v.name === options.name)
+ }
+
+ // ---------------------------------------------------------------------------
+
update (options: OverrideCommandOptions & {
id: number | string
attributes?: VideoEdit
export * from './server-config.model'
export * from './server-debug.model'
export * from './server-error-code.enum'
+export * from './server-follow-create.model'
export * from './server-stats.model'
--- /dev/null
+export interface ServerFollowCreate {
+ hosts?: string[]
+ handles?: string[]
+}
- admin
tags:
- Instance Follows
- summary: Follow a list of servers
+ summary: Follow a list of actors (PeerTube instance, channel or account)
responses:
'204':
description: successful operation
type: string
format: hostname
uniqueItems: true
+ handles:
+ type: array
+ items:
+ type: string
+ uniqueItems: true
- '/server/following/{host}':
+ '/server/following/{hostOrHandle}':
delete:
- summary: Unfollow a server
+ summary: Unfollow an actor (PeerTube instance, channel or account)
security:
- OAuth2:
- admin
tags:
- Instance Follows
parameters:
- - name: host
+ - name: hostOrHandle
in: path
required: true
- description: The host to unfollow
+ description: The hostOrHandle to unfollow
schema:
type: string
- format: hostname
responses:
'204':
description: successful operation
'404':
- description: host not found
+ description: host or handle not found
/users:
post: