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