aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2019-04-08 15:18:04 +0200
committerChocobozzz <me@florianbigard.com>2019-04-08 15:18:04 +0200
commit14893eb71cb2d4ca47e07589c81958863603aba4 (patch)
treea6a5c3130058ca57c10ef15b8b6ee00ef78166ea
parent5b9c965d5aa747f29b081289f930ee215fdc23c8 (diff)
downloadPeerTube-14893eb71cb2d4ca47e07589c81958863603aba4.tar.gz
PeerTube-14893eb71cb2d4ca47e07589c81958863603aba4.tar.zst
PeerTube-14893eb71cb2d4ca47e07589c81958863603aba4.zip
Add ability to manually approves instance followers in REST API
-rw-r--r--config/default.yaml2
-rw-r--r--config/production.yaml.example2
-rw-r--r--server/controllers/api/config.ts3
-rw-r--r--server/controllers/api/server/follows.ts38
-rw-r--r--server/initializers/checker-before-init.ts2
-rw-r--r--server/initializers/constants.ts3
-rw-r--r--server/lib/activitypub/process/process-follow.ts8
-rw-r--r--server/middlewares/validators/config.ts3
-rw-r--r--server/middlewares/validators/follows.ts20
-rw-r--r--server/tests/api/check-params/config.ts3
-rw-r--r--server/tests/api/check-params/follows.ts80
-rw-r--r--server/tests/api/server/config.ts5
-rw-r--r--server/tests/api/server/follows-moderation.ts83
-rw-r--r--shared/models/server/custom-config.model.ts3
-rw-r--r--shared/utils/server/config.ts3
-rw-r--r--shared/utils/server/follows.ts33
16 files changed, 261 insertions, 30 deletions
diff --git a/config/default.yaml b/config/default.yaml
index 51f3ad833..0d6e34d86 100644
--- a/config/default.yaml
+++ b/config/default.yaml
@@ -205,3 +205,5 @@ followers:
205 instance: 205 instance:
206 # Allow or not other instances to follow yours 206 # Allow or not other instances to follow yours
207 enabled: true 207 enabled: true
208 # Whether or not an administrator must manually validate a new follower
209 manual_approval: false
diff --git a/config/production.yaml.example b/config/production.yaml.example
index a2811abd6..5029cc25b 100644
--- a/config/production.yaml.example
+++ b/config/production.yaml.example
@@ -222,3 +222,5 @@ followers:
222 instance: 222 instance:
223 # Allow or not other instances to follow yours 223 # Allow or not other instances to follow yours
224 enabled: true 224 enabled: true
225 # Whether or not an administrator must manually validate a new follower
226 manual_approval: false
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts
index f9bb0b947..5c4f455ee 100644
--- a/server/controllers/api/config.ts
+++ b/server/controllers/api/config.ts
@@ -282,7 +282,8 @@ function customConfig (): CustomConfig {
282 }, 282 },
283 followers: { 283 followers: {
284 instance: { 284 instance: {
285 enabled: CONFIG.FOLLOWERS.INSTANCE.ENABLED 285 enabled: CONFIG.FOLLOWERS.INSTANCE.ENABLED,
286 manualApproval: CONFIG.FOLLOWERS.INSTANCE.MANUAL_APPROVAL
286 } 287 }
287 } 288 }
288 } 289 }
diff --git a/server/controllers/api/server/follows.ts b/server/controllers/api/server/follows.ts
index 87cf091cb..207a09a4c 100644
--- a/server/controllers/api/server/follows.ts
+++ b/server/controllers/api/server/follows.ts
@@ -3,7 +3,7 @@ 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 { sendReject, sendUndoFollow } from '../../../lib/activitypub/send' 6import { sendAccept, sendReject, sendUndoFollow } from '../../../lib/activitypub/send'
7import { 7import {
8 asyncMiddleware, 8 asyncMiddleware,
9 authenticate, 9 authenticate,
@@ -14,10 +14,11 @@ import {
14 setDefaultSort 14 setDefaultSort
15} from '../../../middlewares' 15} from '../../../middlewares'
16import { 16import {
17 acceptOrRejectFollowerValidator,
17 followersSortValidator, 18 followersSortValidator,
18 followingSortValidator, 19 followingSortValidator,
19 followValidator, 20 followValidator,
20 removeFollowerValidator, 21 getFollowerValidator,
21 removeFollowingValidator 22 removeFollowingValidator
22} from '../../../middlewares/validators' 23} from '../../../middlewares/validators'
23import { ActorFollowModel } from '../../../models/activitypub/actor-follow' 24import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
@@ -59,8 +60,24 @@ serverFollowsRouter.get('/followers',
59serverFollowsRouter.delete('/followers/:nameWithHost', 60serverFollowsRouter.delete('/followers/:nameWithHost',
60 authenticate, 61 authenticate,
61 ensureUserHasRight(UserRight.MANAGE_SERVER_FOLLOW), 62 ensureUserHasRight(UserRight.MANAGE_SERVER_FOLLOW),
62 asyncMiddleware(removeFollowerValidator), 63 asyncMiddleware(getFollowerValidator),
63 asyncMiddleware(removeFollower) 64 asyncMiddleware(removeOrRejectFollower)
65)
66
67serverFollowsRouter.post('/followers/:nameWithHost/reject',
68 authenticate,
69 ensureUserHasRight(UserRight.MANAGE_SERVER_FOLLOW),
70 asyncMiddleware(getFollowerValidator),
71 acceptOrRejectFollowerValidator,
72 asyncMiddleware(removeOrRejectFollower)
73)
74
75serverFollowsRouter.post('/followers/:nameWithHost/accept',
76 authenticate,
77 ensureUserHasRight(UserRight.MANAGE_SERVER_FOLLOW),
78 asyncMiddleware(getFollowerValidator),
79 acceptOrRejectFollowerValidator,
80 asyncMiddleware(acceptFollower)
64) 81)
65 82
66// --------------------------------------------------------------------------- 83// ---------------------------------------------------------------------------
@@ -136,7 +153,7 @@ async function removeFollowing (req: express.Request, res: express.Response) {
136 return res.status(204).end() 153 return res.status(204).end()
137} 154}
138 155
139async function removeFollower (req: express.Request, res: express.Response) { 156async function removeOrRejectFollower (req: express.Request, res: express.Response) {
140 const follow = res.locals.follow 157 const follow = res.locals.follow
141 158
142 await sendReject(follow.ActorFollower, follow.ActorFollowing) 159 await sendReject(follow.ActorFollower, follow.ActorFollowing)
@@ -145,3 +162,14 @@ async function removeFollower (req: express.Request, res: express.Response) {
145 162
146 return res.status(204).end() 163 return res.status(204).end()
147} 164}
165
166async function acceptFollower (req: express.Request, res: express.Response) {
167 const follow = res.locals.follow
168
169 await sendAccept(follow)
170
171 follow.state = 'accepted'
172 await follow.save()
173
174 return res.status(204).end()
175}
diff --git a/server/initializers/checker-before-init.ts b/server/initializers/checker-before-init.ts
index a9896907d..3095913a3 100644
--- a/server/initializers/checker-before-init.ts
+++ b/server/initializers/checker-before-init.ts
@@ -25,7 +25,7 @@ function checkMissedConfig () {
25 'instance.name', 'instance.short_description', 'instance.description', 'instance.terms', 'instance.default_client_route', 25 'instance.name', 'instance.short_description', 'instance.description', 'instance.terms', 'instance.default_client_route',
26 'instance.is_nsfw', 'instance.default_nsfw_policy', 'instance.robots', 'instance.securitytxt', 26 'instance.is_nsfw', 'instance.default_nsfw_policy', 'instance.robots', 'instance.securitytxt',
27 'services.twitter.username', 'services.twitter.whitelisted', 27 'services.twitter.username', 'services.twitter.whitelisted',
28 'followers.instance.enabled' 28 'followers.instance.enabled', 'followers.instance.manual_approval'
29 ] 29 ]
30 const requiredAlternatives = [ 30 const requiredAlternatives = [
31 [ // set 31 [ // set
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 43c5ec54c..7d9ffc668 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -327,7 +327,8 @@ const CONFIG = {
327 }, 327 },
328 FOLLOWERS: { 328 FOLLOWERS: {
329 INSTANCE: { 329 INSTANCE: {
330 get ENABLED () { return config.get<boolean>('followers.instance.enabled') } 330 get ENABLED () { return config.get<boolean>('followers.instance.enabled') },
331 get MANUAL_APPROVAL () { return config.get<boolean>('followers.instance.manual_approval') }
331 } 332 }
332 } 333 }
333} 334}
diff --git a/server/lib/activitypub/process/process-follow.ts b/server/lib/activitypub/process/process-follow.ts
index cecf09b47..140bbe9f1 100644
--- a/server/lib/activitypub/process/process-follow.ts
+++ b/server/lib/activitypub/process/process-follow.ts
@@ -32,6 +32,8 @@ async function processFollow (actor: ActorModel, targetActorURL: string) {
32 32
33 const serverActor = await getServerActor() 33 const serverActor = await getServerActor()
34 if (targetActor.id === serverActor.id && CONFIG.FOLLOWERS.INSTANCE.ENABLED === false) { 34 if (targetActor.id === serverActor.id && CONFIG.FOLLOWERS.INSTANCE.ENABLED === false) {
35 logger.info('Rejecting %s because instance followers are disabled.', targetActor.url)
36
35 return sendReject(actor, targetActor) 37 return sendReject(actor, targetActor)
36 } 38 }
37 39
@@ -43,7 +45,7 @@ async function processFollow (actor: ActorModel, targetActorURL: string) {
43 defaults: { 45 defaults: {
44 actorId: actor.id, 46 actorId: actor.id,
45 targetActorId: targetActor.id, 47 targetActorId: targetActor.id,
46 state: 'accepted' 48 state: CONFIG.FOLLOWERS.INSTANCE.MANUAL_APPROVAL ? 'pending' : 'accepted'
47 }, 49 },
48 transaction: t 50 transaction: t
49 }) 51 })
@@ -51,7 +53,7 @@ async function processFollow (actor: ActorModel, targetActorURL: string) {
51 actorFollow.ActorFollower = actor 53 actorFollow.ActorFollower = actor
52 actorFollow.ActorFollowing = targetActor 54 actorFollow.ActorFollowing = targetActor
53 55
54 if (actorFollow.state !== 'accepted') { 56 if (actorFollow.state !== 'accepted' && CONFIG.FOLLOWERS.INSTANCE.MANUAL_APPROVAL === false) {
55 actorFollow.state = 'accepted' 57 actorFollow.state = 'accepted'
56 await actorFollow.save({ transaction: t }) 58 await actorFollow.save({ transaction: t })
57 } 59 }
@@ -60,7 +62,7 @@ async function processFollow (actor: ActorModel, targetActorURL: string) {
60 actorFollow.ActorFollowing = targetActor 62 actorFollow.ActorFollowing = targetActor
61 63
62 // Target sends to actor he accepted the follow request 64 // Target sends to actor he accepted the follow request
63 await sendAccept(actorFollow) 65 if (actorFollow.state === 'accepted') await sendAccept(actorFollow)
64 66
65 return { actorFollow, created } 67 return { actorFollow, created }
66 }) 68 })
diff --git a/server/middlewares/validators/config.ts b/server/middlewares/validators/config.ts
index 270ce66f6..d015fa6fe 100644
--- a/server/middlewares/validators/config.ts
+++ b/server/middlewares/validators/config.ts
@@ -44,6 +44,9 @@ const customConfigUpdateValidator = [
44 body('import.videos.http.enabled').isBoolean().withMessage('Should have a valid import video http enabled boolean'), 44 body('import.videos.http.enabled').isBoolean().withMessage('Should have a valid import video http enabled boolean'),
45 body('import.videos.torrent.enabled').isBoolean().withMessage('Should have a valid import video torrent enabled boolean'), 45 body('import.videos.torrent.enabled').isBoolean().withMessage('Should have a valid import video torrent enabled boolean'),
46 46
47 body('followers.instance.enabled').isBoolean().withMessage('Should have a valid followers of instance boolean'),
48 body('followers.instance.manualApproval').isBoolean().withMessage('Should have a valid manual approval boolean'),
49
47 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 50 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
48 logger.debug('Checking customConfigUpdateValidator parameters', { parameters: req.body }) 51 logger.debug('Checking customConfigUpdateValidator parameters', { parameters: req.body })
49 52
diff --git a/server/middlewares/validators/follows.ts b/server/middlewares/validators/follows.ts
index 38df39fda..b360cf95e 100644
--- a/server/middlewares/validators/follows.ts
+++ b/server/middlewares/validators/follows.ts
@@ -57,11 +57,11 @@ const removeFollowingValidator = [
57 } 57 }
58] 58]
59 59
60const removeFollowerValidator = [ 60const getFollowerValidator = [
61 param('nameWithHost').custom(isValidActorHandle).withMessage('Should have a valid nameWithHost'), 61 param('nameWithHost').custom(isValidActorHandle).withMessage('Should have a valid nameWithHost'),
62 62
63 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 63 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
64 logger.debug('Checking remove follower parameters', { parameters: req.params }) 64 logger.debug('Checking get follower parameters', { parameters: req.params })
65 65
66 if (areValidationErrors(req, res)) return 66 if (areValidationErrors(req, res)) return
67 67
@@ -90,10 +90,24 @@ const removeFollowerValidator = [
90 } 90 }
91] 91]
92 92
93const acceptOrRejectFollowerValidator = [
94 (req: express.Request, res: express.Response, next: express.NextFunction) => {
95 logger.debug('Checking accept/reject follower parameters', { parameters: req.params })
96
97 const follow = res.locals.follow
98 if (follow.state !== 'pending') {
99 return res.status(400).json({ error: 'Follow is not in pending state.' }).end()
100 }
101
102 return next()
103 }
104]
105
93// --------------------------------------------------------------------------- 106// ---------------------------------------------------------------------------
94 107
95export { 108export {
96 followValidator, 109 followValidator,
97 removeFollowingValidator, 110 removeFollowingValidator,
98 removeFollowerValidator 111 getFollowerValidator,
112 acceptOrRejectFollowerValidator
99} 113}
diff --git a/server/tests/api/check-params/config.ts b/server/tests/api/check-params/config.ts
index d117f26e6..01ab84584 100644
--- a/server/tests/api/check-params/config.ts
+++ b/server/tests/api/check-params/config.ts
@@ -90,7 +90,8 @@ describe('Test config API validators', function () {
90 }, 90 },
91 followers: { 91 followers: {
92 instance: { 92 instance: {
93 enabled: false 93 enabled: false,
94 manualApproval: true
94 } 95 }
95 } 96 }
96 } 97 }
diff --git a/server/tests/api/check-params/follows.ts b/server/tests/api/check-params/follows.ts
index 67fa43778..ed1d2db59 100644
--- a/server/tests/api/check-params/follows.ts
+++ b/server/tests/api/check-params/follows.ts
@@ -184,6 +184,86 @@ describe('Test server follows API validators', function () {
184 }) 184 })
185 }) 185 })
186 186
187 describe('When accepting a follower', function () {
188 const path = '/api/v1/server/followers'
189
190 it('Should fail with an invalid token', async function () {
191 await makePostBodyRequest({
192 url: server.url,
193 path: path + '/toto@localhost:9002/accept',
194 token: 'fake_token',
195 statusCodeExpected: 401
196 })
197 })
198
199 it('Should fail if the user is not an administrator', async function () {
200 await makePostBodyRequest({
201 url: server.url,
202 path: path + '/toto@localhost:9002/accept',
203 token: userAccessToken,
204 statusCodeExpected: 403
205 })
206 })
207
208 it('Should fail with an invalid follower', async function () {
209 await makePostBodyRequest({
210 url: server.url,
211 path: path + '/toto/accept',
212 token: server.accessToken,
213 statusCodeExpected: 400
214 })
215 })
216
217 it('Should fail with an unknown follower', async function () {
218 await makePostBodyRequest({
219 url: server.url,
220 path: path + '/toto@localhost:9003/accept',
221 token: server.accessToken,
222 statusCodeExpected: 404
223 })
224 })
225 })
226
227 describe('When rejecting a follower', function () {
228 const path = '/api/v1/server/followers'
229
230 it('Should fail with an invalid token', async function () {
231 await makePostBodyRequest({
232 url: server.url,
233 path: path + '/toto@localhost:9002/reject',
234 token: 'fake_token',
235 statusCodeExpected: 401
236 })
237 })
238
239 it('Should fail if the user is not an administrator', async function () {
240 await makePostBodyRequest({
241 url: server.url,
242 path: path + '/toto@localhost:9002/reject',
243 token: userAccessToken,
244 statusCodeExpected: 403
245 })
246 })
247
248 it('Should fail with an invalid follower', async function () {
249 await makePostBodyRequest({
250 url: server.url,
251 path: path + '/toto/reject',
252 token: server.accessToken,
253 statusCodeExpected: 400
254 })
255 })
256
257 it('Should fail with an unknown follower', async function () {
258 await makePostBodyRequest({
259 url: server.url,
260 path: path + '/toto@localhost:9003/reject',
261 token: server.accessToken,
262 statusCodeExpected: 404
263 })
264 })
265 })
266
187 describe('When removing following', function () { 267 describe('When removing following', function () {
188 const path = '/api/v1/server/following' 268 const path = '/api/v1/server/following'
189 269
diff --git a/server/tests/api/server/config.ts b/server/tests/api/server/config.ts
index cb2700f29..5373d02f2 100644
--- a/server/tests/api/server/config.ts
+++ b/server/tests/api/server/config.ts
@@ -65,6 +65,7 @@ function checkInitialConfig (data: CustomConfig) {
65 expect(data.autoBlacklist.videos.ofUsers.enabled).to.be.false 65 expect(data.autoBlacklist.videos.ofUsers.enabled).to.be.false
66 66
67 expect(data.followers.instance.enabled).to.be.true 67 expect(data.followers.instance.enabled).to.be.true
68 expect(data.followers.instance.manualApproval).to.be.false
68} 69}
69 70
70function checkUpdatedConfig (data: CustomConfig) { 71function checkUpdatedConfig (data: CustomConfig) {
@@ -109,6 +110,7 @@ function checkUpdatedConfig (data: CustomConfig) {
109 expect(data.autoBlacklist.videos.ofUsers.enabled).to.be.true 110 expect(data.autoBlacklist.videos.ofUsers.enabled).to.be.true
110 111
111 expect(data.followers.instance.enabled).to.be.false 112 expect(data.followers.instance.enabled).to.be.false
113 expect(data.followers.instance.manualApproval).to.be.true
112} 114}
113 115
114describe('Test config', function () { 116describe('Test config', function () {
@@ -241,7 +243,8 @@ describe('Test config', function () {
241 }, 243 },
242 followers: { 244 followers: {
243 instance: { 245 instance: {
244 enabled: false 246 enabled: false,
247 manualApproval: true
245 } 248 }
246 } 249 }
247 } 250 }
diff --git a/server/tests/api/server/follows-moderation.ts b/server/tests/api/server/follows-moderation.ts
index a360706f2..0bb3aa866 100644
--- a/server/tests/api/server/follows-moderation.ts
+++ b/server/tests/api/server/follows-moderation.ts
@@ -3,6 +3,7 @@
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { 5import {
6 acceptFollower,
6 flushAndRunMultipleServers, 7 flushAndRunMultipleServers,
7 killallServers, 8 killallServers,
8 ServerInfo, 9 ServerInfo,
@@ -13,19 +14,21 @@ import {
13 follow, 14 follow,
14 getFollowersListPaginationAndSort, 15 getFollowersListPaginationAndSort,
15 getFollowingListPaginationAndSort, 16 getFollowingListPaginationAndSort,
16 removeFollower 17 removeFollower,
18 rejectFollower
17} from '../../../../shared/utils/server/follows' 19} from '../../../../shared/utils/server/follows'
18import { waitJobs } from '../../../../shared/utils/server/jobs' 20import { waitJobs } from '../../../../shared/utils/server/jobs'
19import { ActorFollow } from '../../../../shared/models/actors' 21import { ActorFollow } from '../../../../shared/models/actors'
20 22
21const expect = chai.expect 23const expect = chai.expect
22 24
23async function checkHasFollowers (servers: ServerInfo[]) { 25async function checkServer1And2HasFollowers (servers: ServerInfo[], state = 'accepted') {
24 { 26 {
25 const res = await getFollowingListPaginationAndSort(servers[0].url, 0, 5, 'createdAt') 27 const res = await getFollowingListPaginationAndSort(servers[0].url, 0, 5, 'createdAt')
26 expect(res.body.total).to.equal(1) 28 expect(res.body.total).to.equal(1)
27 29
28 const follow = res.body.data[0] as ActorFollow 30 const follow = res.body.data[0] as ActorFollow
31 expect(follow.state).to.equal(state)
29 expect(follow.follower.url).to.equal('http://localhost:9001/accounts/peertube') 32 expect(follow.follower.url).to.equal('http://localhost:9001/accounts/peertube')
30 expect(follow.following.url).to.equal('http://localhost:9002/accounts/peertube') 33 expect(follow.following.url).to.equal('http://localhost:9002/accounts/peertube')
31 } 34 }
@@ -35,6 +38,7 @@ async function checkHasFollowers (servers: ServerInfo[]) {
35 expect(res.body.total).to.equal(1) 38 expect(res.body.total).to.equal(1)
36 39
37 const follow = res.body.data[0] as ActorFollow 40 const follow = res.body.data[0] as ActorFollow
41 expect(follow.state).to.equal(state)
38 expect(follow.follower.url).to.equal('http://localhost:9001/accounts/peertube') 42 expect(follow.follower.url).to.equal('http://localhost:9001/accounts/peertube')
39 expect(follow.following.url).to.equal('http://localhost:9002/accounts/peertube') 43 expect(follow.following.url).to.equal('http://localhost:9002/accounts/peertube')
40 } 44 }
@@ -58,7 +62,7 @@ describe('Test follows moderation', function () {
58 before(async function () { 62 before(async function () {
59 this.timeout(30000) 63 this.timeout(30000)
60 64
61 servers = await flushAndRunMultipleServers(2) 65 servers = await flushAndRunMultipleServers(3)
62 66
63 // Get the access tokens 67 // Get the access tokens
64 await setAccessTokensToServers(servers) 68 await setAccessTokensToServers(servers)
@@ -73,7 +77,7 @@ describe('Test follows moderation', function () {
73 }) 77 })
74 78
75 it('Should have correct follows', async function () { 79 it('Should have correct follows', async function () {
76 await checkHasFollowers(servers) 80 await checkServer1And2HasFollowers(servers)
77 }) 81 })
78 82
79 it('Should remove follower on server 2', async function () { 83 it('Should remove follower on server 2', async function () {
@@ -90,7 +94,8 @@ describe('Test follows moderation', function () {
90 const subConfig = { 94 const subConfig = {
91 followers: { 95 followers: {
92 instance: { 96 instance: {
93 enabled: false 97 enabled: false,
98 manualApproval: false
94 } 99 }
95 } 100 }
96 } 101 }
@@ -107,7 +112,8 @@ describe('Test follows moderation', function () {
107 const subConfig = { 112 const subConfig = {
108 followers: { 113 followers: {
109 instance: { 114 instance: {
110 enabled: true 115 enabled: true,
116 manualApproval: false
111 } 117 }
112 } 118 }
113 } 119 }
@@ -117,7 +123,70 @@ describe('Test follows moderation', function () {
117 await follow(servers[0].url, [ servers[1].url ], servers[0].accessToken) 123 await follow(servers[0].url, [ servers[1].url ], servers[0].accessToken)
118 await waitJobs(servers) 124 await waitJobs(servers)
119 125
120 await checkHasFollowers(servers) 126 await checkServer1And2HasFollowers(servers)
127 })
128
129 it('Should manually approve followers', async function () {
130 this.timeout(20000)
131
132 await removeFollower(servers[1].url, servers[1].accessToken, servers[0])
133 await waitJobs(servers)
134
135 const subConfig = {
136 followers: {
137 instance: {
138 enabled: true,
139 manualApproval: true
140 }
141 }
142 }
143
144 await updateCustomSubConfig(servers[1].url, servers[1].accessToken, subConfig)
145 await updateCustomSubConfig(servers[2].url, servers[2].accessToken, subConfig)
146
147 await follow(servers[0].url, [ servers[1].url ], servers[0].accessToken)
148 await waitJobs(servers)
149
150 await checkServer1And2HasFollowers(servers, 'pending')
151 })
152
153 it('Should accept a follower', async function () {
154 await acceptFollower(servers[1].url, servers[1].accessToken, 'peertube@localhost:9001')
155 await waitJobs(servers)
156
157 await checkServer1And2HasFollowers(servers)
158 })
159
160 it('Should reject another follower', async function () {
161 this.timeout(20000)
162
163 await follow(servers[0].url, [ servers[2].url ], servers[0].accessToken)
164 await waitJobs(servers)
165
166 {
167 const res = await getFollowingListPaginationAndSort(servers[0].url, 0, 5, 'createdAt')
168 expect(res.body.total).to.equal(2)
169 }
170
171 {
172 const res = await getFollowersListPaginationAndSort(servers[1].url, 0, 5, 'createdAt')
173 expect(res.body.total).to.equal(1)
174 }
175
176 {
177 const res = await getFollowersListPaginationAndSort(servers[2].url, 0, 5, 'createdAt')
178 expect(res.body.total).to.equal(1)
179 }
180
181 await rejectFollower(servers[2].url, servers[2].accessToken, 'peertube@localhost:9001')
182 await waitJobs(servers)
183
184 await checkServer1And2HasFollowers(servers)
185
186 {
187 const res = await getFollowersListPaginationAndSort(servers[ 2 ].url, 0, 5, 'createdAt')
188 expect(res.body.total).to.equal(0)
189 }
121 }) 190 })
122 191
123 after(async function () { 192 after(async function () {
diff --git a/shared/models/server/custom-config.model.ts b/shared/models/server/custom-config.model.ts
index 642ffea39..ca52eff4b 100644
--- a/shared/models/server/custom-config.model.ts
+++ b/shared/models/server/custom-config.model.ts
@@ -88,7 +88,8 @@ export interface CustomConfig {
88 88
89 followers: { 89 followers: {
90 instance: { 90 instance: {
91 enabled: boolean 91 enabled: boolean,
92 manualApproval: boolean
92 } 93 }
93 } 94 }
94 95
diff --git a/shared/utils/server/config.ts b/shared/utils/server/config.ts
index 21c689714..deb77e9c0 100644
--- a/shared/utils/server/config.ts
+++ b/shared/utils/server/config.ts
@@ -122,7 +122,8 @@ function updateCustomSubConfig (url: string, token: string, newConfig: any) {
122 }, 122 },
123 followers: { 123 followers: {
124 instance: { 124 instance: {
125 enabled: true 125 enabled: true,
126 manualApproval: false
126 } 127 }
127 } 128 }
128 } 129 }
diff --git a/shared/utils/server/follows.ts b/shared/utils/server/follows.ts
index 949fd8109..1505804de 100644
--- a/shared/utils/server/follows.ts
+++ b/shared/utils/server/follows.ts
@@ -1,6 +1,7 @@
1import * as request from 'supertest' 1import * as request from 'supertest'
2import { ServerInfo } from './servers' 2import { ServerInfo } from './servers'
3import { waitJobs } from './jobs' 3import { waitJobs } from './jobs'
4import { makeGetRequest, makePostBodyRequest } from '..'
4 5
5function getFollowersListPaginationAndSort (url: string, start: number, count: number, sort: string, search?: string) { 6function getFollowersListPaginationAndSort (url: string, start: number, count: number, sort: string, search?: string) {
6 const path = '/api/v1/server/followers' 7 const path = '/api/v1/server/followers'
@@ -16,6 +17,28 @@ function getFollowersListPaginationAndSort (url: string, start: number, count: n
16 .expect('Content-Type', /json/) 17 .expect('Content-Type', /json/)
17} 18}
18 19
20function acceptFollower (url: string, token: string, follower: string, statusCodeExpected = 204) {
21 const path = '/api/v1/server/followers/' + follower + '/accept'
22
23 return makePostBodyRequest({
24 url,
25 token,
26 path,
27 statusCodeExpected
28 })
29}
30
31function rejectFollower (url: string, token: string, follower: string, statusCodeExpected = 204) {
32 const path = '/api/v1/server/followers/' + follower + '/reject'
33
34 return makePostBodyRequest({
35 url,
36 token,
37 path,
38 statusCodeExpected
39 })
40}
41
19function getFollowingListPaginationAndSort (url: string, start: number, count: number, sort: string, search?: string) { 42function getFollowingListPaginationAndSort (url: string, start: number, count: number, sort: string, search?: string) {
20 const path = '/api/v1/server/following' 43 const path = '/api/v1/server/following'
21 44
@@ -30,18 +53,16 @@ function getFollowingListPaginationAndSort (url: string, start: number, count: n
30 .expect('Content-Type', /json/) 53 .expect('Content-Type', /json/)
31} 54}
32 55
33async function follow (follower: string, following: string[], accessToken: string, expectedStatus = 204) { 56function follow (follower: string, following: string[], accessToken: string, expectedStatus = 204) {
34 const path = '/api/v1/server/following' 57 const path = '/api/v1/server/following'
35 58
36 const followingHosts = following.map(f => f.replace(/^http:\/\//, '')) 59 const followingHosts = following.map(f => f.replace(/^http:\/\//, ''))
37 const res = await request(follower) 60 return request(follower)
38 .post(path) 61 .post(path)
39 .set('Accept', 'application/json') 62 .set('Accept', 'application/json')
40 .set('Authorization', 'Bearer ' + accessToken) 63 .set('Authorization', 'Bearer ' + accessToken)
41 .send({ 'hosts': followingHosts }) 64 .send({ 'hosts': followingHosts })
42 .expect(expectedStatus) 65 .expect(expectedStatus)
43
44 return res
45} 66}
46 67
47async function unfollow (url: string, accessToken: string, target: ServerInfo, expectedStatus = 204) { 68async function unfollow (url: string, accessToken: string, target: ServerInfo, expectedStatus = 204) {
@@ -84,5 +105,7 @@ export {
84 unfollow, 105 unfollow,
85 removeFollower, 106 removeFollower,
86 follow, 107 follow,
87 doubleFollow 108 doubleFollow,
109 acceptFollower,
110 rejectFollower
88} 111}