From 350e31d6b64e4973dfa5e9f7b46841cb09aeb1ad Mon Sep 17 00:00:00 2001 From: Chocobozzz 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[] = [] + const tasks: Promise[] = [] + 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 + 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 - 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((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 { process (data: object, jobId: number): Promise @@ -80,8 +81,12 @@ class JobScheduler { 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 { } } - callback(null) + return callback(null) } private async onJobError (jobHandler: JobHandler, job: JobInstance, err: Error) { @@ -111,7 +116,7 @@ class JobScheduler { 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 export type LoadByUrl = (url: string, transaction?: Sequelize.Transaction) => Bluebird export type LoadAccountByPodAndUUID = (uuid: string, podId: number, transaction: Sequelize.Transaction) => Bluebird - export type LoadLocalAccountByNameAndPod = (name: string, host: string) => Bluebird + export type LoadLocalByName = (name: string) => Bluebird + export type LoadByNameAndHost = (name: string, host: string) => Bluebird export type ListOwned = () => Bluebird export type ListAcceptedFollowerUrlsForApi = (id: number, start: number, count?: number) => Promise< ResultList > export type ListAcceptedFollowingUrlsForApi = (id: number, start: number, count?: number) => Promise< ResultList > @@ -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 = { 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 = { + 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 + export type LoadMigrationVersion = () => Bluebird export type UpdateMigrationVersion = ( newVersion: number, transaction: Sequelize.Transaction - ) => Promise<[ number, ApplicationInstance[] ]> + ) => Bluebird<[ number, ApplicationInstance[] ]> + + export type CountTotal = () => Bluebird } 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 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('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 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