Add ability to forbid followers
authorChocobozzz <me@florianbigard.com>
Mon, 8 Apr 2019 12:04:57 +0000 (14:04 +0200)
committerChocobozzz <me@florianbigard.com>
Mon, 8 Apr 2019 12:06:23 +0000 (14:06 +0200)
19 files changed:
config/default.yaml
config/production.yaml.example
server/controllers/api/config.ts
server/controllers/api/server/follows.ts
server/initializers/checker-before-init.ts
server/initializers/constants.ts
server/lib/activitypub/process/process-follow.ts
server/lib/activitypub/send/send-accept.ts
server/lib/activitypub/send/send-follow.ts
server/lib/activitypub/send/send-reject.ts
server/lib/activitypub/send/send-undo.ts
server/lib/activitypub/url.ts
server/middlewares/validators/follows.ts
server/tests/api/check-params/config.ts
server/tests/api/check-params/follows.ts
server/tests/api/server/config.ts
server/tests/api/server/follows-moderation.ts
shared/models/server/custom-config.model.ts
shared/utils/server/config.ts

index 6159104787c5207d2347faeaf07f5b4aa0faf5a5..51f3ad833ceb7dfbdb82156440cc5a2473b96280 100644 (file)
@@ -200,3 +200,8 @@ services:
     # If false, we use an image link card that will redirect on your PeerTube instance
     # Change it to "true", and then test on https://cards-dev.twitter.com/validator to see if you are whitelisted
     whitelisted: false
+
+followers:
+  instance:
+    # Allow or not other instances to follow yours
+    enabled: true
index 5299484a58a4938c9dbec4d982e323c51c7fe7f2..a2811abd621d69fcbc51a494e66eba0a352e2d70 100644 (file)
@@ -180,8 +180,8 @@ auto_blacklist:
   # New videos automatically blacklisted so moderators can review before publishing
   videos:
     of_users:
-      enabled: false 
+      enabled: false
+
 # Instance settings
 instance:
   name: 'PeerTube'
@@ -217,3 +217,8 @@ services:
     # If false, we use an image link card that will redirect on your PeerTube instance
     # Test on https://cards-dev.twitter.com/validator to see if you are whitelisted
     whitelisted: false
+
+followers:
+  instance:
+    # Allow or not other instances to follow yours
+    enabled: true
index bd0ba4f9d4654b5f1448fa02f9de1143e2c5ea0f..f9bb0b94770c99dcb51646190cc3318034c0895c 100644 (file)
@@ -279,6 +279,11 @@ function customConfig (): CustomConfig {
           enabled: CONFIG.AUTO_BLACKLIST.VIDEOS.OF_USERS.ENABLED
         }
       }
+    },
+    followers: {
+      instance: {
+        enabled: CONFIG.FOLLOWERS.INSTANCE.ENABLED
+      }
     }
   }
 }
index c00069f93f01931fa8eb008da3c9cbef8d932acd..87cf091cbffc0e64c5816c75a4c7de9e4ec6a66a 100644 (file)
@@ -139,7 +139,7 @@ async function removeFollowing (req: express.Request, res: express.Response) {
 async function removeFollower (req: express.Request, res: express.Response) {
   const follow = res.locals.follow
 
-  await sendReject(follow)
+  await sendReject(follow.ActorFollower, follow.ActorFollowing)
 
   await follow.destroy()
 
index e26f3856416bbe2cfddbe2a9fd2a97d95791ce7a..a9896907d5d169ad39a39ec7dc3d3d071cd28ac7 100644 (file)
@@ -24,7 +24,8 @@ function checkMissedConfig () {
     'trending.videos.interval_days',
     'instance.name', 'instance.short_description', 'instance.description', 'instance.terms', 'instance.default_client_route',
     'instance.is_nsfw', 'instance.default_nsfw_policy', 'instance.robots', 'instance.securitytxt',
-    'services.twitter.username', 'services.twitter.whitelisted'
+    'services.twitter.username', 'services.twitter.whitelisted',
+    'followers.instance.enabled'
   ]
   const requiredAlternatives = [
     [ // set
index ac19231d0d3cc485458dde78e908a1ad253a2739..43c5ec54cde1f0f739b9fd8b4582f2d9eef6d5d9 100644 (file)
@@ -324,6 +324,11 @@ const CONFIG = {
       get USERNAME () { return config.get<string>('services.twitter.username') },
       get WHITELISTED () { return config.get<boolean>('services.twitter.whitelisted') }
     }
+  },
+  FOLLOWERS: {
+    INSTANCE: {
+      get ENABLED () { return config.get<boolean>('followers.instance.enabled') }
+    }
   }
 }
 
index 0cd537187a93ccacc939d85f0258ef473f68a28b..cecf09b47bf2d169fcdf9e5ccedc1a824a048cea 100644 (file)
@@ -1,12 +1,13 @@
 import { ActivityFollow } from '../../../../shared/models/activitypub'
 import { retryTransactionWrapper } from '../../../helpers/database-utils'
 import { logger } from '../../../helpers/logger'
-import { sequelizeTypescript } from '../../../initializers'
+import { sequelizeTypescript, CONFIG } from '../../../initializers'
 import { ActorModel } from '../../../models/activitypub/actor'
 import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
-import { sendAccept } from '../send'
+import { sendAccept, sendReject } from '../send'
 import { Notifier } from '../../notifier'
 import { getAPId } from '../../../helpers/activitypub'
+import { getServerActor } from '../../../helpers/utils'
 
 async function processFollowActivity (activity: ActivityFollow, byActor: ActorModel) {
   const activityObject = getAPId(activity.object)
@@ -29,6 +30,11 @@ async function processFollow (actor: ActorModel, targetActorURL: string) {
     if (!targetActor) throw new Error('Unknown actor')
     if (targetActor.isOwned() === false) throw new Error('This is not a local actor.')
 
+    const serverActor = await getServerActor()
+    if (targetActor.id === serverActor.id && CONFIG.FOLLOWERS.INSTANCE.ENABLED === false) {
+      return sendReject(actor, targetActor)
+    }
+
     const [ actorFollow, created ] = await ActorFollowModel.findOrCreate({
       where: {
         actorId: actor.id,
index b6abde13dc8665677997a333b6145920c881a9c7..388a9ed2346285269d7bb9be3f0f4a27e9583b20 100644 (file)
@@ -17,7 +17,7 @@ async function sendAccept (actorFollow: ActorFollowModel) {
 
   logger.info('Creating job to accept follower %s.', follower.url)
 
-  const followUrl = getActorFollowActivityPubUrl(actorFollow)
+  const followUrl = getActorFollowActivityPubUrl(follower, me)
   const followData = buildFollowActivity(followUrl, follower, me)
 
   const url = getActorFollowAcceptActivityPubUrl(actorFollow)
index 170b46b482e035ce3b97ea1acfaf9c136e323d56..2c3d02014e9022a35b781f58246872359ed52499 100644 (file)
@@ -14,7 +14,7 @@ function sendFollow (actorFollow: ActorFollowModel) {
 
   logger.info('Creating job to send follow request to %s.', following.url)
 
-  const url = getActorFollowActivityPubUrl(actorFollow)
+  const url = getActorFollowActivityPubUrl(me, following)
   const data = buildFollowActivity(url, me, following)
 
   return unicastTo(data, me, following.inboxUrl)
index db8c2d86d5685441f5b99cd8c085700f5d1ccdf6..bac7ff556d96e153262fc53cfecfabc94f244dfc 100644 (file)
@@ -1,15 +1,11 @@
 import { ActivityFollow, ActivityReject } from '../../../../shared/models/activitypub'
 import { ActorModel } from '../../../models/activitypub/actor'
-import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
-import { getActorFollowAcceptActivityPubUrl, getActorFollowActivityPubUrl } from '../url'
+import { getActorFollowActivityPubUrl, getActorFollowRejectActivityPubUrl } from '../url'
 import { unicastTo } from './utils'
 import { buildFollowActivity } from './send-follow'
 import { logger } from '../../../helpers/logger'
 
-async function sendReject (actorFollow: ActorFollowModel) {
-  const follower = actorFollow.ActorFollower
-  const me = actorFollow.ActorFollowing
-
+async function sendReject (follower: ActorModel, following: ActorModel) {
   if (!follower.serverId) { // This should never happen
     logger.warn('Do not sending reject to local follower.')
     return
@@ -17,13 +13,13 @@ async function sendReject (actorFollow: ActorFollowModel) {
 
   logger.info('Creating job to reject follower %s.', follower.url)
 
-  const followUrl = getActorFollowActivityPubUrl(actorFollow)
-  const followData = buildFollowActivity(followUrl, follower, me)
+  const followUrl = getActorFollowActivityPubUrl(follower, following)
+  const followData = buildFollowActivity(followUrl, follower, following)
 
-  const url = getActorFollowAcceptActivityPubUrl(actorFollow)
-  const data = buildRejectActivity(url, me, followData)
+  const url = getActorFollowRejectActivityPubUrl(follower, following)
+  const data = buildRejectActivity(url, following, followData)
 
-  return unicastTo(data, me, follower.inboxUrl)
+  return unicastTo(data, following, follower.inboxUrl)
 }
 
 // ---------------------------------------------------------------------------
index ecbf605d664da5f8e6b1aab4672a141673535b2c..8727a121e786751a54713c48569d105118bc0859 100644 (file)
@@ -31,7 +31,7 @@ async function sendUndoFollow (actorFollow: ActorFollowModel, t: Transaction) {
 
   logger.info('Creating job to send an unfollow request to %s.', following.url)
 
-  const followUrl = getActorFollowActivityPubUrl(actorFollow)
+  const followUrl = getActorFollowActivityPubUrl(me, following)
   const undoUrl = getUndoActivityPubUrl(followUrl)
 
   const followActivity = buildFollowActivity(followUrl, me, following)
index 7c2ee5bc6937134000b2363cda7fc771cef7e2e8..401b83fc212f561b9673c705f9998428842e0769 100644 (file)
@@ -74,11 +74,8 @@ function getVideoDislikesActivityPubUrl (video: VideoModel) {
   return video.url + '/dislikes'
 }
 
-function getActorFollowActivityPubUrl (actorFollow: ActorFollowModel) {
-  const me = actorFollow.ActorFollower
-  const following = actorFollow.ActorFollowing
-
-  return me.url + '/follows/' + following.id
+function getActorFollowActivityPubUrl (follower: ActorModel, following: ActorModel) {
+  return follower.url + '/follows/' + following.id
 }
 
 function getActorFollowAcceptActivityPubUrl (actorFollow: ActorFollowModel) {
@@ -88,6 +85,10 @@ function getActorFollowAcceptActivityPubUrl (actorFollow: ActorFollowModel) {
   return follower.url + '/accepts/follows/' + me.id
 }
 
+function getActorFollowRejectActivityPubUrl (follower: ActorModel, following: ActorModel) {
+  return follower.url + '/rejects/follows/' + following.id
+}
+
 function getVideoAnnounceActivityPubUrl (byActor: ActorModel, video: VideoModel) {
   return video.url + '/announces/' + byActor.id
 }
@@ -120,6 +121,7 @@ export {
   getVideoViewActivityPubUrl,
   getVideoLikeActivityPubUrl,
   getVideoDislikeActivityPubUrl,
+  getActorFollowRejectActivityPubUrl,
   getVideoCommentActivityPubUrl,
   getDeleteActivityPubUrl,
   getVideoSharesActivityPubUrl,
index ef4151efef8e35668347fb4f56f33b620d349b30..38df39fdaae04d5b44892a8be301894f1125d1ff 100644 (file)
@@ -9,7 +9,6 @@ import { ActorFollowModel } from '../../models/activitypub/actor-follow'
 import { areValidationErrors } from './utils'
 import { ActorModel } from '../../models/activitypub/actor'
 import { loadActorUrlOrGetFromWebfinger } from '../../helpers/webfinger'
-import { getOrCreateActorAndServerAndModel } from '../../lib/activitypub'
 import { isValidActorHandle } from '../../helpers/custom-validators/activitypub/actor'
 
 const followValidator = [
@@ -66,12 +65,16 @@ const removeFollowerValidator = [
 
     if (areValidationErrors(req, res)) return
 
-    const serverActor = await getServerActor()
-
-    const actorUrl = await loadActorUrlOrGetFromWebfinger(req.params.nameWithHost)
-    const actor = await ActorModel.loadByUrl(actorUrl)
+    let follow: ActorFollowModel
+    try {
+      const actorUrl = await loadActorUrlOrGetFromWebfinger(req.params.nameWithHost)
+      const actor = await ActorModel.loadByUrl(actorUrl)
 
-    const follow = await ActorFollowModel.loadByActorAndTarget(actor.id, serverActor.id)
+      const serverActor = await getServerActor()
+      follow = await ActorFollowModel.loadByActorAndTarget(actor.id, serverActor.id)
+    } catch (err) {
+      logger.warn('Cannot get actor from handle.', { handle: req.params.nameWithHost, err })
+    }
 
     if (!follow) {
       return res
index 0b333e2f45a96b2a8c5bc8199072371f6186f835..d117f26e61b3faeeb924a68bd2d9076e25aa42dd 100644 (file)
@@ -87,6 +87,11 @@ describe('Test config API validators', function () {
           enabled: false
         }
       }
+    },
+    followers: {
+      instance: {
+        enabled: false
+      }
     }
   }
 
index 2ad1575a3b07c98d567221bddcbb95cf1e4ef2d1..67fa43778b406eed0cbd8bcbac2d9ce4fb33b184 100644 (file)
@@ -144,6 +144,46 @@ describe('Test server follows API validators', function () {
       })
     })
 
+    describe('When removing a follower', function () {
+      const path = '/api/v1/server/followers'
+
+      it('Should fail with an invalid token', async function () {
+        await makeDeleteRequest({
+          url: server.url,
+          path: path + '/toto@localhost:9002',
+          token: 'fake_token',
+          statusCodeExpected: 401
+        })
+      })
+
+      it('Should fail if the user is not an administrator', async function () {
+        await makeDeleteRequest({
+          url: server.url,
+          path: path + '/toto@localhost:9002',
+          token: userAccessToken,
+          statusCodeExpected: 403
+        })
+      })
+
+      it('Should fail with an invalid follower', async function () {
+        await makeDeleteRequest({
+          url: server.url,
+          path: path + '/toto',
+          token: server.accessToken,
+          statusCodeExpected: 400
+        })
+      })
+
+      it('Should fail with an unknown follower', async function () {
+        await makeDeleteRequest({
+          url: server.url,
+          path: path + '/toto@localhost:9003',
+          token: server.accessToken,
+          statusCodeExpected: 404
+        })
+      })
+    })
+
     describe('When removing following', function () {
       const path = '/api/v1/server/following'
 
index b9f05e952b9dc1d8d65fe3384797e43b4686c45b..cb2700f2949a862980a030c001c162ad499062ec 100644 (file)
@@ -63,6 +63,8 @@ function checkInitialConfig (data: CustomConfig) {
   expect(data.import.videos.http.enabled).to.be.true
   expect(data.import.videos.torrent.enabled).to.be.true
   expect(data.autoBlacklist.videos.ofUsers.enabled).to.be.false
+
+  expect(data.followers.instance.enabled).to.be.true
 }
 
 function checkUpdatedConfig (data: CustomConfig) {
@@ -105,6 +107,8 @@ function checkUpdatedConfig (data: CustomConfig) {
   expect(data.import.videos.http.enabled).to.be.false
   expect(data.import.videos.torrent.enabled).to.be.false
   expect(data.autoBlacklist.videos.ofUsers.enabled).to.be.true
+
+  expect(data.followers.instance.enabled).to.be.false
 }
 
 describe('Test config', function () {
@@ -234,6 +238,11 @@ describe('Test config', function () {
             enabled: true
           }
         }
+      },
+      followers: {
+        instance: {
+          enabled: false
+        }
       }
     }
     await updateCustomConfig(server.url, server.accessToken, newCustomConfig)
index b1cbfb62ca07e8190b1249f74168c3912da6c1cd..a360706f205df4f9bf28772bf5a8a97fd7acfe94 100644 (file)
@@ -2,7 +2,13 @@
 
 import * as chai from 'chai'
 import 'mocha'
-import { flushAndRunMultipleServers, killallServers, ServerInfo, setAccessTokensToServers } from '../../../../shared/utils/index'
+import {
+  flushAndRunMultipleServers,
+  killallServers,
+  ServerInfo,
+  setAccessTokensToServers,
+  updateCustomSubConfig
+} from '../../../../shared/utils/index'
 import {
   follow,
   getFollowersListPaginationAndSort,
@@ -14,6 +20,38 @@ import { ActorFollow } from '../../../../shared/models/actors'
 
 const expect = chai.expect
 
+async function checkHasFollowers (servers: ServerInfo[]) {
+  {
+    const res = await getFollowingListPaginationAndSort(servers[0].url, 0, 5, 'createdAt')
+    expect(res.body.total).to.equal(1)
+
+    const follow = res.body.data[0] as ActorFollow
+    expect(follow.follower.url).to.equal('http://localhost:9001/accounts/peertube')
+    expect(follow.following.url).to.equal('http://localhost:9002/accounts/peertube')
+  }
+
+  {
+    const res = await getFollowersListPaginationAndSort(servers[1].url, 0, 5, 'createdAt')
+    expect(res.body.total).to.equal(1)
+
+    const follow = res.body.data[0] as ActorFollow
+    expect(follow.follower.url).to.equal('http://localhost:9001/accounts/peertube')
+    expect(follow.following.url).to.equal('http://localhost:9002/accounts/peertube')
+  }
+}
+
+async function checkNoFollowers (servers: ServerInfo[]) {
+  {
+    const res = await getFollowingListPaginationAndSort(servers[ 0 ].url, 0, 5, 'createdAt')
+    expect(res.body.total).to.equal(0)
+  }
+
+  {
+    const res = await getFollowersListPaginationAndSort(servers[ 1 ].url, 0, 5, 'createdAt')
+    expect(res.body.total).to.equal(0)
+  }
+}
+
 describe('Test follows moderation', function () {
   let servers: ServerInfo[] = []
 
@@ -35,23 +73,7 @@ describe('Test follows moderation', function () {
   })
 
   it('Should have correct follows', async function () {
-    {
-      const res = await getFollowingListPaginationAndSort(servers[0].url, 0, 5, 'createdAt')
-      expect(res.body.total).to.equal(1)
-
-      const follow = res.body.data[0] as ActorFollow
-      expect(follow.follower.url).to.equal('http://localhost:9001/accounts/peertube')
-      expect(follow.following.url).to.equal('http://localhost:9002/accounts/peertube')
-    }
-
-    {
-      const res = await getFollowersListPaginationAndSort(servers[1].url, 0, 5, 'createdAt')
-      expect(res.body.total).to.equal(1)
-
-      const follow = res.body.data[0] as ActorFollow
-      expect(follow.follower.url).to.equal('http://localhost:9001/accounts/peertube')
-      expect(follow.following.url).to.equal('http://localhost:9002/accounts/peertube')
-    }
+    await checkHasFollowers(servers)
   })
 
   it('Should remove follower on server 2', async function () {
@@ -61,15 +83,41 @@ describe('Test follows moderation', function () {
   })
 
   it('Should not not have follows anymore', async function () {
-    {
-      const res = await getFollowingListPaginationAndSort(servers[ 0 ].url, 0, 1, 'createdAt')
-      expect(res.body.total).to.equal(0)
+    await checkNoFollowers(servers)
+  })
+
+  it('Should disable followers on server 2', async function () {
+    const subConfig = {
+      followers: {
+        instance: {
+          enabled: false
+        }
+      }
     }
 
-    {
-      const res = await getFollowingListPaginationAndSort(servers[ 0 ].url, 0, 1, 'createdAt')
-      expect(res.body.total).to.equal(0)
+    await updateCustomSubConfig(servers[1].url, servers[1].accessToken, subConfig)
+
+    await follow(servers[0].url, [ servers[1].url ], servers[0].accessToken)
+    await waitJobs(servers)
+
+    await checkNoFollowers(servers)
+  })
+
+  it('Should re enable followers on server 2', async function () {
+    const subConfig = {
+      followers: {
+        instance: {
+          enabled: true
+        }
+      }
     }
+
+    await updateCustomSubConfig(servers[1].url, servers[1].accessToken, subConfig)
+
+    await follow(servers[0].url, [ servers[1].url ], servers[0].accessToken)
+    await waitJobs(servers)
+
+    await checkHasFollowers(servers)
   })
 
   after(async function () {
index 1607b40a8e33fd7304c3be656c7c4eeb88f956b1..642ffea39e6e0b25beba7b21b3af6e9e9934b18f 100644 (file)
@@ -86,4 +86,10 @@ export interface CustomConfig {
     }
   }
 
+  followers: {
+    instance: {
+      enabled: boolean
+    }
+  }
+
 }
index eaa493a932ec11c8d46cac206b0deaf5992f0c42..21c6897143b2d2dcc2db1f014354e58a6455a0f2 100644 (file)
@@ -119,6 +119,11 @@ function updateCustomSubConfig (url: string, token: string, newConfig: any) {
           enabled: false
         }
       }
+    },
+    followers: {
+      instance: {
+        enabled: true
+      }
     }
   }