From 0e9c48c2edbb3871b0ca3ccd6718f2c99f9760b6 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Mon, 8 Apr 2019 11:52:29 +0200 Subject: [PATCH] Add ability to remove an instance follower in API --- server/controllers/api/server/follows.ts | 32 ++++++-- server/lib/activitypub/send/index.ts | 2 + server/lib/activitypub/send/send-reject.ts | 44 +++++++++++ server/lib/hls.ts | 3 +- server/middlewares/validators/follows.ts | 40 +++++++++- server/tests/api/server/follows-moderation.ts | 78 +++++++++++++++++++ server/tests/api/server/index.ts | 1 + shared/utils/server/follows.ts | 13 +++- 8 files changed, 202 insertions(+), 11 deletions(-) create mode 100644 server/lib/activitypub/send/send-reject.ts create mode 100644 server/tests/api/server/follows-moderation.ts diff --git a/server/controllers/api/server/follows.ts b/server/controllers/api/server/follows.ts index 99d211bfc..c00069f93 100644 --- a/server/controllers/api/server/follows.ts +++ b/server/controllers/api/server/follows.ts @@ -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() +} diff --git a/server/lib/activitypub/send/index.ts b/server/lib/activitypub/send/index.ts index 79ba6c7fe..028936810 100644 --- a/server/lib/activitypub/send/index.ts +++ b/server/lib/activitypub/send/index.ts @@ -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 index 000000000..db8c2d86d --- /dev/null +++ b/server/lib/activitypub/send/send-reject.ts @@ -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 + } +} diff --git a/server/lib/hls.ts b/server/lib/hls.ts index 5a7d61dee..c0fc4961a 100644 --- a/server/lib/hls.ts +++ b/server/lib/hls.ts @@ -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 }) }) } diff --git a/server/middlewares/validators/follows.ts b/server/middlewares/validators/follows.ts index 73fa28be9..ef4151efe 100644 --- a/server/middlewares/validators/follows.ts +++ b/server/middlewares/validators/follows.ts @@ -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 index 000000000..b1cbfb62c --- /dev/null +++ b/server/tests/api/server/follows-moderation.ts @@ -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) + }) +}) diff --git a/server/tests/api/server/index.ts b/server/tests/api/server/index.ts index 1f80cc6cf..4e53074ab 100644 --- a/server/tests/api/server/index.ts +++ b/server/tests/api/server/index.ts @@ -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' diff --git a/shared/utils/server/follows.ts b/shared/utils/server/follows.ts index 7741757a6..949fd8109 100644 --- a/shared/utils/server/follows.ts +++ b/shared/utils/server/follows.ts @@ -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 } -- 2.25.1