From c3441b0320f632e22318261bcd614d10187de22d Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 16 Mar 2023 10:36:33 +0100 Subject: [PATCH] Add video AP hooks --- .../lib/activitypub/videos/shared/creator.ts | 3 + server/lib/activitypub/videos/updater.ts | 3 + .../fixtures/peertube-plugin-test/main.js | 71 +++++++++++-------- server/tests/plugins/action-hooks.ts | 24 +++++++ .../plugins/server/server-hook.model.ts | 8 ++- support/doc/plugins/guide.md | 44 ++++++++++++ 6 files changed, 121 insertions(+), 32 deletions(-) diff --git a/server/lib/activitypub/videos/shared/creator.ts b/server/lib/activitypub/videos/shared/creator.ts index 07252fea2..77321d8a5 100644 --- a/server/lib/activitypub/videos/shared/creator.ts +++ b/server/lib/activitypub/videos/shared/creator.ts @@ -1,6 +1,7 @@ import { logger, loggerTagsFactory, LoggerTagsFn } from '@server/helpers/logger' import { sequelizeTypescript } from '@server/initializers/database' +import { Hooks } from '@server/lib/plugins/hooks' import { autoBlacklistVideoIfNeeded } from '@server/lib/video-blacklist' import { VideoModel } from '@server/models/video/video' import { MThumbnail, MVideoFullLight, MVideoThumbnail } from '@server/types/models' @@ -61,6 +62,8 @@ export class APVideoCreator extends APVideoAbstractBuilder { logger.info('Remote video with uuid %s inserted.', this.videoObject.uuid, this.lTags()) + Hooks.runAction('action:activity-pub.remote-video.created', { video: videoCreated, videoAPObject: this.videoObject }) + return { autoBlacklisted, videoCreated } } catch (err) { // FIXME: Use rollback hook when https://github.com/sequelize/sequelize/pull/13038 is released diff --git a/server/lib/activitypub/videos/updater.ts b/server/lib/activitypub/videos/updater.ts index 0bf32f440..3677dc3bb 100644 --- a/server/lib/activitypub/videos/updater.ts +++ b/server/lib/activitypub/videos/updater.ts @@ -3,6 +3,7 @@ import { resetSequelizeInstance, runInReadCommittedTransaction } from '@server/h import { logger, loggerTagsFactory, LoggerTagsFn } from '@server/helpers/logger' import { Notifier } from '@server/lib/notifier' import { PeerTubeSocket } from '@server/lib/peertube-socket' +import { Hooks } from '@server/lib/plugins/hooks' import { autoBlacklistVideoIfNeeded } from '@server/lib/video-blacklist' import { VideoLiveModel } from '@server/models/video/video-live' import { MActor, MChannelAccountLight, MChannelId, MVideoAccountLightBlacklistAllFiles, MVideoFullLight } from '@server/types/models' @@ -81,6 +82,8 @@ export class APVideoUpdater extends APVideoAbstractBuilder { PeerTubeSocket.Instance.sendVideoLiveNewState(videoUpdated) } + Hooks.runAction('action:activity-pub.remote-video.updated', { video: videoUpdated, videoAPObject: this.videoObject }) + logger.info('Remote video with uuid %s updated', this.videoObject.uuid, this.lTags()) return videoUpdated diff --git a/server/tests/fixtures/peertube-plugin-test/main.js b/server/tests/fixtures/peertube-plugin-test/main.js index 60b8b3ccd..be0df6672 100644 --- a/server/tests/fixtures/peertube-plugin-test/main.js +++ b/server/tests/fixtures/peertube-plugin-test/main.js @@ -1,42 +1,53 @@ async function register ({ registerHook, registerSetting, settingsManager, storageManager, peertubeHelpers }) { - const actionHooks = [ - 'action:application.listening', - 'action:notifier.notification.created', + { + const actionHooks = [ + 'action:application.listening', + 'action:notifier.notification.created', - 'action:api.video.updated', - 'action:api.video.deleted', - 'action:api.video.uploaded', - 'action:api.video.viewed', + 'action:api.video.updated', + 'action:api.video.deleted', + 'action:api.video.uploaded', + 'action:api.video.viewed', - 'action:api.video-channel.created', - 'action:api.video-channel.updated', - 'action:api.video-channel.deleted', + 'action:api.video-channel.created', + 'action:api.video-channel.updated', + 'action:api.video-channel.deleted', - 'action:api.live-video.created', + 'action:api.live-video.created', - 'action:api.video-thread.created', - 'action:api.video-comment-reply.created', - 'action:api.video-comment.deleted', + 'action:api.video-thread.created', + 'action:api.video-comment-reply.created', + 'action:api.video-comment.deleted', - 'action:api.video-caption.created', - 'action:api.video-caption.deleted', + 'action:api.video-caption.created', + 'action:api.video-caption.deleted', - 'action:api.user.blocked', - 'action:api.user.unblocked', - 'action:api.user.registered', - 'action:api.user.created', - 'action:api.user.deleted', - 'action:api.user.updated', - 'action:api.user.oauth2-got-token', + 'action:api.user.blocked', + 'action:api.user.unblocked', + 'action:api.user.registered', + 'action:api.user.created', + 'action:api.user.deleted', + 'action:api.user.updated', + 'action:api.user.oauth2-got-token', - 'action:api.video-playlist-element.created' - ] + 'action:api.video-playlist-element.created' + ] - for (const h of actionHooks) { - registerHook({ - target: h, - handler: () => peertubeHelpers.logger.debug('Run hook %s.', h) - }) + for (const h of actionHooks) { + registerHook({ + target: h, + handler: () => peertubeHelpers.logger.debug('Run hook %s.', h) + }) + } + + for (const h of [ 'action:activity-pub.remote-video.created', 'action:activity-pub.remote-video.updated' ]) { + registerHook({ + target: h, + handler: ({ video, videoAPObject }) => { + peertubeHelpers.logger.debug('Run hook %s - AP %s - video %s.', h, video.name, videoAPObject.name ) + } + }) + } } registerHook({ diff --git a/server/tests/plugins/action-hooks.ts b/server/tests/plugins/action-hooks.ts index a266ae7f1..98228f79d 100644 --- a/server/tests/plugins/action-hooks.ts +++ b/server/tests/plugins/action-hooks.ts @@ -4,6 +4,7 @@ import { ServerHookName, VideoPlaylistPrivacy, VideoPrivacy } from '@shared/mode import { cleanupTests, createMultipleServers, + doubleFollow, killallServers, PeerTubeServer, PluginsCommand, @@ -36,6 +37,8 @@ describe('Test plugin action hooks', function () { enabled: true } }) + + await doubleFollow(servers[0], servers[1]) }) describe('Application hooks', function () { @@ -231,6 +234,27 @@ describe('Test plugin action hooks', function () { }) }) + describe('Activity Pub hooks', function () { + let videoUUID: string + + it('Should run action:activity-pub.remote-video.created', async function () { + this.timeout(30000) + + const { uuid } = await servers[1].videos.quickUpload({ name: 'remote video' }) + videoUUID = uuid + + await servers[0].servers.waitUntilLog('action:activity-pub.remote-video.created - AP remote video - video remote video') + }) + + it('Should run action:activity-pub.remote-video.updated', async function () { + this.timeout(30000) + + await servers[1].videos.update({ id: videoUUID, attributes: { name: 'remote video updated' } }) + + await servers[0].servers.waitUntilLog('action:activity-pub.remote-video.updated - AP remote video - video remote video') + }) + }) + after(async function () { await cleanupTests(servers) }) diff --git a/shared/models/plugins/server/server-hook.model.ts b/shared/models/plugins/server/server-hook.model.ts index ca83672d0..d2ebe936e 100644 --- a/shared/models/plugins/server/server-hook.model.ts +++ b/shared/models/plugins/server/server-hook.model.ts @@ -1,4 +1,4 @@ -// {hookType}:{api?}.{location}.{subLocation?}.{actionType}.{target} +// {hookType}:{root}.{location}.{subLocation?}.{actionType}.{target} export const serverFilterHookObject = { // Filter params/result used to list videos for the REST API @@ -184,7 +184,11 @@ export const serverActionHookObject = { 'action:api.user.oauth2-got-token': true, // Fired when a video is added to a playlist - 'action:api.video-playlist-element.created': true + 'action:api.video-playlist-element.created': true, + + // Fired when a remote video has been created/updated + 'action:activity-pub.remote-video.created': true, + 'action:activity-pub.remote-video.updated': true } export type ServerActionHookName = keyof typeof serverActionHookObject diff --git a/support/doc/plugins/guide.md b/support/doc/plugins/guide.md index 337f3d97f..c5e3236ca 100644 --- a/support/doc/plugins/guide.md +++ b/support/doc/plugins/guide.md @@ -16,6 +16,7 @@ - [Add external auth methods](#add-external-auth-methods) - [Add new transcoding profiles](#add-new-transcoding-profiles) - [Server helpers](#server-helpers) + - [Federation](#federation) - [Client API (themes & plugins)](#client-api-themes--plugins) - [Get plugin static and router routes](#get-plugin-static-and-router-routes) - [Notifier](#notifier) @@ -587,6 +588,49 @@ async function register ({ See the [plugin API reference](https://docs.joinpeertube.org/api/plugins) to see the complete helpers list. +#### Federation + +You can use some server hooks to federate plugin data to other PeerTube instances that may have installed your plugin. + +For example to federate additional video metadata: + +```js +async function register ({ registerHook }) { + + // Send plugin metadata to remote instances + // We also update the JSON LD context because we added a new field + { + registerHook({ + target: 'filter:activity-pub.video.json-ld.build.result', + handler: async (jsonld, { video }) => { + return Object.assign(jsonld, { recordedAt: 'https://example.com/event' }) + } + }) + + registerHook({ + target: 'filter:activity-pub.activity.context.build.result', + handler: jsonld => { + return jsonld.concat([ { recordedAt: 'https://schema.org/recordedAt' } ]) + } + }) + } + + // Save remote video metadata + { + for (const h of [ 'action:activity-pub.remote-video.created', 'action:activity-pub.remote-video.updated' ]) { + registerHook({ + target: h, + handler: ({ video, videoAPObject }) => { + if (videoAPObject.recordedAt) { + // Save information about the video + } + } + }) + } + } +``` + + ### Client API (themes & plugins) #### Get plugin static and router routes