From ce548a10db3822c415b30ea0edb59e02a460734a Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Mon, 13 Nov 2017 18:48:28 +0100 Subject: [PATCH] Send follow/accept --- server/controllers/api/pods.ts | 41 +++++++++++++++++++ server/lib/activitypub/process-accept.ts | 6 +-- server/lib/activitypub/process-add.ts | 2 +- server/lib/activitypub/process-follow.ts | 43 ++++++++++++++------ server/lib/activitypub/send-request.ts | 46 +++++++++++++++++++++- server/middlewares/validators/index.ts | 1 + server/middlewares/validators/pods.ts | 32 +++++++++++++++ server/models/account/account-interface.ts | 2 +- server/models/account/account.ts | 6 ++- 9 files changed, 160 insertions(+), 19 deletions(-) create mode 100644 server/middlewares/validators/pods.ts diff --git a/server/controllers/api/pods.ts b/server/controllers/api/pods.ts index aa07b17f6..f662f1c03 100644 --- a/server/controllers/api/pods.ts +++ b/server/controllers/api/pods.ts @@ -1,10 +1,16 @@ +import * as Bluebird from 'bluebird' import * as express from 'express' import { getFormattedObjects } from '../../helpers' +import { getOrCreateAccount } from '../../helpers/activitypub' import { getApplicationAccount } from '../../helpers/utils' +import { REMOTE_SCHEME } from '../../initializers/constants' import { database as db } from '../../initializers/database' import { asyncMiddleware, paginationValidator, setFollowersSort, setPagination } from '../../middlewares' +import { setBodyHostsPort } from '../../middlewares/pods' import { setFollowingSort } from '../../middlewares/sort' +import { followValidator } from '../../middlewares/validators/pods' import { followersSortValidator, followingSortValidator } from '../../middlewares/validators/sort' +import { sendFollow } from '../../lib/activitypub/send-request' const podsRouter = express.Router() @@ -16,6 +22,12 @@ podsRouter.get('/following', asyncMiddleware(listFollowing) ) +podsRouter.post('/follow', + followValidator, + setBodyHostsPort, + asyncMiddleware(follow) +) + podsRouter.get('/followers', paginationValidator, followersSortValidator, @@ -45,3 +57,32 @@ async function listFollowers (req: express.Request, res: express.Response, next: 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[] = [] + 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() +} diff --git a/server/lib/activitypub/process-accept.ts b/server/lib/activitypub/process-accept.ts index 37e42bd3a..9e0cd4032 100644 --- a/server/lib/activitypub/process-accept.ts +++ b/server/lib/activitypub/process-accept.ts @@ -7,7 +7,7 @@ async function processAcceptActivity (activity: ActivityAccept, inboxAccount?: A 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) if (!follow) throw new Error('Cannot find associated follow.') follow.set('state', 'accepted') - return follow.save() + await follow.save() } diff --git a/server/lib/activitypub/process-add.ts b/server/lib/activitypub/process-add.ts index 40541aca3..024dee559 100644 --- a/server/lib/activitypub/process-add.ts +++ b/server/lib/activitypub/process-add.ts @@ -29,7 +29,7 @@ export { function processAddVideo (account: AccountInstance, videoChannelUrl: string, video: VideoTorrentObject) { const options = { - arguments: [ account, videoChannelUrl ,video ], + arguments: [ account, videoChannelUrl, video ], errorMessage: 'Cannot insert the remote video with many retries.' } diff --git a/server/lib/activitypub/process-follow.ts b/server/lib/activitypub/process-follow.ts index a04fc7994..ee5d97a0b 100644 --- a/server/lib/activitypub/process-follow.ts +++ b/server/lib/activitypub/process-follow.ts @@ -1,7 +1,9 @@ import { ActivityFollow } from '../../../shared/models/activitypub/activity' -import { getOrCreateAccount } from '../../helpers' +import { getOrCreateAccount, retryTransactionWrapper } from '../../helpers' import { database as db } from '../../initializers' import { AccountInstance } from '../../models/account/account-interface' +import { sendAccept } from './send-request' +import { logger } from '../../helpers/logger' async function processFollowActivity (activity: ActivityFollow) { const activityObject = activity.object @@ -18,15 +20,34 @@ export { // --------------------------------------------------------------------------- -async function processFollow (account: AccountInstance, targetAccountURL: string) { - const targetAccount = await db.Account.loadByUrl(targetAccountURL) +function processFollow (account: AccountInstance, targetAccountURL: string) { + const options = { + arguments: [ account, targetAccountURL ], + errorMessage: 'Cannot follow with many retries.' + } - if (targetAccount === undefined) throw new Error('Unknown account') - if (targetAccount.isOwned() === false) throw new Error('This is not a local account.') - - return db.AccountFollow.create({ - accountId: account.id, - targetAccountId: targetAccount.id, - state: 'accepted' - }) + return retryTransactionWrapper(follow, options) +} + +async function follow (account: AccountInstance, targetAccountURL: string) { + await db.sequelize.transaction(async t => { + const targetAccount = await db.Account.loadByUrl(targetAccountURL, t) + + 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) } diff --git a/server/lib/activitypub/send-request.ts b/server/lib/activitypub/send-request.ts index ce9a96f14..e6ef5f37a 100644 --- a/server/lib/activitypub/send-request.ts +++ b/server/lib/activitypub/send-request.ts @@ -56,6 +56,18 @@ function sendDeleteAccount (account: AccountInstance, t: Sequelize.Transaction) 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 { @@ -65,7 +77,9 @@ export { sendAddVideo, sendUpdateVideo, sendDeleteVideo, - sendDeleteAccount + sendDeleteAccount, + sendAccept, + sendFollow } // --------------------------------------------------------------------------- @@ -81,6 +95,15 @@ async function broadcastToFollowers (data: any, fromAccount: AccountInstance, t: 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) { const activity = activityPubContextify(data) @@ -142,3 +165,24 @@ async function addActivityData (url: string, byAccount: AccountInstance, target: 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) +} diff --git a/server/middlewares/validators/index.ts b/server/middlewares/validators/index.ts index 0b7573d4f..46c00d679 100644 --- a/server/middlewares/validators/index.ts +++ b/server/middlewares/validators/index.ts @@ -2,6 +2,7 @@ export * from './account' export * from './oembed' export * from './activitypub' export * from './pagination' +export * from './pods' export * from './sort' export * from './users' export * from './videos' diff --git a/server/middlewares/validators/pods.ts b/server/middlewares/validators/pods.ts new file mode 100644 index 000000000..e17369a6f --- /dev/null +++ b/server/middlewares/validators/pods.ts @@ -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 +} diff --git a/server/models/account/account-interface.ts b/server/models/account/account-interface.ts index d49dfbe17..73701f233 100644 --- a/server/models/account/account-interface.ts +++ b/server/models/account/account-interface.ts @@ -10,7 +10,7 @@ export namespace AccountMethods { export type Load = (id: number) => Bluebird export type LoadByUUID = (uuid: string) => Bluebird - export type LoadByUrl = (url: string) => Bluebird + export type LoadByUrl = (url: string, transaction?: Sequelize.Transaction) => Bluebird export type LoadAccountByPodAndUUID = (uuid: string, podId: number, transaction: Sequelize.Transaction) => Bluebird export type LoadLocalAccountByNameAndPod = (name: string, host: string) => Bluebird export type ListOwned = () => Bluebird diff --git a/server/models/account/account.ts b/server/models/account/account.ts index daf8f4703..7ce97b2fd 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts @@ -198,6 +198,7 @@ export default function defineAccount (sequelize: Sequelize.Sequelize, DataTypes loadApplication, load, loadByUUID, + loadByUrl, loadLocalAccountByNameAndPod, listOwned, listFollowerUrlsForApi, @@ -480,11 +481,12 @@ loadLocalAccountByNameAndPod = function (name: string, host: string) { return Account.findOne(query) } -loadByUrl = function (url: string) { +loadByUrl = function (url: string, transaction?: Sequelize.Transaction) { const query: Sequelize.FindOptions = { where: { url - } + }, + transaction } return Account.findOne(query)