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