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