]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Add ability for plugins to alter video jsonld
authorChocobozzz <me@florianbigard.com>
Fri, 10 Mar 2023 11:01:21 +0000 (12:01 +0100)
committerChocobozzz <me@florianbigard.com>
Fri, 10 Mar 2023 14:45:52 +0000 (15:45 +0100)
16 files changed:
client/src/app/+videos/+video-watch/shared/metadata/video-attributes.component.ts
client/src/app/core/plugins/hooks.service.ts
server/controllers/activitypub/client.ts
server/controllers/activitypub/outbox.ts
server/controllers/activitypub/utils.ts
server/lib/activitypub/context.ts
server/lib/activitypub/send/http.ts
server/lib/activitypub/send/send-create.ts
server/lib/activitypub/send/send-update.ts
server/models/account/account.ts
server/models/video/video-channel.ts
server/models/video/video.ts
server/tests/api/activitypub/security.ts
server/tests/shared/requests.ts
shared/core-utils/i18n/i18n.ts
shared/models/plugins/server/server-hook.model.ts

index ebfb427119d60a48987c9e1f6c84ede8e1cabead..1834f7be54db938ce75c0bd998db8c5f98479fe6 100644 (file)
@@ -22,11 +22,11 @@ export class VideoAttributesComponent implements OnInit {
   constructor (private hooks: HooksService) { }
 
   async ngOnInit () {
-    this.pluginMetadata = await this.hooks.wrapFunResult(
-      this.buildPluginMetadata.bind(this),
-      { video: this.video },
+    this.pluginMetadata = await this.hooks.wrapObject(
+      this.pluginMetadata,
       'video-watch',
-      'filter:video-watch.video-plugin-metadata.result'
+      'filter:video-watch.video-plugin-metadata.result',
+      { video: this.video }
     )
   }
 
@@ -39,11 +39,4 @@ export class VideoAttributesComponent implements OnInit {
 
     return this.video.tags
   }
-
-  // Used for plugin hooks
-  private buildPluginMetadata (_options: {
-    video: VideoDetails
-  }): PluginMetadata[] {
-    return []
-  }
 }
index f325605e908870e5fbe503431eac5bc8208801e7..d9fef838978dd874ba8b246d4166da0772946351 100644 (file)
@@ -48,15 +48,6 @@ export class HooksService {
     return this.pluginService.runHook(hookResultName, result, params)
   }
 
-  async wrapFunResult <P, R, H extends ClientFilterHookName>
-  (fun: RawFunction<P, R>, params: P, scope: PluginClientScope, hookResultName: H) {
-    await this.pluginService.ensurePluginsAreLoaded(scope)
-
-    const result = fun(params)
-
-    return this.pluginService.runHook(hookResultName, result, params)
-  }
-
   runAction<T, U extends ClientActionHookName> (hookName: U, scope: PluginClientScope, params?: T) {
     // Use setTimeout to give priority to Angular change detector
     setTimeout(() => {
@@ -66,13 +57,13 @@ export class HooksService {
     })
   }
 
-  async wrapObject<T, U extends ClientFilterHookName> (result: T, scope: PluginClientScope, hookName: U) {
+  async wrapObject<T, U extends ClientFilterHookName> (result: T, scope: PluginClientScope, hookName: U, context?: any) {
     await this.pluginService.ensurePluginsAreLoaded(scope)
 
-    return this.wrapObjectWithoutScopeLoad(result, hookName)
+    return this.wrapObjectWithoutScopeLoad(result, hookName, context)
   }
 
-  private wrapObjectWithoutScopeLoad<T, U extends ClientFilterHookName> (result: T, hookName: U) {
-    return this.pluginService.runHook(hookName, result)
+  private wrapObjectWithoutScopeLoad<T, U extends ClientFilterHookName> (result: T, hookName: U, context?: any) {
+    return this.pluginService.runHook(hookName, result, context)
   }
 }
index def3207300dec8b843e56a0d749235f1ba5048a1..750e3091ca796332bc324e17f2b07b60202d6741 100644 (file)
@@ -48,7 +48,7 @@ activityPubClientRouter.get(
   [ '/accounts?/:name', '/accounts?/:name/video-channels', '/a/:name', '/a/:name/video-channels' ],
   executeIfActivityPub,
   asyncMiddleware(localAccountValidator),
-  accountController
+  asyncMiddleware(accountController)
 )
 activityPubClientRouter.get('/accounts?/:name/followers',
   executeIfActivityPub,
@@ -69,13 +69,13 @@ activityPubClientRouter.get('/accounts?/:name/likes/:videoId',
   executeIfActivityPub,
   cacheRoute(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS),
   asyncMiddleware(getAccountVideoRateValidatorFactory('like')),
-  getAccountVideoRateFactory('like')
+  asyncMiddleware(getAccountVideoRateFactory('like'))
 )
 activityPubClientRouter.get('/accounts?/:name/dislikes/:videoId',
   executeIfActivityPub,
   cacheRoute(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS),
   asyncMiddleware(getAccountVideoRateValidatorFactory('dislike')),
-  getAccountVideoRateFactory('dislike')
+  asyncMiddleware(getAccountVideoRateFactory('dislike'))
 )
 
 activityPubClientRouter.get(
@@ -131,7 +131,7 @@ activityPubClientRouter.get(
   executeIfActivityPub,
   asyncMiddleware(videoChannelsNameWithHostValidator),
   ensureIsLocalChannel,
-  videoChannelController
+  asyncMiddleware(videoChannelController)
 )
 activityPubClientRouter.get('/video-channels/:nameWithHost/followers',
   executeIfActivityPub,
@@ -172,13 +172,13 @@ activityPubClientRouter.get(
 activityPubClientRouter.get('/video-playlists/:playlistId/videos/:playlistElementId',
   executeIfActivityPub,
   asyncMiddleware(videoPlaylistElementAPGetValidator),
-  videoPlaylistElementController
+  asyncMiddleware(videoPlaylistElementController)
 )
 
 activityPubClientRouter.get('/videos/local-viewer/:localViewerId',
   executeIfActivityPub,
   asyncMiddleware(getVideoLocalViewerValidator),
-  getVideoLocalViewerController
+  asyncMiddleware(getVideoLocalViewerController)
 )
 
 // ---------------------------------------------------------------------------
@@ -189,10 +189,10 @@ export {
 
 // ---------------------------------------------------------------------------
 
-function accountController (req: express.Request, res: express.Response) {
+async function accountController (req: express.Request, res: express.Response) {
   const account = res.locals.account
 
-  return activityPubResponse(activityPubContextify(account.toActivityPubObject(), 'Actor'), res)
+  return activityPubResponse(activityPubContextify(await account.toActivityPubObject(), 'Actor'), res)
 }
 
 async function accountFollowersController (req: express.Request, res: express.Response) {
@@ -246,7 +246,7 @@ async function videoController (req: express.Request, res: express.Response) {
   const videoWithCaptions = Object.assign(video, { VideoCaptions: captions })
 
   const audience = getAudience(videoWithCaptions.VideoChannel.Account.Actor, videoWithCaptions.privacy === VideoPrivacy.PUBLIC)
-  const videoObject = audiencify(videoWithCaptions.toActivityPubObject(), audience)
+  const videoObject = audiencify(await videoWithCaptions.toActivityPubObject(), audience)
 
   if (req.path.endsWith('/activity')) {
     const data = buildCreateActivity(videoWithCaptions.url, video.VideoChannel.Account.Actor, videoObject, audience)
@@ -321,10 +321,10 @@ async function videoCommentsController (req: express.Request, res: express.Respo
   return activityPubResponse(activityPubContextify(json, 'Collection'), res)
 }
 
-function videoChannelController (req: express.Request, res: express.Response) {
+async function videoChannelController (req: express.Request, res: express.Response) {
   const videoChannel = res.locals.videoChannel
 
-  return activityPubResponse(activityPubContextify(videoChannel.toActivityPubObject(), 'Actor'), res)
+  return activityPubResponse(activityPubContextify(await videoChannel.toActivityPubObject(), 'Actor'), res)
 }
 
 async function videoChannelFollowersController (req: express.Request, res: express.Response) {
index f385c9927ee5967683a01d289c590f5cf3be5dab..681a5660c73e33b0be02dd2dcf8901f9ab13546f 100644 (file)
@@ -63,7 +63,7 @@ async function buildActivities (actor: MActorLight, start: number, count: number
 
       activities.push(announceActivity)
     } else {
-      const videoObject = video.toActivityPubObject()
+      const videoObject = await video.toActivityPubObject()
       const createActivity = buildCreateActivity(video.url, byActor, videoObject, createActivityAudience)
 
       activities.push(createActivity)
index f851ef652d0c2d2a251e58f6cbf9bd4a44a3f396..5de38eb434c85571b0e18aaa484753b98b82170e 100644 (file)
@@ -1,6 +1,8 @@
 import express from 'express'
 
-function activityPubResponse (data: any, res: express.Response) {
+async function activityPubResponse (promise: Promise<any>, res: express.Response) {
+  const data = await promise
+
   return res.type('application/activity+json; charset=utf-8')
             .json(data)
 }
index 349c4d2273ee6d21c273d25aaa3cfeea5268be94..a3ca52a31569227246b5eff8816e474bc751eab9 100644 (file)
@@ -1,7 +1,8 @@
 import { ContextType } from '@shared/models'
+import { Hooks } from '../plugins/hooks'
 
-function activityPubContextify <T> (data: T, type: ContextType) {
-  return { ...getContextData(type), ...data }
+async function activityPubContextify <T> (data: T, type: ContextType) {
+  return { ...await getContextData(type), ...data }
 }
 
 // ---------------------------------------------------------------------------
@@ -165,10 +166,13 @@ const contextStore: { [ id in ContextType ]: (string | { [ id: string ]: string
   Rate: buildContext()
 }
 
-function getContextData (type: ContextType) {
-  return {
-    '@context': contextStore[type]
-  }
+async function getContextData (type: ContextType) {
+  const contextData = await Hooks.wrapObject(
+    contextStore[type],
+    'filter:activity-pub.activity.context.build.result'
+  )
+
+  return { '@context': contextData }
 }
 
 function buildContext (contextValue?: ContextValue) {
index d8d0b85422f38fcd56f755c943ec4bb6182ef874..ad78698531e9d3a9efe63c56caf6918c0d9f7de6 100644 (file)
@@ -52,9 +52,9 @@ function buildGlobalHeaders (body: any) {
   }
 }
 
-function signAndContextify <T> (byActor: MActor, data: T, contextType: ContextType | null) {
+async function signAndContextify <T> (byActor: MActor, data: T, contextType: ContextType | null) {
   const activity = contextType
-    ? activityPubContextify(data, contextType)
+    ? await activityPubContextify(data, contextType)
     : data
 
   return signJsonLDObject(byActor, activity)
index 7c3a6bdd0a419a39b04f847e12d2340854151d23..0e996ab803b5238c797417384bfaeecc59cb108c 100644 (file)
@@ -33,7 +33,7 @@ async function sendCreateVideo (video: MVideoAP, transaction: Transaction) {
   logger.info('Creating job to send video creation of %s.', video.url, lTags(video.uuid))
 
   const byActor = video.VideoChannel.Account.Actor
-  const videoObject = video.toActivityPubObject()
+  const videoObject = await video.toActivityPubObject()
 
   const audience = getAudience(byActor, video.privacy === VideoPrivacy.PUBLIC)
   const createActivity = buildCreateActivity(video.url, byActor, videoObject, audience)
index 24983dd199d8aad5f00d60e3be5cde10525e93d3..5a66294e6a6104d26b992223c652dff3e36b0586 100644 (file)
@@ -36,7 +36,7 @@ async function sendUpdateVideo (videoArg: MVideoAPWithoutCaption, transaction: T
     video.VideoCaptions = await video.$get('VideoCaptions', { transaction })
   }
 
-  const videoObject = video.toActivityPubObject()
+  const videoObject = await video.toActivityPubObject()
   const audience = getAudience(byActor, video.privacy === VideoPrivacy.PUBLIC)
 
   const updateActivity = buildUpdateActivity(url, byActor, videoObject, audience)
index dc989417bf2ab4c9aea8020e3127584e645c90d4..5bf29f45a8a15f5ad6ca8eefeec6f9c17e1ba3c2 100644 (file)
@@ -447,8 +447,8 @@ export class AccountModel extends Model<Partial<AttributesOnly<AccountModel>>> {
     }
   }
 
-  toActivityPubObject (this: MAccountAP) {
-    const obj = this.Actor.toActivityPubObject(this.name)
+  async toActivityPubObject (this: MAccountAP) {
+    const obj = await this.Actor.toActivityPubObject(this.name)
 
     return Object.assign(obj, {
       summary: this.description
index b71f5a1971602a100119d63ecc42533e6e06804b..67fccab68c11527b5a045f09503bd1d8e39d8874 100644 (file)
@@ -809,8 +809,8 @@ export class VideoChannelModel extends Model<Partial<AttributesOnly<VideoChannel
     return Object.assign(actor, videoChannel)
   }
 
-  toActivityPubObject (this: MChannelAP): ActivityPubActor {
-    const obj = this.Actor.toActivityPubObject(this.name)
+  async toActivityPubObject (this: MChannelAP): Promise<ActivityPubActor> {
+    const obj = await this.Actor.toActivityPubObject(this.name)
 
     return Object.assign(obj, {
       summary: this.description,
index 1a10d2da229ec59d4d8484fc582cea90dbbc07e3..7fe2ec2938795cae5c6d0cbebe5f10e2f2384dce 100644 (file)
@@ -137,6 +137,7 @@ import { VideoShareModel } from './video-share'
 import { VideoSourceModel } from './video-source'
 import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
 import { VideoTagModel } from './video-tag'
+import { Hooks } from '@server/lib/plugins/hooks'
 
 export enum ScopeNames {
   FOR_API = 'FOR_API',
@@ -1713,8 +1714,12 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
     return files
   }
 
-  toActivityPubObject (this: MVideoAP): VideoObject {
-    return videoModelToActivityPubObject(this)
+  toActivityPubObject (this: MVideoAP): Promise<VideoObject> {
+    return Hooks.wrapObject(
+      videoModelToActivityPubObject(this),
+      'filter:activity-pub.video.jsonld.build.result',
+      { video: this }
+    )
   }
 
   getTruncatedDescription () {
index 22fae83316047ec96ee7696be287ae490449201c..c6f1716336030f1de3f569a90483ab6cf8055205 100644 (file)
@@ -2,10 +2,10 @@
 
 import { expect } from 'chai'
 import { buildDigest } from '@server/helpers/peertube-crypto'
-import { HTTP_SIGNATURE } from '@server/initializers/constants'
+import { ACTIVITY_PUB, HTTP_SIGNATURE } from '@server/initializers/constants'
 import { activityPubContextify } from '@server/lib/activitypub/context'
 import { buildGlobalHeaders, signAndContextify } from '@server/lib/activitypub/send'
-import { makeFollowRequest, makePOSTAPRequest } from '@server/tests/shared'
+import { makePOSTAPRequest } from '@server/tests/shared'
 import { buildAbsoluteFixturePath, wait } from '@shared/core-utils'
 import { HttpStatusCode } from '@shared/models'
 import { cleanupTests, createMultipleServers, killallServers, PeerTubeServer } from '@shared/server-commands'
@@ -43,6 +43,32 @@ function getAnnounceWithoutContext (server: PeerTubeServer) {
   return result
 }
 
+async function makeFollowRequest (to: { url: string }, by: { url: string, privateKey }) {
+  const follow = {
+    type: 'Follow',
+    id: by.url + '/' + new Date().getTime(),
+    actor: by.url,
+    object: to.url
+  }
+
+  const body = await activityPubContextify(follow, 'Follow')
+
+  const httpSignature = {
+    algorithm: HTTP_SIGNATURE.ALGORITHM,
+    authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME,
+    keyId: by.url,
+    key: by.privateKey,
+    headers: HTTP_SIGNATURE.HEADERS_TO_SIGN
+  }
+  const headers = {
+    'digest': buildDigest(body),
+    'content-type': 'application/activity+json',
+    'accept': ACTIVITY_PUB.ACCEPT_HEADER
+  }
+
+  return makePOSTAPRequest(to.url + '/inbox', body, httpSignature, headers)
+}
+
 describe('Test ActivityPub security', function () {
   let servers: PeerTubeServer[]
   let url: string
@@ -77,7 +103,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]), 'Announce')
+      const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce')
       const headers = {
         Digest: buildDigest({ hello: 'coucou' })
       }
@@ -91,7 +117,7 @@ describe('Test ActivityPub security', function () {
     })
 
     it('Should fail with an invalid date', async function () {
-      const body = activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce')
+      const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce')
       const headers = buildGlobalHeaders(body)
       headers['date'] = 'Wed, 21 Oct 2015 07:28:00 GMT'
 
@@ -107,7 +133,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]), 'Announce')
+      const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce')
       const headers = buildGlobalHeaders(body)
 
       try {
@@ -122,7 +148,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]), 'Announce')
+      const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce')
       const headers = buildGlobalHeaders(body)
 
       const signatureOptions = baseHttpSignature()
@@ -145,7 +171,7 @@ describe('Test ActivityPub security', function () {
     })
 
     it('Should succeed with a valid HTTP signature draft 11 (without date but with (created))', async function () {
-      const body = activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce')
+      const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce')
       const headers = buildGlobalHeaders(body)
 
       const signatureOptions = baseHttpSignature()
@@ -156,7 +182,7 @@ describe('Test ActivityPub security', function () {
     })
 
     it('Should succeed with a valid HTTP signature', async function () {
-      const body = activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce')
+      const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce')
       const headers = buildGlobalHeaders(body)
 
       const { statusCode } = await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
@@ -175,7 +201,7 @@ describe('Test ActivityPub security', function () {
       await killallServers([ servers[1] ])
       await servers[1].run()
 
-      const body = activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce')
+      const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce')
       const headers = buildGlobalHeaders(body)
 
       try {
index 57120cacacbaf84a785c513e8f95c2ed24238a57..0cfeab7b2fd8844d0c084140af0c26c5c04e10b9 100644 (file)
@@ -1,7 +1,4 @@
-import { buildDigest } from '@server/helpers/peertube-crypto'
 import { doRequest } from '@server/helpers/requests'
-import { ACTIVITY_PUB, HTTP_SIGNATURE } from '@server/initializers/constants'
-import { activityPubContextify } from '@server/lib/activitypub/context'
 
 export function makePOSTAPRequest (url: string, body: any, httpSignature: any, headers: any) {
   const options = {
@@ -13,29 +10,3 @@ export function makePOSTAPRequest (url: string, body: any, httpSignature: any, h
 
   return doRequest(url, options)
 }
-
-export async function makeFollowRequest (to: { url: string }, by: { url: string, privateKey }) {
-  const follow = {
-    type: 'Follow',
-    id: by.url + '/' + new Date().getTime(),
-    actor: by.url,
-    object: to.url
-  }
-
-  const body = activityPubContextify(follow, 'Follow')
-
-  const httpSignature = {
-    algorithm: HTTP_SIGNATURE.ALGORITHM,
-    authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME,
-    keyId: by.url,
-    key: by.privateKey,
-    headers: HTTP_SIGNATURE.HEADERS_TO_SIGN
-  }
-  const headers = {
-    'digest': buildDigest(body),
-    'content-type': 'application/activity+json',
-    'accept': ACTIVITY_PUB.ACCEPT_HEADER
-  }
-
-  return makePOSTAPRequest(to.url + '/inbox', body, httpSignature, headers)
-}
index c43e249a644f317cbe226383ff1823200e9c9484..38c1b0cc9e719e927c0245522b98b2bbc9d69346 100644 (file)
@@ -75,8 +75,7 @@ const I18N_LOCALE_ALIAS = {
   'zh': 'zh-Hans-CN'
 }
 
-export const POSSIBLE_LOCALES = Object.keys(I18N_LOCALES)
-                                      .concat(Object.keys(I18N_LOCALE_ALIAS))
+export const POSSIBLE_LOCALES = (Object.keys(I18N_LOCALES) as string[]).concat(Object.keys(I18N_LOCALE_ALIAS))
 
 export function getDefaultLocale () {
   return 'en-US'
index bbd08365cc4d7bbab3528c7805cdd27fab7e80e8..7881beaa29011f77d4f5e91d2cae0a81f3f677a8 100644 (file)
@@ -113,7 +113,13 @@ export const serverFilterHookObject = {
   'filter:transcoding.manual.resolutions-to-transcode.result': true,
   'filter:transcoding.auto.resolutions-to-transcode.result': true,
 
-  'filter:activity-pub.remote-video-comment.create.accept.result': true
+  'filter:activity-pub.remote-video-comment.create.accept.result': true,
+
+  'filter:activity-pub.activity.context.build.result': true,
+
+  // Filter the result of video JSON LD builder
+  // You may also need to use filter:activity-pub.activity.context.build.result to also update JSON LD context
+  'filter:activity-pub.video.jsonld.build.result': true
 }
 
 export type ServerFilterHookName = keyof typeof serverFilterHookObject