sudo docker-compose pull
sudo docker-compose up -d
```
+
+## Security Model
+
+- HTTP signature verify: This ensures that activity really comes from the activity that it claims
+- check_is_apub_valid : Makes sure its in our allowed instances list
+- Lower level checks: To make sure that the user that creates/updates/removes a post is actually on the same instance as that post
+
+For the last point, note that we are *not* checking whether the actor that sends the create activity for a post is
+actually identical to the post's creator, or that the user that removes a post is a mod/admin. These things are checked
+by the API code, and its the responsibility of each instance to check user permissions. This does not leave any attack
+vector, as a normal instance user cant do actions that violate the API rules. The only one who could do that is the
+admin (and the software deployed by the admin). But the admin can do anything on the instance, including send activities
+from other user accounts. So we wouldnt actually gain any security by checking mod permissions or similar.
\ No newline at end of file
) -> Result<Self::Response, LemmyError>;
}
-pub async fn is_mod_or_admin(
+pub(in crate::api) async fn is_mod_or_admin(
pool: &DbPool,
user_id: i32,
community_id: i32,
})
.await?;
if !is_mod_or_admin {
- return Err(APIError::err("not_an_admin").into());
+ return Err(APIError::err("not_a_mod_or_admin").into());
}
Ok(())
}
}
}
-pub(in crate::api) fn check_slurs(text: &str) -> Result<(), APIError> {
+pub(in crate) fn check_slurs(text: &str) -> Result<(), APIError> {
if let Err(slurs) = slur_check(text) {
Err(APIError::err(&slurs_vec_to_str(slurs)))
} else {
Ok(())
}
}
-pub(in crate::api) fn check_slurs_opt(text: &Option<String>) -> Result<(), APIError> {
+pub(in crate) fn check_slurs_opt(text: &Option<String>) -> Result<(), APIError> {
match text {
Some(t) => check_slurs(t),
None => Ok(()),
use crate::{
apub::{
activities::{generate_activity_id, send_activity_to_community},
+ check_actor_domain,
create_apub_response,
create_apub_tombstone_response,
create_tombstone,
user::User_,
Crud,
};
-use lemmy_utils::{convert_datetime, scrape_text_for_mentions, MentionData};
+use lemmy_utils::{convert_datetime, remove_slurs, scrape_text_for_mentions, MentionData};
use log::debug;
use serde::Deserialize;
use serde_json::Error;
note: &Note,
client: &Client,
pool: &DbPool,
+ expected_domain: Option<Url>,
) -> Result<CommentForm, LemmyError> {
let creator_actor_id = ¬e
.attributed_to()
}
None => None,
};
+ let content = note
+ .content()
+ .unwrap()
+ .as_single_xsd_string()
+ .unwrap()
+ .to_string();
+ let content_slurs_removed = remove_slurs(&content);
Ok(CommentForm {
creator_id: creator.id,
post_id: post.id,
parent_id,
- content: note
- .content()
- .unwrap()
- .as_single_xsd_string()
- .unwrap()
- .to_string(),
+ content: content_slurs_removed,
removed: None,
read: None,
published: note.published().map(|u| u.to_owned().naive_local()),
updated: note.updated().map(|u| u.to_owned().naive_local()),
deleted: None,
- ap_id: note.id_unchecked().unwrap().to_string(),
+ ap_id: check_actor_domain(note, expected_domain)?,
local: false,
})
}
use crate::{
+ api::{check_slurs, check_slurs_opt},
apub::{
activities::{generate_activity_id, send_activity},
+ check_actor_domain,
create_apub_response,
create_apub_tombstone_response,
create_tombstone,
type ApubType = GroupExt;
/// Parse an ActivityPub group received from another instance into a Lemmy community.
- async fn from_apub(group: &GroupExt, client: &Client, pool: &DbPool) -> Result<Self, LemmyError> {
+ async fn from_apub(
+ group: &GroupExt,
+ client: &Client,
+ pool: &DbPool,
+ expected_domain: Option<Url>,
+ ) -> Result<Self, LemmyError> {
let creator_and_moderator_uris = group.inner.attributed_to().unwrap();
let creator_uri = creator_and_moderator_uris
.as_many()
.unwrap();
let creator = get_or_fetch_and_upsert_user(creator_uri, client, pool).await?;
+ let name = group
+ .inner
+ .name()
+ .unwrap()
+ .as_one()
+ .unwrap()
+ .as_xsd_string()
+ .unwrap()
+ .to_string();
+ let title = group.inner.preferred_username().unwrap().to_string();
+ // TODO: should be parsed as html and tags like <script> removed (or use markdown source)
+ // -> same for post.content etc
+ let description = group
+ .inner
+ .content()
+ .map(|s| s.as_single_xsd_string().unwrap().into());
+ check_slurs(&name)?;
+ check_slurs(&title)?;
+ check_slurs_opt(&description)?;
let icon = match group.icon() {
Some(any_image) => Some(
};
Ok(CommunityForm {
- name: group
- .inner
- .name()
- .unwrap()
- .as_one()
- .unwrap()
- .as_xsd_string()
- .unwrap()
- .into(),
- title: group.inner.preferred_username().unwrap().to_string(),
- // TODO: should be parsed as html and tags like <script> removed (or use markdown source)
- // -> same for post.content etc
- description: group
- .inner
- .content()
- .map(|s| s.as_single_xsd_string().unwrap().into()),
+ name,
+ title,
+ description,
category_id: group.ext_one.category.identifier.parse::<i32>()?,
creator_id: creator.id,
removed: None,
updated: group.inner.updated().map(|u| u.to_owned().naive_local()),
deleted: None,
nsfw: group.ext_one.sensitive,
- actor_id: group.inner.id_unchecked().unwrap().to_string(),
+ actor_id: check_actor_domain(group, expected_domain)?,
local: false,
private_key: None,
public_key: Some(group.ext_two.to_owned().public_key.public_key_pem),
insert_activity(community.creator_id, announce.clone(), true, pool).await?;
- // dont send to the instance where the activity originally came from, because that would result
- // in a database error (same data inserted twice)
let mut to = community.get_follower_inboxes(pool).await?;
+ // dont send to the local instance, nor to the instance where the activity originally came from,
+ // because that would result in a database error (same data inserted twice)
// this seems to be the "easiest" stable alternative for remove_item()
to.retain(|x| *x != sender.get_shared_inbox_url());
+ to.retain(|x| *x != community.get_shared_inbox_url());
send_activity(client, &announce.into_any_base()?, community, to).await?;
response
}
SearchAcceptedObjects::Page(p) => {
- let post_form = PostForm::from_apub(&p, client, pool).await?;
+ let post_form = PostForm::from_apub(&p, client, pool, Some(query_url)).await?;
let p = blocking(pool, move |conn| upsert_post(&post_form, conn)).await??;
response.posts = vec![blocking(pool, move |conn| PostView::read(conn, p.id, None)).await??];
// TODO: also fetch parent comments if any
let x = post_url.first().unwrap().as_xsd_any_uri().unwrap();
let post = fetch_remote_object(client, x).await?;
- let post_form = PostForm::from_apub(&post, client, pool).await?;
- let comment_form = CommentForm::from_apub(&c, client, pool).await?;
+ let post_form = PostForm::from_apub(&post, client, pool, Some(query_url.clone())).await?;
+ let comment_form = CommentForm::from_apub(&c, client, pool, Some(query_url)).await?;
blocking(pool, move |conn| upsert_post(&post_form, conn)).await??;
let c = blocking(pool, move |conn| upsert_comment(&comment_form, conn)).await??;
) -> Result<User_, LemmyError> {
let apub_id_owned = apub_id.to_owned();
let user = blocking(pool, move |conn| {
- User_::read_from_actor_id(conn, apub_id_owned.as_str())
+ User_::read_from_actor_id(conn, apub_id_owned.as_ref())
})
.await?;
debug!("Fetching and updating from remote user: {}", apub_id);
let person = fetch_remote_object::<PersonExt>(client, apub_id).await?;
- let mut uf = UserForm::from_apub(&person, client, pool).await?;
+ let mut uf = UserForm::from_apub(&person, client, pool, Some(apub_id.to_owned())).await?;
uf.last_refreshed_at = Some(naive_now());
let user = blocking(pool, move |conn| User_::update(conn, u.id, &uf)).await??;
debug!("Fetching and creating remote user: {}", apub_id);
let person = fetch_remote_object::<PersonExt>(client, apub_id).await?;
- let uf = UserForm::from_apub(&person, client, pool).await?;
+ let uf = UserForm::from_apub(&person, client, pool, Some(apub_id.to_owned())).await?;
let user = blocking(pool, move |conn| User_::create(conn, &uf)).await??;
Ok(user)
) -> Result<Community, LemmyError> {
let group = fetch_remote_object::<GroupExt>(client, apub_id).await?;
- let cf = CommunityForm::from_apub(&group, client, pool).await?;
+ let cf = CommunityForm::from_apub(&group, client, pool, Some(apub_id.to_owned())).await?;
let community = blocking(pool, move |conn| {
if let Some(cid) = community_id {
Community::update(conn, cid, &cf)
let outbox_items = outbox.items().unwrap().clone();
for o in outbox_items.many().unwrap() {
let page = PageExt::from_any_base(o)?.unwrap();
- let post = PostForm::from_apub(&page, client, pool).await?;
+ let post = PostForm::from_apub(&page, client, pool, None).await?;
let post_ap_id = post.ap_id.clone();
// Check whether the post already exists in the local db
let existing = blocking(pool, move |conn| Post::read_from_apub_id(conn, &post_ap_id)).await?;
Ok(e) => blocking(pool, move |conn| Post::update(conn, e.id, &post)).await??,
Err(_) => blocking(pool, move |conn| Post::create(conn, &post)).await??,
};
+ // TODO: we need to send a websocket update here
}
Ok(community)
Err(NotFound {}) => {
debug!("Fetching and creating remote post: {}", post_ap_id);
let post = fetch_remote_object::<PageExt>(client, post_ap_id).await?;
- let post_form = PostForm::from_apub(&post, client, pool).await?;
+ let post_form = PostForm::from_apub(&post, client, pool, Some(post_ap_id.to_owned())).await?;
let post = blocking(pool, move |conn| Post::create(conn, &post_form)).await??;
comment_ap_id
);
let comment = fetch_remote_object::<Note>(client, comment_ap_id).await?;
- let comment_form = CommentForm::from_apub(&comment, client, pool).await?;
+ let comment_form =
+ CommentForm::from_apub(&comment, client, pool, Some(comment_ap_id.to_owned())).await?;
let comment = blocking(pool, move |conn| Comment::create(conn, &comment_form)).await??;
undo::receive_undo,
update::receive_update,
},
- shared_inbox::receive_unhandled_activity,
+ shared_inbox::{get_community_id_from_activity, receive_unhandled_activity},
},
routes::ChatServerParam,
DbPool,
LemmyError,
};
-use activitystreams::{activity::*, base::AnyBase, prelude::ExtendsExt};
+use activitystreams::{
+ activity::*,
+ base::{AnyBase, BaseExt},
+ prelude::ExtendsExt,
+};
use actix_web::{client::Client, HttpResponse};
pub async fn receive_announce(
chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> {
let announce = Announce::from_any_base(activity)?.unwrap();
+
+ // ensure that announce and community come from the same instance
+ let community = get_community_id_from_activity(&announce)?;
+ announce.id(community.domain().unwrap())?;
+
let kind = announce.object().as_single_kind_str();
let object = announce.object();
let object2 = object.clone().one().unwrap();
get_user_from_activity,
receive_unhandled_activity,
},
+ ActorType,
FromApub,
PageExt,
},
chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> {
let create = Create::from_any_base(activity)?.unwrap();
+
+ // ensure that create and actor come from the same instance
+ let user = get_user_from_activity(&create, client, pool).await?;
+ create.id(user.actor_id()?.domain().unwrap())?;
+
match create.object().as_single_kind_str() {
Some("Page") => receive_create_post(create, client, pool, chat_server).await,
Some("Note") => receive_create_comment(create, client, pool, chat_server).await,
let user = get_user_from_activity(&create, client, pool).await?;
let page = PageExt::from_any_base(create.object().to_owned().one().unwrap())?.unwrap();
- let post = PostForm::from_apub(&page, client, pool).await?;
+ let post = PostForm::from_apub(&page, client, pool, Some(user.actor_id()?)).await?;
let inserted_post = blocking(pool, move |conn| Post::create(conn, &post)).await??;
let user = get_user_from_activity(&create, client, pool).await?;
let note = Note::from_any_base(create.object().to_owned().one().unwrap())?.unwrap();
- let comment = CommentForm::from_apub(¬e, client, pool).await?;
+ let comment = CommentForm::from_apub(¬e, client, pool, Some(user.actor_id()?)).await?;
let inserted_comment = blocking(pool, move |conn| Comment::create(conn, &comment)).await??;
get_user_from_activity,
receive_unhandled_activity,
},
+ ActorType,
FromApub,
GroupExt,
PageExt,
let user = get_user_from_activity(&delete, client, pool).await?;
let page = PageExt::from_any_base(delete.object().to_owned().one().unwrap())?.unwrap();
- let post_ap_id = PostForm::from_apub(&page, client, pool)
+ let post_ap_id = PostForm::from_apub(&page, client, pool, Some(user.actor_id()?))
.await?
.get_ap_id()?;
let user = get_user_from_activity(&delete, client, pool).await?;
let note = Note::from_any_base(delete.object().to_owned().one().unwrap())?.unwrap();
- let comment_ap_id = CommentForm::from_apub(¬e, client, pool)
+ let comment_ap_id = CommentForm::from_apub(¬e, client, pool, Some(user.actor_id()?))
.await?
.get_ap_id()?;
let group = GroupExt::from_any_base(delete.object().to_owned().one().unwrap())?.unwrap();
let user = get_user_from_activity(&delete, client, pool).await?;
- let community_actor_id = CommunityForm::from_apub(&group, client, pool)
+ let community_actor_id = CommunityForm::from_apub(&group, client, pool, Some(user.actor_id()?))
.await?
.actor_id;
let user = get_user_from_activity(&dislike, client, pool).await?;
let page = PageExt::from_any_base(dislike.object().to_owned().one().unwrap())?.unwrap();
- let post = PostForm::from_apub(&page, client, pool).await?;
+ let post = PostForm::from_apub(&page, client, pool, None).await?;
let post_id = get_or_fetch_and_insert_post(&post.get_ap_id()?, client, pool)
.await?
let note = Note::from_any_base(dislike.object().to_owned().one().unwrap())?.unwrap();
let user = get_user_from_activity(&dislike, client, pool).await?;
- let comment = CommentForm::from_apub(¬e, client, pool).await?;
+ let comment = CommentForm::from_apub(¬e, client, pool, None).await?;
let comment_id = get_or_fetch_and_insert_comment(&comment.get_ap_id()?, client, pool)
.await?
let user = get_user_from_activity(&like, client, pool).await?;
let page = PageExt::from_any_base(like.object().to_owned().one().unwrap())?.unwrap();
- let post = PostForm::from_apub(&page, client, pool).await?;
+ let post = PostForm::from_apub(&page, client, pool, None).await?;
let post_id = get_or_fetch_and_insert_post(&post.get_ap_id()?, client, pool)
.await?
let note = Note::from_any_base(like.object().to_owned().one().unwrap())?.unwrap();
let user = get_user_from_activity(&like, client, pool).await?;
- let comment = CommentForm::from_apub(¬e, client, pool).await?;
+ let comment = CommentForm::from_apub(¬e, client, pool, None).await?;
let comment_id = get_or_fetch_and_insert_comment(&comment.get_ap_id()?, client, pool)
.await?
fetcher::{get_or_fetch_and_insert_comment, get_or_fetch_and_insert_post},
inbox::shared_inbox::{
announce_if_community_is_local,
+ get_community_id_from_activity,
get_user_from_activity,
receive_unhandled_activity,
},
+ ActorType,
FromApub,
GroupExt,
PageExt,
};
use activitystreams::{activity::Remove, base::AnyBase, object::Note, prelude::*};
use actix_web::{client::Client, HttpResponse};
+use anyhow::anyhow;
use lemmy_db::{
comment::{Comment, CommentForm},
comment_view::CommentView,
chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> {
let remove = Remove::from_any_base(activity)?.unwrap();
+ let actor = get_user_from_activity(&remove, client, pool).await?;
+ let community = get_community_id_from_activity(&remove)?;
+ if actor.actor_id()?.domain() != community.domain() {
+ return Err(anyhow!("Remove activities are only allowed on local objects").into());
+ }
+
match remove.object().as_single_kind_str() {
Some("Page") => receive_remove_post(remove, client, pool, chat_server).await,
Some("Note") => receive_remove_comment(remove, client, pool, chat_server).await,
let mod_ = get_user_from_activity(&remove, client, pool).await?;
let page = PageExt::from_any_base(remove.object().to_owned().one().unwrap())?.unwrap();
- let post_ap_id = PostForm::from_apub(&page, client, pool)
+ let post_ap_id = PostForm::from_apub(&page, client, pool, None)
.await?
.get_ap_id()?;
let mod_ = get_user_from_activity(&remove, client, pool).await?;
let note = Note::from_any_base(remove.object().to_owned().one().unwrap())?.unwrap();
- let comment_ap_id = CommentForm::from_apub(¬e, client, pool)
+ let comment_ap_id = CommentForm::from_apub(¬e, client, pool, None)
.await?
.get_ap_id()?;
let mod_ = get_user_from_activity(&remove, client, pool).await?;
let group = GroupExt::from_any_base(remove.object().to_owned().one().unwrap())?.unwrap();
- let community_actor_id = CommunityForm::from_apub(&group, client, pool)
+ let community_actor_id = CommunityForm::from_apub(&group, client, pool, Some(mod_.actor_id()?))
.await?
.actor_id;
get_user_from_activity,
receive_unhandled_activity,
},
+ ActorType,
FromApub,
GroupExt,
PageExt,
DbPool,
LemmyError,
};
-use activitystreams::{activity::*, base::AnyBase, object::Note, prelude::*};
+use activitystreams::{
+ activity::*,
+ base::{AnyBase, AsBase},
+ object::Note,
+ prelude::*,
+};
use actix_web::{client::Client, HttpResponse};
use anyhow::anyhow;
use lemmy_db::{
Some("Remove") => receive_undo_remove(undo, client, pool, chat_server).await,
Some("Like") => receive_undo_like(undo, client, pool, chat_server).await,
Some("Dislike") => receive_undo_dislike(undo, client, pool, chat_server).await,
- // TODO: handle undo_dislike?
_ => receive_unhandled_activity(undo),
}
}
+fn check_is_undo_valid<T, A>(outer_activity: &Undo, inner_activity: &T) -> Result<(), LemmyError>
+where
+ T: AsBase<A> + ActorAndObjectRef,
+{
+ let outer_actor = outer_activity.actor()?;
+ let outer_actor_uri = outer_actor.as_single_xsd_any_uri().unwrap();
+
+ let inner_actor = inner_activity.actor()?;
+ let inner_actor_uri = inner_actor.as_single_xsd_any_uri().unwrap();
+
+ if outer_actor_uri.domain() != inner_actor_uri.domain() {
+ Err(anyhow!("Cant undo activities from a different instance").into())
+ } else {
+ Ok(())
+ }
+}
+
async fn receive_undo_delete(
undo: Undo,
client: &Client,
chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> {
let delete = Delete::from_any_base(undo.object().to_owned().one().unwrap())?.unwrap();
+ check_is_undo_valid(&undo, &delete)?;
let type_ = delete.object().as_single_kind_str().unwrap();
match type_ {
"Note" => receive_undo_delete_comment(undo, &delete, client, pool, chat_server).await,
chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> {
let remove = Remove::from_any_base(undo.object().to_owned().one().unwrap())?.unwrap();
+ check_is_undo_valid(&undo, &remove)?;
let type_ = remove.object().as_single_kind_str().unwrap();
match type_ {
chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> {
let like = Like::from_any_base(undo.object().to_owned().one().unwrap())?.unwrap();
+ check_is_undo_valid(&undo, &like)?;
let type_ = like.object().as_single_kind_str().unwrap();
match type_ {
_chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> {
let dislike = Dislike::from_any_base(undo.object().to_owned().one().unwrap())?.unwrap();
+ check_is_undo_valid(&undo, &dislike)?;
+
+ // TODO: need to implement Undo<Dislike>
let type_ = dislike.object().as_single_kind_str().unwrap();
Err(anyhow!("Undo Delete type {} not supported", type_).into())
let user = get_user_from_activity(delete, client, pool).await?;
let note = Note::from_any_base(delete.object().to_owned().one().unwrap())?.unwrap();
- let comment_ap_id = CommentForm::from_apub(¬e, client, pool)
+ let comment_ap_id = CommentForm::from_apub(¬e, client, pool, Some(user.actor_id()?))
.await?
.get_ap_id()?;
let mod_ = get_user_from_activity(remove, client, pool).await?;
let note = Note::from_any_base(remove.object().to_owned().one().unwrap())?.unwrap();
- let comment_ap_id = CommentForm::from_apub(¬e, client, pool)
+ let comment_ap_id = CommentForm::from_apub(¬e, client, pool, None)
.await?
.get_ap_id()?;
let user = get_user_from_activity(delete, client, pool).await?;
let page = PageExt::from_any_base(delete.object().to_owned().one().unwrap())?.unwrap();
- let post_ap_id = PostForm::from_apub(&page, client, pool)
+ let post_ap_id = PostForm::from_apub(&page, client, pool, Some(user.actor_id()?))
.await?
.get_ap_id()?;
let mod_ = get_user_from_activity(remove, client, pool).await?;
let page = PageExt::from_any_base(remove.object().to_owned().one().unwrap())?.unwrap();
- let post_ap_id = PostForm::from_apub(&page, client, pool)
+ let post_ap_id = PostForm::from_apub(&page, client, pool, None)
.await?
.get_ap_id()?;
let user = get_user_from_activity(delete, client, pool).await?;
let group = GroupExt::from_any_base(delete.object().to_owned().one().unwrap())?.unwrap();
- let community_actor_id = CommunityForm::from_apub(&group, client, pool)
+ let community_actor_id = CommunityForm::from_apub(&group, client, pool, Some(user.actor_id()?))
.await?
.actor_id;
let mod_ = get_user_from_activity(remove, client, pool).await?;
let group = GroupExt::from_any_base(remove.object().to_owned().one().unwrap())?.unwrap();
- let community_actor_id = CommunityForm::from_apub(&group, client, pool)
+ let community_actor_id = CommunityForm::from_apub(&group, client, pool, Some(mod_.actor_id()?))
.await?
.actor_id;
let user = get_user_from_activity(like, client, pool).await?;
let note = Note::from_any_base(like.object().to_owned().one().unwrap())?.unwrap();
- let comment = CommentForm::from_apub(¬e, client, pool).await?;
+ let comment = CommentForm::from_apub(¬e, client, pool, None).await?;
let comment_id = get_or_fetch_and_insert_comment(&comment.get_ap_id()?, client, pool)
.await?
let user = get_user_from_activity(like, client, pool).await?;
let page = PageExt::from_any_base(like.object().to_owned().one().unwrap())?.unwrap();
- let post = PostForm::from_apub(&page, client, pool).await?;
+ let post = PostForm::from_apub(&page, client, pool, None).await?;
let post_id = get_or_fetch_and_insert_post(&post.get_ap_id()?, client, pool)
.await?
get_user_from_activity,
receive_unhandled_activity,
},
+ ActorType,
FromApub,
PageExt,
},
chat_server: ChatServerParam,
) -> Result<HttpResponse, LemmyError> {
let update = Update::from_any_base(activity)?.unwrap();
+
+ // ensure that update and actor come from the same instance
+ let user = get_user_from_activity(&update, client, pool).await?;
+ update.id(user.actor_id()?.domain().unwrap())?;
+
match update.object().as_single_kind_str() {
Some("Page") => receive_update_post(update, client, pool, chat_server).await,
Some("Note") => receive_update_comment(update, client, pool, chat_server).await,
let user = get_user_from_activity(&update, client, pool).await?;
let page = PageExt::from_any_base(update.object().to_owned().one().unwrap())?.unwrap();
- let post = PostForm::from_apub(&page, client, pool).await?;
+ let post = PostForm::from_apub(&page, client, pool, Some(user.actor_id()?)).await?;
- let post_id = get_or_fetch_and_insert_post(&post.get_ap_id()?, client, pool)
+ let original_post_id = get_or_fetch_and_insert_post(&post.get_ap_id()?, client, pool)
.await?
.id;
- blocking(pool, move |conn| Post::update(conn, post_id, &post)).await??;
+ blocking(pool, move |conn| {
+ Post::update(conn, original_post_id, &post)
+ })
+ .await??;
// Refetch the view
- let post_view = blocking(pool, move |conn| PostView::read(conn, post_id, None)).await??;
+ let post_view = blocking(pool, move |conn| {
+ PostView::read(conn, original_post_id, None)
+ })
+ .await??;
let res = PostResponse { post: post_view };
let note = Note::from_any_base(update.object().to_owned().one().unwrap())?.unwrap();
let user = get_user_from_activity(&update, client, pool).await?;
- let comment = CommentForm::from_apub(¬e, client, pool).await?;
+ let comment = CommentForm::from_apub(¬e, client, pool, Some(user.actor_id()?)).await?;
- let comment_id = get_or_fetch_and_insert_comment(&comment.get_ap_id()?, client, pool)
+ let original_comment_id = get_or_fetch_and_insert_comment(&comment.get_ap_id()?, client, pool)
.await?
.id;
let updated_comment = blocking(pool, move |conn| {
- Comment::update(conn, comment_id, &comment)
+ Comment::update(conn, original_comment_id, &comment)
})
.await??;
send_local_notifs(mentions, updated_comment, &user, post, pool, false).await?;
// Refetch the view
- let comment_view =
- blocking(pool, move |conn| CommentView::read(conn, comment_id, None)).await??;
+ let comment_view = blocking(pool, move |conn| {
+ CommentView::read(conn, original_comment_id, None)
+ })
+ .await??;
let res = CommentResponse {
comment: comment_view,
verify(&request, &user)?;
- insert_activity(user.id, activity.clone(), false, &db).await?;
-
let any_base = activity.clone().into_any_base()?;
let kind = activity.kind().unwrap();
- match kind {
- ValidTypes::Follow => handle_follow(any_base, user, community, &client, db).await,
- ValidTypes::Undo => handle_undo_follow(any_base, user, community, db).await,
- }
+ let user_id = user.id;
+ let res = match kind {
+ ValidTypes::Follow => handle_follow(any_base, user, community, &client, &db).await,
+ ValidTypes::Undo => handle_undo_follow(any_base, user, community, &db).await,
+ };
+
+ insert_activity(user_id, activity.clone(), false, &db).await?;
+ res
}
/// Handle a follow request from a remote user, adding it to the local database and returning an
user: User_,
community: Community,
client: &Client,
- db: DbPoolParam,
+ db: &DbPoolParam,
) -> Result<HttpResponse, LemmyError> {
let follow = Follow::from_any_base(activity)?.unwrap();
let community_follower_form = CommunityFollowerForm {
};
// This will fail if they're already a follower, but ignore the error.
- blocking(&db, move |conn| {
+ blocking(db, move |conn| {
CommunityFollower::follow(&conn, &community_follower_form).ok()
})
.await?;
- community.send_accept_follow(follow, &client, &db).await?;
+ community.send_accept_follow(follow, &client, db).await?;
Ok(HttpResponse::Ok().finish())
}
activity: AnyBase,
user: User_,
community: Community,
- db: DbPoolParam,
+ db: &DbPoolParam,
) -> Result<HttpResponse, LemmyError> {
let _undo = Undo::from_any_base(activity)?.unwrap();
};
// This will fail if they aren't a follower, but ignore the error.
- blocking(&db, move |conn| {
+ blocking(db, move |conn| {
CommunityFollower::unfollow(&conn, &community_follower_form).ok()
})
.await?;
debug!("Shared inbox received activity: {}", json);
let sender = &activity.actor()?.to_owned().single_xsd_any_uri().unwrap();
- // TODO: pass this actor in instead of using get_user_from_activity()
- let actor = get_or_fetch_and_upsert_actor(sender, &client, &pool).await?;
-
- let community = get_community_id_from_activity(&activity).await;
+ let community = get_community_id_from_activity(&activity)?;
check_is_apub_id_valid(sender)?;
check_is_apub_id_valid(&community)?;
- verify(&request, actor.as_ref())?;
- insert_activity(actor.user_id(), activity.clone(), false, &pool).await?;
+ let actor = get_or_fetch_and_upsert_actor(sender, &client, &pool).await?;
+ verify(&request, actor.as_ref())?;
let any_base = activity.clone().into_any_base()?;
let kind = activity.kind().unwrap();
- match kind {
+ let res = match kind {
ValidTypes::Announce => receive_announce(any_base, &client, &pool, chat_server).await,
ValidTypes::Create => receive_create(any_base, &client, &pool, chat_server).await,
ValidTypes::Update => receive_update(any_base, &client, &pool, chat_server).await,
ValidTypes::Remove => receive_remove(any_base, &client, &pool, chat_server).await,
ValidTypes::Delete => receive_delete(any_base, &client, &pool, chat_server).await,
ValidTypes::Undo => receive_undo(any_base, &client, &pool, chat_server).await,
- }
+ };
+
+ insert_activity(actor.user_id(), activity.clone(), false, &pool).await?;
+ res
}
pub(in crate::apub::inbox) fn receive_unhandled_activity<A>(
get_or_fetch_and_upsert_user(&user_uri, client, pool).await
}
-pub(in crate::apub::inbox) async fn get_community_id_from_activity<T, A>(activity: &T) -> Url
+pub(in crate::apub::inbox) fn get_community_id_from_activity<T, A>(
+ activity: &T,
+) -> Result<Url, LemmyError>
where
T: AsBase<A> + ActorAndObjectRef + AsObject<A>,
{
let cc = activity.cc().unwrap();
let cc = cc.as_many().unwrap();
- cc.first().unwrap().as_xsd_any_uri().unwrap().to_owned()
+ Ok(cc.first().unwrap().as_xsd_any_uri().unwrap().to_owned())
}
pub(in crate::apub::inbox) async fn announce_if_community_is_local<T, Kind>(
let actor = get_or_fetch_and_upsert_actor(actor_uri, &client, &pool).await?;
verify(&request, actor.as_ref())?;
- insert_activity(actor.user_id(), activity.clone(), false, &pool).await?;
-
let any_base = activity.clone().into_any_base()?;
let kind = activity.kind().unwrap();
- match kind {
+ let res = match kind {
ValidTypes::Accept => receive_accept(any_base, username, &client, &pool).await,
ValidTypes::Create => {
receive_create_private_message(any_base, &client, &pool, chat_server).await
ValidTypes::Undo => {
receive_undo_delete_private_message(any_base, &client, &pool, chat_server).await
}
- }
+ };
+
+ insert_activity(actor.user_id(), activity.clone(), false, &pool).await?;
+ res
}
/// Handle accepted follows.
let create = Create::from_any_base(activity)?.unwrap();
let note = Note::from_any_base(create.object().as_one().unwrap().to_owned())?.unwrap();
- let private_message = PrivateMessageForm::from_apub(¬e, client, pool).await?;
+ let domain = Some(create.id_unchecked().unwrap().to_owned());
+ let private_message = PrivateMessageForm::from_apub(¬e, client, pool, domain).await?;
let inserted_private_message = blocking(pool, move |conn| {
PrivateMessage::create(conn, &private_message)
let update = Update::from_any_base(activity)?.unwrap();
let note = Note::from_any_base(update.object().as_one().unwrap().to_owned())?.unwrap();
- let private_message_form = PrivateMessageForm::from_apub(¬e, client, pool).await?;
+ let domain = Some(update.id_unchecked().unwrap().to_owned());
+ let private_message_form = PrivateMessageForm::from_apub(¬e, client, pool, domain).await?;
let private_message_ap_id = private_message_form.ap_id.clone();
let private_message = blocking(pool, move |conn| {
let delete = Delete::from_any_base(activity)?.unwrap();
let note = Note::from_any_base(delete.object().as_one().unwrap().to_owned())?.unwrap();
- let private_message_form = PrivateMessageForm::from_apub(¬e, client, pool).await?;
+ let domain = Some(delete.id_unchecked().unwrap().to_owned());
+ let private_message_form = PrivateMessageForm::from_apub(¬e, client, pool, domain).await?;
let private_message_ap_id = private_message_form.ap_id;
let private_message = blocking(pool, move |conn| {
let delete = Delete::from_any_base(undo.object().as_one().unwrap().to_owned())?.unwrap();
let note = Note::from_any_base(delete.object().as_one().unwrap().to_owned())?.unwrap();
- let private_message = PrivateMessageForm::from_apub(¬e, client, pool).await?;
+ let domain = Some(undo.id_unchecked().unwrap().to_owned());
+ let private_message = PrivateMessageForm::from_apub(¬e, client, pool, domain).await?;
let private_message_ap_id = private_message.ap_id.clone();
let private_message_id = blocking(pool, move |conn| {
use activitystreams::{
activity::Follow,
actor::{ApActor, Group, Person},
+ base::AsBase,
+ markers::Base,
object::{Page, Tombstone},
prelude::*,
};
#[async_trait::async_trait(?Send)]
pub trait FromApub {
type ApubType;
+ /// Converts an object from ActivityPub type to Lemmy internal type.
+ ///
+ /// * `apub` The object to read from
+ /// * `client` Web client to fetch remote actors with
+ /// * `pool` Database connection
+ /// * `expected_domain` If present, ensure that the apub object comes from the same domain as
+ /// this URL
+ ///
async fn from_apub(
apub: &Self::ApubType,
client: &Client,
pool: &DbPool,
+ expected_domain: Option<Url>,
) -> Result<Self, LemmyError>
where
Self: Sized;
) -> Result<(), LemmyError>;
}
+pub(in crate::apub) fn check_actor_domain<T, Kind>(
+ apub: &T,
+ expected_domain: Option<Url>,
+) -> Result<String, LemmyError>
+where
+ T: Base + AsBase<Kind>,
+{
+ let actor_id = if let Some(url) = expected_domain {
+ let domain = url.domain().unwrap();
+ apub.id(domain)?.unwrap()
+ } else {
+ let actor_id = apub.id_unchecked().unwrap();
+ check_is_apub_id_valid(&actor_id)?;
+ actor_id
+ };
+ Ok(actor_id.to_string())
+}
+
#[async_trait::async_trait(?Send)]
pub trait ApubLikeableType {
async fn send_like(
use crate::{
+ api::check_slurs,
apub::{
activities::{generate_activity_id, send_activity_to_community},
+ check_actor_domain,
create_apub_response,
create_apub_tombstone_response,
create_tombstone,
user::User_,
Crud,
};
-use lemmy_utils::convert_datetime;
+use lemmy_utils::{convert_datetime, remove_slurs};
use serde::Deserialize;
use url::Url;
page: &PageExt,
client: &Client,
pool: &DbPool,
+ expected_domain: Option<Url>,
) -> Result<PostForm, LemmyError> {
let ext = &page.ext_one;
let creator_actor_id = page
None => (None, None, None),
};
+ let name = page
+ .inner
+ .summary()
+ .as_ref()
+ .unwrap()
+ .as_single_xsd_string()
+ .unwrap()
+ .to_string();
let url = page
.inner
.url()
.content()
.as_ref()
.map(|c| c.as_single_xsd_string().unwrap().to_string());
+ check_slurs(&name)?;
+ let body_slurs_removed = body.map(|b| remove_slurs(&b));
Ok(PostForm {
- name: page
- .inner
- .summary()
- .as_ref()
- .unwrap()
- .as_single_xsd_string()
- .unwrap()
- .to_string(),
+ name,
url,
- body,
+ body: body_slurs_removed,
creator_id: creator.id,
community_id: community.id,
removed: None,
embed_description,
embed_html,
thumbnail_url,
- ap_id: page.inner.id_unchecked().unwrap().to_string(),
+ ap_id: check_actor_domain(page, expected_domain)?,
local: false,
})
}
use crate::{
apub::{
activities::{generate_activity_id, send_activity},
+ check_actor_domain,
+ check_is_apub_id_valid,
create_tombstone,
fetcher::get_or_fetch_and_upsert_user,
insert_activity,
note: &Note,
client: &Client,
pool: &DbPool,
+ expected_domain: Option<Url>,
) -> Result<PrivateMessageForm, LemmyError> {
let creator_actor_id = note
.attributed_to()
.unwrap();
let creator = get_or_fetch_and_upsert_user(&creator_actor_id, client, pool).await?;
-
let recipient_actor_id = note.to().unwrap().clone().single_xsd_any_uri().unwrap();
-
let recipient = get_or_fetch_and_upsert_user(&recipient_actor_id, client, pool).await?;
+ let ap_id = note.id_unchecked().unwrap().to_string();
+ check_is_apub_id_valid(&Url::parse(&ap_id)?)?;
Ok(PrivateMessageForm {
creator_id: creator.id,
updated: note.updated().map(|u| u.to_owned().naive_local()),
deleted: None,
read: None,
- ap_id: note.id_unchecked().unwrap().to_string(),
+ ap_id: check_actor_domain(note, expected_domain)?,
local: false,
})
}
use crate::{
+ api::{check_slurs, check_slurs_opt},
apub::{
activities::{generate_activity_id, send_activity},
+ check_actor_domain,
create_apub_response,
insert_activity,
ActorType,
impl FromApub for UserForm {
type ApubType = PersonExt;
/// Parse an ActivityPub person received from another instance into a Lemmy user.
- async fn from_apub(person: &PersonExt, _: &Client, _: &DbPool) -> Result<Self, LemmyError> {
+ async fn from_apub(
+ person: &PersonExt,
+ _: &Client,
+ _: &DbPool,
+ expected_domain: Option<Url>,
+ ) -> Result<Self, LemmyError> {
let avatar = match person.icon() {
Some(any_image) => Some(
Image::from_any_base(any_image.as_one().unwrap().clone())
None => None,
};
+ let name = person
+ .name()
+ .unwrap()
+ .one()
+ .unwrap()
+ .as_xsd_string()
+ .unwrap()
+ .to_string();
+ let preferred_username = person.inner.preferred_username().map(|u| u.to_string());
+ let bio = person
+ .inner
+ .summary()
+ .map(|s| s.as_single_xsd_string().unwrap().into());
+ check_slurs(&name)?;
+ check_slurs_opt(&preferred_username)?;
+ check_slurs_opt(&bio)?;
+
Ok(UserForm {
- name: person
- .name()
- .unwrap()
- .one()
- .unwrap()
- .as_xsd_string()
- .unwrap()
- .to_string(),
- preferred_username: person.inner.preferred_username().map(|u| u.to_string()),
+ name,
+ preferred_username,
password_encrypted: "".to_string(),
admin: false,
banned: false,
show_avatars: false,
send_notifications_to_email: false,
matrix_user_id: None,
- actor_id: person.id_unchecked().unwrap().to_string(),
- bio: person
- .inner
- .summary()
- .map(|s| s.as_single_xsd_string().unwrap().into()),
+ actor_id: check_actor_domain(person, expected_domain)?,
+ bio,
local: false,
private_key: None,
public_key: Some(person.ext_one.public_key.to_owned().public_key_pem),