From: Dessalines Date: Mon, 29 Mar 2021 20:24:50 +0000 (-0400) Subject: Merge remote-tracking branch 'yerba/split-api-crate' into test_merge_api_crates_reorg X-Git-Url: http://these/git/readmes/%24%7B%60data:application/%7BpictrsAvatarThumbnail%28community.icon%29%7D?a=commitdiff_plain;h=4c8f2e976effe381d4ea1914462c43242a8c64fd;p=lemmy.git Merge remote-tracking branch 'yerba/split-api-crate' into test_merge_api_crates_reorg --- 4c8f2e976effe381d4ea1914462c43242a8c64fd diff --cc crates/api_common/src/lib.rs index 00000000,2337f10b..dccc3bce mode 000000,100644..100644 --- a/crates/api_common/src/lib.rs +++ b/crates/api_common/src/lib.rs @@@ -1,0 -1,420 +1,420 @@@ + pub mod comment; + pub mod community; + pub mod person; + pub mod post; + pub mod site; + pub mod websocket; + + use crate::site::FederatedInstances; + use diesel::PgConnection; + use lemmy_db_queries::{ + source::{ + community::{CommunityModerator_, Community_}, + site::Site_, + }, + Crud, + DbPool, + }; + use lemmy_db_schema::{ + source::{ + comment::Comment, + community::{Community, CommunityModerator}, + person::Person, + person_mention::{PersonMention, PersonMentionForm}, + post::Post, + site::Site, + }, + CommunityId, + LocalUserId, + PersonId, + PostId, + }; + use lemmy_db_views::local_user_view::{LocalUserSettingsView, LocalUserView}; + use lemmy_db_views_actor::{ + community_person_ban_view::CommunityPersonBanView, + community_view::CommunityView, + }; + use lemmy_utils::{ + claims::Claims, + email::send_email, + settings::structs::Settings, + utils::MentionData, + ApiError, + LemmyError, + }; + use log::error; + use serde::{Deserialize, Serialize}; + use url::Url; + + #[derive(Serialize, Deserialize, Debug)] + pub struct WebFingerLink { + pub rel: Option, + #[serde(rename(serialize = "type", deserialize = "type"))] + pub type_: Option, + pub href: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub template: Option, + } + + #[derive(Serialize, Deserialize, Debug)] + pub struct WebFingerResponse { + pub subject: String, + pub aliases: Vec, + pub links: Vec, + } + + pub async fn blocking(pool: &DbPool, f: F) -> Result + where + F: FnOnce(&diesel::PgConnection) -> T + Send + 'static, + T: Send + 'static, + { + let pool = pool.clone(); + let res = actix_web::web::block(move || { + let conn = pool.get()?; + let res = (f)(&conn); + Ok(res) as Result<_, LemmyError> + }) + .await?; + + Ok(res) + } + + pub async fn send_local_notifs( + mentions: Vec, + comment: Comment, + person: Person, + post: Post, + pool: &DbPool, + do_send_email: bool, + ) -> Result, LemmyError> { + let ids = blocking(pool, move |conn| { + do_send_local_notifs(conn, &mentions, &comment, &person, &post, do_send_email) + }) + .await?; + + Ok(ids) + } + + fn do_send_local_notifs( + conn: &PgConnection, + mentions: &[MentionData], + comment: &Comment, + person: &Person, + post: &Post, + do_send_email: bool, + ) -> Vec { + let mut recipient_ids = Vec::new(); + + // Send the local mentions + for mention in mentions + .iter() + .filter(|m| m.is_local() && m.name.ne(&person.name)) + .collect::>() + { + if let Ok(mention_user_view) = LocalUserView::read_from_name(&conn, &mention.name) { + // TODO + // At some point, make it so you can't tag the parent creator either + // This can cause two notifications, one for reply and the other for mention + recipient_ids.push(mention_user_view.local_user.id); + + let user_mention_form = PersonMentionForm { + recipient_id: mention_user_view.person.id, + comment_id: comment.id, + read: None, + }; + + // Allow this to fail softly, since comment edits might re-update or replace it + // Let the uniqueness handle this fail + PersonMention::create(&conn, &user_mention_form).ok(); + + // Send an email to those local users that have notifications on + if do_send_email { + send_email_to_user( + &mention_user_view, + "Mentioned by", + "Person Mention", + &comment.content, + ) + } + } + } + + // Send notifs to the parent commenter / poster + match comment.parent_id { + Some(parent_id) => { + if let Ok(parent_comment) = Comment::read(&conn, parent_id) { + // Don't send a notif to yourself + if parent_comment.creator_id != person.id { + // Get the parent commenter local_user + if let Ok(parent_user_view) = LocalUserView::read_person(&conn, parent_comment.creator_id) + { + recipient_ids.push(parent_user_view.local_user.id); + + if do_send_email { + send_email_to_user( + &parent_user_view, + "Reply from", + "Comment Reply", + &comment.content, + ) + } + } + } + } + } + // Its a post + None => { + if post.creator_id != person.id { + if let Ok(parent_user_view) = LocalUserView::read_person(&conn, post.creator_id) { + recipient_ids.push(parent_user_view.local_user.id); + + if do_send_email { + send_email_to_user( + &parent_user_view, + "Reply from", + "Post Reply", + &comment.content, + ) + } + } + } + } + }; + recipient_ids + } + + pub fn send_email_to_user( + local_user_view: &LocalUserView, + subject_text: &str, + body_text: &str, + comment_content: &str, + ) { + if local_user_view.person.banned || !local_user_view.local_user.send_notifications_to_email { + return; + } + + if let Some(user_email) = &local_user_view.local_user.email { + let subject = &format!( + "{} - {} {}", + subject_text, + Settings::get().hostname(), + local_user_view.person.name, + ); + let html = &format!( + "

{}


{} - {}

inbox", + body_text, + local_user_view.person.name, + comment_content, + Settings::get().get_protocol_and_hostname() + ); + match send_email(subject, &user_email, &local_user_view.person.name, html) { + Ok(_o) => _o, + Err(e) => error!("{}", e), + }; + } + } + + pub async fn is_mod_or_admin( + pool: &DbPool, + person_id: PersonId, + community_id: CommunityId, + ) -> Result<(), LemmyError> { + let is_mod_or_admin = blocking(pool, move |conn| { + CommunityView::is_mod_or_admin(conn, person_id, community_id) + }) + .await?; + if !is_mod_or_admin { + return Err(ApiError::err("not_a_mod_or_admin").into()); + } + Ok(()) + } + + pub fn is_admin(local_user_view: &LocalUserView) -> Result<(), LemmyError> { - if !local_user_view.local_user.admin { ++ if !local_user_view.person.admin { + return Err(ApiError::err("not_an_admin").into()); + } + Ok(()) + } + + pub async fn get_post(post_id: PostId, pool: &DbPool) -> Result { + match blocking(pool, move |conn| Post::read(conn, post_id)).await? { + Ok(post) => Ok(post), + Err(_e) => Err(ApiError::err("couldnt_find_post").into()), + } + } + + pub async fn get_local_user_view_from_jwt( + jwt: &str, + pool: &DbPool, + ) -> Result { + let claims = match Claims::decode(&jwt) { + Ok(claims) => claims.claims, + Err(_e) => return Err(ApiError::err("not_logged_in").into()), + }; + let local_user_id = LocalUserId(claims.sub); + let local_user_view = + blocking(pool, move |conn| LocalUserView::read(conn, local_user_id)).await??; + // Check for a site ban + if local_user_view.person.banned { + return Err(ApiError::err("site_ban").into()); + } + + check_validator_time(&local_user_view.local_user.validator_time, &claims)?; + + Ok(local_user_view) + } + + /// Checks if user's token was issued before user's password reset. + pub fn check_validator_time( + validator_time: &chrono::NaiveDateTime, + claims: &Claims, + ) -> Result<(), LemmyError> { + let user_validation_time = validator_time.timestamp(); + if user_validation_time > claims.iat { + Err(ApiError::err("not_logged_in").into()) + } else { + Ok(()) + } + } + + pub async fn get_local_user_view_from_jwt_opt( + jwt: &Option, + pool: &DbPool, + ) -> Result, LemmyError> { + match jwt { + Some(jwt) => Ok(Some(get_local_user_view_from_jwt(jwt, pool).await?)), + None => Ok(None), + } + } + + pub async fn get_local_user_settings_view_from_jwt( + jwt: &str, + pool: &DbPool, + ) -> Result { + let claims = match Claims::decode(&jwt) { + Ok(claims) => claims.claims, + Err(_e) => return Err(ApiError::err("not_logged_in").into()), + }; + let local_user_id = LocalUserId(claims.sub); + let local_user_view = blocking(pool, move |conn| { + LocalUserSettingsView::read(conn, local_user_id) + }) + .await??; + // Check for a site ban + if local_user_view.person.banned { + return Err(ApiError::err("site_ban").into()); + } + + check_validator_time(&local_user_view.local_user.validator_time, &claims)?; + + Ok(local_user_view) + } + + pub async fn get_local_user_settings_view_from_jwt_opt( + jwt: &Option, + pool: &DbPool, + ) -> Result, LemmyError> { + match jwt { + Some(jwt) => Ok(Some( + get_local_user_settings_view_from_jwt(jwt, pool).await?, + )), + None => Ok(None), + } + } + + pub async fn check_community_ban( + person_id: PersonId, + community_id: CommunityId, + pool: &DbPool, + ) -> Result<(), LemmyError> { + let is_banned = + move |conn: &'_ _| CommunityPersonBanView::get(conn, person_id, community_id).is_ok(); + if blocking(pool, is_banned).await? { + Err(ApiError::err("community_ban").into()) + } else { + Ok(()) + } + } + + pub async fn check_downvotes_enabled(score: i16, pool: &DbPool) -> Result<(), LemmyError> { + if score == -1 { + let site = blocking(pool, move |conn| Site::read_simple(conn)).await??; + if !site.enable_downvotes { + return Err(ApiError::err("downvotes_disabled").into()); + } + } + Ok(()) + } + + /// Returns a list of communities that the user moderates + /// or if a community_id is supplied validates the user is a moderator + /// of that community and returns the community id in a vec + /// + /// * `person_id` - the person id of the moderator + /// * `community_id` - optional community id to check for moderator privileges + /// * `pool` - the diesel db pool + pub async fn collect_moderated_communities( + person_id: PersonId, + community_id: Option, + pool: &DbPool, + ) -> Result, LemmyError> { + if let Some(community_id) = community_id { + // if the user provides a community_id, just check for mod/admin privileges + is_mod_or_admin(pool, person_id, community_id).await?; + Ok(vec![community_id]) + } else { + let ids = blocking(pool, move |conn: &'_ _| { + CommunityModerator::get_person_moderated_communities(conn, person_id) + }) + .await??; + Ok(ids) + } + } + + pub async fn build_federated_instances( + pool: &DbPool, + ) -> Result, LemmyError> { + if Settings::get().federation().enabled { + let distinct_communities = blocking(pool, move |conn| { + Community::distinct_federated_communities(conn) + }) + .await??; + + let allowed = Settings::get().get_allowed_instances(); + let blocked = Settings::get().get_blocked_instances(); + + let mut linked = distinct_communities + .iter() + .map(|actor_id| Ok(Url::parse(actor_id)?.host_str().unwrap_or("").to_string())) + .collect::, LemmyError>>()?; + + if let Some(allowed) = allowed.as_ref() { + linked.extend_from_slice(allowed); + } + + if let Some(blocked) = blocked.as_ref() { + linked.retain(|a| !blocked.contains(a) && !a.eq(&Settings::get().hostname())); + } + + // Sort and remove dupes + linked.sort_unstable(); + linked.dedup(); + + Ok(Some(FederatedInstances { + linked, + allowed, + blocked, + })) + } else { + Ok(None) + } + } + + /// Checks the password length + pub fn password_length_check(pass: &str) -> Result<(), LemmyError> { + if pass.len() > 60 { + Err(ApiError::err("invalid_password").into()) + } else { + Ok(()) + } + } diff --cc crates/api_crud/src/comment/create.rs index 00000000,57c85013..0cdde729 mode 000000,100644..100644 --- a/crates/api_crud/src/comment/create.rs +++ b/crates/api_crud/src/comment/create.rs @@@ -1,0 -1,170 +1,164 @@@ + use crate::PerformCrud; + use actix_web::web::Data; + use lemmy_api_common::{ + blocking, + check_community_ban, + comment::*, + get_local_user_view_from_jwt, + get_post, + send_local_notifs, + }; + use lemmy_apub::{generate_apub_endpoint, ApubLikeableType, ApubObjectType, EndpointType}; + use lemmy_db_queries::{source::comment::Comment_, Crud, Likeable}; + use lemmy_db_schema::source::comment::*; + use lemmy_db_views::comment_view::CommentView; + use lemmy_utils::{ + utils::{remove_slurs, scrape_text_for_mentions}, + ApiError, + ConnectionId, + LemmyError, + }; + use lemmy_websocket::{messages::SendComment, LemmyContext, UserOperationCrud}; + + #[async_trait::async_trait(?Send)] + impl PerformCrud for CreateComment { + type Response = CommentResponse; + + async fn perform( + &self, + context: &Data, + websocket_id: Option, + ) -> Result { + let data: &CreateComment = &self; + let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; + + let content_slurs_removed = remove_slurs(&data.content.to_owned()); + + // Check for a community ban + let post_id = data.post_id; + let post = get_post(post_id, context.pool()).await?; + + check_community_ban(local_user_view.person.id, post.community_id, context.pool()).await?; + + // Check if post is locked, no new comments + if post.locked { + return Err(ApiError::err("locked").into()); + } + + // If there's a parent_id, check to make sure that comment is in that post + if let Some(parent_id) = data.parent_id { + // Make sure the parent comment exists + let parent = + match blocking(context.pool(), move |conn| Comment::read(&conn, parent_id)).await? { + Ok(comment) => comment, + Err(_e) => return Err(ApiError::err("couldnt_create_comment").into()), + }; + if parent.post_id != post_id { + return Err(ApiError::err("couldnt_create_comment").into()); + } + } + + let comment_form = CommentForm { + content: content_slurs_removed, + parent_id: data.parent_id.to_owned(), + post_id: data.post_id, + creator_id: local_user_view.person.id, - removed: None, - deleted: None, - read: None, - published: None, - updated: None, - ap_id: None, - local: true, ++ ..CommentForm::default() + }; + + // Create the comment + let comment_form2 = comment_form.clone(); + let inserted_comment = match blocking(context.pool(), move |conn| { + Comment::create(&conn, &comment_form2) + }) + .await? + { + Ok(comment) => comment, + Err(_e) => return Err(ApiError::err("couldnt_create_comment").into()), + }; + + // Necessary to update the ap_id + let inserted_comment_id = inserted_comment.id; + let updated_comment: Comment = + match blocking(context.pool(), move |conn| -> Result { + let apub_id = + generate_apub_endpoint(EndpointType::Comment, &inserted_comment_id.to_string())?; + Ok(Comment::update_ap_id(&conn, inserted_comment_id, apub_id)?) + }) + .await? + { + Ok(comment) => comment, + Err(_e) => return Err(ApiError::err("couldnt_create_comment").into()), + }; + + updated_comment + .send_create(&local_user_view.person, context) + .await?; + + // Scan the comment for user mentions, add those rows + let post_id = post.id; + let mentions = scrape_text_for_mentions(&comment_form.content); + let recipient_ids = send_local_notifs( + mentions, + updated_comment.clone(), + local_user_view.person.clone(), + post, + context.pool(), + true, + ) + .await?; + + // You like your own comment by default + let like_form = CommentLikeForm { + comment_id: inserted_comment.id, + post_id, + person_id: local_user_view.person.id, + score: 1, + }; + + let like = move |conn: &'_ _| CommentLike::like(&conn, &like_form); + if blocking(context.pool(), like).await?.is_err() { + return Err(ApiError::err("couldnt_like_comment").into()); + } + + updated_comment + .send_like(&local_user_view.person, context) + .await?; + + let person_id = local_user_view.person.id; + let mut comment_view = blocking(context.pool(), move |conn| { + CommentView::read(&conn, inserted_comment.id, Some(person_id)) + }) + .await??; + + // If its a comment to yourself, mark it as read + let comment_id = comment_view.comment.id; + if local_user_view.person.id == comment_view.get_recipient_id() { + match blocking(context.pool(), move |conn| { + Comment::update_read(conn, comment_id, true) + }) + .await? + { + Ok(comment) => comment, + Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()), + }; + comment_view.comment.read = true; + } + + let mut res = CommentResponse { + comment_view, + recipient_ids, + form_id: data.form_id.to_owned(), + }; + + context.chat_server().do_send(SendComment { + op: UserOperationCrud::CreateComment, + comment: res.clone(), + websocket_id, + }); + + res.recipient_ids = Vec::new(); // Necessary to avoid doubles + + Ok(res) + } + } diff --cc crates/api_crud/src/community/create.rs index 00000000,104975b7..bef376f0 mode 000000,100644..100644 --- a/crates/api_crud/src/community/create.rs +++ b/crates/api_crud/src/community/create.rs @@@ -1,0 -1,134 +1,129 @@@ + use crate::PerformCrud; + use actix_web::web::Data; + use lemmy_api_common::{ + blocking, + community::{CommunityResponse, CreateCommunity}, + get_local_user_view_from_jwt, + }; + use lemmy_apub::{ + generate_apub_endpoint, + generate_followers_url, + generate_inbox_url, + generate_shared_inbox_url, + EndpointType, + }; + use lemmy_db_queries::{diesel_option_overwrite_to_url, ApubObject, Crud, Followable, Joinable}; + use lemmy_db_schema::source::community::{ + Community, + CommunityFollower, + CommunityFollowerForm, + CommunityForm, + CommunityModerator, + CommunityModeratorForm, + }; + use lemmy_db_views_actor::community_view::CommunityView; + use lemmy_utils::{ + apub::generate_actor_keypair, + utils::{check_slurs, check_slurs_opt, is_valid_community_name}, + ApiError, + ConnectionId, + LemmyError, + }; + use lemmy_websocket::LemmyContext; + + #[async_trait::async_trait(?Send)] + impl PerformCrud for CreateCommunity { + type Response = CommunityResponse; + + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data: &CreateCommunity = &self; + let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; + + check_slurs(&data.name)?; + check_slurs(&data.title)?; + check_slurs_opt(&data.description)?; + + if !is_valid_community_name(&data.name) { + return Err(ApiError::err("invalid_community_name").into()); + } + + // Double check for duplicate community actor_ids + let community_actor_id = generate_apub_endpoint(EndpointType::Community, &data.name)?; + let actor_id_cloned = community_actor_id.to_owned(); + let community_dupe = blocking(context.pool(), move |conn| { + Community::read_from_apub_id(conn, &actor_id_cloned) + }) + .await?; + if community_dupe.is_ok() { + return Err(ApiError::err("community_already_exists").into()); + } + + // Check to make sure the icon and banners are urls + let icon = diesel_option_overwrite_to_url(&data.icon)?; + let banner = diesel_option_overwrite_to_url(&data.banner)?; + + // When you create a community, make sure the user becomes a moderator and a follower + let keypair = generate_actor_keypair()?; + + let community_form = CommunityForm { + name: data.name.to_owned(), + title: data.title.to_owned(), + description: data.description.to_owned(), + icon, + banner, + creator_id: local_user_view.person.id, - removed: None, - deleted: None, + nsfw: data.nsfw, - updated: None, + actor_id: Some(community_actor_id.to_owned()), - local: true, + private_key: Some(keypair.private_key), + public_key: Some(keypair.public_key), - last_refreshed_at: None, - published: None, + followers_url: Some(generate_followers_url(&community_actor_id)?), + inbox_url: Some(generate_inbox_url(&community_actor_id)?), + shared_inbox_url: Some(Some(generate_shared_inbox_url(&community_actor_id)?)), ++ ..CommunityForm::default() + }; + + let inserted_community = match blocking(context.pool(), move |conn| { + Community::create(conn, &community_form) + }) + .await? + { + Ok(community) => community, + Err(_e) => return Err(ApiError::err("community_already_exists").into()), + }; + + // The community creator becomes a moderator + let community_moderator_form = CommunityModeratorForm { + community_id: inserted_community.id, + person_id: local_user_view.person.id, + }; + + let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form); + if blocking(context.pool(), join).await?.is_err() { + return Err(ApiError::err("community_moderator_already_exists").into()); + } + + // Follow your own community + let community_follower_form = CommunityFollowerForm { + community_id: inserted_community.id, + person_id: local_user_view.person.id, + pending: false, + }; + + let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form); + if blocking(context.pool(), follow).await?.is_err() { + return Err(ApiError::err("community_follower_already_exists").into()); + } + + let person_id = local_user_view.person.id; + let community_view = blocking(context.pool(), move |conn| { + CommunityView::read(conn, inserted_community.id, Some(person_id)) + }) + .await??; + + Ok(CommunityResponse { community_view }) + } + } diff --cc crates/api_crud/src/community/update.rs index 00000000,1e5b9d0c..0a0540fa mode 000000,100644..100644 --- a/crates/api_crud/src/community/update.rs +++ b/crates/api_crud/src/community/update.rs @@@ -1,0 -1,114 +1,104 @@@ + use crate::{community::send_community_websocket, PerformCrud}; + use actix_web::web::Data; + use lemmy_api_common::{ + blocking, + community::{CommunityResponse, EditCommunity}, + get_local_user_view_from_jwt, + }; + use lemmy_db_queries::{diesel_option_overwrite_to_url, Crud}; + use lemmy_db_schema::{ + naive_now, + source::community::{Community, CommunityForm}, + PersonId, + }; + use lemmy_db_views_actor::{ + community_moderator_view::CommunityModeratorView, + community_view::CommunityView, + }; + use lemmy_utils::{ + utils::{check_slurs, check_slurs_opt}, + ApiError, + ConnectionId, + LemmyError, + }; + use lemmy_websocket::{LemmyContext, UserOperationCrud}; + + #[async_trait::async_trait(?Send)] + impl PerformCrud for EditCommunity { + type Response = CommunityResponse; + + async fn perform( + &self, + context: &Data, + websocket_id: Option, + ) -> Result { + let data: &EditCommunity = &self; + let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; + + check_slurs(&data.title)?; + check_slurs_opt(&data.description)?; + + // Verify its a mod (only mods can edit it) + let community_id = data.community_id; + let mods: Vec = blocking(context.pool(), move |conn| { + CommunityModeratorView::for_community(conn, community_id) + .map(|v| v.into_iter().map(|m| m.moderator.id).collect()) + }) + .await??; + if !mods.contains(&local_user_view.person.id) { + return Err(ApiError::err("not_a_moderator").into()); + } + + let community_id = data.community_id; + let read_community = blocking(context.pool(), move |conn| { + Community::read(conn, community_id) + }) + .await??; + + let icon = diesel_option_overwrite_to_url(&data.icon)?; + let banner = diesel_option_overwrite_to_url(&data.banner)?; + + let community_form = CommunityForm { + name: read_community.name, + title: data.title.to_owned(), ++ creator_id: read_community.creator_id, + description: data.description.to_owned(), + icon, + banner, - creator_id: read_community.creator_id, - removed: Some(read_community.removed), - deleted: Some(read_community.deleted), + nsfw: data.nsfw, + updated: Some(naive_now()), - actor_id: Some(read_community.actor_id), - local: read_community.local, - private_key: read_community.private_key, - public_key: read_community.public_key, - last_refreshed_at: None, - published: None, - followers_url: None, - inbox_url: None, - shared_inbox_url: None, ++ ..CommunityForm::default() + }; + + let community_id = data.community_id; + match blocking(context.pool(), move |conn| { + Community::update(conn, community_id, &community_form) + }) + .await? + { + Ok(community) => community, + Err(_e) => return Err(ApiError::err("couldnt_update_community").into()), + }; + + // TODO there needs to be some kind of an apub update + // process for communities and users + + let community_id = data.community_id; + let person_id = local_user_view.person.id; + let community_view = blocking(context.pool(), move |conn| { + CommunityView::read(conn, community_id, Some(person_id)) + }) + .await??; + + let res = CommunityResponse { community_view }; + + send_community_websocket( + &res, + context, + websocket_id, + UserOperationCrud::EditCommunity, + ); + + Ok(res) + } + } diff --cc crates/api_crud/src/post/create.rs index 00000000,f8bce061..2fe86414 mode 000000,100644..100644 --- a/crates/api_crud/src/post/create.rs +++ b/crates/api_crud/src/post/create.rs @@@ -1,0 -1,130 +1,123 @@@ + use crate::PerformCrud; + use actix_web::web::Data; + use lemmy_api_common::{blocking, check_community_ban, get_local_user_view_from_jwt, post::*}; + use lemmy_apub::{generate_apub_endpoint, ApubLikeableType, ApubObjectType, EndpointType}; + use lemmy_db_queries::{source::post::Post_, Crud, Likeable}; + use lemmy_db_schema::source::post::*; + use lemmy_db_views::post_view::PostView; + use lemmy_utils::{ + request::fetch_iframely_and_pictrs_data, + utils::{check_slurs, check_slurs_opt, is_valid_post_title}, + ApiError, + ConnectionId, + LemmyError, + }; + use lemmy_websocket::{messages::SendPost, LemmyContext, UserOperationCrud}; + + #[async_trait::async_trait(?Send)] + impl PerformCrud for CreatePost { + type Response = PostResponse; + + async fn perform( + &self, + context: &Data, + websocket_id: Option, + ) -> Result { + let data: &CreatePost = &self; + let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; + + check_slurs(&data.name)?; + check_slurs_opt(&data.body)?; + + if !is_valid_post_title(&data.name) { + return Err(ApiError::err("invalid_post_title").into()); + } + + check_community_ban(local_user_view.person.id, data.community_id, context.pool()).await?; + + // Fetch Iframely and pictrs cached image + let data_url = data.url.as_ref(); + let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) = + fetch_iframely_and_pictrs_data(context.client(), data_url).await; + + let post_form = PostForm { + name: data.name.trim().to_owned(), + url: data_url.map(|u| u.to_owned().into()), + body: data.body.to_owned(), + community_id: data.community_id, + creator_id: local_user_view.person.id, - removed: None, - deleted: None, + nsfw: data.nsfw, - locked: None, - stickied: None, - updated: None, + embed_title: iframely_title, + embed_description: iframely_description, + embed_html: iframely_html, + thumbnail_url: pictrs_thumbnail.map(|u| u.into()), - ap_id: None, - local: true, - published: None, ++ ..PostForm::default() + }; + + let inserted_post = + match blocking(context.pool(), move |conn| Post::create(conn, &post_form)).await? { + Ok(post) => post, + Err(e) => { + let err_type = if e.to_string() == "value too long for type character varying(200)" { + "post_title_too_long" + } else { + "couldnt_create_post" + }; + + return Err(ApiError::err(err_type).into()); + } + }; + + let inserted_post_id = inserted_post.id; + let updated_post = match blocking(context.pool(), move |conn| -> Result { + let apub_id = generate_apub_endpoint(EndpointType::Post, &inserted_post_id.to_string())?; + Ok(Post::update_ap_id(conn, inserted_post_id, apub_id)?) + }) + .await? + { + Ok(post) => post, + Err(_e) => return Err(ApiError::err("couldnt_create_post").into()), + }; + + updated_post + .send_create(&local_user_view.person, context) + .await?; + + // They like their own post by default + let like_form = PostLikeForm { + post_id: inserted_post.id, + person_id: local_user_view.person.id, + score: 1, + }; + + let like = move |conn: &'_ _| PostLike::like(conn, &like_form); + if blocking(context.pool(), like).await?.is_err() { + return Err(ApiError::err("couldnt_like_post").into()); + } + + updated_post + .send_like(&local_user_view.person, context) + .await?; + + // Refetch the view + let inserted_post_id = inserted_post.id; + let post_view = match blocking(context.pool(), move |conn| { + PostView::read(conn, inserted_post_id, Some(local_user_view.person.id)) + }) + .await? + { + Ok(post) => post, + Err(_e) => return Err(ApiError::err("couldnt_find_post").into()), + }; + + let res = PostResponse { post_view }; + + context.chat_server().do_send(SendPost { + op: UserOperationCrud::CreatePost, + post: res.clone(), + websocket_id, + }); + + Ok(res) + } + } diff --cc crates/api_crud/src/post/update.rs index 00000000,e1efc790..8ca0dcd1 mode 000000,100644..100644 --- a/crates/api_crud/src/post/update.rs +++ b/crates/api_crud/src/post/update.rs @@@ -1,0 -1,116 +1,110 @@@ + use crate::PerformCrud; + use actix_web::web::Data; + use lemmy_api_common::{blocking, check_community_ban, get_local_user_view_from_jwt, post::*}; + use lemmy_apub::ApubObjectType; + use lemmy_db_queries::{source::post::Post_, Crud}; + use lemmy_db_schema::{naive_now, source::post::*}; + use lemmy_db_views::post_view::PostView; + use lemmy_utils::{ + request::fetch_iframely_and_pictrs_data, + utils::{check_slurs, check_slurs_opt, is_valid_post_title}, + ApiError, + ConnectionId, + LemmyError, + }; + use lemmy_websocket::{messages::SendPost, LemmyContext, UserOperationCrud}; + + #[async_trait::async_trait(?Send)] + impl PerformCrud for EditPost { + type Response = PostResponse; + + async fn perform( + &self, + context: &Data, + websocket_id: Option, + ) -> Result { + let data: &EditPost = &self; + let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; + + check_slurs(&data.name)?; + check_slurs_opt(&data.body)?; + + if !is_valid_post_title(&data.name) { + return Err(ApiError::err("invalid_post_title").into()); + } + + let post_id = data.post_id; + let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??; + + check_community_ban( + local_user_view.person.id, + orig_post.community_id, + context.pool(), + ) + .await?; + + // Verify that only the creator can edit + if !Post::is_post_creator(local_user_view.person.id, orig_post.creator_id) { + return Err(ApiError::err("no_post_edit_allowed").into()); + } + + // Fetch Iframely and Pictrs cached image + let data_url = data.url.as_ref(); + let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) = + fetch_iframely_and_pictrs_data(context.client(), data_url).await; + + let post_form = PostForm { ++ creator_id: orig_post.creator_id.to_owned(), ++ community_id: orig_post.community_id, + name: data.name.trim().to_owned(), + url: data_url.map(|u| u.to_owned().into()), + body: data.body.to_owned(), + nsfw: data.nsfw, - creator_id: orig_post.creator_id.to_owned(), - community_id: orig_post.community_id, - removed: Some(orig_post.removed), - deleted: Some(orig_post.deleted), - locked: Some(orig_post.locked), - stickied: Some(orig_post.stickied), + updated: Some(naive_now()), + embed_title: iframely_title, + embed_description: iframely_description, + embed_html: iframely_html, + thumbnail_url: pictrs_thumbnail.map(|u| u.into()), - ap_id: Some(orig_post.ap_id), - local: orig_post.local, - published: None, ++ ..PostForm::default() + }; + + let post_id = data.post_id; + let res = blocking(context.pool(), move |conn| { + Post::update(conn, post_id, &post_form) + }) + .await?; + let updated_post: Post = match res { + Ok(post) => post, + Err(e) => { + let err_type = if e.to_string() == "value too long for type character varying(200)" { + "post_title_too_long" + } else { + "couldnt_update_post" + }; + + return Err(ApiError::err(err_type).into()); + } + }; + + // Send apub update + updated_post + .send_update(&local_user_view.person, context) + .await?; + + let post_id = data.post_id; + let post_view = blocking(context.pool(), move |conn| { + PostView::read(conn, post_id, Some(local_user_view.person.id)) + }) + .await??; + + let res = PostResponse { post_view }; + + context.chat_server().do_send(SendPost { + op: UserOperationCrud::EditPost, + post: res.clone(), + websocket_id, + }); + + Ok(res) + } + } diff --cc crates/api_crud/src/private_message/create.rs index 00000000,02561263..53c0b950 mode 000000,100644..100644 --- a/crates/api_crud/src/private_message/create.rs +++ b/crates/api_crud/src/private_message/create.rs @@@ -1,0 -1,112 +1,107 @@@ + use crate::PerformCrud; + use actix_web::web::Data; + use lemmy_api_common::{ + blocking, + get_local_user_view_from_jwt, + person::{CreatePrivateMessage, PrivateMessageResponse}, + send_email_to_user, + }; + use lemmy_apub::{generate_apub_endpoint, ApubObjectType, EndpointType}; + use lemmy_db_queries::{source::private_message::PrivateMessage_, Crud}; + use lemmy_db_schema::source::private_message::{PrivateMessage, PrivateMessageForm}; + use lemmy_db_views::{local_user_view::LocalUserView, private_message_view::PrivateMessageView}; + use lemmy_utils::{utils::remove_slurs, ApiError, ConnectionId, LemmyError}; + use lemmy_websocket::{messages::SendUserRoomMessage, LemmyContext, UserOperationCrud}; + + #[async_trait::async_trait(?Send)] + impl PerformCrud for CreatePrivateMessage { + type Response = PrivateMessageResponse; + + async fn perform( + &self, + context: &Data, + websocket_id: Option, + ) -> Result { + let data: &CreatePrivateMessage = &self; + let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?; + + let content_slurs_removed = remove_slurs(&data.content.to_owned()); + + let private_message_form = PrivateMessageForm { + content: content_slurs_removed.to_owned(), + creator_id: local_user_view.person.id, + recipient_id: data.recipient_id, - deleted: None, - read: None, - updated: None, - ap_id: None, - local: true, - published: None, ++ ..PrivateMessageForm::default() + }; + + let inserted_private_message = match blocking(context.pool(), move |conn| { + PrivateMessage::create(conn, &private_message_form) + }) + .await? + { + Ok(private_message) => private_message, + Err(_e) => { + return Err(ApiError::err("couldnt_create_private_message").into()); + } + }; + + let inserted_private_message_id = inserted_private_message.id; + let updated_private_message = match blocking( + context.pool(), + move |conn| -> Result { + let apub_id = generate_apub_endpoint( + EndpointType::PrivateMessage, + &inserted_private_message_id.to_string(), + )?; + Ok(PrivateMessage::update_ap_id( + &conn, + inserted_private_message_id, + apub_id, + )?) + }, + ) + .await? + { + Ok(private_message) => private_message, + Err(_e) => return Err(ApiError::err("couldnt_create_private_message").into()), + }; + + updated_private_message + .send_create(&local_user_view.person, context) + .await?; + + let private_message_view = blocking(context.pool(), move |conn| { + PrivateMessageView::read(conn, inserted_private_message.id) + }) + .await??; + + let res = PrivateMessageResponse { + private_message_view, + }; + + // Send notifications to the local recipient, if one exists + let recipient_id = data.recipient_id; + if let Ok(local_recipient) = blocking(context.pool(), move |conn| { + LocalUserView::read_person(conn, recipient_id) + }) + .await? + { + send_email_to_user( + &local_recipient, + "Private Message from", + "Private Message", + &content_slurs_removed, + ); + + let local_recipient_id = local_recipient.local_user.id; + context.chat_server().do_send(SendUserRoomMessage { + op: UserOperationCrud::CreatePrivateMessage, + response: res.clone(), + local_recipient_id, + websocket_id, + }); + } + + Ok(res) + } + } diff --cc crates/api_crud/src/user/create.rs index 00000000,81c7f5d2..63a6474d mode 000000,100644..100644 --- a/crates/api_crud/src/user/create.rs +++ b/crates/api_crud/src/user/create.rs @@@ -1,0 -1,244 +1,226 @@@ + use crate::PerformCrud; + use actix_web::web::Data; + use lemmy_api_common::{blocking, password_length_check, person::*}; + use lemmy_apub::{ + generate_apub_endpoint, + generate_followers_url, + generate_inbox_url, + generate_shared_inbox_url, + EndpointType, + }; + use lemmy_db_queries::{ + source::{local_user::LocalUser_, site::Site_}, + Crud, + Followable, + Joinable, + ListingType, + SortType, + }; + use lemmy_db_schema::{ + source::{ + community::*, + local_user::{LocalUser, LocalUserForm}, + person::*, + site::*, + }, + CommunityId, + }; + use lemmy_db_views_actor::person_view::PersonViewSafe; + use lemmy_utils::{ + apub::generate_actor_keypair, + claims::Claims, + settings::structs::Settings, + utils::{check_slurs, is_valid_username}, + ApiError, + ConnectionId, + LemmyError, + }; + use lemmy_websocket::{messages::CheckCaptcha, LemmyContext}; + + #[async_trait::async_trait(?Send)] + impl PerformCrud for Register { + type Response = LoginResponse; + + async fn perform( + &self, + context: &Data, + _websocket_id: Option, + ) -> Result { + let data: &Register = &self; + + // Make sure site has open registration + if let Ok(site) = blocking(context.pool(), move |conn| Site::read_simple(conn)).await? { + if !site.open_registration { + return Err(ApiError::err("registration_closed").into()); + } + } + + password_length_check(&data.password)?; + + // Make sure passwords match + if data.password != data.password_verify { + return Err(ApiError::err("passwords_dont_match").into()); + } + + // Check if there are admins. False if admins exist + let no_admins = blocking(context.pool(), move |conn| { + PersonViewSafe::admins(conn).map(|a| a.is_empty()) + }) + .await??; + + // If its not the admin, check the captcha + if !no_admins && Settings::get().captcha().enabled { + let check = context + .chat_server() + .send(CheckCaptcha { + uuid: data + .captcha_uuid + .to_owned() + .unwrap_or_else(|| "".to_string()), + answer: data + .captcha_answer + .to_owned() + .unwrap_or_else(|| "".to_string()), + }) + .await?; + if !check { + return Err(ApiError::err("captcha_incorrect").into()); + } + } + + check_slurs(&data.username)?; + + let actor_keypair = generate_actor_keypair()?; + if !is_valid_username(&data.username) { + return Err(ApiError::err("invalid_username").into()); + } + let actor_id = generate_apub_endpoint(EndpointType::Person, &data.username)?; + + // We have to create both a person, and local_user + + // Register the new person + let person_form = PersonForm { + name: data.username.to_owned(), - avatar: None, - banner: None, - preferred_username: None, - published: None, - updated: None, - banned: None, - deleted: None, + actor_id: Some(actor_id.clone()), - bio: None, - local: Some(true), + private_key: Some(Some(actor_keypair.private_key)), + public_key: Some(Some(actor_keypair.public_key)), - last_refreshed_at: None, + inbox_url: Some(generate_inbox_url(&actor_id)?), + shared_inbox_url: Some(Some(generate_shared_inbox_url(&actor_id)?)), ++ admin: Some(no_admins), ++ ..PersonForm::default() + }; + + // insert the person + let inserted_person = match blocking(context.pool(), move |conn| { + Person::create(conn, &person_form) + }) + .await? + { + Ok(u) => u, + Err(_) => { + return Err(ApiError::err("user_already_exists").into()); + } + }; + + // Create the local user + let local_user_form = LocalUserForm { + person_id: inserted_person.id, + email: Some(data.email.to_owned()), - matrix_user_id: None, + password_encrypted: data.password.to_owned(), - admin: Some(no_admins), + show_nsfw: Some(data.show_nsfw), + theme: Some("browser".into()), + default_sort_type: Some(SortType::Active as i16), + default_listing_type: Some(ListingType::Subscribed as i16), + lang: Some("browser".into()), + show_avatars: Some(true), + send_notifications_to_email: Some(false), + }; + + let inserted_local_user = match blocking(context.pool(), move |conn| { + LocalUser::register(conn, &local_user_form) + }) + .await? + { + Ok(lu) => lu, + Err(e) => { + let err_type = if e.to_string() + == "duplicate key value violates unique constraint \"local_user_email_key\"" + { + "email_already_exists" + } else { + "user_already_exists" + }; + + // If the local user creation errored, then delete that person + blocking(context.pool(), move |conn| { + Person::delete(&conn, inserted_person.id) + }) + .await??; + + return Err(ApiError::err(err_type).into()); + } + }; + + let main_community_keypair = generate_actor_keypair()?; + + // Create the main community if it doesn't exist + let main_community = match blocking(context.pool(), move |conn| { + Community::read(conn, CommunityId(2)) + }) + .await? + { + Ok(c) => c, + Err(_e) => { + let default_community_name = "main"; + let actor_id = generate_apub_endpoint(EndpointType::Community, default_community_name)?; + let community_form = CommunityForm { + name: default_community_name.to_string(), + title: "The Default Community".to_string(), + description: Some("The Default Community".to_string()), - nsfw: false, + creator_id: inserted_person.id, - removed: None, - deleted: None, - updated: None, + actor_id: Some(actor_id.to_owned()), - local: true, + private_key: Some(main_community_keypair.private_key), + public_key: Some(main_community_keypair.public_key), - last_refreshed_at: None, - published: None, - icon: None, - banner: None, + followers_url: Some(generate_followers_url(&actor_id)?), + inbox_url: Some(generate_inbox_url(&actor_id)?), + shared_inbox_url: Some(Some(generate_shared_inbox_url(&actor_id)?)), ++ ..CommunityForm::default() + }; + blocking(context.pool(), move |conn| { + Community::create(conn, &community_form) + }) + .await?? + } + }; + + // Sign them up for main community no matter what + let community_follower_form = CommunityFollowerForm { + community_id: main_community.id, + person_id: inserted_person.id, + pending: false, + }; + + let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form); + if blocking(context.pool(), follow).await?.is_err() { + return Err(ApiError::err("community_follower_already_exists").into()); + }; + + // If its an admin, add them as a mod and follower to main + if no_admins { + let community_moderator_form = CommunityModeratorForm { + community_id: main_community.id, + person_id: inserted_person.id, + }; + + let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form); + if blocking(context.pool(), join).await?.is_err() { + return Err(ApiError::err("community_moderator_already_exists").into()); + } + } + + // Return the jwt + Ok(LoginResponse { + jwt: Claims::jwt(inserted_local_user.id.0)?, + }) + } + } diff --cc crates/apub/src/objects/person.rs index 662871dc,c0dee8c1..25c785d8 --- a/crates/apub/src/objects/person.rs +++ b/crates/apub/src/objects/person.rs @@@ -16,9 -16,9 +16,9 @@@ use activitystreams:: object::{ApObject, Image, Tombstone}, prelude::*, }; -use activitystreams_ext::Ext1; +use activitystreams_ext::Ext2; use anyhow::Context; - use lemmy_api_structs::blocking; + use lemmy_api_common::blocking; use lemmy_db_queries::{ApubObject, DbPool}; use lemmy_db_schema::{ naive_now, diff --cc src/api_routes.rs index 5c29073e,34501519..692daedf --- a/src/api_routes.rs +++ b/src/api_routes.rs @@@ -7,9 -8,7 +8,7 @@@ use serde::Deserialize pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) { cfg.service( - web::scope("/api/v2") + web::scope("/api/v3") - // Websockets - .service(web::resource("/ws").to(chat_route)) // Site .service( web::scope("/site")