From 9a6486e153cb080c74a15666c3b08f27235d75e4 Mon Sep 17 00:00:00 2001 From: daywalker90 <8257956+daywalker90@users.noreply.github.com> Date: Mon, 10 Nov 2025 14:52:58 +0100 Subject: [PATCH] cln-plugin: add a hook builder to make use of before, after and filters options --- plugins/examples/cln-plugin-startup.rs | 9 ++- plugins/lsps-plugin/src/client.rs | 10 ++- plugins/lsps-plugin/src/service.rs | 5 +- plugins/src/lib.rs | 95 +++++++++++++++++++++++++- plugins/src/messages.rs | 11 ++- 5 files changed, 123 insertions(+), 7 deletions(-) diff --git a/plugins/examples/cln-plugin-startup.rs b/plugins/examples/cln-plugin-startup.rs index d8500f930d6d..12578fe40a08 100644 --- a/plugins/examples/cln-plugin-startup.rs +++ b/plugins/examples/cln-plugin-startup.rs @@ -7,7 +7,7 @@ use cln_plugin::options::{ DefaultStringArrayConfigOption, IntegerArrayConfigOption, IntegerConfigOption, StringArrayConfigOption, }; -use cln_plugin::{messages, Builder, Error, Plugin}; +use cln_plugin::{messages, Builder, Error, HookBuilder, Plugin}; const TEST_NOTIF_TAG: &str = "test_custom_notification"; @@ -79,7 +79,12 @@ async fn main() -> Result<(), anyhow::Error> { .rpcmethod("test-log-levels", "send on all log levels", test_log_levels) .subscribe("connect", connect_handler) .subscribe("test_custom_notification", test_receive_custom_notification) - .hook("peer_connected", peer_connected_handler) + .hook_from_builder( + HookBuilder::new("peer_connected", peer_connected_handler) + .after(Vec::new()) + .before(Vec::new()) + .filters(Vec::new()), + ) .notification(messages::NotificationTopic::new(TEST_NOTIF_TAG)) .start(state) .await? diff --git a/plugins/lsps-plugin/src/client.rs b/plugins/lsps-plugin/src/client.rs index 0ca25e3341f1..0bcf69d4d547 100644 --- a/plugins/lsps-plugin/src/client.rs +++ b/plugins/lsps-plugin/src/client.rs @@ -5,7 +5,9 @@ use cln_lsps::jsonrpc::client::JsonRpcClient; use cln_lsps::lsps0::primitives::Msat; use cln_lsps::lsps0::{ self, - transport::{Bolt8Transport, CustomMessageHookManager, WithCustomMessageHookManager}, + transport::{ + Bolt8Transport, CustomMessageHookManager, WithCustomMessageHookManager, LSPS0_MESSAGE_TYPE, + }, }; use cln_lsps::lsps2::cln::tlv::encode_tu64; use cln_lsps::lsps2::cln::{ @@ -19,6 +21,7 @@ use cln_lsps::lsps2::model::{ use cln_lsps::util; use cln_lsps::LSP_FEATURE_BIT; use cln_plugin::options; +use cln_plugin::{HookBuilder, HookFilter}; use cln_rpc::model::requests::{ DatastoreMode, DatastoreRequest, DeldatastoreRequest, DelinvoiceRequest, DelinvoiceStatus, ListdatastoreRequest, ListinvoicesRequest, ListpeersRequest, @@ -55,7 +58,10 @@ async fn main() -> Result<(), anyhow::Error> { let state = State { hook_manager }; if let Some(plugin) = cln_plugin::Builder::new(tokio::io::stdin(), tokio::io::stdout()) - .hook("custommsg", CustomMessageHookManager::on_custommsg::) + .hook_from_builder( + HookBuilder::new("custommsg", CustomMessageHookManager::on_custommsg::) + .filters(vec![HookFilter::Int(i64::from(LSPS0_MESSAGE_TYPE))]), + ) .option(OPTION_ENABLED) .rpcmethod( "lsps-listprotocols", diff --git a/plugins/lsps-plugin/src/service.rs b/plugins/lsps-plugin/src/service.rs index 03f3f7e670f6..234055f87695 100644 --- a/plugins/lsps-plugin/src/service.rs +++ b/plugins/lsps-plugin/src/service.rs @@ -12,6 +12,7 @@ use cln_lsps::lsps2::model::{Lsps2BuyRequest, Lsps2GetInfoRequest}; use cln_lsps::util::wrap_payload_with_peer_id; use cln_lsps::{lsps0, lsps2}; use cln_plugin::Plugin; +use cln_plugin::{HookBuilder, HookFilter}; use cln_rpc::notifications::CustomMsgNotification; use cln_rpc::primitives::PublicKey; use log::debug; @@ -42,7 +43,9 @@ async fn main() -> Result<(), anyhow::Error> { // cln_plugin::FeatureBitsKind::Init, // util::feature_bit_to_hex(LSP_FEATURE_BIT), // ) - .hook("custommsg", on_custommsg) + .hook_from_builder(HookBuilder::new("custommsg", on_custommsg).filters(vec![ + HookFilter::Int(i64::from(lsps0::transport::LSPS0_MESSAGE_TYPE)), + ])) .hook("htlc_accepted", on_htlc_accepted) .configure() .await? diff --git a/plugins/src/lib.rs b/plugins/src/lib.rs index dc9cf1ab5a4c..e9a3a10d7888 100644 --- a/plugins/src/lib.rs +++ b/plugins/src/lib.rs @@ -2,6 +2,7 @@ use crate::codec::{JsonCodec, JsonRpcCodec}; pub use anyhow::anyhow; use anyhow::{Context, Result}; use futures::sink::SinkExt; +use serde::Serialize; use tokio::io::{AsyncReadExt, AsyncWriteExt}; extern crate log; use log::trace; @@ -204,12 +205,21 @@ where self.hooks.insert( hookname.to_string(), Hook { + name: hookname.to_string(), callback: Box::new(move |p, r| Box::pin(callback(p, r))), + before: Vec::new(), + after: Vec::new(), + filters: None, }, ); self } + pub fn hook_from_builder(mut self, hook: HookBuilder) -> Builder { + self.hooks.insert(hook.name.clone(), hook.build()); + self + } + /// Register a custom RPC method for the RPC passthrough from the /// main daemon pub fn rpcmethod(mut self, name: &str, description: &str, callback: C) -> Builder @@ -411,10 +421,21 @@ where .chain(self.wildcard_subscription.iter().map(|_| String::from("*"))) .collect(); + let hooks: Vec = self + .hooks + .values() + .map(|v| messages::Hook { + name: v.name.clone(), + before: v.before.clone(), + after: v.after.clone(), + filters: v.filters.clone(), + }) + .collect(); + messages::GetManifestResponse { options: self.options.values().cloned().collect(), subscriptions, - hooks: self.hooks.keys().map(|s| s.clone()).collect(), + hooks, rpcmethods, notifications: self.notifications.clone(), featurebits: self.featurebits.clone(), @@ -458,6 +479,56 @@ where } } +impl HookBuilder +where + S: Send + Clone, +{ + pub fn new(name: &str, callback: C) -> Self + where + C: Send + Sync + 'static, + C: Fn(Plugin, Request) -> F + 'static, + F: Future + Send + 'static, + { + Self { + name: name.to_string(), + callback: Box::new(move |p, r| Box::pin(callback(p, r))), + before: Vec::new(), + after: Vec::new(), + filters: None, + } + } + + pub fn before(mut self, before: Vec) -> Self { + self.before = before; + self + } + + pub fn after(mut self, after: Vec) -> Self { + self.after = after; + self + } + + pub fn filters(mut self, filters: Vec) -> Self { + // Empty Vec would filter everything, must be None instead to not get serialized + if filters.is_empty() { + self.filters = None; + } else { + self.filters = Some(filters); + } + self + } + + fn build(self) -> Hook { + Hook { + callback: self.callback, + name: self.name, + before: self.before, + after: self.after, + filters: self.filters, + } + } +} + impl RpcMethodBuilder where S: Send + Clone, @@ -542,7 +613,29 @@ struct Hook where S: Clone + Send, { + name: String, + callback: AsyncCallback, + before: Vec, + after: Vec, + filters: Option>, +} + +pub struct HookBuilder +where + S: Clone + Send, +{ + name: String, callback: AsyncCallback, + before: Vec, + after: Vec, + filters: Option>, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(untagged)] +pub enum HookFilter { + Str(String), + Int(i64), } impl Plugin diff --git a/plugins/src/messages.rs b/plugins/src/messages.rs index 75402bb21959..4ed65c27be96 100644 --- a/plugins/src/messages.rs +++ b/plugins/src/messages.rs @@ -1,4 +1,5 @@ use crate::options::UntypedConfigOption; +use crate::HookFilter; use serde::de::{self, Deserializer}; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -150,6 +151,14 @@ pub(crate) struct RpcMethod { pub(crate) usage: String, } +#[derive(Serialize, Default, Debug)] +pub(crate) struct Hook { + pub(crate) name: String, + pub(crate) before: Vec, + pub(crate) after: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) filters: Option>, +} #[derive(Serialize, Default, Debug, Clone)] pub struct NotificationTopic { pub method: String, @@ -175,7 +184,7 @@ pub(crate) struct GetManifestResponse { pub(crate) rpcmethods: Vec, pub(crate) subscriptions: Vec, pub(crate) notifications: Vec, - pub(crate) hooks: Vec, + pub(crate) hooks: Vec, pub(crate) dynamic: bool, pub(crate) featurebits: FeatureBits, pub(crate) nonnumericids: bool,