Add commentsEnabled field to AS context
[oweals/peertube.git] / server / lib / activitypub / actor.ts
1 import * as Bluebird from 'bluebird'
2 import { join } from 'path'
3 import { Transaction } from 'sequelize'
4 import * as url from 'url'
5 import * as uuidv4 from 'uuid/v4'
6 import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub'
7 import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects'
8 import { isActorObjectValid } from '../../helpers/custom-validators/activitypub/actor'
9 import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
10 import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils'
11 import { logger } from '../../helpers/logger'
12 import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto'
13 import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests'
14 import { getUrlFromWebfinger } from '../../helpers/webfinger'
15 import { AVATAR_MIMETYPE_EXT, CONFIG, sequelizeTypescript } from '../../initializers'
16 import { AccountModel } from '../../models/account/account'
17 import { ActorModel } from '../../models/activitypub/actor'
18 import { AvatarModel } from '../../models/avatar/avatar'
19 import { ServerModel } from '../../models/server/server'
20 import { VideoChannelModel } from '../../models/video/video-channel'
21
22 // Set account keys, this could be long so process after the account creation and do not block the client
23 function setAsyncActorKeys (actor: ActorModel) {
24   return createPrivateAndPublicKeys()
25     .then(({ publicKey, privateKey }) => {
26       actor.set('publicKey', publicKey)
27       actor.set('privateKey', privateKey)
28       return actor.save()
29     })
30     .catch(err => {
31       logger.error('Cannot set public/private keys of actor %d.', actor.uuid, err)
32       return actor
33     })
34 }
35
36 async function getOrCreateActorAndServerAndModel (actorUrl: string, recurseIfNeeded = true) {
37   let actor = await ActorModel.loadByUrl(actorUrl)
38
39   // We don't have this actor in our database, fetch it on remote
40   if (!actor) {
41     const result = await fetchRemoteActor(actorUrl)
42     if (result === undefined) throw new Error('Cannot fetch remote actor.')
43
44     // Create the attributed to actor
45     // In PeerTube a video channel is owned by an account
46     let ownerActor: ActorModel = undefined
47     if (recurseIfNeeded === true && result.actor.type === 'Group') {
48       const accountAttributedTo = result.attributedTo.find(a => a.type === 'Person')
49       if (!accountAttributedTo) throw new Error('Cannot find account attributed to video channel ' + actor.url)
50
51       try {
52         // Assert we don't recurse another time
53         ownerActor = await getOrCreateActorAndServerAndModel(accountAttributedTo.id, false)
54       } catch (err) {
55         logger.error('Cannot get or create account attributed to video channel ' + actor.url)
56         throw new Error(err)
57       }
58     }
59
60     const options = {
61       arguments: [ result, ownerActor ],
62       errorMessage: 'Cannot save actor and server with many retries.'
63     }
64     actor = await retryTransactionWrapper(saveActorAndServerAndModelIfNotExist, options)
65   }
66
67   return refreshActorIfNeeded(actor)
68 }
69
70 function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string, uuid?: string) {
71   return new ActorModel({
72     type,
73     url,
74     preferredUsername,
75     uuid,
76     publicKey: null,
77     privateKey: null,
78     followersCount: 0,
79     followingCount: 0,
80     inboxUrl: url + '/inbox',
81     outboxUrl: url + '/outbox',
82     sharedInboxUrl: CONFIG.WEBSERVER.URL + '/inbox',
83     followersUrl: url + '/followers',
84     followingUrl: url + '/following'
85   })
86 }
87
88 async function updateActorInstance (actorInstance: ActorModel, attributes: ActivityPubActor) {
89   const followersCount = await fetchActorTotalItems(attributes.followers)
90   const followingCount = await fetchActorTotalItems(attributes.following)
91
92   actorInstance.set('type', attributes.type)
93   actorInstance.set('uuid', attributes.uuid)
94   actorInstance.set('preferredUsername', attributes.preferredUsername)
95   actorInstance.set('url', attributes.id)
96   actorInstance.set('publicKey', attributes.publicKey.publicKeyPem)
97   actorInstance.set('followersCount', followersCount)
98   actorInstance.set('followingCount', followingCount)
99   actorInstance.set('inboxUrl', attributes.inbox)
100   actorInstance.set('outboxUrl', attributes.outbox)
101   actorInstance.set('sharedInboxUrl', attributes.endpoints.sharedInbox)
102   actorInstance.set('followersUrl', attributes.followers)
103   actorInstance.set('followingUrl', attributes.following)
104 }
105
106 async function updateActorAvatarInstance (actorInstance: ActorModel, avatarName: string, t: Transaction) {
107   if (avatarName !== undefined) {
108     if (actorInstance.avatarId) {
109       try {
110         await actorInstance.Avatar.destroy({ transaction: t })
111       } catch (err) {
112         logger.error('Cannot remove old avatar of actor %s.', actorInstance.url, err)
113       }
114     }
115
116     const avatar = await AvatarModel.create({
117       filename: avatarName
118     }, { transaction: t })
119
120     actorInstance.set('avatarId', avatar.id)
121     actorInstance.Avatar = avatar
122   }
123
124   return actorInstance
125 }
126
127 async function fetchActorTotalItems (url: string) {
128   const options = {
129     uri: url,
130     method: 'GET',
131     json: true,
132     activityPub: true
133   }
134
135   let requestResult
136   try {
137     requestResult = await doRequest(options)
138   } catch (err) {
139     logger.warn('Cannot fetch remote actor count %s.', url, err)
140     return undefined
141   }
142
143   return requestResult.totalItems ? requestResult.totalItems : 0
144 }
145
146 async function fetchAvatarIfExists (actorJSON: ActivityPubActor) {
147   if (
148     actorJSON.icon && actorJSON.icon.type === 'Image' && AVATAR_MIMETYPE_EXT[actorJSON.icon.mediaType] !== undefined &&
149     isActivityPubUrlValid(actorJSON.icon.url)
150   ) {
151     const extension = AVATAR_MIMETYPE_EXT[actorJSON.icon.mediaType]
152
153     const avatarName = uuidv4() + extension
154     const destPath = join(CONFIG.STORAGE.AVATARS_DIR, avatarName)
155
156     await doRequestAndSaveToFile({
157       method: 'GET',
158       uri: actorJSON.icon.url
159     }, destPath)
160
161     return avatarName
162   }
163
164   return undefined
165 }
166
167 export {
168   getOrCreateActorAndServerAndModel,
169   buildActorInstance,
170   setAsyncActorKeys,
171   fetchActorTotalItems,
172   fetchAvatarIfExists,
173   updateActorInstance,
174   updateActorAvatarInstance
175 }
176
177 // ---------------------------------------------------------------------------
178
179 function saveActorAndServerAndModelIfNotExist (
180   result: FetchRemoteActorResult,
181   ownerActor?: ActorModel,
182   t?: Transaction
183 ): Bluebird<ActorModel> | Promise<ActorModel> {
184   let actor = result.actor
185
186   if (t !== undefined) return save(t)
187
188   return sequelizeTypescript.transaction(t => save(t))
189
190   async function save (t: Transaction) {
191     const actorHost = url.parse(actor.url).host
192
193     const serverOptions = {
194       where: {
195         host: actorHost
196       },
197       defaults: {
198         host: actorHost
199       },
200       transaction: t
201     }
202     const [ server ] = await ServerModel.findOrCreate(serverOptions)
203
204     // Save our new account in database
205     actor.set('serverId', server.id)
206
207     // Avatar?
208     if (result.avatarName) {
209       const avatar = await AvatarModel.create({
210         filename: result.avatarName
211       }, { transaction: t })
212       actor.set('avatarId', avatar.id)
213     }
214
215     // Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists
216     // (which could be false in a retried query)
217     const actorCreated = await ActorModel.create(actor.toJSON(), { transaction: t })
218
219     if (actorCreated.type === 'Person' || actorCreated.type === 'Application') {
220       const account = await saveAccount(actorCreated, result, t)
221       actorCreated.Account = account
222       actorCreated.Account.Actor = actorCreated
223     } else if (actorCreated.type === 'Group') { // Video channel
224       const videoChannel = await saveVideoChannel(actorCreated, result, ownerActor, t)
225       actorCreated.VideoChannel = videoChannel
226       actorCreated.VideoChannel.Actor = actorCreated
227     }
228
229     return actorCreated
230   }
231 }
232
233 type FetchRemoteActorResult = {
234   actor: ActorModel
235   name: string
236   summary: string
237   avatarName?: string
238   attributedTo: ActivityPubAttributedTo[]
239 }
240 async function fetchRemoteActor (actorUrl: string): Promise<FetchRemoteActorResult> {
241   const options = {
242     uri: actorUrl,
243     method: 'GET',
244     json: true,
245     activityPub: true
246   }
247
248   logger.info('Fetching remote actor %s.', actorUrl)
249
250   const requestResult = await doRequest(options)
251   const actorJSON: ActivityPubActor = requestResult.body
252
253   if (isActorObjectValid(actorJSON) === false) {
254     logger.debug('Remote actor JSON is not valid.', { actorJSON: actorJSON })
255     return undefined
256   }
257
258   const followersCount = await fetchActorTotalItems(actorJSON.followers)
259   const followingCount = await fetchActorTotalItems(actorJSON.following)
260
261   const actor = new ActorModel({
262     type: actorJSON.type,
263     uuid: actorJSON.uuid,
264     preferredUsername: actorJSON.preferredUsername,
265     url: actorJSON.id,
266     publicKey: actorJSON.publicKey.publicKeyPem,
267     privateKey: null,
268     followersCount: followersCount,
269     followingCount: followingCount,
270     inboxUrl: actorJSON.inbox,
271     outboxUrl: actorJSON.outbox,
272     sharedInboxUrl: actorJSON.endpoints.sharedInbox,
273     followersUrl: actorJSON.followers,
274     followingUrl: actorJSON.following
275   })
276
277   const avatarName = await fetchAvatarIfExists(actorJSON)
278
279   const name = actorJSON.name || actorJSON.preferredUsername
280   return {
281     actor,
282     name,
283     avatarName,
284     summary: actorJSON.summary,
285     attributedTo: actorJSON.attributedTo
286   }
287 }
288
289 function saveAccount (actor: ActorModel, result: FetchRemoteActorResult, t: Transaction) {
290   const account = new AccountModel({
291     name: result.name,
292     actorId: actor.id
293   })
294
295   return account.save({ transaction: t })
296 }
297
298 async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResult, ownerActor: ActorModel, t: Transaction) {
299   const videoChannel = new VideoChannelModel({
300     name: result.name,
301     description: result.summary,
302     actorId: actor.id,
303     accountId: ownerActor.Account.id
304   })
305
306   return videoChannel.save({ transaction: t })
307 }
308
309 async function refreshActorIfNeeded (actor: ActorModel) {
310   if (!actor.isOutdated()) return actor
311
312   const actorUrl = await getUrlFromWebfinger(actor.preferredUsername, actor.getHost())
313   const result = await fetchRemoteActor(actorUrl)
314   if (result === undefined) throw new Error('Cannot fetch remote actor in refresh actor.')
315
316   return sequelizeTypescript.transaction(async t => {
317     logger.info('coucou', result.actor.toJSON())
318     updateInstanceWithAnother(actor, result.actor)
319
320     if (result.avatarName !== undefined) {
321       await updateActorAvatarInstance(actor, result.avatarName, t)
322     }
323
324     // Force update
325     actor.setDataValue('updatedAt', new Date())
326     await actor.save({ transaction: t })
327
328     if (actor.Account) {
329       await actor.save({ transaction: t })
330
331       actor.Account.set('name', result.name)
332       await actor.Account.save({ transaction: t })
333     } else if (actor.VideoChannel) {
334       await actor.save({ transaction: t })
335
336       actor.VideoChannel.set('name', result.name)
337       await actor.VideoChannel.save({ transaction: t })
338     }
339
340     return actor
341   })
342 }