From 723045a32a41e5ff345393298df4fcc7783cbc4f Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Tue, 7 May 2024 11:05:13 +0200 Subject: [PATCH] add federation plugin hook --- Cargo.lock | 4 +-- Cargo.toml | 4 --- crates/apub/Cargo.toml | 4 +++ crates/apub/src/lib.rs | 1 + crates/apub/src/objects/post.rs | 23 ++++++++++++++-- crates/apub/src/plugins.rs | 39 +++++++++++++++++++++++++++ crates/db_schema/src/source/post.rs | 2 +- src/plugin_middleware.rs | 41 ++--------------------------- 8 files changed, 70 insertions(+), 48 deletions(-) create mode 100644 crates/apub/src/plugins.rs diff --git a/Cargo.lock b/Cargo.lock index 484935a48..18ddf15e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3206,6 +3206,8 @@ dependencies = [ "chrono", "diesel", "enum_delegate", + "extism", + "extism-convert", "futures", "html2md", "html2text", @@ -3404,8 +3406,6 @@ dependencies = [ "console-subscriber 0.1.10", "diesel", "diesel-async", - "extism", - "extism-convert", "futures-util", "lemmy_api", "lemmy_api_common", diff --git a/Cargo.toml b/Cargo.toml index 51e48b0d6..fb227b241 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -207,10 +207,6 @@ clap = { workspace = true } serde.workspace = true actix-web-prom = "0.7.0" actix-http = "3.6.0" -extism = { version = "1.2.0", features = [ - "register-filesystem", -], default-features = false } -extism-convert = { version = "1.2.0", default-features = false } [dev-dependencies] pretty_assertions = { workspace = true } diff --git a/crates/apub/Cargo.toml b/crates/apub/Cargo.toml index 4c3189a09..0bed832c2 100644 --- a/crates/apub/Cargo.toml +++ b/crates/apub/Cargo.toml @@ -47,6 +47,10 @@ html2md = "0.2.14" html2text = "0.6.0" stringreader = "0.1.1" enum_delegate = "0.2.0" +extism = { version = "1.2.0", features = [ + "register-filesystem", +], default-features = false } +extism-convert = { version = "1.2.0", default-features = false } [dev-dependencies] serial_test = { workspace = true } diff --git a/crates/apub/src/lib.rs b/crates/apub/src/lib.rs index a5643b95c..4ab010247 100644 --- a/crates/apub/src/lib.rs +++ b/crates/apub/src/lib.rs @@ -27,6 +27,7 @@ pub mod fetcher; pub mod http; pub(crate) mod mentions; pub mod objects; +pub mod plugins; pub mod protocol; pub const FEDERATION_HTTP_FETCH_LIMIT: u32 = 50; diff --git a/crates/apub/src/objects/post.rs b/crates/apub/src/objects/post.rs index ff11c985c..9650ef54e 100644 --- a/crates/apub/src/objects/post.rs +++ b/crates/apub/src/objects/post.rs @@ -3,6 +3,7 @@ use crate::{ check_apub_id_valid_with_strictness, local_site_data_cached, objects::{read_from_string_or_source_opt, verify_is_remote_object}, + plugins::{call_plugin, load_plugins}, protocol::{ objects::{ page::{Attachment, AttributedTo, Hashtag, HashtagType, Page, PageType}, @@ -50,6 +51,7 @@ use lemmy_utils::{ }; use std::ops::Deref; use stringreader::StringReader; +use tracing::info; use url::Url; const MAX_TITLE_LENGTH: usize = 200; @@ -224,7 +226,7 @@ impl Object for ApubPost { let first_attachment = page.attachment.first(); let local_site = LocalSite::read(&mut context.pool()).await.ok(); - let form = if !page.is_mod_action(context).await? { + let mut form = if !page.is_mod_action(context).await? { let url = if let Some(attachment) = first_attachment.cloned() { Some(attachment.url()) } else if page.kind == PageType::Video { @@ -275,8 +277,25 @@ impl Object for ApubPost { .build() }; + // TODO: move this all into helper function + let before_plugin_hook = "federation_before_receive_post"; + info!("Calling plugin hook {}", &before_plugin_hook); + if let Some(mut plugins) = load_plugins()? { + if plugins.function_exists(&before_plugin_hook) { + call_plugin(plugins, &before_plugin_hook, &mut form)?; + } + } + let timestamp = page.updated.or(page.published).unwrap_or_else(naive_now); - let post = Post::insert_apub(&mut context.pool(), timestamp, &form).await?; + let mut post = Post::insert_apub(&mut context.pool(), timestamp, &form).await?; + + let after_plugin_hook = "federation_after_receive_post"; + info!("Calling plugin hook {}", &after_plugin_hook); + if let Some(mut plugins) = load_plugins()? { + if plugins.function_exists(&after_plugin_hook) { + call_plugin(plugins, &after_plugin_hook, &mut post)?; + } + } generate_post_link_metadata( post.clone(), diff --git a/crates/apub/src/plugins.rs b/crates/apub/src/plugins.rs new file mode 100644 index 000000000..98a666aac --- /dev/null +++ b/crates/apub/src/plugins.rs @@ -0,0 +1,39 @@ +use extism::{Manifest, Plugin}; +use lemmy_utils::{error::LemmyResult, LemmyErrorType}; +use serde::{Deserialize, Serialize}; +use std::{ffi::OsStr, fs::read_dir}; + +pub fn load_plugins() -> LemmyResult> { + // TODO: make dir configurable via env var + // TODO: should only read fs once at startup for performance + let plugin_paths = read_dir("plugins")?; + + let mut wasm_files = vec![]; + for path in plugin_paths { + let path = path?.path(); + if path.extension() == Some(OsStr::new("wasm")) { + wasm_files.push(path); + } + } + if !wasm_files.is_empty() { + // TODO: what if theres more than one plugin for the same hook? + let manifest = Manifest::new(wasm_files); + let plugin = Plugin::new(manifest, [], true)?; + Ok(Some(plugin)) + } else { + Ok(None) + } +} + +pub fn call_plugin Deserialize<'de> + Clone>( + mut plugins: Plugin, + name: &str, + data: &mut T, +) -> LemmyResult<()> { + *data = plugins + .call::, extism_convert::Json>(name, (*data).clone().into()) + .map_err(|e| LemmyErrorType::PluginError(e.to_string()))? + .0 + .into(); + Ok(()) +} diff --git a/crates/db_schema/src/source/post.rs b/crates/db_schema/src/source/post.rs index 541d9c307..b2a87b081 100644 --- a/crates/db_schema/src/source/post.rs +++ b/crates/db_schema/src/source/post.rs @@ -60,7 +60,7 @@ pub struct Post { pub alt_text: Option, } -#[derive(Debug, Clone, TypedBuilder)] +#[derive(Debug, Clone, TypedBuilder, Serialize, Deserialize)] #[builder(field_defaults(default))] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] #[cfg_attr(feature = "full", diesel(table_name = post))] diff --git a/src/plugin_middleware.rs b/src/plugin_middleware.rs index 29e599eb2..59c60288a 100644 --- a/src/plugin_middleware.rs +++ b/src/plugin_middleware.rs @@ -6,12 +6,10 @@ use actix_web::{ Error, }; use core::future::Ready; -use extism::{Manifest, Plugin}; use futures_util::future::LocalBoxFuture; -use lemmy_utils::{error::LemmyResult, LemmyErrorType}; -use serde::{Deserialize, Serialize}; +use lemmy_apub::plugins::{call_plugin, load_plugins}; use serde_json::Value; -use std::{ffi::OsStr, fs::read_dir, future::ready, rc::Rc}; +use std::{future::ready, rc::Rc}; use tracing::info; #[derive(Clone)] @@ -97,38 +95,3 @@ where }) } } - -fn load_plugins() -> LemmyResult> { - // TODO: make dir configurable via env var - // TODO: should only read fs once at startup for performance - let plugin_paths = read_dir("plugins")?; - - let mut wasm_files = vec![]; - for path in plugin_paths { - let path = path?.path(); - if path.extension() == Some(OsStr::new("wasm")) { - wasm_files.push(path); - } - } - if !wasm_files.is_empty() { - // TODO: what if theres more than one plugin for the same hook? - let manifest = Manifest::new(wasm_files); - let plugin = Plugin::new(manifest, [], true)?; - Ok(Some(plugin)) - } else { - Ok(None) - } -} - -pub fn call_plugin Deserialize<'de> + Clone>( - mut plugins: Plugin, - name: &str, - data: &mut T, -) -> LemmyResult<()> { - *data = plugins - .call::, extism_convert::Json>(name, (*data).clone().into()) - .map_err(|e| LemmyErrorType::PluginError(e.to_string()))? - .0 - .into(); - Ok(()) -}