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