Activitypub crate rewrite (#2782)

* update activitypub-federation crate to 0.4.0

* fixes

* apub compiles!

* everything compiling!

* almost done, federated follow failing

* some test fixes

* use release

* add code back in
pull/2791/head
Nutomic 1 year ago committed by GitHub
parent 6bc49bdd70
commit 6f513793cb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

52
Cargo.lock generated

@ -4,26 +4,33 @@ version = 3
[[package]]
name = "activitypub_federation"
version = "0.3.5"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59fbd2b7fb0aea9bdd738fc1441d34d3e7b585d60b42ed63deeac289c872f119"
checksum = "b52228e706f380074b0722dae97f9c8274264026dbd3aa16d20466758995b6f4"
dependencies = [
"activitystreams-kinds",
"actix-rt",
"actix-web",
"anyhow",
"async-trait",
"background-jobs",
"base64",
"bytes",
"chrono",
"derive_builder 0.11.2",
"derive_builder 0.12.0",
"displaydoc",
"dyn-clone",
"enum_delegate",
"futures-core",
"http",
"http-signature-normalization-actix",
"http-signature-normalization",
"http-signature-normalization-reqwest",
"httpdate",
"itertools",
"once_cell",
"openssl",
"pin-project-lite",
"regex",
"reqwest",
"reqwest-middleware",
"serde",
@ -695,9 +702,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "bytes"
version = "1.2.1"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db"
checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
[[package]]
name = "bytestring"
@ -1232,11 +1239,11 @@ dependencies = [
[[package]]
name = "derive_builder"
version = "0.11.2"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d07adf7be193b71cc36b193d0f5fe60b918a3a9db4dad0449f57bcfd519704a3"
checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8"
dependencies = [
"derive_builder_macro 0.11.2",
"derive_builder_macro 0.12.0",
]
[[package]]
@ -1253,9 +1260,9 @@ dependencies = [
[[package]]
name = "derive_builder_core"
version = "0.11.2"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f91d4cfa921f1c05904dc3c57b4a32c38aed3340cce209f3a6fd1478babafc4"
checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f"
dependencies = [
"darling 0.14.1",
"proc-macro2 1.0.47",
@ -1275,11 +1282,11 @@ dependencies = [
[[package]]
name = "derive_builder_macro"
version = "0.11.2"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f0314b72bed045f3a68671b3c86328386762c93f82d98c65c3cb5e5f573dd68"
checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e"
dependencies = [
"derive_builder_core 0.11.2",
"derive_builder_core 0.12.0",
"syn 1.0.103",
]
@ -1418,6 +1425,17 @@ dependencies = [
"winapi",
]
[[package]]
name = "displaydoc"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3bf95dc3f046b9da4f2d51833c0d3547d8564ef6910f5c1ed130306a75b92886"
dependencies = [
"proc-macro2 1.0.47",
"quote 1.0.21",
"syn 1.0.103",
]
[[package]]
name = "dlv-list"
version = "0.3.0"
@ -2451,7 +2469,6 @@ name = "lemmy_apub"
version = "0.17.1"
dependencies = [
"activitypub_federation",
"activitystreams-kinds",
"actix-rt",
"actix-web",
"anyhow",
@ -2557,6 +2574,7 @@ dependencies = [
name = "lemmy_routes"
version = "0.17.1"
dependencies = [
"activitypub_federation",
"actix-web",
"anyhow",
"chrono",
@ -3998,9 +4016,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.6.0"
version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b"
checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733"
dependencies = [
"aho-corasick",
"memchr",

@ -59,7 +59,7 @@ lemmy_routes = { version = "=0.17.1", path = "./crates/routes" }
lemmy_db_views = { version = "=0.17.1", path = "./crates/db_views" }
lemmy_db_views_actor = { version = "=0.17.1", path = "./crates/db_views_actor" }
lemmy_db_views_moderator = { version = "=0.17.1", path = "./crates/db_views_moderator" }
activitypub_federation = "0.3.5"
activitypub_federation = { version = "0.4.0", default-features = false, features = ["actix-web"] }
diesel = "2.0.2"
diesel_migrations = "2.0.0"
diesel-async = "0.1.1"

@ -7,12 +7,12 @@ use lemmy_utils::{
use reqwest_middleware::ClientWithMiddleware;
use std::sync::Arc;
#[derive(Clone)]
pub struct LemmyContext {
pool: DbPool,
chat_server: Arc<ChatServer>,
client: ClientWithMiddleware,
settings: Settings,
secret: Secret,
client: Arc<ClientWithMiddleware>,
secret: Arc<Secret>,
rate_limit_cell: RateLimitCell,
}
@ -21,16 +21,14 @@ impl LemmyContext {
pool: DbPool,
chat_server: Arc<ChatServer>,
client: ClientWithMiddleware,
settings: Settings,
secret: Secret,
rate_limit_cell: RateLimitCell,
) -> LemmyContext {
LemmyContext {
pool,
chat_server,
client,
settings,
secret,
client: Arc::new(client),
secret: Arc::new(secret),
rate_limit_cell,
}
}
@ -53,16 +51,3 @@ impl LemmyContext {
&self.rate_limit_cell
}
}
impl Clone for LemmyContext {
fn clone(&self) -> Self {
LemmyContext {
pool: self.pool.clone(),
chat_server: self.chat_server.clone(),
client: self.client.clone(),
settings: self.settings.clone(),
secret: self.secret.clone(),
rate_limit_cell: self.rate_limit_cell.clone(),
}
}
}

@ -39,7 +39,6 @@ fn html_to_site_metadata(html_bytes: &[u8]) -> Result<SiteMetadata, LemmyError>
let first_line = html
.trim_start()
.lines()
.into_iter()
.next()
.ok_or_else(|| LemmyError::from_message("No lines in html"))?
.to_lowercase();

@ -73,7 +73,7 @@ pub struct SearchResponse {
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct ResolveObject {
pub q: String,
pub auth: Option<Sensitive<String>>,
pub auth: Sensitive<String>,
}
#[derive(Debug, Serialize, Deserialize, Default)]

@ -142,6 +142,7 @@ pub async fn mark_post_as_unread(
.map_err(|e| LemmyError::from_error_message(e, "couldnt_mark_post_as_read"))
}
// TODO: this should simply take LemmyContext as param
#[tracing::instrument(skip_all)]
pub async fn get_local_user_view_from_jwt(
jwt: &str,

@ -1,5 +1,5 @@
use crate::PerformCrud;
use activitypub_federation::core::signatures::generate_actor_keypair;
use activitypub_federation::http_signatures::generate_actor_keypair;
use actix_web::web::Data;
use lemmy_api_common::{
community::{CommunityResponse, CreateCommunity},

@ -1,5 +1,5 @@
use crate::{site::check_application_question, PerformCrud};
use activitypub_federation::core::signatures::generate_actor_keypair;
use activitypub_federation::http_signatures::generate_actor_keypair;
use actix_web::web::Data;
use lemmy_api_common::{
context::LemmyContext,

@ -1,5 +1,5 @@
use crate::PerformCrud;
use activitypub_federation::core::signatures::generate_actor_keypair;
use activitypub_federation::http_signatures::generate_actor_keypair;
use actix_web::web::Data;
use lemmy_api_common::{
context::LemmyContext,

@ -41,7 +41,6 @@ once_cell = { workspace = true }
tokio = { workspace = true }
html2md = "0.2.13"
serde_with = "1.14.0"
activitystreams-kinds = "0.2.1"
http-signature-normalization-actix = { version = "0.6.1", default-features = false, features = ["server", "sha-2"] }
enum_delegate = "0.2.0"

@ -9,18 +9,16 @@ use crate::{
verify_person_in_community,
},
activity_lists::AnnouncableActivities,
local_instance,
insert_activity,
objects::{instance::remote_instance_inboxes, person::ApubPerson},
protocol::activities::block::block_user::BlockUser,
ActorType,
};
use activitypub_federation::{
core::object_id::ObjectId,
data::Data,
config::Data,
kinds::{activity::BlockType, public},
protocol::verification::verify_domains_match,
traits::{ActivityHandler, Actor},
utils::verify_domains_match,
};
use activitystreams_kinds::{activity::BlockType, public};
use anyhow::anyhow;
use chrono::NaiveDateTime;
use lemmy_api_common::{
@ -51,17 +49,17 @@ impl BlockUser {
remove_data: Option<bool>,
reason: Option<String>,
expires: Option<NaiveDateTime>,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<BlockUser, LemmyError> {
let audience = if let SiteOrCommunity::Community(c) = target {
Some(ObjectId::new(c.actor_id()))
Some(c.id().into())
} else {
None
};
Ok(BlockUser {
actor: ObjectId::new(mod_.actor_id()),
actor: mod_.id().into(),
to: vec![public()],
object: ObjectId::new(user.actor_id()),
object: user.id().into(),
cc: generate_cc(target, context.pool()).await?,
target: target.id(),
kind: BlockType::Block,
@ -84,7 +82,7 @@ impl BlockUser {
remove_data: bool,
reason: Option<String>,
expires: Option<NaiveDateTime>,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
let block = BlockUser::new(
target,
@ -111,7 +109,7 @@ impl BlockUser {
}
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl ActivityHandler for BlockUser {
type DataType = LemmyContext;
type Error = LemmyError;
@ -125,17 +123,9 @@ impl ActivityHandler for BlockUser {
}
#[tracing::instrument(skip_all)]
async fn verify(
&self,
context: &Data<LemmyContext>,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
async fn verify(&self, context: &Data<LemmyContext>) -> Result<(), LemmyError> {
verify_is_public(&self.to, &self.cc)?;
match self
.target
.dereference(context, local_instance(context).await, request_counter)
.await?
{
match self.target.dereference(context).await? {
SiteOrCommunity::Site(site) => {
let domain = self.object.inner().domain().expect("url needs domain");
if context.settings().hostname == domain {
@ -144,43 +134,24 @@ impl ActivityHandler for BlockUser {
);
}
// site ban can only target a user who is on the same instance as the actor (admin)
verify_domains_match(&site.actor_id(), self.actor.inner())?;
verify_domains_match(&site.actor_id(), self.object.inner())?;
verify_domains_match(&site.id(), self.actor.inner())?;
verify_domains_match(&site.id(), self.object.inner())?;
}
SiteOrCommunity::Community(community) => {
verify_person_in_community(&self.actor, &community, context, request_counter).await?;
verify_mod_action(
&self.actor,
self.object.inner(),
community.id,
context,
request_counter,
)
.await?;
verify_person_in_community(&self.actor, &community, context).await?;
verify_mod_action(&self.actor, self.object.inner(), community.id, context).await?;
}
}
Ok(())
}
#[tracing::instrument(skip_all)]
async fn receive(
self,
context: &Data<LemmyContext>,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
async fn receive(self, context: &Data<LemmyContext>) -> Result<(), LemmyError> {
insert_activity(&self.id, &self, false, false, context).await?;
let expires = self.expires.map(|u| u.naive_local());
let mod_person = self
.actor
.dereference(context, local_instance(context).await, request_counter)
.await?;
let blocked_person = self
.object
.dereference(context, local_instance(context).await, request_counter)
.await?;
let target = self
.target
.dereference(context, local_instance(context).await, request_counter)
.await?;
let mod_person = self.actor.dereference(context).await?;
let blocked_person = self.object.dereference(context).await?;
let target = self.target.dereference(context).await?;
match target {
SiteOrCommunity::Site(_site) => {
let blocked_person = Person::update(

@ -4,10 +4,13 @@ use crate::{
activities::block::{block_user::BlockUser, undo_block_user::UndoBlockUser},
objects::{group::Group, instance::Instance},
},
ActorType,
SendActivity,
};
use activitypub_federation::{core::object_id::ObjectId, traits::ApubObject};
use activitypub_federation::{
config::Data,
fetch::object_id::ObjectId,
traits::{Actor, Object},
};
use chrono::NaiveDateTime;
use lemmy_api_common::{
community::{BanFromCommunity, BanFromCommunityResponse},
@ -41,11 +44,10 @@ pub enum InstanceOrGroup {
Group(Group),
}
#[async_trait::async_trait(?Send)]
impl ApubObject for SiteOrCommunity {
#[async_trait::async_trait]
impl Object for SiteOrCommunity {
type DataType = LemmyContext;
type ApubType = InstanceOrGroup;
type DbType = ();
type Kind = InstanceOrGroup;
type Error = LemmyError;
#[tracing::instrument(skip_all)]
@ -57,62 +59,51 @@ impl ApubObject for SiteOrCommunity {
}
#[tracing::instrument(skip_all)]
async fn read_from_apub_id(
async fn read_from_id(
object_id: Url,
data: &Self::DataType,
data: &Data<Self::DataType>,
) -> Result<Option<Self>, LemmyError>
where
Self: Sized,
{
let site = ApubSite::read_from_apub_id(object_id.clone(), data).await?;
let site = ApubSite::read_from_id(object_id.clone(), data).await?;
Ok(match site {
Some(o) => Some(SiteOrCommunity::Site(o)),
None => ApubCommunity::read_from_apub_id(object_id, data)
None => ApubCommunity::read_from_id(object_id, data)
.await?
.map(SiteOrCommunity::Community),
})
}
async fn delete(self, _data: &Self::DataType) -> Result<(), LemmyError> {
async fn delete(self, _data: &Data<Self::DataType>) -> Result<(), LemmyError> {
unimplemented!()
}
async fn into_apub(self, _data: &Self::DataType) -> Result<Self::ApubType, LemmyError> {
async fn into_json(self, _data: &Data<Self::DataType>) -> Result<Self::Kind, LemmyError> {
unimplemented!()
}
#[tracing::instrument(skip_all)]
async fn verify(
apub: &Self::ApubType,
apub: &Self::Kind,
expected_domain: &Url,
data: &Self::DataType,
request_counter: &mut i32,
data: &Data<Self::DataType>,
) -> Result<(), LemmyError> {
match apub {
InstanceOrGroup::Instance(i) => {
ApubSite::verify(i, expected_domain, data, request_counter).await
}
InstanceOrGroup::Group(g) => {
ApubCommunity::verify(g, expected_domain, data, request_counter).await
}
InstanceOrGroup::Instance(i) => ApubSite::verify(i, expected_domain, data).await,
InstanceOrGroup::Group(g) => ApubCommunity::verify(g, expected_domain, data).await,
}
}
#[tracing::instrument(skip_all)]
async fn from_apub(
apub: Self::ApubType,
data: &Self::DataType,
request_counter: &mut i32,
) -> Result<Self, LemmyError>
async fn from_json(apub: Self::Kind, data: &Data<Self::DataType>) -> Result<Self, LemmyError>
where
Self: Sized,
{
Ok(match apub {
InstanceOrGroup::Instance(p) => {
SiteOrCommunity::Site(ApubSite::from_apub(p, data, request_counter).await?)
}
InstanceOrGroup::Instance(p) => SiteOrCommunity::Site(ApubSite::from_json(p, data).await?),
InstanceOrGroup::Group(n) => {
SiteOrCommunity::Community(ApubCommunity::from_apub(n, data, request_counter).await?)
SiteOrCommunity::Community(ApubCommunity::from_json(n, data).await?)
}
})
}
@ -121,8 +112,8 @@ impl ApubObject for SiteOrCommunity {
impl SiteOrCommunity {
fn id(&self) -> ObjectId<SiteOrCommunity> {
match self {
SiteOrCommunity::Site(s) => ObjectId::new(s.actor_id.clone()),
SiteOrCommunity::Community(c) => ObjectId::new(c.actor_id.clone()),
SiteOrCommunity::Site(s) => ObjectId::from(s.actor_id.clone()),
SiteOrCommunity::Community(c) => ObjectId::from(c.actor_id.clone()),
}
}
}
@ -134,18 +125,18 @@ async fn generate_cc(target: &SiteOrCommunity, pool: &DbPool) -> Result<Vec<Url>
.into_iter()
.map(|s| s.actor_id.into())
.collect(),
SiteOrCommunity::Community(c) => vec![c.actor_id()],
SiteOrCommunity::Community(c) => vec![c.id()],
})
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl SendActivity for BanPerson {
type Response = BanPersonResponse;
async fn send_activity(
request: &Self,
_response: &Self::Response,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
let local_user_view =
get_local_user_view_from_jwt(&request.auth, context.pool(), context.secret()).await?;
@ -182,14 +173,14 @@ impl SendActivity for BanPerson {
}
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl SendActivity for BanFromCommunity {
type Response = BanFromCommunityResponse;
async fn send_activity(
request: &Self,
_response: &Self::Response,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
let local_user_view =
get_local_user_view_from_jwt(&request.auth, context.pool(), context.secret()).await?;

@ -7,18 +7,16 @@ use crate::{
verify_is_public,
},
activity_lists::AnnouncableActivities,
local_instance,
insert_activity,
objects::{instance::remote_instance_inboxes, person::ApubPerson},
protocol::activities::block::{block_user::BlockUser, undo_block_user::UndoBlockUser},
ActorType,
};
use activitypub_federation::{
core::object_id::ObjectId,
data::Data,
config::Data,
kinds::{activity::UndoType, public},
protocol::verification::verify_domains_match,
traits::{ActivityHandler, Actor},
utils::verify_domains_match,
};
use activitystreams_kinds::{activity::UndoType, public};
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::{
source::{
@ -38,11 +36,11 @@ impl UndoBlockUser {
user: &ApubPerson,
mod_: &ApubPerson,
reason: Option<String>,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
let block = BlockUser::new(target, user, mod_, None, reason, None, context).await?;
let audience = if let SiteOrCommunity::Community(c) = target {
Some(ObjectId::new(c.actor_id()))
Some(c.id().into())
} else {
None
};
@ -52,7 +50,7 @@ impl UndoBlockUser {
&context.settings().get_protocol_and_hostname(),
)?;
let undo = UndoBlockUser {
actor: ObjectId::new(mod_.actor_id()),
actor: mod_.id().into(),
to: vec![public()],
object: block,
cc: generate_cc(target, context.pool()).await?,
@ -75,7 +73,7 @@ impl UndoBlockUser {
}
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl ActivityHandler for UndoBlockUser {
type DataType = LemmyContext;
type Error = LemmyError;
@ -89,40 +87,20 @@ impl ActivityHandler for UndoBlockUser {
}
#[tracing::instrument(skip_all)]
async fn verify(
&self,
context: &Data<LemmyContext>,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
async fn verify(&self, context: &Data<LemmyContext>) -> Result<(), LemmyError> {
verify_is_public(&self.to, &self.cc)?;
verify_domains_match(self.actor.inner(), self.object.actor.inner())?;
self.object.verify(context, request_counter).await?;
self.object.verify(context).await?;
Ok(())
}
#[tracing::instrument(skip_all)]
async fn receive(
self,
context: &Data<LemmyContext>,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let instance = local_instance(context).await;
async fn receive(self, context: &Data<LemmyContext>) -> Result<(), LemmyError> {
insert_activity(&self.id, &self, false, false, context).await?;
let expires = self.object.expires.map(|u| u.naive_local());
let mod_person = self
.actor
.dereference(context, instance, request_counter)
.await?;
let blocked_person = self
.object
.object
.dereference(context, instance, request_counter)
.await?;
match self
.object
.target
.dereference(context, instance, request_counter)
.await?
{
let mod_person = self.actor.dereference(context).await?;
let blocked_person = self.object.object.dereference(context).await?;
match self.object.target.dereference(context).await? {
SiteOrCommunity::Site(_site) => {
let blocked_person = Person::update(
context.pool(),

@ -14,17 +14,18 @@ use crate::{
IdOrNestedObject,
InCommunity,
},
ActorType,
};
use activitypub_federation::{core::object_id::ObjectId, data::Data, traits::ActivityHandler};
use activitystreams_kinds::{activity::AnnounceType, public};
use activitypub_federation::{
config::Data,
kinds::{activity::AnnounceType, public},
traits::{ActivityHandler, Actor},
};
use lemmy_api_common::context::LemmyContext;
use lemmy_utils::error::LemmyError;
use serde_json::Value;
use tracing::debug;
use url::Url;
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl ActivityHandler for RawAnnouncableActivities {
type DataType = LemmyContext;
type Error = LemmyError;
@ -38,35 +39,27 @@ impl ActivityHandler for RawAnnouncableActivities {
}
#[tracing::instrument(skip_all)]
async fn verify(
&self,
_data: &Data<Self::DataType>,
_request_counter: &mut i32,
) -> Result<(), Self::Error> {
async fn verify(&self, _data: &Data<Self::DataType>) -> Result<(), Self::Error> {
Ok(())
}
#[tracing::instrument(skip_all)]
async fn receive(
self,
data: &Data<Self::DataType>,
request_counter: &mut i32,
) -> Result<(), Self::Error> {
async fn receive(self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
let activity: AnnouncableActivities = self.clone().try_into()?;
// This is only for sending, not receiving so we reject it.
if let AnnouncableActivities::Page(_) = activity {
return Err(LemmyError::from_message("Cant receive page"));
}
let community = activity.community(data, &mut 0).await?;
let actor_id = ObjectId::new(activity.actor().clone());
let community = activity.community(data).await?;
let actor_id = activity.actor().clone().into();
// verify and receive activity
activity.verify(data, request_counter).await?;
activity.receive(data, request_counter).await?;
activity.verify(data).await?;
activity.receive(data).await?;
// send to community followers
if community.local {
verify_person_in_community(&actor_id, &community, data, &mut 0).await?;
verify_person_in_community(&actor_id, &community, data).await?;
AnnounceActivity::send(self, &community, data).await?;
}
Ok(())
@ -77,10 +70,10 @@ impl AnnounceActivity {
pub(crate) fn new(
object: RawAnnouncableActivities,
community: &ApubCommunity,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<AnnounceActivity, LemmyError> {
Ok(AnnounceActivity {
actor: ObjectId::new(community.actor_id()),
actor: community.id().into(),
to: vec![public()],
object: IdOrNestedObject::NestedObject(object),
cc: vec![community.followers_url.clone().into()],
@ -96,7 +89,7 @@ impl AnnounceActivity {
pub async fn send(
object: RawAnnouncableActivities,
community: &ApubCommunity,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
let announce = AnnounceActivity::new(object.clone(), community, context)?;
let inboxes = community.get_follower_inboxes(context).await?;
@ -109,7 +102,10 @@ impl AnnounceActivity {
// Hack: need to convert Page into a format which can be sent as activity, which requires
// adding actor field.
let announcable_page = RawAnnouncableActivities {
id: c.object.id.clone().into_inner(),
id: generate_activity_id(
AnnounceType::Announce,
&context.settings().get_protocol_and_hostname(),
)?,
actor: c.actor.clone().into_inner(),
other: serde_json::to_value(c.object)?
.as_object()
@ -123,7 +119,7 @@ impl AnnounceActivity {
}
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl ActivityHandler for AnnounceActivity {
type DataType = LemmyContext;
type Error = LemmyError;
@ -137,44 +133,23 @@ impl ActivityHandler for AnnounceActivity {
}
#[tracing::instrument(skip_all)]
async fn verify(
&self,
_context: &Data<LemmyContext>,
_request_counter: &mut i32,
) -> Result<(), LemmyError> {
async fn verify(&self, _context: &Data<Self::DataType>) -> Result<(), LemmyError> {
verify_is_public(&self.to, &self.cc)?;
Ok(())
}
#[tracing::instrument(skip_all)]
async fn receive(
self,
context: &Data<LemmyContext>,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let object: AnnouncableActivities = self
.object
.object(context, request_counter)
.await?
.try_into()?;
async fn receive(self, context: &Data<Self::DataType>) -> Result<(), LemmyError> {
insert_activity(&self.id, &self, false, false, context).await?;
let object: AnnouncableActivities = self.object.object(context).await?.try_into()?;
// This is only for sending, not receiving so we reject it.
if let AnnouncableActivities::Page(_) = object {
return Err(LemmyError::from_message("Cant receive page"));
}
// we have to verify this here in order to avoid fetching the object twice over http
object.verify(context, request_counter).await?;
let object_value = serde_json::to_value(&object)?;
let insert = insert_activity(object.id(), object_value, false, true, context.pool()).await?;
if !insert {
debug!(
"Received duplicate activity in announce {}",
object.id().to_string()
);
return Ok(());
}
object.receive(context, request_counter).await
// verify here in order to avoid fetching the object twice over http
object.verify(context).await?;
object.receive(context).await
}
}

@ -7,7 +7,7 @@ use crate::{
verify_person_in_community,
},
activity_lists::AnnouncableActivities,
local_instance,
insert_activity,
objects::{community::ApubCommunity, person::ApubPerson, post::ApubPost},
protocol::{
activities::{
@ -17,15 +17,14 @@ use crate::{
},
InCommunity,
},
ActorType,
SendActivity,
};
use activitypub_federation::{
core::object_id::ObjectId,
data::Data,
config::Data,
fetch::object_id::ObjectId,
kinds::{activity::AddType, public},
traits::{ActivityHandler, Actor},
};
use activitystreams_kinds::{activity::AddType, public};
use lemmy_api_common::{
community::{AddModToCommunity, AddModToCommunityResponse},
context::LemmyContext,
@ -51,21 +50,21 @@ impl CollectionAdd {
community: &ApubCommunity,
added_mod: &ApubPerson,
actor: &ApubPerson,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
let id = generate_activity_id(
AddType::Add,
&context.settings().get_protocol_and_hostname(),
)?;
let add = CollectionAdd {
actor: ObjectId::new(actor.actor_id()),
actor: actor.id().into(),
to: vec![public()],
object: added_mod.actor_id(),
object: added_mod.id(),
target: generate_moderators_url(&community.actor_id)?.into(),
cc: vec![community.actor_id()],
cc: vec![community.id()],
kind: AddType::Add,
id: id.clone(),
audience: Some(ObjectId::new(community.actor_id())),
audience: Some(community.id().into()),
};
let activity = AnnouncableActivities::CollectionAdd(add);
@ -77,28 +76,28 @@ impl CollectionAdd {
community: &ApubCommunity,
featured_post: &ApubPost,
actor: &ApubPerson,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
let id = generate_activity_id(
AddType::Add,
&context.settings().get_protocol_and_hostname(),
)?;
let add = CollectionAdd {
actor: ObjectId::new(actor.actor_id()),
actor: actor.id().into(),
to: vec![public()],
object: featured_post.ap_id.clone().into(),
target: generate_featured_url(&community.actor_id)?.into(),
cc: vec![community.actor_id()],
cc: vec![community.id()],
kind: AddType::Add,
id: id.clone(),
audience: Some(ObjectId::new(community.actor_id())),
audience: Some(community.id().into()),
};
let activity = AnnouncableActivities::CollectionAdd(add);
send_activity_in_community(activity, actor, community, vec![], true, context).await
}
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl ActivityHandler for CollectionAdd {
type DataType = LemmyContext;
type Error = LemmyError;
@ -112,37 +111,23 @@ impl ActivityHandler for CollectionAdd {
}
#[tracing::instrument(skip_all)]
async fn verify(
&self,
context: &Data<LemmyContext>,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
async fn verify(&self, context: &Data<Self::DataType>) -> Result<(), LemmyError> {
verify_is_public(&self.to, &self.cc)?;
let community = self.community(context, request_counter).await?;
verify_person_in_community(&self.actor, &community, context, request_counter).await?;
verify_mod_action(
&self.actor,
&self.object,
community.id,
context,
request_counter,
)
.await?;
let community = self.community(context).await?;
verify_person_in_community(&self.actor, &community, context).await?;
verify_mod_action(&self.actor, &self.object, community.id, context).await?;
Ok(())
}
#[tracing::instrument(skip_all)]
async fn receive(
self,
context: &Data<LemmyContext>,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
async fn receive(self, context: &Data<Self::DataType>) -> Result<(), LemmyError> {
insert_activity(&self.id, &self, false, false, context).await?;
let (community, collection_type) =
Community::get_by_collection_url(context.pool(), &self.target.into()).await?;
match collection_type {
CollectionType::Moderators => {
let new_mod = ObjectId::<ApubPerson>::new(self.object)
.dereference(context, local_instance(context).await, request_counter)
let new_mod = ObjectId::<ApubPerson>::from(self.object)
.dereference(context)
.await?;
// If we had to refetch the community while parsing the activity, then the new mod has already
@ -158,10 +143,7 @@ impl ActivityHandler for CollectionAdd {
CommunityModerator::join(context.pool(), &form).await?;
// write mod log
let actor = self
.actor
.dereference(context, local_instance(context).await, request_counter)
.await?;
let actor = self.actor.dereference(context).await?;
let form = ModAddCommunityForm {
mod_person_id: actor.id,
other_person_id: new_mod.id,
@ -173,8 +155,8 @@ impl ActivityHandler for CollectionAdd {
// TODO: send websocket notification about added mod
}
CollectionType::Featured => {
let post = ObjectId::<ApubPost>::new(self.object)
.dereference(context, local_instance(context).await, request_counter)
let post = ObjectId::<ApubPost>::from(self.object)
.dereference(context)
.await?;
let form = PostUpdateForm::builder()
.featured_community(Some(true))
@ -186,14 +168,14 @@ impl ActivityHandler for CollectionAdd {
}
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl SendActivity for AddModToCommunity {
type Response = AddModToCommunityResponse;
async fn send_activity(
request: &Self,
_response: &Self::Response,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
let local_user_view =
get_local_user_view_from_jwt(&request.auth, context.pool(), context.secret()).await?;
@ -223,14 +205,14 @@ impl SendActivity for AddModToCommunity {
}
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl SendActivity for FeaturePost {
type Response = PostResponse;
async fn send_activity(
request: &Self,
response: &Self::Response,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
let local_user_view =
get_local_user_view_from_jwt(&request.auth, context.pool(), context.secret()).await?;

@ -7,17 +7,16 @@ use crate::{
verify_person_in_community,
},
activity_lists::AnnouncableActivities,
local_instance,
insert_activity,
objects::{community::ApubCommunity, person::ApubPerson, post::ApubPost},
protocol::{activities::community::collection_remove::CollectionRemove, InCommunity},
ActorType,
};
use activitypub_federation::{
core::object_id::ObjectId,
data::Data,
config::Data,
fetch::object_id::ObjectId,
kinds::{activity::RemoveType, public},
traits::{ActivityHandler, Actor},
};
use activitystreams_kinds::{activity::RemoveType, public};
use lemmy_api_common::{
context::LemmyContext,
utils::{generate_featured_url, generate_moderators_url},
@ -40,21 +39,21 @@ impl CollectionRemove {
community: &ApubCommunity,
removed_mod: &ApubPerson,
actor: &ApubPerson,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
let id = generate_activity_id(
RemoveType::Remove,
&context.settings().get_protocol_and_hostname(),
)?;
let remove = CollectionRemove {
actor: ObjectId::new(actor.actor_id()),
actor: actor.id().into(),
to: vec![public()],
object: ObjectId::new(removed_mod.actor_id()),
object: removed_mod.id(),
target: generate_moderators_url(&community.actor_id)?.into(),
id: id.clone(),
cc: vec![community.actor_id()],
cc: vec![community.id()],
kind: RemoveType::Remove,
audience: Some(ObjectId::new(community.actor_id())),
audience: Some(community.id().into()),
};
let activity = AnnouncableActivities::CollectionRemove(remove);
@ -66,28 +65,28 @@ impl CollectionRemove {
community: &ApubCommunity,
featured_post: &ApubPost,
actor: &ApubPerson,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
let id = generate_activity_id(
RemoveType::Remove,
&context.settings().get_protocol_and_hostname(),
)?;
let remove = CollectionRemove {
actor: ObjectId::new(actor.actor_id()),
actor: actor.id().into(),
to: vec![public()],
object: featured_post.ap_id.clone().into(),
target: generate_featured_url(&community.actor_id)?.into(),
cc: vec![community.actor_id()],
cc: vec![community.id()],
kind: RemoveType::Remove,
id: id.clone(),
audience: Some(ObjectId::new(community.actor_id())),
audience: Some(community.id().into()),
};
let activity = AnnouncableActivities::CollectionRemove(remove);
send_activity_in_community(activity, actor, community, vec![], true, context).await
}
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl ActivityHandler for CollectionRemove {
type DataType = LemmyContext;
type Error = LemmyError;
@ -101,38 +100,23 @@ impl ActivityHandler for CollectionRemove {
}
#[tracing::instrument(skip_all)]
async fn verify(
&self,
context: &Data<LemmyContext>,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
async fn verify(&self, context: &Data<Self::DataType>) -> Result<(), LemmyError> {
verify_is_public(&self.to, &self.cc)?;
let community = self.community(context, request_counter).await?;
verify_person_in_community(&self.actor, &community, context, request_counter).await?;
verify_mod_action(
&self.actor,
self.object.inner(),
community.id,
context,
request_counter,
)
.await?;
let community = self.community(context).await?;
verify_person_in_community(&self.actor, &community, context).await?;
verify_mod_action(&self.actor, &self.object, community.id, context).await?;
Ok(())
}
#[tracing::instrument(skip_all)]
async fn receive(
self,
context: &Data<LemmyContext>,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
async fn receive(self, context: &Data<Self::DataType>) -> Result<(), LemmyError> {
insert_activity(&self.id, &self, false, false, context).await?;
let (community, collection_type) =
Community::get_by_collection_url(context.pool(), &self.target.into()).await?;
match collection_type {
CollectionType::Moderators => {
let remove_mod = self
.object
.dereference(context, local_instance(context).await, request_counter)
let remove_mod = ObjectId::<ApubPerson>::from(self.object)
.dereference(context)
.await?;
let form = CommunityModeratorForm {
@ -142,10 +126,7 @@ impl ActivityHandler for CollectionRemove {
CommunityModerator::leave(context.pool(), &form).await?;
// write mod log
let actor = self
.actor
.dereference(context, local_instance(context).await, request_counter)
.await?;
let actor = self.actor.dereference(context).await?;
let form = ModAddCommunityForm {
mod_person_id: actor.id,
other_person_id: remove_mod.id,
@ -157,8 +138,8 @@ impl ActivityHandler for CollectionRemove {
// TODO: send websocket notification about removed mod
}
CollectionType::Featured => {
let post = ObjectId::<ApubPost>::new(self.object)
.dereference(context, local_instance(context).await, request_counter)
let post = ObjectId::<ApubPost>::from(self.object)
.dereference(context)
.await?;
let form = PostUpdateForm::builder()
.featured_community(Some(false))

@ -8,7 +8,7 @@ use crate::{
verify_person_in_community,
},
activity_lists::AnnouncableActivities,
local_instance,
insert_activity,
protocol::{
activities::{
community::lock_page::{LockPage, LockType, UndoLockPage},
@ -19,8 +19,11 @@ use crate::{
},
SendActivity,
};
use activitypub_federation::{core::object_id::ObjectId, data::Data, traits::ActivityHandler};
use activitystreams_kinds::{activity::UndoType, public};
use activitypub_federation::{
config::Data,
kinds::{activity::UndoType, public},
traits::ActivityHandler,
};
use lemmy_api_common::{
context::LemmyContext,
post::{LockPost, PostResponse},
@ -36,7 +39,7 @@ use lemmy_db_schema::{
use lemmy_utils::error::LemmyError;
use url::Url;
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl ActivityHandler for LockPage {
type DataType = LemmyContext;
type Error = LemmyError;
@ -49,42 +52,24 @@ impl ActivityHandler for LockPage {
self.actor.inner()
}
async fn verify(
&self,
context: &Data<Self::DataType>,
request_counter: &mut i32,
) -> Result<(), Self::Error> {
async fn verify(&self, context: &Data<Self::DataType>) -> Result<(), Self::Error> {
verify_is_public(&self.to, &self.cc)?;
let community = self.community(context, request_counter).await?;
verify_person_in_community(&self.actor, &community, context, request_counter).await?;
let community = self.community(context).await?;
verify_person_in_community(&self.actor, &community, context).await?;
check_community_deleted_or_removed(&community)?;
verify_mod_action(
&self.actor,
self.object.inner(),
community.id,
context,
request_counter,
)
.await?;
verify_mod_action(&self.actor, self.object.inner(), community.id, context).await?;
Ok(())
}
async fn receive(
self,
context: &Data<Self::DataType>,
request_counter: &mut i32,
) -> Result<(), Self::Error> {
async fn receive(self, context: &Data<Self::DataType>) -> Result<(), Self::Error> {
let form = PostUpdateForm::builder().locked(Some(true)).build();
let post = self
.object
.dereference(context, local_instance(context).await, request_counter)
.await?;
let post = self.object.dereference(context).await?;
Post::update(context.pool(), post.id, &form).await?;
Ok(())
}
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl ActivityHandler for UndoLockPage {
type DataType = LemmyContext;
type Error = LemmyError;
@ -97,50 +82,38 @@ impl ActivityHandler for UndoLockPage {
self.actor.inner()
}
async fn verify(
&self,
context: &Data<Self::DataType>,
request_counter: &mut i32,
) -> Result<(), Self::Error> {
async fn verify(&self, context: &Data<Self::DataType>) -> Result<(), Self::Error> {
verify_is_public(&self.to, &self.cc)?;
let community = self.community(context, request_counter).await?;
verify_person_in_community(&self.actor, &community, context, request_counter).await?;
let community = self.community(context).await?;
verify_person_in_community(&self.actor, &community, context).await?;
check_community_deleted_or_removed(&community)?;
verify_mod_action(
&self.actor,
self.object.object.inner(),
community.id,
context,
request_counter,
)
.await?;
Ok(())
}
async fn receive(
self,
context: &Data<Self::DataType>,
request_counter: &mut i32,
) -> Result<(), Self::Error> {
async fn receive(self, context: &Data<Self::DataType>) -> Result<(), Self::Error> {
insert_activity(&self.id, &self, false, false, context).await?;
let form = PostUpdateForm::builder().locked(Some(false)).build();
let post = self
.object
.object
.dereference(context, local_instance(context).await, request_counter)
.await?;
let post = self.object.object.dereference(context).await?;
Post::update(context.pool(), post.id, &form).await?;
Ok(())
}
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl SendActivity for LockPost {
type Response = PostResponse;
async fn send_activity(
request: &Self,
response: &Self::Response,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
let local_user_view =
get_local_user_view_from_jwt(&request.auth, context.pool(), context.secret()).await?;
@ -156,16 +129,16 @@ impl SendActivity for LockPost {
LockType::Lock,
&context.settings().get_protocol_and_hostname(),
)?;
let community_id: Url = response.post_view.community.actor_id.clone().into();
let actor = ObjectId::new(local_user_view.person.actor_id.clone());
let community_id = response.post_view.community.actor_id.clone();
let actor = local_user_view.person.actor_id.clone().into();
let lock = LockPage {
actor,
to: vec![public()],
object: ObjectId::new(response.post_view.post.ap_id.clone()),
cc: vec![community_id.clone()],
object: response.post_view.post.ap_id.clone().into(),
cc: vec![community_id.clone().into()],
kind: LockType::Lock,
id,
audience: Some(ObjectId::new(community_id)),
audience: Some(community_id.into()),
};
let activity = if request.locked {
AnnouncableActivities::LockPost(lock)

@ -4,7 +4,7 @@ use crate::{
objects::{community::ApubCommunity, person::ApubPerson},
protocol::activities::community::announce::AnnounceActivity,
};
use activitypub_federation::traits::Actor;
use activitypub_federation::{config::Data, traits::Actor};
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::source::person::PersonFollower;
use lemmy_utils::error::LemmyError;
@ -36,29 +36,30 @@ pub(crate) async fn send_activity_in_community(
community: &ApubCommunity,
extra_inboxes: Vec<Url>,
is_mod_action: bool,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
// send to extra_inboxes
send_lemmy_activity(context, activity.clone(), actor, extra_inboxes, false).await?;
// send to any users which are mentioned or affected directly
let mut inboxes = extra_inboxes;
// send to user followers
if !is_mod_action {
inboxes.append(
&mut PersonFollower::list_followers(context.pool(), actor.id)
.await?
.into_iter()
.map(|p| ApubPerson(p).shared_inbox_or_inbox())
.collect(),
);
}
if community.local {
// send directly to community followers
AnnounceActivity::send(activity.clone().try_into()?, community, context).await?;
} else {
// send to the community, which will then forward to followers
let inbox = vec![community.shared_inbox_or_inbox()];
send_lemmy_activity(context, activity.clone(), actor, inbox, false).await?;
}
// send to those who follow `actor`
if !is_mod_action {
let inboxes = PersonFollower::list_followers(context.pool(), actor.id)
.await?
.into_iter()
.map(|p| ApubPerson(p).shared_inbox_or_inbox())
.collect();
send_lemmy_activity(context, activity, actor, inboxes, false).await?;
inboxes.push(community.shared_inbox_or_inbox());
}
send_lemmy_activity(context, activity.clone(), actor, inboxes, false).await?;
Ok(())
}

@ -1,18 +1,17 @@
use crate::{
activities::{generate_activity_id, send_lemmy_activity, verify_person_in_community},
local_instance,
insert_activity,
objects::{community::ApubCommunity, person::ApubPerson},
protocol::{activities::community::report::Report, InCommunity},
ActorType,
PostOrComment,
SendActivity,
};
use activitypub_federation::{
core::object_id::ObjectId,
data::Data,
config::Data,
fetch::object_id::ObjectId,
kinds::activity::FlagType,
traits::{ActivityHandler, Actor},
};
use activitystreams_kinds::activity::FlagType;
use lemmy_api_common::{
comment::{CommentReportResponse, CreateCommentReport},
context::LemmyContext,
@ -31,21 +30,21 @@ use lemmy_db_views::structs::{CommentReportView, PostReportView};
use lemmy_utils::error::LemmyError;
use url::Url;
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl SendActivity for CreatePostReport {
type Response = PostReportResponse;
async fn send_activity(
request: &Self,
response: &Self::Response,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
let local_user_view =
get_local_user_view_from_jwt(&request.auth, context.pool(), context.secret()).await?;
Report::send(
ObjectId::new(response.post_report_view.post.ap_id.clone()),
ObjectId::from(response.post_report_view.post.ap_id.clone()),
&local_user_view.person.into(),
ObjectId::new(response.post_report_view.community.actor_id.clone()),
ObjectId::from(response.post_report_view.community.actor_id.clone()),
request.reason.to_string(),
context,
)
@ -53,21 +52,21 @@ impl SendActivity for CreatePostReport {
}
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl SendActivity for CreateCommentReport {
type Response = CommentReportResponse;
async fn send_activity(
request: &Self,
response: &Self::Response,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
let local_user_view =
get_local_user_view_from_jwt(&request.auth, context.pool(), context.secret()).await?;
Report::send(
ObjectId::new(response.comment_report_view.comment.ap_id.clone()),
ObjectId::from(response.comment_report_view.comment.ap_id.clone()),
&local_user_view.person.into(),
ObjectId::new(response.comment_report_view.community.actor_id.clone()),
ObjectId::from(response.comment_report_view.community.actor_id.clone()),
request.reason.to_string(),
context,
)
@ -82,7 +81,7 @@ impl Report {
actor: &ApubPerson,
community_id: ObjectId<ApubCommunity>,
reason: String,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
let community = community_id.dereference_local(context).await?;
let kind = FlagType::Flag;
@ -91,13 +90,13 @@ impl Report {
&context.settings().get_protocol_and_hostname(),
)?;
let report = Report {
actor: ObjectId::new(actor.actor_id()),
to: [ObjectId::new(community.actor_id())],
actor: actor.id().into(),
to: [community.id().into()],
object: object_id,
summary: reason,
kind,
id: id.clone(),
audience: Some(ObjectId::new(community.actor_id())),
audience: Some(community.id().into()),
};
let inbox = vec![community.shared_inbox_or_inbox()];
@ -105,7 +104,7 @@ impl Report {
}
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl ActivityHandler for Report {
type DataType = LemmyContext;
type Error = LemmyError;
@ -119,31 +118,17 @@ impl ActivityHandler for Report {
}
#[tracing::instrument(skip_all)]
async fn verify(
&self,
context: &Data<LemmyContext>,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let community = self.community(context, request_counter).await?;
verify_person_in_community(&self.actor, &community, context, request_counter).await?;
async fn verify(&self, context: &Data<Self::DataType>) -> Result<(), LemmyError> {
let community = self.community(context).await?;
verify_person_in_community(&self.actor, &community, context).await?;
Ok(())
}
#[tracing::instrument(skip_all)]
async fn receive(
self,
context: &Data<LemmyContext>,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let actor = self
.actor
.dereference(context, local_instance(context).await, request_counter)
.await?;
match self
.object
.dereference(context, local_instance(context).await, request_counter)
.await?
{
async fn receive(self, context: &Data<Self::DataType>) -> Result<(), LemmyError> {
insert_activity(&self.id, &self, false, true, context).await?;
let actor = self.actor.dereference(context).await?;
match self.object.dereference(context).await? {
PostOrComment::Post(post) => {
let report_form = PostReportForm {
creator_id: actor.id,

@ -7,17 +7,16 @@ use crate::{
verify_person_in_community,
},
activity_lists::AnnouncableActivities,
insert_activity,
objects::{community::ApubCommunity, person::ApubPerson},
protocol::{activities::community::update::UpdateCommunity, InCommunity},
ActorType,
SendActivity,
};
use activitypub_federation::{
core::object_id::ObjectId,
data::Data,
traits::{ActivityHandler, ApubObject},
config::Data,
kinds::{activity::UpdateType, public},
traits::{ActivityHandler, Actor, Object},
};
use activitystreams_kinds::{activity::UpdateType, public};
use lemmy_api_common::{
community::{CommunityResponse, EditCommunity, HideCommunity},
context::LemmyContext,
@ -28,14 +27,14 @@ use lemmy_db_schema::{source::community::Community, traits::Crud};
use lemmy_utils::error::LemmyError;
use url::Url;
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl SendActivity for EditCommunity {
type Response = CommunityResponse;
async fn send_activity(
request: &Self,
_response: &Self::Response,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
let local_user_view =
get_local_user_view_from_jwt(&request.auth, context.pool(), context.secret()).await?;
@ -49,20 +48,20 @@ impl UpdateCommunity {
pub async fn send(
community: ApubCommunity,
actor: &ApubPerson,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
let id = generate_activity_id(
UpdateType::Update,
&context.settings().get_protocol_and_hostname(),
)?;
let update = UpdateCommunity {
actor: ObjectId::new(actor.actor_id()),
actor: actor.id().into(),
to: vec![public()],
object: Box::new(community.clone().into_apub(context).await?),
cc: vec![community.actor_id()],
object: Box::new(community.clone().into_json(context).await?),
cc: vec![community.id()],
kind: UpdateType::Update,
id: id.clone(),
audience: Some(ObjectId::new(community.actor_id())),
audience: Some(community.id().into()),
};
let activity = AnnouncableActivities::UpdateCommunity(update);
@ -70,7 +69,7 @@ impl UpdateCommunity {
}
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl ActivityHandler for UpdateCommunity {
type DataType = LemmyContext;
type Error = LemmyError;
@ -84,39 +83,19 @@ impl ActivityHandler for UpdateCommunity {
}
#[tracing::instrument(skip_all)]
async fn verify(
&self,
context: &Data<LemmyContext>,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
async fn verify(&self, context: &Data<Self::DataType>) -> Result<(), LemmyError> {
verify_is_public(&self.to, &self.cc)?;
let community = self.community(context, request_counter).await?;
verify_person_in_community(&self.actor, &community, context, request_counter).await?;
verify_mod_action(
&self.actor,
self.object.id.inner(),
community.id,
context,
request_counter,
)
.await?;
ApubCommunity::verify(
&self.object,
&community.actor_id.clone().into(),
context,
request_counter,
)
.await?;
let community = self.community(context).await?;
verify_person_in_community(&self.actor, &community, context).await?;
verify_mod_action(&self.actor, self.object.id.inner(), community.id, context).await?;
ApubCommunity::verify(&self.object, &community.actor_id.clone().into(), context).await?;
Ok(())
}
#[tracing::instrument(skip_all)]
async fn receive(
self,
context: &Data<LemmyContext>,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let community = self.community(context, request_counter).await?;
async fn receive(self, context: &Data<Self::DataType>) -> Result<(), LemmyError> {
insert_activity(&self.id, &self, false, false, context).await?;
let community = self.community(context).await?;
let community_update_form = self.object.into_update_form();
@ -135,14 +114,14 @@ impl ActivityHandler for UpdateCommunity {
}
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl SendActivity for HideCommunity {
type Response = CommunityResponse;
async fn send_activity(
request: &Self,
_response: &Self::Response,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
let local_user_view =
get_local_user_view_from_jwt(&request.auth, context.pool(), context.secret()).await?;

@ -8,23 +8,22 @@ use crate::{
verify_person_in_community,
},
activity_lists::AnnouncableActivities,
local_instance,
insert_activity,
mentions::MentionOrValue,
objects::{comment::ApubComment, community::ApubCommunity, person::ApubPerson},
protocol::{
activities::{create_or_update::note::CreateOrUpdateNote, CreateOrUpdateType},
InCommunity,
},
ActorType,
SendActivity,
};
use activitypub_federation::{
core::object_id::ObjectId,
data::Data,
traits::{ActivityHandler, Actor, ApubObject},
utils::verify_domains_match,
config::Data,
fetch::object_id::ObjectId,
kinds::public,
protocol::verification::verify_domains_match,
traits::{ActivityHandler, Actor, Object},
};
use activitystreams_kinds::public;
use lemmy_api_common::{
comment::{CommentResponse, CreateComment, EditComment},
context::LemmyContext,
@ -44,14 +43,14 @@ use lemmy_db_schema::{
use lemmy_utils::error::LemmyError;
use url::Url;
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl SendActivity for CreateComment {
type Response = CommentResponse;
async fn send_activity(
_request: &Self,
response: &Self::Response,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
CreateOrUpdateNote::send(
&response.comment_view.comment,
@ -63,14 +62,14 @@ impl SendActivity for CreateComment {
}
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl SendActivity for EditComment {
type Response = CommentResponse;
async fn send_activity(
_request: &Self,
response: &Self::Response,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
CreateOrUpdateNote::send(
&response.comment_view.comment,
@ -88,7 +87,7 @@ impl CreateOrUpdateNote {
comment: &Comment,
person_id: PersonId,
kind: CreateOrUpdateType,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
// TODO: might be helpful to add a comment method to retrieve community directly
let post_id = comment.post_id;
@ -101,17 +100,17 @@ impl CreateOrUpdateNote {
kind.clone(),
&context.settings().get_protocol_and_hostname(),
)?;
let note = ApubComment(comment.clone()).into_apub(context).await?;
let note = ApubComment(comment.clone()).into_json(context).await?;
let create_or_update = CreateOrUpdateNote {
actor: ObjectId::new(person.actor_id()),
actor: person.id().into(),
to: vec![public()],
cc: note.cc.clone(),
tag: note.tag.clone(),
object: note,
kind,
id: id.clone(),
audience: Some(ObjectId::new(community.actor_id())),
audience: Some(community.id().into()),
};
let tagged_users: Vec<ObjectId<ApubPerson>> = create_or_update
@ -125,13 +124,11 @@ impl CreateOrUpdateNote {
}
})
.map(|t| t.href.clone())
.map(ObjectId::new)
.map(ObjectId::from)
.collect();
let mut inboxes = vec![];
for t in tagged_users {
let person = t
.dereference(context, local_instance(context).await, &mut 0)
.await?;
let person = t.dereference(context).await?;
inboxes.push(person.shared_inbox_or_inbox());
}
@ -140,7 +137,7 @@ impl CreateOrUpdateNote {
}
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl ActivityHandler for CreateOrUpdateNote {
type DataType = LemmyContext;
type Error = LemmyError;
@ -154,47 +151,37 @@ impl ActivityHandler for CreateOrUpdateNote {
}
#[tracing::instrument(skip_all)]
async fn verify(
&self,
context: &Data<LemmyContext>,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
async fn verify(&self, context: &Data<Self::DataType>) -> Result<(), LemmyError> {
verify_is_public(&self.to, &self.cc)?;
let post = self.object.get_parents(context, request_counter).await?.0;
let community = self.community(context, request_counter).await?;
let post = self.object.get_parents(context).await?.0;
let community = self.community(context).await?;
verify_person_in_community(&self.actor, &community, context, request_counter).await?;
verify_person_in_community(&self.actor, &community, context).await?;
verify_domains_match(self.actor.inner(), self.object.id.inner())?;
check_community_deleted_or_removed(&community)?;
check_post_deleted_or_removed(&post)?;
ApubComment::verify(&self.object, self.actor.inner(), context, request_counter).await?;
ApubComment::verify(&self.object, self.actor.inner(), context).await?;
Ok(())
}
#[tracing::instrument(skip_all)]
async fn receive(
self,
context: &Data<LemmyContext>,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
// Need to do this check here instead of Note::from_apub because we need the person who
async fn receive(self, context: &Data<Self::DataType>) -> Result<(), LemmyError> {
insert_activity(&self.id, &self, false, false, context).await?;
// Need to do this check here instead of Note::from_json because we need the person who
// send the activity, not the comment author.
let existing_comment = self.object.id.dereference_local(context).await.ok();
if let (Some(distinguished), Some(existing_comment)) =
(self.object.distinguished, existing_comment)
{
if distinguished != existing_comment.distinguished {
let creator = self
.actor
.dereference(context, local_instance(context).await, request_counter)
.await?;
let (post, _) = self.object.get_parents(context, request_counter).await?;
let creator = self.actor.dereference(context).await?;
let (post, _) = self.object.get_parents(context).await?;
is_mod_or_admin(context.pool(), creator.id, post.community_id).await?;
}
}
let comment = ApubComment::from_apub(self.object, context, request_counter).await?;
let comment = ApubComment::from_json(self.object, context).await?;
// author likes their own comment by default
let like_form = CommentLikeForm {
@ -206,14 +193,8 @@ impl ActivityHandler for CreateOrUpdateNote {
CommentLike::like(context.pool(), &like_form).await?;
let do_send_email = self.kind == CreateOrUpdateType::Create;
let recipients = get_comment_notif_recipients(
&self.actor,
&comment,
do_send_email,
context,
request_counter,
)
.await?;
let recipients =
get_comment_notif_recipients(&self.actor, &comment, do_send_email, context).await?;
let notif_type = match self.kind {
CreateOrUpdateType::Create => UserOperationCrud::CreateComment,
CreateOrUpdateType::Update => UserOperationCrud::EditComment,

@ -1,5 +1,5 @@
use crate::{local_instance, objects::person::ApubPerson};
use activitypub_federation::core::object_id::ObjectId;
use crate::objects::person::ApubPerson;
use activitypub_federation::{config::Data, fetch::object_id::ObjectId};
use lemmy_api_common::{context::LemmyContext, websocket::send::send_local_notifs};
use lemmy_db_schema::{
newtypes::LocalUserId,
@ -17,14 +17,11 @@ async fn get_comment_notif_recipients(
actor: &ObjectId<ApubPerson>,
comment: &Comment,
do_send_email: bool,
context: &LemmyContext,
request_counter: &mut i32,
context: &Data<LemmyContext>,
) -> Result<Vec<LocalUserId>, LemmyError> {
let post_id = comment.post_id;
let post = Post::read(context.pool(), post_id).await?;
let actor = actor
.dereference(context, local_instance(context).await, request_counter)
.await?;
let actor = actor.dereference(context).await?;
// Note:
// Although mentions could be gotten from the post tags (they are included there), or the ccs,

@ -8,21 +8,20 @@ use crate::{
verify_person_in_community,
},
activity_lists::AnnouncableActivities,
insert_activity,
objects::{community::ApubCommunity, person::ApubPerson, post::ApubPost},
protocol::{
activities::{create_or_update::page::CreateOrUpdatePage, CreateOrUpdateType},
InCommunity,
},
ActorType,
SendActivity,
};
use activitypub_federation::{
core::object_id::ObjectId,
data::Data,
traits::{ActivityHandler, ApubObject},
utils::{verify_domains_match, verify_urls_match},
config::Data,
kinds::public,
protocol::verification::{verify_domains_match, verify_urls_match},
traits::{ActivityHandler, Actor, Object},
};
use activitystreams_kinds::public;
use lemmy_api_common::{
context::LemmyContext,
post::{CreatePost, EditPost, PostResponse},
@ -40,14 +39,14 @@ use lemmy_db_schema::{
use lemmy_utils::error::LemmyError;
use url::Url;
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl SendActivity for CreatePost {
type Response = PostResponse;
async fn send_activity(
_request: &Self,
response: &Self::Response,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
CreateOrUpdatePage::send(
&response.post_view.post,
@ -59,14 +58,14 @@ impl SendActivity for CreatePost {
}
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl SendActivity for EditPost {
type Response = PostResponse;
async fn send_activity(
_request: &Self,
response: &Self::Response,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
CreateOrUpdatePage::send(
&response.post_view.post,
@ -84,20 +83,20 @@ impl CreateOrUpdatePage {
actor: &ApubPerson,
community: &ApubCommunity,
kind: CreateOrUpdateType,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<CreateOrUpdatePage, LemmyError> {
let id = generate_activity_id(
kind.clone(),
&context.settings().get_protocol_and_hostname(),
)?;
Ok(CreateOrUpdatePage {
actor: ObjectId::new(actor.actor_id()),
actor: actor.id().into(),
to: vec![public()],
object: post.into_apub(context).await?,
cc: vec![community.actor_id()],
object: post.into_json(context).await?,
cc: vec![community.id()],
kind,
id: id.clone(),
audience: Some(ObjectId::new(community.actor_id())),
audience: Some(community.id().into()),
})
}
@ -106,7 +105,7 @@ impl CreateOrUpdatePage {
post: &Post,
person_id: PersonId,
kind: CreateOrUpdateType,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
let post = ApubPost(post.clone());
let community_id = post.community_id;
@ -130,7 +129,7 @@ impl CreateOrUpdatePage {
}
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl ActivityHandler for CreateOrUpdatePage {
type DataType = LemmyContext;
type Error = LemmyError;
@ -144,14 +143,10 @@ impl ActivityHandler for CreateOrUpdatePage {
}
#[tracing::instrument(skip_all)]
async fn verify(
&self,
context: &Data<LemmyContext>,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
async fn verify(&self, context: &Data<LemmyContext>) -> Result<(), LemmyError> {
verify_is_public(&self.to, &self.cc)?;
let community = self.community(context, request_counter).await?;
verify_person_in_community(&self.actor, &community, context, request_counter).await?;
let community = self.community(context).await?;
verify_person_in_community(&self.actor, &community, context).await?;
check_community_deleted_or_removed(&community)?;
match self.kind {
@ -173,31 +168,21 @@ impl ActivityHandler for CreateOrUpdatePage {
CreateOrUpdateType::Update => {
let is_mod_action = self.object.is_mod_action(context).await?;
if is_mod_action {
verify_mod_action(
&self.actor,
self.object.id.inner(),
community.id,
context,
request_counter,
)
.await?;
verify_mod_action(&self.actor, self.object.id.inner(), community.id, context).await?;
} else {
verify_domains_match(self.actor.inner(), self.object.id.inner())?;
verify_urls_match(self.actor.inner(), self.object.creator()?.inner())?;
}
}
}
ApubPost::verify(&self.object, self.actor.inner(), context, request_counter).await?;
ApubPost::verify(&self.object, self.actor.inner(), context).await?;
Ok(())
}
#[tracing::instrument(skip_all)]
async fn receive(
self,
context: &Data<LemmyContext>,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let post = ApubPost::from_apub(self.object, context, request_counter).await?;
async fn receive(self, context: &Data<LemmyContext>) -> Result<(), LemmyError> {
insert_activity(&self.id, &self, false, false, context).await?;
let post = ApubPost::from_json(self.object, context).await?;
// author likes their own post by default
let like_form = PostLikeForm {

@ -1,18 +1,17 @@
use crate::{
activities::{generate_activity_id, send_lemmy_activity, verify_person},
insert_activity,
objects::{person::ApubPerson, private_message::ApubPrivateMessage},
protocol::activities::{
create_or_update::chat_message::CreateOrUpdateChatMessage,
CreateOrUpdateType,
},
ActorType,
SendActivity,
};
use activitypub_federation::{
core::object_id::ObjectId,
data::Data,
traits::{ActivityHandler, Actor, ApubObject},
utils::verify_domains_match,
config::Data,
protocol::verification::verify_domains_match,
traits::{ActivityHandler, Actor, Object},
};
use lemmy_api_common::{
context::LemmyContext,
@ -27,14 +26,14 @@ use lemmy_db_schema::{
use lemmy_utils::error::LemmyError;
use url::Url;
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl SendActivity for CreatePrivateMessage {
type Response = PrivateMessageResponse;
async fn send_activity(
_request: &Self,
response: &Self::Response,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
CreateOrUpdateChatMessage::send(
&response.private_message_view.private_message,
@ -45,14 +44,14 @@ impl SendActivity for CreatePrivateMessage {
.await
}
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl SendActivity for EditPrivateMessage {
type Response = PrivateMessageResponse;
async fn send_activity(
_request: &Self,
response: &Self::Response,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
CreateOrUpdateChatMessage::send(
&response.private_message_view.private_message,
@ -70,7 +69,7 @@ impl CreateOrUpdateChatMessage {
private_message: &PrivateMessage,
sender_id: PersonId,
kind: CreateOrUpdateType,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
let recipient_id = private_message.recipient_id;
let sender: ApubPerson = Person::read(context.pool(), sender_id).await?.into();
@ -82,10 +81,10 @@ impl CreateOrUpdateChatMessage {
)?;
let create_or_update = CreateOrUpdateChatMessage {
id: id.clone(),
actor: ObjectId::new(sender.actor_id()),
to: [ObjectId::new(recipient.actor_id())],
actor: sender.id().into(),
to: [recipient.id().into()],
object: ApubPrivateMessage(private_message.clone())
.into_apub(context)
.into_json(context)
.await?,
kind,
};
@ -94,7 +93,7 @@ impl CreateOrUpdateChatMessage {
}
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl ActivityHandler for CreateOrUpdateChatMessage {
type DataType = LemmyContext;
type Error = LemmyError;
@ -108,26 +107,18 @@ impl ActivityHandler for CreateOrUpdateChatMessage {
}
#[tracing::instrument(skip_all)]
async fn verify(
&self,
context: &Data<LemmyContext>,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
verify_person(&self.actor, context, request_counter).await?;
async fn verify(&self, context: &Data<Self::DataType>) -> Result<(), LemmyError> {
verify_person(&self.actor, context).await?;
verify_domains_match(self.actor.inner(), self.object.id.inner())?;
verify_domains_match(self.to[0].inner(), self.object.to[0].inner())?;
ApubPrivateMessage::verify(&self.object, self.actor.inner(), context, request_counter).await?;
ApubPrivateMessage::verify(&self.object, self.actor.inner(), context).await?;
Ok(())
}
#[tracing::instrument(skip_all)]
async fn receive(
self,
context: &Data<LemmyContext>,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let private_message =
ApubPrivateMessage::from_apub(self.object, context, request_counter).await?;
async fn receive(self, context: &Data<Self::DataType>) -> Result<(), LemmyError> {
insert_activity(&self.id, &self, false, true, context).await?;
let private_message = ApubPrivateMessage::from_json(self.object, context).await?;
let notif_type = match self.kind {
CreateOrUpdateType::Create => UserOperationCrud::CreatePrivateMessage,

@ -3,12 +3,11 @@ use crate::{
deletion::{receive_delete_action, verify_delete_activity, DeletableObjects},
generate_activity_id,
},
local_instance,
objects::{community::ApubCommunity, person::ApubPerson},
insert_activity,
objects::person::ApubPerson,
protocol::{activities::deletion::delete::Delete, IdOrNestedObject},
};
use activitypub_federation::{core::object_id::ObjectId, data::Data, traits::ActivityHandler};
use activitystreams_kinds::activity::DeleteType;
use activitypub_federation::{config::Data, kinds::activity::DeleteType, traits::ActivityHandler};
use lemmy_api_common::{
context::LemmyContext,
websocket::{
@ -35,7 +34,7 @@ use lemmy_db_schema::{
use lemmy_utils::error::LemmyError;
use url::Url;
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl ActivityHandler for Delete {
type DataType = LemmyContext;
type Error = LemmyError;
@ -49,21 +48,14 @@ impl ActivityHandler for Delete {
}
#[tracing::instrument(skip_all)]
async fn verify(
&self,
context: &Data<LemmyContext>,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
verify_delete_activity(self, self.summary.is_some(), context, request_counter).await?;
async fn verify(&self, context: &Data<Self::DataType>) -> Result<(), LemmyError> {
verify_delete_activity(self, self.summary.is_some(), context).await?;
Ok(())
}
#[tracing::instrument(skip_all)]
async fn receive(
self,
context: &Data<LemmyContext>,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
async fn receive(self, context: &Data<LemmyContext>) -> Result<(), LemmyError> {
insert_activity(&self.id, &self, false, false, context).await?;
if let Some(reason) = self.summary {
// We set reason to empty string if it doesn't exist, to distinguish between delete and
// remove. Here we change it back to option, so we don't write it to db.
@ -73,24 +65,14 @@ impl ActivityHandler for Delete {
Some(reason)
};
receive_remove_action(
&self
.actor
.dereference(context, local_instance(context).await, request_counter)
.await?,
&self.actor.dereference(context).await?,
self.object.id(),
reason,
context,
)
.await
} else {
receive_delete_action(
self.object.id(),
&self.actor,
true,
context,
request_counter,
)
.await
receive_delete_action(self.object.id(), &self.actor, true, context).await
}
}
}
@ -102,7 +84,7 @@ impl Delete {
to: Url,
community: Option<&Community>,
summary: Option<String>,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<Delete, LemmyError> {
let id = generate_activity_id(
DeleteType::Delete,
@ -110,14 +92,14 @@ impl Delete {
)?;
let cc: Option<Url> = community.map(|c| c.actor_id.clone().into());
Ok(Delete {
actor: ObjectId::new(actor.actor_id.clone()),
actor: actor.actor_id.clone().into(),
to: vec![to],
object: IdOrNestedObject::Id(object.id()),
cc: cc.into_iter().collect(),
kind: DeleteType::Delete,
summary,
id,
audience: community.map(|c| ObjectId::<ApubCommunity>::new(c.actor_id.clone())),
audience: community.map(|c| c.actor_id.clone().into()),
})
}
}
@ -127,7 +109,7 @@ pub(in crate::activities) async fn receive_remove_action(
actor: &ApubPerson,
object: &Url,
reason: Option<String>,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
use UserOperationCrud::*;
match DeletableObjects::read_from_db(object, context).await? {

@ -1,17 +1,16 @@
use crate::{
activities::{generate_activity_id, send_lemmy_activity, verify_is_public, verify_person},
local_instance,
insert_activity,
objects::{instance::remote_instance_inboxes, person::ApubPerson},
protocol::activities::deletion::delete_user::DeleteUser,
SendActivity,
};
use activitypub_federation::{
core::object_id::ObjectId,
data::Data,
traits::ActivityHandler,
utils::verify_urls_match,
config::Data,
kinds::{activity::DeleteType, public},
protocol::verification::verify_urls_match,
traits::{ActivityHandler, Actor},
};
use activitystreams_kinds::{activity::DeleteType, public};
use lemmy_api_common::{
context::LemmyContext,
person::{DeleteAccount, DeleteAccountResponse},
@ -20,14 +19,14 @@ use lemmy_api_common::{
use lemmy_utils::error::LemmyError;
use url::Url;
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl SendActivity for DeleteAccount {
type Response = DeleteAccountResponse;
async fn send_activity(
request: &Self,
_response: &Self::Response,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
let local_user_view =
get_local_user_view_from_jwt(&request.auth, context.pool(), context.secret()).await?;
@ -40,15 +39,14 @@ impl SendActivity for DeleteAccount {
)
.await?;
let actor_id = ObjectId::new(actor.actor_id.clone());
let id = generate_activity_id(
DeleteType::Delete,
&context.settings().get_protocol_and_hostname(),
)?;
let delete = DeleteUser {
actor: actor_id.clone(),
actor: actor.id().into(),
to: vec![public()],
object: actor_id,
object: actor.id().into(),
kind: DeleteType::Delete,
id: id.clone(),
cc: vec![],
@ -62,7 +60,7 @@ impl SendActivity for DeleteAccount {
/// This can be separate from Delete activity because it doesn't need to be handled in shared inbox
/// (cause instance actor doesn't have shared inbox).
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl ActivityHandler for DeleteUser {
type DataType = LemmyContext;
type Error = LemmyError;
@ -75,26 +73,16 @@ impl ActivityHandler for DeleteUser {
self.actor.inner()
}
async fn verify(
&self,
context: &Data<LemmyContext>,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
async fn verify(&self, context: &Data<Self::DataType>) -> Result<(), LemmyError> {
verify_is_public(&self.to, &[])?;
verify_person(&self.actor, context, request_counter).await?;
verify_person(&self.actor, context).await?;
verify_urls_match(self.actor.inner(), self.object.inner())?;
Ok(())
}
async fn receive(
self,
context: &Data<LemmyContext>,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let actor = self
.actor
.dereference(context, local_instance(context).await, request_counter)
.await?;
async fn receive(self, context: &Data<Self::DataType>) -> Result<(), LemmyError> {
insert_activity(&self.id, &self, false, false, context).await?;
let actor = self.actor.dereference(context).await?;
delete_user_account(
actor.id,
context.pool(),

@ -8,7 +8,6 @@ use crate::{
verify_person_in_community,
},
activity_lists::AnnouncableActivities,
local_instance,
objects::{
comment::ApubComment,
community::ApubCommunity,
@ -20,15 +19,15 @@ use crate::{
activities::deletion::{delete::Delete, undo_delete::UndoDelete},
InCommunity,
},
ActorType,
SendActivity,
};
use activitypub_federation::{
core::object_id::ObjectId,
traits::{Actor, ApubObject},
utils::verify_domains_match,
config::Data,
fetch::object_id::ObjectId,
kinds::public,
protocol::verification::verify_domains_match,
traits::{Actor, Object},
};
use activitystreams_kinds::public;
use lemmy_api_common::{
comment::{CommentResponse, DeleteComment, RemoveComment},
community::{CommunityResponse, DeleteCommunity, RemoveCommunity},
@ -64,14 +63,14 @@ pub mod delete;
pub mod delete_user;
pub mod undo_delete;
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl SendActivity for DeletePost {
type Response = PostResponse;
async fn send_activity(
request: &Self,
response: &Self::Response,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
let local_user_view =
get_local_user_view_from_jwt(&request.auth, context.pool(), context.secret()).await?;
@ -89,14 +88,14 @@ impl SendActivity for DeletePost {
}
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl SendActivity for RemovePost {
type Response = PostResponse;
async fn send_activity(
request: &Self,
response: &Self::Response,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
let local_user_view =
get_local_user_view_from_jwt(&request.auth, context.pool(), context.secret()).await?;
@ -114,14 +113,14 @@ impl SendActivity for RemovePost {
}
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl SendActivity for DeleteComment {
type Response = CommentResponse;
async fn send_activity(
request: &Self,
response: &Self::Response,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
let community_id = response.comment_view.community.id;
let community = Community::read(context.pool(), community_id).await?;
@ -132,14 +131,14 @@ impl SendActivity for DeleteComment {
}
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl SendActivity for RemoveComment {
type Response = CommentResponse;
async fn send_activity(
request: &Self,
response: &Self::Response,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
let local_user_view =
get_local_user_view_from_jwt(&request.auth, context.pool(), context.secret()).await?;
@ -158,14 +157,14 @@ impl SendActivity for RemoveComment {
}
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl SendActivity for DeletePrivateMessage {
type Response = PrivateMessageResponse;
async fn send_activity(
request: &Self,
response: &Self::Response,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
let local_user_view =
get_local_user_view_from_jwt(&request.auth, context.pool(), context.secret()).await?;
@ -179,14 +178,14 @@ impl SendActivity for DeletePrivateMessage {
}
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl SendActivity for DeleteCommunity {
type Response = CommunityResponse;
async fn send_activity(
request: &Self,
_response: &Self::Response,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
let local_user_view =
get_local_user_view_from_jwt(&request.auth, context.pool(), context.secret()).await?;
@ -204,14 +203,14 @@ impl SendActivity for DeleteCommunity {
}
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl SendActivity for RemoveCommunity {
type Response = CommunityResponse;
async fn send_activity(
request: &Self,
_response: &Self::Response,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
let local_user_view =
get_local_user_view_from_jwt(&request.auth, context.pool(), context.secret()).await?;
@ -238,7 +237,7 @@ async fn send_apub_delete_in_community(
object: DeletableObjects,
reason: Option<String>,
deleted: bool,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
let actor = ApubPerson::from(actor);
let is_mod_action = reason.is_some();
@ -265,7 +264,7 @@ async fn send_apub_delete_private_message(
actor: &ApubPerson,
pm: PrivateMessage,
deleted: bool,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
let recipient_id = pm.recipient_id;
let recipient: ApubPerson = Person::read(context.pool(), recipient_id).await?.into();
@ -273,10 +272,10 @@ async fn send_apub_delete_private_message(
let deletable = DeletableObjects::PrivateMessage(pm.into());
let inbox = vec![recipient.shared_inbox_or_inbox()];
if deleted {
let delete = Delete::new(actor, deletable, recipient.actor_id(), None, None, context)?;
let delete = Delete::new(actor, deletable, recipient.id(), None, None, context)?;
send_lemmy_activity(context, delete, actor, inbox, true).await?;
} else {
let undo = UndoDelete::new(actor, deletable, recipient.actor_id(), None, None, context)?;
let undo = UndoDelete::new(actor, deletable, recipient.id(), None, None, context)?;
send_lemmy_activity(context, undo, actor, inbox, true).await?;
};
Ok(())
@ -293,18 +292,18 @@ impl DeletableObjects {
#[tracing::instrument(skip_all)]
pub(crate) async fn read_from_db(
ap_id: &Url,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<DeletableObjects, LemmyError> {
if let Some(c) = ApubCommunity::read_from_apub_id(ap_id.clone(), context).await? {
if let Some(c) = ApubCommunity::read_from_id(ap_id.clone(), context).await? {
return Ok(DeletableObjects::Community(c));
}
if let Some(p) = ApubPost::read_from_apub_id(ap_id.clone(), context).await? {
if let Some(p) = ApubPost::read_from_id(ap_id.clone(), context).await? {
return Ok(DeletableObjects::Post(p));
}
if let Some(c) = ApubComment::read_from_apub_id(ap_id.clone(), context).await? {
if let Some(c) = ApubComment::read_from_id(ap_id.clone(), context).await? {
return Ok(DeletableObjects::Comment(c));
}
if let Some(p) = ApubPrivateMessage::read_from_apub_id(ap_id.clone(), context).await? {
if let Some(p) = ApubPrivateMessage::read_from_id(ap_id.clone(), context).await? {
return Ok(DeletableObjects::PrivateMessage(p));
}
Err(diesel::NotFound.into())
@ -312,7 +311,7 @@ impl DeletableObjects {
pub(crate) fn id(&self) -> Url {
match self {
DeletableObjects::Community(c) => c.actor_id(),
DeletableObjects::Community(c) => c.id(),
DeletableObjects::Comment(c) => c.ap_id.clone().into(),
DeletableObjects::Post(p) => p.ap_id.clone().into(),
DeletableObjects::PrivateMessage(p) => p.ap_id.clone().into(),
@ -324,8 +323,7 @@ impl DeletableObjects {
pub(in crate::activities) async fn verify_delete_activity(
activity: &Delete,
is_mod_action: bool,
context: &LemmyContext,
request_counter: &mut i32,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
let object = DeletableObjects::read_from_db(activity.object.id(), context).await?;
match object {
@ -334,27 +332,19 @@ pub(in crate::activities) async fn verify_delete_activity(
if community.local {
// can only do this check for local community, in remote case it would try to fetch the
// deleted community (which fails)
verify_person_in_community(&activity.actor, &community, context, request_counter).await?;
verify_person_in_community(&activity.actor, &community, context).await?;
}
// community deletion is always a mod (or admin) action
verify_mod_action(
&activity.actor,
activity.object.id(),
community.id,
context,
request_counter,
)
.await?;
verify_mod_action(&activity.actor, activity.object.id(), community.id, context).await?;
}
DeletableObjects::Post(p) => {
verify_is_public(&activity.to, &[])?;
verify_delete_post_or_comment(
&activity.actor,
&p.ap_id.clone().into(),
&activity.community(context, request_counter).await?,
&activity.community(context).await?,
is_mod_action,
context,
request_counter,
)
.await?;
}
@ -363,15 +353,14 @@ pub(in crate::activities) async fn verify_delete_activity(
verify_delete_post_or_comment(
&activity.actor,
&c.ap_id.clone().into(),
&activity.community(context, request_counter).await?,
&activity.community(context).await?,
is_mod_action,
context,
request_counter,
)
.await?;
}
DeletableObjects::PrivateMessage(_) => {
verify_person(&activity.actor, context, request_counter).await?;
verify_person(&activity.actor, context).await?;
verify_domains_match(activity.actor.inner(), activity.object.id())?;
}
}
@ -384,12 +373,11 @@ async fn verify_delete_post_or_comment(
object_id: &Url,
community: &ApubCommunity,
is_mod_action: bool,
context: &LemmyContext,
request_counter: &mut i32,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
verify_person_in_community(actor, community, context, request_counter).await?;
verify_person_in_community(actor, community, context).await?;
if is_mod_action {
verify_mod_action(actor, object_id, community.id, context, request_counter).await?;
verify_mod_action(actor, object_id, community.id, context).await?;
} else {
// domain of post ap_id and post.creator ap_id are identical, so we just check the former
verify_domains_match(actor.inner(), object_id)?;
@ -403,17 +391,12 @@ async fn receive_delete_action(
object: &Url,
actor: &ObjectId<ApubPerson>,
deleted: bool,
context: &LemmyContext,
request_counter: &mut i32,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
match DeletableObjects::read_from_db(object, context).await? {
DeletableObjects::Community(community) => {
if community.local {
let mod_: Person = actor
.dereference(context, local_instance(context).await, request_counter)
.await?
.deref()
.clone();
let mod_: Person = actor.dereference(context).await?.deref().clone();
let object = DeletableObjects::Community(community.clone());
let c: Community = community.deref().deref().clone();
send_apub_delete_in_community(mod_, c, object, None, true, context).await?;

@ -3,12 +3,11 @@ use crate::{
deletion::{receive_delete_action, verify_delete_activity, DeletableObjects},
generate_activity_id,
},
local_instance,
objects::{community::ApubCommunity, person::ApubPerson},
insert_activity,
objects::person::ApubPerson,
protocol::activities::deletion::{delete::Delete, undo_delete::UndoDelete},
};
use activitypub_federation::{core::object_id::ObjectId, data::Data, traits::ActivityHandler};
use activitystreams_kinds::activity::UndoType;
use activitypub_federation::{config::Data, kinds::activity::UndoType, traits::ActivityHandler};
use lemmy_api_common::{
context::LemmyContext,
websocket::{
@ -35,7 +34,7 @@ use lemmy_db_schema::{
use lemmy_utils::error::LemmyError;
use url::Url;
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl ActivityHandler for UndoDelete {
type DataType = LemmyContext;
type Error = LemmyError;
@ -48,48 +47,24 @@ impl ActivityHandler for UndoDelete {
self.actor.inner()
}
#[tracing::instrument(skip_all)]
async fn verify(
&self,
context: &Data<LemmyContext>,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
self.object.verify(context, request_counter).await?;
verify_delete_activity(
&self.object,
self.object.summary.is_some(),
context,
request_counter,
)
.await?;
async fn verify(&self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
self.object.verify(data).await?;
verify_delete_activity(&self.object, self.object.summary.is_some(), data).await?;
Ok(())
}
#[tracing::instrument(skip_all)]
async fn receive(
self,
context: &Data<LemmyContext>,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
async fn receive(self, context: &Data<LemmyContext>) -> Result<(), LemmyError> {
insert_activity(&self.id, &self, false, false, context).await?;
if self.object.summary.is_some() {
UndoDelete::receive_undo_remove_action(
&self
.actor
.dereference(context, local_instance(context).await, request_counter)
.await?,
&self.actor.dereference(context).await?,
self.object.object.id(),
context,
)
.await
} else {
receive_delete_action(
self.object.object.id(),
&self.actor,
false,
context,
request_counter,
)
.await
receive_delete_action(self.object.object.id(), &self.actor, false, context).await
}
}
}
@ -102,7 +77,7 @@ impl UndoDelete {
to: Url,
community: Option<&Community>,
summary: Option<String>,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<UndoDelete, LemmyError> {
let object = Delete::new(actor, object, to.clone(), community, summary, context)?;
@ -112,13 +87,13 @@ impl UndoDelete {
)?;
let cc: Option<Url> = community.map(|c| c.actor_id.clone().into());
Ok(UndoDelete {
actor: ObjectId::new(actor.actor_id.clone()),
actor: actor.actor_id.clone().into(),
to: vec![to],
object,
cc: cc.into_iter().collect(),
kind: UndoType::Undo,
id,
audience: community.map(|c| ObjectId::<ApubCommunity>::new(c.actor_id.clone())),
audience: community.map(|c| c.actor_id.clone().into()),
})
}
@ -126,7 +101,7 @@ impl UndoDelete {
pub(in crate::activities) async fn receive_undo_remove_action(
actor: &ApubPerson,
object: &Url,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
use UserOperationCrud::*;
match DeletableObjects::read_from_db(object, context).await? {

@ -1,16 +1,14 @@
use crate::{
activities::{generate_activity_id, send_lemmy_activity},
local_instance,
insert_activity,
protocol::activities::following::{accept::AcceptFollow, follow::Follow},
ActorType,
};
use activitypub_federation::{
core::object_id::ObjectId,
data::Data,
config::Data,
kinds::activity::AcceptType,
protocol::verification::verify_urls_match,
traits::{ActivityHandler, Actor},
utils::verify_urls_match,
};
use activitystreams_kinds::activity::AcceptType;
use lemmy_api_common::{
community::CommunityResponse,
context::LemmyContext,
@ -27,19 +25,11 @@ use url::Url;
impl AcceptFollow {
#[tracing::instrument(skip_all)]
pub async fn send(
follow: Follow,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
pub async fn send(follow: Follow, context: &Data<LemmyContext>) -> Result<(), LemmyError> {
let user_or_community = follow.object.dereference_local(context).await?;
let person = follow
.actor
.clone()
.dereference(context, local_instance(context).await, request_counter)
.await?;
let person = follow.actor.clone().dereference(context).await?;
let accept = AcceptFollow {
actor: ObjectId::new(user_or_community.actor_id()),
actor: user_or_community.id().into(),
object: follow,
kind: AcceptType::Accept,
id: generate_activity_id(
@ -53,7 +43,7 @@ impl AcceptFollow {
}
/// Handle accepted follows
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl ActivityHandler for AcceptFollow {
type DataType = LemmyContext;
type Error = LemmyError;
@ -67,31 +57,17 @@ impl ActivityHandler for AcceptFollow {
}
#[tracing::instrument(skip_all)]
async fn verify(
&self,
context: &Data<LemmyContext>,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
async fn verify(&self, context: &Data<LemmyContext>) -> Result<(), LemmyError> {
verify_urls_match(self.actor.inner(), self.object.object.inner())?;
self.object.verify(context, request_counter).await?;
self.object.verify(context).await?;
Ok(())
}
#[tracing::instrument(skip_all)]
async fn receive(
self,
context: &Data<LemmyContext>,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let community = self
.actor
.dereference(context, local_instance(context).await, request_counter)
.await?;
let person = self
.object
.actor
.dereference(context, local_instance(context).await, request_counter)
.await?;
async fn receive(self, context: &Data<LemmyContext>) -> Result<(), LemmyError> {
insert_activity(&self.id, &self, false, true, context).await?;
let community = self.actor.dereference(context).await?;
let person = self.object.actor.dereference(context).await?;
// This will throw an error if no follow was requested
let community_id = community.id;
let person_id = person.id;

@ -6,22 +6,20 @@ use crate::{
verify_person_in_community,
},
fetcher::user_or_community::UserOrCommunity,
local_instance,
insert_activity,
objects::{community::ApubCommunity, person::ApubPerson},
protocol::activities::following::{
accept::AcceptFollow,
follow::Follow,
undo_follow::UndoFollow,
},
ActorType,
SendActivity,
};
use activitypub_federation::{
core::object_id::ObjectId,
data::Data,
config::Data,
kinds::activity::FollowType,
traits::{ActivityHandler, Actor},
};
use activitystreams_kinds::activity::FollowType;
use lemmy_api_common::{
community::{BlockCommunity, BlockCommunityResponse},
context::LemmyContext,
@ -41,11 +39,11 @@ impl Follow {
pub(in crate::activities::following) fn new(
actor: &ApubPerson,
community: &ApubCommunity,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<Follow, LemmyError> {
Ok(Follow {
actor: ObjectId::new(actor.actor_id()),
object: ObjectId::new(community.actor_id()),
actor: actor.id().into(),
object: community.id().into(),
kind: FollowType::Follow,
id: generate_activity_id(
FollowType::Follow,
@ -58,7 +56,7 @@ impl Follow {
pub async fn send(
actor: &ApubPerson,
community: &ApubCommunity,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
let community_follower_form = CommunityFollowerForm {
community_id: community.id,
@ -75,7 +73,7 @@ impl Follow {
}
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl ActivityHandler for Follow {
type DataType = LemmyContext;
type Error = LemmyError;
@ -89,36 +87,20 @@ impl ActivityHandler for Follow {
}
#[tracing::instrument(skip_all)]
async fn verify(
&self,
context: &Data<LemmyContext>,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
verify_person(&self.actor, context, request_counter).await?;
let object = self
.object
.dereference(context, local_instance(context).await, request_counter)
.await?;
async fn verify(&self, context: &Data<LemmyContext>) -> Result<(), LemmyError> {
verify_person(&self.actor, context).await?;
let object = self.object.dereference(context).await?;
if let UserOrCommunity::Community(c) = object {
verify_person_in_community(&self.actor, &c, context, request_counter).await?;
verify_person_in_community(&self.actor, &c, context).await?;
}
Ok(())
}
#[tracing::instrument(skip_all)]
async fn receive(
self,
context: &Data<LemmyContext>,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let actor = self
.actor
.dereference(context, local_instance(context).await, request_counter)
.await?;
let object = self
.object
.dereference(context, local_instance(context).await, request_counter)
.await?;
async fn receive(self, context: &Data<LemmyContext>) -> Result<(), LemmyError> {
insert_activity(&self.id, &self, false, true, context).await?;
let actor = self.actor.dereference(context).await?;
let object = self.object.dereference(context).await?;
match object {
UserOrCommunity::User(u) => {
let form = PersonFollowerForm {
@ -138,18 +120,18 @@ impl ActivityHandler for Follow {
}
}
AcceptFollow::send(self, context, request_counter).await
AcceptFollow::send(self, context).await
}
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl SendActivity for BlockCommunity {
type Response = BlockCommunityResponse;
async fn send_activity(
request: &Self,
_response: &Self::Response,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
let local_user_view =
get_local_user_view_from_jwt(&request.auth, context.pool(), context.secret()).await?;

@ -3,6 +3,7 @@ use crate::{
protocol::activities::following::{follow::Follow, undo_follow::UndoFollow},
SendActivity,
};
use activitypub_federation::config::Data;
use lemmy_api_common::{
community::{CommunityResponse, FollowCommunity},
context::LemmyContext,
@ -15,14 +16,14 @@ pub mod accept;
pub mod follow;
pub mod undo_follow;
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl SendActivity for FollowCommunity {
type Response = CommunityResponse;
async fn send_activity(
request: &Self,
_response: &Self::Response,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
let local_user_view =
get_local_user_view_from_jwt(&request.auth, context.pool(), context.secret()).await?;

@ -1,18 +1,16 @@
use crate::{
activities::{generate_activity_id, send_lemmy_activity, verify_person},
fetcher::user_or_community::UserOrCommunity,
local_instance,
insert_activity,
objects::{community::ApubCommunity, person::ApubPerson},
protocol::activities::following::{follow::Follow, undo_follow::UndoFollow},
ActorType,
};
use activitypub_federation::{
core::object_id::ObjectId,
data::Data,
config::Data,
kinds::activity::UndoType,
protocol::verification::verify_urls_match,
traits::{ActivityHandler, Actor},
utils::verify_urls_match,
};
use activitystreams_kinds::activity::UndoType;
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::{
source::{
@ -29,11 +27,11 @@ impl UndoFollow {
pub async fn send(
actor: &ApubPerson,
community: &ApubCommunity,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
let object = Follow::new(actor, community, context)?;
let undo = UndoFollow {
actor: ObjectId::new(actor.actor_id()),
actor: actor.id().into(),
object,
kind: UndoType::Undo,
id: generate_activity_id(
@ -46,7 +44,7 @@ impl UndoFollow {
}
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl ActivityHandler for UndoFollow {
type DataType = LemmyContext;
type Error = LemmyError;
@ -60,32 +58,18 @@ impl ActivityHandler for UndoFollow {
}
#[tracing::instrument(skip_all)]
async fn verify(
&self,
context: &Data<LemmyContext>,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
async fn verify(&self, context: &Data<LemmyContext>) -> Result<(), LemmyError> {
verify_urls_match(self.actor.inner(), self.object.actor.inner())?;
verify_person(&self.actor, context, request_counter).await?;
self.object.verify(context, request_counter).await?;
verify_person(&self.actor, context).await?;
self.object.verify(context).await?;
Ok(())
}
#[tracing::instrument(skip_all)]
async fn receive(
self,
context: &Data<LemmyContext>,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let person = self
.actor
.dereference(context, local_instance(context).await, request_counter)
.await?;
let object = self
.object
.object
.dereference(context, local_instance(context).await, request_counter)
.await?;
async fn receive(self, context: &Data<LemmyContext>) -> Result<(), LemmyError> {
insert_activity(&self.id, &self, false, true, context).await?;
let person = self.actor.dereference(context).await?;
let object = self.object.object.dereference(context).await?;
match object {
UserOrCommunity::User(u) => {

@ -1,22 +1,19 @@
use crate::{
insert_activity,
local_instance,
objects::{community::ApubCommunity, person::ApubPerson},
ActorType,
CONTEXT,
};
use activitypub_federation::{
core::{activity_queue::send_activity, object_id::ObjectId},
deser::context::WithContext,
activity_queue::send_activity,
config::Data,
fetch::object_id::ObjectId,
kinds::public,
protocol::context::WithContext,
traits::{ActivityHandler, Actor},
};
use activitystreams_kinds::public;
use anyhow::anyhow;
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::{
newtypes::CommunityId,
source::{community::Community, local_site::LocalSite},
};
use lemmy_db_schema::{newtypes::CommunityId, source::community::Community};
use lemmy_db_views_actor::structs::{CommunityPersonBanView, CommunityView};
use lemmy_utils::error::LemmyError;
use serde::Serialize;
@ -38,12 +35,9 @@ pub mod voting;
#[tracing::instrument(skip_all)]
async fn verify_person(
person_id: &ObjectId<ApubPerson>,
context: &LemmyContext,
request_counter: &mut i32,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
let person = person_id
.dereference(context, local_instance(context).await, request_counter)
.await?;
let person = person_id.dereference(context).await?;
if person.banned {
let err = anyhow!("Person {} is banned", person_id);
return Err(LemmyError::from_error_message(err, "banned"));
@ -57,12 +51,9 @@ async fn verify_person(
pub(crate) async fn verify_person_in_community(
person_id: &ObjectId<ApubPerson>,
community: &ApubCommunity,
context: &LemmyContext,
request_counter: &mut i32,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
let person = person_id
.dereference(context, local_instance(context).await, request_counter)
.await?;
let person = person_id.dereference(context).await?;
if person.banned {
return Err(LemmyError::from_message("Person is banned from site"));
}
@ -88,12 +79,9 @@ pub(crate) async fn verify_mod_action(
mod_id: &ObjectId<ApubPerson>,
object_id: &Url,
community_id: CommunityId,
context: &LemmyContext,
request_counter: &mut i32,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
let mod_ = mod_id
.dereference(context, local_instance(context).await, request_counter)
.await?;
let mod_ = mod_id.dereference(context).await?;
let is_mod_or_admin =
CommunityView::is_mod_or_admin(context.pool(), mod_.id, community_id).await?;
@ -159,39 +147,22 @@ where
#[tracing::instrument(skip_all)]
async fn send_lemmy_activity<Activity, ActorT>(
context: &LemmyContext,
data: &Data<LemmyContext>,
activity: Activity,
actor: &ActorT,
inbox: Vec<Url>,
sensitive: bool,
) -> Result<(), LemmyError>
where
Activity: ActivityHandler + Serialize,
ActorT: Actor + ActorType,
Activity: ActivityHandler + Serialize + Send + Sync + Clone,
ActorT: Actor,
Activity: ActivityHandler<Error = LemmyError>,
{
let federation_enabled = LocalSite::read(context.pool())
.await
.map(|l| l.federation_enabled)
.unwrap_or(false);
if !federation_enabled {
return Ok(());
}
info!("Sending activity {}", activity.id().to_string());
let activity = WithContext::new(activity, CONTEXT.deref().clone());
let object_value = serde_json::to_value(&activity)?;
insert_activity(activity.id(), object_value, true, sensitive, context.pool()).await?;
send_activity(
activity,
actor.get_public_key(),
actor.private_key().expect("actor has private key"),
inbox,
local_instance(context).await,
)
.await?;
insert_activity(activity.id(), &activity, true, sensitive, data).await?;
send_activity(activity, actor, inbox, data).await?;
Ok(())
}

@ -9,7 +9,7 @@ use crate::{
},
SendActivity,
};
use activitypub_federation::core::object_id::ObjectId;
use activitypub_federation::{config::Data, fetch::object_id::ObjectId};
use lemmy_api_common::{
comment::{CommentResponse, CreateCommentLike},
context::LemmyContext,
@ -36,16 +36,16 @@ use lemmy_utils::error::LemmyError;
pub mod undo_vote;
pub mod vote;
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl SendActivity for CreatePostLike {
type Response = PostResponse;
async fn send_activity(
request: &Self,
response: &Self::Response,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
let object_id = ObjectId::new(response.post_view.post.ap_id.clone());
let object_id = ObjectId::from(response.post_view.post.ap_id.clone());
let community_id = response.post_view.community.id;
send_activity(
object_id,
@ -58,16 +58,16 @@ impl SendActivity for CreatePostLike {
}
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl SendActivity for CreateCommentLike {
type Response = CommentResponse;
async fn send_activity(
request: &Self,
response: &Self::Response,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
let object_id = ObjectId::new(response.comment_view.comment.ap_id.clone());
let object_id = ObjectId::from(response.comment_view.comment.ap_id.clone());
let community_id = response.comment_view.community.id;
send_activity(
object_id,
@ -85,7 +85,7 @@ async fn send_activity(
community_id: CommunityId,
score: i16,
jwt: &Sensitive<String>,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
let community = Community::read(context.pool(), community_id).await?.into();
let local_user_view = get_local_user_view_from_jwt(jwt, context.pool(), context.secret()).await?;
@ -112,7 +112,7 @@ async fn vote_comment(
vote_type: &VoteType,
actor: ApubPerson,
comment: &ApubComment,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
let comment_id = comment.id;
let like_form = CommentLikeForm {
@ -134,7 +134,7 @@ async fn vote_post(
vote_type: &VoteType,
actor: ApubPerson,
post: &ApubPost,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
let post_id = post.id;
let like_form = PostLikeForm {
@ -154,7 +154,7 @@ async fn vote_post(
async fn undo_vote_comment(
actor: ApubPerson,
comment: &ApubComment,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
let comment_id = comment.id;
let person_id = actor.id;
@ -168,7 +168,7 @@ async fn undo_vote_comment(
async fn undo_vote_post(
actor: ApubPerson,
post: &ApubPost,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
let post_id = post.id;
let person_id = actor.id;

@ -4,22 +4,20 @@ use crate::{
verify_person_in_community,
voting::{undo_vote_comment, undo_vote_post},
},
local_instance,
insert_activity,
objects::{community::ApubCommunity, person::ApubPerson},
protocol::{
activities::voting::{undo_vote::UndoVote, vote::Vote},
InCommunity,
},
ActorType,
PostOrComment,
};
use activitypub_federation::{
core::object_id::ObjectId,
data::Data,
traits::ActivityHandler,
utils::verify_urls_match,
config::Data,
kinds::activity::UndoType,
protocol::verification::verify_urls_match,
traits::{ActivityHandler, Actor},
};
use activitystreams_kinds::activity::UndoType;
use lemmy_api_common::context::LemmyContext;
use lemmy_utils::error::LemmyError;
use url::Url;
@ -29,22 +27,22 @@ impl UndoVote {
vote: Vote,
actor: &ApubPerson,
community: &ApubCommunity,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<Self, LemmyError> {
Ok(UndoVote {
actor: ObjectId::new(actor.actor_id()),
actor: actor.id().into(),
object: vote,
kind: UndoType::Undo,
id: generate_activity_id(
UndoType::Undo,
&context.settings().get_protocol_and_hostname(),
)?,
audience: Some(ObjectId::new(community.actor_id())),
audience: Some(community.id().into()),
})
}
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl ActivityHandler for UndoVote {
type DataType = LemmyContext;
type Error = LemmyError;
@ -58,33 +56,19 @@ impl ActivityHandler for UndoVote {
}
#[tracing::instrument(skip_all)]
async fn verify(
&self,
context: &Data<LemmyContext>,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let community = self.community(context, request_counter).await?;
verify_person_in_community(&self.actor, &community, context, request_counter).await?;
async fn verify(&self, context: &Data<LemmyContext>) -> Result<(), LemmyError> {
let community = self.community(context).await?;
verify_person_in_community(&self.actor, &community, context).await?;
verify_urls_match(self.actor.inner(), self.object.actor.inner())?;
self.object.verify(context, request_counter).await?;
self.object.verify(context).await?;
Ok(())
}
#[tracing::instrument(skip_all)]
async fn receive(
self,
context: &Data<LemmyContext>,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let actor = self
.actor
.dereference(context, local_instance(context).await, request_counter)
.await?;
let object = self
.object
.object
.dereference(context, local_instance(context).await, request_counter)
.await?;
async fn receive(self, context: &Data<LemmyContext>) -> Result<(), LemmyError> {
insert_activity(&self.id, &self, false, true, context).await?;
let actor = self.actor.dereference(context).await?;
let object = self.object.object.dereference(context).await?;
match object {
PostOrComment::Post(p) => undo_vote_post(actor, &p, context).await,
PostOrComment::Comment(c) => undo_vote_comment(actor, &c, context).await,

@ -4,16 +4,19 @@ use crate::{
verify_person_in_community,
voting::{vote_comment, vote_post},
},
local_instance,
insert_activity,
objects::{community::ApubCommunity, person::ApubPerson},
protocol::{
activities::voting::vote::{Vote, VoteType},
InCommunity,
},
ActorType,
PostOrComment,
};
use activitypub_federation::{core::object_id::ObjectId, data::Data, traits::ActivityHandler};
use activitypub_federation::{
config::Data,
fetch::object_id::ObjectId,
traits::{ActivityHandler, Actor},
};
use anyhow::anyhow;
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::source::local_site::LocalSite;
@ -26,19 +29,19 @@ impl Vote {
actor: &ApubPerson,
community: &ApubCommunity,
kind: VoteType,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<Vote, LemmyError> {
Ok(Vote {
actor: ObjectId::new(actor.actor_id()),
actor: actor.id().into(),
object: object_id,
kind: kind.clone(),
id: generate_activity_id(kind, &context.settings().get_protocol_and_hostname())?,
audience: Some(ObjectId::new(community.actor_id())),
audience: Some(community.id().into()),
})
}
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl ActivityHandler for Vote {
type DataType = LemmyContext;
type Error = LemmyError;
@ -52,13 +55,9 @@ impl ActivityHandler for Vote {
}
#[tracing::instrument(skip_all)]
async fn verify(
&self,
context: &Data<LemmyContext>,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let community = self.community(context, request_counter).await?;
verify_person_in_community(&self.actor, &community, context, request_counter).await?;
async fn verify(&self, context: &Data<LemmyContext>) -> Result<(), LemmyError> {
let community = self.community(context).await?;
verify_person_in_community(&self.actor, &community, context).await?;
let enable_downvotes = LocalSite::read(context.pool())
.await
.map(|l| l.enable_downvotes)
@ -70,19 +69,10 @@ impl ActivityHandler for Vote {
}
#[tracing::instrument(skip_all)]
async fn receive(
self,
context: &Data<LemmyContext>,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let actor = self
.actor
.dereference(context, local_instance(context).await, request_counter)
.await?;
let object = self
.object
.dereference(context, local_instance(context).await, request_counter)
.await?;
async fn receive(self, context: &Data<LemmyContext>) -> Result<(), LemmyError> {
insert_activity(&self.id, &self, false, true, context).await?;
let actor = self.actor.dereference(context).await?;
let object = self.object.dereference(context).await?;
match object {
PostOrComment::Post(p) => vote_post(&self.kind, actor, &p, context).await,
PostOrComment::Comment(c) => vote_comment(&self.kind, actor, &c, context).await,

@ -24,7 +24,11 @@ use crate::{
InCommunity,
},
};
use activitypub_federation::{data::Data, deser::context::WithContext, traits::ActivityHandler};
use activitypub_federation::{
config::Data,
protocol::context::WithContext,
traits::ActivityHandler,
};
use lemmy_api_common::context::LemmyContext;
use lemmy_utils::error::LemmyError;
use serde::{Deserialize, Serialize};
@ -104,29 +108,25 @@ pub enum SiteInboxActivities {
DeleteUser(DeleteUser),
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl InCommunity for AnnouncableActivities {
#[tracing::instrument(skip(self, context))]
async fn community(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<ApubCommunity, LemmyError> {
async fn community(&self, context: &Data<LemmyContext>) -> Result<ApubCommunity, LemmyError> {
use AnnouncableActivities::*;
match self {
CreateOrUpdateComment(a) => a.community(context, request_counter).await,
CreateOrUpdatePost(a) => a.community(context, request_counter).await,
Vote(a) => a.community(context, request_counter).await,
UndoVote(a) => a.community(context, request_counter).await,
Delete(a) => a.community(context, request_counter).await,
UndoDelete(a) => a.community(context, request_counter).await,
UpdateCommunity(a) => a.community(context, request_counter).await,
BlockUser(a) => a.community(context, request_counter).await,
UndoBlockUser(a) => a.community(context, request_counter).await,
CollectionAdd(a) => a.community(context, request_counter).await,
CollectionRemove(a) => a.community(context, request_counter).await,
LockPost(a) => a.community(context, request_counter).await,
UndoLockPost(a) => a.community(context, request_counter).await,
CreateOrUpdateComment(a) => a.community(context).await,
CreateOrUpdatePost(a) => a.community(context).await,
Vote(a) => a.community(context).await,
UndoVote(a) => a.community(context).await,
Delete(a) => a.community(context).await,
UndoDelete(a) => a.community(context).await,
UpdateCommunity(a) => a.community(context).await,
BlockUser(a) => a.community(context).await,
UndoBlockUser(a) => a.community(context).await,
CollectionAdd(a) => a.community(context).await,
CollectionRemove(a) => a.community(context).await,
LockPost(a) => a.community(context).await,
UndoLockPost(a) => a.community(context).await,
Page(_) => unimplemented!(),
}
}

@ -3,7 +3,7 @@ use crate::{
fetcher::resolve_actor_identifier,
objects::community::ApubCommunity,
};
use actix_web::web::Data;
use activitypub_federation::config::Data;
use lemmy_api_common::{
comment::{GetComments, GetCommentsResponse},
context::LemmyContext,
@ -20,7 +20,7 @@ use lemmy_db_schema::{
use lemmy_db_views::comment_view::CommentQuery;
use lemmy_utils::{error::LemmyError, ConnectionId};
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl PerformApub for GetComments {
type Response = GetCommentsResponse;
@ -41,10 +41,10 @@ impl PerformApub for GetComments {
let listing_type = listing_type_with_site_default(data.type_, &local_site)?;
let community_actor_id = if let Some(name) = &data.community_name {
resolve_actor_identifier::<ApubCommunity, Community>(name, context, true)
resolve_actor_identifier::<ApubCommunity, Community>(name, context, &None, true)
.await
.ok()
.map(|c| c.actor_id)
.map(|c| c.actor_id.clone())
} else {
None
};

@ -3,7 +3,7 @@ use crate::{
fetcher::resolve_actor_identifier,
objects::community::ApubCommunity,
};
use actix_web::web::Data;
use activitypub_federation::config::Data;
use lemmy_api_common::{
context::LemmyContext,
post::{GetPosts, GetPostsResponse},
@ -18,7 +18,7 @@ use lemmy_db_schema::source::{community::Community, local_site::LocalSite};
use lemmy_db_views::post_view::PostQuery;
use lemmy_utils::{error::LemmyError, ConnectionId};
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl PerformApub for GetPosts {
type Response = GetPostsResponse;
@ -43,10 +43,10 @@ impl PerformApub for GetPosts {
let limit = data.limit;
let community_id = data.community_id;
let community_actor_id = if let Some(name) = &data.community_name {
resolve_actor_identifier::<ApubCommunity, Community>(name, context, true)
resolve_actor_identifier::<ApubCommunity, Community>(name, context, &None, true)
.await
.ok()
.map(|c| c.actor_id)
.map(|c| c.actor_id.clone())
} else {
None
};

@ -1,4 +1,4 @@
use actix_web::web::Data;
use activitypub_federation::config::Data;
use lemmy_api_common::context::LemmyContext;
use lemmy_utils::{error::LemmyError, ConnectionId};
@ -9,7 +9,7 @@ mod read_person;
mod resolve_object;
mod search;
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
pub trait PerformApub {
type Response: serde::ser::Serialize + Send;

@ -3,7 +3,7 @@ use crate::{
fetcher::resolve_actor_identifier,
objects::community::ApubCommunity,
};
use actix_web::web::Data;
use activitypub_federation::config::Data;
use lemmy_api_common::{
community::{GetCommunity, GetCommunityResponse},
context::LemmyContext,
@ -21,7 +21,7 @@ use lemmy_db_schema::{
use lemmy_db_views_actor::structs::{CommunityModeratorView, CommunityView};
use lemmy_utils::{error::LemmyError, ConnectionId};
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl PerformApub for GetCommunity {
type Response = GetCommunityResponse;
@ -49,7 +49,7 @@ impl PerformApub for GetCommunity {
Some(id) => id,
None => {
let name = data.name.clone().unwrap_or_else(|| "main".to_string());
resolve_actor_identifier::<ApubCommunity, Community>(&name, context, true)
resolve_actor_identifier::<ApubCommunity, Community>(&name, context, &local_user_view, true)
.await
.map_err(|e| e.with_message("couldnt_find_community"))?
.id

@ -1,5 +1,5 @@
use crate::{api::PerformApub, fetcher::resolve_actor_identifier, objects::person::ApubPerson};
use actix_web::web::Data;
use activitypub_federation::config::Data;
use lemmy_api_common::{
context::LemmyContext,
person::{GetPersonDetails, GetPersonDetailsResponse},
@ -13,7 +13,7 @@ use lemmy_db_views::{comment_view::CommentQuery, post_view::PostQuery};
use lemmy_db_views_actor::structs::{CommunityModeratorView, PersonView};
use lemmy_utils::{error::LemmyError, ConnectionId};
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl PerformApub for GetPersonDetails {
type Response = GetPersonDetailsResponse;
@ -42,7 +42,7 @@ impl PerformApub for GetPersonDetails {
Some(id) => id,
None => {
if let Some(username) = &data.username {
resolve_actor_identifier::<ApubPerson, Person>(username, context, true)
resolve_actor_identifier::<ApubPerson, Person>(username, context, &local_user_view, true)
.await
.map_err(|e| e.with_message("couldnt_find_that_username_or_email"))?
.id

@ -2,19 +2,19 @@ use crate::{
api::PerformApub,
fetcher::search::{search_query_to_object_id, SearchableObjects},
};
use actix_web::web::Data;
use activitypub_federation::config::Data;
use diesel::NotFound;
use lemmy_api_common::{
context::LemmyContext,
site::{ResolveObject, ResolveObjectResponse},
utils::{check_private_instance, get_local_user_view_from_jwt_opt},
utils::{check_private_instance, get_local_user_view_from_jwt},
};
use lemmy_db_schema::{newtypes::PersonId, source::local_site::LocalSite, utils::DbPool};
use lemmy_db_views::structs::{CommentView, PostView};
use lemmy_db_views_actor::structs::{CommunityView, PersonView};
use lemmy_utils::{error::LemmyError, ConnectionId};
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl PerformApub for ResolveObject {
type Response = ResolveObjectResponse;
@ -25,17 +25,15 @@ impl PerformApub for ResolveObject {
_websocket_id: Option<ConnectionId>,
) -> Result<ResolveObjectResponse, LemmyError> {
let local_user_view =
get_local_user_view_from_jwt_opt(self.auth.as_ref(), context.pool(), context.secret())
.await?;
get_local_user_view_from_jwt(&self.auth, context.pool(), context.secret()).await?;
let local_site = LocalSite::read(context.pool()).await?;
check_private_instance(&local_user_view, &local_site)?;
let person_id = local_user_view.person.id;
check_private_instance(&Some(local_user_view), &local_site)?;
// In release builds only allow for authenticated users to fetch remote objects
let local_only = local_user_view.is_none() && cfg!(not(debug_assertions));
let res = search_query_to_object_id(&self.q, local_only, context)
let res = search_query_to_object_id(&self.q, context)
.await
.map_err(|e| e.with_message("couldnt_find_object"))?;
convert_response(res, local_user_view.map(|l| l.person.id), context.pool())
convert_response(res, person_id, context.pool())
.await
.map_err(|e| e.with_message("couldnt_find_object"))
}
@ -43,7 +41,7 @@ impl PerformApub for ResolveObject {
async fn convert_response(
object: SearchableObjects,
user_id: Option<PersonId>,
user_id: PersonId,
pool: &DbPool,
) -> Result<ResolveObjectResponse, LemmyError> {
use SearchableObjects::*;
@ -56,15 +54,15 @@ async fn convert_response(
}
Community(c) => {
removed_or_deleted = c.deleted || c.removed;
res.community = Some(CommunityView::read(pool, c.id, user_id, None).await?)
res.community = Some(CommunityView::read(pool, c.id, Some(user_id), None).await?)
}
Post(p) => {
removed_or_deleted = p.deleted || p.removed;
res.post = Some(PostView::read(pool, p.id, user_id, None).await?)
res.post = Some(PostView::read(pool, p.id, Some(user_id), None).await?)
}
Comment(c) => {
removed_or_deleted = c.deleted || c.removed;
res.comment = Some(CommentView::read(pool, c.id, user_id).await?)
res.comment = Some(CommentView::read(pool, c.id, Some(user_id)).await?)
}
};
// if the object was deleted from database, dont return it

@ -3,7 +3,7 @@ use crate::{
fetcher::resolve_actor_identifier,
objects::community::ApubCommunity,
};
use actix_web::web::Data;
use activitypub_federation::config::Data;
use lemmy_api_common::{
context::LemmyContext,
site::{Search, SearchResponse},
@ -18,7 +18,7 @@ use lemmy_db_views::{comment_view::CommentQuery, post_view::PostQuery};
use lemmy_db_views_actor::{community_view::CommunityQuery, person_view::PersonQuery};
use lemmy_utils::{error::LemmyError, ConnectionId};
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl PerformApub for Search {
type Response = SearchResponse;
@ -39,8 +39,6 @@ impl PerformApub for Search {
let is_admin = local_user_view.as_ref().map(|luv| is_admin(luv).is_ok());
let local_user = local_user_view.map(|l| l.local_user);
let mut posts = Vec::new();
let mut comments = Vec::new();
let mut communities = Vec::new();
@ -56,14 +54,15 @@ impl PerformApub for Search {
let search_type = data.type_.unwrap_or(SearchType::All);
let community_id = data.community_id;
let community_actor_id = if let Some(name) = &data.community_name {
resolve_actor_identifier::<ApubCommunity, Community>(name, context, false)
resolve_actor_identifier::<ApubCommunity, Community>(name, context, &local_user_view, false)
.await
.ok()
.map(|c| c.actor_id)
.map(|c| c.actor_id.clone())
} else {
None
};
let creator_id = data.creator_id;
let local_user = local_user_view.map(|l| l.local_user);
match search_type {
SearchType::Posts => {
posts = PostQuery::builder()

@ -1,16 +1,15 @@
use crate::{
collections::CommunityContext,
objects::post::ApubPost,
objects::{community::ApubCommunity, post::ApubPost},
protocol::collections::group_featured::GroupFeatured,
};
use activitypub_federation::{
data::Data,
traits::{ActivityHandler, ApubObject},
utils::verify_domains_match,
config::Data,
kinds::collection::OrderedCollectionType,
protocol::verification::verify_domains_match,
traits::{ActivityHandler, Collection, Object},
};
use activitystreams_kinds::collection::OrderedCollectionType;
use futures::future::{join_all, try_join_all};
use lemmy_api_common::utils::generate_featured_url;
use lemmy_api_common::{context::LemmyContext, utils::generate_featured_url};
use lemmy_db_schema::{source::post::Post, utils::FETCH_LIMIT_MAX};
use lemmy_utils::error::LemmyError;
use url::Url;
@ -18,58 +17,46 @@ use url::Url;
#[derive(Clone, Debug)]
pub(crate) struct ApubCommunityFeatured(Vec<ApubPost>);
#[async_trait::async_trait(?Send)]
impl ApubObject for ApubCommunityFeatured {
type DataType = CommunityContext;
type ApubType = GroupFeatured;
type DbType = ();
#[async_trait::async_trait]
impl Collection for ApubCommunityFeatured {
type Owner = ApubCommunity;
type DataType = LemmyContext;
type Kind = GroupFeatured;
type Error = LemmyError;
async fn read_from_apub_id(
_object_id: Url,
data: &Self::DataType,
) -> Result<Option<Self>, Self::Error>
where
Self: Sized,
{
// 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> = Post::list_featured_for_community(data.1.pool(), community_id)
async fn read_local(
owner: &Self::Owner,
data: &Data<Self::DataType>,
) -> Result<Self::Kind, Self::Error> {
let ordered_items = try_join_all(
Post::list_featured_for_community(data.pool(), owner.id)
.await?
.into_iter()
.map(Into::into)
.collect();
Ok(Some(ApubCommunityFeatured(post_list)))
} else {
Ok(None)
}
}
async fn into_apub(self, data: &Self::DataType) -> Result<Self::ApubType, Self::Error> {
let ordered_items = try_join_all(self.0.into_iter().map(|p| p.into_apub(&data.1))).await?;
.map(ApubPost::from)
.map(|p| p.into_json(data)),
)
.await?;
Ok(GroupFeatured {
r#type: OrderedCollectionType::OrderedCollection,
id: generate_featured_url(&data.0.actor_id)?.into(),
id: generate_featured_url(&owner.actor_id)?.into(),
total_items: ordered_items.len() as i32,
ordered_items,
})
}
async fn verify(
apub: &Self::ApubType,
apub: &Self::Kind,
expected_domain: &Url,
_data: &Self::DataType,
_request_counter: &mut i32,
_data: &Data<Self::DataType>,
) -> Result<(), Self::Error> {
verify_domains_match(expected_domain, &apub.id)?;
Ok(())
}
async fn from_apub(
apub: Self::ApubType,
data: &Self::DataType,
_request_counter: &mut i32,
async fn from_json(
apub: Self::Kind,
_owner: &Self::Owner,
data: &Data<Self::DataType>,
) -> Result<Self, Self::Error>
where
Self: Sized,
@ -85,16 +72,14 @@ impl ApubObject for ApubCommunityFeatured {
// 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.
let data = Data::new(data.1.clone());
// process items in parallel, to avoid long delay from fetch_site_metadata() and other processing
join_all(posts.into_iter().map(|post| {
async {
// use separate request counter for each item, otherwise there will be problems with
// parallel processing
let request_counter = &mut 0;
let verify = post.verify(&data, request_counter).await;
let verify = post.verify(data).await;
if verify.is_ok() {
post.receive(&data, request_counter).await.ok();
post.receive(data).await.ok();
}
}
}))

@ -1,17 +1,15 @@
use crate::{
collections::CommunityContext,
local_instance,
objects::person::ApubPerson,
objects::{community::ApubCommunity, person::ApubPerson},
protocol::collections::group_moderators::GroupModerators,
};
use activitypub_federation::{
core::object_id::ObjectId,
traits::ApubObject,
utils::verify_domains_match,
config::Data,
fetch::object_id::ObjectId,
kinds::collection::OrderedCollectionType,
protocol::verification::verify_domains_match,
traits::Collection,
};
use activitystreams_kinds::collection::OrderedCollectionType;
use chrono::NaiveDateTime;
use lemmy_api_common::utils::generate_moderators_url;
use lemmy_api_common::{context::LemmyContext, utils::generate_moderators_url};
use lemmy_db_schema::{
source::community::{CommunityModerator, CommunityModeratorForm},
traits::Joinable,
@ -23,46 +21,26 @@ use url::Url;
#[derive(Clone, Debug)]
pub(crate) struct ApubCommunityModerators(pub(crate) Vec<CommunityModeratorView>);
#[async_trait::async_trait(?Send)]
impl ApubObject for ApubCommunityModerators {
type DataType = CommunityContext;
type ApubType = GroupModerators;
#[async_trait::async_trait]
impl Collection for ApubCommunityModerators {
type Owner = ApubCommunity;
type DataType = LemmyContext;
type Kind = GroupModerators;
type Error = LemmyError;
fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
None
}
#[tracing::instrument(skip_all)]
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 = CommunityModeratorView::for_community(data.1.pool(), cid).await?;
Ok(Some(ApubCommunityModerators(moderators)))
} else {
Ok(None)
}
}
#[tracing::instrument(skip_all)]
async fn delete(self, _data: &Self::DataType) -> Result<(), LemmyError> {
unimplemented!()
}
#[tracing::instrument(skip_all)]
async fn into_apub(self, data: &Self::DataType) -> Result<Self::ApubType, LemmyError> {
let ordered_items = self
.0
async fn read_local(
owner: &Self::Owner,
data: &Data<Self::DataType>,
) -> Result<Self::Kind, LemmyError> {
let moderators = CommunityModeratorView::for_community(data.pool(), owner.id).await?;
let ordered_items = moderators
.into_iter()
.map(|m| ObjectId::<ApubPerson>::new(m.moderator.actor_id))
.map(|m| ObjectId::<ApubPerson>::from(m.moderator.actor_id))
.collect();
Ok(GroupModerators {
r#type: OrderedCollectionType::OrderedCollection,
id: generate_moderators_url(&data.0.actor_id)?.into(),
id: generate_moderators_url(&owner.actor_id)?.into(),
ordered_items,
})
}
@ -71,40 +49,36 @@ impl ApubObject for ApubCommunityModerators {
async fn verify(
group_moderators: &GroupModerators,
expected_domain: &Url,
_context: &CommunityContext,
_request_counter: &mut i32,
_data: &Data<Self::DataType>,
) -> Result<(), LemmyError> {
verify_domains_match(&group_moderators.id, expected_domain)?;
Ok(())
}
#[tracing::instrument(skip_all)]
async fn from_apub(
apub: Self::ApubType,
data: &Self::DataType,
request_counter: &mut i32,
async fn from_json(
apub: Self::Kind,
owner: &Self::Owner,
data: &Data<Self::DataType>,
) -> Result<Self, LemmyError> {
let community_id = data.0.id;
let community_id = owner.id;
let current_moderators =
CommunityModeratorView::for_community(data.1.pool(), community_id).await?;
CommunityModeratorView::for_community(data.pool(), community_id).await?;
// Remove old mods from database which arent in the moderators collection anymore
for mod_user in &current_moderators {
let mod_id = ObjectId::new(mod_user.moderator.actor_id.clone());
let mod_id = ObjectId::from(mod_user.moderator.actor_id.clone());
if !apub.ordered_items.contains(&mod_id) {
let community_moderator_form = CommunityModeratorForm {
community_id: mod_user.community.id,
person_id: mod_user.moderator.id,
};
CommunityModerator::leave(data.1.pool(), &community_moderator_form).await?;
CommunityModerator::leave(data.pool(), &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);
let mod_user: ApubPerson = mod_id
.dereference(&data.1, local_instance(&data.1).await, request_counter)
.await?;
let mod_user: ApubPerson = mod_id.dereference(data).await?;
if !current_moderators
.iter()
@ -112,18 +86,16 @@ impl ApubObject for ApubCommunityModerators {
.any(|x| x == mod_user.actor_id)
{
let community_moderator_form = CommunityModeratorForm {
community_id: data.0.id,
community_id: owner.id,
person_id: mod_user.id,
};
CommunityModerator::join(data.1.pool(), &community_moderator_form).await?;
CommunityModerator::join(data.pool(), &community_moderator_form).await?;
}
}
// This return value is unused, so just set an empty vec
Ok(ApubCommunityModerators(Vec::new()))
}
type DbType = ();
}
#[cfg(test)]
@ -181,15 +153,13 @@ mod tests {
let json: GroupModerators =
file_to_json_object("assets/lemmy/collections/group_moderators.json").unwrap();
let url = Url::parse("https://enterprise.lemmy.ml/c/tenforward").unwrap();
let mut request_counter = 0;
let community_context = CommunityContext(community, context.clone());
ApubCommunityModerators::verify(&json, &url, &community_context, &mut request_counter)
ApubCommunityModerators::verify(&json, &url, &context)
.await
.unwrap();
ApubCommunityModerators::from_apub(json, &community_context, &mut request_counter)
ApubCommunityModerators::from_json(json, &community, &context)
.await
.unwrap();
assert_eq!(request_counter, 0);
assert_eq!(context.request_count(), 0);
let current_moderators = CommunityModeratorView::for_community(context.pool(), community_id)
.await
@ -200,7 +170,7 @@ mod tests {
Person::delete(context.pool(), old_mod.id).await.unwrap();
Person::delete(context.pool(), new_mod.id).await.unwrap();
Community::delete(context.pool(), community_context.0.id)
Community::delete(context.pool(), community.id)
.await
.unwrap();
Site::delete(context.pool(), site.id).await.unwrap();

@ -1,7 +1,6 @@
use crate::{
activity_lists::AnnouncableActivities,
collections::CommunityContext,
objects::post::ApubPost,
objects::{community::ApubCommunity, post::ApubPost},
protocol::{
activities::{
community::announce::AnnounceActivity,
@ -12,14 +11,13 @@ use crate::{
},
};
use activitypub_federation::{
data::Data,
traits::{ActivityHandler, ApubObject},
utils::verify_domains_match,
config::Data,
kinds::collection::OrderedCollectionType,
protocol::verification::verify_domains_match,
traits::{ActivityHandler, Collection},
};
use activitystreams_kinds::collection::OrderedCollectionType;
use chrono::NaiveDateTime;
use futures::future::join_all;
use lemmy_api_common::utils::generate_outbox_url;
use lemmy_api_common::{context::LemmyContext, utils::generate_outbox_url};
use lemmy_db_schema::{
source::{person::Person, post::Post},
traits::Crud,
@ -31,52 +29,36 @@ use url::Url;
#[derive(Clone, Debug)]
pub(crate) struct ApubCommunityOutbox(Vec<ApubPost>);
#[async_trait::async_trait(?Send)]
impl ApubObject for ApubCommunityOutbox {
type DataType = CommunityContext;
type ApubType = GroupOutbox;
#[async_trait::async_trait]
impl Collection for ApubCommunityOutbox {
type Owner = ApubCommunity;
type DataType = LemmyContext;
type Kind = GroupOutbox;
type Error = LemmyError;
type DbType = ();
fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
None
}
#[tracing::instrument(skip_all)]
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> = Post::list_for_community(data.1.pool(), community_id)
.await?
.into_iter()
.map(Into::into)
.collect();
Ok(Some(ApubCommunityOutbox(post_list)))
} else {
Ok(None)
}
}
#[tracing::instrument(skip_all)]
async fn into_apub(self, data: &Self::DataType) -> Result<Self::ApubType, LemmyError> {
async fn read_local(
owner: &Self::Owner,
data: &Data<Self::DataType>,
) -> Result<Self::Kind, LemmyError> {
let post_list: Vec<ApubPost> = Post::list_for_community(data.pool(), owner.id)
.await?
.into_iter()
.map(Into::into)
.collect();
let mut ordered_items = vec![];
for post in self.0 {
let person = Person::read(data.1.pool(), post.creator_id).await?.into();
for post in post_list {
let person = Person::read(data.pool(), post.creator_id).await?.into();
let create =
CreateOrUpdatePage::new(post, &person, &data.0, CreateOrUpdateType::Create, &data.1)
.await?;
CreateOrUpdatePage::new(post, &person, owner, CreateOrUpdateType::Create, data).await?;
let announcable = AnnouncableActivities::CreateOrUpdatePost(create);
let announce = AnnounceActivity::new(announcable.try_into()?, &data.0, &data.1)?;
let announce = AnnounceActivity::new(announcable.try_into()?, owner, data)?;
ordered_items.push(announce);
}
Ok(GroupOutbox {
r#type: OrderedCollectionType::OrderedCollection,
id: generate_outbox_url(&data.0.actor_id)?.into(),
id: generate_outbox_url(&owner.actor_id)?.into(),
total_items: ordered_items.len() as i32,
ordered_items,
})
@ -86,18 +68,17 @@ impl ApubObject for ApubCommunityOutbox {
async fn verify(
group_outbox: &GroupOutbox,
expected_domain: &Url,
_context: &CommunityContext,
_request_counter: &mut i32,
_data: &Data<Self::DataType>,
) -> Result<(), LemmyError> {
verify_domains_match(expected_domain, &group_outbox.id)?;
Ok(())
}
#[tracing::instrument(skip_all)]
async fn from_apub(
apub: Self::ApubType,
data: &Self::DataType,
_request_counter: &mut i32,
async fn from_json(
apub: Self::Kind,
_owner: &Self::Owner,
data: &Data<Self::DataType>,
) -> Result<Self, LemmyError> {
let mut outbox_activities = apub.ordered_items;
if outbox_activities.len() as i64 > FETCH_LIMIT_MAX {
@ -110,16 +91,14 @@ impl ApubObject for ApubCommunityOutbox {
// 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.
let data = Data::new(data.1.clone());
// process items in parallel, to avoid long delay from fetch_site_metadata() and other processing
join_all(outbox_activities.into_iter().map(|activity| {
async {
// use separate request counter for each item, otherwise there will be problems with
// parallel processing
let request_counter = &mut 0;
let verify = activity.verify(&data, request_counter).await;
let verify = activity.verify(data).await;
if verify.is_ok() {
activity.receive(&data, request_counter).await.ok();
activity.receive(data).await.ok();
}
}
}))

@ -1,9 +1,3 @@
use crate::objects::community::ApubCommunity;
use lemmy_api_common::context::LemmyContext;
pub(crate) mod community_featured;
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,31 +1,38 @@
use crate::{fetcher::webfinger::webfinger_resolve_actor, ActorType};
use activitypub_federation::traits::ApubObject;
use activitypub_federation::{
config::Data,
fetch::webfinger::webfinger_resolve_actor,
traits::{Actor, Object},
};
use diesel::NotFound;
use itertools::Itertools;
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::traits::ApubActor;
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::LemmyError;
pub mod post_or_comment;
pub mod search;
pub mod user_or_community;
pub mod webfinger;
/// Resolve actor identifier (eg `!news@example.com`) from local database to avoid network requests.
/// This only works for local actors, and remote actors which were previously fetched (so it doesnt
/// trigger any new fetch).
/// Resolve actor identifier like `!news@example.com` to user or community object.
///
/// In case the requesting user is logged in and the object was not found locally, it is attempted
/// to fetch via webfinger from the original instance.
#[tracing::instrument(skip_all)]
pub async fn resolve_actor_identifier<Actor, DbActor>(
pub async fn resolve_actor_identifier<ActorType, DbActor>(
identifier: &str,
context: &LemmyContext,
context: &Data<LemmyContext>,
local_user_view: &Option<LocalUserView>,
include_deleted: bool,
) -> Result<DbActor, LemmyError>
) -> Result<ActorType, LemmyError>
where
Actor: ApubObject<DataType = LemmyContext, Error = LemmyError>
+ ApubObject<DbType = DbActor>
+ ActorType
ActorType: Object<DataType = LemmyContext, Error = LemmyError>
+ Object
+ Actor
+ From<DbActor>
+ Send
+ 'static,
for<'de2> <Actor as ApubObject>::ApubType: serde::Deserialize<'de2>,
for<'de2> <ActorType as Object>::Kind: serde::Deserialize<'de2>,
DbActor: ApubActor + Send + 'static,
{
// remote actor
@ -38,19 +45,22 @@ where
let domain = format!("{}://{}", context.settings().get_protocol_string(), domain);
let actor = DbActor::read_from_name_and_domain(context.pool(), &name, &domain).await;
if actor.is_ok() {
Ok(actor?)
} else {
Ok(actor?.into())
} else if local_user_view.is_some() {
// Fetch the actor from its home instance using webfinger
let id = webfinger_resolve_actor::<Actor>(identifier, true, context, &mut 0).await?;
let actor: DbActor = DbActor::read_from_apub_id(context.pool(), &id)
.await?
.expect("actor exists as we fetched just before");
let actor: ActorType = webfinger_resolve_actor(identifier, context).await?;
Ok(actor)
} else {
Err(NotFound.into())
}
}
// local actor
else {
let identifier = identifier.to_string();
Ok(DbActor::read_from_name(context.pool(), &identifier, include_deleted).await?)
Ok(
DbActor::read_from_name(context.pool(), &identifier, include_deleted)
.await?
.into(),
)
}
}

@ -5,7 +5,7 @@ use crate::{
InCommunity,
},
};
use activitypub_federation::traits::ApubObject;
use activitypub_federation::{config::Data, traits::Object};
use chrono::NaiveDateTime;
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::{
@ -29,11 +29,10 @@ pub enum PageOrNote {
Note(Note),
}
#[async_trait::async_trait(?Send)]
impl ApubObject for PostOrComment {
#[async_trait::async_trait]
impl Object for PostOrComment {
type DataType = LemmyContext;
type ApubType = PageOrNote;
type DbType = ();
type Kind = PageOrNote;
type Error = LemmyError;
fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
@ -41,68 +40,55 @@ impl ApubObject for PostOrComment {
}
#[tracing::instrument(skip_all)]
async fn read_from_apub_id(
async fn read_from_id(
object_id: Url,
data: &Self::DataType,
data: &Data<Self::DataType>,
) -> Result<Option<Self>, LemmyError> {
let post = ApubPost::read_from_apub_id(object_id.clone(), data).await?;
let post = ApubPost::read_from_id(object_id.clone(), data).await?;
Ok(match post {
Some(o) => Some(PostOrComment::Post(o)),
None => ApubComment::read_from_apub_id(object_id, data)
None => ApubComment::read_from_id(object_id, data)
.await?
.map(PostOrComment::Comment),
})
}
#[tracing::instrument(skip_all)]
async fn delete(self, data: &Self::DataType) -> Result<(), LemmyError> {
async fn delete(self, data: &Data<Self::DataType>) -> Result<(), LemmyError> {
match self {
PostOrComment::Post(p) => p.delete(data).await,
PostOrComment::Comment(c) => c.delete(data).await,
}
}
async fn into_apub(self, _data: &Self::DataType) -> Result<Self::ApubType, LemmyError> {
async fn into_json(self, _data: &Data<Self::DataType>) -> Result<Self::Kind, LemmyError> {
unimplemented!()
}
#[tracing::instrument(skip_all)]
async fn verify(
apub: &Self::ApubType,
apub: &Self::Kind,
expected_domain: &Url,
data: &Self::DataType,
request_counter: &mut i32,
data: &Data<Self::DataType>,
) -> Result<(), LemmyError> {
match apub {
PageOrNote::Page(a) => ApubPost::verify(a, expected_domain, data, request_counter).await,
PageOrNote::Note(a) => ApubComment::verify(a, expected_domain, data, request_counter).await,
PageOrNote::Page(a) => ApubPost::verify(a, expected_domain, data).await,
PageOrNote::Note(a) => ApubComment::verify(a, expected_domain, data).await,
}
}
#[tracing::instrument(skip_all)]
async fn from_apub(
apub: PageOrNote,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<Self, LemmyError> {
async fn from_json(apub: PageOrNote, context: &Data<LemmyContext>) -> Result<Self, LemmyError> {
Ok(match apub {
PageOrNote::Page(p) => {
PostOrComment::Post(ApubPost::from_apub(*p, context, request_counter).await?)
}
PageOrNote::Note(n) => {
PostOrComment::Comment(ApubComment::from_apub(n, context, request_counter).await?)
}
PageOrNote::Page(p) => PostOrComment::Post(ApubPost::from_json(*p, context).await?),
PageOrNote::Note(n) => PostOrComment::Comment(ApubComment::from_json(n, context).await?),
})
}
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl InCommunity for PostOrComment {
async fn community(
&self,
context: &LemmyContext,
_: &mut i32,
) -> Result<ApubCommunity, LemmyError> {
async fn community(&self, context: &Data<LemmyContext>) -> Result<ApubCommunity, LemmyError> {
let cid = match self {
PostOrComment::Post(p) => p.community_id,
PostOrComment::Comment(c) => Post::read(context.pool(), c.post_id).await?.community_id,

@ -1,10 +1,12 @@
use crate::{
fetcher::webfinger::webfinger_resolve_actor,
local_instance,
objects::{comment::ApubComment, community::ApubCommunity, person::ApubPerson, post::ApubPost},
protocol::objects::{group::Group, note::Note, page::Page, person::Person},
};
use activitypub_federation::{core::object_id::ObjectId, traits::ApubObject};
use activitypub_federation::{
config::Data,
fetch::{object_id::ObjectId, webfinger::webfinger_resolve_actor},
traits::Object,
};
use chrono::NaiveDateTime;
use lemmy_api_common::context::LemmyContext;
use lemmy_utils::error::LemmyError;
@ -17,39 +19,29 @@ use url::Url;
#[tracing::instrument(skip_all)]
pub(crate) async fn search_query_to_object_id(
query: &str,
local_only: bool,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> Result<SearchableObjects, LemmyError> {
let request_counter = &mut 0;
let object_id = match Url::parse(query) {
// its already an url, just go with it
Ok(url) => ObjectId::new(url),
Ok(match Url::parse(query) {
Ok(url) => {
// its already an url, just go with it
ObjectId::from(url).dereference(context).await?
}
Err(_) => {
// not an url, try to resolve via webfinger
let mut chars = query.chars();
let kind = chars.next();
let identifier = chars.as_str();
let id = match kind {
Some('@') => {
webfinger_resolve_actor::<ApubPerson>(identifier, local_only, context, request_counter)
.await?
}
Some('!') => {
webfinger_resolve_actor::<ApubCommunity>(identifier, local_only, context, request_counter)
.await?
}
match kind {
Some('@') => SearchableObjects::Person(
webfinger_resolve_actor::<LemmyContext, ApubPerson>(identifier, context).await?,
),
Some('!') => SearchableObjects::Community(
webfinger_resolve_actor::<LemmyContext, ApubCommunity>(identifier, context).await?,
),
_ => return Err(LemmyError::from_message("invalid query")),
};
ObjectId::new(id)
}
}
};
if local_only {
object_id.dereference_local(context).await
} else {
object_id
.dereference(context, local_instance(context).await, request_counter)
.await
}
})
}
/// The types of ActivityPub objects that can be fetched directly by searching for their ID.
@ -63,18 +55,17 @@ pub(crate) enum SearchableObjects {
#[derive(Deserialize)]
#[serde(untagged)]
pub(crate) enum SearchableApubTypes {
pub(crate) enum SearchableKinds {
Group(Group),
Person(Person),
Page(Page),
Note(Note),
}
#[async_trait::async_trait(?Send)]
impl ApubObject for SearchableObjects {
#[async_trait::async_trait]
impl Object for SearchableObjects {
type DataType = LemmyContext;
type ApubType = SearchableApubTypes;
type DbType = ();
type Kind = SearchableKinds;
type Error = LemmyError;
fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
@ -92,23 +83,23 @@ impl ApubObject for SearchableObjects {
// we could skip this and always return an error, but then it would always fetch objects
// over http, and not be able to mark objects as deleted that were deleted by remote server.
#[tracing::instrument(skip_all)]
async fn read_from_apub_id(
async fn read_from_id(
object_id: Url,
context: &LemmyContext,
context: &Data<Self::DataType>,
) -> Result<Option<Self>, LemmyError> {
let c = ApubCommunity::read_from_apub_id(object_id.clone(), context).await?;
let c = ApubCommunity::read_from_id(object_id.clone(), context).await?;
if let Some(c) = c {
return Ok(Some(SearchableObjects::Community(c)));
}
let p = ApubPerson::read_from_apub_id(object_id.clone(), context).await?;
let p = ApubPerson::read_from_id(object_id.clone(), context).await?;
if let Some(p) = p {
return Ok(Some(SearchableObjects::Person(p)));
}
let p = ApubPost::read_from_apub_id(object_id.clone(), context).await?;
let p = ApubPost::read_from_id(object_id.clone(), context).await?;
if let Some(p) = p {
return Ok(Some(SearchableObjects::Post(p)));
}
let c = ApubComment::read_from_apub_id(object_id, context).await?;
let c = ApubComment::read_from_id(object_id, context).await?;
if let Some(c) = c {
return Ok(Some(SearchableObjects::Comment(c)));
}
@ -116,7 +107,7 @@ impl ApubObject for SearchableObjects {
}
#[tracing::instrument(skip_all)]
async fn delete(self, data: &Self::DataType) -> Result<(), LemmyError> {
async fn delete(self, data: &Data<Self::DataType>) -> Result<(), LemmyError> {
match self {
SearchableObjects::Person(p) => p.delete(data).await,
SearchableObjects::Community(c) => c.delete(data).await,
@ -125,46 +116,33 @@ impl ApubObject for SearchableObjects {
}
}
async fn into_apub(self, _data: &Self::DataType) -> Result<Self::ApubType, LemmyError> {
async fn into_json(self, _data: &Data<Self::DataType>) -> Result<Self::Kind, LemmyError> {
unimplemented!()
}
#[tracing::instrument(skip_all)]
async fn verify(
apub: &Self::ApubType,
apub: &Self::Kind,
expected_domain: &Url,
data: &Self::DataType,
request_counter: &mut i32,
data: &Data<Self::DataType>,
) -> Result<(), LemmyError> {
match apub {
SearchableApubTypes::Group(a) => {
ApubCommunity::verify(a, expected_domain, data, request_counter).await
}
SearchableApubTypes::Person(a) => {
ApubPerson::verify(a, expected_domain, data, request_counter).await
}
SearchableApubTypes::Page(a) => {
ApubPost::verify(a, expected_domain, data, request_counter).await
}
SearchableApubTypes::Note(a) => {
ApubComment::verify(a, expected_domain, data, request_counter).await
}
SearchableKinds::Group(a) => ApubCommunity::verify(a, expected_domain, data).await,
SearchableKinds::Person(a) => ApubPerson::verify(a, expected_domain, data).await,
SearchableKinds::Page(a) => ApubPost::verify(a, expected_domain, data).await,
SearchableKinds::Note(a) => ApubComment::verify(a, expected_domain, data).await,
}
}
#[tracing::instrument(skip_all)]
async fn from_apub(
apub: Self::ApubType,
context: &LemmyContext,
rc: &mut i32,
) -> Result<Self, LemmyError> {
use SearchableApubTypes as SAT;
async fn from_json(apub: Self::Kind, context: &Data<LemmyContext>) -> Result<Self, LemmyError> {
use SearchableKinds as SAT;
use SearchableObjects as SO;
Ok(match apub {
SAT::Group(g) => SO::Community(ApubCommunity::from_apub(g, context, rc).await?),
SAT::Person(p) => SO::Person(ApubPerson::from_apub(p, context, rc).await?),
SAT::Page(p) => SO::Post(ApubPost::from_apub(p, context, rc).await?),
SAT::Note(n) => SO::Comment(ApubComment::from_apub(n, context, rc).await?),
SAT::Group(g) => SO::Community(ApubCommunity::from_json(g, context).await?),
SAT::Person(p) => SO::Person(ApubPerson::from_json(p, context).await?),
SAT::Page(p) => SO::Post(ApubPost::from_json(p, context).await?),
SAT::Note(n) => SO::Comment(ApubComment::from_json(n, context).await?),
})
}
}

@ -1,9 +1,11 @@
use crate::{
objects::{community::ApubCommunity, person::ApubPerson},
protocol::objects::{group::Group, person::Person},
ActorType,
};
use activitypub_federation::traits::{Actor, ApubObject};
use activitypub_federation::{
config::Data,
traits::{Actor, Object},
};
use chrono::NaiveDateTime;
use lemmy_api_common::context::LemmyContext;
use lemmy_utils::error::LemmyError;
@ -29,11 +31,10 @@ pub enum PersonOrGroupType {
Group,
}
#[async_trait::async_trait(?Send)]
impl ApubObject for UserOrCommunity {
#[async_trait::async_trait]
impl Object for UserOrCommunity {
type DataType = LemmyContext;
type ApubType = PersonOrGroup;
type DbType = ();
type Kind = PersonOrGroup;
type Error = LemmyError;
fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
@ -44,90 +45,77 @@ impl ApubObject for UserOrCommunity {
}
#[tracing::instrument(skip_all)]
async fn read_from_apub_id(
async fn read_from_id(
object_id: Url,
data: &Self::DataType,
data: &Data<Self::DataType>,
) -> Result<Option<Self>, LemmyError> {
let person = ApubPerson::read_from_apub_id(object_id.clone(), data).await?;
let person = ApubPerson::read_from_id(object_id.clone(), data).await?;
Ok(match person {
Some(o) => Some(UserOrCommunity::User(o)),
None => ApubCommunity::read_from_apub_id(object_id, data)
None => ApubCommunity::read_from_id(object_id, data)
.await?
.map(UserOrCommunity::Community),
})
}
#[tracing::instrument(skip_all)]
async fn delete(self, data: &Self::DataType) -> Result<(), LemmyError> {
async fn delete(self, data: &Data<Self::DataType>) -> Result<(), LemmyError> {
match self {
UserOrCommunity::User(p) => p.delete(data).await,
UserOrCommunity::Community(p) => p.delete(data).await,
}
}
async fn into_apub(self, _data: &Self::DataType) -> Result<Self::ApubType, LemmyError> {
async fn into_json(self, _data: &Data<Self::DataType>) -> Result<Self::Kind, LemmyError> {
unimplemented!()
}
#[tracing::instrument(skip_all)]
async fn verify(
apub: &Self::ApubType,
apub: &Self::Kind,
expected_domain: &Url,
data: &Self::DataType,
request_counter: &mut i32,
data: &Data<Self::DataType>,
) -> Result<(), LemmyError> {
match apub {
PersonOrGroup::Person(a) => {
ApubPerson::verify(a, expected_domain, data, request_counter).await
}
PersonOrGroup::Group(a) => {
ApubCommunity::verify(a, expected_domain, data, request_counter).await
}
PersonOrGroup::Person(a) => ApubPerson::verify(a, expected_domain, data).await,
PersonOrGroup::Group(a) => ApubCommunity::verify(a, expected_domain, data).await,
}
}
#[tracing::instrument(skip_all)]
async fn from_apub(
apub: Self::ApubType,
data: &Self::DataType,
request_counter: &mut i32,
) -> Result<Self, LemmyError> {
async fn from_json(apub: Self::Kind, data: &Data<Self::DataType>) -> Result<Self, LemmyError> {
Ok(match apub {
PersonOrGroup::Person(p) => {
UserOrCommunity::User(ApubPerson::from_apub(p, data, request_counter).await?)
}
PersonOrGroup::Person(p) => UserOrCommunity::User(ApubPerson::from_json(p, data).await?),
PersonOrGroup::Group(p) => {
UserOrCommunity::Community(ApubCommunity::from_apub(p, data, request_counter).await?)
UserOrCommunity::Community(ApubCommunity::from_json(p, data).await?)
}
})
}
}
impl Actor for UserOrCommunity {
fn public_key(&self) -> &str {
fn id(&self) -> Url {
match self {
UserOrCommunity::User(p) => p.public_key(),
UserOrCommunity::Community(p) => p.public_key(),
UserOrCommunity::User(u) => u.id(),
UserOrCommunity::Community(c) => c.id(),
}
}
fn inbox(&self) -> Url {
unimplemented!()
}
}
impl ActorType for UserOrCommunity {
fn actor_id(&self) -> Url {
fn public_key_pem(&self) -> &str {
match self {
UserOrCommunity::User(u) => u.actor_id(),
UserOrCommunity::Community(c) => c.actor_id(),
UserOrCommunity::User(p) => p.public_key_pem(),
UserOrCommunity::Community(p) => p.public_key_pem(),
}
}
fn private_key(&self) -> Option<String> {
fn private_key_pem(&self) -> Option<String> {
match self {
UserOrCommunity::User(u) => u.private_key(),
UserOrCommunity::Community(c) => c.private_key(),
UserOrCommunity::User(p) => p.private_key_pem(),
UserOrCommunity::Community(p) => p.private_key_pem(),
}
}
fn inbox(&self) -> Url {
unimplemented!()
}
}

@ -1,67 +0,0 @@
use crate::{local_instance, ActorType, FEDERATION_HTTP_FETCH_LIMIT};
use activitypub_federation::{core::object_id::ObjectId, traits::ApubObject};
use anyhow::anyhow;
use itertools::Itertools;
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::newtypes::DbUrl;
use lemmy_utils::{error::LemmyError, WebfingerResponse};
use tracing::debug;
use url::Url;
/// Turns a person id like `@name@example.com` into an apub ID, like `https://example.com/user/name`,
/// using webfinger.
pub(crate) async fn webfinger_resolve_actor<Kind>(
identifier: &str,
local_only: bool,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<DbUrl, LemmyError>
where
Kind: ApubObject<DataType = LemmyContext, Error = LemmyError> + ActorType + Send + 'static,
for<'de2> <Kind as ApubObject>::ApubType: serde::Deserialize<'de2>,
{
let protocol = context.settings().get_protocol_string();
let (_, domain) = identifier
.splitn(2, '@')
.collect_tuple()
.ok_or_else(|| LemmyError::from_message("Invalid webfinger query, missing domain"))?;
let fetch_url = format!("{protocol}://{domain}/.well-known/webfinger?resource=acct:{identifier}");
debug!("Fetching webfinger url: {}", &fetch_url);
*request_counter += 1;
if *request_counter > FEDERATION_HTTP_FETCH_LIMIT {
return Err(LemmyError::from_message("Request retry limit reached"));
}
let response = context.client().get(&fetch_url).send().await?;
let res: WebfingerResponse = response.json().await.map_err(LemmyError::from)?;
let links: Vec<Url> = res
.links
.iter()
.filter(|link| {
if let Some(type_) = &link.kind {
type_.starts_with("application/")
} else {
false
}
})
.filter_map(|l| l.href.clone())
.collect();
for l in links {
let object_id = ObjectId::<Kind>::new(l);
let object = if local_only {
object_id.dereference_local(context).await
} else {
object_id
.dereference(context, local_instance(context).await, request_counter)
.await
};
if object.is_ok() {
return object.map(|o| o.actor_id().into());
}
}
let err = anyhow!("Failed to resolve actor for {}", identifier);
Err(LemmyError::from_error_message(err, "failed_to_resolve"))
}

@ -2,8 +2,8 @@ use crate::{
http::{create_apub_response, create_apub_tombstone_response, err_object_not_local},
objects::comment::ApubComment,
};
use activitypub_federation::traits::ApubObject;
use actix_web::{web, web::Path, HttpResponse};
use activitypub_federation::{config::Data, traits::Object};
use actix_web::{web::Path, HttpResponse};
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::{newtypes::CommentId, source::comment::Comment, traits::Crud};
use lemmy_utils::error::LemmyError;
@ -18,7 +18,7 @@ pub(crate) struct CommentQuery {
#[tracing::instrument(skip_all)]
pub(crate) async fn get_apub_comment(
info: Path<CommentQuery>,
context: web::Data<LemmyContext>,
context: Data<LemmyContext>,
) -> Result<HttpResponse, LemmyError> {
let id = CommentId(info.comment_id.parse::<i32>()?);
let comment: ApubComment = Comment::read(context.pool(), id).await?.into();
@ -27,7 +27,7 @@ pub(crate) async fn get_apub_comment(
}
if !comment.deleted && !comment.removed {
Ok(create_apub_response(&comment.into_apub(&context).await?))
Ok(create_apub_response(&comment.into_json(&context).await?))
} else {
Ok(create_apub_tombstone_response(comment.ap_id.clone()))
}

@ -4,23 +4,19 @@ use crate::{
community_featured::ApubCommunityFeatured,
community_moderators::ApubCommunityModerators,
community_outbox::ApubCommunityOutbox,
CommunityContext,
},
http::{create_apub_response, create_apub_tombstone_response, receive_lemmy_activity},
local_instance,
http::{create_apub_response, create_apub_tombstone_response},
objects::{community::ApubCommunity, person::ApubPerson},
protocol::collections::group_followers::GroupFollowers,
};
use activitypub_federation::{
core::object_id::ObjectId,
deser::context::WithContext,
traits::ApubObject,
};
use actix_web::{web, HttpRequest, HttpResponse};
use lemmy_api_common::{
context::LemmyContext,
utils::{generate_featured_url, generate_outbox_url},
actix_web::inbox::receive_activity,
config::Data,
protocol::context::WithContext,
traits::{Collection, Object},
};
use actix_web::{web, web::Bytes, HttpRequest, HttpResponse};
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::{source::community::Community, traits::ApubActor};
use lemmy_utils::error::LemmyError;
use serde::Deserialize;
@ -34,7 +30,7 @@ pub(crate) struct CommunityQuery {
#[tracing::instrument(skip_all)]
pub(crate) async fn get_apub_community_http(
info: web::Path<CommunityQuery>,
context: web::Data<LemmyContext>,
context: Data<LemmyContext>,
) -> Result<HttpResponse, LemmyError> {
let community: ApubCommunity =
Community::read_from_name(context.pool(), &info.community_name, true)
@ -42,7 +38,7 @@ pub(crate) async fn get_apub_community_http(
.into();
if !community.deleted && !community.removed {
let apub = community.into_apub(&context).await?;
let apub = community.into_json(&context).await?;
Ok(create_apub_response(&apub))
} else {
@ -54,17 +50,19 @@ pub(crate) async fn get_apub_community_http(
#[tracing::instrument(skip_all)]
pub async fn community_inbox(
request: HttpRequest,
payload: String,
context: web::Data<LemmyContext>,
body: Bytes,
data: Data<LemmyContext>,
) -> Result<HttpResponse, LemmyError> {
receive_lemmy_activity::<WithContext<GroupInboxActivities>, ApubPerson>(request, payload, context)
.await
receive_activity::<WithContext<GroupInboxActivities>, ApubPerson, LemmyContext>(
request, body, &data,
)
.await
}
/// Returns an empty followers collection, only populating the size (for privacy).
pub(crate) async fn get_apub_community_followers(
info: web::Path<CommunityQuery>,
context: web::Data<LemmyContext>,
context: Data<LemmyContext>,
) -> Result<HttpResponse, LemmyError> {
let community = Community::read_from_name(context.pool(), &info.community_name, false).await?;
let followers = GroupFollowers::new(community, &context).await?;
@ -75,24 +73,23 @@ pub(crate) async fn get_apub_community_followers(
/// activites like votes or comments).
pub(crate) async fn get_apub_community_outbox(
info: web::Path<CommunityQuery>,
context: web::Data<LemmyContext>,
context: Data<LemmyContext>,
) -> Result<HttpResponse, LemmyError> {
let community = Community::read_from_name(context.pool(), &info.community_name, false).await?;
let community: ApubCommunity =
Community::read_from_name(context.pool(), &info.community_name, false)
.await?
.into();
if community.deleted || community.removed {
return Err(LemmyError::from_message("deleted"));
}
let id = ObjectId::new(generate_outbox_url(&community.actor_id)?);
let outbox_data = CommunityContext(community.into(), context.get_ref().clone());
let outbox: ApubCommunityOutbox = id
.dereference(&outbox_data, local_instance(&context).await, &mut 0)
.await?;
Ok(create_apub_response(&outbox.into_apub(&outbox_data).await?))
let outbox = ApubCommunityOutbox::read_local(&community, &context).await?;
Ok(create_apub_response(&outbox))
}
#[tracing::instrument(skip_all)]
pub(crate) async fn get_apub_community_moderators(
info: web::Path<CommunityQuery>,
context: web::Data<LemmyContext>,
context: Data<LemmyContext>,
) -> Result<HttpResponse, LemmyError> {
let community: ApubCommunity =
Community::read_from_name(context.pool(), &info.community_name, false)
@ -101,29 +98,22 @@ pub(crate) async fn get_apub_community_moderators(
if community.deleted || community.removed {
return Err(LemmyError::from_message("deleted"));
}
let id = ObjectId::new(generate_outbox_url(&community.actor_id)?);
let outbox_data = CommunityContext(community, context.get_ref().clone());
let moderators: ApubCommunityModerators = id
.dereference(&outbox_data, local_instance(&context).await, &mut 0)
.await?;
Ok(create_apub_response(
&moderators.into_apub(&outbox_data).await?,
))
let moderators = ApubCommunityModerators::read_local(&community, &context).await?;
Ok(create_apub_response(&moderators))
}
/// Returns collection of featured (stickied) posts.
pub(crate) async fn get_apub_community_featured(
info: web::Path<CommunityQuery>,
context: web::Data<LemmyContext>,
context: Data<LemmyContext>,
) -> Result<HttpResponse, LemmyError> {
let community = Community::read_from_name(context.pool(), &info.community_name, false).await?;
let community: ApubCommunity =
Community::read_from_name(context.pool(), &info.community_name, false)
.await?
.into();
if community.deleted || community.removed {
return Err(LemmyError::from_message("deleted"));
}
let id = ObjectId::new(generate_featured_url(&community.actor_id)?);
let data = CommunityContext(community.into(), context.get_ref().clone());
let featured: ApubCommunityFeatured = id
.dereference(&data, local_instance(&context).await, &mut 0)
.await?;
Ok(create_apub_response(&featured.into_apub(&data).await?))
let featured = ApubCommunityFeatured::read_local(&community, &context).await?;
Ok(create_apub_response(&featured))
}

@ -1,28 +1,22 @@
use crate::{
activity_lists::SharedInboxActivities,
fetcher::user_or_community::UserOrCommunity,
insert_activity,
local_instance,
protocol::objects::tombstone::Tombstone,
CONTEXT,
};
use activitypub_federation::{
core::inbox::receive_activity,
data::Data,
deser::context::WithContext,
traits::{ActivityHandler, Actor, ApubObject},
APUB_JSON_CONTENT_TYPE,
actix_web::inbox::receive_activity,
config::Data,
protocol::context::WithContext,
FEDERATION_CONTENT_TYPE,
};
use actix_web::{web, HttpRequest, HttpResponse};
use actix_web::{web, web::Bytes, HttpRequest, HttpResponse};
use http::StatusCode;
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::source::activity::Activity;
use lemmy_utils::error::LemmyError;
use once_cell::sync::OnceCell;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_json::Value;
use serde::{Deserialize, Serialize};
use std::ops::Deref;
use tracing::{debug, log::info};
use url::Url;
mod comment;
@ -34,45 +28,11 @@ pub mod site;
pub async fn shared_inbox(
request: HttpRequest,
payload: String,
context: web::Data<LemmyContext>,
body: Bytes,
data: Data<LemmyContext>,
) -> Result<HttpResponse, LemmyError> {
receive_lemmy_activity::<SharedInboxActivities, UserOrCommunity>(request, payload, context).await
}
pub async fn receive_lemmy_activity<Activity, ActorT>(
request: HttpRequest,
payload: String,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse, LemmyError>
where
Activity: ActivityHandler<DataType = LemmyContext, Error = LemmyError>
+ DeserializeOwned
+ Send
+ 'static,
ActorT: ApubObject<DataType = LemmyContext, Error = LemmyError> + Actor + Send + 'static,
for<'de2> <ActorT as ApubObject>::ApubType: serde::Deserialize<'de2>,
{
static DATA: OnceCell<Data<LemmyContext>> = OnceCell::new();
let activity_value: Value = serde_json::from_str(&payload)?;
debug!("Parsing activity {}", payload);
let activity: Activity = serde_json::from_value(activity_value.clone())?;
// Log the activity, so we avoid receiving and parsing it twice.
let insert = insert_activity(activity.id(), activity_value, false, true, context.pool()).await?;
if !insert {
debug!("Received duplicate activity {}", activity.id().to_string());
return Ok(HttpResponse::BadRequest().finish());
}
info!("Received activity {}", payload);
let data = DATA.get_or_init(|| Data::new(context.get_ref().clone()));
receive_activity::<Activity, ActorT, LemmyContext>(
request,
activity,
local_instance(&context).await,
data,
)
.await
receive_activity::<SharedInboxActivities, UserOrCommunity, LemmyContext>(request, body, &data)
.await
}
/// Convert the data to json and turn it into an HTTP Response with the correct ActivityPub
@ -82,20 +42,20 @@ where
T: Serialize,
{
HttpResponse::Ok()
.content_type(APUB_JSON_CONTENT_TYPE)
.content_type(FEDERATION_CONTENT_TYPE)
.json(WithContext::new(data, CONTEXT.deref().clone()))
}
fn create_json_apub_response(data: serde_json::Value) -> HttpResponse {
HttpResponse::Ok()
.content_type(APUB_JSON_CONTENT_TYPE)
.content_type(FEDERATION_CONTENT_TYPE)
.json(data)
}
fn create_apub_tombstone_response<T: Into<Url>>(id: T) -> HttpResponse {
let tombstone = Tombstone::new(id.into());
HttpResponse::Gone()
.content_type(APUB_JSON_CONTENT_TYPE)
.content_type(FEDERATION_CONTENT_TYPE)
.status(StatusCode::GONE)
.json(WithContext::new(tombstone, CONTEXT.deref().clone()))
}

@ -1,12 +1,17 @@
use crate::{
activity_lists::PersonInboxActivitiesWithAnnouncable,
fetcher::user_or_community::UserOrCommunity,
http::{create_apub_response, create_apub_tombstone_response, receive_lemmy_activity},
http::{create_apub_response, create_apub_tombstone_response},
objects::person::ApubPerson,
protocol::collections::empty_outbox::EmptyOutbox,
};
use activitypub_federation::{deser::context::WithContext, traits::ApubObject};
use actix_web::{web, HttpRequest, HttpResponse};
use activitypub_federation::{
actix_web::inbox::receive_activity,
config::Data,
protocol::context::WithContext,
traits::Object,
};
use actix_web::{web, web::Bytes, HttpRequest, HttpResponse};
use lemmy_api_common::{context::LemmyContext, utils::generate_outbox_url};
use lemmy_db_schema::{source::person::Person, traits::ApubActor};
use lemmy_utils::error::LemmyError;
@ -21,7 +26,7 @@ pub struct PersonQuery {
#[tracing::instrument(skip_all)]
pub(crate) async fn get_apub_person_http(
info: web::Path<PersonQuery>,
context: web::Data<LemmyContext>,
context: Data<LemmyContext>,
) -> Result<HttpResponse, LemmyError> {
let user_name = info.into_inner().user_name;
// TODO: this needs to be able to read deleted persons, so that it can send tombstones
@ -30,7 +35,7 @@ pub(crate) async fn get_apub_person_http(
.into();
if !person.deleted {
let apub = person.into_apub(&context).await?;
let apub = person.into_json(&context).await?;
Ok(create_apub_response(&apub))
} else {
@ -41,11 +46,11 @@ pub(crate) async fn get_apub_person_http(
#[tracing::instrument(skip_all)]
pub async fn person_inbox(
request: HttpRequest,
payload: String,
context: web::Data<LemmyContext>,
body: Bytes,
data: Data<LemmyContext>,
) -> Result<HttpResponse, LemmyError> {
receive_lemmy_activity::<WithContext<PersonInboxActivitiesWithAnnouncable>, UserOrCommunity>(
request, payload, context,
receive_activity::<WithContext<PersonInboxActivitiesWithAnnouncable>, UserOrCommunity, LemmyContext>(
request, body, &data,
)
.await
}
@ -53,7 +58,7 @@ pub async fn person_inbox(
#[tracing::instrument(skip_all)]
pub(crate) async fn get_apub_person_outbox(
info: web::Path<PersonQuery>,
context: web::Data<LemmyContext>,
context: Data<LemmyContext>,
) -> Result<HttpResponse, LemmyError> {
let person = Person::read_from_name(context.pool(), &info.user_name, false).await?;
let outbox_id = generate_outbox_url(&person.actor_id)?.into();

@ -2,7 +2,7 @@ use crate::{
http::{create_apub_response, create_apub_tombstone_response, err_object_not_local},
objects::post::ApubPost,
};
use activitypub_federation::traits::ApubObject;
use activitypub_federation::{config::Data, traits::Object};
use actix_web::{web, HttpResponse};
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::{newtypes::PostId, source::post::Post, traits::Crud};
@ -18,7 +18,7 @@ pub(crate) struct PostQuery {
#[tracing::instrument(skip_all)]
pub(crate) async fn get_apub_post(
info: web::Path<PostQuery>,
context: web::Data<LemmyContext>,
context: Data<LemmyContext>,
) -> Result<HttpResponse, LemmyError> {
let id = PostId(info.post_id.parse::<i32>()?);
let post: ApubPost = Post::read(context.pool(), id).await?.into();
@ -27,7 +27,7 @@ pub(crate) async fn get_apub_post(
}
if !post.deleted && !post.removed {
Ok(create_apub_response(&post.into_apub(&context).await?))
Ok(create_apub_response(&post.into_json(&context).await?))
} else {
Ok(create_apub_tombstone_response(post.ap_id.clone()))
}

@ -1,28 +1,33 @@
use crate::{
activity_lists::SiteInboxActivities,
http::{create_apub_response, receive_lemmy_activity},
http::create_apub_response,
objects::{instance::ApubSite, person::ApubPerson},
protocol::collections::empty_outbox::EmptyOutbox,
};
use activitypub_federation::{deser::context::WithContext, traits::ApubObject};
use actix_web::{web, HttpRequest, HttpResponse};
use activitypub_federation::{
actix_web::inbox::receive_activity,
config::Data,
protocol::context::WithContext,
traits::Object,
};
use actix_web::{web::Bytes, HttpRequest, HttpResponse};
use lemmy_api_common::context::LemmyContext;
use lemmy_db_views::structs::SiteView;
use lemmy_utils::error::LemmyError;
use url::Url;
pub(crate) async fn get_apub_site_http(
context: web::Data<LemmyContext>,
context: Data<LemmyContext>,
) -> Result<HttpResponse, LemmyError> {
let site: ApubSite = SiteView::read_local(context.pool()).await?.site.into();
let apub = site.into_apub(&context).await?;
let apub = site.into_json(&context).await?;
Ok(create_apub_response(&apub))
}
#[tracing::instrument(skip_all)]
pub(crate) async fn get_apub_site_outbox(
context: web::Data<LemmyContext>,
context: Data<LemmyContext>,
) -> Result<HttpResponse, LemmyError> {
let outbox_id = format!(
"{}/site_outbox",
@ -35,9 +40,11 @@ pub(crate) async fn get_apub_site_outbox(
#[tracing::instrument(skip_all)]
pub async fn get_apub_site_inbox(
request: HttpRequest,
payload: String,
context: web::Data<LemmyContext>,
body: Bytes,
data: Data<LemmyContext>,
) -> Result<HttpResponse, LemmyError> {
receive_lemmy_activity::<WithContext<SiteInboxActivities>, ApubPerson>(request, payload, context)
.await
receive_activity::<WithContext<SiteInboxActivities>, ApubPerson, LemmyContext>(
request, body, &data,
)
.await
}

@ -1,20 +1,19 @@
use crate::fetcher::post_or_comment::PostOrComment;
use activitypub_federation::{
core::signatures::PublicKey,
traits::{Actor, ApubObject},
InstanceSettings,
LocalInstance,
UrlVerifier,
};
use activitypub_federation::config::{Data, UrlVerifier};
use async_trait::async_trait;
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::{
source::{activity::Activity, instance::Instance, local_site::LocalSite},
source::{
activity::{Activity, ActivityInsertForm},
instance::Instance,
local_site::LocalSite,
},
traits::Crud,
utils::DbPool,
};
use lemmy_utils::{error::LemmyError, settings::structs::Settings};
use once_cell::sync::Lazy;
use tokio::sync::OnceCell;
use serde::Serialize;
use url::Url;
pub mod activities;
@ -27,52 +26,23 @@ pub(crate) mod mentions;
pub mod objects;
pub mod protocol;
const FEDERATION_HTTP_FETCH_LIMIT: i32 = 25;
pub const FEDERATION_HTTP_FETCH_LIMIT: u32 = 50;
static CONTEXT: Lazy<Vec<serde_json::Value>> = Lazy::new(|| {
serde_json::from_str(include_str!("../assets/lemmy/context.json")).expect("parse context")
});
// TODO: store this in context? but its only used in this crate, no need to expose it elsewhere
// TODO this singleton needs to be redone to account for live data.
async fn local_instance(context: &LemmyContext) -> &'static LocalInstance {
static LOCAL_INSTANCE: OnceCell<LocalInstance> = OnceCell::const_new();
LOCAL_INSTANCE
.get_or_init(|| async {
// Local site may be missing
let local_site = &LocalSite::read(context.pool()).await;
let worker_count = local_site
.as_ref()
.map(|l| l.federation_worker_count)
.unwrap_or(64) as u64;
let settings = InstanceSettings::builder()
.http_fetch_retry_limit(FEDERATION_HTTP_FETCH_LIMIT)
.worker_count(worker_count)
.debug(cfg!(debug_assertions))
.http_signature_compat(true)
.url_verifier(Box::new(VerifyUrlData(context.clone())))
.build()
.expect("configure federation");
LocalInstance::new(
context.settings().hostname.clone(),
context.client().clone(),
settings,
)
})
.await
}
#[derive(Clone)]
struct VerifyUrlData(LemmyContext);
pub struct VerifyUrlData(pub DbPool);
#[async_trait]
impl UrlVerifier for VerifyUrlData {
async fn verify(&self, url: &Url) -> Result<(), &'static str> {
let local_site_data = fetch_local_site_data(self.0.pool())
let local_site_data = fetch_local_site_data(&self.0)
.await
.expect("read local site data");
check_apub_id_valid(url, &local_site_data, self.0.settings())
check_apub_id_valid(url, &local_site_data)?;
Ok(())
}
}
@ -86,19 +56,9 @@ impl UrlVerifier for VerifyUrlData {
///
/// `use_strict_allowlist` should be true only when parsing a remote community, or when parsing a
/// post/comment in a local community.
#[tracing::instrument(skip(settings, local_site_data))]
fn check_apub_id_valid(
apub_id: &Url,
local_site_data: &LocalSiteData,
settings: &Settings,
) -> Result<(), &'static str> {
#[tracing::instrument(skip(local_site_data))]
fn check_apub_id_valid(apub_id: &Url, local_site_data: &LocalSiteData) -> Result<(), &'static str> {
let domain = apub_id.domain().expect("apud id has domain").to_string();
let local_instance = settings
.get_hostname_without_port()
.expect("local hostname is valid");
if domain == local_instance {
return Ok(());
}
if !local_site_data
.local_site
@ -109,10 +69,6 @@ fn check_apub_id_valid(
return Err("Federation disabled");
}
if apub_id.scheme() != settings.get_protocol_string() {
return Err("Invalid protocol scheme");
}
if let Some(blocked) = local_site_data.blocked_instances.as_ref() {
if blocked.iter().any(|i| domain.eq(&i.domain)) {
return Err("Domain is blocked");
@ -161,7 +117,6 @@ pub(crate) fn check_apub_id_valid_with_strictness(
local_site_data: &LocalSiteData,
settings: &Settings,
) -> Result<(), LemmyError> {
check_apub_id_valid(apub_id, local_site_data, settings).map_err(LemmyError::from_message)?;
let domain = apub_id.domain().expect("apud id has domain").to_string();
let local_instance = settings
.get_hostname_without_port()
@ -169,6 +124,7 @@ pub(crate) fn check_apub_id_valid_with_strictness(
if domain == local_instance {
return Ok(());
}
check_apub_id_valid(apub_id, local_site_data).map_err(LemmyError::from_message)?;
if let Some(allowed) = local_site_data.allowed_instances.as_ref() {
// Only check allowlist if this is a community
@ -179,8 +135,12 @@ pub(crate) fn check_apub_id_valid_with_strictness(
.iter()
.map(|i| i.domain.clone())
.collect::<Vec<String>>();
let local_instance = settings
.get_hostname_without_port()
.expect("local hostname is valid");
allowed_and_local.push(local_instance);
let domain = apub_id.domain().expect("apud id has domain").to_string();
if !allowed_and_local.contains(&domain) {
return Err(LemmyError::from_message(
"Federation forbidden by strict allowlist",
@ -191,40 +151,41 @@ pub(crate) fn check_apub_id_valid_with_strictness(
Ok(())
}
/// Store a sent or received activity in the database, for logging purposes. These records are not
/// persistent.
#[tracing::instrument(skip(pool))]
async fn insert_activity(
/// Store a sent or received activity in the database.
///
/// Stored activities are served over the HTTP endpoint `GET /activities/{type_}/{id}`. This also
/// ensures that the same activity cannot be received more than once.
#[tracing::instrument(skip(data, activity))]
async fn insert_activity<T>(
ap_id: &Url,
activity: serde_json::Value,
activity: &T,
local: bool,
sensitive: bool,
pool: &DbPool,
) -> Result<bool, LemmyError> {
data: &Data<LemmyContext>,
) -> Result<(), LemmyError>
where
T: Serialize,
{
let ap_id = ap_id.clone().into();
Ok(Activity::insert(pool, ap_id, activity, local, Some(sensitive)).await?)
}
/// Common methods provided by ActivityPub actors (community and person). Not all methods are
/// implemented by all actors.
pub trait ActorType: Actor + ApubObject {
fn actor_id(&self) -> Url;
fn private_key(&self) -> Option<String>;
fn get_public_key(&self) -> PublicKey {
PublicKey::new_main_key(self.actor_id(), self.public_key().to_string())
}
let form = ActivityInsertForm {
ap_id,
data: serde_json::to_value(activity)?,
local: Some(local),
sensitive: Some(sensitive),
updated: None,
};
Activity::create(data.pool(), &form).await?;
Ok(())
}
#[async_trait::async_trait(?Send)]
pub trait SendActivity {
type Response;
#[async_trait::async_trait]
pub trait SendActivity: Sync {
type Response: Sync + Send;
async fn send_activity(
_request: &Self,
_response: &Self::Response,
_context: &LemmyContext,
_context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
Ok(())
}

@ -1,10 +1,10 @@
use crate::{
fetcher::webfinger::webfinger_resolve_actor,
objects::{comment::ApubComment, community::ApubCommunity, person::ApubPerson},
ActorType,
use crate::objects::{comment::ApubComment, community::ApubCommunity, person::ApubPerson};
use activitypub_federation::{
config::Data,
fetch::{object_id::ObjectId, webfinger::webfinger_resolve_actor},
kinds::link::MentionType,
traits::Actor,
};
use activitypub_federation::core::object_id::ObjectId;
use activitystreams_kinds::link::MentionType;
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::{
source::{comment::Comment, person::Person, post::Post},
@ -46,11 +46,10 @@ pub struct MentionsAndAddresses {
pub async fn collect_non_local_mentions(
comment: &ApubComment,
community_id: ObjectId<ApubCommunity>,
context: &LemmyContext,
request_counter: &mut i32,
context: &Data<LemmyContext>,
) -> Result<MentionsAndAddresses, LemmyError> {
let parent_creator = get_comment_parent_creator(context.pool(), comment).await?;
let mut addressed_ccs: Vec<Url> = vec![community_id.into(), parent_creator.actor_id()];
let mut addressed_ccs: Vec<Url> = vec![community_id.into(), parent_creator.id()];
// Add the mention tag
let parent_creator_tag = Mention {
@ -58,7 +57,7 @@ pub async fn collect_non_local_mentions(
name: Some(format!(
"@{}@{}",
&parent_creator.name,
&parent_creator.actor_id().domain().expect("has domain")
&parent_creator.id().domain().expect("has domain")
)),
kind: MentionType::Mention,
};
@ -73,14 +72,12 @@ pub async fn collect_non_local_mentions(
for mention in &mentions {
let identifier = format!("{}@{}", mention.name, mention.domain);
let actor_id =
webfinger_resolve_actor::<ApubPerson>(&identifier, true, context, request_counter).await;
if let Ok(actor_id) = actor_id {
let actor_id: ObjectId<ApubPerson> = ObjectId::new(actor_id);
addressed_ccs.push(actor_id.to_string().parse()?);
let person = webfinger_resolve_actor::<LemmyContext, ApubPerson>(&identifier, context).await;
if let Ok(person) = person {
addressed_ccs.push(person.actor_id.to_string().parse()?);
let mention_tag = Mention {
href: actor_id.into(),
href: person.id(),
name: Some(mention.full_name()),
kind: MentionType::Mention,
};

@ -2,7 +2,6 @@ use crate::{
activities::{verify_is_public, verify_person_in_community},
check_apub_id_valid_with_strictness,
fetch_local_site_data,
local_instance,
mentions::collect_non_local_mentions,
objects::{read_from_string_or_source, verify_is_remote_object},
protocol::{
@ -10,15 +9,13 @@ use crate::{
InCommunity,
Source,
},
PostOrComment,
};
use activitypub_federation::{
core::object_id::ObjectId,
deser::values::MediaTypeMarkdownOrHtml,
traits::ApubObject,
utils::verify_domains_match,
config::Data,
kinds::{object::NoteType, public},
protocol::{values::MediaTypeMarkdownOrHtml, verification::verify_domains_match},
traits::Object,
};
use activitystreams_kinds::{object::NoteType, public};
use chrono::NaiveDateTime;
use lemmy_api_common::{context::LemmyContext, utils::local_site_opt_to_slur_regex};
use lemmy_db_schema::{
@ -54,11 +51,10 @@ impl From<Comment> for ApubComment {
}
}
#[async_trait::async_trait(?Send)]
impl ApubObject for ApubComment {
#[async_trait::async_trait]
impl Object for ApubComment {
type DataType = LemmyContext;
type ApubType = Note;
type DbType = Comment;
type Kind = Note;
type Error = LemmyError;
fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
@ -66,9 +62,9 @@ impl ApubObject for ApubComment {
}
#[tracing::instrument(skip_all)]
async fn read_from_apub_id(
async fn read_from_id(
object_id: Url,
context: &LemmyContext,
context: &Data<Self::DataType>,
) -> Result<Option<Self>, LemmyError> {
Ok(
Comment::read_from_apub_id(context.pool(), object_id)
@ -78,7 +74,7 @@ impl ApubObject for ApubComment {
}
#[tracing::instrument(skip_all)]
async fn delete(self, context: &LemmyContext) -> Result<(), LemmyError> {
async fn delete(self, context: &Data<Self::DataType>) -> Result<(), LemmyError> {
if !self.deleted {
let form = CommentUpdateForm::builder().deleted(Some(true)).build();
Comment::update(context.pool(), self.id, &form).await?;
@ -87,7 +83,7 @@ impl ApubObject for ApubComment {
}
#[tracing::instrument(skip_all)]
async fn into_apub(self, context: &LemmyContext) -> Result<Note, LemmyError> {
async fn into_json(self, context: &Data<Self::DataType>) -> Result<Note, LemmyError> {
let creator_id = self.creator_id;
let creator = Person::read(context.pool(), creator_id).await?;
@ -98,23 +94,17 @@ impl ApubObject for ApubComment {
let in_reply_to = if let Some(comment_id) = self.parent_comment_id() {
let parent_comment = Comment::read(context.pool(), comment_id).await?;
ObjectId::<PostOrComment>::new(parent_comment.ap_id)
parent_comment.ap_id.into()
} else {
ObjectId::<PostOrComment>::new(post.ap_id)
post.ap_id.into()
};
let language = LanguageTag::new_single(self.language_id, context.pool()).await?;
let maa = collect_non_local_mentions(
&self,
ObjectId::new(community.actor_id.clone()),
context,
&mut 0,
)
.await?;
let maa = collect_non_local_mentions(&self, community.actor_id.clone().into(), context).await?;
let note = Note {
r#type: NoteType::Note,
id: ObjectId::new(self.ap_id.clone()),
attributed_to: ObjectId::new(creator.actor_id),
id: self.ap_id.clone().into(),
attributed_to: creator.actor_id.into(),
to: vec![public()],
cc: maa.ccs,
content: markdown_to_html(&self.content),
@ -126,7 +116,7 @@ impl ApubObject for ApubComment {
tag: maa.tags,
distinguished: Some(self.distinguished),
language,
audience: Some(ObjectId::new(community.actor_id)),
audience: Some(community.actor_id.into()),
};
Ok(note)
@ -136,13 +126,12 @@ impl ApubObject for ApubComment {
async fn verify(
note: &Note,
expected_domain: &Url,
context: &LemmyContext,
request_counter: &mut i32,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
verify_domains_match(note.id.inner(), expected_domain)?;
verify_domains_match(note.attributed_to.inner(), note.id.inner())?;
verify_is_public(&note.to, &note.cc)?;
let community = note.community(context, request_counter).await?;
let community = note.community(context).await?;
let local_site_data = fetch_local_site_data(context.pool()).await?;
check_apub_id_valid_with_strictness(
@ -152,8 +141,8 @@ impl ApubObject for ApubComment {
context.settings(),
)?;
verify_is_remote_object(note.id.inner(), context.settings())?;
verify_person_in_community(&note.attributed_to, &community, context, request_counter).await?;
let (post, _) = note.get_parents(context, request_counter).await?;
verify_person_in_community(&note.attributed_to, &community, context).await?;
let (post, _) = note.get_parents(context).await?;
if post.locked {
return Err(LemmyError::from_message("Post is locked"));
}
@ -164,16 +153,9 @@ impl ApubObject for ApubComment {
///
/// If the parent community, post and comment(s) are not known locally, these are also fetched.
#[tracing::instrument(skip_all)]
async fn from_apub(
note: Note,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<ApubComment, LemmyError> {
let creator = note
.attributed_to
.dereference(context, local_instance(context).await, request_counter)
.await?;
let (post, parent_comment) = note.get_parents(context, request_counter).await?;
async fn from_json(note: Note, context: &Data<LemmyContext>) -> Result<ApubComment, LemmyError> {
let creator = note.attributed_to.dereference(context).await?;
let (post, parent_comment) = note.get_parents(context).await?;
let content = read_from_string_or_source(&note.content, &note.media_type, &note.source);
@ -221,17 +203,15 @@ pub(crate) mod tests {
async fn prepare_comment_test(
url: &Url,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> (ApubPerson, ApubCommunity, ApubPost, ApubSite) {
let (person, site) = parse_lemmy_person(context).await;
let community = parse_lemmy_community(context).await;
// use separate counter so this doesnt affect tests
let context2 = context.reset_request_count();
let (person, site) = parse_lemmy_person(&context2).await;
let community = parse_lemmy_community(&context2).await;
let post_json = file_to_json_object("assets/lemmy/objects/page.json").unwrap();
ApubPost::verify(&post_json, url, context, &mut 0)
.await
.unwrap();
let post = ApubPost::from_apub(post_json, context, &mut 0)
.await
.unwrap();
ApubPost::verify(&post_json, url, &context2).await.unwrap();
let post = ApubPost::from_json(post_json, &context2).await.unwrap();
(person, community, post, site)
}
@ -251,21 +231,18 @@ pub(crate) mod tests {
let data = prepare_comment_test(&url, &context).await;
let json: Note = file_to_json_object("assets/lemmy/objects/note.json").unwrap();
let mut request_counter = 0;
ApubComment::verify(&json, &url, &context, &mut request_counter)
.await
.unwrap();
let comment = ApubComment::from_apub(json.clone(), &context, &mut request_counter)
ApubComment::verify(&json, &url, &context).await.unwrap();
let comment = ApubComment::from_json(json.clone(), &context)
.await
.unwrap();
assert_eq!(comment.ap_id, url.into());
assert_eq!(comment.content.len(), 14);
assert!(!comment.local);
assert_eq!(request_counter, 0);
assert_eq!(context.request_count(), 0);
let comment_id = comment.id;
let to_apub = comment.into_apub(&context).await.unwrap();
let to_apub = comment.into_json(&context).await.unwrap();
assert_json_include!(actual: json, expected: to_apub);
Comment::delete(context.pool(), comment_id).await.unwrap();
@ -283,25 +260,20 @@ pub(crate) mod tests {
Url::parse("https://queer.hacktivis.me/objects/8d4973f4-53de-49cd-8c27-df160e16a9c2")
.unwrap();
let person_json = file_to_json_object("assets/pleroma/objects/person.json").unwrap();
ApubPerson::verify(&person_json, &pleroma_url, &context, &mut 0)
.await
.unwrap();
ApubPerson::from_apub(person_json, &context, &mut 0)
ApubPerson::verify(&person_json, &pleroma_url, &context)
.await
.unwrap();
ApubPerson::from_json(person_json, &context).await.unwrap();
let json = file_to_json_object("assets/pleroma/objects/note.json").unwrap();
let mut request_counter = 0;
ApubComment::verify(&json, &pleroma_url, &context, &mut request_counter)
.await
.unwrap();
let comment = ApubComment::from_apub(json, &context, &mut request_counter)
ApubComment::verify(&json, &pleroma_url, &context)
.await
.unwrap();
let comment = ApubComment::from_json(json, &context).await.unwrap();
assert_eq!(comment.ap_id, pleroma_url.into());
assert_eq!(comment.content.len(), 64);
assert!(!comment.local);
assert_eq!(request_counter, 0);
assert_eq!(context.request_count(), 1);
Comment::delete(context.pool(), comment.id).await.unwrap();
cleanup(data, &context).await;

@ -1,21 +1,18 @@
use crate::{
check_apub_id_valid_with_strictness,
collections::CommunityContext,
fetch_local_site_data,
local_instance,
objects::instance::fetch_instance_actor_for_object,
protocol::{
objects::{group::Group, Endpoints, LanguageTag},
ImageObject,
Source,
},
ActorType,
};
use activitypub_federation::{
core::object_id::ObjectId,
traits::{Actor, ApubObject},
config::Data,
kinds::actor::GroupType,
traits::{Actor, Object},
};
use activitystreams_kinds::actor::GroupType;
use chrono::NaiveDateTime;
use itertools::Itertools;
use lemmy_api_common::{
@ -54,11 +51,10 @@ impl From<Community> for ApubCommunity {
}
}
#[async_trait::async_trait(?Send)]
impl ApubObject for ApubCommunity {
#[async_trait::async_trait]
impl Object for ApubCommunity {
type DataType = LemmyContext;
type ApubType = Group;
type DbType = Community;
type Kind = Group;
type Error = LemmyError;
fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
@ -66,9 +62,9 @@ impl ApubObject for ApubCommunity {
}
#[tracing::instrument(skip_all)]
async fn read_from_apub_id(
async fn read_from_id(
object_id: Url,
context: &LemmyContext,
context: &Data<Self::DataType>,
) -> Result<Option<Self>, LemmyError> {
Ok(
Community::read_from_apub_id(context.pool(), &object_id.into())
@ -78,21 +74,21 @@ impl ApubObject for ApubCommunity {
}
#[tracing::instrument(skip_all)]
async fn delete(self, context: &LemmyContext) -> Result<(), LemmyError> {
async fn delete(self, context: &Data<Self::DataType>) -> Result<(), LemmyError> {
let form = CommunityUpdateForm::builder().deleted(Some(true)).build();
Community::update(context.pool(), self.id, &form).await?;
Ok(())
}
#[tracing::instrument(skip_all)]
async fn into_apub(self, data: &LemmyContext) -> Result<Group, LemmyError> {
async fn into_json(self, data: &Data<Self::DataType>) -> Result<Group, LemmyError> {
let community_id = self.id;
let langs = CommunityLanguage::read(data.pool(), community_id).await?;
let language = LanguageTag::new_multiple(langs, data.pool()).await?;
let group = Group {
kind: GroupType::Group,
id: ObjectId::new(self.actor_id()),
id: self.id().into(),
preferred_username: self.name.clone(),
name: Some(self.title.clone()),
summary: self.description.as_ref().map(|b| markdown_to_html(b)),
@ -103,12 +99,12 @@ impl ApubObject for ApubCommunity {
moderators: Some(generate_moderators_url(&self.actor_id)?.into()),
featured: Some(generate_featured_url(&self.actor_id)?.into()),
inbox: self.inbox_url.clone().into(),
outbox: ObjectId::new(generate_outbox_url(&self.actor_id)?),
outbox: generate_outbox_url(&self.actor_id)?.into(),
followers: self.followers_url.clone().into(),
endpoints: self.shared_inbox_url.clone().map(|s| Endpoints {
shared_inbox: s.into(),
}),
public_key: self.get_public_key(),
public_key: self.public_key(),
language,
published: Some(convert_datetime(self.published)),
updated: self.updated.map(convert_datetime),
@ -122,20 +118,18 @@ impl ApubObject for ApubCommunity {
async fn verify(
group: &Group,
expected_domain: &Url,
context: &LemmyContext,
_request_counter: &mut i32,
context: &Data<Self::DataType>,
) -> Result<(), LemmyError> {
group.verify(expected_domain, context).await
}
/// Converts a `Group` to `Community`, inserts it into the database and updates moderators.
#[tracing::instrument(skip_all)]
async fn from_apub(
async fn from_json(
group: Group,
context: &LemmyContext,
request_counter: &mut i32,
context: &Data<Self::DataType>,
) -> Result<ApubCommunity, LemmyError> {
let instance_id = fetch_instance_actor_for_object(&group.id, context, request_counter).await?;
let instance_id = fetch_instance_actor_for_object(&group.id, context).await?;
let form = Group::into_insert_form(group.clone(), instance_id);
let languages = LanguageTag::to_language_id_multiple(group.language, context.pool()).await?;
@ -144,20 +138,19 @@ impl ApubObject for ApubCommunity {
CommunityLanguage::update(context.pool(), languages, community.id).await?;
let community: ApubCommunity = community.into();
let outbox_data = CommunityContext(community.clone(), context.clone());
// 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.
group
.outbox
.dereference(&outbox_data, local_instance(context).await, request_counter)
.dereference(&community, context)
.await
.map_err(|e| debug!("{}", e))
.ok();
if let Some(moderators) = group.attributed_to.or(group.moderators) {
moderators
.dereference(&outbox_data, local_instance(context).await, request_counter)
.dereference(&community, context)
.await
.map_err(|e| debug!("{}", e))
.ok();
@ -168,10 +161,18 @@ impl ApubObject for ApubCommunity {
}
impl Actor for ApubCommunity {
fn public_key(&self) -> &str {
fn id(&self) -> Url {
self.actor_id.inner().clone()
}
fn public_key_pem(&self) -> &str {
&self.public_key
}
fn private_key_pem(&self) -> Option<String> {
self.private_key.clone()
}
fn inbox(&self) -> Url {
self.inbox_url.clone().into()
}
@ -181,15 +182,6 @@ impl Actor for ApubCommunity {
}
}
impl ActorType for ApubCommunity {
fn actor_id(&self) -> Url {
self.actor_id.clone().into()
}
fn private_key(&self) -> Option<String> {
self.private_key.clone()
}
}
impl ApubCommunity {
/// For a given community, returns the inboxes of all followers.
#[tracing::instrument(skip_all)]
@ -230,27 +222,25 @@ pub(crate) mod tests {
objects::{instance::tests::parse_lemmy_instance, tests::init_context},
protocol::tests::file_to_json_object,
};
use activitypub_federation::fetch::collection_id::CollectionId;
use lemmy_db_schema::{source::site::Site, traits::Crud};
use serial_test::serial;
pub(crate) async fn parse_lemmy_community(context: &LemmyContext) -> ApubCommunity {
pub(crate) async fn parse_lemmy_community(context: &Data<LemmyContext>) -> ApubCommunity {
// use separate counter so this doesnt affect tests
let context2 = context.reset_request_count();
let mut json: Group = file_to_json_object("assets/lemmy/objects/group.json").unwrap();
// change these links so they dont fetch over the network
json.moderators = None;
json.attributed_to = None;
json.outbox =
ObjectId::new(Url::parse("https://enterprise.lemmy.ml/c/tenforward/not_outbox").unwrap());
CollectionId::parse("https://enterprise.lemmy.ml/c/tenforward/not_outbox").unwrap();
let url = Url::parse("https://enterprise.lemmy.ml/c/tenforward").unwrap();
let mut request_counter = 0;
ApubCommunity::verify(&json, &url, context, &mut request_counter)
.await
.unwrap();
let community = ApubCommunity::from_apub(json, context, &mut request_counter)
.await
.unwrap();
ApubCommunity::verify(&json, &url, &context2).await.unwrap();
let community = ApubCommunity::from_json(json, &context2).await.unwrap();
// this makes one requests to the (intentionally broken) outbox collection
assert_eq!(request_counter, 1);
assert_eq!(context2.request_count(), 1);
community
}

@ -1,22 +1,20 @@
use crate::{
check_apub_id_valid_with_strictness,
fetch_local_site_data,
local_instance,
objects::read_from_string_or_source_opt,
protocol::{
objects::{instance::Instance, LanguageTag},
ImageObject,
Source,
},
ActorType,
};
use activitypub_federation::{
core::object_id::ObjectId,
deser::values::MediaTypeHtml,
traits::{Actor, ApubObject},
utils::verify_domains_match,
config::Data,
fetch::object_id::ObjectId,
kinds::actor::ApplicationType,
protocol::{values::MediaTypeHtml, verification::verify_domains_match},
traits::{Actor, Object},
};
use activitystreams_kinds::actor::ApplicationType;
use chrono::NaiveDateTime;
use lemmy_api_common::{context::LemmyContext, utils::local_site_opt_to_slur_regex};
use lemmy_db_schema::{
@ -57,11 +55,10 @@ impl From<Site> for ApubSite {
}
}
#[async_trait::async_trait(?Send)]
impl ApubObject for ApubSite {
#[async_trait::async_trait]
impl Object for ApubSite {
type DataType = LemmyContext;
type ApubType = Instance;
type DbType = Site;
type Kind = Instance;
type Error = LemmyError;
fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
@ -69,9 +66,9 @@ impl ApubObject for ApubSite {
}
#[tracing::instrument(skip_all)]
async fn read_from_apub_id(
async fn read_from_id(
object_id: Url,
data: &Self::DataType,
data: &Data<Self::DataType>,
) -> Result<Option<Self>, LemmyError> {
Ok(
Site::read_from_apub_id(data.pool(), object_id)
@ -80,19 +77,19 @@ impl ApubObject for ApubSite {
)
}
async fn delete(self, _data: &Self::DataType) -> Result<(), LemmyError> {
async fn delete(self, _data: &Data<Self::DataType>) -> Result<(), LemmyError> {
unimplemented!()
}
#[tracing::instrument(skip_all)]
async fn into_apub(self, data: &Self::DataType) -> Result<Self::ApubType, LemmyError> {
async fn into_json(self, data: &Data<Self::DataType>) -> Result<Self::Kind, LemmyError> {
let site_id = self.id;
let langs = SiteLanguage::read(data.pool(), site_id).await?;
let language = LanguageTag::new_multiple(langs, data.pool()).await?;
let instance = Instance {
kind: ApplicationType::Application,
id: ObjectId::new(self.actor_id()),
id: self.id().into(),
name: self.name.clone(),
content: self.sidebar.as_ref().map(|d| markdown_to_html(d)),
source: self.sidebar.clone().map(Source::new),
@ -102,7 +99,7 @@ impl ApubObject for ApubSite {
image: self.banner.clone().map(ImageObject::new),
inbox: self.inbox_url.clone().into(),
outbox: Url::parse(&format!("{}/site_outbox", self.actor_id))?,
public_key: self.get_public_key(),
public_key: self.public_key(),
language,
published: convert_datetime(self.published),
updated: self.updated.map(convert_datetime),
@ -112,10 +109,9 @@ impl ApubObject for ApubSite {
#[tracing::instrument(skip_all)]
async fn verify(
apub: &Self::ApubType,
apub: &Self::Kind,
expected_domain: &Url,
data: &Self::DataType,
_request_counter: &mut i32,
data: &Data<Self::DataType>,
) -> Result<(), LemmyError> {
let local_site_data = fetch_local_site_data(data.pool()).await?;
@ -130,11 +126,7 @@ impl ApubObject for ApubSite {
}
#[tracing::instrument(skip_all)]
async fn from_apub(
apub: Self::ApubType,
data: &Self::DataType,
_request_counter: &mut i32,
) -> Result<Self, LemmyError> {
async fn from_json(apub: Self::Kind, data: &Data<Self::DataType>) -> Result<Self, LemmyError> {
let domain = apub.id.inner().domain().expect("group id has domain");
let instance = DbInstance::read_or_create(data.pool(), domain.to_string()).await?;
@ -160,20 +152,19 @@ impl ApubObject for ApubSite {
}
}
impl ActorType for ApubSite {
fn actor_id(&self) -> Url {
self.actor_id.clone().into()
}
fn private_key(&self) -> Option<String> {
self.private_key.clone()
impl Actor for ApubSite {
fn id(&self) -> Url {
self.actor_id.inner().clone()
}
}
impl Actor for ApubSite {
fn public_key(&self) -> &str {
fn public_key_pem(&self) -> &str {
&self.public_key
}
fn private_key_pem(&self) -> Option<String> {
self.private_key.clone()
}
fn inbox(&self) -> Url {
self.inbox_url.clone().into()
}
@ -182,13 +173,12 @@ impl Actor for ApubSite {
/// Try to fetch the instance actor (to make things like instance rules available).
pub(in crate::objects) async fn fetch_instance_actor_for_object<T: Into<Url> + Clone>(
object_id: &T,
context: &LemmyContext,
request_counter: &mut i32,
context: &Data<LemmyContext>,
) -> Result<InstanceId, LemmyError> {
let object_id: Url = object_id.clone().into();
let instance_id = Site::instance_actor_id_from_url(object_id);
let site = ObjectId::<ApubSite>::new(instance_id.clone())
.dereference(context, local_instance(context).await, request_counter)
let site = ObjectId::<ApubSite>::from(instance_id.clone())
.dereference(context)
.await;
match site {
Ok(s) => Ok(s.instance_id),
@ -222,17 +212,12 @@ pub(crate) mod tests {
use lemmy_db_schema::traits::Crud;
use serial_test::serial;
pub(crate) async fn parse_lemmy_instance(context: &LemmyContext) -> ApubSite {
pub(crate) async fn parse_lemmy_instance(context: &Data<LemmyContext>) -> ApubSite {
let json: Instance = file_to_json_object("assets/lemmy/objects/instance.json").unwrap();
let id = Url::parse("https://enterprise.lemmy.ml/").unwrap();
let mut request_counter = 0;
ApubSite::verify(&json, &id, context, &mut request_counter)
.await
.unwrap();
let site = ApubSite::from_apub(json, context, &mut request_counter)
.await
.unwrap();
assert_eq!(request_counter, 0);
ApubSite::verify(&json, &id, context).await.unwrap();
let site = ApubSite::from_json(json, context).await.unwrap();
assert_eq!(context.request_count(), 0);
site
}

@ -1,5 +1,5 @@
use crate::protocol::Source;
use activitypub_federation::deser::values::MediaTypeMarkdownOrHtml;
use activitypub_federation::protocol::values::MediaTypeMarkdownOrHtml;
use anyhow::anyhow;
use html2md::parse_html;
use lemmy_utils::{error::LemmyError, settings::structs::Settings};
@ -54,6 +54,7 @@ pub(crate) fn verify_is_remote_object(id: &Url, settings: &Settings) -> Result<(
#[cfg(test)]
pub(crate) mod tests {
use activitypub_federation::config::{Data, FederationConfig};
use anyhow::anyhow;
use lemmy_api_common::{
context::LemmyContext,
@ -87,7 +88,7 @@ pub(crate) mod tests {
}
// TODO: would be nice if we didnt have to use a full context for tests.
pub(crate) async fn init_context() -> LemmyContext {
pub(crate) async fn init_context() -> Data<LemmyContext> {
async fn x() -> Result<String, LemmyError> {
Ok(String::new())
}
@ -110,13 +111,12 @@ pub(crate) mod tests {
let rate_limit_cell = RateLimitCell::new(rate_limit_config).await;
let chat_server = Arc::new(ChatServer::startup());
LemmyContext::create(
pool,
chat_server,
client,
settings,
secret,
rate_limit_cell.clone(),
)
let context = LemmyContext::create(pool, chat_server, client, secret, rate_limit_cell.clone());
let config = FederationConfig::builder()
.domain("example.com")
.app_data(context)
.build()
.unwrap();
config.to_request_data()
}
}

@ -10,12 +10,11 @@ use crate::{
ImageObject,
Source,
},
ActorType,
};
use activitypub_federation::{
core::object_id::ObjectId,
traits::{Actor, ApubObject},
utils::verify_domains_match,
config::Data,
protocol::verification::verify_domains_match,
traits::{Actor, Object},
};
use chrono::NaiveDateTime;
use lemmy_api_common::{
@ -54,11 +53,10 @@ impl From<DbPerson> for ApubPerson {
}
}
#[async_trait::async_trait(?Send)]
impl ApubObject for ApubPerson {
#[async_trait::async_trait]
impl Object for ApubPerson {
type DataType = LemmyContext;
type ApubType = Person;
type DbType = DbPerson;
type Kind = Person;
type Error = LemmyError;
fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
@ -66,9 +64,9 @@ impl ApubObject for ApubPerson {
}
#[tracing::instrument(skip_all)]
async fn read_from_apub_id(
async fn read_from_id(
object_id: Url,
context: &LemmyContext,
context: &Data<Self::DataType>,
) -> Result<Option<Self>, LemmyError> {
Ok(
DbPerson::read_from_apub_id(context.pool(), &object_id.into())
@ -78,14 +76,14 @@ impl ApubObject for ApubPerson {
}
#[tracing::instrument(skip_all)]
async fn delete(self, context: &LemmyContext) -> Result<(), LemmyError> {
async fn delete(self, context: &Data<Self::DataType>) -> Result<(), LemmyError> {
let form = PersonUpdateForm::builder().deleted(Some(true)).build();
DbPerson::update(context.pool(), self.id, &form).await?;
Ok(())
}
#[tracing::instrument(skip_all)]
async fn into_apub(self, _pool: &LemmyContext) -> Result<Person, LemmyError> {
async fn into_json(self, _context: &Data<Self::DataType>) -> Result<Person, LemmyError> {
let kind = if self.bot_account {
UserTypes::Service
} else {
@ -94,7 +92,7 @@ impl ApubObject for ApubPerson {
let person = Person {
kind,
id: ObjectId::new(self.actor_id.clone()),
id: self.actor_id.clone().into(),
preferred_username: self.name.clone(),
name: self.display_name.clone(),
summary: self.bio.as_ref().map(|b| markdown_to_html(b)),
@ -107,7 +105,7 @@ impl ApubObject for ApubPerson {
endpoints: self.shared_inbox_url.clone().map(|s| Endpoints {
shared_inbox: s.into(),
}),
public_key: self.get_public_key(),
public_key: self.public_key(),
updated: self.updated.map(convert_datetime),
inbox: self.inbox_url.clone().into(),
};
@ -118,8 +116,7 @@ impl ApubObject for ApubPerson {
async fn verify(
person: &Person,
expected_domain: &Url,
context: &LemmyContext,
_request_counter: &mut i32,
context: &Data<Self::DataType>,
) -> Result<(), LemmyError> {
let local_site_data = fetch_local_site_data(context.pool()).await?;
let slur_regex = &local_site_opt_to_slur_regex(&local_site_data.local_site);
@ -141,12 +138,11 @@ impl ApubObject for ApubPerson {
}
#[tracing::instrument(skip_all)]
async fn from_apub(
async fn from_json(
person: Person,
context: &LemmyContext,
request_counter: &mut i32,
context: &Data<Self::DataType>,
) -> Result<ApubPerson, LemmyError> {
let instance_id = fetch_instance_actor_for_object(&person.id, context, request_counter).await?;
let instance_id = fetch_instance_actor_for_object(&person.id, context).await?;
let person_form = PersonInsertForm {
name: person.preferred_username,
@ -177,19 +173,17 @@ impl ApubObject for ApubPerson {
}
}
impl ActorType for ApubPerson {
fn actor_id(&self) -> Url {
self.actor_id.clone().into()
impl Actor for ApubPerson {
fn id(&self) -> Url {
self.actor_id.inner().clone()
}
fn private_key(&self) -> Option<String> {
self.private_key.clone()
fn public_key_pem(&self) -> &str {
&self.public_key
}
}
impl Actor for ApubPerson {
fn public_key(&self) -> &str {
&self.public_key
fn private_key_pem(&self) -> Option<String> {
self.private_key.clone()
}
fn inbox(&self) -> Url {
@ -211,21 +205,17 @@ pub(crate) mod tests {
},
protocol::{objects::instance::Instance, tests::file_to_json_object},
};
use activitypub_federation::fetch::object_id::ObjectId;
use lemmy_db_schema::{source::site::Site, traits::Crud};
use serial_test::serial;
pub(crate) async fn parse_lemmy_person(context: &LemmyContext) -> (ApubPerson, ApubSite) {
pub(crate) async fn parse_lemmy_person(context: &Data<LemmyContext>) -> (ApubPerson, ApubSite) {
let site = parse_lemmy_instance(context).await;
let json = file_to_json_object("assets/lemmy/objects/person.json").unwrap();
let url = Url::parse("https://enterprise.lemmy.ml/u/picard").unwrap();
let mut request_counter = 0;
ApubPerson::verify(&json, &url, context, &mut request_counter)
.await
.unwrap();
let person = ApubPerson::from_apub(json, context, &mut request_counter)
.await
.unwrap();
assert_eq!(request_counter, 0);
ApubPerson::verify(&json, &url, context).await.unwrap();
let person = ApubPerson::from_json(json, context).await.unwrap();
assert_eq!(context.request_count(), 0);
(person, site)
}
@ -249,27 +239,19 @@ pub(crate) mod tests {
// create and parse a fake pleroma instance actor, to avoid network request during test
let mut json: Instance = file_to_json_object("assets/lemmy/objects/instance.json").unwrap();
let id = Url::parse("https://queer.hacktivis.me/").unwrap();
json.id = ObjectId::new(id);
let mut request_counter = 0;
let site = ApubSite::from_apub(json, &context, &mut request_counter)
.await
.unwrap();
json.id = ObjectId::parse("https://queer.hacktivis.me/").unwrap();
let url = Url::parse("https://queer.hacktivis.me/users/lanodan").unwrap();
ApubSite::verify(&json, &url, &context).await.unwrap();
let site = ApubSite::from_json(json, &context).await.unwrap();
let json = file_to_json_object("assets/pleroma/objects/person.json").unwrap();
let url = Url::parse("https://queer.hacktivis.me/users/lanodan").unwrap();
let mut request_counter = 0;
ApubPerson::verify(&json, &url, &context, &mut request_counter)
.await
.unwrap();
let person = ApubPerson::from_apub(json, &context, &mut request_counter)
.await
.unwrap();
ApubPerson::verify(&json, &url, &context).await.unwrap();
let person = ApubPerson::from_json(json, &context).await.unwrap();
assert_eq!(person.actor_id, url.into());
assert_eq!(person.name, "lanodan");
assert!(!person.local);
assert_eq!(request_counter, 0);
assert_eq!(context.request_count(), 0);
assert_eq!(person.bio.as_ref().unwrap().len(), 873);
cleanup((person, site), &context).await;

@ -2,7 +2,6 @@ use crate::{
activities::{verify_is_public, verify_person_in_community},
check_apub_id_valid_with_strictness,
fetch_local_site_data,
local_instance,
objects::{read_from_string_or_source_opt, verify_is_remote_object},
protocol::{
objects::{
@ -15,12 +14,11 @@ use crate::{
},
};
use activitypub_federation::{
core::object_id::ObjectId,
deser::values::MediaTypeMarkdownOrHtml,
traits::ApubObject,
utils::verify_domains_match,
config::Data,
kinds::public,
protocol::{values::MediaTypeMarkdownOrHtml, verification::verify_domains_match},
traits::Object,
};
use activitystreams_kinds::public;
use anyhow::anyhow;
use chrono::NaiveDateTime;
use html2md::parse_html;
@ -69,11 +67,10 @@ impl From<Post> for ApubPost {
}
}
#[async_trait::async_trait(?Send)]
impl ApubObject for ApubPost {
#[async_trait::async_trait]
impl Object for ApubPost {
type DataType = LemmyContext;
type ApubType = Page;
type DbType = Post;
type Kind = Page;
type Error = LemmyError;
fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
@ -81,9 +78,9 @@ impl ApubObject for ApubPost {
}
#[tracing::instrument(skip_all)]
async fn read_from_apub_id(
async fn read_from_id(
object_id: Url,
context: &LemmyContext,
context: &Data<Self::DataType>,
) -> Result<Option<Self>, LemmyError> {
Ok(
Post::read_from_apub_id(context.pool(), object_id)
@ -93,7 +90,7 @@ impl ApubObject for ApubPost {
}
#[tracing::instrument(skip_all)]
async fn delete(self, context: &LemmyContext) -> Result<(), LemmyError> {
async fn delete(self, context: &Data<Self::DataType>) -> Result<(), LemmyError> {
if !self.deleted {
let form = PostUpdateForm::builder().deleted(Some(true)).build();
Post::update(context.pool(), self.id, &form).await?;
@ -103,7 +100,7 @@ impl ApubObject for ApubPost {
// Turn a Lemmy post into an ActivityPub page that can be sent out over the network.
#[tracing::instrument(skip_all)]
async fn into_apub(self, context: &LemmyContext) -> Result<Page, LemmyError> {
async fn into_json(self, context: &Data<Self::DataType>) -> Result<Page, LemmyError> {
let creator_id = self.creator_id;
let creator = Person::read(context.pool(), creator_id).await?;
let community_id = self.community_id;
@ -112,8 +109,8 @@ impl ApubObject for ApubPost {
let page = Page {
kind: PageType::Page,
id: ObjectId::new(self.ap_id.clone()),
attributed_to: AttributedTo::Lemmy(ObjectId::new(creator.actor_id)),
id: self.ap_id.clone().into(),
attributed_to: AttributedTo::Lemmy(creator.actor_id.into()),
to: vec![community.actor_id.clone().into(), public()],
cc: vec![],
name: Some(self.name.clone()),
@ -128,7 +125,7 @@ impl ApubObject for ApubPost {
language,
published: Some(convert_datetime(self.published)),
updated: self.updated.map(convert_datetime),
audience: Some(ObjectId::new(community.actor_id)),
audience: Some(community.actor_id.into()),
in_reply_to: None,
};
Ok(page)
@ -138,8 +135,7 @@ impl ApubObject for ApubPost {
async fn verify(
page: &Page,
expected_domain: &Url,
context: &LemmyContext,
request_counter: &mut i32,
context: &Data<Self::DataType>,
) -> Result<(), LemmyError> {
// We can't verify the domain in case of mod action, because the mod may be on a different
// instance from the post author.
@ -150,14 +146,14 @@ impl ApubObject for ApubPost {
let local_site_data = fetch_local_site_data(context.pool()).await?;
let community = page.community(context, request_counter).await?;
let community = page.community(context).await?;
check_apub_id_valid_with_strictness(
page.id.inner(),
community.local,
&local_site_data,
context.settings(),
)?;
verify_person_in_community(&page.creator()?, &community, context, request_counter).await?;
verify_person_in_community(&page.creator()?, &community, context).await?;
let slur_regex = &local_site_opt_to_slur_regex(&local_site_data.local_site);
check_slurs_opt(&page.name, slur_regex)?;
@ -168,16 +164,9 @@ impl ApubObject for ApubPost {
}
#[tracing::instrument(skip_all)]
async fn from_apub(
page: Page,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<ApubPost, LemmyError> {
let creator = page
.creator()?
.dereference(context, local_instance(context).await, request_counter)
.await?;
let community = page.community(context, request_counter).await?;
async fn from_json(page: Page, context: &Data<Self::DataType>) -> Result<ApubPost, LemmyError> {
let creator = page.creator()?.dereference(context).await?;
let community = page.community(context).await?;
if community.posting_restricted_to_mods {
is_mod_or_admin(context.pool(), creator.id, community.id).await?;
}
@ -257,9 +246,7 @@ impl ApubObject for ApubPost {
.build()
};
// read existing, local post if any (for generating mod log)
let old_post = ObjectId::<ApubPost>::new(page.id.clone())
.dereference_local(context)
.await;
let old_post = page.id.dereference_local(context).await;
let post = Post::create(context.pool(), &form).await?;
@ -310,13 +297,8 @@ mod tests {
let json = file_to_json_object("assets/lemmy/objects/page.json").unwrap();
let url = Url::parse("https://enterprise.lemmy.ml/post/55143").unwrap();
let mut request_counter = 0;
ApubPost::verify(&json, &url, &context, &mut request_counter)
.await
.unwrap();
let post = ApubPost::from_apub(json, &context, &mut request_counter)
.await
.unwrap();
ApubPost::verify(&json, &url, &context).await.unwrap();
let post = ApubPost::from_json(json, &context).await.unwrap();
assert_eq!(post.ap_id, url.into());
assert_eq!(post.name, "Post title");
@ -324,7 +306,7 @@ mod tests {
assert_eq!(post.body.as_ref().unwrap().len(), 45);
assert!(!post.locked);
assert!(post.featured_community);
assert_eq!(request_counter, 0);
assert_eq!(context.request_count(), 0);
Post::delete(context.pool(), post.id).await.unwrap();
Person::delete(context.pool(), person.id).await.unwrap();

@ -1,7 +1,6 @@
use crate::{
check_apub_id_valid_with_strictness,
fetch_local_site_data,
local_instance,
objects::read_from_string_or_source,
protocol::{
objects::chat_message::{ChatMessage, ChatMessageType},
@ -9,10 +8,9 @@ use crate::{
},
};
use activitypub_federation::{
core::object_id::ObjectId,
deser::values::MediaTypeHtml,
traits::ApubObject,
utils::verify_domains_match,
config::Data,
protocol::{values::MediaTypeHtml, verification::verify_domains_match},
traits::Object,
};
use chrono::NaiveDateTime;
use lemmy_api_common::{context::LemmyContext, utils::check_person_block};
@ -46,11 +44,10 @@ impl From<PrivateMessage> for ApubPrivateMessage {
}
}
#[async_trait::async_trait(?Send)]
impl ApubObject for ApubPrivateMessage {
#[async_trait::async_trait]
impl Object for ApubPrivateMessage {
type DataType = LemmyContext;
type ApubType = ChatMessage;
type DbType = PrivateMessage;
type Kind = ChatMessage;
type Error = LemmyError;
fn last_refreshed_at(&self) -> Option<NaiveDateTime> {
@ -58,9 +55,9 @@ impl ApubObject for ApubPrivateMessage {
}
#[tracing::instrument(skip_all)]
async fn read_from_apub_id(
async fn read_from_id(
object_id: Url,
context: &LemmyContext,
context: &Data<Self::DataType>,
) -> Result<Option<Self>, LemmyError> {
Ok(
PrivateMessage::read_from_apub_id(context.pool(), object_id)
@ -69,13 +66,13 @@ impl ApubObject for ApubPrivateMessage {
)
}
async fn delete(self, _context: &LemmyContext) -> Result<(), LemmyError> {
async fn delete(self, _context: &Data<Self::DataType>) -> Result<(), LemmyError> {
// do nothing, because pm can't be fetched over http
unimplemented!()
}
#[tracing::instrument(skip_all)]
async fn into_apub(self, context: &LemmyContext) -> Result<ChatMessage, LemmyError> {
async fn into_json(self, context: &Data<Self::DataType>) -> Result<ChatMessage, LemmyError> {
let creator_id = self.creator_id;
let creator = Person::read(context.pool(), creator_id).await?;
@ -84,9 +81,9 @@ impl ApubObject for ApubPrivateMessage {
let note = ChatMessage {
r#type: ChatMessageType::ChatMessage,
id: ObjectId::new(self.ap_id.clone()),
attributed_to: ObjectId::new(creator.actor_id),
to: [ObjectId::new(recipient.actor_id)],
id: self.ap_id.clone().into(),
attributed_to: creator.actor_id.into(),
to: [recipient.actor_id.into()],
content: markdown_to_html(&self.content),
media_type: Some(MediaTypeHtml::Html),
source: Some(Source::new(self.content.clone())),
@ -100,8 +97,7 @@ impl ApubObject for ApubPrivateMessage {
async fn verify(
note: &ChatMessage,
expected_domain: &Url,
context: &LemmyContext,
request_counter: &mut i32,
context: &Data<Self::DataType>,
) -> Result<(), LemmyError> {
verify_domains_match(note.id.inner(), expected_domain)?;
verify_domains_match(note.attributed_to.inner(), note.id.inner())?;
@ -114,10 +110,7 @@ impl ApubObject for ApubPrivateMessage {
&local_site_data,
context.settings(),
)?;
let person = note
.attributed_to
.dereference(context, local_instance(context).await, request_counter)
.await?;
let person = note.attributed_to.dereference(context).await?;
if person.banned {
return Err(LemmyError::from_message("Person is banned from site"));
}
@ -125,18 +118,12 @@ impl ApubObject for ApubPrivateMessage {
}
#[tracing::instrument(skip_all)]
async fn from_apub(
async fn from_json(
note: ChatMessage,
context: &LemmyContext,
request_counter: &mut i32,
context: &Data<Self::DataType>,
) -> Result<ApubPrivateMessage, LemmyError> {
let creator = note
.attributed_to
.dereference(context, local_instance(context).await, request_counter)
.await?;
let recipient = note.to[0]
.dereference(context, local_instance(context).await, request_counter)
.await?;
let creator = note.attributed_to.dereference(context).await?;
let recipient = note.to[0].dereference(context).await?;
check_person_block(creator.id, recipient.id, context.pool()).await?;
let form = PrivateMessageInsertForm {
@ -172,28 +159,29 @@ mod tests {
async fn prepare_comment_test(
url: &Url,
context: &LemmyContext,
context: &Data<LemmyContext>,
) -> (ApubPerson, ApubPerson, ApubSite) {
let context2 = context.reset_request_count();
let lemmy_person = file_to_json_object("assets/lemmy/objects/person.json").unwrap();
let site = parse_lemmy_instance(context).await;
ApubPerson::verify(&lemmy_person, url, context, &mut 0)
let site = parse_lemmy_instance(&context2).await;
ApubPerson::verify(&lemmy_person, url, &context2)
.await
.unwrap();
let person1 = ApubPerson::from_apub(lemmy_person, context, &mut 0)
let person1 = ApubPerson::from_json(lemmy_person, &context2)
.await
.unwrap();
let pleroma_person = file_to_json_object("assets/pleroma/objects/person.json").unwrap();
let pleroma_url = Url::parse("https://queer.hacktivis.me/users/lanodan").unwrap();
ApubPerson::verify(&pleroma_person, &pleroma_url, context, &mut 0)
ApubPerson::verify(&pleroma_person, &pleroma_url, &context2)
.await
.unwrap();
let person2 = ApubPerson::from_apub(pleroma_person, context, &mut 0)
let person2 = ApubPerson::from_json(pleroma_person, &context2)
.await
.unwrap();
(person1, person2, site)
}
async fn cleanup(data: (ApubPerson, ApubPerson, ApubSite), context: &LemmyContext) {
async fn cleanup(data: (ApubPerson, ApubPerson, ApubSite), context: &Data<LemmyContext>) {
Person::delete(context.pool(), data.0.id).await.unwrap();
Person::delete(context.pool(), data.1.id).await.unwrap();
Site::delete(context.pool(), data.2.id).await.unwrap();
@ -206,20 +194,19 @@ mod tests {
let url = Url::parse("https://enterprise.lemmy.ml/private_message/1621").unwrap();
let data = prepare_comment_test(&url, &context).await;
let json: ChatMessage = file_to_json_object("assets/lemmy/objects/chat_message.json").unwrap();
let mut request_counter = 0;
ApubPrivateMessage::verify(&json, &url, &context, &mut request_counter)
ApubPrivateMessage::verify(&json, &url, &context)
.await
.unwrap();
let pm = ApubPrivateMessage::from_apub(json.clone(), &context, &mut request_counter)
let pm = ApubPrivateMessage::from_json(json.clone(), &context)
.await
.unwrap();
assert_eq!(pm.ap_id.clone(), url.into());
assert_eq!(pm.content.len(), 20);
assert_eq!(request_counter, 0);
assert_eq!(context.request_count(), 0);
let pm_id = pm.id;
let to_apub = pm.into_apub(&context).await.unwrap();
let to_apub = pm.into_json(&context).await.unwrap();
assert_json_include!(actual: json, expected: to_apub);
PrivateMessage::delete(context.pool(), pm_id).await.unwrap();
@ -234,17 +221,14 @@ mod tests {
let data = prepare_comment_test(&url, &context).await;
let pleroma_url = Url::parse("https://queer.hacktivis.me/objects/2").unwrap();
let json = file_to_json_object("assets/pleroma/objects/chat_message.json").unwrap();
let mut request_counter = 0;
ApubPrivateMessage::verify(&json, &pleroma_url, &context, &mut request_counter)
.await
.unwrap();
let pm = ApubPrivateMessage::from_apub(json, &context, &mut request_counter)
ApubPrivateMessage::verify(&json, &pleroma_url, &context)
.await
.unwrap();
let pm = ApubPrivateMessage::from_json(json, &context).await.unwrap();
assert_eq!(pm.ap_id, pleroma_url.into());
assert_eq!(pm.content.len(), 3);
assert_eq!(request_counter, 0);
assert_eq!(context.request_count(), 0);
PrivateMessage::delete(context.pool(), pm.id).await.unwrap();
cleanup(data, &context).await;

@ -1,11 +1,14 @@
use crate::{
activities::{block::SiteOrCommunity, verify_community_matches},
local_instance,
objects::{community::ApubCommunity, person::ApubPerson},
protocol::InCommunity,
};
use activitypub_federation::{core::object_id::ObjectId, deser::helpers::deserialize_one_or_many};
use activitystreams_kinds::activity::BlockType;
use activitypub_federation::{
config::Data,
fetch::object_id::ObjectId,
kinds::activity::BlockType,
protocol::helpers::deserialize_one_or_many,
};
use anyhow::anyhow;
use chrono::{DateTime, FixedOffset};
use lemmy_api_common::context::LemmyContext;
@ -38,17 +41,10 @@ pub struct BlockUser {
pub(crate) expires: Option<DateTime<FixedOffset>>,
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl InCommunity for BlockUser {
async fn community(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<ApubCommunity, LemmyError> {
let target = self
.target
.dereference(context, local_instance(context).await, request_counter)
.await?;
async fn community(&self, context: &Data<LemmyContext>) -> Result<ApubCommunity, LemmyError> {
let target = self.target.dereference(context).await?;
let community = match target {
SiteOrCommunity::Community(c) => c,
SiteOrCommunity::Site(_) => return Err(anyhow!("activity is not in community").into()),

@ -3,8 +3,12 @@ use crate::{
objects::{community::ApubCommunity, person::ApubPerson},
protocol::{activities::block::block_user::BlockUser, InCommunity},
};
use activitypub_federation::{core::object_id::ObjectId, deser::helpers::deserialize_one_or_many};
use activitystreams_kinds::activity::UndoType;
use activitypub_federation::{
config::Data,
fetch::object_id::ObjectId,
kinds::activity::UndoType,
protocol::helpers::deserialize_one_or_many,
};
use lemmy_api_common::context::LemmyContext;
use lemmy_utils::error::LemmyError;
use serde::{Deserialize, Serialize};
@ -27,14 +31,10 @@ pub struct UndoBlockUser {
pub(crate) audience: Option<ObjectId<ApubCommunity>>,
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl InCommunity for UndoBlockUser {
async fn community(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<ApubCommunity, LemmyError> {
let community = self.object.community(context, request_counter).await?;
async fn community(&self, context: &Data<LemmyContext>) -> Result<ApubCommunity, LemmyError> {
let community = self.object.community(context).await?;
if let Some(audience) = &self.audience {
verify_community_matches(audience, community.actor_id.clone())?;
}

@ -1,6 +1,9 @@
use crate::{objects::community::ApubCommunity, protocol::IdOrNestedObject};
use activitypub_federation::{core::object_id::ObjectId, deser::helpers::deserialize_one_or_many};
use activitystreams_kinds::activity::AnnounceType;
use activitypub_federation::{
fetch::object_id::ObjectId,
kinds::activity::AnnounceType,
protocol::helpers::deserialize_one_or_many,
};
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use url::Url;

@ -3,8 +3,12 @@ use crate::{
objects::{community::ApubCommunity, person::ApubPerson},
protocol::InCommunity,
};
use activitypub_federation::{core::object_id::ObjectId, deser::helpers::deserialize_one_or_many};
use activitystreams_kinds::activity::AddType;
use activitypub_federation::{
config::Data,
fetch::object_id::ObjectId,
kinds::activity::AddType,
protocol::helpers::deserialize_one_or_many,
};
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::source::community::Community;
use lemmy_utils::error::LemmyError;
@ -27,13 +31,9 @@ pub struct CollectionAdd {
pub(crate) audience: Option<ObjectId<ApubCommunity>>,
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl InCommunity for CollectionAdd {
async fn community(
&self,
context: &LemmyContext,
_request_counter: &mut i32,
) -> Result<ApubCommunity, LemmyError> {
async fn community(&self, context: &Data<LemmyContext>) -> Result<ApubCommunity, LemmyError> {
let (community, _) =
Community::get_by_collection_url(context.pool(), &self.clone().target.into()).await?;
if let Some(audience) = &self.audience {

@ -3,8 +3,12 @@ use crate::{
objects::{community::ApubCommunity, person::ApubPerson},
protocol::InCommunity,
};
use activitypub_federation::{core::object_id::ObjectId, deser::helpers::deserialize_one_or_many};
use activitystreams_kinds::activity::RemoveType;
use activitypub_federation::{
config::Data,
fetch::object_id::ObjectId,
kinds::activity::RemoveType,
protocol::helpers::deserialize_one_or_many,
};
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::source::community::Community;
use lemmy_utils::error::LemmyError;
@ -17,7 +21,7 @@ pub struct CollectionRemove {
pub(crate) actor: ObjectId<ApubPerson>,
#[serde(deserialize_with = "deserialize_one_or_many")]
pub(crate) to: Vec<Url>,
pub(crate) object: ObjectId<ApubPerson>,
pub(crate) object: Url,
#[serde(deserialize_with = "deserialize_one_or_many")]
pub(crate) cc: Vec<Url>,
#[serde(rename = "type")]
@ -27,13 +31,9 @@ pub struct CollectionRemove {
pub(crate) audience: Option<ObjectId<ApubCommunity>>,
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl InCommunity for CollectionRemove {
async fn community(
&self,
context: &LemmyContext,
_request_counter: &mut i32,
) -> Result<ApubCommunity, LemmyError> {
async fn community(&self, context: &Data<LemmyContext>) -> Result<ApubCommunity, LemmyError> {
let (community, _) =
Community::get_by_collection_url(context.pool(), &self.clone().target.into()).await?;
if let Some(audience) = &self.audience {

@ -1,11 +1,14 @@
use crate::{
activities::verify_community_matches,
local_instance,
objects::{community::ApubCommunity, person::ApubPerson, post::ApubPost},
protocol::InCommunity,
};
use activitypub_federation::{core::object_id::ObjectId, deser::helpers::deserialize_one_or_many};
use activitystreams_kinds::activity::UndoType;
use activitypub_federation::{
config::Data,
fetch::object_id::ObjectId,
kinds::activity::UndoType,
protocol::helpers::deserialize_one_or_many,
};
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::{source::community::Community, traits::Crud};
use lemmy_utils::error::LemmyError;
@ -48,17 +51,10 @@ pub struct UndoLockPage {
pub(crate) audience: Option<ObjectId<ApubCommunity>>,
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl InCommunity for LockPage {
async fn community(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<ApubCommunity, LemmyError> {
let post = self
.object
.dereference(context, local_instance(context).await, request_counter)
.await?;
async fn community(&self, context: &Data<LemmyContext>) -> Result<ApubCommunity, LemmyError> {
let post = self.object.dereference(context).await?;
let community = Community::read(context.pool(), post.community_id).await?;
if let Some(audience) = &self.audience {
verify_community_matches(audience, community.actor_id.clone())?;
@ -67,14 +63,10 @@ impl InCommunity for LockPage {
}
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl InCommunity for UndoLockPage {
async fn community(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<ApubCommunity, LemmyError> {
let community = self.object.community(context, request_counter).await?;
async fn community(&self, context: &Data<LemmyContext>) -> Result<ApubCommunity, LemmyError> {
let community = self.object.community(context).await?;
if let Some(audience) = &self.audience {
verify_community_matches(audience, community.actor_id.clone())?;
}

@ -1,12 +1,15 @@
use crate::{
activities::verify_community_matches,
fetcher::post_or_comment::PostOrComment,
local_instance,
objects::{community::ApubCommunity, person::ApubPerson},
protocol::InCommunity,
};
use activitypub_federation::{core::object_id::ObjectId, deser::helpers::deserialize_one};
use activitystreams_kinds::activity::FlagType;
use activitypub_federation::{
config::Data,
fetch::object_id::ObjectId,
kinds::activity::FlagType,
protocol::helpers::deserialize_one,
};
use lemmy_api_common::context::LemmyContext;
use lemmy_utils::error::LemmyError;
use serde::{Deserialize, Serialize};
@ -26,16 +29,10 @@ pub struct Report {
pub(crate) audience: Option<ObjectId<ApubCommunity>>,
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl InCommunity for Report {
async fn community(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<ApubCommunity, LemmyError> {
let community = self.to[0]
.dereference(context, local_instance(context).await, request_counter)
.await?;
async fn community(&self, context: &Data<LemmyContext>) -> Result<ApubCommunity, LemmyError> {
let community = self.to[0].dereference(context).await?;
if let Some(audience) = &self.audience {
verify_community_matches(audience, community.actor_id.clone())?;
}

@ -1,11 +1,14 @@
use crate::{
activities::verify_community_matches,
local_instance,
objects::{community::ApubCommunity, person::ApubPerson},
protocol::{objects::group::Group, InCommunity},
};
use activitypub_federation::{core::object_id::ObjectId, deser::helpers::deserialize_one_or_many};
use activitystreams_kinds::activity::UpdateType;
use activitypub_federation::{
config::Data,
fetch::object_id::ObjectId,
kinds::activity::UpdateType,
protocol::helpers::deserialize_one_or_many,
};
use lemmy_api_common::context::LemmyContext;
use lemmy_utils::error::LemmyError;
use serde::{Deserialize, Serialize};
@ -29,16 +32,10 @@ pub struct UpdateCommunity {
pub(crate) audience: Option<ObjectId<ApubCommunity>>,
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl InCommunity for UpdateCommunity {
async fn community(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<ApubCommunity, LemmyError> {
let community: ApubCommunity = ObjectId::new(self.object.id.clone())
.dereference(context, local_instance(context).await, request_counter)
.await?;
async fn community(&self, context: &Data<LemmyContext>) -> Result<ApubCommunity, LemmyError> {
let community: ApubCommunity = self.object.id.clone().dereference(context).await?;
if let Some(audience) = &self.audience {
verify_community_matches(audience, community.actor_id.clone())?;
}

@ -2,7 +2,7 @@ use crate::{
objects::person::ApubPerson,
protocol::{activities::CreateOrUpdateType, objects::chat_message::ChatMessage},
};
use activitypub_federation::{core::object_id::ObjectId, deser::helpers::deserialize_one};
use activitypub_federation::{fetch::object_id::ObjectId, protocol::helpers::deserialize_one};
use serde::{Deserialize, Serialize};
use url::Url;

@ -4,7 +4,11 @@ use crate::{
objects::{community::ApubCommunity, person::ApubPerson},
protocol::{activities::CreateOrUpdateType, objects::note::Note, InCommunity},
};
use activitypub_federation::{core::object_id::ObjectId, deser::helpers::deserialize_one_or_many};
use activitypub_federation::{
config::Data,
fetch::object_id::ObjectId,
protocol::helpers::deserialize_one_or_many,
};
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::{source::community::Community, traits::Crud};
use lemmy_utils::error::LemmyError;
@ -28,14 +32,10 @@ pub struct CreateOrUpdateNote {
pub(crate) audience: Option<ObjectId<ApubCommunity>>,
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl InCommunity for CreateOrUpdateNote {
async fn community(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<ApubCommunity, LemmyError> {
let post = self.object.get_parents(context, request_counter).await?.0;
async fn community(&self, context: &Data<LemmyContext>) -> Result<ApubCommunity, LemmyError> {
let post = self.object.get_parents(context).await?.0;
let community = Community::read(context.pool(), post.community_id).await?;
if let Some(audience) = &self.audience {
verify_community_matches(audience, community.actor_id.clone())?;

@ -3,7 +3,11 @@ use crate::{
objects::{community::ApubCommunity, person::ApubPerson},
protocol::{activities::CreateOrUpdateType, objects::page::Page, InCommunity},
};
use activitypub_federation::{core::object_id::ObjectId, deser::helpers::deserialize_one_or_many};
use activitypub_federation::{
config::Data,
fetch::object_id::ObjectId,
protocol::helpers::deserialize_one_or_many,
};
use lemmy_api_common::context::LemmyContext;
use lemmy_utils::error::LemmyError;
use serde::{Deserialize, Serialize};
@ -24,14 +28,10 @@ pub struct CreateOrUpdatePage {
pub(crate) audience: Option<ObjectId<ApubCommunity>>,
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl InCommunity for CreateOrUpdatePage {
async fn community(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<ApubCommunity, LemmyError> {
let community = self.object.community(context, request_counter).await?;
async fn community(&self, context: &Data<LemmyContext>) -> Result<ApubCommunity, LemmyError> {
let community = self.object.community(context).await?;
if let Some(audience) = &self.audience {
verify_community_matches(audience, community.actor_id.clone())?;
}

@ -3,8 +3,12 @@ use crate::{
objects::{community::ApubCommunity, person::ApubPerson},
protocol::{objects::tombstone::Tombstone, IdOrNestedObject, InCommunity},
};
use activitypub_federation::{core::object_id::ObjectId, deser::helpers::deserialize_one_or_many};
use activitystreams_kinds::activity::DeleteType;
use activitypub_federation::{
config::Data,
fetch::object_id::ObjectId,
kinds::activity::DeleteType,
protocol::helpers::deserialize_one_or_many,
};
use anyhow::anyhow;
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::{
@ -38,13 +42,9 @@ pub struct Delete {
pub(crate) summary: Option<String>,
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl InCommunity for Delete {
async fn community(
&self,
context: &LemmyContext,
_request_counter: &mut i32,
) -> Result<ApubCommunity, LemmyError> {
async fn community(&self, context: &Data<LemmyContext>) -> Result<ApubCommunity, LemmyError> {
let community_id = match DeletableObjects::read_from_db(self.object.id(), context).await? {
DeletableObjects::Community(c) => c.id,
DeletableObjects::Comment(c) => {

@ -1,6 +1,9 @@
use crate::objects::person::ApubPerson;
use activitypub_federation::{core::object_id::ObjectId, deser::helpers::deserialize_one_or_many};
use activitystreams_kinds::activity::DeleteType;
use activitypub_federation::{
fetch::object_id::ObjectId,
kinds::activity::DeleteType,
protocol::helpers::deserialize_one_or_many,
};
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
use url::Url;

@ -3,8 +3,12 @@ use crate::{
objects::{community::ApubCommunity, person::ApubPerson},
protocol::{activities::deletion::delete::Delete, InCommunity},
};
use activitypub_federation::{core::object_id::ObjectId, deser::helpers::deserialize_one_or_many};
use activitystreams_kinds::activity::UndoType;
use activitypub_federation::{
config::Data,
fetch::object_id::ObjectId,
kinds::activity::UndoType,
protocol::helpers::deserialize_one_or_many,
};
use lemmy_api_common::context::LemmyContext;
use lemmy_utils::error::LemmyError;
use serde::{Deserialize, Serialize};
@ -29,14 +33,10 @@ pub struct UndoDelete {
pub(crate) cc: Vec<Url>,
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl InCommunity for UndoDelete {
async fn community(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<ApubCommunity, LemmyError> {
let community = self.object.community(context, request_counter).await?;
async fn community(&self, context: &Data<LemmyContext>) -> Result<ApubCommunity, LemmyError> {
let community = self.object.community(context).await?;
if let Some(audience) = &self.audience {
verify_community_matches(audience, community.actor_id.clone())?;
}

@ -1,6 +1,5 @@
use crate::{objects::community::ApubCommunity, protocol::activities::following::follow::Follow};
use activitypub_federation::core::object_id::ObjectId;
use activitystreams_kinds::activity::AcceptType;
use activitypub_federation::{fetch::object_id::ObjectId, kinds::activity::AcceptType};
use serde::{Deserialize, Serialize};
use url::Url;

@ -1,6 +1,5 @@
use crate::{fetcher::user_or_community::UserOrCommunity, objects::person::ApubPerson};
use activitypub_federation::core::object_id::ObjectId;
use activitystreams_kinds::activity::FollowType;
use activitypub_federation::{fetch::object_id::ObjectId, kinds::activity::FollowType};
use serde::{Deserialize, Serialize};
use url::Url;

@ -1,6 +1,5 @@
use crate::{objects::person::ApubPerson, protocol::activities::following::follow::Follow};
use activitypub_federation::core::object_id::ObjectId;
use activitystreams_kinds::activity::UndoType;
use activitypub_federation::{fetch::object_id::ObjectId, kinds::activity::UndoType};
use serde::{Deserialize, Serialize};
use url::Url;

@ -3,8 +3,7 @@ use crate::{
objects::{community::ApubCommunity, person::ApubPerson},
protocol::{activities::voting::vote::Vote, InCommunity},
};
use activitypub_federation::core::object_id::ObjectId;
use activitystreams_kinds::activity::UndoType;
use activitypub_federation::{config::Data, fetch::object_id::ObjectId, kinds::activity::UndoType};
use lemmy_api_common::context::LemmyContext;
use lemmy_utils::error::LemmyError;
use serde::{Deserialize, Serialize};
@ -21,14 +20,10 @@ pub struct UndoVote {
pub(crate) audience: Option<ObjectId<ApubCommunity>>,
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl InCommunity for UndoVote {
async fn community(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<ApubCommunity, LemmyError> {
let community = self.object.community(context, request_counter).await?;
async fn community(&self, context: &Data<LemmyContext>) -> Result<ApubCommunity, LemmyError> {
let community = self.object.community(context).await?;
if let Some(audience) = &self.audience {
verify_community_matches(audience, community.actor_id.clone())?;
}

@ -1,11 +1,10 @@
use crate::{
activities::verify_community_matches,
fetcher::post_or_comment::PostOrComment,
local_instance,
objects::{community::ApubCommunity, person::ApubPerson},
protocol::InCommunity,
};
use activitypub_federation::core::object_id::ObjectId;
use activitypub_federation::{config::Data, fetch::object_id::ObjectId};
use lemmy_api_common::context::LemmyContext;
use lemmy_utils::error::LemmyError;
use serde::{Deserialize, Serialize};
@ -51,19 +50,14 @@ impl From<&VoteType> for i16 {
}
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl InCommunity for Vote {
async fn community(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<ApubCommunity, LemmyError> {
let local_instance = local_instance(context).await;
async fn community(&self, context: &Data<LemmyContext>) -> Result<ApubCommunity, LemmyError> {
let community = self
.object
.dereference(context, local_instance, request_counter)
.dereference(context)
.await?
.community(context, request_counter)
.community(context)
.await?;
if let Some(audience) = &self.audience {
verify_community_matches(audience, community.actor_id.clone())?;

@ -1,4 +1,4 @@
use activitystreams_kinds::collection::OrderedCollectionType;
use activitypub_federation::kinds::collection::OrderedCollectionType;
use lemmy_utils::error::LemmyError;
use serde::{Deserialize, Serialize};
use url::Url;

@ -1,5 +1,5 @@
use crate::protocol::objects::page::Page;
use activitystreams_kinds::collection::OrderedCollectionType;
use activitypub_federation::kinds::collection::OrderedCollectionType;
use serde::{Deserialize, Serialize};
use url::Url;

@ -1,4 +1,4 @@
use activitystreams_kinds::collection::CollectionType;
use activitypub_federation::kinds::collection::CollectionType;
use lemmy_api_common::{context::LemmyContext, utils::generate_followers_url};
use lemmy_db_schema::source::community::Community;
use lemmy_db_views_actor::structs::CommunityFollowerView;

@ -1,6 +1,8 @@
use crate::objects::person::ApubPerson;
use activitypub_federation::core::object_id::ObjectId;
use activitystreams_kinds::collection::OrderedCollectionType;
use activitypub_federation::{
fetch::object_id::ObjectId,
kinds::collection::OrderedCollectionType,
};
use serde::{Deserialize, Serialize};
use url::Url;

@ -1,5 +1,5 @@
use crate::protocol::activities::community::announce::AnnounceActivity;
use activitystreams_kinds::collection::OrderedCollectionType;
use activitypub_federation::kinds::collection::OrderedCollectionType;
use serde::{Deserialize, Serialize};
use url::Url;

@ -1,6 +1,10 @@
use crate::{local_instance, objects::community::ApubCommunity};
use activitypub_federation::{deser::values::MediaTypeMarkdown, utils::fetch_object_http};
use activitystreams_kinds::object::ImageType;
use crate::objects::community::ApubCommunity;
use activitypub_federation::{
config::Data,
fetch::fetch_object_http,
kinds::object::ImageType,
protocol::values::MediaTypeMarkdown,
};
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::newtypes::DbUrl;
use lemmy_utils::error::LemmyError;
@ -60,41 +64,32 @@ pub(crate) enum IdOrNestedObject<Kind: Id> {
NestedObject(Kind),
}
impl<Kind: Id + DeserializeOwned> IdOrNestedObject<Kind> {
impl<Kind: Id + DeserializeOwned + Send> IdOrNestedObject<Kind> {
pub(crate) fn id(&self) -> &Url {
match self {
IdOrNestedObject::Id(i) => i,
IdOrNestedObject::NestedObject(n) => n.object_id(),
}
}
pub(crate) async fn object(
self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<Kind, LemmyError> {
pub(crate) async fn object(self, context: &Data<LemmyContext>) -> Result<Kind, LemmyError> {
match self {
IdOrNestedObject::Id(i) => {
Ok(fetch_object_http(&i, local_instance(context).await, request_counter).await?)
}
// TODO: move IdOrNestedObject struct to library and make fetch_object_http private
IdOrNestedObject::Id(i) => Ok(fetch_object_http(&i, context).await?),
IdOrNestedObject::NestedObject(o) => Ok(o),
}
}
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
pub trait InCommunity {
// TODO: after we use audience field and remove backwards compat, it should be possible to change
// this to simply `fn community(&self) -> Result<ObjectId<ApubCommunity>, LemmyError>`
async fn community(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<ApubCommunity, LemmyError>;
async fn community(&self, context: &Data<LemmyContext>) -> Result<ApubCommunity, LemmyError>;
}
#[cfg(test)]
pub(crate) mod tests {
use activitypub_federation::deser::context::WithContext;
use activitypub_federation::protocol::context::WithContext;
use assert_json_diff::assert_json_include;
use lemmy_utils::error::LemmyError;
use serde::{de::DeserializeOwned, Serialize};

@ -3,8 +3,8 @@ use crate::{
protocol::Source,
};
use activitypub_federation::{
core::object_id::ObjectId,
deser::{
fetch::object_id::ObjectId,
protocol::{
helpers::{deserialize_one, deserialize_skip_error},
values::MediaTypeHtml,
},

@ -14,11 +14,14 @@ use crate::{
},
};
use activitypub_federation::{
core::{object_id::ObjectId, signatures::PublicKey},
deser::helpers::deserialize_skip_error,
utils::verify_domains_match,
fetch::{collection_id::CollectionId, object_id::ObjectId},
kinds::actor::GroupType,
protocol::{
helpers::deserialize_skip_error,
public_key::PublicKey,
verification::verify_domains_match,
},
};
use activitystreams_kinds::actor::GroupType;
use chrono::{DateTime, FixedOffset};
use lemmy_api_common::{context::LemmyContext, utils::local_site_opt_to_slur_regex};
use lemmy_db_schema::{
@ -59,14 +62,14 @@ pub struct Group {
// lemmy extension
pub(crate) sensitive: Option<bool>,
// deprecated, use attributed_to instead
pub(crate) moderators: Option<ObjectId<ApubCommunityModerators>>,
pub(crate) moderators: Option<CollectionId<ApubCommunityModerators>>,
#[serde(deserialize_with = "deserialize_skip_error", default)]
pub(crate) attributed_to: Option<ObjectId<ApubCommunityModerators>>,
pub(crate) attributed_to: Option<CollectionId<ApubCommunityModerators>>,
// lemmy extension
pub(crate) posting_restricted_to_mods: Option<bool>,
pub(crate) outbox: ObjectId<ApubCommunityOutbox>,
pub(crate) outbox: CollectionId<ApubCommunityOutbox>,
pub(crate) endpoints: Option<Endpoints>,
pub(crate) featured: Option<ObjectId<ApubCommunityFeatured>>,
pub(crate) featured: Option<CollectionId<ApubCommunityFeatured>>,
#[serde(default)]
pub(crate) language: Vec<LanguageTag>,
pub(crate) published: Option<DateTime<FixedOffset>>,

@ -3,10 +3,10 @@ use crate::{
protocol::{objects::LanguageTag, ImageObject, Source},
};
use activitypub_federation::{
core::{object_id::ObjectId, signatures::PublicKey},
deser::{helpers::deserialize_skip_error, values::MediaTypeHtml},
fetch::object_id::ObjectId,
kinds::actor::ApplicationType,
protocol::{helpers::deserialize_skip_error, public_key::PublicKey, values::MediaTypeHtml},
};
use activitystreams_kinds::actor::ApplicationType;
use chrono::{DateTime, FixedOffset};
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;

@ -1,19 +1,19 @@
use crate::{
activities::verify_community_matches,
fetcher::post_or_comment::PostOrComment,
local_instance,
mentions::MentionOrValue,
objects::{comment::ApubComment, community::ApubCommunity, person::ApubPerson, post::ApubPost},
protocol::{objects::LanguageTag, InCommunity, Source},
};
use activitypub_federation::{
core::object_id::ObjectId,
deser::{
config::Data,
fetch::object_id::ObjectId,
kinds::object::NoteType,
protocol::{
helpers::{deserialize_one_or_many, deserialize_skip_error},
values::MediaTypeMarkdownOrHtml,
},
};
use activitystreams_kinds::object::NoteType;
use chrono::{DateTime, FixedOffset};
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::{
@ -56,16 +56,10 @@ pub struct Note {
impl Note {
pub(crate) async fn get_parents(
&self,
context: &LemmyContext,
request_counter: &mut i32,
context: &Data<LemmyContext>,
) -> Result<(ApubPost, Option<ApubComment>), LemmyError> {
// Fetch parent comment chain in a box, otherwise it can cause a stack overflow.
let parent = Box::pin(
self
.in_reply_to
.dereference(context, local_instance(context).await, request_counter)
.await?,
);
let parent = Box::pin(self.in_reply_to.dereference(context).await?);
match parent.deref() {
PostOrComment::Post(p) => Ok((p.clone(), None)),
PostOrComment::Comment(c) => {
@ -77,14 +71,10 @@ impl Note {
}
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl InCommunity for Note {
async fn community(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<ApubCommunity, LemmyError> {
let (post, _) = self.get_parents(context, request_counter).await?;
async fn community(&self, context: &Data<LemmyContext>) -> Result<ApubCommunity, LemmyError> {
let (post, _) = self.get_parents(context).await?;
let community = Community::read(context.pool(), post.community_id).await?;
if let Some(audience) = &self.audience {
verify_community_matches(audience, community.actor_id.clone())?;

@ -1,22 +1,21 @@
use crate::{
activities::verify_community_matches,
fetcher::user_or_community::{PersonOrGroupType, UserOrCommunity},
local_instance,
objects::{community::ApubCommunity, person::ApubPerson, post::ApubPost},
protocol::{objects::LanguageTag, ImageObject, InCommunity, Source},
};
use activitypub_federation::{
core::object_id::ObjectId,
data::Data,
deser::{
config::Data,
fetch::object_id::ObjectId,
kinds::{
link::LinkType,
object::{DocumentType, ImageType},
},
protocol::{
helpers::{deserialize_one_or_many, deserialize_skip_error},
values::MediaTypeMarkdownOrHtml,
},
traits::{ActivityHandler, ApubObject},
};
use activitystreams_kinds::{
link::LinkType,
object::{DocumentType, ImageType},
traits::{ActivityHandler, Object},
};
use chrono::{DateTime, FixedOffset};
use itertools::Itertools;
@ -136,10 +135,11 @@ impl Page {
/// the current value, it is a mod action and needs to be verified as such.
///
/// Both stickied and locked need to be false on a newly created post (verified in [[CreatePost]].
pub(crate) async fn is_mod_action(&self, context: &LemmyContext) -> Result<bool, LemmyError> {
let old_post = ObjectId::<ApubPost>::new(self.id.clone())
.dereference_local(context)
.await;
pub(crate) async fn is_mod_action(
&self,
context: &Data<LemmyContext>,
) -> Result<bool, LemmyError> {
let old_post = self.id.clone().dereference_local(context).await;
let featured_changed = Page::is_featured_changed(&old_post, &self.stickied);
let locked_changed = Page::is_locked_changed(&old_post, &self.comments_enabled);
@ -178,7 +178,7 @@ impl Page {
AttributedTo::Peertube(p) => p
.iter()
.find(|a| a.kind == PersonOrGroupType::Person)
.map(|a| ObjectId::<ApubPerson>::new(a.id.clone().into_inner()))
.map(|a| ObjectId::<ApubPerson>::from(a.id.clone().into_inner()))
.ok_or_else(|| LemmyError::from_message("page does not specify creator person")),
}
}
@ -194,7 +194,7 @@ impl Attachment {
}
// Used for community outbox, so that it can be compatible with Pleroma/Mastodon.
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl ActivityHandler for Page {
type DataType = LemmyContext;
type Error = LemmyError;
@ -204,38 +204,25 @@ impl ActivityHandler for Page {
fn actor(&self) -> &Url {
unimplemented!()
}
async fn verify(
&self,
data: &Data<Self::DataType>,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
ApubPost::verify(self, self.id.inner(), data, request_counter).await
async fn verify(&self, data: &Data<Self::DataType>) -> Result<(), LemmyError> {
ApubPost::verify(self, self.id.inner(), data).await
}
async fn receive(
self,
data: &Data<Self::DataType>,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
ApubPost::from_apub(self, data, request_counter).await?;
async fn receive(self, data: &Data<Self::DataType>) -> Result<(), LemmyError> {
ApubPost::from_json(self, data).await?;
Ok(())
}
}
#[async_trait::async_trait(?Send)]
#[async_trait::async_trait]
impl InCommunity for Page {
async fn community(
&self,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<ApubCommunity, LemmyError> {
let instance = local_instance(context).await;
async fn community(&self, context: &Data<LemmyContext>) -> Result<ApubCommunity, LemmyError> {
let community = match &self.attributed_to {
AttributedTo::Lemmy(_) => {
let mut iter = self.to.iter().merge(self.cc.iter());
loop {
if let Some(cid) = iter.next() {
let cid = ObjectId::new(cid.clone());
if let Ok(c) = cid.dereference(context, instance, request_counter).await {
let cid = ObjectId::from(cid.clone());
if let Ok(c) = cid.dereference(context).await {
break c;
}
} else {
@ -246,9 +233,9 @@ impl InCommunity for Page {
AttributedTo::Peertube(p) => {
p.iter()
.find(|a| a.kind == PersonOrGroupType::Group)
.map(|a| ObjectId::<ApubCommunity>::new(a.id.clone().into_inner()))
.map(|a| ObjectId::<ApubCommunity>::from(a.id.clone().into_inner()))
.ok_or_else(|| LemmyError::from_message("page does not specify group"))?
.dereference(context, instance, request_counter)
.dereference(context)
.await?
}
};

@ -3,8 +3,8 @@ use crate::{
protocol::{objects::Endpoints, ImageObject, Source},
};
use activitypub_federation::{
core::{object_id::ObjectId, signatures::PublicKey},
deser::helpers::deserialize_skip_error,
fetch::object_id::ObjectId,
protocol::{helpers::deserialize_skip_error, public_key::PublicKey},
};
use chrono::{DateTime, FixedOffset};
use serde::{Deserialize, Serialize};

@ -1,5 +1,5 @@
use crate::protocol::Id;
use activitystreams_kinds::object::TombstoneType;
use activitypub_federation::kinds::object::TombstoneType;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
use url::Url;

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save