From 350e31d6b64e4973dfa5e9f7b46841cb09aeb1ad Mon Sep 17 00:00:00 2001
From: Chocobozzz <florian.bigard@gmail.com>
Date: Tue, 14 Nov 2017 17:31:26 +0100
Subject: Follow works

---
 server/controllers/activitypub/client.ts           |  4 +-
 server/controllers/activitypub/inbox.ts            |  4 +-
 server/controllers/activitypub/index.ts            | 10 ++--
 server/controllers/api/pods.ts                     | 68 ++++++++++++++++------
 server/controllers/index.ts                        |  2 +
 server/controllers/webfinger.ts                    | 39 +++++++++++++
 server/helpers/activitypub.ts                      | 29 +++++----
 server/helpers/custom-validators/accounts.ts       | 53 +++++++++++++++++
 .../custom-validators/activitypub/account.ts       | 31 ++++++----
 .../custom-validators/activitypub/activity.ts      | 19 ++++--
 .../helpers/custom-validators/activitypub/misc.ts  | 10 ++--
 .../custom-validators/activitypub/videos.ts        | 14 ++++-
 server/helpers/custom-validators/index.ts          |  3 +-
 server/helpers/custom-validators/video-accounts.ts | 53 -----------------
 server/helpers/custom-validators/webfinger.ts      | 25 ++++++++
 server/helpers/webfinger.ts                        | 21 ++++---
 server/initializers/checker.ts                     | 14 ++++-
 server/initializers/constants.ts                   |  5 ++
 server/initializers/installer.ts                   | 10 +++-
 server/lib/activitypub/process-add.ts              |  2 +-
 server/lib/activitypub/process-follow.ts           | 18 +++---
 server/lib/activitypub/send-request.ts             | 38 ++++++------
 .../http-request-broadcast-handler.ts              |  1 +
 .../http-request-unicast-handler.ts                |  1 +
 server/lib/jobs/job-scheduler.ts                   | 13 +++--
 server/middlewares/activitypub.ts                  | 32 +++++-----
 server/middlewares/validators/account.ts           | 10 ++--
 .../middlewares/validators/activitypub/activity.ts |  7 +--
 server/middlewares/validators/index.ts             |  1 +
 server/middlewares/validators/webfinger.ts         | 42 +++++++++++++
 server/models/account/account-follow.ts            | 15 +++--
 server/models/account/account-interface.ts         |  6 +-
 server/models/account/account.ts                   | 44 ++++++++++----
 server/models/application/application-interface.ts |  9 ++-
 server/models/application/application.ts           | 11 +++-
 server/models/job/job.ts                           |  9 +--
 36 files changed, 466 insertions(+), 207 deletions(-)
 create mode 100644 server/controllers/webfinger.ts
 create mode 100644 server/helpers/custom-validators/accounts.ts
 delete mode 100644 server/helpers/custom-validators/video-accounts.ts
 create mode 100644 server/helpers/custom-validators/webfinger.ts
 create mode 100644 server/middlewares/validators/webfinger.ts

(limited to 'server')

diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts
index 461a619dd..56a4054fa 100644
--- a/server/controllers/activitypub/client.ts
+++ b/server/controllers/activitypub/client.ts
@@ -16,12 +16,12 @@ activityPubClientRouter.get('/account/:name',
   executeIfActivityPub(asyncMiddleware(accountController))
 )
 
-activityPubClientRouter.get('/account/:nameWithHost/followers',
+activityPubClientRouter.get('/account/:name/followers',
   executeIfActivityPub(localAccountValidator),
   executeIfActivityPub(asyncMiddleware(accountFollowersController))
 )
 
-activityPubClientRouter.get('/account/:nameWithHost/following',
+activityPubClientRouter.get('/account/:name/following',
   executeIfActivityPub(localAccountValidator),
   executeIfActivityPub(asyncMiddleware(accountFollowingController))
 )
diff --git a/server/controllers/activitypub/inbox.ts b/server/controllers/activitypub/inbox.ts
index eedb518b9..e62125d85 100644
--- a/server/controllers/activitypub/inbox.ts
+++ b/server/controllers/activitypub/inbox.ts
@@ -30,7 +30,7 @@ inboxRouter.post('/inbox',
   asyncMiddleware(inboxController)
 )
 
-inboxRouter.post('/:nameWithHost/inbox',
+inboxRouter.post('/account/:name/inbox',
   signatureValidator,
   asyncMiddleware(checkSignature),
   localAccountValidator,
@@ -59,7 +59,9 @@ async function inboxController (req: express.Request, res: express.Response, nex
   }
 
   // Only keep activities we are able to process
+  logger.debug('Filtering activities...', { activities })
   activities = activities.filter(a => isActivityValid(a))
+  logger.debug('We keep %d activities.', activities.length, { activities })
 
   await processActivities(activities, res.locals.account)
 
diff --git a/server/controllers/activitypub/index.ts b/server/controllers/activitypub/index.ts
index 6c7bafc6e..0c8574ef7 100644
--- a/server/controllers/activitypub/index.ts
+++ b/server/controllers/activitypub/index.ts
@@ -4,14 +4,14 @@ import { badRequest } from '../../helpers'
 import { inboxRouter } from './inbox'
 import { activityPubClientRouter } from './client'
 
-const remoteRouter = express.Router()
+const activityPubRouter = express.Router()
 
-remoteRouter.use('/', inboxRouter)
-remoteRouter.use('/', activityPubClientRouter)
-remoteRouter.use('/*', badRequest)
+activityPubRouter.use('/', inboxRouter)
+activityPubRouter.use('/', activityPubClientRouter)
+activityPubRouter.use('/*', badRequest)
 
 // ---------------------------------------------------------------------------
 
 export {
-  remoteRouter
+  activityPubRouter
 }
diff --git a/server/controllers/api/pods.ts b/server/controllers/api/pods.ts
index 2231a05fa..0bd6971bb 100644
--- a/server/controllers/api/pods.ts
+++ b/server/controllers/api/pods.ts
@@ -1,19 +1,19 @@
-import * as Bluebird from 'bluebird'
 import * as express from 'express'
+import { UserRight } from '../../../shared/models/users/user-right.enum'
 import { getFormattedObjects } from '../../helpers'
-import { getOrCreateAccount } from '../../helpers/activitypub'
+import { logger } from '../../helpers/logger'
 import { getApplicationAccount } from '../../helpers/utils'
-import { REMOTE_SCHEME } from '../../initializers/constants'
+import { getAccountFromWebfinger } from '../../helpers/webfinger'
+import { SERVER_ACCOUNT_NAME } from '../../initializers/constants'
 import { database as db } from '../../initializers/database'
+import { sendFollow } from '../../lib/activitypub/send-request'
 import { asyncMiddleware, paginationValidator, setFollowersSort, setPagination } from '../../middlewares'
+import { authenticate } from '../../middlewares/oauth'
 import { setBodyHostsPort } from '../../middlewares/pods'
 import { setFollowingSort } from '../../middlewares/sort'
+import { ensureUserHasRight } from '../../middlewares/user-right'
 import { followValidator } from '../../middlewares/validators/pods'
 import { followersSortValidator, followingSortValidator } from '../../middlewares/validators/sort'
-import { sendFollow } from '../../lib/activitypub/send-request'
-import { authenticate } from '../../middlewares/oauth'
-import { ensureUserHasRight } from '../../middlewares/user-right'
-import { UserRight } from '../../../shared/models/users/user-right.enum'
 
 const podsRouter = express.Router()
 
@@ -67,22 +67,43 @@ async function follow (req: express.Request, res: express.Response, next: expres
   const hosts = req.body.hosts as string[]
   const fromAccount = await getApplicationAccount()
 
-  const tasks: Bluebird<any>[] = []
+  const tasks: Promise<any>[] = []
+  const accountName = SERVER_ACCOUNT_NAME
+
   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'
+    const p = loadLocalOrGetAccountFromWebfinger(accountName, host)
+      .then(accountResult => {
+        let targetAccount = accountResult.account
+
+        return db.sequelize.transaction(async t => {
+          if (accountResult.loadedFromDB === false) {
+            targetAccount = await targetAccount.save({ transaction: t })
+          }
+
+          const [ accountFollow ] = await db.AccountFollow.findOrCreate({
+            where: {
+              accountId: fromAccount.id,
+              targetAccountId: targetAccount.id
+            },
+            defaults: {
+              state: 'pending',
+              accountId: fromAccount.id,
+              targetAccountId: targetAccount.id
+            },
+            transaction: t
+          })
+
+          // Send a notification to remote server
+          if (accountFollow.state === 'pending') {
+            await sendFollow(fromAccount, targetAccount, t)
+          }
+        })
       })
-      .then(() => sendFollow(fromAccount, targetAccount, t))
-    })
+      .catch(err => logger.warn('Cannot follow server %s.', `${accountName}@${host}`, err))
 
     tasks.push(p)
   }
@@ -91,3 +112,16 @@ async function follow (req: express.Request, res: express.Response, next: expres
 
   return res.status(204).end()
 }
+
+async function loadLocalOrGetAccountFromWebfinger (name: string, host: string) {
+  let loadedFromDB = true
+  let account = await db.Account.loadByNameAndHost(name, host)
+
+  if (!account) {
+    const nameWithDomain = name + '@' + host
+    account = await getAccountFromWebfinger(nameWithDomain)
+    loadedFromDB = false
+  }
+
+  return { account, loadedFromDB }
+}
diff --git a/server/controllers/index.ts b/server/controllers/index.ts
index 51cb480a3..457d0a12e 100644
--- a/server/controllers/index.ts
+++ b/server/controllers/index.ts
@@ -1,4 +1,6 @@
+export * from './activitypub'
 export * from './static'
 export * from './client'
 export * from './services'
 export * from './api'
+export * from './webfinger'
diff --git a/server/controllers/webfinger.ts b/server/controllers/webfinger.ts
new file mode 100644
index 000000000..1c726f0cb
--- /dev/null
+++ b/server/controllers/webfinger.ts
@@ -0,0 +1,39 @@
+import * as express from 'express'
+
+import { CONFIG, PREVIEWS_SIZE, EMBED_SIZE } from '../initializers'
+import { oembedValidator } from '../middlewares'
+import { VideoInstance } from '../models'
+import { webfingerValidator } from '../middlewares/validators/webfinger'
+import { AccountInstance } from '../models/account/account-interface'
+
+const webfingerRouter = express.Router()
+
+webfingerRouter.use('/.well-known/webfinger',
+  webfingerValidator,
+  webfingerController
+)
+
+// ---------------------------------------------------------------------------
+
+export {
+  webfingerRouter
+}
+
+// ---------------------------------------------------------------------------
+
+function webfingerController (req: express.Request, res: express.Response, next: express.NextFunction) {
+  const account: AccountInstance = res.locals.account
+
+  const json = {
+    subject: req.query.resource,
+    aliases: [ account.url ],
+    links: [
+      {
+        rel: 'self',
+        href: account.url
+      }
+    ]
+  }
+
+  return res.json(json).end()
+}
diff --git a/server/helpers/activitypub.ts b/server/helpers/activitypub.ts
index a1493e5c1..b91490a0b 100644
--- a/server/helpers/activitypub.ts
+++ b/server/helpers/activitypub.ts
@@ -5,7 +5,7 @@ import { ActivityIconObject } from '../../shared/index'
 import { ActivityPubActor } from '../../shared/models/activitypub/activitypub-actor'
 import { ResultList } from '../../shared/models/result-list.model'
 import { database as db, REMOTE_SCHEME } from '../initializers'
-import { CONFIG, STATIC_PATHS } from '../initializers/constants'
+import { ACTIVITY_PUB_ACCEPT_HEADER, CONFIG, STATIC_PATHS } from '../initializers/constants'
 import { VideoInstance } from '../models/video/video-interface'
 import { isRemoteAccountValid } from './custom-validators'
 import { logger } from './logger'
@@ -35,11 +35,11 @@ async function getOrCreateAccount (accountUrl: string) {
 
   // We don't have this account in our database, fetch it on remote
   if (!account) {
-    const { account } = await fetchRemoteAccountAndCreatePod(accountUrl)
-
-    if (!account) throw new Error('Cannot fetch remote account.')
+    const res = await fetchRemoteAccountAndCreatePod(accountUrl)
+    if (res === undefined) throw new Error('Cannot fetch remote account.')
 
     // Save our new account in database
+    const account = res.account
     await account.save()
   }
 
@@ -49,19 +49,27 @@ async function getOrCreateAccount (accountUrl: string) {
 async function fetchRemoteAccountAndCreatePod (accountUrl: string) {
   const options = {
     uri: accountUrl,
-    method: 'GET'
+    method: 'GET',
+    headers: {
+      'Accept': ACTIVITY_PUB_ACCEPT_HEADER
+    }
   }
 
+  logger.info('Fetching remote account %s.', accountUrl)
+
   let requestResult
   try {
     requestResult = await doRequest(options)
   } catch (err) {
-    logger.warning('Cannot fetch remote account %s.', accountUrl, err)
+    logger.warn('Cannot fetch remote account %s.', accountUrl, err)
     return undefined
   }
 
-  const accountJSON: ActivityPubActor = requestResult.body
-  if (isRemoteAccountValid(accountJSON) === false) return undefined
+  const accountJSON: ActivityPubActor = JSON.parse(requestResult.body)
+  if (isRemoteAccountValid(accountJSON) === false) {
+    logger.debug('Remote account JSON is not valid.', { accountJSON })
+    return undefined
+  }
 
   const followersCount = await fetchAccountCount(accountJSON.followers)
   const followingCount = await fetchAccountCount(accountJSON.following)
@@ -90,7 +98,8 @@ async function fetchRemoteAccountAndCreatePod (accountUrl: string) {
       host: accountHost
     }
   }
-  const pod = await db.Pod.findOrCreate(podOptions)
+  const [ pod ] = await db.Pod.findOrCreate(podOptions)
+  account.set('podId', pod.id)
 
   return { account, pod }
 }
@@ -176,7 +185,7 @@ async function fetchAccountCount (url: string) {
   try {
     requestResult = await doRequest(options)
   } catch (err) {
-    logger.warning('Cannot fetch remote account count %s.', url, err)
+    logger.warn('Cannot fetch remote account count %s.', url, err)
     return undefined
   }
 
diff --git a/server/helpers/custom-validators/accounts.ts b/server/helpers/custom-validators/accounts.ts
new file mode 100644
index 000000000..6d6219a95
--- /dev/null
+++ b/server/helpers/custom-validators/accounts.ts
@@ -0,0 +1,53 @@
+import * as Promise from 'bluebird'
+import * as validator from 'validator'
+import * as express from 'express'
+import 'express-validator'
+
+import { database as db } from '../../initializers'
+import { AccountInstance } from '../../models'
+import { logger } from '../logger'
+
+import { isUserUsernameValid } from './users'
+import { isHostValid } from './pods'
+
+function isAccountNameValid (value: string) {
+  return isUserUsernameValid(value)
+}
+
+function isAccountNameWithHostValid (value: string) {
+  const [ name, host ] = value.split('@')
+
+  return isAccountNameValid(name) && isHostValid(host)
+}
+
+function checkVideoAccountExists (id: string, res: express.Response, callback: () => void) {
+  let promise: Promise<AccountInstance>
+  if (validator.isInt(id)) {
+    promise = db.Account.load(+id)
+  } else { // UUID
+    promise = db.Account.loadByUUID(id)
+  }
+
+  promise.then(account => {
+    if (!account) {
+      return res.status(404)
+        .json({ error: 'Video account not found' })
+        .end()
+    }
+
+    res.locals.account = account
+    callback()
+  })
+  .catch(err => {
+    logger.error('Error in video account request validator.', err)
+    return res.sendStatus(500)
+  })
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+  checkVideoAccountExists,
+  isAccountNameWithHostValid,
+  isAccountNameValid
+}
diff --git a/server/helpers/custom-validators/activitypub/account.ts b/server/helpers/custom-validators/activitypub/account.ts
index acd2b8058..645f55a5a 100644
--- a/server/helpers/custom-validators/activitypub/account.ts
+++ b/server/helpers/custom-validators/activitypub/account.ts
@@ -1,9 +1,8 @@
 import * as validator from 'validator'
-
-import { exists, isUUIDValid } from '../misc'
-import { isActivityPubUrlValid } from './misc'
-import { isUserUsernameValid } from '../users'
 import { CONSTRAINTS_FIELDS } from '../../../initializers/constants'
+import { isAccountNameValid } from '../accounts'
+import { exists, isUUIDValid } from '../misc'
+import { isActivityPubUrlValid, isBaseActivityValid } from './misc'
 
 function isAccountEndpointsObjectValid (endpointObject: any) {
   return isAccountSharedInboxValid(endpointObject.sharedInbox)
@@ -59,10 +58,6 @@ function isAccountOutboxValid (outbox: string) {
   return isActivityPubUrlValid(outbox)
 }
 
-function isAccountNameValid (name: string) {
-  return isUserUsernameValid(name)
-}
-
 function isAccountPreferredUsernameValid (preferredUsername: string) {
   return isAccountNameValid(preferredUsername)
 }
@@ -90,7 +85,7 @@ function isRemoteAccountValid (remoteAccount: any) {
     isAccountPreferredUsernameValid(remoteAccount.preferredUsername) &&
     isAccountUrlValid(remoteAccount.url) &&
     isAccountPublicKeyObjectValid(remoteAccount.publicKey) &&
-    isAccountEndpointsObjectValid(remoteAccount.endpoint)
+    isAccountEndpointsObjectValid(remoteAccount.endpoints)
 }
 
 function isAccountFollowingCountValid (value: string) {
@@ -101,6 +96,19 @@ function isAccountFollowersCountValid (value: string) {
   return exists(value) && validator.isInt('' + value, { min: 0 })
 }
 
+function isAccountDeleteActivityValid (activity: any) {
+  return isBaseActivityValid(activity, 'Delete')
+}
+
+function isAccountFollowActivityValid (activity: any) {
+  return isBaseActivityValid(activity, 'Follow') &&
+    isActivityPubUrlValid(activity.object)
+}
+
+function isAccountAcceptActivityValid (activity: any) {
+  return isBaseActivityValid(activity, 'Accept')
+}
+
 // ---------------------------------------------------------------------------
 
 export {
@@ -122,5 +130,8 @@ export {
   isRemoteAccountValid,
   isAccountFollowingCountValid,
   isAccountFollowersCountValid,
-  isAccountNameValid
+  isAccountNameValid,
+  isAccountFollowActivityValid,
+  isAccountAcceptActivityValid,
+  isAccountDeleteActivityValid
 }
diff --git a/server/helpers/custom-validators/activitypub/activity.ts b/server/helpers/custom-validators/activitypub/activity.ts
index dd671c4cf..b5ba0f7af 100644
--- a/server/helpers/custom-validators/activitypub/activity.ts
+++ b/server/helpers/custom-validators/activitypub/activity.ts
@@ -1,9 +1,13 @@
 import * as validator from 'validator'
+import { isAccountAcceptActivityValid, isAccountDeleteActivityValid, isAccountFollowActivityValid } from './account'
+import { isActivityPubUrlValid } from './misc'
 import {
   isVideoChannelCreateActivityValid,
+  isVideoChannelDeleteActivityValid,
+  isVideoChannelUpdateActivityValid,
   isVideoTorrentAddActivityValid,
-  isVideoTorrentUpdateActivityValid,
-  isVideoChannelUpdateActivityValid
+  isVideoTorrentDeleteActivityValid,
+  isVideoTorrentUpdateActivityValid
 } from './videos'
 
 function isRootActivityValid (activity: any) {
@@ -14,8 +18,8 @@ function isRootActivityValid (activity: any) {
       Array.isArray(activity.items)
     ) ||
     (
-      validator.isURL(activity.id) &&
-      validator.isURL(activity.actor)
+      isActivityPubUrlValid(activity.id) &&
+      isActivityPubUrlValid(activity.actor)
     )
 }
 
@@ -23,7 +27,12 @@ function isActivityValid (activity: any) {
   return isVideoTorrentAddActivityValid(activity) ||
     isVideoChannelCreateActivityValid(activity) ||
     isVideoTorrentUpdateActivityValid(activity) ||
-    isVideoChannelUpdateActivityValid(activity)
+    isVideoChannelUpdateActivityValid(activity) ||
+    isVideoTorrentDeleteActivityValid(activity) ||
+    isVideoChannelDeleteActivityValid(activity) ||
+    isAccountDeleteActivityValid(activity) ||
+    isAccountFollowActivityValid(activity) ||
+    isAccountAcceptActivityValid(activity)
 }
 
 // ---------------------------------------------------------------------------
diff --git a/server/helpers/custom-validators/activitypub/misc.ts b/server/helpers/custom-validators/activitypub/misc.ts
index a94c36b51..665a63a73 100644
--- a/server/helpers/custom-validators/activitypub/misc.ts
+++ b/server/helpers/custom-validators/activitypub/misc.ts
@@ -23,10 +23,12 @@ function isActivityPubUrlValid (url: string) {
 function isBaseActivityValid (activity: any, type: string) {
   return Array.isArray(activity['@context']) &&
     activity.type === type &&
-    validator.isURL(activity.id) &&
-    validator.isURL(activity.actor) &&
-    Array.isArray(activity.to) &&
-    activity.to.every(t => validator.isURL(t))
+    isActivityPubUrlValid(activity.id) &&
+    isActivityPubUrlValid(activity.actor) &&
+    (
+      activity.to === undefined ||
+      (Array.isArray(activity.to) && activity.to.every(t => isActivityPubUrlValid(t)))
+    )
 }
 
 export {
diff --git a/server/helpers/custom-validators/activitypub/videos.ts b/server/helpers/custom-validators/activitypub/videos.ts
index 8f6d50f50..c9ecf1f3d 100644
--- a/server/helpers/custom-validators/activitypub/videos.ts
+++ b/server/helpers/custom-validators/activitypub/videos.ts
@@ -14,7 +14,7 @@ import {
   isVideoUrlValid
 } from '../videos'
 import { isVideoChannelDescriptionValid, isVideoChannelNameValid } from '../video-channels'
-import { isBaseActivityValid } from './misc'
+import { isActivityPubUrlValid, isBaseActivityValid } from './misc'
 
 function isVideoTorrentAddActivityValid (activity: any) {
   return isBaseActivityValid(activity, 'Add') &&
@@ -26,6 +26,10 @@ function isVideoTorrentUpdateActivityValid (activity: any) {
     isVideoTorrentObjectValid(activity.object)
 }
 
+function isVideoTorrentDeleteActivityValid (activity: any) {
+  return isBaseActivityValid(activity, 'Delete')
+}
+
 function isVideoTorrentObjectValid (video: any) {
   return video.type === 'Video' &&
     isVideoNameValid(video.name) &&
@@ -54,6 +58,10 @@ function isVideoChannelUpdateActivityValid (activity: any) {
     isVideoChannelObjectValid(activity.object)
 }
 
+function isVideoChannelDeleteActivityValid (activity: any) {
+  return isBaseActivityValid(activity, 'Delete')
+}
+
 function isVideoChannelObjectValid (videoChannel: any) {
   return videoChannel.type === 'VideoChannel' &&
     isVideoChannelNameValid(videoChannel.name) &&
@@ -67,7 +75,9 @@ export {
   isVideoTorrentAddActivityValid,
   isVideoChannelCreateActivityValid,
   isVideoTorrentUpdateActivityValid,
-  isVideoChannelUpdateActivityValid
+  isVideoChannelUpdateActivityValid,
+  isVideoChannelDeleteActivityValid,
+  isVideoTorrentDeleteActivityValid
 }
 
 // ---------------------------------------------------------------------------
diff --git a/server/helpers/custom-validators/index.ts b/server/helpers/custom-validators/index.ts
index 33922b8fe..1c475e301 100644
--- a/server/helpers/custom-validators/index.ts
+++ b/server/helpers/custom-validators/index.ts
@@ -3,6 +3,7 @@ export * from './misc'
 export * from './pods'
 export * from './pods'
 export * from './users'
-export * from './video-accounts'
+export * from './accounts'
 export * from './video-channels'
 export * from './videos'
+export * from './webfinger'
diff --git a/server/helpers/custom-validators/video-accounts.ts b/server/helpers/custom-validators/video-accounts.ts
deleted file mode 100644
index 31808ae1e..000000000
--- a/server/helpers/custom-validators/video-accounts.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import * as Promise from 'bluebird'
-import * as validator from 'validator'
-import * as express from 'express'
-import 'express-validator'
-
-import { database as db } from '../../initializers'
-import { AccountInstance } from '../../models'
-import { logger } from '../logger'
-
-import { isUserUsernameValid } from './users'
-import { isHostValid } from './pods'
-
-function isVideoAccountNameValid (value: string) {
-  return isUserUsernameValid(value)
-}
-
-function isAccountNameWithHostValid (value: string) {
-  const [ name, host ] = value.split('@')
-
-  return isVideoAccountNameValid(name) && isHostValid(host)
-}
-
-function checkVideoAccountExists (id: string, res: express.Response, callback: () => void) {
-  let promise: Promise<AccountInstance>
-  if (validator.isInt(id)) {
-    promise = db.Account.load(+id)
-  } else { // UUID
-    promise = db.Account.loadByUUID(id)
-  }
-
-  promise.then(account => {
-    if (!account) {
-      return res.status(404)
-        .json({ error: 'Video account not found' })
-        .end()
-    }
-
-    res.locals.account = account
-    callback()
-  })
-    .catch(err => {
-      logger.error('Error in video account request validator.', err)
-      return res.sendStatus(500)
-    })
-}
-
-// ---------------------------------------------------------------------------
-
-export {
-  checkVideoAccountExists,
-  isAccountNameWithHostValid,
-  isVideoAccountNameValid
-}
diff --git a/server/helpers/custom-validators/webfinger.ts b/server/helpers/custom-validators/webfinger.ts
new file mode 100644
index 000000000..e93115d81
--- /dev/null
+++ b/server/helpers/custom-validators/webfinger.ts
@@ -0,0 +1,25 @@
+import 'express-validator'
+import 'multer'
+import { CONFIG } from '../../initializers/constants'
+import { exists } from './misc'
+
+function isWebfingerResourceValid (value: string) {
+  if (!exists(value)) return false
+  if (value.startsWith('acct:') === false) return false
+
+  const accountWithHost = value.substr(5)
+  const accountParts = accountWithHost.split('@')
+  if (accountParts.length !== 2) return false
+
+  const host = accountParts[1]
+
+  if (host !== CONFIG.WEBSERVER.HOST) return false
+
+  return true
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+  isWebfingerResourceValid
+}
diff --git a/server/helpers/webfinger.ts b/server/helpers/webfinger.ts
index 164ae4951..0155e5f3e 100644
--- a/server/helpers/webfinger.ts
+++ b/server/helpers/webfinger.ts
@@ -12,17 +12,20 @@ const webfinger = new WebFinger({
   request_timeout: 3000
 })
 
-async function getAccountFromWebfinger (url: string) {
-  const webfingerData: WebFingerData = await webfingerLookup(url)
+async function getAccountFromWebfinger (nameWithHost: string) {
+  const webfingerData: WebFingerData = await webfingerLookup(nameWithHost)
 
-  if (Array.isArray(webfingerData.links) === false) return undefined
+  if (Array.isArray(webfingerData.links) === false) throw new Error('WebFinger links is not an array.')
 
   const selfLink = webfingerData.links.find(l => l.rel === 'self')
-  if (selfLink === undefined || isActivityPubUrlValid(selfLink.href) === false) return undefined
+  if (selfLink === undefined || isActivityPubUrlValid(selfLink.href) === false) {
+    throw new Error('Cannot find self link or href is not a valid URL.')
+  }
 
-  const { account } = await fetchRemoteAccountAndCreatePod(selfLink.href)
+  const res = await fetchRemoteAccountAndCreatePod(selfLink.href)
+  if (res === undefined) throw new Error('Cannot fetch and create pod of remote account ' + selfLink.href)
 
-  return account
+  return res.account
 }
 
 // ---------------------------------------------------------------------------
@@ -33,12 +36,12 @@ export {
 
 // ---------------------------------------------------------------------------
 
-function webfingerLookup (url: string) {
+function webfingerLookup (nameWithHost: string) {
   return new Promise<WebFingerData>((res, rej) => {
-    webfinger.lookup(url, (err, p) => {
+    webfinger.lookup(nameWithHost, (err, p) => {
       if (err) return rej(err)
 
-      return p
+      return res(p.object)
     })
   })
 }
diff --git a/server/initializers/checker.ts b/server/initializers/checker.ts
index b69188f7e..317d59423 100644
--- a/server/initializers/checker.ts
+++ b/server/initializers/checker.ts
@@ -1,8 +1,8 @@
 import * as config from 'config'
-
 import { promisify0 } from '../helpers/core-utils'
-import { OAuthClientModel } from '../models/oauth/oauth-client-interface'
 import { UserModel } from '../models/account/user-interface'
+import { ApplicationModel } from '../models/application/application-interface'
+import { OAuthClientModel } from '../models/oauth/oauth-client-interface'
 
 // Some checks on configuration files
 function checkConfig () {
@@ -70,6 +70,13 @@ async function usersExist (User: UserModel) {
   return totalUsers !== 0
 }
 
+// We get db by param to not import it in this file (import orders)
+async function applicationExist (Application: ApplicationModel) {
+  const totalApplication = await Application.countTotal()
+
+  return totalApplication !== 0
+}
+
 // ---------------------------------------------------------------------------
 
 export {
@@ -77,5 +84,6 @@ export {
   checkFFmpeg,
   checkMissedConfig,
   clientsExist,
-  usersExist
+  usersExist,
+  applicationExist
 }
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index e27d011fa..4a49c1ab3 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -226,6 +226,9 @@ const FRIEND_SCORE = {
   MAX: 1000
 }
 
+const SERVER_ACCOUNT_NAME = 'peertube'
+const ACTIVITY_PUB_ACCEPT_HEADER = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
+
 const ACTIVITY_PUB = {
   COLLECTION_ITEMS_PER_PAGE: 10,
   VIDEO_URL_MIME_TYPES: [
@@ -352,8 +355,10 @@ export {
   PODS_SCORE,
   PREVIEWS_SIZE,
   REMOTE_SCHEME,
+  ACTIVITY_PUB_ACCEPT_HEADER,
   FOLLOW_STATES,
   SEARCHABLE_COLUMNS,
+  SERVER_ACCOUNT_NAME,
   PRIVATE_RSA_KEY_SIZE,
   SORTABLE_COLUMNS,
   STATIC_MAX_AGE,
diff --git a/server/initializers/installer.ts b/server/initializers/installer.ts
index 5221b81a5..c3521a9e4 100644
--- a/server/initializers/installer.ts
+++ b/server/initializers/installer.ts
@@ -3,8 +3,8 @@ import { UserRole } from '../../shared'
 import { logger, mkdirpPromise, rimrafPromise } from '../helpers'
 import { createUserAccountAndChannel } from '../lib'
 import { createLocalAccount } from '../lib/user'
-import { clientsExist, usersExist } from './checker'
-import { CACHE, CONFIG, LAST_MIGRATION_VERSION } from './constants'
+import { applicationExist, clientsExist, usersExist } from './checker'
+import { CACHE, CONFIG, LAST_MIGRATION_VERSION, SERVER_ACCOUNT_NAME } from './constants'
 
 import { database as db } from './database'
 
@@ -128,9 +128,13 @@ async function createOAuthAdminIfNotExist () {
 }
 
 async function createApplicationIfNotExist () {
+  const exist = await applicationExist(db.Application)
+  // Nothing to do, application already exist
+  if (exist === true) return undefined
+
   logger.info('Creating Application table.')
   const applicationInstance = await db.Application.create({ migrationVersion: LAST_MIGRATION_VERSION })
 
   logger.info('Creating application account.')
-  return createLocalAccount('peertube', null, applicationInstance.id, undefined)
+  return createLocalAccount(SERVER_ACCOUNT_NAME, null, applicationInstance.id, undefined)
 }
diff --git a/server/lib/activitypub/process-add.ts b/server/lib/activitypub/process-add.ts
index 024dee559..06d23a2ea 100644
--- a/server/lib/activitypub/process-add.ts
+++ b/server/lib/activitypub/process-add.ts
@@ -54,7 +54,7 @@ async function addRemoteVideo (account: AccountInstance, videoChannelUrl: string
 
     // Don't block on request
     generateThumbnailFromUrl(video, videoToCreateData.icon)
-      .catch(err => logger.warning('Cannot generate thumbnail of %s.', videoToCreateData.id, err))
+      .catch(err => logger.warn('Cannot generate thumbnail of %s.', videoToCreateData.id, err))
 
     const videoCreated = await video.save(sequelizeOptions)
 
diff --git a/server/lib/activitypub/process-follow.ts b/server/lib/activitypub/process-follow.ts
index ee5d97a0b..a805c0757 100644
--- a/server/lib/activitypub/process-follow.ts
+++ b/server/lib/activitypub/process-follow.ts
@@ -36,14 +36,18 @@ async function follow (account: AccountInstance, targetAccountURL: string) {
     if (targetAccount === undefined) throw new Error('Unknown account')
     if (targetAccount.isOwned() === false) throw new Error('This is not a local account.')
 
-    const sequelizeOptions = {
+    await db.AccountFollow.findOrCreate({
+      where: {
+        accountId: account.id,
+        targetAccountId: targetAccount.id
+      },
+      defaults: {
+        accountId: account.id,
+        targetAccountId: targetAccount.id,
+        state: 'accepted'
+      },
       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)
diff --git a/server/lib/activitypub/send-request.ts b/server/lib/activitypub/send-request.ts
index c18a69784..d47040d6d 100644
--- a/server/lib/activitypub/send-request.ts
+++ b/server/lib/activitypub/send-request.ts
@@ -10,60 +10,60 @@ import { httpRequestJobScheduler } from '../jobs'
 import { signObject, activityPubContextify } from '../../helpers'
 import { Activity } from '../../../shared'
 
-function sendCreateVideoChannel (videoChannel: VideoChannelInstance, t: Sequelize.Transaction) {
+async function sendCreateVideoChannel (videoChannel: VideoChannelInstance, t: Sequelize.Transaction) {
   const videoChannelObject = videoChannel.toActivityPubObject()
-  const data = createActivityData(videoChannel.url, videoChannel.Account, videoChannelObject)
+  const data = await createActivityData(videoChannel.url, videoChannel.Account, videoChannelObject)
 
   return broadcastToFollowers(data, videoChannel.Account, t)
 }
 
-function sendUpdateVideoChannel (videoChannel: VideoChannelInstance, t: Sequelize.Transaction) {
+async function sendUpdateVideoChannel (videoChannel: VideoChannelInstance, t: Sequelize.Transaction) {
   const videoChannelObject = videoChannel.toActivityPubObject()
-  const data = updateActivityData(videoChannel.url, videoChannel.Account, videoChannelObject)
+  const data = await updateActivityData(videoChannel.url, videoChannel.Account, videoChannelObject)
 
   return broadcastToFollowers(data, videoChannel.Account, t)
 }
 
-function sendDeleteVideoChannel (videoChannel: VideoChannelInstance, t: Sequelize.Transaction) {
-  const data = deleteActivityData(videoChannel.url, videoChannel.Account)
+async function sendDeleteVideoChannel (videoChannel: VideoChannelInstance, t: Sequelize.Transaction) {
+  const data = await deleteActivityData(videoChannel.url, videoChannel.Account)
 
   return broadcastToFollowers(data, videoChannel.Account, t)
 }
 
-function sendAddVideo (video: VideoInstance, t: Sequelize.Transaction) {
+async function sendAddVideo (video: VideoInstance, t: Sequelize.Transaction) {
   const videoObject = video.toActivityPubObject()
-  const data = addActivityData(video.url, video.VideoChannel.Account, video.VideoChannel.url, videoObject)
+  const data = await addActivityData(video.url, video.VideoChannel.Account, video.VideoChannel.url, videoObject)
 
   return broadcastToFollowers(data, video.VideoChannel.Account, t)
 }
 
-function sendUpdateVideo (video: VideoInstance, t: Sequelize.Transaction) {
+async function sendUpdateVideo (video: VideoInstance, t: Sequelize.Transaction) {
   const videoObject = video.toActivityPubObject()
-  const data = updateActivityData(video.url, video.VideoChannel.Account, videoObject)
+  const data = await updateActivityData(video.url, video.VideoChannel.Account, videoObject)
 
   return broadcastToFollowers(data, video.VideoChannel.Account, t)
 }
 
-function sendDeleteVideo (video: VideoInstance, t: Sequelize.Transaction) {
-  const data = deleteActivityData(video.url, video.VideoChannel.Account)
+async function sendDeleteVideo (video: VideoInstance, t: Sequelize.Transaction) {
+  const data = await deleteActivityData(video.url, video.VideoChannel.Account)
 
   return broadcastToFollowers(data, video.VideoChannel.Account, t)
 }
 
-function sendDeleteAccount (account: AccountInstance, t: Sequelize.Transaction) {
-  const data = deleteActivityData(account.url, account)
+async function sendDeleteAccount (account: AccountInstance, t: Sequelize.Transaction) {
+  const data = await deleteActivityData(account.url, account)
 
   return broadcastToFollowers(data, account, t)
 }
 
-function sendAccept (fromAccount: AccountInstance, toAccount: AccountInstance, t: Sequelize.Transaction) {
-  const data = acceptActivityData(fromAccount)
+async function sendAccept (fromAccount: AccountInstance, toAccount: AccountInstance, t: Sequelize.Transaction) {
+  const data = await acceptActivityData(fromAccount)
 
   return unicastTo(data, toAccount, t)
 }
 
-function sendFollow (fromAccount: AccountInstance, toAccount: AccountInstance, t: Sequelize.Transaction) {
-  const data = followActivityData(toAccount.url, fromAccount)
+async function sendFollow (fromAccount: AccountInstance, toAccount: AccountInstance, t: Sequelize.Transaction) {
+  const data = await followActivityData(toAccount.url, fromAccount)
 
   return unicastTo(data, toAccount, t)
 }
@@ -97,7 +97,7 @@ async function broadcastToFollowers (data: any, fromAccount: AccountInstance, t:
 
 async function unicastTo (data: any, toAccount: AccountInstance, t: Sequelize.Transaction) {
   const jobPayload = {
-    uris: [ toAccount.url ],
+    uris: [ toAccount.inboxUrl ],
     body: data
   }
 
diff --git a/server/lib/jobs/http-request-job-scheduler/http-request-broadcast-handler.ts b/server/lib/jobs/http-request-job-scheduler/http-request-broadcast-handler.ts
index 799b86e1c..2f1d9ee92 100644
--- a/server/lib/jobs/http-request-job-scheduler/http-request-broadcast-handler.ts
+++ b/server/lib/jobs/http-request-job-scheduler/http-request-broadcast-handler.ts
@@ -6,6 +6,7 @@ async function process (payload: HTTPRequestPayload, jobId: number) {
   logger.info('Processing broadcast in job %d.', jobId)
 
   const options = {
+    method: 'POST',
     uri: '',
     json: payload.body
   }
diff --git a/server/lib/jobs/http-request-job-scheduler/http-request-unicast-handler.ts b/server/lib/jobs/http-request-job-scheduler/http-request-unicast-handler.ts
index 13451f042..3a1a7fabf 100644
--- a/server/lib/jobs/http-request-job-scheduler/http-request-unicast-handler.ts
+++ b/server/lib/jobs/http-request-job-scheduler/http-request-unicast-handler.ts
@@ -7,6 +7,7 @@ async function process (payload: HTTPRequestPayload, jobId: number) {
 
   const uri = payload.uris[0]
   const options = {
+    method: 'POST',
     uri,
     json: payload.body
   }
diff --git a/server/lib/jobs/job-scheduler.ts b/server/lib/jobs/job-scheduler.ts
index f10f745b3..b25bb7ab3 100644
--- a/server/lib/jobs/job-scheduler.ts
+++ b/server/lib/jobs/job-scheduler.ts
@@ -4,6 +4,7 @@ import { JobCategory } from '../../../shared'
 import { logger } from '../../helpers'
 import { database as db, JOB_STATES, JOBS_FETCH_LIMIT_PER_CYCLE, JOBS_FETCHING_INTERVAL } from '../../initializers'
 import { JobInstance } from '../../models'
+import { error } from 'util'
 
 export interface JobHandler<P, T> {
   process (data: object, jobId: number): Promise<T>
@@ -80,8 +81,12 @@ class JobScheduler<P, T> {
   private async processJob (job: JobInstance, callback: (err: Error) => void) {
     const jobHandler = this.jobHandlers[job.handlerName]
     if (jobHandler === undefined) {
-      logger.error('Unknown job handler for job %s.', job.handlerName)
-      return callback(null)
+      const errorString = 'Unknown job handler ' + job.handlerName + ' for job ' + job.id
+      logger.error(errorString)
+
+      const error = new Error(errorString)
+      await this.onJobError(jobHandler, job, error)
+      return callback(error)
     }
 
     logger.info('Processing job %d with handler %s.', job.id, job.handlerName)
@@ -103,7 +108,7 @@ class JobScheduler<P, T> {
       }
     }
 
-    callback(null)
+    return callback(null)
   }
 
   private async onJobError (jobHandler: JobHandler<P, T>, job: JobInstance, err: Error) {
@@ -111,7 +116,7 @@ class JobScheduler<P, T> {
 
     try {
       await job.save()
-      await jobHandler.onError(err, job.id)
+      if (jobHandler) await jobHandler.onError(err, job.id)
     } catch (err) {
       this.cannotSaveJobError(err)
     }
diff --git a/server/middlewares/activitypub.ts b/server/middlewares/activitypub.ts
index 6cf8eea6f..bed2bfeab 100644
--- a/server/middlewares/activitypub.ts
+++ b/server/middlewares/activitypub.ts
@@ -1,12 +1,9 @@
-import { Request, Response, NextFunction } from 'express'
-
-import { database as db } from '../initializers'
-import {
-  logger,
-  getAccountFromWebfinger,
-  isSignatureVerified
-} from '../helpers'
+import { NextFunction, Request, Response, RequestHandler } from 'express'
 import { ActivityPubSignature } from '../../shared'
+import { isSignatureVerified, logger } from '../helpers'
+import { fetchRemoteAccountAndCreatePod } from '../helpers/activitypub'
+import { database as db, ACTIVITY_PUB_ACCEPT_HEADER } from '../initializers'
+import { each, eachSeries, waterfall } from 'async'
 
 async function checkSignature (req: Request, res: Response, next: NextFunction) {
   const signatureObject: ActivityPubSignature = req.body.signature
@@ -17,35 +14,40 @@ async function checkSignature (req: Request, res: Response, next: NextFunction)
 
   // We don't have this account in our database, fetch it on remote
   if (!account) {
-    account = await getAccountFromWebfinger(signatureObject.creator)
+    const accountResult = await fetchRemoteAccountAndCreatePod(signatureObject.creator)
 
-    if (!account) {
+    if (!accountResult) {
       return res.sendStatus(403)
     }
 
     // Save our new account in database
+    account = accountResult.account
     await account.save()
   }
 
   const verified = await isSignatureVerified(account, req.body)
   if (verified === false) return res.sendStatus(403)
 
-  res.locals.signature.account = account
+  res.locals.signature = {
+    account
+  }
 
   return next()
 }
 
-function executeIfActivityPub (fun: any | any[]) {
+function executeIfActivityPub (fun: RequestHandler | RequestHandler[]) {
   return (req: Request, res: Response, next: NextFunction) => {
-    if (req.header('Accept') !== 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') {
+    if (req.header('Accept') !== ACTIVITY_PUB_ACCEPT_HEADER) {
       return next()
     }
 
     if (Array.isArray(fun) === true) {
-      fun[0](req, res, next) // FIXME: doesn't work
+      return eachSeries(fun as RequestHandler[], (f, cb) => {
+        f(req, res, cb)
+      }, next)
     }
 
-    return fun(req, res, next)
+    return (fun as RequestHandler)(req, res, next)
   }
 }
 
diff --git a/server/middlewares/validators/account.ts b/server/middlewares/validators/account.ts
index 3ccf2ea21..58eeed3cc 100644
--- a/server/middlewares/validators/account.ts
+++ b/server/middlewares/validators/account.ts
@@ -8,13 +8,13 @@ import {
   isUserVideoQuotaValid,
   logger
 } from '../../helpers'
-import { isAccountNameWithHostValid } from '../../helpers/custom-validators/video-accounts'
+import { isAccountNameValid } from '../../helpers/custom-validators/accounts'
 import { database as db } from '../../initializers/database'
 import { AccountInstance } from '../../models'
 import { checkErrors } from './utils'
 
 const localAccountValidator = [
-  param('nameWithHost').custom(isAccountNameWithHostValid).withMessage('Should have a valid account with domain name (myuser@domain.tld)'),
+  param('name').custom(isAccountNameValid).withMessage('Should have a valid account name'),
 
   (req: express.Request, res: express.Response, next: express.NextFunction) => {
     logger.debug('Checking localAccountValidator parameters', { parameters: req.params })
@@ -33,10 +33,8 @@ export {
 
 // ---------------------------------------------------------------------------
 
-function checkLocalAccountExists (nameWithHost: string, res: express.Response, callback: (err: Error, account: AccountInstance) => void) {
-  const [ name, host ] = nameWithHost.split('@')
-
-  db.Account.loadLocalAccountByNameAndPod(name, host)
+function checkLocalAccountExists (name: string, res: express.Response, callback: (err: Error, account: AccountInstance) => void) {
+  db.Account.loadLocalByName(name)
     .then(account => {
       if (!account) {
         return res.status(404)
diff --git a/server/middlewares/validators/activitypub/activity.ts b/server/middlewares/validators/activitypub/activity.ts
index 78a6d1444..0de8b2d85 100644
--- a/server/middlewares/validators/activitypub/activity.ts
+++ b/server/middlewares/validators/activitypub/activity.ts
@@ -1,11 +1,10 @@
-import { body } from 'express-validator/check'
 import * as express from 'express'
-
-import { logger, isRootActivityValid } from '../../../helpers'
+import { body } from 'express-validator/check'
+import { isRootActivityValid, logger } from '../../../helpers'
 import { checkErrors } from '../utils'
 
 const activityPubValidator = [
-  body('data').custom(isRootActivityValid),
+  body('').custom((value, { req }) => isRootActivityValid(req.body)),
 
   (req: express.Request, res: express.Response, next: express.NextFunction) => {
     logger.debug('Checking activity pub parameters', { parameters: req.body })
diff --git a/server/middlewares/validators/index.ts b/server/middlewares/validators/index.ts
index 46c00d679..92a4bad28 100644
--- a/server/middlewares/validators/index.ts
+++ b/server/middlewares/validators/index.ts
@@ -8,3 +8,4 @@ export * from './users'
 export * from './videos'
 export * from './video-blacklist'
 export * from './video-channels'
+export * from './webfinger'
diff --git a/server/middlewares/validators/webfinger.ts b/server/middlewares/validators/webfinger.ts
new file mode 100644
index 000000000..068e03ad7
--- /dev/null
+++ b/server/middlewares/validators/webfinger.ts
@@ -0,0 +1,42 @@
+import { query } from 'express-validator/check'
+import * as express from 'express'
+
+import { checkErrors } from './utils'
+import { logger, isWebfingerResourceValid } from '../../helpers'
+import { database as db } from '../../initializers'
+
+const webfingerValidator = [
+  query('resource').custom(isWebfingerResourceValid).withMessage('Should have a valid webfinger resource'),
+
+  (req: express.Request, res: express.Response, next: express.NextFunction) => {
+    logger.debug('Checking webfinger parameters', { parameters: req.query })
+
+    checkErrors(req, res, () => {
+      // Remove 'acct:' from the beginning of the string
+      const nameWithHost = req.query.resource.substr(5)
+      const [ name, ] = nameWithHost.split('@')
+
+      db.Account.loadLocalByName(name)
+        .then(account => {
+          if (!account) {
+            return res.status(404)
+              .send({ error: 'Account not found' })
+              .end()
+          }
+
+          res.locals.account = account
+          return next()
+        })
+        .catch(err => {
+          logger.error('Error in webfinger validator.', err)
+          return res.sendStatus(500)
+        })
+    })
+  }
+]
+
+// ---------------------------------------------------------------------------
+
+export {
+  webfingerValidator
+}
diff --git a/server/models/account/account-follow.ts b/server/models/account/account-follow.ts
index e6abc893a..7c129ab9d 100644
--- a/server/models/account/account-follow.ts
+++ b/server/models/account/account-follow.ts
@@ -19,11 +19,13 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
     {
       indexes: [
         {
-          fields: [ 'accountId' ],
-          unique: true
+          fields: [ 'accountId' ]
+        },
+        {
+          fields: [ 'targetAccountId' ]
         },
         {
-          fields: [ 'targetAccountId' ],
+          fields: [ 'accountId', 'targetAccountId' ],
           unique: true
         }
       ]
@@ -31,7 +33,8 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
   )
 
   const classMethods = [
-    associate
+    associate,
+    loadByAccountAndTarget
   ]
   addMethodsToModel(AccountFollow, classMethods)
 
@@ -46,7 +49,7 @@ function associate (models) {
       name: 'accountId',
       allowNull: false
     },
-    as: 'followers',
+    as: 'accountFollowers',
     onDelete: 'CASCADE'
   })
 
@@ -55,7 +58,7 @@ function associate (models) {
       name: 'targetAccountId',
       allowNull: false
     },
-    as: 'following',
+    as: 'accountFollowing',
     onDelete: 'CASCADE'
   })
 }
diff --git a/server/models/account/account-interface.ts b/server/models/account/account-interface.ts
index 2468dc6e1..6fc36ae9d 100644
--- a/server/models/account/account-interface.ts
+++ b/server/models/account/account-interface.ts
@@ -12,7 +12,8 @@ export namespace AccountMethods {
   export type LoadByUUID = (uuid: 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 LoadLocalByName = (name: string) => Bluebird<AccountInstance>
+  export type LoadByNameAndHost = (name: string, host: string) => Bluebird<AccountInstance>
   export type ListOwned = () => Bluebird<AccountInstance[]>
   export type ListAcceptedFollowerUrlsForApi = (id: number, start: number, count?: number) => Promise< ResultList<string> >
   export type ListAcceptedFollowingUrlsForApi = (id: number, start: number, count?: number) => Promise< ResultList<string> >
@@ -34,7 +35,8 @@ export interface AccountClass {
   load: AccountMethods.Load
   loadByUUID: AccountMethods.LoadByUUID
   loadByUrl: AccountMethods.LoadByUrl
-  loadLocalAccountByNameAndPod: AccountMethods.LoadLocalAccountByNameAndPod
+  loadLocalByName: AccountMethods.LoadLocalByName
+  loadByNameAndHost: AccountMethods.LoadByNameAndHost
   listOwned: AccountMethods.ListOwned
   listAcceptedFollowerUrlsForApi: AccountMethods.ListAcceptedFollowerUrlsForApi
   listAcceptedFollowingUrlsForApi: AccountMethods.ListAcceptedFollowingUrlsForApi
diff --git a/server/models/account/account.ts b/server/models/account/account.ts
index cd6c822f1..d2293a939 100644
--- a/server/models/account/account.ts
+++ b/server/models/account/account.ts
@@ -31,7 +31,8 @@ let load: AccountMethods.Load
 let loadApplication: AccountMethods.LoadApplication
 let loadByUUID: AccountMethods.LoadByUUID
 let loadByUrl: AccountMethods.LoadByUrl
-let loadLocalAccountByNameAndPod: AccountMethods.LoadLocalAccountByNameAndPod
+let loadLocalByName: AccountMethods.LoadLocalByName
+let loadByNameAndHost: AccountMethods.LoadByNameAndHost
 let listOwned: AccountMethods.ListOwned
 let listAcceptedFollowerUrlsForApi: AccountMethods.ListAcceptedFollowerUrlsForApi
 let listAcceptedFollowingUrlsForApi: AccountMethods.ListAcceptedFollowingUrlsForApi
@@ -88,7 +89,7 @@ export default function defineAccount (sequelize: Sequelize.Sequelize, DataTypes
       },
       privateKey: {
         type: DataTypes.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.PRIVATE_KEY.max),
-        allowNull: false,
+        allowNull: true,
         validate: {
           privateKeyValid: value => {
             const res = isAccountPrivateKeyValid(value)
@@ -199,7 +200,8 @@ export default function defineAccount (sequelize: Sequelize.Sequelize, DataTypes
     load,
     loadByUUID,
     loadByUrl,
-    loadLocalAccountByNameAndPod,
+    loadLocalByName,
+    loadByNameAndHost,
     listOwned,
     listAcceptedFollowerUrlsForApi,
     listAcceptedFollowingUrlsForApi,
@@ -330,6 +332,8 @@ getFollowerSharedInboxUrls = function (this: AccountInstance) {
     include: [
       {
         model: Account['sequelize'].models.AccountFollow,
+        required: true,
+        as: 'followers',
         where: {
           targetAccountId: this.id
         }
@@ -387,7 +391,7 @@ listFollowingForApi = function (id: number, start: number, count: number, sort:
         include: [
           {
             model: Account['sequelize'].models.Account,
-            as: 'following',
+            as: 'accountFollowing',
             required: true,
             include: [ Account['sequelize'].models.Pod ]
           }
@@ -418,7 +422,7 @@ listFollowersForApi = function (id: number, start: number, count: number, sort:
         include: [
           {
             model: Account['sequelize'].models.Account,
-            as: 'followers',
+            as: 'accountFollowers',
             required: true,
             include: [ Account['sequelize'].models.Pod ]
           }
@@ -439,7 +443,7 @@ loadApplication = function () {
   return Account.findOne({
     include: [
       {
-        model: Account['sequelize'].model.Application,
+        model: Account['sequelize'].models.Application,
         required: true
       }
     ]
@@ -460,17 +464,37 @@ loadByUUID = function (uuid: string) {
   return Account.findOne(query)
 }
 
-loadLocalAccountByNameAndPod = function (name: string, host: string) {
+loadLocalByName = function (name: string) {
   const query: Sequelize.FindOptions<AccountAttributes> = {
     where: {
       name,
-      userId: {
-        [Sequelize.Op.ne]: null
-      }
+      [Sequelize.Op.or]: [
+        {
+          userId: {
+            [Sequelize.Op.ne]: null
+          }
+        },
+        {
+          applicationId: {
+            [Sequelize.Op.ne]: null
+          }
+        }
+      ]
+    }
+  }
+
+  return Account.findOne(query)
+}
+
+loadByNameAndHost = function (name: string, host: string) {
+  const query: Sequelize.FindOptions<AccountAttributes> = {
+    where: {
+      name
     },
     include: [
       {
         model: Account['sequelize'].models.Pod,
+        required: true,
         where: {
           host
         }
diff --git a/server/models/application/application-interface.ts b/server/models/application/application-interface.ts
index 33254ba2d..2c391dba3 100644
--- a/server/models/application/application-interface.ts
+++ b/server/models/application/application-interface.ts
@@ -1,18 +1,21 @@
 import * as Sequelize from 'sequelize'
-import * as Promise from 'bluebird'
+import * as Bluebird from 'bluebird'
 
 export namespace ApplicationMethods {
-  export type LoadMigrationVersion = () => Promise<number>
+  export type LoadMigrationVersion = () => Bluebird<number>
 
   export type UpdateMigrationVersion = (
     newVersion: number,
     transaction: Sequelize.Transaction
-  ) => Promise<[ number, ApplicationInstance[] ]>
+  ) => Bluebird<[ number, ApplicationInstance[] ]>
+
+  export type CountTotal = () => Bluebird<number>
 }
 
 export interface ApplicationClass {
   loadMigrationVersion: ApplicationMethods.LoadMigrationVersion
   updateMigrationVersion: ApplicationMethods.UpdateMigrationVersion
+  countTotal: ApplicationMethods.CountTotal
 }
 
 export interface ApplicationAttributes {
diff --git a/server/models/application/application.ts b/server/models/application/application.ts
index 507b7a843..8ba40a895 100644
--- a/server/models/application/application.ts
+++ b/server/models/application/application.ts
@@ -11,6 +11,7 @@ import {
 let Application: Sequelize.Model<ApplicationInstance, ApplicationAttributes>
 let loadMigrationVersion: ApplicationMethods.LoadMigrationVersion
 let updateMigrationVersion: ApplicationMethods.UpdateMigrationVersion
+let countTotal: ApplicationMethods.CountTotal
 
 export default function defineApplication (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
   Application = sequelize.define<ApplicationInstance, ApplicationAttributes>('Application',
@@ -26,7 +27,11 @@ export default function defineApplication (sequelize: Sequelize.Sequelize, DataT
     }
   )
 
-  const classMethods = [ loadMigrationVersion, updateMigrationVersion ]
+  const classMethods = [
+    countTotal,
+    loadMigrationVersion,
+    updateMigrationVersion
+  ]
   addMethodsToModel(Application, classMethods)
 
   return Application
@@ -34,6 +39,10 @@ export default function defineApplication (sequelize: Sequelize.Sequelize, DataT
 
 // ---------------------------------------------------------------------------
 
+countTotal = function () {
+  return this.count()
+}
+
 loadMigrationVersion = function () {
   const query = {
     attributes: [ 'migrationVersion' ]
diff --git a/server/models/job/job.ts b/server/models/job/job.ts
index ce1203e5a..c2d088090 100644
--- a/server/models/job/job.ts
+++ b/server/models/job/job.ts
@@ -10,7 +10,7 @@ import {
 
   JobMethods
 } from './job-interface'
-import { JobState } from '../../../shared/models/job.model'
+import { JobCategory, JobState } from '../../../shared/models/job.model'
 
 let Job: Sequelize.Model<JobInstance, JobAttributes>
 let listWithLimitByCategory: JobMethods.ListWithLimitByCategory
@@ -38,7 +38,7 @@ export default function defineJob (sequelize: Sequelize.Sequelize, DataTypes: Se
     {
       indexes: [
         {
-          fields: [ 'state' ]
+          fields: [ 'state', 'category' ]
         }
       ]
     }
@@ -52,14 +52,15 @@ export default function defineJob (sequelize: Sequelize.Sequelize, DataTypes: Se
 
 // ---------------------------------------------------------------------------
 
-listWithLimitByCategory = function (limit: number, state: JobState) {
+listWithLimitByCategory = function (limit: number, state: JobState, jobCategory: JobCategory) {
   const query = {
     order: [
       [ 'id', 'ASC' ]
     ],
     limit: limit,
     where: {
-      state
+      state,
+      category: jobCategory
     }
   }
 
-- 
cgit v1.2.3