mirror of
https://github.com/LemmyNet/lemmy.git
synced 2024-11-26 19:31:05 +00:00
Rewrite collections to use new fetcher (#1861)
* Merge traits ToApub and FromApub into ApubObject * Rewrite community outbox to use new fetcher * Rewrite community moderators collection * Rewrite tombstone
This commit is contained in:
parent
d9ecabee87
commit
61189efe72
31 changed files with 655 additions and 668 deletions
|
@ -1,7 +1,7 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
cargo +nightly fmt -- --check
|
cargo +nightly fmt
|
||||||
|
|
||||||
cargo +nightly clippy --workspace --tests --all-targets --all-features -- \
|
cargo +nightly clippy --workspace --tests --all-targets --all-features -- \
|
||||||
-D warnings -D deprecated -D clippy::perf -D clippy::complexity -D clippy::dbg_macro
|
-D warnings -D deprecated -D clippy::perf -D clippy::complexity -D clippy::dbg_macro
|
||||||
|
|
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1833,6 +1833,7 @@ dependencies = [
|
||||||
"http",
|
"http",
|
||||||
"http-signature-normalization-actix",
|
"http-signature-normalization-actix",
|
||||||
"itertools",
|
"itertools",
|
||||||
|
"lazy_static",
|
||||||
"lemmy_api_common",
|
"lemmy_api_common",
|
||||||
"lemmy_apub_lib",
|
"lemmy_apub_lib",
|
||||||
"lemmy_db_schema",
|
"lemmy_db_schema",
|
||||||
|
|
|
@ -50,6 +50,7 @@ thiserror = "1.0.29"
|
||||||
background-jobs = "0.9.0"
|
background-jobs = "0.9.0"
|
||||||
reqwest = { version = "0.11.4", features = ["json"] }
|
reqwest = { version = "0.11.4", features = ["json"] }
|
||||||
html2md = "0.2.13"
|
html2md = "0.2.13"
|
||||||
|
lazy_static = "1.4.0"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
serial_test = "0.5.1"
|
serial_test = "0.5.1"
|
||||||
|
|
|
@ -21,7 +21,7 @@ use activitystreams::{base::AnyBase, link::Mention, primitives::OneOrMany, unpar
|
||||||
use lemmy_api_common::{blocking, check_post_deleted_or_removed};
|
use lemmy_api_common::{blocking, check_post_deleted_or_removed};
|
||||||
use lemmy_apub_lib::{
|
use lemmy_apub_lib::{
|
||||||
data::Data,
|
data::Data,
|
||||||
traits::{ActivityFields, ActivityHandler, ActorType, FromApub, ToApub},
|
traits::{ActivityFields, ActivityHandler, ActorType, ApubObject},
|
||||||
values::PublicUrl,
|
values::PublicUrl,
|
||||||
verify::verify_domains_match,
|
verify::verify_domains_match,
|
||||||
};
|
};
|
||||||
|
@ -77,7 +77,7 @@ impl CreateOrUpdateComment {
|
||||||
let create_or_update = CreateOrUpdateComment {
|
let create_or_update = CreateOrUpdateComment {
|
||||||
actor: ObjectId::new(actor.actor_id()),
|
actor: ObjectId::new(actor.actor_id()),
|
||||||
to: [PublicUrl::Public],
|
to: [PublicUrl::Public],
|
||||||
object: comment.to_apub(context.pool()).await?,
|
object: comment.to_apub(context).await?,
|
||||||
cc: maa.ccs,
|
cc: maa.ccs,
|
||||||
tag: maa.tags,
|
tag: maa.tags,
|
||||||
kind,
|
kind,
|
||||||
|
|
|
@ -22,7 +22,7 @@ use activitystreams::{
|
||||||
use lemmy_api_common::blocking;
|
use lemmy_api_common::blocking;
|
||||||
use lemmy_apub_lib::{
|
use lemmy_apub_lib::{
|
||||||
data::Data,
|
data::Data,
|
||||||
traits::{ActivityFields, ActivityHandler, ActorType, ToApub},
|
traits::{ActivityFields, ActivityHandler, ActorType, ApubObject},
|
||||||
values::PublicUrl,
|
values::PublicUrl,
|
||||||
};
|
};
|
||||||
use lemmy_db_schema::{
|
use lemmy_db_schema::{
|
||||||
|
@ -66,7 +66,7 @@ impl UpdateCommunity {
|
||||||
let update = UpdateCommunity {
|
let update = UpdateCommunity {
|
||||||
actor: ObjectId::new(actor.actor_id()),
|
actor: ObjectId::new(actor.actor_id()),
|
||||||
to: [PublicUrl::Public],
|
to: [PublicUrl::Public],
|
||||||
object: community.to_apub(context.pool()).await?,
|
object: community.to_apub(context).await?,
|
||||||
cc: [ObjectId::new(community.actor_id())],
|
cc: [ObjectId::new(community.actor_id())],
|
||||||
kind: UpdateType::Update,
|
kind: UpdateType::Update,
|
||||||
id: id.clone(),
|
id: id.clone(),
|
||||||
|
|
|
@ -21,7 +21,7 @@ use anyhow::anyhow;
|
||||||
use lemmy_api_common::blocking;
|
use lemmy_api_common::blocking;
|
||||||
use lemmy_apub_lib::{
|
use lemmy_apub_lib::{
|
||||||
data::Data,
|
data::Data,
|
||||||
traits::{ActivityFields, ActivityHandler, ActorType, FromApub, ToApub},
|
traits::{ActivityFields, ActivityHandler, ActorType, ApubObject},
|
||||||
values::PublicUrl,
|
values::PublicUrl,
|
||||||
verify::{verify_domains_match, verify_urls_match},
|
verify::{verify_domains_match, verify_urls_match},
|
||||||
};
|
};
|
||||||
|
@ -48,6 +48,28 @@ pub struct CreateOrUpdatePost {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CreateOrUpdatePost {
|
impl CreateOrUpdatePost {
|
||||||
|
pub(crate) async fn new(
|
||||||
|
post: &ApubPost,
|
||||||
|
actor: &ApubPerson,
|
||||||
|
community: &ApubCommunity,
|
||||||
|
kind: CreateOrUpdateType,
|
||||||
|
context: &LemmyContext,
|
||||||
|
) -> Result<CreateOrUpdatePost, LemmyError> {
|
||||||
|
let id = generate_activity_id(
|
||||||
|
kind.clone(),
|
||||||
|
&context.settings().get_protocol_and_hostname(),
|
||||||
|
)?;
|
||||||
|
Ok(CreateOrUpdatePost {
|
||||||
|
actor: ObjectId::new(actor.actor_id()),
|
||||||
|
to: [PublicUrl::Public],
|
||||||
|
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(
|
pub async fn send(
|
||||||
post: &ApubPost,
|
post: &ApubPost,
|
||||||
actor: &ApubPerson,
|
actor: &ApubPerson,
|
||||||
|
@ -60,22 +82,8 @@ impl CreateOrUpdatePost {
|
||||||
})
|
})
|
||||||
.await??
|
.await??
|
||||||
.into();
|
.into();
|
||||||
|
let create_or_update = CreateOrUpdatePost::new(post, actor, &community, kind, context).await?;
|
||||||
let id = generate_activity_id(
|
let id = create_or_update.id.clone();
|
||||||
kind.clone(),
|
|
||||||
&context.settings().get_protocol_and_hostname(),
|
|
||||||
)?;
|
|
||||||
let create_or_update = CreateOrUpdatePost {
|
|
||||||
actor: ObjectId::new(actor.actor_id()),
|
|
||||||
to: [PublicUrl::Public],
|
|
||||||
object: post.to_apub(context.pool()).await?,
|
|
||||||
cc: [ObjectId::new(community.actor_id())],
|
|
||||||
kind,
|
|
||||||
id: id.clone(),
|
|
||||||
context: lemmy_context(),
|
|
||||||
unparsed: Default::default(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let activity = AnnouncableActivities::CreateOrUpdatePost(Box::new(create_or_update));
|
let activity = AnnouncableActivities::CreateOrUpdatePost(Box::new(create_or_update));
|
||||||
send_to_community(activity, &id, actor, &community, vec![], context).await
|
send_to_community(activity, &id, actor, &community, vec![], context).await
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ use activitystreams::{base::AnyBase, primitives::OneOrMany, unparsed::Unparsed};
|
||||||
use lemmy_api_common::blocking;
|
use lemmy_api_common::blocking;
|
||||||
use lemmy_apub_lib::{
|
use lemmy_apub_lib::{
|
||||||
data::Data,
|
data::Data,
|
||||||
traits::{ActivityFields, ActivityHandler, ActorType, FromApub, ToApub},
|
traits::{ActivityFields, ActivityHandler, ActorType, ApubObject},
|
||||||
verify::verify_domains_match,
|
verify::verify_domains_match,
|
||||||
};
|
};
|
||||||
use lemmy_db_schema::{source::person::Person, traits::Crud};
|
use lemmy_db_schema::{source::person::Person, traits::Crud};
|
||||||
|
@ -58,7 +58,7 @@ impl CreateOrUpdatePrivateMessage {
|
||||||
id: id.clone(),
|
id: id.clone(),
|
||||||
actor: ObjectId::new(actor.actor_id()),
|
actor: ObjectId::new(actor.actor_id()),
|
||||||
to: ObjectId::new(recipient.actor_id()),
|
to: ObjectId::new(recipient.actor_id()),
|
||||||
object: private_message.to_apub(context.pool()).await?,
|
object: private_message.to_apub(context).await?,
|
||||||
kind,
|
kind,
|
||||||
unparsed: Default::default(),
|
unparsed: Default::default(),
|
||||||
};
|
};
|
||||||
|
|
141
crates/apub/src/collections/community_moderators.rs
Normal file
141
crates/apub/src/collections/community_moderators.rs
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
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![] })
|
||||||
|
}
|
||||||
|
}
|
128
crates/apub/src/collections/community_outbox.rs
Normal file
128
crates/apub/src/collections/community_outbox.rs
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
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![] })
|
||||||
|
}
|
||||||
|
}
|
7
crates/apub/src/collections/mod.rs
Normal file
7
crates/apub/src/collections/mod.rs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
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);
|
|
@ -1,144 +0,0 @@
|
||||||
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![])
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,49 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
|
@ -1,5 +1,3 @@
|
||||||
pub mod community;
|
|
||||||
mod fetch;
|
|
||||||
pub mod object_id;
|
pub mod object_id;
|
||||||
pub mod post_or_comment;
|
pub mod post_or_comment;
|
||||||
pub mod search;
|
pub mod search;
|
||||||
|
@ -47,7 +45,7 @@ pub(crate) async fn get_or_fetch_and_upsert_actor(
|
||||||
///
|
///
|
||||||
/// TODO it won't pick up new avatars, summaries etc until a day after.
|
/// 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.
|
/// 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) {
|
let update_interval = if cfg!(debug_assertions) {
|
||||||
// avoid infinite loop when fetching community outbox
|
// avoid infinite loop when fetching community outbox
|
||||||
chrono::Duration::seconds(ACTOR_REFETCH_INTERVAL_SECONDS_DEBUG)
|
chrono::Duration::seconds(ACTOR_REFETCH_INTERVAL_SECONDS_DEBUG)
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
use crate::fetcher::should_refetch_actor;
|
use crate::fetcher::should_refetch_object;
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use diesel::NotFound;
|
use diesel::NotFound;
|
||||||
use lemmy_apub_lib::{
|
use lemmy_apub_lib::{traits::ApubObject, APUB_JSON_CONTENT_TYPE};
|
||||||
traits::{ApubObject, FromApub},
|
|
||||||
APUB_JSON_CONTENT_TYPE,
|
|
||||||
};
|
|
||||||
use lemmy_db_schema::newtypes::DbUrl;
|
use lemmy_db_schema::newtypes::DbUrl;
|
||||||
use lemmy_utils::{request::retry, settings::structs::Settings, LemmyError};
|
use lemmy_utils::{
|
||||||
use lemmy_websocket::LemmyContext;
|
request::{build_user_agent, retry},
|
||||||
use reqwest::StatusCode;
|
settings::structs::Settings,
|
||||||
|
LemmyError,
|
||||||
|
};
|
||||||
|
use log::info;
|
||||||
|
use reqwest::{Client, StatusCode};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
fmt::{Debug, Display, Formatter},
|
fmt::{Debug, Display, Formatter},
|
||||||
|
@ -21,17 +22,25 @@ use url::Url;
|
||||||
/// fetch through the search). This should be configurable.
|
/// fetch through the search). This should be configurable.
|
||||||
static REQUEST_LIMIT: i32 = 25;
|
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)]
|
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
|
||||||
#[serde(transparent)]
|
#[serde(transparent)]
|
||||||
pub struct ObjectId<Kind>(Url, #[serde(skip)] PhantomData<Kind>)
|
pub struct ObjectId<Kind>(Url, #[serde(skip)] PhantomData<Kind>)
|
||||||
where
|
where
|
||||||
Kind: FromApub<DataType = LemmyContext> + ApubObject<DataType = LemmyContext> + Send + 'static,
|
Kind: ApubObject + Send + 'static,
|
||||||
for<'de2> <Kind as FromApub>::ApubType: serde::Deserialize<'de2>;
|
for<'de2> <Kind as ApubObject>::ApubType: serde::Deserialize<'de2>;
|
||||||
|
|
||||||
impl<Kind> ObjectId<Kind>
|
impl<Kind> ObjectId<Kind>
|
||||||
where
|
where
|
||||||
Kind: FromApub<DataType = LemmyContext> + ApubObject<DataType = LemmyContext> + Send + 'static,
|
Kind: ApubObject + Send + 'static,
|
||||||
for<'de> <Kind as FromApub>::ApubType: serde::Deserialize<'de>,
|
for<'de2> <Kind as ApubObject>::ApubType: serde::Deserialize<'de2>,
|
||||||
{
|
{
|
||||||
pub fn new<T>(url: T) -> Self
|
pub fn new<T>(url: T) -> Self
|
||||||
where
|
where
|
||||||
|
@ -47,10 +56,10 @@ where
|
||||||
/// Fetches an activitypub object, either from local database (if possible), or over http.
|
/// Fetches an activitypub object, either from local database (if possible), or over http.
|
||||||
pub async fn dereference(
|
pub async fn dereference(
|
||||||
&self,
|
&self,
|
||||||
context: &LemmyContext,
|
data: &<Kind as ApubObject>::DataType,
|
||||||
request_counter: &mut i32,
|
request_counter: &mut i32,
|
||||||
) -> Result<Kind, LemmyError> {
|
) -> 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 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()?) {
|
if self.0.domain() == Some(&Settings::get().get_hostname_without_port()?) {
|
||||||
|
@ -60,44 +69,54 @@ where
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// object found in database
|
||||||
if let Some(object) = db_object {
|
if let Some(object) = db_object {
|
||||||
|
// object is old and should be refetched
|
||||||
if let Some(last_refreshed_at) = object.last_refreshed_at() {
|
if let Some(last_refreshed_at) = object.last_refreshed_at() {
|
||||||
// TODO: rename to should_refetch_object()
|
if should_refetch_object(last_refreshed_at) {
|
||||||
if should_refetch_actor(last_refreshed_at) {
|
|
||||||
return self
|
return self
|
||||||
.dereference_from_http(context, request_counter, Some(object))
|
.dereference_from_http(data, request_counter, Some(object))
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(object)
|
Ok(object)
|
||||||
} else {
|
}
|
||||||
|
// object not found, need to fetch over http
|
||||||
|
else {
|
||||||
self
|
self
|
||||||
.dereference_from_http(context, request_counter, None)
|
.dereference_from_http(data, request_counter, None)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch an object from the local db. Instead of falling back to http, this throws an error if
|
/// 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.
|
/// the object is not found in the database.
|
||||||
pub async fn dereference_local(&self, context: &LemmyContext) -> Result<Kind, LemmyError> {
|
pub async fn dereference_local(
|
||||||
let object = self.dereference_from_db(context).await?;
|
&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())
|
object.ok_or_else(|| anyhow!("object not found in database {}", self).into())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// returning none means the object was not found in local db
|
/// 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();
|
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(
|
async fn dereference_from_http(
|
||||||
&self,
|
&self,
|
||||||
context: &LemmyContext,
|
data: &<Kind as ApubObject>::DataType,
|
||||||
request_counter: &mut i32,
|
request_counter: &mut i32,
|
||||||
db_object: Option<Kind>,
|
db_object: Option<Kind>,
|
||||||
) -> Result<Kind, LemmyError> {
|
) -> Result<Kind, LemmyError> {
|
||||||
// dont fetch local objects this way
|
// dont fetch local objects this way
|
||||||
debug_assert!(self.0.domain() != Some(&Settings::get().hostname));
|
debug_assert!(self.0.domain() != Some(&Settings::get().hostname));
|
||||||
|
info!("Fetching remote object {}", self.to_string());
|
||||||
|
|
||||||
*request_counter += 1;
|
*request_counter += 1;
|
||||||
if *request_counter > REQUEST_LIMIT {
|
if *request_counter > REQUEST_LIMIT {
|
||||||
|
@ -105,8 +124,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
let res = retry(|| {
|
let res = retry(|| {
|
||||||
context
|
CLIENT
|
||||||
.client()
|
|
||||||
.get(self.0.as_str())
|
.get(self.0.as_str())
|
||||||
.header("Accept", APUB_JSON_CONTENT_TYPE)
|
.header("Accept", APUB_JSON_CONTENT_TYPE)
|
||||||
.timeout(Duration::from_secs(60))
|
.timeout(Duration::from_secs(60))
|
||||||
|
@ -116,21 +134,21 @@ where
|
||||||
|
|
||||||
if res.status() == StatusCode::GONE {
|
if res.status() == StatusCode::GONE {
|
||||||
if let Some(db_object) = db_object {
|
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());
|
return Err(anyhow!("Fetched remote object {} which was deleted", self).into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let res2: Kind::ApubType = res.json().await?;
|
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>
|
impl<Kind> Display for ObjectId<Kind>
|
||||||
where
|
where
|
||||||
Kind: FromApub<DataType = LemmyContext> + ApubObject<DataType = LemmyContext> + Send + 'static,
|
Kind: ApubObject + Send + 'static,
|
||||||
for<'de> <Kind as FromApub>::ApubType: serde::Deserialize<'de>,
|
for<'de2> <Kind as ApubObject>::ApubType: serde::Deserialize<'de2>,
|
||||||
{
|
{
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
write!(f, "{}", self.0.to_string())
|
write!(f, "{}", self.0.to_string())
|
||||||
|
@ -139,8 +157,8 @@ where
|
||||||
|
|
||||||
impl<Kind> From<ObjectId<Kind>> for Url
|
impl<Kind> From<ObjectId<Kind>> for Url
|
||||||
where
|
where
|
||||||
Kind: FromApub<DataType = LemmyContext> + ApubObject<DataType = LemmyContext> + Send + 'static,
|
Kind: ApubObject + Send + 'static,
|
||||||
for<'de> <Kind as FromApub>::ApubType: serde::Deserialize<'de>,
|
for<'de2> <Kind as ApubObject>::ApubType: serde::Deserialize<'de2>,
|
||||||
{
|
{
|
||||||
fn from(id: ObjectId<Kind>) -> Self {
|
fn from(id: ObjectId<Kind>) -> Self {
|
||||||
id.0
|
id.0
|
||||||
|
@ -149,8 +167,8 @@ where
|
||||||
|
|
||||||
impl<Kind> From<ObjectId<Kind>> for DbUrl
|
impl<Kind> From<ObjectId<Kind>> for DbUrl
|
||||||
where
|
where
|
||||||
Kind: FromApub<DataType = LemmyContext> + ApubObject<DataType = LemmyContext> + Send + 'static,
|
Kind: ApubObject + Send + 'static,
|
||||||
for<'de> <Kind as FromApub>::ApubType: serde::Deserialize<'de>,
|
for<'de2> <Kind as ApubObject>::ApubType: serde::Deserialize<'de2>,
|
||||||
{
|
{
|
||||||
fn from(id: ObjectId<Kind>) -> Self {
|
fn from(id: ObjectId<Kind>) -> Self {
|
||||||
id.0.into()
|
id.0.into()
|
||||||
|
|
|
@ -3,7 +3,7 @@ use crate::objects::{
|
||||||
post::{ApubPost, Page},
|
post::{ApubPost, Page},
|
||||||
};
|
};
|
||||||
use activitystreams::chrono::NaiveDateTime;
|
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_db_schema::source::{comment::CommentForm, post::PostForm};
|
||||||
use lemmy_utils::LemmyError;
|
use lemmy_utils::LemmyError;
|
||||||
use lemmy_websocket::LemmyContext;
|
use lemmy_websocket::LemmyContext;
|
||||||
|
@ -31,6 +31,8 @@ pub enum PageOrNote {
|
||||||
#[async_trait::async_trait(?Send)]
|
#[async_trait::async_trait(?Send)]
|
||||||
impl ApubObject for PostOrComment {
|
impl ApubObject for PostOrComment {
|
||||||
type DataType = LemmyContext;
|
type DataType = LemmyContext;
|
||||||
|
type ApubType = PageOrNote;
|
||||||
|
type TombstoneType = ();
|
||||||
|
|
||||||
fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
|
fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
|
||||||
None
|
None
|
||||||
|
@ -59,12 +61,14 @@ impl ApubObject for PostOrComment {
|
||||||
PostOrComment::Comment(c) => c.delete(data).await,
|
PostOrComment::Comment(c) => c.delete(data).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
async fn to_apub(&self, _data: &Self::DataType) -> Result<Self::ApubType, LemmyError> {
|
||||||
impl FromApub for PostOrComment {
|
unimplemented!()
|
||||||
type ApubType = PageOrNote;
|
}
|
||||||
type DataType = LemmyContext;
|
|
||||||
|
fn to_tombstone(&self) -> Result<Self::TombstoneType, LemmyError> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
async fn from_apub(
|
async fn from_apub(
|
||||||
apub: &PageOrNote,
|
apub: &PageOrNote,
|
||||||
|
|
|
@ -12,7 +12,7 @@ use anyhow::anyhow;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use lemmy_api_common::blocking;
|
use lemmy_api_common::blocking;
|
||||||
use lemmy_apub_lib::{
|
use lemmy_apub_lib::{
|
||||||
traits::{ApubObject, FromApub},
|
traits::ApubObject,
|
||||||
webfinger::{webfinger_resolve_actor, WebfingerType},
|
webfinger::{webfinger_resolve_actor, WebfingerType},
|
||||||
};
|
};
|
||||||
use lemmy_db_schema::{
|
use lemmy_db_schema::{
|
||||||
|
@ -110,6 +110,8 @@ pub enum SearchableApubTypes {
|
||||||
#[async_trait::async_trait(?Send)]
|
#[async_trait::async_trait(?Send)]
|
||||||
impl ApubObject for SearchableObjects {
|
impl ApubObject for SearchableObjects {
|
||||||
type DataType = LemmyContext;
|
type DataType = LemmyContext;
|
||||||
|
type ApubType = SearchableApubTypes;
|
||||||
|
type TombstoneType = ();
|
||||||
|
|
||||||
fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
|
fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
|
||||||
match self {
|
match self {
|
||||||
|
@ -156,12 +158,14 @@ impl ApubObject for SearchableObjects {
|
||||||
SearchableObjects::Comment(c) => c.delete(data).await,
|
SearchableObjects::Comment(c) => c.delete(data).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
async fn to_apub(&self, _data: &Self::DataType) -> Result<Self::ApubType, LemmyError> {
|
||||||
impl FromApub for SearchableObjects {
|
unimplemented!()
|
||||||
type ApubType = SearchableApubTypes;
|
}
|
||||||
type DataType = LemmyContext;
|
|
||||||
|
fn to_tombstone(&self) -> Result<Self::TombstoneType, LemmyError> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
async fn from_apub(
|
async fn from_apub(
|
||||||
apub: &Self::ApubType,
|
apub: &Self::ApubType,
|
||||||
|
|
|
@ -5,7 +5,7 @@ use crate::{
|
||||||
use actix_web::{body::Body, web, web::Path, HttpResponse};
|
use actix_web::{body::Body, web, web::Path, HttpResponse};
|
||||||
use diesel::result::Error::NotFound;
|
use diesel::result::Error::NotFound;
|
||||||
use lemmy_api_common::blocking;
|
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_db_schema::{newtypes::CommentId, source::comment::Comment, traits::Crud};
|
||||||
use lemmy_utils::LemmyError;
|
use lemmy_utils::LemmyError;
|
||||||
use lemmy_websocket::LemmyContext;
|
use lemmy_websocket::LemmyContext;
|
||||||
|
@ -30,9 +30,7 @@ pub(crate) async fn get_apub_comment(
|
||||||
}
|
}
|
||||||
|
|
||||||
if !comment.deleted {
|
if !comment.deleted {
|
||||||
Ok(create_apub_response(
|
Ok(create_apub_response(&comment.to_apub(&**context).await?))
|
||||||
&comment.to_apub(context.pool()).await?,
|
|
||||||
))
|
|
||||||
} else {
|
} else {
|
||||||
Ok(create_apub_tombstone_response(&comment.to_tombstone()?))
|
Ok(create_apub_tombstone_response(&comment.to_tombstone()?))
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,13 @@ use crate::{
|
||||||
following::{follow::FollowCommunity, undo::UndoFollowCommunity},
|
following::{follow::FollowCommunity, undo::UndoFollowCommunity},
|
||||||
report::Report,
|
report::Report,
|
||||||
},
|
},
|
||||||
|
collections::{
|
||||||
|
community_moderators::ApubCommunityModerators,
|
||||||
|
community_outbox::ApubCommunityOutbox,
|
||||||
|
CommunityContext,
|
||||||
|
},
|
||||||
context::lemmy_context,
|
context::lemmy_context,
|
||||||
generate_moderators_url,
|
fetcher::object_id::ObjectId,
|
||||||
generate_outbox_url,
|
generate_outbox_url,
|
||||||
http::{
|
http::{
|
||||||
create_apub_response,
|
create_apub_response,
|
||||||
|
@ -17,18 +22,14 @@ use crate::{
|
||||||
objects::community::ApubCommunity,
|
objects::community::ApubCommunity,
|
||||||
};
|
};
|
||||||
use activitystreams::{
|
use activitystreams::{
|
||||||
base::{AnyBase, BaseExt},
|
base::BaseExt,
|
||||||
collection::{CollectionExt, OrderedCollection, UnorderedCollection},
|
collection::{CollectionExt, UnorderedCollection},
|
||||||
url::Url,
|
|
||||||
};
|
};
|
||||||
use actix_web::{body::Body, web, web::Payload, HttpRequest, HttpResponse};
|
use actix_web::{body::Body, web, web::Payload, HttpRequest, HttpResponse};
|
||||||
use lemmy_api_common::blocking;
|
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::{activity::Activity, community::Community};
|
use lemmy_db_schema::source::community::Community;
|
||||||
use lemmy_db_views_actor::{
|
use lemmy_db_views_actor::community_follower_view::CommunityFollowerView;
|
||||||
community_follower_view::CommunityFollowerView,
|
|
||||||
community_moderator_view::CommunityModeratorView,
|
|
||||||
};
|
|
||||||
use lemmy_utils::LemmyError;
|
use lemmy_utils::LemmyError;
|
||||||
use lemmy_websocket::LemmyContext;
|
use lemmy_websocket::LemmyContext;
|
||||||
use log::trace;
|
use log::trace;
|
||||||
|
@ -51,7 +52,7 @@ pub(crate) async fn get_apub_community_http(
|
||||||
.into();
|
.into();
|
||||||
|
|
||||||
if !community.deleted {
|
if !community.deleted {
|
||||||
let apub = community.to_apub(context.pool()).await?;
|
let apub = community.to_apub(&**context).await?;
|
||||||
|
|
||||||
Ok(create_apub_response(&apub))
|
Ok(create_apub_response(&apub))
|
||||||
} else {
|
} else {
|
||||||
|
@ -133,41 +134,10 @@ pub(crate) async fn get_apub_community_outbox(
|
||||||
Community::read_from_name(conn, &info.community_name)
|
Community::read_from_name(conn, &info.community_name)
|
||||||
})
|
})
|
||||||
.await??;
|
.await??;
|
||||||
|
let id = ObjectId::new(generate_outbox_url(&community.actor_id)?.into_inner());
|
||||||
let community_actor_id = community.actor_id.to_owned();
|
let outbox_data = CommunityContext(community.into(), context.get_ref().clone());
|
||||||
let activities = blocking(context.pool(), move |conn| {
|
let outbox: ApubCommunityOutbox = id.dereference(&outbox_data, &mut 0).await?;
|
||||||
Activity::read_community_outbox(conn, &community_actor_id)
|
Ok(create_apub_response(&outbox.to_apub(&outbox_data).await?))
|
||||||
})
|
|
||||||
.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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn get_apub_community_moderators(
|
pub(crate) async fn get_apub_community_moderators(
|
||||||
|
@ -179,26 +149,10 @@ pub(crate) async fn get_apub_community_moderators(
|
||||||
})
|
})
|
||||||
.await??
|
.await??
|
||||||
.into();
|
.into();
|
||||||
|
let id = ObjectId::new(generate_outbox_url(&community.actor_id)?.into_inner());
|
||||||
// The attributed to, is an ordered vector with the creator actor_ids first,
|
let outbox_data = CommunityContext(community, context.get_ref().clone());
|
||||||
// then the rest of the moderators
|
let moderators: ApubCommunityModerators = id.dereference(&outbox_data, &mut 0).await?;
|
||||||
// TODO Technically the instance admins can mod the community, but lets
|
Ok(create_apub_response(
|
||||||
// ignore that for now
|
&moderators.to_apub(&outbox_data).await?,
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ use activitystreams::{
|
||||||
};
|
};
|
||||||
use actix_web::{body::Body, web, web::Payload, HttpRequest, HttpResponse};
|
use actix_web::{body::Body, web, web::Payload, HttpRequest, HttpResponse};
|
||||||
use lemmy_api_common::blocking;
|
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_db_schema::source::person::Person;
|
||||||
use lemmy_utils::LemmyError;
|
use lemmy_utils::LemmyError;
|
||||||
use lemmy_websocket::LemmyContext;
|
use lemmy_websocket::LemmyContext;
|
||||||
|
@ -51,7 +51,7 @@ pub(crate) async fn get_apub_person_http(
|
||||||
.into();
|
.into();
|
||||||
|
|
||||||
if !person.deleted {
|
if !person.deleted {
|
||||||
let apub = person.to_apub(context.pool()).await?;
|
let apub = person.to_apub(&context).await?;
|
||||||
|
|
||||||
Ok(create_apub_response(&apub))
|
Ok(create_apub_response(&apub))
|
||||||
} else {
|
} else {
|
||||||
|
@ -109,19 +109,3 @@ pub(crate) async fn get_apub_person_outbox(
|
||||||
.set_total_items(0_u64);
|
.set_total_items(0_u64);
|
||||||
Ok(create_apub_response(&collection))
|
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))
|
|
||||||
}
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ use crate::{
|
||||||
use actix_web::{body::Body, web, HttpResponse};
|
use actix_web::{body::Body, web, HttpResponse};
|
||||||
use diesel::result::Error::NotFound;
|
use diesel::result::Error::NotFound;
|
||||||
use lemmy_api_common::blocking;
|
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_db_schema::{newtypes::PostId, source::post::Post, traits::Crud};
|
||||||
use lemmy_utils::LemmyError;
|
use lemmy_utils::LemmyError;
|
||||||
use lemmy_websocket::LemmyContext;
|
use lemmy_websocket::LemmyContext;
|
||||||
|
@ -30,7 +30,7 @@ pub(crate) async fn get_apub_post(
|
||||||
}
|
}
|
||||||
|
|
||||||
if !post.deleted {
|
if !post.deleted {
|
||||||
Ok(create_apub_response(&post.to_apub(context.pool()).await?))
|
Ok(create_apub_response(&post.to_apub(&context).await?))
|
||||||
} else {
|
} else {
|
||||||
Ok(create_apub_tombstone_response(&post.to_tombstone()?))
|
Ok(create_apub_tombstone_response(&post.to_tombstone()?))
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,12 +4,11 @@ use crate::http::{
|
||||||
community_inbox,
|
community_inbox,
|
||||||
get_apub_community_followers,
|
get_apub_community_followers,
|
||||||
get_apub_community_http,
|
get_apub_community_http,
|
||||||
get_apub_community_inbox,
|
|
||||||
get_apub_community_moderators,
|
get_apub_community_moderators,
|
||||||
get_apub_community_outbox,
|
get_apub_community_outbox,
|
||||||
},
|
},
|
||||||
get_activity,
|
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,
|
post::get_apub_post,
|
||||||
shared_inbox,
|
shared_inbox,
|
||||||
};
|
};
|
||||||
|
@ -49,10 +48,6 @@ pub fn config(cfg: &mut web::ServiceConfig, settings: &Settings) {
|
||||||
"/c/{community_name}/outbox",
|
"/c/{community_name}/outbox",
|
||||||
web::get().to(get_apub_community_outbox),
|
web::get().to(get_apub_community_outbox),
|
||||||
)
|
)
|
||||||
.route(
|
|
||||||
"/c/{community_name}/inbox",
|
|
||||||
web::get().to(get_apub_community_inbox),
|
|
||||||
)
|
|
||||||
.route(
|
.route(
|
||||||
"/c/{community_name}/moderators",
|
"/c/{community_name}/moderators",
|
||||||
web::get().to(get_apub_community_moderators),
|
web::get().to(get_apub_community_moderators),
|
||||||
|
@ -62,7 +57,6 @@ pub fn config(cfg: &mut web::ServiceConfig, settings: &Settings) {
|
||||||
"/u/{user_name}/outbox",
|
"/u/{user_name}/outbox",
|
||||||
web::get().to(get_apub_person_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("/post/{post_id}", web::get().to(get_apub_post))
|
||||||
.route("/comment/{comment_id}", web::get().to(get_apub_comment))
|
.route("/comment/{comment_id}", web::get().to(get_apub_comment))
|
||||||
.route("/activities/{type_}/{id}", web::get().to(get_activity)),
|
.route("/activities/{type_}/{id}", web::get().to(get_activity)),
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
pub mod activities;
|
pub mod activities;
|
||||||
|
pub(crate) mod collections;
|
||||||
mod context;
|
mod context;
|
||||||
pub mod fetcher;
|
pub mod fetcher;
|
||||||
pub mod http;
|
pub mod http;
|
||||||
pub mod migrations;
|
pub mod migrations;
|
||||||
pub mod objects;
|
pub mod objects;
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
extern crate lazy_static;
|
||||||
|
|
||||||
use crate::fetcher::post_or_comment::PostOrComment;
|
use crate::fetcher::post_or_comment::PostOrComment;
|
||||||
use anyhow::{anyhow, Context};
|
use anyhow::{anyhow, Context};
|
||||||
use lemmy_api_common::blocking;
|
use lemmy_api_common::blocking;
|
||||||
|
|
|
@ -2,13 +2,13 @@ use crate::{
|
||||||
activities::{verify_is_public, verify_person_in_community},
|
activities::{verify_is_public, verify_person_in_community},
|
||||||
context::lemmy_context,
|
context::lemmy_context,
|
||||||
fetcher::object_id::ObjectId,
|
fetcher::object_id::ObjectId,
|
||||||
objects::{create_tombstone, person::ApubPerson, post::ApubPost, Source},
|
objects::{person::ApubPerson, post::ApubPost, tombstone::Tombstone, Source},
|
||||||
PostOrComment,
|
PostOrComment,
|
||||||
};
|
};
|
||||||
use activitystreams::{
|
use activitystreams::{
|
||||||
base::AnyBase,
|
base::AnyBase,
|
||||||
chrono::NaiveDateTime,
|
chrono::NaiveDateTime,
|
||||||
object::{kind::NoteType, Tombstone},
|
object::kind::NoteType,
|
||||||
primitives::OneOrMany,
|
primitives::OneOrMany,
|
||||||
public,
|
public,
|
||||||
unparsed::Unparsed,
|
unparsed::Unparsed,
|
||||||
|
@ -18,7 +18,7 @@ use chrono::{DateTime, FixedOffset};
|
||||||
use html2md::parse_html;
|
use html2md::parse_html;
|
||||||
use lemmy_api_common::blocking;
|
use lemmy_api_common::blocking;
|
||||||
use lemmy_apub_lib::{
|
use lemmy_apub_lib::{
|
||||||
traits::{ApubObject, FromApub, ToApub},
|
traits::ApubObject,
|
||||||
values::{MediaTypeHtml, MediaTypeMarkdown},
|
values::{MediaTypeHtml, MediaTypeMarkdown},
|
||||||
verify::verify_domains_match,
|
verify::verify_domains_match,
|
||||||
};
|
};
|
||||||
|
@ -31,7 +31,6 @@ use lemmy_db_schema::{
|
||||||
post::Post,
|
post::Post,
|
||||||
},
|
},
|
||||||
traits::Crud,
|
traits::Crud,
|
||||||
DbPool,
|
|
||||||
};
|
};
|
||||||
use lemmy_utils::{
|
use lemmy_utils::{
|
||||||
utils::{convert_datetime, remove_slurs},
|
utils::{convert_datetime, remove_slurs},
|
||||||
|
@ -159,6 +158,8 @@ impl From<Comment> for ApubComment {
|
||||||
#[async_trait::async_trait(?Send)]
|
#[async_trait::async_trait(?Send)]
|
||||||
impl ApubObject for ApubComment {
|
impl ApubObject for ApubComment {
|
||||||
type DataType = LemmyContext;
|
type DataType = LemmyContext;
|
||||||
|
type ApubType = Note;
|
||||||
|
type TombstoneType = Tombstone;
|
||||||
|
|
||||||
fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
|
fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
|
||||||
None
|
None
|
||||||
|
@ -184,23 +185,17 @@ impl ApubObject for ApubComment {
|
||||||
.await??;
|
.await??;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
async fn to_apub(&self, context: &LemmyContext) -> Result<Note, LemmyError> {
|
||||||
impl ToApub for ApubComment {
|
|
||||||
type ApubType = Note;
|
|
||||||
type TombstoneType = Tombstone;
|
|
||||||
type DataType = DbPool;
|
|
||||||
|
|
||||||
async fn to_apub(&self, pool: &DbPool) -> Result<Note, LemmyError> {
|
|
||||||
let creator_id = self.creator_id;
|
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_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 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())
|
ObjectId::<PostOrComment>::new(parent_comment.ap_id.into_inner())
|
||||||
} else {
|
} else {
|
||||||
ObjectId::<PostOrComment>::new(post.ap_id.into_inner())
|
ObjectId::<PostOrComment>::new(post.ap_id.into_inner())
|
||||||
|
@ -228,19 +223,11 @@ impl ToApub for ApubComment {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
|
fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
|
||||||
create_tombstone(
|
Ok(Tombstone::new(
|
||||||
self.deleted,
|
|
||||||
self.ap_id.to_owned().into(),
|
|
||||||
self.updated,
|
|
||||||
NoteType::Note,
|
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`.
|
/// Converts a `Note` to `Comment`.
|
||||||
///
|
///
|
||||||
|
@ -324,6 +311,8 @@ mod tests {
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
#[serial]
|
#[serial]
|
||||||
async fn test_parse_lemmy_comment() {
|
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 context = init_context();
|
||||||
let url = Url::parse("https://enterprise.lemmy.ml/comment/38741").unwrap();
|
let url = Url::parse("https://enterprise.lemmy.ml/comment/38741").unwrap();
|
||||||
let data = prepare_comment_test(&url, &context).await;
|
let data = prepare_comment_test(&url, &context).await;
|
||||||
|
@ -339,7 +328,7 @@ mod tests {
|
||||||
assert!(!comment.local);
|
assert!(!comment.local);
|
||||||
assert_eq!(request_counter, 0);
|
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);
|
assert_json_include!(actual: json, expected: to_apub);
|
||||||
|
|
||||||
Comment::delete(&*context.pool().get().unwrap(), comment.id).unwrap();
|
Comment::delete(&*context.pool().get().unwrap(), comment.id).unwrap();
|
||||||
|
|
|
@ -1,17 +1,22 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
check_is_apub_id_valid,
|
check_is_apub_id_valid,
|
||||||
|
collections::{
|
||||||
|
community_moderators::ApubCommunityModerators,
|
||||||
|
community_outbox::ApubCommunityOutbox,
|
||||||
|
CommunityContext,
|
||||||
|
},
|
||||||
context::lemmy_context,
|
context::lemmy_context,
|
||||||
fetcher::community::{fetch_community_outbox, update_community_mods},
|
fetcher::object_id::ObjectId,
|
||||||
generate_moderators_url,
|
generate_moderators_url,
|
||||||
generate_outbox_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,
|
CommunityType,
|
||||||
};
|
};
|
||||||
use activitystreams::{
|
use activitystreams::{
|
||||||
actor::{kind::GroupType, Endpoints},
|
actor::{kind::GroupType, Endpoints},
|
||||||
base::AnyBase,
|
base::AnyBase,
|
||||||
chrono::NaiveDateTime,
|
chrono::NaiveDateTime,
|
||||||
object::{kind::ImageType, Tombstone},
|
object::kind::ImageType,
|
||||||
primitives::OneOrMany,
|
primitives::OneOrMany,
|
||||||
unparsed::Unparsed,
|
unparsed::Unparsed,
|
||||||
};
|
};
|
||||||
|
@ -20,7 +25,7 @@ use itertools::Itertools;
|
||||||
use lemmy_api_common::blocking;
|
use lemmy_api_common::blocking;
|
||||||
use lemmy_apub_lib::{
|
use lemmy_apub_lib::{
|
||||||
signatures::PublicKey,
|
signatures::PublicKey,
|
||||||
traits::{ActorType, ApubObject, FromApub, ToApub},
|
traits::{ActorType, ApubObject},
|
||||||
values::MediaTypeMarkdown,
|
values::MediaTypeMarkdown,
|
||||||
verify::verify_domains_match,
|
verify::verify_domains_match,
|
||||||
};
|
};
|
||||||
|
@ -63,9 +68,9 @@ pub struct Group {
|
||||||
// lemmy extension
|
// lemmy extension
|
||||||
sensitive: Option<bool>,
|
sensitive: Option<bool>,
|
||||||
// lemmy extension
|
// lemmy extension
|
||||||
pub(crate) moderators: Option<Url>,
|
pub(crate) moderators: Option<ObjectId<ApubCommunityModerators>>,
|
||||||
inbox: Url,
|
inbox: Url,
|
||||||
pub(crate) outbox: Url,
|
pub(crate) outbox: ObjectId<ApubCommunityOutbox>,
|
||||||
followers: Url,
|
followers: Url,
|
||||||
endpoints: Endpoints<Url>,
|
endpoints: Endpoints<Url>,
|
||||||
public_key: PublicKey,
|
public_key: PublicKey,
|
||||||
|
@ -138,6 +143,8 @@ impl From<Community> for ApubCommunity {
|
||||||
#[async_trait::async_trait(?Send)]
|
#[async_trait::async_trait(?Send)]
|
||||||
impl ApubObject for ApubCommunity {
|
impl ApubObject for ApubCommunity {
|
||||||
type DataType = LemmyContext;
|
type DataType = LemmyContext;
|
||||||
|
type ApubType = Group;
|
||||||
|
type TombstoneType = Tombstone;
|
||||||
|
|
||||||
fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
|
fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
|
||||||
Some(self.last_refreshed_at)
|
Some(self.last_refreshed_at)
|
||||||
|
@ -163,6 +170,91 @@ impl ApubObject for ApubCommunity {
|
||||||
.await??;
|
.await??;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn to_apub(&self, _context: &LemmyContext) -> Result<Group, LemmyError> {
|
||||||
|
let source = self.description.clone().map(|bio| Source {
|
||||||
|
content: bio,
|
||||||
|
media_type: MediaTypeMarkdown::Markdown,
|
||||||
|
});
|
||||||
|
let icon = self.icon.clone().map(|url| ImageObject {
|
||||||
|
kind: ImageType::Image,
|
||||||
|
url: url.into(),
|
||||||
|
});
|
||||||
|
let image = self.banner.clone().map(|url| ImageObject {
|
||||||
|
kind: ImageType::Image,
|
||||||
|
url: url.into(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let group = Group {
|
||||||
|
context: lemmy_context(),
|
||||||
|
kind: GroupType::Group,
|
||||||
|
id: self.actor_id(),
|
||||||
|
preferred_username: self.name.clone(),
|
||||||
|
name: self.title.clone(),
|
||||||
|
summary: self.description.as_ref().map(|b| markdown_to_html(b)),
|
||||||
|
source,
|
||||||
|
icon,
|
||||||
|
image,
|
||||||
|
sensitive: Some(self.nsfw),
|
||||||
|
moderators: Some(ObjectId::<ApubCommunityModerators>::new(
|
||||||
|
generate_moderators_url(&self.actor_id)?.into_inner(),
|
||||||
|
)),
|
||||||
|
inbox: self.inbox_url.clone().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()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
public_key: self.get_public_key()?,
|
||||||
|
published: Some(convert_datetime(self.published)),
|
||||||
|
updated: self.updated.map(convert_datetime),
|
||||||
|
unparsed: Default::default(),
|
||||||
|
};
|
||||||
|
Ok(group)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
|
||||||
|
Ok(Tombstone::new(
|
||||||
|
GroupType::Group,
|
||||||
|
self.updated.unwrap_or(self.published),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts a `Group` to `Community`, inserts it into the database and updates moderators.
|
||||||
|
async fn from_apub(
|
||||||
|
group: &Group,
|
||||||
|
context: &LemmyContext,
|
||||||
|
expected_domain: &Url,
|
||||||
|
request_counter: &mut i32,
|
||||||
|
) -> Result<ApubCommunity, LemmyError> {
|
||||||
|
let form = Group::from_apub_to_form(group, expected_domain, &context.settings()).await?;
|
||||||
|
|
||||||
|
// 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: 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();
|
||||||
|
|
||||||
|
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 {
|
impl ActorType for ApubCommunity {
|
||||||
|
@ -191,95 +283,6 @@ impl ActorType for ApubCommunity {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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> {
|
|
||||||
let source = self.description.clone().map(|bio| Source {
|
|
||||||
content: bio,
|
|
||||||
media_type: MediaTypeMarkdown::Markdown,
|
|
||||||
});
|
|
||||||
let icon = self.icon.clone().map(|url| ImageObject {
|
|
||||||
kind: ImageType::Image,
|
|
||||||
url: url.into(),
|
|
||||||
});
|
|
||||||
let image = self.banner.clone().map(|url| ImageObject {
|
|
||||||
kind: ImageType::Image,
|
|
||||||
url: url.into(),
|
|
||||||
});
|
|
||||||
|
|
||||||
let group = Group {
|
|
||||||
context: lemmy_context(),
|
|
||||||
kind: GroupType::Group,
|
|
||||||
id: self.actor_id(),
|
|
||||||
preferred_username: self.name.clone(),
|
|
||||||
name: self.title.clone(),
|
|
||||||
summary: self.description.as_ref().map(|b| markdown_to_html(b)),
|
|
||||||
source,
|
|
||||||
icon,
|
|
||||||
image,
|
|
||||||
sensitive: Some(self.nsfw),
|
|
||||||
moderators: Some(generate_moderators_url(&self.actor_id)?.into()),
|
|
||||||
inbox: self.inbox_url.clone().into(),
|
|
||||||
outbox: generate_outbox_url(&self.actor_id)?.into(),
|
|
||||||
followers: self.followers_url.clone().into(),
|
|
||||||
endpoints: Endpoints {
|
|
||||||
shared_inbox: self.shared_inbox_url.clone().map(|s| s.into()),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
public_key: self.get_public_key()?,
|
|
||||||
published: Some(convert_datetime(self.published)),
|
|
||||||
updated: self.updated.map(convert_datetime),
|
|
||||||
unparsed: Default::default(),
|
|
||||||
};
|
|
||||||
Ok(group)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
|
|
||||||
create_tombstone(
|
|
||||||
self.deleted,
|
|
||||||
self.actor_id.to_owned().into(),
|
|
||||||
self.updated,
|
|
||||||
GroupType::Group,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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(
|
|
||||||
group: &Group,
|
|
||||||
context: &LemmyContext,
|
|
||||||
expected_domain: &Url,
|
|
||||||
request_counter: &mut i32,
|
|
||||||
) -> Result<ApubCommunity, LemmyError> {
|
|
||||||
let form = Group::from_apub_to_form(group, expected_domain, &context.settings()).await?;
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
.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();
|
|
||||||
|
|
||||||
Ok(community.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
#[async_trait::async_trait(?Send)]
|
||||||
impl CommunityType for Community {
|
impl CommunityType for Community {
|
||||||
fn followers_url(&self) -> Url {
|
fn followers_url(&self) -> Url {
|
||||||
|
@ -327,9 +330,11 @@ mod tests {
|
||||||
let mut json: Group = file_to_json_object("assets/lemmy-community.json");
|
let mut json: Group = file_to_json_object("assets/lemmy-community.json");
|
||||||
let json_orig = json.clone();
|
let json_orig = json.clone();
|
||||||
// change these links so they dont fetch over the network
|
// change these links so they dont fetch over the network
|
||||||
json.moderators =
|
json.moderators = Some(ObjectId::new(
|
||||||
Some(Url::parse("https://enterprise.lemmy.ml/c/tenforward/not_moderators").unwrap());
|
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.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 url = Url::parse("https://enterprise.lemmy.ml/c/tenforward").unwrap();
|
||||||
let mut request_counter = 0;
|
let mut request_counter = 0;
|
||||||
|
@ -345,7 +350,7 @@ mod tests {
|
||||||
// this makes two requests to the (intentionally) broken outbox/moderators collections
|
// this makes two requests to the (intentionally) broken outbox/moderators collections
|
||||||
assert_eq!(request_counter, 2);
|
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);
|
assert_json_include!(actual: json_orig, expected: to_apub);
|
||||||
|
|
||||||
Community::delete(&*context.pool().get().unwrap(), community.id).unwrap();
|
Community::delete(&*context.pool().get().unwrap(), community.id).unwrap();
|
||||||
|
|
|
@ -1,12 +1,6 @@
|
||||||
use activitystreams::{
|
use activitystreams::object::kind::ImageType;
|
||||||
base::BaseExt,
|
|
||||||
object::{kind::ImageType, Tombstone, TombstoneExt},
|
|
||||||
};
|
|
||||||
use anyhow::anyhow;
|
|
||||||
use chrono::NaiveDateTime;
|
|
||||||
use html2md::parse_html;
|
use html2md::parse_html;
|
||||||
use lemmy_apub_lib::values::MediaTypeMarkdown;
|
use lemmy_apub_lib::values::MediaTypeMarkdown;
|
||||||
use lemmy_utils::{utils::convert_datetime, LemmyError};
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
@ -15,6 +9,7 @@ pub mod community;
|
||||||
pub mod person;
|
pub mod person;
|
||||||
pub mod post;
|
pub mod post;
|
||||||
pub mod private_message;
|
pub mod private_message;
|
||||||
|
pub mod tombstone;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
@ -31,31 +26,6 @@ pub struct ImageObject {
|
||||||
url: Url,
|
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(
|
fn get_summary_from_string_or_source(
|
||||||
raw: &Option<String>,
|
raw: &Option<String>,
|
||||||
source: &Option<Source>,
|
source: &Option<Source>,
|
||||||
|
@ -69,7 +39,6 @@ fn get_summary_from_string_or_source(
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
|
||||||
use actix::Actor;
|
use actix::Actor;
|
||||||
use diesel::{
|
use diesel::{
|
||||||
r2d2::{ConnectionManager, Pool},
|
r2d2::{ConnectionManager, Pool},
|
||||||
|
@ -85,6 +54,7 @@ mod tests {
|
||||||
rate_limit::{rate_limiter::RateLimiter, RateLimit},
|
rate_limit::{rate_limiter::RateLimiter, RateLimit},
|
||||||
request::build_user_agent,
|
request::build_user_agent,
|
||||||
settings::structs::Settings,
|
settings::structs::Settings,
|
||||||
|
LemmyError,
|
||||||
};
|
};
|
||||||
use lemmy_websocket::{chat_server::ChatServer, LemmyContext};
|
use lemmy_websocket::{chat_server::ChatServer, LemmyContext};
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
|
|
|
@ -16,14 +16,13 @@ use chrono::{DateTime, FixedOffset};
|
||||||
use lemmy_api_common::blocking;
|
use lemmy_api_common::blocking;
|
||||||
use lemmy_apub_lib::{
|
use lemmy_apub_lib::{
|
||||||
signatures::PublicKey,
|
signatures::PublicKey,
|
||||||
traits::{ActorType, ApubObject, FromApub, ToApub},
|
traits::{ActorType, ApubObject},
|
||||||
values::MediaTypeMarkdown,
|
values::MediaTypeMarkdown,
|
||||||
verify::verify_domains_match,
|
verify::verify_domains_match,
|
||||||
};
|
};
|
||||||
use lemmy_db_schema::{
|
use lemmy_db_schema::{
|
||||||
naive_now,
|
naive_now,
|
||||||
source::person::{Person as DbPerson, PersonForm},
|
source::person::{Person as DbPerson, PersonForm},
|
||||||
DbPool,
|
|
||||||
};
|
};
|
||||||
use lemmy_utils::{
|
use lemmy_utils::{
|
||||||
utils::{check_slurs, check_slurs_opt, convert_datetime, markdown_to_html},
|
utils::{check_slurs, check_slurs_opt, convert_datetime, markdown_to_html},
|
||||||
|
@ -80,7 +79,7 @@ impl Person {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct ApubPerson(DbPerson);
|
pub struct ApubPerson(DbPerson);
|
||||||
|
|
||||||
impl Deref for ApubPerson {
|
impl Deref for ApubPerson {
|
||||||
|
@ -99,6 +98,8 @@ impl From<DbPerson> for ApubPerson {
|
||||||
#[async_trait::async_trait(?Send)]
|
#[async_trait::async_trait(?Send)]
|
||||||
impl ApubObject for ApubPerson {
|
impl ApubObject for ApubPerson {
|
||||||
type DataType = LemmyContext;
|
type DataType = LemmyContext;
|
||||||
|
type ApubType = Person;
|
||||||
|
type TombstoneType = Tombstone;
|
||||||
|
|
||||||
fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
|
fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
|
||||||
Some(self.last_refreshed_at)
|
Some(self.last_refreshed_at)
|
||||||
|
@ -124,43 +125,8 @@ impl ApubObject for ApubPerson {
|
||||||
.await??;
|
.await??;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl ActorType for ApubPerson {
|
async fn to_apub(&self, _pool: &LemmyContext) -> Result<Person, LemmyError> {
|
||||||
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> {
|
|
||||||
let kind = if self.bot_account {
|
let kind = if self.bot_account {
|
||||||
UserTypes::Service
|
UserTypes::Service
|
||||||
} else {
|
} else {
|
||||||
|
@ -203,15 +169,10 @@ impl ToApub for ApubPerson {
|
||||||
};
|
};
|
||||||
Ok(person)
|
Ok(person)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
|
fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
|
||||||
impl FromApub for ApubPerson {
|
|
||||||
type ApubType = Person;
|
|
||||||
type DataType = LemmyContext;
|
|
||||||
|
|
||||||
async fn from_apub(
|
async fn from_apub(
|
||||||
person: &Person,
|
person: &Person,
|
||||||
|
@ -265,6 +226,34 @@ impl FromApub for ApubPerson {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -291,7 +280,7 @@ mod tests {
|
||||||
assert_eq!(person.bio.as_ref().unwrap().len(), 39);
|
assert_eq!(person.bio.as_ref().unwrap().len(), 39);
|
||||||
assert_eq!(request_counter, 0);
|
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);
|
assert_json_include!(actual: json, expected: to_apub);
|
||||||
|
|
||||||
DbPerson::delete(&*context.pool().get().unwrap(), person.id).unwrap();
|
DbPerson::delete(&*context.pool().get().unwrap(), person.id).unwrap();
|
||||||
|
|
|
@ -2,14 +2,11 @@ use crate::{
|
||||||
activities::{extract_community, verify_is_public, verify_person_in_community},
|
activities::{extract_community, verify_is_public, verify_person_in_community},
|
||||||
context::lemmy_context,
|
context::lemmy_context,
|
||||||
fetcher::object_id::ObjectId,
|
fetcher::object_id::ObjectId,
|
||||||
objects::{create_tombstone, person::ApubPerson, ImageObject, Source},
|
objects::{person::ApubPerson, tombstone::Tombstone, ImageObject, Source},
|
||||||
};
|
};
|
||||||
use activitystreams::{
|
use activitystreams::{
|
||||||
base::AnyBase,
|
base::AnyBase,
|
||||||
object::{
|
object::kind::{ImageType, PageType},
|
||||||
kind::{ImageType, PageType},
|
|
||||||
Tombstone,
|
|
||||||
},
|
|
||||||
primitives::OneOrMany,
|
primitives::OneOrMany,
|
||||||
public,
|
public,
|
||||||
unparsed::Unparsed,
|
unparsed::Unparsed,
|
||||||
|
@ -17,7 +14,7 @@ use activitystreams::{
|
||||||
use chrono::{DateTime, FixedOffset, NaiveDateTime};
|
use chrono::{DateTime, FixedOffset, NaiveDateTime};
|
||||||
use lemmy_api_common::blocking;
|
use lemmy_api_common::blocking;
|
||||||
use lemmy_apub_lib::{
|
use lemmy_apub_lib::{
|
||||||
traits::{ActorType, ApubObject, FromApub, ToApub},
|
traits::{ActorType, ApubObject},
|
||||||
values::{MediaTypeHtml, MediaTypeMarkdown},
|
values::{MediaTypeHtml, MediaTypeMarkdown},
|
||||||
verify::verify_domains_match,
|
verify::verify_domains_match,
|
||||||
};
|
};
|
||||||
|
@ -29,7 +26,6 @@ use lemmy_db_schema::{
|
||||||
post::{Post, PostForm},
|
post::{Post, PostForm},
|
||||||
},
|
},
|
||||||
traits::Crud,
|
traits::Crud,
|
||||||
DbPool,
|
|
||||||
};
|
};
|
||||||
use lemmy_utils::{
|
use lemmy_utils::{
|
||||||
request::fetch_site_data,
|
request::fetch_site_data,
|
||||||
|
@ -133,6 +129,8 @@ impl From<Post> for ApubPost {
|
||||||
#[async_trait::async_trait(?Send)]
|
#[async_trait::async_trait(?Send)]
|
||||||
impl ApubObject for ApubPost {
|
impl ApubObject for ApubPost {
|
||||||
type DataType = LemmyContext;
|
type DataType = LemmyContext;
|
||||||
|
type ApubType = Page;
|
||||||
|
type TombstoneType = Tombstone;
|
||||||
|
|
||||||
fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
|
fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
|
||||||
None
|
None
|
||||||
|
@ -158,20 +156,16 @@ impl ApubObject for ApubPost {
|
||||||
.await??;
|
.await??;
|
||||||
Ok(())
|
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.
|
// 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_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_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 {
|
let source = self.body.clone().map(|body| Source {
|
||||||
content: body,
|
content: body,
|
||||||
|
@ -205,19 +199,11 @@ impl ToApub for ApubPost {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
|
fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
|
||||||
create_tombstone(
|
Ok(Tombstone::new(
|
||||||
self.deleted,
|
|
||||||
self.ap_id.to_owned().into(),
|
|
||||||
self.updated,
|
|
||||||
PageType::Page,
|
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(
|
async fn from_apub(
|
||||||
page: &Page,
|
page: &Page,
|
||||||
|
@ -315,7 +301,7 @@ mod tests {
|
||||||
assert!(post.stickied);
|
assert!(post.stickied);
|
||||||
assert_eq!(request_counter, 0);
|
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);
|
assert_json_include!(actual: json, expected: to_apub);
|
||||||
|
|
||||||
Post::delete(&*context.pool().get().unwrap(), post.id).unwrap();
|
Post::delete(&*context.pool().get().unwrap(), post.id).unwrap();
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
context::lemmy_context,
|
context::lemmy_context,
|
||||||
fetcher::object_id::ObjectId,
|
fetcher::object_id::ObjectId,
|
||||||
objects::{create_tombstone, person::ApubPerson, Source},
|
objects::{person::ApubPerson, Source},
|
||||||
};
|
};
|
||||||
use activitystreams::{
|
use activitystreams::{
|
||||||
base::AnyBase,
|
base::AnyBase,
|
||||||
chrono::NaiveDateTime,
|
chrono::NaiveDateTime,
|
||||||
object::{kind::NoteType, Tombstone},
|
object::Tombstone,
|
||||||
primitives::OneOrMany,
|
primitives::OneOrMany,
|
||||||
unparsed::Unparsed,
|
unparsed::Unparsed,
|
||||||
};
|
};
|
||||||
|
@ -15,7 +15,7 @@ use chrono::{DateTime, FixedOffset};
|
||||||
use html2md::parse_html;
|
use html2md::parse_html;
|
||||||
use lemmy_api_common::blocking;
|
use lemmy_api_common::blocking;
|
||||||
use lemmy_apub_lib::{
|
use lemmy_apub_lib::{
|
||||||
traits::{ApubObject, FromApub, ToApub},
|
traits::ApubObject,
|
||||||
values::{MediaTypeHtml, MediaTypeMarkdown},
|
values::{MediaTypeHtml, MediaTypeMarkdown},
|
||||||
verify::verify_domains_match,
|
verify::verify_domains_match,
|
||||||
};
|
};
|
||||||
|
@ -25,7 +25,6 @@ use lemmy_db_schema::{
|
||||||
private_message::{PrivateMessage, PrivateMessageForm},
|
private_message::{PrivateMessage, PrivateMessageForm},
|
||||||
},
|
},
|
||||||
traits::Crud,
|
traits::Crud,
|
||||||
DbPool,
|
|
||||||
};
|
};
|
||||||
use lemmy_utils::{utils::convert_datetime, LemmyError};
|
use lemmy_utils::{utils::convert_datetime, LemmyError};
|
||||||
use lemmy_websocket::LemmyContext;
|
use lemmy_websocket::LemmyContext;
|
||||||
|
@ -104,6 +103,8 @@ impl From<PrivateMessage> for ApubPrivateMessage {
|
||||||
#[async_trait::async_trait(?Send)]
|
#[async_trait::async_trait(?Send)]
|
||||||
impl ApubObject for ApubPrivateMessage {
|
impl ApubObject for ApubPrivateMessage {
|
||||||
type DataType = LemmyContext;
|
type DataType = LemmyContext;
|
||||||
|
type ApubType = Note;
|
||||||
|
type TombstoneType = Tombstone;
|
||||||
|
|
||||||
fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
|
fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
|
||||||
None
|
None
|
||||||
|
@ -126,20 +127,14 @@ impl ApubObject for ApubPrivateMessage {
|
||||||
// do nothing, because pm can't be fetched over http
|
// do nothing, because pm can't be fetched over http
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
async fn to_apub(&self, context: &LemmyContext) -> Result<Note, LemmyError> {
|
||||||
impl ToApub for ApubPrivateMessage {
|
|
||||||
type ApubType = Note;
|
|
||||||
type TombstoneType = Tombstone;
|
|
||||||
type DataType = DbPool;
|
|
||||||
|
|
||||||
async fn to_apub(&self, pool: &DbPool) -> Result<Note, LemmyError> {
|
|
||||||
let creator_id = self.creator_id;
|
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_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 {
|
let note = Note {
|
||||||
context: lemmy_context(),
|
context: lemmy_context(),
|
||||||
|
@ -161,19 +156,8 @@ impl ToApub for ApubPrivateMessage {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
|
fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
|
||||||
create_tombstone(
|
unimplemented!()
|
||||||
self.deleted,
|
|
||||||
self.ap_id.to_owned().into(),
|
|
||||||
self.updated,
|
|
||||||
NoteType::Note,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
|
||||||
impl FromApub for ApubPrivateMessage {
|
|
||||||
type ApubType = Note;
|
|
||||||
type DataType = LemmyContext;
|
|
||||||
|
|
||||||
async fn from_apub(
|
async fn from_apub(
|
||||||
note: &Note,
|
note: &Note,
|
||||||
|
@ -253,7 +237,7 @@ mod tests {
|
||||||
assert_eq!(pm.content.len(), 20);
|
assert_eq!(pm.content.len(), 20);
|
||||||
assert_eq!(request_counter, 0);
|
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);
|
assert_json_include!(actual: json, expected: to_apub);
|
||||||
|
|
||||||
PrivateMessage::delete(&*context.pool().get().unwrap(), pm.id).unwrap();
|
PrivateMessage::delete(&*context.pool().get().unwrap(), pm.id).unwrap();
|
||||||
|
|
33
crates/apub/src/objects/tombstone.rs
Normal file
33
crates/apub/src/objects/tombstone.rs
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -30,6 +30,9 @@ pub trait ActivityHandler {
|
||||||
#[async_trait::async_trait(?Send)]
|
#[async_trait::async_trait(?Send)]
|
||||||
pub trait ApubObject {
|
pub trait ApubObject {
|
||||||
type DataType;
|
type DataType;
|
||||||
|
type ApubType;
|
||||||
|
type TombstoneType;
|
||||||
|
|
||||||
/// If this object should be refetched after a certain interval, it should return the last refresh
|
/// 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.
|
/// time here. This is mainly used to update remote actors.
|
||||||
fn last_refreshed_at(&self) -> Option<NaiveDateTime>;
|
fn last_refreshed_at(&self) -> Option<NaiveDateTime>;
|
||||||
|
@ -42,6 +45,25 @@ pub trait ApubObject {
|
||||||
Self: Sized;
|
Self: Sized;
|
||||||
/// Marks the object as deleted in local db. Called when a tombstone is received.
|
/// Marks the object as deleted in local db. Called when a tombstone is received.
|
||||||
async fn delete(self, data: &Self::DataType) -> Result<(), LemmyError>;
|
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
|
/// Common methods provided by ActivityPub actors (community and person). Not all methods are
|
||||||
|
@ -71,35 +93,3 @@ pub trait ActorType {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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;
|
|
||||||
}
|
|
||||||
|
|
|
@ -126,16 +126,6 @@ impl Community {
|
||||||
community.select(actor_id).distinct().load::<String>(conn)
|
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> {
|
pub fn upsert(conn: &PgConnection, community_form: &CommunityForm) -> Result<Community, Error> {
|
||||||
use crate::schema::community::dsl::*;
|
use crate::schema::community::dsl::*;
|
||||||
insert_into(community)
|
insert_into(community)
|
||||||
|
|
Loading…
Reference in a new issue