<ng-template pTemplate="caption">
<div class="caption">
<div class="ms-auto">
- <my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter>
+ <my-advanced-input-filter [filters]="searchFilters" (search)="onSearch($event)"></my-advanced-input-filter>
</div>
</div>
</ng-template>
<ng-template pTemplate="body" let-follow>
<tr>
<td class="action-cell">
- <ng-container *ngIf="follow.state === 'pending'">
- <my-button i18n-title title="Accept" icon="tick" (click)="acceptFollower(follow)"></my-button>
- <my-button i18n-title title="Refuse" icon="cross" (click)="rejectFollower(follow)"></my-button>
- </ng-container>
+ <my-button *ngIf="follow.state !== 'accepted'" i18n-title title="Accept" icon="tick" (click)="acceptFollower(follow)"></my-button>
+ <my-button *ngIf="follow.state !== 'rejected'" i18n-title title="Refuse" icon="cross" (click)="rejectFollower(follow)"></my-button>
- <my-delete-button label *ngIf="follow.state === 'accepted'" (click)="deleteFollower(follow)"></my-delete-button>
+ <my-delete-button *ngIf="follow.state === 'rejected'" (click)="deleteFollower(follow)"></my-delete-button>
</td>
<td>
<a [href]="follow.follower.url" i18n-title title="Open actor page in a new tab" target="_blank" rel="noopener noreferrer">
</a>
</td>
- <td *ngIf="follow.state === 'accepted'">
- <span class="pt-badge badge-green" i18n>Accepted</span>
- </td>
- <td *ngIf="follow.state === 'pending'">
- <span class="pt-badge badge-yellow" i18n>Pending</span>
+ <td>
+ <span *ngIf="follow.state === 'accepted'" class="pt-badge badge-green" i18n>Accepted</span>
+ <span *ngIf="follow.state === 'pending'" class="pt-badge badge-yellow" i18n>Pending</span>
+ <span *ngIf="follow.state === 'rejected'" class="pt-badge badge-red" i18n>Rejected</span>
</td>
<td>{{ follow.score }}</td>
import { SortMeta } from 'primeng/api'
import { Component, OnInit } from '@angular/core'
import { ConfirmService, Notifier, RestPagination, RestTable } from '@app/core'
+import { AdvancedInputFilter } from '@app/shared/shared-forms'
import { InstanceFollowService } from '@app/shared/shared-instance'
import { ActorFollow } from '@shared/models'
sort: SortMeta = { field: 'createdAt', order: -1 }
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
+ searchFilters: AdvancedInputFilter[]
+
constructor (
private confirmService: ConfirmService,
private notifier: Notifier,
private followService: InstanceFollowService
) {
super()
+
+ this.searchFilters = this.followService.buildFollowsListFilters()
}
ngOnInit () {
}
async deleteFollower (follow: ActorFollow) {
- const message = $localize`Do you really want to delete this follower?`
+ const message = $localize`Do you really want to delete this follower? It will be able to send again another follow request.`
const res = await this.confirmService.confirm(message, $localize`Delete`)
if (res === false) return
</div>
<div class="ms-auto">
- <my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter>
+ <my-advanced-input-filter [filters]="searchFilters" (search)="onSearch($event)"></my-advanced-input-filter>
</div>
</div>
</ng-template>
</a>
</td>
- <td *ngIf="follow.state === 'accepted'">
- <span class="pt-badge badge-green" i18n>Accepted</span>
- </td>
- <td *ngIf="follow.state === 'pending'">
- <span class="pt-badge badge-yellow" i18n>Pending</span>
+ <td>
+ <span *ngIf="follow.state === 'accepted'" class="pt-badge badge-green" i18n>Accepted</span>
+ <span *ngIf="follow.state === 'pending'" class="pt-badge badge-yellow" i18n>Pending</span>
+ <span *ngIf="follow.state === 'rejected'" class="pt-badge badge-red" i18n>Rejected</span>
</td>
<td>{{ follow.createdAt | date: 'short' }}</td>
<ng-template pTemplate="emptymessage">
<tr>
- <td colspan="6">
+ <td colspan="5">
<div class="no-results">
<ng-container *ngIf="search" i18n>No host found matching current filters.</ng-container>
<ng-container *ngIf="!search" i18n>Your instance is not following anyone.</ng-container>
import { SortMeta } from 'primeng/api'
import { Component, OnInit, ViewChild } from '@angular/core'
import { ConfirmService, Notifier, RestPagination, RestTable } from '@app/core'
+import { AdvancedInputFilter } from '@app/shared/shared-forms'
import { InstanceFollowService } from '@app/shared/shared-instance'
import { ActorFollow } from '@shared/models'
import { FollowModalComponent } from './follow-modal.component'
sort: SortMeta = { field: 'createdAt', order: -1 }
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
+ searchFilters: AdvancedInputFilter[]
+
constructor (
private notifier: Notifier,
private confirmService: ConfirmService,
private followService: InstanceFollowService
) {
super()
+
+ this.searchFilters = this.followService.buildFollowsListFilters()
}
ngOnInit () {
import { RestExtractor, RestPagination, RestService } from '@app/core'
import { ActivityPubActorType, ActorFollow, FollowState, ResultList, ServerFollowCreate } from '@shared/models'
import { environment } from '../../../environments/environment'
+import { AdvancedInputFilter } from '../shared-forms'
@Injectable()
export class InstanceFollowService {
let params = new HttpParams()
params = this.restService.addRestGetParams(params, pagination, sort)
- if (search) params = params.append('search', search)
+ if (search) {
+ params = this.restService.addObjectParams(params, this.parseFollowsListFilters(search))
+ }
+
if (state) params = params.append('state', state)
if (actorType) params = params.append('actorType', actorType)
let params = new HttpParams()
params = this.restService.addRestGetParams(params, pagination, sort)
- if (search) params = params.append('search', search)
+ if (search) {
+ params = this.restService.addObjectParams(params, this.parseFollowsListFilters(search))
+ }
+
if (state) params = params.append('state', state)
if (actorType) params = params.append('actorType', actorType)
return this.authHttp.delete(`${InstanceFollowService.BASE_APPLICATION_URL}/followers/${handle}`)
.pipe(catchError(res => this.restExtractor.handleError(res)))
}
+
+ buildFollowsListFilters (): AdvancedInputFilter[] {
+ return [
+ {
+ title: $localize`Advanced filters`,
+ children: [
+ {
+ value: 'state:accepted',
+ label: $localize`Accepted follows`
+ },
+ {
+ value: 'state:rejected',
+ label: $localize`Rejected follows`
+ },
+ {
+ value: 'state:pending',
+ label: $localize`Pending follows`
+ }
+ ]
+ }
+ ]
+ }
+
+ private parseFollowsListFilters (search: string) {
+ return this.restService.parseQueryStringFilter(search, {
+ state: {
+ prefix: 'state:'
+ }
+ })
+ }
}
+import { Transaction } from 'sequelize/types'
+import { isBlockedByServerOrAccount } from '@server/lib/blocklist'
+import { AccountModel } from '@server/models/account/account'
import { getServerActor } from '@server/models/application/application'
import { ActivityFollow } from '../../../../shared/models/activitypub'
import { retryTransactionWrapper } from '../../../helpers/database-utils'
import { ActorModel } from '../../../models/actor/actor'
import { ActorFollowModel } from '../../../models/actor/actor-follow'
import { APProcessorOptions } from '../../../types/activitypub-processor.model'
-import { MActorFollowActors, MActorSignature } from '../../../types/models'
+import { MActorFollow, MActorFull, MActorId, MActorSignature } from '../../../types/models'
import { Notifier } from '../../notifier'
import { autoFollowBackIfNeeded } from '../follow'
import { sendAccept, sendReject } from '../send'
// ---------------------------------------------------------------------------
async function processFollow (byActor: MActorSignature, activityId: string, targetActorURL: string) {
- const { actorFollow, created, isFollowingInstance, targetActor } = await sequelizeTypescript.transaction(async t => {
+ const { actorFollow, created, targetActor } = await sequelizeTypescript.transaction(async t => {
const targetActor = await ActorModel.loadByUrlAndPopulateAccountAndChannel(targetActorURL, t)
if (!targetActor) throw new Error('Unknown actor')
if (targetActor.isOwned() === false) throw new Error('This is not a local actor.')
- const serverActor = await getServerActor()
- const isFollowingInstance = targetActor.id === serverActor.id
-
- if (isFollowingInstance && CONFIG.FOLLOWERS.INSTANCE.ENABLED === false) {
- logger.info('Rejecting %s because instance followers are disabled.', targetActor.url)
-
- sendReject(activityId, byActor, targetActor)
-
- return { actorFollow: undefined as MActorFollowActors }
- }
+ if (rejectIfInstanceFollowDisabled(byActor, activityId, targetActor)) return { actorFollow: undefined }
+ if (await rejectIfMuted(byActor, activityId, targetActor)) return { actorFollow: undefined }
const [ actorFollow, created ] = await ActorFollowModel.findOrCreateCustom({
byActor,
transaction: t
})
- // Already rejected
- if (actorFollow.state === 'rejected') {
- return { actorFollow: undefined as MActorFollowActors }
- }
-
- // Set the follow as accepted if the remote actor follows a channel or account
- // Or if the instance automatically accepts followers
- if (actorFollow.state !== 'accepted' && (isFollowingInstance === false || CONFIG.FOLLOWERS.INSTANCE.MANUAL_APPROVAL === false)) {
- actorFollow.state = 'accepted'
+ if (rejectIfAlreadyRejected(actorFollow, byActor, activityId, targetActor)) return { actorFollow: undefined }
- await actorFollow.save({ transaction: t })
- }
+ await acceptIfNeeded(actorFollow, targetActor, t)
- // Before PeerTube V3 we did not save the follow ID. Try to fix these old follows
- if (!actorFollow.url) {
- actorFollow.url = activityId
- await actorFollow.save({ transaction: t })
- }
+ await fixFollowURLIfNeeded(actorFollow, activityId, t)
actorFollow.ActorFollower = byActor
actorFollow.ActorFollowing = targetActor
await autoFollowBackIfNeeded(actorFollow, t)
}
- return { actorFollow, created, isFollowingInstance, targetActor }
+ return { actorFollow, created, targetActor }
})
// Rejected
const follower = await ActorModel.loadFull(byActor.id)
const actorFollowFull = Object.assign(actorFollow, { ActorFollowing: targetActor, ActorFollower: follower })
- if (isFollowingInstance) {
+ if (isFollowingInstance(targetActor)) {
Notifier.Instance.notifyOfNewInstanceFollow(actorFollowFull)
} else {
Notifier.Instance.notifyOfNewUserFollow(actorFollowFull)
logger.info('Actor %s is followed by actor %s.', targetActorURL, byActor.url)
}
+
+function rejectIfInstanceFollowDisabled (byActor: MActorSignature, activityId: string, targetActor: MActorFull) {
+ if (isFollowingInstance(targetActor) && CONFIG.FOLLOWERS.INSTANCE.ENABLED === false) {
+ logger.info('Rejecting %s because instance followers are disabled.', targetActor.url)
+
+ sendReject(activityId, byActor, targetActor)
+
+ return true
+ }
+
+ return false
+}
+
+async function rejectIfMuted (byActor: MActorSignature, activityId: string, targetActor: MActorFull) {
+ const followerAccount = await AccountModel.load(byActor.Account.id)
+ const followingAccountId = targetActor.Account
+
+ if (followerAccount && await isBlockedByServerOrAccount(followerAccount, followingAccountId)) {
+ logger.info('Rejecting %s because follower is muted.', byActor.url)
+
+ sendReject(activityId, byActor, targetActor)
+
+ return true
+ }
+
+ return false
+}
+
+function rejectIfAlreadyRejected (actorFollow: MActorFollow, byActor: MActorSignature, activityId: string, targetActor: MActorFull) {
+ // Already rejected
+ if (actorFollow.state === 'rejected') {
+ logger.info('Rejecting %s because follow is already rejected.', byActor.url)
+
+ sendReject(activityId, byActor, targetActor)
+
+ return true
+ }
+
+ return false
+}
+
+async function acceptIfNeeded (actorFollow: MActorFollow, targetActor: MActorFull, transaction: Transaction) {
+ // Set the follow as accepted if the remote actor follows a channel or account
+ // Or if the instance automatically accepts followers
+ if (actorFollow.state === 'accepted') return
+ if (!isFollowingInstance(targetActor)) return
+ if (CONFIG.FOLLOWERS.INSTANCE.MANUAL_APPROVAL === true) return
+
+ actorFollow.state = 'accepted'
+
+ await actorFollow.save({ transaction })
+}
+
+async function fixFollowURLIfNeeded (actorFollow: MActorFollow, activityId: string, transaction: Transaction) {
+ // Before PeerTube V3 we did not save the follow ID. Try to fix these old follows
+ if (!actorFollow.url) {
+ actorFollow.url = activityId
+ await actorFollow.save({ transaction })
+ }
+}
+
+async function isFollowingInstance (targetActor: MActorId) {
+ const serverActor = await getServerActor()
+
+ return targetActor.id === serverActor.id
+}
}
async function checkFollows (options: {
- follower: {
- server: PeerTubeServer
- state?: FollowState // if not provided, it means it does not exist
- }
- following: {
- server: PeerTubeServer
- state?: FollowState // if not provided, it means it does not exist
- }
+ follower: PeerTubeServer
+ followerState: FollowState | 'deleted'
+
+ following: PeerTubeServer
+ followingState: FollowState | 'deleted'
}) {
- const { follower, following } = options
+ const { follower, followerState, followingState, following } = options
- const followerUrl = follower.server.url + '/accounts/peertube'
- const followingUrl = following.server.url + '/accounts/peertube'
+ const followerUrl = follower.url + '/accounts/peertube'
+ const followingUrl = following.url + '/accounts/peertube'
const finder = (d: ActorFollow) => d.follower.url === followerUrl && d.following.url === followingUrl
{
- const { data } = await follower.server.follows.getFollowings()
+ const { data } = await follower.follows.getFollowings()
const follow = data.find(finder)
- if (!follower.state) {
+ if (followerState === 'deleted') {
expect(follow).to.not.exist
} else {
- expect(follow.state).to.equal(follower.state)
+ expect(follow.state).to.equal(followerState)
expect(follow.follower.url).to.equal(followerUrl)
expect(follow.following.url).to.equal(followingUrl)
}
}
{
- const { data } = await following.server.follows.getFollowers()
+ const { data } = await following.follows.getFollowers()
const follow = data.find(finder)
- if (!following.state) {
+ if (followingState === 'deleted') {
expect(follow).to.not.exist
} else {
- expect(follow.state).to.equal(following.state)
+ expect(follow.state).to.equal(followingState)
expect(follow.follower.url).to.equal(followerUrl)
expect(follow.following.url).to.equal(followingUrl)
}
await waitJobs(servers)
await checkFollows({
- follower: {
- server: servers[0],
- state: 'rejected'
- },
- following: {
- server: servers[2],
- state: 'rejected'
- }
+ follower: servers[0],
+ followerState: 'rejected',
+ following: servers[2],
+ followingState: 'rejected'
})
}
await waitJobs(servers)
await checkFollows({
- follower: {
- server: servers[0]
- },
- following: {
- server: servers[2],
- state: 'rejected'
- }
+ follower: servers[0],
+ followerState: 'deleted',
+ following: servers[2],
+ followingState: 'rejected'
})
})
await waitJobs(servers)
await checkFollows({
- follower: {
- server: servers[0],
- state: 'pending'
- },
- following: {
- server: servers[2],
- state: 'pending'
- }
+ follower: servers[0],
+ followerState: 'pending',
+ following: servers[2],
+ followingState: 'pending'
})
})
await waitJobs(servers)
await checkFollows({
- follower: {
- server: servers[0],
- state: 'rejected'
- },
- following: {
- server: servers[1],
- state: 'rejected'
- }
+ follower: servers[0],
+ followerState: 'rejected',
+ following: servers[1],
+ followingState: 'rejected'
})
})
await waitJobs(servers)
await checkFollows({
- follower: {
- server: servers[0],
- state: 'accepted'
- },
- following: {
- server: servers[1],
- state: 'accepted'
- }
+ follower: servers[0],
+ followerState: 'accepted',
+ following: servers[1],
+ followingState: 'accepted'
})
})
it('Should ignore follow requests of muted servers', async function () {
+ await servers[1].blocklist.addToServerBlocklist({ server: servers[0].host })
+
+ await commands[0].unfollow({ target: servers[1] })
+ await waitJobs(servers)
+
+ await checkFollows({
+ follower: servers[0],
+ followerState: 'deleted',
+ following: servers[1],
+ followingState: 'deleted'
+ })
+
+ await commands[0].follow({ hosts: [ servers[1].host ] })
+ await waitJobs(servers)
+
+ await checkFollows({
+ follower: servers[0],
+ followerState: 'rejected',
+ following: servers[1],
+ followingState: 'deleted'
+ })
})
after(async function () {