aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--server/controllers/api/pods.ts41
-rw-r--r--server/lib/activitypub/process-accept.ts6
-rw-r--r--server/lib/activitypub/process-add.ts2
-rw-r--r--server/lib/activitypub/process-follow.ts39
-rw-r--r--server/lib/activitypub/send-request.ts46
-rw-r--r--server/middlewares/validators/index.ts1
-rw-r--r--server/middlewares/validators/pods.ts32
-rw-r--r--server/models/account/account-interface.ts2
-rw-r--r--server/models/account/account.ts6
9 files changed, 158 insertions, 17 deletions
diff --git a/server/controllers/api/pods.ts b/server/controllers/api/pods.ts
index aa07b17f6..f662f1c03 100644
--- a/server/controllers/api/pods.ts
+++ b/server/controllers/api/pods.ts
@@ -1,10 +1,16 @@
1import * as Bluebird from 'bluebird'
1import * as express from 'express' 2import * as express from 'express'
2import { getFormattedObjects } from '../../helpers' 3import { getFormattedObjects } from '../../helpers'
4import { getOrCreateAccount } from '../../helpers/activitypub'
3import { getApplicationAccount } from '../../helpers/utils' 5import { getApplicationAccount } from '../../helpers/utils'
6import { REMOTE_SCHEME } from '../../initializers/constants'
4import { database as db } from '../../initializers/database' 7import { database as db } from '../../initializers/database'
5import { asyncMiddleware, paginationValidator, setFollowersSort, setPagination } from '../../middlewares' 8import { asyncMiddleware, paginationValidator, setFollowersSort, setPagination } from '../../middlewares'
9import { setBodyHostsPort } from '../../middlewares/pods'
6import { setFollowingSort } from '../../middlewares/sort' 10import { setFollowingSort } from '../../middlewares/sort'
11import { followValidator } from '../../middlewares/validators/pods'
7import { followersSortValidator, followingSortValidator } from '../../middlewares/validators/sort' 12import { followersSortValidator, followingSortValidator } from '../../middlewares/validators/sort'
13import { sendFollow } from '../../lib/activitypub/send-request'
8 14
9const podsRouter = express.Router() 15const podsRouter = express.Router()
10 16
@@ -16,6 +22,12 @@ podsRouter.get('/following',
16 asyncMiddleware(listFollowing) 22 asyncMiddleware(listFollowing)
17) 23)
18 24
25podsRouter.post('/follow',
26 followValidator,
27 setBodyHostsPort,
28 asyncMiddleware(follow)
29)
30
19podsRouter.get('/followers', 31podsRouter.get('/followers',
20 paginationValidator, 32 paginationValidator,
21 followersSortValidator, 33 followersSortValidator,
@@ -45,3 +57,32 @@ async function listFollowers (req: express.Request, res: express.Response, next:
45 57
46 return res.json(getFormattedObjects(resultList.data, resultList.total)) 58 return res.json(getFormattedObjects(resultList.data, resultList.total))
47} 59}
60
61async function follow (req: express.Request, res: express.Response, next: express.NextFunction) {
62 const hosts = req.body.hosts as string[]
63 const fromAccount = await getApplicationAccount()
64
65 const tasks: Bluebird<any>[] = []
66 for (const host of hosts) {
67 const url = REMOTE_SCHEME.HTTP + '://' + host
68 const targetAccount = await getOrCreateAccount(url)
69
70 // We process each host in a specific transaction
71 // First, we add the follow request in the database
72 // Then we send the follow request to other account
73 const p = db.sequelize.transaction(async t => {
74 return db.AccountFollow.create({
75 accountId: fromAccount.id,
76 targetAccountId: targetAccount.id,
77 state: 'pending'
78 })
79 .then(() => sendFollow(fromAccount, targetAccount, t))
80 })
81
82 tasks.push(p)
83 }
84
85 await Promise.all(tasks)
86
87 return res.status(204).end()
88}
diff --git a/server/lib/activitypub/process-accept.ts b/server/lib/activitypub/process-accept.ts
index 37e42bd3a..9e0cd4032 100644
--- a/server/lib/activitypub/process-accept.ts
+++ b/server/lib/activitypub/process-accept.ts
@@ -7,7 +7,7 @@ async function processAcceptActivity (activity: ActivityAccept, inboxAccount?: A
7 7
8 const targetAccount = await db.Account.loadByUrl(activity.actor) 8 const targetAccount = await db.Account.loadByUrl(activity.actor)
9 9
10 return processFollow(inboxAccount, targetAccount) 10 return processAccept(inboxAccount, targetAccount)
11} 11}
12 12
13// --------------------------------------------------------------------------- 13// ---------------------------------------------------------------------------
@@ -18,10 +18,10 @@ export {
18 18
19// --------------------------------------------------------------------------- 19// ---------------------------------------------------------------------------
20 20
21async function processFollow (account: AccountInstance, targetAccount: AccountInstance) { 21async function processAccept (account: AccountInstance, targetAccount: AccountInstance) {
22 const follow = await db.AccountFollow.loadByAccountAndTarget(account.id, targetAccount.id) 22 const follow = await db.AccountFollow.loadByAccountAndTarget(account.id, targetAccount.id)
23 if (!follow) throw new Error('Cannot find associated follow.') 23 if (!follow) throw new Error('Cannot find associated follow.')
24 24
25 follow.set('state', 'accepted') 25 follow.set('state', 'accepted')
26 return follow.save() 26 await follow.save()
27} 27}
diff --git a/server/lib/activitypub/process-add.ts b/server/lib/activitypub/process-add.ts
index 40541aca3..024dee559 100644
--- a/server/lib/activitypub/process-add.ts
+++ b/server/lib/activitypub/process-add.ts
@@ -29,7 +29,7 @@ export {
29 29
30function processAddVideo (account: AccountInstance, videoChannelUrl: string, video: VideoTorrentObject) { 30function processAddVideo (account: AccountInstance, videoChannelUrl: string, video: VideoTorrentObject) {
31 const options = { 31 const options = {
32 arguments: [ account, videoChannelUrl ,video ], 32 arguments: [ account, videoChannelUrl, video ],
33 errorMessage: 'Cannot insert the remote video with many retries.' 33 errorMessage: 'Cannot insert the remote video with many retries.'
34 } 34 }
35 35
diff --git a/server/lib/activitypub/process-follow.ts b/server/lib/activitypub/process-follow.ts
index a04fc7994..ee5d97a0b 100644
--- a/server/lib/activitypub/process-follow.ts
+++ b/server/lib/activitypub/process-follow.ts
@@ -1,7 +1,9 @@
1import { ActivityFollow } from '../../../shared/models/activitypub/activity' 1import { ActivityFollow } from '../../../shared/models/activitypub/activity'
2import { getOrCreateAccount } from '../../helpers' 2import { getOrCreateAccount, retryTransactionWrapper } from '../../helpers'
3import { database as db } from '../../initializers' 3import { database as db } from '../../initializers'
4import { AccountInstance } from '../../models/account/account-interface' 4import { AccountInstance } from '../../models/account/account-interface'
5import { sendAccept } from './send-request'
6import { logger } from '../../helpers/logger'
5 7
6async function processFollowActivity (activity: ActivityFollow) { 8async function processFollowActivity (activity: ActivityFollow) {
7 const activityObject = activity.object 9 const activityObject = activity.object
@@ -18,15 +20,34 @@ export {
18 20
19// --------------------------------------------------------------------------- 21// ---------------------------------------------------------------------------
20 22
21async function processFollow (account: AccountInstance, targetAccountURL: string) { 23function processFollow (account: AccountInstance, targetAccountURL: string) {
22 const targetAccount = await db.Account.loadByUrl(targetAccountURL) 24 const options = {
25 arguments: [ account, targetAccountURL ],
26 errorMessage: 'Cannot follow with many retries.'
27 }
23 28
24 if (targetAccount === undefined) throw new Error('Unknown account') 29 return retryTransactionWrapper(follow, options)
25 if (targetAccount.isOwned() === false) throw new Error('This is not a local account.') 30}
31
32async function follow (account: AccountInstance, targetAccountURL: string) {
33 await db.sequelize.transaction(async t => {
34 const targetAccount = await db.Account.loadByUrl(targetAccountURL, t)
35
36 if (targetAccount === undefined) throw new Error('Unknown account')
37 if (targetAccount.isOwned() === false) throw new Error('This is not a local account.')
26 38
27 return db.AccountFollow.create({ 39 const sequelizeOptions = {
28 accountId: account.id, 40 transaction: t
29 targetAccountId: targetAccount.id, 41 }
30 state: 'accepted' 42 await db.AccountFollow.create({
43 accountId: account.id,
44 targetAccountId: targetAccount.id,
45 state: 'accepted'
46 }, sequelizeOptions)
47
48 // Target sends to account he accepted the follow request
49 return sendAccept(targetAccount, account, t)
31 }) 50 })
51
52 logger.info('Account uuid %s is followed by account %s.', account.url, targetAccountURL)
32} 53}
diff --git a/server/lib/activitypub/send-request.ts b/server/lib/activitypub/send-request.ts
index ce9a96f14..e6ef5f37a 100644
--- a/server/lib/activitypub/send-request.ts
+++ b/server/lib/activitypub/send-request.ts
@@ -56,6 +56,18 @@ function sendDeleteAccount (account: AccountInstance, t: Sequelize.Transaction)
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) {
60 const data = acceptActivityData(fromAccount)
61
62 return unicastTo(data, toAccount, t)
63}
64
65function sendFollow (fromAccount: AccountInstance, toAccount: AccountInstance, t: Sequelize.Transaction) {
66 const data = followActivityData(toAccount.url, fromAccount)
67
68 return unicastTo(data, toAccount, t)
69}
70
59// --------------------------------------------------------------------------- 71// ---------------------------------------------------------------------------
60 72
61export { 73export {
@@ -65,7 +77,9 @@ export {
65 sendAddVideo, 77 sendAddVideo,
66 sendUpdateVideo, 78 sendUpdateVideo,
67 sendDeleteVideo, 79 sendDeleteVideo,
68 sendDeleteAccount 80 sendDeleteAccount,
81 sendAccept,
82 sendFollow
69} 83}
70 84
71// --------------------------------------------------------------------------- 85// ---------------------------------------------------------------------------
@@ -81,6 +95,15 @@ async function broadcastToFollowers (data: any, fromAccount: AccountInstance, t:
81 return httpRequestJobScheduler.createJob(t, 'httpRequestBroadcastHandler', jobPayload) 95 return httpRequestJobScheduler.createJob(t, 'httpRequestBroadcastHandler', jobPayload)
82} 96}
83 97
98async function unicastTo (data: any, toAccount: AccountInstance, t: Sequelize.Transaction) {
99 const jobPayload = {
100 uris: [ toAccount.url ],
101 body: data
102 }
103
104 return httpRequestJobScheduler.createJob(t, 'httpRequestUnicastHandler', jobPayload)
105}
106
84function buildSignedActivity (byAccount: AccountInstance, data: Object) { 107function buildSignedActivity (byAccount: AccountInstance, data: Object) {
85 const activity = activityPubContextify(data) 108 const activity = activityPubContextify(data)
86 109
@@ -142,3 +165,24 @@ async function addActivityData (url: string, byAccount: AccountInstance, target:
142 165
143 return buildSignedActivity(byAccount, base) 166 return buildSignedActivity(byAccount, base)
144} 167}
168
169async function followActivityData (url: string, byAccount: AccountInstance) {
170 const base = {
171 type: 'Follow',
172 id: byAccount.url,
173 actor: byAccount.url,
174 object: url
175 }
176
177 return buildSignedActivity(byAccount, base)
178}
179
180async function acceptActivityData (byAccount: AccountInstance) {
181 const base = {
182 type: 'Accept',
183 id: byAccount.url,
184 actor: byAccount.url
185 }
186
187 return buildSignedActivity(byAccount, base)
188}
diff --git a/server/middlewares/validators/index.ts b/server/middlewares/validators/index.ts
index 0b7573d4f..46c00d679 100644
--- a/server/middlewares/validators/index.ts
+++ b/server/middlewares/validators/index.ts
@@ -2,6 +2,7 @@ export * from './account'
2export * from './oembed' 2export * from './oembed'
3export * from './activitypub' 3export * from './activitypub'
4export * from './pagination' 4export * from './pagination'
5export * from './pods'
5export * from './sort' 6export * from './sort'
6export * from './users' 7export * from './users'
7export * from './videos' 8export * from './videos'
diff --git a/server/middlewares/validators/pods.ts b/server/middlewares/validators/pods.ts
new file mode 100644
index 000000000..e17369a6f
--- /dev/null
+++ b/server/middlewares/validators/pods.ts
@@ -0,0 +1,32 @@
1import * as express from 'express'
2import { body } from 'express-validator/check'
3import { isEachUniqueHostValid } from '../../helpers/custom-validators/pods'
4import { isTestInstance } from '../../helpers/core-utils'
5import { CONFIG } from '../../initializers/constants'
6import { logger } from '../../helpers/logger'
7import { checkErrors } from './utils'
8
9const followValidator = [
10 body('hosts').custom(isEachUniqueHostValid).withMessage('Should have an array of unique hosts'),
11
12 (req: express.Request, res: express.Response, next: express.NextFunction) => {
13 // Force https if the administrator wants to make friends
14 if (isTestInstance() === false && CONFIG.WEBSERVER.SCHEME === 'http') {
15 return res.status(400)
16 .json({
17 error: 'Cannot follow non HTTPS web server.'
18 })
19 .end()
20 }
21
22 logger.debug('Checking follow parameters', { parameters: req.body })
23
24 checkErrors(req, res, next)
25 }
26]
27
28// ---------------------------------------------------------------------------
29
30export {
31 followValidator
32}
diff --git a/server/models/account/account-interface.ts b/server/models/account/account-interface.ts
index d49dfbe17..73701f233 100644
--- a/server/models/account/account-interface.ts
+++ b/server/models/account/account-interface.ts
@@ -10,7 +10,7 @@ export namespace AccountMethods {
10 10
11 export type Load = (id: number) => Bluebird<AccountInstance> 11 export type Load = (id: number) => Bluebird<AccountInstance>
12 export type LoadByUUID = (uuid: string) => Bluebird<AccountInstance> 12 export type LoadByUUID = (uuid: string) => Bluebird<AccountInstance>
13 export type LoadByUrl = (url: string) => 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 LoadLocalAccountByNameAndPod = (name: string, host: string) => Bluebird<AccountInstance>
16 export type ListOwned = () => Bluebird<AccountInstance[]> 16 export type ListOwned = () => Bluebird<AccountInstance[]>
diff --git a/server/models/account/account.ts b/server/models/account/account.ts
index daf8f4703..7ce97b2fd 100644
--- a/server/models/account/account.ts
+++ b/server/models/account/account.ts
@@ -198,6 +198,7 @@ export default function defineAccount (sequelize: Sequelize.Sequelize, DataTypes
198 loadApplication, 198 loadApplication,
199 load, 199 load,
200 loadByUUID, 200 loadByUUID,
201 loadByUrl,
201 loadLocalAccountByNameAndPod, 202 loadLocalAccountByNameAndPod,
202 listOwned, 203 listOwned,
203 listFollowerUrlsForApi, 204 listFollowerUrlsForApi,
@@ -480,11 +481,12 @@ loadLocalAccountByNameAndPod = function (name: string, host: string) {
480 return Account.findOne(query) 481 return Account.findOne(query)
481} 482}
482 483
483loadByUrl = function (url: string) { 484loadByUrl = function (url: string, transaction?: Sequelize.Transaction) {
484 const query: Sequelize.FindOptions<AccountAttributes> = { 485 const query: Sequelize.FindOptions<AccountAttributes> = {
485 where: { 486 where: {
486 url 487 url
487 } 488 },
489 transaction
488 } 490 }
489 491
490 return Account.findOne(query) 492 return Account.findOne(query)