Federate likes/dislikes
[oweals/peertube.git] / server / lib / activitypub / account.ts
1 import * as Bluebird from 'bluebird'
2 import * as url from 'url'
3 import { ActivityPubActor } from '../../../shared/models/activitypub/activitypub-actor'
4 import { isRemoteAccountValid } from '../../helpers/custom-validators/activitypub/account'
5 import { retryTransactionWrapper } from '../../helpers/database-utils'
6 import { logger } from '../../helpers/logger'
7 import { doRequest } from '../../helpers/requests'
8 import { ACTIVITY_PUB } from '../../initializers/constants'
9 import { database as db } from '../../initializers/database'
10 import { AccountInstance } from '../../models/account/account-interface'
11 import { Transaction } from 'sequelize'
12
13 async function getOrCreateAccountAndServer (accountUrl: string) {
14   let account = await db.Account.loadByUrl(accountUrl)
15
16   // We don't have this account in our database, fetch it on remote
17   if (!account) {
18     account = await fetchRemoteAccount(accountUrl)
19     if (account === undefined) throw new Error('Cannot fetch remote account.')
20
21     const options = {
22       arguments: [ account ],
23       errorMessage: 'Cannot save account and server with many retries.'
24     }
25     account = await retryTransactionWrapper(saveAccountAndServerIfNotExist, options)
26   }
27
28   return account
29 }
30
31 function saveAccountAndServerIfNotExist (account: AccountInstance, t?: Transaction): Bluebird<AccountInstance> | Promise<AccountInstance> {
32   if (t !== undefined) {
33     return save(t)
34   } else {
35     return db.sequelize.transaction(t => {
36       return save(t)
37     })
38   }
39
40   async function save (t: Transaction) {
41     const accountHost = url.parse(account.url).host
42
43     const serverOptions = {
44       where: {
45         host: accountHost
46       },
47       defaults: {
48         host: accountHost
49       },
50       transaction: t
51     }
52     const [ server ] = await db.Server.findOrCreate(serverOptions)
53
54     // Save our new account in database
55     account.set('serverId', server.id)
56     account = await account.save({ transaction: t })
57
58     return account
59   }
60 }
61
62 async function fetchRemoteAccount (accountUrl: string) {
63   const options = {
64     uri: accountUrl,
65     method: 'GET',
66     headers: {
67       'Accept': ACTIVITY_PUB.ACCEPT_HEADER
68     }
69   }
70
71   logger.info('Fetching remote account %s.', accountUrl)
72
73   let requestResult
74   try {
75     requestResult = await doRequest(options)
76   } catch (err) {
77     logger.warn('Cannot fetch remote account %s.', accountUrl, err)
78     return undefined
79   }
80
81   const accountJSON: ActivityPubActor = JSON.parse(requestResult.body)
82   if (isRemoteAccountValid(accountJSON) === false) {
83     logger.debug('Remote account JSON is not valid.', { accountJSON })
84     return undefined
85   }
86
87   const followersCount = await fetchAccountCount(accountJSON.followers)
88   const followingCount = await fetchAccountCount(accountJSON.following)
89
90   const account = db.Account.build({
91     uuid: accountJSON.uuid,
92     name: accountJSON.preferredUsername,
93     url: accountJSON.url,
94     publicKey: accountJSON.publicKey.publicKeyPem,
95     privateKey: null,
96     followersCount: followersCount,
97     followingCount: followingCount,
98     inboxUrl: accountJSON.inbox,
99     outboxUrl: accountJSON.outbox,
100     sharedInboxUrl: accountJSON.endpoints.sharedInbox,
101     followersUrl: accountJSON.followers,
102     followingUrl: accountJSON.following
103   })
104
105   return account
106 }
107
108 export {
109   getOrCreateAccountAndServer,
110   fetchRemoteAccount,
111   saveAccountAndServerIfNotExist
112 }
113
114 // ---------------------------------------------------------------------------
115
116 async function fetchAccountCount (url: string) {
117   const options = {
118     uri: url,
119     method: 'GET'
120   }
121
122   let requestResult
123   try {
124     requestResult = await doRequest(options)
125   } catch (err) {
126     logger.warn('Cannot fetch remote account count %s.', url, err)
127     return undefined
128   }
129
130   return requestResult.totalItems ? requestResult.totalItems : 0
131 }