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