Add ability to forbid followers

pull/1765/head
Chocobozzz 2019-04-08 14:04:57 +02:00
parent 594d0c6a7c
commit 5b9c965d5a
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
19 changed files with 196 additions and 55 deletions

View File

@ -200,3 +200,8 @@ services:
# If false, we use an image link card that will redirect on your PeerTube instance
# Change it to "true", and then test on https://cards-dev.twitter.com/validator to see if you are whitelisted
whitelisted: false
followers:
instance:
# Allow or not other instances to follow yours
enabled: true

View File

@ -180,8 +180,8 @@ auto_blacklist:
# New videos automatically blacklisted so moderators can review before publishing
videos:
of_users:
enabled: false
enabled: false
# Instance settings
instance:
name: 'PeerTube'
@ -217,3 +217,8 @@ services:
# If false, we use an image link card that will redirect on your PeerTube instance
# Test on https://cards-dev.twitter.com/validator to see if you are whitelisted
whitelisted: false
followers:
instance:
# Allow or not other instances to follow yours
enabled: true

View File

@ -279,6 +279,11 @@ function customConfig (): CustomConfig {
enabled: CONFIG.AUTO_BLACKLIST.VIDEOS.OF_USERS.ENABLED
}
}
},
followers: {
instance: {
enabled: CONFIG.FOLLOWERS.INSTANCE.ENABLED
}
}
}
}

View File

@ -139,7 +139,7 @@ async function removeFollowing (req: express.Request, res: express.Response) {
async function removeFollower (req: express.Request, res: express.Response) {
const follow = res.locals.follow
await sendReject(follow)
await sendReject(follow.ActorFollower, follow.ActorFollowing)
await follow.destroy()

View File

@ -24,7 +24,8 @@ function checkMissedConfig () {
'trending.videos.interval_days',
'instance.name', 'instance.short_description', 'instance.description', 'instance.terms', 'instance.default_client_route',
'instance.is_nsfw', 'instance.default_nsfw_policy', 'instance.robots', 'instance.securitytxt',
'services.twitter.username', 'services.twitter.whitelisted'
'services.twitter.username', 'services.twitter.whitelisted',
'followers.instance.enabled'
]
const requiredAlternatives = [
[ // set

View File

@ -324,6 +324,11 @@ const CONFIG = {
get USERNAME () { return config.get<string>('services.twitter.username') },
get WHITELISTED () { return config.get<boolean>('services.twitter.whitelisted') }
}
},
FOLLOWERS: {
INSTANCE: {
get ENABLED () { return config.get<boolean>('followers.instance.enabled') }
}
}
}

View File

@ -1,12 +1,13 @@
import { ActivityFollow } from '../../../../shared/models/activitypub'
import { retryTransactionWrapper } from '../../../helpers/database-utils'
import { logger } from '../../../helpers/logger'
import { sequelizeTypescript } from '../../../initializers'
import { sequelizeTypescript, CONFIG } from '../../../initializers'
import { ActorModel } from '../../../models/activitypub/actor'
import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
import { sendAccept } from '../send'
import { sendAccept, sendReject } from '../send'
import { Notifier } from '../../notifier'
import { getAPId } from '../../../helpers/activitypub'
import { getServerActor } from '../../../helpers/utils'
async function processFollowActivity (activity: ActivityFollow, byActor: ActorModel) {
const activityObject = getAPId(activity.object)
@ -29,6 +30,11 @@ async function processFollow (actor: ActorModel, targetActorURL: string) {
if (!targetActor) throw new Error('Unknown actor')
if (targetActor.isOwned() === false) throw new Error('This is not a local actor.')
const serverActor = await getServerActor()
if (targetActor.id === serverActor.id && CONFIG.FOLLOWERS.INSTANCE.ENABLED === false) {
return sendReject(actor, targetActor)
}
const [ actorFollow, created ] = await ActorFollowModel.findOrCreate({
where: {
actorId: actor.id,

View File

@ -17,7 +17,7 @@ async function sendAccept (actorFollow: ActorFollowModel) {
logger.info('Creating job to accept follower %s.', follower.url)
const followUrl = getActorFollowActivityPubUrl(actorFollow)
const followUrl = getActorFollowActivityPubUrl(follower, me)
const followData = buildFollowActivity(followUrl, follower, me)
const url = getActorFollowAcceptActivityPubUrl(actorFollow)

View File

@ -14,7 +14,7 @@ function sendFollow (actorFollow: ActorFollowModel) {
logger.info('Creating job to send follow request to %s.', following.url)
const url = getActorFollowActivityPubUrl(actorFollow)
const url = getActorFollowActivityPubUrl(me, following)
const data = buildFollowActivity(url, me, following)
return unicastTo(data, me, following.inboxUrl)

View File

@ -1,15 +1,11 @@
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 { getActorFollowActivityPubUrl, getActorFollowRejectActivityPubUrl } 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
async function sendReject (follower: ActorModel, following: ActorModel) {
if (!follower.serverId) { // This should never happen
logger.warn('Do not sending reject to local follower.')
return
@ -17,13 +13,13 @@ async function sendReject (actorFollow: ActorFollowModel) {
logger.info('Creating job to reject follower %s.', follower.url)
const followUrl = getActorFollowActivityPubUrl(actorFollow)
const followData = buildFollowActivity(followUrl, follower, me)
const followUrl = getActorFollowActivityPubUrl(follower, following)
const followData = buildFollowActivity(followUrl, follower, following)
const url = getActorFollowAcceptActivityPubUrl(actorFollow)
const data = buildRejectActivity(url, me, followData)
const url = getActorFollowRejectActivityPubUrl(follower, following)
const data = buildRejectActivity(url, following, followData)
return unicastTo(data, me, follower.inboxUrl)
return unicastTo(data, following, follower.inboxUrl)
}
// ---------------------------------------------------------------------------

View File

@ -31,7 +31,7 @@ async function sendUndoFollow (actorFollow: ActorFollowModel, t: Transaction) {
logger.info('Creating job to send an unfollow request to %s.', following.url)
const followUrl = getActorFollowActivityPubUrl(actorFollow)
const followUrl = getActorFollowActivityPubUrl(me, following)
const undoUrl = getUndoActivityPubUrl(followUrl)
const followActivity = buildFollowActivity(followUrl, me, following)

View File

@ -74,11 +74,8 @@ function getVideoDislikesActivityPubUrl (video: VideoModel) {
return video.url + '/dislikes'
}
function getActorFollowActivityPubUrl (actorFollow: ActorFollowModel) {
const me = actorFollow.ActorFollower
const following = actorFollow.ActorFollowing
return me.url + '/follows/' + following.id
function getActorFollowActivityPubUrl (follower: ActorModel, following: ActorModel) {
return follower.url + '/follows/' + following.id
}
function getActorFollowAcceptActivityPubUrl (actorFollow: ActorFollowModel) {
@ -88,6 +85,10 @@ function getActorFollowAcceptActivityPubUrl (actorFollow: ActorFollowModel) {
return follower.url + '/accepts/follows/' + me.id
}
function getActorFollowRejectActivityPubUrl (follower: ActorModel, following: ActorModel) {
return follower.url + '/rejects/follows/' + following.id
}
function getVideoAnnounceActivityPubUrl (byActor: ActorModel, video: VideoModel) {
return video.url + '/announces/' + byActor.id
}
@ -120,6 +121,7 @@ export {
getVideoViewActivityPubUrl,
getVideoLikeActivityPubUrl,
getVideoDislikeActivityPubUrl,
getActorFollowRejectActivityPubUrl,
getVideoCommentActivityPubUrl,
getDeleteActivityPubUrl,
getVideoSharesActivityPubUrl,

View File

@ -9,7 +9,6 @@ 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 = [
@ -66,12 +65,16 @@ const removeFollowerValidator = [
if (areValidationErrors(req, res)) return
const serverActor = await getServerActor()
let follow: ActorFollowModel
try {
const actorUrl = await loadActorUrlOrGetFromWebfinger(req.params.nameWithHost)
const actor = await ActorModel.loadByUrl(actorUrl)
const actorUrl = await loadActorUrlOrGetFromWebfinger(req.params.nameWithHost)
const actor = await ActorModel.loadByUrl(actorUrl)
const follow = await ActorFollowModel.loadByActorAndTarget(actor.id, serverActor.id)
const serverActor = await getServerActor()
follow = await ActorFollowModel.loadByActorAndTarget(actor.id, serverActor.id)
} catch (err) {
logger.warn('Cannot get actor from handle.', { handle: req.params.nameWithHost, err })
}
if (!follow) {
return res

View File

@ -87,6 +87,11 @@ describe('Test config API validators', function () {
enabled: false
}
}
},
followers: {
instance: {
enabled: false
}
}
}

View File

@ -144,6 +144,46 @@ describe('Test server follows API validators', function () {
})
})
describe('When removing a follower', function () {
const path = '/api/v1/server/followers'
it('Should fail with an invalid token', async function () {
await makeDeleteRequest({
url: server.url,
path: path + '/toto@localhost:9002',
token: 'fake_token',
statusCodeExpected: 401
})
})
it('Should fail if the user is not an administrator', async function () {
await makeDeleteRequest({
url: server.url,
path: path + '/toto@localhost:9002',
token: userAccessToken,
statusCodeExpected: 403
})
})
it('Should fail with an invalid follower', async function () {
await makeDeleteRequest({
url: server.url,
path: path + '/toto',
token: server.accessToken,
statusCodeExpected: 400
})
})
it('Should fail with an unknown follower', async function () {
await makeDeleteRequest({
url: server.url,
path: path + '/toto@localhost:9003',
token: server.accessToken,
statusCodeExpected: 404
})
})
})
describe('When removing following', function () {
const path = '/api/v1/server/following'

View File

@ -63,6 +63,8 @@ function checkInitialConfig (data: CustomConfig) {
expect(data.import.videos.http.enabled).to.be.true
expect(data.import.videos.torrent.enabled).to.be.true
expect(data.autoBlacklist.videos.ofUsers.enabled).to.be.false
expect(data.followers.instance.enabled).to.be.true
}
function checkUpdatedConfig (data: CustomConfig) {
@ -105,6 +107,8 @@ function checkUpdatedConfig (data: CustomConfig) {
expect(data.import.videos.http.enabled).to.be.false
expect(data.import.videos.torrent.enabled).to.be.false
expect(data.autoBlacklist.videos.ofUsers.enabled).to.be.true
expect(data.followers.instance.enabled).to.be.false
}
describe('Test config', function () {
@ -234,6 +238,11 @@ describe('Test config', function () {
enabled: true
}
}
},
followers: {
instance: {
enabled: false
}
}
}
await updateCustomConfig(server.url, server.accessToken, newCustomConfig)

View File

@ -2,7 +2,13 @@
import * as chai from 'chai'
import 'mocha'
import { flushAndRunMultipleServers, killallServers, ServerInfo, setAccessTokensToServers } from '../../../../shared/utils/index'
import {
flushAndRunMultipleServers,
killallServers,
ServerInfo,
setAccessTokensToServers,
updateCustomSubConfig
} from '../../../../shared/utils/index'
import {
follow,
getFollowersListPaginationAndSort,
@ -14,6 +20,38 @@ import { ActorFollow } from '../../../../shared/models/actors'
const expect = chai.expect
async function checkHasFollowers (servers: ServerInfo[]) {
{
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')
}
}
async function checkNoFollowers (servers: ServerInfo[]) {
{
const res = await getFollowingListPaginationAndSort(servers[ 0 ].url, 0, 5, 'createdAt')
expect(res.body.total).to.equal(0)
}
{
const res = await getFollowersListPaginationAndSort(servers[ 1 ].url, 0, 5, 'createdAt')
expect(res.body.total).to.equal(0)
}
}
describe('Test follows moderation', function () {
let servers: ServerInfo[] = []
@ -35,23 +73,7 @@ describe('Test follows moderation', function () {
})
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')
}
await checkHasFollowers(servers)
})
it('Should remove follower on server 2', async function () {
@ -61,15 +83,41 @@ describe('Test follows moderation', function () {
})
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)
await checkNoFollowers(servers)
})
it('Should disable followers on server 2', async function () {
const subConfig = {
followers: {
instance: {
enabled: false
}
}
}
{
const res = await getFollowingListPaginationAndSort(servers[ 0 ].url, 0, 1, 'createdAt')
expect(res.body.total).to.equal(0)
await updateCustomSubConfig(servers[1].url, servers[1].accessToken, subConfig)
await follow(servers[0].url, [ servers[1].url ], servers[0].accessToken)
await waitJobs(servers)
await checkNoFollowers(servers)
})
it('Should re enable followers on server 2', async function () {
const subConfig = {
followers: {
instance: {
enabled: true
}
}
}
await updateCustomSubConfig(servers[1].url, servers[1].accessToken, subConfig)
await follow(servers[0].url, [ servers[1].url ], servers[0].accessToken)
await waitJobs(servers)
await checkHasFollowers(servers)
})
after(async function () {

View File

@ -86,4 +86,10 @@ export interface CustomConfig {
}
}
followers: {
instance: {
enabled: boolean
}
}
}

View File

@ -119,6 +119,11 @@ function updateCustomSubConfig (url: string, token: string, newConfig: any) {
enabled: false
}
}
},
followers: {
instance: {
enabled: true
}
}
}