Add ability to remove an instance follower in API
This commit is contained in:
parent
ae9bbed46d
commit
0e9c48c2ed
|
@ -3,18 +3,23 @@ import { UserRight } from '../../../../shared/models/users'
|
||||||
import { logger } from '../../../helpers/logger'
|
import { logger } from '../../../helpers/logger'
|
||||||
import { getFormattedObjects, getServerActor } from '../../../helpers/utils'
|
import { getFormattedObjects, getServerActor } from '../../../helpers/utils'
|
||||||
import { sequelizeTypescript, SERVER_ACTOR_NAME } from '../../../initializers'
|
import { sequelizeTypescript, SERVER_ACTOR_NAME } from '../../../initializers'
|
||||||
import { sendUndoFollow } from '../../../lib/activitypub/send'
|
import { sendReject, sendUndoFollow } from '../../../lib/activitypub/send'
|
||||||
import {
|
import {
|
||||||
asyncMiddleware,
|
asyncMiddleware,
|
||||||
authenticate,
|
authenticate,
|
||||||
ensureUserHasRight,
|
ensureUserHasRight,
|
||||||
paginationValidator,
|
paginationValidator,
|
||||||
removeFollowingValidator,
|
|
||||||
setBodyHostsPort,
|
setBodyHostsPort,
|
||||||
setDefaultPagination,
|
setDefaultPagination,
|
||||||
setDefaultSort
|
setDefaultSort
|
||||||
} from '../../../middlewares'
|
} 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 { ActorFollowModel } from '../../../models/activitypub/actor-follow'
|
||||||
import { JobQueue } from '../../../lib/job-queue'
|
import { JobQueue } from '../../../lib/job-queue'
|
||||||
import { removeRedundancyOf } from '../../../lib/redundancy'
|
import { removeRedundancyOf } from '../../../lib/redundancy'
|
||||||
|
@ -40,7 +45,7 @@ serverFollowsRouter.delete('/following/:host',
|
||||||
authenticate,
|
authenticate,
|
||||||
ensureUserHasRight(UserRight.MANAGE_SERVER_FOLLOW),
|
ensureUserHasRight(UserRight.MANAGE_SERVER_FOLLOW),
|
||||||
asyncMiddleware(removeFollowingValidator),
|
asyncMiddleware(removeFollowingValidator),
|
||||||
asyncMiddleware(removeFollow)
|
asyncMiddleware(removeFollowing)
|
||||||
)
|
)
|
||||||
|
|
||||||
serverFollowsRouter.get('/followers',
|
serverFollowsRouter.get('/followers',
|
||||||
|
@ -51,6 +56,13 @@ serverFollowsRouter.get('/followers',
|
||||||
asyncMiddleware(listFollowers)
|
asyncMiddleware(listFollowers)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
serverFollowsRouter.delete('/followers/:nameWithHost',
|
||||||
|
authenticate,
|
||||||
|
ensureUserHasRight(UserRight.MANAGE_SERVER_FOLLOW),
|
||||||
|
asyncMiddleware(removeFollowerValidator),
|
||||||
|
asyncMiddleware(removeFollower)
|
||||||
|
)
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
@ -103,7 +115,7 @@ async function followInstance (req: express.Request, res: express.Response) {
|
||||||
return res.status(204).end()
|
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
|
const follow = res.locals.follow
|
||||||
|
|
||||||
await sequelizeTypescript.transaction(async t => {
|
await sequelizeTypescript.transaction(async t => {
|
||||||
|
@ -123,3 +135,13 @@ async function removeFollow (req: express.Request, res: express.Response) {
|
||||||
|
|
||||||
return res.status(204).end()
|
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()
|
||||||
|
}
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
export * from './send-accept'
|
export * from './send-accept'
|
||||||
|
export * from './send-accept'
|
||||||
export * from './send-announce'
|
export * from './send-announce'
|
||||||
export * from './send-create'
|
export * from './send-create'
|
||||||
export * from './send-delete'
|
export * from './send-delete'
|
||||||
export * from './send-follow'
|
export * from './send-follow'
|
||||||
export * from './send-like'
|
export * from './send-like'
|
||||||
|
export * from './send-reject'
|
||||||
export * from './send-undo'
|
export * from './send-undo'
|
||||||
export * from './send-update'
|
export * from './send-update'
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import { VideoModel } from '../models/video/video'
|
import { VideoModel } from '../models/video/video'
|
||||||
import { basename, dirname, join } from 'path'
|
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 { close, ensureDir, move, open, outputJSON, pathExists, read, readFile, remove, writeFile } from 'fs-extra'
|
||||||
import { getVideoFileSize } from '../helpers/ffmpeg-utils'
|
import { getVideoFileSize } from '../helpers/ffmpeg-utils'
|
||||||
import { sha256 } from '../helpers/core-utils'
|
import { sha256 } from '../helpers/core-utils'
|
||||||
|
@ -20,6 +20,7 @@ async function updateStreamingPlaylistsInfohashesIfNeeded () {
|
||||||
const videoFiles = await VideoFileModel.listByStreamingPlaylist(playlist.id, t)
|
const videoFiles = await VideoFileModel.listByStreamingPlaylist(playlist.id, t)
|
||||||
|
|
||||||
playlist.p2pMediaLoaderInfohashes = await VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlist.playlistUrl, videoFiles)
|
playlist.p2pMediaLoaderInfohashes = await VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlist.playlistUrl, videoFiles)
|
||||||
|
playlist.p2pMediaLoaderPeerVersion = P2P_MEDIA_LOADER_PEER_VERSION
|
||||||
await playlist.save({ transaction: t })
|
await playlist.save({ transaction: t })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,10 @@ import { getServerActor } from '../../helpers/utils'
|
||||||
import { CONFIG, SERVER_ACTOR_NAME } from '../../initializers'
|
import { CONFIG, SERVER_ACTOR_NAME } from '../../initializers'
|
||||||
import { ActorFollowModel } from '../../models/activitypub/actor-follow'
|
import { ActorFollowModel } from '../../models/activitypub/actor-follow'
|
||||||
import { areValidationErrors } from './utils'
|
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 = [
|
const followValidator = [
|
||||||
body('hosts').custom(isEachUniqueHostValid).withMessage('Should have an array of unique hosts'),
|
body('hosts').custom(isEachUniqueHostValid).withMessage('Should have an array of unique hosts'),
|
||||||
|
@ -33,7 +37,7 @@ const removeFollowingValidator = [
|
||||||
param('host').custom(isHostValid).withMessage('Should have a valid host'),
|
param('host').custom(isHostValid).withMessage('Should have a valid host'),
|
||||||
|
|
||||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
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
|
if (areValidationErrors(req, res)) return
|
||||||
|
|
||||||
|
@ -44,7 +48,36 @@ const removeFollowingValidator = [
|
||||||
return res
|
return res
|
||||||
.status(404)
|
.status(404)
|
||||||
.json({
|
.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()
|
.end()
|
||||||
}
|
}
|
||||||
|
@ -58,5 +91,6 @@ const removeFollowingValidator = [
|
||||||
|
|
||||||
export {
|
export {
|
||||||
followValidator,
|
followValidator,
|
||||||
removeFollowingValidator
|
removeFollowingValidator,
|
||||||
|
removeFollowerValidator
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
/* 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)
|
||||||
|
})
|
||||||
|
})
|
|
@ -3,6 +3,7 @@ import './contact-form'
|
||||||
import './email'
|
import './email'
|
||||||
import './follow-constraints'
|
import './follow-constraints'
|
||||||
import './follows'
|
import './follows'
|
||||||
|
import './follows-moderation'
|
||||||
import './handle-down'
|
import './handle-down'
|
||||||
import './jobs'
|
import './jobs'
|
||||||
import './reverse-proxy'
|
import './reverse-proxy'
|
||||||
|
|
|
@ -47,13 +47,21 @@ async function follow (follower: string, following: string[], accessToken: strin
|
||||||
async function unfollow (url: string, accessToken: string, target: ServerInfo, expectedStatus = 204) {
|
async function unfollow (url: string, accessToken: string, target: ServerInfo, expectedStatus = 204) {
|
||||||
const path = '/api/v1/server/following/' + target.host
|
const path = '/api/v1/server/following/' + target.host
|
||||||
|
|
||||||
const res = await request(url)
|
return request(url)
|
||||||
.delete(path)
|
.delete(path)
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', 'Bearer ' + accessToken)
|
.set('Authorization', 'Bearer ' + accessToken)
|
||||||
.expect(expectedStatus)
|
.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) {
|
async function doubleFollow (server1: ServerInfo, server2: ServerInfo) {
|
||||||
|
@ -74,6 +82,7 @@ export {
|
||||||
getFollowersListPaginationAndSort,
|
getFollowersListPaginationAndSort,
|
||||||
getFollowingListPaginationAndSort,
|
getFollowingListPaginationAndSort,
|
||||||
unfollow,
|
unfollow,
|
||||||
|
removeFollower,
|
||||||
follow,
|
follow,
|
||||||
doubleFollow
|
doubleFollow
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue