Send follow/accept

This commit is contained in:
Chocobozzz 2017-11-13 18:48:28 +01:00
parent 7a7724e66e
commit ce548a10db
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
9 changed files with 160 additions and 19 deletions

View File

@ -1,10 +1,16 @@
import * as Bluebird from 'bluebird'
import * as express from 'express' import * as express from 'express'
import { getFormattedObjects } from '../../helpers' import { getFormattedObjects } from '../../helpers'
import { getOrCreateAccount } from '../../helpers/activitypub'
import { getApplicationAccount } from '../../helpers/utils' import { getApplicationAccount } from '../../helpers/utils'
import { REMOTE_SCHEME } from '../../initializers/constants'
import { database as db } from '../../initializers/database' import { database as db } from '../../initializers/database'
import { asyncMiddleware, paginationValidator, setFollowersSort, setPagination } from '../../middlewares' import { asyncMiddleware, paginationValidator, setFollowersSort, setPagination } from '../../middlewares'
import { setBodyHostsPort } from '../../middlewares/pods'
import { setFollowingSort } from '../../middlewares/sort' import { setFollowingSort } from '../../middlewares/sort'
import { followValidator } from '../../middlewares/validators/pods'
import { followersSortValidator, followingSortValidator } from '../../middlewares/validators/sort' import { followersSortValidator, followingSortValidator } from '../../middlewares/validators/sort'
import { sendFollow } from '../../lib/activitypub/send-request'
const podsRouter = express.Router() const podsRouter = express.Router()
@ -16,6 +22,12 @@ podsRouter.get('/following',
asyncMiddleware(listFollowing) asyncMiddleware(listFollowing)
) )
podsRouter.post('/follow',
followValidator,
setBodyHostsPort,
asyncMiddleware(follow)
)
podsRouter.get('/followers', podsRouter.get('/followers',
paginationValidator, paginationValidator,
followersSortValidator, followersSortValidator,
@ -45,3 +57,32 @@ async function listFollowers (req: express.Request, res: express.Response, next:
return res.json(getFormattedObjects(resultList.data, resultList.total)) return res.json(getFormattedObjects(resultList.data, resultList.total))
} }
async function follow (req: express.Request, res: express.Response, next: express.NextFunction) {
const hosts = req.body.hosts as string[]
const fromAccount = await getApplicationAccount()
const tasks: Bluebird<any>[] = []
for (const host of hosts) {
const url = REMOTE_SCHEME.HTTP + '://' + host
const targetAccount = await getOrCreateAccount(url)
// We process each host in a specific transaction
// First, we add the follow request in the database
// Then we send the follow request to other account
const p = db.sequelize.transaction(async t => {
return db.AccountFollow.create({
accountId: fromAccount.id,
targetAccountId: targetAccount.id,
state: 'pending'
})
.then(() => sendFollow(fromAccount, targetAccount, t))
})
tasks.push(p)
}
await Promise.all(tasks)
return res.status(204).end()
}

View File

@ -7,7 +7,7 @@ async function processAcceptActivity (activity: ActivityAccept, inboxAccount?: A
const targetAccount = await db.Account.loadByUrl(activity.actor) const targetAccount = await db.Account.loadByUrl(activity.actor)
return processFollow(inboxAccount, targetAccount) return processAccept(inboxAccount, targetAccount)
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -18,10 +18,10 @@ export {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
async function processFollow (account: AccountInstance, targetAccount: AccountInstance) { async function processAccept (account: AccountInstance, targetAccount: AccountInstance) {
const follow = await db.AccountFollow.loadByAccountAndTarget(account.id, targetAccount.id) const follow = await db.AccountFollow.loadByAccountAndTarget(account.id, targetAccount.id)
if (!follow) throw new Error('Cannot find associated follow.') if (!follow) throw new Error('Cannot find associated follow.')
follow.set('state', 'accepted') follow.set('state', 'accepted')
return follow.save() await follow.save()
} }

View File

@ -29,7 +29,7 @@ export {
function processAddVideo (account: AccountInstance, videoChannelUrl: string, video: VideoTorrentObject) { function processAddVideo (account: AccountInstance, videoChannelUrl: string, video: VideoTorrentObject) {
const options = { const options = {
arguments: [ account, videoChannelUrl ,video ], arguments: [ account, videoChannelUrl, video ],
errorMessage: 'Cannot insert the remote video with many retries.' errorMessage: 'Cannot insert the remote video with many retries.'
} }

View File

@ -1,7 +1,9 @@
import { ActivityFollow } from '../../../shared/models/activitypub/activity' import { ActivityFollow } from '../../../shared/models/activitypub/activity'
import { getOrCreateAccount } from '../../helpers' import { getOrCreateAccount, retryTransactionWrapper } from '../../helpers'
import { database as db } from '../../initializers' import { database as db } from '../../initializers'
import { AccountInstance } from '../../models/account/account-interface' import { AccountInstance } from '../../models/account/account-interface'
import { sendAccept } from './send-request'
import { logger } from '../../helpers/logger'
async function processFollowActivity (activity: ActivityFollow) { async function processFollowActivity (activity: ActivityFollow) {
const activityObject = activity.object const activityObject = activity.object
@ -18,15 +20,34 @@ export {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
async function processFollow (account: AccountInstance, targetAccountURL: string) { function processFollow (account: AccountInstance, targetAccountURL: string) {
const targetAccount = await db.Account.loadByUrl(targetAccountURL) const options = {
arguments: [ account, targetAccountURL ],
errorMessage: 'Cannot follow with many retries.'
}
if (targetAccount === undefined) throw new Error('Unknown account') return retryTransactionWrapper(follow, options)
if (targetAccount.isOwned() === false) throw new Error('This is not a local account.') }
return db.AccountFollow.create({ async function follow (account: AccountInstance, targetAccountURL: string) {
accountId: account.id, await db.sequelize.transaction(async t => {
targetAccountId: targetAccount.id, const targetAccount = await db.Account.loadByUrl(targetAccountURL, t)
state: 'accepted'
}) if (targetAccount === undefined) throw new Error('Unknown account')
if (targetAccount.isOwned() === false) throw new Error('This is not a local account.')
const sequelizeOptions = {
transaction: t
}
await db.AccountFollow.create({
accountId: account.id,
targetAccountId: targetAccount.id,
state: 'accepted'
}, sequelizeOptions)
// Target sends to account he accepted the follow request
return sendAccept(targetAccount, account, t)
})
logger.info('Account uuid %s is followed by account %s.', account.url, targetAccountURL)
} }

View File

@ -56,6 +56,18 @@ function sendDeleteAccount (account: AccountInstance, t: Sequelize.Transaction)
return broadcastToFollowers(data, account, t) return broadcastToFollowers(data, account, t)
} }
function sendAccept (fromAccount: AccountInstance, toAccount: AccountInstance, t: Sequelize.Transaction) {
const data = acceptActivityData(fromAccount)
return unicastTo(data, toAccount, t)
}
function sendFollow (fromAccount: AccountInstance, toAccount: AccountInstance, t: Sequelize.Transaction) {
const data = followActivityData(toAccount.url, fromAccount)
return unicastTo(data, toAccount, t)
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export { export {
@ -65,7 +77,9 @@ export {
sendAddVideo, sendAddVideo,
sendUpdateVideo, sendUpdateVideo,
sendDeleteVideo, sendDeleteVideo,
sendDeleteAccount sendDeleteAccount,
sendAccept,
sendFollow
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -81,6 +95,15 @@ async function broadcastToFollowers (data: any, fromAccount: AccountInstance, t:
return httpRequestJobScheduler.createJob(t, 'httpRequestBroadcastHandler', jobPayload) return httpRequestJobScheduler.createJob(t, 'httpRequestBroadcastHandler', jobPayload)
} }
async function unicastTo (data: any, toAccount: AccountInstance, t: Sequelize.Transaction) {
const jobPayload = {
uris: [ toAccount.url ],
body: data
}
return httpRequestJobScheduler.createJob(t, 'httpRequestUnicastHandler', jobPayload)
}
function buildSignedActivity (byAccount: AccountInstance, data: Object) { function buildSignedActivity (byAccount: AccountInstance, data: Object) {
const activity = activityPubContextify(data) const activity = activityPubContextify(data)
@ -142,3 +165,24 @@ async function addActivityData (url: string, byAccount: AccountInstance, target:
return buildSignedActivity(byAccount, base) return buildSignedActivity(byAccount, base)
} }
async function followActivityData (url: string, byAccount: AccountInstance) {
const base = {
type: 'Follow',
id: byAccount.url,
actor: byAccount.url,
object: url
}
return buildSignedActivity(byAccount, base)
}
async function acceptActivityData (byAccount: AccountInstance) {
const base = {
type: 'Accept',
id: byAccount.url,
actor: byAccount.url
}
return buildSignedActivity(byAccount, base)
}

View File

@ -2,6 +2,7 @@ export * from './account'
export * from './oembed' export * from './oembed'
export * from './activitypub' export * from './activitypub'
export * from './pagination' export * from './pagination'
export * from './pods'
export * from './sort' export * from './sort'
export * from './users' export * from './users'
export * from './videos' export * from './videos'

View File

@ -0,0 +1,32 @@
import * as express from 'express'
import { body } from 'express-validator/check'
import { isEachUniqueHostValid } from '../../helpers/custom-validators/pods'
import { isTestInstance } from '../../helpers/core-utils'
import { CONFIG } from '../../initializers/constants'
import { logger } from '../../helpers/logger'
import { checkErrors } from './utils'
const followValidator = [
body('hosts').custom(isEachUniqueHostValid).withMessage('Should have an array of unique hosts'),
(req: express.Request, res: express.Response, next: express.NextFunction) => {
// Force https if the administrator wants to make friends
if (isTestInstance() === false && CONFIG.WEBSERVER.SCHEME === 'http') {
return res.status(400)
.json({
error: 'Cannot follow non HTTPS web server.'
})
.end()
}
logger.debug('Checking follow parameters', { parameters: req.body })
checkErrors(req, res, next)
}
]
// ---------------------------------------------------------------------------
export {
followValidator
}

View File

@ -10,7 +10,7 @@ export namespace AccountMethods {
export type Load = (id: number) => Bluebird<AccountInstance> export type Load = (id: number) => Bluebird<AccountInstance>
export type LoadByUUID = (uuid: string) => Bluebird<AccountInstance> export type LoadByUUID = (uuid: string) => Bluebird<AccountInstance>
export type LoadByUrl = (url: string) => Bluebird<AccountInstance> export type LoadByUrl = (url: string, transaction?: Sequelize.Transaction) => Bluebird<AccountInstance>
export type LoadAccountByPodAndUUID = (uuid: string, podId: number, transaction: Sequelize.Transaction) => Bluebird<AccountInstance> export type LoadAccountByPodAndUUID = (uuid: string, podId: number, transaction: Sequelize.Transaction) => Bluebird<AccountInstance>
export type LoadLocalAccountByNameAndPod = (name: string, host: string) => Bluebird<AccountInstance> export type LoadLocalAccountByNameAndPod = (name: string, host: string) => Bluebird<AccountInstance>
export type ListOwned = () => Bluebird<AccountInstance[]> export type ListOwned = () => Bluebird<AccountInstance[]>

View File

@ -198,6 +198,7 @@ export default function defineAccount (sequelize: Sequelize.Sequelize, DataTypes
loadApplication, loadApplication,
load, load,
loadByUUID, loadByUUID,
loadByUrl,
loadLocalAccountByNameAndPod, loadLocalAccountByNameAndPod,
listOwned, listOwned,
listFollowerUrlsForApi, listFollowerUrlsForApi,
@ -480,11 +481,12 @@ loadLocalAccountByNameAndPod = function (name: string, host: string) {
return Account.findOne(query) return Account.findOne(query)
} }
loadByUrl = function (url: string) { loadByUrl = function (url: string, transaction?: Sequelize.Transaction) {
const query: Sequelize.FindOptions<AccountAttributes> = { const query: Sequelize.FindOptions<AccountAttributes> = {
where: { where: {
url url
} },
transaction
} }
return Account.findOne(query) return Account.findOne(query)