mirror of
https://github.com/LemmyNet/lemmy.git
synced 2025-09-03 11:43:51 +00:00
* 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:
parent
1743f21258
commit
4556a94387
28 changed files with 4860 additions and 1718 deletions
1294
Cargo.lock
generated
1294
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
8
api_tests/plugins/go_replace_words.json
Normal file
8
api_tests/plugins/go_replace_words.json
Normal 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
|
@ -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 &
|
||||
|
||||
|
|
|
@ -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":
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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![],
|
||||
}))
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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;
|
||||
|
|
175
crates/api_common/src/plugins.rs
Normal file
175
crates/api_common/src/plugins.rs
Normal 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()
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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?;
|
||||
|
||||
|
|
|
@ -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?;
|
||||
|
||||
|
|
|
@ -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(),
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -158,6 +158,7 @@ pub enum LemmyErrorType {
|
|||
error: Option<FederationError>,
|
||||
},
|
||||
CouldntParsePaginationToken,
|
||||
PluginError(String),
|
||||
}
|
||||
|
||||
/// Federation related errors, these dont need to be translated.
|
||||
|
|
Loading…
Reference in a new issue