#!/bin/bash
set -e
-cargo +nightly fmt -- --check
+cargo +nightly fmt
cargo +nightly clippy --workspace --tests --all-targets --all-features -- \
-D warnings -D deprecated -D clippy::perf -D clippy::complexity -D clippy::dbg_macro
"http",
"http-signature-normalization-actix",
"itertools",
+ "lazy_static",
"lemmy_api_common",
"lemmy_apub_lib",
"lemmy_db_schema",
background-jobs = "0.9.0"
reqwest = { version = "0.11.4", features = ["json"] }
html2md = "0.2.13"
+lazy_static = "1.4.0"
[dev-dependencies]
serial_test = "0.5.1"
use lemmy_api_common::{blocking, check_post_deleted_or_removed};
use lemmy_apub_lib::{
data::Data,
- traits::{ActivityFields, ActivityHandler, ActorType, FromApub, ToApub},
+ traits::{ActivityFields, ActivityHandler, ActorType, ApubObject},
values::PublicUrl,
verify::verify_domains_match,
};
let create_or_update = CreateOrUpdateComment {
actor: ObjectId::new(actor.actor_id()),
to: [PublicUrl::Public],
- object: comment.to_apub(context.pool()).await?,
+ object: comment.to_apub(context).await?,
cc: maa.ccs,
tag: maa.tags,
kind,
use lemmy_api_common::blocking;
use lemmy_apub_lib::{
data::Data,
- traits::{ActivityFields, ActivityHandler, ActorType, ToApub},
+ traits::{ActivityFields, ActivityHandler, ActorType, ApubObject},
values::PublicUrl,
};
use lemmy_db_schema::{
let update = UpdateCommunity {
actor: ObjectId::new(actor.actor_id()),
to: [PublicUrl::Public],
- object: community.to_apub(context.pool()).await?,
+ object: community.to_apub(context).await?,
cc: [ObjectId::new(community.actor_id())],
kind: UpdateType::Update,
id: id.clone(),
use lemmy_api_common::blocking;
use lemmy_apub_lib::{
data::Data,
- traits::{ActivityFields, ActivityHandler, ActorType, FromApub, ToApub},
+ traits::{ActivityFields, ActivityHandler, ActorType, ApubObject},
values::PublicUrl,
verify::{verify_domains_match, verify_urls_match},
};
}
impl CreateOrUpdatePost {
- pub async fn send(
+ pub(crate) async fn new(
post: &ApubPost,
actor: &ApubPerson,
+ community: &ApubCommunity,
kind: CreateOrUpdateType,
context: &LemmyContext,
- ) -> Result<(), LemmyError> {
- let community_id = post.community_id;
- let community: ApubCommunity = blocking(context.pool(), move |conn| {
- Community::read(conn, community_id)
- })
- .await??
- .into();
-
+ ) -> Result<CreateOrUpdatePost, LemmyError> {
let id = generate_activity_id(
kind.clone(),
&context.settings().get_protocol_and_hostname(),
)?;
- let create_or_update = CreateOrUpdatePost {
+ Ok(CreateOrUpdatePost {
actor: ObjectId::new(actor.actor_id()),
to: [PublicUrl::Public],
- object: post.to_apub(context.pool()).await?,
+ object: post.to_apub(context).await?,
cc: [ObjectId::new(community.actor_id())],
kind,
id: id.clone(),
context: lemmy_context(),
unparsed: Default::default(),
- };
-
+ })
+ }
+ pub async fn send(
+ post: &ApubPost,
+ actor: &ApubPerson,
+ kind: CreateOrUpdateType,
+ context: &LemmyContext,
+ ) -> Result<(), LemmyError> {
+ let community_id = post.community_id;
+ let community: ApubCommunity = blocking(context.pool(), move |conn| {
+ Community::read(conn, community_id)
+ })
+ .await??
+ .into();
+ let create_or_update = CreateOrUpdatePost::new(post, actor, &community, kind, context).await?;
+ let id = create_or_update.id.clone();
let activity = AnnouncableActivities::CreateOrUpdatePost(Box::new(create_or_update));
send_to_community(activity, &id, actor, &community, vec![], context).await
}
use lemmy_api_common::blocking;
use lemmy_apub_lib::{
data::Data,
- traits::{ActivityFields, ActivityHandler, ActorType, FromApub, ToApub},
+ traits::{ActivityFields, ActivityHandler, ActorType, ApubObject},
verify::verify_domains_match,
};
use lemmy_db_schema::{source::person::Person, traits::Crud};
id: id.clone(),
actor: ObjectId::new(actor.actor_id()),
to: ObjectId::new(recipient.actor_id()),
- object: private_message.to_apub(context.pool()).await?,
+ object: private_message.to_apub(context).await?,
kind,
unparsed: Default::default(),
};
--- /dev/null
+use crate::{
+ collections::CommunityContext,
+ context::lemmy_context,
+ fetcher::object_id::ObjectId,
+ generate_moderators_url,
+ objects::person::ApubPerson,
+};
+use activitystreams::{
+ base::AnyBase,
+ chrono::NaiveDateTime,
+ collection::kind::OrderedCollectionType,
+ primitives::OneOrMany,
+ url::Url,
+};
+use lemmy_api_common::blocking;
+use lemmy_apub_lib::{traits::ApubObject, verify::verify_domains_match};
+use lemmy_db_schema::{
+ source::community::{CommunityModerator, CommunityModeratorForm},
+ traits::Joinable,
+};
+use lemmy_db_views_actor::community_moderator_view::CommunityModeratorView;
+use lemmy_utils::LemmyError;
+use serde::{Deserialize, Serialize};
+use serde_with::skip_serializing_none;
+
+#[skip_serializing_none]
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct GroupModerators {
+ #[serde(rename = "@context")]
+ context: OneOrMany<AnyBase>,
+ r#type: OrderedCollectionType,
+ id: Url,
+ ordered_items: Vec<ObjectId<ApubPerson>>,
+}
+
+#[derive(Clone, Debug)]
+pub(crate) struct ApubCommunityModerators(pub(crate) Vec<CommunityModeratorView>);
+
+#[async_trait::async_trait(?Send)]
+impl ApubObject for ApubCommunityModerators {
+ type DataType = CommunityContext;
+ type TombstoneType = ();
+ type ApubType = GroupModerators;
+
+ fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
+ None
+ }
+
+ async fn read_from_apub_id(
+ _object_id: Url,
+ data: &Self::DataType,
+ ) -> Result<Option<Self>, LemmyError> {
+ // Only read from database if its a local community, otherwise fetch over http
+ if data.0.local {
+ let cid = data.0.id;
+ let moderators = blocking(data.1.pool(), move |conn| {
+ CommunityModeratorView::for_community(conn, cid)
+ })
+ .await??;
+ Ok(Some(ApubCommunityModerators { 0: moderators }))
+ } else {
+ Ok(None)
+ }
+ }
+
+ async fn delete(self, _data: &Self::DataType) -> Result<(), LemmyError> {
+ unimplemented!()
+ }
+
+ async fn to_apub(&self, data: &Self::DataType) -> Result<Self::ApubType, LemmyError> {
+ let ordered_items = self
+ .0
+ .iter()
+ .map(|m| ObjectId::<ApubPerson>::new(m.moderator.actor_id.clone().into_inner()))
+ .collect();
+ Ok(GroupModerators {
+ context: lemmy_context(),
+ r#type: OrderedCollectionType::OrderedCollection,
+ id: generate_moderators_url(&data.0.actor_id)?.into(),
+ ordered_items,
+ })
+ }
+
+ fn to_tombstone(&self) -> Result<Self::TombstoneType, LemmyError> {
+ unimplemented!()
+ }
+
+ async fn from_apub(
+ apub: &Self::ApubType,
+ data: &Self::DataType,
+ expected_domain: &Url,
+ request_counter: &mut i32,
+ ) -> Result<Self, LemmyError> {
+ verify_domains_match(expected_domain, &apub.id)?;
+ let community_id = data.0.id;
+ let current_moderators = blocking(data.1.pool(), move |conn| {
+ CommunityModeratorView::for_community(conn, community_id)
+ })
+ .await??;
+ // Remove old mods from database which arent in the moderators collection anymore
+ for mod_user in ¤t_moderators {
+ let mod_id = ObjectId::new(mod_user.moderator.actor_id.clone().into_inner());
+ if !apub.ordered_items.contains(&mod_id) {
+ let community_moderator_form = CommunityModeratorForm {
+ community_id: mod_user.community.id,
+ person_id: mod_user.moderator.id,
+ };
+ blocking(data.1.pool(), move |conn| {
+ CommunityModerator::leave(conn, &community_moderator_form)
+ })
+ .await??;
+ }
+ }
+
+ // Add new mods to database which have been added to moderators collection
+ for mod_id in &apub.ordered_items {
+ let mod_id = ObjectId::new(mod_id.clone());
+ let mod_user: ApubPerson = mod_id.dereference(&data.1, request_counter).await?;
+
+ if !current_moderators
+ .clone()
+ .iter()
+ .map(|c| c.moderator.actor_id.clone())
+ .any(|x| x == mod_user.actor_id)
+ {
+ let community_moderator_form = CommunityModeratorForm {
+ community_id: data.0.id,
+ person_id: mod_user.id,
+ };
+ blocking(data.1.pool(), move |conn| {
+ CommunityModerator::join(conn, &community_moderator_form)
+ })
+ .await??;
+ }
+ }
+
+ // This return value is unused, so just set an empty vec
+ Ok(ApubCommunityModerators { 0: vec![] })
+ }
+}
--- /dev/null
+use crate::{
+ activities::{post::create_or_update::CreateOrUpdatePost, CreateOrUpdateType},
+ collections::CommunityContext,
+ context::lemmy_context,
+ generate_outbox_url,
+ objects::{person::ApubPerson, post::ApubPost},
+};
+use activitystreams::{
+ base::AnyBase,
+ chrono::NaiveDateTime,
+ collection::kind::OrderedCollectionType,
+ primitives::OneOrMany,
+ url::Url,
+};
+use lemmy_api_common::blocking;
+use lemmy_apub_lib::{
+ data::Data,
+ traits::{ActivityHandler, ApubObject},
+ verify::verify_domains_match,
+};
+use lemmy_db_schema::{
+ source::{person::Person, post::Post},
+ traits::Crud,
+};
+use lemmy_utils::LemmyError;
+use serde::{Deserialize, Serialize};
+use serde_with::skip_serializing_none;
+
+#[skip_serializing_none]
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct GroupOutbox {
+ #[serde(rename = "@context")]
+ context: OneOrMany<AnyBase>,
+ r#type: OrderedCollectionType,
+ id: Url,
+ ordered_items: Vec<CreateOrUpdatePost>,
+}
+
+#[derive(Clone, Debug)]
+pub(crate) struct ApubCommunityOutbox(Vec<ApubPost>);
+
+#[async_trait::async_trait(?Send)]
+impl ApubObject for ApubCommunityOutbox {
+ type DataType = CommunityContext;
+ type TombstoneType = ();
+ type ApubType = GroupOutbox;
+
+ fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
+ None
+ }
+
+ async fn read_from_apub_id(
+ _object_id: Url,
+ data: &Self::DataType,
+ ) -> Result<Option<Self>, LemmyError> {
+ // Only read from database if its a local community, otherwise fetch over http
+ if data.0.local {
+ let community_id = data.0.id;
+ let post_list: Vec<ApubPost> = blocking(data.1.pool(), move |conn| {
+ Post::list_for_community(conn, community_id)
+ })
+ .await??
+ .into_iter()
+ .map(Into::into)
+ .collect();
+ Ok(Some(ApubCommunityOutbox(post_list)))
+ } else {
+ Ok(None)
+ }
+ }
+
+ async fn delete(self, _data: &Self::DataType) -> Result<(), LemmyError> {
+ // do nothing (it gets deleted automatically with the community)
+ Ok(())
+ }
+
+ async fn to_apub(&self, data: &Self::DataType) -> Result<Self::ApubType, LemmyError> {
+ let mut ordered_items = vec![];
+ for post in &self.0 {
+ let actor = post.creator_id;
+ let actor: ApubPerson = blocking(data.1.pool(), move |conn| Person::read(conn, actor))
+ .await??
+ .into();
+ let a =
+ CreateOrUpdatePost::new(post, &actor, &data.0, CreateOrUpdateType::Create, &data.1).await?;
+ ordered_items.push(a);
+ }
+
+ Ok(GroupOutbox {
+ context: lemmy_context(),
+ r#type: OrderedCollectionType::OrderedCollection,
+ id: generate_outbox_url(&data.0.actor_id)?.into(),
+ ordered_items,
+ })
+ }
+
+ fn to_tombstone(&self) -> Result<Self::TombstoneType, LemmyError> {
+ // no tombstone for this, there is only a tombstone for the community
+ unimplemented!()
+ }
+
+ async fn from_apub(
+ apub: &Self::ApubType,
+ data: &Self::DataType,
+ expected_domain: &Url,
+ request_counter: &mut i32,
+ ) -> Result<Self, LemmyError> {
+ verify_domains_match(expected_domain, &apub.id)?;
+ let mut outbox_activities = apub.ordered_items.clone();
+ if outbox_activities.len() > 20 {
+ outbox_activities = outbox_activities[0..20].to_vec();
+ }
+
+ // We intentionally ignore errors here. This is because the outbox might contain posts from old
+ // Lemmy versions, or from other software which we cant parse. In that case, we simply skip the
+ // item and only parse the ones that work.
+ for activity in outbox_activities {
+ activity
+ .receive(&Data::new(data.1.clone()), request_counter)
+ .await
+ .ok();
+ }
+
+ // This return value is unused, so just set an empty vec
+ Ok(ApubCommunityOutbox { 0: vec![] })
+ }
+}
--- /dev/null
+use crate::objects::community::ApubCommunity;
+use lemmy_websocket::LemmyContext;
+pub(crate) mod community_moderators;
+pub(crate) mod community_outbox;
+
+/// Put community in the data, so we dont have to read it again from the database.
+pub(crate) struct CommunityContext(pub ApubCommunity, pub LemmyContext);
+++ /dev/null
-use crate::{
- activities::community::announce::AnnounceActivity,
- fetcher::{fetch::fetch_remote_object, object_id::ObjectId},
- objects::{community::Group, person::ApubPerson},
-};
-use activitystreams::{
- base::AnyBase,
- collection::{CollectionExt, OrderedCollection},
-};
-use anyhow::Context;
-use lemmy_api_common::blocking;
-use lemmy_apub_lib::{data::Data, traits::ActivityHandler};
-use lemmy_db_schema::{
- source::community::{Community, CommunityModerator, CommunityModeratorForm},
- traits::Joinable,
-};
-use lemmy_db_views_actor::community_moderator_view::CommunityModeratorView;
-use lemmy_utils::{location_info, LemmyError};
-use lemmy_websocket::LemmyContext;
-use url::Url;
-
-pub(crate) async fn update_community_mods(
- group: &Group,
- community: &Community,
- context: &LemmyContext,
- request_counter: &mut i32,
-) -> Result<(), LemmyError> {
- let new_moderators = fetch_community_mods(context, group, request_counter).await?;
- let community_id = community.id;
- let current_moderators = blocking(context.pool(), move |conn| {
- CommunityModeratorView::for_community(conn, community_id)
- })
- .await??;
- // Remove old mods from database which arent in the moderators collection anymore
- for mod_user in ¤t_moderators {
- if !new_moderators.contains(&mod_user.moderator.actor_id.clone().into()) {
- let community_moderator_form = CommunityModeratorForm {
- community_id: mod_user.community.id,
- person_id: mod_user.moderator.id,
- };
- blocking(context.pool(), move |conn| {
- CommunityModerator::leave(conn, &community_moderator_form)
- })
- .await??;
- }
- }
-
- // Add new mods to database which have been added to moderators collection
- for mod_id in new_moderators {
- let mod_id = ObjectId::new(mod_id);
- let mod_user: ApubPerson = mod_id.dereference(context, request_counter).await?;
-
- if !current_moderators
- .clone()
- .iter()
- .map(|c| c.moderator.actor_id.clone())
- .any(|x| x == mod_user.actor_id)
- {
- let community_moderator_form = CommunityModeratorForm {
- community_id: community.id,
- person_id: mod_user.id,
- };
- blocking(context.pool(), move |conn| {
- CommunityModerator::join(conn, &community_moderator_form)
- })
- .await??;
- }
- }
-
- Ok(())
-}
-
-pub(crate) async fn fetch_community_outbox(
- context: &LemmyContext,
- outbox: &Url,
- recursion_counter: &mut i32,
-) -> Result<(), LemmyError> {
- let outbox = fetch_remote_object::<OrderedCollection>(
- context.client(),
- &context.settings(),
- outbox,
- recursion_counter,
- )
- .await?;
- let outbox_activities = outbox.items().context(location_info!())?.clone();
- let mut outbox_activities = outbox_activities.many().context(location_info!())?;
- if outbox_activities.len() > 20 {
- outbox_activities = outbox_activities[0..20].to_vec();
- }
-
- // We intentionally ignore errors here. This is because the outbox might contain posts from old
- // Lemmy versions, or from other software which we cant parse. In that case, we simply skip the
- // item and only parse the ones that work.
- for activity in outbox_activities {
- parse_outbox_item(activity, context, recursion_counter)
- .await
- .ok();
- }
-
- Ok(())
-}
-
-async fn parse_outbox_item(
- announce: AnyBase,
- context: &LemmyContext,
- request_counter: &mut i32,
-) -> Result<(), LemmyError> {
- // TODO: instead of converting like this, we should create a struct CommunityOutbox with
- // AnnounceActivity as inner type, but that gives me stackoverflow
- let ser = serde_json::to_string(&announce)?;
- let announce: AnnounceActivity = serde_json::from_str(&ser)?;
- announce
- .receive(&Data::new(context.clone()), request_counter)
- .await?;
- Ok(())
-}
-
-async fn fetch_community_mods(
- context: &LemmyContext,
- group: &Group,
- recursion_counter: &mut i32,
-) -> Result<Vec<Url>, LemmyError> {
- if let Some(mods_url) = &group.moderators {
- let mods = fetch_remote_object::<OrderedCollection>(
- context.client(),
- &context.settings(),
- mods_url,
- recursion_counter,
- )
- .await?;
- let mods = mods
- .items()
- .map(|i| i.as_many())
- .flatten()
- .context(location_info!())?
- .iter()
- .filter_map(|i| i.as_xsd_any_uri())
- .map(|u| u.to_owned())
- .collect();
- Ok(mods)
- } else {
- Ok(vec![])
- }
-}
+++ /dev/null
-use crate::check_is_apub_id_valid;
-use anyhow::anyhow;
-use lemmy_apub_lib::APUB_JSON_CONTENT_TYPE;
-use lemmy_utils::{request::retry, settings::structs::Settings, LemmyError};
-use log::info;
-use reqwest::Client;
-use serde::Deserialize;
-use std::time::Duration;
-use url::Url;
-
-/// Maximum number of HTTP requests allowed to handle a single incoming activity (or a single object
-/// fetch through the search).
-///
-/// A community fetch will load the outbox with up to 20 items, and fetch the creator for each item.
-/// So we are looking at a maximum of 22 requests (rounded up just to be safe).
-static MAX_REQUEST_NUMBER: i32 = 25;
-
-/// Fetch any type of ActivityPub object, handling things like HTTP headers, deserialisation,
-/// timeouts etc.
-pub(in crate::fetcher) async fn fetch_remote_object<Response>(
- client: &Client,
- settings: &Settings,
- url: &Url,
- recursion_counter: &mut i32,
-) -> Result<Response, LemmyError>
-where
- Response: for<'de> Deserialize<'de> + std::fmt::Debug,
-{
- *recursion_counter += 1;
- if *recursion_counter > MAX_REQUEST_NUMBER {
- return Err(anyhow!("Maximum recursion depth reached").into());
- }
- check_is_apub_id_valid(url, false, settings)?;
-
- let timeout = Duration::from_secs(60);
-
- let res = retry(|| {
- client
- .get(url.as_str())
- .header("Accept", APUB_JSON_CONTENT_TYPE)
- .timeout(timeout)
- .send()
- })
- .await?;
-
- let object = res.json().await?;
- info!("Fetched remote object {}", url);
- Ok(object)
-}
-pub mod community;
-mod fetch;
pub mod object_id;
pub mod post_or_comment;
pub mod search;
///
/// TODO it won't pick up new avatars, summaries etc until a day after.
/// Actors need an "update" activity pushed to other servers to fix this.
-fn should_refetch_actor(last_refreshed: NaiveDateTime) -> bool {
+fn should_refetch_object(last_refreshed: NaiveDateTime) -> bool {
let update_interval = if cfg!(debug_assertions) {
// avoid infinite loop when fetching community outbox
chrono::Duration::seconds(ACTOR_REFETCH_INTERVAL_SECONDS_DEBUG)
-use crate::fetcher::should_refetch_actor;
+use crate::fetcher::should_refetch_object;
use anyhow::anyhow;
use diesel::NotFound;
-use lemmy_apub_lib::{
- traits::{ApubObject, FromApub},
- APUB_JSON_CONTENT_TYPE,
-};
+use lemmy_apub_lib::{traits::ApubObject, APUB_JSON_CONTENT_TYPE};
use lemmy_db_schema::newtypes::DbUrl;
-use lemmy_utils::{request::retry, settings::structs::Settings, LemmyError};
-use lemmy_websocket::LemmyContext;
-use reqwest::StatusCode;
+use lemmy_utils::{
+ request::{build_user_agent, retry},
+ settings::structs::Settings,
+ LemmyError,
+};
+use log::info;
+use reqwest::{Client, StatusCode};
use serde::{Deserialize, Serialize};
use std::{
fmt::{Debug, Display, Formatter},
/// fetch through the search). This should be configurable.
static REQUEST_LIMIT: i32 = 25;
+// TODO: after moving this file to library, remove lazy_static dependency from apub crate
+lazy_static! {
+ static ref CLIENT: Client = Client::builder()
+ .user_agent(build_user_agent(&Settings::get()))
+ .build()
+ .unwrap();
+}
+
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
#[serde(transparent)]
pub struct ObjectId<Kind>(Url, #[serde(skip)] PhantomData<Kind>)
where
- Kind: FromApub<DataType = LemmyContext> + ApubObject<DataType = LemmyContext> + Send + 'static,
- for<'de2> <Kind as FromApub>::ApubType: serde::Deserialize<'de2>;
+ Kind: ApubObject + Send + 'static,
+ for<'de2> <Kind as ApubObject>::ApubType: serde::Deserialize<'de2>;
impl<Kind> ObjectId<Kind>
where
- Kind: FromApub<DataType = LemmyContext> + ApubObject<DataType = LemmyContext> + Send + 'static,
- for<'de> <Kind as FromApub>::ApubType: serde::Deserialize<'de>,
+ Kind: ApubObject + Send + 'static,
+ for<'de2> <Kind as ApubObject>::ApubType: serde::Deserialize<'de2>,
{
pub fn new<T>(url: T) -> Self
where
/// Fetches an activitypub object, either from local database (if possible), or over http.
pub async fn dereference(
&self,
- context: &LemmyContext,
+ data: &<Kind as ApubObject>::DataType,
request_counter: &mut i32,
) -> Result<Kind, LemmyError> {
- let db_object = self.dereference_from_db(context).await?;
+ let db_object = self.dereference_from_db(data).await?;
// if its a local object, only fetch it from the database and not over http
if self.0.domain() == Some(&Settings::get().get_hostname_without_port()?) {
};
}
+ // object found in database
if let Some(object) = db_object {
+ // object is old and should be refetched
if let Some(last_refreshed_at) = object.last_refreshed_at() {
- // TODO: rename to should_refetch_object()
- if should_refetch_actor(last_refreshed_at) {
+ if should_refetch_object(last_refreshed_at) {
return self
- .dereference_from_http(context, request_counter, Some(object))
+ .dereference_from_http(data, request_counter, Some(object))
.await;
}
}
Ok(object)
- } else {
+ }
+ // object not found, need to fetch over http
+ else {
self
- .dereference_from_http(context, request_counter, None)
+ .dereference_from_http(data, request_counter, None)
.await
}
}
/// Fetch an object from the local db. Instead of falling back to http, this throws an error if
/// the object is not found in the database.
- pub async fn dereference_local(&self, context: &LemmyContext) -> Result<Kind, LemmyError> {
- let object = self.dereference_from_db(context).await?;
+ pub async fn dereference_local(
+ &self,
+ data: &<Kind as ApubObject>::DataType,
+ ) -> Result<Kind, LemmyError> {
+ let object = self.dereference_from_db(data).await?;
object.ok_or_else(|| anyhow!("object not found in database {}", self).into())
}
/// returning none means the object was not found in local db
- async fn dereference_from_db(&self, context: &LemmyContext) -> Result<Option<Kind>, LemmyError> {
+ async fn dereference_from_db(
+ &self,
+ data: &<Kind as ApubObject>::DataType,
+ ) -> Result<Option<Kind>, LemmyError> {
let id = self.0.clone();
- ApubObject::read_from_apub_id(id, context).await
+ ApubObject::read_from_apub_id(id, data).await
}
async fn dereference_from_http(
&self,
- context: &LemmyContext,
+ data: &<Kind as ApubObject>::DataType,
request_counter: &mut i32,
db_object: Option<Kind>,
) -> Result<Kind, LemmyError> {
// dont fetch local objects this way
debug_assert!(self.0.domain() != Some(&Settings::get().hostname));
+ info!("Fetching remote object {}", self.to_string());
*request_counter += 1;
if *request_counter > REQUEST_LIMIT {
}
let res = retry(|| {
- context
- .client()
+ CLIENT
.get(self.0.as_str())
.header("Accept", APUB_JSON_CONTENT_TYPE)
.timeout(Duration::from_secs(60))
if res.status() == StatusCode::GONE {
if let Some(db_object) = db_object {
- db_object.delete(context).await?;
+ db_object.delete(data).await?;
}
return Err(anyhow!("Fetched remote object {} which was deleted", self).into());
}
let res2: Kind::ApubType = res.json().await?;
- Ok(Kind::from_apub(&res2, context, self.inner(), request_counter).await?)
+ Ok(Kind::from_apub(&res2, data, self.inner(), request_counter).await?)
}
}
impl<Kind> Display for ObjectId<Kind>
where
- Kind: FromApub<DataType = LemmyContext> + ApubObject<DataType = LemmyContext> + Send + 'static,
- for<'de> <Kind as FromApub>::ApubType: serde::Deserialize<'de>,
+ Kind: ApubObject + Send + 'static,
+ for<'de2> <Kind as ApubObject>::ApubType: serde::Deserialize<'de2>,
{
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0.to_string())
impl<Kind> From<ObjectId<Kind>> for Url
where
- Kind: FromApub<DataType = LemmyContext> + ApubObject<DataType = LemmyContext> + Send + 'static,
- for<'de> <Kind as FromApub>::ApubType: serde::Deserialize<'de>,
+ Kind: ApubObject + Send + 'static,
+ for<'de2> <Kind as ApubObject>::ApubType: serde::Deserialize<'de2>,
{
fn from(id: ObjectId<Kind>) -> Self {
id.0
impl<Kind> From<ObjectId<Kind>> for DbUrl
where
- Kind: FromApub<DataType = LemmyContext> + ApubObject<DataType = LemmyContext> + Send + 'static,
- for<'de> <Kind as FromApub>::ApubType: serde::Deserialize<'de>,
+ Kind: ApubObject + Send + 'static,
+ for<'de2> <Kind as ApubObject>::ApubType: serde::Deserialize<'de2>,
{
fn from(id: ObjectId<Kind>) -> Self {
id.0.into()
post::{ApubPost, Page},
};
use activitystreams::chrono::NaiveDateTime;
-use lemmy_apub_lib::traits::{ApubObject, FromApub};
+use lemmy_apub_lib::traits::ApubObject;
use lemmy_db_schema::source::{comment::CommentForm, post::PostForm};
use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
#[async_trait::async_trait(?Send)]
impl ApubObject for PostOrComment {
type DataType = LemmyContext;
+ type ApubType = PageOrNote;
+ type TombstoneType = ();
fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
None
PostOrComment::Comment(c) => c.delete(data).await,
}
}
-}
-#[async_trait::async_trait(?Send)]
-impl FromApub for PostOrComment {
- type ApubType = PageOrNote;
- type DataType = LemmyContext;
+ async fn to_apub(&self, _data: &Self::DataType) -> Result<Self::ApubType, LemmyError> {
+ unimplemented!()
+ }
+
+ fn to_tombstone(&self) -> Result<Self::TombstoneType, LemmyError> {
+ unimplemented!()
+ }
async fn from_apub(
apub: &PageOrNote,
use itertools::Itertools;
use lemmy_api_common::blocking;
use lemmy_apub_lib::{
- traits::{ApubObject, FromApub},
+ traits::ApubObject,
webfinger::{webfinger_resolve_actor, WebfingerType},
};
use lemmy_db_schema::{
#[async_trait::async_trait(?Send)]
impl ApubObject for SearchableObjects {
type DataType = LemmyContext;
+ type ApubType = SearchableApubTypes;
+ type TombstoneType = ();
fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
match self {
SearchableObjects::Comment(c) => c.delete(data).await,
}
}
-}
-#[async_trait::async_trait(?Send)]
-impl FromApub for SearchableObjects {
- type ApubType = SearchableApubTypes;
- type DataType = LemmyContext;
+ async fn to_apub(&self, _data: &Self::DataType) -> Result<Self::ApubType, LemmyError> {
+ unimplemented!()
+ }
+
+ fn to_tombstone(&self) -> Result<Self::TombstoneType, LemmyError> {
+ unimplemented!()
+ }
async fn from_apub(
apub: &Self::ApubType,
use actix_web::{body::Body, web, web::Path, HttpResponse};
use diesel::result::Error::NotFound;
use lemmy_api_common::blocking;
-use lemmy_apub_lib::traits::ToApub;
+use lemmy_apub_lib::traits::ApubObject;
use lemmy_db_schema::{newtypes::CommentId, source::comment::Comment, traits::Crud};
use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
}
if !comment.deleted {
- Ok(create_apub_response(
- &comment.to_apub(context.pool()).await?,
- ))
+ Ok(create_apub_response(&comment.to_apub(&**context).await?))
} else {
Ok(create_apub_tombstone_response(&comment.to_tombstone()?))
}
following::{follow::FollowCommunity, undo::UndoFollowCommunity},
report::Report,
},
+ collections::{
+ community_moderators::ApubCommunityModerators,
+ community_outbox::ApubCommunityOutbox,
+ CommunityContext,
+ },
context::lemmy_context,
- generate_moderators_url,
+ fetcher::object_id::ObjectId,
generate_outbox_url,
http::{
create_apub_response,
objects::community::ApubCommunity,
};
use activitystreams::{
- base::{AnyBase, BaseExt},
- collection::{CollectionExt, OrderedCollection, UnorderedCollection},
- url::Url,
+ base::BaseExt,
+ collection::{CollectionExt, UnorderedCollection},
};
use actix_web::{body::Body, web, web::Payload, HttpRequest, HttpResponse};
use lemmy_api_common::blocking;
-use lemmy_apub_lib::traits::{ActivityFields, ActivityHandler, ToApub};
-use lemmy_db_schema::source::{activity::Activity, community::Community};
-use lemmy_db_views_actor::{
- community_follower_view::CommunityFollowerView,
- community_moderator_view::CommunityModeratorView,
-};
+use lemmy_apub_lib::traits::{ActivityFields, ActivityHandler, ApubObject};
+use lemmy_db_schema::source::community::Community;
+use lemmy_db_views_actor::community_follower_view::CommunityFollowerView;
use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
use log::trace;
.into();
if !community.deleted {
- let apub = community.to_apub(context.pool()).await?;
+ let apub = community.to_apub(&**context).await?;
Ok(create_apub_response(&apub))
} else {
Community::read_from_name(conn, &info.community_name)
})
.await??;
-
- let community_actor_id = community.actor_id.to_owned();
- let activities = blocking(context.pool(), move |conn| {
- Activity::read_community_outbox(conn, &community_actor_id)
- })
- .await??;
-
- let activities = activities
- .iter()
- .map(AnyBase::from_arbitrary_json)
- .collect::<Result<Vec<AnyBase>, serde_json::Error>>()?;
- let len = activities.len();
- let mut collection = OrderedCollection::new();
- collection
- .set_many_items(activities)
- .set_many_contexts(lemmy_context())
- .set_id(generate_outbox_url(&community.actor_id)?.into())
- .set_total_items(len as u64);
- Ok(create_apub_response(&collection))
-}
-
-pub(crate) async fn get_apub_community_inbox(
- info: web::Path<CommunityQuery>,
- context: web::Data<LemmyContext>,
-) -> Result<HttpResponse<Body>, LemmyError> {
- let community = blocking(context.pool(), move |conn| {
- Community::read_from_name(conn, &info.community_name)
- })
- .await??;
-
- let mut collection = OrderedCollection::new();
- collection
- .set_id(community.inbox_url.into())
- .set_many_contexts(lemmy_context());
- Ok(create_apub_response(&collection))
+ let id = ObjectId::new(generate_outbox_url(&community.actor_id)?.into_inner());
+ let outbox_data = CommunityContext(community.into(), context.get_ref().clone());
+ let outbox: ApubCommunityOutbox = id.dereference(&outbox_data, &mut 0).await?;
+ Ok(create_apub_response(&outbox.to_apub(&outbox_data).await?))
}
pub(crate) async fn get_apub_community_moderators(
})
.await??
.into();
-
- // The attributed to, is an ordered vector with the creator actor_ids first,
- // then the rest of the moderators
- // TODO Technically the instance admins can mod the community, but lets
- // ignore that for now
- let cid = community.id;
- let moderators = blocking(context.pool(), move |conn| {
- CommunityModeratorView::for_community(conn, cid)
- })
- .await??;
-
- let moderators: Vec<Url> = moderators
- .into_iter()
- .map(|m| m.moderator.actor_id.into())
- .collect();
- let mut collection = OrderedCollection::new();
- collection
- .set_id(generate_moderators_url(&community.actor_id)?.into())
- .set_total_items(moderators.len() as u64)
- .set_many_items(moderators)
- .set_many_contexts(lemmy_context());
- Ok(create_apub_response(&collection))
+ let id = ObjectId::new(generate_outbox_url(&community.actor_id)?.into_inner());
+ let outbox_data = CommunityContext(community, context.get_ref().clone());
+ let moderators: ApubCommunityModerators = id.dereference(&outbox_data, &mut 0).await?;
+ Ok(create_apub_response(
+ &moderators.to_apub(&outbox_data).await?,
+ ))
}
};
use actix_web::{body::Body, web, web::Payload, HttpRequest, HttpResponse};
use lemmy_api_common::blocking;
-use lemmy_apub_lib::traits::{ActivityFields, ActivityHandler, ToApub};
+use lemmy_apub_lib::traits::{ActivityFields, ActivityHandler, ApubObject};
use lemmy_db_schema::source::person::Person;
use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
.into();
if !person.deleted {
- let apub = person.to_apub(context.pool()).await?;
+ let apub = person.to_apub(&context).await?;
Ok(create_apub_response(&apub))
} else {
.set_total_items(0_u64);
Ok(create_apub_response(&collection))
}
-
-pub(crate) async fn get_apub_person_inbox(
- info: web::Path<PersonQuery>,
- context: web::Data<LemmyContext>,
-) -> Result<HttpResponse<Body>, LemmyError> {
- let person = blocking(context.pool(), move |conn| {
- Person::find_by_name(conn, &info.user_name)
- })
- .await??;
-
- let mut collection = OrderedCollection::new();
- collection
- .set_id(person.inbox_url.into())
- .set_many_contexts(lemmy_context());
- Ok(create_apub_response(&collection))
-}
use actix_web::{body::Body, web, HttpResponse};
use diesel::result::Error::NotFound;
use lemmy_api_common::blocking;
-use lemmy_apub_lib::traits::ToApub;
+use lemmy_apub_lib::traits::ApubObject;
use lemmy_db_schema::{newtypes::PostId, source::post::Post, traits::Crud};
use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
}
if !post.deleted {
- Ok(create_apub_response(&post.to_apub(context.pool()).await?))
+ Ok(create_apub_response(&post.to_apub(&context).await?))
} else {
Ok(create_apub_tombstone_response(&post.to_tombstone()?))
}
community_inbox,
get_apub_community_followers,
get_apub_community_http,
- get_apub_community_inbox,
get_apub_community_moderators,
get_apub_community_outbox,
},
get_activity,
- person::{get_apub_person_http, get_apub_person_inbox, get_apub_person_outbox, person_inbox},
+ person::{get_apub_person_http, get_apub_person_outbox, person_inbox},
post::get_apub_post,
shared_inbox,
};
"/c/{community_name}/outbox",
web::get().to(get_apub_community_outbox),
)
- .route(
- "/c/{community_name}/inbox",
- web::get().to(get_apub_community_inbox),
- )
.route(
"/c/{community_name}/moderators",
web::get().to(get_apub_community_moderators),
"/u/{user_name}/outbox",
web::get().to(get_apub_person_outbox),
)
- .route("/u/{user_name}/inbox", web::get().to(get_apub_person_inbox))
.route("/post/{post_id}", web::get().to(get_apub_post))
.route("/comment/{comment_id}", web::get().to(get_apub_comment))
.route("/activities/{type_}/{id}", web::get().to(get_activity)),
pub mod activities;
+pub(crate) mod collections;
mod context;
pub mod fetcher;
pub mod http;
pub mod migrations;
pub mod objects;
+#[macro_use]
+extern crate lazy_static;
+
use crate::fetcher::post_or_comment::PostOrComment;
use anyhow::{anyhow, Context};
use lemmy_api_common::blocking;
activities::{verify_is_public, verify_person_in_community},
context::lemmy_context,
fetcher::object_id::ObjectId,
- objects::{create_tombstone, person::ApubPerson, post::ApubPost, Source},
+ objects::{person::ApubPerson, post::ApubPost, tombstone::Tombstone, Source},
PostOrComment,
};
use activitystreams::{
base::AnyBase,
chrono::NaiveDateTime,
- object::{kind::NoteType, Tombstone},
+ object::kind::NoteType,
primitives::OneOrMany,
public,
unparsed::Unparsed,
use html2md::parse_html;
use lemmy_api_common::blocking;
use lemmy_apub_lib::{
- traits::{ApubObject, FromApub, ToApub},
+ traits::ApubObject,
values::{MediaTypeHtml, MediaTypeMarkdown},
verify::verify_domains_match,
};
post::Post,
},
traits::Crud,
- DbPool,
};
use lemmy_utils::{
utils::{convert_datetime, remove_slurs},
#[async_trait::async_trait(?Send)]
impl ApubObject for ApubComment {
type DataType = LemmyContext;
+ type ApubType = Note;
+ type TombstoneType = Tombstone;
fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
None
.await??;
Ok(())
}
-}
-
-#[async_trait::async_trait(?Send)]
-impl ToApub for ApubComment {
- type ApubType = Note;
- type TombstoneType = Tombstone;
- type DataType = DbPool;
- async fn to_apub(&self, pool: &DbPool) -> Result<Note, LemmyError> {
+ async fn to_apub(&self, context: &LemmyContext) -> Result<Note, LemmyError> {
let creator_id = self.creator_id;
- let creator = blocking(pool, move |conn| Person::read(conn, creator_id)).await??;
+ let creator = blocking(context.pool(), move |conn| Person::read(conn, creator_id)).await??;
let post_id = self.post_id;
- let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
+ let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
let in_reply_to = if let Some(comment_id) = self.parent_id {
- let parent_comment = blocking(pool, move |conn| Comment::read(conn, comment_id)).await??;
+ let parent_comment =
+ blocking(context.pool(), move |conn| Comment::read(conn, comment_id)).await??;
ObjectId::<PostOrComment>::new(parent_comment.ap_id.into_inner())
} else {
ObjectId::<PostOrComment>::new(post.ap_id.into_inner())
}
fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
- create_tombstone(
- self.deleted,
- self.ap_id.to_owned().into(),
- self.updated,
+ Ok(Tombstone::new(
NoteType::Note,
- )
+ self.updated.unwrap_or(self.published),
+ ))
}
-}
-
-#[async_trait::async_trait(?Send)]
-impl FromApub for ApubComment {
- type ApubType = Note;
- type DataType = LemmyContext;
/// Converts a `Note` to `Comment`.
///
#[actix_rt::test]
#[serial]
async fn test_parse_lemmy_comment() {
+ // TODO: changed ObjectId::dereference() so that it always fetches if
+ // last_refreshed_at() == None. But post doesnt store that and expects to never be refetched
let context = init_context();
let url = Url::parse("https://enterprise.lemmy.ml/comment/38741").unwrap();
let data = prepare_comment_test(&url, &context).await;
assert!(!comment.local);
assert_eq!(request_counter, 0);
- let to_apub = comment.to_apub(context.pool()).await.unwrap();
+ let to_apub = comment.to_apub(&context).await.unwrap();
assert_json_include!(actual: json, expected: to_apub);
Comment::delete(&*context.pool().get().unwrap(), comment.id).unwrap();
use crate::{
check_is_apub_id_valid,
+ collections::{
+ community_moderators::ApubCommunityModerators,
+ community_outbox::ApubCommunityOutbox,
+ CommunityContext,
+ },
context::lemmy_context,
- fetcher::community::{fetch_community_outbox, update_community_mods},
+ fetcher::object_id::ObjectId,
generate_moderators_url,
generate_outbox_url,
- objects::{create_tombstone, get_summary_from_string_or_source, ImageObject, Source},
+ objects::{get_summary_from_string_or_source, tombstone::Tombstone, ImageObject, Source},
CommunityType,
};
use activitystreams::{
actor::{kind::GroupType, Endpoints},
base::AnyBase,
chrono::NaiveDateTime,
- object::{kind::ImageType, Tombstone},
+ object::kind::ImageType,
primitives::OneOrMany,
unparsed::Unparsed,
};
use lemmy_api_common::blocking;
use lemmy_apub_lib::{
signatures::PublicKey,
- traits::{ActorType, ApubObject, FromApub, ToApub},
+ traits::{ActorType, ApubObject},
values::MediaTypeMarkdown,
verify::verify_domains_match,
};
// lemmy extension
sensitive: Option<bool>,
// lemmy extension
- pub(crate) moderators: Option<Url>,
+ pub(crate) moderators: Option<ObjectId<ApubCommunityModerators>>,
inbox: Url,
- pub(crate) outbox: Url,
+ pub(crate) outbox: ObjectId<ApubCommunityOutbox>,
followers: Url,
endpoints: Endpoints<Url>,
public_key: PublicKey,
#[async_trait::async_trait(?Send)]
impl ApubObject for ApubCommunity {
type DataType = LemmyContext;
+ type ApubType = Group;
+ type TombstoneType = Tombstone;
fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
Some(self.last_refreshed_at)
.await??;
Ok(())
}
-}
-
-impl ActorType for ApubCommunity {
- fn is_local(&self) -> bool {
- self.local
- }
- fn actor_id(&self) -> Url {
- self.actor_id.to_owned().into()
- }
- fn name(&self) -> String {
- self.name.clone()
- }
- fn public_key(&self) -> Option<String> {
- self.public_key.to_owned()
- }
- fn private_key(&self) -> Option<String> {
- self.private_key.to_owned()
- }
- fn inbox_url(&self) -> Url {
- self.inbox_url.clone().into()
- }
-
- fn shared_inbox_url(&self) -> Option<Url> {
- self.shared_inbox_url.clone().map(|s| s.into_inner())
- }
-}
-
-#[async_trait::async_trait(?Send)]
-impl ToApub for ApubCommunity {
- type ApubType = Group;
- type TombstoneType = Tombstone;
- type DataType = DbPool;
-
- async fn to_apub(&self, _pool: &DbPool) -> Result<Group, LemmyError> {
+ async fn to_apub(&self, _context: &LemmyContext) -> Result<Group, LemmyError> {
let source = self.description.clone().map(|bio| Source {
content: bio,
media_type: MediaTypeMarkdown::Markdown,
icon,
image,
sensitive: Some(self.nsfw),
- moderators: Some(generate_moderators_url(&self.actor_id)?.into()),
+ moderators: Some(ObjectId::<ApubCommunityModerators>::new(
+ generate_moderators_url(&self.actor_id)?.into_inner(),
+ )),
inbox: self.inbox_url.clone().into(),
- outbox: generate_outbox_url(&self.actor_id)?.into(),
+ outbox: ObjectId::new(generate_outbox_url(&self.actor_id)?),
followers: self.followers_url.clone().into(),
endpoints: Endpoints {
shared_inbox: self.shared_inbox_url.clone().map(|s| s.into()),
}
fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
- create_tombstone(
- self.deleted,
- self.actor_id.to_owned().into(),
- self.updated,
+ Ok(Tombstone::new(
GroupType::Group,
- )
+ self.updated.unwrap_or(self.published),
+ ))
}
-}
-
-#[async_trait::async_trait(?Send)]
-impl FromApub for ApubCommunity {
- type ApubType = Group;
- type DataType = LemmyContext;
/// Converts a `Group` to `Community`, inserts it into the database and updates moderators.
async fn from_apub(
// Fetching mods and outbox is not necessary for Lemmy to work, so ignore errors. Besides,
// we need to ignore these errors so that tests can work entirely offline.
- let community = blocking(context.pool(), move |conn| Community::upsert(conn, &form)).await??;
- update_community_mods(group, &community, context, request_counter)
+ let community: ApubCommunity =
+ blocking(context.pool(), move |conn| Community::upsert(conn, &form))
+ .await??
+ .into();
+ let outbox_data = CommunityContext(community.clone(), context.clone());
+
+ group
+ .outbox
+ .dereference(&outbox_data, request_counter)
.await
.map_err(|e| debug!("{}", e))
.ok();
- // TODO: doing this unconditionally might cause infinite loop for some reason
- fetch_community_outbox(context, &group.outbox, request_counter)
- .await
- .map_err(|e| debug!("{}", e))
- .ok();
+ if let Some(moderators) = &group.moderators {
+ moderators
+ .dereference(&outbox_data, request_counter)
+ .await
+ .map_err(|e| debug!("{}", e))
+ .ok();
+ }
+
+ Ok(community)
+ }
+}
+
+impl ActorType for ApubCommunity {
+ fn is_local(&self) -> bool {
+ self.local
+ }
+ fn actor_id(&self) -> Url {
+ self.actor_id.to_owned().into()
+ }
+ fn name(&self) -> String {
+ self.name.clone()
+ }
+ fn public_key(&self) -> Option<String> {
+ self.public_key.to_owned()
+ }
+ fn private_key(&self) -> Option<String> {
+ self.private_key.to_owned()
+ }
+
+ fn inbox_url(&self) -> Url {
+ self.inbox_url.clone().into()
+ }
- Ok(community.into())
+ fn shared_inbox_url(&self) -> Option<Url> {
+ self.shared_inbox_url.clone().map(|s| s.into_inner())
}
}
let mut json: Group = file_to_json_object("assets/lemmy-community.json");
let json_orig = json.clone();
// change these links so they dont fetch over the network
- json.moderators =
- Some(Url::parse("https://enterprise.lemmy.ml/c/tenforward/not_moderators").unwrap());
- json.outbox = Url::parse("https://enterprise.lemmy.ml/c/tenforward/not_outbox").unwrap();
+ json.moderators = Some(ObjectId::new(
+ Url::parse("https://enterprise.lemmy.ml/c/tenforward/not_moderators").unwrap(),
+ ));
+ json.outbox =
+ ObjectId::new(Url::parse("https://enterprise.lemmy.ml/c/tenforward/not_outbox").unwrap());
let url = Url::parse("https://enterprise.lemmy.ml/c/tenforward").unwrap();
let mut request_counter = 0;
// this makes two requests to the (intentionally) broken outbox/moderators collections
assert_eq!(request_counter, 2);
- let to_apub = community.to_apub(context.pool()).await.unwrap();
+ let to_apub = community.to_apub(&context).await.unwrap();
assert_json_include!(actual: json_orig, expected: to_apub);
Community::delete(&*context.pool().get().unwrap(), community.id).unwrap();
-use activitystreams::{
- base::BaseExt,
- object::{kind::ImageType, Tombstone, TombstoneExt},
-};
-use anyhow::anyhow;
-use chrono::NaiveDateTime;
+use activitystreams::object::kind::ImageType;
use html2md::parse_html;
use lemmy_apub_lib::values::MediaTypeMarkdown;
-use lemmy_utils::{utils::convert_datetime, LemmyError};
use serde::{Deserialize, Serialize};
use url::Url;
pub mod person;
pub mod post;
pub mod private_message;
+pub mod tombstone;
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
url: Url,
}
-/// Updated is actually the deletion time
-fn create_tombstone<T>(
- deleted: bool,
- object_id: Url,
- updated: Option<NaiveDateTime>,
- former_type: T,
-) -> Result<Tombstone, LemmyError>
-where
- T: ToString,
-{
- if deleted {
- if let Some(updated) = updated {
- let mut tombstone = Tombstone::new();
- tombstone.set_id(object_id);
- tombstone.set_former_type(former_type.to_string());
- tombstone.set_deleted(convert_datetime(updated));
- Ok(tombstone)
- } else {
- Err(anyhow!("Cant convert to tombstone because updated time was None.").into())
- }
- } else {
- Err(anyhow!("Cant convert object to tombstone if it wasnt deleted").into())
- }
-}
-
fn get_summary_from_string_or_source(
raw: &Option<String>,
source: &Option<Source>,
#[cfg(test)]
mod tests {
- use super::*;
use actix::Actor;
use diesel::{
r2d2::{ConnectionManager, Pool},
rate_limit::{rate_limiter::RateLimiter, RateLimit},
request::build_user_agent,
settings::structs::Settings,
+ LemmyError,
};
use lemmy_websocket::{chat_server::ChatServer, LemmyContext};
use reqwest::Client;
use lemmy_api_common::blocking;
use lemmy_apub_lib::{
signatures::PublicKey,
- traits::{ActorType, ApubObject, FromApub, ToApub},
+ traits::{ActorType, ApubObject},
values::MediaTypeMarkdown,
verify::verify_domains_match,
};
use lemmy_db_schema::{
naive_now,
source::person::{Person as DbPerson, PersonForm},
- DbPool,
};
use lemmy_utils::{
utils::{check_slurs, check_slurs_opt, convert_datetime, markdown_to_html},
}
}
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, PartialEq)]
pub struct ApubPerson(DbPerson);
impl Deref for ApubPerson {
#[async_trait::async_trait(?Send)]
impl ApubObject for ApubPerson {
type DataType = LemmyContext;
+ type ApubType = Person;
+ type TombstoneType = Tombstone;
fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
Some(self.last_refreshed_at)
.await??;
Ok(())
}
-}
-
-impl ActorType for ApubPerson {
- fn is_local(&self) -> bool {
- self.local
- }
- fn actor_id(&self) -> Url {
- self.actor_id.to_owned().into_inner()
- }
- fn name(&self) -> String {
- self.name.clone()
- }
-
- fn public_key(&self) -> Option<String> {
- self.public_key.to_owned()
- }
-
- fn private_key(&self) -> Option<String> {
- self.private_key.to_owned()
- }
-
- fn inbox_url(&self) -> Url {
- self.inbox_url.clone().into()
- }
-
- fn shared_inbox_url(&self) -> Option<Url> {
- self.shared_inbox_url.clone().map(|s| s.into_inner())
- }
-}
-
-#[async_trait::async_trait(?Send)]
-impl ToApub for ApubPerson {
- type ApubType = Person;
- type TombstoneType = Tombstone;
- type DataType = DbPool;
- async fn to_apub(&self, _pool: &DbPool) -> Result<Person, LemmyError> {
+ async fn to_apub(&self, _pool: &LemmyContext) -> Result<Person, LemmyError> {
let kind = if self.bot_account {
UserTypes::Service
} else {
};
Ok(person)
}
+
fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
unimplemented!()
}
-}
-
-#[async_trait::async_trait(?Send)]
-impl FromApub for ApubPerson {
- type ApubType = Person;
- type DataType = LemmyContext;
async fn from_apub(
person: &Person,
}
}
+impl ActorType for ApubPerson {
+ fn is_local(&self) -> bool {
+ self.local
+ }
+ fn actor_id(&self) -> Url {
+ self.actor_id.to_owned().into_inner()
+ }
+ fn name(&self) -> String {
+ self.name.clone()
+ }
+
+ fn public_key(&self) -> Option<String> {
+ self.public_key.to_owned()
+ }
+
+ fn private_key(&self) -> Option<String> {
+ self.private_key.to_owned()
+ }
+
+ fn inbox_url(&self) -> Url {
+ self.inbox_url.clone().into()
+ }
+
+ fn shared_inbox_url(&self) -> Option<Url> {
+ self.shared_inbox_url.clone().map(|s| s.into_inner())
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
assert_eq!(person.bio.as_ref().unwrap().len(), 39);
assert_eq!(request_counter, 0);
- let to_apub = person.to_apub(context.pool()).await.unwrap();
+ let to_apub = person.to_apub(&context).await.unwrap();
assert_json_include!(actual: json, expected: to_apub);
DbPerson::delete(&*context.pool().get().unwrap(), person.id).unwrap();
activities::{extract_community, verify_is_public, verify_person_in_community},
context::lemmy_context,
fetcher::object_id::ObjectId,
- objects::{create_tombstone, person::ApubPerson, ImageObject, Source},
+ objects::{person::ApubPerson, tombstone::Tombstone, ImageObject, Source},
};
use activitystreams::{
base::AnyBase,
- object::{
- kind::{ImageType, PageType},
- Tombstone,
- },
+ object::kind::{ImageType, PageType},
primitives::OneOrMany,
public,
unparsed::Unparsed,
use chrono::{DateTime, FixedOffset, NaiveDateTime};
use lemmy_api_common::blocking;
use lemmy_apub_lib::{
- traits::{ActorType, ApubObject, FromApub, ToApub},
+ traits::{ActorType, ApubObject},
values::{MediaTypeHtml, MediaTypeMarkdown},
verify::verify_domains_match,
};
post::{Post, PostForm},
},
traits::Crud,
- DbPool,
};
use lemmy_utils::{
request::fetch_site_data,
#[async_trait::async_trait(?Send)]
impl ApubObject for ApubPost {
type DataType = LemmyContext;
+ type ApubType = Page;
+ type TombstoneType = Tombstone;
fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
None
.await??;
Ok(())
}
-}
-
-#[async_trait::async_trait(?Send)]
-impl ToApub for ApubPost {
- type ApubType = Page;
- type TombstoneType = Tombstone;
- type DataType = DbPool;
// Turn a Lemmy post into an ActivityPub page that can be sent out over the network.
- async fn to_apub(&self, pool: &DbPool) -> Result<Page, LemmyError> {
+ async fn to_apub(&self, context: &LemmyContext) -> Result<Page, LemmyError> {
let creator_id = self.creator_id;
- let creator = blocking(pool, move |conn| Person::read(conn, creator_id)).await??;
+ let creator = blocking(context.pool(), move |conn| Person::read(conn, creator_id)).await??;
let community_id = self.community_id;
- let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
+ let community = blocking(context.pool(), move |conn| {
+ Community::read(conn, community_id)
+ })
+ .await??;
let source = self.body.clone().map(|body| Source {
content: body,
}
fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
- create_tombstone(
- self.deleted,
- self.ap_id.to_owned().into(),
- self.updated,
+ Ok(Tombstone::new(
PageType::Page,
- )
+ self.updated.unwrap_or(self.published),
+ ))
}
-}
-
-#[async_trait::async_trait(?Send)]
-impl FromApub for ApubPost {
- type ApubType = Page;
- type DataType = LemmyContext;
async fn from_apub(
page: &Page,
assert!(post.stickied);
assert_eq!(request_counter, 0);
- let to_apub = post.to_apub(context.pool()).await.unwrap();
+ let to_apub = post.to_apub(&context).await.unwrap();
assert_json_include!(actual: json, expected: to_apub);
Post::delete(&*context.pool().get().unwrap(), post.id).unwrap();
use crate::{
context::lemmy_context,
fetcher::object_id::ObjectId,
- objects::{create_tombstone, person::ApubPerson, Source},
+ objects::{person::ApubPerson, Source},
};
use activitystreams::{
base::AnyBase,
chrono::NaiveDateTime,
- object::{kind::NoteType, Tombstone},
+ object::Tombstone,
primitives::OneOrMany,
unparsed::Unparsed,
};
use html2md::parse_html;
use lemmy_api_common::blocking;
use lemmy_apub_lib::{
- traits::{ApubObject, FromApub, ToApub},
+ traits::ApubObject,
values::{MediaTypeHtml, MediaTypeMarkdown},
verify::verify_domains_match,
};
private_message::{PrivateMessage, PrivateMessageForm},
},
traits::Crud,
- DbPool,
};
use lemmy_utils::{utils::convert_datetime, LemmyError};
use lemmy_websocket::LemmyContext;
#[async_trait::async_trait(?Send)]
impl ApubObject for ApubPrivateMessage {
type DataType = LemmyContext;
+ type ApubType = Note;
+ type TombstoneType = Tombstone;
fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
None
// do nothing, because pm can't be fetched over http
unimplemented!()
}
-}
-
-#[async_trait::async_trait(?Send)]
-impl ToApub for ApubPrivateMessage {
- type ApubType = Note;
- type TombstoneType = Tombstone;
- type DataType = DbPool;
- async fn to_apub(&self, pool: &DbPool) -> Result<Note, LemmyError> {
+ async fn to_apub(&self, context: &LemmyContext) -> Result<Note, LemmyError> {
let creator_id = self.creator_id;
- let creator = blocking(pool, move |conn| Person::read(conn, creator_id)).await??;
+ let creator = blocking(context.pool(), move |conn| Person::read(conn, creator_id)).await??;
let recipient_id = self.recipient_id;
- let recipient = blocking(pool, move |conn| Person::read(conn, recipient_id)).await??;
+ let recipient =
+ blocking(context.pool(), move |conn| Person::read(conn, recipient_id)).await??;
let note = Note {
context: lemmy_context(),
}
fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
- create_tombstone(
- self.deleted,
- self.ap_id.to_owned().into(),
- self.updated,
- NoteType::Note,
- )
+ unimplemented!()
}
-}
-
-#[async_trait::async_trait(?Send)]
-impl FromApub for ApubPrivateMessage {
- type ApubType = Note;
- type DataType = LemmyContext;
async fn from_apub(
note: &Note,
assert_eq!(pm.content.len(), 20);
assert_eq!(request_counter, 0);
- let to_apub = pm.to_apub(context.pool()).await.unwrap();
+ let to_apub = pm.to_apub(&context).await.unwrap();
assert_json_include!(actual: json, expected: to_apub);
PrivateMessage::delete(&*context.pool().get().unwrap(), pm.id).unwrap();
--- /dev/null
+use crate::context::lemmy_context;
+use activitystreams::{
+ base::AnyBase,
+ chrono::{DateTime, FixedOffset, NaiveDateTime},
+ object::kind::TombstoneType,
+ primitives::OneOrMany,
+};
+use lemmy_utils::utils::convert_datetime;
+use serde::{Deserialize, Serialize};
+use serde_with::skip_serializing_none;
+
+#[skip_serializing_none]
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Tombstone {
+ #[serde(rename = "@context")]
+ context: OneOrMany<AnyBase>,
+ #[serde(rename = "type")]
+ kind: TombstoneType,
+ former_type: String,
+ deleted: DateTime<FixedOffset>,
+}
+
+impl Tombstone {
+ pub fn new<T: ToString>(former_type: T, updated_time: NaiveDateTime) -> Tombstone {
+ Tombstone {
+ context: lemmy_context(),
+ kind: TombstoneType::Tombstone,
+ former_type: former_type.to_string(),
+ deleted: convert_datetime(updated_time),
+ }
+ }
+}
#[async_trait::async_trait(?Send)]
pub trait ApubObject {
type DataType;
+ type ApubType;
+ type TombstoneType;
+
/// If this object should be refetched after a certain interval, it should return the last refresh
/// time here. This is mainly used to update remote actors.
fn last_refreshed_at(&self) -> Option<NaiveDateTime>;
Self: Sized;
/// Marks the object as deleted in local db. Called when a tombstone is received.
async fn delete(self, data: &Self::DataType) -> Result<(), LemmyError>;
+
+ /// Trait for converting an object or actor into the respective ActivityPub type.
+ async fn to_apub(&self, data: &Self::DataType) -> Result<Self::ApubType, LemmyError>;
+ fn to_tombstone(&self) -> Result<Self::TombstoneType, LemmyError>;
+
+ /// Converts an object from ActivityPub type to Lemmy internal type.
+ ///
+ /// * `apub` The object to read from
+ /// * `context` LemmyContext which holds DB pool, HTTP client etc
+ /// * `expected_domain` Domain where the object was received from. None in case of mod action.
+ /// * `mod_action_allowed` True if the object can be a mod activity, ignore `expected_domain` in this case
+ async fn from_apub(
+ apub: &Self::ApubType,
+ data: &Self::DataType,
+ expected_domain: &Url,
+ request_counter: &mut i32,
+ ) -> Result<Self, LemmyError>
+ where
+ Self: Sized;
}
/// Common methods provided by ActivityPub actors (community and person). Not all methods are
})
}
}
-
-/// Trait for converting an object or actor into the respective ActivityPub type.
-#[async_trait::async_trait(?Send)]
-pub trait ToApub {
- type ApubType;
- type TombstoneType;
- type DataType;
-
- async fn to_apub(&self, data: &Self::DataType) -> Result<Self::ApubType, LemmyError>;
- fn to_tombstone(&self) -> Result<Self::TombstoneType, LemmyError>;
-}
-
-#[async_trait::async_trait(?Send)]
-pub trait FromApub {
- type ApubType;
- type DataType;
-
- /// Converts an object from ActivityPub type to Lemmy internal type.
- ///
- /// * `apub` The object to read from
- /// * `context` LemmyContext which holds DB pool, HTTP client etc
- /// * `expected_domain` Domain where the object was received from. None in case of mod action.
- /// * `mod_action_allowed` True if the object can be a mod activity, ignore `expected_domain` in this case
- async fn from_apub(
- apub: &Self::ApubType,
- data: &Self::DataType,
- expected_domain: &Url,
- request_counter: &mut i32,
- ) -> Result<Self, LemmyError>
- where
- Self: Sized;
-}
community.select(actor_id).distinct().load::<String>(conn)
}
- pub fn read_from_followers_url(
- conn: &PgConnection,
- followers_url_: &DbUrl,
- ) -> Result<Community, Error> {
- use crate::schema::community::dsl::*;
- community
- .filter(followers_url.eq(followers_url_))
- .first::<Self>(conn)
- }
-
pub fn upsert(conn: &PgConnection, community_form: &CommunityForm) -> Result<Community, Error> {
use crate::schema::community::dsl::*;
insert_into(community)