mirror of
https://github.com/LemmyNet/lemmy.git
synced 2024-11-22 20:31:19 +00:00
Use middleware for plugin
This commit is contained in:
parent
2abe6300da
commit
e6795946e4
13 changed files with 140 additions and 59 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -36,4 +36,4 @@ dev_pgdata/
|
||||||
*.sqldump
|
*.sqldump
|
||||||
|
|
||||||
# compiled example plugin
|
# compiled example plugin
|
||||||
example_plugin/plugin.wasm
|
plugins/plugin.wasm
|
||||||
|
|
|
@ -229,7 +229,7 @@ steps:
|
||||||
DO_WRITE_HOSTS_FILE: "1"
|
DO_WRITE_HOSTS_FILE: "1"
|
||||||
commands:
|
commands:
|
||||||
- *install_pnpm
|
- *install_pnpm
|
||||||
- apt update && apt install -y bash curl postgresql-client golang tinygo
|
- apt update && apt install -y bash curl postgresql-client golang
|
||||||
- bash api_tests/prepare-drone-federation-test.sh
|
- bash api_tests/prepare-drone-federation-test.sh
|
||||||
- cd api_tests/
|
- cd api_tests/
|
||||||
- pnpm i
|
- pnpm i
|
||||||
|
|
6
Cargo.lock
generated
6
Cargo.lock
generated
|
@ -3140,8 +3140,6 @@ dependencies = [
|
||||||
"actix-web",
|
"actix-web",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bcrypt",
|
"bcrypt",
|
||||||
"extism",
|
|
||||||
"extism-convert",
|
|
||||||
"futures",
|
"futures",
|
||||||
"lemmy_api_common",
|
"lemmy_api_common",
|
||||||
"lemmy_db_schema",
|
"lemmy_db_schema",
|
||||||
|
@ -3358,6 +3356,7 @@ version = "0.19.4-beta.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"activitypub_federation",
|
"activitypub_federation",
|
||||||
"actix-cors",
|
"actix-cors",
|
||||||
|
"actix-http",
|
||||||
"actix-web",
|
"actix-web",
|
||||||
"actix-web-prom",
|
"actix-web-prom",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
@ -3366,6 +3365,8 @@ dependencies = [
|
||||||
"console-subscriber 0.1.10",
|
"console-subscriber 0.1.10",
|
||||||
"diesel",
|
"diesel",
|
||||||
"diesel-async",
|
"diesel-async",
|
||||||
|
"extism",
|
||||||
|
"extism-convert",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"lemmy_api",
|
"lemmy_api",
|
||||||
"lemmy_api_common",
|
"lemmy_api_common",
|
||||||
|
@ -3383,6 +3384,7 @@ dependencies = [
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"reqwest-middleware",
|
"reqwest-middleware",
|
||||||
"reqwest-tracing",
|
"reqwest-tracing",
|
||||||
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serial_test",
|
"serial_test",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|
|
@ -204,7 +204,11 @@ chrono = { workspace = true }
|
||||||
prometheus = { version = "0.13.3", features = ["process"] }
|
prometheus = { version = "0.13.3", features = ["process"] }
|
||||||
serial_test = { workspace = true }
|
serial_test = { workspace = true }
|
||||||
clap = { workspace = true }
|
clap = { workspace = true }
|
||||||
|
serde.workspace = true
|
||||||
actix-web-prom = "0.7.0"
|
actix-web-prom = "0.7.0"
|
||||||
|
actix-http = "3.6.0"
|
||||||
|
extism = "1.2.0"
|
||||||
|
extism-convert = "1.2.0"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
pretty_assertions = { workspace = true }
|
pretty_assertions = { workspace = true }
|
||||||
|
|
|
@ -74,10 +74,9 @@ LEMMY_CONFIG_LOCATION=./docker/federation/lemmy_delta.hjson \
|
||||||
target/lemmy_server >$LOG_DIR/lemmy_delta.out 2>&1 &
|
target/lemmy_server >$LOG_DIR/lemmy_delta.out 2>&1 &
|
||||||
|
|
||||||
# plugin setup
|
# plugin setup
|
||||||
pushd example_plugin
|
pushd plugins
|
||||||
# TODO: not in ubuntu repos, better to use only `go`
|
# need to use tinygo because apparently go has only experimental support for wasm target
|
||||||
# TODO: prevent it from creating useless `go` folder in home dir
|
GOPATH=$HOME/.local/share/go tinygo build -o plugin.wasm -target wasi main.go
|
||||||
tinygo build -o plugin.wasm -target wasi main.go
|
|
||||||
popd
|
popd
|
||||||
|
|
||||||
echo "start epsilon"
|
echo "start epsilon"
|
||||||
|
|
|
@ -31,8 +31,6 @@ anyhow.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
webmention = "0.5.0"
|
webmention = "0.5.0"
|
||||||
accept-language = "3.1.0"
|
accept-language = "3.1.0"
|
||||||
extism = "1.2.0"
|
|
||||||
extism-convert = "1.2.0"
|
|
||||||
|
|
||||||
[package.metadata.cargo-machete]
|
[package.metadata.cargo-machete]
|
||||||
ignored = ["futures"]
|
ignored = ["futures"]
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
use activitypub_federation::config::Data;
|
use activitypub_federation::config::Data;
|
||||||
use actix_web::web::Json;
|
use actix_web::web::Json;
|
||||||
use extism::*;
|
|
||||||
use lemmy_api_common::{
|
use lemmy_api_common::{
|
||||||
build_response::build_post_response,
|
build_response::build_post_response,
|
||||||
context::LemmyContext,
|
context::LemmyContext,
|
||||||
|
@ -47,20 +46,16 @@ use lemmy_utils::{
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use serde::Serialize;
|
|
||||||
use std::{ffi::OsStr, fs::read_dir};
|
|
||||||
use tracing::Instrument;
|
use tracing::Instrument;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use webmention::{Webmention, WebmentionError};
|
use webmention::{Webmention, WebmentionError};
|
||||||
|
|
||||||
#[tracing::instrument(skip(context))]
|
#[tracing::instrument(skip(context))]
|
||||||
pub async fn create_post(
|
pub async fn create_post(
|
||||||
mut data: Json<CreatePost>,
|
data: Json<CreatePost>,
|
||||||
context: Data<LemmyContext>,
|
context: Data<LemmyContext>,
|
||||||
local_user_view: LocalUserView,
|
local_user_view: LocalUserView,
|
||||||
) -> LemmyResult<Json<PostResponse>> {
|
) -> LemmyResult<Json<PostResponse>> {
|
||||||
plugin_hook("api_before_create_post", &mut (*data))?;
|
|
||||||
|
|
||||||
let local_site = LocalSite::read(&mut context.pool()).await?;
|
let local_site = LocalSite::read(&mut context.pool()).await?;
|
||||||
|
|
||||||
honeypot_check(&data.honeypot)?;
|
honeypot_check(&data.honeypot)?;
|
||||||
|
@ -205,45 +200,5 @@ pub async fn create_post(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut res = build_post_response(&context, community_id, &local_user_view.person, post_id)
|
build_post_response(&context, community_id, &local_user_view.person, post_id).await
|
||||||
.await?
|
|
||||||
.0;
|
|
||||||
|
|
||||||
plugin_hook("api_after_create_post", &mut res)?;
|
|
||||||
Ok(Json(res))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_plugins() -> LemmyResult<Plugin> {
|
|
||||||
// TODO: make dir configurable via env var
|
|
||||||
// TODO: should only read fs once at startup for performance
|
|
||||||
let plugin_paths = read_dir("example_plugin")?;
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let manifest = Manifest::new(wasm_files);
|
|
||||||
let plugin = Plugin::new(manifest, [], true)?;
|
|
||||||
Ok(plugin)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn plugin_hook<T: Serialize + for<'de> serde::Deserialize<'de> + Clone>(
|
|
||||||
name: &'static str,
|
|
||||||
data: &mut T,
|
|
||||||
) -> LemmyResult<()> {
|
|
||||||
let mut plugin = load_plugins()?;
|
|
||||||
if plugin.function_exists(name) {
|
|
||||||
*data = plugin
|
|
||||||
.call::<extism_convert::Json<T>, extism_convert::Json<T>>(name, (*data).clone().into())
|
|
||||||
.map_err(|e| {
|
|
||||||
dbg!(&e);
|
|
||||||
LemmyErrorType::PluginError(e.to_string())
|
|
||||||
})?
|
|
||||||
.0
|
|
||||||
.into();
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,8 +16,8 @@ type CreatePost struct {
|
||||||
Custom_thumbnail *string `json:"custom_thumbnail,omitempty"`
|
Custom_thumbnail *string `json:"custom_thumbnail,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
//export api_before_create_post
|
//export api_before_post_post
|
||||||
func api_before_create_post() int32 {
|
func api_before_post_post() int32 {
|
||||||
params := CreatePost{}
|
params := CreatePost{}
|
||||||
// use json input helper, which automatically unmarshals the plugin input into your struct
|
// use json input helper, which automatically unmarshals the plugin input into your struct
|
||||||
err := pdk.InputJSON(¶ms)
|
err := pdk.InputJSON(¶ms)
|
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::plugin_middleware::PluginMiddleware;
|
||||||
use actix_web::{guard, web};
|
use actix_web::{guard, web};
|
||||||
use lemmy_api::{
|
use lemmy_api::{
|
||||||
comment::{
|
comment::{
|
||||||
|
@ -139,6 +140,7 @@ use lemmy_utils::rate_limit::RateLimitCell;
|
||||||
pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) {
|
pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) {
|
||||||
cfg.service(
|
cfg.service(
|
||||||
web::scope("/api/v3")
|
web::scope("/api/v3")
|
||||||
|
.wrap(PluginMiddleware::new())
|
||||||
.route("/image_proxy", web::get().to(image_proxy))
|
.route("/image_proxy", web::get().to(image_proxy))
|
||||||
// Site
|
// Site
|
||||||
.service(
|
.service(
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
pub mod api_routes_http;
|
pub mod api_routes_http;
|
||||||
pub mod code_migrations;
|
pub mod code_migrations;
|
||||||
|
pub mod plugin_middleware;
|
||||||
pub mod prometheus_metrics;
|
pub mod prometheus_metrics;
|
||||||
pub mod root_span_builder;
|
pub mod root_span_builder;
|
||||||
pub mod scheduled_tasks;
|
pub mod scheduled_tasks;
|
||||||
|
|
120
src/plugin_middleware.rs
Normal file
120
src/plugin_middleware.rs
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
use actix_http::h1::Payload;
|
||||||
|
use actix_web::{
|
||||||
|
body::MessageBody,
|
||||||
|
dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
|
||||||
|
web::Bytes,
|
||||||
|
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 serde_json::Value;
|
||||||
|
use std::{ffi::OsStr, fs::read_dir, future::ready, rc::Rc};
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct PluginMiddleware {}
|
||||||
|
|
||||||
|
impl PluginMiddleware {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
PluginMiddleware {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<S, B> Transform<S, ServiceRequest> for PluginMiddleware
|
||||||
|
where
|
||||||
|
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
|
||||||
|
S::Future: 'static,
|
||||||
|
B: MessageBody + 'static,
|
||||||
|
{
|
||||||
|
type Response = ServiceResponse<B>;
|
||||||
|
type Error = Error;
|
||||||
|
type Transform = SessionService<S>;
|
||||||
|
type InitError = ();
|
||||||
|
type Future = Ready<Result<Self::Transform, Self::InitError>>;
|
||||||
|
|
||||||
|
fn new_transform(&self, service: S) -> Self::Future {
|
||||||
|
ready(Ok(SessionService {
|
||||||
|
service: Rc::new(service),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SessionService<S> {
|
||||||
|
service: Rc<S>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S, B> Service<ServiceRequest> for SessionService<S>
|
||||||
|
where
|
||||||
|
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
|
||||||
|
S::Future: 'static,
|
||||||
|
B: 'static,
|
||||||
|
{
|
||||||
|
type Response = ServiceResponse<B>;
|
||||||
|
type Error = Error;
|
||||||
|
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
|
||||||
|
|
||||||
|
forward_ready!(service);
|
||||||
|
|
||||||
|
fn call(&self, mut service_req: ServiceRequest) -> Self::Future {
|
||||||
|
let svc = self.service.clone();
|
||||||
|
|
||||||
|
Box::pin(async move {
|
||||||
|
let method = service_req.method();
|
||||||
|
let path = service_req.path().replace("/api/v3/", "").replace("/", "_");
|
||||||
|
// TODO: naming can be a bit silly, `POST /api/v3/post` becomes `api_before_post_post`
|
||||||
|
let plugin_hook = format!("api_before_{method}_{path}").to_lowercase();
|
||||||
|
info!("Calling plugin hook {}", &plugin_hook);
|
||||||
|
if let Some(mut plugins) = load_plugins()? {
|
||||||
|
if plugins.function_exists(&plugin_hook) {
|
||||||
|
let payload = service_req.extract::<Bytes>().await?;
|
||||||
|
|
||||||
|
let mut json: Value = serde_json::from_slice(&payload.to_vec())?;
|
||||||
|
call_plugin(plugins, &plugin_hook, &mut json)?;
|
||||||
|
|
||||||
|
let (_, mut new_payload) = Payload::create(true);
|
||||||
|
new_payload.unread_data(Bytes::from(serde_json::to_vec_pretty(&json)?));
|
||||||
|
service_req.set_payload(new_payload.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let res = svc.call(service_req).await?;
|
||||||
|
Ok(res)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_plugins() -> LemmyResult<Option<Plugin>> {
|
||||||
|
// 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<T: Serialize + for<'de> Deserialize<'de> + Clone>(
|
||||||
|
mut plugins: Plugin,
|
||||||
|
name: &str,
|
||||||
|
data: &mut T,
|
||||||
|
) -> LemmyResult<()> {
|
||||||
|
*data = plugins
|
||||||
|
.call::<extism_convert::Json<T>, extism_convert::Json<T>>(name, (*data).clone().into())
|
||||||
|
.map_err(|e| LemmyErrorType::PluginError(e.to_string()))?
|
||||||
|
.0
|
||||||
|
.into();
|
||||||
|
Ok(())
|
||||||
|
}
|
Loading…
Reference in a new issue