]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/lib/activitypub/actor.ts
Update follower/following counts
[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 { AVATAR_MIMETYPE_EXT, CONFIG, sequelizeTypescript } 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
22 // Set account keys, this could be long so process after the account creation and do not block the client
23 function 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
36 async 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
67 return refreshActorIfNeeded(actor)
68 }
69
70 function 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
88 async 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
106 async 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
127 async 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
146 async 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
167 export {
168 getOrCreateActorAndServerAndModel,
169 buildActorInstance,
170 setAsyncActorKeys,
171 fetchActorTotalItems,
172 fetchAvatarIfExists,
173 updateActorInstance,
174 updateActorAvatarInstance
175 }
176
177 // ---------------------------------------------------------------------------
178
179 function 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
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
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
233 type FetchRemoteActorResult = {
234 actor: ActorModel
235 name: string
236 summary: string
237 avatarName?: string
238 attributedTo: ActivityPubAttributedTo[]
239 }
240 async function fetchRemoteActor (actorUrl: string): Promise<FetchRemoteActorResult> {
241 const options = {
242 uri: actorUrl,
243 method: 'GET',
244 json: true,
245 activityPub: true
246 }
247
248 logger.info('Fetching remote actor %s.', actorUrl)
249
250 const requestResult = await doRequest(options)
251 const actorJSON: ActivityPubActor = requestResult.body
252
253 if (isActorObjectValid(actorJSON) === false) {
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,
264 preferredUsername: actorJSON.preferredUsername,
265 url: actorJSON.id,
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
277 const avatarName = await fetchAvatarIfExists(actorJSON)
278
279 const name = actorJSON.name || actorJSON.preferredUsername
280 return {
281 actor,
282 name,
283 avatarName,
284 summary: actorJSON.summary,
285 attributedTo: actorJSON.attributedTo
286 }
287 }
288
289 function saveAccount (actor: ActorModel, result: FetchRemoteActorResult, t: Transaction) {
290 const account = new AccountModel({
291 name: result.name,
292 actorId: actor.id
293 })
294
295 return account.save({ transaction: t })
296 }
297
298 async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResult, ownerActor: ActorModel, t: Transaction) {
299 const videoChannel = new VideoChannelModel({
300 name: result.name,
301 description: result.summary,
302 actorId: actor.id,
303 accountId: ownerActor.Account.id
304 })
305
306 return videoChannel.save({ transaction: t })
307 }
308
309 async 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 logger.info('coucou', result.actor.toJSON())
318 updateInstanceWithAnother(actor, result.actor)
319
320 if (result.avatarName !== undefined) {
321 await updateActorAvatarInstance(actor, result.avatarName, t)
322 }
323
324 // Force update
325 actor.setDataValue('updatedAt', new Date())
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 }