aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorclarkzjw <[email protected]>2023-07-26 12:37:38 -0700
committerclarkzjw <[email protected]>2023-07-26 12:37:38 -0700
commitaf2f09ea4cbb97d3ee91e30bb58e85508989d63a (patch)
tree671ead9c450a0abf71efc00ba1f2966ae6e60e02 /fedi/live_federation
parentf847de64eb8f724fa512801b43a26522afff61ae (diff)
downloadphoto-af2f09ea4cbb97d3ee91e30bb58e85508989d63a.tar.gz
add example from https://github.com/LemmyNet/activitypub-federation-rust
Diffstat (limited to 'fedi/live_federation')
-rw-r--r--fedi/live_federation/activities/create_post.rs69
-rw-r--r--fedi/live_federation/activities/mod.rs1
-rw-r--r--fedi/live_federation/database.rs26
-rw-r--r--fedi/live_federation/error.rs20
-rw-r--r--fedi/live_federation/http.rs69
-rw-r--r--fedi/live_federation/main.rs70
-rw-r--r--fedi/live_federation/objects/mod.rs2
-rw-r--r--fedi/live_federation/objects/person.rs140
-rw-r--r--fedi/live_federation/objects/post.rs104
-rw-r--r--fedi/live_federation/utils.rs13
10 files changed, 514 insertions, 0 deletions
diff --git a/fedi/live_federation/activities/create_post.rs b/fedi/live_federation/activities/create_post.rs
new file mode 100644
index 0000000..66928a6
--- /dev/null
+++ b/fedi/live_federation/activities/create_post.rs
@@ -0,0 +1,69 @@
1use crate::{
2 database::DatabaseHandle,
3 error::Error,
4 objects::{person::DbUser, post::Note},
5 utils::generate_object_id,
6 DbPost,
7};
8use activitypub_federation::{
9 activity_queue::send_activity,
10 config::Data,
11 fetch::object_id::ObjectId,
12 kinds::activity::CreateType,
13 protocol::{context::WithContext, helpers::deserialize_one_or_many},
14 traits::{ActivityHandler, Object},
15};
16use serde::{Deserialize, Serialize};
17use url::Url;
18
19#[derive(Deserialize, Serialize, Debug)]
20#[serde(rename_all = "camelCase")]
21pub struct CreatePost {
22 pub(crate) actor: ObjectId<DbUser>,
23 #[serde(deserialize_with = "deserialize_one_or_many")]
24 pub(crate) to: Vec<Url>,
25 pub(crate) object: Note,
26 #[serde(rename = "type")]
27 pub(crate) kind: CreateType,
28 pub(crate) id: Url,
29}
30
31impl CreatePost {
32 pub async fn send(note: Note, inbox: Url, data: &Data<DatabaseHandle>) -> Result<(), Error> {
33 print!("Sending reply to {}", &note.attributed_to);
34 let create = CreatePost {
35 actor: note.attributed_to.clone(),
36 to: note.to.clone(),
37 object: note,
38 kind: CreateType::Create,
39 id: generate_object_id(data.domain())?,
40 };
41 let create_with_context = WithContext::new_default(create);
42 send_activity(create_with_context, &data.local_user(), vec![inbox], data).await?;
43 Ok(())
44 }
45}
46
47#[async_trait::async_trait]
48impl ActivityHandler for CreatePost {
49 type DataType = DatabaseHandle;
50 type Error = crate::error::Error;
51
52 fn id(&self) -> &Url {
53 &self.id
54 }
55
56 fn actor(&self) -> &Url {
57 self.actor.inner()
58 }
59
60 async fn verify(&self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
61 DbPost::verify(&self.object, &self.id, data).await?;
62 Ok(())
63 }
64
65 async fn receive(self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
66 DbPost::from_json(self.object, data).await?;
67 Ok(())
68 }
69}
diff --git a/fedi/live_federation/activities/mod.rs b/fedi/live_federation/activities/mod.rs
new file mode 100644
index 0000000..7e15ee0
--- /dev/null
+++ b/fedi/live_federation/activities/mod.rs
@@ -0,0 +1 @@
pub mod create_post;
diff --git a/fedi/live_federation/database.rs b/fedi/live_federation/database.rs
new file mode 100644
index 0000000..967c534
--- /dev/null
+++ b/fedi/live_federation/database.rs
@@ -0,0 +1,26 @@
1use crate::{objects::person::DbUser, Error};
2use anyhow::anyhow;
3use std::sync::{Arc, Mutex};
4
5pub type DatabaseHandle = Arc<Database>;
6
7/// Our "database" which contains all known users (local and federated)
8pub struct Database {
9 pub users: Mutex<Vec<DbUser>>,
10}
11
12impl Database {
13 pub fn local_user(&self) -> DbUser {
14 let lock = self.users.lock().unwrap();
15 lock.first().unwrap().clone()
16 }
17
18 pub fn read_user(&self, name: &str) -> Result<DbUser, Error> {
19 let db_user = self.local_user();
20 if name == db_user.name {
21 Ok(db_user)
22 } else {
23 Err(anyhow!("Invalid user {name}").into())
24 }
25 }
26}
diff --git a/fedi/live_federation/error.rs b/fedi/live_federation/error.rs
new file mode 100644
index 0000000..3ef1819
--- /dev/null
+++ b/fedi/live_federation/error.rs
@@ -0,0 +1,20 @@
1use std::fmt::{Display, Formatter};
2
3/// Necessary because of this issue: https://github.com/actix/actix-web/issues/1711
4#[derive(Debug)]
5pub struct Error(pub(crate) anyhow::Error);
6
7impl Display for Error {
8 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
9 std::fmt::Display::fmt(&self.0, f)
10 }
11}
12
13impl<T> From<T> for Error
14where
15 T: Into<anyhow::Error>,
16{
17 fn from(t: T) -> Self {
18 Error(t.into())
19 }
20}
diff --git a/fedi/live_federation/http.rs b/fedi/live_federation/http.rs
new file mode 100644
index 0000000..d626396
--- /dev/null
+++ b/fedi/live_federation/http.rs
@@ -0,0 +1,69 @@
1use crate::{
2 database::DatabaseHandle,
3 error::Error,
4 objects::person::{DbUser, Person, PersonAcceptedActivities},
5};
6use activitypub_federation::{
7 axum::{
8 inbox::{receive_activity, ActivityData},
9 json::FederationJson,
10 },
11 config::Data,
12 fetch::webfinger::{build_webfinger_response, extract_webfinger_name, Webfinger},
13 protocol::context::WithContext,
14 traits::Object,
15};
16use axum::{
17 extract::{Path, Query},
18 response::{IntoResponse, Response},
19 Json,
20};
21use axum_macros::debug_handler;
22use http::StatusCode;
23use serde::Deserialize;
24
25impl IntoResponse for Error {
26 fn into_response(self) -> Response {
27 (StatusCode::INTERNAL_SERVER_ERROR, format!("{}", self.0)).into_response()
28 }
29}
30
31#[debug_handler]
32pub async fn http_get_user(
33 Path(name): Path<String>,
34 data: Data<DatabaseHandle>,
35) -> Result<FederationJson<WithContext<Person>>, Error> {
36 let db_user = data.read_user(&name)?;
37 let json_user = db_user.into_json(&data).await?;
38 Ok(FederationJson(WithContext::new_default(json_user)))
39}
40
41#[debug_handler]
42pub async fn http_post_user_inbox(
43 data: Data<DatabaseHandle>,
44 activity_data: ActivityData,
45) -> impl IntoResponse {
46 receive_activity::<WithContext<PersonAcceptedActivities>, DbUser, DatabaseHandle>(
47 activity_data,
48 &data,
49 )
50 .await
51}
52
53#[derive(Deserialize)]
54pub struct WebfingerQuery {
55 resource: String,
56}
57
58#[debug_handler]
59pub async fn webfinger(
60 Query(query): Query<WebfingerQuery>,
61 data: Data<DatabaseHandle>,
62) -> Result<Json<Webfinger>, Error> {
63 let name = extract_webfinger_name(&query.resource, &data)?;
64 let db_user = data.read_user(&name)?;
65 Ok(Json(build_webfinger_response(
66 query.resource,
67 db_user.ap_id.into_inner(),
68 )))
69}
diff --git a/fedi/live_federation/main.rs b/fedi/live_federation/main.rs
new file mode 100644
index 0000000..4326226
--- /dev/null
+++ b/fedi/live_federation/main.rs
@@ -0,0 +1,70 @@
1use crate::{
2 database::Database,
3 http::{http_get_user, http_post_user_inbox, webfinger},
4 objects::{person::DbUser, post::DbPost},
5 utils::generate_object_id,
6};
7use activitypub_federation::config::{FederationConfig, FederationMiddleware};
8use axum::{
9 routing::{get, post},
10 Router,
11};
12use error::Error;
13use std::{
14 net::ToSocketAddrs,
15 sync::{Arc, Mutex},
16};
17use tracing::log::{info, LevelFilter};
18
19mod activities;
20mod database;
21mod error;
22#[allow(clippy::diverging_sub_expression, clippy::items_after_statements)]
23mod http;
24mod objects;
25mod utils;
26
27const DOMAIN: &str = "example.com";
28const LOCAL_USER_NAME: &str = "alison";
29const BIND_ADDRESS: &str = "localhost:8003";
30
31#[tokio::main]
32async fn main() -> Result<(), Error> {
33 env_logger::builder()
34 .filter_level(LevelFilter::Warn)
35 .filter_module("activitypub_federation", LevelFilter::Info)
36 .filter_module("live_federation", LevelFilter::Info)
37 .format_timestamp(None)
38 .init();
39
40 info!("Setup local user and database");
41 let local_user = DbUser::new(DOMAIN, LOCAL_USER_NAME)?;
42 let database = Arc::new(Database {
43 users: Mutex::new(vec![local_user]),
44 });
45
46 info!("Setup configuration");
47 let config = FederationConfig::builder()
48 .domain(DOMAIN)
49 .app_data(database)
50 .build()
51 .await?;
52
53 info!("Listen with HTTP server on {BIND_ADDRESS}");
54 let config = config.clone();
55 let app = Router::new()
56 .route("/:user", get(http_get_user))
57 .route("/:user/inbox", post(http_post_user_inbox))
58 .route("/.well-known/webfinger", get(webfinger))
59 .layer(FederationMiddleware::new(config));
60
61 let addr = BIND_ADDRESS
62 .to_socket_addrs()?
63 .next()
64 .expect("Failed to lookup domain name");
65 axum::Server::bind(&addr)
66 .serve(app.into_make_service())
67 .await?;
68
69 Ok(())
70}
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 @@
1pub mod person;
2pub 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 @@
1use crate::{activities::create_post::CreatePost, database::DatabaseHandle, error::Error};
2use activitypub_federation::{
3 config::Data,
4 fetch::object_id::ObjectId,
5 http_signatures::generate_actor_keypair,
6 kinds::actor::PersonType,
7 protocol::{public_key::PublicKey, verification::verify_domains_match},
8 traits::{ActivityHandler, Actor, Object},
9};
10use chrono::{DateTime, Utc};
11use serde::{Deserialize, Serialize};
12use std::fmt::Debug;
13use url::Url;
14
15#[derive(Debug, Clone)]
16pub struct DbUser {
17 pub name: String,
18 pub ap_id: ObjectId<DbUser>,
19 pub inbox: Url,
20 // exists for all users (necessary to verify http signatures)
21 pub public_key: String,
22 // exists only for local users
23 pub private_key: Option<String>,
24 last_refreshed_at: DateTime<Utc>,
25 pub followers: Vec<Url>,
26 pub local: bool,
27}
28
29/// List of all activities which this actor can receive.
30#[derive(Deserialize, Serialize, Debug)]
31#[serde(untagged)]
32#[enum_delegate::implement(ActivityHandler)]
33pub enum PersonAcceptedActivities {
34 CreateNote(CreatePost),
35}
36
37impl DbUser {
38 pub fn new(hostname: &str, name: &str) -> Result<DbUser, Error> {
39 let ap_id = Url::parse(&format!("https://{}/{}", hostname, &name))?.into();
40 let inbox = Url::parse(&format!("https://{}/{}/inbox", hostname, &name))?;
41 let keypair = generate_actor_keypair()?;
42 Ok(DbUser {
43 name: name.to_string(),
44 ap_id,
45 inbox,
46 public_key: keypair.public_key,
47 private_key: Some(keypair.private_key),
48 last_refreshed_at: Utc::now(),
49 followers: vec![],
50 local: true,
51 })
52 }
53}
54
55#[derive(Clone, Debug, Deserialize, Serialize)]
56#[serde(rename_all = "camelCase")]
57pub struct Person {
58 #[serde(rename = "type")]
59 kind: PersonType,
60 preferred_username: String,
61 id: ObjectId<DbUser>,
62 inbox: Url,
63 public_key: PublicKey,
64}
65
66#[async_trait::async_trait]
67impl Object for DbUser {
68 type DataType = DatabaseHandle;
69 type Kind = Person;
70 type Error = Error;
71
72 fn last_refreshed_at(&self) -> Option<DateTime<Utc>> {
73 Some(self.last_refreshed_at)
74 }
75
76 async fn read_from_id(
77 object_id: Url,
78 data: &Data<Self::DataType>,
79 ) -> Result<Option<Self>, Self::Error> {
80 let users = data.users.lock().unwrap();
81 let res = users
82 .clone()
83 .into_iter()
84 .find(|u| u.ap_id.inner() == &object_id);
85 Ok(res)
86 }
87
88 async fn into_json(self, _data: &Data<Self::DataType>) -> Result<Self::Kind, Self::Error> {
89 Ok(Person {
90 preferred_username: self.name.clone(),
91 kind: Default::default(),
92 id: self.ap_id.clone(),
93 inbox: self.inbox.clone(),
94 public_key: self.public_key(),
95 })
96 }
97
98 async fn verify(
99 json: &Self::Kind,
100 expected_domain: &Url,
101 _data: &Data<Self::DataType>,
102 ) -> Result<(), Self::Error> {
103 verify_domains_match(json.id.inner(), expected_domain)?;
104 Ok(())
105 }
106
107 async fn from_json(
108 json: Self::Kind,
109 _data: &Data<Self::DataType>,
110 ) -> Result<Self, Self::Error> {
111 Ok(DbUser {
112 name: json.preferred_username,
113 ap_id: json.id,
114 inbox: json.inbox,
115 public_key: json.public_key.public_key_pem,
116 private_key: None,
117 last_refreshed_at: Utc::now(),
118 followers: vec![],
119 local: false,
120 })
121 }
122}
123
124impl Actor for DbUser {
125 fn id(&self) -> Url {
126 self.ap_id.inner().clone()
127 }
128
129 fn public_key_pem(&self) -> &str {
130 &self.public_key
131 }
132
133 fn private_key_pem(&self) -> Option<String> {
134 self.private_key.clone()
135 }
136
137 fn inbox(&self) -> Url {
138 self.inbox.clone()
139 }
140}
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 @@
1use crate::{
2 activities::create_post::CreatePost,
3 database::DatabaseHandle,
4 error::Error,
5 generate_object_id,
6 objects::person::DbUser,
7};
8use activitypub_federation::{
9 config::Data,
10 fetch::object_id::ObjectId,
11 kinds::{object::NoteType, public},
12 protocol::{helpers::deserialize_one_or_many, verification::verify_domains_match},
13 traits::{Actor, Object},
14};
15use activitystreams_kinds::link::MentionType;
16use serde::{Deserialize, Serialize};
17use url::Url;
18
19#[derive(Clone, Debug)]
20pub struct DbPost {
21 pub text: String,
22 pub ap_id: ObjectId<DbPost>,
23 pub creator: ObjectId<DbUser>,
24 pub local: bool,
25}
26
27#[derive(Deserialize, Serialize, Debug)]
28#[serde(rename_all = "camelCase")]
29pub struct Note {
30 #[serde(rename = "type")]
31 kind: NoteType,
32 id: ObjectId<DbPost>,
33 pub(crate) attributed_to: ObjectId<DbUser>,
34 #[serde(deserialize_with = "deserialize_one_or_many")]
35 pub(crate) to: Vec<Url>,
36 content: String,
37 in_reply_to: Option<ObjectId<DbPost>>,
38 tag: Vec<Mention>,
39}
40
41#[derive(Clone, Debug, Deserialize, Serialize)]
42pub struct Mention {
43 pub href: Url,
44 #[serde(rename = "type")]
45 pub kind: MentionType,
46}
47
48#[async_trait::async_trait]
49impl Object for DbPost {
50 type DataType = DatabaseHandle;
51 type Kind = Note;
52 type Error = Error;
53
54 async fn read_from_id(
55 _object_id: Url,
56 _data: &Data<Self::DataType>,
57 ) -> Result<Option<Self>, Self::Error> {
58 Ok(None)
59 }
60
61 async fn into_json(self, _data: &Data<Self::DataType>) -> Result<Self::Kind, Self::Error> {
62 unimplemented!()
63 }
64
65 async fn verify(
66 json: &Self::Kind,
67 expected_domain: &Url,
68 _data: &Data<Self::DataType>,
69 ) -> Result<(), Self::Error> {
70 verify_domains_match(json.id.inner(), expected_domain)?;
71 Ok(())
72 }
73
74 async fn from_json(json: Self::Kind, data: &Data<Self::DataType>) -> Result<Self, Self::Error> {
75 println!(
76 "Received post with content {} and id {}",
77 &json.content, &json.id
78 );
79 let creator = json.attributed_to.dereference(data).await?;
80 let post = DbPost {
81 text: json.content,
82 ap_id: json.id.clone(),
83 creator: json.attributed_to.clone(),
84 local: false,
85 };
86
87 let mention = Mention {
88 href: creator.ap_id.clone().into_inner(),
89 kind: Default::default(),
90 };
91 let note = Note {
92 kind: Default::default(),
93 id: generate_object_id(data.domain())?.into(),
94 attributed_to: data.local_user().ap_id,
95 to: vec![public()],
96 content: format!("Hello {}", creator.name),
97 in_reply_to: Some(json.id.clone()),
98 tag: vec![mention],
99 };
100 CreatePost::send(note, creator.shared_inbox_or_inbox(), data).await?;
101
102 Ok(post)
103 }
104}
diff --git a/fedi/live_federation/utils.rs b/fedi/live_federation/utils.rs
new file mode 100644
index 0000000..0b2b098
--- /dev/null
+++ b/fedi/live_federation/utils.rs
@@ -0,0 +1,13 @@
1use rand::{distributions::Alphanumeric, thread_rng, Rng};
2use url::{ParseError, Url};
3
4/// Just generate random url as object id. In a real project, you probably want to use
5/// an url which contains the database id for easy retrieval (or store the random id in db).
6pub fn generate_object_id(domain: &str) -> Result<Url, ParseError> {
7 let id: String = thread_rng()
8 .sample_iter(&Alphanumeric)
9 .take(7)
10 .map(char::from)
11 .collect();
12 Url::parse(&format!("https://{}/objects/{}", domain, id))
13}
Powered by cgit v1.2.3 (git 2.41.0)