aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/+admin/friends/friend-list/friend-list.component.html8
-rw-r--r--client/src/app/+admin/friends/friend-list/friend-list.component.ts2
-rw-r--r--client/src/app/+admin/friends/shared/friend.service.ts4
-rw-r--r--server.ts5
-rw-r--r--server/controllers/activitypub/client.ts4
-rw-r--r--server/controllers/activitypub/inbox.ts4
-rw-r--r--server/controllers/activitypub/index.ts10
-rw-r--r--server/controllers/api/pods.ts68
-rw-r--r--server/controllers/index.ts2
-rw-r--r--server/controllers/webfinger.ts39
-rw-r--r--server/helpers/activitypub.ts29
-rw-r--r--server/helpers/custom-validators/accounts.ts (renamed from server/helpers/custom-validators/video-accounts.ts)14
-rw-r--r--server/helpers/custom-validators/activitypub/account.ts31
-rw-r--r--server/helpers/custom-validators/activitypub/activity.ts19
-rw-r--r--server/helpers/custom-validators/activitypub/misc.ts10
-rw-r--r--server/helpers/custom-validators/activitypub/videos.ts14
-rw-r--r--server/helpers/custom-validators/index.ts3
-rw-r--r--server/helpers/custom-validators/webfinger.ts25
-rw-r--r--server/helpers/webfinger.ts21
-rw-r--r--server/initializers/checker.ts14
-rw-r--r--server/initializers/constants.ts5
-rw-r--r--server/initializers/installer.ts10
-rw-r--r--server/lib/activitypub/process-add.ts2
-rw-r--r--server/lib/activitypub/process-follow.ts18
-rw-r--r--server/lib/activitypub/send-request.ts38
-rw-r--r--server/lib/jobs/http-request-job-scheduler/http-request-broadcast-handler.ts1
-rw-r--r--server/lib/jobs/http-request-job-scheduler/http-request-unicast-handler.ts1
-rw-r--r--server/lib/jobs/job-scheduler.ts13
-rw-r--r--server/middlewares/activitypub.ts32
-rw-r--r--server/middlewares/validators/account.ts10
-rw-r--r--server/middlewares/validators/activitypub/activity.ts7
-rw-r--r--server/middlewares/validators/index.ts1
-rw-r--r--server/middlewares/validators/webfinger.ts42
-rw-r--r--server/models/account/account-follow.ts15
-rw-r--r--server/models/account/account-interface.ts6
-rw-r--r--server/models/account/account.ts44
-rw-r--r--server/models/application/application-interface.ts9
-rw-r--r--server/models/application/application.ts11
-rw-r--r--server/models/job/job.ts9
39 files changed, 431 insertions, 169 deletions
diff --git a/client/src/app/+admin/friends/friend-list/friend-list.component.html b/client/src/app/+admin/friends/friend-list/friend-list.component.html
index 7e92ced54..df5a570fd 100644
--- a/client/src/app/+admin/friends/friend-list/friend-list.component.html
+++ b/client/src/app/+admin/friends/friend-list/friend-list.component.html
@@ -4,12 +4,12 @@
4 4
5 <p-dataTable 5 <p-dataTable
6 [value]="friends" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage" 6 [value]="friends" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
7 sortField="id" (onLazyLoad)="loadLazy($event)" 7 sortField="createdAt" (onLazyLoad)="loadLazy($event)"
8 > 8 >
9 <p-column field="id" header="ID" [sortable]="true"></p-column> 9 <p-column field="id" header="ID"></p-column>
10 <p-column field="host" header="Host" [sortable]="true"></p-column> 10 <p-column field="host" header="Host"></p-column>
11 <p-column field="email" header="Email"></p-column> 11 <p-column field="email" header="Email"></p-column>
12 <p-column field="score" header="Score" [sortable]="true"></p-column> 12 <p-column field="score" header="Score"></p-column>
13 <p-column field="createdAt" header="Created date" [sortable]="true"></p-column> 13 <p-column field="createdAt" header="Created date" [sortable]="true"></p-column>
14 <p-column header="Delete" styleClass="action-cell"> 14 <p-column header="Delete" styleClass="action-cell">
15 <ng-template pTemplate="body" let-pod="rowData"> 15 <ng-template pTemplate="body" let-pod="rowData">
diff --git a/client/src/app/+admin/friends/friend-list/friend-list.component.ts b/client/src/app/+admin/friends/friend-list/friend-list.component.ts
index 0323ae96d..3fa8ef19f 100644
--- a/client/src/app/+admin/friends/friend-list/friend-list.component.ts
+++ b/client/src/app/+admin/friends/friend-list/friend-list.component.ts
@@ -17,7 +17,7 @@ export class FriendListComponent extends RestTable implements OnInit {
17 friends: Pod[] = [] 17 friends: Pod[] = []
18 totalRecords = 0 18 totalRecords = 0
19 rowsPerPage = 10 19 rowsPerPage = 10
20 sort: SortMeta = { field: 'id', order: 1 } 20 sort: SortMeta = { field: 'createdAt', order: 1 }
21 pagination: RestPagination = { count: this.rowsPerPage, start: 0 } 21 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
22 22
23 constructor ( 23 constructor (
diff --git a/client/src/app/+admin/friends/shared/friend.service.ts b/client/src/app/+admin/friends/shared/friend.service.ts
index 083a2fce0..867656a53 100644
--- a/client/src/app/+admin/friends/shared/friend.service.ts
+++ b/client/src/app/+admin/friends/shared/friend.service.ts
@@ -23,7 +23,7 @@ export class FriendService {
23 let params = new HttpParams() 23 let params = new HttpParams()
24 params = this.restService.addRestGetParams(params, pagination, sort) 24 params = this.restService.addRestGetParams(params, pagination, sort)
25 25
26 return this.authHttp.get<ResultList<Account>>(API_URL + '/followers', { params }) 26 return this.authHttp.get<ResultList<Account>>(API_URL + '/api/v1/pods/followers', { params })
27 .map(res => this.restExtractor.convertResultListDateToHuman(res)) 27 .map(res => this.restExtractor.convertResultListDateToHuman(res))
28 .catch(res => this.restExtractor.handleError(res)) 28 .catch(res => this.restExtractor.handleError(res))
29 } 29 }
@@ -33,7 +33,7 @@ export class FriendService {
33 hosts: notEmptyHosts 33 hosts: notEmptyHosts
34 } 34 }
35 35
36 return this.authHttp.post(API_URL + '/follow', body) 36 return this.authHttp.post(API_URL + '/api/v1/pods/follow', body)
37 .map(this.restExtractor.extractDataBool) 37 .map(this.restExtractor.extractDataBool)
38 .catch(res => this.restExtractor.handleError(res)) 38 .catch(res => this.restExtractor.handleError(res))
39 } 39 }
diff --git a/server.ts b/server.ts
index 0878fe757..62219f7df 100644
--- a/server.ts
+++ b/server.ts
@@ -47,7 +47,7 @@ db.init(false).then(() => onDatabaseInitDone())
47// ----------- PeerTube modules ----------- 47// ----------- PeerTube modules -----------
48import { migrate, installApplication } from './server/initializers' 48import { migrate, installApplication } from './server/initializers'
49import { httpRequestJobScheduler, transcodingJobScheduler, VideosPreviewCache } from './server/lib' 49import { httpRequestJobScheduler, transcodingJobScheduler, VideosPreviewCache } from './server/lib'
50import { apiRouter, clientsRouter, staticRouter, servicesRouter } from './server/controllers' 50import { apiRouter, clientsRouter, staticRouter, servicesRouter, webfingerRouter, activityPubRouter } from './server/controllers'
51 51
52// ----------- Command line ----------- 52// ----------- Command line -----------
53 53
@@ -115,6 +115,9 @@ app.use(apiRoute, apiRouter)
115// Services (oembed...) 115// Services (oembed...)
116app.use('/services', servicesRouter) 116app.use('/services', servicesRouter)
117 117
118app.use('/', webfingerRouter)
119app.use('/', activityPubRouter)
120
118// Client files 121// Client files
119app.use('/', clientsRouter) 122app.use('/', clientsRouter)
120 123
diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts
index 461a619dd..56a4054fa 100644
--- a/server/controllers/activitypub/client.ts
+++ b/server/controllers/activitypub/client.ts
@@ -16,12 +16,12 @@ activityPubClientRouter.get('/account/:name',
16 executeIfActivityPub(asyncMiddleware(accountController)) 16 executeIfActivityPub(asyncMiddleware(accountController))
17) 17)
18 18
19activityPubClientRouter.get('/account/:nameWithHost/followers', 19activityPubClientRouter.get('/account/:name/followers',
20 executeIfActivityPub(localAccountValidator), 20 executeIfActivityPub(localAccountValidator),
21 executeIfActivityPub(asyncMiddleware(accountFollowersController)) 21 executeIfActivityPub(asyncMiddleware(accountFollowersController))
22) 22)
23 23
24activityPubClientRouter.get('/account/:nameWithHost/following', 24activityPubClientRouter.get('/account/:name/following',
25 executeIfActivityPub(localAccountValidator), 25 executeIfActivityPub(localAccountValidator),
26 executeIfActivityPub(asyncMiddleware(accountFollowingController)) 26 executeIfActivityPub(asyncMiddleware(accountFollowingController))
27) 27)
diff --git a/server/controllers/activitypub/inbox.ts b/server/controllers/activitypub/inbox.ts
index eedb518b9..e62125d85 100644
--- a/server/controllers/activitypub/inbox.ts
+++ b/server/controllers/activitypub/inbox.ts
@@ -30,7 +30,7 @@ inboxRouter.post('/inbox',
30 asyncMiddleware(inboxController) 30 asyncMiddleware(inboxController)
31) 31)
32 32
33inboxRouter.post('/:nameWithHost/inbox', 33inboxRouter.post('/account/:name/inbox',
34 signatureValidator, 34 signatureValidator,
35 asyncMiddleware(checkSignature), 35 asyncMiddleware(checkSignature),
36 localAccountValidator, 36 localAccountValidator,
@@ -59,7 +59,9 @@ async function inboxController (req: express.Request, res: express.Response, nex
59 } 59 }
60 60
61 // Only keep activities we are able to process 61 // Only keep activities we are able to process
62 logger.debug('Filtering activities...', { activities })
62 activities = activities.filter(a => isActivityValid(a)) 63 activities = activities.filter(a => isActivityValid(a))
64 logger.debug('We keep %d activities.', activities.length, { activities })
63 65
64 await processActivities(activities, res.locals.account) 66 await processActivities(activities, res.locals.account)
65 67
diff --git a/server/controllers/activitypub/index.ts b/server/controllers/activitypub/index.ts
index 6c7bafc6e..0c8574ef7 100644
--- a/server/controllers/activitypub/index.ts
+++ b/server/controllers/activitypub/index.ts
@@ -4,14 +4,14 @@ import { badRequest } from '../../helpers'
4import { inboxRouter } from './inbox' 4import { inboxRouter } from './inbox'
5import { activityPubClientRouter } from './client' 5import { activityPubClientRouter } from './client'
6 6
7const remoteRouter = express.Router() 7const activityPubRouter = express.Router()
8 8
9remoteRouter.use('/', inboxRouter) 9activityPubRouter.use('/', inboxRouter)
10remoteRouter.use('/', activityPubClientRouter) 10activityPubRouter.use('/', activityPubClientRouter)
11remoteRouter.use('/*', badRequest) 11activityPubRouter.use('/*', badRequest)
12 12
13// --------------------------------------------------------------------------- 13// ---------------------------------------------------------------------------
14 14
15export { 15export {
16 remoteRouter 16 activityPubRouter
17} 17}
diff --git a/server/controllers/api/pods.ts b/server/controllers/api/pods.ts
index 2231a05fa..0bd6971bb 100644
--- a/server/controllers/api/pods.ts
+++ b/server/controllers/api/pods.ts
@@ -1,19 +1,19 @@
1import * as Bluebird from 'bluebird'
2import * as express from 'express' 1import * as express from 'express'
2import { UserRight } from '../../../shared/models/users/user-right.enum'
3import { getFormattedObjects } from '../../helpers' 3import { getFormattedObjects } from '../../helpers'
4import { getOrCreateAccount } from '../../helpers/activitypub' 4import { logger } from '../../helpers/logger'
5import { getApplicationAccount } from '../../helpers/utils' 5import { getApplicationAccount } from '../../helpers/utils'
6import { REMOTE_SCHEME } from '../../initializers/constants' 6import { getAccountFromWebfinger } from '../../helpers/webfinger'
7import { SERVER_ACCOUNT_NAME } from '../../initializers/constants'
7import { database as db } from '../../initializers/database' 8import { database as db } from '../../initializers/database'
9import { sendFollow } from '../../lib/activitypub/send-request'
8import { asyncMiddleware, paginationValidator, setFollowersSort, setPagination } from '../../middlewares' 10import { asyncMiddleware, paginationValidator, setFollowersSort, setPagination } from '../../middlewares'
11import { authenticate } from '../../middlewares/oauth'
9import { setBodyHostsPort } from '../../middlewares/pods' 12import { setBodyHostsPort } from '../../middlewares/pods'
10import { setFollowingSort } from '../../middlewares/sort' 13import { setFollowingSort } from '../../middlewares/sort'
14import { ensureUserHasRight } from '../../middlewares/user-right'
11import { followValidator } from '../../middlewares/validators/pods' 15import { followValidator } from '../../middlewares/validators/pods'
12import { followersSortValidator, followingSortValidator } from '../../middlewares/validators/sort' 16import { followersSortValidator, followingSortValidator } from '../../middlewares/validators/sort'
13import { sendFollow } from '../../lib/activitypub/send-request'
14import { authenticate } from '../../middlewares/oauth'
15import { ensureUserHasRight } from '../../middlewares/user-right'
16import { UserRight } from '../../../shared/models/users/user-right.enum'
17 17
18const podsRouter = express.Router() 18const podsRouter = express.Router()
19 19
@@ -67,22 +67,43 @@ async function follow (req: express.Request, res: express.Response, next: expres
67 const hosts = req.body.hosts as string[] 67 const hosts = req.body.hosts as string[]
68 const fromAccount = await getApplicationAccount() 68 const fromAccount = await getApplicationAccount()
69 69
70 const tasks: Bluebird<any>[] = [] 70 const tasks: Promise<any>[] = []
71 const accountName = SERVER_ACCOUNT_NAME
72
71 for (const host of hosts) { 73 for (const host of hosts) {
72 const url = REMOTE_SCHEME.HTTP + '://' + host
73 const targetAccount = await getOrCreateAccount(url)
74 74
75 // We process each host in a specific transaction 75 // We process each host in a specific transaction
76 // First, we add the follow request in the database 76 // First, we add the follow request in the database
77 // Then we send the follow request to other account 77 // Then we send the follow request to other account
78 const p = db.sequelize.transaction(async t => { 78 const p = loadLocalOrGetAccountFromWebfinger(accountName, host)
79 return db.AccountFollow.create({ 79 .then(accountResult => {
80 accountId: fromAccount.id, 80 let targetAccount = accountResult.account
81 targetAccountId: targetAccount.id, 81
82 state: 'pending' 82 return db.sequelize.transaction(async t => {
83 if (accountResult.loadedFromDB === false) {
84 targetAccount = await targetAccount.save({ transaction: t })
85 }
86
87 const [ accountFollow ] = await db.AccountFollow.findOrCreate({
88 where: {
89 accountId: fromAccount.id,
90 targetAccountId: targetAccount.id
91 },
92 defaults: {
93 state: 'pending',
94 accountId: fromAccount.id,
95 targetAccountId: targetAccount.id
96 },
97 transaction: t
98 })
99
100 // Send a notification to remote server
101 if (accountFollow.state === 'pending') {
102 await sendFollow(fromAccount, targetAccount, t)
103 }
104 })
83 }) 105 })
84 .then(() => sendFollow(fromAccount, targetAccount, t)) 106 .catch(err => logger.warn('Cannot follow server %s.', `${accountName}@${host}`, err))
85 })
86 107
87 tasks.push(p) 108 tasks.push(p)
88 } 109 }
@@ -91,3 +112,16 @@ async function follow (req: express.Request, res: express.Response, next: expres
91 112
92 return res.status(204).end() 113 return res.status(204).end()
93} 114}
115
116async function loadLocalOrGetAccountFromWebfinger (name: string, host: string) {
117 let loadedFromDB = true
118 let account = await db.Account.loadByNameAndHost(name, host)
119
120 if (!account) {
121 const nameWithDomain = name + '@' + host
122 account = await getAccountFromWebfinger(nameWithDomain)
123 loadedFromDB = false
124 }
125
126 return { account, loadedFromDB }
127}
diff --git a/server/controllers/index.ts b/server/controllers/index.ts
index 51cb480a3..457d0a12e 100644
--- a/server/controllers/index.ts
+++ b/server/controllers/index.ts
@@ -1,4 +1,6 @@
1export * from './activitypub'
1export * from './static' 2export * from './static'
2export * from './client' 3export * from './client'
3export * from './services' 4export * from './services'
4export * from './api' 5export * from './api'
6export * from './webfinger'
diff --git a/server/controllers/webfinger.ts b/server/controllers/webfinger.ts
new file mode 100644
index 000000000..1c726f0cb
--- /dev/null
+++ b/server/controllers/webfinger.ts
@@ -0,0 +1,39 @@
1import * as express from 'express'
2
3import { CONFIG, PREVIEWS_SIZE, EMBED_SIZE } from '../initializers'
4import { oembedValidator } from '../middlewares'
5import { VideoInstance } from '../models'
6import { webfingerValidator } from '../middlewares/validators/webfinger'
7import { AccountInstance } from '../models/account/account-interface'
8
9const webfingerRouter = express.Router()
10
11webfingerRouter.use('/.well-known/webfinger',
12 webfingerValidator,
13 webfingerController
14)
15
16// ---------------------------------------------------------------------------
17
18export {
19 webfingerRouter
20}
21
22// ---------------------------------------------------------------------------
23
24function webfingerController (req: express.Request, res: express.Response, next: express.NextFunction) {
25 const account: AccountInstance = res.locals.account
26
27 const json = {
28 subject: req.query.resource,
29 aliases: [ account.url ],
30 links: [
31 {
32 rel: 'self',
33 href: account.url
34 }
35 ]
36 }
37
38 return res.json(json).end()
39}
diff --git a/server/helpers/activitypub.ts b/server/helpers/activitypub.ts
index a1493e5c1..b91490a0b 100644
--- a/server/helpers/activitypub.ts
+++ b/server/helpers/activitypub.ts
@@ -5,7 +5,7 @@ import { ActivityIconObject } from '../../shared/index'
5import { ActivityPubActor } from '../../shared/models/activitypub/activitypub-actor' 5import { ActivityPubActor } from '../../shared/models/activitypub/activitypub-actor'
6import { ResultList } from '../../shared/models/result-list.model' 6import { ResultList } from '../../shared/models/result-list.model'
7import { database as db, REMOTE_SCHEME } from '../initializers' 7import { database as db, REMOTE_SCHEME } from '../initializers'
8import { CONFIG, STATIC_PATHS } from '../initializers/constants' 8import { ACTIVITY_PUB_ACCEPT_HEADER, CONFIG, STATIC_PATHS } from '../initializers/constants'
9import { VideoInstance } from '../models/video/video-interface' 9import { VideoInstance } from '../models/video/video-interface'
10import { isRemoteAccountValid } from './custom-validators' 10import { isRemoteAccountValid } from './custom-validators'
11import { logger } from './logger' 11import { logger } from './logger'
@@ -35,11 +35,11 @@ async function getOrCreateAccount (accountUrl: string) {
35 35
36 // We don't have this account in our database, fetch it on remote 36 // We don't have this account in our database, fetch it on remote
37 if (!account) { 37 if (!account) {
38 const { account } = await fetchRemoteAccountAndCreatePod(accountUrl) 38 const res = await fetchRemoteAccountAndCreatePod(accountUrl)
39 39 if (res === undefined) throw new Error('Cannot fetch remote account.')
40 if (!account) throw new Error('Cannot fetch remote account.')
41 40
42 // Save our new account in database 41 // Save our new account in database
42 const account = res.account
43 await account.save() 43 await account.save()
44 } 44 }
45 45
@@ -49,19 +49,27 @@ async function getOrCreateAccount (accountUrl: string) {
49async function fetchRemoteAccountAndCreatePod (accountUrl: string) { 49async function fetchRemoteAccountAndCreatePod (accountUrl: string) {
50 const options = { 50 const options = {
51 uri: accountUrl, 51 uri: accountUrl,
52 method: 'GET' 52 method: 'GET',
53 headers: {
54 'Accept': ACTIVITY_PUB_ACCEPT_HEADER
55 }
53 } 56 }
54 57
58 logger.info('Fetching remote account %s.', accountUrl)
59
55 let requestResult 60 let requestResult
56 try { 61 try {
57 requestResult = await doRequest(options) 62 requestResult = await doRequest(options)
58 } catch (err) { 63 } catch (err) {
59 logger.warning('Cannot fetch remote account %s.', accountUrl, err) 64 logger.warn('Cannot fetch remote account %s.', accountUrl, err)
60 return undefined 65 return undefined
61 } 66 }
62 67
63 const accountJSON: ActivityPubActor = requestResult.body 68 const accountJSON: ActivityPubActor = JSON.parse(requestResult.body)
64 if (isRemoteAccountValid(accountJSON) === false) return undefined 69 if (isRemoteAccountValid(accountJSON) === false) {
70 logger.debug('Remote account JSON is not valid.', { accountJSON })
71 return undefined
72 }
65 73
66 const followersCount = await fetchAccountCount(accountJSON.followers) 74 const followersCount = await fetchAccountCount(accountJSON.followers)
67 const followingCount = await fetchAccountCount(accountJSON.following) 75 const followingCount = await fetchAccountCount(accountJSON.following)
@@ -90,7 +98,8 @@ async function fetchRemoteAccountAndCreatePod (accountUrl: string) {
90 host: accountHost 98 host: accountHost
91 } 99 }
92 } 100 }
93 const pod = await db.Pod.findOrCreate(podOptions) 101 const [ pod ] = await db.Pod.findOrCreate(podOptions)
102 account.set('podId', pod.id)
94 103
95 return { account, pod } 104 return { account, pod }
96} 105}
@@ -176,7 +185,7 @@ async function fetchAccountCount (url: string) {
176 try { 185 try {
177 requestResult = await doRequest(options) 186 requestResult = await doRequest(options)
178 } catch (err) { 187 } catch (err) {
179 logger.warning('Cannot fetch remote account count %s.', url, err) 188 logger.warn('Cannot fetch remote account count %s.', url, err)
180 return undefined 189 return undefined
181 } 190 }
182 191
diff --git a/server/helpers/custom-validators/video-accounts.ts b/server/helpers/custom-validators/accounts.ts
index 31808ae1e..6d6219a95 100644
--- a/server/helpers/custom-validators/video-accounts.ts
+++ b/server/helpers/custom-validators/accounts.ts
@@ -10,14 +10,14 @@ import { logger } from '../logger'
10import { isUserUsernameValid } from './users' 10import { isUserUsernameValid } from './users'
11import { isHostValid } from './pods' 11import { isHostValid } from './pods'
12 12
13function isVideoAccountNameValid (value: string) { 13function isAccountNameValid (value: string) {
14 return isUserUsernameValid(value) 14 return isUserUsernameValid(value)
15} 15}
16 16
17function isAccountNameWithHostValid (value: string) { 17function isAccountNameWithHostValid (value: string) {
18 const [ name, host ] = value.split('@') 18 const [ name, host ] = value.split('@')
19 19
20 return isVideoAccountNameValid(name) && isHostValid(host) 20 return isAccountNameValid(name) && isHostValid(host)
21} 21}
22 22
23function checkVideoAccountExists (id: string, res: express.Response, callback: () => void) { 23function checkVideoAccountExists (id: string, res: express.Response, callback: () => void) {
@@ -38,10 +38,10 @@ function checkVideoAccountExists (id: string, res: express.Response, callback: (
38 res.locals.account = account 38 res.locals.account = account
39 callback() 39 callback()
40 }) 40 })
41 .catch(err => { 41 .catch(err => {
42 logger.error('Error in video account request validator.', err) 42 logger.error('Error in video account request validator.', err)
43 return res.sendStatus(500) 43 return res.sendStatus(500)
44 }) 44 })
45} 45}
46 46
47// --------------------------------------------------------------------------- 47// ---------------------------------------------------------------------------
@@ -49,5 +49,5 @@ function checkVideoAccountExists (id: string, res: express.Response, callback: (
49export { 49export {
50 checkVideoAccountExists, 50 checkVideoAccountExists,
51 isAccountNameWithHostValid, 51 isAccountNameWithHostValid,
52 isVideoAccountNameValid 52 isAccountNameValid
53} 53}
diff --git a/server/helpers/custom-validators/activitypub/account.ts b/server/helpers/custom-validators/activitypub/account.ts
index acd2b8058..645f55a5a 100644
--- a/server/helpers/custom-validators/activitypub/account.ts
+++ b/server/helpers/custom-validators/activitypub/account.ts
@@ -1,9 +1,8 @@
1import * as validator from 'validator' 1import * as validator from 'validator'
2
3import { exists, isUUIDValid } from '../misc'
4import { isActivityPubUrlValid } from './misc'
5import { isUserUsernameValid } from '../users'
6import { CONSTRAINTS_FIELDS } from '../../../initializers/constants' 2import { CONSTRAINTS_FIELDS } from '../../../initializers/constants'
3import { isAccountNameValid } from '../accounts'
4import { exists, isUUIDValid } from '../misc'
5import { isActivityPubUrlValid, isBaseActivityValid } from './misc'
7 6
8function isAccountEndpointsObjectValid (endpointObject: any) { 7function isAccountEndpointsObjectValid (endpointObject: any) {
9 return isAccountSharedInboxValid(endpointObject.sharedInbox) 8 return isAccountSharedInboxValid(endpointObject.sharedInbox)
@@ -59,10 +58,6 @@ function isAccountOutboxValid (outbox: string) {
59 return isActivityPubUrlValid(outbox) 58 return isActivityPubUrlValid(outbox)
60} 59}
61 60
62function isAccountNameValid (name: string) {
63 return isUserUsernameValid(name)
64}
65
66function isAccountPreferredUsernameValid (preferredUsername: string) { 61function isAccountPreferredUsernameValid (preferredUsername: string) {
67 return isAccountNameValid(preferredUsername) 62 return isAccountNameValid(preferredUsername)
68} 63}
@@ -90,7 +85,7 @@ function isRemoteAccountValid (remoteAccount: any) {
90 isAccountPreferredUsernameValid(remoteAccount.preferredUsername) && 85 isAccountPreferredUsernameValid(remoteAccount.preferredUsername) &&
91 isAccountUrlValid(remoteAccount.url) && 86 isAccountUrlValid(remoteAccount.url) &&
92 isAccountPublicKeyObjectValid(remoteAccount.publicKey) && 87 isAccountPublicKeyObjectValid(remoteAccount.publicKey) &&
93 isAccountEndpointsObjectValid(remoteAccount.endpoint) 88 isAccountEndpointsObjectValid(remoteAccount.endpoints)
94} 89}
95 90
96function isAccountFollowingCountValid (value: string) { 91function isAccountFollowingCountValid (value: string) {
@@ -101,6 +96,19 @@ function isAccountFollowersCountValid (value: string) {
101 return exists(value) && validator.isInt('' + value, { min: 0 }) 96 return exists(value) && validator.isInt('' + value, { min: 0 })
102} 97}
103 98
99function isAccountDeleteActivityValid (activity: any) {
100 return isBaseActivityValid(activity, 'Delete')
101}
102
103function isAccountFollowActivityValid (activity: any) {
104 return isBaseActivityValid(activity, 'Follow') &&
105 isActivityPubUrlValid(activity.object)
106}
107
108function isAccountAcceptActivityValid (activity: any) {
109 return isBaseActivityValid(activity, 'Accept')
110}
111
104// --------------------------------------------------------------------------- 112// ---------------------------------------------------------------------------
105 113
106export { 114export {
@@ -122,5 +130,8 @@ export {
122 isRemoteAccountValid, 130 isRemoteAccountValid,
123 isAccountFollowingCountValid, 131 isAccountFollowingCountValid,
124 isAccountFollowersCountValid, 132 isAccountFollowersCountValid,
125 isAccountNameValid 133 isAccountNameValid,
134 isAccountFollowActivityValid,
135 isAccountAcceptActivityValid,
136 isAccountDeleteActivityValid
126} 137}
diff --git a/server/helpers/custom-validators/activitypub/activity.ts b/server/helpers/custom-validators/activitypub/activity.ts
index dd671c4cf..b5ba0f7af 100644
--- a/server/helpers/custom-validators/activitypub/activity.ts
+++ b/server/helpers/custom-validators/activitypub/activity.ts
@@ -1,9 +1,13 @@
1import * as validator from 'validator' 1import * as validator from 'validator'
2import { isAccountAcceptActivityValid, isAccountDeleteActivityValid, isAccountFollowActivityValid } from './account'
3import { isActivityPubUrlValid } from './misc'
2import { 4import {
3 isVideoChannelCreateActivityValid, 5 isVideoChannelCreateActivityValid,
6 isVideoChannelDeleteActivityValid,
7 isVideoChannelUpdateActivityValid,
4 isVideoTorrentAddActivityValid, 8 isVideoTorrentAddActivityValid,
5 isVideoTorrentUpdateActivityValid, 9 isVideoTorrentDeleteActivityValid,
6 isVideoChannelUpdateActivityValid 10 isVideoTorrentUpdateActivityValid
7} from './videos' 11} from './videos'
8 12
9function isRootActivityValid (activity: any) { 13function isRootActivityValid (activity: any) {
@@ -14,8 +18,8 @@ function isRootActivityValid (activity: any) {
14 Array.isArray(activity.items) 18 Array.isArray(activity.items)
15 ) || 19 ) ||
16 ( 20 (
17 validator.isURL(activity.id) && 21 isActivityPubUrlValid(activity.id) &&
18 validator.isURL(activity.actor) 22 isActivityPubUrlValid(activity.actor)
19 ) 23 )
20} 24}
21 25
@@ -23,7 +27,12 @@ function isActivityValid (activity: any) {
23 return isVideoTorrentAddActivityValid(activity) || 27 return isVideoTorrentAddActivityValid(activity) ||
24 isVideoChannelCreateActivityValid(activity) || 28 isVideoChannelCreateActivityValid(activity) ||
25 isVideoTorrentUpdateActivityValid(activity) || 29 isVideoTorrentUpdateActivityValid(activity) ||
26 isVideoChannelUpdateActivityValid(activity) 30 isVideoChannelUpdateActivityValid(activity) ||
31 isVideoTorrentDeleteActivityValid(activity) ||
32 isVideoChannelDeleteActivityValid(activity) ||
33 isAccountDeleteActivityValid(activity) ||
34 isAccountFollowActivityValid(activity) ||
35 isAccountAcceptActivityValid(activity)
27} 36}
28 37
29// --------------------------------------------------------------------------- 38// ---------------------------------------------------------------------------
diff --git a/server/helpers/custom-validators/activitypub/misc.ts b/server/helpers/custom-validators/activitypub/misc.ts
index a94c36b51..665a63a73 100644
--- a/server/helpers/custom-validators/activitypub/misc.ts
+++ b/server/helpers/custom-validators/activitypub/misc.ts
@@ -23,10 +23,12 @@ function isActivityPubUrlValid (url: string) {
23function isBaseActivityValid (activity: any, type: string) { 23function isBaseActivityValid (activity: any, type: string) {
24 return Array.isArray(activity['@context']) && 24 return Array.isArray(activity['@context']) &&
25 activity.type === type && 25 activity.type === type &&
26 validator.isURL(activity.id) && 26 isActivityPubUrlValid(activity.id) &&
27 validator.isURL(activity.actor) && 27 isActivityPubUrlValid(activity.actor) &&
28 Array.isArray(activity.to) && 28 (
29 activity.to.every(t => validator.isURL(t)) 29 activity.to === undefined ||
30 (Array.isArray(activity.to) && activity.to.every(t => isActivityPubUrlValid(t)))
31 )
30} 32}
31 33
32export { 34export {
diff --git a/server/helpers/custom-validators/activitypub/videos.ts b/server/helpers/custom-validators/activitypub/videos.ts
index 8f6d50f50..c9ecf1f3d 100644
--- a/server/helpers/custom-validators/activitypub/videos.ts
+++ b/server/helpers/custom-validators/activitypub/videos.ts
@@ -14,7 +14,7 @@ import {
14 isVideoUrlValid 14 isVideoUrlValid
15} from '../videos' 15} from '../videos'
16import { isVideoChannelDescriptionValid, isVideoChannelNameValid } from '../video-channels' 16import { isVideoChannelDescriptionValid, isVideoChannelNameValid } from '../video-channels'
17import { isBaseActivityValid } from './misc' 17import { isActivityPubUrlValid, isBaseActivityValid } from './misc'
18 18
19function isVideoTorrentAddActivityValid (activity: any) { 19function isVideoTorrentAddActivityValid (activity: any) {
20 return isBaseActivityValid(activity, 'Add') && 20 return isBaseActivityValid(activity, 'Add') &&
@@ -26,6 +26,10 @@ function isVideoTorrentUpdateActivityValid (activity: any) {
26 isVideoTorrentObjectValid(activity.object) 26 isVideoTorrentObjectValid(activity.object)
27} 27}
28 28
29function isVideoTorrentDeleteActivityValid (activity: any) {
30 return isBaseActivityValid(activity, 'Delete')
31}
32
29function isVideoTorrentObjectValid (video: any) { 33function isVideoTorrentObjectValid (video: any) {
30 return video.type === 'Video' && 34 return video.type === 'Video' &&
31 isVideoNameValid(video.name) && 35 isVideoNameValid(video.name) &&
@@ -54,6 +58,10 @@ function isVideoChannelUpdateActivityValid (activity: any) {
54 isVideoChannelObjectValid(activity.object) 58 isVideoChannelObjectValid(activity.object)
55} 59}
56 60
61function isVideoChannelDeleteActivityValid (activity: any) {
62 return isBaseActivityValid(activity, 'Delete')
63}
64
57function isVideoChannelObjectValid (videoChannel: any) { 65function isVideoChannelObjectValid (videoChannel: any) {
58 return videoChannel.type === 'VideoChannel' && 66 return videoChannel.type === 'VideoChannel' &&
59 isVideoChannelNameValid(videoChannel.name) && 67 isVideoChannelNameValid(videoChannel.name) &&
@@ -67,7 +75,9 @@ export {
67 isVideoTorrentAddActivityValid, 75 isVideoTorrentAddActivityValid,
68 isVideoChannelCreateActivityValid, 76 isVideoChannelCreateActivityValid,
69 isVideoTorrentUpdateActivityValid, 77 isVideoTorrentUpdateActivityValid,
70 isVideoChannelUpdateActivityValid 78 isVideoChannelUpdateActivityValid,
79 isVideoChannelDeleteActivityValid,
80 isVideoTorrentDeleteActivityValid
71} 81}
72 82
73// --------------------------------------------------------------------------- 83// ---------------------------------------------------------------------------
diff --git a/server/helpers/custom-validators/index.ts b/server/helpers/custom-validators/index.ts
index 33922b8fe..1c475e301 100644
--- a/server/helpers/custom-validators/index.ts
+++ b/server/helpers/custom-validators/index.ts
@@ -3,6 +3,7 @@ export * from './misc'
3export * from './pods' 3export * from './pods'
4export * from './pods' 4export * from './pods'
5export * from './users' 5export * from './users'
6export * from './video-accounts' 6export * from './accounts'
7export * from './video-channels' 7export * from './video-channels'
8export * from './videos' 8export * from './videos'
9export * from './webfinger'
diff --git a/server/helpers/custom-validators/webfinger.ts b/server/helpers/custom-validators/webfinger.ts
new file mode 100644
index 000000000..e93115d81
--- /dev/null
+++ b/server/helpers/custom-validators/webfinger.ts
@@ -0,0 +1,25 @@
1import 'express-validator'
2import 'multer'
3import { CONFIG } from '../../initializers/constants'
4import { exists } from './misc'
5
6function isWebfingerResourceValid (value: string) {
7 if (!exists(value)) return false
8 if (value.startsWith('acct:') === false) return false
9
10 const accountWithHost = value.substr(5)
11 const accountParts = accountWithHost.split('@')
12 if (accountParts.length !== 2) return false
13
14 const host = accountParts[1]
15
16 if (host !== CONFIG.WEBSERVER.HOST) return false
17
18 return true
19}
20
21// ---------------------------------------------------------------------------
22
23export {
24 isWebfingerResourceValid
25}
diff --git a/server/helpers/webfinger.ts b/server/helpers/webfinger.ts
index 164ae4951..0155e5f3e 100644
--- a/server/helpers/webfinger.ts
+++ b/server/helpers/webfinger.ts
@@ -12,17 +12,20 @@ const webfinger = new WebFinger({
12 request_timeout: 3000 12 request_timeout: 3000
13}) 13})
14 14
15async function getAccountFromWebfinger (url: string) { 15async function getAccountFromWebfinger (nameWithHost: string) {
16 const webfingerData: WebFingerData = await webfingerLookup(url) 16 const webfingerData: WebFingerData = await webfingerLookup(nameWithHost)
17 17
18 if (Array.isArray(webfingerData.links) === false) return undefined 18 if (Array.isArray(webfingerData.links) === false) throw new Error('WebFinger links is not an array.')
19 19
20 const selfLink = webfingerData.links.find(l => l.rel === 'self') 20 const selfLink = webfingerData.links.find(l => l.rel === 'self')
21 if (selfLink === undefined || isActivityPubUrlValid(selfLink.href) === false) return undefined 21 if (selfLink === undefined || isActivityPubUrlValid(selfLink.href) === false) {
22 throw new Error('Cannot find self link or href is not a valid URL.')
23 }
22 24
23 const { account } = await fetchRemoteAccountAndCreatePod(selfLink.href) 25 const res = await fetchRemoteAccountAndCreatePod(selfLink.href)
26 if (res === undefined) throw new Error('Cannot fetch and create pod of remote account ' + selfLink.href)
24 27
25 return account 28 return res.account
26} 29}
27 30
28// --------------------------------------------------------------------------- 31// ---------------------------------------------------------------------------
@@ -33,12 +36,12 @@ export {
33 36
34// --------------------------------------------------------------------------- 37// ---------------------------------------------------------------------------
35 38
36function webfingerLookup (url: string) { 39function webfingerLookup (nameWithHost: string) {
37 return new Promise<WebFingerData>((res, rej) => { 40 return new Promise<WebFingerData>((res, rej) => {
38 webfinger.lookup(url, (err, p) => { 41 webfinger.lookup(nameWithHost, (err, p) => {
39 if (err) return rej(err) 42 if (err) return rej(err)
40 43
41 return p 44 return res(p.object)
42 }) 45 })
43 }) 46 })
44} 47}
diff --git a/server/initializers/checker.ts b/server/initializers/checker.ts
index b69188f7e..317d59423 100644
--- a/server/initializers/checker.ts
+++ b/server/initializers/checker.ts
@@ -1,8 +1,8 @@
1import * as config from 'config' 1import * as config from 'config'
2
3import { promisify0 } from '../helpers/core-utils' 2import { promisify0 } from '../helpers/core-utils'
4import { OAuthClientModel } from '../models/oauth/oauth-client-interface'
5import { UserModel } from '../models/account/user-interface' 3import { UserModel } from '../models/account/user-interface'
4import { ApplicationModel } from '../models/application/application-interface'
5import { OAuthClientModel } from '../models/oauth/oauth-client-interface'
6 6
7// Some checks on configuration files 7// Some checks on configuration files
8function checkConfig () { 8function checkConfig () {
@@ -70,6 +70,13 @@ async function usersExist (User: UserModel) {
70 return totalUsers !== 0 70 return totalUsers !== 0
71} 71}
72 72
73// We get db by param to not import it in this file (import orders)
74async function applicationExist (Application: ApplicationModel) {
75 const totalApplication = await Application.countTotal()
76
77 return totalApplication !== 0
78}
79
73// --------------------------------------------------------------------------- 80// ---------------------------------------------------------------------------
74 81
75export { 82export {
@@ -77,5 +84,6 @@ export {
77 checkFFmpeg, 84 checkFFmpeg,
78 checkMissedConfig, 85 checkMissedConfig,
79 clientsExist, 86 clientsExist,
80 usersExist 87 usersExist,
88 applicationExist
81} 89}
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index e27d011fa..4a49c1ab3 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -226,6 +226,9 @@ const FRIEND_SCORE = {
226 MAX: 1000 226 MAX: 1000
227} 227}
228 228
229const SERVER_ACCOUNT_NAME = 'peertube'
230const ACTIVITY_PUB_ACCEPT_HEADER = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
231
229const ACTIVITY_PUB = { 232const ACTIVITY_PUB = {
230 COLLECTION_ITEMS_PER_PAGE: 10, 233 COLLECTION_ITEMS_PER_PAGE: 10,
231 VIDEO_URL_MIME_TYPES: [ 234 VIDEO_URL_MIME_TYPES: [
@@ -352,8 +355,10 @@ export {
352 PODS_SCORE, 355 PODS_SCORE,
353 PREVIEWS_SIZE, 356 PREVIEWS_SIZE,
354 REMOTE_SCHEME, 357 REMOTE_SCHEME,
358 ACTIVITY_PUB_ACCEPT_HEADER,
355 FOLLOW_STATES, 359 FOLLOW_STATES,
356 SEARCHABLE_COLUMNS, 360 SEARCHABLE_COLUMNS,
361 SERVER_ACCOUNT_NAME,
357 PRIVATE_RSA_KEY_SIZE, 362 PRIVATE_RSA_KEY_SIZE,
358 SORTABLE_COLUMNS, 363 SORTABLE_COLUMNS,
359 STATIC_MAX_AGE, 364 STATIC_MAX_AGE,
diff --git a/server/initializers/installer.ts b/server/initializers/installer.ts
index 5221b81a5..c3521a9e4 100644
--- a/server/initializers/installer.ts
+++ b/server/initializers/installer.ts
@@ -3,8 +3,8 @@ import { UserRole } from '../../shared'
3import { logger, mkdirpPromise, rimrafPromise } from '../helpers' 3import { logger, mkdirpPromise, rimrafPromise } from '../helpers'
4import { createUserAccountAndChannel } from '../lib' 4import { createUserAccountAndChannel } from '../lib'
5import { createLocalAccount } from '../lib/user' 5import { createLocalAccount } from '../lib/user'
6import { clientsExist, usersExist } from './checker' 6import { applicationExist, clientsExist, usersExist } from './checker'
7import { CACHE, CONFIG, LAST_MIGRATION_VERSION } from './constants' 7import { CACHE, CONFIG, LAST_MIGRATION_VERSION, SERVER_ACCOUNT_NAME } from './constants'
8 8
9import { database as db } from './database' 9import { database as db } from './database'
10 10
@@ -128,9 +128,13 @@ async function createOAuthAdminIfNotExist () {
128} 128}
129 129
130async function createApplicationIfNotExist () { 130async function createApplicationIfNotExist () {
131 const exist = await applicationExist(db.Application)
132 // Nothing to do, application already exist
133 if (exist === true) return undefined
134
131 logger.info('Creating Application table.') 135 logger.info('Creating Application table.')
132 const applicationInstance = await db.Application.create({ migrationVersion: LAST_MIGRATION_VERSION }) 136 const applicationInstance = await db.Application.create({ migrationVersion: LAST_MIGRATION_VERSION })
133 137
134 logger.info('Creating application account.') 138 logger.info('Creating application account.')
135 return createLocalAccount('peertube', null, applicationInstance.id, undefined) 139 return createLocalAccount(SERVER_ACCOUNT_NAME, null, applicationInstance.id, undefined)
136} 140}
diff --git a/server/lib/activitypub/process-add.ts b/server/lib/activitypub/process-add.ts
index 024dee559..06d23a2ea 100644
--- a/server/lib/activitypub/process-add.ts
+++ b/server/lib/activitypub/process-add.ts
@@ -54,7 +54,7 @@ async function addRemoteVideo (account: AccountInstance, videoChannelUrl: string
54 54
55 // Don't block on request 55 // Don't block on request
56 generateThumbnailFromUrl(video, videoToCreateData.icon) 56 generateThumbnailFromUrl(video, videoToCreateData.icon)
57 .catch(err => logger.warning('Cannot generate thumbnail of %s.', videoToCreateData.id, err)) 57 .catch(err => logger.warn('Cannot generate thumbnail of %s.', videoToCreateData.id, err))
58 58
59 const videoCreated = await video.save(sequelizeOptions) 59 const videoCreated = await video.save(sequelizeOptions)
60 60
diff --git a/server/lib/activitypub/process-follow.ts b/server/lib/activitypub/process-follow.ts
index ee5d97a0b..a805c0757 100644
--- a/server/lib/activitypub/process-follow.ts
+++ b/server/lib/activitypub/process-follow.ts
@@ -36,14 +36,18 @@ async function follow (account: AccountInstance, targetAccountURL: string) {
36 if (targetAccount === undefined) throw new Error('Unknown account') 36 if (targetAccount === undefined) throw new Error('Unknown account')
37 if (targetAccount.isOwned() === false) throw new Error('This is not a local account.') 37 if (targetAccount.isOwned() === false) throw new Error('This is not a local account.')
38 38
39 const sequelizeOptions = { 39 await db.AccountFollow.findOrCreate({
40 where: {
41 accountId: account.id,
42 targetAccountId: targetAccount.id
43 },
44 defaults: {
45 accountId: account.id,
46 targetAccountId: targetAccount.id,
47 state: 'accepted'
48 },
40 transaction: t 49 transaction: t
41 } 50 })
42 await db.AccountFollow.create({
43 accountId: account.id,
44 targetAccountId: targetAccount.id,
45 state: 'accepted'
46 }, sequelizeOptions)
47 51
48 // Target sends to account he accepted the follow request 52 // Target sends to account he accepted the follow request
49 return sendAccept(targetAccount, account, t) 53 return sendAccept(targetAccount, account, t)
diff --git a/server/lib/activitypub/send-request.ts b/server/lib/activitypub/send-request.ts
index c18a69784..d47040d6d 100644
--- a/server/lib/activitypub/send-request.ts
+++ b/server/lib/activitypub/send-request.ts
@@ -10,60 +10,60 @@ import { httpRequestJobScheduler } from '../jobs'
10import { signObject, activityPubContextify } from '../../helpers' 10import { signObject, activityPubContextify } from '../../helpers'
11import { Activity } from '../../../shared' 11import { Activity } from '../../../shared'
12 12
13function sendCreateVideoChannel (videoChannel: VideoChannelInstance, t: Sequelize.Transaction) { 13async function sendCreateVideoChannel (videoChannel: VideoChannelInstance, t: Sequelize.Transaction) {
14 const videoChannelObject = videoChannel.toActivityPubObject() 14 const videoChannelObject = videoChannel.toActivityPubObject()
15 const data = createActivityData(videoChannel.url, videoChannel.Account, videoChannelObject) 15 const data = await createActivityData(videoChannel.url, videoChannel.Account, videoChannelObject)
16 16
17 return broadcastToFollowers(data, videoChannel.Account, t) 17 return broadcastToFollowers(data, videoChannel.Account, t)
18} 18}
19 19
20function sendUpdateVideoChannel (videoChannel: VideoChannelInstance, t: Sequelize.Transaction) { 20async function sendUpdateVideoChannel (videoChannel: VideoChannelInstance, t: Sequelize.Transaction) {
21 const videoChannelObject = videoChannel.toActivityPubObject() 21 const videoChannelObject = videoChannel.toActivityPubObject()
22 const data = updateActivityData(videoChannel.url, videoChannel.Account, videoChannelObject) 22 const data = await updateActivityData(videoChannel.url, videoChannel.Account, videoChannelObject)
23 23
24 return broadcastToFollowers(data, videoChannel.Account, t) 24 return broadcastToFollowers(data, videoChannel.Account, t)
25} 25}
26 26
27function sendDeleteVideoChannel (videoChannel: VideoChannelInstance, t: Sequelize.Transaction) { 27async function sendDeleteVideoChannel (videoChannel: VideoChannelInstance, t: Sequelize.Transaction) {
28 const data = deleteActivityData(videoChannel.url, videoChannel.Account) 28 const data = await deleteActivityData(videoChannel.url, videoChannel.Account)
29 29
30 return broadcastToFollowers(data, videoChannel.Account, t) 30 return broadcastToFollowers(data, videoChannel.Account, t)
31} 31}
32 32
33function sendAddVideo (video: VideoInstance, t: Sequelize.Transaction) { 33async function sendAddVideo (video: VideoInstance, t: Sequelize.Transaction) {
34 const videoObject = video.toActivityPubObject() 34 const videoObject = video.toActivityPubObject()
35 const data = addActivityData(video.url, video.VideoChannel.Account, video.VideoChannel.url, videoObject) 35 const data = await addActivityData(video.url, video.VideoChannel.Account, video.VideoChannel.url, videoObject)
36 36
37 return broadcastToFollowers(data, video.VideoChannel.Account, t) 37 return broadcastToFollowers(data, video.VideoChannel.Account, t)
38} 38}
39 39
40function sendUpdateVideo (video: VideoInstance, t: Sequelize.Transaction) { 40async function sendUpdateVideo (video: VideoInstance, t: Sequelize.Transaction) {
41 const videoObject = video.toActivityPubObject() 41 const videoObject = video.toActivityPubObject()
42 const data = updateActivityData(video.url, video.VideoChannel.Account, videoObject) 42 const data = await updateActivityData(video.url, video.VideoChannel.Account, videoObject)
43 43
44 return broadcastToFollowers(data, video.VideoChannel.Account, t) 44 return broadcastToFollowers(data, video.VideoChannel.Account, t)
45} 45}
46 46
47function sendDeleteVideo (video: VideoInstance, t: Sequelize.Transaction) { 47async function sendDeleteVideo (video: VideoInstance, t: Sequelize.Transaction) {
48 const data = deleteActivityData(video.url, video.VideoChannel.Account) 48 const data = await deleteActivityData(video.url, video.VideoChannel.Account)
49 49
50 return broadcastToFollowers(data, video.VideoChannel.Account, t) 50 return broadcastToFollowers(data, video.VideoChannel.Account, t)
51} 51}
52 52
53function sendDeleteAccount (account: AccountInstance, t: Sequelize.Transaction) { 53async function sendDeleteAccount (account: AccountInstance, t: Sequelize.Transaction) {
54 const data = deleteActivityData(account.url, account) 54 const data = await deleteActivityData(account.url, account)
55 55
56 return broadcastToFollowers(data, account, t) 56 return broadcastToFollowers(data, account, t)
57} 57}
58 58
59function sendAccept (fromAccount: AccountInstance, toAccount: AccountInstance, t: Sequelize.Transaction) { 59async function sendAccept (fromAccount: AccountInstance, toAccount: AccountInstance, t: Sequelize.Transaction) {
60 const data = acceptActivityData(fromAccount) 60 const data = await acceptActivityData(fromAccount)
61 61
62 return unicastTo(data, toAccount, t) 62 return unicastTo(data, toAccount, t)
63} 63}
64 64
65function sendFollow (fromAccount: AccountInstance, toAccount: AccountInstance, t: Sequelize.Transaction) { 65async function sendFollow (fromAccount: AccountInstance, toAccount: AccountInstance, t: Sequelize.Transaction) {
66 const data = followActivityData(toAccount.url, fromAccount) 66 const data = await followActivityData(toAccount.url, fromAccount)
67 67
68 return unicastTo(data, toAccount, t) 68 return unicastTo(data, toAccount, t)
69} 69}
@@ -97,7 +97,7 @@ async function broadcastToFollowers (data: any, fromAccount: AccountInstance, t:
97 97
98async function unicastTo (data: any, toAccount: AccountInstance, t: Sequelize.Transaction) { 98async function unicastTo (data: any, toAccount: AccountInstance, t: Sequelize.Transaction) {
99 const jobPayload = { 99 const jobPayload = {
100 uris: [ toAccount.url ], 100 uris: [ toAccount.inboxUrl ],
101 body: data 101 body: data
102 } 102 }
103 103
diff --git a/server/lib/jobs/http-request-job-scheduler/http-request-broadcast-handler.ts b/server/lib/jobs/http-request-job-scheduler/http-request-broadcast-handler.ts
index 799b86e1c..2f1d9ee92 100644
--- a/server/lib/jobs/http-request-job-scheduler/http-request-broadcast-handler.ts
+++ b/server/lib/jobs/http-request-job-scheduler/http-request-broadcast-handler.ts
@@ -6,6 +6,7 @@ async function process (payload: HTTPRequestPayload, jobId: number) {
6 logger.info('Processing broadcast in job %d.', jobId) 6 logger.info('Processing broadcast in job %d.', jobId)
7 7
8 const options = { 8 const options = {
9 method: 'POST',
9 uri: '', 10 uri: '',
10 json: payload.body 11 json: payload.body
11 } 12 }
diff --git a/server/lib/jobs/http-request-job-scheduler/http-request-unicast-handler.ts b/server/lib/jobs/http-request-job-scheduler/http-request-unicast-handler.ts
index 13451f042..3a1a7fabf 100644
--- a/server/lib/jobs/http-request-job-scheduler/http-request-unicast-handler.ts
+++ b/server/lib/jobs/http-request-job-scheduler/http-request-unicast-handler.ts
@@ -7,6 +7,7 @@ async function process (payload: HTTPRequestPayload, jobId: number) {
7 7
8 const uri = payload.uris[0] 8 const uri = payload.uris[0]
9 const options = { 9 const options = {
10 method: 'POST',
10 uri, 11 uri,
11 json: payload.body 12 json: payload.body
12 } 13 }
diff --git a/server/lib/jobs/job-scheduler.ts b/server/lib/jobs/job-scheduler.ts
index f10f745b3..b25bb7ab3 100644
--- a/server/lib/jobs/job-scheduler.ts
+++ b/server/lib/jobs/job-scheduler.ts
@@ -4,6 +4,7 @@ import { JobCategory } from '../../../shared'
4import { logger } from '../../helpers' 4import { logger } from '../../helpers'
5import { database as db, JOB_STATES, JOBS_FETCH_LIMIT_PER_CYCLE, JOBS_FETCHING_INTERVAL } from '../../initializers' 5import { database as db, JOB_STATES, JOBS_FETCH_LIMIT_PER_CYCLE, JOBS_FETCHING_INTERVAL } from '../../initializers'
6import { JobInstance } from '../../models' 6import { JobInstance } from '../../models'
7import { error } from 'util'
7 8
8export interface JobHandler<P, T> { 9export interface JobHandler<P, T> {
9 process (data: object, jobId: number): Promise<T> 10 process (data: object, jobId: number): Promise<T>
@@ -80,8 +81,12 @@ class JobScheduler<P, T> {
80 private async processJob (job: JobInstance, callback: (err: Error) => void) { 81 private async processJob (job: JobInstance, callback: (err: Error) => void) {
81 const jobHandler = this.jobHandlers[job.handlerName] 82 const jobHandler = this.jobHandlers[job.handlerName]
82 if (jobHandler === undefined) { 83 if (jobHandler === undefined) {
83 logger.error('Unknown job handler for job %s.', job.handlerName) 84 const errorString = 'Unknown job handler ' + job.handlerName + ' for job ' + job.id
84 return callback(null) 85 logger.error(errorString)
86
87 const error = new Error(errorString)
88 await this.onJobError(jobHandler, job, error)
89 return callback(error)
85 } 90 }
86 91
87 logger.info('Processing job %d with handler %s.', job.id, job.handlerName) 92 logger.info('Processing job %d with handler %s.', job.id, job.handlerName)
@@ -103,7 +108,7 @@ class JobScheduler<P, T> {
103 } 108 }
104 } 109 }
105 110
106 callback(null) 111 return callback(null)
107 } 112 }
108 113
109 private async onJobError (jobHandler: JobHandler<P, T>, job: JobInstance, err: Error) { 114 private async onJobError (jobHandler: JobHandler<P, T>, job: JobInstance, err: Error) {
@@ -111,7 +116,7 @@ class JobScheduler<P, T> {
111 116
112 try { 117 try {
113 await job.save() 118 await job.save()
114 await jobHandler.onError(err, job.id) 119 if (jobHandler) await jobHandler.onError(err, job.id)
115 } catch (err) { 120 } catch (err) {
116 this.cannotSaveJobError(err) 121 this.cannotSaveJobError(err)
117 } 122 }
diff --git a/server/middlewares/activitypub.ts b/server/middlewares/activitypub.ts
index 6cf8eea6f..bed2bfeab 100644
--- a/server/middlewares/activitypub.ts
+++ b/server/middlewares/activitypub.ts
@@ -1,12 +1,9 @@
1import { Request, Response, NextFunction } from 'express' 1import { NextFunction, Request, Response, RequestHandler } from 'express'
2
3import { database as db } from '../initializers'
4import {
5 logger,
6 getAccountFromWebfinger,
7 isSignatureVerified
8} from '../helpers'
9import { ActivityPubSignature } from '../../shared' 2import { ActivityPubSignature } from '../../shared'
3import { isSignatureVerified, logger } from '../helpers'
4import { fetchRemoteAccountAndCreatePod } from '../helpers/activitypub'
5import { database as db, ACTIVITY_PUB_ACCEPT_HEADER } from '../initializers'
6import { each, eachSeries, waterfall } from 'async'
10 7
11async function checkSignature (req: Request, res: Response, next: NextFunction) { 8async function checkSignature (req: Request, res: Response, next: NextFunction) {
12 const signatureObject: ActivityPubSignature = req.body.signature 9 const signatureObject: ActivityPubSignature = req.body.signature
@@ -17,35 +14,40 @@ async function checkSignature (req: Request, res: Response, next: NextFunction)
17 14
18 // We don't have this account in our database, fetch it on remote 15 // We don't have this account in our database, fetch it on remote
19 if (!account) { 16 if (!account) {
20 account = await getAccountFromWebfinger(signatureObject.creator) 17 const accountResult = await fetchRemoteAccountAndCreatePod(signatureObject.creator)
21 18
22 if (!account) { 19 if (!accountResult) {
23 return res.sendStatus(403) 20 return res.sendStatus(403)
24 } 21 }
25 22
26 // Save our new account in database 23 // Save our new account in database
24 account = accountResult.account
27 await account.save() 25 await account.save()
28 } 26 }
29 27
30 const verified = await isSignatureVerified(account, req.body) 28 const verified = await isSignatureVerified(account, req.body)
31 if (verified === false) return res.sendStatus(403) 29 if (verified === false) return res.sendStatus(403)
32 30
33 res.locals.signature.account = account 31 res.locals.signature = {
32 account
33 }
34 34
35 return next() 35 return next()
36} 36}
37 37
38function executeIfActivityPub (fun: any | any[]) { 38function executeIfActivityPub (fun: RequestHandler | RequestHandler[]) {
39 return (req: Request, res: Response, next: NextFunction) => { 39 return (req: Request, res: Response, next: NextFunction) => {
40 if (req.header('Accept') !== 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') { 40 if (req.header('Accept') !== ACTIVITY_PUB_ACCEPT_HEADER) {
41 return next() 41 return next()
42 } 42 }
43 43
44 if (Array.isArray(fun) === true) { 44 if (Array.isArray(fun) === true) {
45 fun[0](req, res, next) // FIXME: doesn't work 45 return eachSeries(fun as RequestHandler[], (f, cb) => {
46 f(req, res, cb)
47 }, next)
46 } 48 }
47 49
48 return fun(req, res, next) 50 return (fun as RequestHandler)(req, res, next)
49 } 51 }
50} 52}
51 53
diff --git a/server/middlewares/validators/account.ts b/server/middlewares/validators/account.ts
index 3ccf2ea21..58eeed3cc 100644
--- a/server/middlewares/validators/account.ts
+++ b/server/middlewares/validators/account.ts
@@ -8,13 +8,13 @@ import {
8 isUserVideoQuotaValid, 8 isUserVideoQuotaValid,
9 logger 9 logger
10} from '../../helpers' 10} from '../../helpers'
11import { isAccountNameWithHostValid } from '../../helpers/custom-validators/video-accounts' 11import { isAccountNameValid } from '../../helpers/custom-validators/accounts'
12import { database as db } from '../../initializers/database' 12import { database as db } from '../../initializers/database'
13import { AccountInstance } from '../../models' 13import { AccountInstance } from '../../models'
14import { checkErrors } from './utils' 14import { checkErrors } from './utils'
15 15
16const localAccountValidator = [ 16const localAccountValidator = [
17 param('nameWithHost').custom(isAccountNameWithHostValid).withMessage('Should have a valid account with domain name (myuser@domain.tld)'), 17 param('name').custom(isAccountNameValid).withMessage('Should have a valid account name'),
18 18
19 (req: express.Request, res: express.Response, next: express.NextFunction) => { 19 (req: express.Request, res: express.Response, next: express.NextFunction) => {
20 logger.debug('Checking localAccountValidator parameters', { parameters: req.params }) 20 logger.debug('Checking localAccountValidator parameters', { parameters: req.params })
@@ -33,10 +33,8 @@ export {
33 33
34// --------------------------------------------------------------------------- 34// ---------------------------------------------------------------------------
35 35
36function checkLocalAccountExists (nameWithHost: string, res: express.Response, callback: (err: Error, account: AccountInstance) => void) { 36function checkLocalAccountExists (name: string, res: express.Response, callback: (err: Error, account: AccountInstance) => void) {
37 const [ name, host ] = nameWithHost.split('@') 37 db.Account.loadLocalByName(name)
38
39 db.Account.loadLocalAccountByNameAndPod(name, host)
40 .then(account => { 38 .then(account => {
41 if (!account) { 39 if (!account) {
42 return res.status(404) 40 return res.status(404)
diff --git a/server/middlewares/validators/activitypub/activity.ts b/server/middlewares/validators/activitypub/activity.ts
index 78a6d1444..0de8b2d85 100644
--- a/server/middlewares/validators/activitypub/activity.ts
+++ b/server/middlewares/validators/activitypub/activity.ts
@@ -1,11 +1,10 @@
1import { body } from 'express-validator/check'
2import * as express from 'express' 1import * as express from 'express'
3 2import { body } from 'express-validator/check'
4import { logger, isRootActivityValid } from '../../../helpers' 3import { isRootActivityValid, logger } from '../../../helpers'
5import { checkErrors } from '../utils' 4import { checkErrors } from '../utils'
6 5
7const activityPubValidator = [ 6const activityPubValidator = [
8 body('data').custom(isRootActivityValid), 7 body('').custom((value, { req }) => isRootActivityValid(req.body)),
9 8
10 (req: express.Request, res: express.Response, next: express.NextFunction) => { 9 (req: express.Request, res: express.Response, next: express.NextFunction) => {
11 logger.debug('Checking activity pub parameters', { parameters: req.body }) 10 logger.debug('Checking activity pub parameters', { parameters: req.body })
diff --git a/server/middlewares/validators/index.ts b/server/middlewares/validators/index.ts
index 46c00d679..92a4bad28 100644
--- a/server/middlewares/validators/index.ts
+++ b/server/middlewares/validators/index.ts
@@ -8,3 +8,4 @@ export * from './users'
8export * from './videos' 8export * from './videos'
9export * from './video-blacklist' 9export * from './video-blacklist'
10export * from './video-channels' 10export * from './video-channels'
11export * from './webfinger'
diff --git a/server/middlewares/validators/webfinger.ts b/server/middlewares/validators/webfinger.ts
new file mode 100644
index 000000000..068e03ad7
--- /dev/null
+++ b/server/middlewares/validators/webfinger.ts
@@ -0,0 +1,42 @@
1import { query } from 'express-validator/check'
2import * as express from 'express'
3
4import { checkErrors } from './utils'
5import { logger, isWebfingerResourceValid } from '../../helpers'
6import { database as db } from '../../initializers'
7
8const webfingerValidator = [
9 query('resource').custom(isWebfingerResourceValid).withMessage('Should have a valid webfinger resource'),
10
11 (req: express.Request, res: express.Response, next: express.NextFunction) => {
12 logger.debug('Checking webfinger parameters', { parameters: req.query })
13
14 checkErrors(req, res, () => {
15 // Remove 'acct:' from the beginning of the string
16 const nameWithHost = req.query.resource.substr(5)
17 const [ name, ] = nameWithHost.split('@')
18
19 db.Account.loadLocalByName(name)
20 .then(account => {
21 if (!account) {
22 return res.status(404)
23 .send({ error: 'Account not found' })
24 .end()
25 }
26
27 res.locals.account = account
28 return next()
29 })
30 .catch(err => {
31 logger.error('Error in webfinger validator.', err)
32 return res.sendStatus(500)
33 })
34 })
35 }
36]
37
38// ---------------------------------------------------------------------------
39
40export {
41 webfingerValidator
42}
diff --git a/server/models/account/account-follow.ts b/server/models/account/account-follow.ts
index e6abc893a..7c129ab9d 100644
--- a/server/models/account/account-follow.ts
+++ b/server/models/account/account-follow.ts
@@ -19,11 +19,13 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
19 { 19 {
20 indexes: [ 20 indexes: [
21 { 21 {
22 fields: [ 'accountId' ], 22 fields: [ 'accountId' ]
23 unique: true 23 },
24 {
25 fields: [ 'targetAccountId' ]
24 }, 26 },
25 { 27 {
26 fields: [ 'targetAccountId' ], 28 fields: [ 'accountId', 'targetAccountId' ],
27 unique: true 29 unique: true
28 } 30 }
29 ] 31 ]
@@ -31,7 +33,8 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
31 ) 33 )
32 34
33 const classMethods = [ 35 const classMethods = [
34 associate 36 associate,
37 loadByAccountAndTarget
35 ] 38 ]
36 addMethodsToModel(AccountFollow, classMethods) 39 addMethodsToModel(AccountFollow, classMethods)
37 40
@@ -46,7 +49,7 @@ function associate (models) {
46 name: 'accountId', 49 name: 'accountId',
47 allowNull: false 50 allowNull: false
48 }, 51 },
49 as: 'followers', 52 as: 'accountFollowers',
50 onDelete: 'CASCADE' 53 onDelete: 'CASCADE'
51 }) 54 })
52 55
@@ -55,7 +58,7 @@ function associate (models) {
55 name: 'targetAccountId', 58 name: 'targetAccountId',
56 allowNull: false 59 allowNull: false
57 }, 60 },
58 as: 'following', 61 as: 'accountFollowing',
59 onDelete: 'CASCADE' 62 onDelete: 'CASCADE'
60 }) 63 })
61} 64}
diff --git a/server/models/account/account-interface.ts b/server/models/account/account-interface.ts
index 2468dc6e1..6fc36ae9d 100644
--- a/server/models/account/account-interface.ts
+++ b/server/models/account/account-interface.ts
@@ -12,7 +12,8 @@ export namespace AccountMethods {
12 export type LoadByUUID = (uuid: string) => Bluebird<AccountInstance> 12 export type LoadByUUID = (uuid: string) => Bluebird<AccountInstance>
13 export type LoadByUrl = (url: string, transaction?: Sequelize.Transaction) => Bluebird<AccountInstance> 13 export type LoadByUrl = (url: string, transaction?: Sequelize.Transaction) => Bluebird<AccountInstance>
14 export type LoadAccountByPodAndUUID = (uuid: string, podId: number, transaction: Sequelize.Transaction) => Bluebird<AccountInstance> 14 export type LoadAccountByPodAndUUID = (uuid: string, podId: number, transaction: Sequelize.Transaction) => Bluebird<AccountInstance>
15 export type LoadLocalAccountByNameAndPod = (name: string, host: string) => Bluebird<AccountInstance> 15 export type LoadLocalByName = (name: string) => Bluebird<AccountInstance>
16 export type LoadByNameAndHost = (name: string, host: string) => Bluebird<AccountInstance>
16 export type ListOwned = () => Bluebird<AccountInstance[]> 17 export type ListOwned = () => Bluebird<AccountInstance[]>
17 export type ListAcceptedFollowerUrlsForApi = (id: number, start: number, count?: number) => Promise< ResultList<string> > 18 export type ListAcceptedFollowerUrlsForApi = (id: number, start: number, count?: number) => Promise< ResultList<string> >
18 export type ListAcceptedFollowingUrlsForApi = (id: number, start: number, count?: number) => Promise< ResultList<string> > 19 export type ListAcceptedFollowingUrlsForApi = (id: number, start: number, count?: number) => Promise< ResultList<string> >
@@ -34,7 +35,8 @@ export interface AccountClass {
34 load: AccountMethods.Load 35 load: AccountMethods.Load
35 loadByUUID: AccountMethods.LoadByUUID 36 loadByUUID: AccountMethods.LoadByUUID
36 loadByUrl: AccountMethods.LoadByUrl 37 loadByUrl: AccountMethods.LoadByUrl
37 loadLocalAccountByNameAndPod: AccountMethods.LoadLocalAccountByNameAndPod 38 loadLocalByName: AccountMethods.LoadLocalByName
39 loadByNameAndHost: AccountMethods.LoadByNameAndHost
38 listOwned: AccountMethods.ListOwned 40 listOwned: AccountMethods.ListOwned
39 listAcceptedFollowerUrlsForApi: AccountMethods.ListAcceptedFollowerUrlsForApi 41 listAcceptedFollowerUrlsForApi: AccountMethods.ListAcceptedFollowerUrlsForApi
40 listAcceptedFollowingUrlsForApi: AccountMethods.ListAcceptedFollowingUrlsForApi 42 listAcceptedFollowingUrlsForApi: AccountMethods.ListAcceptedFollowingUrlsForApi
diff --git a/server/models/account/account.ts b/server/models/account/account.ts
index cd6c822f1..d2293a939 100644
--- a/server/models/account/account.ts
+++ b/server/models/account/account.ts
@@ -31,7 +31,8 @@ let load: AccountMethods.Load
31let loadApplication: AccountMethods.LoadApplication 31let loadApplication: AccountMethods.LoadApplication
32let loadByUUID: AccountMethods.LoadByUUID 32let loadByUUID: AccountMethods.LoadByUUID
33let loadByUrl: AccountMethods.LoadByUrl 33let loadByUrl: AccountMethods.LoadByUrl
34let loadLocalAccountByNameAndPod: AccountMethods.LoadLocalAccountByNameAndPod 34let loadLocalByName: AccountMethods.LoadLocalByName
35let loadByNameAndHost: AccountMethods.LoadByNameAndHost
35let listOwned: AccountMethods.ListOwned 36let listOwned: AccountMethods.ListOwned
36let listAcceptedFollowerUrlsForApi: AccountMethods.ListAcceptedFollowerUrlsForApi 37let listAcceptedFollowerUrlsForApi: AccountMethods.ListAcceptedFollowerUrlsForApi
37let listAcceptedFollowingUrlsForApi: AccountMethods.ListAcceptedFollowingUrlsForApi 38let listAcceptedFollowingUrlsForApi: AccountMethods.ListAcceptedFollowingUrlsForApi
@@ -88,7 +89,7 @@ export default function defineAccount (sequelize: Sequelize.Sequelize, DataTypes
88 }, 89 },
89 privateKey: { 90 privateKey: {
90 type: DataTypes.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.PRIVATE_KEY.max), 91 type: DataTypes.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.PRIVATE_KEY.max),
91 allowNull: false, 92 allowNull: true,
92 validate: { 93 validate: {
93 privateKeyValid: value => { 94 privateKeyValid: value => {
94 const res = isAccountPrivateKeyValid(value) 95 const res = isAccountPrivateKeyValid(value)
@@ -199,7 +200,8 @@ export default function defineAccount (sequelize: Sequelize.Sequelize, DataTypes
199 load, 200 load,
200 loadByUUID, 201 loadByUUID,
201 loadByUrl, 202 loadByUrl,
202 loadLocalAccountByNameAndPod, 203 loadLocalByName,
204 loadByNameAndHost,
203 listOwned, 205 listOwned,
204 listAcceptedFollowerUrlsForApi, 206 listAcceptedFollowerUrlsForApi,
205 listAcceptedFollowingUrlsForApi, 207 listAcceptedFollowingUrlsForApi,
@@ -330,6 +332,8 @@ getFollowerSharedInboxUrls = function (this: AccountInstance) {
330 include: [ 332 include: [
331 { 333 {
332 model: Account['sequelize'].models.AccountFollow, 334 model: Account['sequelize'].models.AccountFollow,
335 required: true,
336 as: 'followers',
333 where: { 337 where: {
334 targetAccountId: this.id 338 targetAccountId: this.id
335 } 339 }
@@ -387,7 +391,7 @@ listFollowingForApi = function (id: number, start: number, count: number, sort:
387 include: [ 391 include: [
388 { 392 {
389 model: Account['sequelize'].models.Account, 393 model: Account['sequelize'].models.Account,
390 as: 'following', 394 as: 'accountFollowing',
391 required: true, 395 required: true,
392 include: [ Account['sequelize'].models.Pod ] 396 include: [ Account['sequelize'].models.Pod ]
393 } 397 }
@@ -418,7 +422,7 @@ listFollowersForApi = function (id: number, start: number, count: number, sort:
418 include: [ 422 include: [
419 { 423 {
420 model: Account['sequelize'].models.Account, 424 model: Account['sequelize'].models.Account,
421 as: 'followers', 425 as: 'accountFollowers',
422 required: true, 426 required: true,
423 include: [ Account['sequelize'].models.Pod ] 427 include: [ Account['sequelize'].models.Pod ]
424 } 428 }
@@ -439,7 +443,7 @@ loadApplication = function () {
439 return Account.findOne({ 443 return Account.findOne({
440 include: [ 444 include: [
441 { 445 {
442 model: Account['sequelize'].model.Application, 446 model: Account['sequelize'].models.Application,
443 required: true 447 required: true
444 } 448 }
445 ] 449 ]
@@ -460,17 +464,37 @@ loadByUUID = function (uuid: string) {
460 return Account.findOne(query) 464 return Account.findOne(query)
461} 465}
462 466
463loadLocalAccountByNameAndPod = function (name: string, host: string) { 467loadLocalByName = function (name: string) {
464 const query: Sequelize.FindOptions<AccountAttributes> = { 468 const query: Sequelize.FindOptions<AccountAttributes> = {
465 where: { 469 where: {
466 name, 470 name,
467 userId: { 471 [Sequelize.Op.or]: [
468 [Sequelize.Op.ne]: null 472 {
469 } 473 userId: {
474 [Sequelize.Op.ne]: null
475 }
476 },
477 {
478 applicationId: {
479 [Sequelize.Op.ne]: null
480 }
481 }
482 ]
483 }
484 }
485
486 return Account.findOne(query)
487}
488
489loadByNameAndHost = function (name: string, host: string) {
490 const query: Sequelize.FindOptions<AccountAttributes> = {
491 where: {
492 name
470 }, 493 },
471 include: [ 494 include: [
472 { 495 {
473 model: Account['sequelize'].models.Pod, 496 model: Account['sequelize'].models.Pod,
497 required: true,
474 where: { 498 where: {
475 host 499 host
476 } 500 }
diff --git a/server/models/application/application-interface.ts b/server/models/application/application-interface.ts
index 33254ba2d..2c391dba3 100644
--- a/server/models/application/application-interface.ts
+++ b/server/models/application/application-interface.ts
@@ -1,18 +1,21 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird' 2import * as Bluebird from 'bluebird'
3 3
4export namespace ApplicationMethods { 4export namespace ApplicationMethods {
5 export type LoadMigrationVersion = () => Promise<number> 5 export type LoadMigrationVersion = () => Bluebird<number>
6 6
7 export type UpdateMigrationVersion = ( 7 export type UpdateMigrationVersion = (
8 newVersion: number, 8 newVersion: number,
9 transaction: Sequelize.Transaction 9 transaction: Sequelize.Transaction
10 ) => Promise<[ number, ApplicationInstance[] ]> 10 ) => Bluebird<[ number, ApplicationInstance[] ]>
11
12 export type CountTotal = () => Bluebird<number>
11} 13}
12 14
13export interface ApplicationClass { 15export interface ApplicationClass {
14 loadMigrationVersion: ApplicationMethods.LoadMigrationVersion 16 loadMigrationVersion: ApplicationMethods.LoadMigrationVersion
15 updateMigrationVersion: ApplicationMethods.UpdateMigrationVersion 17 updateMigrationVersion: ApplicationMethods.UpdateMigrationVersion
18 countTotal: ApplicationMethods.CountTotal
16} 19}
17 20
18export interface ApplicationAttributes { 21export interface ApplicationAttributes {
diff --git a/server/models/application/application.ts b/server/models/application/application.ts
index 507b7a843..8ba40a895 100644
--- a/server/models/application/application.ts
+++ b/server/models/application/application.ts
@@ -11,6 +11,7 @@ import {
11let Application: Sequelize.Model<ApplicationInstance, ApplicationAttributes> 11let Application: Sequelize.Model<ApplicationInstance, ApplicationAttributes>
12let loadMigrationVersion: ApplicationMethods.LoadMigrationVersion 12let loadMigrationVersion: ApplicationMethods.LoadMigrationVersion
13let updateMigrationVersion: ApplicationMethods.UpdateMigrationVersion 13let updateMigrationVersion: ApplicationMethods.UpdateMigrationVersion
14let countTotal: ApplicationMethods.CountTotal
14 15
15export default function defineApplication (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { 16export default function defineApplication (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
16 Application = sequelize.define<ApplicationInstance, ApplicationAttributes>('Application', 17 Application = sequelize.define<ApplicationInstance, ApplicationAttributes>('Application',
@@ -26,7 +27,11 @@ export default function defineApplication (sequelize: Sequelize.Sequelize, DataT
26 } 27 }
27 ) 28 )
28 29
29 const classMethods = [ loadMigrationVersion, updateMigrationVersion ] 30 const classMethods = [
31 countTotal,
32 loadMigrationVersion,
33 updateMigrationVersion
34 ]
30 addMethodsToModel(Application, classMethods) 35 addMethodsToModel(Application, classMethods)
31 36
32 return Application 37 return Application
@@ -34,6 +39,10 @@ export default function defineApplication (sequelize: Sequelize.Sequelize, DataT
34 39
35// --------------------------------------------------------------------------- 40// ---------------------------------------------------------------------------
36 41
42countTotal = function () {
43 return this.count()
44}
45
37loadMigrationVersion = function () { 46loadMigrationVersion = function () {
38 const query = { 47 const query = {
39 attributes: [ 'migrationVersion' ] 48 attributes: [ 'migrationVersion' ]
diff --git a/server/models/job/job.ts b/server/models/job/job.ts
index ce1203e5a..c2d088090 100644
--- a/server/models/job/job.ts
+++ b/server/models/job/job.ts
@@ -10,7 +10,7 @@ import {
10 10
11 JobMethods 11 JobMethods
12} from './job-interface' 12} from './job-interface'
13import { JobState } from '../../../shared/models/job.model' 13import { JobCategory, JobState } from '../../../shared/models/job.model'
14 14
15let Job: Sequelize.Model<JobInstance, JobAttributes> 15let Job: Sequelize.Model<JobInstance, JobAttributes>
16let listWithLimitByCategory: JobMethods.ListWithLimitByCategory 16let listWithLimitByCategory: JobMethods.ListWithLimitByCategory
@@ -38,7 +38,7 @@ export default function defineJob (sequelize: Sequelize.Sequelize, DataTypes: Se
38 { 38 {
39 indexes: [ 39 indexes: [
40 { 40 {
41 fields: [ 'state' ] 41 fields: [ 'state', 'category' ]
42 } 42 }
43 ] 43 ]
44 } 44 }
@@ -52,14 +52,15 @@ export default function defineJob (sequelize: Sequelize.Sequelize, DataTypes: Se
52 52
53// --------------------------------------------------------------------------- 53// ---------------------------------------------------------------------------
54 54
55listWithLimitByCategory = function (limit: number, state: JobState) { 55listWithLimitByCategory = function (limit: number, state: JobState, jobCategory: JobCategory) {
56 const query = { 56 const query = {
57 order: [ 57 order: [
58 [ 'id', 'ASC' ] 58 [ 'id', 'ASC' ]
59 ], 59 ],
60 limit: limit, 60 limit: limit,
61 where: { 61 where: {
62 state 62 state,
63 category: jobCategory
63 } 64 }
64 } 65 }
65 66