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