Follow works

This commit is contained in:
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 <p-dataTable
[value]="friends" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage" [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="id" header="ID"></p-column>
<p-column field="host" header="Host" [sortable]="true"></p-column> <p-column field="host" header="Host"></p-column>
<p-column field="email" header="Email"></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 field="createdAt" header="Created date" [sortable]="true"></p-column>
<p-column header="Delete" styleClass="action-cell"> <p-column header="Delete" styleClass="action-cell">
<ng-template pTemplate="body" let-pod="rowData"> <ng-template pTemplate="body" let-pod="rowData">

View File

@ -17,7 +17,7 @@ export class FriendListComponent extends RestTable implements OnInit {
friends: Pod[] = [] friends: Pod[] = []
totalRecords = 0 totalRecords = 0
rowsPerPage = 10 rowsPerPage = 10
sort: SortMeta = { field: 'id', order: 1 } sort: SortMeta = { field: 'createdAt', order: 1 }
pagination: RestPagination = { count: this.rowsPerPage, start: 0 } pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
constructor ( constructor (

View File

@ -23,7 +23,7 @@ export class FriendService {
let params = new HttpParams() let params = new HttpParams()
params = this.restService.addRestGetParams(params, pagination, sort) 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)) .map(res => this.restExtractor.convertResultListDateToHuman(res))
.catch(res => this.restExtractor.handleError(res)) .catch(res => this.restExtractor.handleError(res))
} }
@ -33,7 +33,7 @@ export class FriendService {
hosts: notEmptyHosts 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) .map(this.restExtractor.extractDataBool)
.catch(res => this.restExtractor.handleError(res)) .catch(res => this.restExtractor.handleError(res))
} }

View File

@ -47,7 +47,7 @@ db.init(false).then(() => onDatabaseInitDone())
// ----------- PeerTube modules ----------- // ----------- PeerTube modules -----------
import { migrate, installApplication } from './server/initializers' import { migrate, installApplication } from './server/initializers'
import { httpRequestJobScheduler, transcodingJobScheduler, VideosPreviewCache } from './server/lib' 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 ----------- // ----------- Command line -----------
@ -115,6 +115,9 @@ app.use(apiRoute, apiRouter)
// Services (oembed...) // Services (oembed...)
app.use('/services', servicesRouter) app.use('/services', servicesRouter)
app.use('/', webfingerRouter)
app.use('/', activityPubRouter)
// Client files // Client files
app.use('/', clientsRouter) app.use('/', clientsRouter)

View File

@ -16,12 +16,12 @@ activityPubClientRouter.get('/account/:name',
executeIfActivityPub(asyncMiddleware(accountController)) executeIfActivityPub(asyncMiddleware(accountController))
) )
activityPubClientRouter.get('/account/:nameWithHost/followers', activityPubClientRouter.get('/account/:name/followers',
executeIfActivityPub(localAccountValidator), executeIfActivityPub(localAccountValidator),
executeIfActivityPub(asyncMiddleware(accountFollowersController)) executeIfActivityPub(asyncMiddleware(accountFollowersController))
) )
activityPubClientRouter.get('/account/:nameWithHost/following', activityPubClientRouter.get('/account/:name/following',
executeIfActivityPub(localAccountValidator), executeIfActivityPub(localAccountValidator),
executeIfActivityPub(asyncMiddleware(accountFollowingController)) executeIfActivityPub(asyncMiddleware(accountFollowingController))
) )

View File

@ -30,7 +30,7 @@ inboxRouter.post('/inbox',
asyncMiddleware(inboxController) asyncMiddleware(inboxController)
) )
inboxRouter.post('/:nameWithHost/inbox', inboxRouter.post('/account/:name/inbox',
signatureValidator, signatureValidator,
asyncMiddleware(checkSignature), asyncMiddleware(checkSignature),
localAccountValidator, localAccountValidator,
@ -59,7 +59,9 @@ async function inboxController (req: express.Request, res: express.Response, nex
} }
// Only keep activities we are able to process // Only keep activities we are able to process
logger.debug('Filtering activities...', { activities })
activities = activities.filter(a => isActivityValid(a)) activities = activities.filter(a => isActivityValid(a))
logger.debug('We keep %d activities.', activities.length, { activities })
await processActivities(activities, res.locals.account) await processActivities(activities, res.locals.account)

View File

@ -4,14 +4,14 @@ import { badRequest } from '../../helpers'
import { inboxRouter } from './inbox' import { inboxRouter } from './inbox'
import { activityPubClientRouter } from './client' import { activityPubClientRouter } from './client'
const remoteRouter = express.Router() const activityPubRouter = express.Router()
remoteRouter.use('/', inboxRouter) activityPubRouter.use('/', inboxRouter)
remoteRouter.use('/', activityPubClientRouter) activityPubRouter.use('/', activityPubClientRouter)
remoteRouter.use('/*', badRequest) activityPubRouter.use('/*', badRequest)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export { export {
remoteRouter activityPubRouter
} }

View File

@ -1,19 +1,19 @@
import * as Bluebird from 'bluebird'
import * as express from 'express' import * as express from 'express'
import { UserRight } from '../../../shared/models/users/user-right.enum'
import { getFormattedObjects } from '../../helpers' import { getFormattedObjects } from '../../helpers'
import { getOrCreateAccount } from '../../helpers/activitypub' import { logger } from '../../helpers/logger'
import { getApplicationAccount } from '../../helpers/utils' 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 { database as db } from '../../initializers/database'
import { sendFollow } from '../../lib/activitypub/send-request'
import { asyncMiddleware, paginationValidator, setFollowersSort, setPagination } from '../../middlewares' import { asyncMiddleware, paginationValidator, setFollowersSort, setPagination } from '../../middlewares'
import { authenticate } from '../../middlewares/oauth'
import { setBodyHostsPort } from '../../middlewares/pods' import { setBodyHostsPort } from '../../middlewares/pods'
import { setFollowingSort } from '../../middlewares/sort' import { setFollowingSort } from '../../middlewares/sort'
import { ensureUserHasRight } from '../../middlewares/user-right'
import { followValidator } from '../../middlewares/validators/pods' import { followValidator } from '../../middlewares/validators/pods'
import { followersSortValidator, followingSortValidator } from '../../middlewares/validators/sort' 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 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 hosts = req.body.hosts as string[]
const fromAccount = await getApplicationAccount() const fromAccount = await getApplicationAccount()
const tasks: Bluebird<any>[] = [] const tasks: Promise<any>[] = []
const accountName = SERVER_ACCOUNT_NAME
for (const host of hosts) { for (const host of hosts) {
const url = REMOTE_SCHEME.HTTP + '://' + host
const targetAccount = await getOrCreateAccount(url)
// We process each host in a specific transaction // We process each host in a specific transaction
// First, we add the follow request in the database // First, we add the follow request in the database
// Then we send the follow request to other account // Then we send the follow request to other account
const p = db.sequelize.transaction(async t => { const p = loadLocalOrGetAccountFromWebfinger(accountName, host)
return db.AccountFollow.create({ .then(accountResult => {
accountId: fromAccount.id, let targetAccount = accountResult.account
targetAccountId: targetAccount.id,
state: 'pending' 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) tasks.push(p)
} }
@ -91,3 +112,16 @@ async function follow (req: express.Request, res: express.Response, next: expres
return res.status(204).end() 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 './static'
export * from './client' export * from './client'
export * from './services' export * from './services'
export * from './api' 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 { ActivityPubActor } from '../../shared/models/activitypub/activitypub-actor'
import { ResultList } from '../../shared/models/result-list.model' import { ResultList } from '../../shared/models/result-list.model'
import { database as db, REMOTE_SCHEME } from '../initializers' 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 { VideoInstance } from '../models/video/video-interface'
import { isRemoteAccountValid } from './custom-validators' import { isRemoteAccountValid } from './custom-validators'
import { logger } from './logger' 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 // We don't have this account in our database, fetch it on remote
if (!account) { if (!account) {
const { account } = await fetchRemoteAccountAndCreatePod(accountUrl) const res = await fetchRemoteAccountAndCreatePod(accountUrl)
if (res === undefined) throw new Error('Cannot fetch remote account.')
if (!account) throw new Error('Cannot fetch remote account.')
// Save our new account in database // Save our new account in database
const account = res.account
await account.save() await account.save()
} }
@ -49,19 +49,27 @@ async function getOrCreateAccount (accountUrl: string) {
async function fetchRemoteAccountAndCreatePod (accountUrl: string) { async function fetchRemoteAccountAndCreatePod (accountUrl: string) {
const options = { const options = {
uri: accountUrl, uri: accountUrl,
method: 'GET' method: 'GET',
headers: {
'Accept': ACTIVITY_PUB_ACCEPT_HEADER
}
} }
logger.info('Fetching remote account %s.', accountUrl)
let requestResult let requestResult
try { try {
requestResult = await doRequest(options) requestResult = await doRequest(options)
} catch (err) { } catch (err) {
logger.warning('Cannot fetch remote account %s.', accountUrl, err) logger.warn('Cannot fetch remote account %s.', accountUrl, err)
return undefined return undefined
} }
const accountJSON: ActivityPubActor = requestResult.body const accountJSON: ActivityPubActor = JSON.parse(requestResult.body)
if (isRemoteAccountValid(accountJSON) === false) return undefined if (isRemoteAccountValid(accountJSON) === false) {
logger.debug('Remote account JSON is not valid.', { accountJSON })
return undefined
}
const followersCount = await fetchAccountCount(accountJSON.followers) const followersCount = await fetchAccountCount(accountJSON.followers)
const followingCount = await fetchAccountCount(accountJSON.following) const followingCount = await fetchAccountCount(accountJSON.following)
@ -90,7 +98,8 @@ async function fetchRemoteAccountAndCreatePod (accountUrl: string) {
host: accountHost host: accountHost
} }
} }
const pod = await db.Pod.findOrCreate(podOptions) const [ pod ] = await db.Pod.findOrCreate(podOptions)
account.set('podId', pod.id)
return { account, pod } return { account, pod }
} }
@ -176,7 +185,7 @@ async function fetchAccountCount (url: string) {
try { try {
requestResult = await doRequest(options) requestResult = await doRequest(options)
} catch (err) { } catch (err) {
logger.warning('Cannot fetch remote account count %s.', url, err) logger.warn('Cannot fetch remote account count %s.', url, err)
return undefined return undefined
} }

View File

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

View File

@ -1,9 +1,8 @@
import * as validator from 'validator' 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 { CONSTRAINTS_FIELDS } from '../../../initializers/constants'
import { isAccountNameValid } from '../accounts'
import { exists, isUUIDValid } from '../misc'
import { isActivityPubUrlValid, isBaseActivityValid } from './misc'
function isAccountEndpointsObjectValid (endpointObject: any) { function isAccountEndpointsObjectValid (endpointObject: any) {
return isAccountSharedInboxValid(endpointObject.sharedInbox) return isAccountSharedInboxValid(endpointObject.sharedInbox)
@ -59,10 +58,6 @@ function isAccountOutboxValid (outbox: string) {
return isActivityPubUrlValid(outbox) return isActivityPubUrlValid(outbox)
} }
function isAccountNameValid (name: string) {
return isUserUsernameValid(name)
}
function isAccountPreferredUsernameValid (preferredUsername: string) { function isAccountPreferredUsernameValid (preferredUsername: string) {
return isAccountNameValid(preferredUsername) return isAccountNameValid(preferredUsername)
} }
@ -90,7 +85,7 @@ function isRemoteAccountValid (remoteAccount: any) {
isAccountPreferredUsernameValid(remoteAccount.preferredUsername) && isAccountPreferredUsernameValid(remoteAccount.preferredUsername) &&
isAccountUrlValid(remoteAccount.url) && isAccountUrlValid(remoteAccount.url) &&
isAccountPublicKeyObjectValid(remoteAccount.publicKey) && isAccountPublicKeyObjectValid(remoteAccount.publicKey) &&
isAccountEndpointsObjectValid(remoteAccount.endpoint) isAccountEndpointsObjectValid(remoteAccount.endpoints)
} }
function isAccountFollowingCountValid (value: string) { function isAccountFollowingCountValid (value: string) {
@ -101,6 +96,19 @@ function isAccountFollowersCountValid (value: string) {
return exists(value) && validator.isInt('' + value, { min: 0 }) 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 { export {
@ -122,5 +130,8 @@ export {
isRemoteAccountValid, isRemoteAccountValid,
isAccountFollowingCountValid, isAccountFollowingCountValid,
isAccountFollowersCountValid, isAccountFollowersCountValid,
isAccountNameValid isAccountNameValid,
isAccountFollowActivityValid,
isAccountAcceptActivityValid,
isAccountDeleteActivityValid
} }

View File

@ -1,9 +1,13 @@
import * as validator from 'validator' import * as validator from 'validator'
import { isAccountAcceptActivityValid, isAccountDeleteActivityValid, isAccountFollowActivityValid } from './account'
import { isActivityPubUrlValid } from './misc'
import { import {
isVideoChannelCreateActivityValid, isVideoChannelCreateActivityValid,
isVideoChannelDeleteActivityValid,
isVideoChannelUpdateActivityValid,
isVideoTorrentAddActivityValid, isVideoTorrentAddActivityValid,
isVideoTorrentUpdateActivityValid, isVideoTorrentDeleteActivityValid,
isVideoChannelUpdateActivityValid isVideoTorrentUpdateActivityValid
} from './videos' } from './videos'
function isRootActivityValid (activity: any) { function isRootActivityValid (activity: any) {
@ -14,8 +18,8 @@ function isRootActivityValid (activity: any) {
Array.isArray(activity.items) Array.isArray(activity.items)
) || ) ||
( (
validator.isURL(activity.id) && isActivityPubUrlValid(activity.id) &&
validator.isURL(activity.actor) isActivityPubUrlValid(activity.actor)
) )
} }
@ -23,7 +27,12 @@ function isActivityValid (activity: any) {
return isVideoTorrentAddActivityValid(activity) || return isVideoTorrentAddActivityValid(activity) ||
isVideoChannelCreateActivityValid(activity) || isVideoChannelCreateActivityValid(activity) ||
isVideoTorrentUpdateActivityValid(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) { function isBaseActivityValid (activity: any, type: string) {
return Array.isArray(activity['@context']) && return Array.isArray(activity['@context']) &&
activity.type === type && activity.type === type &&
validator.isURL(activity.id) && isActivityPubUrlValid(activity.id) &&
validator.isURL(activity.actor) && isActivityPubUrlValid(activity.actor) &&
Array.isArray(activity.to) && (
activity.to.every(t => validator.isURL(t)) activity.to === undefined ||
(Array.isArray(activity.to) && activity.to.every(t => isActivityPubUrlValid(t)))
)
} }
export { export {

View File

@ -14,7 +14,7 @@ import {
isVideoUrlValid isVideoUrlValid
} from '../videos' } from '../videos'
import { isVideoChannelDescriptionValid, isVideoChannelNameValid } from '../video-channels' import { isVideoChannelDescriptionValid, isVideoChannelNameValid } from '../video-channels'
import { isBaseActivityValid } from './misc' import { isActivityPubUrlValid, isBaseActivityValid } from './misc'
function isVideoTorrentAddActivityValid (activity: any) { function isVideoTorrentAddActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Add') && return isBaseActivityValid(activity, 'Add') &&
@ -26,6 +26,10 @@ function isVideoTorrentUpdateActivityValid (activity: any) {
isVideoTorrentObjectValid(activity.object) isVideoTorrentObjectValid(activity.object)
} }
function isVideoTorrentDeleteActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Delete')
}
function isVideoTorrentObjectValid (video: any) { function isVideoTorrentObjectValid (video: any) {
return video.type === 'Video' && return video.type === 'Video' &&
isVideoNameValid(video.name) && isVideoNameValid(video.name) &&
@ -54,6 +58,10 @@ function isVideoChannelUpdateActivityValid (activity: any) {
isVideoChannelObjectValid(activity.object) isVideoChannelObjectValid(activity.object)
} }
function isVideoChannelDeleteActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Delete')
}
function isVideoChannelObjectValid (videoChannel: any) { function isVideoChannelObjectValid (videoChannel: any) {
return videoChannel.type === 'VideoChannel' && return videoChannel.type === 'VideoChannel' &&
isVideoChannelNameValid(videoChannel.name) && isVideoChannelNameValid(videoChannel.name) &&
@ -67,7 +75,9 @@ export {
isVideoTorrentAddActivityValid, isVideoTorrentAddActivityValid,
isVideoChannelCreateActivityValid, isVideoChannelCreateActivityValid,
isVideoTorrentUpdateActivityValid, isVideoTorrentUpdateActivityValid,
isVideoChannelUpdateActivityValid isVideoChannelUpdateActivityValid,
isVideoChannelDeleteActivityValid,
isVideoTorrentDeleteActivityValid
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -3,6 +3,7 @@ export * from './misc'
export * from './pods' export * from './pods'
export * from './pods' export * from './pods'
export * from './users' export * from './users'
export * from './video-accounts' export * from './accounts'
export * from './video-channels' export * from './video-channels'
export * from './videos' 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 request_timeout: 3000
}) })
async function getAccountFromWebfinger (url: string) { async function getAccountFromWebfinger (nameWithHost: string) {
const webfingerData: WebFingerData = await webfingerLookup(url) 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') 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) => { return new Promise<WebFingerData>((res, rej) => {
webfinger.lookup(url, (err, p) => { webfinger.lookup(nameWithHost, (err, p) => {
if (err) return rej(err) if (err) return rej(err)
return p return res(p.object)
}) })
}) })
} }

View File

@ -1,8 +1,8 @@
import * as config from 'config' import * as config from 'config'
import { promisify0 } from '../helpers/core-utils' import { promisify0 } from '../helpers/core-utils'
import { OAuthClientModel } from '../models/oauth/oauth-client-interface'
import { UserModel } from '../models/account/user-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 // Some checks on configuration files
function checkConfig () { function checkConfig () {
@ -70,6 +70,13 @@ async function usersExist (User: UserModel) {
return totalUsers !== 0 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 { export {
@ -77,5 +84,6 @@ export {
checkFFmpeg, checkFFmpeg,
checkMissedConfig, checkMissedConfig,
clientsExist, clientsExist,
usersExist usersExist,
applicationExist
} }

View File

@ -226,6 +226,9 @@ const FRIEND_SCORE = {
MAX: 1000 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 = { const ACTIVITY_PUB = {
COLLECTION_ITEMS_PER_PAGE: 10, COLLECTION_ITEMS_PER_PAGE: 10,
VIDEO_URL_MIME_TYPES: [ VIDEO_URL_MIME_TYPES: [
@ -352,8 +355,10 @@ export {
PODS_SCORE, PODS_SCORE,
PREVIEWS_SIZE, PREVIEWS_SIZE,
REMOTE_SCHEME, REMOTE_SCHEME,
ACTIVITY_PUB_ACCEPT_HEADER,
FOLLOW_STATES, FOLLOW_STATES,
SEARCHABLE_COLUMNS, SEARCHABLE_COLUMNS,
SERVER_ACCOUNT_NAME,
PRIVATE_RSA_KEY_SIZE, PRIVATE_RSA_KEY_SIZE,
SORTABLE_COLUMNS, SORTABLE_COLUMNS,
STATIC_MAX_AGE, STATIC_MAX_AGE,

View File

@ -3,8 +3,8 @@ import { UserRole } from '../../shared'
import { logger, mkdirpPromise, rimrafPromise } from '../helpers' import { logger, mkdirpPromise, rimrafPromise } from '../helpers'
import { createUserAccountAndChannel } from '../lib' import { createUserAccountAndChannel } from '../lib'
import { createLocalAccount } from '../lib/user' import { createLocalAccount } from '../lib/user'
import { clientsExist, usersExist } from './checker' import { applicationExist, clientsExist, usersExist } from './checker'
import { CACHE, CONFIG, LAST_MIGRATION_VERSION } from './constants' import { CACHE, CONFIG, LAST_MIGRATION_VERSION, SERVER_ACCOUNT_NAME } from './constants'
import { database as db } from './database' import { database as db } from './database'
@ -128,9 +128,13 @@ async function createOAuthAdminIfNotExist () {
} }
async function createApplicationIfNotExist () { 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.') logger.info('Creating Application table.')
const applicationInstance = await db.Application.create({ migrationVersion: LAST_MIGRATION_VERSION }) const applicationInstance = await db.Application.create({ migrationVersion: LAST_MIGRATION_VERSION })
logger.info('Creating application account.') 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 // Don't block on request
generateThumbnailFromUrl(video, videoToCreateData.icon) 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) 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 === undefined) throw new Error('Unknown account')
if (targetAccount.isOwned() === false) throw new Error('This is not a local 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 transaction: t
} })
await db.AccountFollow.create({
accountId: account.id,
targetAccountId: targetAccount.id,
state: 'accepted'
}, sequelizeOptions)
// Target sends to account he accepted the follow request // Target sends to account he accepted the follow request
return sendAccept(targetAccount, account, t) return sendAccept(targetAccount, account, t)

View File

@ -10,60 +10,60 @@ import { httpRequestJobScheduler } from '../jobs'
import { signObject, activityPubContextify } from '../../helpers' import { signObject, activityPubContextify } from '../../helpers'
import { Activity } from '../../../shared' import { Activity } from '../../../shared'
function sendCreateVideoChannel (videoChannel: VideoChannelInstance, t: Sequelize.Transaction) { async function sendCreateVideoChannel (videoChannel: VideoChannelInstance, t: Sequelize.Transaction) {
const videoChannelObject = videoChannel.toActivityPubObject() 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) 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 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) return broadcastToFollowers(data, videoChannel.Account, t)
} }
function sendDeleteVideoChannel (videoChannel: VideoChannelInstance, t: Sequelize.Transaction) { async function sendDeleteVideoChannel (videoChannel: VideoChannelInstance, t: Sequelize.Transaction) {
const data = deleteActivityData(videoChannel.url, videoChannel.Account) const data = await deleteActivityData(videoChannel.url, videoChannel.Account)
return broadcastToFollowers(data, videoChannel.Account, t) 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 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) 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 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) return broadcastToFollowers(data, video.VideoChannel.Account, t)
} }
function sendDeleteVideo (video: VideoInstance, t: Sequelize.Transaction) { async function sendDeleteVideo (video: VideoInstance, t: Sequelize.Transaction) {
const data = deleteActivityData(video.url, video.VideoChannel.Account) const data = await deleteActivityData(video.url, video.VideoChannel.Account)
return broadcastToFollowers(data, video.VideoChannel.Account, t) return broadcastToFollowers(data, video.VideoChannel.Account, t)
} }
function sendDeleteAccount (account: AccountInstance, t: Sequelize.Transaction) { async function sendDeleteAccount (account: AccountInstance, t: Sequelize.Transaction) {
const data = deleteActivityData(account.url, account) const data = await deleteActivityData(account.url, account)
return broadcastToFollowers(data, account, t) return broadcastToFollowers(data, account, t)
} }
function sendAccept (fromAccount: AccountInstance, toAccount: AccountInstance, t: Sequelize.Transaction) { async function sendAccept (fromAccount: AccountInstance, toAccount: AccountInstance, t: Sequelize.Transaction) {
const data = acceptActivityData(fromAccount) const data = await acceptActivityData(fromAccount)
return unicastTo(data, toAccount, t) return unicastTo(data, toAccount, t)
} }
function sendFollow (fromAccount: AccountInstance, toAccount: AccountInstance, t: Sequelize.Transaction) { async function sendFollow (fromAccount: AccountInstance, toAccount: AccountInstance, t: Sequelize.Transaction) {
const data = followActivityData(toAccount.url, fromAccount) const data = await followActivityData(toAccount.url, fromAccount)
return unicastTo(data, toAccount, t) 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) { async function unicastTo (data: any, toAccount: AccountInstance, t: Sequelize.Transaction) {
const jobPayload = { const jobPayload = {
uris: [ toAccount.url ], uris: [ toAccount.inboxUrl ],
body: data body: data
} }

View File

@ -6,6 +6,7 @@ async function process (payload: HTTPRequestPayload, jobId: number) {
logger.info('Processing broadcast in job %d.', jobId) logger.info('Processing broadcast in job %d.', jobId)
const options = { const options = {
method: 'POST',
uri: '', uri: '',
json: payload.body json: payload.body
} }

View File

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

View File

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

View File

@ -1,12 +1,9 @@
import { Request, Response, NextFunction } from 'express' import { NextFunction, Request, Response, RequestHandler } from 'express'
import { database as db } from '../initializers'
import {
logger,
getAccountFromWebfinger,
isSignatureVerified
} from '../helpers'
import { ActivityPubSignature } from '../../shared' 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) { async function checkSignature (req: Request, res: Response, next: NextFunction) {
const signatureObject: ActivityPubSignature = req.body.signature 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 // We don't have this account in our database, fetch it on remote
if (!account) { if (!account) {
account = await getAccountFromWebfinger(signatureObject.creator) const accountResult = await fetchRemoteAccountAndCreatePod(signatureObject.creator)
if (!account) { if (!accountResult) {
return res.sendStatus(403) return res.sendStatus(403)
} }
// Save our new account in database // Save our new account in database
account = accountResult.account
await account.save() await account.save()
} }
const verified = await isSignatureVerified(account, req.body) const verified = await isSignatureVerified(account, req.body)
if (verified === false) return res.sendStatus(403) if (verified === false) return res.sendStatus(403)
res.locals.signature.account = account res.locals.signature = {
account
}
return next() return next()
} }
function executeIfActivityPub (fun: any | any[]) { function executeIfActivityPub (fun: RequestHandler | RequestHandler[]) {
return (req: Request, res: Response, next: NextFunction) => { 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() return next()
} }
if (Array.isArray(fun) === true) { 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, isUserVideoQuotaValid,
logger logger
} from '../../helpers' } 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 { database as db } from '../../initializers/database'
import { AccountInstance } from '../../models' import { AccountInstance } from '../../models'
import { checkErrors } from './utils' import { checkErrors } from './utils'
const localAccountValidator = [ 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) => { (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking localAccountValidator parameters', { parameters: req.params }) 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) { function checkLocalAccountExists (name: string, res: express.Response, callback: (err: Error, account: AccountInstance) => void) {
const [ name, host ] = nameWithHost.split('@') db.Account.loadLocalByName(name)
db.Account.loadLocalAccountByNameAndPod(name, host)
.then(account => { .then(account => {
if (!account) { if (!account) {
return res.status(404) return res.status(404)

View File

@ -1,11 +1,10 @@
import { body } from 'express-validator/check'
import * as express from 'express' import * as express from 'express'
import { body } from 'express-validator/check'
import { logger, isRootActivityValid } from '../../../helpers' import { isRootActivityValid, logger } from '../../../helpers'
import { checkErrors } from '../utils' import { checkErrors } from '../utils'
const activityPubValidator = [ const activityPubValidator = [
body('data').custom(isRootActivityValid), body('').custom((value, { req }) => isRootActivityValid(req.body)),
(req: express.Request, res: express.Response, next: express.NextFunction) => { (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking activity pub parameters', { parameters: req.body }) logger.debug('Checking activity pub parameters', { parameters: req.body })

View File

@ -8,3 +8,4 @@ export * from './users'
export * from './videos' export * from './videos'
export * from './video-blacklist' export * from './video-blacklist'
export * from './video-channels' 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: [ indexes: [
{ {
fields: [ 'accountId' ], fields: [ 'accountId' ]
unique: true
}, },
{ {
fields: [ 'targetAccountId' ], fields: [ 'targetAccountId' ]
},
{
fields: [ 'accountId', 'targetAccountId' ],
unique: true unique: true
} }
] ]
@ -31,7 +33,8 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
) )
const classMethods = [ const classMethods = [
associate associate,
loadByAccountAndTarget
] ]
addMethodsToModel(AccountFollow, classMethods) addMethodsToModel(AccountFollow, classMethods)
@ -46,7 +49,7 @@ function associate (models) {
name: 'accountId', name: 'accountId',
allowNull: false allowNull: false
}, },
as: 'followers', as: 'accountFollowers',
onDelete: 'CASCADE' onDelete: 'CASCADE'
}) })
@ -55,7 +58,7 @@ function associate (models) {
name: 'targetAccountId', name: 'targetAccountId',
allowNull: false allowNull: false
}, },
as: 'following', as: 'accountFollowing',
onDelete: 'CASCADE' onDelete: 'CASCADE'
}) })
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -10,7 +10,7 @@ import {
JobMethods JobMethods
} from './job-interface' } 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 Job: Sequelize.Model<JobInstance, JobAttributes>
let listWithLimitByCategory: JobMethods.ListWithLimitByCategory let listWithLimitByCategory: JobMethods.ListWithLimitByCategory
@ -38,7 +38,7 @@ export default function defineJob (sequelize: Sequelize.Sequelize, DataTypes: Se
{ {
indexes: [ 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 = { const query = {
order: [ order: [
[ 'id', 'ASC' ] [ 'id', 'ASC' ]
], ],
limit: limit, limit: limit,
where: { where: {
state state,
category: jobCategory
} }
} }