aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/+admin/follows/followers-list/followers-list.component.html19
-rw-r--r--client/src/app/+admin/follows/followers-list/followers-list.component.ts7
-rw-r--r--client/src/app/+admin/follows/following-list/following-list.component.html13
-rw-r--r--client/src/app/+admin/follows/following-list/following-list.component.ts5
-rw-r--r--client/src/app/shared/shared-instance/instance-follow.service.ts41
-rw-r--r--server/lib/activitypub/process/process-follow.ts108
-rw-r--r--server/tests/api/server/follows-moderation.ts111
7 files changed, 197 insertions, 107 deletions
diff --git a/client/src/app/+admin/follows/followers-list/followers-list.component.html b/client/src/app/+admin/follows/followers-list/followers-list.component.html
index 3081098c4..4f11f261d 100644
--- a/client/src/app/+admin/follows/followers-list/followers-list.component.html
+++ b/client/src/app/+admin/follows/followers-list/followers-list.component.html
@@ -13,7 +13,7 @@
13 <ng-template pTemplate="caption"> 13 <ng-template pTemplate="caption">
14 <div class="caption"> 14 <div class="caption">
15 <div class="ms-auto"> 15 <div class="ms-auto">
16 <my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter> 16 <my-advanced-input-filter [filters]="searchFilters" (search)="onSearch($event)"></my-advanced-input-filter>
17 </div> 17 </div>
18 </div> 18 </div>
19 </ng-template> 19 </ng-template>
@@ -31,12 +31,10 @@
31 <ng-template pTemplate="body" let-follow> 31 <ng-template pTemplate="body" let-follow>
32 <tr> 32 <tr>
33 <td class="action-cell"> 33 <td class="action-cell">
34 <ng-container *ngIf="follow.state === 'pending'"> 34 <my-button *ngIf="follow.state !== 'accepted'" i18n-title title="Accept" icon="tick" (click)="acceptFollower(follow)"></my-button>
35 <my-button i18n-title title="Accept" icon="tick" (click)="acceptFollower(follow)"></my-button> 35 <my-button *ngIf="follow.state !== 'rejected'" i18n-title title="Refuse" icon="cross" (click)="rejectFollower(follow)"></my-button>
36 <my-button i18n-title title="Refuse" icon="cross" (click)="rejectFollower(follow)"></my-button>
37 </ng-container>
38 36
39 <my-delete-button label *ngIf="follow.state === 'accepted'" (click)="deleteFollower(follow)"></my-delete-button> 37 <my-delete-button *ngIf="follow.state === 'rejected'" (click)="deleteFollower(follow)"></my-delete-button>
40 </td> 38 </td>
41 <td> 39 <td>
42 <a [href]="follow.follower.url" i18n-title title="Open actor page in a new tab" target="_blank" rel="noopener noreferrer"> 40 <a [href]="follow.follower.url" i18n-title title="Open actor page in a new tab" target="_blank" rel="noopener noreferrer">
@@ -45,11 +43,10 @@
45 </a> 43 </a>
46 </td> 44 </td>
47 45
48 <td *ngIf="follow.state === 'accepted'"> 46 <td>
49 <span class="pt-badge badge-green" i18n>Accepted</span> 47 <span *ngIf="follow.state === 'accepted'" class="pt-badge badge-green" i18n>Accepted</span>
50 </td> 48 <span *ngIf="follow.state === 'pending'" class="pt-badge badge-yellow" i18n>Pending</span>
51 <td *ngIf="follow.state === 'pending'"> 49 <span *ngIf="follow.state === 'rejected'" class="pt-badge badge-red" i18n>Rejected</span>
52 <span class="pt-badge badge-yellow" i18n>Pending</span>
53 </td> 50 </td>
54 51
55 <td>{{ follow.score }}</td> 52 <td>{{ follow.score }}</td>
diff --git a/client/src/app/+admin/follows/followers-list/followers-list.component.ts b/client/src/app/+admin/follows/followers-list/followers-list.component.ts
index 329e3bcc7..d09e74fef 100644
--- a/client/src/app/+admin/follows/followers-list/followers-list.component.ts
+++ b/client/src/app/+admin/follows/followers-list/followers-list.component.ts
@@ -1,6 +1,7 @@
1import { SortMeta } from 'primeng/api' 1import { SortMeta } from 'primeng/api'
2import { Component, OnInit } from '@angular/core' 2import { Component, OnInit } from '@angular/core'
3import { ConfirmService, Notifier, RestPagination, RestTable } from '@app/core' 3import { ConfirmService, Notifier, RestPagination, RestTable } from '@app/core'
4import { AdvancedInputFilter } from '@app/shared/shared-forms'
4import { InstanceFollowService } from '@app/shared/shared-instance' 5import { InstanceFollowService } from '@app/shared/shared-instance'
5import { ActorFollow } from '@shared/models' 6import { ActorFollow } from '@shared/models'
6 7
@@ -15,12 +16,16 @@ export class FollowersListComponent extends RestTable implements OnInit {
15 sort: SortMeta = { field: 'createdAt', order: -1 } 16 sort: SortMeta = { field: 'createdAt', order: -1 }
16 pagination: RestPagination = { count: this.rowsPerPage, start: 0 } 17 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
17 18
19 searchFilters: AdvancedInputFilter[]
20
18 constructor ( 21 constructor (
19 private confirmService: ConfirmService, 22 private confirmService: ConfirmService,
20 private notifier: Notifier, 23 private notifier: Notifier,
21 private followService: InstanceFollowService 24 private followService: InstanceFollowService
22 ) { 25 ) {
23 super() 26 super()
27
28 this.searchFilters = this.followService.buildFollowsListFilters()
24 } 29 }
25 30
26 ngOnInit () { 31 ngOnInit () {
@@ -70,7 +75,7 @@ export class FollowersListComponent extends RestTable implements OnInit {
70 } 75 }
71 76
72 async deleteFollower (follow: ActorFollow) { 77 async deleteFollower (follow: ActorFollow) {
73 const message = $localize`Do you really want to delete this follower?` 78 const message = $localize`Do you really want to delete this follower? It will be able to send again another follow request.`
74 const res = await this.confirmService.confirm(message, $localize`Delete`) 79 const res = await this.confirmService.confirm(message, $localize`Delete`)
75 if (res === false) return 80 if (res === false) return
76 81
diff --git a/client/src/app/+admin/follows/following-list/following-list.component.html b/client/src/app/+admin/follows/following-list/following-list.component.html
index 302dc9528..856c4a31f 100644
--- a/client/src/app/+admin/follows/following-list/following-list.component.html
+++ b/client/src/app/+admin/follows/following-list/following-list.component.html
@@ -20,7 +20,7 @@
20 </div> 20 </div>
21 21
22 <div class="ms-auto"> 22 <div class="ms-auto">
23 <my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter> 23 <my-advanced-input-filter [filters]="searchFilters" (search)="onSearch($event)"></my-advanced-input-filter>
24 </div> 24 </div>
25 </div> 25 </div>
26 </ng-template> 26 </ng-template>
@@ -47,11 +47,10 @@
47 </a> 47 </a>
48 </td> 48 </td>
49 49
50 <td *ngIf="follow.state === 'accepted'"> 50 <td>
51 <span class="pt-badge badge-green" i18n>Accepted</span> 51 <span *ngIf="follow.state === 'accepted'" class="pt-badge badge-green" i18n>Accepted</span>
52 </td> 52 <span *ngIf="follow.state === 'pending'" class="pt-badge badge-yellow" i18n>Pending</span>
53 <td *ngIf="follow.state === 'pending'"> 53 <span *ngIf="follow.state === 'rejected'" class="pt-badge badge-red" i18n>Rejected</span>
54 <span class="pt-badge badge-yellow" i18n>Pending</span>
55 </td> 54 </td>
56 55
57 <td>{{ follow.createdAt | date: 'short' }}</td> 56 <td>{{ follow.createdAt | date: 'short' }}</td>
@@ -66,7 +65,7 @@
66 65
67 <ng-template pTemplate="emptymessage"> 66 <ng-template pTemplate="emptymessage">
68 <tr> 67 <tr>
69 <td colspan="6"> 68 <td colspan="5">
70 <div class="no-results"> 69 <div class="no-results">
71 <ng-container *ngIf="search" i18n>No host found matching current filters.</ng-container> 70 <ng-container *ngIf="search" i18n>No host found matching current filters.</ng-container>
72 <ng-container *ngIf="!search" i18n>Your instance is not following anyone.</ng-container> 71 <ng-container *ngIf="!search" i18n>Your instance is not following anyone.</ng-container>
diff --git a/client/src/app/+admin/follows/following-list/following-list.component.ts b/client/src/app/+admin/follows/following-list/following-list.component.ts
index 2c0f6db0c..7a854be81 100644
--- a/client/src/app/+admin/follows/following-list/following-list.component.ts
+++ b/client/src/app/+admin/follows/following-list/following-list.component.ts
@@ -1,6 +1,7 @@
1import { SortMeta } from 'primeng/api' 1import { SortMeta } from 'primeng/api'
2import { Component, OnInit, ViewChild } from '@angular/core' 2import { Component, OnInit, ViewChild } from '@angular/core'
3import { ConfirmService, Notifier, RestPagination, RestTable } from '@app/core' 3import { ConfirmService, Notifier, RestPagination, RestTable } from '@app/core'
4import { AdvancedInputFilter } from '@app/shared/shared-forms'
4import { InstanceFollowService } from '@app/shared/shared-instance' 5import { InstanceFollowService } from '@app/shared/shared-instance'
5import { ActorFollow } from '@shared/models' 6import { ActorFollow } from '@shared/models'
6import { FollowModalComponent } from './follow-modal.component' 7import { FollowModalComponent } from './follow-modal.component'
@@ -17,12 +18,16 @@ export class FollowingListComponent extends RestTable implements OnInit {
17 sort: SortMeta = { field: 'createdAt', order: -1 } 18 sort: SortMeta = { field: 'createdAt', order: -1 }
18 pagination: RestPagination = { count: this.rowsPerPage, start: 0 } 19 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
19 20
21 searchFilters: AdvancedInputFilter[]
22
20 constructor ( 23 constructor (
21 private notifier: Notifier, 24 private notifier: Notifier,
22 private confirmService: ConfirmService, 25 private confirmService: ConfirmService,
23 private followService: InstanceFollowService 26 private followService: InstanceFollowService
24 ) { 27 ) {
25 super() 28 super()
29
30 this.searchFilters = this.followService.buildFollowsListFilters()
26 } 31 }
27 32
28 ngOnInit () { 33 ngOnInit () {
diff --git a/client/src/app/shared/shared-instance/instance-follow.service.ts b/client/src/app/shared/shared-instance/instance-follow.service.ts
index a83f7c4ad..06484d938 100644
--- a/client/src/app/shared/shared-instance/instance-follow.service.ts
+++ b/client/src/app/shared/shared-instance/instance-follow.service.ts
@@ -6,6 +6,7 @@ import { Injectable } from '@angular/core'
6import { RestExtractor, RestPagination, RestService } from '@app/core' 6import { RestExtractor, RestPagination, RestService } from '@app/core'
7import { ActivityPubActorType, ActorFollow, FollowState, ResultList, ServerFollowCreate } from '@shared/models' 7import { ActivityPubActorType, ActorFollow, FollowState, ResultList, ServerFollowCreate } from '@shared/models'
8import { environment } from '../../../environments/environment' 8import { environment } from '../../../environments/environment'
9import { AdvancedInputFilter } from '../shared-forms'
9 10
10@Injectable() 11@Injectable()
11export class InstanceFollowService { 12export class InstanceFollowService {
@@ -30,7 +31,10 @@ export class InstanceFollowService {
30 let params = new HttpParams() 31 let params = new HttpParams()
31 params = this.restService.addRestGetParams(params, pagination, sort) 32 params = this.restService.addRestGetParams(params, pagination, sort)
32 33
33 if (search) params = params.append('search', search) 34 if (search) {
35 params = this.restService.addObjectParams(params, this.parseFollowsListFilters(search))
36 }
37
34 if (state) params = params.append('state', state) 38 if (state) params = params.append('state', state)
35 if (actorType) params = params.append('actorType', actorType) 39 if (actorType) params = params.append('actorType', actorType)
36 40
@@ -53,7 +57,10 @@ export class InstanceFollowService {
53 let params = new HttpParams() 57 let params = new HttpParams()
54 params = this.restService.addRestGetParams(params, pagination, sort) 58 params = this.restService.addRestGetParams(params, pagination, sort)
55 59
56 if (search) params = params.append('search', search) 60 if (search) {
61 params = this.restService.addObjectParams(params, this.parseFollowsListFilters(search))
62 }
63
57 if (state) params = params.append('state', state) 64 if (state) params = params.append('state', state)
58 if (actorType) params = params.append('actorType', actorType) 65 if (actorType) params = params.append('actorType', actorType)
59 66
@@ -101,4 +108,34 @@ export class InstanceFollowService {
101 return this.authHttp.delete(`${InstanceFollowService.BASE_APPLICATION_URL}/followers/${handle}`) 108 return this.authHttp.delete(`${InstanceFollowService.BASE_APPLICATION_URL}/followers/${handle}`)
102 .pipe(catchError(res => this.restExtractor.handleError(res))) 109 .pipe(catchError(res => this.restExtractor.handleError(res)))
103 } 110 }
111
112 buildFollowsListFilters (): AdvancedInputFilter[] {
113 return [
114 {
115 title: $localize`Advanced filters`,
116 children: [
117 {
118 value: 'state:accepted',
119 label: $localize`Accepted follows`
120 },
121 {
122 value: 'state:rejected',
123 label: $localize`Rejected follows`
124 },
125 {
126 value: 'state:pending',
127 label: $localize`Pending follows`
128 }
129 ]
130 }
131 ]
132 }
133
134 private parseFollowsListFilters (search: string) {
135 return this.restService.parseQueryStringFilter(search, {
136 state: {
137 prefix: 'state:'
138 }
139 })
140 }
104} 141}
diff --git a/server/lib/activitypub/process/process-follow.ts b/server/lib/activitypub/process/process-follow.ts
index a1958f464..e633cd3ae 100644
--- a/server/lib/activitypub/process/process-follow.ts
+++ b/server/lib/activitypub/process/process-follow.ts
@@ -1,3 +1,6 @@
1import { Transaction } from 'sequelize/types'
2import { isBlockedByServerOrAccount } from '@server/lib/blocklist'
3import { AccountModel } from '@server/models/account/account'
1import { getServerActor } from '@server/models/application/application' 4import { getServerActor } from '@server/models/application/application'
2import { ActivityFollow } from '../../../../shared/models/activitypub' 5import { ActivityFollow } from '../../../../shared/models/activitypub'
3import { retryTransactionWrapper } from '../../../helpers/database-utils' 6import { retryTransactionWrapper } from '../../../helpers/database-utils'
@@ -8,7 +11,7 @@ import { getAPId } from '../../../lib/activitypub/activity'
8import { ActorModel } from '../../../models/actor/actor' 11import { ActorModel } from '../../../models/actor/actor'
9import { ActorFollowModel } from '../../../models/actor/actor-follow' 12import { ActorFollowModel } from '../../../models/actor/actor-follow'
10import { APProcessorOptions } from '../../../types/activitypub-processor.model' 13import { APProcessorOptions } from '../../../types/activitypub-processor.model'
11import { MActorFollowActors, MActorSignature } from '../../../types/models' 14import { MActorFollow, MActorFull, MActorId, MActorSignature } from '../../../types/models'
12import { Notifier } from '../../notifier' 15import { Notifier } from '../../notifier'
13import { autoFollowBackIfNeeded } from '../follow' 16import { autoFollowBackIfNeeded } from '../follow'
14import { sendAccept, sendReject } from '../send' 17import { sendAccept, sendReject } from '../send'
@@ -31,22 +34,14 @@ export {
31// --------------------------------------------------------------------------- 34// ---------------------------------------------------------------------------
32 35
33async function processFollow (byActor: MActorSignature, activityId: string, targetActorURL: string) { 36async function processFollow (byActor: MActorSignature, activityId: string, targetActorURL: string) {
34 const { actorFollow, created, isFollowingInstance, targetActor } = await sequelizeTypescript.transaction(async t => { 37 const { actorFollow, created, targetActor } = await sequelizeTypescript.transaction(async t => {
35 const targetActor = await ActorModel.loadByUrlAndPopulateAccountAndChannel(targetActorURL, t) 38 const targetActor = await ActorModel.loadByUrlAndPopulateAccountAndChannel(targetActorURL, t)
36 39
37 if (!targetActor) throw new Error('Unknown actor') 40 if (!targetActor) throw new Error('Unknown actor')
38 if (targetActor.isOwned() === false) throw new Error('This is not a local actor.') 41 if (targetActor.isOwned() === false) throw new Error('This is not a local actor.')
39 42
40 const serverActor = await getServerActor() 43 if (rejectIfInstanceFollowDisabled(byActor, activityId, targetActor)) return { actorFollow: undefined }
41 const isFollowingInstance = targetActor.id === serverActor.id 44 if (await rejectIfMuted(byActor, activityId, targetActor)) return { actorFollow: undefined }
42
43 if (isFollowingInstance && CONFIG.FOLLOWERS.INSTANCE.ENABLED === false) {
44 logger.info('Rejecting %s because instance followers are disabled.', targetActor.url)
45
46 sendReject(activityId, byActor, targetActor)
47
48 return { actorFollow: undefined as MActorFollowActors }
49 }
50 45
51 const [ actorFollow, created ] = await ActorFollowModel.findOrCreateCustom({ 46 const [ actorFollow, created ] = await ActorFollowModel.findOrCreateCustom({
52 byActor, 47 byActor,
@@ -58,24 +53,11 @@ async function processFollow (byActor: MActorSignature, activityId: string, targ
58 transaction: t 53 transaction: t
59 }) 54 })
60 55
61 // Already rejected 56 if (rejectIfAlreadyRejected(actorFollow, byActor, activityId, targetActor)) return { actorFollow: undefined }
62 if (actorFollow.state === 'rejected') {
63 return { actorFollow: undefined as MActorFollowActors }
64 }
65
66 // Set the follow as accepted if the remote actor follows a channel or account
67 // Or if the instance automatically accepts followers
68 if (actorFollow.state !== 'accepted' && (isFollowingInstance === false || CONFIG.FOLLOWERS.INSTANCE.MANUAL_APPROVAL === false)) {
69 actorFollow.state = 'accepted'
70 57
71 await actorFollow.save({ transaction: t }) 58 await acceptIfNeeded(actorFollow, targetActor, t)
72 }
73 59
74 // Before PeerTube V3 we did not save the follow ID. Try to fix these old follows 60 await fixFollowURLIfNeeded(actorFollow, activityId, t)
75 if (!actorFollow.url) {
76 actorFollow.url = activityId
77 await actorFollow.save({ transaction: t })
78 }
79 61
80 actorFollow.ActorFollower = byActor 62 actorFollow.ActorFollower = byActor
81 actorFollow.ActorFollowing = targetActor 63 actorFollow.ActorFollowing = targetActor
@@ -87,7 +69,7 @@ async function processFollow (byActor: MActorSignature, activityId: string, targ
87 await autoFollowBackIfNeeded(actorFollow, t) 69 await autoFollowBackIfNeeded(actorFollow, t)
88 } 70 }
89 71
90 return { actorFollow, created, isFollowingInstance, targetActor } 72 return { actorFollow, created, targetActor }
91 }) 73 })
92 74
93 // Rejected 75 // Rejected
@@ -97,7 +79,7 @@ async function processFollow (byActor: MActorSignature, activityId: string, targ
97 const follower = await ActorModel.loadFull(byActor.id) 79 const follower = await ActorModel.loadFull(byActor.id)
98 const actorFollowFull = Object.assign(actorFollow, { ActorFollowing: targetActor, ActorFollower: follower }) 80 const actorFollowFull = Object.assign(actorFollow, { ActorFollowing: targetActor, ActorFollower: follower })
99 81
100 if (isFollowingInstance) { 82 if (isFollowingInstance(targetActor)) {
101 Notifier.Instance.notifyOfNewInstanceFollow(actorFollowFull) 83 Notifier.Instance.notifyOfNewInstanceFollow(actorFollowFull)
102 } else { 84 } else {
103 Notifier.Instance.notifyOfNewUserFollow(actorFollowFull) 85 Notifier.Instance.notifyOfNewUserFollow(actorFollowFull)
@@ -106,3 +88,69 @@ async function processFollow (byActor: MActorSignature, activityId: string, targ
106 88
107 logger.info('Actor %s is followed by actor %s.', targetActorURL, byActor.url) 89 logger.info('Actor %s is followed by actor %s.', targetActorURL, byActor.url)
108} 90}
91
92function rejectIfInstanceFollowDisabled (byActor: MActorSignature, activityId: string, targetActor: MActorFull) {
93 if (isFollowingInstance(targetActor) && CONFIG.FOLLOWERS.INSTANCE.ENABLED === false) {
94 logger.info('Rejecting %s because instance followers are disabled.', targetActor.url)
95
96 sendReject(activityId, byActor, targetActor)
97
98 return true
99 }
100
101 return false
102}
103
104async function rejectIfMuted (byActor: MActorSignature, activityId: string, targetActor: MActorFull) {
105 const followerAccount = await AccountModel.load(byActor.Account.id)
106 const followingAccountId = targetActor.Account
107
108 if (followerAccount && await isBlockedByServerOrAccount(followerAccount, followingAccountId)) {
109 logger.info('Rejecting %s because follower is muted.', byActor.url)
110
111 sendReject(activityId, byActor, targetActor)
112
113 return true
114 }
115
116 return false
117}
118
119function rejectIfAlreadyRejected (actorFollow: MActorFollow, byActor: MActorSignature, activityId: string, targetActor: MActorFull) {
120 // Already rejected
121 if (actorFollow.state === 'rejected') {
122 logger.info('Rejecting %s because follow is already rejected.', byActor.url)
123
124 sendReject(activityId, byActor, targetActor)
125
126 return true
127 }
128
129 return false
130}
131
132async function acceptIfNeeded (actorFollow: MActorFollow, targetActor: MActorFull, transaction: Transaction) {
133 // Set the follow as accepted if the remote actor follows a channel or account
134 // Or if the instance automatically accepts followers
135 if (actorFollow.state === 'accepted') return
136 if (!isFollowingInstance(targetActor)) return
137 if (CONFIG.FOLLOWERS.INSTANCE.MANUAL_APPROVAL === true) return
138
139 actorFollow.state = 'accepted'
140
141 await actorFollow.save({ transaction })
142}
143
144async function fixFollowURLIfNeeded (actorFollow: MActorFollow, activityId: string, transaction: Transaction) {
145 // Before PeerTube V3 we did not save the follow ID. Try to fix these old follows
146 if (!actorFollow.url) {
147 actorFollow.url = activityId
148 await actorFollow.save({ transaction })
149 }
150}
151
152async function isFollowingInstance (targetActor: MActorId) {
153 const serverActor = await getServerActor()
154
155 return targetActor.id === serverActor.id
156}
diff --git a/server/tests/api/server/follows-moderation.ts b/server/tests/api/server/follows-moderation.ts
index a0e94c10e..a34eb9bf0 100644
--- a/server/tests/api/server/follows-moderation.ts
+++ b/server/tests/api/server/follows-moderation.ts
@@ -33,42 +33,39 @@ async function checkServer1And2HasFollowers (servers: PeerTubeServer[], state =
33} 33}
34 34
35async function checkFollows (options: { 35async function checkFollows (options: {
36 follower: { 36 follower: PeerTubeServer
37 server: PeerTubeServer 37 followerState: FollowState | 'deleted'
38 state?: FollowState // if not provided, it means it does not exist 38
39 } 39 following: PeerTubeServer
40 following: { 40 followingState: FollowState | 'deleted'
41 server: PeerTubeServer
42 state?: FollowState // if not provided, it means it does not exist
43 }
44}) { 41}) {
45 const { follower, following } = options 42 const { follower, followerState, followingState, following } = options
46 43
47 const followerUrl = follower.server.url + '/accounts/peertube' 44 const followerUrl = follower.url + '/accounts/peertube'
48 const followingUrl = following.server.url + '/accounts/peertube' 45 const followingUrl = following.url + '/accounts/peertube'
49 const finder = (d: ActorFollow) => d.follower.url === followerUrl && d.following.url === followingUrl 46 const finder = (d: ActorFollow) => d.follower.url === followerUrl && d.following.url === followingUrl
50 47
51 { 48 {
52 const { data } = await follower.server.follows.getFollowings() 49 const { data } = await follower.follows.getFollowings()
53 const follow = data.find(finder) 50 const follow = data.find(finder)
54 51
55 if (!follower.state) { 52 if (followerState === 'deleted') {
56 expect(follow).to.not.exist 53 expect(follow).to.not.exist
57 } else { 54 } else {
58 expect(follow.state).to.equal(follower.state) 55 expect(follow.state).to.equal(followerState)
59 expect(follow.follower.url).to.equal(followerUrl) 56 expect(follow.follower.url).to.equal(followerUrl)
60 expect(follow.following.url).to.equal(followingUrl) 57 expect(follow.following.url).to.equal(followingUrl)
61 } 58 }
62 } 59 }
63 60
64 { 61 {
65 const { data } = await following.server.follows.getFollowers() 62 const { data } = await following.follows.getFollowers()
66 const follow = data.find(finder) 63 const follow = data.find(finder)
67 64
68 if (!following.state) { 65 if (followingState === 'deleted') {
69 expect(follow).to.not.exist 66 expect(follow).to.not.exist
70 } else { 67 } else {
71 expect(follow.state).to.equal(following.state) 68 expect(follow.state).to.equal(followingState)
72 expect(follow.follower.url).to.equal(followerUrl) 69 expect(follow.follower.url).to.equal(followerUrl)
73 expect(follow.following.url).to.equal(followingUrl) 70 expect(follow.following.url).to.equal(followingUrl)
74 } 71 }
@@ -256,14 +253,10 @@ describe('Test follows moderation', function () {
256 await waitJobs(servers) 253 await waitJobs(servers)
257 254
258 await checkFollows({ 255 await checkFollows({
259 follower: { 256 follower: servers[0],
260 server: servers[0], 257 followerState: 'rejected',
261 state: 'rejected' 258 following: servers[2],
262 }, 259 followingState: 'rejected'
263 following: {
264 server: servers[2],
265 state: 'rejected'
266 }
267 }) 260 })
268 } 261 }
269 262
@@ -279,13 +272,10 @@ describe('Test follows moderation', function () {
279 await waitJobs(servers) 272 await waitJobs(servers)
280 273
281 await checkFollows({ 274 await checkFollows({
282 follower: { 275 follower: servers[0],
283 server: servers[0] 276 followerState: 'deleted',
284 }, 277 following: servers[2],
285 following: { 278 followingState: 'rejected'
286 server: servers[2],
287 state: 'rejected'
288 }
289 }) 279 })
290 }) 280 })
291 281
@@ -297,14 +287,10 @@ describe('Test follows moderation', function () {
297 await waitJobs(servers) 287 await waitJobs(servers)
298 288
299 await checkFollows({ 289 await checkFollows({
300 follower: { 290 follower: servers[0],
301 server: servers[0], 291 followerState: 'pending',
302 state: 'pending' 292 following: servers[2],
303 }, 293 followingState: 'pending'
304 following: {
305 server: servers[2],
306 state: 'pending'
307 }
308 }) 294 })
309 }) 295 })
310 296
@@ -313,14 +299,10 @@ describe('Test follows moderation', function () {
313 await waitJobs(servers) 299 await waitJobs(servers)
314 300
315 await checkFollows({ 301 await checkFollows({
316 follower: { 302 follower: servers[0],
317 server: servers[0], 303 followerState: 'rejected',
318 state: 'rejected' 304 following: servers[1],
319 }, 305 followingState: 'rejected'
320 following: {
321 server: servers[1],
322 state: 'rejected'
323 }
324 }) 306 })
325 }) 307 })
326 308
@@ -329,19 +311,36 @@ describe('Test follows moderation', function () {
329 await waitJobs(servers) 311 await waitJobs(servers)
330 312
331 await checkFollows({ 313 await checkFollows({
332 follower: { 314 follower: servers[0],
333 server: servers[0], 315 followerState: 'accepted',
334 state: 'accepted' 316 following: servers[1],
335 }, 317 followingState: 'accepted'
336 following: {
337 server: servers[1],
338 state: 'accepted'
339 }
340 }) 318 })
341 }) 319 })
342 320
343 it('Should ignore follow requests of muted servers', async function () { 321 it('Should ignore follow requests of muted servers', async function () {
322 await servers[1].blocklist.addToServerBlocklist({ server: servers[0].host })
323
324 await commands[0].unfollow({ target: servers[1] })
344 325
326 await waitJobs(servers)
327
328 await checkFollows({
329 follower: servers[0],
330 followerState: 'deleted',
331 following: servers[1],
332 followingState: 'deleted'
333 })
334
335 await commands[0].follow({ hosts: [ servers[1].host ] })
336 await waitJobs(servers)
337
338 await checkFollows({
339 follower: servers[0],
340 followerState: 'rejected',
341 following: servers[1],
342 followingState: 'deleted'
343 })
345 }) 344 })
346 345
347 after(async function () { 346 after(async function () {