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