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