Add ability to remove an instance follower in API
authorChocobozzz <me@florianbigard.com>
Mon, 8 Apr 2019 09:52:29 +0000 (11:52 +0200)
committerChocobozzz <me@florianbigard.com>
Mon, 8 Apr 2019 09:52:43 +0000 (11:52 +0200)
server/controllers/api/server/follows.ts
server/lib/activitypub/send/index.ts
server/lib/activitypub/send/send-reject.ts [new file with mode: 0644]
server/lib/hls.ts
server/middlewares/validators/follows.ts
server/tests/api/server/follows-moderation.ts [new file with mode: 0644]
server/tests/api/server/index.ts
shared/utils/server/follows.ts

index 99d211bfc2ec06849aac07b56c7e0500fd5660f1..c00069f93f01931fa8eb008da3c9cbef8d932acd 100644 (file)
@@ -3,18 +3,23 @@ import { UserRight } from '../../../../shared/models/users'
 import { logger } from '../../../helpers/logger'
 import { getFormattedObjects, getServerActor } from '../../../helpers/utils'
 import { sequelizeTypescript, SERVER_ACTOR_NAME } from '../../../initializers'
-import { sendUndoFollow } from '../../../lib/activitypub/send'
+import { sendReject, sendUndoFollow } from '../../../lib/activitypub/send'
 import {
   asyncMiddleware,
   authenticate,
   ensureUserHasRight,
   paginationValidator,
-  removeFollowingValidator,
   setBodyHostsPort,
   setDefaultPagination,
   setDefaultSort
 } from '../../../middlewares'
-import { followersSortValidator, followingSortValidator, followValidator } from '../../../middlewares/validators'
+import {
+  followersSortValidator,
+  followingSortValidator,
+  followValidator,
+  removeFollowerValidator,
+  removeFollowingValidator
+} from '../../../middlewares/validators'
 import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
 import { JobQueue } from '../../../lib/job-queue'
 import { removeRedundancyOf } from '../../../lib/redundancy'
@@ -40,7 +45,7 @@ serverFollowsRouter.delete('/following/:host',
   authenticate,
   ensureUserHasRight(UserRight.MANAGE_SERVER_FOLLOW),
   asyncMiddleware(removeFollowingValidator),
-  asyncMiddleware(removeFollow)
+  asyncMiddleware(removeFollowing)
 )
 
 serverFollowsRouter.get('/followers',
@@ -51,6 +56,13 @@ serverFollowsRouter.get('/followers',
   asyncMiddleware(listFollowers)
 )
 
+serverFollowsRouter.delete('/followers/:nameWithHost',
+  authenticate,
+  ensureUserHasRight(UserRight.MANAGE_SERVER_FOLLOW),
+  asyncMiddleware(removeFollowerValidator),
+  asyncMiddleware(removeFollower)
+)
+
 // ---------------------------------------------------------------------------
 
 export {
@@ -103,7 +115,7 @@ async function followInstance (req: express.Request, res: express.Response) {
   return res.status(204).end()
 }
 
-async function removeFollow (req: express.Request, res: express.Response) {
+async function removeFollowing (req: express.Request, res: express.Response) {
   const follow = res.locals.follow
 
   await sequelizeTypescript.transaction(async t => {
@@ -123,3 +135,13 @@ async function removeFollow (req: express.Request, res: express.Response) {
 
   return res.status(204).end()
 }
+
+async function removeFollower (req: express.Request, res: express.Response) {
+  const follow = res.locals.follow
+
+  await sendReject(follow)
+
+  await follow.destroy()
+
+  return res.status(204).end()
+}
index 79ba6c7fedd42298efd82ba51145c2bd920e918f..0289368100044f8dd0afee9586f4daba610d529b 100644 (file)
@@ -1,8 +1,10 @@
 export * from './send-accept'
+export * from './send-accept'
 export * from './send-announce'
 export * from './send-create'
 export * from './send-delete'
 export * from './send-follow'
 export * from './send-like'
+export * from './send-reject'
 export * from './send-undo'
 export * from './send-update'
diff --git a/server/lib/activitypub/send/send-reject.ts b/server/lib/activitypub/send/send-reject.ts
new file mode 100644 (file)
index 0000000..db8c2d8
--- /dev/null
@@ -0,0 +1,44 @@
+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 { 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
+
+  if (!follower.serverId) { // This should never happen
+    logger.warn('Do not sending reject to local follower.')
+    return
+  }
+
+  logger.info('Creating job to reject follower %s.', follower.url)
+
+  const followUrl = getActorFollowActivityPubUrl(actorFollow)
+  const followData = buildFollowActivity(followUrl, follower, me)
+
+  const url = getActorFollowAcceptActivityPubUrl(actorFollow)
+  const data = buildRejectActivity(url, me, followData)
+
+  return unicastTo(data, me, follower.inboxUrl)
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+  sendReject
+}
+
+// ---------------------------------------------------------------------------
+
+function buildRejectActivity (url: string, byActor: ActorModel, followActivityData: ActivityFollow): ActivityReject {
+  return {
+    type: 'Reject',
+    id: url,
+    actor: byActor.url,
+    object: followActivityData
+  }
+}
index 5a7d61dee6034ad180cb6fbcdf8671f3531c1506..c0fc4961a021ed3983482ef76ada1d0a0697dba0 100644 (file)
@@ -1,6 +1,6 @@
 import { VideoModel } from '../models/video/video'
 import { basename, dirname, join } from 'path'
-import { CONFIG, HLS_STREAMING_PLAYLIST_DIRECTORY, sequelizeTypescript } from '../initializers'
+import { CONFIG, HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION, sequelizeTypescript } from '../initializers'
 import { close, ensureDir, move, open, outputJSON, pathExists, read, readFile, remove, writeFile } from 'fs-extra'
 import { getVideoFileSize } from '../helpers/ffmpeg-utils'
 import { sha256 } from '../helpers/core-utils'
@@ -20,6 +20,7 @@ async function updateStreamingPlaylistsInfohashesIfNeeded () {
       const videoFiles = await VideoFileModel.listByStreamingPlaylist(playlist.id, t)
 
       playlist.p2pMediaLoaderInfohashes = await VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlist.playlistUrl, videoFiles)
+      playlist.p2pMediaLoaderPeerVersion = P2P_MEDIA_LOADER_PEER_VERSION
       await playlist.save({ transaction: t })
     })
   }
index 73fa28be95556bf83a7e7efc50716c6eba497e37..ef4151efef8e35668347fb4f56f33b620d349b30 100644 (file)
@@ -7,6 +7,10 @@ import { getServerActor } from '../../helpers/utils'
 import { CONFIG, SERVER_ACTOR_NAME } from '../../initializers'
 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 = [
   body('hosts').custom(isEachUniqueHostValid).withMessage('Should have an array of unique hosts'),
@@ -33,7 +37,7 @@ const removeFollowingValidator = [
   param('host').custom(isHostValid).withMessage('Should have a valid host'),
 
   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
-    logger.debug('Checking unfollow parameters', { parameters: req.params })
+    logger.debug('Checking unfollowing parameters', { parameters: req.params })
 
     if (areValidationErrors(req, res)) return
 
@@ -44,7 +48,36 @@ const removeFollowingValidator = [
       return res
         .status(404)
         .json({
-          error: `Follower ${req.params.host} not found.`
+          error: `Following ${req.params.host} not found.`
+        })
+        .end()
+    }
+
+    res.locals.follow = follow
+    return next()
+  }
+]
+
+const removeFollowerValidator = [
+  param('nameWithHost').custom(isValidActorHandle).withMessage('Should have a valid nameWithHost'),
+
+  async (req: express.Request, res: express.Response, next: express.NextFunction) => {
+    logger.debug('Checking remove follower parameters', { parameters: req.params })
+
+    if (areValidationErrors(req, res)) return
+
+    const serverActor = await getServerActor()
+
+    const actorUrl = await loadActorUrlOrGetFromWebfinger(req.params.nameWithHost)
+    const actor = await ActorModel.loadByUrl(actorUrl)
+
+    const follow = await ActorFollowModel.loadByActorAndTarget(actor.id, serverActor.id)
+
+    if (!follow) {
+      return res
+        .status(404)
+        .json({
+          error: `Follower ${req.params.nameWithHost} not found.`
         })
         .end()
     }
@@ -58,5 +91,6 @@ const removeFollowingValidator = [
 
 export {
   followValidator,
-  removeFollowingValidator
+  removeFollowingValidator,
+  removeFollowerValidator
 }
diff --git a/server/tests/api/server/follows-moderation.ts b/server/tests/api/server/follows-moderation.ts
new file mode 100644 (file)
index 0000000..b1cbfb6
--- /dev/null
@@ -0,0 +1,78 @@
+/* tslint:disable:no-unused-expression */
+
+import * as chai from 'chai'
+import 'mocha'
+import { flushAndRunMultipleServers, killallServers, ServerInfo, setAccessTokensToServers } from '../../../../shared/utils/index'
+import {
+  follow,
+  getFollowersListPaginationAndSort,
+  getFollowingListPaginationAndSort,
+  removeFollower
+} from '../../../../shared/utils/server/follows'
+import { waitJobs } from '../../../../shared/utils/server/jobs'
+import { ActorFollow } from '../../../../shared/models/actors'
+
+const expect = chai.expect
+
+describe('Test follows moderation', function () {
+  let servers: ServerInfo[] = []
+
+  before(async function () {
+    this.timeout(30000)
+
+    servers = await flushAndRunMultipleServers(2)
+
+    // Get the access tokens
+    await setAccessTokensToServers(servers)
+  })
+
+  it('Should have server 1 following server 2', async function () {
+    this.timeout(30000)
+
+    await follow(servers[0].url, [ servers[1].url ], servers[0].accessToken)
+
+    await waitJobs(servers)
+  })
+
+  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')
+    }
+  })
+
+  it('Should remove follower on server 2', async function () {
+    await removeFollower(servers[1].url, servers[1].accessToken, servers[0])
+
+    await waitJobs(servers)
+  })
+
+  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)
+    }
+
+    {
+      const res = await getFollowingListPaginationAndSort(servers[ 0 ].url, 0, 1, 'createdAt')
+      expect(res.body.total).to.equal(0)
+    }
+  })
+
+  after(async function () {
+    killallServers(servers)
+  })
+})
index 1f80cc6cf5ee8445fb10875fcccf5362c4f55208..4e53074ab4535d7a68d9ab7b62d389bba4852672 100644 (file)
@@ -3,6 +3,7 @@ import './contact-form'
 import './email'
 import './follow-constraints'
 import './follows'
+import './follows-moderation'
 import './handle-down'
 import './jobs'
 import './reverse-proxy'
index 7741757a67247f20a8c662102f3b736094f0c8c7..949fd8109c7bcd7b6524a2dee74a2a30ebea2405 100644 (file)
@@ -47,13 +47,21 @@ async function follow (follower: string, following: string[], accessToken: strin
 async function unfollow (url: string, accessToken: string, target: ServerInfo, expectedStatus = 204) {
   const path = '/api/v1/server/following/' + target.host
 
-  const res = await request(url)
+  return request(url)
     .delete(path)
     .set('Accept', 'application/json')
     .set('Authorization', 'Bearer ' + accessToken)
     .expect(expectedStatus)
+}
 
-  return res
+function removeFollower (url: string, accessToken: string, follower: ServerInfo, expectedStatus = 204) {
+  const path = '/api/v1/server/followers/peertube@' + follower.host
+
+  return request(url)
+    .delete(path)
+    .set('Accept', 'application/json')
+    .set('Authorization', 'Bearer ' + accessToken)
+    .expect(expectedStatus)
 }
 
 async function doubleFollow (server1: ServerInfo, server2: ServerInfo) {
@@ -74,6 +82,7 @@ export {
   getFollowersListPaginationAndSort,
   getFollowingListPaginationAndSort,
   unfollow,
+  removeFollower,
   follow,
   doubleFollow
 }