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