aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/controllers/api/search.ts4
-rw-r--r--server/controllers/api/users/me.ts2
-rw-r--r--server/controllers/api/video-channel.ts2
-rw-r--r--server/controllers/lazy-static.ts2
-rw-r--r--server/lib/activitypub/actor.ts593
-rw-r--r--server/lib/activitypub/actors/get.ts119
-rw-r--r--server/lib/activitypub/actors/image.ts94
-rw-r--r--server/lib/activitypub/actors/index.ts5
-rw-r--r--server/lib/activitypub/actors/keys.ts16
-rw-r--r--server/lib/activitypub/actors/refresh.ts63
-rw-r--r--server/lib/activitypub/actors/shared/creator.ts149
-rw-r--r--server/lib/activitypub/actors/shared/index.ts3
-rw-r--r--server/lib/activitypub/actors/shared/object-to-model-attributes.ts70
-rw-r--r--server/lib/activitypub/actors/shared/url-to-object.ts54
-rw-r--r--server/lib/activitypub/actors/updater.ts90
-rw-r--r--server/lib/activitypub/outbox.ts24
-rw-r--r--server/lib/activitypub/playlists/create-update.ts4
-rw-r--r--server/lib/activitypub/process/process-accept.ts2
-rw-r--r--server/lib/activitypub/process/process-update.ts62
-rw-r--r--server/lib/activitypub/process/process.ts14
-rw-r--r--server/lib/activitypub/share.ts4
-rw-r--r--server/lib/activitypub/video-comments.ts4
-rw-r--r--server/lib/activitypub/video-rates.ts4
-rw-r--r--server/lib/activitypub/videos/shared/abstract-builder.ts4
-rw-r--r--server/lib/activitypub/videos/updater.ts2
-rw-r--r--server/lib/job-queue/handlers/activitypub-follow.ts4
-rw-r--r--server/lib/job-queue/handlers/activitypub-refresher.ts2
-rw-r--r--server/lib/job-queue/handlers/actor-keys.ts2
-rw-r--r--server/lib/local-actor.ts (renamed from server/lib/actor-image.ts)29
-rw-r--r--server/lib/user.ts3
-rw-r--r--server/lib/video-channel.ts2
-rw-r--r--server/middlewares/activitypub.ts6
32 files changed, 753 insertions, 685 deletions
diff --git a/server/controllers/api/search.ts b/server/controllers/api/search.ts
index 0cb5674c2..ef0f4285d 100644
--- a/server/controllers/api/search.ts
+++ b/server/controllers/api/search.ts
@@ -15,7 +15,7 @@ import { buildNSFWFilter, isUserAbleToSearchRemoteURI } from '../../helpers/expr
15import { logger } from '../../helpers/logger' 15import { logger } from '../../helpers/logger'
16import { getFormattedObjects } from '../../helpers/utils' 16import { getFormattedObjects } from '../../helpers/utils'
17import { loadActorUrlOrGetFromWebfinger } from '../../helpers/webfinger' 17import { loadActorUrlOrGetFromWebfinger } from '../../helpers/webfinger'
18import { getOrCreateActorAndServerAndModel } from '../../lib/activitypub/actor' 18import { getOrCreateAPActor } from '../../lib/activitypub/actors'
19import { 19import {
20 asyncMiddleware, 20 asyncMiddleware,
21 commonVideosFiltersValidator, 21 commonVideosFiltersValidator,
@@ -145,7 +145,7 @@ async function searchVideoChannelURI (search: string, isWebfingerSearch: boolean
145 145
146 if (isUserAbleToSearchRemoteURI(res)) { 146 if (isUserAbleToSearchRemoteURI(res)) {
147 try { 147 try {
148 const actor = await getOrCreateActorAndServerAndModel(uri, 'all', true, true) 148 const actor = await getOrCreateAPActor(uri, 'all', true, true)
149 videoChannel = actor.VideoChannel 149 videoChannel = actor.VideoChannel
150 } catch (err) { 150 } catch (err) {
151 logger.info('Cannot search remote video channel %s.', uri, { err }) 151 logger.info('Cannot search remote video channel %s.', uri, { err })
diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts
index 810e4295e..1f2b2f9dd 100644
--- a/server/controllers/api/users/me.ts
+++ b/server/controllers/api/users/me.ts
@@ -11,7 +11,7 @@ import { CONFIG } from '../../../initializers/config'
11import { MIMETYPES } from '../../../initializers/constants' 11import { MIMETYPES } from '../../../initializers/constants'
12import { sequelizeTypescript } from '../../../initializers/database' 12import { sequelizeTypescript } from '../../../initializers/database'
13import { sendUpdateActor } from '../../../lib/activitypub/send' 13import { sendUpdateActor } from '../../../lib/activitypub/send'
14import { deleteLocalActorImageFile, updateLocalActorImageFile } from '../../../lib/actor-image' 14import { deleteLocalActorImageFile, updateLocalActorImageFile } from '../../../lib/local-actor'
15import { getOriginalVideoFileTotalDailyFromUser, getOriginalVideoFileTotalFromUser, sendVerifyUserEmail } from '../../../lib/user' 15import { getOriginalVideoFileTotalDailyFromUser, getOriginalVideoFileTotalFromUser, sendVerifyUserEmail } from '../../../lib/user'
16import { 16import {
17 asyncMiddleware, 17 asyncMiddleware,
diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts
index 34207ea8a..03aa918d3 100644
--- a/server/controllers/api/video-channel.ts
+++ b/server/controllers/api/video-channel.ts
@@ -13,8 +13,8 @@ import { CONFIG } from '../../initializers/config'
13import { MIMETYPES } from '../../initializers/constants' 13import { MIMETYPES } from '../../initializers/constants'
14import { sequelizeTypescript } from '../../initializers/database' 14import { sequelizeTypescript } from '../../initializers/database'
15import { sendUpdateActor } from '../../lib/activitypub/send' 15import { sendUpdateActor } from '../../lib/activitypub/send'
16import { deleteLocalActorImageFile, updateLocalActorImageFile } from '../../lib/actor-image'
17import { JobQueue } from '../../lib/job-queue' 16import { JobQueue } from '../../lib/job-queue'
17import { deleteLocalActorImageFile, updateLocalActorImageFile } from '../../lib/local-actor'
18import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel' 18import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel'
19import { 19import {
20 asyncMiddleware, 20 asyncMiddleware,
diff --git a/server/controllers/lazy-static.ts b/server/controllers/lazy-static.ts
index 9f260cef0..27b1b7160 100644
--- a/server/controllers/lazy-static.ts
+++ b/server/controllers/lazy-static.ts
@@ -4,8 +4,8 @@ import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache
4import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' 4import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
5import { logger } from '../helpers/logger' 5import { logger } from '../helpers/logger'
6import { LAZY_STATIC_PATHS, STATIC_MAX_AGE } from '../initializers/constants' 6import { LAZY_STATIC_PATHS, STATIC_MAX_AGE } from '../initializers/constants'
7import { actorImagePathUnsafeCache, pushActorImageProcessInQueue } from '../lib/actor-image'
8import { VideosCaptionCache, VideosPreviewCache } from '../lib/files-cache' 7import { VideosCaptionCache, VideosPreviewCache } from '../lib/files-cache'
8import { actorImagePathUnsafeCache, pushActorImageProcessInQueue } from '../lib/local-actor'
9import { asyncMiddleware } from '../middlewares' 9import { asyncMiddleware } from '../middlewares'
10import { ActorImageModel } from '../models/actor/actor-image' 10import { ActorImageModel } from '../models/actor/actor-image'
11 11
diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts
deleted file mode 100644
index 1bcee7ef9..000000000
--- a/server/lib/activitypub/actor.ts
+++ /dev/null
@@ -1,593 +0,0 @@
1import * as Bluebird from 'bluebird'
2import { extname } from 'path'
3import { Op, Transaction } from 'sequelize'
4import { URL } from 'url'
5import { v4 as uuidv4 } from 'uuid'
6import { getServerActor } from '@server/models/application/application'
7import { ActorImageType } from '@shared/models'
8import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
9import { ActivityPubActor, ActivityPubActorType, ActivityPubOrderedCollection } from '../../../shared/models/activitypub'
10import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects'
11import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
12import { ActorFetchByUrlType, fetchActorByUrl } from '../../helpers/actor'
13import { sanitizeAndCheckActorObject } from '../../helpers/custom-validators/activitypub/actor'
14import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
15import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils'
16import { logger } from '../../helpers/logger'
17import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto'
18import { doJSONRequest, PeerTubeRequestError } from '../../helpers/requests'
19import { getUrlFromWebfinger } from '../../helpers/webfinger'
20import { MIMETYPES, WEBSERVER } from '../../initializers/constants'
21import { sequelizeTypescript } from '../../initializers/database'
22import { AccountModel } from '../../models/account/account'
23import { ActorModel } from '../../models/actor/actor'
24import { ActorImageModel } from '../../models/actor/actor-image'
25import { ServerModel } from '../../models/server/server'
26import { VideoChannelModel } from '../../models/video/video-channel'
27import {
28 MAccount,
29 MAccountDefault,
30 MActor,
31 MActorAccountChannelId,
32 MActorAccountChannelIdActor,
33 MActorAccountId,
34 MActorFull,
35 MActorFullActor,
36 MActorId,
37 MActorImage,
38 MActorImages,
39 MChannel
40} from '../../types/models'
41import { JobQueue } from '../job-queue'
42
43// Set account keys, this could be long so process after the account creation and do not block the client
44async function generateAndSaveActorKeys <T extends MActor> (actor: T) {
45 const { publicKey, privateKey } = await createPrivateAndPublicKeys()
46
47 actor.publicKey = publicKey
48 actor.privateKey = privateKey
49
50 return actor.save()
51}
52
53function getOrCreateActorAndServerAndModel (
54 activityActor: string | ActivityPubActor,
55 fetchType: 'all',
56 recurseIfNeeded?: boolean,
57 updateCollections?: boolean
58): Promise<MActorFullActor>
59
60function getOrCreateActorAndServerAndModel (
61 activityActor: string | ActivityPubActor,
62 fetchType?: 'association-ids',
63 recurseIfNeeded?: boolean,
64 updateCollections?: boolean
65): Promise<MActorAccountChannelId>
66
67async function getOrCreateActorAndServerAndModel (
68 activityActor: string | ActivityPubActor,
69 fetchType: ActorFetchByUrlType = 'association-ids',
70 recurseIfNeeded = true,
71 updateCollections = false
72): Promise<MActorFullActor | MActorAccountChannelId> {
73 const actorUrl = getAPId(activityActor)
74 let created = false
75 let accountPlaylistsUrl: string
76
77 let actor = await fetchActorByUrl(actorUrl, fetchType)
78 // Orphan actor (not associated to an account of channel) so recreate it
79 if (actor && (!actor.Account && !actor.VideoChannel)) {
80 await actor.destroy()
81 actor = null
82 }
83
84 // We don't have this actor in our database, fetch it on remote
85 if (!actor) {
86 const { result } = await fetchRemoteActor(actorUrl)
87 if (result === undefined) throw new Error('Cannot fetch remote actor ' + actorUrl)
88
89 // Create the attributed to actor
90 // In PeerTube a video channel is owned by an account
91 let ownerActor: MActorFullActor
92 if (recurseIfNeeded === true && result.actor.type === 'Group') {
93 const accountAttributedTo = result.attributedTo.find(a => a.type === 'Person')
94 if (!accountAttributedTo) throw new Error('Cannot find account attributed to video channel ' + actor.url)
95
96 if (checkUrlsSameHost(accountAttributedTo.id, actorUrl) !== true) {
97 throw new Error(`Account attributed to ${accountAttributedTo.id} does not have the same host than actor url ${actorUrl}`)
98 }
99
100 try {
101 // Don't recurse another time
102 const recurseIfNeeded = false
103 ownerActor = await getOrCreateActorAndServerAndModel(accountAttributedTo.id, 'all', recurseIfNeeded)
104 } catch (err) {
105 logger.error('Cannot get or create account attributed to video channel ' + actorUrl)
106 throw new Error(err)
107 }
108 }
109
110 actor = await retryTransactionWrapper(saveActorAndServerAndModelIfNotExist, result, ownerActor)
111 created = true
112 accountPlaylistsUrl = result.playlists
113 }
114
115 if (actor.Account) (actor as MActorAccountChannelIdActor).Account.Actor = actor
116 if (actor.VideoChannel) (actor as MActorAccountChannelIdActor).VideoChannel.Actor = actor
117
118 const { actor: actorRefreshed, refreshed } = await retryTransactionWrapper(refreshActorIfNeeded, actor, fetchType)
119 if (!actorRefreshed) throw new Error('Actor ' + actor.url + ' does not exist anymore.')
120
121 if ((created === true || refreshed === true) && updateCollections === true) {
122 const payload = { uri: actor.outboxUrl, type: 'activity' as 'activity' }
123 await JobQueue.Instance.createJobWithPromise({ type: 'activitypub-http-fetcher', payload })
124 }
125
126 // We created a new account: fetch the playlists
127 if (created === true && actor.Account && accountPlaylistsUrl) {
128 const payload = { uri: accountPlaylistsUrl, accountId: actor.Account.id, type: 'account-playlists' as 'account-playlists' }
129 await JobQueue.Instance.createJobWithPromise({ type: 'activitypub-http-fetcher', payload })
130 }
131
132 return actorRefreshed
133}
134
135function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string) {
136 return new ActorModel({
137 type,
138 url,
139 preferredUsername,
140 publicKey: null,
141 privateKey: null,
142 followersCount: 0,
143 followingCount: 0,
144 inboxUrl: url + '/inbox',
145 outboxUrl: url + '/outbox',
146 sharedInboxUrl: WEBSERVER.URL + '/inbox',
147 followersUrl: url + '/followers',
148 followingUrl: url + '/following'
149 }) as MActor
150}
151
152async function updateActorInstance (actorInstance: ActorModel, attributes: ActivityPubActor) {
153 const followersCount = await fetchActorTotalItems(attributes.followers)
154 const followingCount = await fetchActorTotalItems(attributes.following)
155
156 actorInstance.type = attributes.type
157 actorInstance.preferredUsername = attributes.preferredUsername
158 actorInstance.url = attributes.id
159 actorInstance.publicKey = attributes.publicKey.publicKeyPem
160 actorInstance.followersCount = followersCount
161 actorInstance.followingCount = followingCount
162 actorInstance.inboxUrl = attributes.inbox
163 actorInstance.outboxUrl = attributes.outbox
164 actorInstance.followersUrl = attributes.followers
165 actorInstance.followingUrl = attributes.following
166
167 if (attributes.published) actorInstance.remoteCreatedAt = new Date(attributes.published)
168
169 if (attributes.endpoints?.sharedInbox) {
170 actorInstance.sharedInboxUrl = attributes.endpoints.sharedInbox
171 }
172}
173
174type ImageInfo = {
175 name: string
176 fileUrl: string
177 height: number
178 width: number
179 onDisk?: boolean
180}
181async function updateActorImageInstance (actor: MActorImages, type: ActorImageType, imageInfo: ImageInfo | null, t: Transaction) {
182 const oldImageModel = type === ActorImageType.AVATAR
183 ? actor.Avatar
184 : actor.Banner
185
186 if (oldImageModel) {
187 // Don't update the avatar if the file URL did not change
188 if (imageInfo?.fileUrl && oldImageModel.fileUrl === imageInfo.fileUrl) return actor
189
190 try {
191 await oldImageModel.destroy({ transaction: t })
192
193 setActorImage(actor, type, null)
194 } catch (err) {
195 logger.error('Cannot remove old actor image of actor %s.', actor.url, { err })
196 }
197 }
198
199 if (imageInfo) {
200 const imageModel = await ActorImageModel.create({
201 filename: imageInfo.name,
202 onDisk: imageInfo.onDisk ?? false,
203 fileUrl: imageInfo.fileUrl,
204 height: imageInfo.height,
205 width: imageInfo.width,
206 type
207 }, { transaction: t })
208
209 setActorImage(actor, type, imageModel)
210 }
211
212 return actor
213}
214
215async function deleteActorImageInstance (actor: MActorImages, type: ActorImageType, t: Transaction) {
216 try {
217 if (type === ActorImageType.AVATAR) {
218 await actor.Avatar.destroy({ transaction: t })
219
220 actor.avatarId = null
221 actor.Avatar = null
222 } else {
223 await actor.Banner.destroy({ transaction: t })
224
225 actor.bannerId = null
226 actor.Banner = null
227 }
228 } catch (err) {
229 logger.error('Cannot remove old image of actor %s.', actor.url, { err })
230 }
231
232 return actor
233}
234
235async function fetchActorTotalItems (url: string) {
236 try {
237 const { body } = await doJSONRequest<ActivityPubOrderedCollection<unknown>>(url, { activityPub: true })
238
239 return body.totalItems || 0
240 } catch (err) {
241 logger.warn('Cannot fetch remote actor count %s.', url, { err })
242 return 0
243 }
244}
245
246function getImageInfoIfExists (actorJSON: ActivityPubActor, type: ActorImageType) {
247 const mimetypes = MIMETYPES.IMAGE
248 const icon = type === ActorImageType.AVATAR
249 ? actorJSON.icon
250 : actorJSON.image
251
252 if (!icon || icon.type !== 'Image' || !isActivityPubUrlValid(icon.url)) return undefined
253
254 let extension: string
255
256 if (icon.mediaType) {
257 extension = mimetypes.MIMETYPE_EXT[icon.mediaType]
258 } else {
259 const tmp = extname(icon.url)
260
261 if (mimetypes.EXT_MIMETYPE[tmp] !== undefined) extension = tmp
262 }
263
264 if (!extension) return undefined
265
266 return {
267 name: uuidv4() + extension,
268 fileUrl: icon.url,
269 height: icon.height,
270 width: icon.width,
271 type
272 }
273}
274
275async function addFetchOutboxJob (actor: Pick<ActorModel, 'id' | 'outboxUrl'>) {
276 // Don't fetch ourselves
277 const serverActor = await getServerActor()
278 if (serverActor.id === actor.id) {
279 logger.error('Cannot fetch our own outbox!')
280 return undefined
281 }
282
283 const payload = {
284 uri: actor.outboxUrl,
285 type: 'activity' as 'activity'
286 }
287
288 return JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload })
289}
290
291async function refreshActorIfNeeded <T extends MActorFull | MActorAccountChannelId> (
292 actorArg: T,
293 fetchedType: ActorFetchByUrlType
294): Promise<{ actor: T | MActorFull, refreshed: boolean }> {
295 if (!actorArg.isOutdated()) return { actor: actorArg, refreshed: false }
296
297 // We need more attributes
298 const actor = fetchedType === 'all'
299 ? actorArg as MActorFull
300 : await ActorModel.loadByUrlAndPopulateAccountAndChannel(actorArg.url)
301
302 try {
303 let actorUrl: string
304 try {
305 actorUrl = await getUrlFromWebfinger(actor.preferredUsername + '@' + actor.getHost())
306 } catch (err) {
307 logger.warn('Cannot get actor URL from webfinger, keeping the old one.', err)
308 actorUrl = actor.url
309 }
310
311 const { result } = await fetchRemoteActor(actorUrl)
312
313 if (result === undefined) {
314 logger.warn('Cannot fetch remote actor in refresh actor.')
315 return { actor, refreshed: false }
316 }
317
318 return sequelizeTypescript.transaction(async t => {
319 updateInstanceWithAnother(actor, result.actor)
320
321 await updateActorImageInstance(actor, ActorImageType.AVATAR, result.avatar, t)
322 await updateActorImageInstance(actor, ActorImageType.BANNER, result.banner, t)
323
324 // Force update
325 actor.setDataValue('updatedAt', new Date())
326 await actor.save({ transaction: t })
327
328 if (actor.Account) {
329 actor.Account.name = result.name
330 actor.Account.description = result.summary
331
332 await actor.Account.save({ transaction: t })
333 } else if (actor.VideoChannel) {
334 actor.VideoChannel.name = result.name
335 actor.VideoChannel.description = result.summary
336 actor.VideoChannel.support = result.support
337
338 await actor.VideoChannel.save({ transaction: t })
339 }
340
341 return { refreshed: true, actor }
342 })
343 } catch (err) {
344 if ((err as PeerTubeRequestError).statusCode === HttpStatusCode.NOT_FOUND_404) {
345 logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url)
346 actor.Account
347 ? await actor.Account.destroy()
348 : await actor.VideoChannel.destroy()
349
350 return { actor: undefined, refreshed: false }
351 }
352
353 logger.warn('Cannot refresh actor %s.', actor.url, { err })
354 return { actor, refreshed: false }
355 }
356}
357
358export {
359 getOrCreateActorAndServerAndModel,
360 buildActorInstance,
361 generateAndSaveActorKeys,
362 fetchActorTotalItems,
363 getImageInfoIfExists,
364 updateActorInstance,
365 deleteActorImageInstance,
366 refreshActorIfNeeded,
367 updateActorImageInstance,
368 addFetchOutboxJob
369}
370
371// ---------------------------------------------------------------------------
372
373function setActorImage (actorModel: MActorImages, type: ActorImageType, imageModel: MActorImage) {
374 const id = imageModel
375 ? imageModel.id
376 : null
377
378 if (type === ActorImageType.AVATAR) {
379 actorModel.avatarId = id
380 actorModel.Avatar = imageModel
381 } else {
382 actorModel.bannerId = id
383 actorModel.Banner = imageModel
384 }
385
386 return actorModel
387}
388
389function saveActorAndServerAndModelIfNotExist (
390 result: FetchRemoteActorResult,
391 ownerActor?: MActorFullActor,
392 t?: Transaction
393): Bluebird<MActorFullActor> | Promise<MActorFullActor> {
394 const actor = result.actor
395
396 if (t !== undefined) return save(t)
397
398 return sequelizeTypescript.transaction(t => save(t))
399
400 async function save (t: Transaction) {
401 const actorHost = new URL(actor.url).host
402
403 const serverOptions = {
404 where: {
405 host: actorHost
406 },
407 defaults: {
408 host: actorHost
409 },
410 transaction: t
411 }
412 const [ server ] = await ServerModel.findOrCreate(serverOptions)
413
414 // Save our new account in database
415 actor.serverId = server.id
416
417 // Avatar?
418 if (result.avatar) {
419 const avatar = await ActorImageModel.create({
420 filename: result.avatar.name,
421 fileUrl: result.avatar.fileUrl,
422 width: result.avatar.width,
423 height: result.avatar.height,
424 onDisk: false,
425 type: ActorImageType.AVATAR
426 }, { transaction: t })
427
428 actor.avatarId = avatar.id
429 }
430
431 // Banner?
432 if (result.banner) {
433 const banner = await ActorImageModel.create({
434 filename: result.banner.name,
435 fileUrl: result.banner.fileUrl,
436 width: result.banner.width,
437 height: result.banner.height,
438 onDisk: false,
439 type: ActorImageType.BANNER
440 }, { transaction: t })
441
442 actor.bannerId = banner.id
443 }
444
445 // Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists
446 // (which could be false in a retried query)
447 const [ actorCreated, created ] = await ActorModel.findOrCreate<MActorFullActor>({
448 defaults: actor.toJSON(),
449 where: {
450 [Op.or]: [
451 {
452 url: actor.url
453 },
454 {
455 serverId: actor.serverId,
456 preferredUsername: actor.preferredUsername
457 }
458 ]
459 },
460 transaction: t
461 })
462
463 // Try to fix non HTTPS accounts of remote instances that fixed their URL afterwards
464 if (created !== true && actorCreated.url !== actor.url) {
465 // Only fix http://example.com/account/djidane to https://example.com/account/djidane
466 if (actorCreated.url.replace(/^http:\/\//, '') !== actor.url.replace(/^https:\/\//, '')) {
467 throw new Error(`Actor from DB with URL ${actorCreated.url} does not correspond to actor ${actor.url}`)
468 }
469
470 actorCreated.url = actor.url
471 await actorCreated.save({ transaction: t })
472 }
473
474 if (actorCreated.type === 'Person' || actorCreated.type === 'Application') {
475 actorCreated.Account = await saveAccount(actorCreated, result, t) as MAccountDefault
476 actorCreated.Account.Actor = actorCreated
477 } else if (actorCreated.type === 'Group') { // Video channel
478 const channel = await saveVideoChannel(actorCreated, result, ownerActor, t)
479 actorCreated.VideoChannel = Object.assign(channel, { Actor: actorCreated, Account: ownerActor.Account })
480 }
481
482 actorCreated.Server = server
483
484 return actorCreated
485 }
486}
487
488type ImageResult = {
489 name: string
490 fileUrl: string
491 height: number
492 width: number
493}
494
495type FetchRemoteActorResult = {
496 actor: MActor
497 name: string
498 summary: string
499 support?: string
500 playlists?: string
501 avatar?: ImageResult
502 banner?: ImageResult
503 attributedTo: ActivityPubAttributedTo[]
504}
505async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: number, result: FetchRemoteActorResult }> {
506 logger.info('Fetching remote actor %s.', actorUrl)
507
508 const requestResult = await doJSONRequest<ActivityPubActor>(actorUrl, { activityPub: true })
509 const actorJSON = requestResult.body
510
511 if (sanitizeAndCheckActorObject(actorJSON) === false) {
512 logger.debug('Remote actor JSON is not valid.', { actorJSON })
513 return { result: undefined, statusCode: requestResult.statusCode }
514 }
515
516 if (checkUrlsSameHost(actorJSON.id, actorUrl) !== true) {
517 logger.warn('Actor url %s has not the same host than its AP id %s', actorUrl, actorJSON.id)
518 return { result: undefined, statusCode: requestResult.statusCode }
519 }
520
521 const followersCount = await fetchActorTotalItems(actorJSON.followers)
522 const followingCount = await fetchActorTotalItems(actorJSON.following)
523
524 const actor = new ActorModel({
525 type: actorJSON.type,
526 preferredUsername: actorJSON.preferredUsername,
527 url: actorJSON.id,
528 publicKey: actorJSON.publicKey.publicKeyPem,
529 privateKey: null,
530 followersCount: followersCount,
531 followingCount: followingCount,
532 inboxUrl: actorJSON.inbox,
533 outboxUrl: actorJSON.outbox,
534 followersUrl: actorJSON.followers,
535 followingUrl: actorJSON.following,
536
537 sharedInboxUrl: actorJSON.endpoints?.sharedInbox
538 ? actorJSON.endpoints.sharedInbox
539 : null
540 })
541
542 const avatarInfo = getImageInfoIfExists(actorJSON, ActorImageType.AVATAR)
543 const bannerInfo = getImageInfoIfExists(actorJSON, ActorImageType.BANNER)
544
545 const name = actorJSON.name || actorJSON.preferredUsername
546 return {
547 statusCode: requestResult.statusCode,
548 result: {
549 actor,
550 name,
551 avatar: avatarInfo,
552 banner: bannerInfo,
553 summary: actorJSON.summary,
554 support: actorJSON.support,
555 playlists: actorJSON.playlists,
556 attributedTo: actorJSON.attributedTo
557 }
558 }
559}
560
561async function saveAccount (actor: MActorId, result: FetchRemoteActorResult, t: Transaction) {
562 const [ accountCreated ] = await AccountModel.findOrCreate({
563 defaults: {
564 name: result.name,
565 description: result.summary,
566 actorId: actor.id
567 },
568 where: {
569 actorId: actor.id
570 },
571 transaction: t
572 })
573
574 return accountCreated as MAccount
575}
576
577async function saveVideoChannel (actor: MActorId, result: FetchRemoteActorResult, ownerActor: MActorAccountId, t: Transaction) {
578 const [ videoChannelCreated ] = await VideoChannelModel.findOrCreate({
579 defaults: {
580 name: result.name,
581 description: result.summary,
582 support: result.support,
583 actorId: actor.id,
584 accountId: ownerActor.Account.id
585 },
586 where: {
587 actorId: actor.id
588 },
589 transaction: t
590 })
591
592 return videoChannelCreated as MChannel
593}
diff --git a/server/lib/activitypub/actors/get.ts b/server/lib/activitypub/actors/get.ts
new file mode 100644
index 000000000..0d5bea789
--- /dev/null
+++ b/server/lib/activitypub/actors/get.ts
@@ -0,0 +1,119 @@
1
2import { checkUrlsSameHost, getAPId } from '@server/helpers/activitypub'
3import { ActorFetchByUrlType, fetchActorByUrl } from '@server/helpers/actor'
4import { retryTransactionWrapper } from '@server/helpers/database-utils'
5import { logger } from '@server/helpers/logger'
6import { JobQueue } from '@server/lib/job-queue'
7import { MActor, MActorAccountChannelId, MActorAccountChannelIdActor, MActorAccountId, MActorFullActor } from '@server/types/models'
8import { ActivityPubActor } from '@shared/models'
9import { refreshActorIfNeeded } from './refresh'
10import { APActorCreator, fetchRemoteActor } from './shared'
11
12function getOrCreateAPActor (
13 activityActor: string | ActivityPubActor,
14 fetchType: 'all',
15 recurseIfNeeded?: boolean,
16 updateCollections?: boolean
17): Promise<MActorFullActor>
18
19function getOrCreateAPActor (
20 activityActor: string | ActivityPubActor,
21 fetchType?: 'association-ids',
22 recurseIfNeeded?: boolean,
23 updateCollections?: boolean
24): Promise<MActorAccountChannelId>
25
26async function getOrCreateAPActor (
27 activityActor: string | ActivityPubActor,
28 fetchType: ActorFetchByUrlType = 'association-ids',
29 recurseIfNeeded = true,
30 updateCollections = false
31): Promise<MActorFullActor | MActorAccountChannelId> {
32 const actorUrl = getAPId(activityActor)
33 let actor = await loadActorFromDB(actorUrl, fetchType)
34
35 let created = false
36 let accountPlaylistsUrl: string
37
38 // We don't have this actor in our database, fetch it on remote
39 if (!actor) {
40 const { actorObject } = await fetchRemoteActor(actorUrl)
41 if (actorObject === undefined) throw new Error('Cannot fetch remote actor ' + actorUrl)
42
43 // Create the attributed to actor
44 // In PeerTube a video channel is owned by an account
45 let ownerActor: MActorFullActor
46 if (recurseIfNeeded === true && actorObject.type === 'Group') {
47 ownerActor = await getOrCreateAPOwner(actorObject, actorUrl)
48 }
49
50 const creator = new APActorCreator(actorObject, ownerActor)
51 actor = await retryTransactionWrapper(creator.create.bind(creator))
52 created = true
53 accountPlaylistsUrl = actorObject.playlists
54 }
55
56 if (actor.Account) (actor as MActorAccountChannelIdActor).Account.Actor = actor
57 if (actor.VideoChannel) (actor as MActorAccountChannelIdActor).VideoChannel.Actor = actor
58
59 const { actor: actorRefreshed, refreshed } = await retryTransactionWrapper(refreshActorIfNeeded, actor, fetchType)
60 if (!actorRefreshed) throw new Error('Actor ' + actor.url + ' does not exist anymore.')
61
62 await scheduleOutboxFetchIfNeeded(actor, created, refreshed, updateCollections)
63 await schedulePlaylistFetchIfNeeded(actor, created, accountPlaylistsUrl)
64
65 return actorRefreshed
66}
67
68// ---------------------------------------------------------------------------
69
70export {
71 getOrCreateAPActor
72}
73
74// ---------------------------------------------------------------------------
75
76async function loadActorFromDB (actorUrl: string, fetchType: ActorFetchByUrlType) {
77 let actor = await fetchActorByUrl(actorUrl, fetchType)
78
79 // Orphan actor (not associated to an account of channel) so recreate it
80 if (actor && (!actor.Account && !actor.VideoChannel)) {
81 await actor.destroy()
82 actor = null
83 }
84
85 return actor
86}
87
88function getOrCreateAPOwner (actorObject: ActivityPubActor, actorUrl: string) {
89 const accountAttributedTo = actorObject.attributedTo.find(a => a.type === 'Person')
90 if (!accountAttributedTo) throw new Error('Cannot find account attributed to video channel ' + actorUrl)
91
92 if (checkUrlsSameHost(accountAttributedTo.id, actorUrl) !== true) {
93 throw new Error(`Account attributed to ${accountAttributedTo.id} does not have the same host than actor url ${actorUrl}`)
94 }
95
96 try {
97 // Don't recurse another time
98 const recurseIfNeeded = false
99 return getOrCreateAPActor(accountAttributedTo.id, 'all', recurseIfNeeded)
100 } catch (err) {
101 logger.error('Cannot get or create account attributed to video channel ' + actorUrl)
102 throw new Error(err)
103 }
104}
105
106async function scheduleOutboxFetchIfNeeded (actor: MActor, created: boolean, refreshed: boolean, updateCollections: boolean) {
107 if ((created === true || refreshed === true) && updateCollections === true) {
108 const payload = { uri: actor.outboxUrl, type: 'activity' as 'activity' }
109 await JobQueue.Instance.createJobWithPromise({ type: 'activitypub-http-fetcher', payload })
110 }
111}
112
113async function schedulePlaylistFetchIfNeeded (actor: MActorAccountId, created: boolean, accountPlaylistsUrl: string) {
114 // We created a new account: fetch the playlists
115 if (created === true && actor.Account && accountPlaylistsUrl) {
116 const payload = { uri: accountPlaylistsUrl, accountId: actor.Account.id, type: 'account-playlists' as 'account-playlists' }
117 await JobQueue.Instance.createJobWithPromise({ type: 'activitypub-http-fetcher', payload })
118 }
119}
diff --git a/server/lib/activitypub/actors/image.ts b/server/lib/activitypub/actors/image.ts
new file mode 100644
index 000000000..443ad0a63
--- /dev/null
+++ b/server/lib/activitypub/actors/image.ts
@@ -0,0 +1,94 @@
1import { Transaction } from 'sequelize/types'
2import { logger } from '@server/helpers/logger'
3import { ActorImageModel } from '@server/models/actor/actor-image'
4import { MActorImage, MActorImages } from '@server/types/models'
5import { ActorImageType } from '@shared/models'
6
7type ImageInfo = {
8 name: string
9 fileUrl: string
10 height: number
11 width: number
12 onDisk?: boolean
13}
14
15async function updateActorImageInstance (actor: MActorImages, type: ActorImageType, imageInfo: ImageInfo | null, t: Transaction) {
16 const oldImageModel = type === ActorImageType.AVATAR
17 ? actor.Avatar
18 : actor.Banner
19
20 if (oldImageModel) {
21 // Don't update the avatar if the file URL did not change
22 if (imageInfo?.fileUrl && oldImageModel.fileUrl === imageInfo.fileUrl) return actor
23
24 try {
25 await oldImageModel.destroy({ transaction: t })
26
27 setActorImage(actor, type, null)
28 } catch (err) {
29 logger.error('Cannot remove old actor image of actor %s.', actor.url, { err })
30 }
31 }
32
33 if (imageInfo) {
34 const imageModel = await ActorImageModel.create({
35 filename: imageInfo.name,
36 onDisk: imageInfo.onDisk ?? false,
37 fileUrl: imageInfo.fileUrl,
38 height: imageInfo.height,
39 width: imageInfo.width,
40 type
41 }, { transaction: t })
42
43 setActorImage(actor, type, imageModel)
44 }
45
46 return actor
47}
48
49async function deleteActorImageInstance (actor: MActorImages, type: ActorImageType, t: Transaction) {
50 try {
51 if (type === ActorImageType.AVATAR) {
52 await actor.Avatar.destroy({ transaction: t })
53
54 actor.avatarId = null
55 actor.Avatar = null
56 } else {
57 await actor.Banner.destroy({ transaction: t })
58
59 actor.bannerId = null
60 actor.Banner = null
61 }
62 } catch (err) {
63 logger.error('Cannot remove old image of actor %s.', actor.url, { err })
64 }
65
66 return actor
67}
68
69// ---------------------------------------------------------------------------
70
71export {
72 ImageInfo,
73
74 updateActorImageInstance,
75 deleteActorImageInstance
76}
77
78// ---------------------------------------------------------------------------
79
80function setActorImage (actorModel: MActorImages, type: ActorImageType, imageModel: MActorImage) {
81 const id = imageModel
82 ? imageModel.id
83 : null
84
85 if (type === ActorImageType.AVATAR) {
86 actorModel.avatarId = id
87 actorModel.Avatar = imageModel
88 } else {
89 actorModel.bannerId = id
90 actorModel.Banner = imageModel
91 }
92
93 return actorModel
94}
diff --git a/server/lib/activitypub/actors/index.ts b/server/lib/activitypub/actors/index.ts
new file mode 100644
index 000000000..a54da6798
--- /dev/null
+++ b/server/lib/activitypub/actors/index.ts
@@ -0,0 +1,5 @@
1export * from './get'
2export * from './image'
3export * from './keys'
4export * from './refresh'
5export * from './updater'
diff --git a/server/lib/activitypub/actors/keys.ts b/server/lib/activitypub/actors/keys.ts
new file mode 100644
index 000000000..c3d18abd8
--- /dev/null
+++ b/server/lib/activitypub/actors/keys.ts
@@ -0,0 +1,16 @@
1import { createPrivateAndPublicKeys } from '@server/helpers/peertube-crypto'
2import { MActor } from '@server/types/models'
3
4// Set account keys, this could be long so process after the account creation and do not block the client
5async function generateAndSaveActorKeys <T extends MActor> (actor: T) {
6 const { publicKey, privateKey } = await createPrivateAndPublicKeys()
7
8 actor.publicKey = publicKey
9 actor.privateKey = privateKey
10
11 return actor.save()
12}
13
14export {
15 generateAndSaveActorKeys
16}
diff --git a/server/lib/activitypub/actors/refresh.ts b/server/lib/activitypub/actors/refresh.ts
new file mode 100644
index 000000000..ff3b249d0
--- /dev/null
+++ b/server/lib/activitypub/actors/refresh.ts
@@ -0,0 +1,63 @@
1import { ActorFetchByUrlType } from '@server/helpers/actor'
2import { logger } from '@server/helpers/logger'
3import { PeerTubeRequestError } from '@server/helpers/requests'
4import { getUrlFromWebfinger } from '@server/helpers/webfinger'
5import { ActorModel } from '@server/models/actor/actor'
6import { MActorAccountChannelId, MActorFull } from '@server/types/models'
7import { HttpStatusCode } from '@shared/core-utils'
8import { fetchRemoteActor } from './shared'
9import { APActorUpdater } from './updater'
10
11async function refreshActorIfNeeded <T extends MActorFull | MActorAccountChannelId> (
12 actorArg: T,
13 fetchedType: ActorFetchByUrlType
14): Promise<{ actor: T | MActorFull, refreshed: boolean }> {
15 if (!actorArg.isOutdated()) return { actor: actorArg, refreshed: false }
16
17 // We need more attributes
18 const actor = fetchedType === 'all'
19 ? actorArg as MActorFull
20 : await ActorModel.loadByUrlAndPopulateAccountAndChannel(actorArg.url)
21
22 try {
23 const actorUrl = await getActorUrl(actor)
24 const { actorObject } = await fetchRemoteActor(actorUrl)
25
26 if (actorObject === undefined) {
27 logger.warn('Cannot fetch remote actor in refresh actor.')
28 return { actor, refreshed: false }
29 }
30
31 const updater = new APActorUpdater(actorObject, actor)
32 await updater.update()
33
34 return { refreshed: true, actor }
35 } catch (err) {
36 if ((err as PeerTubeRequestError).statusCode === HttpStatusCode.NOT_FOUND_404) {
37 logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url)
38
39 actor.Account
40 ? await actor.Account.destroy()
41 : await actor.VideoChannel.destroy()
42
43 return { actor: undefined, refreshed: false }
44 }
45
46 logger.warn('Cannot refresh actor %s.', actor.url, { err })
47 return { actor, refreshed: false }
48 }
49}
50
51export {
52 refreshActorIfNeeded
53}
54
55// ---------------------------------------------------------------------------
56
57function getActorUrl (actor: MActorFull) {
58 return getUrlFromWebfinger(actor.preferredUsername + '@' + actor.getHost())
59 .catch(err => {
60 logger.warn('Cannot get actor URL from webfinger, keeping the old one.', err)
61 return actor.url
62 })
63}
diff --git a/server/lib/activitypub/actors/shared/creator.ts b/server/lib/activitypub/actors/shared/creator.ts
new file mode 100644
index 000000000..999aed97d
--- /dev/null
+++ b/server/lib/activitypub/actors/shared/creator.ts
@@ -0,0 +1,149 @@
1import { Op, Transaction } from 'sequelize'
2import { sequelizeTypescript } from '@server/initializers/database'
3import { AccountModel } from '@server/models/account/account'
4import { ActorModel } from '@server/models/actor/actor'
5import { ServerModel } from '@server/models/server/server'
6import { VideoChannelModel } from '@server/models/video/video-channel'
7import { MAccount, MAccountDefault, MActor, MActorFullActor, MActorId, MActorImages, MChannel, MServer } from '@server/types/models'
8import { ActivityPubActor, ActorImageType } from '@shared/models'
9import { updateActorImageInstance } from '../image'
10import { getActorAttributesFromObject, getActorDisplayNameFromObject, getImageInfoFromObject } from './object-to-model-attributes'
11import { fetchActorFollowsCount } from './url-to-object'
12
13export class APActorCreator {
14
15 constructor (
16 private readonly actorObject: ActivityPubActor,
17 private readonly ownerActor?: MActorFullActor
18 ) {
19
20 }
21
22 async create (): Promise<MActorFullActor> {
23 const { followersCount, followingCount } = await fetchActorFollowsCount(this.actorObject)
24
25 const actorInstance = new ActorModel(getActorAttributesFromObject(this.actorObject, followersCount, followingCount))
26
27 return sequelizeTypescript.transaction(async t => {
28 const server = await this.setServer(actorInstance, t)
29
30 await this.setImageIfNeeded(actorInstance, ActorImageType.AVATAR, t)
31 await this.setImageIfNeeded(actorInstance, ActorImageType.BANNER, t)
32
33 const { actorCreated, created } = await this.saveActor(actorInstance, t)
34
35 await this.tryToFixActorUrlIfNeeded(actorCreated, actorInstance, created, t)
36
37 if (actorCreated.type === 'Person' || actorCreated.type === 'Application') { // Account or PeerTube instance
38 actorCreated.Account = await this.saveAccount(actorCreated, t) as MAccountDefault
39 actorCreated.Account.Actor = actorCreated
40 }
41
42 if (actorCreated.type === 'Group') { // Video channel
43 const channel = await this.saveVideoChannel(actorCreated, t)
44 actorCreated.VideoChannel = Object.assign(channel, { Actor: actorCreated, Account: this.ownerActor.Account })
45 }
46
47 actorCreated.Server = server
48
49 return actorCreated
50 })
51 }
52
53 private async setServer (actor: MActor, t: Transaction) {
54 const actorHost = new URL(actor.url).host
55
56 const serverOptions = {
57 where: {
58 host: actorHost
59 },
60 defaults: {
61 host: actorHost
62 },
63 transaction: t
64 }
65 const [ server ] = await ServerModel.findOrCreate(serverOptions)
66
67 // Save our new account in database
68 actor.serverId = server.id
69
70 return server as MServer
71 }
72
73 private async setImageIfNeeded (actor: MActor, type: ActorImageType, t: Transaction) {
74 const imageInfo = getImageInfoFromObject(this.actorObject, type)
75 if (!imageInfo) return
76
77 return updateActorImageInstance(actor as MActorImages, type, imageInfo, t)
78 }
79
80 private async saveActor (actor: MActor, t: Transaction) {
81 // Force the actor creation using findOrCreate() instead of save()
82 // Sometimes Sequelize skips the save() when it thinks the instance already exists
83 // (which could be false in a retried query)
84 const [ actorCreated, created ] = await ActorModel.findOrCreate<MActorFullActor>({
85 defaults: actor.toJSON(),
86 where: {
87 [Op.or]: [
88 {
89 url: actor.url
90 },
91 {
92 serverId: actor.serverId,
93 preferredUsername: actor.preferredUsername
94 }
95 ]
96 },
97 transaction: t
98 })
99
100 return { actorCreated, created }
101 }
102
103 private async tryToFixActorUrlIfNeeded (actorCreated: MActor, newActor: MActor, created: boolean, t: Transaction) {
104 // Try to fix non HTTPS accounts of remote instances that fixed their URL afterwards
105 if (created !== true && actorCreated.url !== newActor.url) {
106 // Only fix http://example.com/account/djidane to https://example.com/account/djidane
107 if (actorCreated.url.replace(/^http:\/\//, '') !== newActor.url.replace(/^https:\/\//, '')) {
108 throw new Error(`Actor from DB with URL ${actorCreated.url} does not correspond to actor ${newActor.url}`)
109 }
110
111 actorCreated.url = newActor.url
112 await actorCreated.save({ transaction: t })
113 }
114 }
115
116 private async saveAccount (actor: MActorId, t: Transaction) {
117 const [ accountCreated ] = await AccountModel.findOrCreate({
118 defaults: {
119 name: getActorDisplayNameFromObject(this.actorObject),
120 description: this.actorObject.summary,
121 actorId: actor.id
122 },
123 where: {
124 actorId: actor.id
125 },
126 transaction: t
127 })
128
129 return accountCreated as MAccount
130 }
131
132 private async saveVideoChannel (actor: MActorId, t: Transaction) {
133 const [ videoChannelCreated ] = await VideoChannelModel.findOrCreate({
134 defaults: {
135 name: getActorDisplayNameFromObject(this.actorObject),
136 description: this.actorObject.summary,
137 support: this.actorObject.support,
138 actorId: actor.id,
139 accountId: this.ownerActor.Account.id
140 },
141 where: {
142 actorId: actor.id
143 },
144 transaction: t
145 })
146
147 return videoChannelCreated as MChannel
148 }
149}
diff --git a/server/lib/activitypub/actors/shared/index.ts b/server/lib/activitypub/actors/shared/index.ts
new file mode 100644
index 000000000..a2ff468cf
--- /dev/null
+++ b/server/lib/activitypub/actors/shared/index.ts
@@ -0,0 +1,3 @@
1export * from './creator'
2export * from './url-to-object'
3export * from './object-to-model-attributes'
diff --git a/server/lib/activitypub/actors/shared/object-to-model-attributes.ts b/server/lib/activitypub/actors/shared/object-to-model-attributes.ts
new file mode 100644
index 000000000..66b22c952
--- /dev/null
+++ b/server/lib/activitypub/actors/shared/object-to-model-attributes.ts
@@ -0,0 +1,70 @@
1import { extname } from 'path'
2import { v4 as uuidv4 } from 'uuid'
3import { isActivityPubUrlValid } from '@server/helpers/custom-validators/activitypub/misc'
4import { MIMETYPES } from '@server/initializers/constants'
5import { ActorModel } from '@server/models/actor/actor'
6import { FilteredModelAttributes } from '@server/types'
7import { ActivityPubActor, ActorImageType } from '@shared/models'
8
9function getActorAttributesFromObject (
10 actorObject: ActivityPubActor,
11 followersCount: number,
12 followingCount: number
13): FilteredModelAttributes<ActorModel> {
14 return {
15 type: actorObject.type,
16 preferredUsername: actorObject.preferredUsername,
17 url: actorObject.id,
18 publicKey: actorObject.publicKey.publicKeyPem,
19 privateKey: null,
20 followersCount,
21 followingCount,
22 inboxUrl: actorObject.inbox,
23 outboxUrl: actorObject.outbox,
24 followersUrl: actorObject.followers,
25 followingUrl: actorObject.following,
26
27 sharedInboxUrl: actorObject.endpoints?.sharedInbox
28 ? actorObject.endpoints.sharedInbox
29 : null
30 }
31}
32
33function getImageInfoFromObject (actorObject: ActivityPubActor, type: ActorImageType) {
34 const mimetypes = MIMETYPES.IMAGE
35 const icon = type === ActorImageType.AVATAR
36 ? actorObject.icon
37 : actorObject.image
38
39 if (!icon || icon.type !== 'Image' || !isActivityPubUrlValid(icon.url)) return undefined
40
41 let extension: string
42
43 if (icon.mediaType) {
44 extension = mimetypes.MIMETYPE_EXT[icon.mediaType]
45 } else {
46 const tmp = extname(icon.url)
47
48 if (mimetypes.EXT_MIMETYPE[tmp] !== undefined) extension = tmp
49 }
50
51 if (!extension) return undefined
52
53 return {
54 name: uuidv4() + extension,
55 fileUrl: icon.url,
56 height: icon.height,
57 width: icon.width,
58 type
59 }
60}
61
62function getActorDisplayNameFromObject (actorObject: ActivityPubActor) {
63 return actorObject.name || actorObject.preferredUsername
64}
65
66export {
67 getActorAttributesFromObject,
68 getImageInfoFromObject,
69 getActorDisplayNameFromObject
70}
diff --git a/server/lib/activitypub/actors/shared/url-to-object.ts b/server/lib/activitypub/actors/shared/url-to-object.ts
new file mode 100644
index 000000000..f4f16b044
--- /dev/null
+++ b/server/lib/activitypub/actors/shared/url-to-object.ts
@@ -0,0 +1,54 @@
1
2import { checkUrlsSameHost } from '@server/helpers/activitypub'
3import { sanitizeAndCheckActorObject } from '@server/helpers/custom-validators/activitypub/actor'
4import { logger } from '@server/helpers/logger'
5import { doJSONRequest } from '@server/helpers/requests'
6import { ActivityPubActor, ActivityPubOrderedCollection } from '@shared/models'
7
8async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode: number, actorObject: ActivityPubActor }> {
9 logger.info('Fetching remote actor %s.', actorUrl)
10
11 const { body, statusCode } = await doJSONRequest<ActivityPubActor>(actorUrl, { activityPub: true })
12
13 if (sanitizeAndCheckActorObject(body) === false) {
14 logger.debug('Remote actor JSON is not valid.', { actorJSON: body })
15 return { actorObject: undefined, statusCode: statusCode }
16 }
17
18 if (checkUrlsSameHost(body.id, actorUrl) !== true) {
19 logger.warn('Actor url %s has not the same host than its AP id %s', actorUrl, body.id)
20 return { actorObject: undefined, statusCode: statusCode }
21 }
22
23 return {
24 statusCode,
25
26 actorObject: body
27 }
28}
29
30async function fetchActorFollowsCount (actorObject: ActivityPubActor) {
31 const followersCount = await fetchActorTotalItems(actorObject.followers)
32 const followingCount = await fetchActorTotalItems(actorObject.following)
33
34 return { followersCount, followingCount }
35}
36
37// ---------------------------------------------------------------------------
38export {
39 fetchActorFollowsCount,
40 fetchRemoteActor
41}
42
43// ---------------------------------------------------------------------------
44
45async function fetchActorTotalItems (url: string) {
46 try {
47 const { body } = await doJSONRequest<ActivityPubOrderedCollection<unknown>>(url, { activityPub: true })
48
49 return body.totalItems || 0
50 } catch (err) {
51 logger.warn('Cannot fetch remote actor count %s.', url, { err })
52 return 0
53 }
54}
diff --git a/server/lib/activitypub/actors/updater.ts b/server/lib/activitypub/actors/updater.ts
new file mode 100644
index 000000000..471688f11
--- /dev/null
+++ b/server/lib/activitypub/actors/updater.ts
@@ -0,0 +1,90 @@
1import { resetSequelizeInstance } from '@server/helpers/database-utils'
2import { logger } from '@server/helpers/logger'
3import { sequelizeTypescript } from '@server/initializers/database'
4import { VideoChannelModel } from '@server/models/video/video-channel'
5import { MAccount, MActor, MActorFull, MChannel } from '@server/types/models'
6import { ActivityPubActor, ActorImageType } from '@shared/models'
7import { updateActorImageInstance } from './image'
8import { fetchActorFollowsCount } from './shared'
9import { getImageInfoFromObject } from './shared/object-to-model-attributes'
10
11export class APActorUpdater {
12
13 private accountOrChannel: MAccount | MChannel
14
15 private readonly actorFieldsSave: object
16 private readonly accountOrChannelFieldsSave: object
17
18 constructor (
19 private readonly actorObject: ActivityPubActor,
20 private readonly actor: MActorFull
21 ) {
22 this.actorFieldsSave = this.actor.toJSON()
23
24 if (this.actorObject.type === 'Group') this.accountOrChannel = this.actor.VideoChannel
25 else this.accountOrChannel = this.actor.Account
26
27 this.accountOrChannelFieldsSave = this.accountOrChannel.toJSON()
28 }
29
30 async update () {
31 const avatarInfo = getImageInfoFromObject(this.actorObject, ActorImageType.AVATAR)
32 const bannerInfo = getImageInfoFromObject(this.actorObject, ActorImageType.BANNER)
33
34 try {
35 await sequelizeTypescript.transaction(async t => {
36 await this.updateActorInstance(this.actor, this.actorObject)
37
38 await updateActorImageInstance(this.actor, ActorImageType.AVATAR, avatarInfo, t)
39 await updateActorImageInstance(this.actor, ActorImageType.BANNER, bannerInfo, t)
40
41 await this.actor.save({ transaction: t })
42
43 this.accountOrChannel.name = this.actorObject.name || this.actorObject.preferredUsername
44 this.accountOrChannel.description = this.actorObject.summary
45
46 if (this.accountOrChannel instanceof VideoChannelModel) this.accountOrChannel.support = this.actorObject.support
47
48 await this.accountOrChannel.save({ transaction: t })
49 })
50
51 logger.info('Remote account %s updated', this.actorObject.url)
52 } catch (err) {
53 if (this.actor !== undefined && this.actorFieldsSave !== undefined) {
54 resetSequelizeInstance(this.actor, this.actorFieldsSave)
55 }
56
57 if (this.accountOrChannel !== undefined && this.accountOrChannelFieldsSave !== undefined) {
58 resetSequelizeInstance(this.accountOrChannel, this.accountOrChannelFieldsSave)
59 }
60
61 // This is just a debug because we will retry the insert
62 logger.debug('Cannot update the remote account.', { err })
63 throw err
64 }
65 }
66
67 private async updateActorInstance (actorInstance: MActor, actorObject: ActivityPubActor) {
68 const { followersCount, followingCount } = await fetchActorFollowsCount(actorObject)
69
70 actorInstance.type = actorObject.type
71 actorInstance.preferredUsername = actorObject.preferredUsername
72 actorInstance.url = actorObject.id
73 actorInstance.publicKey = actorObject.publicKey.publicKeyPem
74 actorInstance.followersCount = followersCount
75 actorInstance.followingCount = followingCount
76 actorInstance.inboxUrl = actorObject.inbox
77 actorInstance.outboxUrl = actorObject.outbox
78 actorInstance.followersUrl = actorObject.followers
79 actorInstance.followingUrl = actorObject.following
80
81 if (actorObject.published) actorInstance.remoteCreatedAt = new Date(actorObject.published)
82
83 if (actorObject.endpoints?.sharedInbox) {
84 actorInstance.sharedInboxUrl = actorObject.endpoints.sharedInbox
85 }
86
87 // Force actor update
88 actorInstance.changed('updatedAt', true)
89 }
90}
diff --git a/server/lib/activitypub/outbox.ts b/server/lib/activitypub/outbox.ts
new file mode 100644
index 000000000..ecdc33a77
--- /dev/null
+++ b/server/lib/activitypub/outbox.ts
@@ -0,0 +1,24 @@
1import { logger } from '@server/helpers/logger'
2import { ActorModel } from '@server/models/actor/actor'
3import { getServerActor } from '@server/models/application/application'
4import { JobQueue } from '../job-queue'
5
6async function addFetchOutboxJob (actor: Pick<ActorModel, 'id' | 'outboxUrl'>) {
7 // Don't fetch ourselves
8 const serverActor = await getServerActor()
9 if (serverActor.id === actor.id) {
10 logger.error('Cannot fetch our own outbox!')
11 return undefined
12 }
13
14 const payload = {
15 uri: actor.outboxUrl,
16 type: 'activity' as 'activity'
17 }
18
19 return JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload })
20}
21
22export {
23 addFetchOutboxJob
24}
diff --git a/server/lib/activitypub/playlists/create-update.ts b/server/lib/activitypub/playlists/create-update.ts
index 886b1f288..fcfcc41a2 100644
--- a/server/lib/activitypub/playlists/create-update.ts
+++ b/server/lib/activitypub/playlists/create-update.ts
@@ -9,7 +9,7 @@ import { FilteredModelAttributes } from '@server/types'
9import { MAccountDefault, MAccountId, MVideoPlaylist, MVideoPlaylistFull } from '@server/types/models' 9import { MAccountDefault, MAccountId, MVideoPlaylist, MVideoPlaylistFull } from '@server/types/models'
10import { AttributesOnly } from '@shared/core-utils' 10import { AttributesOnly } from '@shared/core-utils'
11import { PlaylistObject } from '@shared/models' 11import { PlaylistObject } from '@shared/models'
12import { getOrCreateActorAndServerAndModel } from '../actor' 12import { getOrCreateAPActor } from '../actors'
13import { crawlCollectionPage } from '../crawl' 13import { crawlCollectionPage } from '../crawl'
14import { getOrCreateAPVideo } from '../videos' 14import { getOrCreateAPVideo } from '../videos'
15import { 15import {
@@ -75,7 +75,7 @@ export {
75async function setVideoChannelIfNeeded (playlistObject: PlaylistObject, playlistAttributes: AttributesOnly<VideoPlaylistModel>) { 75async function setVideoChannelIfNeeded (playlistObject: PlaylistObject, playlistAttributes: AttributesOnly<VideoPlaylistModel>) {
76 if (!isArray(playlistObject.attributedTo) || playlistObject.attributedTo.length !== 1) return 76 if (!isArray(playlistObject.attributedTo) || playlistObject.attributedTo.length !== 1) return
77 77
78 const actor = await getOrCreateActorAndServerAndModel(playlistObject.attributedTo[0]) 78 const actor = await getOrCreateAPActor(playlistObject.attributedTo[0])
79 79
80 if (!actor.VideoChannel) { 80 if (!actor.VideoChannel) {
81 logger.warn('Playlist "attributedTo" %s is not a video channel.', playlistObject.id, { playlistObject, ...lTags(playlistObject.id) }) 81 logger.warn('Playlist "attributedTo" %s is not a video channel.', playlistObject.id, { playlistObject, ...lTags(playlistObject.id) })
diff --git a/server/lib/activitypub/process/process-accept.ts b/server/lib/activitypub/process/process-accept.ts
index 8ad470cf4..077b01eda 100644
--- a/server/lib/activitypub/process/process-accept.ts
+++ b/server/lib/activitypub/process/process-accept.ts
@@ -2,7 +2,7 @@ import { ActivityAccept } from '../../../../shared/models/activitypub'
2import { ActorFollowModel } from '../../../models/actor/actor-follow' 2import { ActorFollowModel } from '../../../models/actor/actor-follow'
3import { APProcessorOptions } from '../../../types/activitypub-processor.model' 3import { APProcessorOptions } from '../../../types/activitypub-processor.model'
4import { MActorDefault, MActorSignature } from '../../../types/models' 4import { MActorDefault, MActorSignature } from '../../../types/models'
5import { addFetchOutboxJob } from '../actor' 5import { addFetchOutboxJob } from '../outbox'
6 6
7async function processAcceptActivity (options: APProcessorOptions<ActivityAccept>) { 7async function processAcceptActivity (options: APProcessorOptions<ActivityAccept>) {
8 const { byActor: targetActor, inboxActor } = options 8 const { byActor: targetActor, inboxActor } = options
diff --git a/server/lib/activitypub/process/process-update.ts b/server/lib/activitypub/process/process-update.ts
index d2b63c901..aa80d5d09 100644
--- a/server/lib/activitypub/process/process-update.ts
+++ b/server/lib/activitypub/process/process-update.ts
@@ -1,19 +1,16 @@
1import { isRedundancyAccepted } from '@server/lib/redundancy' 1import { isRedundancyAccepted } from '@server/lib/redundancy'
2import { ActorImageType } from '@shared/models'
3import { ActivityUpdate, CacheFileObject, VideoObject } from '../../../../shared/models/activitypub' 2import { ActivityUpdate, CacheFileObject, VideoObject } from '../../../../shared/models/activitypub'
4import { ActivityPubActor } from '../../../../shared/models/activitypub/activitypub-actor' 3import { ActivityPubActor } from '../../../../shared/models/activitypub/activitypub-actor'
5import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object' 4import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object'
6import { isCacheFileObjectValid } from '../../../helpers/custom-validators/activitypub/cache-file' 5import { isCacheFileObjectValid } from '../../../helpers/custom-validators/activitypub/cache-file'
7import { sanitizeAndCheckVideoTorrentObject } from '../../../helpers/custom-validators/activitypub/videos' 6import { sanitizeAndCheckVideoTorrentObject } from '../../../helpers/custom-validators/activitypub/videos'
8import { resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers/database-utils' 7import { retryTransactionWrapper } from '../../../helpers/database-utils'
9import { logger } from '../../../helpers/logger' 8import { logger } from '../../../helpers/logger'
10import { sequelizeTypescript } from '../../../initializers/database' 9import { sequelizeTypescript } from '../../../initializers/database'
11import { AccountModel } from '../../../models/account/account'
12import { ActorModel } from '../../../models/actor/actor' 10import { ActorModel } from '../../../models/actor/actor'
13import { VideoChannelModel } from '../../../models/video/video-channel'
14import { APProcessorOptions } from '../../../types/activitypub-processor.model' 11import { APProcessorOptions } from '../../../types/activitypub-processor.model'
15import { MActorSignature } from '../../../types/models' 12import { MActorFull, MActorSignature } from '../../../types/models'
16import { getImageInfoIfExists, updateActorImageInstance, updateActorInstance } from '../actor' 13import { APActorUpdater } from '../actors/updater'
17import { createOrUpdateCacheFile } from '../cache-file' 14import { createOrUpdateCacheFile } from '../cache-file'
18import { createOrUpdateVideoPlaylist } from '../playlists' 15import { createOrUpdateVideoPlaylist } from '../playlists'
19import { forwardVideoRelatedActivity } from '../send/utils' 16import { forwardVideoRelatedActivity } from '../send/utils'
@@ -99,56 +96,13 @@ async function processUpdateCacheFile (byActor: MActorSignature, activity: Activ
99 } 96 }
100} 97}
101 98
102async function processUpdateActor (actor: ActorModel, activity: ActivityUpdate) { 99async function processUpdateActor (actor: MActorFull, activity: ActivityUpdate) {
103 const actorAttributesToUpdate = activity.object as ActivityPubActor 100 const actorObject = activity.object as ActivityPubActor
104 101
105 logger.debug('Updating remote account "%s".', actorAttributesToUpdate.url) 102 logger.debug('Updating remote account "%s".', actorObject.url)
106 let accountOrChannelInstance: AccountModel | VideoChannelModel
107 let actorFieldsSave: object
108 let accountOrChannelFieldsSave: object
109 103
110 // Fetch icon? 104 const updater = new APActorUpdater(actorObject, actor)
111 const avatarInfo = getImageInfoIfExists(actorAttributesToUpdate, ActorImageType.AVATAR) 105 return updater.update()
112 const bannerInfo = getImageInfoIfExists(actorAttributesToUpdate, ActorImageType.BANNER)
113
114 try {
115 await sequelizeTypescript.transaction(async t => {
116 actorFieldsSave = actor.toJSON()
117
118 if (actorAttributesToUpdate.type === 'Group') accountOrChannelInstance = actor.VideoChannel
119 else accountOrChannelInstance = actor.Account
120
121 accountOrChannelFieldsSave = accountOrChannelInstance.toJSON()
122
123 await updateActorInstance(actor, actorAttributesToUpdate)
124
125 await updateActorImageInstance(actor, ActorImageType.AVATAR, avatarInfo, t)
126 await updateActorImageInstance(actor, ActorImageType.BANNER, bannerInfo, t)
127
128 await actor.save({ transaction: t })
129
130 accountOrChannelInstance.name = actorAttributesToUpdate.name || actorAttributesToUpdate.preferredUsername
131 accountOrChannelInstance.description = actorAttributesToUpdate.summary
132
133 if (accountOrChannelInstance instanceof VideoChannelModel) accountOrChannelInstance.support = actorAttributesToUpdate.support
134
135 await accountOrChannelInstance.save({ transaction: t })
136 })
137
138 logger.info('Remote account %s updated', actorAttributesToUpdate.url)
139 } catch (err) {
140 if (actor !== undefined && actorFieldsSave !== undefined) {
141 resetSequelizeInstance(actor, actorFieldsSave)
142 }
143
144 if (accountOrChannelInstance !== undefined && accountOrChannelFieldsSave !== undefined) {
145 resetSequelizeInstance(accountOrChannelInstance, accountOrChannelFieldsSave)
146 }
147
148 // This is just a debug because we will retry the insert
149 logger.debug('Cannot update the remote account.', { err })
150 throw err
151 }
152} 106}
153 107
154async function processUpdatePlaylist (byActor: MActorSignature, activity: ActivityUpdate) { 108async function processUpdatePlaylist (byActor: MActorSignature, activity: ActivityUpdate) {
diff --git a/server/lib/activitypub/process/process.ts b/server/lib/activitypub/process/process.ts
index 5cef75665..02a23d098 100644
--- a/server/lib/activitypub/process/process.ts
+++ b/server/lib/activitypub/process/process.ts
@@ -1,22 +1,22 @@
1import { StatsManager } from '@server/lib/stat-manager'
1import { Activity, ActivityType } from '../../../../shared/models/activitypub' 2import { Activity, ActivityType } from '../../../../shared/models/activitypub'
2import { checkUrlsSameHost, getAPId } from '../../../helpers/activitypub' 3import { checkUrlsSameHost, getAPId } from '../../../helpers/activitypub'
3import { logger } from '../../../helpers/logger' 4import { logger } from '../../../helpers/logger'
5import { APProcessorOptions } from '../../../types/activitypub-processor.model'
6import { MActorDefault, MActorSignature } from '../../../types/models'
7import { getOrCreateAPActor } from '../actors'
4import { processAcceptActivity } from './process-accept' 8import { processAcceptActivity } from './process-accept'
5import { processAnnounceActivity } from './process-announce' 9import { processAnnounceActivity } from './process-announce'
6import { processCreateActivity } from './process-create' 10import { processCreateActivity } from './process-create'
7import { processDeleteActivity } from './process-delete' 11import { processDeleteActivity } from './process-delete'
12import { processDislikeActivity } from './process-dislike'
13import { processFlagActivity } from './process-flag'
8import { processFollowActivity } from './process-follow' 14import { processFollowActivity } from './process-follow'
9import { processLikeActivity } from './process-like' 15import { processLikeActivity } from './process-like'
10import { processRejectActivity } from './process-reject' 16import { processRejectActivity } from './process-reject'
11import { processUndoActivity } from './process-undo' 17import { processUndoActivity } from './process-undo'
12import { processUpdateActivity } from './process-update' 18import { processUpdateActivity } from './process-update'
13import { getOrCreateActorAndServerAndModel } from '../actor'
14import { processDislikeActivity } from './process-dislike'
15import { processFlagActivity } from './process-flag'
16import { processViewActivity } from './process-view' 19import { processViewActivity } from './process-view'
17import { APProcessorOptions } from '../../../types/activitypub-processor.model'
18import { MActorDefault, MActorSignature } from '../../../types/models'
19import { StatsManager } from '@server/lib/stat-manager'
20 20
21const processActivity: { [ P in ActivityType ]: (options: APProcessorOptions<Activity>) => Promise<any> } = { 21const processActivity: { [ P in ActivityType ]: (options: APProcessorOptions<Activity>) => Promise<any> } = {
22 Create: processCreateActivity, 22 Create: processCreateActivity,
@@ -65,7 +65,7 @@ async function processActivities (
65 continue 65 continue
66 } 66 }
67 67
68 const byActor = signatureActor || actorsCache[actorUrl] || await getOrCreateActorAndServerAndModel(actorUrl) 68 const byActor = signatureActor || actorsCache[actorUrl] || await getOrCreateAPActor(actorUrl)
69 actorsCache[actorUrl] = byActor 69 actorsCache[actorUrl] = byActor
70 70
71 const activityProcessor = processActivity[activity.type] 71 const activityProcessor = processActivity[activity.type]
diff --git a/server/lib/activitypub/share.ts b/server/lib/activitypub/share.ts
index 327955dd2..1ff01a175 100644
--- a/server/lib/activitypub/share.ts
+++ b/server/lib/activitypub/share.ts
@@ -7,7 +7,7 @@ import { doJSONRequest } from '../../helpers/requests'
7import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' 7import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
8import { VideoShareModel } from '../../models/video/video-share' 8import { VideoShareModel } from '../../models/video/video-share'
9import { MChannelActorLight, MVideo, MVideoAccountLight, MVideoId } from '../../types/models/video' 9import { MChannelActorLight, MVideo, MVideoAccountLight, MVideoId } from '../../types/models/video'
10import { getOrCreateActorAndServerAndModel } from './actor' 10import { getOrCreateAPActor } from './actors'
11import { sendUndoAnnounce, sendVideoAnnounce } from './send' 11import { sendUndoAnnounce, sendVideoAnnounce } from './send'
12import { getLocalVideoAnnounceActivityPubUrl } from './url' 12import { getLocalVideoAnnounceActivityPubUrl } from './url'
13 13
@@ -64,7 +64,7 @@ async function addVideoShare (shareUrl: string, video: MVideoId) {
64 throw new Error(`Actor url ${actorUrl} has not the same host than the share url ${shareUrl}`) 64 throw new Error(`Actor url ${actorUrl} has not the same host than the share url ${shareUrl}`)
65 } 65 }
66 66
67 const actor = await getOrCreateActorAndServerAndModel(actorUrl) 67 const actor = await getOrCreateAPActor(actorUrl)
68 68
69 const entry = { 69 const entry = {
70 actorId: actor.id, 70 actorId: actor.id,
diff --git a/server/lib/activitypub/video-comments.ts b/server/lib/activitypub/video-comments.ts
index 760da719d..6b7f9504f 100644
--- a/server/lib/activitypub/video-comments.ts
+++ b/server/lib/activitypub/video-comments.ts
@@ -6,7 +6,7 @@ import { doJSONRequest } from '../../helpers/requests'
6import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' 6import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
7import { VideoCommentModel } from '../../models/video/video-comment' 7import { VideoCommentModel } from '../../models/video/video-comment'
8import { MCommentOwner, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../types/models/video' 8import { MCommentOwner, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../types/models/video'
9import { getOrCreateActorAndServerAndModel } from './actor' 9import { getOrCreateAPActor } from './actors'
10import { getOrCreateAPVideo } from './videos' 10import { getOrCreateAPVideo } from './videos'
11 11
12type ResolveThreadParams = { 12type ResolveThreadParams = {
@@ -147,7 +147,7 @@ async function resolveRemoteParentComment (params: ResolveThreadParams) {
147 } 147 }
148 148
149 const actor = actorUrl 149 const actor = actorUrl
150 ? await getOrCreateActorAndServerAndModel(actorUrl, 'all') 150 ? await getOrCreateAPActor(actorUrl, 'all')
151 : null 151 : null
152 152
153 const comment = new VideoCommentModel({ 153 const comment = new VideoCommentModel({
diff --git a/server/lib/activitypub/video-rates.ts b/server/lib/activitypub/video-rates.ts
index 091f4ec23..0eec806f9 100644
--- a/server/lib/activitypub/video-rates.ts
+++ b/server/lib/activitypub/video-rates.ts
@@ -7,7 +7,7 @@ import { logger } from '../../helpers/logger'
7import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' 7import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
8import { AccountVideoRateModel } from '../../models/account/account-video-rate' 8import { AccountVideoRateModel } from '../../models/account/account-video-rate'
9import { MAccountActor, MActorUrl, MVideo, MVideoAccountLight, MVideoId } from '../../types/models' 9import { MAccountActor, MActorUrl, MVideo, MVideoAccountLight, MVideoId } from '../../types/models'
10import { getOrCreateActorAndServerAndModel } from './actor' 10import { getOrCreateAPActor } from './actors'
11import { sendLike, sendUndoDislike, sendUndoLike } from './send' 11import { sendLike, sendUndoDislike, sendUndoLike } from './send'
12import { sendDislike } from './send/send-dislike' 12import { sendDislike } from './send/send-dislike'
13import { getVideoDislikeActivityPubUrlByLocalActor, getVideoLikeActivityPubUrlByLocalActor } from './url' 13import { getVideoDislikeActivityPubUrlByLocalActor, getVideoLikeActivityPubUrlByLocalActor } from './url'
@@ -74,7 +74,7 @@ async function createRate (rateUrl: string, video: MVideo, rate: VideoRateType)
74 throw new Error(`Rate url ${rateUrl} host is different from the AP object id ${body.id}`) 74 throw new Error(`Rate url ${rateUrl} host is different from the AP object id ${body.id}`)
75 } 75 }
76 76
77 const actor = await getOrCreateActorAndServerAndModel(actorUrl) 77 const actor = await getOrCreateAPActor(actorUrl)
78 78
79 const entry = { 79 const entry = {
80 videoId: video.id, 80 videoId: video.id,
diff --git a/server/lib/activitypub/videos/shared/abstract-builder.ts b/server/lib/activitypub/videos/shared/abstract-builder.ts
index 953710f6c..f8e4d6aa3 100644
--- a/server/lib/activitypub/videos/shared/abstract-builder.ts
+++ b/server/lib/activitypub/videos/shared/abstract-builder.ts
@@ -10,7 +10,7 @@ import { VideoLiveModel } from '@server/models/video/video-live'
10import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' 10import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist'
11import { MStreamingPlaylistFilesVideo, MThumbnail, MVideoCaption, MVideoFile, MVideoFullLight, MVideoThumbnail } from '@server/types/models' 11import { MStreamingPlaylistFilesVideo, MThumbnail, MVideoCaption, MVideoFile, MVideoFullLight, MVideoThumbnail } from '@server/types/models'
12import { ActivityTagObject, ThumbnailType, VideoObject, VideoStreamingPlaylistType } from '@shared/models' 12import { ActivityTagObject, ThumbnailType, VideoObject, VideoStreamingPlaylistType } from '@shared/models'
13import { getOrCreateActorAndServerAndModel } from '../../actor' 13import { getOrCreateAPActor } from '../../actors'
14import { 14import {
15 getCaptionAttributesFromObject, 15 getCaptionAttributesFromObject,
16 getFileAttributesFromUrl, 16 getFileAttributesFromUrl,
@@ -34,7 +34,7 @@ export abstract class APVideoAbstractBuilder {
34 throw new Error(`Video channel url ${channel.id} does not have the same host than video object id ${this.videoObject.id}`) 34 throw new Error(`Video channel url ${channel.id} does not have the same host than video object id ${this.videoObject.id}`)
35 } 35 }
36 36
37 return getOrCreateActorAndServerAndModel(channel.id, 'all') 37 return getOrCreateAPActor(channel.id, 'all')
38 } 38 }
39 39
40 protected tryToGenerateThumbnail (video: MVideoThumbnail): Promise<MThumbnail> { 40 protected tryToGenerateThumbnail (video: MVideoThumbnail): Promise<MThumbnail> {
diff --git a/server/lib/activitypub/videos/updater.ts b/server/lib/activitypub/videos/updater.ts
index 9e1c74969..6745e2efd 100644
--- a/server/lib/activitypub/videos/updater.ts
+++ b/server/lib/activitypub/videos/updater.ts
@@ -126,7 +126,7 @@ export class APVideoUpdater extends APVideoAbstractBuilder {
126 this.video.views = videoData.views 126 this.video.views = videoData.views
127 this.video.isLive = videoData.isLive 127 this.video.isLive = videoData.isLive
128 128
129 // Ensures we update the updated video attribute 129 // Ensures we update the updatedAt attribute, even if main attributes did not change
130 this.video.changed('updatedAt', true) 130 this.video.changed('updatedAt', true)
131 131
132 return this.video.save({ transaction }) as Promise<MVideoFullLight> 132 return this.video.save({ transaction }) as Promise<MVideoFullLight>
diff --git a/server/lib/job-queue/handlers/activitypub-follow.ts b/server/lib/job-queue/handlers/activitypub-follow.ts
index ec8df8969..76b6fcaae 100644
--- a/server/lib/job-queue/handlers/activitypub-follow.ts
+++ b/server/lib/job-queue/handlers/activitypub-follow.ts
@@ -10,7 +10,7 @@ import { sequelizeTypescript } from '../../../initializers/database'
10import { ActorModel } from '../../../models/actor/actor' 10import { ActorModel } from '../../../models/actor/actor'
11import { ActorFollowModel } from '../../../models/actor/actor-follow' 11import { ActorFollowModel } from '../../../models/actor/actor-follow'
12import { MActor, MActorFollowActors, MActorFull } from '../../../types/models' 12import { MActor, MActorFollowActors, MActorFull } from '../../../types/models'
13import { getOrCreateActorAndServerAndModel } from '../../activitypub/actor' 13import { getOrCreateAPActor } from '../../activitypub/actors'
14import { sendFollow } from '../../activitypub/send' 14import { sendFollow } from '../../activitypub/send'
15import { Notifier } from '../../notifier' 15import { Notifier } from '../../notifier'
16 16
@@ -26,7 +26,7 @@ async function processActivityPubFollow (job: Bull.Job) {
26 } else { 26 } else {
27 const sanitizedHost = sanitizeHost(host, REMOTE_SCHEME.HTTP) 27 const sanitizedHost = sanitizeHost(host, REMOTE_SCHEME.HTTP)
28 const actorUrl = await loadActorUrlOrGetFromWebfinger(payload.name + '@' + sanitizedHost) 28 const actorUrl = await loadActorUrlOrGetFromWebfinger(payload.name + '@' + sanitizedHost)
29 targetActor = await getOrCreateActorAndServerAndModel(actorUrl, 'all') 29 targetActor = await getOrCreateAPActor(actorUrl, 'all')
30 } 30 }
31 31
32 if (payload.assertIsChannel && !targetActor.VideoChannel) { 32 if (payload.assertIsChannel && !targetActor.VideoChannel) {
diff --git a/server/lib/job-queue/handlers/activitypub-refresher.ts b/server/lib/job-queue/handlers/activitypub-refresher.ts
index 10e6895da..29483f310 100644
--- a/server/lib/job-queue/handlers/activitypub-refresher.ts
+++ b/server/lib/job-queue/handlers/activitypub-refresher.ts
@@ -6,7 +6,7 @@ import { logger } from '../../../helpers/logger'
6import { fetchVideoByUrl } from '../../../helpers/video' 6import { fetchVideoByUrl } from '../../../helpers/video'
7import { ActorModel } from '../../../models/actor/actor' 7import { ActorModel } from '../../../models/actor/actor'
8import { VideoPlaylistModel } from '../../../models/video/video-playlist' 8import { VideoPlaylistModel } from '../../../models/video/video-playlist'
9import { refreshActorIfNeeded } from '../../activitypub/actor' 9import { refreshActorIfNeeded } from '../../activitypub/actors'
10 10
11async function refreshAPObject (job: Bull.Job) { 11async function refreshAPObject (job: Bull.Job) {
12 const payload = job.data as RefreshPayload 12 const payload = job.data as RefreshPayload
diff --git a/server/lib/job-queue/handlers/actor-keys.ts b/server/lib/job-queue/handlers/actor-keys.ts
index 3eef565d0..60ac61afd 100644
--- a/server/lib/job-queue/handlers/actor-keys.ts
+++ b/server/lib/job-queue/handlers/actor-keys.ts
@@ -1,5 +1,5 @@
1import * as Bull from 'bull' 1import * as Bull from 'bull'
2import { generateAndSaveActorKeys } from '@server/lib/activitypub/actor' 2import { generateAndSaveActorKeys } from '@server/lib/activitypub/actors'
3import { ActorModel } from '@server/models/actor/actor' 3import { ActorModel } from '@server/models/actor/actor'
4import { ActorKeysPayload } from '@shared/models' 4import { ActorKeysPayload } from '@shared/models'
5import { logger } from '../../../helpers/logger' 5import { logger } from '../../../helpers/logger'
diff --git a/server/lib/actor-image.ts b/server/lib/local-actor.ts
index f271f0b5b..55e77dd04 100644
--- a/server/lib/actor-image.ts
+++ b/server/lib/local-actor.ts
@@ -3,17 +3,35 @@ import { queue } from 'async'
3import * as LRUCache from 'lru-cache' 3import * as LRUCache from 'lru-cache'
4import { extname, join } from 'path' 4import { extname, join } from 'path'
5import { v4 as uuidv4 } from 'uuid' 5import { v4 as uuidv4 } from 'uuid'
6import { ActorImageType } from '@shared/models' 6import { ActorModel } from '@server/models/actor/actor'
7import { ActivityPubActorType, ActorImageType } from '@shared/models'
7import { retryTransactionWrapper } from '../helpers/database-utils' 8import { retryTransactionWrapper } from '../helpers/database-utils'
8import { processImage } from '../helpers/image-utils' 9import { processImage } from '../helpers/image-utils'
9import { downloadImage } from '../helpers/requests' 10import { downloadImage } from '../helpers/requests'
10import { CONFIG } from '../initializers/config' 11import { CONFIG } from '../initializers/config'
11import { ACTOR_IMAGES_SIZE, LRU_CACHE, QUEUE_CONCURRENCY } from '../initializers/constants' 12import { ACTOR_IMAGES_SIZE, LRU_CACHE, QUEUE_CONCURRENCY, WEBSERVER } from '../initializers/constants'
12import { sequelizeTypescript } from '../initializers/database' 13import { sequelizeTypescript } from '../initializers/database'
13import { MAccountDefault, MChannelDefault } from '../types/models' 14import { MAccountDefault, MActor, MChannelDefault } from '../types/models'
14import { deleteActorImageInstance, updateActorImageInstance } from './activitypub/actor' 15import { deleteActorImageInstance, updateActorImageInstance } from './activitypub/actors'
15import { sendUpdateActor } from './activitypub/send' 16import { sendUpdateActor } from './activitypub/send'
16 17
18function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string) {
19 return new ActorModel({
20 type,
21 url,
22 preferredUsername,
23 publicKey: null,
24 privateKey: null,
25 followersCount: 0,
26 followingCount: 0,
27 inboxUrl: url + '/inbox',
28 outboxUrl: url + '/outbox',
29 sharedInboxUrl: WEBSERVER.URL + '/inbox',
30 followersUrl: url + '/followers',
31 followingUrl: url + '/following'
32 }) as MActor
33}
34
17async function updateLocalActorImageFile ( 35async function updateLocalActorImageFile (
18 accountOrChannel: MAccountDefault | MChannelDefault, 36 accountOrChannel: MAccountDefault | MChannelDefault,
19 imagePhysicalFile: Express.Multer.File, 37 imagePhysicalFile: Express.Multer.File,
@@ -93,5 +111,6 @@ export {
93 actorImagePathUnsafeCache, 111 actorImagePathUnsafeCache,
94 updateLocalActorImageFile, 112 updateLocalActorImageFile,
95 deleteLocalActorImageFile, 113 deleteLocalActorImageFile,
96 pushActorImageProcessInQueue 114 pushActorImageProcessInQueue,
115 buildActorInstance
97} 116}
diff --git a/server/lib/user.ts b/server/lib/user.ts
index 8a6fcebc7..a2163abb1 100644
--- a/server/lib/user.ts
+++ b/server/lib/user.ts
@@ -11,10 +11,11 @@ import { ActorModel } from '../models/actor/actor'
11import { UserNotificationSettingModel } from '../models/user/user-notification-setting' 11import { UserNotificationSettingModel } from '../models/user/user-notification-setting'
12import { MAccountDefault, MChannelActor } from '../types/models' 12import { MAccountDefault, MChannelActor } from '../types/models'
13import { MUser, MUserDefault, MUserId } from '../types/models/user' 13import { MUser, MUserDefault, MUserId } from '../types/models/user'
14import { buildActorInstance, generateAndSaveActorKeys } from './activitypub/actor' 14import { generateAndSaveActorKeys } from './activitypub/actors'
15import { getLocalAccountActivityPubUrl } from './activitypub/url' 15import { getLocalAccountActivityPubUrl } from './activitypub/url'
16import { Emailer } from './emailer' 16import { Emailer } from './emailer'
17import { LiveManager } from './live-manager' 17import { LiveManager } from './live-manager'
18import { buildActorInstance } from './local-actor'
18import { Redis } from './redis' 19import { Redis } from './redis'
19import { createLocalVideoChannel } from './video-channel' 20import { createLocalVideoChannel } from './video-channel'
20import { createWatchLaterPlaylist } from './video-playlist' 21import { createWatchLaterPlaylist } from './video-playlist'
diff --git a/server/lib/video-channel.ts b/server/lib/video-channel.ts
index d57e832fe..2fd63a8c4 100644
--- a/server/lib/video-channel.ts
+++ b/server/lib/video-channel.ts
@@ -3,9 +3,9 @@ import { VideoChannelCreate } from '../../shared/models'
3import { VideoModel } from '../models/video/video' 3import { VideoModel } from '../models/video/video'
4import { VideoChannelModel } from '../models/video/video-channel' 4import { VideoChannelModel } from '../models/video/video-channel'
5import { MAccountId, MChannelId } from '../types/models' 5import { MAccountId, MChannelId } from '../types/models'
6import { buildActorInstance } from './activitypub/actor'
7import { getLocalVideoChannelActivityPubUrl } from './activitypub/url' 6import { getLocalVideoChannelActivityPubUrl } from './activitypub/url'
8import { federateVideoIfNeeded } from './activitypub/videos' 7import { federateVideoIfNeeded } from './activitypub/videos'
8import { buildActorInstance } from './local-actor'
9 9
10async function createLocalVideoChannel (videoChannelInfo: VideoChannelCreate, account: MAccountId, t: Sequelize.Transaction) { 10async function createLocalVideoChannel (videoChannelInfo: VideoChannelCreate, account: MAccountId, t: Sequelize.Transaction) {
11 const url = getLocalVideoChannelActivityPubUrl(videoChannelInfo.name) 11 const url = getLocalVideoChannelActivityPubUrl(videoChannelInfo.name)
diff --git a/server/middlewares/activitypub.ts b/server/middlewares/activitypub.ts
index 6cd23f230..a1fdfafcf 100644
--- a/server/middlewares/activitypub.ts
+++ b/server/middlewares/activitypub.ts
@@ -3,7 +3,7 @@ import { ActivityDelete, ActivityPubSignature } from '../../shared'
3import { logger } from '../helpers/logger' 3import { logger } from '../helpers/logger'
4import { isHTTPSignatureVerified, isJsonLDSignatureVerified, parseHTTPSignature } from '../helpers/peertube-crypto' 4import { isHTTPSignatureVerified, isJsonLDSignatureVerified, parseHTTPSignature } from '../helpers/peertube-crypto'
5import { ACCEPT_HEADERS, ACTIVITY_PUB, HTTP_SIGNATURE } from '../initializers/constants' 5import { ACCEPT_HEADERS, ACTIVITY_PUB, HTTP_SIGNATURE } from '../initializers/constants'
6import { getOrCreateActorAndServerAndModel } from '../lib/activitypub/actor' 6import { getOrCreateAPActor } from '../lib/activitypub/actors'
7import { loadActorUrlOrGetFromWebfinger } from '../helpers/webfinger' 7import { loadActorUrlOrGetFromWebfinger } from '../helpers/webfinger'
8import { isActorDeleteActivityValid } from '@server/helpers/custom-validators/activitypub/actor' 8import { isActorDeleteActivityValid } from '@server/helpers/custom-validators/activitypub/actor'
9import { getAPId } from '@server/helpers/activitypub' 9import { getAPId } from '@server/helpers/activitypub'
@@ -100,7 +100,7 @@ async function checkHttpSignature (req: Request, res: Response) {
100 actorUrl = await loadActorUrlOrGetFromWebfinger(actorUrl.replace(/^acct:/, '')) 100 actorUrl = await loadActorUrlOrGetFromWebfinger(actorUrl.replace(/^acct:/, ''))
101 } 101 }
102 102
103 const actor = await getOrCreateActorAndServerAndModel(actorUrl) 103 const actor = await getOrCreateAPActor(actorUrl)
104 104
105 const verified = isHTTPSignatureVerified(parsed, actor) 105 const verified = isHTTPSignatureVerified(parsed, actor)
106 if (verified !== true) { 106 if (verified !== true) {
@@ -135,7 +135,7 @@ async function checkJsonLDSignature (req: Request, res: Response) {
135 135
136 logger.debug('Checking JsonLD signature of actor %s...', creator) 136 logger.debug('Checking JsonLD signature of actor %s...', creator)
137 137
138 const actor = await getOrCreateActorAndServerAndModel(creator) 138 const actor = await getOrCreateAPActor(creator)
139 const verified = await isJsonLDSignatureVerified(actor, req.body) 139 const verified = await isJsonLDSignatureVerified(actor, req.body)
140 140
141 if (verified !== true) { 141 if (verified !== true) {