Add plugin system (fixes #3562) (#5498)

* Add plugin system (fixes #3562)

* loading changes

* Use golang for default plugin (faster to compile)

* add remaining pre hooks

* Add remaining plugin hooks

* clippy

* Dont crash if plugin folder cant be read

* add metadata to /api/v4/site

* use plugin pool

* fix api common

* move plugin code to separate repo

* remove dbg

* fix api tests

* Add private message hooks

* only load plugin for epsilon test instance

* 1s timeout

* load plugin over http

* no return value for pre hooks

* dont run plugin hook code if no plugins loaded

* make plugin calls async

* clippy

* spawn_blocking

* fix vote hooks, add lemmy_url

* update before hooks, vote hooks

* adjust post hooks

* rename functions

* expose lemmy version to plugins

* fix hooks

* fix again

* update plugin

* not mut

* clippy

---------

Co-authored-by: Dessalines <dessalines@users.noreply.github.com>
This commit is contained in:
Nutomic 2025-03-28 09:54:02 +00:00 committed by GitHub
parent 1743f21258
commit 4556a94387
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 4860 additions and 1718 deletions

1294
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,8 @@
{
"wasm": [
{
"url": "https://github.com/LemmyNet/lemmy-plugins/releases/download/0.1.0/go_replace_words.wasm",
"hash": "d4f4fcc10360b24ea2f805aa89427b4e4dcf5c34263aedd55b528d2e28ef04b4"
}
]
}

File diff suppressed because it is too large Load diff

View file

@ -92,6 +92,7 @@ LEMMY_CONFIG_LOCATION=./docker/federation/lemmy_delta.hjson \
echo "start epsilon"
LEMMY_CONFIG_LOCATION=./docker/federation/lemmy_epsilon.hjson \
LEMMY_PLUGIN_PATH=api_tests/plugins \
LEMMY_DATABASE_URL="${LEMMY_DATABASE_URL}/lemmy_epsilon" \
target/lemmy_server >$LOG_DIR/lemmy_epsilon.out 2>&1 &

View file

@ -977,6 +977,28 @@ test("Don't allow NSFW posts on instances that disable it", async () => {
);
});
test("Plugin test", async () => {
let community = await createCommunity(epsilon);
let postRes1 = await createPost(
epsilon,
community.community_view.community.id,
"https://example.com/",
randomString(10),
"Rust",
);
expect(postRes1.post_view.post.name).toBe("Go");
await expect(
createPost(
epsilon,
community.community_view.community.id,
"https://example.com/",
randomString(10),
"Java",
),
).rejects.toStrictEqual(Error("plugin_error"));
});
function checkPostReportName(rcv: ReportCombinedView, report: PostReport) {
switch (rcv.type_) {
case "Post":

View file

@ -4,6 +4,7 @@ use lemmy_api_common::{
build_response::build_comment_response,
comment::{CommentResponse, CreateCommentLike},
context::LemmyContext,
plugins::{plugin_hook_after, plugin_hook_before},
send_activity::{ActivityChannel, SendActivityData},
utils::{check_bot_account, check_community_user_action, check_local_vote_mode},
};
@ -64,7 +65,7 @@ pub async fn like_comment(
}
}
let like_form = CommentLikeForm::new(local_user_view.person.id, data.comment_id, data.score);
let mut like_form = CommentLikeForm::new(local_user_view.person.id, data.comment_id, data.score);
// Remove any likes first
let person_id = local_user_view.person.id;
@ -75,7 +76,9 @@ pub async fn like_comment(
let do_add =
like_form.like_score != 0 && (like_form.like_score == 1 || like_form.like_score == -1);
if do_add {
CommentActions::like(&mut context.pool(), &like_form).await?;
like_form = plugin_hook_before("before_comment_vote", like_form).await?;
let like = CommentActions::like(&mut context.pool(), &like_form).await?;
plugin_hook_after("after_comment_vote", &like)?;
}
ActivityChannel::submit_activity(

View file

@ -3,6 +3,7 @@ use actix_web::web::Json;
use lemmy_api_common::{
build_response::build_post_response,
context::LemmyContext,
plugins::{plugin_hook_after, plugin_hook_before},
post::{CreatePostLike, PostResponse},
send_activity::{ActivityChannel, SendActivityData},
utils::{check_bot_account, check_community_user_action, check_local_vote_mode},
@ -47,7 +48,7 @@ pub async fn like_post(
)
.await?;
let like_form = PostLikeForm::new(data.post_id, local_user_view.person.id, data.score);
let mut like_form = PostLikeForm::new(data.post_id, local_user_view.person.id, data.score);
// Remove any likes first
let person_id = local_user_view.person.id;
@ -58,7 +59,9 @@ pub async fn like_post(
let do_add =
like_form.like_score != 0 && (like_form.like_score == 1 || like_form.like_score == -1);
if do_add {
PostActions::like(&mut context.pool(), &like_form).await?;
like_form = plugin_hook_before("before_post_vote", like_form).await?;
let like = PostActions::like(&mut context.pool(), &like_form).await?;
plugin_hook_after("after_post_vote", &like)?;
}
// Mark Post Read

View file

@ -88,5 +88,6 @@ pub async fn leave_admin(
tagline,
my_user: None,
image_upload_disabled: context.settings().pictrs()?.image_upload_disabled,
active_plugins: vec![],
}))
}

View file

@ -36,6 +36,10 @@ full = [
"moka",
"actix-web-httpauth",
"webmention",
"extism",
"extism-convert",
"once_cell",
"serde_json",
]
[dependencies]
@ -60,6 +64,7 @@ anyhow.workspace = true
enum-map = { workspace = true }
actix-web = { workspace = true, optional = true }
urlencoding = { workspace = true }
serde_json = { workspace = true, optional = true }
mime = { version = "0.3.17", optional = true }
mime_guess = "2.0.5"
infer = "0.19.0"
@ -70,6 +75,9 @@ encoding_rs = { version = "0.8.35", optional = true }
jsonwebtoken = { version = "9.3.1", optional = true }
actix-web-httpauth = { version = "0.8.2", optional = true }
webmention = { version = "0.6.0", optional = true }
extism = { git = "https://github.com/extism/extism.git", branch = "pool", optional = true }
extism-convert = { git = "https://github.com/extism/extism.git", branch = "pool", optional = true }
once_cell = { version = "1.21.0", optional = true }
[dev-dependencies]
serial_test = { workspace = true }

View file

@ -10,6 +10,8 @@ pub mod custom_emoji;
pub mod image;
pub mod oauth_provider;
pub mod person;
#[cfg(feature = "full")]
pub mod plugins;
pub mod post;
pub mod private_message;
pub mod reports;

View file

@ -0,0 +1,175 @@
use crate::{site::PluginMetadata, LemmyErrorType};
use anyhow::anyhow;
use extism::{Manifest, PluginBuilder, Pool};
use extism_convert::Json;
use lemmy_utils::{
error::{LemmyError, LemmyResult},
settings::SETTINGS,
VERSION,
};
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use std::{
env,
ffi::OsStr,
fs::{read_dir, File},
io::BufReader,
ops::Deref,
path::PathBuf,
thread::available_parallelism,
time::Duration,
};
use tokio::task::spawn_blocking;
use tracing::warn;
const GET_PLUGIN_TIMEOUT: Duration = Duration::from_secs(1);
/// Call a plugin hook without rewriting data
pub fn plugin_hook_after<T>(name: &'static str, data: &T) -> LemmyResult<()>
where
T: Clone + Serialize + for<'b> Deserialize<'b> + Sync + Send + 'static,
{
let plugins = LemmyPlugins::init();
if !plugins.loaded(name) {
return Ok(());
}
let data = data.clone();
spawn_blocking(move || {
run_plugin_hook_after(plugins, name, data).inspect_err(|e| warn!("Plugin error: {e}"))
});
Ok(())
}
fn run_plugin_hook_after<T>(plugins: LemmyPlugins, name: &'static str, data: T) -> LemmyResult<()>
where
T: Clone + Serialize + for<'b> Deserialize<'b>,
{
for p in plugins.0 {
// TODO: add helper method (requires PoolPlugin to be public)
// https://github.com/extism/extism/pull/696/files#r2003467812
let p = p
.plugin_pool
.get(&(), GET_PLUGIN_TIMEOUT)?
.ok_or(anyhow!("plugin timeout"))?;
if p.plugin().function_exists(name) {
let params: Json<T> = data.clone().into();
p.call::<Json<T>, ()>(name, params)
.map_err(|e| LemmyErrorType::PluginError(e.to_string()))?;
}
}
Ok(())
}
/// Call a plugin hook which can rewrite data
pub async fn plugin_hook_before<T>(name: &'static str, data: T) -> LemmyResult<T>
where
T: Clone + Serialize + for<'a> Deserialize<'a> + Sync + Send + 'static,
{
let plugins = LemmyPlugins::init();
if !plugins.loaded(name) {
return Ok(data);
}
spawn_blocking(move || {
let mut res: Json<T> = data.into();
for p in plugins.0 {
// TODO: add helper method (see above)
let plugin = p
.plugin_pool
.get(&(), GET_PLUGIN_TIMEOUT)?
.ok_or(anyhow!("plugin timeout"))?;
if plugin.plugin().function_exists(name) {
let r = plugin
.call(name, res)
.map_err(|e| LemmyErrorType::PluginError(e.to_string()))?;
res = r;
}
}
Ok::<_, LemmyError>(res.0)
})
.await?
}
pub fn plugin_metadata() -> Vec<PluginMetadata> {
LemmyPlugins::init()
.0
.into_iter()
.map(|p| p.metadata)
.collect()
}
#[derive(Clone)]
struct LemmyPlugins(Vec<LemmyPlugin>);
#[derive(Clone)]
struct LemmyPlugin {
plugin_pool: Pool<()>,
metadata: PluginMetadata,
}
impl LemmyPlugin {
fn init(path: &PathBuf) -> LemmyResult<Self> {
let file = File::open(path)?;
let reader = BufReader::new(file);
let mut manifest: Manifest = serde_json::from_reader(reader)?;
manifest.config.insert(
"lemmy_url".to_string(),
format!("http://{}:{}/", SETTINGS.bind, SETTINGS.port),
);
manifest
.config
.insert("lemmy_version".to_string(), VERSION.to_string());
let plugin_pool: Pool<()> = Pool::new(available_parallelism()?.into());
let builder = PluginBuilder::new(manifest).with_wasi(true);
let metadata: PluginMetadata = builder.clone().build()?.call("metadata", 0)?;
plugin_pool.add_builder((), builder);
Ok(LemmyPlugin {
plugin_pool,
metadata,
})
}
}
impl LemmyPlugins {
/// Load and initialize all plugins
fn init() -> Self {
// TODO: use std::sync::OnceLock once get_mut_or_init() is stabilized
// https://doc.rust-lang.org/std/sync/struct.OnceLock.html#method.get_mut_or_init
static PLUGINS: Lazy<LemmyPlugins> = Lazy::new(|| {
let dir = env::var("LEMMY_PLUGIN_PATH").unwrap_or("plugins".to_string());
let plugin_paths = match read_dir(dir) {
Ok(r) => r,
Err(e) => {
warn!("Failed to read plugin folder: {e}");
return LemmyPlugins(vec![]);
}
};
let plugins = plugin_paths
.flat_map(Result::ok)
.map(|p| p.path())
.filter(|p| p.extension() == Some(OsStr::new("json")))
.flat_map(|p| {
LemmyPlugin::init(&p)
.inspect_err(|e| warn!("Failed to load plugin {}: {e}", p.to_string_lossy()))
.ok()
})
.collect();
LemmyPlugins(plugins)
});
PLUGINS.deref().clone()
}
/// Return early if no plugin is loaded for the given hook name
fn loaded(&self, _name: &'static str) -> bool {
// Check if there is any plugin active for this hook, to avoid unnecessary data cloning
// TODO: not currently supported by pool
/*
if !self.0.iter().any(|p| p.plugin_pool.function_exists(name)) {
return Ok(None);
}
*/
!self.0.is_empty()
}
}

View file

@ -46,8 +46,9 @@ use lemmy_db_views::structs::{
};
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
use url::Url;
#[cfg(feature = "full")]
use ts_rs::TS;
use {extism::FromBytes, extism_convert::encoding, extism_convert::Json, ts_rs::TS};
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
@ -433,6 +434,7 @@ pub struct GetSiteResponse {
// If true then uploads for post images or markdown images are disabled. Only avatars, icons and
// banners can be set.
pub image_upload_disabled: bool,
pub active_plugins: Vec<PluginMetadata>,
}
#[skip_serializing_none]
@ -642,3 +644,13 @@ pub struct AdminAllowInstanceParams {
#[cfg_attr(feature = "full", ts(optional))]
pub reason: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[cfg_attr(feature = "full", derive(TS, FromBytes))]
#[cfg_attr(feature = "full", ts(export))]
#[cfg_attr(feature = "full", encoding(Json))]
pub struct PluginMetadata {
name: String,
url: Url,
description: String,
}

View file

@ -30,7 +30,7 @@ regex = { workspace = true }
serde_json = { workspace = true }
serde = { workspace = true }
serde_with = { workspace = true }
diesel-async = { workspace = true, features = ["deadpool", "postgres"] }
diesel-async = { workspace = true }
[package.metadata.cargo-shear]
ignored = ["futures"]

View file

@ -5,6 +5,7 @@ use lemmy_api_common::{
build_response::{build_comment_response, send_local_notifs},
comment::{CommentResponse, CreateComment},
context::LemmyContext,
plugins::{plugin_hook_after, plugin_hook_before},
send_activity::{ActivityChannel, SendActivityData},
utils::{
check_community_user_action,
@ -98,17 +99,19 @@ pub async fn create_comment(
)
.await?;
let comment_form = CommentInsertForm {
let mut comment_form = CommentInsertForm {
language_id: Some(language_id),
federation_pending: Some(community_use_pending(&post_view.community, &context).await),
..CommentInsertForm::new(local_user_view.person.id, data.post_id, content.clone())
};
comment_form = plugin_hook_before("before_create_local_comment", comment_form).await?;
// Create the comment
let parent_path = parent_opt.clone().map(|t| t.path);
let inserted_comment = Comment::create(&mut context.pool(), &comment_form, parent_path.as_ref())
.await
.with_lemmy_type(LemmyErrorType::CouldntCreateComment)?;
plugin_hook_after("after_create_local_comment", &inserted_comment)?;
let inserted_comment_id = inserted_comment.id;

View file

@ -5,6 +5,7 @@ use lemmy_api_common::{
build_response::{build_comment_response, send_local_notifs},
comment::{CommentResponse, EditComment},
context::LemmyContext,
plugins::{plugin_hook_after, plugin_hook_before},
send_activity::{ActivityChannel, SendActivityData},
utils::{check_community_user_action, get_url_blocklist, process_markdown_opt, slur_regex},
};
@ -61,15 +62,17 @@ pub async fn update_comment(
}
let comment_id = data.comment_id;
let form = CommentUpdateForm {
let mut form = CommentUpdateForm {
content,
language_id: Some(language_id),
updated: Some(Some(Utc::now())),
..Default::default()
};
form = plugin_hook_before("before_update_local_comment", form).await?;
let updated_comment = Comment::update(&mut context.pool(), comment_id, &form)
.await
.with_lemmy_type(LemmyErrorType::CouldntUpdateComment)?;
plugin_hook_after("after_update_local_comment", &updated_comment)?;
// Do the mentions / recipients
let updated_comment_content = updated_comment.content.clone();

View file

@ -5,6 +5,7 @@ use actix_web::web::Json;
use lemmy_api_common::{
build_response::{build_post_response, send_local_notifs},
context::LemmyContext,
plugins::{plugin_hook_after, plugin_hook_before},
post::{CreatePost, PostResponse},
request::generate_post_link_metadata,
send_activity::SendActivityData,
@ -107,7 +108,7 @@ pub async fn create_post(
let scheduled_publish_time =
convert_published_time(data.scheduled_publish_time, &local_user_view, &context).await?;
let post_form = PostInsertForm {
let mut post_form = PostInsertForm {
url,
body,
alt_text: data.alt_text.clone(),
@ -122,9 +123,12 @@ pub async fn create_post(
)
};
post_form = plugin_hook_before("before_create_local_post", post_form).await?;
let inserted_post = Post::create(&mut context.pool(), &post_form)
.await
.with_lemmy_type(LemmyErrorType::CouldntCreatePost)?;
plugin_hook_after("after_create_local_post", &inserted_post)?;
let community_id = community.id;
let federate_post = if scheduled_publish_time.is_none() {

View file

@ -5,6 +5,7 @@ use chrono::Utc;
use lemmy_api_common::{
build_response::{build_post_response, send_local_notifs},
context::LemmyContext,
plugins::{plugin_hook_after, plugin_hook_before},
post::{EditPost, PostResponse},
request::generate_post_link_metadata,
send_activity::SendActivityData,
@ -129,7 +130,7 @@ pub async fn update_post(
(_, _) => None,
};
let post_form = PostUpdateForm {
let mut post_form = PostUpdateForm {
name: data.name.clone(),
url,
body,
@ -140,11 +141,13 @@ pub async fn update_post(
scheduled_publish_time,
..Default::default()
};
post_form = plugin_hook_before("before_update_local_post", post_form).await?;
let post_id = data.post_id;
let updated_post = Post::update(&mut context.pool(), post_id, &post_form)
.await
.with_lemmy_type(LemmyErrorType::CouldntUpdatePost)?;
plugin_hook_after("after_update_local_post", &post_form)?;
// Scan the post body for user mentions, add those rows
let mentions = scrape_text_for_mentions(&updated_post.body.clone().unwrap_or_default());

View file

@ -2,6 +2,7 @@ use activitypub_federation::config::Data;
use actix_web::web::Json;
use lemmy_api_common::{
context::LemmyContext,
plugins::{plugin_hook_after, plugin_hook_before},
private_message::{CreatePrivateMessage, PrivateMessageResponse},
send_activity::{ActivityChannel, SendActivityData},
utils::{
@ -52,15 +53,20 @@ pub async fn create_private_message(
check_private_messages_enabled(&recipient_local_user)?;
}
let private_message_form = PrivateMessageInsertForm::new(
let mut form = PrivateMessageInsertForm::new(
local_user_view.person.id,
data.recipient_id,
content.clone(),
);
let inserted_private_message = PrivateMessage::create(&mut context.pool(), &private_message_form)
form = plugin_hook_before("before_create_local_private_message", form).await?;
let inserted_private_message = PrivateMessage::create(&mut context.pool(), &form)
.await
.with_lemmy_type(LemmyErrorType::CouldntCreatePrivateMessage)?;
plugin_hook_after(
"after_create_local_private_message",
&inserted_private_message,
)?;
let view = PrivateMessageView::read(&mut context.pool(), inserted_private_message.id).await?;

View file

@ -3,6 +3,7 @@ use actix_web::web::Json;
use chrono::Utc;
use lemmy_api_common::{
context::LemmyContext,
plugins::{plugin_hook_after, plugin_hook_before},
private_message::{EditPrivateMessage, PrivateMessageResponse},
send_activity::{ActivityChannel, SendActivityData},
utils::{get_url_blocklist, process_markdown, slur_regex},
@ -36,17 +37,16 @@ pub async fn update_private_message(
is_valid_body_field(&content, false)?;
let private_message_id = data.private_message_id;
PrivateMessage::update(
&mut context.pool(),
private_message_id,
&PrivateMessageUpdateForm {
content: Some(content),
updated: Some(Some(Utc::now())),
..Default::default()
},
)
.await
.with_lemmy_type(LemmyErrorType::CouldntUpdatePrivateMessage)?;
let mut form = PrivateMessageUpdateForm {
content: Some(content),
updated: Some(Some(Utc::now())),
..Default::default()
};
form = plugin_hook_before("before_update_local_private_message", form).await?;
let private_message = PrivateMessage::update(&mut context.pool(), private_message_id, &form)
.await
.with_lemmy_type(LemmyErrorType::CouldntUpdatePrivateMessage)?;
plugin_hook_after("after_update_local_private_message", &private_message)?;
let view = PrivateMessageView::read(&mut context.pool(), private_message_id).await?;

View file

@ -1,6 +1,6 @@
use crate::user::my_user::get_my_user;
use actix_web::web::{Data, Json};
use lemmy_api_common::{context::LemmyContext, site::GetSiteResponse};
use lemmy_api_common::{context::LemmyContext, plugins::plugin_metadata, site::GetSiteResponse};
use lemmy_db_schema::source::{
actor_language::SiteLanguage,
language::Language,
@ -75,5 +75,6 @@ async fn read_site(context: &LemmyContext) -> LemmyResult<GetSiteResponse> {
oauth_providers,
admin_oauth_providers,
image_upload_disabled: context.settings().pictrs()?.image_upload_disabled,
active_plugins: plugin_metadata(),
})
}

View file

@ -9,7 +9,10 @@ use crate::{
},
};
use activitypub_federation::{config::Data, fetch::object_id::ObjectId};
use lemmy_api_common::context::LemmyContext;
use lemmy_api_common::{
context::LemmyContext,
plugins::{plugin_hook_after, plugin_hook_before},
};
use lemmy_db_schema::{
newtypes::DbUrl,
source::{
@ -59,11 +62,13 @@ async fn vote_comment(
context: &Data<LemmyContext>,
) -> LemmyResult<()> {
let comment_id = comment.id;
let like_form = CommentLikeForm::new(actor.id, comment_id, vote_type.into());
let mut like_form = CommentLikeForm::new(actor.id, comment_id, vote_type.into());
let person_id = actor.id;
comment.set_not_pending(&mut context.pool()).await?;
CommentActions::remove_like(&mut context.pool(), person_id, comment_id).await?;
CommentActions::like(&mut context.pool(), &like_form).await?;
like_form = plugin_hook_before("before_comment_vote", like_form).await?;
let like = CommentActions::like(&mut context.pool(), &like_form).await?;
plugin_hook_after("after_comment_vote", &like)?;
Ok(())
}
@ -74,11 +79,13 @@ async fn vote_post(
context: &Data<LemmyContext>,
) -> LemmyResult<()> {
let post_id = post.id;
let like_form = PostLikeForm::new(post.id, actor.id, vote_type.into());
let mut like_form = PostLikeForm::new(post.id, actor.id, vote_type.into());
let person_id = actor.id;
post.set_not_pending(&mut context.pool()).await?;
PostActions::remove_like(&mut context.pool(), person_id, post_id).await?;
PostActions::like(&mut context.pool(), &like_form).await?;
like_form = plugin_hook_before("before_post_vote", like_form).await?;
let like = PostActions::like(&mut context.pool(), &like_form).await?;
plugin_hook_after("after_post_vote", &like)?;
Ok(())
}

View file

@ -22,6 +22,7 @@ use activitypub_federation::{
use chrono::{DateTime, Utc};
use lemmy_api_common::{
context::LemmyContext,
plugins::{plugin_hook_after, plugin_hook_before},
utils::{get_url_blocklist, is_mod_or_admin, process_markdown, slur_regex},
};
use lemmy_db_schema::{
@ -189,7 +190,7 @@ impl Object for ApubComment {
.await?,
);
let form = CommentInsertForm {
let mut form = CommentInsertForm {
creator_id: creator.id,
post_id: post.id,
content,
@ -203,6 +204,7 @@ impl Object for ApubComment {
language_id,
federation_pending: Some(false),
};
form = plugin_hook_before("before_receive_federated_comment", form).await?;
let parent_comment_path = parent_comment.map(|t| t.0.path);
let timestamp: DateTime<Utc> = note.updated.or(note.published).unwrap_or_else(Utc::now);
let comment = Comment::insert_apub(
@ -212,6 +214,7 @@ impl Object for ApubComment {
parent_comment_path.as_ref(),
)
.await?;
plugin_hook_after("after_receive_federated_comment", &comment)?;
Ok(comment.into())
}
}

View file

@ -27,6 +27,7 @@ use chrono::{DateTime, Utc};
use html2text::{from_read_with_decorator, render::TrivialDecorator};
use lemmy_api_common::{
context::LemmyContext,
plugins::{plugin_hook_after, plugin_hook_before},
request::generate_post_link_metadata,
utils::{
check_nsfw_allowed,
@ -271,7 +272,7 @@ impl Object for ApubPost {
.await?,
);
let form = PostInsertForm {
let mut form = PostInsertForm {
url: url.map(Into::into),
body,
alt_text,
@ -284,9 +285,11 @@ impl Object for ApubPost {
language_id,
..PostInsertForm::new(name, creator.id, community.id)
};
form = plugin_hook_before("before_receive_federated_post", form).await?;
let timestamp = page.updated.or(page.published).unwrap_or_else(Utc::now);
let post = Post::insert_apub(&mut context.pool(), timestamp, &form).await?;
plugin_hook_after("after_receive_federated_post", &post)?;
let post_ = post.clone();
let context_ = context.reset_request_count();

View file

@ -18,6 +18,7 @@ use activitypub_federation::{
use chrono::{DateTime, Utc};
use lemmy_api_common::{
context::LemmyContext,
plugins::{plugin_hook_after, plugin_hook_before},
utils::{check_private_messages_enabled, get_url_blocklist, process_markdown, slur_regex},
};
use lemmy_db_schema::{
@ -152,7 +153,7 @@ impl Object for ApubPrivateMessage {
let content = process_markdown(&content, &slur_regex, &url_blocklist, context).await?;
let content = markdown_rewrite_remote_links(content, context).await;
let form = PrivateMessageInsertForm {
let mut form = PrivateMessageInsertForm {
creator_id: creator.id,
recipient_id: recipient.id,
content,
@ -163,8 +164,10 @@ impl Object for ApubPrivateMessage {
ap_id: Some(note.id.into()),
local: Some(false),
};
form = plugin_hook_before("before_receive_federated_private_message", form).await?;
let timestamp = note.updated.or(note.published).unwrap_or_else(Utc::now);
let pm = DbPrivateMessage::insert_apub(&mut context.pool(), timestamp, &form).await?;
plugin_hook_after("after_receive_federated_private_message", &pm)?;
Ok(pm.into())
}
}

View file

@ -66,7 +66,10 @@ pub struct Comment {
}
#[derive(Debug, Clone, derive_new::new)]
#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))]
#[cfg_attr(
feature = "full",
derive(Insertable, AsChangeset, Serialize, Deserialize)
)]
#[cfg_attr(feature = "full", diesel(table_name = comment))]
pub struct CommentInsertForm {
pub creator_id: PersonId,
@ -93,7 +96,7 @@ pub struct CommentInsertForm {
}
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "full", derive(AsChangeset))]
#[cfg_attr(feature = "full", derive(AsChangeset, Serialize, Deserialize))]
#[cfg_attr(feature = "full", diesel(table_name = comment))]
pub struct CommentUpdateForm {
pub content: Option<String>,
@ -112,7 +115,7 @@ pub struct CommentUpdateForm {
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(
feature = "full",
derive(Identifiable, Queryable, Selectable, Associations, TS)
derive(Identifiable, Queryable, Selectable, Associations, TS,)
)]
#[cfg_attr(feature = "full", diesel(belongs_to(crate::source::comment::Comment)))]
#[cfg_attr(feature = "full", diesel(table_name = comment_actions))]
@ -134,7 +137,10 @@ pub struct CommentActions {
}
#[derive(Clone, derive_new::new)]
#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))]
#[cfg_attr(
feature = "full",
derive(Insertable, AsChangeset, Serialize, Deserialize)
)]
#[cfg_attr(feature = "full", diesel(table_name = comment_actions))]
pub struct CommentLikeForm {
pub person_id: PersonId,

View file

@ -98,8 +98,12 @@ pub struct Post {
pub federation_pending: bool,
}
// TODO: FromBytes, ToBytes are only needed to develop wasm plugin, could be behind feature flag
#[derive(Debug, Clone, derive_new::new)]
#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))]
#[cfg_attr(
feature = "full",
derive(Insertable, AsChangeset, Serialize, Deserialize)
)]
#[cfg_attr(feature = "full", diesel(table_name = post))]
pub struct PostInsertForm {
pub name: String,
@ -150,7 +154,7 @@ pub struct PostInsertForm {
}
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "full", derive(AsChangeset))]
#[cfg_attr(feature = "full", derive(AsChangeset, Serialize, Deserialize))]
#[cfg_attr(feature = "full", diesel(table_name = post))]
pub struct PostUpdateForm {
pub name: Option<String>,
@ -177,11 +181,10 @@ pub struct PostUpdateForm {
pub federation_pending: Option<bool>,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(
feature = "full",
derive(Identifiable, Queryable, Selectable, Associations, TS)
derive(Identifiable, Queryable, Selectable, Associations, TS,)
)]
#[cfg_attr(feature = "full", diesel(belongs_to(crate::source::post::Post)))]
#[cfg_attr(feature = "full", diesel(table_name = post_actions))]
@ -216,7 +219,10 @@ pub struct PostActions {
}
#[derive(Clone, derive_new::new)]
#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))]
#[cfg_attr(
feature = "full",
derive(Insertable, AsChangeset, Serialize, Deserialize)
)]
#[cfg_attr(feature = "full", diesel(table_name = post_actions))]
pub struct PostLikeForm {
pub post_id: PostId,

View file

@ -37,7 +37,10 @@ pub struct PrivateMessage {
}
#[derive(Clone, derive_new::new)]
#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))]
#[cfg_attr(
feature = "full",
derive(Insertable, AsChangeset, Serialize, Deserialize)
)]
#[cfg_attr(feature = "full", diesel(table_name = private_message))]
pub struct PrivateMessageInsertForm {
pub creator_id: PersonId,
@ -58,7 +61,7 @@ pub struct PrivateMessageInsertForm {
}
#[derive(Clone, Default)]
#[cfg_attr(feature = "full", derive(AsChangeset))]
#[cfg_attr(feature = "full", derive(AsChangeset, Serialize, Deserialize))]
#[cfg_attr(feature = "full", diesel(table_name = private_message))]
pub struct PrivateMessageUpdateForm {
pub content: Option<String>,

View file

@ -158,6 +158,7 @@ pub enum LemmyErrorType {
error: Option<FederationError>,
},
CouldntParsePaginationToken,
PluginError(String),
}
/// Federation related errors, these dont need to be translated.