--- /dev/null
- if !local_user_view.local_user.admin {
+ 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<String>,
+ #[serde(rename(serialize = "type", deserialize = "type"))]
+ pub type_: Option<String>,
+ pub href: Option<Url>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub template: Option<String>,
+ }
+
+ #[derive(Serialize, Deserialize, Debug)]
+ pub struct WebFingerResponse {
+ pub subject: String,
+ pub aliases: Vec<Url>,
+ pub links: Vec<WebFingerLink>,
+ }
+
+ pub async fn blocking<F, T>(pool: &DbPool, f: F) -> Result<T, LemmyError>
+ 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<MentionData>,
+ comment: Comment,
+ person: Person,
+ post: Post,
+ pool: &DbPool,
+ do_send_email: bool,
+ ) -> Result<Vec<LocalUserId>, 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<LocalUserId> {
+ 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::<Vec<&MentionData>>()
+ {
+ 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!(
+ "<h1>{}</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
+ 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.person.admin {
+ return Err(ApiError::err("not_an_admin").into());
+ }
+ Ok(())
+ }
+
+ pub async fn get_post(post_id: PostId, pool: &DbPool) -> Result<Post, LemmyError> {
+ 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<LocalUserView, LemmyError> {
+ 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<String>,
+ pool: &DbPool,
+ ) -> Result<Option<LocalUserView>, 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<LocalUserSettingsView, LemmyError> {
+ 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<String>,
+ pool: &DbPool,
+ ) -> Result<Option<LocalUserSettingsView>, 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<CommunityId>,
+ pool: &DbPool,
+ ) -> Result<Vec<CommunityId>, 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<Option<FederatedInstances>, 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::<Result<Vec<String>, 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(())
+ }
+ }
--- /dev/null
- removed: None,
- deleted: None,
- read: None,
- published: None,
- updated: None,
- ap_id: None,
- local: true,
+ 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<LemmyContext>,
+ websocket_id: Option<ConnectionId>,
+ ) -> Result<CommentResponse, LemmyError> {
+ 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,
++ ..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<Comment, LemmyError> {
+ 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)
+ }
+ }
--- /dev/null
- removed: None,
- deleted: None,
+ 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<LemmyContext>,
+ _websocket_id: Option<ConnectionId>,
+ ) -> Result<CommunityResponse, LemmyError> {
+ 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,
- updated: None,
+ nsfw: data.nsfw,
- local: true,
+ actor_id: Some(community_actor_id.to_owned()),
- last_refreshed_at: None,
- published: None,
+ private_key: Some(keypair.private_key),
+ public_key: Some(keypair.public_key),
+ 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 })
+ }
+ }
--- /dev/null
- creator_id: read_community.creator_id,
- removed: Some(read_community.removed),
- deleted: Some(read_community.deleted),
+ 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<LemmyContext>,
+ websocket_id: Option<ConnectionId>,
+ ) -> Result<CommunityResponse, LemmyError> {
+ 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<PersonId> = 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,
- 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,
+ nsfw: data.nsfw,
+ updated: Some(naive_now()),
++ ..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)
+ }
+ }
--- /dev/null
- removed: None,
- deleted: None,
+ 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<LemmyContext>,
+ websocket_id: Option<ConnectionId>,
+ ) -> Result<PostResponse, LemmyError> {
+ 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,
- locked: None,
- stickied: None,
- updated: None,
+ nsfw: data.nsfw,
- ap_id: None,
- local: true,
- published: None,
+ embed_title: iframely_title,
+ embed_description: iframely_description,
+ embed_html: iframely_html,
+ thumbnail_url: pictrs_thumbnail.map(|u| u.into()),
++ ..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<Post, LemmyError> {
+ 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)
+ }
+ }
--- /dev/null
- 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),
+ 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<LemmyContext>,
+ websocket_id: Option<ConnectionId>,
+ ) -> Result<PostResponse, LemmyError> {
+ 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,
- ap_id: Some(orig_post.ap_id),
- local: orig_post.local,
- published: None,
+ updated: Some(naive_now()),
+ embed_title: iframely_title,
+ embed_description: iframely_description,
+ embed_html: iframely_html,
+ thumbnail_url: pictrs_thumbnail.map(|u| u.into()),
++ ..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)
+ }
+ }
--- /dev/null
- deleted: None,
- read: None,
- updated: None,
- ap_id: None,
- local: true,
- published: None,
+ 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<LemmyContext>,
+ websocket_id: Option<ConnectionId>,
+ ) -> Result<PrivateMessageResponse, LemmyError> {
+ 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,
++ ..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<PrivateMessage, LemmyError> {
+ 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)
+ }
+ }
--- /dev/null
- avatar: None,
- banner: None,
- preferred_username: None,
- published: None,
- updated: None,
- banned: None,
- deleted: None,
+ 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<LemmyContext>,
+ _websocket_id: Option<ConnectionId>,
+ ) -> Result<LoginResponse, LemmyError> {
+ 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(),
- bio: None,
- local: Some(true),
+ actor_id: Some(actor_id.clone()),
- last_refreshed_at: None,
+ private_key: Some(Some(actor_keypair.private_key)),
+ public_key: Some(Some(actor_keypair.public_key)),
- matrix_user_id: 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()),
- admin: Some(no_admins),
+ password_encrypted: data.password.to_owned(),
- nsfw: false,
+ 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()),
- removed: None,
- deleted: None,
- updated: None,
+ creator_id: inserted_person.id,
- local: true,
+ actor_id: Some(actor_id.to_owned()),
- last_refreshed_at: None,
- published: None,
- icon: None,
- banner: None,
+ private_key: Some(main_community_keypair.private_key),
+ public_key: Some(main_community_keypair.public_key),
+ 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)?,
+ })
+ }
+ }
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,
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")