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