Skip to content

Commit

Permalink
Handle rejected follows in client
Browse files Browse the repository at this point in the history
Also add quick filters so it's easier to find pending follows
  • Loading branch information
Chocobozzz committed Jul 27, 2022
1 parent 3267d38 commit 073deef
Show file tree
Hide file tree
Showing 7 changed files with 197 additions and 107 deletions.
Expand Up @@ -13,7 +13,7 @@ <h1>
<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>
Expand All @@ -31,12 +31,10 @@ <h1>
<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">
Expand All @@ -45,11 +43,10 @@ <h1>
</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>
Expand Down
@@ -1,6 +1,7 @@
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'

Expand All @@ -15,12 +16,16 @@ export class FollowersListComponent extends RestTable implements OnInit {
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 () {
Expand Down Expand Up @@ -70,7 +75,7 @@ export class FollowersListComponent extends RestTable implements OnInit {
}

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

Expand Down
Expand Up @@ -20,7 +20,7 @@ <h1>
</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>
Expand All @@ -47,11 +47,10 @@ <h1>
</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>
Expand All @@ -66,7 +65,7 @@ <h1>

<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>
Expand Down
@@ -1,6 +1,7 @@
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'
Expand All @@ -17,12 +18,16 @@ export class FollowingListComponent extends RestTable implements OnInit {
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 () {
Expand Down
41 changes: 39 additions & 2 deletions client/src/app/shared/shared-instance/instance-follow.service.ts
Expand Up @@ -6,6 +6,7 @@ import { Injectable } from '@angular/core'
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 {
Expand All @@ -30,7 +31,10 @@ 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)

Expand All @@ -53,7 +57,10 @@ 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)

Expand Down Expand Up @@ -101,4 +108,34 @@ export class InstanceFollowService {
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:'
}
})
}
}
108 changes: 78 additions & 30 deletions server/lib/activitypub/process/process-follow.ts
@@ -1,3 +1,6 @@
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'
Expand All @@ -8,7 +11,7 @@ import { getAPId } from '../../../lib/activitypub/activity'
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'
Expand All @@ -31,22 +34,14 @@ export {
// ---------------------------------------------------------------------------

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,
Expand All @@ -58,24 +53,11 @@ async function processFollow (byActor: MActorSignature, activityId: string, targ
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
Expand All @@ -87,7 +69,7 @@ async function processFollow (byActor: MActorSignature, activityId: string, targ
await autoFollowBackIfNeeded(actorFollow, t)
}

return { actorFollow, created, isFollowingInstance, targetActor }
return { actorFollow, created, targetActor }
})

// Rejected
Expand All @@ -97,7 +79,7 @@ async function processFollow (byActor: MActorSignature, activityId: string, targ
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)
Expand All @@ -106,3 +88,69 @@ async function processFollow (byActor: MActorSignature, activityId: string, targ

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
}

0 comments on commit 073deef

Please sign in to comment.