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