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