aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2022-07-26 14:46:15 +0200
committerChocobozzz <me@florianbigard.com>2022-07-27 13:52:13 +0200
commit927fa4b11f692174d6296aa096d7a74bacdeea8b (patch)
tree20866dda219bbb5504d5645a980565fbbc25398a
parent0f58b11f5cace6e57cab5b4a18380eb297b43fe4 (diff)
downloadPeerTube-927fa4b11f692174d6296aa096d7a74bacdeea8b.tar.gz
PeerTube-927fa4b11f692174d6296aa096d7a74bacdeea8b.tar.zst
PeerTube-927fa4b11f692174d6296aa096d7a74bacdeea8b.zip
Add rejected state to follows
Prevent reprocessing already rejected follows
-rw-r--r--server/controllers/api/server/follows.ts34
-rw-r--r--server/helpers/custom-validators/follows.ts2
-rw-r--r--server/initializers/constants.ts3
-rw-r--r--server/lib/activitypub/process/process-follow.ts5
-rw-r--r--server/lib/activitypub/process/process-reject.ts3
-rw-r--r--server/middlewares/validators/follows.ts30
-rw-r--r--server/middlewares/validators/user-subscriptions.ts7
-rw-r--r--server/models/actor/actor-follow.ts40
-rw-r--r--server/models/actor/actor.ts2
-rw-r--r--server/tests/api/server/follows-moderation.ts184
-rw-r--r--shared/models/actors/follow.model.ts2
-rw-r--r--shared/server-commands/server/follows-command.ts8
12 files changed, 266 insertions, 54 deletions
diff --git a/server/controllers/api/server/follows.ts b/server/controllers/api/server/follows.ts
index 37e8d88f7..60d36ed59 100644
--- a/server/controllers/api/server/follows.ts
+++ b/server/controllers/api/server/follows.ts
@@ -1,5 +1,6 @@
1import express from 'express' 1import express from 'express'
2import { getServerActor } from '@server/models/application/application' 2import { getServerActor } from '@server/models/application/application'
3import { ServerFollowCreate } from '@shared/models'
3import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' 4import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
4import { UserRight } from '../../../../shared/models/users' 5import { UserRight } from '../../../../shared/models/users'
5import { logger } from '../../../helpers/logger' 6import { logger } from '../../../helpers/logger'
@@ -20,16 +21,16 @@ import {
20 setDefaultSort 21 setDefaultSort
21} from '../../../middlewares' 22} from '../../../middlewares'
22import { 23import {
23 acceptOrRejectFollowerValidator, 24 acceptFollowerValidator,
24 instanceFollowersSortValidator,
25 instanceFollowingSortValidator,
26 followValidator, 25 followValidator,
27 getFollowerValidator, 26 getFollowerValidator,
27 instanceFollowersSortValidator,
28 instanceFollowingSortValidator,
28 listFollowsValidator, 29 listFollowsValidator,
30 rejectFollowerValidator,
29 removeFollowingValidator 31 removeFollowingValidator
30} from '../../../middlewares/validators' 32} from '../../../middlewares/validators'
31import { ActorFollowModel } from '../../../models/actor/actor-follow' 33import { ActorFollowModel } from '../../../models/actor/actor-follow'
32import { ServerFollowCreate } from '@shared/models'
33 34
34const serverFollowsRouter = express.Router() 35const serverFollowsRouter = express.Router()
35serverFollowsRouter.get('/following', 36serverFollowsRouter.get('/following',
@@ -69,22 +70,22 @@ serverFollowsRouter.delete('/followers/:nameWithHost',
69 authenticate, 70 authenticate,
70 ensureUserHasRight(UserRight.MANAGE_SERVER_FOLLOW), 71 ensureUserHasRight(UserRight.MANAGE_SERVER_FOLLOW),
71 asyncMiddleware(getFollowerValidator), 72 asyncMiddleware(getFollowerValidator),
72 asyncMiddleware(removeOrRejectFollower) 73 asyncMiddleware(removeFollower)
73) 74)
74 75
75serverFollowsRouter.post('/followers/:nameWithHost/reject', 76serverFollowsRouter.post('/followers/:nameWithHost/reject',
76 authenticate, 77 authenticate,
77 ensureUserHasRight(UserRight.MANAGE_SERVER_FOLLOW), 78 ensureUserHasRight(UserRight.MANAGE_SERVER_FOLLOW),
78 asyncMiddleware(getFollowerValidator), 79 asyncMiddleware(getFollowerValidator),
79 acceptOrRejectFollowerValidator, 80 rejectFollowerValidator,
80 asyncMiddleware(removeOrRejectFollower) 81 asyncMiddleware(rejectFollower)
81) 82)
82 83
83serverFollowsRouter.post('/followers/:nameWithHost/accept', 84serverFollowsRouter.post('/followers/:nameWithHost/accept',
84 authenticate, 85 authenticate,
85 ensureUserHasRight(UserRight.MANAGE_SERVER_FOLLOW), 86 ensureUserHasRight(UserRight.MANAGE_SERVER_FOLLOW),
86 asyncMiddleware(getFollowerValidator), 87 asyncMiddleware(getFollowerValidator),
87 acceptOrRejectFollowerValidator, 88 acceptFollowerValidator,
88 asyncMiddleware(acceptFollower) 89 asyncMiddleware(acceptFollower)
89) 90)
90 91
@@ -176,10 +177,23 @@ async function removeFollowing (req: express.Request, res: express.Response) {
176 return res.status(HttpStatusCode.NO_CONTENT_204).end() 177 return res.status(HttpStatusCode.NO_CONTENT_204).end()
177} 178}
178 179
179async function removeOrRejectFollower (req: express.Request, res: express.Response) { 180async function rejectFollower (req: express.Request, res: express.Response) {
180 const follow = res.locals.follow 181 const follow = res.locals.follow
181 182
182 await sendReject(follow.url, follow.ActorFollower, follow.ActorFollowing) 183 follow.state = 'rejected'
184 await follow.save()
185
186 sendReject(follow.url, follow.ActorFollower, follow.ActorFollowing)
187
188 return res.status(HttpStatusCode.NO_CONTENT_204).end()
189}
190
191async function removeFollower (req: express.Request, res: express.Response) {
192 const follow = res.locals.follow
193
194 if (follow.state === 'accepted' || follow.state === 'pending') {
195 sendReject(follow.url, follow.ActorFollower, follow.ActorFollowing)
196 }
183 197
184 await follow.destroy() 198 await follow.destroy()
185 199
diff --git a/server/helpers/custom-validators/follows.ts b/server/helpers/custom-validators/follows.ts
index 8f65552c3..0bec683c1 100644
--- a/server/helpers/custom-validators/follows.ts
+++ b/server/helpers/custom-validators/follows.ts
@@ -4,7 +4,7 @@ import { FollowState } from '@shared/models'
4function isFollowStateValid (value: FollowState) { 4function isFollowStateValid (value: FollowState) {
5 if (!exists(value)) return false 5 if (!exists(value)) return false
6 6
7 return value === 'pending' || value === 'accepted' 7 return value === 'pending' || value === 'accepted' || value === 'rejected'
8} 8}
9 9
10function isRemoteHandleValid (value: string) { 10function isRemoteHandleValid (value: string) {
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index e3f7ceb4a..99ae64f8d 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -129,7 +129,8 @@ const ACTOR_FOLLOW_SCORE = {
129 129
130const FOLLOW_STATES: { [ id: string ]: FollowState } = { 130const FOLLOW_STATES: { [ id: string ]: FollowState } = {
131 PENDING: 'pending', 131 PENDING: 'pending',
132 ACCEPTED: 'accepted' 132 ACCEPTED: 'accepted',
133 REJECTED: 'rejected'
133} 134}
134 135
135const REMOTE_SCHEME = { 136const REMOTE_SCHEME = {
diff --git a/server/lib/activitypub/process/process-follow.ts b/server/lib/activitypub/process/process-follow.ts
index 93df7e191..a1958f464 100644
--- a/server/lib/activitypub/process/process-follow.ts
+++ b/server/lib/activitypub/process/process-follow.ts
@@ -58,6 +58,11 @@ async function processFollow (byActor: MActorSignature, activityId: string, targ
58 transaction: t 58 transaction: t
59 }) 59 })
60 60
61 // Already rejected
62 if (actorFollow.state === 'rejected') {
63 return { actorFollow: undefined as MActorFollowActors }
64 }
65
61 // Set the follow as accepted if the remote actor follows a channel or account 66 // Set the follow as accepted if the remote actor follows a channel or account
62 // Or if the instance automatically accepts followers 67 // Or if the instance automatically accepts followers
63 if (actorFollow.state !== 'accepted' && (isFollowingInstance === false || CONFIG.FOLLOWERS.INSTANCE.MANUAL_APPROVAL === false)) { 68 if (actorFollow.state !== 'accepted' && (isFollowingInstance === false || CONFIG.FOLLOWERS.INSTANCE.MANUAL_APPROVAL === false)) {
diff --git a/server/lib/activitypub/process/process-reject.ts b/server/lib/activitypub/process/process-reject.ts
index 7f7ab305f..db7ff24d8 100644
--- a/server/lib/activitypub/process/process-reject.ts
+++ b/server/lib/activitypub/process/process-reject.ts
@@ -25,7 +25,8 @@ async function processReject (follower: MActor, targetActor: MActor) {
25 25
26 if (!actorFollow) throw new Error(`'Unknown actor follow ${follower.id} -> ${targetActor.id}.`) 26 if (!actorFollow) throw new Error(`'Unknown actor follow ${follower.id} -> ${targetActor.id}.`)
27 27
28 await actorFollow.destroy({ transaction: t }) 28 actorFollow.state = 'rejected'
29 await actorFollow.save({ transaction: t })
29 30
30 return undefined 31 return undefined
31 }) 32 })
diff --git a/server/middlewares/validators/follows.ts b/server/middlewares/validators/follows.ts
index 86d2d6228..023dba5b5 100644
--- a/server/middlewares/validators/follows.ts
+++ b/server/middlewares/validators/follows.ts
@@ -81,7 +81,11 @@ const removeFollowingValidator = [
81 const serverActor = await getServerActor() 81 const serverActor = await getServerActor()
82 82
83 const { name, host } = getRemoteNameAndHost(req.params.hostOrHandle) 83 const { name, host } = getRemoteNameAndHost(req.params.hostOrHandle)
84 const follow = await ActorFollowModel.loadByActorAndTargetNameAndHostForAPI(serverActor.id, name, host) 84 const follow = await ActorFollowModel.loadByActorAndTargetNameAndHostForAPI({
85 actorId: serverActor.id,
86 targetName: name,
87 targetHost: host
88 })
85 89
86 if (!follow) { 90 if (!follow) {
87 return res.fail({ 91 return res.fail({
@@ -126,13 +130,26 @@ const getFollowerValidator = [
126 } 130 }
127] 131]
128 132
129const acceptOrRejectFollowerValidator = [ 133const acceptFollowerValidator = [
130 (req: express.Request, res: express.Response, next: express.NextFunction) => { 134 (req: express.Request, res: express.Response, next: express.NextFunction) => {
131 logger.debug('Checking accept/reject follower parameters', { parameters: req.params }) 135 logger.debug('Checking accept follower parameters', { parameters: req.params })
132 136
133 const follow = res.locals.follow 137 const follow = res.locals.follow
134 if (follow.state !== 'pending') { 138 if (follow.state !== 'pending' && follow.state !== 'rejected') {
135 return res.fail({ message: 'Follow is not in pending state.' }) 139 return res.fail({ message: 'Follow is not in pending/rejected state.' })
140 }
141
142 return next()
143 }
144]
145
146const rejectFollowerValidator = [
147 (req: express.Request, res: express.Response, next: express.NextFunction) => {
148 logger.debug('Checking reject follower parameters', { parameters: req.params })
149
150 const follow = res.locals.follow
151 if (follow.state !== 'pending' && follow.state !== 'accepted') {
152 return res.fail({ message: 'Follow is not in pending/accepted state.' })
136 } 153 }
137 154
138 return next() 155 return next()
@@ -145,6 +162,7 @@ export {
145 followValidator, 162 followValidator,
146 removeFollowingValidator, 163 removeFollowingValidator,
147 getFollowerValidator, 164 getFollowerValidator,
148 acceptOrRejectFollowerValidator, 165 acceptFollowerValidator,
166 rejectFollowerValidator,
149 listFollowsValidator 167 listFollowsValidator
150} 168}
diff --git a/server/middlewares/validators/user-subscriptions.ts b/server/middlewares/validators/user-subscriptions.ts
index 48ce90d7b..73da3142a 100644
--- a/server/middlewares/validators/user-subscriptions.ts
+++ b/server/middlewares/validators/user-subscriptions.ts
@@ -58,7 +58,12 @@ const userSubscriptionGetValidator = [
58 if (host === WEBSERVER.HOST) host = null 58 if (host === WEBSERVER.HOST) host = null
59 59
60 const user = res.locals.oauth.token.User 60 const user = res.locals.oauth.token.User
61 const subscription = await ActorFollowModel.loadByActorAndTargetNameAndHostForAPI(user.Account.Actor.id, name, host) 61 const subscription = await ActorFollowModel.loadByActorAndTargetNameAndHostForAPI({
62 actorId: user.Account.Actor.id,
63 targetName: name,
64 targetHost: host,
65 state: 'accepted'
66 })
62 67
63 if (!subscription || !subscription.ActorFollowing.VideoChannel) { 68 if (!subscription || !subscription.ActorFollowing.VideoChannel) {
64 return res.fail({ 69 return res.fail({
diff --git a/server/models/actor/actor-follow.ts b/server/models/actor/actor-follow.ts
index 8870bec05..566bb5f31 100644
--- a/server/models/actor/actor-follow.ts
+++ b/server/models/actor/actor-follow.ts
@@ -1,5 +1,5 @@
1import { difference, values } from 'lodash' 1import { difference, values } from 'lodash'
2import { Includeable, IncludeOptions, Op, QueryTypes, Transaction } from 'sequelize' 2import { Attributes, FindOptions, Includeable, IncludeOptions, Op, QueryTypes, Transaction, WhereAttributeHash } from 'sequelize'
3import { 3import {
4 AfterCreate, 4 AfterCreate,
5 AfterDestroy, 5 AfterDestroy,
@@ -209,7 +209,9 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo
209 } 209 }
210 210
211 static isFollowedBy (actorId: number, followerActorId: number) { 211 static isFollowedBy (actorId: number, followerActorId: number) {
212 const query = 'SELECT 1 FROM "actorFollow" WHERE "actorId" = $followerActorId AND "targetActorId" = $actorId LIMIT 1' 212 const query = `SELECT 1 FROM "actorFollow" ` +
213 `WHERE "actorId" = $followerActorId AND "targetActorId" = $actorId AND "state" = 'accepted' ` +
214 `LIMIT 1`
213 215
214 return doesExist(query, { actorId, followerActorId }) 216 return doesExist(query, { actorId, followerActorId })
215 } 217 }
@@ -238,12 +240,15 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo
238 return ActorFollowModel.findOne(query) 240 return ActorFollowModel.findOne(query)
239 } 241 }
240 242
241 static loadByActorAndTargetNameAndHostForAPI ( 243 static loadByActorAndTargetNameAndHostForAPI (options: {
242 actorId: number, 244 actorId: number
243 targetName: string, 245 targetName: string
244 targetHost: string, 246 targetHost: string
245 t?: Transaction 247 state?: FollowState
246 ): Promise<MActorFollowActorsDefaultSubscription> { 248 transaction?: Transaction
249 }): Promise<MActorFollowActorsDefaultSubscription> {
250 const { actorId, targetHost, targetName, state, transaction } = options
251
247 const actorFollowingPartInclude: IncludeOptions = { 252 const actorFollowingPartInclude: IncludeOptions = {
248 model: ActorModel, 253 model: ActorModel,
249 required: true, 254 required: true,
@@ -271,10 +276,11 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo
271 }) 276 })
272 } 277 }
273 278
274 const query = { 279 const where: WhereAttributeHash<Attributes<ActorFollowModel>> = { actorId}
275 where: { 280 if (state) where.state = state
276 actorId 281
277 }, 282 const query: FindOptions<Attributes<ActorFollowModel>> = {
283 where,
278 include: [ 284 include: [
279 actorFollowingPartInclude, 285 actorFollowingPartInclude,
280 { 286 {
@@ -283,7 +289,7 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo
283 as: 'ActorFollower' 289 as: 'ActorFollower'
284 } 290 }
285 ], 291 ],
286 transaction: t 292 transaction
287 } 293 }
288 294
289 return ActorFollowModel.findOne(query) 295 return ActorFollowModel.findOne(query)
@@ -325,6 +331,7 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo
325 [Op.or]: whereTab 331 [Op.or]: whereTab
326 }, 332 },
327 { 333 {
334 state: 'accepted',
328 actorId 335 actorId
329 } 336 }
330 ] 337 ]
@@ -372,6 +379,7 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo
372 }) { 379 }) {
373 const { actorId, start, count, sort } = options 380 const { actorId, start, count, sort } = options
374 const where = { 381 const where = {
382 state: 'accepted',
375 actorId 383 actorId
376 } 384 }
377 385
@@ -512,13 +520,15 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo
512 520
513 const totalInstanceFollowing = await ActorFollowModel.count({ 521 const totalInstanceFollowing = await ActorFollowModel.count({
514 where: { 522 where: {
515 actorId: serverActor.id 523 actorId: serverActor.id,
524 state: 'accepted'
516 } 525 }
517 }) 526 })
518 527
519 const totalInstanceFollowers = await ActorFollowModel.count({ 528 const totalInstanceFollowers = await ActorFollowModel.count({
520 where: { 529 where: {
521 targetActorId: serverActor.id 530 targetActorId: serverActor.id,
531 state: 'accepted'
522 } 532 }
523 }) 533 })
524 534
diff --git a/server/models/actor/actor.ts b/server/models/actor/actor.ts
index 943b7364f..7be5a140c 100644
--- a/server/models/actor/actor.ts
+++ b/server/models/actor/actor.ts
@@ -462,7 +462,7 @@ export class ActorModel extends Model<Partial<AttributesOnly<ActorModel>>> {
462 } 462 }
463 463
464 return ActorModel.update({ 464 return ActorModel.update({
465 [columnToUpdate]: literal(`(SELECT COUNT(*) FROM "actorFollow" WHERE "${columnOfCount}" = ${sanitizedOfId})`) 465 [columnToUpdate]: literal(`(SELECT COUNT(*) FROM "actorFollow" WHERE "${columnOfCount}" = ${sanitizedOfId} AND "state" = 'accepted')`)
466 }, { where, transaction }) 466 }, { where, transaction })
467 } 467 }
468 468
diff --git a/server/tests/api/server/follows-moderation.ts b/server/tests/api/server/follows-moderation.ts
index 120bd7f88..a0e94c10e 100644
--- a/server/tests/api/server/follows-moderation.ts
+++ b/server/tests/api/server/follows-moderation.ts
@@ -2,6 +2,8 @@
2 2
3import 'mocha' 3import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
5import { expectStartWith } from '@server/tests/shared'
6import { ActorFollow, FollowState } from '@shared/models'
5import { 7import {
6 cleanupTests, 8 cleanupTests,
7 createMultipleServers, 9 createMultipleServers,
@@ -25,8 +27,51 @@ async function checkServer1And2HasFollowers (servers: PeerTubeServer[], state =
25 27
26 const follow = body.data[0] 28 const follow = body.data[0]
27 expect(follow.state).to.equal(state) 29 expect(follow.state).to.equal(state)
28 expect(follow.follower.url).to.equal('http://localhost:' + servers[0].port + '/accounts/peertube') 30 expect(follow.follower.url).to.equal(servers[0].url + '/accounts/peertube')
29 expect(follow.following.url).to.equal('http://localhost:' + servers[1].port + '/accounts/peertube') 31 expect(follow.following.url).to.equal(servers[1].url + '/accounts/peertube')
32 }
33}
34
35async function checkFollows (options: {
36 follower: {
37 server: PeerTubeServer
38 state?: FollowState // if not provided, it means it does not exist
39 }
40 following: {
41 server: PeerTubeServer
42 state?: FollowState // if not provided, it means it does not exist
43 }
44}) {
45 const { follower, following } = options
46
47 const followerUrl = follower.server.url + '/accounts/peertube'
48 const followingUrl = following.server.url + '/accounts/peertube'
49 const finder = (d: ActorFollow) => d.follower.url === followerUrl && d.following.url === followingUrl
50
51 {
52 const { data } = await follower.server.follows.getFollowings()
53 const follow = data.find(finder)
54
55 if (!follower.state) {
56 expect(follow).to.not.exist
57 } else {
58 expect(follow.state).to.equal(follower.state)
59 expect(follow.follower.url).to.equal(followerUrl)
60 expect(follow.following.url).to.equal(followingUrl)
61 }
62 }
63
64 {
65 const { data } = await following.server.follows.getFollowers()
66 const follow = data.find(finder)
67
68 if (!following.state) {
69 expect(follow).to.not.exist
70 } else {
71 expect(follow.state).to.equal(following.state)
72 expect(follow.follower.url).to.equal(followerUrl)
73 expect(follow.following.url).to.equal(followingUrl)
74 }
30 } 75 }
31} 76}
32 77
@@ -37,7 +82,7 @@ async function checkNoFollowers (servers: PeerTubeServer[]) {
37 ] 82 ]
38 83
39 for (const fn of fns) { 84 for (const fn of fns) {
40 const body = await fn({ start: 0, count: 5, sort: 'createdAt' }) 85 const body = await fn({ start: 0, count: 5, sort: 'createdAt', state: 'accepted' })
41 expect(body.total).to.equal(0) 86 expect(body.total).to.equal(0)
42 } 87 }
43} 88}
@@ -124,7 +169,7 @@ describe('Test follows moderation', function () {
124 it('Should manually approve followers', async function () { 169 it('Should manually approve followers', async function () {
125 this.timeout(20000) 170 this.timeout(20000)
126 171
127 await commands[1].removeFollower({ follower: servers[0] }) 172 await commands[0].unfollow({ target: servers[1] })
128 await waitJobs(servers) 173 await waitJobs(servers)
129 174
130 const subConfig = { 175 const subConfig = {
@@ -148,7 +193,7 @@ describe('Test follows moderation', function () {
148 it('Should accept a follower', async function () { 193 it('Should accept a follower', async function () {
149 this.timeout(10000) 194 this.timeout(10000)
150 195
151 await commands[1].acceptFollower({ follower: 'peertube@localhost:' + servers[0].port }) 196 await commands[1].acceptFollower({ follower: 'peertube@' + servers[0].host })
152 await waitJobs(servers) 197 await waitJobs(servers)
153 198
154 await checkServer1And2HasFollowers(servers) 199 await checkServer1And2HasFollowers(servers)
@@ -161,31 +206,144 @@ describe('Test follows moderation', function () {
161 await waitJobs(servers) 206 await waitJobs(servers)
162 207
163 { 208 {
164 const body = await commands[0].getFollowings({ start: 0, count: 5, sort: 'createdAt' }) 209 const body = await commands[0].getFollowings()
165 expect(body.total).to.equal(2) 210 expect(body.total).to.equal(2)
166 } 211 }
167 212
168 { 213 {
169 const body = await commands[1].getFollowers({ start: 0, count: 5, sort: 'createdAt' }) 214 const body = await commands[1].getFollowers()
170 expect(body.total).to.equal(1) 215 expect(body.total).to.equal(1)
171 } 216 }
172 217
173 { 218 {
174 const body = await commands[2].getFollowers({ start: 0, count: 5, sort: 'createdAt' }) 219 const body = await commands[2].getFollowers()
175 expect(body.total).to.equal(1) 220 expect(body.total).to.equal(1)
176 } 221 }
177 222
178 await commands[2].rejectFollower({ follower: 'peertube@localhost:' + servers[0].port }) 223 await commands[2].rejectFollower({ follower: 'peertube@' + servers[0].host })
179 await waitJobs(servers) 224 await waitJobs(servers)
180 225
181 await checkServer1And2HasFollowers(servers) 226 { // server 1
227 {
228 const { data } = await commands[0].getFollowings({ state: 'accepted' })
229 expect(data).to.have.lengthOf(1)
230 }
182 231
183 { 232 {
184 const body = await commands[2].getFollowers({ start: 0, count: 5, sort: 'createdAt' }) 233 const { data } = await commands[0].getFollowings({ state: 'rejected' })
185 expect(body.total).to.equal(0) 234 expect(data).to.have.lengthOf(1)
235 expectStartWith(data[0].following.url, servers[2].url)
236 }
237 }
238
239 { // server 3
240 {
241 const { data } = await commands[2].getFollowers({ state: 'accepted' })
242 expect(data).to.have.lengthOf(0)
243 }
244
245 {
246 const { data } = await commands[2].getFollowers({ state: 'rejected' })
247 expect(data).to.have.lengthOf(1)
248 expectStartWith(data[0].follower.url, servers[0].url)
249 }
186 } 250 }
187 }) 251 })
188 252
253 it('Should not change the follow on refollow with and without auto accept', async function () {
254 const run = async () => {
255 await commands[0].follow({ hosts: [ servers[2].url ] })
256 await waitJobs(servers)
257
258 await checkFollows({
259 follower: {
260 server: servers[0],
261 state: 'rejected'
262 },
263 following: {
264 server: servers[2],
265 state: 'rejected'
266 }
267 })
268 }
269
270 await servers[2].config.updateExistingSubConfig({ newConfig: { followers: { instance: { manualApproval: false } } } })
271 await run()
272
273 await servers[2].config.updateExistingSubConfig({ newConfig: { followers: { instance: { manualApproval: true } } } })
274 await run()
275 })
276
277 it('Should not change the rejected status on unfollow', async function () {
278 await commands[0].unfollow({ target: servers[2] })
279 await waitJobs(servers)
280
281 await checkFollows({
282 follower: {
283 server: servers[0]
284 },
285 following: {
286 server: servers[2],
287 state: 'rejected'
288 }
289 })
290 })
291
292 it('Should delete the follower and add again the follower', async function () {
293 await commands[2].removeFollower({ follower: servers[0] })
294 await waitJobs(servers)
295
296 await commands[0].follow({ hosts: [ servers[2].url ] })
297 await waitJobs(servers)
298
299 await checkFollows({
300 follower: {
301 server: servers[0],
302 state: 'pending'
303 },
304 following: {
305 server: servers[2],
306 state: 'pending'
307 }
308 })
309 })
310
311 it('Should be able to reject a previously accepted follower', async function () {
312 await commands[1].rejectFollower({ follower: 'peertube@' + servers[0].host })
313 await waitJobs(servers)
314
315 await checkFollows({
316 follower: {
317 server: servers[0],
318 state: 'rejected'
319 },
320 following: {
321 server: servers[1],
322 state: 'rejected'
323 }
324 })
325 })
326
327 it('Should be able to re accept a previously rejected follower', async function () {
328 await commands[1].acceptFollower({ follower: 'peertube@' + servers[0].host })
329 await waitJobs(servers)
330
331 await checkFollows({
332 follower: {
333 server: servers[0],
334 state: 'accepted'
335 },
336 following: {
337 server: servers[1],
338 state: 'accepted'
339 }
340 })
341 })
342
343 it('Should ignore follow requests of muted servers', async function () {
344
345 })
346
189 after(async function () { 347 after(async function () {
190 await cleanupTests(servers) 348 await cleanupTests(servers)
191 }) 349 })
diff --git a/shared/models/actors/follow.model.ts b/shared/models/actors/follow.model.ts
index 7de638cba..244d6d97e 100644
--- a/shared/models/actors/follow.model.ts
+++ b/shared/models/actors/follow.model.ts
@@ -1,6 +1,6 @@
1import { Actor } from './actor.model' 1import { Actor } from './actor.model'
2 2
3export type FollowState = 'pending' | 'accepted' 3export type FollowState = 'pending' | 'accepted' | 'rejected'
4 4
5export interface ActorFollow { 5export interface ActorFollow {
6 id: number 6 id: number
diff --git a/shared/server-commands/server/follows-command.ts b/shared/server-commands/server/follows-command.ts
index 01ef6f179..496e11df1 100644
--- a/shared/server-commands/server/follows-command.ts
+++ b/shared/server-commands/server/follows-command.ts
@@ -6,13 +6,13 @@ import { PeerTubeServer } from './server'
6export class FollowsCommand extends AbstractCommand { 6export class FollowsCommand extends AbstractCommand {
7 7
8 getFollowers (options: OverrideCommandOptions & { 8 getFollowers (options: OverrideCommandOptions & {
9 start: number 9 start?: number
10 count: number 10 count?: number
11 sort: string 11 sort?: string
12 search?: string 12 search?: string
13 actorType?: ActivityPubActorType 13 actorType?: ActivityPubActorType
14 state?: FollowState 14 state?: FollowState
15 }) { 15 } = {}) {
16 const path = '/api/v1/server/followers' 16 const path = '/api/v1/server/followers'
17 17
18 const query = pick(options, [ 'start', 'count', 'sort', 'search', 'state', 'actorType' ]) 18 const query = pick(options, [ 'start', 'count', 'sort', 'search', 'state', 'actorType' ])