diff options
author | Chocobozzz <me@florianbigard.com> | 2022-07-26 14:46:15 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2022-07-27 13:52:13 +0200 |
commit | 927fa4b11f692174d6296aa096d7a74bacdeea8b (patch) | |
tree | 20866dda219bbb5504d5645a980565fbbc25398a | |
parent | 0f58b11f5cace6e57cab5b4a18380eb297b43fe4 (diff) | |
download | PeerTube-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.ts | 34 | ||||
-rw-r--r-- | server/helpers/custom-validators/follows.ts | 2 | ||||
-rw-r--r-- | server/initializers/constants.ts | 3 | ||||
-rw-r--r-- | server/lib/activitypub/process/process-follow.ts | 5 | ||||
-rw-r--r-- | server/lib/activitypub/process/process-reject.ts | 3 | ||||
-rw-r--r-- | server/middlewares/validators/follows.ts | 30 | ||||
-rw-r--r-- | server/middlewares/validators/user-subscriptions.ts | 7 | ||||
-rw-r--r-- | server/models/actor/actor-follow.ts | 40 | ||||
-rw-r--r-- | server/models/actor/actor.ts | 2 | ||||
-rw-r--r-- | server/tests/api/server/follows-moderation.ts | 184 | ||||
-rw-r--r-- | shared/models/actors/follow.model.ts | 2 | ||||
-rw-r--r-- | shared/server-commands/server/follows-command.ts | 8 |
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 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import { getServerActor } from '@server/models/application/application' | 2 | import { getServerActor } from '@server/models/application/application' |
3 | import { ServerFollowCreate } from '@shared/models' | ||
3 | import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' | 4 | import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' |
4 | import { UserRight } from '../../../../shared/models/users' | 5 | import { UserRight } from '../../../../shared/models/users' |
5 | import { logger } from '../../../helpers/logger' | 6 | import { logger } from '../../../helpers/logger' |
@@ -20,16 +21,16 @@ import { | |||
20 | setDefaultSort | 21 | setDefaultSort |
21 | } from '../../../middlewares' | 22 | } from '../../../middlewares' |
22 | import { | 23 | import { |
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' |
31 | import { ActorFollowModel } from '../../../models/actor/actor-follow' | 33 | import { ActorFollowModel } from '../../../models/actor/actor-follow' |
32 | import { ServerFollowCreate } from '@shared/models' | ||
33 | 34 | ||
34 | const serverFollowsRouter = express.Router() | 35 | const serverFollowsRouter = express.Router() |
35 | serverFollowsRouter.get('/following', | 36 | serverFollowsRouter.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 | ||
75 | serverFollowsRouter.post('/followers/:nameWithHost/reject', | 76 | serverFollowsRouter.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 | ||
83 | serverFollowsRouter.post('/followers/:nameWithHost/accept', | 84 | serverFollowsRouter.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 | ||
179 | async function removeOrRejectFollower (req: express.Request, res: express.Response) { | 180 | async 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 | |||
191 | async 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' | |||
4 | function isFollowStateValid (value: FollowState) { | 4 | function 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 | ||
10 | function isRemoteHandleValid (value: string) { | 10 | function 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 | ||
130 | const FOLLOW_STATES: { [ id: string ]: FollowState } = { | 130 | const FOLLOW_STATES: { [ id: string ]: FollowState } = { |
131 | PENDING: 'pending', | 131 | PENDING: 'pending', |
132 | ACCEPTED: 'accepted' | 132 | ACCEPTED: 'accepted', |
133 | REJECTED: 'rejected' | ||
133 | } | 134 | } |
134 | 135 | ||
135 | const REMOTE_SCHEME = { | 136 | const 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 | ||
129 | const acceptOrRejectFollowerValidator = [ | 133 | const 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 | |||
146 | const 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 @@ | |||
1 | import { difference, values } from 'lodash' | 1 | import { difference, values } from 'lodash' |
2 | import { Includeable, IncludeOptions, Op, QueryTypes, Transaction } from 'sequelize' | 2 | import { Attributes, FindOptions, Includeable, IncludeOptions, Op, QueryTypes, Transaction, WhereAttributeHash } from 'sequelize' |
3 | import { | 3 | import { |
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 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { expectStartWith } from '@server/tests/shared' | ||
6 | import { ActorFollow, FollowState } from '@shared/models' | ||
5 | import { | 7 | import { |
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 | |||
35 | async 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 @@ | |||
1 | import { Actor } from './actor.model' | 1 | import { Actor } from './actor.model' |
2 | 2 | ||
3 | export type FollowState = 'pending' | 'accepted' | 3 | export type FollowState = 'pending' | 'accepted' | 'rejected' |
4 | 4 | ||
5 | export interface ActorFollow { | 5 | export 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' | |||
6 | export class FollowsCommand extends AbstractCommand { | 6 | export 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' ]) |