]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Send follow/accept
authorChocobozzz <florian.bigard@gmail.com>
Mon, 13 Nov 2017 17:48:28 +0000 (18:48 +0100)
committerChocobozzz <florian.bigard@gmail.com>
Mon, 27 Nov 2017 18:40:51 +0000 (19:40 +0100)
server/controllers/api/pods.ts
server/lib/activitypub/process-accept.ts
server/lib/activitypub/process-add.ts
server/lib/activitypub/process-follow.ts
server/lib/activitypub/send-request.ts
server/middlewares/validators/index.ts
server/middlewares/validators/pods.ts [new file with mode: 0644]
server/models/account/account-interface.ts
server/models/account/account.ts

index aa07b17f650d59d980f4ced72342bd621e573142..f662f1c0327d59a45714bb3d308309371599cff8 100644 (file)
@@ -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<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()
+}
index 37e42bd3a94c4e9959313260bab18eddd173fc79..9e0cd4032df31dccd63c4dbd0ff11b432954be9d 100644 (file)
@@ -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()
 }
index 40541aca364ccb3373a74c5fc114725e90b6147c..024dee5591c149e1b48d2f7b2ae801bdf8b705e2 100644 (file)
@@ -29,7 +29,7 @@ export {
 
 function processAddVideo (account: AccountInstance, videoChannelUrl: string, video: VideoTorrentObject) {
   const options = {
-    arguments: [ account, videoChannelUrl ,video ],
+    arguments: [ account, videoChannelUrlvideo ],
     errorMessage: 'Cannot insert the remote video with many retries.'
   }
 
index a04fc79945060fd84d843fd0c08d902ea8c314e3..ee5d97a0be6f6dd5ab196f6ead0124e0b718a8cc 100644 (file)
@@ -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 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)
 }
index ce9a96f1464a98559f7a86e14fcc37e787f16f77..e6ef5f37afebd576d6c22ae2694fa399753cb4e7 100644 (file)
@@ -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)
+}
index 0b7573d4fac1caae17d221e66e3381281c270ce0..46c00d679e998ebdc9c5a95aaf4bb6ace1baa0a7 100644 (file)
@@ -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 (file)
index 0000000..e17369a
--- /dev/null
@@ -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
+}
index d49dfbe1764690de30334dc95ef887eeda5157f7..73701f233d5e55327c5fe4ac042f7a205d8d156b 100644 (file)
@@ -10,7 +10,7 @@ export namespace AccountMethods {
 
   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[]>
index daf8f47035c009a581496577bbe9a48183bcaf32..7ce97b2fd97c1ce747dbd054934c34aa209e13d5 100644 (file)
@@ -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<AccountAttributes> = {
     where: {
       url
-    }
+    },
+    transaction
   }
 
   return Account.findOne(query)