+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()
asyncMiddleware(listFollowing)
)
+podsRouter.post('/follow',
+ followValidator,
+ setBodyHostsPort,
+ asyncMiddleware(follow)
+)
+
podsRouter.get('/followers',
paginationValidator,
followersSortValidator,
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()
+}
const targetAccount = await db.Account.loadByUrl(activity.actor)
- return processFollow(inboxAccount, targetAccount)
+ return processAccept(inboxAccount, targetAccount)
}
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
-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()
}
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.'
}
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
// ---------------------------------------------------------------------------
-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 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.')
- return db.AccountFollow.create({
- accountId: account.id,
- targetAccountId: targetAccount.id,
- state: 'accepted'
+ 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)
}
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 {
sendAddVideo,
sendUpdateVideo,
sendDeleteVideo,
- sendDeleteAccount
+ sendDeleteAccount,
+ sendAccept,
+ sendFollow
}
// ---------------------------------------------------------------------------
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)
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)
+}
export * from './oembed'
export * from './activitypub'
export * from './pagination'
+export * from './pods'
export * from './sort'
export * from './users'
export * from './videos'
--- /dev/null
+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
+}
export type Load = (id: number) => 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 LoadLocalAccountByNameAndPod = (name: string, host: string) => Bluebird<AccountInstance>
export type ListOwned = () => Bluebird<AccountInstance[]>
loadApplication,
load,
loadByUUID,
+ loadByUrl,
loadLocalAccountByNameAndPod,
listOwned,
listFollowerUrlsForApi,
return Account.findOne(query)
}
-loadByUrl = function (url: string) {
+loadByUrl = function (url: string, transaction?: Sequelize.Transaction) {
const query: Sequelize.FindOptions<AccountAttributes> = {
where: {
url
- }
+ },
+ transaction
}
return Account.findOne(query)