<p-dataTable
[value]="friends" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
- sortField="id" (onLazyLoad)="loadLazy($event)"
+ sortField="createdAt" (onLazyLoad)="loadLazy($event)"
>
- <p-column field="id" header="ID" [sortable]="true"></p-column>
- <p-column field="host" header="Host" [sortable]="true"></p-column>
+ <p-column field="id" header="ID"></p-column>
+ <p-column field="host" header="Host"></p-column>
<p-column field="email" header="Email"></p-column>
- <p-column field="score" header="Score" [sortable]="true"></p-column>
+ <p-column field="score" header="Score"></p-column>
<p-column field="createdAt" header="Created date" [sortable]="true"></p-column>
<p-column header="Delete" styleClass="action-cell">
<ng-template pTemplate="body" let-pod="rowData">
friends: Pod[] = []
totalRecords = 0
rowsPerPage = 10
- sort: SortMeta = { field: 'id', order: 1 }
+ sort: SortMeta = { field: 'createdAt', order: 1 }
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
constructor (
let params = new HttpParams()
params = this.restService.addRestGetParams(params, pagination, sort)
- return this.authHttp.get<ResultList<Account>>(API_URL + '/followers', { params })
+ return this.authHttp.get<ResultList<Account>>(API_URL + '/api/v1/pods/followers', { params })
.map(res => this.restExtractor.convertResultListDateToHuman(res))
.catch(res => this.restExtractor.handleError(res))
}
hosts: notEmptyHosts
}
- return this.authHttp.post(API_URL + '/follow', body)
+ return this.authHttp.post(API_URL + '/api/v1/pods/follow', body)
.map(this.restExtractor.extractDataBool)
.catch(res => this.restExtractor.handleError(res))
}
// ----------- PeerTube modules -----------
import { migrate, installApplication } from './server/initializers'
import { httpRequestJobScheduler, transcodingJobScheduler, VideosPreviewCache } from './server/lib'
-import { apiRouter, clientsRouter, staticRouter, servicesRouter } from './server/controllers'
+import { apiRouter, clientsRouter, staticRouter, servicesRouter, webfingerRouter, activityPubRouter } from './server/controllers'
// ----------- Command line -----------
// Services (oembed...)
app.use('/services', servicesRouter)
+app.use('/', webfingerRouter)
+app.use('/', activityPubRouter)
+
// Client files
app.use('/', clientsRouter)
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))
)
asyncMiddleware(inboxController)
)
-inboxRouter.post('/:nameWithHost/inbox',
+inboxRouter.post('/account/:name/inbox',
signatureValidator,
asyncMiddleware(checkSignature),
localAccountValidator,
}
// 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)
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
}
-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()
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)
}
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 }
+}
+export * from './activitypub'
export * from './static'
export * from './client'
export * from './services'
export * from './api'
+export * from './webfinger'
--- /dev/null
+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()
+}
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'
// 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()
}
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)
host: accountHost
}
}
- const pod = await db.Pod.findOrCreate(podOptions)
+ const [ pod ] = await db.Pod.findOrCreate(podOptions)
+ account.set('podId', pod.id)
return { account, pod }
}
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
}
--- /dev/null
+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
+}
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)
return isActivityPubUrlValid(outbox)
}
-function isAccountNameValid (name: string) {
- return isUserUsernameValid(name)
-}
-
function isAccountPreferredUsernameValid (preferredUsername: string) {
return isAccountNameValid(preferredUsername)
}
isAccountPreferredUsernameValid(remoteAccount.preferredUsername) &&
isAccountUrlValid(remoteAccount.url) &&
isAccountPublicKeyObjectValid(remoteAccount.publicKey) &&
- isAccountEndpointsObjectValid(remoteAccount.endpoint)
+ isAccountEndpointsObjectValid(remoteAccount.endpoints)
}
function isAccountFollowingCountValid (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 {
isRemoteAccountValid,
isAccountFollowingCountValid,
isAccountFollowersCountValid,
- isAccountNameValid
+ isAccountNameValid,
+ isAccountFollowActivityValid,
+ isAccountAcceptActivityValid,
+ isAccountDeleteActivityValid
}
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) {
Array.isArray(activity.items)
) ||
(
- validator.isURL(activity.id) &&
- validator.isURL(activity.actor)
+ isActivityPubUrlValid(activity.id) &&
+ isActivityPubUrlValid(activity.actor)
)
}
return isVideoTorrentAddActivityValid(activity) ||
isVideoChannelCreateActivityValid(activity) ||
isVideoTorrentUpdateActivityValid(activity) ||
- isVideoChannelUpdateActivityValid(activity)
+ isVideoChannelUpdateActivityValid(activity) ||
+ isVideoTorrentDeleteActivityValid(activity) ||
+ isVideoChannelDeleteActivityValid(activity) ||
+ isAccountDeleteActivityValid(activity) ||
+ isAccountFollowActivityValid(activity) ||
+ isAccountAcceptActivityValid(activity)
}
// ---------------------------------------------------------------------------
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 {
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') &&
isVideoTorrentObjectValid(activity.object)
}
+function isVideoTorrentDeleteActivityValid (activity: any) {
+ return isBaseActivityValid(activity, 'Delete')
+}
+
function isVideoTorrentObjectValid (video: any) {
return video.type === 'Video' &&
isVideoNameValid(video.name) &&
isVideoChannelObjectValid(activity.object)
}
+function isVideoChannelDeleteActivityValid (activity: any) {
+ return isBaseActivityValid(activity, 'Delete')
+}
+
function isVideoChannelObjectValid (videoChannel: any) {
return videoChannel.type === 'VideoChannel' &&
isVideoChannelNameValid(videoChannel.name) &&
isVideoTorrentAddActivityValid,
isVideoChannelCreateActivityValid,
isVideoTorrentUpdateActivityValid,
- isVideoChannelUpdateActivityValid
+ isVideoChannelUpdateActivityValid,
+ isVideoChannelDeleteActivityValid,
+ isVideoTorrentDeleteActivityValid
}
// ---------------------------------------------------------------------------
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'
+++ /dev/null
-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
-}
--- /dev/null
+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
+}
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
}
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
-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)
})
})
}
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 () {
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 {
checkFFmpeg,
checkMissedConfig,
clientsExist,
- usersExist
+ usersExist,
+ applicationExist
}
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: [
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,
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'
}
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)
}
// 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)
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)
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)
}
async function unicastTo (data: any, toAccount: AccountInstance, t: Sequelize.Transaction) {
const jobPayload = {
- uris: [ toAccount.url ],
+ uris: [ toAccount.inboxUrl ],
body: data
}
logger.info('Processing broadcast in job %d.', jobId)
const options = {
+ method: 'POST',
uri: '',
json: payload.body
}
const uri = payload.uris[0]
const options = {
+ method: 'POST',
uri,
json: payload.body
}
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>
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)
}
}
- callback(null)
+ return callback(null)
}
private async onJobError (jobHandler: JobHandler<P, T>, job: JobInstance, err: Error) {
try {
await job.save()
- await jobHandler.onError(err, job.id)
+ if (jobHandler) await jobHandler.onError(err, job.id)
} catch (err) {
this.cannotSaveJobError(err)
}
-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
// 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)
}
}
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 })
// ---------------------------------------------------------------------------
-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)
-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 })
export * from './videos'
export * from './video-blacklist'
export * from './video-channels'
+export * from './webfinger'
--- /dev/null
+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
+}
{
indexes: [
{
- fields: [ 'accountId' ],
- unique: true
+ fields: [ 'accountId' ]
+ },
+ {
+ fields: [ 'targetAccountId' ]
},
{
- fields: [ 'targetAccountId' ],
+ fields: [ 'accountId', 'targetAccountId' ],
unique: true
}
]
)
const classMethods = [
- associate
+ associate,
+ loadByAccountAndTarget
]
addMethodsToModel(AccountFollow, classMethods)
name: 'accountId',
allowNull: false
},
- as: 'followers',
+ as: 'accountFollowers',
onDelete: 'CASCADE'
})
name: 'targetAccountId',
allowNull: false
},
- as: 'following',
+ as: 'accountFollowing',
onDelete: 'CASCADE'
})
}
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> >
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
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
},
privateKey: {
type: DataTypes.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.PRIVATE_KEY.max),
- allowNull: false,
+ allowNull: true,
validate: {
privateKeyValid: value => {
const res = isAccountPrivateKeyValid(value)
load,
loadByUUID,
loadByUrl,
- loadLocalAccountByNameAndPod,
+ loadLocalByName,
+ loadByNameAndHost,
listOwned,
listAcceptedFollowerUrlsForApi,
listAcceptedFollowingUrlsForApi,
include: [
{
model: Account['sequelize'].models.AccountFollow,
+ required: true,
+ as: 'followers',
where: {
targetAccountId: this.id
}
include: [
{
model: Account['sequelize'].models.Account,
- as: 'following',
+ as: 'accountFollowing',
required: true,
include: [ Account['sequelize'].models.Pod ]
}
include: [
{
model: Account['sequelize'].models.Account,
- as: 'followers',
+ as: 'accountFollowers',
required: true,
include: [ Account['sequelize'].models.Pod ]
}
return Account.findOne({
include: [
{
- model: Account['sequelize'].model.Application,
+ model: Account['sequelize'].models.Application,
required: true
}
]
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
}
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 {
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',
}
)
- const classMethods = [ loadMigrationVersion, updateMigrationVersion ]
+ const classMethods = [
+ countTotal,
+ loadMigrationVersion,
+ updateMigrationVersion
+ ]
addMethodsToModel(Application, classMethods)
return Application
// ---------------------------------------------------------------------------
+countTotal = function () {
+ return this.count()
+}
+
loadMigrationVersion = function () {
const query = {
attributes: [ 'migrationVersion' ]
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
{
indexes: [
{
- fields: [ 'state' ]
+ fields: [ 'state', 'category' ]
}
]
}
// ---------------------------------------------------------------------------
-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
}
}