Send follow/accept
This commit is contained in:
parent
7a7724e66e
commit
ce548a10db
|
@ -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()
|
||||||
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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[]>
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue