]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Handle rejected follows in client
authorChocobozzz <me@florianbigard.com>
Wed, 27 Jul 2022 09:05:32 +0000 (11:05 +0200)
committerChocobozzz <me@florianbigard.com>
Wed, 27 Jul 2022 11:52:13 +0000 (13:52 +0200)
Also add quick filters so it's easier to find pending follows

client/src/app/+admin/follows/followers-list/followers-list.component.html
client/src/app/+admin/follows/followers-list/followers-list.component.ts
client/src/app/+admin/follows/following-list/following-list.component.html
client/src/app/+admin/follows/following-list/following-list.component.ts
client/src/app/shared/shared-instance/instance-follow.service.ts
server/lib/activitypub/process/process-follow.ts
server/tests/api/server/follows-moderation.ts

index 3081098c401defc8acde0b29a0f97b530c1dd935..4f11f261d9daecd4c948d23c3028c9f3b0225f3e 100644 (file)
@@ -13,7 +13,7 @@
   <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>
index 329e3bcc7071bd0ace53255f541deec7f90cd487..d09e74fefc038245eadd7d185b86051a46e350b3 100644 (file)
@@ -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'
 
@@ -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 () {
@@ -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
 
index 302dc9528b71041988654ba41b30c782b91038b7..856c4a31fd465c913a27e5b128145e213e146dec 100644 (file)
@@ -20,7 +20,7 @@
       </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>
@@ -66,7 +65,7 @@
 
   <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>
index 2c0f6db0c900d9f36eb8d7fd34723fa03fb6869c..7a854be81dda5e6a9ffc0e0da45b32e41bcef1c6 100644 (file)
@@ -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'
@@ -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 () {
index a83f7c4ad56cae17ba18c71b5826dbb0fcd29e18..06484d938f9b60de425338afc533c5c888de7afe 100644 (file)
@@ -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 {
@@ -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)
 
@@ -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)
 
@@ -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:'
+      }
+    })
+  }
 }
index a1958f46498744b985bf56af9adfeaccf370cdec..e633cd3aeea6bda044ba13f6ae84af032e943b06 100644 (file)
@@ -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'
@@ -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'
@@ -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,
@@ -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
@@ -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
@@ -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)
@@ -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
+}
index a0e94c10e9f011de68e8108d38bcd671bdd1f296..a34eb9bf062bb494deb8a2d69878758ceeb2f528 100644 (file)
@@ -33,42 +33,39 @@ async function checkServer1And2HasFollowers (servers: PeerTubeServer[], state =
 }
 
 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)
     }
@@ -256,14 +253,10 @@ describe('Test follows moderation', function () {
       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'
       })
     }
 
@@ -279,13 +272,10 @@ describe('Test follows moderation', function () {
     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'
     })
   })
 
@@ -297,14 +287,10 @@ describe('Test follows moderation', function () {
     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'
     })
   })
 
@@ -313,14 +299,10 @@ describe('Test follows moderation', function () {
     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'
     })
   })
 
@@ -329,19 +311,36 @@ describe('Test follows moderation', function () {
     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 () {