From 604709066ef1e730e76258451bee520fb0f2996a 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 | 9 ++- plugins/lsps-plugin/src/service.rs | 9 ++- plugins/src/lib.rs | 95 +++++++++++++++++++++++++- plugins/src/messages.rs | 11 ++- 5 files changed, 123 insertions(+), 10 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 df648ee52b8a..bdf3475fb0f2 100644 --- a/plugins/lsps-plugin/src/client.rs +++ b/plugins/lsps-plugin/src/client.rs @@ -17,11 +17,11 @@ use cln_lsps::{ transport::{MultiplexedTransport, PendingRequests}, }, proto::{ - lsps0::{Msat, LSP_FEATURE_BIT}, + lsps0::{Msat, LSPS0_MESSAGE_TYPE, LSP_FEATURE_BIT}, lsps2::{compute_opening_fee, Lsps2BuyResponse, Lsps2GetInfoResponse, OpeningFeeParams}, }, }; -use cln_plugin::options; +use cln_plugin::{options, HookBuilder, HookFilter}; use cln_rpc::{ model::{ requests::{ @@ -82,7 +82,10 @@ impl ClientState for State { #[tokio::main] async fn main() -> Result<(), anyhow::Error> { if let Some(plugin) = cln_plugin::Builder::new(tokio::io::stdin(), tokio::io::stdout()) - .hook("custommsg", hooks::client_custommsg_hook) + .hook_from_builder( + HookBuilder::new("custommsg", hooks::client_custommsg_hook) + .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 bc59ba51dbe2..2e9ae10c28ce 100644 --- a/plugins/lsps-plugin/src/service.rs +++ b/plugins/lsps-plugin/src/service.rs @@ -12,9 +12,9 @@ use cln_lsps::{ }, server::LspsService, }, - proto::lsps0::Msat, + proto::lsps0::{Msat, LSPS0_MESSAGE_TYPE}, }; -use cln_plugin::{options, Plugin}; +use cln_plugin::{options, HookBuilder, HookFilter, Plugin}; use log::{debug, error, trace}; use std::path::{Path, PathBuf}; use std::sync::Arc; @@ -78,7 +78,10 @@ async fn main() -> Result<(), anyhow::Error> { // cln_plugin::FeatureBitsKind::Init, // util::feature_bit_to_hex(LSP_FEATURE_BIT), // ) - .hook("custommsg", service_custommsg_hook) + .hook_from_builder( + HookBuilder::new("custommsg", service_custommsg_hook) + .filters(vec![HookFilter::Int(i64::from(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,