]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/lib/activitypub/actor.ts
Fix video views
[github/Chocobozzz/PeerTube.git] / server / lib / activitypub / actor.ts
CommitLineData
50d6de9c
C
1import * as Bluebird from 'bluebird'
2import { Transaction } from 'sequelize'
3import * as url from 'url'
c5911fd3 4import * as uuidv4 from 'uuid/v4'
50d6de9c
C
5import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub'
6import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects'
848f499d 7import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
938d3fa0 8import { isActorObjectValid, normalizeActor } from '../../helpers/custom-validators/activitypub/actor'
c5911fd3 9import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
a5625b41 10import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils'
da854ddd
C
11import { logger } from '../../helpers/logger'
12import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto'
361805c4 13import { doRequest, downloadImage } from '../../helpers/requests'
a5625b41 14import { getUrlFromWebfinger } from '../../helpers/webfinger'
74dc3bca 15import { AVATARS_SIZE, MIMETYPES, WEBSERVER } from '../../initializers/constants'
50d6de9c
C
16import { AccountModel } from '../../models/account/account'
17import { ActorModel } from '../../models/activitypub/actor'
c5911fd3 18import { AvatarModel } from '../../models/avatar/avatar'
50d6de9c
C
19import { ServerModel } from '../../models/server/server'
20import { VideoChannelModel } from '../../models/video/video-channel'
16f29007
C
21import { JobQueue } from '../job-queue'
22import { getServerActor } from '../../helpers/utils'
e587e0ec 23import { ActorFetchByUrlType, fetchActorByUrl } from '../../helpers/actor'
6dd9de95 24import { CONFIG } from '../../initializers/config'
74dc3bca 25import { sequelizeTypescript } from '../../initializers/database'
50d6de9c 26
e12a0092 27// Set account keys, this could be long so process after the account creation and do not block the client
50d6de9c
C
28function setAsyncActorKeys (actor: ActorModel) {
29 return createPrivateAndPublicKeys()
30 .then(({ publicKey, privateKey }) => {
31 actor.set('publicKey', publicKey)
32 actor.set('privateKey', privateKey)
33 return actor.save()
34 })
35 .catch(err => {
d5b7d911 36 logger.error('Cannot set public/private keys of actor %d.', actor.uuid, { err })
50d6de9c
C
37 return actor
38 })
39}
40
687d638c
C
41async function getOrCreateActorAndServerAndModel (
42 activityActor: string | ActivityPubActor,
e587e0ec 43 fetchType: ActorFetchByUrlType = 'actor-and-association-ids',
687d638c
C
44 recurseIfNeeded = true,
45 updateCollections = false
46) {
848f499d 47 const actorUrl = getAPId(activityActor)
687d638c 48 let created = false
418d092a 49 let accountPlaylistsUrl: string
6be84cbc 50
e587e0ec 51 let actor = await fetchActorByUrl(actorUrl, fetchType)
25e4d6ee 52 // Orphan actor (not associated to an account of channel) so recreate it
6104adc3 53 if (actor && (!actor.Account && !actor.VideoChannel)) {
25e4d6ee
C
54 await actor.destroy()
55 actor = null
56 }
50d6de9c
C
57
58 // We don't have this actor in our database, fetch it on remote
59 if (!actor) {
f5b0af50 60 const { result } = await fetchRemoteActor(actorUrl)
601527d7 61 if (result === undefined) throw new Error('Cannot fetch remote actor ' + actorUrl)
50d6de9c
C
62
63 // Create the attributed to actor
64 // In PeerTube a video channel is owned by an account
65 let ownerActor: ActorModel = undefined
66 if (recurseIfNeeded === true && result.actor.type === 'Group') {
67 const accountAttributedTo = result.attributedTo.find(a => a.type === 'Person')
68 if (!accountAttributedTo) throw new Error('Cannot find account attributed to video channel ' + actor.url)
69
5c6d985f
C
70 if (checkUrlsSameHost(accountAttributedTo.id, actorUrl) !== true) {
71 throw new Error(`Account attributed to ${accountAttributedTo.id} does not have the same host than actor url ${actorUrl}`)
72 }
73
50d6de9c 74 try {
5c6d985f 75 // Don't recurse another time
418d092a
C
76 const recurseIfNeeded = false
77 ownerActor = await getOrCreateActorAndServerAndModel(accountAttributedTo.id, 'all', recurseIfNeeded)
50d6de9c
C
78 } catch (err) {
79 logger.error('Cannot get or create account attributed to video channel ' + actor.url)
80 throw new Error(err)
81 }
82 }
83
90d4bb81 84 actor = await retryTransactionWrapper(saveActorAndServerAndModelIfNotExist, result, ownerActor)
687d638c 85 created = true
418d092a 86 accountPlaylistsUrl = result.playlists
50d6de9c
C
87 }
88
d9bdd007
C
89 if (actor.Account) actor.Account.Actor = actor
90 if (actor.VideoChannel) actor.VideoChannel.Actor = actor
91
e587e0ec 92 const { actor: actorRefreshed, refreshed } = await retryTransactionWrapper(refreshActorIfNeeded, actor, fetchType)
687d638c 93 if (!actorRefreshed) throw new Error('Actor ' + actorRefreshed.url + ' does not exist anymore.')
f5b0af50 94
687d638c
C
95 if ((created === true || refreshed === true) && updateCollections === true) {
96 const payload = { uri: actor.outboxUrl, type: 'activity' as 'activity' }
97 await JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload })
98 }
99
418d092a
C
100 // We created a new account: fetch the playlists
101 if (created === true && actor.Account && accountPlaylistsUrl) {
102 const payload = { uri: accountPlaylistsUrl, accountId: actor.Account.id, type: 'account-playlists' as 'account-playlists' }
103 await JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload })
104 }
105
687d638c 106 return actorRefreshed
50d6de9c
C
107}
108
c5911fd3
C
109function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string, uuid?: string) {
110 return new ActorModel({
111 type,
112 url,
113 preferredUsername,
114 uuid,
115 publicKey: null,
116 privateKey: null,
117 followersCount: 0,
118 followingCount: 0,
119 inboxUrl: url + '/inbox',
120 outboxUrl: url + '/outbox',
6dd9de95 121 sharedInboxUrl: WEBSERVER.URL + '/inbox',
c5911fd3
C
122 followersUrl: url + '/followers',
123 followingUrl: url + '/following'
124 })
125}
126
a5625b41
C
127async function updateActorInstance (actorInstance: ActorModel, attributes: ActivityPubActor) {
128 const followersCount = await fetchActorTotalItems(attributes.followers)
129 const followingCount = await fetchActorTotalItems(attributes.following)
130
131 actorInstance.set('type', attributes.type)
132 actorInstance.set('uuid', attributes.uuid)
133 actorInstance.set('preferredUsername', attributes.preferredUsername)
134 actorInstance.set('url', attributes.id)
135 actorInstance.set('publicKey', attributes.publicKey.publicKeyPem)
136 actorInstance.set('followersCount', followersCount)
137 actorInstance.set('followingCount', followingCount)
138 actorInstance.set('inboxUrl', attributes.inbox)
139 actorInstance.set('outboxUrl', attributes.outbox)
140 actorInstance.set('sharedInboxUrl', attributes.endpoints.sharedInbox)
141 actorInstance.set('followersUrl', attributes.followers)
142 actorInstance.set('followingUrl', attributes.following)
143}
144
145async function updateActorAvatarInstance (actorInstance: ActorModel, avatarName: string, t: Transaction) {
146 if (avatarName !== undefined) {
147 if (actorInstance.avatarId) {
148 try {
149 await actorInstance.Avatar.destroy({ transaction: t })
150 } catch (err) {
d5b7d911 151 logger.error('Cannot remove old avatar of actor %s.', actorInstance.url, { err })
a5625b41
C
152 }
153 }
154
155 const avatar = await AvatarModel.create({
156 filename: avatarName
157 }, { transaction: t })
158
159 actorInstance.set('avatarId', avatar.id)
160 actorInstance.Avatar = avatar
161 }
162
163 return actorInstance
164}
165
265ba139
C
166async function fetchActorTotalItems (url: string) {
167 const options = {
168 uri: url,
169 method: 'GET',
170 json: true,
171 activityPub: true
172 }
173
265ba139 174 try {
7006bc63
C
175 const { body } = await doRequest(options)
176 return body.totalItems ? body.totalItems : 0
265ba139 177 } catch (err) {
d5b7d911 178 logger.warn('Cannot fetch remote actor count %s.', url, { err })
7006bc63 179 return 0
265ba139 180 }
265ba139
C
181}
182
183async function fetchAvatarIfExists (actorJSON: ActivityPubActor) {
184 if (
14e2014a 185 actorJSON.icon && actorJSON.icon.type === 'Image' && MIMETYPES.IMAGE.MIMETYPE_EXT[actorJSON.icon.mediaType] !== undefined &&
265ba139
C
186 isActivityPubUrlValid(actorJSON.icon.url)
187 ) {
14e2014a 188 const extension = MIMETYPES.IMAGE.MIMETYPE_EXT[actorJSON.icon.mediaType]
265ba139
C
189
190 const avatarName = uuidv4() + extension
6040f87d 191 await downloadImage(actorJSON.icon.url, CONFIG.STORAGE.AVATARS_DIR, avatarName, AVATARS_SIZE)
265ba139
C
192
193 return avatarName
194 }
195
196 return undefined
197}
198
16f29007
C
199async function addFetchOutboxJob (actor: ActorModel) {
200 // Don't fetch ourselves
201 const serverActor = await getServerActor()
202 if (serverActor.id === actor.id) {
203 logger.error('Cannot fetch our own outbox!')
204 return undefined
205 }
206
207 const payload = {
f6eebcb3
C
208 uri: actor.outboxUrl,
209 type: 'activity' as 'activity'
16f29007
C
210 }
211
212 return JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload })
213}
214
744d0eca
C
215async function refreshActorIfNeeded (
216 actorArg: ActorModel,
217 fetchedType: ActorFetchByUrlType
218): Promise<{ actor: ActorModel, refreshed: boolean }> {
219 if (!actorArg.isOutdated()) return { actor: actorArg, refreshed: false }
220
221 // We need more attributes
222 const actor = fetchedType === 'all' ? actorArg : await ActorModel.loadByUrlAndPopulateAccountAndChannel(actorArg.url)
223
224 try {
699b059e
C
225 let actorUrl: string
226 try {
227 actorUrl = await getUrlFromWebfinger(actor.preferredUsername + '@' + actor.getHost())
228 } catch (err) {
229 logger.warn('Cannot get actor URL from webfinger, keeping the old one.', err)
230 actorUrl = actor.url
231 }
232
744d0eca
C
233 const { result, statusCode } = await fetchRemoteActor(actorUrl)
234
235 if (statusCode === 404) {
236 logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url)
237 actor.Account ? actor.Account.destroy() : actor.VideoChannel.destroy()
238 return { actor: undefined, refreshed: false }
239 }
240
241 if (result === undefined) {
242 logger.warn('Cannot fetch remote actor in refresh actor.')
243 return { actor, refreshed: false }
244 }
245
246 return sequelizeTypescript.transaction(async t => {
247 updateInstanceWithAnother(actor, result.actor)
248
249 if (result.avatarName !== undefined) {
250 await updateActorAvatarInstance(actor, result.avatarName, t)
251 }
252
253 // Force update
254 actor.setDataValue('updatedAt', new Date())
255 await actor.save({ transaction: t })
256
257 if (actor.Account) {
258 actor.Account.set('name', result.name)
259 actor.Account.set('description', result.summary)
260
261 await actor.Account.save({ transaction: t })
262 } else if (actor.VideoChannel) {
263 actor.VideoChannel.set('name', result.name)
264 actor.VideoChannel.set('description', result.summary)
265 actor.VideoChannel.set('support', result.support)
266
267 await actor.VideoChannel.save({ transaction: t })
268 }
269
270 return { refreshed: true, actor }
271 })
272 } catch (err) {
4ee7a4c9 273 logger.warn('Cannot refresh actor %s.', actor.url, { err })
744d0eca
C
274 return { actor, refreshed: false }
275 }
276}
277
c5911fd3
C
278export {
279 getOrCreateActorAndServerAndModel,
280 buildActorInstance,
265ba139
C
281 setAsyncActorKeys,
282 fetchActorTotalItems,
a5625b41
C
283 fetchAvatarIfExists,
284 updateActorInstance,
744d0eca 285 refreshActorIfNeeded,
16f29007
C
286 updateActorAvatarInstance,
287 addFetchOutboxJob
c5911fd3
C
288}
289
290// ---------------------------------------------------------------------------
291
50d6de9c
C
292function saveActorAndServerAndModelIfNotExist (
293 result: FetchRemoteActorResult,
294 ownerActor?: ActorModel,
295 t?: Transaction
296): Bluebird<ActorModel> | Promise<ActorModel> {
297 let actor = result.actor
298
299 if (t !== undefined) return save(t)
300
301 return sequelizeTypescript.transaction(t => save(t))
302
303 async function save (t: Transaction) {
304 const actorHost = url.parse(actor.url).host
305
306 const serverOptions = {
307 where: {
308 host: actorHost
309 },
310 defaults: {
311 host: actorHost
312 },
313 transaction: t
314 }
315 const [ server ] = await ServerModel.findOrCreate(serverOptions)
316
317 // Save our new account in database
318 actor.set('serverId', server.id)
319
c5911fd3
C
320 // Avatar?
321 if (result.avatarName) {
322 const avatar = await AvatarModel.create({
323 filename: result.avatarName
324 }, { transaction: t })
325 actor.set('avatarId', avatar.id)
326 }
327
50d6de9c
C
328 // Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists
329 // (which could be false in a retried query)
2c897999
C
330 const [ actorCreated ] = await ActorModel.findOrCreate({
331 defaults: actor.toJSON(),
332 where: {
333 url: actor.url
334 },
335 transaction: t
336 })
50d6de9c
C
337
338 if (actorCreated.type === 'Person' || actorCreated.type === 'Application') {
2422c46b 339 actorCreated.Account = await saveAccount(actorCreated, result, t)
50d6de9c
C
340 actorCreated.Account.Actor = actorCreated
341 } else if (actorCreated.type === 'Group') { // Video channel
2422c46b 342 actorCreated.VideoChannel = await saveVideoChannel(actorCreated, result, ownerActor, t)
50d6de9c 343 actorCreated.VideoChannel.Actor = actorCreated
f6eebcb3 344 actorCreated.VideoChannel.Account = ownerActor.Account
50d6de9c
C
345 }
346
883993c8
C
347 actorCreated.Server = server
348
50d6de9c
C
349 return actorCreated
350 }
351}
352
353type FetchRemoteActorResult = {
354 actor: ActorModel
e12a0092 355 name: string
50d6de9c 356 summary: string
2422c46b 357 support?: string
418d092a 358 playlists?: string
c5911fd3 359 avatarName?: string
50d6de9c
C
360 attributedTo: ActivityPubAttributedTo[]
361}
f5b0af50 362async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: number, result: FetchRemoteActorResult }> {
50d6de9c
C
363 const options = {
364 uri: actorUrl,
365 method: 'GET',
da854ddd
C
366 json: true,
367 activityPub: true
50d6de9c
C
368 }
369
370 logger.info('Fetching remote actor %s.', actorUrl)
371
4c280004 372 const requestResult = await doRequest<ActivityPubActor>(options)
f47776e2
C
373 normalizeActor(requestResult.body)
374
4c280004 375 const actorJSON = requestResult.body
265ba139 376 if (isActorObjectValid(actorJSON) === false) {
b4593cd7 377 logger.debug('Remote actor JSON is not valid.', { actorJSON })
f5b0af50 378 return { result: undefined, statusCode: requestResult.response.statusCode }
50d6de9c
C
379 }
380
5c6d985f 381 if (checkUrlsSameHost(actorJSON.id, actorUrl) !== true) {
9f79ade6
C
382 logger.warn('Actor url %s has not the same host than its AP id %s', actorUrl, actorJSON.id)
383 return { result: undefined, statusCode: requestResult.response.statusCode }
5c6d985f
C
384 }
385
50d6de9c
C
386 const followersCount = await fetchActorTotalItems(actorJSON.followers)
387 const followingCount = await fetchActorTotalItems(actorJSON.following)
388
389 const actor = new ActorModel({
390 type: actorJSON.type,
391 uuid: actorJSON.uuid,
e12a0092
C
392 preferredUsername: actorJSON.preferredUsername,
393 url: actorJSON.id,
50d6de9c
C
394 publicKey: actorJSON.publicKey.publicKeyPem,
395 privateKey: null,
396 followersCount: followersCount,
397 followingCount: followingCount,
398 inboxUrl: actorJSON.inbox,
399 outboxUrl: actorJSON.outbox,
400 sharedInboxUrl: actorJSON.endpoints.sharedInbox,
401 followersUrl: actorJSON.followers,
402 followingUrl: actorJSON.following
403 })
404
265ba139 405 const avatarName = await fetchAvatarIfExists(actorJSON)
c5911fd3 406
e12a0092 407 const name = actorJSON.name || actorJSON.preferredUsername
50d6de9c 408 return {
f5b0af50
C
409 statusCode: requestResult.response.statusCode,
410 result: {
411 actor,
412 name,
413 avatarName,
414 summary: actorJSON.summary,
415 support: actorJSON.support,
418d092a 416 playlists: actorJSON.playlists,
f5b0af50
C
417 attributedTo: actorJSON.attributedTo
418 }
50d6de9c
C
419 }
420}
421
2c897999
C
422async function saveAccount (actor: ActorModel, result: FetchRemoteActorResult, t: Transaction) {
423 const [ accountCreated ] = await AccountModel.findOrCreate({
424 defaults: {
425 name: result.name,
2422c46b 426 description: result.summary,
2c897999
C
427 actorId: actor.id
428 },
429 where: {
430 actorId: actor.id
431 },
432 transaction: t
50d6de9c
C
433 })
434
2c897999 435 return accountCreated
50d6de9c
C
436}
437
438async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResult, ownerActor: ActorModel, t: Transaction) {
2c897999
C
439 const [ videoChannelCreated ] = await VideoChannelModel.findOrCreate({
440 defaults: {
441 name: result.name,
442 description: result.summary,
2422c46b 443 support: result.support,
2c897999
C
444 actorId: actor.id,
445 accountId: ownerActor.Account.id
446 },
447 where: {
448 actorId: actor.id
449 },
450 transaction: t
50d6de9c
C
451 })
452
2c897999 453 return videoChannelCreated
50d6de9c 454}