aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib/activitypub
diff options
context:
space:
mode:
Diffstat (limited to 'server/lib/activitypub')
-rw-r--r--server/lib/activitypub/activity.ts74
-rw-r--r--server/lib/activitypub/actors/get.ts143
-rw-r--r--server/lib/activitypub/actors/image.ts112
-rw-r--r--server/lib/activitypub/actors/index.ts6
-rw-r--r--server/lib/activitypub/actors/keys.ts16
-rw-r--r--server/lib/activitypub/actors/refresh.ts81
-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.ts84
-rw-r--r--server/lib/activitypub/actors/shared/url-to-object.ts56
-rw-r--r--server/lib/activitypub/actors/updater.ts91
-rw-r--r--server/lib/activitypub/actors/webfinger.ts67
-rw-r--r--server/lib/activitypub/audience.ts34
-rw-r--r--server/lib/activitypub/cache-file.ts82
-rw-r--r--server/lib/activitypub/collection.ts63
-rw-r--r--server/lib/activitypub/context.ts212
-rw-r--r--server/lib/activitypub/crawl.ts58
-rw-r--r--server/lib/activitypub/follow.ts51
-rw-r--r--server/lib/activitypub/inbox-manager.ts47
-rw-r--r--server/lib/activitypub/local-video-viewer.ts44
-rw-r--r--server/lib/activitypub/outbox.ts24
-rw-r--r--server/lib/activitypub/playlists/create-update.ts157
-rw-r--r--server/lib/activitypub/playlists/get.ts35
-rw-r--r--server/lib/activitypub/playlists/index.ts3
-rw-r--r--server/lib/activitypub/playlists/refresh.ts53
-rw-r--r--server/lib/activitypub/playlists/shared/index.ts2
-rw-r--r--server/lib/activitypub/playlists/shared/object-to-model-attributes.ts40
-rw-r--r--server/lib/activitypub/playlists/shared/url-to-object.ts47
-rw-r--r--server/lib/activitypub/process/index.ts1
-rw-r--r--server/lib/activitypub/process/process-accept.ts32
-rw-r--r--server/lib/activitypub/process/process-announce.ts75
-rw-r--r--server/lib/activitypub/process/process-create.ts170
-rw-r--r--server/lib/activitypub/process/process-delete.ts153
-rw-r--r--server/lib/activitypub/process/process-dislike.ts58
-rw-r--r--server/lib/activitypub/process/process-flag.ts103
-rw-r--r--server/lib/activitypub/process/process-follow.ts156
-rw-r--r--server/lib/activitypub/process/process-like.ts60
-rw-r--r--server/lib/activitypub/process/process-reject.ts33
-rw-r--r--server/lib/activitypub/process/process-undo.ts183
-rw-r--r--server/lib/activitypub/process/process-update.ts119
-rw-r--r--server/lib/activitypub/process/process-view.ts42
-rw-r--r--server/lib/activitypub/process/process.ts92
-rw-r--r--server/lib/activitypub/send/http.ts73
-rw-r--r--server/lib/activitypub/send/index.ts10
-rw-r--r--server/lib/activitypub/send/send-accept.ts47
-rw-r--r--server/lib/activitypub/send/send-announce.ts58
-rw-r--r--server/lib/activitypub/send/send-create.ts226
-rw-r--r--server/lib/activitypub/send/send-delete.ts158
-rw-r--r--server/lib/activitypub/send/send-dislike.ts40
-rw-r--r--server/lib/activitypub/send/send-flag.ts42
-rw-r--r--server/lib/activitypub/send/send-follow.ts37
-rw-r--r--server/lib/activitypub/send/send-like.ts40
-rw-r--r--server/lib/activitypub/send/send-reject.ts39
-rw-r--r--server/lib/activitypub/send/send-undo.ts172
-rw-r--r--server/lib/activitypub/send/send-update.ts157
-rw-r--r--server/lib/activitypub/send/send-view.ts62
-rw-r--r--server/lib/activitypub/send/shared/audience-utils.ts74
-rw-r--r--server/lib/activitypub/send/shared/index.ts2
-rw-r--r--server/lib/activitypub/send/shared/send-utils.ts291
-rw-r--r--server/lib/activitypub/share.ts120
-rw-r--r--server/lib/activitypub/url.ts177
-rw-r--r--server/lib/activitypub/video-comments.ts205
-rw-r--r--server/lib/activitypub/video-rates.ts59
-rw-r--r--server/lib/activitypub/videos/federate.ts29
-rw-r--r--server/lib/activitypub/videos/get.ts116
-rw-r--r--server/lib/activitypub/videos/index.ts4
-rw-r--r--server/lib/activitypub/videos/refresh.ts68
-rw-r--r--server/lib/activitypub/videos/shared/abstract-builder.ts190
-rw-r--r--server/lib/activitypub/videos/shared/creator.ts65
-rw-r--r--server/lib/activitypub/videos/shared/index.ts6
-rw-r--r--server/lib/activitypub/videos/shared/object-to-model-attributes.ts285
-rw-r--r--server/lib/activitypub/videos/shared/trackers.ts43
-rw-r--r--server/lib/activitypub/videos/shared/url-to-object.ts25
-rw-r--r--server/lib/activitypub/videos/shared/video-sync-attributes.ts107
-rw-r--r--server/lib/activitypub/videos/updater.ts180
75 files changed, 0 insertions, 6318 deletions
diff --git a/server/lib/activitypub/activity.ts b/server/lib/activitypub/activity.ts
deleted file mode 100644
index 391bcd9c6..000000000
--- a/server/lib/activitypub/activity.ts
+++ /dev/null
@@ -1,74 +0,0 @@
1import { doJSONRequest, PeerTubeRequestOptions } from '@server/helpers/requests'
2import { CONFIG } from '@server/initializers/config'
3import { ActivityObject, ActivityPubActor, ActivityType, APObjectId } from '@shared/models'
4import { buildSignedRequestOptions } from './send'
5
6export function getAPId (object: string | { id: string }) {
7 if (typeof object === 'string') return object
8
9 return object.id
10}
11
12export function getActivityStreamDuration (duration: number) {
13 // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration
14 return 'PT' + duration + 'S'
15}
16
17export function getDurationFromActivityStream (duration: string) {
18 return parseInt(duration.replace(/[^\d]+/, ''))
19}
20
21// ---------------------------------------------------------------------------
22
23export function buildAvailableActivities (): ActivityType[] {
24 return [
25 'Create',
26 'Update',
27 'Delete',
28 'Follow',
29 'Accept',
30 'Announce',
31 'Undo',
32 'Like',
33 'Reject',
34 'View',
35 'Dislike',
36 'Flag'
37 ]
38}
39
40// ---------------------------------------------------------------------------
41
42export async function fetchAP <T> (url: string, moreOptions: PeerTubeRequestOptions = {}) {
43 const options = {
44 activityPub: true,
45
46 httpSignature: CONFIG.FEDERATION.SIGN_FEDERATED_FETCHES
47 ? await buildSignedRequestOptions({ hasPayload: false })
48 : undefined,
49
50 ...moreOptions
51 }
52
53 return doJSONRequest<T>(url, options)
54}
55
56export async function fetchAPObjectIfNeeded <T extends (ActivityObject | ActivityPubActor)> (object: APObjectId) {
57 if (typeof object === 'string') {
58 const { body } = await fetchAP<Exclude<T, string>>(object)
59
60 return body
61 }
62
63 return object as Exclude<T, string>
64}
65
66export async function findLatestAPRedirection (url: string, iteration = 1) {
67 if (iteration > 10) throw new Error('Too much iterations to find final URL ' + url)
68
69 const { headers } = await fetchAP(url, { followRedirect: false })
70
71 if (headers.location) return findLatestAPRedirection(headers.location, iteration + 1)
72
73 return url
74}
diff --git a/server/lib/activitypub/actors/get.ts b/server/lib/activitypub/actors/get.ts
deleted file mode 100644
index dd2bc9f03..000000000
--- a/server/lib/activitypub/actors/get.ts
+++ /dev/null
@@ -1,143 +0,0 @@
1import { retryTransactionWrapper } from '@server/helpers/database-utils'
2import { logger } from '@server/helpers/logger'
3import { JobQueue } from '@server/lib/job-queue'
4import { ActorLoadByUrlType, loadActorByUrl } from '@server/lib/model-loaders'
5import { MActor, MActorAccountChannelId, MActorAccountChannelIdActor, MActorAccountId, MActorFullActor } from '@server/types/models'
6import { arrayify } from '@shared/core-utils'
7import { ActivityPubActor, APObjectId } from '@shared/models'
8import { fetchAPObjectIfNeeded, getAPId } from '../activity'
9import { checkUrlsSameHost } from '../url'
10import { refreshActorIfNeeded } from './refresh'
11import { APActorCreator, fetchRemoteActor } from './shared'
12
13function getOrCreateAPActor (
14 activityActor: string | ActivityPubActor,
15 fetchType: 'all',
16 recurseIfNeeded?: boolean,
17 updateCollections?: boolean
18): Promise<MActorFullActor>
19
20function getOrCreateAPActor (
21 activityActor: string | ActivityPubActor,
22 fetchType?: 'association-ids',
23 recurseIfNeeded?: boolean,
24 updateCollections?: boolean
25): Promise<MActorAccountChannelId>
26
27async function getOrCreateAPActor (
28 activityActor: string | ActivityPubActor,
29 fetchType: ActorLoadByUrlType = 'association-ids',
30 recurseIfNeeded = true,
31 updateCollections = false
32): Promise<MActorFullActor | MActorAccountChannelId> {
33 const actorUrl = getAPId(activityActor)
34 let actor = await loadActorFromDB(actorUrl, fetchType)
35
36 let created = false
37 let accountPlaylistsUrl: string
38
39 // We don't have this actor in our database, fetch it on remote
40 if (!actor) {
41 const { actorObject } = await fetchRemoteActor(actorUrl)
42 if (actorObject === undefined) throw new Error('Cannot fetch remote actor ' + actorUrl)
43
44 // actorUrl is just an alias/redirection, so process object id instead
45 if (actorObject.id !== actorUrl) return getOrCreateAPActor(actorObject, 'all', recurseIfNeeded, updateCollections)
46
47 // Create the attributed to actor
48 // In PeerTube a video channel is owned by an account
49 let ownerActor: MActorFullActor
50 if (recurseIfNeeded === true && actorObject.type === 'Group') {
51 ownerActor = await getOrCreateAPOwner(actorObject, actorUrl)
52 }
53
54 const creator = new APActorCreator(actorObject, ownerActor)
55 actor = await retryTransactionWrapper(creator.create.bind(creator))
56 created = true
57 accountPlaylistsUrl = actorObject.playlists
58 }
59
60 if (actor.Account) (actor as MActorAccountChannelIdActor).Account.Actor = actor
61 if (actor.VideoChannel) (actor as MActorAccountChannelIdActor).VideoChannel.Actor = actor
62
63 const { actor: actorRefreshed, refreshed } = await refreshActorIfNeeded({ actor, fetchedType: fetchType })
64 if (!actorRefreshed) throw new Error('Actor ' + actor.url + ' does not exist anymore.')
65
66 await scheduleOutboxFetchIfNeeded(actor, created, refreshed, updateCollections)
67 await schedulePlaylistFetchIfNeeded(actor, created, accountPlaylistsUrl)
68
69 return actorRefreshed
70}
71
72async function getOrCreateAPOwner (actorObject: ActivityPubActor, actorUrl: string) {
73 const accountAttributedTo = await findOwner(actorUrl, actorObject.attributedTo, 'Person')
74 if (!accountAttributedTo) {
75 throw new Error(`Cannot find account attributed to video channel ${actorUrl}`)
76 }
77
78 try {
79 // Don't recurse another time
80 const recurseIfNeeded = false
81 return getOrCreateAPActor(accountAttributedTo, 'all', recurseIfNeeded)
82 } catch (err) {
83 logger.error('Cannot get or create account attributed to video channel ' + actorUrl)
84 throw new Error(err)
85 }
86}
87
88async function findOwner (rootUrl: string, attributedTo: APObjectId[] | APObjectId, type: 'Person' | 'Group') {
89 for (const actorToCheck of arrayify(attributedTo)) {
90 const actorObject = await fetchAPObjectIfNeeded<ActivityPubActor>(getAPId(actorToCheck))
91
92 if (!actorObject) {
93 logger.warn('Unknown attributed to actor %s for owner %s', actorToCheck, rootUrl)
94 continue
95 }
96
97 if (checkUrlsSameHost(actorObject.id, rootUrl) !== true) {
98 logger.warn(`Account attributed to ${actorObject.id} does not have the same host than owner actor url ${rootUrl}`)
99 continue
100 }
101
102 if (actorObject.type === type) return actorObject
103 }
104
105 return undefined
106}
107
108// ---------------------------------------------------------------------------
109
110export {
111 getOrCreateAPOwner,
112 getOrCreateAPActor,
113 findOwner
114}
115
116// ---------------------------------------------------------------------------
117
118async function loadActorFromDB (actorUrl: string, fetchType: ActorLoadByUrlType) {
119 let actor = await loadActorByUrl(actorUrl, fetchType)
120
121 // Orphan actor (not associated to an account of channel) so recreate it
122 if (actor && (!actor.Account && !actor.VideoChannel)) {
123 await actor.destroy()
124 actor = null
125 }
126
127 return actor
128}
129
130async function scheduleOutboxFetchIfNeeded (actor: MActor, created: boolean, refreshed: boolean, updateCollections: boolean) {
131 if ((created === true || refreshed === true) && updateCollections === true) {
132 const payload = { uri: actor.outboxUrl, type: 'activity' as 'activity' }
133 await JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload })
134 }
135}
136
137async function schedulePlaylistFetchIfNeeded (actor: MActorAccountId, created: boolean, accountPlaylistsUrl: string) {
138 // We created a new account: fetch the playlists
139 if (created === true && actor.Account && accountPlaylistsUrl) {
140 const payload = { uri: accountPlaylistsUrl, type: 'account-playlists' as 'account-playlists' }
141 await JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload })
142 }
143}
diff --git a/server/lib/activitypub/actors/image.ts b/server/lib/activitypub/actors/image.ts
deleted file mode 100644
index e1d29af5b..000000000
--- a/server/lib/activitypub/actors/image.ts
+++ /dev/null
@@ -1,112 +0,0 @@
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 updateActorImages (actor: MActorImages, type: ActorImageType, imagesInfo: ImageInfo[], t: Transaction) {
16 const getAvatarsOrBanners = () => {
17 const result = type === ActorImageType.AVATAR
18 ? actor.Avatars
19 : actor.Banners
20
21 return result || []
22 }
23
24 if (imagesInfo.length === 0) {
25 await deleteActorImages(actor, type, t)
26 }
27
28 // Cleanup old images that did not have a width
29 for (const oldImageModel of getAvatarsOrBanners()) {
30 if (oldImageModel.width) continue
31
32 await safeDeleteActorImage(actor, oldImageModel, type, t)
33 }
34
35 for (const imageInfo of imagesInfo) {
36 const oldImageModel = getAvatarsOrBanners().find(i => imageInfo.width && i.width === imageInfo.width)
37
38 if (oldImageModel) {
39 // Don't update the avatar if the file URL did not change
40 if (imageInfo.fileUrl && oldImageModel.fileUrl === imageInfo.fileUrl) {
41 continue
42 }
43
44 await safeDeleteActorImage(actor, oldImageModel, type, t)
45 }
46
47 const imageModel = await ActorImageModel.create({
48 filename: imageInfo.name,
49 onDisk: imageInfo.onDisk ?? false,
50 fileUrl: imageInfo.fileUrl,
51 height: imageInfo.height,
52 width: imageInfo.width,
53 type,
54 actorId: actor.id
55 }, { transaction: t })
56
57 addActorImage(actor, type, imageModel)
58 }
59
60 return actor
61}
62
63async function deleteActorImages (actor: MActorImages, type: ActorImageType, t: Transaction) {
64 try {
65 const association = buildAssociationName(type)
66
67 for (const image of actor[association]) {
68 await image.destroy({ transaction: t })
69 }
70
71 actor[association] = []
72 } catch (err) {
73 logger.error('Cannot remove old image of actor %s.', actor.url, { err })
74 }
75
76 return actor
77}
78
79async function safeDeleteActorImage (actor: MActorImages, toDelete: MActorImage, type: ActorImageType, t: Transaction) {
80 try {
81 await toDelete.destroy({ transaction: t })
82
83 const association = buildAssociationName(type)
84 actor[association] = actor[association].filter(image => image.id !== toDelete.id)
85 } catch (err) {
86 logger.error('Cannot remove old actor image of actor %s.', actor.url, { err })
87 }
88}
89
90// ---------------------------------------------------------------------------
91
92export {
93 ImageInfo,
94
95 updateActorImages,
96 deleteActorImages
97}
98
99// ---------------------------------------------------------------------------
100
101function addActorImage (actor: MActorImages, type: ActorImageType, imageModel: MActorImage) {
102 const association = buildAssociationName(type)
103 if (!actor[association]) actor[association] = []
104
105 actor[association].push(imageModel)
106}
107
108function buildAssociationName (type: ActorImageType) {
109 return type === ActorImageType.AVATAR
110 ? 'Avatars'
111 : 'Banners'
112}
diff --git a/server/lib/activitypub/actors/index.ts b/server/lib/activitypub/actors/index.ts
deleted file mode 100644
index 5ee2a6f1a..000000000
--- a/server/lib/activitypub/actors/index.ts
+++ /dev/null
@@ -1,6 +0,0 @@
1export * from './get'
2export * from './image'
3export * from './keys'
4export * from './refresh'
5export * from './updater'
6export * from './webfinger'
diff --git a/server/lib/activitypub/actors/keys.ts b/server/lib/activitypub/actors/keys.ts
deleted file mode 100644
index c3d18abd8..000000000
--- a/server/lib/activitypub/actors/keys.ts
+++ /dev/null
@@ -1,16 +0,0 @@
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
deleted file mode 100644
index d15cb5e90..000000000
--- a/server/lib/activitypub/actors/refresh.ts
+++ /dev/null
@@ -1,81 +0,0 @@
1import { logger, loggerTagsFactory } from '@server/helpers/logger'
2import { CachePromiseFactory } from '@server/helpers/promise-cache'
3import { PeerTubeRequestError } from '@server/helpers/requests'
4import { ActorLoadByUrlType } from '@server/lib/model-loaders'
5import { ActorModel } from '@server/models/actor/actor'
6import { MActorAccountChannelId, MActorFull } from '@server/types/models'
7import { HttpStatusCode } from '@shared/models'
8import { fetchRemoteActor } from './shared'
9import { APActorUpdater } from './updater'
10import { getUrlFromWebfinger } from './webfinger'
11
12type RefreshResult <T> = Promise<{ actor: T | MActorFull, refreshed: boolean }>
13
14type RefreshOptions <T> = {
15 actor: T
16 fetchedType: ActorLoadByUrlType
17}
18
19const promiseCache = new CachePromiseFactory(doRefresh, (options: RefreshOptions<MActorFull | MActorAccountChannelId>) => options.actor.url)
20
21function refreshActorIfNeeded <T extends MActorFull | MActorAccountChannelId> (options: RefreshOptions<T>): RefreshResult <T> {
22 const actorArg = options.actor
23 if (!actorArg.isOutdated()) return Promise.resolve({ actor: actorArg, refreshed: false })
24
25 return promiseCache.run(options)
26}
27
28export {
29 refreshActorIfNeeded
30}
31
32// ---------------------------------------------------------------------------
33
34async function doRefresh <T extends MActorFull | MActorAccountChannelId> (options: RefreshOptions<T>): RefreshResult <MActorFull> {
35 const { actor: actorArg, fetchedType } = options
36
37 // We need more attributes
38 const actor = fetchedType === 'all'
39 ? actorArg as MActorFull
40 : await ActorModel.loadByUrlAndPopulateAccountAndChannel(actorArg.url)
41
42 const lTags = loggerTagsFactory('ap', 'actor', 'refresh', actor.url)
43
44 logger.info('Refreshing actor %s.', actor.url, lTags())
45
46 try {
47 const actorUrl = await getActorUrl(actor)
48 const { actorObject } = await fetchRemoteActor(actorUrl)
49
50 if (actorObject === undefined) {
51 logger.info('Cannot fetch remote actor %s in refresh actor.', actorUrl)
52 return { actor, refreshed: false }
53 }
54
55 const updater = new APActorUpdater(actorObject, actor)
56 await updater.update()
57
58 return { refreshed: true, actor }
59 } catch (err) {
60 if ((err as PeerTubeRequestError).statusCode === HttpStatusCode.NOT_FOUND_404) {
61 logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url, lTags())
62
63 actor.Account
64 ? await actor.Account.destroy()
65 : await actor.VideoChannel.destroy()
66
67 return { actor: undefined, refreshed: false }
68 }
69
70 logger.info('Cannot refresh actor %s.', actor.url, { err, ...lTags() })
71 return { actor, refreshed: false }
72 }
73}
74
75function getActorUrl (actor: MActorFull) {
76 return getUrlFromWebfinger(actor.preferredUsername + '@' + actor.getHost())
77 .catch(err => {
78 logger.warn('Cannot get actor URL from webfinger, keeping the old one.', { err })
79 return actor.url
80 })
81}
diff --git a/server/lib/activitypub/actors/shared/creator.ts b/server/lib/activitypub/actors/shared/creator.ts
deleted file mode 100644
index 500bc9912..000000000
--- a/server/lib/activitypub/actors/shared/creator.ts
+++ /dev/null
@@ -1,149 +0,0 @@
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 { updateActorImages } from '../image'
10import { getActorAttributesFromObject, getActorDisplayNameFromObject, getImagesInfoFromObject } 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 const { actorCreated, created } = await this.saveActor(actorInstance, t)
31
32 await this.setImageIfNeeded(actorCreated, ActorImageType.AVATAR, t)
33 await this.setImageIfNeeded(actorCreated, ActorImageType.BANNER, 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 imagesInfo = getImagesInfoFromObject(this.actorObject, type)
75 if (imagesInfo.length === 0) return
76
77 return updateActorImages(actor as MActorImages, type, imagesInfo, 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
deleted file mode 100644
index 52af1a8e1..000000000
--- a/server/lib/activitypub/actors/shared/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
1export * from './creator'
2export * from './object-to-model-attributes'
3export * from './url-to-object'
diff --git a/server/lib/activitypub/actors/shared/object-to-model-attributes.ts b/server/lib/activitypub/actors/shared/object-to-model-attributes.ts
deleted file mode 100644
index 3ce332681..000000000
--- a/server/lib/activitypub/actors/shared/object-to-model-attributes.ts
+++ /dev/null
@@ -1,84 +0,0 @@
1import { isActivityPubUrlValid } from '@server/helpers/custom-validators/activitypub/misc'
2import { MIMETYPES } from '@server/initializers/constants'
3import { ActorModel } from '@server/models/actor/actor'
4import { FilteredModelAttributes } from '@server/types'
5import { getLowercaseExtension } from '@shared/core-utils'
6import { buildUUID } from '@shared/extra-utils'
7import { ActivityIconObject, 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 getImagesInfoFromObject (actorObject: ActivityPubActor, type: ActorImageType) {
34 const iconsOrImages = type === ActorImageType.AVATAR
35 ? actorObject.icon
36 : actorObject.image
37
38 return normalizeIconOrImage(iconsOrImages)
39 .map(iconOrImage => {
40 const mimetypes = MIMETYPES.IMAGE
41
42 if (iconOrImage.type !== 'Image' || !isActivityPubUrlValid(iconOrImage.url)) return undefined
43
44 let extension: string
45
46 if (iconOrImage.mediaType) {
47 extension = mimetypes.MIMETYPE_EXT[iconOrImage.mediaType]
48 } else {
49 const tmp = getLowercaseExtension(iconOrImage.url)
50
51 if (mimetypes.EXT_MIMETYPE[tmp] !== undefined) extension = tmp
52 }
53
54 if (!extension) return undefined
55
56 return {
57 name: buildUUID() + extension,
58 fileUrl: iconOrImage.url,
59 height: iconOrImage.height,
60 width: iconOrImage.width,
61 type
62 }
63 })
64 .filter(i => !!i)
65}
66
67function getActorDisplayNameFromObject (actorObject: ActivityPubActor) {
68 return actorObject.name || actorObject.preferredUsername
69}
70
71export {
72 getActorAttributesFromObject,
73 getImagesInfoFromObject,
74 getActorDisplayNameFromObject
75}
76
77// ---------------------------------------------------------------------------
78
79function normalizeIconOrImage (icon: ActivityIconObject | ActivityIconObject[]): ActivityIconObject[] {
80 if (Array.isArray(icon)) return icon
81 if (icon) return [ icon ]
82
83 return []
84}
diff --git a/server/lib/activitypub/actors/shared/url-to-object.ts b/server/lib/activitypub/actors/shared/url-to-object.ts
deleted file mode 100644
index 73766bd50..000000000
--- a/server/lib/activitypub/actors/shared/url-to-object.ts
+++ /dev/null
@@ -1,56 +0,0 @@
1import { sanitizeAndCheckActorObject } from '@server/helpers/custom-validators/activitypub/actor'
2import { logger } from '@server/helpers/logger'
3import { ActivityPubActor, ActivityPubOrderedCollection } from '@shared/models'
4import { fetchAP } from '../../activity'
5import { checkUrlsSameHost } from '../../url'
6
7async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode: number, actorObject: ActivityPubActor }> {
8 logger.info('Fetching remote actor %s.', actorUrl)
9
10 const { body, statusCode } = await fetchAP<ActivityPubActor>(actorUrl)
11
12 if (sanitizeAndCheckActorObject(body) === false) {
13 logger.debug('Remote actor JSON is not valid.', { actorJSON: body })
14 return { actorObject: undefined, statusCode }
15 }
16
17 if (checkUrlsSameHost(body.id, actorUrl) !== true) {
18 logger.warn('Actor url %s has not the same host than its AP id %s', actorUrl, body.id)
19 return { actorObject: undefined, statusCode }
20 }
21
22 return {
23 statusCode,
24
25 actorObject: body
26 }
27}
28
29async function fetchActorFollowsCount (actorObject: ActivityPubActor) {
30 let followersCount = 0
31 let followingCount = 0
32
33 if (actorObject.followers) followersCount = await fetchActorTotalItems(actorObject.followers)
34 if (actorObject.following) followingCount = await fetchActorTotalItems(actorObject.following)
35
36 return { followersCount, followingCount }
37}
38
39// ---------------------------------------------------------------------------
40export {
41 fetchActorFollowsCount,
42 fetchRemoteActor
43}
44
45// ---------------------------------------------------------------------------
46
47async function fetchActorTotalItems (url: string) {
48 try {
49 const { body } = await fetchAP<ActivityPubOrderedCollection<unknown>>(url)
50
51 return body.totalItems || 0
52 } catch (err) {
53 logger.info('Cannot fetch remote actor count %s.', url, { err })
54 return 0
55 }
56}
diff --git a/server/lib/activitypub/actors/updater.ts b/server/lib/activitypub/actors/updater.ts
deleted file mode 100644
index 5a92e7a22..000000000
--- a/server/lib/activitypub/actors/updater.ts
+++ /dev/null
@@ -1,91 +0,0 @@
1import { resetSequelizeInstance, runInReadCommittedTransaction } from '@server/helpers/database-utils'
2import { logger } from '@server/helpers/logger'
3import { AccountModel } from '@server/models/account/account'
4import { VideoChannelModel } from '@server/models/video/video-channel'
5import { MAccount, MActor, MActorFull, MChannel } from '@server/types/models'
6import { ActivityPubActor, ActorImageType } from '@shared/models'
7import { getOrCreateAPOwner } from './get'
8import { updateActorImages } from './image'
9import { fetchActorFollowsCount } from './shared'
10import { getImagesInfoFromObject } from './shared/object-to-model-attributes'
11
12export class APActorUpdater {
13
14 private readonly accountOrChannel: MAccount | MChannel
15
16 constructor (
17 private readonly actorObject: ActivityPubActor,
18 private readonly actor: MActorFull
19 ) {
20 if (this.actorObject.type === 'Group') this.accountOrChannel = this.actor.VideoChannel
21 else this.accountOrChannel = this.actor.Account
22 }
23
24 async update () {
25 const avatarsInfo = getImagesInfoFromObject(this.actorObject, ActorImageType.AVATAR)
26 const bannersInfo = getImagesInfoFromObject(this.actorObject, ActorImageType.BANNER)
27
28 try {
29 await this.updateActorInstance(this.actor, this.actorObject)
30
31 this.accountOrChannel.name = this.actorObject.name || this.actorObject.preferredUsername
32 this.accountOrChannel.description = this.actorObject.summary
33
34 if (this.accountOrChannel instanceof VideoChannelModel) {
35 const owner = await getOrCreateAPOwner(this.actorObject, this.actorObject.url)
36 this.accountOrChannel.accountId = owner.Account.id
37 this.accountOrChannel.Account = owner.Account as AccountModel
38
39 this.accountOrChannel.support = this.actorObject.support
40 }
41
42 await runInReadCommittedTransaction(async t => {
43 await updateActorImages(this.actor, ActorImageType.BANNER, bannersInfo, t)
44 await updateActorImages(this.actor, ActorImageType.AVATAR, avatarsInfo, t)
45 })
46
47 await runInReadCommittedTransaction(async t => {
48 await this.actor.save({ transaction: t })
49 await this.accountOrChannel.save({ transaction: t })
50 })
51
52 logger.info('Remote account %s updated', this.actorObject.url)
53 } catch (err) {
54 if (this.actor !== undefined) {
55 await resetSequelizeInstance(this.actor)
56 }
57
58 if (this.accountOrChannel !== undefined) {
59 await resetSequelizeInstance(this.accountOrChannel)
60 }
61
62 // This is just a debug because we will retry the insert
63 logger.debug('Cannot update the remote account.', { err })
64 throw err
65 }
66 }
67
68 private async updateActorInstance (actorInstance: MActor, actorObject: ActivityPubActor) {
69 const { followersCount, followingCount } = await fetchActorFollowsCount(actorObject)
70
71 actorInstance.type = actorObject.type
72 actorInstance.preferredUsername = actorObject.preferredUsername
73 actorInstance.url = actorObject.id
74 actorInstance.publicKey = actorObject.publicKey.publicKeyPem
75 actorInstance.followersCount = followersCount
76 actorInstance.followingCount = followingCount
77 actorInstance.inboxUrl = actorObject.inbox
78 actorInstance.outboxUrl = actorObject.outbox
79 actorInstance.followersUrl = actorObject.followers
80 actorInstance.followingUrl = actorObject.following
81
82 if (actorObject.published) actorInstance.remoteCreatedAt = new Date(actorObject.published)
83
84 if (actorObject.endpoints?.sharedInbox) {
85 actorInstance.sharedInboxUrl = actorObject.endpoints.sharedInbox
86 }
87
88 // Force actor update
89 actorInstance.changed('updatedAt', true)
90 }
91}
diff --git a/server/lib/activitypub/actors/webfinger.ts b/server/lib/activitypub/actors/webfinger.ts
deleted file mode 100644
index b20a724da..000000000
--- a/server/lib/activitypub/actors/webfinger.ts
+++ /dev/null
@@ -1,67 +0,0 @@
1import WebFinger from 'webfinger.js'
2import { isProdInstance } from '@server/helpers/core-utils'
3import { isActivityPubUrlValid } from '@server/helpers/custom-validators/activitypub/misc'
4import { REQUEST_TIMEOUTS, WEBSERVER } from '@server/initializers/constants'
5import { ActorModel } from '@server/models/actor/actor'
6import { MActorFull } from '@server/types/models'
7import { WebFingerData } from '@shared/models'
8
9const webfinger = new WebFinger({
10 webfist_fallback: false,
11 tls_only: isProdInstance(),
12 uri_fallback: false,
13 request_timeout: REQUEST_TIMEOUTS.DEFAULT
14})
15
16async function loadActorUrlOrGetFromWebfinger (uriArg: string) {
17 // Handle strings like @toto@example.com
18 const uri = uriArg.startsWith('@') ? uriArg.slice(1) : uriArg
19
20 const [ name, host ] = uri.split('@')
21 let actor: MActorFull
22
23 if (!host || host === WEBSERVER.HOST) {
24 actor = await ActorModel.loadLocalByName(name)
25 } else {
26 actor = await ActorModel.loadByNameAndHost(name, host)
27 }
28
29 if (actor) return actor.url
30
31 return getUrlFromWebfinger(uri)
32}
33
34async function getUrlFromWebfinger (uri: string) {
35 const webfingerData: WebFingerData = await webfingerLookup(uri)
36 return getLinkOrThrow(webfingerData)
37}
38
39// ---------------------------------------------------------------------------
40
41export {
42 getUrlFromWebfinger,
43 loadActorUrlOrGetFromWebfinger
44}
45
46// ---------------------------------------------------------------------------
47
48function getLinkOrThrow (webfingerData: WebFingerData) {
49 if (Array.isArray(webfingerData.links) === false) throw new Error('WebFinger links is not an array.')
50
51 const selfLink = webfingerData.links.find(l => l.rel === 'self')
52 if (selfLink === undefined || isActivityPubUrlValid(selfLink.href) === false) {
53 throw new Error('Cannot find self link or href is not a valid URL.')
54 }
55
56 return selfLink.href
57}
58
59function webfingerLookup (nameWithHost: string) {
60 return new Promise<WebFingerData>((res, rej) => {
61 webfinger.lookup(nameWithHost, (err, p) => {
62 if (err) return rej(err)
63
64 return res(p.object)
65 })
66 })
67}
diff --git a/server/lib/activitypub/audience.ts b/server/lib/activitypub/audience.ts
deleted file mode 100644
index 6f5491387..000000000
--- a/server/lib/activitypub/audience.ts
+++ /dev/null
@@ -1,34 +0,0 @@
1import { ActivityAudience } from '../../../shared/models/activitypub'
2import { ACTIVITY_PUB } from '../../initializers/constants'
3import { MActorFollowersUrl } from '../../types/models'
4
5function getAudience (actorSender: MActorFollowersUrl, isPublic = true) {
6 return buildAudience([ actorSender.followersUrl ], isPublic)
7}
8
9function buildAudience (followerUrls: string[], isPublic = true) {
10 let to: string[] = []
11 let cc: string[] = []
12
13 if (isPublic) {
14 to = [ ACTIVITY_PUB.PUBLIC ]
15 cc = followerUrls
16 } else { // Unlisted
17 to = []
18 cc = []
19 }
20
21 return { to, cc }
22}
23
24function audiencify<T> (object: T, audience: ActivityAudience) {
25 return { ...audience, ...object }
26}
27
28// ---------------------------------------------------------------------------
29
30export {
31 buildAudience,
32 getAudience,
33 audiencify
34}
diff --git a/server/lib/activitypub/cache-file.ts b/server/lib/activitypub/cache-file.ts
deleted file mode 100644
index c3acd7112..000000000
--- a/server/lib/activitypub/cache-file.ts
+++ /dev/null
@@ -1,82 +0,0 @@
1import { Transaction } from 'sequelize'
2import { MActorId, MVideoRedundancy, MVideoWithAllFiles } from '@server/types/models'
3import { CacheFileObject, VideoStreamingPlaylistType } from '@shared/models'
4import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy'
5
6async function createOrUpdateCacheFile (cacheFileObject: CacheFileObject, video: MVideoWithAllFiles, byActor: MActorId, t: Transaction) {
7 const redundancyModel = await VideoRedundancyModel.loadByUrl(cacheFileObject.id, t)
8
9 if (redundancyModel) {
10 return updateCacheFile(cacheFileObject, redundancyModel, video, byActor, t)
11 }
12
13 return createCacheFile(cacheFileObject, video, byActor, t)
14}
15
16// ---------------------------------------------------------------------------
17
18export {
19 createOrUpdateCacheFile
20}
21
22// ---------------------------------------------------------------------------
23
24function createCacheFile (cacheFileObject: CacheFileObject, video: MVideoWithAllFiles, byActor: MActorId, t: Transaction) {
25 const attributes = cacheFileActivityObjectToDBAttributes(cacheFileObject, video, byActor)
26
27 return VideoRedundancyModel.create(attributes, { transaction: t })
28}
29
30function updateCacheFile (
31 cacheFileObject: CacheFileObject,
32 redundancyModel: MVideoRedundancy,
33 video: MVideoWithAllFiles,
34 byActor: MActorId,
35 t: Transaction
36) {
37 if (redundancyModel.actorId !== byActor.id) {
38 throw new Error('Cannot update redundancy ' + redundancyModel.url + ' of another actor.')
39 }
40
41 const attributes = cacheFileActivityObjectToDBAttributes(cacheFileObject, video, byActor)
42
43 redundancyModel.expiresOn = attributes.expiresOn
44 redundancyModel.fileUrl = attributes.fileUrl
45
46 return redundancyModel.save({ transaction: t })
47}
48
49function cacheFileActivityObjectToDBAttributes (cacheFileObject: CacheFileObject, video: MVideoWithAllFiles, byActor: MActorId) {
50
51 if (cacheFileObject.url.mediaType === 'application/x-mpegURL') {
52 const url = cacheFileObject.url
53
54 const playlist = video.VideoStreamingPlaylists.find(t => t.type === VideoStreamingPlaylistType.HLS)
55 if (!playlist) throw new Error('Cannot find HLS playlist of video ' + video.url)
56
57 return {
58 expiresOn: cacheFileObject.expires ? new Date(cacheFileObject.expires) : null,
59 url: cacheFileObject.id,
60 fileUrl: url.href,
61 strategy: null,
62 videoStreamingPlaylistId: playlist.id,
63 actorId: byActor.id
64 }
65 }
66
67 const url = cacheFileObject.url
68 const videoFile = video.VideoFiles.find(f => {
69 return f.resolution === url.height && f.fps === url.fps
70 })
71
72 if (!videoFile) throw new Error(`Cannot find video file ${url.height} ${url.fps} of video ${video.url}`)
73
74 return {
75 expiresOn: cacheFileObject.expires ? new Date(cacheFileObject.expires) : null,
76 url: cacheFileObject.id,
77 fileUrl: url.href,
78 strategy: null,
79 videoFileId: videoFile.id,
80 actorId: byActor.id
81 }
82}
diff --git a/server/lib/activitypub/collection.ts b/server/lib/activitypub/collection.ts
deleted file mode 100644
index a176cab51..000000000
--- a/server/lib/activitypub/collection.ts
+++ /dev/null
@@ -1,63 +0,0 @@
1import Bluebird from 'bluebird'
2import validator from 'validator'
3import { pageToStartAndCount } from '@server/helpers/core-utils'
4import { ACTIVITY_PUB } from '@server/initializers/constants'
5import { ResultList } from '@shared/models'
6import { forceNumber } from '@shared/core-utils'
7
8type ActivityPubCollectionPaginationHandler = (start: number, count: number) => Bluebird<ResultList<any>> | Promise<ResultList<any>>
9
10async function activityPubCollectionPagination (
11 baseUrl: string,
12 handler: ActivityPubCollectionPaginationHandler,
13 page?: any,
14 size = ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE
15) {
16 if (!page || !validator.isInt(page)) {
17 // We just display the first page URL, we only need the total items
18 const result = await handler(0, 1)
19
20 return {
21 id: baseUrl,
22 type: 'OrderedCollection',
23 totalItems: result.total,
24 first: result.data.length === 0
25 ? undefined
26 : baseUrl + '?page=1'
27 }
28 }
29
30 const { start, count } = pageToStartAndCount(page, size)
31 const result = await handler(start, count)
32
33 let next: string | undefined
34 let prev: string | undefined
35
36 // Assert page is a number
37 page = forceNumber(page)
38
39 // There are more results
40 if (result.total > page * size) {
41 next = baseUrl + '?page=' + (page + 1)
42 }
43
44 if (page > 1) {
45 prev = baseUrl + '?page=' + (page - 1)
46 }
47
48 return {
49 id: baseUrl + '?page=' + page,
50 type: 'OrderedCollectionPage',
51 prev,
52 next,
53 partOf: baseUrl,
54 orderedItems: result.data,
55 totalItems: result.total
56 }
57}
58
59// ---------------------------------------------------------------------------
60
61export {
62 activityPubCollectionPagination
63}
diff --git a/server/lib/activitypub/context.ts b/server/lib/activitypub/context.ts
deleted file mode 100644
index 87eb498a3..000000000
--- a/server/lib/activitypub/context.ts
+++ /dev/null
@@ -1,212 +0,0 @@
1import { ContextType } from '@shared/models'
2import { Hooks } from '../plugins/hooks'
3
4async function activityPubContextify <T> (data: T, type: ContextType) {
5 return { ...await getContextData(type), ...data }
6}
7
8// ---------------------------------------------------------------------------
9
10export {
11 getContextData,
12 activityPubContextify
13}
14
15// ---------------------------------------------------------------------------
16
17type ContextValue = { [ id: string ]: (string | { '@type': string, '@id': string }) }
18
19const contextStore: { [ id in ContextType ]: (string | { [ id: string ]: string })[] } = {
20 Video: buildContext({
21 Hashtag: 'as:Hashtag',
22 uuid: 'sc:identifier',
23 category: 'sc:category',
24 licence: 'sc:license',
25 subtitleLanguage: 'sc:subtitleLanguage',
26 sensitive: 'as:sensitive',
27 language: 'sc:inLanguage',
28 identifier: 'sc:identifier',
29
30 isLiveBroadcast: 'sc:isLiveBroadcast',
31 liveSaveReplay: {
32 '@type': 'sc:Boolean',
33 '@id': 'pt:liveSaveReplay'
34 },
35 permanentLive: {
36 '@type': 'sc:Boolean',
37 '@id': 'pt:permanentLive'
38 },
39 latencyMode: {
40 '@type': 'sc:Number',
41 '@id': 'pt:latencyMode'
42 },
43
44 Infohash: 'pt:Infohash',
45
46 tileWidth: {
47 '@type': 'sc:Number',
48 '@id': 'pt:tileWidth'
49 },
50 tileHeight: {
51 '@type': 'sc:Number',
52 '@id': 'pt:tileHeight'
53 },
54 tileDuration: {
55 '@type': 'sc:Number',
56 '@id': 'pt:tileDuration'
57 },
58
59 originallyPublishedAt: 'sc:datePublished',
60
61 uploadDate: 'sc:uploadDate',
62
63 views: {
64 '@type': 'sc:Number',
65 '@id': 'pt:views'
66 },
67 state: {
68 '@type': 'sc:Number',
69 '@id': 'pt:state'
70 },
71 size: {
72 '@type': 'sc:Number',
73 '@id': 'pt:size'
74 },
75 fps: {
76 '@type': 'sc:Number',
77 '@id': 'pt:fps'
78 },
79 commentsEnabled: {
80 '@type': 'sc:Boolean',
81 '@id': 'pt:commentsEnabled'
82 },
83 downloadEnabled: {
84 '@type': 'sc:Boolean',
85 '@id': 'pt:downloadEnabled'
86 },
87 waitTranscoding: {
88 '@type': 'sc:Boolean',
89 '@id': 'pt:waitTranscoding'
90 },
91 support: {
92 '@type': 'sc:Text',
93 '@id': 'pt:support'
94 },
95 likes: {
96 '@id': 'as:likes',
97 '@type': '@id'
98 },
99 dislikes: {
100 '@id': 'as:dislikes',
101 '@type': '@id'
102 },
103 shares: {
104 '@id': 'as:shares',
105 '@type': '@id'
106 },
107 comments: {
108 '@id': 'as:comments',
109 '@type': '@id'
110 }
111 }),
112
113 Playlist: buildContext({
114 Playlist: 'pt:Playlist',
115 PlaylistElement: 'pt:PlaylistElement',
116 position: {
117 '@type': 'sc:Number',
118 '@id': 'pt:position'
119 },
120 startTimestamp: {
121 '@type': 'sc:Number',
122 '@id': 'pt:startTimestamp'
123 },
124 stopTimestamp: {
125 '@type': 'sc:Number',
126 '@id': 'pt:stopTimestamp'
127 },
128 uuid: 'sc:identifier'
129 }),
130
131 CacheFile: buildContext({
132 expires: 'sc:expires',
133 CacheFile: 'pt:CacheFile'
134 }),
135
136 Flag: buildContext({
137 Hashtag: 'as:Hashtag'
138 }),
139
140 Actor: buildContext({
141 playlists: {
142 '@id': 'pt:playlists',
143 '@type': '@id'
144 },
145 support: {
146 '@type': 'sc:Text',
147 '@id': 'pt:support'
148 },
149
150 // TODO: remove in a few versions, introduced in 4.2
151 icons: 'as:icon'
152 }),
153
154 WatchAction: buildContext({
155 WatchAction: 'sc:WatchAction',
156 startTimestamp: {
157 '@type': 'sc:Number',
158 '@id': 'pt:startTimestamp'
159 },
160 stopTimestamp: {
161 '@type': 'sc:Number',
162 '@id': 'pt:stopTimestamp'
163 },
164 watchSection: {
165 '@type': 'sc:Number',
166 '@id': 'pt:stopTimestamp'
167 },
168 uuid: 'sc:identifier'
169 }),
170
171 Collection: buildContext(),
172 Follow: buildContext(),
173 Reject: buildContext(),
174 Accept: buildContext(),
175 View: buildContext(),
176 Announce: buildContext(),
177 Comment: buildContext(),
178 Delete: buildContext(),
179 Rate: buildContext()
180}
181
182async function getContextData (type: ContextType) {
183 const contextData = await Hooks.wrapObject(
184 contextStore[type],
185 'filter:activity-pub.activity.context.build.result'
186 )
187
188 return { '@context': contextData }
189}
190
191function buildContext (contextValue?: ContextValue) {
192 const baseContext = [
193 'https://www.w3.org/ns/activitystreams',
194 'https://w3id.org/security/v1',
195 {
196 RsaSignature2017: 'https://w3id.org/security#RsaSignature2017'
197 }
198 ]
199
200 if (!contextValue) return baseContext
201
202 return [
203 ...baseContext,
204
205 {
206 pt: 'https://joinpeertube.org/ns#',
207 sc: 'http://schema.org/',
208
209 ...contextValue
210 }
211 ]
212}
diff --git a/server/lib/activitypub/crawl.ts b/server/lib/activitypub/crawl.ts
deleted file mode 100644
index b8348e8cf..000000000
--- a/server/lib/activitypub/crawl.ts
+++ /dev/null
@@ -1,58 +0,0 @@
1import Bluebird from 'bluebird'
2import { URL } from 'url'
3import { retryTransactionWrapper } from '@server/helpers/database-utils'
4import { ActivityPubOrderedCollection } from '../../../shared/models/activitypub'
5import { logger } from '../../helpers/logger'
6import { ACTIVITY_PUB, WEBSERVER } from '../../initializers/constants'
7import { fetchAP } from './activity'
8
9type HandlerFunction<T> = (items: T[]) => (Promise<any> | Bluebird<any>)
10type CleanerFunction = (startedDate: Date) => Promise<any>
11
12async function crawlCollectionPage <T> (argUrl: string, handler: HandlerFunction<T>, cleaner?: CleanerFunction) {
13 let url = argUrl
14
15 logger.info('Crawling ActivityPub data on %s.', url)
16
17 const startDate = new Date()
18
19 const response = await fetchAP<ActivityPubOrderedCollection<T>>(url)
20 const firstBody = response.body
21
22 const limit = ACTIVITY_PUB.FETCH_PAGE_LIMIT
23 let i = 0
24 let nextLink = firstBody.first
25 while (nextLink && i < limit) {
26 let body: any
27
28 if (typeof nextLink === 'string') {
29 // Don't crawl ourselves
30 const remoteHost = new URL(nextLink).host
31 if (remoteHost === WEBSERVER.HOST) continue
32
33 url = nextLink
34
35 const res = await fetchAP<ActivityPubOrderedCollection<T>>(url)
36 body = res.body
37 } else {
38 // nextLink is already the object we want
39 body = nextLink
40 }
41
42 nextLink = body.next
43 i++
44
45 if (Array.isArray(body.orderedItems)) {
46 const items = body.orderedItems
47 logger.info('Processing %i ActivityPub items for %s.', items.length, url)
48
49 await handler(items)
50 }
51 }
52
53 if (cleaner) await retryTransactionWrapper(cleaner, startDate)
54}
55
56export {
57 crawlCollectionPage
58}
diff --git a/server/lib/activitypub/follow.ts b/server/lib/activitypub/follow.ts
deleted file mode 100644
index f6e2a48fd..000000000
--- a/server/lib/activitypub/follow.ts
+++ /dev/null
@@ -1,51 +0,0 @@
1import { Transaction } from 'sequelize'
2import { getServerActor } from '@server/models/application/application'
3import { logger } from '../../helpers/logger'
4import { CONFIG } from '../../initializers/config'
5import { SERVER_ACTOR_NAME } from '../../initializers/constants'
6import { ServerModel } from '../../models/server/server'
7import { MActorFollowActors } from '../../types/models'
8import { JobQueue } from '../job-queue'
9
10async function autoFollowBackIfNeeded (actorFollow: MActorFollowActors, transaction?: Transaction) {
11 if (!CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_BACK.ENABLED) return
12
13 const follower = actorFollow.ActorFollower
14
15 if (follower.type === 'Application' && follower.preferredUsername === SERVER_ACTOR_NAME) {
16 logger.info('Auto follow back %s.', follower.url)
17
18 const me = await getServerActor()
19
20 const server = await ServerModel.load(follower.serverId, transaction)
21 const host = server.host
22
23 const payload = {
24 host,
25 name: SERVER_ACTOR_NAME,
26 followerActorId: me.id,
27 isAutoFollow: true
28 }
29
30 JobQueue.Instance.createJobAsync({ type: 'activitypub-follow', payload })
31 }
32}
33
34// If we only have an host, use a default account handle
35function getRemoteNameAndHost (handleOrHost: string) {
36 let name = SERVER_ACTOR_NAME
37 let host = handleOrHost
38
39 const splitted = handleOrHost.split('@')
40 if (splitted.length === 2) {
41 name = splitted[0]
42 host = splitted[1]
43 }
44
45 return { name, host }
46}
47
48export {
49 autoFollowBackIfNeeded,
50 getRemoteNameAndHost
51}
diff --git a/server/lib/activitypub/inbox-manager.ts b/server/lib/activitypub/inbox-manager.ts
deleted file mode 100644
index 27778cc9d..000000000
--- a/server/lib/activitypub/inbox-manager.ts
+++ /dev/null
@@ -1,47 +0,0 @@
1import PQueue from 'p-queue'
2import { logger } from '@server/helpers/logger'
3import { SCHEDULER_INTERVALS_MS } from '@server/initializers/constants'
4import { MActorDefault, MActorSignature } from '@server/types/models'
5import { Activity } from '@shared/models'
6import { StatsManager } from '../stat-manager'
7import { processActivities } from './process'
8
9class InboxManager {
10
11 private static instance: InboxManager
12 private readonly inboxQueue: PQueue
13
14 private constructor () {
15 this.inboxQueue = new PQueue({ concurrency: 1 })
16
17 setInterval(() => {
18 StatsManager.Instance.updateInboxWaiting(this.getActivityPubMessagesWaiting())
19 }, SCHEDULER_INTERVALS_MS.UPDATE_INBOX_STATS)
20 }
21
22 addInboxMessage (param: {
23 activities: Activity[]
24 signatureActor?: MActorSignature
25 inboxActor?: MActorDefault
26 }) {
27 this.inboxQueue.add(() => {
28 const options = { signatureActor: param.signatureActor, inboxActor: param.inboxActor }
29
30 return processActivities(param.activities, options)
31 }).catch(err => logger.error('Error with inbox queue.', { err }))
32 }
33
34 getActivityPubMessagesWaiting () {
35 return this.inboxQueue.size + this.inboxQueue.pending
36 }
37
38 static get Instance () {
39 return this.instance || (this.instance = new this())
40 }
41}
42
43// ---------------------------------------------------------------------------
44
45export {
46 InboxManager
47}
diff --git a/server/lib/activitypub/local-video-viewer.ts b/server/lib/activitypub/local-video-viewer.ts
deleted file mode 100644
index bdd746791..000000000
--- a/server/lib/activitypub/local-video-viewer.ts
+++ /dev/null
@@ -1,44 +0,0 @@
1import { Transaction } from 'sequelize'
2import { LocalVideoViewerModel } from '@server/models/view/local-video-viewer'
3import { LocalVideoViewerWatchSectionModel } from '@server/models/view/local-video-viewer-watch-section'
4import { MVideo } from '@server/types/models'
5import { WatchActionObject } from '@shared/models'
6import { getDurationFromActivityStream } from './activity'
7
8async function createOrUpdateLocalVideoViewer (watchAction: WatchActionObject, video: MVideo, t: Transaction) {
9 const stats = await LocalVideoViewerModel.loadByUrl(watchAction.id)
10 if (stats) await stats.destroy({ transaction: t })
11
12 const localVideoViewer = await LocalVideoViewerModel.create({
13 url: watchAction.id,
14 uuid: watchAction.uuid,
15
16 watchTime: getDurationFromActivityStream(watchAction.duration),
17
18 startDate: new Date(watchAction.startTime),
19 endDate: new Date(watchAction.endTime),
20
21 country: watchAction.location
22 ? watchAction.location.addressCountry
23 : null,
24
25 videoId: video.id
26 }, { transaction: t })
27
28 await LocalVideoViewerWatchSectionModel.bulkCreateSections({
29 localVideoViewerId: localVideoViewer.id,
30
31 watchSections: watchAction.watchSections.map(s => ({
32 start: s.startTimestamp,
33 end: s.endTimestamp
34 })),
35
36 transaction: t
37 })
38}
39
40// ---------------------------------------------------------------------------
41
42export {
43 createOrUpdateLocalVideoViewer
44}
diff --git a/server/lib/activitypub/outbox.ts b/server/lib/activitypub/outbox.ts
deleted file mode 100644
index 5eef76871..000000000
--- a/server/lib/activitypub/outbox.ts
+++ /dev/null
@@ -1,24 +0,0 @@
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.createJobAsync({ 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
deleted file mode 100644
index b24299f29..000000000
--- a/server/lib/activitypub/playlists/create-update.ts
+++ /dev/null
@@ -1,157 +0,0 @@
1import { map } from 'bluebird'
2import { isArray } from '@server/helpers/custom-validators/misc'
3import { retryTransactionWrapper } from '@server/helpers/database-utils'
4import { logger, loggerTagsFactory } from '@server/helpers/logger'
5import { CRAWL_REQUEST_CONCURRENCY } from '@server/initializers/constants'
6import { sequelizeTypescript } from '@server/initializers/database'
7import { updateRemotePlaylistMiniatureFromUrl } from '@server/lib/thumbnail'
8import { VideoPlaylistModel } from '@server/models/video/video-playlist'
9import { VideoPlaylistElementModel } from '@server/models/video/video-playlist-element'
10import { FilteredModelAttributes } from '@server/types'
11import { MThumbnail, MVideoPlaylist, MVideoPlaylistFull, MVideoPlaylistVideosLength } from '@server/types/models'
12import { PlaylistObject } from '@shared/models'
13import { AttributesOnly } from '@shared/typescript-utils'
14import { getAPId } from '../activity'
15import { getOrCreateAPActor } from '../actors'
16import { crawlCollectionPage } from '../crawl'
17import { getOrCreateAPVideo } from '../videos'
18import {
19 fetchRemotePlaylistElement,
20 fetchRemoteVideoPlaylist,
21 playlistElementObjectToDBAttributes,
22 playlistObjectToDBAttributes
23} from './shared'
24
25const lTags = loggerTagsFactory('ap', 'video-playlist')
26
27async function createAccountPlaylists (playlistUrls: string[]) {
28 await map(playlistUrls, async playlistUrl => {
29 try {
30 const exists = await VideoPlaylistModel.doesPlaylistExist(playlistUrl)
31 if (exists === true) return
32
33 const { playlistObject } = await fetchRemoteVideoPlaylist(playlistUrl)
34
35 if (playlistObject === undefined) {
36 throw new Error(`Cannot refresh remote playlist ${playlistUrl}: invalid body.`)
37 }
38
39 return createOrUpdateVideoPlaylist(playlistObject)
40 } catch (err) {
41 logger.warn('Cannot add playlist element %s.', playlistUrl, { err, ...lTags(playlistUrl) })
42 }
43 }, { concurrency: CRAWL_REQUEST_CONCURRENCY })
44}
45
46async function createOrUpdateVideoPlaylist (playlistObject: PlaylistObject, to?: string[]) {
47 const playlistAttributes = playlistObjectToDBAttributes(playlistObject, to || playlistObject.to)
48
49 await setVideoChannel(playlistObject, playlistAttributes)
50
51 const [ upsertPlaylist ] = await VideoPlaylistModel.upsert<MVideoPlaylistVideosLength>(playlistAttributes, { returning: true })
52
53 const playlistElementUrls = await fetchElementUrls(playlistObject)
54
55 // Refetch playlist from DB since elements fetching could be long in time
56 const playlist = await VideoPlaylistModel.loadWithAccountAndChannel(upsertPlaylist.id, null)
57
58 await updatePlaylistThumbnail(playlistObject, playlist)
59
60 const elementsLength = await rebuildVideoPlaylistElements(playlistElementUrls, playlist)
61 playlist.setVideosLength(elementsLength)
62
63 return playlist
64}
65
66// ---------------------------------------------------------------------------
67
68export {
69 createAccountPlaylists,
70 createOrUpdateVideoPlaylist
71}
72
73// ---------------------------------------------------------------------------
74
75async function setVideoChannel (playlistObject: PlaylistObject, playlistAttributes: AttributesOnly<VideoPlaylistModel>) {
76 if (!isArray(playlistObject.attributedTo) || playlistObject.attributedTo.length !== 1) {
77 throw new Error('Not attributed to for playlist object ' + getAPId(playlistObject))
78 }
79
80 const actor = await getOrCreateAPActor(getAPId(playlistObject.attributedTo[0]), 'all')
81
82 if (!actor.VideoChannel) {
83 logger.warn('Playlist "attributedTo" %s is not a video channel.', playlistObject.id, { playlistObject, ...lTags(playlistObject.id) })
84 return
85 }
86
87 playlistAttributes.videoChannelId = actor.VideoChannel.id
88 playlistAttributes.ownerAccountId = actor.VideoChannel.Account.id
89}
90
91async function fetchElementUrls (playlistObject: PlaylistObject) {
92 let accItems: string[] = []
93 await crawlCollectionPage<string>(playlistObject.id, items => {
94 accItems = accItems.concat(items)
95
96 return Promise.resolve()
97 })
98
99 return accItems
100}
101
102async function updatePlaylistThumbnail (playlistObject: PlaylistObject, playlist: MVideoPlaylistFull) {
103 if (playlistObject.icon) {
104 let thumbnailModel: MThumbnail
105
106 try {
107 thumbnailModel = await updateRemotePlaylistMiniatureFromUrl({ downloadUrl: playlistObject.icon.url, playlist })
108 await playlist.setAndSaveThumbnail(thumbnailModel, undefined)
109 } catch (err) {
110 logger.warn('Cannot set thumbnail of %s.', playlistObject.id, { err, ...lTags(playlistObject.id, playlist.uuid, playlist.url) })
111
112 if (thumbnailModel) await thumbnailModel.removeThumbnail()
113 }
114
115 return
116 }
117
118 // Playlist does not have an icon, destroy existing one
119 if (playlist.hasThumbnail()) {
120 await playlist.Thumbnail.destroy()
121 playlist.Thumbnail = null
122 }
123}
124
125async function rebuildVideoPlaylistElements (elementUrls: string[], playlist: MVideoPlaylist) {
126 const elementsToCreate = await buildElementsDBAttributes(elementUrls, playlist)
127
128 await retryTransactionWrapper(() => sequelizeTypescript.transaction(async t => {
129 await VideoPlaylistElementModel.deleteAllOf(playlist.id, t)
130
131 for (const element of elementsToCreate) {
132 await VideoPlaylistElementModel.create(element, { transaction: t })
133 }
134 }))
135
136 logger.info('Rebuilt playlist %s with %s elements.', playlist.url, elementsToCreate.length, lTags(playlist.uuid, playlist.url))
137
138 return elementsToCreate.length
139}
140
141async function buildElementsDBAttributes (elementUrls: string[], playlist: MVideoPlaylist) {
142 const elementsToCreate: FilteredModelAttributes<VideoPlaylistElementModel>[] = []
143
144 await map(elementUrls, async elementUrl => {
145 try {
146 const { elementObject } = await fetchRemotePlaylistElement(elementUrl)
147
148 const { video } = await getOrCreateAPVideo({ videoObject: { id: elementObject.url }, fetchType: 'only-video' })
149
150 elementsToCreate.push(playlistElementObjectToDBAttributes(elementObject, playlist, video))
151 } catch (err) {
152 logger.warn('Cannot add playlist element %s.', elementUrl, { err, ...lTags(playlist.uuid, playlist.url) })
153 }
154 }, { concurrency: CRAWL_REQUEST_CONCURRENCY })
155
156 return elementsToCreate
157}
diff --git a/server/lib/activitypub/playlists/get.ts b/server/lib/activitypub/playlists/get.ts
deleted file mode 100644
index c34554d69..000000000
--- a/server/lib/activitypub/playlists/get.ts
+++ /dev/null
@@ -1,35 +0,0 @@
1import { VideoPlaylistModel } from '@server/models/video/video-playlist'
2import { MVideoPlaylistFullSummary } from '@server/types/models'
3import { APObjectId } from '@shared/models'
4import { getAPId } from '../activity'
5import { createOrUpdateVideoPlaylist } from './create-update'
6import { scheduleRefreshIfNeeded } from './refresh'
7import { fetchRemoteVideoPlaylist } from './shared'
8
9async function getOrCreateAPVideoPlaylist (playlistObjectArg: APObjectId): Promise<MVideoPlaylistFullSummary> {
10 const playlistUrl = getAPId(playlistObjectArg)
11
12 const playlistFromDatabase = await VideoPlaylistModel.loadByUrlWithAccountAndChannelSummary(playlistUrl)
13
14 if (playlistFromDatabase) {
15 scheduleRefreshIfNeeded(playlistFromDatabase)
16
17 return playlistFromDatabase
18 }
19
20 const { playlistObject } = await fetchRemoteVideoPlaylist(playlistUrl)
21 if (!playlistObject) throw new Error('Cannot fetch remote playlist with url: ' + playlistUrl)
22
23 // playlistUrl is just an alias/redirection, so process object id instead
24 if (playlistObject.id !== playlistUrl) return getOrCreateAPVideoPlaylist(playlistObject)
25
26 const playlistCreated = await createOrUpdateVideoPlaylist(playlistObject)
27
28 return playlistCreated
29}
30
31// ---------------------------------------------------------------------------
32
33export {
34 getOrCreateAPVideoPlaylist
35}
diff --git a/server/lib/activitypub/playlists/index.ts b/server/lib/activitypub/playlists/index.ts
deleted file mode 100644
index e2470a674..000000000
--- a/server/lib/activitypub/playlists/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
1export * from './get'
2export * from './create-update'
3export * from './refresh'
diff --git a/server/lib/activitypub/playlists/refresh.ts b/server/lib/activitypub/playlists/refresh.ts
deleted file mode 100644
index 33260ea02..000000000
--- a/server/lib/activitypub/playlists/refresh.ts
+++ /dev/null
@@ -1,53 +0,0 @@
1import { logger, loggerTagsFactory } from '@server/helpers/logger'
2import { PeerTubeRequestError } from '@server/helpers/requests'
3import { JobQueue } from '@server/lib/job-queue'
4import { MVideoPlaylist, MVideoPlaylistOwner } from '@server/types/models'
5import { HttpStatusCode } from '@shared/models'
6import { createOrUpdateVideoPlaylist } from './create-update'
7import { fetchRemoteVideoPlaylist } from './shared'
8
9function scheduleRefreshIfNeeded (playlist: MVideoPlaylist) {
10 if (!playlist.isOutdated()) return
11
12 JobQueue.Instance.createJobAsync({ type: 'activitypub-refresher', payload: { type: 'video-playlist', url: playlist.url } })
13}
14
15async function refreshVideoPlaylistIfNeeded (videoPlaylist: MVideoPlaylistOwner): Promise<MVideoPlaylistOwner> {
16 if (!videoPlaylist.isOutdated()) return videoPlaylist
17
18 const lTags = loggerTagsFactory('ap', 'video-playlist', 'refresh', videoPlaylist.uuid, videoPlaylist.url)
19
20 logger.info('Refreshing playlist %s.', videoPlaylist.url, lTags())
21
22 try {
23 const { playlistObject } = await fetchRemoteVideoPlaylist(videoPlaylist.url)
24
25 if (playlistObject === undefined) {
26 logger.warn('Cannot refresh remote playlist %s: invalid body.', videoPlaylist.url, lTags())
27
28 await videoPlaylist.setAsRefreshed()
29 return videoPlaylist
30 }
31
32 await createOrUpdateVideoPlaylist(playlistObject)
33
34 return videoPlaylist
35 } catch (err) {
36 if ((err as PeerTubeRequestError).statusCode === HttpStatusCode.NOT_FOUND_404) {
37 logger.info('Cannot refresh not existing playlist %s. Deleting it.', videoPlaylist.url, lTags())
38
39 await videoPlaylist.destroy()
40 return undefined
41 }
42
43 logger.warn('Cannot refresh video playlist %s.', videoPlaylist.url, { err, ...lTags() })
44
45 await videoPlaylist.setAsRefreshed()
46 return videoPlaylist
47 }
48}
49
50export {
51 scheduleRefreshIfNeeded,
52 refreshVideoPlaylistIfNeeded
53}
diff --git a/server/lib/activitypub/playlists/shared/index.ts b/server/lib/activitypub/playlists/shared/index.ts
deleted file mode 100644
index a217f2291..000000000
--- a/server/lib/activitypub/playlists/shared/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
1export * from './object-to-model-attributes'
2export * from './url-to-object'
diff --git a/server/lib/activitypub/playlists/shared/object-to-model-attributes.ts b/server/lib/activitypub/playlists/shared/object-to-model-attributes.ts
deleted file mode 100644
index 753b5e660..000000000
--- a/server/lib/activitypub/playlists/shared/object-to-model-attributes.ts
+++ /dev/null
@@ -1,40 +0,0 @@
1import { ACTIVITY_PUB } from '@server/initializers/constants'
2import { VideoPlaylistModel } from '@server/models/video/video-playlist'
3import { VideoPlaylistElementModel } from '@server/models/video/video-playlist-element'
4import { MVideoId, MVideoPlaylistId } from '@server/types/models'
5import { AttributesOnly } from '@shared/typescript-utils'
6import { PlaylistElementObject, PlaylistObject, VideoPlaylistPrivacy } from '@shared/models'
7
8function playlistObjectToDBAttributes (playlistObject: PlaylistObject, to: string[]) {
9 const privacy = to.includes(ACTIVITY_PUB.PUBLIC)
10 ? VideoPlaylistPrivacy.PUBLIC
11 : VideoPlaylistPrivacy.UNLISTED
12
13 return {
14 name: playlistObject.name,
15 description: playlistObject.content,
16 privacy,
17 url: playlistObject.id,
18 uuid: playlistObject.uuid,
19 ownerAccountId: null,
20 videoChannelId: null,
21 createdAt: new Date(playlistObject.published),
22 updatedAt: new Date(playlistObject.updated)
23 } as AttributesOnly<VideoPlaylistModel>
24}
25
26function playlistElementObjectToDBAttributes (elementObject: PlaylistElementObject, videoPlaylist: MVideoPlaylistId, video: MVideoId) {
27 return {
28 position: elementObject.position,
29 url: elementObject.id,
30 startTimestamp: elementObject.startTimestamp || null,
31 stopTimestamp: elementObject.stopTimestamp || null,
32 videoPlaylistId: videoPlaylist.id,
33 videoId: video.id
34 } as AttributesOnly<VideoPlaylistElementModel>
35}
36
37export {
38 playlistObjectToDBAttributes,
39 playlistElementObjectToDBAttributes
40}
diff --git a/server/lib/activitypub/playlists/shared/url-to-object.ts b/server/lib/activitypub/playlists/shared/url-to-object.ts
deleted file mode 100644
index fd9fe5558..000000000
--- a/server/lib/activitypub/playlists/shared/url-to-object.ts
+++ /dev/null
@@ -1,47 +0,0 @@
1import { isPlaylistElementObjectValid, isPlaylistObjectValid } from '@server/helpers/custom-validators/activitypub/playlist'
2import { isArray } from '@server/helpers/custom-validators/misc'
3import { logger, loggerTagsFactory } from '@server/helpers/logger'
4import { PlaylistElementObject, PlaylistObject } from '@shared/models'
5import { fetchAP } from '../../activity'
6import { checkUrlsSameHost } from '../../url'
7
8async function fetchRemoteVideoPlaylist (playlistUrl: string): Promise<{ statusCode: number, playlistObject: PlaylistObject }> {
9 const lTags = loggerTagsFactory('ap', 'video-playlist', playlistUrl)
10
11 logger.info('Fetching remote playlist %s.', playlistUrl, lTags())
12
13 const { body, statusCode } = await fetchAP<any>(playlistUrl)
14
15 if (isPlaylistObjectValid(body) === false || checkUrlsSameHost(body.id, playlistUrl) !== true) {
16 logger.debug('Remote video playlist JSON is not valid.', { body, ...lTags() })
17 return { statusCode, playlistObject: undefined }
18 }
19
20 if (!isArray(body.to)) {
21 logger.debug('Remote video playlist JSON does not have a valid audience.', { body, ...lTags() })
22 return { statusCode, playlistObject: undefined }
23 }
24
25 return { statusCode, playlistObject: body }
26}
27
28async function fetchRemotePlaylistElement (elementUrl: string): Promise<{ statusCode: number, elementObject: PlaylistElementObject }> {
29 const lTags = loggerTagsFactory('ap', 'video-playlist', 'element', elementUrl)
30
31 logger.debug('Fetching remote playlist element %s.', elementUrl, lTags())
32
33 const { body, statusCode } = await fetchAP<PlaylistElementObject>(elementUrl)
34
35 if (!isPlaylistElementObjectValid(body)) throw new Error(`Invalid body in fetch playlist element ${elementUrl}`)
36
37 if (checkUrlsSameHost(body.id, elementUrl) !== true) {
38 throw new Error(`Playlist element url ${elementUrl} host is different from the AP object id ${body.id}`)
39 }
40
41 return { statusCode, elementObject: body }
42}
43
44export {
45 fetchRemoteVideoPlaylist,
46 fetchRemotePlaylistElement
47}
diff --git a/server/lib/activitypub/process/index.ts b/server/lib/activitypub/process/index.ts
deleted file mode 100644
index 5466739c1..000000000
--- a/server/lib/activitypub/process/index.ts
+++ /dev/null
@@ -1 +0,0 @@
1export * from './process'
diff --git a/server/lib/activitypub/process/process-accept.ts b/server/lib/activitypub/process/process-accept.ts
deleted file mode 100644
index 077b01eda..000000000
--- a/server/lib/activitypub/process/process-accept.ts
+++ /dev/null
@@ -1,32 +0,0 @@
1import { ActivityAccept } from '../../../../shared/models/activitypub'
2import { ActorFollowModel } from '../../../models/actor/actor-follow'
3import { APProcessorOptions } from '../../../types/activitypub-processor.model'
4import { MActorDefault, MActorSignature } from '../../../types/models'
5import { addFetchOutboxJob } from '../outbox'
6
7async function processAcceptActivity (options: APProcessorOptions<ActivityAccept>) {
8 const { byActor: targetActor, inboxActor } = options
9 if (inboxActor === undefined) throw new Error('Need to accept on explicit inbox.')
10
11 return processAccept(inboxActor, targetActor)
12}
13
14// ---------------------------------------------------------------------------
15
16export {
17 processAcceptActivity
18}
19
20// ---------------------------------------------------------------------------
21
22async function processAccept (actor: MActorDefault, targetActor: MActorSignature) {
23 const follow = await ActorFollowModel.loadByActorAndTarget(actor.id, targetActor.id)
24 if (!follow) throw new Error('Cannot find associated follow.')
25
26 if (follow.state !== 'accepted') {
27 follow.state = 'accepted'
28 await follow.save()
29
30 await addFetchOutboxJob(targetActor)
31 }
32}
diff --git a/server/lib/activitypub/process/process-announce.ts b/server/lib/activitypub/process/process-announce.ts
deleted file mode 100644
index 9cc87ee27..000000000
--- a/server/lib/activitypub/process/process-announce.ts
+++ /dev/null
@@ -1,75 +0,0 @@
1import { getAPId } from '@server/lib/activitypub/activity'
2import { ActivityAnnounce } from '../../../../shared/models/activitypub'
3import { retryTransactionWrapper } from '../../../helpers/database-utils'
4import { logger } from '../../../helpers/logger'
5import { sequelizeTypescript } from '../../../initializers/database'
6import { VideoShareModel } from '../../../models/video/video-share'
7import { APProcessorOptions } from '../../../types/activitypub-processor.model'
8import { MActorSignature, MVideoAccountLightBlacklistAllFiles } from '../../../types/models'
9import { Notifier } from '../../notifier'
10import { forwardVideoRelatedActivity } from '../send/shared/send-utils'
11import { getOrCreateAPVideo } from '../videos'
12
13async function processAnnounceActivity (options: APProcessorOptions<ActivityAnnounce>) {
14 const { activity, byActor: actorAnnouncer } = options
15 // Only notify if it is not from a fetcher job
16 const notify = options.fromFetch !== true
17
18 // Announces on accounts are not supported
19 if (actorAnnouncer.type !== 'Application' && actorAnnouncer.type !== 'Group') return
20
21 return retryTransactionWrapper(processVideoShare, actorAnnouncer, activity, notify)
22}
23
24// ---------------------------------------------------------------------------
25
26export {
27 processAnnounceActivity
28}
29
30// ---------------------------------------------------------------------------
31
32async function processVideoShare (actorAnnouncer: MActorSignature, activity: ActivityAnnounce, notify: boolean) {
33 const objectUri = getAPId(activity.object)
34
35 let video: MVideoAccountLightBlacklistAllFiles
36 let videoCreated: boolean
37
38 try {
39 const result = await getOrCreateAPVideo({ videoObject: objectUri })
40 video = result.video
41 videoCreated = result.created
42 } catch (err) {
43 logger.debug('Cannot process share of %s. Maybe this is not a video object, so just skipping.', objectUri, { err })
44 return
45 }
46
47 await sequelizeTypescript.transaction(async t => {
48 // Add share entry
49
50 const share = {
51 actorId: actorAnnouncer.id,
52 videoId: video.id,
53 url: activity.id
54 }
55
56 const [ , created ] = await VideoShareModel.findOrCreate({
57 where: {
58 url: activity.id
59 },
60 defaults: share,
61 transaction: t
62 })
63
64 if (video.isOwned() && created === true) {
65 // Don't resend the activity to the sender
66 const exceptions = [ actorAnnouncer ]
67
68 await forwardVideoRelatedActivity(activity, t, exceptions, video)
69 }
70
71 return undefined
72 })
73
74 if (videoCreated && notify) Notifier.Instance.notifyOnNewVideoIfNeeded(video)
75}
diff --git a/server/lib/activitypub/process/process-create.ts b/server/lib/activitypub/process/process-create.ts
deleted file mode 100644
index 5f980de65..000000000
--- a/server/lib/activitypub/process/process-create.ts
+++ /dev/null
@@ -1,170 +0,0 @@
1import { isBlockedByServerOrAccount } from '@server/lib/blocklist'
2import { isRedundancyAccepted } from '@server/lib/redundancy'
3import { VideoModel } from '@server/models/video/video'
4import {
5 AbuseObject,
6 ActivityCreate,
7 ActivityCreateObject,
8 ActivityObject,
9 CacheFileObject,
10 PlaylistObject,
11 VideoCommentObject,
12 VideoObject,
13 WatchActionObject
14} from '@shared/models'
15import { retryTransactionWrapper } from '../../../helpers/database-utils'
16import { logger } from '../../../helpers/logger'
17import { sequelizeTypescript } from '../../../initializers/database'
18import { APProcessorOptions } from '../../../types/activitypub-processor.model'
19import { MActorSignature, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../../types/models'
20import { Notifier } from '../../notifier'
21import { fetchAPObjectIfNeeded } from '../activity'
22import { createOrUpdateCacheFile } from '../cache-file'
23import { createOrUpdateLocalVideoViewer } from '../local-video-viewer'
24import { createOrUpdateVideoPlaylist } from '../playlists'
25import { forwardVideoRelatedActivity } from '../send/shared/send-utils'
26import { resolveThread } from '../video-comments'
27import { getOrCreateAPVideo } from '../videos'
28
29async function processCreateActivity (options: APProcessorOptions<ActivityCreate<ActivityCreateObject>>) {
30 const { activity, byActor } = options
31
32 // Only notify if it is not from a fetcher job
33 const notify = options.fromFetch !== true
34 const activityObject = await fetchAPObjectIfNeeded<Exclude<ActivityObject, AbuseObject>>(activity.object)
35 const activityType = activityObject.type
36
37 if (activityType === 'Video') {
38 return processCreateVideo(activityObject, notify)
39 }
40
41 if (activityType === 'Note') {
42 // Comments will be fetched from videos
43 if (options.fromFetch) return
44
45 return retryTransactionWrapper(processCreateVideoComment, activity, activityObject, byActor, notify)
46 }
47
48 if (activityType === 'WatchAction') {
49 return retryTransactionWrapper(processCreateWatchAction, activityObject)
50 }
51
52 if (activityType === 'CacheFile') {
53 return retryTransactionWrapper(processCreateCacheFile, activity, activityObject, byActor)
54 }
55
56 if (activityType === 'Playlist') {
57 return retryTransactionWrapper(processCreatePlaylist, activity, activityObject, byActor)
58 }
59
60 logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id })
61 return Promise.resolve(undefined)
62}
63
64// ---------------------------------------------------------------------------
65
66export {
67 processCreateActivity
68}
69
70// ---------------------------------------------------------------------------
71
72async function processCreateVideo (videoToCreateData: VideoObject, notify: boolean) {
73 const syncParam = { rates: false, shares: false, comments: false, refreshVideo: false }
74 const { video, created } = await getOrCreateAPVideo({ videoObject: videoToCreateData, syncParam })
75
76 if (created && notify) Notifier.Instance.notifyOnNewVideoIfNeeded(video)
77
78 return video
79}
80
81async function processCreateCacheFile (
82 activity: ActivityCreate<CacheFileObject | string>,
83 cacheFile: CacheFileObject,
84 byActor: MActorSignature
85) {
86 if (await isRedundancyAccepted(activity, byActor) !== true) return
87
88 const { video } = await getOrCreateAPVideo({ videoObject: cacheFile.object })
89
90 await sequelizeTypescript.transaction(async t => {
91 return createOrUpdateCacheFile(cacheFile, video, byActor, t)
92 })
93
94 if (video.isOwned()) {
95 // Don't resend the activity to the sender
96 const exceptions = [ byActor ]
97 await forwardVideoRelatedActivity(activity, undefined, exceptions, video)
98 }
99}
100
101async function processCreateWatchAction (watchAction: WatchActionObject) {
102 if (watchAction.actionStatus !== 'CompletedActionStatus') return
103
104 const video = await VideoModel.loadByUrl(watchAction.object)
105 if (video.remote) return
106
107 await sequelizeTypescript.transaction(async t => {
108 return createOrUpdateLocalVideoViewer(watchAction, video, t)
109 })
110}
111
112async function processCreateVideoComment (
113 activity: ActivityCreate<VideoCommentObject | string>,
114 commentObject: VideoCommentObject,
115 byActor: MActorSignature,
116 notify: boolean
117) {
118 const byAccount = byActor.Account
119
120 if (!byAccount) throw new Error('Cannot create video comment with the non account actor ' + byActor.url)
121
122 let video: MVideoAccountLightBlacklistAllFiles
123 let created: boolean
124 let comment: MCommentOwnerVideo
125
126 try {
127 const resolveThreadResult = await resolveThread({ url: commentObject.id, isVideo: false })
128 if (!resolveThreadResult) return // Comment not accepted
129
130 video = resolveThreadResult.video
131 created = resolveThreadResult.commentCreated
132 comment = resolveThreadResult.comment
133 } catch (err) {
134 logger.debug(
135 'Cannot process video comment because we could not resolve thread %s. Maybe it was not a video thread, so skip it.',
136 commentObject.inReplyTo,
137 { err }
138 )
139 return
140 }
141
142 // Try to not forward unwanted comments on our videos
143 if (video.isOwned()) {
144 if (await isBlockedByServerOrAccount(comment.Account, video.VideoChannel.Account)) {
145 logger.info('Skip comment forward from blocked account or server %s.', comment.Account.Actor.url)
146 return
147 }
148
149 if (created === true) {
150 // Don't resend the activity to the sender
151 const exceptions = [ byActor ]
152
153 await forwardVideoRelatedActivity(activity, undefined, exceptions, video)
154 }
155 }
156
157 if (created && notify) Notifier.Instance.notifyOnNewComment(comment)
158}
159
160async function processCreatePlaylist (
161 activity: ActivityCreate<PlaylistObject | string>,
162 playlistObject: PlaylistObject,
163 byActor: MActorSignature
164) {
165 const byAccount = byActor.Account
166
167 if (!byAccount) throw new Error('Cannot create video playlist with the non account actor ' + byActor.url)
168
169 await createOrUpdateVideoPlaylist(playlistObject, activity.to)
170}
diff --git a/server/lib/activitypub/process/process-delete.ts b/server/lib/activitypub/process/process-delete.ts
deleted file mode 100644
index ac0e7e235..000000000
--- a/server/lib/activitypub/process/process-delete.ts
+++ /dev/null
@@ -1,153 +0,0 @@
1import { ActivityDelete } from '../../../../shared/models/activitypub'
2import { retryTransactionWrapper } from '../../../helpers/database-utils'
3import { logger } from '../../../helpers/logger'
4import { sequelizeTypescript } from '../../../initializers/database'
5import { ActorModel } from '../../../models/actor/actor'
6import { VideoModel } from '../../../models/video/video'
7import { VideoCommentModel } from '../../../models/video/video-comment'
8import { VideoPlaylistModel } from '../../../models/video/video-playlist'
9import { APProcessorOptions } from '../../../types/activitypub-processor.model'
10import {
11 MAccountActor,
12 MActor,
13 MActorFull,
14 MActorSignature,
15 MChannelAccountActor,
16 MChannelActor,
17 MCommentOwnerVideo
18} from '../../../types/models'
19import { forwardVideoRelatedActivity } from '../send/shared/send-utils'
20
21async function processDeleteActivity (options: APProcessorOptions<ActivityDelete>) {
22 const { activity, byActor } = options
23
24 const objectUrl = typeof activity.object === 'string' ? activity.object : activity.object.id
25
26 if (activity.actor === objectUrl) {
27 // We need more attributes (all the account and channel)
28 const byActorFull = await ActorModel.loadByUrlAndPopulateAccountAndChannel(byActor.url)
29
30 if (byActorFull.type === 'Person') {
31 if (!byActorFull.Account) throw new Error('Actor ' + byActorFull.url + ' is a person but we cannot find it in database.')
32
33 const accountToDelete = byActorFull.Account as MAccountActor
34 accountToDelete.Actor = byActorFull
35
36 return retryTransactionWrapper(processDeleteAccount, accountToDelete)
37 } else if (byActorFull.type === 'Group') {
38 if (!byActorFull.VideoChannel) throw new Error('Actor ' + byActorFull.url + ' is a group but we cannot find it in database.')
39
40 const channelToDelete = byActorFull.VideoChannel as MChannelAccountActor & { Actor: MActorFull }
41 channelToDelete.Actor = byActorFull
42 return retryTransactionWrapper(processDeleteVideoChannel, channelToDelete)
43 }
44 }
45
46 {
47 const videoCommentInstance = await VideoCommentModel.loadByUrlAndPopulateAccountAndVideo(objectUrl)
48 if (videoCommentInstance) {
49 return retryTransactionWrapper(processDeleteVideoComment, byActor, videoCommentInstance, activity)
50 }
51 }
52
53 {
54 const videoInstance = await VideoModel.loadByUrlAndPopulateAccount(objectUrl)
55 if (videoInstance) {
56 if (videoInstance.isOwned()) throw new Error(`Remote instance cannot delete owned video ${videoInstance.url}.`)
57
58 return retryTransactionWrapper(processDeleteVideo, byActor, videoInstance)
59 }
60 }
61
62 {
63 const videoPlaylist = await VideoPlaylistModel.loadByUrlAndPopulateAccount(objectUrl)
64 if (videoPlaylist) {
65 if (videoPlaylist.isOwned()) throw new Error(`Remote instance cannot delete owned playlist ${videoPlaylist.url}.`)
66
67 return retryTransactionWrapper(processDeleteVideoPlaylist, byActor, videoPlaylist)
68 }
69 }
70
71 return undefined
72}
73
74// ---------------------------------------------------------------------------
75
76export {
77 processDeleteActivity
78}
79
80// ---------------------------------------------------------------------------
81
82async function processDeleteVideo (actor: MActor, videoToDelete: VideoModel) {
83 logger.debug('Removing remote video "%s".', videoToDelete.uuid)
84
85 await sequelizeTypescript.transaction(async t => {
86 if (videoToDelete.VideoChannel.Account.Actor.id !== actor.id) {
87 throw new Error('Account ' + actor.url + ' does not own video channel ' + videoToDelete.VideoChannel.Actor.url)
88 }
89
90 await videoToDelete.destroy({ transaction: t })
91 })
92
93 logger.info('Remote video with uuid %s removed.', videoToDelete.uuid)
94}
95
96async function processDeleteVideoPlaylist (actor: MActor, playlistToDelete: VideoPlaylistModel) {
97 logger.debug('Removing remote video playlist "%s".', playlistToDelete.uuid)
98
99 await sequelizeTypescript.transaction(async t => {
100 if (playlistToDelete.OwnerAccount.Actor.id !== actor.id) {
101 throw new Error('Account ' + actor.url + ' does not own video playlist ' + playlistToDelete.url)
102 }
103
104 await playlistToDelete.destroy({ transaction: t })
105 })
106
107 logger.info('Remote video playlist with uuid %s removed.', playlistToDelete.uuid)
108}
109
110async function processDeleteAccount (accountToRemove: MAccountActor) {
111 logger.debug('Removing remote account "%s".', accountToRemove.Actor.url)
112
113 await sequelizeTypescript.transaction(async t => {
114 await accountToRemove.destroy({ transaction: t })
115 })
116
117 logger.info('Remote account %s removed.', accountToRemove.Actor.url)
118}
119
120async function processDeleteVideoChannel (videoChannelToRemove: MChannelActor) {
121 logger.debug('Removing remote video channel "%s".', videoChannelToRemove.Actor.url)
122
123 await sequelizeTypescript.transaction(async t => {
124 await videoChannelToRemove.destroy({ transaction: t })
125 })
126
127 logger.info('Remote video channel %s removed.', videoChannelToRemove.Actor.url)
128}
129
130function processDeleteVideoComment (byActor: MActorSignature, videoComment: MCommentOwnerVideo, activity: ActivityDelete) {
131 // Already deleted
132 if (videoComment.isDeleted()) return Promise.resolve()
133
134 logger.debug('Removing remote video comment "%s".', videoComment.url)
135
136 return sequelizeTypescript.transaction(async t => {
137 if (byActor.Account.id !== videoComment.Account.id && byActor.Account.id !== videoComment.Video.VideoChannel.accountId) {
138 throw new Error(`Account ${byActor.url} does not own video comment ${videoComment.url} or video ${videoComment.Video.url}`)
139 }
140
141 videoComment.markAsDeleted()
142
143 await videoComment.save({ transaction: t })
144
145 if (videoComment.Video.isOwned()) {
146 // Don't resend the activity to the sender
147 const exceptions = [ byActor ]
148 await forwardVideoRelatedActivity(activity, t, exceptions, videoComment.Video)
149 }
150
151 logger.info('Remote video comment %s removed.', videoComment.url)
152 })
153}
diff --git a/server/lib/activitypub/process/process-dislike.ts b/server/lib/activitypub/process/process-dislike.ts
deleted file mode 100644
index 4e270f917..000000000
--- a/server/lib/activitypub/process/process-dislike.ts
+++ /dev/null
@@ -1,58 +0,0 @@
1import { VideoModel } from '@server/models/video/video'
2import { ActivityDislike } from '@shared/models'
3import { retryTransactionWrapper } from '../../../helpers/database-utils'
4import { sequelizeTypescript } from '../../../initializers/database'
5import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
6import { APProcessorOptions } from '../../../types/activitypub-processor.model'
7import { MActorSignature } from '../../../types/models'
8import { federateVideoIfNeeded, getOrCreateAPVideo } from '../videos'
9
10async function processDislikeActivity (options: APProcessorOptions<ActivityDislike>) {
11 const { activity, byActor } = options
12 return retryTransactionWrapper(processDislike, activity, byActor)
13}
14
15// ---------------------------------------------------------------------------
16
17export {
18 processDislikeActivity
19}
20
21// ---------------------------------------------------------------------------
22
23async function processDislike (activity: ActivityDislike, byActor: MActorSignature) {
24 const dislikeObject = activity.object
25 const byAccount = byActor.Account
26
27 if (!byAccount) throw new Error('Cannot create dislike with the non account actor ' + byActor.url)
28
29 const { video: onlyVideo } = await getOrCreateAPVideo({ videoObject: dislikeObject, fetchType: 'only-video' })
30
31 // We don't care about dislikes of remote videos
32 if (!onlyVideo.isOwned()) return
33
34 return sequelizeTypescript.transaction(async t => {
35 const video = await VideoModel.loadFull(onlyVideo.id, t)
36
37 const existingRate = await AccountVideoRateModel.loadByAccountAndVideoOrUrl(byAccount.id, video.id, activity.id, t)
38 if (existingRate && existingRate.type === 'dislike') return
39
40 await video.increment('dislikes', { transaction: t })
41 video.dislikes++
42
43 if (existingRate && existingRate.type === 'like') {
44 await video.decrement('likes', { transaction: t })
45 video.likes--
46 }
47
48 const rate = existingRate || new AccountVideoRateModel()
49 rate.type = 'dislike'
50 rate.videoId = video.id
51 rate.accountId = byAccount.id
52 rate.url = activity.id
53
54 await rate.save({ transaction: t })
55
56 await federateVideoIfNeeded(video, false, t)
57 })
58}
diff --git a/server/lib/activitypub/process/process-flag.ts b/server/lib/activitypub/process/process-flag.ts
deleted file mode 100644
index bea285670..000000000
--- a/server/lib/activitypub/process/process-flag.ts
+++ /dev/null
@@ -1,103 +0,0 @@
1import { createAccountAbuse, createVideoAbuse, createVideoCommentAbuse } from '@server/lib/moderation'
2import { AccountModel } from '@server/models/account/account'
3import { VideoModel } from '@server/models/video/video'
4import { VideoCommentModel } from '@server/models/video/video-comment'
5import { abusePredefinedReasonsMap } from '@shared/core-utils/abuse'
6import { AbuseState, ActivityFlag } from '@shared/models'
7import { retryTransactionWrapper } from '../../../helpers/database-utils'
8import { logger } from '../../../helpers/logger'
9import { sequelizeTypescript } from '../../../initializers/database'
10import { getAPId } from '../../../lib/activitypub/activity'
11import { APProcessorOptions } from '../../../types/activitypub-processor.model'
12import { MAccountDefault, MActorSignature, MCommentOwnerVideo } from '../../../types/models'
13
14async function processFlagActivity (options: APProcessorOptions<ActivityFlag>) {
15 const { activity, byActor } = options
16
17 return retryTransactionWrapper(processCreateAbuse, activity, byActor)
18}
19
20// ---------------------------------------------------------------------------
21
22export {
23 processFlagActivity
24}
25
26// ---------------------------------------------------------------------------
27
28async function processCreateAbuse (flag: ActivityFlag, byActor: MActorSignature) {
29 const account = byActor.Account
30 if (!account) throw new Error('Cannot create abuse with the non account actor ' + byActor.url)
31
32 const reporterAccount = await AccountModel.load(account.id)
33
34 const objects = Array.isArray(flag.object) ? flag.object : [ flag.object ]
35
36 const tags = Array.isArray(flag.tag) ? flag.tag : []
37 const predefinedReasons = tags.map(tag => abusePredefinedReasonsMap[tag.name])
38 .filter(v => !isNaN(v))
39
40 const startAt = flag.startAt
41 const endAt = flag.endAt
42
43 for (const object of objects) {
44 try {
45 const uri = getAPId(object)
46
47 logger.debug('Reporting remote abuse for object %s.', uri)
48
49 await sequelizeTypescript.transaction(async t => {
50 const video = await VideoModel.loadByUrlAndPopulateAccount(uri, t)
51 let videoComment: MCommentOwnerVideo
52 let flaggedAccount: MAccountDefault
53
54 if (!video) videoComment = await VideoCommentModel.loadByUrlAndPopulateAccountAndVideo(uri, t)
55 if (!videoComment) flaggedAccount = await AccountModel.loadByUrl(uri, t)
56
57 if (!video && !videoComment && !flaggedAccount) {
58 logger.warn('Cannot flag unknown entity %s.', object)
59 return
60 }
61
62 const baseAbuse = {
63 reporterAccountId: reporterAccount.id,
64 reason: flag.content,
65 state: AbuseState.PENDING,
66 predefinedReasons
67 }
68
69 if (video) {
70 return createVideoAbuse({
71 baseAbuse,
72 startAt,
73 endAt,
74 reporterAccount,
75 transaction: t,
76 videoInstance: video,
77 skipNotification: false
78 })
79 }
80
81 if (videoComment) {
82 return createVideoCommentAbuse({
83 baseAbuse,
84 reporterAccount,
85 transaction: t,
86 commentInstance: videoComment,
87 skipNotification: false
88 })
89 }
90
91 return await createAccountAbuse({
92 baseAbuse,
93 reporterAccount,
94 transaction: t,
95 accountInstance: flaggedAccount,
96 skipNotification: false
97 })
98 })
99 } catch (err) {
100 logger.debug('Cannot process report of %s', getAPId(object), { err })
101 }
102 }
103}
diff --git a/server/lib/activitypub/process/process-follow.ts b/server/lib/activitypub/process/process-follow.ts
deleted file mode 100644
index 7def753d5..000000000
--- a/server/lib/activitypub/process/process-follow.ts
+++ /dev/null
@@ -1,156 +0,0 @@
1import { Transaction } from 'sequelize/types'
2import { isBlockedByServerOrAccount } from '@server/lib/blocklist'
3import { AccountModel } from '@server/models/account/account'
4import { getServerActor } from '@server/models/application/application'
5import { ActivityFollow } from '../../../../shared/models/activitypub'
6import { retryTransactionWrapper } from '../../../helpers/database-utils'
7import { logger } from '../../../helpers/logger'
8import { CONFIG } from '../../../initializers/config'
9import { sequelizeTypescript } from '../../../initializers/database'
10import { getAPId } from '../../../lib/activitypub/activity'
11import { ActorModel } from '../../../models/actor/actor'
12import { ActorFollowModel } from '../../../models/actor/actor-follow'
13import { APProcessorOptions } from '../../../types/activitypub-processor.model'
14import { MActorFollow, MActorFull, MActorId, MActorSignature } from '../../../types/models'
15import { Notifier } from '../../notifier'
16import { autoFollowBackIfNeeded } from '../follow'
17import { sendAccept, sendReject } from '../send'
18
19async function processFollowActivity (options: APProcessorOptions<ActivityFollow>) {
20 const { activity, byActor } = options
21
22 const activityId = activity.id
23 const objectId = getAPId(activity.object)
24
25 return retryTransactionWrapper(processFollow, byActor, activityId, objectId)
26}
27
28// ---------------------------------------------------------------------------
29
30export {
31 processFollowActivity
32}
33
34// ---------------------------------------------------------------------------
35
36async function processFollow (byActor: MActorSignature, activityId: string, targetActorURL: string) {
37 const { actorFollow, created, targetActor } = await sequelizeTypescript.transaction(async t => {
38 const targetActor = await ActorModel.loadByUrlAndPopulateAccountAndChannel(targetActorURL, t)
39
40 if (!targetActor) throw new Error('Unknown actor')
41 if (targetActor.isOwned() === false) throw new Error('This is not a local actor.')
42
43 if (await rejectIfInstanceFollowDisabled(byActor, activityId, targetActor)) return { actorFollow: undefined }
44 if (await rejectIfMuted(byActor, activityId, targetActor)) return { actorFollow: undefined }
45
46 const [ actorFollow, created ] = await ActorFollowModel.findOrCreateCustom({
47 byActor,
48 targetActor,
49 activityId,
50 state: await isFollowingInstance(targetActor) && CONFIG.FOLLOWERS.INSTANCE.MANUAL_APPROVAL
51 ? 'pending'
52 : 'accepted',
53 transaction: t
54 })
55
56 if (rejectIfAlreadyRejected(actorFollow, byActor, activityId, targetActor)) return { actorFollow: undefined }
57
58 await acceptIfNeeded(actorFollow, targetActor, t)
59
60 await fixFollowURLIfNeeded(actorFollow, activityId, t)
61
62 actorFollow.ActorFollower = byActor
63 actorFollow.ActorFollowing = targetActor
64
65 // Target sends to actor he accepted the follow request
66 if (actorFollow.state === 'accepted') {
67 sendAccept(actorFollow)
68
69 await autoFollowBackIfNeeded(actorFollow, t)
70 }
71
72 return { actorFollow, created, targetActor }
73 })
74
75 // Rejected
76 if (!actorFollow) return
77
78 if (created) {
79 const follower = await ActorModel.loadFull(byActor.id)
80 const actorFollowFull = Object.assign(actorFollow, { ActorFollowing: targetActor, ActorFollower: follower })
81
82 if (await isFollowingInstance(targetActor)) {
83 Notifier.Instance.notifyOfNewInstanceFollow(actorFollowFull)
84 } else {
85 Notifier.Instance.notifyOfNewUserFollow(actorFollowFull)
86 }
87 }
88
89 logger.info('Actor %s is followed by actor %s.', targetActorURL, byActor.url)
90}
91
92async function rejectIfInstanceFollowDisabled (byActor: MActorSignature, activityId: string, targetActor: MActorFull) {
93 if (await isFollowingInstance(targetActor) && CONFIG.FOLLOWERS.INSTANCE.ENABLED === false) {
94 logger.info('Rejecting %s because instance followers are disabled.', targetActor.url)
95
96 sendReject(activityId, byActor, targetActor)
97
98 return true
99 }
100
101 return false
102}
103
104async function rejectIfMuted (byActor: MActorSignature, activityId: string, targetActor: MActorFull) {
105 const followerAccount = await AccountModel.load(byActor.Account.id)
106 const followingAccountId = targetActor.Account
107
108 if (followerAccount && await isBlockedByServerOrAccount(followerAccount, followingAccountId)) {
109 logger.info('Rejecting %s because follower is muted.', byActor.url)
110
111 sendReject(activityId, byActor, targetActor)
112
113 return true
114 }
115
116 return false
117}
118
119function rejectIfAlreadyRejected (actorFollow: MActorFollow, byActor: MActorSignature, activityId: string, targetActor: MActorFull) {
120 // Already rejected
121 if (actorFollow.state === 'rejected') {
122 logger.info('Rejecting %s because follow is already rejected.', byActor.url)
123
124 sendReject(activityId, byActor, targetActor)
125
126 return true
127 }
128
129 return false
130}
131
132async function acceptIfNeeded (actorFollow: MActorFollow, targetActor: MActorFull, transaction: Transaction) {
133 // Set the follow as accepted if the remote actor follows a channel or account
134 // Or if the instance automatically accepts followers
135 if (actorFollow.state === 'accepted') return
136 if (!await isFollowingInstance(targetActor)) return
137 if (CONFIG.FOLLOWERS.INSTANCE.MANUAL_APPROVAL === true && await isFollowingInstance(targetActor)) return
138
139 actorFollow.state = 'accepted'
140
141 await actorFollow.save({ transaction })
142}
143
144async function fixFollowURLIfNeeded (actorFollow: MActorFollow, activityId: string, transaction: Transaction) {
145 // Before PeerTube V3 we did not save the follow ID. Try to fix these old follows
146 if (!actorFollow.url) {
147 actorFollow.url = activityId
148 await actorFollow.save({ transaction })
149 }
150}
151
152async function isFollowingInstance (targetActor: MActorId) {
153 const serverActor = await getServerActor()
154
155 return targetActor.id === serverActor.id
156}
diff --git a/server/lib/activitypub/process/process-like.ts b/server/lib/activitypub/process/process-like.ts
deleted file mode 100644
index 580a05bcd..000000000
--- a/server/lib/activitypub/process/process-like.ts
+++ /dev/null
@@ -1,60 +0,0 @@
1import { VideoModel } from '@server/models/video/video'
2import { ActivityLike } from '../../../../shared/models/activitypub'
3import { retryTransactionWrapper } from '../../../helpers/database-utils'
4import { sequelizeTypescript } from '../../../initializers/database'
5import { getAPId } from '../../../lib/activitypub/activity'
6import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
7import { APProcessorOptions } from '../../../types/activitypub-processor.model'
8import { MActorSignature } from '../../../types/models'
9import { federateVideoIfNeeded, getOrCreateAPVideo } from '../videos'
10
11async function processLikeActivity (options: APProcessorOptions<ActivityLike>) {
12 const { activity, byActor } = options
13
14 return retryTransactionWrapper(processLikeVideo, byActor, activity)
15}
16
17// ---------------------------------------------------------------------------
18
19export {
20 processLikeActivity
21}
22
23// ---------------------------------------------------------------------------
24
25async function processLikeVideo (byActor: MActorSignature, activity: ActivityLike) {
26 const videoUrl = getAPId(activity.object)
27
28 const byAccount = byActor.Account
29 if (!byAccount) throw new Error('Cannot create like with the non account actor ' + byActor.url)
30
31 const { video: onlyVideo } = await getOrCreateAPVideo({ videoObject: videoUrl, fetchType: 'only-video' })
32
33 // We don't care about likes of remote videos
34 if (!onlyVideo.isOwned()) return
35
36 return sequelizeTypescript.transaction(async t => {
37 const video = await VideoModel.loadFull(onlyVideo.id, t)
38
39 const existingRate = await AccountVideoRateModel.loadByAccountAndVideoOrUrl(byAccount.id, video.id, activity.id, t)
40 if (existingRate && existingRate.type === 'like') return
41
42 if (existingRate && existingRate.type === 'dislike') {
43 await video.decrement('dislikes', { transaction: t })
44 video.dislikes--
45 }
46
47 await video.increment('likes', { transaction: t })
48 video.likes++
49
50 const rate = existingRate || new AccountVideoRateModel()
51 rate.type = 'like'
52 rate.videoId = video.id
53 rate.accountId = byAccount.id
54 rate.url = activity.id
55
56 await rate.save({ transaction: t })
57
58 await federateVideoIfNeeded(video, false, t)
59 })
60}
diff --git a/server/lib/activitypub/process/process-reject.ts b/server/lib/activitypub/process/process-reject.ts
deleted file mode 100644
index db7ff24d8..000000000
--- a/server/lib/activitypub/process/process-reject.ts
+++ /dev/null
@@ -1,33 +0,0 @@
1import { ActivityReject } from '../../../../shared/models/activitypub/activity'
2import { sequelizeTypescript } from '../../../initializers/database'
3import { ActorFollowModel } from '../../../models/actor/actor-follow'
4import { APProcessorOptions } from '../../../types/activitypub-processor.model'
5import { MActor } from '../../../types/models'
6
7async function processRejectActivity (options: APProcessorOptions<ActivityReject>) {
8 const { byActor: targetActor, inboxActor } = options
9 if (inboxActor === undefined) throw new Error('Need to reject on explicit inbox.')
10
11 return processReject(inboxActor, targetActor)
12}
13
14// ---------------------------------------------------------------------------
15
16export {
17 processRejectActivity
18}
19
20// ---------------------------------------------------------------------------
21
22async function processReject (follower: MActor, targetActor: MActor) {
23 return sequelizeTypescript.transaction(async t => {
24 const actorFollow = await ActorFollowModel.loadByActorAndTarget(follower.id, targetActor.id, t)
25
26 if (!actorFollow) throw new Error(`'Unknown actor follow ${follower.id} -> ${targetActor.id}.`)
27
28 actorFollow.state = 'rejected'
29 await actorFollow.save({ transaction: t })
30
31 return undefined
32 })
33}
diff --git a/server/lib/activitypub/process/process-undo.ts b/server/lib/activitypub/process/process-undo.ts
deleted file mode 100644
index a9d8199de..000000000
--- a/server/lib/activitypub/process/process-undo.ts
+++ /dev/null
@@ -1,183 +0,0 @@
1import { VideoModel } from '@server/models/video/video'
2import {
3 ActivityAnnounce,
4 ActivityCreate,
5 ActivityDislike,
6 ActivityFollow,
7 ActivityLike,
8 ActivityUndo,
9 ActivityUndoObject,
10 CacheFileObject
11} from '../../../../shared/models/activitypub'
12import { retryTransactionWrapper } from '../../../helpers/database-utils'
13import { logger } from '../../../helpers/logger'
14import { sequelizeTypescript } from '../../../initializers/database'
15import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
16import { ActorModel } from '../../../models/actor/actor'
17import { ActorFollowModel } from '../../../models/actor/actor-follow'
18import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy'
19import { VideoShareModel } from '../../../models/video/video-share'
20import { APProcessorOptions } from '../../../types/activitypub-processor.model'
21import { MActorSignature } from '../../../types/models'
22import { fetchAPObjectIfNeeded } from '../activity'
23import { forwardVideoRelatedActivity } from '../send/shared/send-utils'
24import { federateVideoIfNeeded, getOrCreateAPVideo } from '../videos'
25
26async function processUndoActivity (options: APProcessorOptions<ActivityUndo<ActivityUndoObject>>) {
27 const { activity, byActor } = options
28 const activityToUndo = activity.object
29
30 if (activityToUndo.type === 'Like') {
31 return retryTransactionWrapper(processUndoLike, byActor, activity)
32 }
33
34 if (activityToUndo.type === 'Create') {
35 const objectToUndo = await fetchAPObjectIfNeeded<CacheFileObject>(activityToUndo.object)
36
37 if (objectToUndo.type === 'CacheFile') {
38 return retryTransactionWrapper(processUndoCacheFile, byActor, activity, objectToUndo)
39 }
40 }
41
42 if (activityToUndo.type === 'Dislike') {
43 return retryTransactionWrapper(processUndoDislike, byActor, activity)
44 }
45
46 if (activityToUndo.type === 'Follow') {
47 return retryTransactionWrapper(processUndoFollow, byActor, activityToUndo)
48 }
49
50 if (activityToUndo.type === 'Announce') {
51 return retryTransactionWrapper(processUndoAnnounce, byActor, activityToUndo)
52 }
53
54 logger.warn('Unknown activity object type %s -> %s when undo activity.', activityToUndo.type, { activity: activity.id })
55
56 return undefined
57}
58
59// ---------------------------------------------------------------------------
60
61export {
62 processUndoActivity
63}
64
65// ---------------------------------------------------------------------------
66
67async function processUndoLike (byActor: MActorSignature, activity: ActivityUndo<ActivityLike>) {
68 const likeActivity = activity.object
69
70 const { video: onlyVideo } = await getOrCreateAPVideo({ videoObject: likeActivity.object })
71 // We don't care about likes of remote videos
72 if (!onlyVideo.isOwned()) return
73
74 return sequelizeTypescript.transaction(async t => {
75 if (!byActor.Account) throw new Error('Unknown account ' + byActor.url)
76
77 const video = await VideoModel.loadFull(onlyVideo.id, t)
78 const rate = await AccountVideoRateModel.loadByAccountAndVideoOrUrl(byActor.Account.id, video.id, likeActivity.id, t)
79 if (!rate || rate.type !== 'like') {
80 logger.warn('Unknown like by account %d for video %d.', byActor.Account.id, video.id)
81 return
82 }
83
84 await rate.destroy({ transaction: t })
85 await video.decrement('likes', { transaction: t })
86
87 video.likes--
88 await federateVideoIfNeeded(video, false, t)
89 })
90}
91
92async function processUndoDislike (byActor: MActorSignature, activity: ActivityUndo<ActivityDislike>) {
93 const dislikeActivity = activity.object
94
95 const { video: onlyVideo } = await getOrCreateAPVideo({ videoObject: dislikeActivity.object })
96 // We don't care about likes of remote videos
97 if (!onlyVideo.isOwned()) return
98
99 return sequelizeTypescript.transaction(async t => {
100 if (!byActor.Account) throw new Error('Unknown account ' + byActor.url)
101
102 const video = await VideoModel.loadFull(onlyVideo.id, t)
103 const rate = await AccountVideoRateModel.loadByAccountAndVideoOrUrl(byActor.Account.id, video.id, dislikeActivity.id, t)
104 if (!rate || rate.type !== 'dislike') {
105 logger.warn(`Unknown dislike by account %d for video %d.`, byActor.Account.id, video.id)
106 return
107 }
108
109 await rate.destroy({ transaction: t })
110 await video.decrement('dislikes', { transaction: t })
111 video.dislikes--
112
113 await federateVideoIfNeeded(video, false, t)
114 })
115}
116
117// ---------------------------------------------------------------------------
118
119async function processUndoCacheFile (
120 byActor: MActorSignature,
121 activity: ActivityUndo<ActivityCreate<CacheFileObject>>,
122 cacheFileObject: CacheFileObject
123) {
124 const { video } = await getOrCreateAPVideo({ videoObject: cacheFileObject.object })
125
126 return sequelizeTypescript.transaction(async t => {
127 const cacheFile = await VideoRedundancyModel.loadByUrl(cacheFileObject.id, t)
128 if (!cacheFile) {
129 logger.debug('Cannot undo unknown video cache %s.', cacheFileObject.id)
130 return
131 }
132
133 if (cacheFile.actorId !== byActor.id) throw new Error('Cannot delete redundancy ' + cacheFile.url + ' of another actor.')
134
135 await cacheFile.destroy({ transaction: t })
136
137 if (video.isOwned()) {
138 // Don't resend the activity to the sender
139 const exceptions = [ byActor ]
140
141 await forwardVideoRelatedActivity(activity, t, exceptions, video)
142 }
143 })
144}
145
146function processUndoAnnounce (byActor: MActorSignature, announceActivity: ActivityAnnounce) {
147 return sequelizeTypescript.transaction(async t => {
148 const share = await VideoShareModel.loadByUrl(announceActivity.id, t)
149 if (!share) {
150 logger.warn('Unknown video share %d', announceActivity.id)
151 return
152 }
153
154 if (share.actorId !== byActor.id) throw new Error(`${share.url} is not shared by ${byActor.url}.`)
155
156 await share.destroy({ transaction: t })
157
158 if (share.Video.isOwned()) {
159 // Don't resend the activity to the sender
160 const exceptions = [ byActor ]
161
162 await forwardVideoRelatedActivity(announceActivity, t, exceptions, share.Video)
163 }
164 })
165}
166
167// ---------------------------------------------------------------------------
168
169function processUndoFollow (follower: MActorSignature, followActivity: ActivityFollow) {
170 return sequelizeTypescript.transaction(async t => {
171 const following = await ActorModel.loadByUrlAndPopulateAccountAndChannel(followActivity.object, t)
172 const actorFollow = await ActorFollowModel.loadByActorAndTarget(follower.id, following.id, t)
173
174 if (!actorFollow) {
175 logger.warn('Unknown actor follow %d -> %d.', follower.id, following.id)
176 return
177 }
178
179 await actorFollow.destroy({ transaction: t })
180
181 return undefined
182 })
183}
diff --git a/server/lib/activitypub/process/process-update.ts b/server/lib/activitypub/process/process-update.ts
deleted file mode 100644
index 304ed9de6..000000000
--- a/server/lib/activitypub/process/process-update.ts
+++ /dev/null
@@ -1,119 +0,0 @@
1import { isRedundancyAccepted } from '@server/lib/redundancy'
2import { ActivityUpdate, ActivityUpdateObject, CacheFileObject, VideoObject } from '../../../../shared/models/activitypub'
3import { ActivityPubActor } from '../../../../shared/models/activitypub/activitypub-actor'
4import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object'
5import { isCacheFileObjectValid } from '../../../helpers/custom-validators/activitypub/cache-file'
6import { sanitizeAndCheckVideoTorrentObject } from '../../../helpers/custom-validators/activitypub/videos'
7import { retryTransactionWrapper } from '../../../helpers/database-utils'
8import { logger } from '../../../helpers/logger'
9import { sequelizeTypescript } from '../../../initializers/database'
10import { ActorModel } from '../../../models/actor/actor'
11import { APProcessorOptions } from '../../../types/activitypub-processor.model'
12import { MActorFull, MActorSignature } from '../../../types/models'
13import { fetchAPObjectIfNeeded } from '../activity'
14import { APActorUpdater } from '../actors/updater'
15import { createOrUpdateCacheFile } from '../cache-file'
16import { createOrUpdateVideoPlaylist } from '../playlists'
17import { forwardVideoRelatedActivity } from '../send/shared/send-utils'
18import { APVideoUpdater, getOrCreateAPVideo } from '../videos'
19
20async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate<ActivityUpdateObject>>) {
21 const { activity, byActor } = options
22
23 const object = await fetchAPObjectIfNeeded(activity.object)
24 const objectType = object.type
25
26 if (objectType === 'Video') {
27 return retryTransactionWrapper(processUpdateVideo, activity)
28 }
29
30 if (objectType === 'Person' || objectType === 'Application' || objectType === 'Group') {
31 // We need more attributes
32 const byActorFull = await ActorModel.loadByUrlAndPopulateAccountAndChannel(byActor.url)
33 return retryTransactionWrapper(processUpdateActor, byActorFull, object)
34 }
35
36 if (objectType === 'CacheFile') {
37 // We need more attributes
38 const byActorFull = await ActorModel.loadByUrlAndPopulateAccountAndChannel(byActor.url)
39 return retryTransactionWrapper(processUpdateCacheFile, byActorFull, activity, object)
40 }
41
42 if (objectType === 'Playlist') {
43 return retryTransactionWrapper(processUpdatePlaylist, byActor, activity, object)
44 }
45
46 return undefined
47}
48
49// ---------------------------------------------------------------------------
50
51export {
52 processUpdateActivity
53}
54
55// ---------------------------------------------------------------------------
56
57async function processUpdateVideo (activity: ActivityUpdate<VideoObject | string>) {
58 const videoObject = activity.object as VideoObject
59
60 if (sanitizeAndCheckVideoTorrentObject(videoObject) === false) {
61 logger.debug('Video sent by update is not valid.', { videoObject })
62 return undefined
63 }
64
65 const { video, created } = await getOrCreateAPVideo({
66 videoObject: videoObject.id,
67 allowRefresh: false,
68 fetchType: 'all'
69 })
70 // We did not have this video, it has been created so no need to update
71 if (created) return
72
73 const updater = new APVideoUpdater(videoObject, video)
74 return updater.update(activity.to)
75}
76
77async function processUpdateCacheFile (
78 byActor: MActorSignature,
79 activity: ActivityUpdate<CacheFileObject | string>,
80 cacheFileObject: CacheFileObject
81) {
82 if (await isRedundancyAccepted(activity, byActor) !== true) return
83
84 if (!isCacheFileObjectValid(cacheFileObject)) {
85 logger.debug('Cache file object sent by update is not valid.', { cacheFileObject })
86 return undefined
87 }
88
89 const { video } = await getOrCreateAPVideo({ videoObject: cacheFileObject.object })
90
91 await sequelizeTypescript.transaction(async t => {
92 await createOrUpdateCacheFile(cacheFileObject, video, byActor, t)
93 })
94
95 if (video.isOwned()) {
96 // Don't resend the activity to the sender
97 const exceptions = [ byActor ]
98
99 await forwardVideoRelatedActivity(activity, undefined, exceptions, video)
100 }
101}
102
103async function processUpdateActor (actor: MActorFull, actorObject: ActivityPubActor) {
104 logger.debug('Updating remote account "%s".', actorObject.url)
105
106 const updater = new APActorUpdater(actorObject, actor)
107 return updater.update()
108}
109
110async function processUpdatePlaylist (
111 byActor: MActorSignature,
112 activity: ActivityUpdate<PlaylistObject | string>,
113 playlistObject: PlaylistObject
114) {
115 const byAccount = byActor.Account
116 if (!byAccount) throw new Error('Cannot update video playlist with the non account actor ' + byActor.url)
117
118 await createOrUpdateVideoPlaylist(playlistObject, activity.to)
119}
diff --git a/server/lib/activitypub/process/process-view.ts b/server/lib/activitypub/process/process-view.ts
deleted file mode 100644
index e49506d82..000000000
--- a/server/lib/activitypub/process/process-view.ts
+++ /dev/null
@@ -1,42 +0,0 @@
1import { VideoViewsManager } from '@server/lib/views/video-views-manager'
2import { ActivityView } from '../../../../shared/models/activitypub'
3import { APProcessorOptions } from '../../../types/activitypub-processor.model'
4import { MActorSignature } from '../../../types/models'
5import { forwardVideoRelatedActivity } from '../send/shared/send-utils'
6import { getOrCreateAPVideo } from '../videos'
7
8async function processViewActivity (options: APProcessorOptions<ActivityView>) {
9 const { activity, byActor } = options
10
11 return processCreateView(activity, byActor)
12}
13
14// ---------------------------------------------------------------------------
15
16export {
17 processViewActivity
18}
19
20// ---------------------------------------------------------------------------
21
22async function processCreateView (activity: ActivityView, byActor: MActorSignature) {
23 const videoObject = activity.object
24
25 const { video } = await getOrCreateAPVideo({
26 videoObject,
27 fetchType: 'only-video',
28 allowRefresh: false
29 })
30
31 const viewerExpires = activity.expires
32 ? new Date(activity.expires)
33 : undefined
34
35 await VideoViewsManager.Instance.processRemoteView({ video, viewerId: activity.id, viewerExpires })
36
37 if (video.isOwned()) {
38 // Forward the view but don't resend the activity to the sender
39 const exceptions = [ byActor ]
40 await forwardVideoRelatedActivity(activity, undefined, exceptions, video)
41 }
42}
diff --git a/server/lib/activitypub/process/process.ts b/server/lib/activitypub/process/process.ts
deleted file mode 100644
index 2bc3dce03..000000000
--- a/server/lib/activitypub/process/process.ts
+++ /dev/null
@@ -1,92 +0,0 @@
1import { StatsManager } from '@server/lib/stat-manager'
2import { Activity, ActivityType } from '../../../../shared/models/activitypub'
3import { logger } from '../../../helpers/logger'
4import { APProcessorOptions } from '../../../types/activitypub-processor.model'
5import { MActorDefault, MActorSignature } from '../../../types/models'
6import { getAPId } from '../activity'
7import { getOrCreateAPActor } from '../actors'
8import { checkUrlsSameHost } from '../url'
9import { processAcceptActivity } from './process-accept'
10import { processAnnounceActivity } from './process-announce'
11import { processCreateActivity } from './process-create'
12import { processDeleteActivity } from './process-delete'
13import { processDislikeActivity } from './process-dislike'
14import { processFlagActivity } from './process-flag'
15import { processFollowActivity } from './process-follow'
16import { processLikeActivity } from './process-like'
17import { processRejectActivity } from './process-reject'
18import { processUndoActivity } from './process-undo'
19import { processUpdateActivity } from './process-update'
20import { processViewActivity } from './process-view'
21
22const processActivity: { [ P in ActivityType ]: (options: APProcessorOptions<Activity>) => Promise<any> } = {
23 Create: processCreateActivity,
24 Update: processUpdateActivity,
25 Delete: processDeleteActivity,
26 Follow: processFollowActivity,
27 Accept: processAcceptActivity,
28 Reject: processRejectActivity,
29 Announce: processAnnounceActivity,
30 Undo: processUndoActivity,
31 Like: processLikeActivity,
32 Dislike: processDislikeActivity,
33 Flag: processFlagActivity,
34 View: processViewActivity
35}
36
37async function processActivities (
38 activities: Activity[],
39 options: {
40 signatureActor?: MActorSignature
41 inboxActor?: MActorDefault
42 outboxUrl?: string
43 fromFetch?: boolean
44 } = {}
45) {
46 const { outboxUrl, signatureActor, inboxActor, fromFetch = false } = options
47
48 const actorsCache: { [ url: string ]: MActorSignature } = {}
49
50 for (const activity of activities) {
51 if (!signatureActor && [ 'Create', 'Announce', 'Like' ].includes(activity.type) === false) {
52 logger.error('Cannot process activity %s (type: %s) without the actor signature.', activity.id, activity.type)
53 continue
54 }
55
56 const actorUrl = getAPId(activity.actor)
57
58 // When we fetch remote data, we don't have signature
59 if (signatureActor && actorUrl !== signatureActor.url) {
60 logger.warn('Signature mismatch between %s and %s, skipping.', actorUrl, signatureActor.url)
61 continue
62 }
63
64 if (outboxUrl && checkUrlsSameHost(outboxUrl, actorUrl) !== true) {
65 logger.warn('Host mismatch between outbox URL %s and actor URL %s, skipping.', outboxUrl, actorUrl)
66 continue
67 }
68
69 const byActor = signatureActor || actorsCache[actorUrl] || await getOrCreateAPActor(actorUrl)
70 actorsCache[actorUrl] = byActor
71
72 const activityProcessor = processActivity[activity.type]
73 if (activityProcessor === undefined) {
74 logger.warn('Unknown activity type %s.', activity.type, { activityId: activity.id })
75 continue
76 }
77
78 try {
79 await activityProcessor({ activity, byActor, inboxActor, fromFetch })
80
81 StatsManager.Instance.addInboxProcessedSuccess(activity.type)
82 } catch (err) {
83 logger.warn('Cannot process activity %s.', activity.type, { err })
84
85 StatsManager.Instance.addInboxProcessedError(activity.type)
86 }
87 }
88}
89
90export {
91 processActivities
92}
diff --git a/server/lib/activitypub/send/http.ts b/server/lib/activitypub/send/http.ts
deleted file mode 100644
index b461aa55d..000000000
--- a/server/lib/activitypub/send/http.ts
+++ /dev/null
@@ -1,73 +0,0 @@
1import { buildDigest, signJsonLDObject } from '@server/helpers/peertube-crypto'
2import { ACTIVITY_PUB, HTTP_SIGNATURE } from '@server/initializers/constants'
3import { ActorModel } from '@server/models/actor/actor'
4import { getServerActor } from '@server/models/application/application'
5import { MActor } from '@server/types/models'
6import { ContextType } from '@shared/models/activitypub/context'
7import { activityPubContextify } from '../context'
8
9type Payload <T> = { body: T, contextType: ContextType, signatureActorId?: number }
10
11async function computeBody <T> (
12 payload: Payload<T>
13): Promise<T | T & { type: 'RsaSignature2017', creator: string, created: string }> {
14 let body = payload.body
15
16 if (payload.signatureActorId) {
17 const actorSignature = await ActorModel.load(payload.signatureActorId)
18 if (!actorSignature) throw new Error('Unknown signature actor id.')
19
20 body = await signAndContextify(actorSignature, payload.body, payload.contextType)
21 }
22
23 return body
24}
25
26async function buildSignedRequestOptions (options: {
27 signatureActorId?: number
28 hasPayload: boolean
29}) {
30 let actor: MActor | null
31
32 if (options.signatureActorId) {
33 actor = await ActorModel.load(options.signatureActorId)
34 if (!actor) throw new Error('Unknown signature actor id.')
35 } else {
36 // We need to sign the request, so use the server
37 actor = await getServerActor()
38 }
39
40 const keyId = actor.url
41 return {
42 algorithm: HTTP_SIGNATURE.ALGORITHM,
43 authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME,
44 keyId,
45 key: actor.privateKey,
46 headers: options.hasPayload
47 ? HTTP_SIGNATURE.HEADERS_TO_SIGN_WITH_PAYLOAD
48 : HTTP_SIGNATURE.HEADERS_TO_SIGN_WITHOUT_PAYLOAD
49 }
50}
51
52function buildGlobalHeaders (body: any) {
53 return {
54 'digest': buildDigest(body),
55 'content-type': 'application/activity+json',
56 'accept': ACTIVITY_PUB.ACCEPT_HEADER
57 }
58}
59
60async function signAndContextify <T> (byActor: MActor, data: T, contextType: ContextType | null) {
61 const activity = contextType
62 ? await activityPubContextify(data, contextType)
63 : data
64
65 return signJsonLDObject(byActor, activity)
66}
67
68export {
69 buildGlobalHeaders,
70 computeBody,
71 buildSignedRequestOptions,
72 signAndContextify
73}
diff --git a/server/lib/activitypub/send/index.ts b/server/lib/activitypub/send/index.ts
deleted file mode 100644
index 852ea2e74..000000000
--- a/server/lib/activitypub/send/index.ts
+++ /dev/null
@@ -1,10 +0,0 @@
1export * from './http'
2export * from './send-accept'
3export * from './send-announce'
4export * from './send-create'
5export * from './send-delete'
6export * from './send-follow'
7export * from './send-like'
8export * from './send-reject'
9export * from './send-undo'
10export * from './send-update'
diff --git a/server/lib/activitypub/send/send-accept.ts b/server/lib/activitypub/send/send-accept.ts
deleted file mode 100644
index 4c9bcbb0b..000000000
--- a/server/lib/activitypub/send/send-accept.ts
+++ /dev/null
@@ -1,47 +0,0 @@
1import { ActivityAccept, ActivityFollow } from '@shared/models'
2import { logger } from '../../../helpers/logger'
3import { MActor, MActorFollowActors } from '../../../types/models'
4import { getLocalActorFollowAcceptActivityPubUrl } from '../url'
5import { buildFollowActivity } from './send-follow'
6import { unicastTo } from './shared/send-utils'
7
8function sendAccept (actorFollow: MActorFollowActors) {
9 const follower = actorFollow.ActorFollower
10 const me = actorFollow.ActorFollowing
11
12 if (!follower.serverId) { // This should never happen
13 logger.warn('Do not sending accept to local follower.')
14 return
15 }
16
17 logger.info('Creating job to accept follower %s.', follower.url)
18
19 const followData = buildFollowActivity(actorFollow.url, follower, me)
20
21 const url = getLocalActorFollowAcceptActivityPubUrl(actorFollow)
22 const data = buildAcceptActivity(url, me, followData)
23
24 return unicastTo({
25 data,
26 byActor: me,
27 toActorUrl: follower.inboxUrl,
28 contextType: 'Accept'
29 })
30}
31
32// ---------------------------------------------------------------------------
33
34export {
35 sendAccept
36}
37
38// ---------------------------------------------------------------------------
39
40function buildAcceptActivity (url: string, byActor: MActor, followActivityData: ActivityFollow): ActivityAccept {
41 return {
42 type: 'Accept',
43 id: url,
44 actor: byActor.url,
45 object: followActivityData
46 }
47}
diff --git a/server/lib/activitypub/send/send-announce.ts b/server/lib/activitypub/send/send-announce.ts
deleted file mode 100644
index 6c078b047..000000000
--- a/server/lib/activitypub/send/send-announce.ts
+++ /dev/null
@@ -1,58 +0,0 @@
1import { Transaction } from 'sequelize'
2import { ActivityAnnounce, ActivityAudience } from '@shared/models'
3import { logger } from '../../../helpers/logger'
4import { MActorLight, MVideo } from '../../../types/models'
5import { MVideoShare } from '../../../types/models/video'
6import { audiencify, getAudience } from '../audience'
7import { getActorsInvolvedInVideo, getAudienceFromFollowersOf } from './shared'
8import { broadcastToFollowers } from './shared/send-utils'
9
10async function buildAnnounceWithVideoAudience (
11 byActor: MActorLight,
12 videoShare: MVideoShare,
13 video: MVideo,
14 t: Transaction
15) {
16 const announcedObject = video.url
17
18 const actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, t)
19 const audience = getAudienceFromFollowersOf(actorsInvolvedInVideo)
20
21 const activity = buildAnnounceActivity(videoShare.url, byActor, announcedObject, audience)
22
23 return { activity, actorsInvolvedInVideo }
24}
25
26async function sendVideoAnnounce (byActor: MActorLight, videoShare: MVideoShare, video: MVideo, transaction: Transaction) {
27 const { activity, actorsInvolvedInVideo } = await buildAnnounceWithVideoAudience(byActor, videoShare, video, transaction)
28
29 logger.info('Creating job to send announce %s.', videoShare.url)
30
31 return broadcastToFollowers({
32 data: activity,
33 byActor,
34 toFollowersOf: actorsInvolvedInVideo,
35 transaction,
36 actorsException: [ byActor ],
37 contextType: 'Announce'
38 })
39}
40
41function buildAnnounceActivity (url: string, byActor: MActorLight, object: string, audience?: ActivityAudience): ActivityAnnounce {
42 if (!audience) audience = getAudience(byActor)
43
44 return audiencify({
45 type: 'Announce' as 'Announce',
46 id: url,
47 actor: byActor.url,
48 object
49 }, audience)
50}
51
52// ---------------------------------------------------------------------------
53
54export {
55 sendVideoAnnounce,
56 buildAnnounceActivity,
57 buildAnnounceWithVideoAudience
58}
diff --git a/server/lib/activitypub/send/send-create.ts b/server/lib/activitypub/send/send-create.ts
deleted file mode 100644
index 2cd4db14d..000000000
--- a/server/lib/activitypub/send/send-create.ts
+++ /dev/null
@@ -1,226 +0,0 @@
1import { Transaction } from 'sequelize'
2import { getServerActor } from '@server/models/application/application'
3import {
4 ActivityAudience,
5 ActivityCreate,
6 ActivityCreateObject,
7 ContextType,
8 VideoCommentObject,
9 VideoPlaylistPrivacy,
10 VideoPrivacy
11} from '@shared/models'
12import { logger, loggerTagsFactory } from '../../../helpers/logger'
13import { VideoCommentModel } from '../../../models/video/video-comment'
14import {
15 MActorLight,
16 MCommentOwnerVideo,
17 MLocalVideoViewerWithWatchSections,
18 MVideoAccountLight,
19 MVideoAP,
20 MVideoPlaylistFull,
21 MVideoRedundancyFileVideo,
22 MVideoRedundancyStreamingPlaylistVideo
23} from '../../../types/models'
24import { audiencify, getAudience } from '../audience'
25import {
26 broadcastToActors,
27 broadcastToFollowers,
28 getActorsInvolvedInVideo,
29 getAudienceFromFollowersOf,
30 getVideoCommentAudience,
31 sendVideoActivityToOrigin,
32 sendVideoRelatedActivity,
33 unicastTo
34} from './shared'
35
36const lTags = loggerTagsFactory('ap', 'create')
37
38async function sendCreateVideo (video: MVideoAP, transaction: Transaction) {
39 if (!video.hasPrivacyForFederation()) return undefined
40
41 logger.info('Creating job to send video creation of %s.', video.url, lTags(video.uuid))
42
43 const byActor = video.VideoChannel.Account.Actor
44 const videoObject = await video.toActivityPubObject()
45
46 const audience = getAudience(byActor, video.privacy === VideoPrivacy.PUBLIC)
47 const createActivity = buildCreateActivity(video.url, byActor, videoObject, audience)
48
49 return broadcastToFollowers({
50 data: createActivity,
51 byActor,
52 toFollowersOf: [ byActor ],
53 transaction,
54 contextType: 'Video'
55 })
56}
57
58async function sendCreateCacheFile (
59 byActor: MActorLight,
60 video: MVideoAccountLight,
61 fileRedundancy: MVideoRedundancyStreamingPlaylistVideo | MVideoRedundancyFileVideo
62) {
63 logger.info('Creating job to send file cache of %s.', fileRedundancy.url, lTags(video.uuid))
64
65 return sendVideoRelatedCreateActivity({
66 byActor,
67 video,
68 url: fileRedundancy.url,
69 object: fileRedundancy.toActivityPubObject(),
70 contextType: 'CacheFile'
71 })
72}
73
74async function sendCreateWatchAction (stats: MLocalVideoViewerWithWatchSections, transaction: Transaction) {
75 logger.info('Creating job to send create watch action %s.', stats.url, lTags(stats.uuid))
76
77 const byActor = await getServerActor()
78
79 const activityBuilder = (audience: ActivityAudience) => {
80 return buildCreateActivity(stats.url, byActor, stats.toActivityPubObject(), audience)
81 }
82
83 return sendVideoActivityToOrigin(activityBuilder, { byActor, video: stats.Video, transaction, contextType: 'WatchAction' })
84}
85
86async function sendCreateVideoPlaylist (playlist: MVideoPlaylistFull, transaction: Transaction) {
87 if (playlist.privacy === VideoPlaylistPrivacy.PRIVATE) return undefined
88
89 logger.info('Creating job to send create video playlist of %s.', playlist.url, lTags(playlist.uuid))
90
91 const byActor = playlist.OwnerAccount.Actor
92 const audience = getAudience(byActor, playlist.privacy === VideoPlaylistPrivacy.PUBLIC)
93
94 const object = await playlist.toActivityPubObject(null, transaction)
95 const createActivity = buildCreateActivity(playlist.url, byActor, object, audience)
96
97 const serverActor = await getServerActor()
98 const toFollowersOf = [ byActor, serverActor ]
99
100 if (playlist.VideoChannel) toFollowersOf.push(playlist.VideoChannel.Actor)
101
102 return broadcastToFollowers({
103 data: createActivity,
104 byActor,
105 toFollowersOf,
106 transaction,
107 contextType: 'Playlist'
108 })
109}
110
111async function sendCreateVideoComment (comment: MCommentOwnerVideo, transaction: Transaction) {
112 logger.info('Creating job to send comment %s.', comment.url)
113
114 const isOrigin = comment.Video.isOwned()
115
116 const byActor = comment.Account.Actor
117 const threadParentComments = await VideoCommentModel.listThreadParentComments(comment, transaction)
118 const commentObject = comment.toActivityPubObject(threadParentComments) as VideoCommentObject
119
120 const actorsInvolvedInComment = await getActorsInvolvedInVideo(comment.Video, transaction)
121 // Add the actor that commented too
122 actorsInvolvedInComment.push(byActor)
123
124 const parentsCommentActors = threadParentComments.filter(c => !c.isDeleted())
125 .map(c => c.Account.Actor)
126
127 let audience: ActivityAudience
128 if (isOrigin) {
129 audience = getVideoCommentAudience(comment, threadParentComments, actorsInvolvedInComment, isOrigin)
130 } else {
131 audience = getAudienceFromFollowersOf(actorsInvolvedInComment.concat(parentsCommentActors))
132 }
133
134 const createActivity = buildCreateActivity(comment.url, byActor, commentObject, audience)
135
136 // This was a reply, send it to the parent actors
137 const actorsException = [ byActor ]
138 await broadcastToActors({
139 data: createActivity,
140 byActor,
141 toActors: parentsCommentActors,
142 transaction,
143 actorsException,
144 contextType: 'Comment'
145 })
146
147 // Broadcast to our followers
148 await broadcastToFollowers({
149 data: createActivity,
150 byActor,
151 toFollowersOf: [ byActor ],
152 transaction,
153 contextType: 'Comment'
154 })
155
156 // Send to actors involved in the comment
157 if (isOrigin) {
158 return broadcastToFollowers({
159 data: createActivity,
160 byActor,
161 toFollowersOf: actorsInvolvedInComment,
162 transaction,
163 actorsException,
164 contextType: 'Comment'
165 })
166 }
167
168 // Send to origin
169 return transaction.afterCommit(() => {
170 return unicastTo({
171 data: createActivity,
172 byActor,
173 toActorUrl: comment.Video.VideoChannel.Account.Actor.getSharedInbox(),
174 contextType: 'Comment'
175 })
176 })
177}
178
179function buildCreateActivity <T extends ActivityCreateObject> (
180 url: string,
181 byActor: MActorLight,
182 object: T,
183 audience?: ActivityAudience
184): ActivityCreate<T> {
185 if (!audience) audience = getAudience(byActor)
186
187 return audiencify(
188 {
189 type: 'Create' as 'Create',
190 id: url + '/activity',
191 actor: byActor.url,
192 object: typeof object === 'string'
193 ? object
194 : audiencify(object, audience)
195 },
196 audience
197 )
198}
199
200// ---------------------------------------------------------------------------
201
202export {
203 sendCreateVideo,
204 buildCreateActivity,
205 sendCreateVideoComment,
206 sendCreateVideoPlaylist,
207 sendCreateCacheFile,
208 sendCreateWatchAction
209}
210
211// ---------------------------------------------------------------------------
212
213async function sendVideoRelatedCreateActivity (options: {
214 byActor: MActorLight
215 video: MVideoAccountLight
216 url: string
217 object: any
218 contextType: ContextType
219 transaction?: Transaction
220}) {
221 const activityBuilder = (audience: ActivityAudience) => {
222 return buildCreateActivity(options.url, options.byActor, options.object, audience)
223 }
224
225 return sendVideoRelatedActivity(activityBuilder, options)
226}
diff --git a/server/lib/activitypub/send/send-delete.ts b/server/lib/activitypub/send/send-delete.ts
deleted file mode 100644
index 0d85d9001..000000000
--- a/server/lib/activitypub/send/send-delete.ts
+++ /dev/null
@@ -1,158 +0,0 @@
1import { Transaction } from 'sequelize'
2import { getServerActor } from '@server/models/application/application'
3import { ActivityAudience, ActivityDelete } from '@shared/models'
4import { logger } from '../../../helpers/logger'
5import { ActorModel } from '../../../models/actor/actor'
6import { VideoCommentModel } from '../../../models/video/video-comment'
7import { VideoShareModel } from '../../../models/video/video-share'
8import { MActorUrl } from '../../../types/models'
9import { MCommentOwnerVideo, MVideoAccountLight, MVideoPlaylistFullSummary } from '../../../types/models/video'
10import { audiencify } from '../audience'
11import { getDeleteActivityPubUrl } from '../url'
12import { getActorsInvolvedInVideo, getVideoCommentAudience } from './shared'
13import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './shared/send-utils'
14
15async function sendDeleteVideo (video: MVideoAccountLight, transaction: Transaction) {
16 logger.info('Creating job to broadcast delete of video %s.', video.url)
17
18 const byActor = video.VideoChannel.Account.Actor
19
20 const activityBuilder = (audience: ActivityAudience) => {
21 const url = getDeleteActivityPubUrl(video.url)
22
23 return buildDeleteActivity(url, video.url, byActor, audience)
24 }
25
26 return sendVideoRelatedActivity(activityBuilder, { byActor, video, contextType: 'Delete', transaction })
27}
28
29async function sendDeleteActor (byActor: ActorModel, transaction: Transaction) {
30 logger.info('Creating job to broadcast delete of actor %s.', byActor.url)
31
32 const url = getDeleteActivityPubUrl(byActor.url)
33 const activity = buildDeleteActivity(url, byActor.url, byActor)
34
35 const actorsInvolved = await VideoShareModel.loadActorsWhoSharedVideosOf(byActor.id, transaction)
36
37 // In case the actor did not have any videos
38 const serverActor = await getServerActor()
39 actorsInvolved.push(serverActor)
40
41 actorsInvolved.push(byActor)
42
43 return broadcastToFollowers({
44 data: activity,
45 byActor,
46 toFollowersOf: actorsInvolved,
47 contextType: 'Delete',
48 transaction
49 })
50}
51
52async function sendDeleteVideoComment (videoComment: MCommentOwnerVideo, transaction: Transaction) {
53 logger.info('Creating job to send delete of comment %s.', videoComment.url)
54
55 const isVideoOrigin = videoComment.Video.isOwned()
56
57 const url = getDeleteActivityPubUrl(videoComment.url)
58 const byActor = videoComment.isOwned()
59 ? videoComment.Account.Actor
60 : videoComment.Video.VideoChannel.Account.Actor
61
62 const threadParentComments = await VideoCommentModel.listThreadParentComments(videoComment, transaction)
63 const threadParentCommentsFiltered = threadParentComments.filter(c => !c.isDeleted())
64
65 const actorsInvolvedInComment = await getActorsInvolvedInVideo(videoComment.Video, transaction)
66 actorsInvolvedInComment.push(byActor) // Add the actor that commented the video
67
68 const audience = getVideoCommentAudience(videoComment, threadParentCommentsFiltered, actorsInvolvedInComment, isVideoOrigin)
69 const activity = buildDeleteActivity(url, videoComment.url, byActor, audience)
70
71 // This was a reply, send it to the parent actors
72 const actorsException = [ byActor ]
73 await broadcastToActors({
74 data: activity,
75 byActor,
76 toActors: threadParentCommentsFiltered.map(c => c.Account.Actor),
77 transaction,
78 contextType: 'Delete',
79 actorsException
80 })
81
82 // Broadcast to our followers
83 await broadcastToFollowers({
84 data: activity,
85 byActor,
86 toFollowersOf: [ byActor ],
87 contextType: 'Delete',
88 transaction
89 })
90
91 // Send to actors involved in the comment
92 if (isVideoOrigin) {
93 return broadcastToFollowers({
94 data: activity,
95 byActor,
96 toFollowersOf: actorsInvolvedInComment,
97 transaction,
98 contextType: 'Delete',
99 actorsException
100 })
101 }
102
103 // Send to origin
104 return transaction.afterCommit(() => {
105 return unicastTo({
106 data: activity,
107 byActor,
108 toActorUrl: videoComment.Video.VideoChannel.Account.Actor.getSharedInbox(),
109 contextType: 'Delete'
110 })
111 })
112}
113
114async function sendDeleteVideoPlaylist (videoPlaylist: MVideoPlaylistFullSummary, transaction: Transaction) {
115 logger.info('Creating job to send delete of playlist %s.', videoPlaylist.url)
116
117 const byActor = videoPlaylist.OwnerAccount.Actor
118
119 const url = getDeleteActivityPubUrl(videoPlaylist.url)
120 const activity = buildDeleteActivity(url, videoPlaylist.url, byActor)
121
122 const serverActor = await getServerActor()
123 const toFollowersOf = [ byActor, serverActor ]
124
125 if (videoPlaylist.VideoChannel) toFollowersOf.push(videoPlaylist.VideoChannel.Actor)
126
127 return broadcastToFollowers({
128 data: activity,
129 byActor,
130 toFollowersOf,
131 contextType: 'Delete',
132 transaction
133 })
134}
135
136// ---------------------------------------------------------------------------
137
138export {
139 sendDeleteVideo,
140 sendDeleteActor,
141 sendDeleteVideoComment,
142 sendDeleteVideoPlaylist
143}
144
145// ---------------------------------------------------------------------------
146
147function buildDeleteActivity (url: string, object: string, byActor: MActorUrl, audience?: ActivityAudience): ActivityDelete {
148 const activity = {
149 type: 'Delete' as 'Delete',
150 id: url,
151 actor: byActor.url,
152 object
153 }
154
155 if (audience) return audiencify(activity, audience)
156
157 return activity
158}
diff --git a/server/lib/activitypub/send/send-dislike.ts b/server/lib/activitypub/send/send-dislike.ts
deleted file mode 100644
index 959e74823..000000000
--- a/server/lib/activitypub/send/send-dislike.ts
+++ /dev/null
@@ -1,40 +0,0 @@
1import { Transaction } from 'sequelize'
2import { ActivityAudience, ActivityDislike } from '@shared/models'
3import { logger } from '../../../helpers/logger'
4import { MActor, MActorAudience, MVideoAccountLight, MVideoUrl } from '../../../types/models'
5import { audiencify, getAudience } from '../audience'
6import { getVideoDislikeActivityPubUrlByLocalActor } from '../url'
7import { sendVideoActivityToOrigin } from './shared/send-utils'
8
9function sendDislike (byActor: MActor, video: MVideoAccountLight, transaction: Transaction) {
10 logger.info('Creating job to dislike %s.', video.url)
11
12 const activityBuilder = (audience: ActivityAudience) => {
13 const url = getVideoDislikeActivityPubUrlByLocalActor(byActor, video)
14
15 return buildDislikeActivity(url, byActor, video, audience)
16 }
17
18 return sendVideoActivityToOrigin(activityBuilder, { byActor, video, transaction, contextType: 'Rate' })
19}
20
21function buildDislikeActivity (url: string, byActor: MActorAudience, video: MVideoUrl, audience?: ActivityAudience): ActivityDislike {
22 if (!audience) audience = getAudience(byActor)
23
24 return audiencify(
25 {
26 id: url,
27 type: 'Dislike' as 'Dislike',
28 actor: byActor.url,
29 object: video.url
30 },
31 audience
32 )
33}
34
35// ---------------------------------------------------------------------------
36
37export {
38 sendDislike,
39 buildDislikeActivity
40}
diff --git a/server/lib/activitypub/send/send-flag.ts b/server/lib/activitypub/send/send-flag.ts
deleted file mode 100644
index 138eb5adc..000000000
--- a/server/lib/activitypub/send/send-flag.ts
+++ /dev/null
@@ -1,42 +0,0 @@
1import { Transaction } from 'sequelize'
2import { ActivityAudience, ActivityFlag } from '@shared/models'
3import { logger } from '../../../helpers/logger'
4import { MAbuseAP, MAccountLight, MActor } from '../../../types/models'
5import { audiencify, getAudience } from '../audience'
6import { getLocalAbuseActivityPubUrl } from '../url'
7import { unicastTo } from './shared/send-utils'
8
9function sendAbuse (byActor: MActor, abuse: MAbuseAP, flaggedAccount: MAccountLight, t: Transaction) {
10 if (!flaggedAccount.Actor.serverId) return // Local user
11
12 const url = getLocalAbuseActivityPubUrl(abuse)
13
14 logger.info('Creating job to send abuse %s.', url)
15
16 // Custom audience, we only send the abuse to the origin instance
17 const audience = { to: [ flaggedAccount.Actor.url ], cc: [] }
18 const flagActivity = buildFlagActivity(url, byActor, abuse, audience)
19
20 return t.afterCommit(() => {
21 return unicastTo({
22 data: flagActivity,
23 byActor,
24 toActorUrl: flaggedAccount.Actor.getSharedInbox(),
25 contextType: 'Flag'
26 })
27 })
28}
29
30function buildFlagActivity (url: string, byActor: MActor, abuse: MAbuseAP, audience: ActivityAudience): ActivityFlag {
31 if (!audience) audience = getAudience(byActor)
32
33 const activity = { id: url, actor: byActor.url, ...abuse.toActivityPubObject() }
34
35 return audiencify(activity, audience)
36}
37
38// ---------------------------------------------------------------------------
39
40export {
41 sendAbuse
42}
diff --git a/server/lib/activitypub/send/send-follow.ts b/server/lib/activitypub/send/send-follow.ts
deleted file mode 100644
index 57501dadb..000000000
--- a/server/lib/activitypub/send/send-follow.ts
+++ /dev/null
@@ -1,37 +0,0 @@
1import { Transaction } from 'sequelize'
2import { ActivityFollow } from '@shared/models'
3import { logger } from '../../../helpers/logger'
4import { MActor, MActorFollowActors } from '../../../types/models'
5import { unicastTo } from './shared/send-utils'
6
7function sendFollow (actorFollow: MActorFollowActors, t: Transaction) {
8 const me = actorFollow.ActorFollower
9 const following = actorFollow.ActorFollowing
10
11 // Same server as ours
12 if (!following.serverId) return
13
14 logger.info('Creating job to send follow request to %s.', following.url)
15
16 const data = buildFollowActivity(actorFollow.url, me, following)
17
18 return t.afterCommit(() => {
19 return unicastTo({ data, byActor: me, toActorUrl: following.inboxUrl, contextType: 'Follow' })
20 })
21}
22
23function buildFollowActivity (url: string, byActor: MActor, targetActor: MActor): ActivityFollow {
24 return {
25 type: 'Follow',
26 id: url,
27 actor: byActor.url,
28 object: targetActor.url
29 }
30}
31
32// ---------------------------------------------------------------------------
33
34export {
35 sendFollow,
36 buildFollowActivity
37}
diff --git a/server/lib/activitypub/send/send-like.ts b/server/lib/activitypub/send/send-like.ts
deleted file mode 100644
index 46c9fdec9..000000000
--- a/server/lib/activitypub/send/send-like.ts
+++ /dev/null
@@ -1,40 +0,0 @@
1import { Transaction } from 'sequelize'
2import { ActivityAudience, ActivityLike } from '@shared/models'
3import { logger } from '../../../helpers/logger'
4import { MActor, MActorAudience, MVideoAccountLight, MVideoUrl } from '../../../types/models'
5import { audiencify, getAudience } from '../audience'
6import { getVideoLikeActivityPubUrlByLocalActor } from '../url'
7import { sendVideoActivityToOrigin } from './shared/send-utils'
8
9function sendLike (byActor: MActor, video: MVideoAccountLight, transaction: Transaction) {
10 logger.info('Creating job to like %s.', video.url)
11
12 const activityBuilder = (audience: ActivityAudience) => {
13 const url = getVideoLikeActivityPubUrlByLocalActor(byActor, video)
14
15 return buildLikeActivity(url, byActor, video, audience)
16 }
17
18 return sendVideoActivityToOrigin(activityBuilder, { byActor, video, transaction, contextType: 'Rate' })
19}
20
21function buildLikeActivity (url: string, byActor: MActorAudience, video: MVideoUrl, audience?: ActivityAudience): ActivityLike {
22 if (!audience) audience = getAudience(byActor)
23
24 return audiencify(
25 {
26 id: url,
27 type: 'Like' as 'Like',
28 actor: byActor.url,
29 object: video.url
30 },
31 audience
32 )
33}
34
35// ---------------------------------------------------------------------------
36
37export {
38 sendLike,
39 buildLikeActivity
40}
diff --git a/server/lib/activitypub/send/send-reject.ts b/server/lib/activitypub/send/send-reject.ts
deleted file mode 100644
index a5f8c2ecf..000000000
--- a/server/lib/activitypub/send/send-reject.ts
+++ /dev/null
@@ -1,39 +0,0 @@
1import { ActivityFollow, ActivityReject } from '@shared/models'
2import { logger } from '../../../helpers/logger'
3import { MActor } from '../../../types/models'
4import { getLocalActorFollowRejectActivityPubUrl } from '../url'
5import { buildFollowActivity } from './send-follow'
6import { unicastTo } from './shared/send-utils'
7
8function sendReject (followUrl: string, follower: MActor, following: MActor) {
9 if (!follower.serverId) { // This should never happen
10 logger.warn('Do not sending reject to local follower.')
11 return
12 }
13
14 logger.info('Creating job to reject follower %s.', follower.url)
15
16 const followData = buildFollowActivity(followUrl, follower, following)
17
18 const url = getLocalActorFollowRejectActivityPubUrl()
19 const data = buildRejectActivity(url, following, followData)
20
21 return unicastTo({ data, byActor: following, toActorUrl: follower.inboxUrl, contextType: 'Reject' })
22}
23
24// ---------------------------------------------------------------------------
25
26export {
27 sendReject
28}
29
30// ---------------------------------------------------------------------------
31
32function buildRejectActivity (url: string, byActor: MActor, followActivityData: ActivityFollow): ActivityReject {
33 return {
34 type: 'Reject',
35 id: url,
36 actor: byActor.url,
37 object: followActivityData
38 }
39}
diff --git a/server/lib/activitypub/send/send-undo.ts b/server/lib/activitypub/send/send-undo.ts
deleted file mode 100644
index b0b48c9c4..000000000
--- a/server/lib/activitypub/send/send-undo.ts
+++ /dev/null
@@ -1,172 +0,0 @@
1import { Transaction } from 'sequelize'
2import { ActivityAudience, ActivityDislike, ActivityLike, ActivityUndo, ActivityUndoObject, ContextType } from '@shared/models'
3import { logger } from '../../../helpers/logger'
4import { VideoModel } from '../../../models/video/video'
5import {
6 MActor,
7 MActorAudience,
8 MActorFollowActors,
9 MActorLight,
10 MVideo,
11 MVideoAccountLight,
12 MVideoRedundancyVideo,
13 MVideoShare
14} from '../../../types/models'
15import { audiencify, getAudience } from '../audience'
16import { getUndoActivityPubUrl, getVideoDislikeActivityPubUrlByLocalActor, getVideoLikeActivityPubUrlByLocalActor } from '../url'
17import { buildAnnounceWithVideoAudience } from './send-announce'
18import { buildCreateActivity } from './send-create'
19import { buildDislikeActivity } from './send-dislike'
20import { buildFollowActivity } from './send-follow'
21import { buildLikeActivity } from './send-like'
22import { broadcastToFollowers, sendVideoActivityToOrigin, sendVideoRelatedActivity, unicastTo } from './shared/send-utils'
23
24function sendUndoFollow (actorFollow: MActorFollowActors, t: Transaction) {
25 const me = actorFollow.ActorFollower
26 const following = actorFollow.ActorFollowing
27
28 // Same server as ours
29 if (!following.serverId) return
30
31 logger.info('Creating job to send an unfollow request to %s.', following.url)
32
33 const undoUrl = getUndoActivityPubUrl(actorFollow.url)
34
35 const followActivity = buildFollowActivity(actorFollow.url, me, following)
36 const undoActivity = undoActivityData(undoUrl, me, followActivity)
37
38 t.afterCommit(() => {
39 return unicastTo({
40 data: undoActivity,
41 byActor: me,
42 toActorUrl: following.inboxUrl,
43 contextType: 'Follow'
44 })
45 })
46}
47
48// ---------------------------------------------------------------------------
49
50async function sendUndoAnnounce (byActor: MActorLight, videoShare: MVideoShare, video: MVideo, transaction: Transaction) {
51 logger.info('Creating job to undo announce %s.', videoShare.url)
52
53 const undoUrl = getUndoActivityPubUrl(videoShare.url)
54
55 const { activity: announce, actorsInvolvedInVideo } = await buildAnnounceWithVideoAudience(byActor, videoShare, video, transaction)
56 const undoActivity = undoActivityData(undoUrl, byActor, announce)
57
58 return broadcastToFollowers({
59 data: undoActivity,
60 byActor,
61 toFollowersOf: actorsInvolvedInVideo,
62 transaction,
63 actorsException: [ byActor ],
64 contextType: 'Announce'
65 })
66}
67
68async function sendUndoCacheFile (byActor: MActor, redundancyModel: MVideoRedundancyVideo, transaction: Transaction) {
69 logger.info('Creating job to undo cache file %s.', redundancyModel.url)
70
71 const associatedVideo = redundancyModel.getVideo()
72 if (!associatedVideo) {
73 logger.warn('Cannot send undo activity for redundancy %s: no video files associated.', redundancyModel.url)
74 return
75 }
76
77 const video = await VideoModel.loadFull(associatedVideo.id)
78 const createActivity = buildCreateActivity(redundancyModel.url, byActor, redundancyModel.toActivityPubObject())
79
80 return sendUndoVideoRelatedActivity({
81 byActor,
82 video,
83 url: redundancyModel.url,
84 activity: createActivity,
85 contextType: 'CacheFile',
86 transaction
87 })
88}
89
90// ---------------------------------------------------------------------------
91
92async function sendUndoLike (byActor: MActor, video: MVideoAccountLight, t: Transaction) {
93 logger.info('Creating job to undo a like of video %s.', video.url)
94
95 const likeUrl = getVideoLikeActivityPubUrlByLocalActor(byActor, video)
96 const likeActivity = buildLikeActivity(likeUrl, byActor, video)
97
98 return sendUndoVideoRateToOriginActivity({ byActor, video, url: likeUrl, activity: likeActivity, transaction: t })
99}
100
101async function sendUndoDislike (byActor: MActor, video: MVideoAccountLight, t: Transaction) {
102 logger.info('Creating job to undo a dislike of video %s.', video.url)
103
104 const dislikeUrl = getVideoDislikeActivityPubUrlByLocalActor(byActor, video)
105 const dislikeActivity = buildDislikeActivity(dislikeUrl, byActor, video)
106
107 return sendUndoVideoRateToOriginActivity({ byActor, video, url: dislikeUrl, activity: dislikeActivity, transaction: t })
108}
109
110// ---------------------------------------------------------------------------
111
112export {
113 sendUndoFollow,
114 sendUndoLike,
115 sendUndoDislike,
116 sendUndoAnnounce,
117 sendUndoCacheFile
118}
119
120// ---------------------------------------------------------------------------
121
122function undoActivityData <T extends ActivityUndoObject> (
123 url: string,
124 byActor: MActorAudience,
125 object: T,
126 audience?: ActivityAudience
127): ActivityUndo<T> {
128 if (!audience) audience = getAudience(byActor)
129
130 return audiencify(
131 {
132 type: 'Undo' as 'Undo',
133 id: url,
134 actor: byActor.url,
135 object
136 },
137 audience
138 )
139}
140
141async function sendUndoVideoRelatedActivity (options: {
142 byActor: MActor
143 video: MVideoAccountLight
144 url: string
145 activity: ActivityUndoObject
146 contextType: ContextType
147 transaction: Transaction
148}) {
149 const activityBuilder = (audience: ActivityAudience) => {
150 const undoUrl = getUndoActivityPubUrl(options.url)
151
152 return undoActivityData(undoUrl, options.byActor, options.activity, audience)
153 }
154
155 return sendVideoRelatedActivity(activityBuilder, options)
156}
157
158async function sendUndoVideoRateToOriginActivity (options: {
159 byActor: MActor
160 video: MVideoAccountLight
161 url: string
162 activity: ActivityLike | ActivityDislike
163 transaction: Transaction
164}) {
165 const activityBuilder = (audience: ActivityAudience) => {
166 const undoUrl = getUndoActivityPubUrl(options.url)
167
168 return undoActivityData(undoUrl, options.byActor, options.activity, audience)
169 }
170
171 return sendVideoActivityToOrigin(activityBuilder, { ...options, contextType: 'Rate' })
172}
diff --git a/server/lib/activitypub/send/send-update.ts b/server/lib/activitypub/send/send-update.ts
deleted file mode 100644
index f3fb741c6..000000000
--- a/server/lib/activitypub/send/send-update.ts
+++ /dev/null
@@ -1,157 +0,0 @@
1import { Transaction } from 'sequelize'
2import { getServerActor } from '@server/models/application/application'
3import { ActivityAudience, ActivityUpdate, ActivityUpdateObject, VideoPlaylistPrivacy, VideoPrivacy } from '@shared/models'
4import { logger } from '../../../helpers/logger'
5import { AccountModel } from '../../../models/account/account'
6import { VideoModel } from '../../../models/video/video'
7import { VideoShareModel } from '../../../models/video/video-share'
8import {
9 MAccountDefault,
10 MActor,
11 MActorLight,
12 MChannelDefault,
13 MVideoAPLight,
14 MVideoPlaylistFull,
15 MVideoRedundancyVideo
16} from '../../../types/models'
17import { audiencify, getAudience } from '../audience'
18import { getUpdateActivityPubUrl } from '../url'
19import { getActorsInvolvedInVideo } from './shared'
20import { broadcastToFollowers, sendVideoRelatedActivity } from './shared/send-utils'
21
22async function sendUpdateVideo (videoArg: MVideoAPLight, transaction: Transaction, overriddenByActor?: MActor) {
23 if (!videoArg.hasPrivacyForFederation()) return undefined
24
25 const video = await videoArg.lightAPToFullAP(transaction)
26
27 logger.info('Creating job to update video %s.', video.url)
28
29 const byActor = overriddenByActor || video.VideoChannel.Account.Actor
30
31 const url = getUpdateActivityPubUrl(video.url, video.updatedAt.toISOString())
32
33 const videoObject = await video.toActivityPubObject()
34 const audience = getAudience(byActor, video.privacy === VideoPrivacy.PUBLIC)
35
36 const updateActivity = buildUpdateActivity(url, byActor, videoObject, audience)
37
38 const actorsInvolved = await getActorsInvolvedInVideo(video, transaction)
39 if (overriddenByActor) actorsInvolved.push(overriddenByActor)
40
41 return broadcastToFollowers({
42 data: updateActivity,
43 byActor,
44 toFollowersOf: actorsInvolved,
45 contextType: 'Video',
46 transaction
47 })
48}
49
50async function sendUpdateActor (accountOrChannel: MChannelDefault | MAccountDefault, transaction: Transaction) {
51 const byActor = accountOrChannel.Actor
52
53 logger.info('Creating job to update actor %s.', byActor.url)
54
55 const url = getUpdateActivityPubUrl(byActor.url, byActor.updatedAt.toISOString())
56 const accountOrChannelObject = await (accountOrChannel as any).toActivityPubObject() // FIXME: typescript bug?
57 const audience = getAudience(byActor)
58 const updateActivity = buildUpdateActivity(url, byActor, accountOrChannelObject, audience)
59
60 let actorsInvolved: MActor[]
61 if (accountOrChannel instanceof AccountModel) {
62 // Actors that shared my videos are involved too
63 actorsInvolved = await VideoShareModel.loadActorsWhoSharedVideosOf(byActor.id, transaction)
64 } else {
65 // Actors that shared videos of my channel are involved too
66 actorsInvolved = await VideoShareModel.loadActorsByVideoChannel(accountOrChannel.id, transaction)
67 }
68
69 actorsInvolved.push(byActor)
70
71 return broadcastToFollowers({
72 data: updateActivity,
73 byActor,
74 toFollowersOf: actorsInvolved,
75 transaction,
76 contextType: 'Actor'
77 })
78}
79
80async function sendUpdateCacheFile (byActor: MActorLight, redundancyModel: MVideoRedundancyVideo) {
81 logger.info('Creating job to update cache file %s.', redundancyModel.url)
82
83 const associatedVideo = redundancyModel.getVideo()
84 if (!associatedVideo) {
85 logger.warn('Cannot send update activity for redundancy %s: no video files associated.', redundancyModel.url)
86 return
87 }
88
89 const video = await VideoModel.loadFull(associatedVideo.id)
90
91 const activityBuilder = (audience: ActivityAudience) => {
92 const redundancyObject = redundancyModel.toActivityPubObject()
93 const url = getUpdateActivityPubUrl(redundancyModel.url, redundancyModel.updatedAt.toISOString())
94
95 return buildUpdateActivity(url, byActor, redundancyObject, audience)
96 }
97
98 return sendVideoRelatedActivity(activityBuilder, { byActor, video, contextType: 'CacheFile' })
99}
100
101async function sendUpdateVideoPlaylist (videoPlaylist: MVideoPlaylistFull, transaction: Transaction) {
102 if (videoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) return undefined
103
104 const byActor = videoPlaylist.OwnerAccount.Actor
105
106 logger.info('Creating job to update video playlist %s.', videoPlaylist.url)
107
108 const url = getUpdateActivityPubUrl(videoPlaylist.url, videoPlaylist.updatedAt.toISOString())
109
110 const object = await videoPlaylist.toActivityPubObject(null, transaction)
111 const audience = getAudience(byActor, videoPlaylist.privacy === VideoPlaylistPrivacy.PUBLIC)
112
113 const updateActivity = buildUpdateActivity(url, byActor, object, audience)
114
115 const serverActor = await getServerActor()
116 const toFollowersOf = [ byActor, serverActor ]
117
118 if (videoPlaylist.VideoChannel) toFollowersOf.push(videoPlaylist.VideoChannel.Actor)
119
120 return broadcastToFollowers({
121 data: updateActivity,
122 byActor,
123 toFollowersOf,
124 transaction,
125 contextType: 'Playlist'
126 })
127}
128
129// ---------------------------------------------------------------------------
130
131export {
132 sendUpdateActor,
133 sendUpdateVideo,
134 sendUpdateCacheFile,
135 sendUpdateVideoPlaylist
136}
137
138// ---------------------------------------------------------------------------
139
140function buildUpdateActivity (
141 url: string,
142 byActor: MActorLight,
143 object: ActivityUpdateObject,
144 audience?: ActivityAudience
145): ActivityUpdate<ActivityUpdateObject> {
146 if (!audience) audience = getAudience(byActor)
147
148 return audiencify(
149 {
150 type: 'Update' as 'Update',
151 id: url,
152 actor: byActor.url,
153 object: audiencify(object, audience)
154 },
155 audience
156 )
157}
diff --git a/server/lib/activitypub/send/send-view.ts b/server/lib/activitypub/send/send-view.ts
deleted file mode 100644
index bf3451603..000000000
--- a/server/lib/activitypub/send/send-view.ts
+++ /dev/null
@@ -1,62 +0,0 @@
1import { Transaction } from 'sequelize'
2import { VideoViewsManager } from '@server/lib/views/video-views-manager'
3import { MActorAudience, MActorLight, MVideoImmutable, MVideoUrl } from '@server/types/models'
4import { ActivityAudience, ActivityView } from '@shared/models'
5import { logger } from '../../../helpers/logger'
6import { audiencify, getAudience } from '../audience'
7import { getLocalVideoViewActivityPubUrl } from '../url'
8import { sendVideoRelatedActivity } from './shared/send-utils'
9
10type ViewType = 'view' | 'viewer'
11
12async function sendView (options: {
13 byActor: MActorLight
14 type: ViewType
15 video: MVideoImmutable
16 viewerIdentifier: string
17 transaction?: Transaction
18}) {
19 const { byActor, type, video, viewerIdentifier, transaction } = options
20
21 logger.info('Creating job to send %s of %s.', type, video.url)
22
23 const activityBuilder = (audience: ActivityAudience) => {
24 const url = getLocalVideoViewActivityPubUrl(byActor, video, viewerIdentifier)
25
26 return buildViewActivity({ url, byActor, video, audience, type })
27 }
28
29 return sendVideoRelatedActivity(activityBuilder, { byActor, video, transaction, contextType: 'View', parallelizable: true })
30}
31
32// ---------------------------------------------------------------------------
33
34export {
35 sendView
36}
37
38// ---------------------------------------------------------------------------
39
40function buildViewActivity (options: {
41 url: string
42 byActor: MActorAudience
43 video: MVideoUrl
44 type: ViewType
45 audience?: ActivityAudience
46}): ActivityView {
47 const { url, byActor, type, video, audience = getAudience(byActor) } = options
48
49 return audiencify(
50 {
51 id: url,
52 type: 'View' as 'View',
53 actor: byActor.url,
54 object: video.url,
55
56 expires: type === 'viewer'
57 ? new Date(VideoViewsManager.Instance.buildViewerExpireTime()).toISOString()
58 : undefined
59 },
60 audience
61 )
62}
diff --git a/server/lib/activitypub/send/shared/audience-utils.ts b/server/lib/activitypub/send/shared/audience-utils.ts
deleted file mode 100644
index 2f6b0741d..000000000
--- a/server/lib/activitypub/send/shared/audience-utils.ts
+++ /dev/null
@@ -1,74 +0,0 @@
1import { Transaction } from 'sequelize'
2import { ACTIVITY_PUB } from '@server/initializers/constants'
3import { ActorModel } from '@server/models/actor/actor'
4import { VideoModel } from '@server/models/video/video'
5import { VideoShareModel } from '@server/models/video/video-share'
6import { MActorFollowersUrl, MActorUrl, MCommentOwner, MCommentOwnerVideo, MVideoId } from '@server/types/models'
7import { ActivityAudience } from '@shared/models'
8
9function getOriginVideoAudience (accountActor: MActorUrl, actorsInvolvedInVideo: MActorFollowersUrl[] = []): ActivityAudience {
10 return {
11 to: [ accountActor.url ],
12 cc: actorsInvolvedInVideo.map(a => a.followersUrl)
13 }
14}
15
16function getVideoCommentAudience (
17 videoComment: MCommentOwnerVideo,
18 threadParentComments: MCommentOwner[],
19 actorsInvolvedInVideo: MActorFollowersUrl[],
20 isOrigin = false
21): ActivityAudience {
22 const to = [ ACTIVITY_PUB.PUBLIC ]
23 const cc: string[] = []
24
25 // Owner of the video we comment
26 if (isOrigin === false) {
27 cc.push(videoComment.Video.VideoChannel.Account.Actor.url)
28 }
29
30 // Followers of the poster
31 cc.push(videoComment.Account.Actor.followersUrl)
32
33 // Send to actors we reply to
34 for (const parentComment of threadParentComments) {
35 if (parentComment.isDeleted()) continue
36
37 cc.push(parentComment.Account.Actor.url)
38 }
39
40 return {
41 to,
42 cc: cc.concat(actorsInvolvedInVideo.map(a => a.followersUrl))
43 }
44}
45
46function getAudienceFromFollowersOf (actorsInvolvedInObject: MActorFollowersUrl[]): ActivityAudience {
47 return {
48 to: [ ACTIVITY_PUB.PUBLIC ].concat(actorsInvolvedInObject.map(a => a.followersUrl)),
49 cc: []
50 }
51}
52
53async function getActorsInvolvedInVideo (video: MVideoId, t: Transaction) {
54 const actors = await VideoShareModel.listActorIdsAndFollowerUrlsByShare(video.id, t)
55
56 const videoAll = video as VideoModel
57
58 const videoActor = videoAll.VideoChannel?.Account
59 ? videoAll.VideoChannel.Account.Actor
60 : await ActorModel.loadAccountActorFollowerUrlByVideoId(video.id, t)
61
62 actors.push(videoActor)
63
64 return actors
65}
66
67// ---------------------------------------------------------------------------
68
69export {
70 getOriginVideoAudience,
71 getActorsInvolvedInVideo,
72 getAudienceFromFollowersOf,
73 getVideoCommentAudience
74}
diff --git a/server/lib/activitypub/send/shared/index.ts b/server/lib/activitypub/send/shared/index.ts
deleted file mode 100644
index bda579115..000000000
--- a/server/lib/activitypub/send/shared/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
1export * from './audience-utils'
2export * from './send-utils'
diff --git a/server/lib/activitypub/send/shared/send-utils.ts b/server/lib/activitypub/send/shared/send-utils.ts
deleted file mode 100644
index 2bc1ef8f5..000000000
--- a/server/lib/activitypub/send/shared/send-utils.ts
+++ /dev/null
@@ -1,291 +0,0 @@
1import { Transaction } from 'sequelize'
2import { ActorFollowHealthCache } from '@server/lib/actor-follow-health-cache'
3import { getServerActor } from '@server/models/application/application'
4import { Activity, ActivityAudience, ActivitypubHttpBroadcastPayload } from '@shared/models'
5import { ContextType } from '@shared/models/activitypub/context'
6import { afterCommitIfTransaction } from '../../../../helpers/database-utils'
7import { logger } from '../../../../helpers/logger'
8import { ActorModel } from '../../../../models/actor/actor'
9import { ActorFollowModel } from '../../../../models/actor/actor-follow'
10import { MActor, MActorId, MActorLight, MActorWithInboxes, MVideoAccountLight, MVideoId, MVideoImmutable } from '../../../../types/models'
11import { JobQueue } from '../../../job-queue'
12import { getActorsInvolvedInVideo, getAudienceFromFollowersOf, getOriginVideoAudience } from './audience-utils'
13
14async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAudience) => Activity, options: {
15 byActor: MActorLight
16 video: MVideoImmutable | MVideoAccountLight
17 contextType: ContextType
18 parallelizable?: boolean
19 transaction?: Transaction
20}) {
21 const { byActor, video, transaction, contextType, parallelizable } = options
22
23 // Send to origin
24 if (video.isOwned() === false) {
25 return sendVideoActivityToOrigin(activityBuilder, options)
26 }
27
28 const actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, transaction)
29
30 // Send to followers
31 const audience = getAudienceFromFollowersOf(actorsInvolvedInVideo)
32 const activity = activityBuilder(audience)
33
34 const actorsException = [ byActor ]
35
36 return broadcastToFollowers({
37 data: activity,
38 byActor,
39 toFollowersOf: actorsInvolvedInVideo,
40 transaction,
41 actorsException,
42 parallelizable,
43 contextType
44 })
45}
46
47async function sendVideoActivityToOrigin (activityBuilder: (audience: ActivityAudience) => Activity, options: {
48 byActor: MActorLight
49 video: MVideoImmutable | MVideoAccountLight
50 contextType: ContextType
51
52 actorsInvolvedInVideo?: MActorLight[]
53 transaction?: Transaction
54}) {
55 const { byActor, video, actorsInvolvedInVideo, transaction, contextType } = options
56
57 if (video.isOwned()) throw new Error('Cannot send activity to owned video origin ' + video.url)
58
59 let accountActor: MActorLight = (video as MVideoAccountLight).VideoChannel?.Account?.Actor
60 if (!accountActor) accountActor = await ActorModel.loadAccountActorByVideoId(video.id, transaction)
61
62 const audience = getOriginVideoAudience(accountActor, actorsInvolvedInVideo)
63 const activity = activityBuilder(audience)
64
65 return afterCommitIfTransaction(transaction, () => {
66 return unicastTo({
67 data: activity,
68 byActor,
69 toActorUrl: accountActor.getSharedInbox(),
70 contextType
71 })
72 })
73}
74
75// ---------------------------------------------------------------------------
76
77async function forwardVideoRelatedActivity (
78 activity: Activity,
79 t: Transaction,
80 followersException: MActorWithInboxes[],
81 video: MVideoId
82) {
83 // Mastodon does not add our announces in audience, so we forward to them manually
84 const additionalActors = await getActorsInvolvedInVideo(video, t)
85 const additionalFollowerUrls = additionalActors.map(a => a.followersUrl)
86
87 return forwardActivity(activity, t, followersException, additionalFollowerUrls)
88}
89
90async function forwardActivity (
91 activity: Activity,
92 t: Transaction,
93 followersException: MActorWithInboxes[] = [],
94 additionalFollowerUrls: string[] = []
95) {
96 logger.info('Forwarding activity %s.', activity.id)
97
98 const to = activity.to || []
99 const cc = activity.cc || []
100
101 const followersUrls = additionalFollowerUrls
102 for (const dest of to.concat(cc)) {
103 if (dest.endsWith('/followers')) {
104 followersUrls.push(dest)
105 }
106 }
107
108 const toActorFollowers = await ActorModel.listByFollowersUrls(followersUrls, t)
109 const uris = await computeFollowerUris(toActorFollowers, followersException, t)
110
111 if (uris.length === 0) {
112 logger.info('0 followers for %s, no forwarding.', toActorFollowers.map(a => a.id).join(', '))
113 return undefined
114 }
115
116 logger.debug('Creating forwarding job.', { uris })
117
118 const payload: ActivitypubHttpBroadcastPayload = {
119 uris,
120 body: activity,
121 contextType: null
122 }
123 return afterCommitIfTransaction(t, () => JobQueue.Instance.createJobAsync({ type: 'activitypub-http-broadcast', payload }))
124}
125
126// ---------------------------------------------------------------------------
127
128async function broadcastToFollowers (options: {
129 data: any
130 byActor: MActorId
131 toFollowersOf: MActorId[]
132 transaction: Transaction
133 contextType: ContextType
134
135 parallelizable?: boolean
136 actorsException?: MActorWithInboxes[]
137}) {
138 const { data, byActor, toFollowersOf, transaction, contextType, actorsException = [], parallelizable } = options
139
140 const uris = await computeFollowerUris(toFollowersOf, actorsException, transaction)
141
142 return afterCommitIfTransaction(transaction, () => {
143 return broadcastTo({
144 uris,
145 data,
146 byActor,
147 parallelizable,
148 contextType
149 })
150 })
151}
152
153async function broadcastToActors (options: {
154 data: any
155 byActor: MActorId
156 toActors: MActor[]
157 transaction: Transaction
158 contextType: ContextType
159 actorsException?: MActorWithInboxes[]
160}) {
161 const { data, byActor, toActors, transaction, contextType, actorsException = [] } = options
162
163 const uris = await computeUris(toActors, actorsException)
164
165 return afterCommitIfTransaction(transaction, () => {
166 return broadcastTo({
167 uris,
168 data,
169 byActor,
170 contextType
171 })
172 })
173}
174
175function broadcastTo (options: {
176 uris: string[]
177 data: any
178 byActor: MActorId
179 contextType: ContextType
180 parallelizable?: boolean // default to false
181}) {
182 const { uris, data, byActor, contextType, parallelizable } = options
183
184 if (uris.length === 0) return undefined
185
186 const broadcastUris: string[] = []
187 const unicastUris: string[] = []
188
189 // Bad URIs could be slow to respond, prefer to process them in a dedicated queue
190 for (const uri of uris) {
191 if (ActorFollowHealthCache.Instance.isBadInbox(uri)) {
192 unicastUris.push(uri)
193 } else {
194 broadcastUris.push(uri)
195 }
196 }
197
198 logger.debug('Creating broadcast job.', { broadcastUris, unicastUris })
199
200 if (broadcastUris.length !== 0) {
201 const payload = {
202 uris: broadcastUris,
203 signatureActorId: byActor.id,
204 body: data,
205 contextType
206 }
207
208 JobQueue.Instance.createJobAsync({
209 type: parallelizable
210 ? 'activitypub-http-broadcast-parallel'
211 : 'activitypub-http-broadcast',
212
213 payload
214 })
215 }
216
217 for (const unicastUri of unicastUris) {
218 const payload = {
219 uri: unicastUri,
220 signatureActorId: byActor.id,
221 body: data,
222 contextType
223 }
224
225 JobQueue.Instance.createJobAsync({ type: 'activitypub-http-unicast', payload })
226 }
227}
228
229function unicastTo (options: {
230 data: any
231 byActor: MActorId
232 toActorUrl: string
233 contextType: ContextType
234}) {
235 const { data, byActor, toActorUrl, contextType } = options
236
237 logger.debug('Creating unicast job.', { uri: toActorUrl })
238
239 const payload = {
240 uri: toActorUrl,
241 signatureActorId: byActor.id,
242 body: data,
243 contextType
244 }
245
246 JobQueue.Instance.createJobAsync({ type: 'activitypub-http-unicast', payload })
247}
248
249// ---------------------------------------------------------------------------
250
251export {
252 broadcastToFollowers,
253 unicastTo,
254 forwardActivity,
255 broadcastToActors,
256 sendVideoActivityToOrigin,
257 forwardVideoRelatedActivity,
258 sendVideoRelatedActivity
259}
260
261// ---------------------------------------------------------------------------
262
263async function computeFollowerUris (toFollowersOf: MActorId[], actorsException: MActorWithInboxes[], t: Transaction) {
264 const toActorFollowerIds = toFollowersOf.map(a => a.id)
265
266 const result = await ActorFollowModel.listAcceptedFollowerSharedInboxUrls(toActorFollowerIds, t)
267 const sharedInboxesException = await buildSharedInboxesException(actorsException)
268
269 return result.data.filter(sharedInbox => sharedInboxesException.includes(sharedInbox) === false)
270}
271
272async function computeUris (toActors: MActor[], actorsException: MActorWithInboxes[] = []) {
273 const serverActor = await getServerActor()
274 const targetUrls = toActors
275 .filter(a => a.id !== serverActor.id) // Don't send to ourselves
276 .map(a => a.getSharedInbox())
277
278 const toActorSharedInboxesSet = new Set(targetUrls)
279
280 const sharedInboxesException = await buildSharedInboxesException(actorsException)
281 return Array.from(toActorSharedInboxesSet)
282 .filter(sharedInbox => sharedInboxesException.includes(sharedInbox) === false)
283}
284
285async function buildSharedInboxesException (actorsException: MActorWithInboxes[]) {
286 const serverActor = await getServerActor()
287
288 return actorsException
289 .map(f => f.getSharedInbox())
290 .concat([ serverActor.sharedInboxUrl ])
291}
diff --git a/server/lib/activitypub/share.ts b/server/lib/activitypub/share.ts
deleted file mode 100644
index 792a73f2a..000000000
--- a/server/lib/activitypub/share.ts
+++ /dev/null
@@ -1,120 +0,0 @@
1import { map } from 'bluebird'
2import { Transaction } from 'sequelize'
3import { getServerActor } from '@server/models/application/application'
4import { logger, loggerTagsFactory } from '../../helpers/logger'
5import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
6import { VideoShareModel } from '../../models/video/video-share'
7import { MChannelActorLight, MVideo, MVideoAccountLight, MVideoId } from '../../types/models/video'
8import { fetchAP, getAPId } from './activity'
9import { getOrCreateAPActor } from './actors'
10import { sendUndoAnnounce, sendVideoAnnounce } from './send'
11import { checkUrlsSameHost, getLocalVideoAnnounceActivityPubUrl } from './url'
12
13const lTags = loggerTagsFactory('share')
14
15async function shareVideoByServerAndChannel (video: MVideoAccountLight, t: Transaction) {
16 if (!video.hasPrivacyForFederation()) return undefined
17
18 return Promise.all([
19 shareByServer(video, t),
20 shareByVideoChannel(video, t)
21 ])
22}
23
24async function changeVideoChannelShare (
25 video: MVideoAccountLight,
26 oldVideoChannel: MChannelActorLight,
27 t: Transaction
28) {
29 logger.info(
30 'Updating video channel of video %s: %s -> %s.', video.uuid, oldVideoChannel.name, video.VideoChannel.name,
31 lTags(video.uuid)
32 )
33
34 await undoShareByVideoChannel(video, oldVideoChannel, t)
35
36 await shareByVideoChannel(video, t)
37}
38
39async function addVideoShares (shareUrls: string[], video: MVideoId) {
40 await map(shareUrls, async shareUrl => {
41 try {
42 await addVideoShare(shareUrl, video)
43 } catch (err) {
44 logger.warn('Cannot add share %s.', shareUrl, { err })
45 }
46 }, { concurrency: CRAWL_REQUEST_CONCURRENCY })
47}
48
49export {
50 changeVideoChannelShare,
51 addVideoShares,
52 shareVideoByServerAndChannel
53}
54
55// ---------------------------------------------------------------------------
56
57async function addVideoShare (shareUrl: string, video: MVideoId) {
58 const { body } = await fetchAP<any>(shareUrl)
59 if (!body?.actor) throw new Error('Body or body actor is invalid')
60
61 const actorUrl = getAPId(body.actor)
62 if (checkUrlsSameHost(shareUrl, actorUrl) !== true) {
63 throw new Error(`Actor url ${actorUrl} has not the same host than the share url ${shareUrl}`)
64 }
65
66 const actor = await getOrCreateAPActor(actorUrl)
67
68 const entry = {
69 actorId: actor.id,
70 videoId: video.id,
71 url: shareUrl
72 }
73
74 await VideoShareModel.upsert(entry)
75}
76
77async function shareByServer (video: MVideo, t: Transaction) {
78 const serverActor = await getServerActor()
79
80 const serverShareUrl = getLocalVideoAnnounceActivityPubUrl(serverActor, video)
81 const [ serverShare ] = await VideoShareModel.findOrCreate({
82 defaults: {
83 actorId: serverActor.id,
84 videoId: video.id,
85 url: serverShareUrl
86 },
87 where: {
88 url: serverShareUrl
89 },
90 transaction: t
91 })
92
93 return sendVideoAnnounce(serverActor, serverShare, video, t)
94}
95
96async function shareByVideoChannel (video: MVideoAccountLight, t: Transaction) {
97 const videoChannelShareUrl = getLocalVideoAnnounceActivityPubUrl(video.VideoChannel.Actor, video)
98 const [ videoChannelShare ] = await VideoShareModel.findOrCreate({
99 defaults: {
100 actorId: video.VideoChannel.actorId,
101 videoId: video.id,
102 url: videoChannelShareUrl
103 },
104 where: {
105 url: videoChannelShareUrl
106 },
107 transaction: t
108 })
109
110 return sendVideoAnnounce(video.VideoChannel.Actor, videoChannelShare, video, t)
111}
112
113async function undoShareByVideoChannel (video: MVideo, oldVideoChannel: MChannelActorLight, t: Transaction) {
114 // Load old share
115 const oldShare = await VideoShareModel.load(oldVideoChannel.actorId, video.id, t)
116 if (!oldShare) return new Error('Cannot find old video channel share ' + oldVideoChannel.actorId + ' for video ' + video.id)
117
118 await sendUndoAnnounce(oldVideoChannel.Actor, oldShare, video, t)
119 await oldShare.destroy({ transaction: t })
120}
diff --git a/server/lib/activitypub/url.ts b/server/lib/activitypub/url.ts
deleted file mode 100644
index 5cdac71bf..000000000
--- a/server/lib/activitypub/url.ts
+++ /dev/null
@@ -1,177 +0,0 @@
1import { REMOTE_SCHEME, WEBSERVER } from '../../initializers/constants'
2import {
3 MAbuseFull,
4 MAbuseId,
5 MActor,
6 MActorFollow,
7 MActorId,
8 MActorUrl,
9 MCommentId,
10 MLocalVideoViewer,
11 MVideoId,
12 MVideoPlaylistElement,
13 MVideoUrl,
14 MVideoUUID,
15 MVideoWithHost
16} from '../../types/models'
17import { MVideoFileVideoUUID } from '../../types/models/video/video-file'
18import { MVideoPlaylist, MVideoPlaylistUUID } from '../../types/models/video/video-playlist'
19import { MStreamingPlaylist } from '../../types/models/video/video-streaming-playlist'
20
21function getLocalVideoActivityPubUrl (video: MVideoUUID) {
22 return WEBSERVER.URL + '/videos/watch/' + video.uuid
23}
24
25function getLocalVideoPlaylistActivityPubUrl (videoPlaylist: MVideoPlaylist) {
26 return WEBSERVER.URL + '/video-playlists/' + videoPlaylist.uuid
27}
28
29function getLocalVideoPlaylistElementActivityPubUrl (videoPlaylist: MVideoPlaylistUUID, videoPlaylistElement: MVideoPlaylistElement) {
30 return WEBSERVER.URL + '/video-playlists/' + videoPlaylist.uuid + '/videos/' + videoPlaylistElement.id
31}
32
33function getLocalVideoCacheFileActivityPubUrl (videoFile: MVideoFileVideoUUID) {
34 const suffixFPS = videoFile.fps && videoFile.fps !== -1 ? '-' + videoFile.fps : ''
35
36 return `${WEBSERVER.URL}/redundancy/videos/${videoFile.Video.uuid}/${videoFile.resolution}${suffixFPS}`
37}
38
39function getLocalVideoCacheStreamingPlaylistActivityPubUrl (video: MVideoUUID, playlist: MStreamingPlaylist) {
40 return `${WEBSERVER.URL}/redundancy/streaming-playlists/${playlist.getStringType()}/${video.uuid}`
41}
42
43function getLocalVideoCommentActivityPubUrl (video: MVideoUUID, videoComment: MCommentId) {
44 return WEBSERVER.URL + '/videos/watch/' + video.uuid + '/comments/' + videoComment.id
45}
46
47function getLocalVideoChannelActivityPubUrl (videoChannelName: string) {
48 return WEBSERVER.URL + '/video-channels/' + videoChannelName
49}
50
51function getLocalAccountActivityPubUrl (accountName: string) {
52 return WEBSERVER.URL + '/accounts/' + accountName
53}
54
55function getLocalAbuseActivityPubUrl (abuse: MAbuseId) {
56 return WEBSERVER.URL + '/admin/abuses/' + abuse.id
57}
58
59function getLocalVideoViewActivityPubUrl (byActor: MActorUrl, video: MVideoId, viewerIdentifier: string) {
60 return byActor.url + '/views/videos/' + video.id + '/' + viewerIdentifier
61}
62
63function getLocalVideoViewerActivityPubUrl (stats: MLocalVideoViewer) {
64 return WEBSERVER.URL + '/videos/local-viewer/' + stats.uuid
65}
66
67function getVideoLikeActivityPubUrlByLocalActor (byActor: MActorUrl, video: MVideoId) {
68 return byActor.url + '/likes/' + video.id
69}
70
71function getVideoDislikeActivityPubUrlByLocalActor (byActor: MActorUrl, video: MVideoId) {
72 return byActor.url + '/dislikes/' + video.id
73}
74
75function getLocalVideoSharesActivityPubUrl (video: MVideoUrl) {
76 return video.url + '/announces'
77}
78
79function getLocalVideoCommentsActivityPubUrl (video: MVideoUrl) {
80 return video.url + '/comments'
81}
82
83function getLocalVideoLikesActivityPubUrl (video: MVideoUrl) {
84 return video.url + '/likes'
85}
86
87function getLocalVideoDislikesActivityPubUrl (video: MVideoUrl) {
88 return video.url + '/dislikes'
89}
90
91function getLocalActorFollowActivityPubUrl (follower: MActor, following: MActorId) {
92 return follower.url + '/follows/' + following.id
93}
94
95function getLocalActorFollowAcceptActivityPubUrl (actorFollow: MActorFollow) {
96 return WEBSERVER.URL + '/accepts/follows/' + actorFollow.id
97}
98
99function getLocalActorFollowRejectActivityPubUrl () {
100 return WEBSERVER.URL + '/rejects/follows/' + new Date().toISOString()
101}
102
103function getLocalVideoAnnounceActivityPubUrl (byActor: MActorId, video: MVideoUrl) {
104 return video.url + '/announces/' + byActor.id
105}
106
107function getDeleteActivityPubUrl (originalUrl: string) {
108 return originalUrl + '/delete'
109}
110
111function getUpdateActivityPubUrl (originalUrl: string, updatedAt: string) {
112 return originalUrl + '/updates/' + updatedAt
113}
114
115function getUndoActivityPubUrl (originalUrl: string) {
116 return originalUrl + '/undo'
117}
118
119// ---------------------------------------------------------------------------
120
121function getAbuseTargetUrl (abuse: MAbuseFull) {
122 return abuse.VideoAbuse?.Video?.url ||
123 abuse.VideoCommentAbuse?.VideoComment?.url ||
124 abuse.FlaggedAccount.Actor.url
125}
126
127// ---------------------------------------------------------------------------
128
129function buildRemoteVideoBaseUrl (video: MVideoWithHost, path: string, scheme?: string) {
130 if (!scheme) scheme = REMOTE_SCHEME.HTTP
131
132 const host = video.VideoChannel.Actor.Server.host
133
134 return scheme + '://' + host + path
135}
136
137// ---------------------------------------------------------------------------
138
139function checkUrlsSameHost (url1: string, url2: string) {
140 const idHost = new URL(url1).host
141 const actorHost = new URL(url2).host
142
143 return idHost && actorHost && idHost.toLowerCase() === actorHost.toLowerCase()
144}
145
146// ---------------------------------------------------------------------------
147
148export {
149 getLocalVideoActivityPubUrl,
150 getLocalVideoPlaylistActivityPubUrl,
151 getLocalVideoPlaylistElementActivityPubUrl,
152 getLocalVideoCacheFileActivityPubUrl,
153 getLocalVideoCacheStreamingPlaylistActivityPubUrl,
154 getLocalVideoCommentActivityPubUrl,
155 getLocalVideoChannelActivityPubUrl,
156 getLocalAccountActivityPubUrl,
157 getLocalAbuseActivityPubUrl,
158 getLocalActorFollowActivityPubUrl,
159 getLocalActorFollowAcceptActivityPubUrl,
160 getLocalVideoAnnounceActivityPubUrl,
161 getUpdateActivityPubUrl,
162 getUndoActivityPubUrl,
163 getVideoLikeActivityPubUrlByLocalActor,
164 getLocalVideoViewActivityPubUrl,
165 getVideoDislikeActivityPubUrlByLocalActor,
166 getLocalActorFollowRejectActivityPubUrl,
167 getDeleteActivityPubUrl,
168 getLocalVideoSharesActivityPubUrl,
169 getLocalVideoCommentsActivityPubUrl,
170 getLocalVideoLikesActivityPubUrl,
171 getLocalVideoDislikesActivityPubUrl,
172 getLocalVideoViewerActivityPubUrl,
173
174 getAbuseTargetUrl,
175 checkUrlsSameHost,
176 buildRemoteVideoBaseUrl
177}
diff --git a/server/lib/activitypub/video-comments.ts b/server/lib/activitypub/video-comments.ts
deleted file mode 100644
index b861be5bd..000000000
--- a/server/lib/activitypub/video-comments.ts
+++ /dev/null
@@ -1,205 +0,0 @@
1import { map } from 'bluebird'
2
3import { sanitizeAndCheckVideoCommentObject } from '../../helpers/custom-validators/activitypub/video-comments'
4import { logger } from '../../helpers/logger'
5import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
6import { VideoCommentModel } from '../../models/video/video-comment'
7import { MComment, MCommentOwner, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../types/models/video'
8import { isRemoteVideoCommentAccepted } from '../moderation'
9import { Hooks } from '../plugins/hooks'
10import { fetchAP } from './activity'
11import { getOrCreateAPActor } from './actors'
12import { checkUrlsSameHost } from './url'
13import { getOrCreateAPVideo } from './videos'
14
15type ResolveThreadParams = {
16 url: string
17 comments?: MCommentOwner[]
18 isVideo?: boolean
19 commentCreated?: boolean
20}
21type ResolveThreadResult = Promise<{ video: MVideoAccountLightBlacklistAllFiles, comment: MCommentOwnerVideo, commentCreated: boolean }>
22
23async function addVideoComments (commentUrls: string[]) {
24 return map(commentUrls, async commentUrl => {
25 try {
26 await resolveThread({ url: commentUrl, isVideo: false })
27 } catch (err) {
28 logger.warn('Cannot resolve thread %s.', commentUrl, { err })
29 }
30 }, { concurrency: CRAWL_REQUEST_CONCURRENCY })
31}
32
33async function resolveThread (params: ResolveThreadParams): ResolveThreadResult {
34 const { url, isVideo } = params
35
36 if (params.commentCreated === undefined) params.commentCreated = false
37 if (params.comments === undefined) params.comments = []
38
39 // If it is not a video, or if we don't know if it's a video, try to get the thread from DB
40 if (isVideo === false || isVideo === undefined) {
41 const result = await resolveCommentFromDB(params)
42 if (result) return result
43 }
44
45 try {
46 // If it is a video, or if we don't know if it's a video
47 if (isVideo === true || isVideo === undefined) {
48 // Keep await so we catch the exception
49 return await tryToResolveThreadFromVideo(params)
50 }
51 } catch (err) {
52 logger.debug('Cannot resolve thread from video %s, maybe because it was not a video', url, { err })
53 }
54
55 return resolveRemoteParentComment(params)
56}
57
58export {
59 addVideoComments,
60 resolveThread
61}
62
63// ---------------------------------------------------------------------------
64
65async function resolveCommentFromDB (params: ResolveThreadParams) {
66 const { url, comments, commentCreated } = params
67
68 const commentFromDatabase = await VideoCommentModel.loadByUrlAndPopulateReplyAndVideoUrlAndAccount(url)
69 if (!commentFromDatabase) return undefined
70
71 let parentComments = comments.concat([ commentFromDatabase ])
72
73 // Speed up things and resolve directly the thread
74 if (commentFromDatabase.InReplyToVideoComment) {
75 const data = await VideoCommentModel.listThreadParentComments(commentFromDatabase, undefined, 'DESC')
76
77 parentComments = parentComments.concat(data)
78 }
79
80 return resolveThread({
81 url: commentFromDatabase.Video.url,
82 comments: parentComments,
83 isVideo: true,
84 commentCreated
85 })
86}
87
88async function tryToResolveThreadFromVideo (params: ResolveThreadParams) {
89 const { url, comments, commentCreated } = params
90
91 // Maybe it's a reply to a video?
92 // If yes, it's done: we resolved all the thread
93 const syncParam = { rates: true, shares: true, comments: false, refreshVideo: false }
94 const { video } = await getOrCreateAPVideo({ videoObject: url, syncParam })
95
96 if (video.isOwned() && !video.hasPrivacyForFederation()) {
97 throw new Error('Cannot resolve thread of video with privacy that is not compatible with federation')
98 }
99
100 let resultComment: MCommentOwnerVideo
101 if (comments.length !== 0) {
102 const firstReply = comments[comments.length - 1] as MCommentOwnerVideo
103 firstReply.inReplyToCommentId = null
104 firstReply.originCommentId = null
105 firstReply.videoId = video.id
106 firstReply.changed('updatedAt', true)
107 firstReply.Video = video
108
109 if (await isRemoteCommentAccepted(firstReply) !== true) {
110 return undefined
111 }
112
113 comments[comments.length - 1] = await firstReply.save()
114
115 for (let i = comments.length - 2; i >= 0; i--) {
116 const comment = comments[i] as MCommentOwnerVideo
117 comment.originCommentId = firstReply.id
118 comment.inReplyToCommentId = comments[i + 1].id
119 comment.videoId = video.id
120 comment.changed('updatedAt', true)
121 comment.Video = video
122
123 if (await isRemoteCommentAccepted(comment) !== true) {
124 return undefined
125 }
126
127 comments[i] = await comment.save()
128 }
129
130 resultComment = comments[0] as MCommentOwnerVideo
131 }
132
133 return { video, comment: resultComment, commentCreated }
134}
135
136async function resolveRemoteParentComment (params: ResolveThreadParams) {
137 const { url, comments } = params
138
139 if (comments.length > ACTIVITY_PUB.MAX_RECURSION_COMMENTS) {
140 throw new Error('Recursion limit reached when resolving a thread')
141 }
142
143 const { body } = await fetchAP<any>(url)
144
145 if (sanitizeAndCheckVideoCommentObject(body) === false) {
146 throw new Error(`Remote video comment JSON ${url} is not valid:` + JSON.stringify(body))
147 }
148
149 const actorUrl = body.attributedTo
150 if (!actorUrl && body.type !== 'Tombstone') throw new Error('Miss attributed to in comment')
151
152 if (actorUrl && checkUrlsSameHost(url, actorUrl) !== true) {
153 throw new Error(`Actor url ${actorUrl} has not the same host than the comment url ${url}`)
154 }
155
156 if (checkUrlsSameHost(body.id, url) !== true) {
157 throw new Error(`Comment url ${url} host is different from the AP object id ${body.id}`)
158 }
159
160 const actor = actorUrl
161 ? await getOrCreateAPActor(actorUrl, 'all')
162 : null
163
164 const comment = new VideoCommentModel({
165 url: body.id,
166 text: body.content ? body.content : '',
167 videoId: null,
168 accountId: actor ? actor.Account.id : null,
169 inReplyToCommentId: null,
170 originCommentId: null,
171 createdAt: new Date(body.published),
172 updatedAt: new Date(body.updated),
173 deletedAt: body.deleted ? new Date(body.deleted) : null
174 }) as MCommentOwner
175 comment.Account = actor ? actor.Account : null
176
177 return resolveThread({
178 url: body.inReplyTo,
179 comments: comments.concat([ comment ]),
180 commentCreated: true
181 })
182}
183
184async function isRemoteCommentAccepted (comment: MComment) {
185 // Already created
186 if (comment.id) return true
187
188 const acceptParameters = {
189 comment
190 }
191
192 const acceptedResult = await Hooks.wrapFun(
193 isRemoteVideoCommentAccepted,
194 acceptParameters,
195 'filter:activity-pub.remote-video-comment.create.accept.result'
196 )
197
198 if (!acceptedResult || acceptedResult.accepted !== true) {
199 logger.info('Refused to create a remote comment.', { acceptedResult, acceptParameters })
200
201 return false
202 }
203
204 return true
205}
diff --git a/server/lib/activitypub/video-rates.ts b/server/lib/activitypub/video-rates.ts
deleted file mode 100644
index 2e7920f4e..000000000
--- a/server/lib/activitypub/video-rates.ts
+++ /dev/null
@@ -1,59 +0,0 @@
1import { Transaction } from 'sequelize'
2import { VideoRateType } from '../../../shared/models/videos'
3import { MAccountActor, MActorUrl, MVideoAccountLight, MVideoFullLight, MVideoId } from '../../types/models'
4import { sendLike, sendUndoDislike, sendUndoLike } from './send'
5import { sendDislike } from './send/send-dislike'
6import { getVideoDislikeActivityPubUrlByLocalActor, getVideoLikeActivityPubUrlByLocalActor } from './url'
7import { federateVideoIfNeeded } from './videos'
8
9async function sendVideoRateChange (
10 account: MAccountActor,
11 video: MVideoFullLight,
12 likes: number,
13 dislikes: number,
14 t: Transaction
15) {
16 if (video.isOwned()) return federateVideoIfNeeded(video, false, t)
17
18 return sendVideoRateChangeToOrigin(account, video, likes, dislikes, t)
19}
20
21function getLocalRateUrl (rateType: VideoRateType, actor: MActorUrl, video: MVideoId) {
22 return rateType === 'like'
23 ? getVideoLikeActivityPubUrlByLocalActor(actor, video)
24 : getVideoDislikeActivityPubUrlByLocalActor(actor, video)
25}
26
27// ---------------------------------------------------------------------------
28
29export {
30 getLocalRateUrl,
31 sendVideoRateChange
32}
33
34// ---------------------------------------------------------------------------
35
36async function sendVideoRateChangeToOrigin (
37 account: MAccountActor,
38 video: MVideoAccountLight,
39 likes: number,
40 dislikes: number,
41 t: Transaction
42) {
43 // Local video, we don't need to send like
44 if (video.isOwned()) return
45
46 const actor = account.Actor
47
48 // Keep the order: first we undo and then we create
49
50 // Undo Like
51 if (likes < 0) await sendUndoLike(actor, video, t)
52 // Undo Dislike
53 if (dislikes < 0) await sendUndoDislike(actor, video, t)
54
55 // Like
56 if (likes > 0) await sendLike(actor, video, t)
57 // Dislike
58 if (dislikes > 0) await sendDislike(actor, video, t)
59}
diff --git a/server/lib/activitypub/videos/federate.ts b/server/lib/activitypub/videos/federate.ts
deleted file mode 100644
index d7e251153..000000000
--- a/server/lib/activitypub/videos/federate.ts
+++ /dev/null
@@ -1,29 +0,0 @@
1import { Transaction } from 'sequelize/types'
2import { MVideoAP, MVideoAPLight } from '@server/types/models'
3import { sendCreateVideo, sendUpdateVideo } from '../send'
4import { shareVideoByServerAndChannel } from '../share'
5
6async function federateVideoIfNeeded (videoArg: MVideoAPLight, isNewVideo: boolean, transaction?: Transaction) {
7 const video = videoArg as MVideoAP
8
9 if (
10 // Check this is not a blacklisted video, or unfederated blacklisted video
11 (video.isBlacklisted() === false || (isNewVideo === false && video.VideoBlacklist.unfederated === false)) &&
12 // Check the video is public/unlisted and published
13 video.hasPrivacyForFederation() && video.hasStateForFederation()
14 ) {
15 const video = await videoArg.lightAPToFullAP(transaction)
16
17 if (isNewVideo) {
18 // Now we'll add the video's meta data to our followers
19 await sendCreateVideo(video, transaction)
20 await shareVideoByServerAndChannel(video, transaction)
21 } else {
22 await sendUpdateVideo(video, transaction)
23 }
24 }
25}
26
27export {
28 federateVideoIfNeeded
29}
diff --git a/server/lib/activitypub/videos/get.ts b/server/lib/activitypub/videos/get.ts
deleted file mode 100644
index 288c506ee..000000000
--- a/server/lib/activitypub/videos/get.ts
+++ /dev/null
@@ -1,116 +0,0 @@
1import { retryTransactionWrapper } from '@server/helpers/database-utils'
2import { logger } from '@server/helpers/logger'
3import { JobQueue } from '@server/lib/job-queue'
4import { loadVideoByUrl, VideoLoadByUrlType } from '@server/lib/model-loaders'
5import { MVideoAccountLightBlacklistAllFiles, MVideoImmutable, MVideoThumbnail } from '@server/types/models'
6import { APObjectId } from '@shared/models'
7import { getAPId } from '../activity'
8import { refreshVideoIfNeeded } from './refresh'
9import { APVideoCreator, fetchRemoteVideo, SyncParam, syncVideoExternalAttributes } from './shared'
10
11type GetVideoResult <T> = Promise<{
12 video: T
13 created: boolean
14 autoBlacklisted?: boolean
15}>
16
17type GetVideoParamAll = {
18 videoObject: APObjectId
19 syncParam?: SyncParam
20 fetchType?: 'all'
21 allowRefresh?: boolean
22}
23
24type GetVideoParamImmutable = {
25 videoObject: APObjectId
26 syncParam?: SyncParam
27 fetchType: 'only-immutable-attributes'
28 allowRefresh: false
29}
30
31type GetVideoParamOther = {
32 videoObject: APObjectId
33 syncParam?: SyncParam
34 fetchType?: 'all' | 'only-video'
35 allowRefresh?: boolean
36}
37
38function getOrCreateAPVideo (options: GetVideoParamAll): GetVideoResult<MVideoAccountLightBlacklistAllFiles>
39function getOrCreateAPVideo (options: GetVideoParamImmutable): GetVideoResult<MVideoImmutable>
40function getOrCreateAPVideo (options: GetVideoParamOther): GetVideoResult<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail>
41
42async function getOrCreateAPVideo (
43 options: GetVideoParamAll | GetVideoParamImmutable | GetVideoParamOther
44): GetVideoResult<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail | MVideoImmutable> {
45 // Default params
46 const syncParam = options.syncParam || { rates: true, shares: true, comments: true, refreshVideo: false }
47 const fetchType = options.fetchType || 'all'
48 const allowRefresh = options.allowRefresh !== false
49
50 // Get video url
51 const videoUrl = getAPId(options.videoObject)
52 let videoFromDatabase = await loadVideoByUrl(videoUrl, fetchType)
53
54 if (videoFromDatabase) {
55 if (allowRefresh === true) {
56 // Typings ensure allowRefresh === false in only-immutable-attributes fetch type
57 videoFromDatabase = await scheduleRefresh(videoFromDatabase as MVideoThumbnail, fetchType, syncParam)
58 }
59
60 return { video: videoFromDatabase, created: false }
61 }
62
63 const { videoObject } = await fetchRemoteVideo(videoUrl)
64 if (!videoObject) throw new Error('Cannot fetch remote video with url: ' + videoUrl)
65
66 // videoUrl is just an alias/rediraction, so process object id instead
67 if (videoObject.id !== videoUrl) return getOrCreateAPVideo({ ...options, fetchType: 'all', videoObject })
68
69 try {
70 const creator = new APVideoCreator(videoObject)
71 const { autoBlacklisted, videoCreated } = await retryTransactionWrapper(creator.create.bind(creator))
72
73 await syncVideoExternalAttributes(videoCreated, videoObject, syncParam)
74
75 return { video: videoCreated, created: true, autoBlacklisted }
76 } catch (err) {
77 // Maybe a concurrent getOrCreateAPVideo call created this video
78 if (err.name === 'SequelizeUniqueConstraintError') {
79 const alreadyCreatedVideo = await loadVideoByUrl(videoUrl, fetchType)
80 if (alreadyCreatedVideo) return { video: alreadyCreatedVideo, created: false }
81
82 logger.error('Cannot create video %s because of SequelizeUniqueConstraintError error, but cannot find it in database.', videoUrl)
83 }
84
85 throw err
86 }
87}
88
89// ---------------------------------------------------------------------------
90
91export {
92 getOrCreateAPVideo
93}
94
95// ---------------------------------------------------------------------------
96
97async function scheduleRefresh (video: MVideoThumbnail, fetchType: VideoLoadByUrlType, syncParam: SyncParam) {
98 if (!video.isOutdated()) return video
99
100 const refreshOptions = {
101 video,
102 fetchedType: fetchType,
103 syncParam
104 }
105
106 if (syncParam.refreshVideo === true) {
107 return refreshVideoIfNeeded(refreshOptions)
108 }
109
110 await JobQueue.Instance.createJob({
111 type: 'activitypub-refresher',
112 payload: { type: 'video', url: video.url }
113 })
114
115 return video
116}
diff --git a/server/lib/activitypub/videos/index.ts b/server/lib/activitypub/videos/index.ts
deleted file mode 100644
index b22062598..000000000
--- a/server/lib/activitypub/videos/index.ts
+++ /dev/null
@@ -1,4 +0,0 @@
1export * from './federate'
2export * from './get'
3export * from './refresh'
4export * from './updater'
diff --git a/server/lib/activitypub/videos/refresh.ts b/server/lib/activitypub/videos/refresh.ts
deleted file mode 100644
index 9f952a218..000000000
--- a/server/lib/activitypub/videos/refresh.ts
+++ /dev/null
@@ -1,68 +0,0 @@
1import { logger, loggerTagsFactory } from '@server/helpers/logger'
2import { PeerTubeRequestError } from '@server/helpers/requests'
3import { VideoLoadByUrlType } from '@server/lib/model-loaders'
4import { VideoModel } from '@server/models/video/video'
5import { MVideoAccountLightBlacklistAllFiles, MVideoThumbnail } from '@server/types/models'
6import { HttpStatusCode } from '@shared/models'
7import { ActorFollowHealthCache } from '../../actor-follow-health-cache'
8import { fetchRemoteVideo, SyncParam, syncVideoExternalAttributes } from './shared'
9import { APVideoUpdater } from './updater'
10
11async function refreshVideoIfNeeded (options: {
12 video: MVideoThumbnail
13 fetchedType: VideoLoadByUrlType
14 syncParam: SyncParam
15}): Promise<MVideoThumbnail> {
16 if (!options.video.isOutdated()) return options.video
17
18 // We need more attributes if the argument video was fetched with not enough joints
19 const video = options.fetchedType === 'all'
20 ? options.video as MVideoAccountLightBlacklistAllFiles
21 : await VideoModel.loadByUrlAndPopulateAccount(options.video.url)
22
23 const lTags = loggerTagsFactory('ap', 'video', 'refresh', video.uuid, video.url)
24
25 logger.info('Refreshing video %s.', video.url, lTags())
26
27 try {
28 const { videoObject } = await fetchRemoteVideo(video.url)
29
30 if (videoObject === undefined) {
31 logger.warn('Cannot refresh remote video %s: invalid body.', video.url, lTags())
32
33 await video.setAsRefreshed()
34 return video
35 }
36
37 const videoUpdater = new APVideoUpdater(videoObject, video)
38 await videoUpdater.update()
39
40 await syncVideoExternalAttributes(video, videoObject, options.syncParam)
41
42 ActorFollowHealthCache.Instance.addGoodServerId(video.VideoChannel.Actor.serverId)
43
44 return video
45 } catch (err) {
46 if ((err as PeerTubeRequestError).statusCode === HttpStatusCode.NOT_FOUND_404) {
47 logger.info('Cannot refresh remote video %s: video does not exist anymore. Deleting it.', video.url, lTags())
48
49 // Video does not exist anymore
50 await video.destroy()
51 return undefined
52 }
53
54 logger.warn('Cannot refresh video %s.', options.video.url, { err, ...lTags() })
55
56 ActorFollowHealthCache.Instance.addBadServerId(video.VideoChannel.Actor.serverId)
57
58 // Don't refresh in loop
59 await video.setAsRefreshed()
60 return video
61 }
62}
63
64// ---------------------------------------------------------------------------
65
66export {
67 refreshVideoIfNeeded
68}
diff --git a/server/lib/activitypub/videos/shared/abstract-builder.ts b/server/lib/activitypub/videos/shared/abstract-builder.ts
deleted file mode 100644
index 98c2f58eb..000000000
--- a/server/lib/activitypub/videos/shared/abstract-builder.ts
+++ /dev/null
@@ -1,190 +0,0 @@
1import { CreationAttributes, Transaction } from 'sequelize/types'
2import { deleteAllModels, filterNonExistingModels } from '@server/helpers/database-utils'
3import { logger, LoggerTagsFn } from '@server/helpers/logger'
4import { updateRemoteVideoThumbnail } from '@server/lib/thumbnail'
5import { setVideoTags } from '@server/lib/video'
6import { StoryboardModel } from '@server/models/video/storyboard'
7import { VideoCaptionModel } from '@server/models/video/video-caption'
8import { VideoFileModel } from '@server/models/video/video-file'
9import { VideoLiveModel } from '@server/models/video/video-live'
10import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist'
11import {
12 MStreamingPlaylistFiles,
13 MStreamingPlaylistFilesVideo,
14 MVideoCaption,
15 MVideoFile,
16 MVideoFullLight,
17 MVideoThumbnail
18} from '@server/types/models'
19import { ActivityTagObject, ThumbnailType, VideoObject, VideoStreamingPlaylistType } from '@shared/models'
20import { findOwner, getOrCreateAPActor } from '../../actors'
21import {
22 getCaptionAttributesFromObject,
23 getFileAttributesFromUrl,
24 getLiveAttributesFromObject,
25 getPreviewFromIcons,
26 getStoryboardAttributeFromObject,
27 getStreamingPlaylistAttributesFromObject,
28 getTagsFromObject,
29 getThumbnailFromIcons
30} from './object-to-model-attributes'
31import { getTrackerUrls, setVideoTrackers } from './trackers'
32
33export abstract class APVideoAbstractBuilder {
34 protected abstract videoObject: VideoObject
35 protected abstract lTags: LoggerTagsFn
36
37 protected async getOrCreateVideoChannelFromVideoObject () {
38 const channel = await findOwner(this.videoObject.id, this.videoObject.attributedTo, 'Group')
39 if (!channel) throw new Error('Cannot find associated video channel to video ' + this.videoObject.url)
40
41 return getOrCreateAPActor(channel.id, 'all')
42 }
43
44 protected async setThumbnail (video: MVideoThumbnail, t?: Transaction) {
45 const miniatureIcon = getThumbnailFromIcons(this.videoObject)
46 if (!miniatureIcon) {
47 logger.warn('Cannot find thumbnail in video object', { object: this.videoObject })
48 return undefined
49 }
50
51 const miniatureModel = updateRemoteVideoThumbnail({
52 fileUrl: miniatureIcon.url,
53 video,
54 type: ThumbnailType.MINIATURE,
55 size: miniatureIcon,
56 onDisk: false // Lazy download remote thumbnails
57 })
58
59 await video.addAndSaveThumbnail(miniatureModel, t)
60 }
61
62 protected async setPreview (video: MVideoFullLight, t?: Transaction) {
63 const previewIcon = getPreviewFromIcons(this.videoObject)
64 if (!previewIcon) return
65
66 const previewModel = updateRemoteVideoThumbnail({
67 fileUrl: previewIcon.url,
68 video,
69 type: ThumbnailType.PREVIEW,
70 size: previewIcon,
71 onDisk: false // Lazy download remote previews
72 })
73
74 await video.addAndSaveThumbnail(previewModel, t)
75 }
76
77 protected async setTags (video: MVideoFullLight, t: Transaction) {
78 const tags = getTagsFromObject(this.videoObject)
79 await setVideoTags({ video, tags, transaction: t })
80 }
81
82 protected async setTrackers (video: MVideoFullLight, t: Transaction) {
83 const trackers = getTrackerUrls(this.videoObject, video)
84 await setVideoTrackers({ video, trackers, transaction: t })
85 }
86
87 protected async insertOrReplaceCaptions (video: MVideoFullLight, t: Transaction) {
88 const existingCaptions = await VideoCaptionModel.listVideoCaptions(video.id, t)
89
90 let captionsToCreate = getCaptionAttributesFromObject(video, this.videoObject)
91 .map(a => new VideoCaptionModel(a) as MVideoCaption)
92
93 for (const existingCaption of existingCaptions) {
94 // Only keep captions that do not already exist
95 const filtered = captionsToCreate.filter(c => !c.isEqual(existingCaption))
96
97 // This caption already exists, we don't need to destroy and create it
98 if (filtered.length !== captionsToCreate.length) {
99 captionsToCreate = filtered
100 continue
101 }
102
103 // Destroy this caption that does not exist anymore
104 await existingCaption.destroy({ transaction: t })
105 }
106
107 for (const captionToCreate of captionsToCreate) {
108 await captionToCreate.save({ transaction: t })
109 }
110 }
111
112 protected async insertOrReplaceStoryboard (video: MVideoFullLight, t: Transaction) {
113 const existingStoryboard = await StoryboardModel.loadByVideo(video.id, t)
114 if (existingStoryboard) await existingStoryboard.destroy({ transaction: t })
115
116 const storyboardAttributes = getStoryboardAttributeFromObject(video, this.videoObject)
117 if (!storyboardAttributes) return
118
119 return StoryboardModel.create(storyboardAttributes, { transaction: t })
120 }
121
122 protected async insertOrReplaceLive (video: MVideoFullLight, transaction: Transaction) {
123 const attributes = getLiveAttributesFromObject(video, this.videoObject)
124 const [ videoLive ] = await VideoLiveModel.upsert(attributes, { transaction, returning: true })
125
126 video.VideoLive = videoLive
127 }
128
129 protected async setWebVideoFiles (video: MVideoFullLight, t: Transaction) {
130 const videoFileAttributes = getFileAttributesFromUrl(video, this.videoObject.url)
131 const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a))
132
133 // Remove video files that do not exist anymore
134 await deleteAllModels(filterNonExistingModels(video.VideoFiles || [], newVideoFiles), t)
135
136 // Update or add other one
137 const upsertTasks = newVideoFiles.map(f => VideoFileModel.customUpsert(f, 'video', t))
138 video.VideoFiles = await Promise.all(upsertTasks)
139 }
140
141 protected async setStreamingPlaylists (video: MVideoFullLight, t: Transaction) {
142 const streamingPlaylistAttributes = getStreamingPlaylistAttributesFromObject(video, this.videoObject)
143 const newStreamingPlaylists = streamingPlaylistAttributes.map(a => new VideoStreamingPlaylistModel(a))
144
145 // Remove video playlists that do not exist anymore
146 await deleteAllModels(filterNonExistingModels(video.VideoStreamingPlaylists || [], newStreamingPlaylists), t)
147
148 const oldPlaylists = video.VideoStreamingPlaylists
149 video.VideoStreamingPlaylists = []
150
151 for (const playlistAttributes of streamingPlaylistAttributes) {
152 const streamingPlaylistModel = await this.insertOrReplaceStreamingPlaylist(playlistAttributes, t)
153 streamingPlaylistModel.Video = video
154
155 await this.setStreamingPlaylistFiles(oldPlaylists, streamingPlaylistModel, playlistAttributes.tagAPObject, t)
156
157 video.VideoStreamingPlaylists.push(streamingPlaylistModel)
158 }
159 }
160
161 private async insertOrReplaceStreamingPlaylist (attributes: CreationAttributes<VideoStreamingPlaylistModel>, t: Transaction) {
162 const [ streamingPlaylist ] = await VideoStreamingPlaylistModel.upsert(attributes, { returning: true, transaction: t })
163
164 return streamingPlaylist as MStreamingPlaylistFilesVideo
165 }
166
167 private getStreamingPlaylistFiles (oldPlaylists: MStreamingPlaylistFiles[], type: VideoStreamingPlaylistType) {
168 const playlist = oldPlaylists.find(s => s.type === type)
169 if (!playlist) return []
170
171 return playlist.VideoFiles
172 }
173
174 private async setStreamingPlaylistFiles (
175 oldPlaylists: MStreamingPlaylistFiles[],
176 playlistModel: MStreamingPlaylistFilesVideo,
177 tagObjects: ActivityTagObject[],
178 t: Transaction
179 ) {
180 const oldStreamingPlaylistFiles = this.getStreamingPlaylistFiles(oldPlaylists || [], playlistModel.type)
181
182 const newVideoFiles: MVideoFile[] = getFileAttributesFromUrl(playlistModel, tagObjects).map(a => new VideoFileModel(a))
183
184 await deleteAllModels(filterNonExistingModels(oldStreamingPlaylistFiles, newVideoFiles), t)
185
186 // Update or add other one
187 const upsertTasks = newVideoFiles.map(f => VideoFileModel.customUpsert(f, 'streaming-playlist', t))
188 playlistModel.VideoFiles = await Promise.all(upsertTasks)
189 }
190}
diff --git a/server/lib/activitypub/videos/shared/creator.ts b/server/lib/activitypub/videos/shared/creator.ts
deleted file mode 100644
index e44fd0d52..000000000
--- a/server/lib/activitypub/videos/shared/creator.ts
+++ /dev/null
@@ -1,65 +0,0 @@
1
2import { logger, loggerTagsFactory, LoggerTagsFn } from '@server/helpers/logger'
3import { sequelizeTypescript } from '@server/initializers/database'
4import { Hooks } from '@server/lib/plugins/hooks'
5import { autoBlacklistVideoIfNeeded } from '@server/lib/video-blacklist'
6import { VideoModel } from '@server/models/video/video'
7import { MVideoFullLight, MVideoThumbnail } from '@server/types/models'
8import { VideoObject } from '@shared/models'
9import { APVideoAbstractBuilder } from './abstract-builder'
10import { getVideoAttributesFromObject } from './object-to-model-attributes'
11
12export class APVideoCreator extends APVideoAbstractBuilder {
13 protected lTags: LoggerTagsFn
14
15 constructor (protected readonly videoObject: VideoObject) {
16 super()
17
18 this.lTags = loggerTagsFactory('ap', 'video', 'create', this.videoObject.uuid, this.videoObject.id)
19 }
20
21 async create () {
22 logger.debug('Adding remote video %s.', this.videoObject.id, this.lTags())
23
24 const channelActor = await this.getOrCreateVideoChannelFromVideoObject()
25 const channel = channelActor.VideoChannel
26
27 const videoData = getVideoAttributesFromObject(channel, this.videoObject, this.videoObject.to)
28 const video = VideoModel.build({ ...videoData, likes: 0, dislikes: 0 }) as MVideoThumbnail
29
30 const { autoBlacklisted, videoCreated } = await sequelizeTypescript.transaction(async t => {
31 const videoCreated = await video.save({ transaction: t }) as MVideoFullLight
32 videoCreated.VideoChannel = channel
33
34 await this.setThumbnail(videoCreated, t)
35 await this.setPreview(videoCreated, t)
36 await this.setWebVideoFiles(videoCreated, t)
37 await this.setStreamingPlaylists(videoCreated, t)
38 await this.setTags(videoCreated, t)
39 await this.setTrackers(videoCreated, t)
40 await this.insertOrReplaceCaptions(videoCreated, t)
41 await this.insertOrReplaceLive(videoCreated, t)
42 await this.insertOrReplaceStoryboard(videoCreated, t)
43
44 // We added a video in this channel, set it as updated
45 await channel.setAsUpdated(t)
46
47 const autoBlacklisted = await autoBlacklistVideoIfNeeded({
48 video: videoCreated,
49 user: undefined,
50 isRemote: true,
51 isNew: true,
52 isNewFile: true,
53 transaction: t
54 })
55
56 logger.info('Remote video with uuid %s inserted.', this.videoObject.uuid, this.lTags())
57
58 Hooks.runAction('action:activity-pub.remote-video.created', { video: videoCreated, videoAPObject: this.videoObject })
59
60 return { autoBlacklisted, videoCreated }
61 })
62
63 return { autoBlacklisted, videoCreated }
64 }
65}
diff --git a/server/lib/activitypub/videos/shared/index.ts b/server/lib/activitypub/videos/shared/index.ts
deleted file mode 100644
index 951403493..000000000
--- a/server/lib/activitypub/videos/shared/index.ts
+++ /dev/null
@@ -1,6 +0,0 @@
1export * from './abstract-builder'
2export * from './creator'
3export * from './object-to-model-attributes'
4export * from './trackers'
5export * from './url-to-object'
6export * from './video-sync-attributes'
diff --git a/server/lib/activitypub/videos/shared/object-to-model-attributes.ts b/server/lib/activitypub/videos/shared/object-to-model-attributes.ts
deleted file mode 100644
index 6cbe72e27..000000000
--- a/server/lib/activitypub/videos/shared/object-to-model-attributes.ts
+++ /dev/null
@@ -1,285 +0,0 @@
1import { maxBy, minBy } from 'lodash'
2import { decode as magnetUriDecode } from 'magnet-uri'
3import { basename, extname } from 'path'
4import { isAPVideoFileUrlMetadataObject } from '@server/helpers/custom-validators/activitypub/videos'
5import { isVideoFileInfoHashValid } from '@server/helpers/custom-validators/videos'
6import { logger } from '@server/helpers/logger'
7import { getExtFromMimetype } from '@server/helpers/video'
8import { ACTIVITY_PUB, MIMETYPES, P2P_MEDIA_LOADER_PEER_VERSION, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '@server/initializers/constants'
9import { generateTorrentFileName } from '@server/lib/paths'
10import { VideoCaptionModel } from '@server/models/video/video-caption'
11import { VideoFileModel } from '@server/models/video/video-file'
12import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist'
13import { FilteredModelAttributes } from '@server/types'
14import { isStreamingPlaylist, MChannelId, MStreamingPlaylistVideo, MVideo, MVideoId } from '@server/types/models'
15import {
16 ActivityHashTagObject,
17 ActivityMagnetUrlObject,
18 ActivityPlaylistSegmentHashesObject,
19 ActivityPlaylistUrlObject,
20 ActivityTagObject,
21 ActivityUrlObject,
22 ActivityVideoUrlObject,
23 VideoObject,
24 VideoPrivacy,
25 VideoStreamingPlaylistType
26} from '@shared/models'
27import { getDurationFromActivityStream } from '../../activity'
28import { isArray } from '@server/helpers/custom-validators/misc'
29import { generateImageFilename } from '@server/helpers/image-utils'
30import { arrayify } from '@shared/core-utils'
31
32function getThumbnailFromIcons (videoObject: VideoObject) {
33 let validIcons = videoObject.icon.filter(i => i.width > THUMBNAILS_SIZE.minWidth)
34 // Fallback if there are not valid icons
35 if (validIcons.length === 0) validIcons = videoObject.icon
36
37 return minBy(validIcons, 'width')
38}
39
40function getPreviewFromIcons (videoObject: VideoObject) {
41 const validIcons = videoObject.icon.filter(i => i.width > PREVIEWS_SIZE.minWidth)
42
43 return maxBy(validIcons, 'width')
44}
45
46function getTagsFromObject (videoObject: VideoObject) {
47 return videoObject.tag
48 .filter(isAPHashTagObject)
49 .map(t => t.name)
50}
51
52function getFileAttributesFromUrl (
53 videoOrPlaylist: MVideo | MStreamingPlaylistVideo,
54 urls: (ActivityTagObject | ActivityUrlObject)[]
55) {
56 const fileUrls = urls.filter(u => isAPVideoUrlObject(u)) as ActivityVideoUrlObject[]
57
58 if (fileUrls.length === 0) return []
59
60 const attributes: FilteredModelAttributes<VideoFileModel>[] = []
61 for (const fileUrl of fileUrls) {
62 // Fetch associated magnet uri
63 const magnet = urls.filter(isAPMagnetUrlObject)
64 .find(u => u.height === fileUrl.height)
65
66 if (!magnet) throw new Error('Cannot find associated magnet uri for file ' + fileUrl.href)
67
68 const parsed = magnetUriDecode(magnet.href)
69 if (!parsed || isVideoFileInfoHashValid(parsed.infoHash) === false) {
70 throw new Error('Cannot parse magnet URI ' + magnet.href)
71 }
72
73 const torrentUrl = Array.isArray(parsed.xs)
74 ? parsed.xs[0]
75 : parsed.xs
76
77 // Fetch associated metadata url, if any
78 const metadata = urls.filter(isAPVideoFileUrlMetadataObject)
79 .find(u => {
80 return u.height === fileUrl.height &&
81 u.fps === fileUrl.fps &&
82 u.rel.includes(fileUrl.mediaType)
83 })
84
85 const extname = getExtFromMimetype(MIMETYPES.VIDEO.MIMETYPE_EXT, fileUrl.mediaType)
86 const resolution = fileUrl.height
87 const videoId = isStreamingPlaylist(videoOrPlaylist) ? null : videoOrPlaylist.id
88 const videoStreamingPlaylistId = isStreamingPlaylist(videoOrPlaylist) ? videoOrPlaylist.id : null
89
90 const attribute = {
91 extname,
92 infoHash: parsed.infoHash,
93 resolution,
94 size: fileUrl.size,
95 fps: fileUrl.fps || -1,
96 metadataUrl: metadata?.href,
97
98 // Use the name of the remote file because we don't proxify video file requests
99 filename: basename(fileUrl.href),
100 fileUrl: fileUrl.href,
101
102 torrentUrl,
103 // Use our own torrent name since we proxify torrent requests
104 torrentFilename: generateTorrentFileName(videoOrPlaylist, resolution),
105
106 // This is a video file owned by a video or by a streaming playlist
107 videoId,
108 videoStreamingPlaylistId
109 }
110
111 attributes.push(attribute)
112 }
113
114 return attributes
115}
116
117function getStreamingPlaylistAttributesFromObject (video: MVideoId, videoObject: VideoObject) {
118 const playlistUrls = videoObject.url.filter(u => isAPStreamingPlaylistUrlObject(u)) as ActivityPlaylistUrlObject[]
119 if (playlistUrls.length === 0) return []
120
121 const attributes: (FilteredModelAttributes<VideoStreamingPlaylistModel> & { tagAPObject?: ActivityTagObject[] })[] = []
122 for (const playlistUrlObject of playlistUrls) {
123 const segmentsSha256UrlObject = playlistUrlObject.tag.find(isAPPlaylistSegmentHashesUrlObject)
124
125 const files: unknown[] = playlistUrlObject.tag.filter(u => isAPVideoUrlObject(u)) as ActivityVideoUrlObject[]
126
127 if (!segmentsSha256UrlObject) {
128 logger.warn('No segment sha256 URL found in AP playlist object.', { playlistUrl: playlistUrlObject })
129 continue
130 }
131
132 const attribute = {
133 type: VideoStreamingPlaylistType.HLS,
134
135 playlistFilename: basename(playlistUrlObject.href),
136 playlistUrl: playlistUrlObject.href,
137
138 segmentsSha256Filename: basename(segmentsSha256UrlObject.href),
139 segmentsSha256Url: segmentsSha256UrlObject.href,
140
141 p2pMediaLoaderInfohashes: VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlistUrlObject.href, files),
142 p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION,
143 videoId: video.id,
144
145 tagAPObject: playlistUrlObject.tag
146 }
147
148 attributes.push(attribute)
149 }
150
151 return attributes
152}
153
154function getLiveAttributesFromObject (video: MVideoId, videoObject: VideoObject) {
155 return {
156 saveReplay: videoObject.liveSaveReplay,
157 permanentLive: videoObject.permanentLive,
158 latencyMode: videoObject.latencyMode,
159 videoId: video.id
160 }
161}
162
163function getCaptionAttributesFromObject (video: MVideoId, videoObject: VideoObject) {
164 return videoObject.subtitleLanguage.map(c => ({
165 videoId: video.id,
166 filename: VideoCaptionModel.generateCaptionName(c.identifier),
167 language: c.identifier,
168 fileUrl: c.url
169 }))
170}
171
172function getStoryboardAttributeFromObject (video: MVideoId, videoObject: VideoObject) {
173 if (!isArray(videoObject.preview)) return undefined
174
175 const storyboard = videoObject.preview.find(p => p.rel.includes('storyboard'))
176 if (!storyboard) return undefined
177
178 const url = arrayify(storyboard.url).find(u => u.mediaType === 'image/jpeg')
179
180 return {
181 filename: generateImageFilename(extname(url.href)),
182 totalHeight: url.height,
183 totalWidth: url.width,
184 spriteHeight: url.tileHeight,
185 spriteWidth: url.tileWidth,
186 spriteDuration: getDurationFromActivityStream(url.tileDuration),
187 fileUrl: url.href,
188 videoId: video.id
189 }
190}
191
192function getVideoAttributesFromObject (videoChannel: MChannelId, videoObject: VideoObject, to: string[] = []) {
193 const privacy = to.includes(ACTIVITY_PUB.PUBLIC)
194 ? VideoPrivacy.PUBLIC
195 : VideoPrivacy.UNLISTED
196
197 const language = videoObject.language?.identifier
198
199 const category = videoObject.category
200 ? parseInt(videoObject.category.identifier, 10)
201 : undefined
202
203 const licence = videoObject.licence
204 ? parseInt(videoObject.licence.identifier, 10)
205 : undefined
206
207 const description = videoObject.content || null
208 const support = videoObject.support || null
209
210 return {
211 name: videoObject.name,
212 uuid: videoObject.uuid,
213 url: videoObject.id,
214 category,
215 licence,
216 language,
217 description,
218 support,
219 nsfw: videoObject.sensitive,
220 commentsEnabled: videoObject.commentsEnabled,
221 downloadEnabled: videoObject.downloadEnabled,
222 waitTranscoding: videoObject.waitTranscoding,
223 isLive: videoObject.isLiveBroadcast,
224 state: videoObject.state,
225 channelId: videoChannel.id,
226 duration: getDurationFromActivityStream(videoObject.duration),
227 createdAt: new Date(videoObject.published),
228 publishedAt: new Date(videoObject.published),
229
230 originallyPublishedAt: videoObject.originallyPublishedAt
231 ? new Date(videoObject.originallyPublishedAt)
232 : null,
233
234 inputFileUpdatedAt: videoObject.uploadDate
235 ? new Date(videoObject.uploadDate)
236 : null,
237
238 updatedAt: new Date(videoObject.updated),
239 views: videoObject.views,
240 remote: true,
241 privacy
242 }
243}
244
245// ---------------------------------------------------------------------------
246
247export {
248 getThumbnailFromIcons,
249 getPreviewFromIcons,
250
251 getTagsFromObject,
252
253 getFileAttributesFromUrl,
254 getStreamingPlaylistAttributesFromObject,
255
256 getLiveAttributesFromObject,
257 getCaptionAttributesFromObject,
258 getStoryboardAttributeFromObject,
259
260 getVideoAttributesFromObject
261}
262
263// ---------------------------------------------------------------------------
264
265function isAPVideoUrlObject (url: any): url is ActivityVideoUrlObject {
266 const urlMediaType = url.mediaType
267
268 return MIMETYPES.VIDEO.MIMETYPE_EXT[urlMediaType] && urlMediaType.startsWith('video/')
269}
270
271function isAPStreamingPlaylistUrlObject (url: any): url is ActivityPlaylistUrlObject {
272 return url && url.mediaType === 'application/x-mpegURL'
273}
274
275function isAPPlaylistSegmentHashesUrlObject (tag: any): tag is ActivityPlaylistSegmentHashesObject {
276 return tag && tag.name === 'sha256' && tag.type === 'Link' && tag.mediaType === 'application/json'
277}
278
279function isAPMagnetUrlObject (url: any): url is ActivityMagnetUrlObject {
280 return url && url.mediaType === 'application/x-bittorrent;x-scheme-handler/magnet'
281}
282
283function isAPHashTagObject (url: any): url is ActivityHashTagObject {
284 return url && url.type === 'Hashtag'
285}
diff --git a/server/lib/activitypub/videos/shared/trackers.ts b/server/lib/activitypub/videos/shared/trackers.ts
deleted file mode 100644
index 2418f45c2..000000000
--- a/server/lib/activitypub/videos/shared/trackers.ts
+++ /dev/null
@@ -1,43 +0,0 @@
1import { Transaction } from 'sequelize/types'
2import { isAPVideoTrackerUrlObject } from '@server/helpers/custom-validators/activitypub/videos'
3import { isArray } from '@server/helpers/custom-validators/misc'
4import { REMOTE_SCHEME } from '@server/initializers/constants'
5import { TrackerModel } from '@server/models/server/tracker'
6import { MVideo, MVideoWithHost } from '@server/types/models'
7import { ActivityTrackerUrlObject, VideoObject } from '@shared/models'
8import { buildRemoteVideoBaseUrl } from '../../url'
9
10function getTrackerUrls (object: VideoObject, video: MVideoWithHost) {
11 let wsFound = false
12
13 const trackers = object.url.filter(u => isAPVideoTrackerUrlObject(u))
14 .map((u: ActivityTrackerUrlObject) => {
15 if (isArray(u.rel) && u.rel.includes('websocket')) wsFound = true
16
17 return u.href
18 })
19
20 if (wsFound) return trackers
21
22 return [
23 buildRemoteVideoBaseUrl(video, '/tracker/socket', REMOTE_SCHEME.WS),
24 buildRemoteVideoBaseUrl(video, '/tracker/announce')
25 ]
26}
27
28async function setVideoTrackers (options: {
29 video: MVideo
30 trackers: string[]
31 transaction: Transaction
32}) {
33 const { video, trackers, transaction } = options
34
35 const trackerInstances = await TrackerModel.findOrCreateTrackers(trackers, transaction)
36
37 await video.$set('Trackers', trackerInstances, { transaction })
38}
39
40export {
41 getTrackerUrls,
42 setVideoTrackers
43}
diff --git a/server/lib/activitypub/videos/shared/url-to-object.ts b/server/lib/activitypub/videos/shared/url-to-object.ts
deleted file mode 100644
index 7fe008419..000000000
--- a/server/lib/activitypub/videos/shared/url-to-object.ts
+++ /dev/null
@@ -1,25 +0,0 @@
1import { sanitizeAndCheckVideoTorrentObject } from '@server/helpers/custom-validators/activitypub/videos'
2import { logger, loggerTagsFactory } from '@server/helpers/logger'
3import { VideoObject } from '@shared/models'
4import { fetchAP } from '../../activity'
5import { checkUrlsSameHost } from '../../url'
6
7const lTags = loggerTagsFactory('ap', 'video')
8
9async function fetchRemoteVideo (videoUrl: string): Promise<{ statusCode: number, videoObject: VideoObject }> {
10 logger.info('Fetching remote video %s.', videoUrl, lTags(videoUrl))
11
12 const { statusCode, body } = await fetchAP<any>(videoUrl)
13
14 if (sanitizeAndCheckVideoTorrentObject(body) === false || checkUrlsSameHost(body.id, videoUrl) !== true) {
15 logger.debug('Remote video JSON is not valid.', { body, ...lTags(videoUrl) })
16
17 return { statusCode, videoObject: undefined }
18 }
19
20 return { statusCode, videoObject: body }
21}
22
23export {
24 fetchRemoteVideo
25}
diff --git a/server/lib/activitypub/videos/shared/video-sync-attributes.ts b/server/lib/activitypub/videos/shared/video-sync-attributes.ts
deleted file mode 100644
index 7fb933559..000000000
--- a/server/lib/activitypub/videos/shared/video-sync-attributes.ts
+++ /dev/null
@@ -1,107 +0,0 @@
1import { runInReadCommittedTransaction } from '@server/helpers/database-utils'
2import { logger, loggerTagsFactory } from '@server/helpers/logger'
3import { JobQueue } from '@server/lib/job-queue'
4import { VideoModel } from '@server/models/video/video'
5import { VideoCommentModel } from '@server/models/video/video-comment'
6import { VideoShareModel } from '@server/models/video/video-share'
7import { MVideo } from '@server/types/models'
8import { ActivitypubHttpFetcherPayload, ActivityPubOrderedCollection, VideoObject } from '@shared/models'
9import { fetchAP } from '../../activity'
10import { crawlCollectionPage } from '../../crawl'
11import { addVideoShares } from '../../share'
12import { addVideoComments } from '../../video-comments'
13
14const lTags = loggerTagsFactory('ap', 'video')
15
16type SyncParam = {
17 rates: boolean
18 shares: boolean
19 comments: boolean
20 refreshVideo?: boolean
21}
22
23async function syncVideoExternalAttributes (
24 video: MVideo,
25 fetchedVideo: VideoObject,
26 syncParam: Pick<SyncParam, 'rates' | 'shares' | 'comments'>
27) {
28 logger.info('Adding likes/dislikes/shares/comments of video %s.', video.uuid)
29
30 const ratePromise = updateVideoRates(video, fetchedVideo)
31 if (syncParam.rates) await ratePromise
32
33 await syncShares(video, fetchedVideo, syncParam.shares)
34
35 await syncComments(video, fetchedVideo, syncParam.comments)
36}
37
38async function updateVideoRates (video: MVideo, fetchedVideo: VideoObject) {
39 const [ likes, dislikes ] = await Promise.all([
40 getRatesCount('like', video, fetchedVideo),
41 getRatesCount('dislike', video, fetchedVideo)
42 ])
43
44 return runInReadCommittedTransaction(async t => {
45 await VideoModel.updateRatesOf(video.id, 'like', likes, t)
46 await VideoModel.updateRatesOf(video.id, 'dislike', dislikes, t)
47 })
48}
49
50// ---------------------------------------------------------------------------
51
52export {
53 SyncParam,
54 syncVideoExternalAttributes,
55 updateVideoRates
56}
57
58// ---------------------------------------------------------------------------
59
60async function getRatesCount (type: 'like' | 'dislike', video: MVideo, fetchedVideo: VideoObject) {
61 const uri = type === 'like'
62 ? fetchedVideo.likes
63 : fetchedVideo.dislikes
64
65 logger.info('Sync %s of video %s', type, video.url)
66
67 const { body } = await fetchAP<ActivityPubOrderedCollection<any>>(uri)
68
69 if (isNaN(body.totalItems)) {
70 logger.error('Cannot sync %s of video %s, totalItems is not a number', type, video.url, { body })
71 return
72 }
73
74 return body.totalItems
75}
76
77function syncShares (video: MVideo, fetchedVideo: VideoObject, isSync: boolean) {
78 const uri = fetchedVideo.shares
79
80 if (!isSync) {
81 return createJob({ uri, videoId: video.id, type: 'video-shares' })
82 }
83
84 const handler = items => addVideoShares(items, video)
85 const cleaner = crawlStartDate => VideoShareModel.cleanOldSharesOf(video.id, crawlStartDate)
86
87 return crawlCollectionPage<string>(uri, handler, cleaner)
88 .catch(err => logger.error('Cannot add shares of video %s.', video.uuid, { err, rootUrl: uri, ...lTags(video.uuid, video.url) }))
89}
90
91function syncComments (video: MVideo, fetchedVideo: VideoObject, isSync: boolean) {
92 const uri = fetchedVideo.comments
93
94 if (!isSync) {
95 return createJob({ uri, videoId: video.id, type: 'video-comments' })
96 }
97
98 const handler = items => addVideoComments(items)
99 const cleaner = crawlStartDate => VideoCommentModel.cleanOldCommentsOf(video.id, crawlStartDate)
100
101 return crawlCollectionPage<string>(uri, handler, cleaner)
102 .catch(err => logger.error('Cannot add comments of video %s.', video.uuid, { err, rootUrl: uri, ...lTags(video.uuid, video.url) }))
103}
104
105function createJob (payload: ActivitypubHttpFetcherPayload) {
106 return JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload })
107}
diff --git a/server/lib/activitypub/videos/updater.ts b/server/lib/activitypub/videos/updater.ts
deleted file mode 100644
index acb087895..000000000
--- a/server/lib/activitypub/videos/updater.ts
+++ /dev/null
@@ -1,180 +0,0 @@
1import { Transaction } from 'sequelize/types'
2import { resetSequelizeInstance, runInReadCommittedTransaction } from '@server/helpers/database-utils'
3import { logger, loggerTagsFactory, LoggerTagsFn } from '@server/helpers/logger'
4import { Notifier } from '@server/lib/notifier'
5import { PeerTubeSocket } from '@server/lib/peertube-socket'
6import { Hooks } from '@server/lib/plugins/hooks'
7import { autoBlacklistVideoIfNeeded } from '@server/lib/video-blacklist'
8import { VideoLiveModel } from '@server/models/video/video-live'
9import { MActor, MChannelAccountLight, MChannelId, MVideoAccountLightBlacklistAllFiles, MVideoFullLight } from '@server/types/models'
10import { VideoObject, VideoPrivacy } from '@shared/models'
11import { APVideoAbstractBuilder, getVideoAttributesFromObject, updateVideoRates } from './shared'
12
13export class APVideoUpdater extends APVideoAbstractBuilder {
14 private readonly wasPrivateVideo: boolean
15 private readonly wasUnlistedVideo: boolean
16
17 private readonly oldVideoChannel: MChannelAccountLight
18
19 protected lTags: LoggerTagsFn
20
21 constructor (
22 protected readonly videoObject: VideoObject,
23 private readonly video: MVideoAccountLightBlacklistAllFiles
24 ) {
25 super()
26
27 this.wasPrivateVideo = this.video.privacy === VideoPrivacy.PRIVATE
28 this.wasUnlistedVideo = this.video.privacy === VideoPrivacy.UNLISTED
29
30 this.oldVideoChannel = this.video.VideoChannel
31
32 this.lTags = loggerTagsFactory('ap', 'video', 'update', video.uuid, video.url)
33 }
34
35 async update (overrideTo?: string[]) {
36 logger.debug(
37 'Updating remote video "%s".', this.videoObject.uuid,
38 { videoObject: this.videoObject, ...this.lTags() }
39 )
40
41 const oldInputFileUpdatedAt = this.video.inputFileUpdatedAt
42
43 try {
44 const channelActor = await this.getOrCreateVideoChannelFromVideoObject()
45
46 const thumbnailModel = await this.setThumbnail(this.video)
47
48 this.checkChannelUpdateOrThrow(channelActor)
49
50 const videoUpdated = await this.updateVideo(channelActor.VideoChannel, undefined, overrideTo)
51
52 if (thumbnailModel) await videoUpdated.addAndSaveThumbnail(thumbnailModel)
53
54 await runInReadCommittedTransaction(async t => {
55 await this.setWebVideoFiles(videoUpdated, t)
56 await this.setStreamingPlaylists(videoUpdated, t)
57 })
58
59 await Promise.all([
60 runInReadCommittedTransaction(t => this.setTags(videoUpdated, t)),
61 runInReadCommittedTransaction(t => this.setTrackers(videoUpdated, t)),
62 runInReadCommittedTransaction(t => this.setStoryboard(videoUpdated, t)),
63 runInReadCommittedTransaction(t => {
64 return Promise.all([
65 this.setPreview(videoUpdated, t),
66 this.setThumbnail(videoUpdated, t)
67 ])
68 }),
69 this.setOrDeleteLive(videoUpdated)
70 ])
71
72 await runInReadCommittedTransaction(t => this.setCaptions(videoUpdated, t))
73
74 await autoBlacklistVideoIfNeeded({
75 video: videoUpdated,
76 user: undefined,
77 isRemote: true,
78 isNew: false,
79 isNewFile: oldInputFileUpdatedAt !== videoUpdated.inputFileUpdatedAt,
80 transaction: undefined
81 })
82
83 await updateVideoRates(videoUpdated, this.videoObject)
84
85 // Notify our users?
86 if (this.wasPrivateVideo || this.wasUnlistedVideo) {
87 Notifier.Instance.notifyOnNewVideoIfNeeded(videoUpdated)
88 }
89
90 if (videoUpdated.isLive) {
91 PeerTubeSocket.Instance.sendVideoLiveNewState(videoUpdated)
92 }
93
94 Hooks.runAction('action:activity-pub.remote-video.updated', { video: videoUpdated, videoAPObject: this.videoObject })
95
96 logger.info('Remote video with uuid %s updated', this.videoObject.uuid, this.lTags())
97
98 return videoUpdated
99 } catch (err) {
100 await this.catchUpdateError(err)
101 }
102 }
103
104 // Check we can update the channel: we trust the remote server
105 private checkChannelUpdateOrThrow (newChannelActor: MActor) {
106 if (!this.oldVideoChannel.Actor.serverId || !newChannelActor.serverId) {
107 throw new Error('Cannot check old channel/new channel validity because `serverId` is null')
108 }
109
110 if (this.oldVideoChannel.Actor.serverId !== newChannelActor.serverId) {
111 throw new Error(`New channel ${newChannelActor.url} is not on the same server than new channel ${this.oldVideoChannel.Actor.url}`)
112 }
113 }
114
115 private updateVideo (channel: MChannelId, transaction?: Transaction, overrideTo?: string[]) {
116 const to = overrideTo || this.videoObject.to
117 const videoData = getVideoAttributesFromObject(channel, this.videoObject, to)
118 this.video.name = videoData.name
119 this.video.uuid = videoData.uuid
120 this.video.url = videoData.url
121 this.video.category = videoData.category
122 this.video.licence = videoData.licence
123 this.video.language = videoData.language
124 this.video.description = videoData.description
125 this.video.support = videoData.support
126 this.video.nsfw = videoData.nsfw
127 this.video.commentsEnabled = videoData.commentsEnabled
128 this.video.downloadEnabled = videoData.downloadEnabled
129 this.video.waitTranscoding = videoData.waitTranscoding
130 this.video.state = videoData.state
131 this.video.duration = videoData.duration
132 this.video.createdAt = videoData.createdAt
133 this.video.publishedAt = videoData.publishedAt
134 this.video.originallyPublishedAt = videoData.originallyPublishedAt
135 this.video.inputFileUpdatedAt = videoData.inputFileUpdatedAt
136 this.video.privacy = videoData.privacy
137 this.video.channelId = videoData.channelId
138 this.video.views = videoData.views
139 this.video.isLive = videoData.isLive
140
141 // Ensures we update the updatedAt attribute, even if main attributes did not change
142 this.video.changed('updatedAt', true)
143
144 return this.video.save({ transaction }) as Promise<MVideoFullLight>
145 }
146
147 private async setCaptions (videoUpdated: MVideoFullLight, t: Transaction) {
148 await this.insertOrReplaceCaptions(videoUpdated, t)
149 }
150
151 private async setStoryboard (videoUpdated: MVideoFullLight, t: Transaction) {
152 await this.insertOrReplaceStoryboard(videoUpdated, t)
153 }
154
155 private async setOrDeleteLive (videoUpdated: MVideoFullLight, transaction?: Transaction) {
156 if (!this.video.isLive) return
157
158 if (this.video.isLive) return this.insertOrReplaceLive(videoUpdated, transaction)
159
160 // Delete existing live if it exists
161 await VideoLiveModel.destroy({
162 where: {
163 videoId: this.video.id
164 },
165 transaction
166 })
167
168 videoUpdated.VideoLive = null
169 }
170
171 private async catchUpdateError (err: Error) {
172 if (this.video !== undefined) {
173 await resetSequelizeInstance(this.video)
174 }
175
176 // This is just a debug because we will retry the insert
177 logger.debug('Cannot update the remote video.', { err, ...this.lTags() })
178 throw err
179 }
180}