From a219c9100b3ce8774d454497d46be87465bf664e Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Wed, 23 Mar 2022 16:14:33 +0100 Subject: Refactor AP context builder --- server/controllers/activitypub/client.ts | 38 +-- server/controllers/activitypub/outbox.ts | 2 +- server/lib/activitypub/activity.ts | 12 - server/lib/activitypub/audience.ts | 2 +- server/lib/activitypub/context.ts | 275 ++++++++++++--------- server/lib/activitypub/send/http.ts | 68 +++++ server/lib/activitypub/send/index.ts | 2 +- server/lib/activitypub/send/send-accept.ts | 7 +- server/lib/activitypub/send/send-announce.ts | 14 +- server/lib/activitypub/send/send-create.ts | 67 ++++- server/lib/activitypub/send/send-delete.ts | 67 ++++- server/lib/activitypub/send/send-dislike.ts | 4 +- server/lib/activitypub/send/send-flag.ts | 14 +- server/lib/activitypub/send/send-follow.ts | 4 +- server/lib/activitypub/send/send-like.ts | 4 +- server/lib/activitypub/send/send-reject.ts | 2 +- server/lib/activitypub/send/send-undo.ts | 50 ++-- server/lib/activitypub/send/send-update.ts | 44 +++- server/lib/activitypub/send/shared/send-utils.ts | 104 +++++--- .../handlers/activitypub-http-broadcast.ts | 2 +- .../job-queue/handlers/activitypub-http-unicast.ts | 2 +- .../handlers/utils/activitypub-http-utils.ts | 59 ----- server/models/actor/actor.ts | 2 +- server/tests/api/activitypub/helpers.ts | 6 +- server/tests/api/activitypub/security.ts | 23 +- server/tests/shared/requests.ts | 2 +- shared/models/activitypub/context.ts | 16 +- .../models/activitypub/objects/common-objects.ts | 4 +- shared/models/server/job.model.ts | 6 +- 29 files changed, 565 insertions(+), 337 deletions(-) create mode 100644 server/lib/activitypub/send/http.ts delete mode 100644 server/lib/job-queue/handlers/utils/activitypub-http-utils.ts diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts index 99637dbab..d0f761009 100644 --- a/server/controllers/activitypub/client.ts +++ b/server/controllers/activitypub/client.ts @@ -186,35 +186,35 @@ export { function accountController (req: express.Request, res: express.Response) { const account = res.locals.account - return activityPubResponse(activityPubContextify(account.toActivityPubObject()), res) + return activityPubResponse(activityPubContextify(account.toActivityPubObject(), 'Actor'), res) } async function accountFollowersController (req: express.Request, res: express.Response) { const account = res.locals.account const activityPubResult = await actorFollowers(req, account.Actor) - return activityPubResponse(activityPubContextify(activityPubResult), res) + return activityPubResponse(activityPubContextify(activityPubResult, 'Collection'), res) } async function accountFollowingController (req: express.Request, res: express.Response) { const account = res.locals.account const activityPubResult = await actorFollowing(req, account.Actor) - return activityPubResponse(activityPubContextify(activityPubResult), res) + return activityPubResponse(activityPubContextify(activityPubResult, 'Collection'), res) } async function accountPlaylistsController (req: express.Request, res: express.Response) { const account = res.locals.account const activityPubResult = await actorPlaylists(req, { account }) - return activityPubResponse(activityPubContextify(activityPubResult), res) + return activityPubResponse(activityPubContextify(activityPubResult, 'Collection'), res) } async function videoChannelPlaylistsController (req: express.Request, res: express.Response) { const channel = res.locals.videoChannel const activityPubResult = await actorPlaylists(req, { channel }) - return activityPubResponse(activityPubContextify(activityPubResult), res) + return activityPubResponse(activityPubContextify(activityPubResult, 'Collection'), res) } function getAccountVideoRateFactory (rateType: VideoRateType) { @@ -226,7 +226,7 @@ function getAccountVideoRateFactory (rateType: VideoRateType) { ? buildLikeActivity(accountVideoRate.url, byActor, accountVideoRate.Video) : buildDislikeActivity(accountVideoRate.url, byActor, accountVideoRate.Video) - return activityPubResponse(activityPubContextify(APObject), res) + return activityPubResponse(activityPubContextify(APObject, 'Rate'), res) } } @@ -244,10 +244,10 @@ async function videoController (req: express.Request, res: express.Response) { if (req.path.endsWith('/activity')) { const data = buildCreateActivity(videoWithCaptions.url, video.VideoChannel.Account.Actor, videoObject, audience) - return activityPubResponse(activityPubContextify(data), res) + return activityPubResponse(activityPubContextify(data, 'Video'), res) } - return activityPubResponse(activityPubContextify(videoObject), res) + return activityPubResponse(activityPubContextify(videoObject, 'Video'), res) } async function videoAnnounceController (req: express.Request, res: express.Response) { @@ -274,7 +274,7 @@ async function videoAnnouncesController (req: express.Request, res: express.Resp } const json = await activityPubCollectionPagination(getLocalVideoSharesActivityPubUrl(video), handler, req.query.page) - return activityPubResponse(activityPubContextify(json), res) + return activityPubResponse(activityPubContextify(json, 'Collection'), res) } async function videoLikesController (req: express.Request, res: express.Response) { @@ -284,7 +284,7 @@ async function videoLikesController (req: express.Request, res: express.Response const json = await videoRates(req, 'like', video, getLocalVideoLikesActivityPubUrl(video)) - return activityPubResponse(activityPubContextify(json), res) + return activityPubResponse(activityPubContextify(json, 'Collection'), res) } async function videoDislikesController (req: express.Request, res: express.Response) { @@ -294,7 +294,7 @@ async function videoDislikesController (req: express.Request, res: express.Respo const json = await videoRates(req, 'dislike', video, getLocalVideoDislikesActivityPubUrl(video)) - return activityPubResponse(activityPubContextify(json), res) + return activityPubResponse(activityPubContextify(json, 'Collection'), res) } async function videoCommentsController (req: express.Request, res: express.Response) { @@ -312,27 +312,27 @@ async function videoCommentsController (req: express.Request, res: express.Respo } const json = await activityPubCollectionPagination(getLocalVideoCommentsActivityPubUrl(video), handler, req.query.page) - return activityPubResponse(activityPubContextify(json), res) + return activityPubResponse(activityPubContextify(json, 'Collection'), res) } function videoChannelController (req: express.Request, res: express.Response) { const videoChannel = res.locals.videoChannel - return activityPubResponse(activityPubContextify(videoChannel.toActivityPubObject()), res) + return activityPubResponse(activityPubContextify(videoChannel.toActivityPubObject(), 'Actor'), res) } async function videoChannelFollowersController (req: express.Request, res: express.Response) { const videoChannel = res.locals.videoChannel const activityPubResult = await actorFollowers(req, videoChannel.Actor) - return activityPubResponse(activityPubContextify(activityPubResult), res) + return activityPubResponse(activityPubContextify(activityPubResult, 'Collection'), res) } async function videoChannelFollowingController (req: express.Request, res: express.Response) { const videoChannel = res.locals.videoChannel const activityPubResult = await actorFollowing(req, videoChannel.Actor) - return activityPubResponse(activityPubContextify(activityPubResult), res) + return activityPubResponse(activityPubContextify(activityPubResult, 'Collection'), res) } async function videoCommentController (req: express.Request, res: express.Response) { @@ -350,11 +350,11 @@ async function videoCommentController (req: express.Request, res: express.Respon if (req.path.endsWith('/activity')) { const data = buildCreateActivity(videoComment.url, videoComment.Account.Actor, videoCommentObject, audience) - return activityPubResponse(activityPubContextify(data), res) + return activityPubResponse(activityPubContextify(data, 'Comment'), res) } } - return activityPubResponse(activityPubContextify(videoCommentObject), res) + return activityPubResponse(activityPubContextify(videoCommentObject, 'Comment'), res) } async function videoRedundancyController (req: express.Request, res: express.Response) { @@ -387,7 +387,7 @@ async function videoPlaylistController (req: express.Request, res: express.Respo const audience = getAudience(playlist.OwnerAccount.Actor, playlist.privacy === VideoPlaylistPrivacy.PUBLIC) const object = audiencify(json, audience) - return activityPubResponse(activityPubContextify(object), res) + return activityPubResponse(activityPubContextify(object, 'Playlist'), res) } function videoPlaylistElementController (req: express.Request, res: express.Response) { @@ -396,7 +396,7 @@ function videoPlaylistElementController (req: express.Request, res: express.Resp if (redirectIfNotOwned(videoPlaylistElement.url, res)) return const json = videoPlaylistElement.toActivityPubObject() - return activityPubResponse(activityPubContextify(json), res) + return activityPubResponse(activityPubContextify(json, 'Playlist'), res) } // --------------------------------------------------------------------------- diff --git a/server/controllers/activitypub/outbox.ts b/server/controllers/activitypub/outbox.ts index 4e7a3afeb..f385c9927 100644 --- a/server/controllers/activitypub/outbox.ts +++ b/server/controllers/activitypub/outbox.ts @@ -45,7 +45,7 @@ async function outboxController (req: express.Request, res: express.Response) { const handler = (start: number, count: number) => buildActivities(actor, start, count) const json = await activityPubCollectionPagination(actorOutboxUrl, handler, req.query.page, req.query.size) - return activityPubResponse(activityPubContextify(json), res) + return activityPubResponse(activityPubContextify(json, 'Collection'), res) } async function buildActivities (actor: MActorLight, start: number, count: number) { diff --git a/server/lib/activitypub/activity.ts b/server/lib/activitypub/activity.ts index 215b50b69..cccb7b1c1 100644 --- a/server/lib/activitypub/activity.ts +++ b/server/lib/activitypub/activity.ts @@ -1,14 +1,3 @@ -import { signJsonLDObject } from '@server/helpers/peertube-crypto' -import { MActor } from '@server/types/models' -import { ContextType } from '@shared/models' -import { activityPubContextify } from './context' - -function buildSignedActivity (byActor: MActor, data: T, contextType?: ContextType) { - const activity = activityPubContextify(data, contextType) - - return signJsonLDObject(byActor, activity) -} - function getAPId (object: string | { id: string }) { if (typeof object === 'string') return object @@ -16,6 +5,5 @@ function getAPId (object: string | { id: string }) { } export { - buildSignedActivity, getAPId } diff --git a/server/lib/activitypub/audience.ts b/server/lib/activitypub/audience.ts index 2bd5bb066..6f5491387 100644 --- a/server/lib/activitypub/audience.ts +++ b/server/lib/activitypub/audience.ts @@ -22,7 +22,7 @@ function buildAudience (followerUrls: string[], isPublic = true) { } function audiencify (object: T, audience: ActivityAudience) { - return Object.assign(object, audience) + return { ...audience, ...object } } // --------------------------------------------------------------------------- diff --git a/server/lib/activitypub/context.ts b/server/lib/activitypub/context.ts index 71f08da80..3bc40e2aa 100644 --- a/server/lib/activitypub/context.ts +++ b/server/lib/activitypub/context.ts @@ -1,137 +1,168 @@ import { ContextType } from '@shared/models' -function getContextData (type: ContextType) { - const context: any[] = [ - 'https://www.w3.org/ns/activitystreams', - 'https://w3id.org/security/v1', - { - RsaSignature2017: 'https://w3id.org/security#RsaSignature2017' +function activityPubContextify (data: T, type: ContextType) { + return { ...getContextData(type), ...data } +} + +// --------------------------------------------------------------------------- + +export { + getContextData, + activityPubContextify +} + +// --------------------------------------------------------------------------- + +type ContextValue = { [ id: string ]: (string | { '@type': string, '@id': string }) } + +const contextStore = { + Video: buildContext({ + Hashtag: 'as:Hashtag', + uuid: 'sc:identifier', + category: 'sc:category', + licence: 'sc:license', + subtitleLanguage: 'sc:subtitleLanguage', + sensitive: 'as:sensitive', + language: 'sc:inLanguage', + + // TODO: remove in a few versions, introduced in 4.2 + icons: 'as:icon', + + isLiveBroadcast: 'sc:isLiveBroadcast', + liveSaveReplay: { + '@type': 'sc:Boolean', + '@id': 'pt:liveSaveReplay' + }, + permanentLive: { + '@type': 'sc:Boolean', + '@id': 'pt:permanentLive' + }, + latencyMode: { + '@type': 'sc:Number', + '@id': 'pt:latencyMode' + }, + + Infohash: 'pt:Infohash', + + originallyPublishedAt: 'sc:datePublished', + views: { + '@type': 'sc:Number', + '@id': 'pt:views' + }, + state: { + '@type': 'sc:Number', + '@id': 'pt:state' + }, + size: { + '@type': 'sc:Number', + '@id': 'pt:size' + }, + fps: { + '@type': 'sc:Number', + '@id': 'pt:fps' + }, + commentsEnabled: { + '@type': 'sc:Boolean', + '@id': 'pt:commentsEnabled' + }, + downloadEnabled: { + '@type': 'sc:Boolean', + '@id': 'pt:downloadEnabled' + }, + waitTranscoding: { + '@type': 'sc:Boolean', + '@id': 'pt:waitTranscoding' + }, + support: { + '@type': 'sc:Text', + '@id': 'pt:support' + }, + likes: { + '@id': 'as:likes', + '@type': '@id' + }, + dislikes: { + '@id': 'as:dislikes', + '@type': '@id' + }, + shares: { + '@id': 'as:shares', + '@type': '@id' + }, + comments: { + '@id': 'as:comments', + '@type': '@id' } - ] + }), - if (type !== 'View' && type !== 'Announce') { - const additional = { - pt: 'https://joinpeertube.org/ns#', - sc: 'http://schema.org#' + Playlist: buildContext({ + Playlist: 'pt:Playlist', + PlaylistElement: 'pt:PlaylistElement', + position: { + '@type': 'sc:Number', + '@id': 'pt:position' + }, + startTimestamp: { + '@type': 'sc:Number', + '@id': 'pt:startTimestamp' + }, + stopTimestamp: { + '@type': 'sc:Number', + '@id': 'pt:stopTimestamp' } + }), + + CacheFile: buildContext({ + expires: 'sc:expires', + CacheFile: 'pt:CacheFile' + }), - if (type === 'CacheFile') { - Object.assign(additional, { - expires: 'sc:expires', - CacheFile: 'pt:CacheFile' - }) - } else { - Object.assign(additional, { - Hashtag: 'as:Hashtag', - uuid: 'sc:identifier', - category: 'sc:category', - licence: 'sc:license', - subtitleLanguage: 'sc:subtitleLanguage', - sensitive: 'as:sensitive', - language: 'sc:inLanguage', - - // TODO: remove in a few versions, introduced in 4.2 - icons: 'as:icon', - - isLiveBroadcast: 'sc:isLiveBroadcast', - liveSaveReplay: { - '@type': 'sc:Boolean', - '@id': 'pt:liveSaveReplay' - }, - permanentLive: { - '@type': 'sc:Boolean', - '@id': 'pt:permanentLive' - }, - latencyMode: { - '@type': 'sc:Number', - '@id': 'pt:latencyMode' - }, - - Infohash: 'pt:Infohash', - Playlist: 'pt:Playlist', - PlaylistElement: 'pt:PlaylistElement', - - originallyPublishedAt: 'sc:datePublished', - views: { - '@type': 'sc:Number', - '@id': 'pt:views' - }, - state: { - '@type': 'sc:Number', - '@id': 'pt:state' - }, - size: { - '@type': 'sc:Number', - '@id': 'pt:size' - }, - fps: { - '@type': 'sc:Number', - '@id': 'pt:fps' - }, - startTimestamp: { - '@type': 'sc:Number', - '@id': 'pt:startTimestamp' - }, - stopTimestamp: { - '@type': 'sc:Number', - '@id': 'pt:stopTimestamp' - }, - position: { - '@type': 'sc:Number', - '@id': 'pt:position' - }, - commentsEnabled: { - '@type': 'sc:Boolean', - '@id': 'pt:commentsEnabled' - }, - downloadEnabled: { - '@type': 'sc:Boolean', - '@id': 'pt:downloadEnabled' - }, - waitTranscoding: { - '@type': 'sc:Boolean', - '@id': 'pt:waitTranscoding' - }, - support: { - '@type': 'sc:Text', - '@id': 'pt:support' - }, - likes: { - '@id': 'as:likes', - '@type': '@id' - }, - dislikes: { - '@id': 'as:dislikes', - '@type': '@id' - }, - playlists: { - '@id': 'pt:playlists', - '@type': '@id' - }, - shares: { - '@id': 'as:shares', - '@type': '@id' - }, - comments: { - '@id': 'as:comments', - '@type': '@id' - } - }) + Flag: buildContext({ + Hashtag: 'as:Hashtag' + }), + + Actor: buildContext({ + playlists: { + '@id': 'pt:playlists', + '@type': '@id' } + }), - context.push(additional) - } + Follow: buildContext(), + Reject: buildContext(), + Accept: buildContext(), + View: buildContext(), + Announce: buildContext(), + Comment: buildContext(), + Delete: buildContext(), + Rate: buildContext() +} +function getContextData (type: ContextType) { return { - '@context': context + '@context': contextStore[type] } } -function activityPubContextify (data: T, type: ContextType = 'All') { - return Object.assign({}, data, getContextData(type)) -} +function buildContext (contextValue?: ContextValue) { + const baseContext = [ + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1', + { + RsaSignature2017: 'https://w3id.org/security#RsaSignature2017' + } + ] -export { - getContextData, - activityPubContextify + if (!contextValue) return baseContext + + return [ + ...baseContext, + + { + pt: 'https://joinpeertube.org/ns#', + sc: 'http://schema.org#', + + ...contextValue + } + ] } diff --git a/server/lib/activitypub/send/http.ts b/server/lib/activitypub/send/http.ts new file mode 100644 index 000000000..d8d0b8542 --- /dev/null +++ b/server/lib/activitypub/send/http.ts @@ -0,0 +1,68 @@ +import { buildDigest, signJsonLDObject } from '@server/helpers/peertube-crypto' +import { ACTIVITY_PUB, HTTP_SIGNATURE } from '@server/initializers/constants' +import { ActorModel } from '@server/models/actor/actor' +import { getServerActor } from '@server/models/application/application' +import { MActor } from '@server/types/models' +import { ContextType } from '@shared/models/activitypub/context' +import { activityPubContextify } from '../context' + +type Payload = { body: T, contextType: ContextType, signatureActorId?: number } + +async function computeBody ( + payload: Payload +): Promise { + let body = payload.body + + if (payload.signatureActorId) { + const actorSignature = await ActorModel.load(payload.signatureActorId) + if (!actorSignature) throw new Error('Unknown signature actor id.') + + body = await signAndContextify(actorSignature, payload.body, payload.contextType) + } + + return body +} + +async function buildSignedRequestOptions (payload: Payload) { + let actor: MActor | null + + if (payload.signatureActorId) { + actor = await ActorModel.load(payload.signatureActorId) + if (!actor) throw new Error('Unknown signature actor id.') + } else { + // We need to sign the request, so use the server + actor = await getServerActor() + } + + const keyId = actor.url + return { + algorithm: HTTP_SIGNATURE.ALGORITHM, + authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME, + keyId, + key: actor.privateKey, + headers: HTTP_SIGNATURE.HEADERS_TO_SIGN + } +} + +function buildGlobalHeaders (body: any) { + return { + 'digest': buildDigest(body), + 'content-type': 'application/activity+json', + 'accept': ACTIVITY_PUB.ACCEPT_HEADER + } +} + +function signAndContextify (byActor: MActor, data: T, contextType: ContextType | null) { + const activity = contextType + ? activityPubContextify(data, contextType) + : data + + return signJsonLDObject(byActor, activity) +} + +export { + buildGlobalHeaders, + computeBody, + buildSignedRequestOptions, + signAndContextify +} diff --git a/server/lib/activitypub/send/index.ts b/server/lib/activitypub/send/index.ts index 028936810..852ea2e74 100644 --- a/server/lib/activitypub/send/index.ts +++ b/server/lib/activitypub/send/index.ts @@ -1,4 +1,4 @@ -export * from './send-accept' +export * from './http' export * from './send-accept' export * from './send-announce' export * from './send-create' diff --git a/server/lib/activitypub/send/send-accept.ts b/server/lib/activitypub/send/send-accept.ts index 939f06d9e..4c9bcbb0b 100644 --- a/server/lib/activitypub/send/send-accept.ts +++ b/server/lib/activitypub/send/send-accept.ts @@ -21,7 +21,12 @@ function sendAccept (actorFollow: MActorFollowActors) { const url = getLocalActorFollowAcceptActivityPubUrl(actorFollow) const data = buildAcceptActivity(url, me, followData) - return unicastTo(data, me, follower.inboxUrl) + return unicastTo({ + data, + byActor: me, + toActorUrl: follower.inboxUrl, + contextType: 'Accept' + }) } // --------------------------------------------------------------------------- diff --git a/server/lib/activitypub/send/send-announce.ts b/server/lib/activitypub/send/send-announce.ts index 7897beb75..6c078b047 100644 --- a/server/lib/activitypub/send/send-announce.ts +++ b/server/lib/activitypub/send/send-announce.ts @@ -23,13 +23,19 @@ async function buildAnnounceWithVideoAudience ( return { activity, actorsInvolvedInVideo } } -async function sendVideoAnnounce (byActor: MActorLight, videoShare: MVideoShare, video: MVideo, t: Transaction) { - const { activity, actorsInvolvedInVideo } = await buildAnnounceWithVideoAudience(byActor, videoShare, video, t) +async function sendVideoAnnounce (byActor: MActorLight, videoShare: MVideoShare, video: MVideo, transaction: Transaction) { + const { activity, actorsInvolvedInVideo } = await buildAnnounceWithVideoAudience(byActor, videoShare, video, transaction) logger.info('Creating job to send announce %s.', videoShare.url) - const followersException = [ byActor ] - return broadcastToFollowers(activity, byActor, actorsInvolvedInVideo, t, followersException, 'Announce') + return broadcastToFollowers({ + data: activity, + byActor, + toFollowersOf: actorsInvolvedInVideo, + transaction, + actorsException: [ byActor ], + contextType: 'Announce' + }) } function buildAnnounceActivity (url: string, byActor: MActorLight, object: string, audience?: ActivityAudience): ActivityAnnounce { diff --git a/server/lib/activitypub/send/send-create.ts b/server/lib/activitypub/send/send-create.ts index f6d897220..5d8763495 100644 --- a/server/lib/activitypub/send/send-create.ts +++ b/server/lib/activitypub/send/send-create.ts @@ -25,7 +25,7 @@ import { const lTags = loggerTagsFactory('ap', 'create') -async function sendCreateVideo (video: MVideoAP, t: Transaction) { +async function sendCreateVideo (video: MVideoAP, transaction: Transaction) { if (!video.hasPrivacyForFederation()) return undefined logger.info('Creating job to send video creation of %s.', video.url, lTags(video.uuid)) @@ -36,7 +36,13 @@ async function sendCreateVideo (video: MVideoAP, t: Transaction) { const audience = getAudience(byActor, video.privacy === VideoPrivacy.PUBLIC) const createActivity = buildCreateActivity(video.url, byActor, videoObject, audience) - return broadcastToFollowers(createActivity, byActor, [ byActor ], t) + return broadcastToFollowers({ + data: createActivity, + byActor, + toFollowersOf: [ byActor ], + transaction, + contextType: 'Video' + }) } async function sendCreateCacheFile ( @@ -55,7 +61,7 @@ async function sendCreateCacheFile ( }) } -async function sendCreateVideoPlaylist (playlist: MVideoPlaylistFull, t: Transaction) { +async function sendCreateVideoPlaylist (playlist: MVideoPlaylistFull, transaction: Transaction) { if (playlist.privacy === VideoPlaylistPrivacy.PRIVATE) return undefined logger.info('Creating job to send create video playlist of %s.', playlist.url, lTags(playlist.uuid)) @@ -63,7 +69,7 @@ async function sendCreateVideoPlaylist (playlist: MVideoPlaylistFull, t: Transac const byActor = playlist.OwnerAccount.Actor const audience = getAudience(byActor, playlist.privacy === VideoPlaylistPrivacy.PUBLIC) - const object = await playlist.toActivityPubObject(null, t) + const object = await playlist.toActivityPubObject(null, transaction) const createActivity = buildCreateActivity(playlist.url, byActor, object, audience) const serverActor = await getServerActor() @@ -71,19 +77,25 @@ async function sendCreateVideoPlaylist (playlist: MVideoPlaylistFull, t: Transac if (playlist.VideoChannel) toFollowersOf.push(playlist.VideoChannel.Actor) - return broadcastToFollowers(createActivity, byActor, toFollowersOf, t) + return broadcastToFollowers({ + data: createActivity, + byActor, + toFollowersOf, + transaction, + contextType: 'Playlist' + }) } -async function sendCreateVideoComment (comment: MCommentOwnerVideo, t: Transaction) { +async function sendCreateVideoComment (comment: MCommentOwnerVideo, transaction: Transaction) { logger.info('Creating job to send comment %s.', comment.url) const isOrigin = comment.Video.isOwned() const byActor = comment.Account.Actor - const threadParentComments = await VideoCommentModel.listThreadParentComments(comment, t) + const threadParentComments = await VideoCommentModel.listThreadParentComments(comment, transaction) const commentObject = comment.toActivityPubObject(threadParentComments) - const actorsInvolvedInComment = await getActorsInvolvedInVideo(comment.Video, t) + const actorsInvolvedInComment = await getActorsInvolvedInVideo(comment.Video, transaction) // Add the actor that commented too actorsInvolvedInComment.push(byActor) @@ -101,16 +113,45 @@ async function sendCreateVideoComment (comment: MCommentOwnerVideo, t: Transacti // This was a reply, send it to the parent actors const actorsException = [ byActor ] - await broadcastToActors(createActivity, byActor, parentsCommentActors, t, actorsException) + await broadcastToActors({ + data: createActivity, + byActor, + toActors: parentsCommentActors, + transaction, + actorsException, + contextType: 'Comment' + }) // Broadcast to our followers - await broadcastToFollowers(createActivity, byActor, [ byActor ], t) + await broadcastToFollowers({ + data: createActivity, + byActor, + toFollowersOf: [ byActor ], + transaction, + contextType: 'Comment' + }) // Send to actors involved in the comment - if (isOrigin) return broadcastToFollowers(createActivity, byActor, actorsInvolvedInComment, t, actorsException) + if (isOrigin) { + return broadcastToFollowers({ + data: createActivity, + byActor, + toFollowersOf: actorsInvolvedInComment, + transaction, + actorsException, + contextType: 'Comment' + }) + } // Send to origin - t.afterCommit(() => unicastTo(createActivity, byActor, comment.Video.VideoChannel.Account.Actor.getSharedInbox())) + return transaction.afterCommit(() => { + return unicastTo({ + data: createActivity, + byActor, + toActorUrl: comment.Video.VideoChannel.Account.Actor.getSharedInbox(), + contextType: 'Comment' + }) + }) } function buildCreateActivity (url: string, byActor: MActorLight, object: any, audience?: ActivityAudience): ActivityCreate { @@ -144,8 +185,8 @@ async function sendVideoRelatedCreateActivity (options: { video: MVideoAccountLight url: string object: any + contextType: ContextType transaction?: Transaction - contextType?: ContextType }) { const activityBuilder = (audience: ActivityAudience) => { return buildCreateActivity(options.url, options.byActor, options.object, audience) diff --git a/server/lib/activitypub/send/send-delete.ts b/server/lib/activitypub/send/send-delete.ts index 39216cdeb..0d85d9001 100644 --- a/server/lib/activitypub/send/send-delete.ts +++ b/server/lib/activitypub/send/send-delete.ts @@ -23,16 +23,16 @@ async function sendDeleteVideo (video: MVideoAccountLight, transaction: Transact return buildDeleteActivity(url, video.url, byActor, audience) } - return sendVideoRelatedActivity(activityBuilder, { byActor, video, transaction }) + return sendVideoRelatedActivity(activityBuilder, { byActor, video, contextType: 'Delete', transaction }) } -async function sendDeleteActor (byActor: ActorModel, t: Transaction) { +async function sendDeleteActor (byActor: ActorModel, transaction: Transaction) { logger.info('Creating job to broadcast delete of actor %s.', byActor.url) const url = getDeleteActivityPubUrl(byActor.url) const activity = buildDeleteActivity(url, byActor.url, byActor) - const actorsInvolved = await VideoShareModel.loadActorsWhoSharedVideosOf(byActor.id, t) + const actorsInvolved = await VideoShareModel.loadActorsWhoSharedVideosOf(byActor.id, transaction) // In case the actor did not have any videos const serverActor = await getServerActor() @@ -40,10 +40,16 @@ async function sendDeleteActor (byActor: ActorModel, t: Transaction) { actorsInvolved.push(byActor) - return broadcastToFollowers(activity, byActor, actorsInvolved, t) + return broadcastToFollowers({ + data: activity, + byActor, + toFollowersOf: actorsInvolved, + contextType: 'Delete', + transaction + }) } -async function sendDeleteVideoComment (videoComment: MCommentOwnerVideo, t: Transaction) { +async function sendDeleteVideoComment (videoComment: MCommentOwnerVideo, transaction: Transaction) { logger.info('Creating job to send delete of comment %s.', videoComment.url) const isVideoOrigin = videoComment.Video.isOwned() @@ -53,10 +59,10 @@ async function sendDeleteVideoComment (videoComment: MCommentOwnerVideo, t: Tran ? videoComment.Account.Actor : videoComment.Video.VideoChannel.Account.Actor - const threadParentComments = await VideoCommentModel.listThreadParentComments(videoComment, t) + const threadParentComments = await VideoCommentModel.listThreadParentComments(videoComment, transaction) const threadParentCommentsFiltered = threadParentComments.filter(c => !c.isDeleted()) - const actorsInvolvedInComment = await getActorsInvolvedInVideo(videoComment.Video, t) + const actorsInvolvedInComment = await getActorsInvolvedInVideo(videoComment.Video, transaction) actorsInvolvedInComment.push(byActor) // Add the actor that commented the video const audience = getVideoCommentAudience(videoComment, threadParentCommentsFiltered, actorsInvolvedInComment, isVideoOrigin) @@ -64,19 +70,48 @@ async function sendDeleteVideoComment (videoComment: MCommentOwnerVideo, t: Tran // This was a reply, send it to the parent actors const actorsException = [ byActor ] - await broadcastToActors(activity, byActor, threadParentCommentsFiltered.map(c => c.Account.Actor), t, actorsException) + await broadcastToActors({ + data: activity, + byActor, + toActors: threadParentCommentsFiltered.map(c => c.Account.Actor), + transaction, + contextType: 'Delete', + actorsException + }) // Broadcast to our followers - await broadcastToFollowers(activity, byActor, [ byActor ], t) + await broadcastToFollowers({ + data: activity, + byActor, + toFollowersOf: [ byActor ], + contextType: 'Delete', + transaction + }) // Send to actors involved in the comment - if (isVideoOrigin) return broadcastToFollowers(activity, byActor, actorsInvolvedInComment, t, actorsException) + if (isVideoOrigin) { + return broadcastToFollowers({ + data: activity, + byActor, + toFollowersOf: actorsInvolvedInComment, + transaction, + contextType: 'Delete', + actorsException + }) + } // Send to origin - t.afterCommit(() => unicastTo(activity, byActor, videoComment.Video.VideoChannel.Account.Actor.getSharedInbox())) + return transaction.afterCommit(() => { + return unicastTo({ + data: activity, + byActor, + toActorUrl: videoComment.Video.VideoChannel.Account.Actor.getSharedInbox(), + contextType: 'Delete' + }) + }) } -async function sendDeleteVideoPlaylist (videoPlaylist: MVideoPlaylistFullSummary, t: Transaction) { +async function sendDeleteVideoPlaylist (videoPlaylist: MVideoPlaylistFullSummary, transaction: Transaction) { logger.info('Creating job to send delete of playlist %s.', videoPlaylist.url) const byActor = videoPlaylist.OwnerAccount.Actor @@ -89,7 +124,13 @@ async function sendDeleteVideoPlaylist (videoPlaylist: MVideoPlaylistFullSummary if (videoPlaylist.VideoChannel) toFollowersOf.push(videoPlaylist.VideoChannel.Actor) - return broadcastToFollowers(activity, byActor, toFollowersOf, t) + return broadcastToFollowers({ + data: activity, + byActor, + toFollowersOf, + contextType: 'Delete', + transaction + }) } // --------------------------------------------------------------------------- diff --git a/server/lib/activitypub/send/send-dislike.ts b/server/lib/activitypub/send/send-dislike.ts index ecb11e9bf..959e74823 100644 --- a/server/lib/activitypub/send/send-dislike.ts +++ b/server/lib/activitypub/send/send-dislike.ts @@ -6,7 +6,7 @@ import { audiencify, getAudience } from '../audience' import { getVideoDislikeActivityPubUrlByLocalActor } from '../url' import { sendVideoActivityToOrigin } from './shared/send-utils' -function sendDislike (byActor: MActor, video: MVideoAccountLight, t: Transaction) { +function sendDislike (byActor: MActor, video: MVideoAccountLight, transaction: Transaction) { logger.info('Creating job to dislike %s.', video.url) const activityBuilder = (audience: ActivityAudience) => { @@ -15,7 +15,7 @@ function sendDislike (byActor: MActor, video: MVideoAccountLight, t: Transaction return buildDislikeActivity(url, byActor, video, audience) } - return sendVideoActivityToOrigin(activityBuilder, { byActor, video, transaction: t }) + return sendVideoActivityToOrigin(activityBuilder, { byActor, video, transaction, contextType: 'Rate' }) } function buildDislikeActivity (url: string, byActor: MActorAudience, video: MVideoUrl, audience?: ActivityAudience): ActivityDislike { diff --git a/server/lib/activitypub/send/send-flag.ts b/server/lib/activitypub/send/send-flag.ts index 6df4e7eb8..138eb5adc 100644 --- a/server/lib/activitypub/send/send-flag.ts +++ b/server/lib/activitypub/send/send-flag.ts @@ -17,16 +17,20 @@ function sendAbuse (byActor: MActor, abuse: MAbuseAP, flaggedAccount: MAccountLi const audience = { to: [ flaggedAccount.Actor.url ], cc: [] } const flagActivity = buildFlagActivity(url, byActor, abuse, audience) - t.afterCommit(() => unicastTo(flagActivity, byActor, flaggedAccount.Actor.getSharedInbox())) + return t.afterCommit(() => { + return unicastTo({ + data: flagActivity, + byActor, + toActorUrl: flaggedAccount.Actor.getSharedInbox(), + contextType: 'Flag' + }) + }) } function buildFlagActivity (url: string, byActor: MActor, abuse: MAbuseAP, audience: ActivityAudience): ActivityFlag { if (!audience) audience = getAudience(byActor) - const activity = Object.assign( - { id: url, actor: byActor.url }, - abuse.toActivityPubObject() - ) + const activity = { id: url, actor: byActor.url, ...abuse.toActivityPubObject() } return audiencify(activity, audience) } diff --git a/server/lib/activitypub/send/send-follow.ts b/server/lib/activitypub/send/send-follow.ts index aeeb50a2a..57501dadb 100644 --- a/server/lib/activitypub/send/send-follow.ts +++ b/server/lib/activitypub/send/send-follow.ts @@ -15,7 +15,9 @@ function sendFollow (actorFollow: MActorFollowActors, t: Transaction) { const data = buildFollowActivity(actorFollow.url, me, following) - t.afterCommit(() => unicastTo(data, me, following.inboxUrl)) + return t.afterCommit(() => { + return unicastTo({ data, byActor: me, toActorUrl: following.inboxUrl, contextType: 'Follow' }) + }) } function buildFollowActivity (url: string, byActor: MActor, targetActor: MActor): ActivityFollow { diff --git a/server/lib/activitypub/send/send-like.ts b/server/lib/activitypub/send/send-like.ts index a5fe95e0a..46c9fdec9 100644 --- a/server/lib/activitypub/send/send-like.ts +++ b/server/lib/activitypub/send/send-like.ts @@ -6,7 +6,7 @@ import { audiencify, getAudience } from '../audience' import { getVideoLikeActivityPubUrlByLocalActor } from '../url' import { sendVideoActivityToOrigin } from './shared/send-utils' -function sendLike (byActor: MActor, video: MVideoAccountLight, t: Transaction) { +function sendLike (byActor: MActor, video: MVideoAccountLight, transaction: Transaction) { logger.info('Creating job to like %s.', video.url) const activityBuilder = (audience: ActivityAudience) => { @@ -15,7 +15,7 @@ function sendLike (byActor: MActor, video: MVideoAccountLight, t: Transaction) { return buildLikeActivity(url, byActor, video, audience) } - return sendVideoActivityToOrigin(activityBuilder, { byActor, video, transaction: t }) + return sendVideoActivityToOrigin(activityBuilder, { byActor, video, transaction, contextType: 'Rate' }) } function buildLikeActivity (url: string, byActor: MActorAudience, video: MVideoUrl, audience?: ActivityAudience): ActivityLike { diff --git a/server/lib/activitypub/send/send-reject.ts b/server/lib/activitypub/send/send-reject.ts index 01b8f743b..83d8dfba7 100644 --- a/server/lib/activitypub/send/send-reject.ts +++ b/server/lib/activitypub/send/send-reject.ts @@ -18,7 +18,7 @@ function sendReject (followUrl: string, follower: MActor, following: MActor) { const url = getLocalActorFollowRejectActivityPubUrl(follower, following) const data = buildRejectActivity(url, following, followData) - return unicastTo(data, following, follower.inboxUrl) + return unicastTo({ data, byActor: following, toActorUrl: follower.inboxUrl, contextType: 'Reject' }) } // --------------------------------------------------------------------------- diff --git a/server/lib/activitypub/send/send-undo.ts b/server/lib/activitypub/send/send-undo.ts index 948ca0d7a..36d7ef991 100644 --- a/server/lib/activitypub/send/send-undo.ts +++ b/server/lib/activitypub/send/send-undo.ts @@ -6,7 +6,8 @@ import { ActivityDislike, ActivityFollow, ActivityLike, - ActivityUndo + ActivityUndo, + ContextType } from '@shared/models' import { logger } from '../../../helpers/logger' import { VideoModel } from '../../../models/video/video' @@ -43,24 +44,37 @@ function sendUndoFollow (actorFollow: MActorFollowActors, t: Transaction) { const followActivity = buildFollowActivity(actorFollow.url, me, following) const undoActivity = undoActivityData(undoUrl, me, followActivity) - t.afterCommit(() => unicastTo(undoActivity, me, following.inboxUrl)) + return t.afterCommit(() => { + return unicastTo({ + data: undoActivity, + byActor: me, + toActorUrl: following.inboxUrl, + contextType: 'Follow' + }) + }) } // --------------------------------------------------------------------------- -async function sendUndoAnnounce (byActor: MActorLight, videoShare: MVideoShare, video: MVideo, t: Transaction) { +async function sendUndoAnnounce (byActor: MActorLight, videoShare: MVideoShare, video: MVideo, transaction: Transaction) { logger.info('Creating job to undo announce %s.', videoShare.url) const undoUrl = getUndoActivityPubUrl(videoShare.url) - const { activity: announceActivity, actorsInvolvedInVideo } = await buildAnnounceWithVideoAudience(byActor, videoShare, video, t) - const undoActivity = undoActivityData(undoUrl, byActor, announceActivity) - - const followersException = [ byActor ] - return broadcastToFollowers(undoActivity, byActor, actorsInvolvedInVideo, t, followersException) + const { activity: announce, actorsInvolvedInVideo } = await buildAnnounceWithVideoAudience(byActor, videoShare, video, transaction) + const undoActivity = undoActivityData(undoUrl, byActor, announce) + + return broadcastToFollowers({ + data: undoActivity, + byActor, + toFollowersOf: actorsInvolvedInVideo, + transaction, + actorsException: [ byActor ], + contextType: 'Announce' + }) } -async function sendUndoCacheFile (byActor: MActor, redundancyModel: MVideoRedundancyVideo, t: Transaction) { +async function sendUndoCacheFile (byActor: MActor, redundancyModel: MVideoRedundancyVideo, transaction: Transaction) { logger.info('Creating job to undo cache file %s.', redundancyModel.url) const associatedVideo = redundancyModel.getVideo() @@ -72,7 +86,14 @@ async function sendUndoCacheFile (byActor: MActor, redundancyModel: MVideoRedund const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(associatedVideo.id) const createActivity = buildCreateActivity(redundancyModel.url, byActor, redundancyModel.toActivityPubObject()) - return sendUndoVideoRelatedActivity({ byActor, video, url: redundancyModel.url, activity: createActivity, transaction: t }) + return sendUndoVideoRelatedActivity({ + byActor, + video, + url: redundancyModel.url, + activity: createActivity, + contextType: 'CacheFile', + transaction + }) } // --------------------------------------------------------------------------- @@ -83,7 +104,7 @@ async function sendUndoLike (byActor: MActor, video: MVideoAccountLight, t: Tran const likeUrl = getVideoLikeActivityPubUrlByLocalActor(byActor, video) const likeActivity = buildLikeActivity(likeUrl, byActor, video) - return sendUndoVideoToOriginActivity({ byActor, video, url: likeUrl, activity: likeActivity, transaction: t }) + return sendUndoVideoRateToOriginActivity({ byActor, video, url: likeUrl, activity: likeActivity, transaction: t }) } async function sendUndoDislike (byActor: MActor, video: MVideoAccountLight, t: Transaction) { @@ -92,7 +113,7 @@ async function sendUndoDislike (byActor: MActor, video: MVideoAccountLight, t: T const dislikeUrl = getVideoDislikeActivityPubUrlByLocalActor(byActor, video) const dislikeActivity = buildDislikeActivity(dislikeUrl, byActor, video) - return sendUndoVideoToOriginActivity({ byActor, video, url: dislikeUrl, activity: dislikeActivity, transaction: t }) + return sendUndoVideoRateToOriginActivity({ byActor, video, url: dislikeUrl, activity: dislikeActivity, transaction: t }) } // --------------------------------------------------------------------------- @@ -131,6 +152,7 @@ async function sendUndoVideoRelatedActivity (options: { video: MVideoAccountLight url: string activity: ActivityFollow | ActivityCreate | ActivityAnnounce + contextType: ContextType transaction: Transaction }) { const activityBuilder = (audience: ActivityAudience) => { @@ -142,7 +164,7 @@ async function sendUndoVideoRelatedActivity (options: { return sendVideoRelatedActivity(activityBuilder, options) } -async function sendUndoVideoToOriginActivity (options: { +async function sendUndoVideoRateToOriginActivity (options: { byActor: MActor video: MVideoAccountLight url: string @@ -155,5 +177,5 @@ async function sendUndoVideoToOriginActivity (options: { return undoActivityData(undoUrl, options.byActor, options.activity, audience) } - return sendVideoActivityToOrigin(activityBuilder, options) + return sendVideoActivityToOrigin(activityBuilder, { ...options, contextType: 'Rate' }) } diff --git a/server/lib/activitypub/send/send-update.ts b/server/lib/activitypub/send/send-update.ts index 7c9e72cbc..3577ece02 100644 --- a/server/lib/activitypub/send/send-update.ts +++ b/server/lib/activitypub/send/send-update.ts @@ -20,20 +20,20 @@ import { getUpdateActivityPubUrl } from '../url' import { getActorsInvolvedInVideo } from './shared' import { broadcastToFollowers, sendVideoRelatedActivity } from './shared/send-utils' -async function sendUpdateVideo (videoArg: MVideoAPWithoutCaption, t: Transaction, overrodeByActor?: MActor) { +async function sendUpdateVideo (videoArg: MVideoAPWithoutCaption, transaction: Transaction, overriddenByActor?: MActor) { const video = videoArg as MVideoAP if (!video.hasPrivacyForFederation()) return undefined logger.info('Creating job to update video %s.', video.url) - const byActor = overrodeByActor || video.VideoChannel.Account.Actor + const byActor = overriddenByActor || video.VideoChannel.Account.Actor const url = getUpdateActivityPubUrl(video.url, video.updatedAt.toISOString()) // Needed to build the AP object if (!video.VideoCaptions) { - video.VideoCaptions = await video.$get('VideoCaptions', { transaction: t }) + video.VideoCaptions = await video.$get('VideoCaptions', { transaction }) } const videoObject = video.toActivityPubObject() @@ -41,13 +41,19 @@ async function sendUpdateVideo (videoArg: MVideoAPWithoutCaption, t: Transaction const updateActivity = buildUpdateActivity(url, byActor, videoObject, audience) - const actorsInvolved = await getActorsInvolvedInVideo(video, t) - if (overrodeByActor) actorsInvolved.push(overrodeByActor) + const actorsInvolved = await getActorsInvolvedInVideo(video, transaction) + if (overriddenByActor) actorsInvolved.push(overriddenByActor) - return broadcastToFollowers(updateActivity, byActor, actorsInvolved, t) + return broadcastToFollowers({ + data: updateActivity, + byActor, + toFollowersOf: actorsInvolved, + contextType: 'Video', + transaction + }) } -async function sendUpdateActor (accountOrChannel: MChannelDefault | MAccountDefault, t: Transaction) { +async function sendUpdateActor (accountOrChannel: MChannelDefault | MAccountDefault, transaction: Transaction) { const byActor = accountOrChannel.Actor logger.info('Creating job to update actor %s.', byActor.url) @@ -60,15 +66,21 @@ async function sendUpdateActor (accountOrChannel: MChannelDefault | MAccountDefa let actorsInvolved: MActor[] if (accountOrChannel instanceof AccountModel) { // Actors that shared my videos are involved too - actorsInvolved = await VideoShareModel.loadActorsWhoSharedVideosOf(byActor.id, t) + actorsInvolved = await VideoShareModel.loadActorsWhoSharedVideosOf(byActor.id, transaction) } else { // Actors that shared videos of my channel are involved too - actorsInvolved = await VideoShareModel.loadActorsByVideoChannel(accountOrChannel.id, t) + actorsInvolved = await VideoShareModel.loadActorsByVideoChannel(accountOrChannel.id, transaction) } actorsInvolved.push(byActor) - return broadcastToFollowers(updateActivity, byActor, actorsInvolved, t) + return broadcastToFollowers({ + data: updateActivity, + byActor, + toFollowersOf: actorsInvolved, + transaction, + contextType: 'Actor' + }) } async function sendUpdateCacheFile (byActor: MActorLight, redundancyModel: MVideoRedundancyVideo) { @@ -92,7 +104,7 @@ async function sendUpdateCacheFile (byActor: MActorLight, redundancyModel: MVide return sendVideoRelatedActivity(activityBuilder, { byActor, video, contextType: 'CacheFile' }) } -async function sendUpdateVideoPlaylist (videoPlaylist: MVideoPlaylistFull, t: Transaction) { +async function sendUpdateVideoPlaylist (videoPlaylist: MVideoPlaylistFull, transaction: Transaction) { if (videoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) return undefined const byActor = videoPlaylist.OwnerAccount.Actor @@ -101,7 +113,7 @@ async function sendUpdateVideoPlaylist (videoPlaylist: MVideoPlaylistFull, t: Tr const url = getUpdateActivityPubUrl(videoPlaylist.url, videoPlaylist.updatedAt.toISOString()) - const object = await videoPlaylist.toActivityPubObject(null, t) + const object = await videoPlaylist.toActivityPubObject(null, transaction) const audience = getAudience(byActor, videoPlaylist.privacy === VideoPlaylistPrivacy.PUBLIC) const updateActivity = buildUpdateActivity(url, byActor, object, audience) @@ -111,7 +123,13 @@ async function sendUpdateVideoPlaylist (videoPlaylist: MVideoPlaylistFull, t: Tr if (videoPlaylist.VideoChannel) toFollowersOf.push(videoPlaylist.VideoChannel.Actor) - return broadcastToFollowers(updateActivity, byActor, toFollowersOf, t) + return broadcastToFollowers({ + data: updateActivity, + byActor, + toFollowersOf, + transaction, + contextType: 'Playlist' + }) } // --------------------------------------------------------------------------- diff --git a/server/lib/activitypub/send/shared/send-utils.ts b/server/lib/activitypub/send/shared/send-utils.ts index 9e8f12fa8..dbcde91ee 100644 --- a/server/lib/activitypub/send/shared/send-utils.ts +++ b/server/lib/activitypub/send/shared/send-utils.ts @@ -1,7 +1,7 @@ import { Transaction } from 'sequelize' import { ActorFollowHealthCache } from '@server/lib/actor-follow-health-cache' import { getServerActor } from '@server/models/application/application' -import { Activity, ActivityAudience } from '@shared/models' +import { Activity, ActivityAudience, ActivitypubHttpBroadcastPayload } from '@shared/models' import { ContextType } from '@shared/models/activitypub/context' import { afterCommitIfTransaction } from '../../../../helpers/database-utils' import { logger } from '../../../../helpers/logger' @@ -14,8 +14,8 @@ import { getActorsInvolvedInVideo, getAudienceFromFollowersOf, getOriginVideoAud async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAudience) => Activity, options: { byActor: MActorLight video: MVideoImmutable | MVideoAccountLight + contextType: ContextType transaction?: Transaction - contextType?: ContextType }) { const { byActor, video, transaction, contextType } = options @@ -32,15 +32,23 @@ async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAud const actorsException = [ byActor ] - return broadcastToFollowers(activity, byActor, actorsInvolvedInVideo, transaction, actorsException, contextType) + return broadcastToFollowers({ + data: activity, + byActor, + toFollowersOf: actorsInvolvedInVideo, + transaction, + actorsException, + contextType + }) } async function sendVideoActivityToOrigin (activityBuilder: (audience: ActivityAudience) => Activity, options: { byActor: MActorLight video: MVideoImmutable | MVideoAccountLight + contextType: ContextType + actorsInvolvedInVideo?: MActorLight[] transaction?: Transaction - contextType?: ContextType }) { const { byActor, video, actorsInvolvedInVideo, transaction, contextType } = options @@ -53,7 +61,12 @@ async function sendVideoActivityToOrigin (activityBuilder: (audience: ActivityAu const activity = activityBuilder(audience) return afterCommitIfTransaction(transaction, () => { - return unicastTo(activity, byActor, accountActor.getSharedInbox(), contextType) + return unicastTo({ + data: activity, + byActor, + toActorUrl: accountActor.getSharedInbox(), + contextType + }) }) } @@ -100,41 +113,69 @@ async function forwardActivity ( logger.debug('Creating forwarding job.', { uris }) - const payload = { + const payload: ActivitypubHttpBroadcastPayload = { uris, - body: activity + body: activity, + contextType: null } return afterCommitIfTransaction(t, () => JobQueue.Instance.createJob({ type: 'activitypub-http-broadcast', payload })) } // --------------------------------------------------------------------------- -async function broadcastToFollowers ( - data: any, - byActor: MActorId, - toFollowersOf: MActorId[], - t: Transaction, - actorsException: MActorWithInboxes[] = [], - contextType?: ContextType -) { - const uris = await computeFollowerUris(toFollowersOf, actorsException, t) +async function broadcastToFollowers (options: { + data: any + byActor: MActorId + toFollowersOf: MActorId[] + transaction: Transaction + contextType: ContextType + + actorsException?: MActorWithInboxes[] +}) { + const { data, byActor, toFollowersOf, transaction, contextType, actorsException = [] } = options - return afterCommitIfTransaction(t, () => broadcastTo(uris, data, byActor, contextType)) + const uris = await computeFollowerUris(toFollowersOf, actorsException, transaction) + + return afterCommitIfTransaction(transaction, () => { + return broadcastTo({ + uris, + data, + byActor, + contextType + }) + }) } -async function broadcastToActors ( - data: any, - byActor: MActorId, - toActors: MActor[], - t?: Transaction, - actorsException: MActorWithInboxes[] = [], - contextType?: ContextType -) { +async function broadcastToActors (options: { + data: any + byActor: MActorId + toActors: MActor[] + transaction: Transaction + contextType: ContextType + actorsException?: MActorWithInboxes[] +}) { + const { data, byActor, toActors, transaction, contextType, actorsException = [] } = options + const uris = await computeUris(toActors, actorsException) - return afterCommitIfTransaction(t, () => broadcastTo(uris, data, byActor, contextType)) + + return afterCommitIfTransaction(transaction, () => { + return broadcastTo({ + uris, + data, + byActor, + contextType + }) + }) } -function broadcastTo (uris: string[], data: any, byActor: MActorId, contextType?: ContextType) { +function broadcastTo (options: { + uris: string[] + data: any + byActor: MActorId + contextType: ContextType +}) { + const { uris, data, byActor, contextType } = options + if (uris.length === 0) return undefined const broadcastUris: string[] = [] @@ -174,7 +215,14 @@ function broadcastTo (uris: string[], data: any, byActor: MActorId, contextType? } } -function unicastTo (data: any, byActor: MActorId, toActorUrl: string, contextType?: ContextType) { +function unicastTo (options: { + data: any + byActor: MActorId + toActorUrl: string + contextType: ContextType +}) { + const { data, byActor, toActorUrl, contextType } = options + logger.debug('Creating unicast job.', { uri: toActorUrl }) const payload = { diff --git a/server/lib/job-queue/handlers/activitypub-http-broadcast.ts b/server/lib/job-queue/handlers/activitypub-http-broadcast.ts index fbf01d276..709e8501f 100644 --- a/server/lib/job-queue/handlers/activitypub-http-broadcast.ts +++ b/server/lib/job-queue/handlers/activitypub-http-broadcast.ts @@ -1,11 +1,11 @@ import { map } from 'bluebird' import { Job } from 'bull' +import { buildGlobalHeaders, buildSignedRequestOptions, computeBody } from '@server/lib/activitypub/send' import { ActorFollowHealthCache } from '@server/lib/actor-follow-health-cache' import { ActivitypubHttpBroadcastPayload } from '@shared/models' import { logger } from '../../../helpers/logger' import { doRequest } from '../../../helpers/requests' import { BROADCAST_CONCURRENCY } from '../../../initializers/constants' -import { buildGlobalHeaders, buildSignedRequestOptions, computeBody } from './utils/activitypub-http-utils' async function processActivityPubHttpBroadcast (job: Job) { logger.info('Processing ActivityPub broadcast in job %d.', job.id) diff --git a/server/lib/job-queue/handlers/activitypub-http-unicast.ts b/server/lib/job-queue/handlers/activitypub-http-unicast.ts index 673583d2b..99bcd3e8d 100644 --- a/server/lib/job-queue/handlers/activitypub-http-unicast.ts +++ b/server/lib/job-queue/handlers/activitypub-http-unicast.ts @@ -1,9 +1,9 @@ import { Job } from 'bull' +import { buildGlobalHeaders, buildSignedRequestOptions, computeBody } from '@server/lib/activitypub/send' import { ActivitypubHttpUnicastPayload } from '@shared/models' import { logger } from '../../../helpers/logger' import { doRequest } from '../../../helpers/requests' import { ActorFollowHealthCache } from '../../actor-follow-health-cache' -import { buildGlobalHeaders, buildSignedRequestOptions, computeBody } from './utils/activitypub-http-utils' async function processActivityPubHttpUnicast (job: Job) { logger.info('Processing ActivityPub unicast in job %d.', job.id) diff --git a/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts b/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts deleted file mode 100644 index 2a03325b7..000000000 --- a/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { buildDigest } from '@server/helpers/peertube-crypto' -import { buildSignedActivity } from '@server/lib/activitypub/activity' -import { getServerActor } from '@server/models/application/application' -import { ContextType } from '@shared/models/activitypub/context' -import { ACTIVITY_PUB, HTTP_SIGNATURE } from '../../../../initializers/constants' -import { ActorModel } from '../../../../models/actor/actor' -import { MActor } from '../../../../types/models' - -type Payload = { body: T, contextType?: ContextType, signatureActorId?: number } - -async function computeBody ( - payload: Payload -): Promise { - let body = payload.body - - if (payload.signatureActorId) { - const actorSignature = await ActorModel.load(payload.signatureActorId) - if (!actorSignature) throw new Error('Unknown signature actor id.') - - body = await buildSignedActivity(actorSignature, payload.body, payload.contextType) - } - - return body -} - -async function buildSignedRequestOptions (payload: Payload) { - let actor: MActor | null - - if (payload.signatureActorId) { - actor = await ActorModel.load(payload.signatureActorId) - if (!actor) throw new Error('Unknown signature actor id.') - } else { - // We need to sign the request, so use the server - actor = await getServerActor() - } - - const keyId = actor.url - return { - algorithm: HTTP_SIGNATURE.ALGORITHM, - authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME, - keyId, - key: actor.privateKey, - headers: HTTP_SIGNATURE.HEADERS_TO_SIGN - } -} - -function buildGlobalHeaders (body: any) { - return { - 'digest': buildDigest(body), - 'content-type': 'application/activity+json', - 'accept': ACTIVITY_PUB.ACCEPT_HEADER - } -} - -export { - buildGlobalHeaders, - computeBody, - buildSignedRequestOptions -} diff --git a/server/models/actor/actor.ts b/server/models/actor/actor.ts index fad2070ea..93145b8ae 100644 --- a/server/models/actor/actor.ts +++ b/server/models/actor/actor.ts @@ -605,7 +605,7 @@ export class ActorModel extends Model>> { image } - return activityPubContextify(json) + return activityPubContextify(json, 'Actor') } getFollowerSharedInboxUrls (t: Transaction) { diff --git a/server/tests/api/activitypub/helpers.ts b/server/tests/api/activitypub/helpers.ts index e516cf49e..bc1de35d1 100644 --- a/server/tests/api/activitypub/helpers.ts +++ b/server/tests/api/activitypub/helpers.ts @@ -3,10 +3,10 @@ import 'mocha' import { expect } from 'chai' import { cloneDeep } from 'lodash' +import { signAndContextify } from '@server/lib/activitypub/send' import { buildRequestStub } from '@server/tests/shared' import { buildAbsoluteFixturePath } from '@shared/core-utils' import { isHTTPSignatureVerified, isJsonLDSignatureVerified, parseHTTPSignature } from '../../../helpers/peertube-crypto' -import { buildSignedActivity } from '../../../lib/activitypub/activity' describe('Test activity pub helpers', function () { describe('When checking the Linked Signature', function () { @@ -46,7 +46,7 @@ describe('Test activity pub helpers', function () { const body = require(buildAbsoluteFixturePath('./ap-json/peertube/announce-without-context.json')) const actorSignature = { url: 'http://localhost:9002/accounts/peertube', privateKey: keys.privateKey } - const signedBody = await buildSignedActivity(actorSignature as any, body) + const signedBody = await signAndContextify(actorSignature as any, body, 'Announce') const fromActor = { publicKey: keys.publicKey, url: 'http://localhost:9002/accounts/peertube' } const result = await isJsonLDSignatureVerified(fromActor as any, signedBody) @@ -59,7 +59,7 @@ describe('Test activity pub helpers', function () { const body = require(buildAbsoluteFixturePath('./ap-json/peertube/announce-without-context.json')) const actorSignature = { url: 'http://localhost:9002/accounts/peertube', privateKey: keys.privateKey } - const signedBody = await buildSignedActivity(actorSignature as any, body) + const signedBody = await signAndContextify(actorSignature as any, body, 'Announce') const fromActor = { publicKey: keys.publicKey, url: 'http://localhost:9002/accounts/peertube' } const result = await isJsonLDSignatureVerified(fromActor as any, signedBody) diff --git a/server/tests/api/activitypub/security.ts b/server/tests/api/activitypub/security.ts index da9880d7d..a070517b8 100644 --- a/server/tests/api/activitypub/security.ts +++ b/server/tests/api/activitypub/security.ts @@ -4,9 +4,8 @@ import 'mocha' import * as chai from 'chai' import { buildDigest } from '@server/helpers/peertube-crypto' import { HTTP_SIGNATURE } from '@server/initializers/constants' -import { buildSignedActivity } from '@server/lib/activitypub/activity' import { activityPubContextify } from '@server/lib/activitypub/context' -import { buildGlobalHeaders } from '@server/lib/job-queue/handlers/utils/activitypub-http-utils' +import { buildGlobalHeaders, signAndContextify } from '@server/lib/activitypub/send' import { makeFollowRequest, makePOSTAPRequest } from '@server/tests/shared' import { buildAbsoluteFixturePath, wait } from '@shared/core-utils' import { HttpStatusCode } from '@shared/models' @@ -81,7 +80,7 @@ describe('Test ActivityPub security', function () { describe('When checking HTTP signature', function () { it('Should fail with an invalid digest', async function () { - const body = activityPubContextify(getAnnounceWithoutContext(servers[1])) + const body = activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce') const headers = { Digest: buildDigest({ hello: 'coucou' }) } @@ -95,7 +94,7 @@ describe('Test ActivityPub security', function () { }) it('Should fail with an invalid date', async function () { - const body = activityPubContextify(getAnnounceWithoutContext(servers[1])) + const body = activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce') const headers = buildGlobalHeaders(body) headers['date'] = 'Wed, 21 Oct 2015 07:28:00 GMT' @@ -111,7 +110,7 @@ describe('Test ActivityPub security', function () { await setKeysOfServer(servers[0], servers[1], invalidKeys.publicKey, invalidKeys.privateKey) await setKeysOfServer(servers[1], servers[1], invalidKeys.publicKey, invalidKeys.privateKey) - const body = activityPubContextify(getAnnounceWithoutContext(servers[1])) + const body = activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce') const headers = buildGlobalHeaders(body) try { @@ -126,7 +125,7 @@ describe('Test ActivityPub security', function () { await setKeysOfServer(servers[0], servers[1], keys.publicKey, keys.privateKey) await setKeysOfServer(servers[1], servers[1], keys.publicKey, keys.privateKey) - const body = activityPubContextify(getAnnounceWithoutContext(servers[1])) + const body = activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce') const headers = buildGlobalHeaders(body) const signatureOptions = baseHttpSignature() @@ -149,7 +148,7 @@ describe('Test ActivityPub security', function () { }) it('Should succeed with a valid HTTP signature', async function () { - const body = activityPubContextify(getAnnounceWithoutContext(servers[1])) + const body = activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce') const headers = buildGlobalHeaders(body) const { statusCode } = await makePOSTAPRequest(url, body, baseHttpSignature(), headers) @@ -168,7 +167,7 @@ describe('Test ActivityPub security', function () { await killallServers([ servers[1] ]) await servers[1].run() - const body = activityPubContextify(getAnnounceWithoutContext(servers[1])) + const body = activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce') const headers = buildGlobalHeaders(body) try { @@ -204,7 +203,7 @@ describe('Test ActivityPub security', function () { body.actor = 'http://localhost:' + servers[2].port + '/accounts/peertube' const signer: any = { privateKey: invalidKeys.privateKey, url: 'http://localhost:' + servers[2].port + '/accounts/peertube' } - const signedBody = await buildSignedActivity(signer, body) + const signedBody = await signAndContextify(signer, body, 'Announce') const headers = buildGlobalHeaders(signedBody) @@ -226,7 +225,7 @@ describe('Test ActivityPub security', function () { body.actor = 'http://localhost:' + servers[2].port + '/accounts/peertube' const signer: any = { privateKey: keys.privateKey, url: 'http://localhost:' + servers[2].port + '/accounts/peertube' } - const signedBody = await buildSignedActivity(signer, body) + const signedBody = await signAndContextify(signer, body, 'Announce') signedBody.actor = 'http://localhost:' + servers[2].port + '/account/peertube' @@ -247,7 +246,7 @@ describe('Test ActivityPub security', function () { body.actor = 'http://localhost:' + servers[2].port + '/accounts/peertube' const signer: any = { privateKey: keys.privateKey, url: 'http://localhost:' + servers[2].port + '/accounts/peertube' } - const signedBody = await buildSignedActivity(signer, body) + const signedBody = await signAndContextify(signer, body, 'Announce') const headers = buildGlobalHeaders(signedBody) @@ -269,7 +268,7 @@ describe('Test ActivityPub security', function () { body.actor = 'http://localhost:' + servers[2].port + '/accounts/peertube' const signer: any = { privateKey: keys.privateKey, url: 'http://localhost:' + servers[2].port + '/accounts/peertube' } - const signedBody = await buildSignedActivity(signer, body) + const signedBody = await signAndContextify(signer, body, 'Announce') const headers = buildGlobalHeaders(signedBody) diff --git a/server/tests/shared/requests.ts b/server/tests/shared/requests.ts index d7aedf82f..57120caca 100644 --- a/server/tests/shared/requests.ts +++ b/server/tests/shared/requests.ts @@ -22,7 +22,7 @@ export async function makeFollowRequest (to: { url: string }, by: { url: string, object: to.url } - const body = activityPubContextify(follow) + const body = activityPubContextify(follow, 'Follow') const httpSignature = { algorithm: HTTP_SIGNATURE.ALGORITHM, diff --git a/shared/models/activitypub/context.ts b/shared/models/activitypub/context.ts index bd795a2fd..4ada3b083 100644 --- a/shared/models/activitypub/context.ts +++ b/shared/models/activitypub/context.ts @@ -1 +1,15 @@ -export type ContextType = 'All' | 'View' | 'Announce' | 'CacheFile' +export type ContextType = + 'Video' | + 'Comment' | + 'Playlist' | + 'Follow' | + 'Reject' | + 'Accept' | + 'View' | + 'Announce' | + 'CacheFile' | + 'Delete' | + 'Rate' | + 'Flag' | + 'Actor' | + 'Collection' diff --git a/shared/models/activitypub/objects/common-objects.ts b/shared/models/activitypub/objects/common-objects.ts index 43d7f7f74..9bf994379 100644 --- a/shared/models/activitypub/objects/common-objects.ts +++ b/shared/models/activitypub/objects/common-objects.ts @@ -46,7 +46,7 @@ export type ActivityTrackerUrlObject = { href: string } -export type ActivityPlaylistInfohashesObject = { +export type ActivityStreamingPlaylistInfohashesObject = { type: 'Infohash' name: string } @@ -97,7 +97,7 @@ export interface ActivityFlagReasonObject { export type ActivityTagObject = ActivityPlaylistSegmentHashesObject - | ActivityPlaylistInfohashesObject + | ActivityStreamingPlaylistInfohashesObject | ActivityVideoUrlObject | ActivityHashTagObject | ActivityMentionObject diff --git a/shared/models/server/job.model.ts b/shared/models/server/job.model.ts index 91469d010..92d1b5698 100644 --- a/shared/models/server/job.model.ts +++ b/shared/models/server/job.model.ts @@ -40,9 +40,9 @@ export interface Job { export type ActivitypubHttpBroadcastPayload = { uris: string[] - signatureActorId?: number + contextType: ContextType body: any - contextType?: ContextType + signatureActorId?: number } export type ActivitypubFollowPayload = { @@ -62,9 +62,9 @@ export type ActivitypubHttpFetcherPayload = { export type ActivitypubHttpUnicastPayload = { uri: string + contextType: ContextType signatureActorId?: number body: object - contextType?: ContextType } export type RefreshPayload = { -- cgit v1.2.3