diff options
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 @@ | |||
1 | import { SortMeta } from 'primeng/api' | 1 | import { SortMeta } from 'primeng/api' |
2 | import { Component, OnInit } from '@angular/core' | 2 | import { Component, OnInit } from '@angular/core' |
3 | import { ConfirmService, Notifier, RestPagination, RestTable } from '@app/core' | 3 | import { ConfirmService, Notifier, RestPagination, RestTable } from '@app/core' |
4 | import { AdvancedInputFilter } from '@app/shared/shared-forms' | ||
4 | import { InstanceFollowService } from '@app/shared/shared-instance' | 5 | import { InstanceFollowService } from '@app/shared/shared-instance' |
5 | import { ActorFollow } from '@shared/models' | 6 | import { 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 @@ | |||
1 | import { SortMeta } from 'primeng/api' | 1 | import { SortMeta } from 'primeng/api' |
2 | import { Component, OnInit, ViewChild } from '@angular/core' | 2 | import { Component, OnInit, ViewChild } from '@angular/core' |
3 | import { ConfirmService, Notifier, RestPagination, RestTable } from '@app/core' | 3 | import { ConfirmService, Notifier, RestPagination, RestTable } from '@app/core' |
4 | import { AdvancedInputFilter } from '@app/shared/shared-forms' | ||
4 | import { InstanceFollowService } from '@app/shared/shared-instance' | 5 | import { InstanceFollowService } from '@app/shared/shared-instance' |
5 | import { ActorFollow } from '@shared/models' | 6 | import { ActorFollow } from '@shared/models' |
6 | import { FollowModalComponent } from './follow-modal.component' | 7 | import { 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' | |||
6 | import { RestExtractor, RestPagination, RestService } from '@app/core' | 6 | import { RestExtractor, RestPagination, RestService } from '@app/core' |
7 | import { ActivityPubActorType, ActorFollow, FollowState, ResultList, ServerFollowCreate } from '@shared/models' | 7 | import { ActivityPubActorType, ActorFollow, FollowState, ResultList, ServerFollowCreate } from '@shared/models' |
8 | import { environment } from '../../../environments/environment' | 8 | import { environment } from '../../../environments/environment' |
9 | import { AdvancedInputFilter } from '../shared-forms' | ||
9 | 10 | ||
10 | @Injectable() | 11 | @Injectable() |
11 | export class InstanceFollowService { | 12 | export 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 @@ | |||
1 | import { Transaction } from 'sequelize/types' | ||
2 | import { isBlockedByServerOrAccount } from '@server/lib/blocklist' | ||
3 | import { AccountModel } from '@server/models/account/account' | ||
1 | import { getServerActor } from '@server/models/application/application' | 4 | import { getServerActor } from '@server/models/application/application' |
2 | import { ActivityFollow } from '../../../../shared/models/activitypub' | 5 | import { ActivityFollow } from '../../../../shared/models/activitypub' |
3 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | 6 | import { retryTransactionWrapper } from '../../../helpers/database-utils' |
@@ -8,7 +11,7 @@ import { getAPId } from '../../../lib/activitypub/activity' | |||
8 | import { ActorModel } from '../../../models/actor/actor' | 11 | import { ActorModel } from '../../../models/actor/actor' |
9 | import { ActorFollowModel } from '../../../models/actor/actor-follow' | 12 | import { ActorFollowModel } from '../../../models/actor/actor-follow' |
10 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' | 13 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' |
11 | import { MActorFollowActors, MActorSignature } from '../../../types/models' | 14 | import { MActorFollow, MActorFull, MActorId, MActorSignature } from '../../../types/models' |
12 | import { Notifier } from '../../notifier' | 15 | import { Notifier } from '../../notifier' |
13 | import { autoFollowBackIfNeeded } from '../follow' | 16 | import { autoFollowBackIfNeeded } from '../follow' |
14 | import { sendAccept, sendReject } from '../send' | 17 | import { sendAccept, sendReject } from '../send' |
@@ -31,22 +34,14 @@ export { | |||
31 | // --------------------------------------------------------------------------- | 34 | // --------------------------------------------------------------------------- |
32 | 35 | ||
33 | async function processFollow (byActor: MActorSignature, activityId: string, targetActorURL: string) { | 36 | async 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 | |||
92 | function 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 | |||
104 | async 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 | |||
119 | function 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 | |||
132 | async 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 | |||
144 | async 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 | |||
152 | async 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 | ||
35 | async function checkFollows (options: { | 35 | async 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 () { |