# 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
# New videos automatically blacklisted so moderators can review before publishing
videos:
of_users:
- enabled: false
-
+ enabled: false
+
# Instance settings
instance:
name: 'PeerTube'
# 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
enabled: CONFIG.AUTO_BLACKLIST.VIDEOS.OF_USERS.ENABLED
}
}
+ },
+ followers: {
+ instance: {
+ enabled: CONFIG.FOLLOWERS.INSTANCE.ENABLED
+ }
}
}
}
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()
'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
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') }
+ }
}
}
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)
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,
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)
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)
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
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)
}
// ---------------------------------------------------------------------------
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)
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) {
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
}
getVideoViewActivityPubUrl,
getVideoLikeActivityPubUrl,
getVideoDislikeActivityPubUrl,
+ getActorFollowRejectActivityPubUrl,
getVideoCommentActivityPubUrl,
getDeleteActivityPubUrl,
getVideoSharesActivityPubUrl,
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 = [
if (areValidationErrors(req, res)) return
- const serverActor = await getServerActor()
-
- const actorUrl = await loadActorUrlOrGetFromWebfinger(req.params.nameWithHost)
- const actor = await ActorModel.loadByUrl(actorUrl)
+ let follow: ActorFollowModel
+ try {
+ 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
enabled: false
}
}
+ },
+ followers: {
+ instance: {
+ enabled: false
+ }
}
}
})
})
+ 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'
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) {
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 () {
enabled: true
}
}
+ },
+ followers: {
+ instance: {
+ enabled: false
+ }
}
}
await updateCustomConfig(server.url, server.accessToken, newCustomConfig)
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,
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[] = []
})
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 () {
})
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 () {
}
}
+ followers: {
+ instance: {
+ enabled: boolean
+ }
+ }
+
}
enabled: false
}
}
+ },
+ followers: {
+ instance: {
+ enabled: true
+ }
}
}