Improve check services parameters tests
[oweals/peertube.git] / server / lib / activitypub / actor.ts
1 import * as Bluebird from 'bluebird'
2 import { Transaction } from 'sequelize'
3 import * as url from 'url'
4 import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub'
5 import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects'
6 import { isRemoteActorValid } from '../../helpers/custom-validators/activitypub/actor'
7 import { retryTransactionWrapper } from '../../helpers/database-utils'
8 import { logger } from '../../helpers/logger'
9 import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto'
10 import { doRequest } from '../../helpers/requests'
11 import { CONFIG, sequelizeTypescript } from '../../initializers'
12 import { AccountModel } from '../../models/account/account'
13 import { ActorModel } from '../../models/activitypub/actor'
14 import { ServerModel } from '../../models/server/server'
15 import { VideoChannelModel } from '../../models/video/video-channel'
16
17 // Set account keys, this could be long so process after the account creation and do not block the client
18 function setAsyncActorKeys (actor: ActorModel) {
19   return createPrivateAndPublicKeys()
20     .then(({ publicKey, privateKey }) => {
21       actor.set('publicKey', publicKey)
22       actor.set('privateKey', privateKey)
23       return actor.save()
24     })
25     .catch(err => {
26       logger.error('Cannot set public/private keys of actor %d.', actor.uuid, err)
27       return actor
28     })
29 }
30
31 async function getOrCreateActorAndServerAndModel (actorUrl: string, recurseIfNeeded = true) {
32   let actor = await ActorModel.loadByUrl(actorUrl)
33
34   // We don't have this actor in our database, fetch it on remote
35   if (!actor) {
36     const result = await fetchRemoteActor(actorUrl)
37     if (result === undefined) throw new Error('Cannot fetch remote actor.')
38
39     // Create the attributed to actor
40     // In PeerTube a video channel is owned by an account
41     let ownerActor: ActorModel = undefined
42     if (recurseIfNeeded === true && result.actor.type === 'Group') {
43       const accountAttributedTo = result.attributedTo.find(a => a.type === 'Person')
44       if (!accountAttributedTo) throw new Error('Cannot find account attributed to video channel ' + actor.url)
45
46       try {
47         // Assert we don't recurse another time
48         ownerActor = await getOrCreateActorAndServerAndModel(accountAttributedTo.id, false)
49       } catch (err) {
50         logger.error('Cannot get or create account attributed to video channel ' + actor.url)
51         throw new Error(err)
52       }
53     }
54
55     const options = {
56       arguments: [ result, ownerActor ],
57       errorMessage: 'Cannot save actor and server with many retries.'
58     }
59     actor = await retryTransactionWrapper(saveActorAndServerAndModelIfNotExist, options)
60   }
61
62   return actor
63 }
64
65 function saveActorAndServerAndModelIfNotExist (
66   result: FetchRemoteActorResult,
67   ownerActor?: ActorModel,
68   t?: Transaction
69 ): Bluebird<ActorModel> | Promise<ActorModel> {
70   let actor = result.actor
71
72   if (t !== undefined) return save(t)
73
74   return sequelizeTypescript.transaction(t => save(t))
75
76   async function save (t: Transaction) {
77     const actorHost = url.parse(actor.url).host
78
79     const serverOptions = {
80       where: {
81         host: actorHost
82       },
83       defaults: {
84         host: actorHost
85       },
86       transaction: t
87     }
88     const [ server ] = await ServerModel.findOrCreate(serverOptions)
89
90     // Save our new account in database
91     actor.set('serverId', server.id)
92
93     // Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists
94     // (which could be false in a retried query)
95     const actorCreated = await ActorModel.create(actor.toJSON(), { transaction: t })
96
97     if (actorCreated.type === 'Person' || actorCreated.type === 'Application') {
98       const account = await saveAccount(actorCreated, result, t)
99       actorCreated.Account = account
100       actorCreated.Account.Actor = actorCreated
101     } else if (actorCreated.type === 'Group') { // Video channel
102       const videoChannel = await saveVideoChannel(actorCreated, result, ownerActor, t)
103       actorCreated.VideoChannel = videoChannel
104       actorCreated.VideoChannel.Actor = actorCreated
105     }
106
107     return actorCreated
108   }
109 }
110
111 type FetchRemoteActorResult = {
112   actor: ActorModel
113   name: string
114   summary: string
115   attributedTo: ActivityPubAttributedTo[]
116 }
117 async function fetchRemoteActor (actorUrl: string): Promise<FetchRemoteActorResult> {
118   const options = {
119     uri: actorUrl,
120     method: 'GET',
121     json: true,
122     activityPub: true
123   }
124
125   logger.info('Fetching remote actor %s.', actorUrl)
126
127   const requestResult = await doRequest(options)
128   const actorJSON: ActivityPubActor = requestResult.body
129
130   if (isRemoteActorValid(actorJSON) === false) {
131     logger.debug('Remote actor JSON is not valid.', { actorJSON: actorJSON })
132     return undefined
133   }
134
135   const followersCount = await fetchActorTotalItems(actorJSON.followers)
136   const followingCount = await fetchActorTotalItems(actorJSON.following)
137
138   const actor = new ActorModel({
139     type: actorJSON.type,
140     uuid: actorJSON.uuid,
141     preferredUsername: actorJSON.preferredUsername,
142     url: actorJSON.id,
143     publicKey: actorJSON.publicKey.publicKeyPem,
144     privateKey: null,
145     followersCount: followersCount,
146     followingCount: followingCount,
147     inboxUrl: actorJSON.inbox,
148     outboxUrl: actorJSON.outbox,
149     sharedInboxUrl: actorJSON.endpoints.sharedInbox,
150     followersUrl: actorJSON.followers,
151     followingUrl: actorJSON.following
152   })
153
154   const name = actorJSON.name || actorJSON.preferredUsername
155   return {
156     actor,
157     name,
158     summary: actorJSON.summary,
159     attributedTo: actorJSON.attributedTo
160   }
161 }
162
163 function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string, uuid?: string) {
164   return new ActorModel({
165     type,
166     url,
167     preferredUsername,
168     uuid,
169     publicKey: null,
170     privateKey: null,
171     followersCount: 0,
172     followingCount: 0,
173     inboxUrl: url + '/inbox',
174     outboxUrl: url + '/outbox',
175     sharedInboxUrl: CONFIG.WEBSERVER.URL + '/inbox',
176     followersUrl: url + '/followers',
177     followingUrl: url + '/following'
178   })
179 }
180
181 export {
182   getOrCreateActorAndServerAndModel,
183   saveActorAndServerAndModelIfNotExist,
184   fetchRemoteActor,
185   buildActorInstance,
186   setAsyncActorKeys
187 }
188
189 // ---------------------------------------------------------------------------
190
191 async function fetchActorTotalItems (url: string) {
192   const options = {
193     uri: url,
194     method: 'GET',
195     json: true,
196     activityPub: true
197   }
198
199   let requestResult
200   try {
201     requestResult = await doRequest(options)
202   } catch (err) {
203     logger.warn('Cannot fetch remote actor count %s.', url, err)
204     return undefined
205   }
206
207   return requestResult.totalItems ? requestResult.totalItems : 0
208 }
209
210 function saveAccount (actor: ActorModel, result: FetchRemoteActorResult, t: Transaction) {
211   const account = new AccountModel({
212     name: result.name,
213     actorId: actor.id
214   })
215
216   return account.save({ transaction: t })
217 }
218
219 async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResult, ownerActor: ActorModel, t: Transaction) {
220   const videoChannel = new VideoChannelModel({
221     name: result.name,
222     description: result.summary,
223     actorId: actor.id,
224     accountId: ownerActor.Account.id
225   })
226
227   return videoChannel.save({ transaction: t })
228 }