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