]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/lib/activitypub/actor.ts
Fix max buffer reached in youtube import
[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'
265ba139 8import { isActorObjectValid } 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'
c5911fd3 13import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests'
a5625b41 14import { getUrlFromWebfinger } from '../../helpers/webfinger'
ac81d1a0 15import { IMAGE_MIMETYPE_EXT, CONFIG, sequelizeTypescript } from '../../initializers'
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'
21
e12a0092 22// Set account keys, this could be long so process after the account creation and do not block the client
50d6de9c
C
23function 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
36async 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
94a5ff8a
C
67 const options = {
68 arguments: [ actor ],
69 errorMessage: 'Cannot refresh actor if needed with many retries.'
70 }
71 return retryTransactionWrapper(refreshActorIfNeeded, options)
50d6de9c
C
72}
73
c5911fd3
C
74function 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
a5625b41
C
92async 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
110async 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
265ba139
C
131async function fetchActorTotalItems (url: string) {
132 const options = {
133 uri: url,
134 method: 'GET',
135 json: true,
136 activityPub: true
137 }
138
265ba139 139 try {
7006bc63
C
140 const { body } = await doRequest(options)
141 return body.totalItems ? body.totalItems : 0
265ba139
C
142 } catch (err) {
143 logger.warn('Cannot fetch remote actor count %s.', url, err)
7006bc63 144 return 0
265ba139 145 }
265ba139
C
146}
147
148async function fetchAvatarIfExists (actorJSON: ActivityPubActor) {
149 if (
ac81d1a0 150 actorJSON.icon && actorJSON.icon.type === 'Image' && IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType] !== undefined &&
265ba139
C
151 isActivityPubUrlValid(actorJSON.icon.url)
152 ) {
ac81d1a0 153 const extension = IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType]
265ba139
C
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
c5911fd3
C
169export {
170 getOrCreateActorAndServerAndModel,
171 buildActorInstance,
265ba139
C
172 setAsyncActorKeys,
173 fetchActorTotalItems,
a5625b41
C
174 fetchAvatarIfExists,
175 updateActorInstance,
176 updateActorAvatarInstance
c5911fd3
C
177}
178
179// ---------------------------------------------------------------------------
180
50d6de9c
C
181function 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
c5911fd3
C
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
50d6de9c
C
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)
2c897999
C
219 const [ actorCreated ] = await ActorModel.findOrCreate({
220 defaults: actor.toJSON(),
221 where: {
222 url: actor.url
223 },
224 transaction: t
225 })
50d6de9c
C
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
241type FetchRemoteActorResult = {
242 actor: ActorModel
e12a0092 243 name: string
50d6de9c 244 summary: string
c5911fd3 245 avatarName?: string
50d6de9c
C
246 attributedTo: ActivityPubAttributedTo[]
247}
248async function fetchRemoteActor (actorUrl: string): Promise<FetchRemoteActorResult> {
249 const options = {
250 uri: actorUrl,
251 method: 'GET',
da854ddd
C
252 json: true,
253 activityPub: true
50d6de9c
C
254 }
255
256 logger.info('Fetching remote actor %s.', actorUrl)
257
da854ddd 258 const requestResult = await doRequest(options)
d765fafc 259 const actorJSON: ActivityPubActor = normalizeActor(requestResult.body)
50d6de9c 260
265ba139 261 if (isActorObjectValid(actorJSON) === false) {
50d6de9c
C
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,
e12a0092
C
272 preferredUsername: actorJSON.preferredUsername,
273 url: actorJSON.id,
50d6de9c
C
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
265ba139 285 const avatarName = await fetchAvatarIfExists(actorJSON)
c5911fd3 286
e12a0092 287 const name = actorJSON.name || actorJSON.preferredUsername
50d6de9c
C
288 return {
289 actor,
e12a0092 290 name,
c5911fd3 291 avatarName,
50d6de9c
C
292 summary: actorJSON.summary,
293 attributedTo: actorJSON.attributedTo
294 }
295}
296
2c897999
C
297async 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
50d6de9c
C
307 })
308
2c897999 309 return accountCreated
50d6de9c
C
310}
311
312async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResult, ownerActor: ActorModel, t: Transaction) {
2c897999
C
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
50d6de9c
C
324 })
325
2c897999 326 return videoChannelCreated
50d6de9c 327}
a5625b41
C
328
329async function refreshActorIfNeeded (actor: ActorModel) {
330 if (!actor.isOutdated()) return actor
331
94a5ff8a
C
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
a5625b41
C
338 }
339
94a5ff8a
C
340 return sequelizeTypescript.transaction(async t => {
341 updateInstanceWithAnother(actor, result.actor)
a5625b41 342
94a5ff8a
C
343 if (result.avatarName !== undefined) {
344 await updateActorAvatarInstance(actor, result.avatarName, t)
345 }
a5625b41 346
94a5ff8a
C
347 // Force update
348 actor.setDataValue('updatedAt', new Date())
a5625b41
C
349 await actor.save({ transaction: t })
350
94a5ff8a
C
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 }
a5625b41 362
94a5ff8a
C
363 return actor
364 })
365 } catch (err) {
366 logger.warn('Cannot refresh actor.', err)
a5625b41 367 return actor
94a5ff8a 368 }
a5625b41 369}
d765fafc
C
370
371function 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}