From af2f09ea4cbb97d3ee91e30bb58e85508989d63a Mon Sep 17 00:00:00 2001 From: clarkzjw Date: Wed, 26 Jul 2023 12:37:38 -0700 Subject: add example from https://github.com/LemmyNet/activitypub-federation-rust --- fedi/live_federation/objects/mod.rs | 2 + fedi/live_federation/objects/person.rs | 140 +++++++++++++++++++++++++++++++++ fedi/live_federation/objects/post.rs | 104 ++++++++++++++++++++++++ 3 files changed, 246 insertions(+) create mode 100644 fedi/live_federation/objects/mod.rs create mode 100644 fedi/live_federation/objects/person.rs create mode 100644 fedi/live_federation/objects/post.rs (limited to 'fedi/live_federation/objects') diff --git a/fedi/live_federation/objects/mod.rs b/fedi/live_federation/objects/mod.rs new file mode 100644 index 0000000..b5239ab --- /dev/null +++ b/fedi/live_federation/objects/mod.rs @@ -0,0 +1,2 @@ +pub mod person; +pub mod post; diff --git a/fedi/live_federation/objects/person.rs b/fedi/live_federation/objects/person.rs new file mode 100644 index 0000000..d9439ea --- /dev/null +++ b/fedi/live_federation/objects/person.rs @@ -0,0 +1,140 @@ +use crate::{activities::create_post::CreatePost, database::DatabaseHandle, error::Error}; +use activitypub_federation::{ + config::Data, + fetch::object_id::ObjectId, + http_signatures::generate_actor_keypair, + kinds::actor::PersonType, + protocol::{public_key::PublicKey, verification::verify_domains_match}, + traits::{ActivityHandler, Actor, Object}, +}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use std::fmt::Debug; +use url::Url; + +#[derive(Debug, Clone)] +pub struct DbUser { + pub name: String, + pub ap_id: ObjectId, + pub inbox: Url, + // exists for all users (necessary to verify http signatures) + pub public_key: String, + // exists only for local users + pub private_key: Option, + last_refreshed_at: DateTime, + pub followers: Vec, + pub local: bool, +} + +/// List of all activities which this actor can receive. +#[derive(Deserialize, Serialize, Debug)] +#[serde(untagged)] +#[enum_delegate::implement(ActivityHandler)] +pub enum PersonAcceptedActivities { + CreateNote(CreatePost), +} + +impl DbUser { + pub fn new(hostname: &str, name: &str) -> Result { + let ap_id = Url::parse(&format!("https://{}/{}", hostname, &name))?.into(); + let inbox = Url::parse(&format!("https://{}/{}/inbox", hostname, &name))?; + let keypair = generate_actor_keypair()?; + Ok(DbUser { + name: name.to_string(), + ap_id, + inbox, + public_key: keypair.public_key, + private_key: Some(keypair.private_key), + last_refreshed_at: Utc::now(), + followers: vec![], + local: true, + }) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Person { + #[serde(rename = "type")] + kind: PersonType, + preferred_username: String, + id: ObjectId, + inbox: Url, + public_key: PublicKey, +} + +#[async_trait::async_trait] +impl Object for DbUser { + type DataType = DatabaseHandle; + type Kind = Person; + type Error = Error; + + fn last_refreshed_at(&self) -> Option> { + Some(self.last_refreshed_at) + } + + async fn read_from_id( + object_id: Url, + data: &Data, + ) -> Result, Self::Error> { + let users = data.users.lock().unwrap(); + let res = users + .clone() + .into_iter() + .find(|u| u.ap_id.inner() == &object_id); + Ok(res) + } + + async fn into_json(self, _data: &Data) -> Result { + Ok(Person { + preferred_username: self.name.clone(), + kind: Default::default(), + id: self.ap_id.clone(), + inbox: self.inbox.clone(), + public_key: self.public_key(), + }) + } + + async fn verify( + json: &Self::Kind, + expected_domain: &Url, + _data: &Data, + ) -> Result<(), Self::Error> { + verify_domains_match(json.id.inner(), expected_domain)?; + Ok(()) + } + + async fn from_json( + json: Self::Kind, + _data: &Data, + ) -> Result { + Ok(DbUser { + name: json.preferred_username, + ap_id: json.id, + inbox: json.inbox, + public_key: json.public_key.public_key_pem, + private_key: None, + last_refreshed_at: Utc::now(), + followers: vec![], + local: false, + }) + } +} + +impl Actor for DbUser { + fn id(&self) -> Url { + self.ap_id.inner().clone() + } + + fn public_key_pem(&self) -> &str { + &self.public_key + } + + fn private_key_pem(&self) -> Option { + self.private_key.clone() + } + + fn inbox(&self) -> Url { + self.inbox.clone() + } +} diff --git a/fedi/live_federation/objects/post.rs b/fedi/live_federation/objects/post.rs new file mode 100644 index 0000000..9a08b9d --- /dev/null +++ b/fedi/live_federation/objects/post.rs @@ -0,0 +1,104 @@ +use crate::{ + activities::create_post::CreatePost, + database::DatabaseHandle, + error::Error, + generate_object_id, + objects::person::DbUser, +}; +use activitypub_federation::{ + config::Data, + fetch::object_id::ObjectId, + kinds::{object::NoteType, public}, + protocol::{helpers::deserialize_one_or_many, verification::verify_domains_match}, + traits::{Actor, Object}, +}; +use activitystreams_kinds::link::MentionType; +use serde::{Deserialize, Serialize}; +use url::Url; + +#[derive(Clone, Debug)] +pub struct DbPost { + pub text: String, + pub ap_id: ObjectId, + pub creator: ObjectId, + pub local: bool, +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Note { + #[serde(rename = "type")] + kind: NoteType, + id: ObjectId, + pub(crate) attributed_to: ObjectId, + #[serde(deserialize_with = "deserialize_one_or_many")] + pub(crate) to: Vec, + content: String, + in_reply_to: Option>, + tag: Vec, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Mention { + pub href: Url, + #[serde(rename = "type")] + pub kind: MentionType, +} + +#[async_trait::async_trait] +impl Object for DbPost { + type DataType = DatabaseHandle; + type Kind = Note; + type Error = Error; + + async fn read_from_id( + _object_id: Url, + _data: &Data, + ) -> Result, Self::Error> { + Ok(None) + } + + async fn into_json(self, _data: &Data) -> Result { + unimplemented!() + } + + async fn verify( + json: &Self::Kind, + expected_domain: &Url, + _data: &Data, + ) -> Result<(), Self::Error> { + verify_domains_match(json.id.inner(), expected_domain)?; + Ok(()) + } + + async fn from_json(json: Self::Kind, data: &Data) -> Result { + println!( + "Received post with content {} and id {}", + &json.content, &json.id + ); + let creator = json.attributed_to.dereference(data).await?; + let post = DbPost { + text: json.content, + ap_id: json.id.clone(), + creator: json.attributed_to.clone(), + local: false, + }; + + let mention = Mention { + href: creator.ap_id.clone().into_inner(), + kind: Default::default(), + }; + let note = Note { + kind: Default::default(), + id: generate_object_id(data.domain())?.into(), + attributed_to: data.local_user().ap_id, + to: vec![public()], + content: format!("Hello {}", creator.name), + in_reply_to: Some(json.id.clone()), + tag: vec![mention], + }; + CreatePost::send(note, creator.shared_inbox_or_inbox(), data).await?; + + Ok(post) + } +} -- cgit v1.2.3