aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--server/controllers/api/server/follows.ts32
-rw-r--r--server/lib/activitypub/send/index.ts2
-rw-r--r--server/lib/activitypub/send/send-reject.ts44
-rw-r--r--server/lib/hls.ts3
-rw-r--r--server/middlewares/validators/follows.ts40
-rw-r--r--server/tests/api/server/follows-moderation.ts78
-rw-r--r--server/tests/api/server/index.ts1
-rw-r--r--shared/utils/server/follows.ts13
8 files changed, 202 insertions, 11 deletions
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'
3import { logger } from '../../../helpers/logger' 3import { logger } from '../../../helpers/logger'
4import { getFormattedObjects, getServerActor } from '../../../helpers/utils' 4import { getFormattedObjects, getServerActor } from '../../../helpers/utils'
5import { sequelizeTypescript, SERVER_ACTOR_NAME } from '../../../initializers' 5import { sequelizeTypescript, SERVER_ACTOR_NAME } from '../../../initializers'
6import { sendUndoFollow } from '../../../lib/activitypub/send' 6import { sendReject, sendUndoFollow } from '../../../lib/activitypub/send'
7import { 7import {
8 asyncMiddleware, 8 asyncMiddleware,
9 authenticate, 9 authenticate,
10 ensureUserHasRight, 10 ensureUserHasRight,
11 paginationValidator, 11 paginationValidator,
12 removeFollowingValidator,
13 setBodyHostsPort, 12 setBodyHostsPort,
14 setDefaultPagination, 13 setDefaultPagination,
15 setDefaultSort 14 setDefaultSort
16} from '../../../middlewares' 15} from '../../../middlewares'
17import { followersSortValidator, followingSortValidator, followValidator } from '../../../middlewares/validators' 16import {
17 followersSortValidator,
18 followingSortValidator,
19 followValidator,
20 removeFollowerValidator,
21 removeFollowingValidator
22} from '../../../middlewares/validators'
18import { ActorFollowModel } from '../../../models/activitypub/actor-follow' 23import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
19import { JobQueue } from '../../../lib/job-queue' 24import { JobQueue } from '../../../lib/job-queue'
20import { removeRedundancyOf } from '../../../lib/redundancy' 25import { removeRedundancyOf } from '../../../lib/redundancy'
@@ -40,7 +45,7 @@ serverFollowsRouter.delete('/following/:host',
40 authenticate, 45 authenticate,
41 ensureUserHasRight(UserRight.MANAGE_SERVER_FOLLOW), 46 ensureUserHasRight(UserRight.MANAGE_SERVER_FOLLOW),
42 asyncMiddleware(removeFollowingValidator), 47 asyncMiddleware(removeFollowingValidator),
43 asyncMiddleware(removeFollow) 48 asyncMiddleware(removeFollowing)
44) 49)
45 50
46serverFollowsRouter.get('/followers', 51serverFollowsRouter.get('/followers',
@@ -51,6 +56,13 @@ serverFollowsRouter.get('/followers',
51 asyncMiddleware(listFollowers) 56 asyncMiddleware(listFollowers)
52) 57)
53 58
59serverFollowsRouter.delete('/followers/:nameWithHost',
60 authenticate,
61 ensureUserHasRight(UserRight.MANAGE_SERVER_FOLLOW),
62 asyncMiddleware(removeFollowerValidator),
63 asyncMiddleware(removeFollower)
64)
65
54// --------------------------------------------------------------------------- 66// ---------------------------------------------------------------------------
55 67
56export { 68export {
@@ -103,7 +115,7 @@ async function followInstance (req: express.Request, res: express.Response) {
103 return res.status(204).end() 115 return res.status(204).end()
104} 116}
105 117
106async function removeFollow (req: express.Request, res: express.Response) { 118async function removeFollowing (req: express.Request, res: express.Response) {
107 const follow = res.locals.follow 119 const follow = res.locals.follow
108 120
109 await sequelizeTypescript.transaction(async t => { 121 await sequelizeTypescript.transaction(async t => {
@@ -123,3 +135,13 @@ async function removeFollow (req: express.Request, res: express.Response) {
123 135
124 return res.status(204).end() 136 return res.status(204).end()
125} 137}
138
139async function removeFollower (req: express.Request, res: express.Response) {
140 const follow = res.locals.follow
141
142 await sendReject(follow)
143
144 await follow.destroy()
145
146 return res.status(204).end()
147}
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 @@
1export * from './send-accept' 1export * from './send-accept'
2export * from './send-accept'
2export * from './send-announce' 3export * from './send-announce'
3export * from './send-create' 4export * from './send-create'
4export * from './send-delete' 5export * from './send-delete'
5export * from './send-follow' 6export * from './send-follow'
6export * from './send-like' 7export * from './send-like'
8export * from './send-reject'
7export * from './send-undo' 9export * from './send-undo'
8export * from './send-update' 10export * 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 @@
1import { ActivityFollow, ActivityReject } from '../../../../shared/models/activitypub'
2import { ActorModel } from '../../../models/activitypub/actor'
3import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
4import { getActorFollowAcceptActivityPubUrl, getActorFollowActivityPubUrl } from '../url'
5import { unicastTo } from './utils'
6import { buildFollowActivity } from './send-follow'
7import { logger } from '../../../helpers/logger'
8
9async function sendReject (actorFollow: ActorFollowModel) {
10 const follower = actorFollow.ActorFollower
11 const me = actorFollow.ActorFollowing
12
13 if (!follower.serverId) { // This should never happen
14 logger.warn('Do not sending reject to local follower.')
15 return
16 }
17
18 logger.info('Creating job to reject follower %s.', follower.url)
19
20 const followUrl = getActorFollowActivityPubUrl(actorFollow)
21 const followData = buildFollowActivity(followUrl, follower, me)
22
23 const url = getActorFollowAcceptActivityPubUrl(actorFollow)
24 const data = buildRejectActivity(url, me, followData)
25
26 return unicastTo(data, me, follower.inboxUrl)
27}
28
29// ---------------------------------------------------------------------------
30
31export {
32 sendReject
33}
34
35// ---------------------------------------------------------------------------
36
37function buildRejectActivity (url: string, byActor: ActorModel, followActivityData: ActivityFollow): ActivityReject {
38 return {
39 type: 'Reject',
40 id: url,
41 actor: byActor.url,
42 object: followActivityData
43 }
44}
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 @@
1import { VideoModel } from '../models/video/video' 1import { VideoModel } from '../models/video/video'
2import { basename, dirname, join } from 'path' 2import { basename, dirname, join } from 'path'
3import { CONFIG, HLS_STREAMING_PLAYLIST_DIRECTORY, sequelizeTypescript } from '../initializers' 3import { CONFIG, HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION, sequelizeTypescript } from '../initializers'
4import { close, ensureDir, move, open, outputJSON, pathExists, read, readFile, remove, writeFile } from 'fs-extra' 4import { close, ensureDir, move, open, outputJSON, pathExists, read, readFile, remove, writeFile } from 'fs-extra'
5import { getVideoFileSize } from '../helpers/ffmpeg-utils' 5import { getVideoFileSize } from '../helpers/ffmpeg-utils'
6import { sha256 } from '../helpers/core-utils' 6import { sha256 } from '../helpers/core-utils'
@@ -20,6 +20,7 @@ async function updateStreamingPlaylistsInfohashesIfNeeded () {
20 const videoFiles = await VideoFileModel.listByStreamingPlaylist(playlist.id, t) 20 const videoFiles = await VideoFileModel.listByStreamingPlaylist(playlist.id, t)
21 21
22 playlist.p2pMediaLoaderInfohashes = await VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlist.playlistUrl, videoFiles) 22 playlist.p2pMediaLoaderInfohashes = await VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlist.playlistUrl, videoFiles)
23 playlist.p2pMediaLoaderPeerVersion = P2P_MEDIA_LOADER_PEER_VERSION
23 await playlist.save({ transaction: t }) 24 await playlist.save({ transaction: t })
24 }) 25 })
25 } 26 }
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'
7import { CONFIG, SERVER_ACTOR_NAME } from '../../initializers' 7import { CONFIG, SERVER_ACTOR_NAME } from '../../initializers'
8import { ActorFollowModel } from '../../models/activitypub/actor-follow' 8import { ActorFollowModel } from '../../models/activitypub/actor-follow'
9import { areValidationErrors } from './utils' 9import { areValidationErrors } from './utils'
10import { ActorModel } from '../../models/activitypub/actor'
11import { loadActorUrlOrGetFromWebfinger } from '../../helpers/webfinger'
12import { getOrCreateActorAndServerAndModel } from '../../lib/activitypub'
13import { isValidActorHandle } from '../../helpers/custom-validators/activitypub/actor'
10 14
11const followValidator = [ 15const followValidator = [
12 body('hosts').custom(isEachUniqueHostValid).withMessage('Should have an array of unique hosts'), 16 body('hosts').custom(isEachUniqueHostValid).withMessage('Should have an array of unique hosts'),
@@ -33,7 +37,7 @@ const removeFollowingValidator = [
33 param('host').custom(isHostValid).withMessage('Should have a valid host'), 37 param('host').custom(isHostValid).withMessage('Should have a valid host'),
34 38
35 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 39 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
36 logger.debug('Checking unfollow parameters', { parameters: req.params }) 40 logger.debug('Checking unfollowing parameters', { parameters: req.params })
37 41
38 if (areValidationErrors(req, res)) return 42 if (areValidationErrors(req, res)) return
39 43
@@ -44,7 +48,36 @@ const removeFollowingValidator = [
44 return res 48 return res
45 .status(404) 49 .status(404)
46 .json({ 50 .json({
47 error: `Follower ${req.params.host} not found.` 51 error: `Following ${req.params.host} not found.`
52 })
53 .end()
54 }
55
56 res.locals.follow = follow
57 return next()
58 }
59]
60
61const removeFollowerValidator = [
62 param('nameWithHost').custom(isValidActorHandle).withMessage('Should have a valid nameWithHost'),
63
64 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
65 logger.debug('Checking remove follower parameters', { parameters: req.params })
66
67 if (areValidationErrors(req, res)) return
68
69 const serverActor = await getServerActor()
70
71 const actorUrl = await loadActorUrlOrGetFromWebfinger(req.params.nameWithHost)
72 const actor = await ActorModel.loadByUrl(actorUrl)
73
74 const follow = await ActorFollowModel.loadByActorAndTarget(actor.id, serverActor.id)
75
76 if (!follow) {
77 return res
78 .status(404)
79 .json({
80 error: `Follower ${req.params.nameWithHost} not found.`
48 }) 81 })
49 .end() 82 .end()
50 } 83 }
@@ -58,5 +91,6 @@ const removeFollowingValidator = [
58 91
59export { 92export {
60 followValidator, 93 followValidator,
61 removeFollowingValidator 94 removeFollowingValidator,
95 removeFollowerValidator
62} 96}
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 @@
1/* tslint:disable:no-unused-expression */
2
3import * as chai from 'chai'
4import 'mocha'
5import { flushAndRunMultipleServers, killallServers, ServerInfo, setAccessTokensToServers } from '../../../../shared/utils/index'
6import {
7 follow,
8 getFollowersListPaginationAndSort,
9 getFollowingListPaginationAndSort,
10 removeFollower
11} from '../../../../shared/utils/server/follows'
12import { waitJobs } from '../../../../shared/utils/server/jobs'
13import { ActorFollow } from '../../../../shared/models/actors'
14
15const expect = chai.expect
16
17describe('Test follows moderation', function () {
18 let servers: ServerInfo[] = []
19
20 before(async function () {
21 this.timeout(30000)
22
23 servers = await flushAndRunMultipleServers(2)
24
25 // Get the access tokens
26 await setAccessTokensToServers(servers)
27 })
28
29 it('Should have server 1 following server 2', async function () {
30 this.timeout(30000)
31
32 await follow(servers[0].url, [ servers[1].url ], servers[0].accessToken)
33
34 await waitJobs(servers)
35 })
36
37 it('Should have correct follows', async function () {
38 {
39 const res = await getFollowingListPaginationAndSort(servers[0].url, 0, 5, 'createdAt')
40 expect(res.body.total).to.equal(1)
41
42 const follow = res.body.data[0] as ActorFollow
43 expect(follow.follower.url).to.equal('http://localhost:9001/accounts/peertube')
44 expect(follow.following.url).to.equal('http://localhost:9002/accounts/peertube')
45 }
46
47 {
48 const res = await getFollowersListPaginationAndSort(servers[1].url, 0, 5, 'createdAt')
49 expect(res.body.total).to.equal(1)
50
51 const follow = res.body.data[0] as ActorFollow
52 expect(follow.follower.url).to.equal('http://localhost:9001/accounts/peertube')
53 expect(follow.following.url).to.equal('http://localhost:9002/accounts/peertube')
54 }
55 })
56
57 it('Should remove follower on server 2', async function () {
58 await removeFollower(servers[1].url, servers[1].accessToken, servers[0])
59
60 await waitJobs(servers)
61 })
62
63 it('Should not not have follows anymore', async function () {
64 {
65 const res = await getFollowingListPaginationAndSort(servers[ 0 ].url, 0, 1, 'createdAt')
66 expect(res.body.total).to.equal(0)
67 }
68
69 {
70 const res = await getFollowingListPaginationAndSort(servers[ 0 ].url, 0, 1, 'createdAt')
71 expect(res.body.total).to.equal(0)
72 }
73 })
74
75 after(async function () {
76 killallServers(servers)
77 })
78})
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'
3import './email' 3import './email'
4import './follow-constraints' 4import './follow-constraints'
5import './follows' 5import './follows'
6import './follows-moderation'
6import './handle-down' 7import './handle-down'
7import './jobs' 8import './jobs'
8import './reverse-proxy' 9import './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
47async function unfollow (url: string, accessToken: string, target: ServerInfo, expectedStatus = 204) { 47async function unfollow (url: string, accessToken: string, target: ServerInfo, expectedStatus = 204) {
48 const path = '/api/v1/server/following/' + target.host 48 const path = '/api/v1/server/following/' + target.host
49 49
50 const res = await request(url) 50 return request(url)
51 .delete(path) 51 .delete(path)
52 .set('Accept', 'application/json') 52 .set('Accept', 'application/json')
53 .set('Authorization', 'Bearer ' + accessToken) 53 .set('Authorization', 'Bearer ' + accessToken)
54 .expect(expectedStatus) 54 .expect(expectedStatus)
55}
55 56
56 return res 57function removeFollower (url: string, accessToken: string, follower: ServerInfo, expectedStatus = 204) {
58 const path = '/api/v1/server/followers/peertube@' + follower.host
59
60 return request(url)
61 .delete(path)
62 .set('Accept', 'application/json')
63 .set('Authorization', 'Bearer ' + accessToken)
64 .expect(expectedStatus)
57} 65}
58 66
59async function doubleFollow (server1: ServerInfo, server2: ServerInfo) { 67async function doubleFollow (server1: ServerInfo, server2: ServerInfo) {
@@ -74,6 +82,7 @@ export {
74 getFollowersListPaginationAndSort, 82 getFollowersListPaginationAndSort,
75 getFollowingListPaginationAndSort, 83 getFollowingListPaginationAndSort,
76 unfollow, 84 unfollow,
85 removeFollower,
77 follow, 86 follow,
78 doubleFollow 87 doubleFollow
79} 88}