]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/lib/activitypub/actor.ts
Add tests to handle down server
[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
135 let requestResult
136 try {
137 requestResult = await doRequest(options)
138 } catch (err) {
139 logger.warn('Cannot fetch remote actor count %s.', url, err)
140 return undefined
141 }
142
143 return requestResult.totalItems ? requestResult.totalItems : 0
144}
145
146async function fetchAvatarIfExists (actorJSON: ActivityPubActor) {
147 if (
148 actorJSON.icon && actorJSON.icon.type === 'Image' && AVATAR_MIMETYPE_EXT[actorJSON.icon.mediaType] !== undefined &&
149 isActivityPubUrlValid(actorJSON.icon.url)
150 ) {
151 const extension = AVATAR_MIMETYPE_EXT[actorJSON.icon.mediaType]
152
153 const avatarName = uuidv4() + extension
154 const destPath = join(CONFIG.STORAGE.AVATARS_DIR, avatarName)
155
156 await doRequestAndSaveToFile({
157 method: 'GET',
158 uri: actorJSON.icon.url
159 }, destPath)
160
161 return avatarName
162 }
163
164 return undefined
165}
166
c5911fd3
C
167export {
168 getOrCreateActorAndServerAndModel,
169 buildActorInstance,
265ba139
C
170 setAsyncActorKeys,
171 fetchActorTotalItems,
a5625b41
C
172 fetchAvatarIfExists,
173 updateActorInstance,
174 updateActorAvatarInstance
c5911fd3
C
175}
176
177// ---------------------------------------------------------------------------
178
50d6de9c
C
179function saveActorAndServerAndModelIfNotExist (
180 result: FetchRemoteActorResult,
181 ownerActor?: ActorModel,
182 t?: Transaction
183): Bluebird<ActorModel> | Promise<ActorModel> {
184 let actor = result.actor
185
186 if (t !== undefined) return save(t)
187
188 return sequelizeTypescript.transaction(t => save(t))
189
190 async function save (t: Transaction) {
191 const actorHost = url.parse(actor.url).host
192
193 const serverOptions = {
194 where: {
195 host: actorHost
196 },
197 defaults: {
198 host: actorHost
199 },
200 transaction: t
201 }
202 const [ server ] = await ServerModel.findOrCreate(serverOptions)
203
204 // Save our new account in database
205 actor.set('serverId', server.id)
206
c5911fd3
C
207 // Avatar?
208 if (result.avatarName) {
209 const avatar = await AvatarModel.create({
210 filename: result.avatarName
211 }, { transaction: t })
212 actor.set('avatarId', avatar.id)
213 }
214
50d6de9c
C
215 // Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists
216 // (which could be false in a retried query)
217 const actorCreated = await ActorModel.create(actor.toJSON(), { transaction: t })
218
219 if (actorCreated.type === 'Person' || actorCreated.type === 'Application') {
220 const account = await saveAccount(actorCreated, result, t)
221 actorCreated.Account = account
222 actorCreated.Account.Actor = actorCreated
223 } else if (actorCreated.type === 'Group') { // Video channel
224 const videoChannel = await saveVideoChannel(actorCreated, result, ownerActor, t)
225 actorCreated.VideoChannel = videoChannel
226 actorCreated.VideoChannel.Actor = actorCreated
227 }
228
229 return actorCreated
230 }
231}
232
233type FetchRemoteActorResult = {
234 actor: ActorModel
e12a0092 235 name: string
50d6de9c 236 summary: string
c5911fd3 237 avatarName?: string
50d6de9c
C
238 attributedTo: ActivityPubAttributedTo[]
239}
240async function fetchRemoteActor (actorUrl: string): Promise<FetchRemoteActorResult> {
241 const options = {
242 uri: actorUrl,
243 method: 'GET',
da854ddd
C
244 json: true,
245 activityPub: true
50d6de9c
C
246 }
247
248 logger.info('Fetching remote actor %s.', actorUrl)
249
da854ddd
C
250 const requestResult = await doRequest(options)
251 const actorJSON: ActivityPubActor = requestResult.body
50d6de9c 252
265ba139 253 if (isActorObjectValid(actorJSON) === false) {
50d6de9c
C
254 logger.debug('Remote actor JSON is not valid.', { actorJSON: actorJSON })
255 return undefined
256 }
257
258 const followersCount = await fetchActorTotalItems(actorJSON.followers)
259 const followingCount = await fetchActorTotalItems(actorJSON.following)
260
261 const actor = new ActorModel({
262 type: actorJSON.type,
263 uuid: actorJSON.uuid,
e12a0092
C
264 preferredUsername: actorJSON.preferredUsername,
265 url: actorJSON.id,
50d6de9c
C
266 publicKey: actorJSON.publicKey.publicKeyPem,
267 privateKey: null,
268 followersCount: followersCount,
269 followingCount: followingCount,
270 inboxUrl: actorJSON.inbox,
271 outboxUrl: actorJSON.outbox,
272 sharedInboxUrl: actorJSON.endpoints.sharedInbox,
273 followersUrl: actorJSON.followers,
274 followingUrl: actorJSON.following
275 })
276
265ba139 277 const avatarName = await fetchAvatarIfExists(actorJSON)
c5911fd3 278
e12a0092 279 const name = actorJSON.name || actorJSON.preferredUsername
50d6de9c
C
280 return {
281 actor,
e12a0092 282 name,
c5911fd3 283 avatarName,
50d6de9c
C
284 summary: actorJSON.summary,
285 attributedTo: actorJSON.attributedTo
286 }
287}
288
50d6de9c
C
289function saveAccount (actor: ActorModel, result: FetchRemoteActorResult, t: Transaction) {
290 const account = new AccountModel({
e12a0092 291 name: result.name,
50d6de9c
C
292 actorId: actor.id
293 })
294
295 return account.save({ transaction: t })
296}
297
298async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResult, ownerActor: ActorModel, t: Transaction) {
299 const videoChannel = new VideoChannelModel({
e12a0092 300 name: result.name,
50d6de9c
C
301 description: result.summary,
302 actorId: actor.id,
303 accountId: ownerActor.Account.id
304 })
305
306 return videoChannel.save({ transaction: t })
307}
a5625b41
C
308
309async function refreshActorIfNeeded (actor: ActorModel) {
310 if (!actor.isOutdated()) return actor
311
312 const actorUrl = await getUrlFromWebfinger(actor.preferredUsername, actor.getHost())
313 const result = await fetchRemoteActor(actorUrl)
314 if (result === undefined) throw new Error('Cannot fetch remote actor in refresh actor.')
315
316 return sequelizeTypescript.transaction(async t => {
317 updateInstanceWithAnother(actor, result.actor)
318
319 if (result.avatarName !== undefined) {
320 await updateActorAvatarInstance(actor, result.avatarName, t)
321 }
322
323 await actor.save({ transaction: t })
324
325 if (actor.Account) {
326 await actor.save({ transaction: t })
327
328 actor.Account.set('name', result.name)
329 await actor.Account.save({ transaction: t })
330 } else if (actor.VideoChannel) {
331 await actor.save({ transaction: t })
332
333 actor.VideoChannel.set('name', result.name)
334 await actor.VideoChannel.save({ transaction: t })
335 }
336
337 return actor
338 })
339}