aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib/activitypub/send
diff options
context:
space:
mode:
Diffstat (limited to 'server/lib/activitypub/send')
-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
17 files changed, 0 insertions, 1528 deletions
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}