]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/lib/activitypub/actor.ts
Avoid too many requests and fetching 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)
215 const actorCreated = await ActorModel.create(actor.toJSON(), { transaction: t })
216
217 if (actorCreated.type === 'Person' || actorCreated.type === 'Application') {
218 const account = await saveAccount(actorCreated, result, t)
219 actorCreated.Account = account
220 actorCreated.Account.Actor = actorCreated
221 } else if (actorCreated.type === 'Group') { // Video channel
222 const videoChannel = await saveVideoChannel(actorCreated, result, ownerActor, t)
223 actorCreated.VideoChannel = videoChannel
224 actorCreated.VideoChannel.Actor = actorCreated
225 }
226
227 return actorCreated
228 }
229}
230
231type FetchRemoteActorResult = {
232 actor: ActorModel
e12a0092 233 name: string
50d6de9c 234 summary: string
c5911fd3 235 avatarName?: string
50d6de9c
C
236 attributedTo: ActivityPubAttributedTo[]
237}
238async function fetchRemoteActor (actorUrl: string): Promise<FetchRemoteActorResult> {
239 const options = {
240 uri: actorUrl,
241 method: 'GET',
da854ddd
C
242 json: true,
243 activityPub: true
50d6de9c
C
244 }
245
246 logger.info('Fetching remote actor %s.', actorUrl)
247
da854ddd
C
248 const requestResult = await doRequest(options)
249 const actorJSON: ActivityPubActor = requestResult.body
50d6de9c 250
265ba139 251 if (isActorObjectValid(actorJSON) === false) {
50d6de9c
C
252 logger.debug('Remote actor JSON is not valid.', { actorJSON: actorJSON })
253 return undefined
254 }
255
256 const followersCount = await fetchActorTotalItems(actorJSON.followers)
257 const followingCount = await fetchActorTotalItems(actorJSON.following)
258
259 const actor = new ActorModel({
260 type: actorJSON.type,
261 uuid: actorJSON.uuid,
e12a0092
C
262 preferredUsername: actorJSON.preferredUsername,
263 url: actorJSON.id,
50d6de9c
C
264 publicKey: actorJSON.publicKey.publicKeyPem,
265 privateKey: null,
266 followersCount: followersCount,
267 followingCount: followingCount,
268 inboxUrl: actorJSON.inbox,
269 outboxUrl: actorJSON.outbox,
270 sharedInboxUrl: actorJSON.endpoints.sharedInbox,
271 followersUrl: actorJSON.followers,
272 followingUrl: actorJSON.following
273 })
274
265ba139 275 const avatarName = await fetchAvatarIfExists(actorJSON)
c5911fd3 276
e12a0092 277 const name = actorJSON.name || actorJSON.preferredUsername
50d6de9c
C
278 return {
279 actor,
e12a0092 280 name,
c5911fd3 281 avatarName,
50d6de9c
C
282 summary: actorJSON.summary,
283 attributedTo: actorJSON.attributedTo
284 }
285}
286
50d6de9c
C
287function saveAccount (actor: ActorModel, result: FetchRemoteActorResult, t: Transaction) {
288 const account = new AccountModel({
e12a0092 289 name: result.name,
50d6de9c
C
290 actorId: actor.id
291 })
292
293 return account.save({ transaction: t })
294}
295
296async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResult, ownerActor: ActorModel, t: Transaction) {
297 const videoChannel = new VideoChannelModel({
e12a0092 298 name: result.name,
50d6de9c
C
299 description: result.summary,
300 actorId: actor.id,
301 accountId: ownerActor.Account.id
302 })
303
304 return videoChannel.save({ transaction: t })
305}
a5625b41
C
306
307async function refreshActorIfNeeded (actor: ActorModel) {
308 if (!actor.isOutdated()) return actor
309
310 const actorUrl = await getUrlFromWebfinger(actor.preferredUsername, actor.getHost())
311 const result = await fetchRemoteActor(actorUrl)
f05a1c30
C
312 if (result === undefined) {
313 logger.warn('Cannot fetch remote actor in refresh actor.')
314 return actor
315 }
a5625b41
C
316
317 return sequelizeTypescript.transaction(async t => {
318 updateInstanceWithAnother(actor, result.actor)
319
320 if (result.avatarName !== undefined) {
321 await updateActorAvatarInstance(actor, result.avatarName, t)
322 }
323
6fd5ca1e
C
324 // Force update
325 actor.setDataValue('updatedAt', new Date())
a5625b41
C
326 await actor.save({ transaction: t })
327
328 if (actor.Account) {
329 await actor.save({ transaction: t })
330
331 actor.Account.set('name', result.name)
332 await actor.Account.save({ transaction: t })
333 } else if (actor.VideoChannel) {
334 await actor.save({ transaction: t })
335
336 actor.VideoChannel.set('name', result.name)
337 await actor.VideoChannel.save({ transaction: t })
338 }
339
340 return actor
341 })
342}