import { logger } from '../../../helpers/logger'
import { getFormattedObjects, getServerActor } from '../../../helpers/utils'
import { sequelizeTypescript, SERVER_ACTOR_NAME } from '../../../initializers'
-import { sendUndoFollow } from '../../../lib/activitypub/send'
+import { sendReject, sendUndoFollow } from '../../../lib/activitypub/send'
import {
asyncMiddleware,
authenticate,
ensureUserHasRight,
paginationValidator,
- removeFollowingValidator,
setBodyHostsPort,
setDefaultPagination,
setDefaultSort
} from '../../../middlewares'
-import { followersSortValidator, followingSortValidator, followValidator } from '../../../middlewares/validators'
+import {
+ followersSortValidator,
+ followingSortValidator,
+ followValidator,
+ removeFollowerValidator,
+ removeFollowingValidator
+} from '../../../middlewares/validators'
import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
import { JobQueue } from '../../../lib/job-queue'
import { removeRedundancyOf } from '../../../lib/redundancy'
authenticate,
ensureUserHasRight(UserRight.MANAGE_SERVER_FOLLOW),
asyncMiddleware(removeFollowingValidator),
- asyncMiddleware(removeFollow)
+ asyncMiddleware(removeFollowing)
)
serverFollowsRouter.get('/followers',
asyncMiddleware(listFollowers)
)
+serverFollowsRouter.delete('/followers/:nameWithHost',
+ authenticate,
+ ensureUserHasRight(UserRight.MANAGE_SERVER_FOLLOW),
+ asyncMiddleware(removeFollowerValidator),
+ asyncMiddleware(removeFollower)
+)
+
// ---------------------------------------------------------------------------
export {
return res.status(204).end()
}
-async function removeFollow (req: express.Request, res: express.Response) {
+async function removeFollowing (req: express.Request, res: express.Response) {
const follow = res.locals.follow
await sequelizeTypescript.transaction(async t => {
return res.status(204).end()
}
+
+async function removeFollower (req: express.Request, res: express.Response) {
+ const follow = res.locals.follow
+
+ await sendReject(follow)
+
+ await follow.destroy()
+
+ return res.status(204).end()
+}
export * from './send-accept'
+export * from './send-accept'
export * from './send-announce'
export * from './send-create'
export * from './send-delete'
export * from './send-follow'
export * from './send-like'
+export * from './send-reject'
export * from './send-undo'
export * from './send-update'
--- /dev/null
+import { ActivityFollow, ActivityReject } from '../../../../shared/models/activitypub'
+import { ActorModel } from '../../../models/activitypub/actor'
+import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
+import { getActorFollowAcceptActivityPubUrl, getActorFollowActivityPubUrl } from '../url'
+import { unicastTo } from './utils'
+import { buildFollowActivity } from './send-follow'
+import { logger } from '../../../helpers/logger'
+
+async function sendReject (actorFollow: ActorFollowModel) {
+ const follower = actorFollow.ActorFollower
+ const me = actorFollow.ActorFollowing
+
+ if (!follower.serverId) { // This should never happen
+ logger.warn('Do not sending reject to local follower.')
+ return
+ }
+
+ logger.info('Creating job to reject follower %s.', follower.url)
+
+ const followUrl = getActorFollowActivityPubUrl(actorFollow)
+ const followData = buildFollowActivity(followUrl, follower, me)
+
+ const url = getActorFollowAcceptActivityPubUrl(actorFollow)
+ const data = buildRejectActivity(url, me, followData)
+
+ return unicastTo(data, me, follower.inboxUrl)
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+ sendReject
+}
+
+// ---------------------------------------------------------------------------
+
+function buildRejectActivity (url: string, byActor: ActorModel, followActivityData: ActivityFollow): ActivityReject {
+ return {
+ type: 'Reject',
+ id: url,
+ actor: byActor.url,
+ object: followActivityData
+ }
+}
import { VideoModel } from '../models/video/video'
import { basename, dirname, join } from 'path'
-import { CONFIG, HLS_STREAMING_PLAYLIST_DIRECTORY, sequelizeTypescript } from '../initializers'
+import { CONFIG, HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION, sequelizeTypescript } from '../initializers'
import { close, ensureDir, move, open, outputJSON, pathExists, read, readFile, remove, writeFile } from 'fs-extra'
import { getVideoFileSize } from '../helpers/ffmpeg-utils'
import { sha256 } from '../helpers/core-utils'
const videoFiles = await VideoFileModel.listByStreamingPlaylist(playlist.id, t)
playlist.p2pMediaLoaderInfohashes = await VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlist.playlistUrl, videoFiles)
+ playlist.p2pMediaLoaderPeerVersion = P2P_MEDIA_LOADER_PEER_VERSION
await playlist.save({ transaction: t })
})
}
import { CONFIG, SERVER_ACTOR_NAME } from '../../initializers'
import { ActorFollowModel } from '../../models/activitypub/actor-follow'
import { areValidationErrors } from './utils'
+import { ActorModel } from '../../models/activitypub/actor'
+import { loadActorUrlOrGetFromWebfinger } from '../../helpers/webfinger'
+import { getOrCreateActorAndServerAndModel } from '../../lib/activitypub'
+import { isValidActorHandle } from '../../helpers/custom-validators/activitypub/actor'
const followValidator = [
body('hosts').custom(isEachUniqueHostValid).withMessage('Should have an array of unique hosts'),
param('host').custom(isHostValid).withMessage('Should have a valid host'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
- logger.debug('Checking unfollow parameters', { parameters: req.params })
+ logger.debug('Checking unfollowing parameters', { parameters: req.params })
if (areValidationErrors(req, res)) return
return res
.status(404)
.json({
- error: `Follower ${req.params.host} not found.`
+ error: `Following ${req.params.host} not found.`
+ })
+ .end()
+ }
+
+ res.locals.follow = follow
+ return next()
+ }
+]
+
+const removeFollowerValidator = [
+ param('nameWithHost').custom(isValidActorHandle).withMessage('Should have a valid nameWithHost'),
+
+ async (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ logger.debug('Checking remove follower parameters', { parameters: req.params })
+
+ if (areValidationErrors(req, res)) return
+
+ const serverActor = await getServerActor()
+
+ const actorUrl = await loadActorUrlOrGetFromWebfinger(req.params.nameWithHost)
+ const actor = await ActorModel.loadByUrl(actorUrl)
+
+ const follow = await ActorFollowModel.loadByActorAndTarget(actor.id, serverActor.id)
+
+ if (!follow) {
+ return res
+ .status(404)
+ .json({
+ error: `Follower ${req.params.nameWithHost} not found.`
})
.end()
}
export {
followValidator,
- removeFollowingValidator
+ removeFollowingValidator,
+ removeFollowerValidator
}
--- /dev/null
+/* tslint:disable:no-unused-expression */
+
+import * as chai from 'chai'
+import 'mocha'
+import { flushAndRunMultipleServers, killallServers, ServerInfo, setAccessTokensToServers } from '../../../../shared/utils/index'
+import {
+ follow,
+ getFollowersListPaginationAndSort,
+ getFollowingListPaginationAndSort,
+ removeFollower
+} from '../../../../shared/utils/server/follows'
+import { waitJobs } from '../../../../shared/utils/server/jobs'
+import { ActorFollow } from '../../../../shared/models/actors'
+
+const expect = chai.expect
+
+describe('Test follows moderation', function () {
+ let servers: ServerInfo[] = []
+
+ before(async function () {
+ this.timeout(30000)
+
+ servers = await flushAndRunMultipleServers(2)
+
+ // Get the access tokens
+ await setAccessTokensToServers(servers)
+ })
+
+ it('Should have server 1 following server 2', async function () {
+ this.timeout(30000)
+
+ await follow(servers[0].url, [ servers[1].url ], servers[0].accessToken)
+
+ await waitJobs(servers)
+ })
+
+ it('Should have correct follows', async function () {
+ {
+ const res = await getFollowingListPaginationAndSort(servers[0].url, 0, 5, 'createdAt')
+ expect(res.body.total).to.equal(1)
+
+ const follow = res.body.data[0] as ActorFollow
+ expect(follow.follower.url).to.equal('http://localhost:9001/accounts/peertube')
+ expect(follow.following.url).to.equal('http://localhost:9002/accounts/peertube')
+ }
+
+ {
+ const res = await getFollowersListPaginationAndSort(servers[1].url, 0, 5, 'createdAt')
+ expect(res.body.total).to.equal(1)
+
+ const follow = res.body.data[0] as ActorFollow
+ expect(follow.follower.url).to.equal('http://localhost:9001/accounts/peertube')
+ expect(follow.following.url).to.equal('http://localhost:9002/accounts/peertube')
+ }
+ })
+
+ it('Should remove follower on server 2', async function () {
+ await removeFollower(servers[1].url, servers[1].accessToken, servers[0])
+
+ await waitJobs(servers)
+ })
+
+ it('Should not not have follows anymore', async function () {
+ {
+ const res = await getFollowingListPaginationAndSort(servers[ 0 ].url, 0, 1, 'createdAt')
+ expect(res.body.total).to.equal(0)
+ }
+
+ {
+ const res = await getFollowingListPaginationAndSort(servers[ 0 ].url, 0, 1, 'createdAt')
+ expect(res.body.total).to.equal(0)
+ }
+ })
+
+ after(async function () {
+ killallServers(servers)
+ })
+})
import './email'
import './follow-constraints'
import './follows'
+import './follows-moderation'
import './handle-down'
import './jobs'
import './reverse-proxy'
async function unfollow (url: string, accessToken: string, target: ServerInfo, expectedStatus = 204) {
const path = '/api/v1/server/following/' + target.host
- const res = await request(url)
+ return request(url)
.delete(path)
.set('Accept', 'application/json')
.set('Authorization', 'Bearer ' + accessToken)
.expect(expectedStatus)
+}
- return res
+function removeFollower (url: string, accessToken: string, follower: ServerInfo, expectedStatus = 204) {
+ const path = '/api/v1/server/followers/peertube@' + follower.host
+
+ return request(url)
+ .delete(path)
+ .set('Accept', 'application/json')
+ .set('Authorization', 'Bearer ' + accessToken)
+ .expect(expectedStatus)
}
async function doubleFollow (server1: ServerInfo, server2: ServerInfo) {
getFollowersListPaginationAndSort,
getFollowingListPaginationAndSort,
unfollow,
+ removeFollower,
follow,
doubleFollow
}