diff options
-rw-r--r-- | server/controllers/api/pods.ts | 41 | ||||
-rw-r--r-- | server/lib/activitypub/process-accept.ts | 6 | ||||
-rw-r--r-- | server/lib/activitypub/process-add.ts | 2 | ||||
-rw-r--r-- | server/lib/activitypub/process-follow.ts | 39 | ||||
-rw-r--r-- | server/lib/activitypub/send-request.ts | 46 | ||||
-rw-r--r-- | server/middlewares/validators/index.ts | 1 | ||||
-rw-r--r-- | server/middlewares/validators/pods.ts | 32 | ||||
-rw-r--r-- | server/models/account/account-interface.ts | 2 | ||||
-rw-r--r-- | server/models/account/account.ts | 6 |
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 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
1 | import * as express from 'express' | 2 | import * as express from 'express' |
2 | import { getFormattedObjects } from '../../helpers' | 3 | import { getFormattedObjects } from '../../helpers' |
4 | import { getOrCreateAccount } from '../../helpers/activitypub' | ||
3 | import { getApplicationAccount } from '../../helpers/utils' | 5 | import { getApplicationAccount } from '../../helpers/utils' |
6 | import { REMOTE_SCHEME } from '../../initializers/constants' | ||
4 | import { database as db } from '../../initializers/database' | 7 | import { database as db } from '../../initializers/database' |
5 | import { asyncMiddleware, paginationValidator, setFollowersSort, setPagination } from '../../middlewares' | 8 | import { asyncMiddleware, paginationValidator, setFollowersSort, setPagination } from '../../middlewares' |
9 | import { setBodyHostsPort } from '../../middlewares/pods' | ||
6 | import { setFollowingSort } from '../../middlewares/sort' | 10 | import { setFollowingSort } from '../../middlewares/sort' |
11 | import { followValidator } from '../../middlewares/validators/pods' | ||
7 | import { followersSortValidator, followingSortValidator } from '../../middlewares/validators/sort' | 12 | import { followersSortValidator, followingSortValidator } from '../../middlewares/validators/sort' |
13 | import { sendFollow } from '../../lib/activitypub/send-request' | ||
8 | 14 | ||
9 | const podsRouter = express.Router() | 15 | const podsRouter = express.Router() |
10 | 16 | ||
@@ -16,6 +22,12 @@ podsRouter.get('/following', | |||
16 | asyncMiddleware(listFollowing) | 22 | asyncMiddleware(listFollowing) |
17 | ) | 23 | ) |
18 | 24 | ||
25 | podsRouter.post('/follow', | ||
26 | followValidator, | ||
27 | setBodyHostsPort, | ||
28 | asyncMiddleware(follow) | ||
29 | ) | ||
30 | |||
19 | podsRouter.get('/followers', | 31 | podsRouter.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 | |||
61 | async 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 | ||
21 | async function processFollow (account: AccountInstance, targetAccount: AccountInstance) { | 21 | async 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 | ||
30 | function processAddVideo (account: AccountInstance, videoChannelUrl: string, video: VideoTorrentObject) { | 30 | function 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 @@ | |||
1 | import { ActivityFollow } from '../../../shared/models/activitypub/activity' | 1 | import { ActivityFollow } from '../../../shared/models/activitypub/activity' |
2 | import { getOrCreateAccount } from '../../helpers' | 2 | import { getOrCreateAccount, retryTransactionWrapper } from '../../helpers' |
3 | import { database as db } from '../../initializers' | 3 | import { database as db } from '../../initializers' |
4 | import { AccountInstance } from '../../models/account/account-interface' | 4 | import { AccountInstance } from '../../models/account/account-interface' |
5 | import { sendAccept } from './send-request' | ||
6 | import { logger } from '../../helpers/logger' | ||
5 | 7 | ||
6 | async function processFollowActivity (activity: ActivityFollow) { | 8 | async 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 | ||
21 | async function processFollow (account: AccountInstance, targetAccountURL: string) { | 23 | function 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 | |||
32 | async 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 | ||
59 | function sendAccept (fromAccount: AccountInstance, toAccount: AccountInstance, t: Sequelize.Transaction) { | ||
60 | const data = acceptActivityData(fromAccount) | ||
61 | |||
62 | return unicastTo(data, toAccount, t) | ||
63 | } | ||
64 | |||
65 | function 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 | ||
61 | export { | 73 | export { |
@@ -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 | ||
98 | async 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 | |||
84 | function buildSignedActivity (byAccount: AccountInstance, data: Object) { | 107 | function 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 | |||
169 | async 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 | |||
180 | async 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' | |||
2 | export * from './oembed' | 2 | export * from './oembed' |
3 | export * from './activitypub' | 3 | export * from './activitypub' |
4 | export * from './pagination' | 4 | export * from './pagination' |
5 | export * from './pods' | ||
5 | export * from './sort' | 6 | export * from './sort' |
6 | export * from './users' | 7 | export * from './users' |
7 | export * from './videos' | 8 | export * 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 @@ | |||
1 | import * as express from 'express' | ||
2 | import { body } from 'express-validator/check' | ||
3 | import { isEachUniqueHostValid } from '../../helpers/custom-validators/pods' | ||
4 | import { isTestInstance } from '../../helpers/core-utils' | ||
5 | import { CONFIG } from '../../initializers/constants' | ||
6 | import { logger } from '../../helpers/logger' | ||
7 | import { checkErrors } from './utils' | ||
8 | |||
9 | const 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 | |||
30 | export { | ||
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 | ||
483 | loadByUrl = function (url: string) { | 484 | loadByUrl = 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) |