Follow works

pull/128/head
Chocobozzz 2017-11-14 17:31:26 +01:00
parent e34c85e527
commit 350e31d6b6
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
39 changed files with 431 additions and 169 deletions

View File

@ -4,12 +4,12 @@
<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">

View File

@ -17,7 +17,7 @@ export class FriendListComponent extends RestTable implements OnInit {
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 (

View File

@ -23,7 +23,7 @@ export class FriendService {
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))
}
@ -33,7 +33,7 @@ export class FriendService {
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))
}

View File

@ -47,7 +47,7 @@ db.init(false).then(() => onDatabaseInitDone())
// ----------- 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 -----------
@ -115,6 +115,9 @@ app.use(apiRoute, apiRouter)
// Services (oembed...)
app.use('/services', servicesRouter)
app.use('/', webfingerRouter)
app.use('/', activityPubRouter)
// Client files
app.use('/', clientsRouter)

View File

@ -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))
)

View File

@ -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)

View File

@ -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
}

View File

@ -1,19 +1,19 @@
import * as Bluebird from 'bluebird'
import * as express from 'express'
import { UserRight } from '../../../shared/models/users/user-right.enum'
import { getFormattedObjects } from '../../helpers'
import { getOrCreateAccount } from '../../helpers/activitypub'
import { logger } from '../../helpers/logger'
import { getApplicationAccount } from '../../helpers/utils'
import { REMOTE_SCHEME } from '../../initializers/constants'
import { getAccountFromWebfinger } from '../../helpers/webfinger'
import { SERVER_ACCOUNT_NAME } from '../../initializers/constants'
import { database as db } from '../../initializers/database'
import { sendFollow } from '../../lib/activitypub/send-request'
import { asyncMiddleware, paginationValidator, setFollowersSort, setPagination } from '../../middlewares'
import { authenticate } from '../../middlewares/oauth'
import { setBodyHostsPort } from '../../middlewares/pods'
import { setFollowingSort } from '../../middlewares/sort'
import { ensureUserHasRight } from '../../middlewares/user-right'
import { followValidator } from '../../middlewares/validators/pods'
import { followersSortValidator, followingSortValidator } from '../../middlewares/validators/sort'
import { sendFollow } from '../../lib/activitypub/send-request'
import { authenticate } from '../../middlewares/oauth'
import { ensureUserHasRight } from '../../middlewares/user-right'
import { UserRight } from '../../../shared/models/users/user-right.enum'
const podsRouter = express.Router()
@ -67,22 +67,43 @@ async function follow (req: express.Request, res: express.Response, next: expres
const hosts = req.body.hosts as string[]
const fromAccount = await getApplicationAccount()
const tasks: Bluebird<any>[] = []
const tasks: Promise<any>[] = []
const accountName = SERVER_ACCOUNT_NAME
for (const host of hosts) {
const url = REMOTE_SCHEME.HTTP + '://' + host
const targetAccount = await getOrCreateAccount(url)
// We process each host in a specific transaction
// First, we add the follow request in the database
// Then we send the follow request to other account
const p = db.sequelize.transaction(async t => {
return db.AccountFollow.create({
accountId: fromAccount.id,
targetAccountId: targetAccount.id,
state: 'pending'
const p = loadLocalOrGetAccountFromWebfinger(accountName, host)
.then(accountResult => {
let targetAccount = accountResult.account
return db.sequelize.transaction(async t => {
if (accountResult.loadedFromDB === false) {
targetAccount = await targetAccount.save({ transaction: t })
}
const [ accountFollow ] = await db.AccountFollow.findOrCreate({
where: {
accountId: fromAccount.id,
targetAccountId: targetAccount.id
},
defaults: {
state: 'pending',
accountId: fromAccount.id,
targetAccountId: targetAccount.id
},
transaction: t
})
// Send a notification to remote server
if (accountFollow.state === 'pending') {
await sendFollow(fromAccount, targetAccount, t)
}
})
})
.then(() => sendFollow(fromAccount, targetAccount, t))
})
.catch(err => logger.warn('Cannot follow server %s.', `${accountName}@${host}`, err))
tasks.push(p)
}
@ -91,3 +112,16 @@ async function follow (req: express.Request, res: express.Response, next: expres
return res.status(204).end()
}
async function loadLocalOrGetAccountFromWebfinger (name: string, host: string) {
let loadedFromDB = true
let account = await db.Account.loadByNameAndHost(name, host)
if (!account) {
const nameWithDomain = name + '@' + host
account = await getAccountFromWebfinger(nameWithDomain)
loadedFromDB = false
}
return { account, loadedFromDB }
}

View File

@ -1,4 +1,6 @@
export * from './activitypub'
export * from './static'
export * from './client'
export * from './services'
export * from './api'
export * from './webfinger'

View File

@ -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()
}

View File

@ -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
}

View File

@ -10,14 +10,14 @@ import { logger } from '../logger'
import { isUserUsernameValid } from './users'
import { isHostValid } from './pods'
function isVideoAccountNameValid (value: string) {
function isAccountNameValid (value: string) {
return isUserUsernameValid(value)
}
function isAccountNameWithHostValid (value: string) {
const [ name, host ] = value.split('@')
return isVideoAccountNameValid(name) && isHostValid(host)
return isAccountNameValid(name) && isHostValid(host)
}
function checkVideoAccountExists (id: string, res: express.Response, callback: () => void) {
@ -38,10 +38,10 @@ function checkVideoAccountExists (id: string, res: express.Response, callback: (
res.locals.account = account
callback()
})
.catch(err => {
logger.error('Error in video account request validator.', err)
return res.sendStatus(500)
})
.catch(err => {
logger.error('Error in video account request validator.', err)
return res.sendStatus(500)
})
}
// ---------------------------------------------------------------------------
@ -49,5 +49,5 @@ function checkVideoAccountExists (id: string, res: express.Response, callback: (
export {
checkVideoAccountExists,
isAccountNameWithHostValid,
isVideoAccountNameValid
isAccountNameValid
}

View File

@ -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
}

View File

@ -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)
}
// ---------------------------------------------------------------------------

View File

@ -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 {

View File

@ -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
}
// ---------------------------------------------------------------------------

View File

@ -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'

View File

@ -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
}

View File

@ -12,17 +12,20 @@ const webfinger = new WebFinger({
request_timeout: 3000
})
async function getAccountFromWebfinger (url: string) {
const webfingerData: WebFingerData = await webfingerLookup(url)
async function getAccountFromWebfinger (nameWithHost: string) {
const webfingerData: WebFingerData = await webfingerLookup(nameWithHost)
if (Array.isArray(webfingerData.links) === false) return undefined
if (Array.isArray(webfingerData.links) === false) throw new Error('WebFinger links is not an array.')
const selfLink = webfingerData.links.find(l => l.rel === 'self')
if (selfLink === undefined || isActivityPubUrlValid(selfLink.href) === false) return undefined
if (selfLink === undefined || isActivityPubUrlValid(selfLink.href) === false) {
throw new Error('Cannot find self link or href is not a valid URL.')
}
const { account } = await fetchRemoteAccountAndCreatePod(selfLink.href)
const res = await fetchRemoteAccountAndCreatePod(selfLink.href)
if (res === undefined) throw new Error('Cannot fetch and create pod of remote account ' + selfLink.href)
return account
return res.account
}
// ---------------------------------------------------------------------------
@ -33,12 +36,12 @@ export {
// ---------------------------------------------------------------------------
function webfingerLookup (url: string) {
function webfingerLookup (nameWithHost: string) {
return new Promise<WebFingerData>((res, rej) => {
webfinger.lookup(url, (err, p) => {
webfinger.lookup(nameWithHost, (err, p) => {
if (err) return rej(err)
return p
return res(p.object)
})
})
}

View File

@ -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
}

View File

@ -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,

View File

@ -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)
}

View File

@ -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)

View File

@ -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)

View File

@ -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
}

View File

@ -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
}

View File

@ -7,6 +7,7 @@ async function process (payload: HTTPRequestPayload, jobId: number) {
const uri = payload.uris[0]
const options = {
method: 'POST',
uri,
json: payload.body
}

View File

@ -4,6 +4,7 @@ import { JobCategory } from '../../../shared'
import { logger } from '../../helpers'
import { database as db, JOB_STATES, JOBS_FETCH_LIMIT_PER_CYCLE, JOBS_FETCHING_INTERVAL } from '../../initializers'
import { JobInstance } from '../../models'
import { error } from 'util'
export interface JobHandler<P, T> {
process (data: object, jobId: number): Promise<T>
@ -80,8 +81,12 @@ class JobScheduler<P, T> {
private async processJob (job: JobInstance, callback: (err: Error) => void) {
const jobHandler = this.jobHandlers[job.handlerName]
if (jobHandler === undefined) {
logger.error('Unknown job handler for job %s.', job.handlerName)
return callback(null)
const errorString = 'Unknown job handler ' + job.handlerName + ' for job ' + job.id
logger.error(errorString)
const error = new Error(errorString)
await this.onJobError(jobHandler, job, error)
return callback(error)
}
logger.info('Processing job %d with handler %s.', job.id, job.handlerName)
@ -103,7 +108,7 @@ class JobScheduler<P, T> {
}
}
callback(null)
return callback(null)
}
private async onJobError (jobHandler: JobHandler<P, T>, job: JobInstance, err: Error) {
@ -111,7 +116,7 @@ class JobScheduler<P, T> {
try {
await job.save()
await jobHandler.onError(err, job.id)
if (jobHandler) await jobHandler.onError(err, job.id)
} catch (err) {
this.cannotSaveJobError(err)
}

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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 })

View File

@ -8,3 +8,4 @@ export * from './users'
export * from './videos'
export * from './video-blacklist'
export * from './video-channels'
export * from './webfinger'

View File

@ -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
}

View File

@ -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'
})
}

View File

@ -12,7 +12,8 @@ export namespace AccountMethods {
export type LoadByUUID = (uuid: string) => Bluebird<AccountInstance>
export type LoadByUrl = (url: string, transaction?: Sequelize.Transaction) => Bluebird<AccountInstance>
export type LoadAccountByPodAndUUID = (uuid: string, podId: number, transaction: Sequelize.Transaction) => Bluebird<AccountInstance>
export type LoadLocalAccountByNameAndPod = (name: string, host: string) => Bluebird<AccountInstance>
export type LoadLocalByName = (name: string) => Bluebird<AccountInstance>
export type LoadByNameAndHost = (name: string, host: string) => Bluebird<AccountInstance>
export type ListOwned = () => Bluebird<AccountInstance[]>
export type ListAcceptedFollowerUrlsForApi = (id: number, start: number, count?: number) => Promise< ResultList<string> >
export type ListAcceptedFollowingUrlsForApi = (id: number, start: number, count?: number) => Promise< ResultList<string> >
@ -34,7 +35,8 @@ export interface AccountClass {
load: AccountMethods.Load
loadByUUID: AccountMethods.LoadByUUID
loadByUrl: AccountMethods.LoadByUrl
loadLocalAccountByNameAndPod: AccountMethods.LoadLocalAccountByNameAndPod
loadLocalByName: AccountMethods.LoadLocalByName
loadByNameAndHost: AccountMethods.LoadByNameAndHost
listOwned: AccountMethods.ListOwned
listAcceptedFollowerUrlsForApi: AccountMethods.ListAcceptedFollowerUrlsForApi
listAcceptedFollowingUrlsForApi: AccountMethods.ListAcceptedFollowingUrlsForApi

View File

@ -31,7 +31,8 @@ let load: AccountMethods.Load
let loadApplication: AccountMethods.LoadApplication
let loadByUUID: AccountMethods.LoadByUUID
let loadByUrl: AccountMethods.LoadByUrl
let loadLocalAccountByNameAndPod: AccountMethods.LoadLocalAccountByNameAndPod
let loadLocalByName: AccountMethods.LoadLocalByName
let loadByNameAndHost: AccountMethods.LoadByNameAndHost
let listOwned: AccountMethods.ListOwned
let listAcceptedFollowerUrlsForApi: AccountMethods.ListAcceptedFollowerUrlsForApi
let listAcceptedFollowingUrlsForApi: AccountMethods.ListAcceptedFollowingUrlsForApi
@ -88,7 +89,7 @@ export default function defineAccount (sequelize: Sequelize.Sequelize, DataTypes
},
privateKey: {
type: DataTypes.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.PRIVATE_KEY.max),
allowNull: false,
allowNull: true,
validate: {
privateKeyValid: value => {
const res = isAccountPrivateKeyValid(value)
@ -199,7 +200,8 @@ export default function defineAccount (sequelize: Sequelize.Sequelize, DataTypes
load,
loadByUUID,
loadByUrl,
loadLocalAccountByNameAndPod,
loadLocalByName,
loadByNameAndHost,
listOwned,
listAcceptedFollowerUrlsForApi,
listAcceptedFollowingUrlsForApi,
@ -330,6 +332,8 @@ getFollowerSharedInboxUrls = function (this: AccountInstance) {
include: [
{
model: Account['sequelize'].models.AccountFollow,
required: true,
as: 'followers',
where: {
targetAccountId: this.id
}
@ -387,7 +391,7 @@ listFollowingForApi = function (id: number, start: number, count: number, sort:
include: [
{
model: Account['sequelize'].models.Account,
as: 'following',
as: 'accountFollowing',
required: true,
include: [ Account['sequelize'].models.Pod ]
}
@ -418,7 +422,7 @@ listFollowersForApi = function (id: number, start: number, count: number, sort:
include: [
{
model: Account['sequelize'].models.Account,
as: 'followers',
as: 'accountFollowers',
required: true,
include: [ Account['sequelize'].models.Pod ]
}
@ -439,7 +443,7 @@ loadApplication = function () {
return Account.findOne({
include: [
{
model: Account['sequelize'].model.Application,
model: Account['sequelize'].models.Application,
required: true
}
]
@ -460,17 +464,37 @@ loadByUUID = function (uuid: string) {
return Account.findOne(query)
}
loadLocalAccountByNameAndPod = function (name: string, host: string) {
loadLocalByName = function (name: string) {
const query: Sequelize.FindOptions<AccountAttributes> = {
where: {
name,
userId: {
[Sequelize.Op.ne]: null
}
[Sequelize.Op.or]: [
{
userId: {
[Sequelize.Op.ne]: null
}
},
{
applicationId: {
[Sequelize.Op.ne]: null
}
}
]
}
}
return Account.findOne(query)
}
loadByNameAndHost = function (name: string, host: string) {
const query: Sequelize.FindOptions<AccountAttributes> = {
where: {
name
},
include: [
{
model: Account['sequelize'].models.Pod,
required: true,
where: {
host
}

View File

@ -1,18 +1,21 @@
import * as Sequelize from 'sequelize'
import * as Promise from 'bluebird'
import * as Bluebird from 'bluebird'
export namespace ApplicationMethods {
export type LoadMigrationVersion = () => Promise<number>
export type LoadMigrationVersion = () => Bluebird<number>
export type UpdateMigrationVersion = (
newVersion: number,
transaction: Sequelize.Transaction
) => Promise<[ number, ApplicationInstance[] ]>
) => Bluebird<[ number, ApplicationInstance[] ]>
export type CountTotal = () => Bluebird<number>
}
export interface ApplicationClass {
loadMigrationVersion: ApplicationMethods.LoadMigrationVersion
updateMigrationVersion: ApplicationMethods.UpdateMigrationVersion
countTotal: ApplicationMethods.CountTotal
}
export interface ApplicationAttributes {

View File

@ -11,6 +11,7 @@ import {
let Application: Sequelize.Model<ApplicationInstance, ApplicationAttributes>
let loadMigrationVersion: ApplicationMethods.LoadMigrationVersion
let updateMigrationVersion: ApplicationMethods.UpdateMigrationVersion
let countTotal: ApplicationMethods.CountTotal
export default function defineApplication (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
Application = sequelize.define<ApplicationInstance, ApplicationAttributes>('Application',
@ -26,7 +27,11 @@ export default function defineApplication (sequelize: Sequelize.Sequelize, DataT
}
)
const classMethods = [ loadMigrationVersion, updateMigrationVersion ]
const classMethods = [
countTotal,
loadMigrationVersion,
updateMigrationVersion
]
addMethodsToModel(Application, classMethods)
return Application
@ -34,6 +39,10 @@ export default function defineApplication (sequelize: Sequelize.Sequelize, DataT
// ---------------------------------------------------------------------------
countTotal = function () {
return this.count()
}
loadMigrationVersion = function () {
const query = {
attributes: [ 'migrationVersion' ]

View File

@ -10,7 +10,7 @@ import {
JobMethods
} from './job-interface'
import { JobState } from '../../../shared/models/job.model'
import { JobCategory, JobState } from '../../../shared/models/job.model'
let Job: Sequelize.Model<JobInstance, JobAttributes>
let listWithLimitByCategory: JobMethods.ListWithLimitByCategory
@ -38,7 +38,7 @@ export default function defineJob (sequelize: Sequelize.Sequelize, DataTypes: Se
{
indexes: [
{
fields: [ 'state' ]
fields: [ 'state', 'category' ]
}
]
}
@ -52,14 +52,15 @@ export default function defineJob (sequelize: Sequelize.Sequelize, DataTypes: Se
// ---------------------------------------------------------------------------
listWithLimitByCategory = function (limit: number, state: JobState) {
listWithLimitByCategory = function (limit: number, state: JobState, jobCategory: JobCategory) {
const query = {
order: [
[ 'id', 'ASC' ]
],
limit: limit,
where: {
state
state,
category: jobCategory
}
}