diff options
-rw-r--r-- | server/controllers/api/server/follows.ts | 32 | ||||
-rw-r--r-- | server/lib/activitypub/send/index.ts | 2 | ||||
-rw-r--r-- | server/lib/activitypub/send/send-reject.ts | 44 | ||||
-rw-r--r-- | server/lib/hls.ts | 3 | ||||
-rw-r--r-- | server/middlewares/validators/follows.ts | 40 | ||||
-rw-r--r-- | server/tests/api/server/follows-moderation.ts | 78 | ||||
-rw-r--r-- | server/tests/api/server/index.ts | 1 | ||||
-rw-r--r-- | shared/utils/server/follows.ts | 13 |
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' | |||
3 | import { logger } from '../../../helpers/logger' | 3 | import { logger } from '../../../helpers/logger' |
4 | import { getFormattedObjects, getServerActor } from '../../../helpers/utils' | 4 | import { getFormattedObjects, getServerActor } from '../../../helpers/utils' |
5 | import { sequelizeTypescript, SERVER_ACTOR_NAME } from '../../../initializers' | 5 | import { sequelizeTypescript, SERVER_ACTOR_NAME } from '../../../initializers' |
6 | import { sendUndoFollow } from '../../../lib/activitypub/send' | 6 | import { sendReject, sendUndoFollow } from '../../../lib/activitypub/send' |
7 | import { | 7 | import { |
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' |
17 | import { followersSortValidator, followingSortValidator, followValidator } from '../../../middlewares/validators' | 16 | import { |
17 | followersSortValidator, | ||
18 | followingSortValidator, | ||
19 | followValidator, | ||
20 | removeFollowerValidator, | ||
21 | removeFollowingValidator | ||
22 | } from '../../../middlewares/validators' | ||
18 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | 23 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' |
19 | import { JobQueue } from '../../../lib/job-queue' | 24 | import { JobQueue } from '../../../lib/job-queue' |
20 | import { removeRedundancyOf } from '../../../lib/redundancy' | 25 | import { 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 | ||
46 | serverFollowsRouter.get('/followers', | 51 | serverFollowsRouter.get('/followers', |
@@ -51,6 +56,13 @@ serverFollowsRouter.get('/followers', | |||
51 | asyncMiddleware(listFollowers) | 56 | asyncMiddleware(listFollowers) |
52 | ) | 57 | ) |
53 | 58 | ||
59 | serverFollowsRouter.delete('/followers/:nameWithHost', | ||
60 | authenticate, | ||
61 | ensureUserHasRight(UserRight.MANAGE_SERVER_FOLLOW), | ||
62 | asyncMiddleware(removeFollowerValidator), | ||
63 | asyncMiddleware(removeFollower) | ||
64 | ) | ||
65 | |||
54 | // --------------------------------------------------------------------------- | 66 | // --------------------------------------------------------------------------- |
55 | 67 | ||
56 | export { | 68 | export { |
@@ -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 | ||
106 | async function removeFollow (req: express.Request, res: express.Response) { | 118 | async 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 | |||
139 | async 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 @@ | |||
1 | export * from './send-accept' | 1 | export * from './send-accept' |
2 | export * from './send-accept' | ||
2 | export * from './send-announce' | 3 | export * from './send-announce' |
3 | export * from './send-create' | 4 | export * from './send-create' |
4 | export * from './send-delete' | 5 | export * from './send-delete' |
5 | export * from './send-follow' | 6 | export * from './send-follow' |
6 | export * from './send-like' | 7 | export * from './send-like' |
8 | export * from './send-reject' | ||
7 | export * from './send-undo' | 9 | export * from './send-undo' |
8 | export * from './send-update' | 10 | export * from './send-update' |
diff --git a/server/lib/activitypub/send/send-reject.ts b/server/lib/activitypub/send/send-reject.ts new file mode 100644 index 000000000..db8c2d86d --- /dev/null +++ b/server/lib/activitypub/send/send-reject.ts | |||
@@ -0,0 +1,44 @@ | |||
1 | import { ActivityFollow, ActivityReject } from '../../../../shared/models/activitypub' | ||
2 | import { ActorModel } from '../../../models/activitypub/actor' | ||
3 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | ||
4 | import { getActorFollowAcceptActivityPubUrl, getActorFollowActivityPubUrl } from '../url' | ||
5 | import { unicastTo } from './utils' | ||
6 | import { buildFollowActivity } from './send-follow' | ||
7 | import { logger } from '../../../helpers/logger' | ||
8 | |||
9 | async 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 | |||
31 | export { | ||
32 | sendReject | ||
33 | } | ||
34 | |||
35 | // --------------------------------------------------------------------------- | ||
36 | |||
37 | function 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 @@ | |||
1 | import { VideoModel } from '../models/video/video' | 1 | import { VideoModel } from '../models/video/video' |
2 | import { basename, dirname, join } from 'path' | 2 | import { basename, dirname, join } from 'path' |
3 | import { CONFIG, HLS_STREAMING_PLAYLIST_DIRECTORY, sequelizeTypescript } from '../initializers' | 3 | import { CONFIG, HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION, sequelizeTypescript } from '../initializers' |
4 | import { close, ensureDir, move, open, outputJSON, pathExists, read, readFile, remove, writeFile } from 'fs-extra' | 4 | import { close, ensureDir, move, open, outputJSON, pathExists, read, readFile, remove, writeFile } from 'fs-extra' |
5 | import { getVideoFileSize } from '../helpers/ffmpeg-utils' | 5 | import { getVideoFileSize } from '../helpers/ffmpeg-utils' |
6 | import { sha256 } from '../helpers/core-utils' | 6 | import { 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' | |||
7 | import { CONFIG, SERVER_ACTOR_NAME } from '../../initializers' | 7 | import { CONFIG, SERVER_ACTOR_NAME } from '../../initializers' |
8 | import { ActorFollowModel } from '../../models/activitypub/actor-follow' | 8 | import { ActorFollowModel } from '../../models/activitypub/actor-follow' |
9 | import { areValidationErrors } from './utils' | 9 | import { areValidationErrors } from './utils' |
10 | import { ActorModel } from '../../models/activitypub/actor' | ||
11 | import { loadActorUrlOrGetFromWebfinger } from '../../helpers/webfinger' | ||
12 | import { getOrCreateActorAndServerAndModel } from '../../lib/activitypub' | ||
13 | import { isValidActorHandle } from '../../helpers/custom-validators/activitypub/actor' | ||
10 | 14 | ||
11 | const followValidator = [ | 15 | const 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 | |||
61 | const 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 | ||
59 | export { | 92 | export { |
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 | |||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | ||
5 | import { flushAndRunMultipleServers, killallServers, ServerInfo, setAccessTokensToServers } from '../../../../shared/utils/index' | ||
6 | import { | ||
7 | follow, | ||
8 | getFollowersListPaginationAndSort, | ||
9 | getFollowingListPaginationAndSort, | ||
10 | removeFollower | ||
11 | } from '../../../../shared/utils/server/follows' | ||
12 | import { waitJobs } from '../../../../shared/utils/server/jobs' | ||
13 | import { ActorFollow } from '../../../../shared/models/actors' | ||
14 | |||
15 | const expect = chai.expect | ||
16 | |||
17 | describe('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' | |||
3 | import './email' | 3 | import './email' |
4 | import './follow-constraints' | 4 | import './follow-constraints' |
5 | import './follows' | 5 | import './follows' |
6 | import './follows-moderation' | ||
6 | import './handle-down' | 7 | import './handle-down' |
7 | import './jobs' | 8 | import './jobs' |
8 | import './reverse-proxy' | 9 | import './reverse-proxy' |
diff --git a/shared/utils/server/follows.ts b/shared/utils/server/follows.ts index 7741757a6..949fd8109 100644 --- a/shared/utils/server/follows.ts +++ b/shared/utils/server/follows.ts | |||
@@ -47,13 +47,21 @@ async function follow (follower: string, following: string[], accessToken: strin | |||
47 | async function unfollow (url: string, accessToken: string, target: ServerInfo, expectedStatus = 204) { | 47 | async 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 | 57 | function 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 | ||
59 | async function doubleFollow (server1: ServerInfo, server2: ServerInfo) { | 67 | async 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 | } |