Skip to content

Commit c8ad38e

Browse files
committed
feat: implement follow activity
1 parent 25c3dcf commit c8ad38e

File tree

29 files changed

+284
-221
lines changed

29 files changed

+284
-221
lines changed

.env

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Only needed by sqlx to pick the database url
2+
DATABASE_URL="postgres://postgres:postgres@localhost/gill"

config.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
domain = "localhost:3000"
2+
debug = true
3+
port = 3000
24

35
[database]
46
host = "postgres"

crates/gill-api/src/api/user.rs

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
use crate::error::AppError;
2-
use gill_settings::SETTINGS;
32
use activitypub_federation::core::signatures::generate_actor_keypair;
43
use axum::http::StatusCode;
54
use axum::response::{IntoResponse, Response};
65
use axum::{Extension, Json};
76
use gill_db::user::{CreateSSHKey, CreateUser, User};
7+
use gill_settings::SETTINGS;
88
use serde::{Deserialize, Serialize};
99
use sqlx::PgPool;
1010

@@ -19,16 +19,23 @@ pub async fn create(
1919
Json(user): Json<CreateUserDto>,
2020
) -> Result<Response, AppError> {
2121
let keys = generate_actor_keypair()?;
22-
println!("{:?}", SETTINGS);
22+
let scheme = if gill_settings::debug_mod() {
23+
"http://"
24+
} else {
25+
"https://"
26+
};
27+
let domain = &SETTINGS.domain;
28+
let username = user.username;
29+
2330
let user = CreateUser {
24-
username: user.username.clone(),
25-
email: user.email,
31+
username: username.clone(),
32+
email: Some(user.email),
2633
private_key: Some(keys.private_key),
2734
public_key: keys.public_key,
28-
followers_url: format!("http://{}/apub/{}/followers", SETTINGS.domain, user.username),
29-
outbox_url: format!("http://{}/apub/{}/outbox", SETTINGS.domain, user.username),
30-
inbox_url: format!("http://{}/apub/{}/inbox", SETTINGS.domain, user.username),
31-
activity_pub_id: format!("http://{}/apub/users/{}", SETTINGS.domain, user.username),
35+
followers_url: format!("{scheme}{domain}/apub/{username}/followers"),
36+
outbox_url: format!("{scheme}{domain}/apub/{username}/outbox"),
37+
inbox_url: format!("{scheme}{domain}/apub/{username}/inbox"),
38+
activity_pub_id: format!("{scheme}{domain}/apub/users/{username}"),
3239
domain: SETTINGS.domain.clone(),
3340
is_local: true,
3441
};

crates/gill-api/src/apub/activities/follow.rs

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::apub::object::user::ApubUser;
22
use crate::error::AppError;
33
use crate::instance::InstanceHandle;
4+
45
use activitypub_federation::{core::object_id::ObjectId, data::Data, traits::ActivityHandler};
56
use activitystreams_kinds::activity::FollowType;
67
use axum::async_trait;
@@ -11,17 +12,17 @@ use url::Url;
1112
#[serde(rename_all = "camelCase")]
1213
pub struct Follow {
1314
id: Url,
14-
pub actor: ObjectId<ApubUser>,
15-
pub object: ObjectId<ApubUser>,
15+
pub follower: ObjectId<ApubUser>,
16+
pub followed: ObjectId<ApubUser>,
1617
r#type: FollowType,
1718
}
1819

1920
impl Follow {
20-
pub fn new(actor: ObjectId<ApubUser>, object: ObjectId<ApubUser>, id: Url) -> Follow {
21+
pub fn new(follower: ObjectId<ApubUser>, followed: ObjectId<ApubUser>, id: Url) -> Follow {
2122
Follow {
2223
id,
23-
actor,
24-
object,
24+
follower,
25+
followed,
2526
r#type: Default::default(),
2627
}
2728
}
@@ -37,7 +38,7 @@ impl ActivityHandler for Follow {
3738
}
3839

3940
fn actor(&self) -> &Url {
40-
self.actor.inner()
41+
self.follower.inner()
4142
}
4243

4344
async fn verify(
@@ -53,8 +54,15 @@ impl ActivityHandler for Follow {
5354
data: &Data<Self::DataType>,
5455
_request_counter: &mut i32,
5556
) -> Result<(), Self::Error> {
56-
println!("Got APUB Follow event");
57-
println!("{:?}", self);
57+
let followed = ObjectId::<ApubUser>::new(self.followed)
58+
.dereference_local(data)
59+
.await?;
60+
61+
let follower = ObjectId::<ApubUser>::new(self.follower)
62+
.dereference(data, data.local_instance(), &mut 0)
63+
.await?;
64+
65+
followed.add_follower(follower.local_id(), data).await?;
5866
Ok(())
5967
}
6068
}

crates/gill-api/src/apub/mod.rs

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
use std::string::ParseError;
21
use crate::apub::object::user::{ApubUser, Person, PersonAcceptedActivities};
32
use crate::error::AppError;
43
use crate::instance::{Instance, InstanceHandle};
@@ -15,27 +14,28 @@ use axum::response::IntoResponse;
1514
use axum::routing::{get, post};
1615
use axum::{body, middleware, Extension, Json, Router};
1716
use http::{HeaderMap, Method, Request};
17+
1818
use std::sync::Arc;
1919
use tower::ServiceBuilder;
2020
use tower_http::ServiceBuilderExt;
21-
use tracing::debug;
21+
2222
use url::Url;
23-
use uuid::Uuid;
2423

2524
pub mod activities;
2625
pub mod object;
2726

2827
pub fn router(instance: Arc<Instance>) -> Router {
29-
Router::new()
30-
.route("/inbox", post(http_post_user_inbox))
31-
// FIXME: the layer seems to apply to all route
32-
// .layer(
33-
// ServiceBuilder::new()
34-
// .map_request_body(body::boxed)
35-
// .layer(middleware::from_fn(verify_request_payload)),
36-
// )
37-
.route("/users/:user_name", get(http_get_user))
38-
.with_state(instance)
28+
let public = Router::new().route("/users/:user_name", get(http_get_user));
29+
30+
let private = Router::new()
31+
.route("/:user/inbox", post(http_post_user_inbox))
32+
.layer(
33+
ServiceBuilder::new()
34+
.map_request_body(body::boxed)
35+
.layer(middleware::from_fn(verify_request_payload)),
36+
);
37+
38+
public.merge(private).with_state(instance)
3939
}
4040

4141
async fn http_get_user(
@@ -51,7 +51,9 @@ async fn http_get_user(
5151
.into_apub(&data)
5252
.await;
5353

54-
Ok(ApubJson(WithContext::new_default(user?)))
54+
let user = WithContext::new_default(user?);
55+
println!("{:?}", user);
56+
Ok(ApubJson(user))
5557
}
5658

5759
async fn http_post_user_inbox(
@@ -73,4 +75,3 @@ async fn http_post_user_inbox(
7375
)
7476
.await
7577
}
76-

crates/gill-api/src/apub/object/repository.rs

Whitespace-only changes.

crates/gill-api/src/apub/object/user.rs

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,23 @@ use activitypub_federation::{
99
LocalInstance,
1010
};
1111
use activitystreams_kinds::actor::PersonType;
12+
use std::str::FromStr;
13+
1214
use axum::async_trait;
13-
use gill_db::user::User;
15+
use gill_db::user::{CreateUser, User};
1416
use serde::{Deserialize, Serialize};
1517
use url::Url;
1618
use uuid::Uuid;
1719

1820
#[derive(Debug, Clone)]
1921
pub struct ApubUser(User);
2022

23+
impl From<User> for ApubUser {
24+
fn from(user: User) -> Self {
25+
ApubUser(user)
26+
}
27+
}
28+
2129
/// List of all activities which this actor can receive.
2230
#[derive(Deserialize, Serialize, Debug)]
2331
#[serde(untagged)]
@@ -32,14 +40,18 @@ pub struct Person {
3240
#[serde(rename = "type")]
3341
kind: PersonType,
3442
id: ObjectId<ApubUser>,
43+
email: Option<String>,
44+
username: String,
45+
outbox: Url,
3546
inbox: Url,
47+
domain: String,
48+
followers: Url,
3649
public_key: PublicKey,
3750
}
3851

3952
impl ApubUser {
4053
pub async fn followers(&self, instance: &InstanceHandle) -> Result<Vec<Url>, AppError> {
4154
let db = instance.database();
42-
4355
let followers = self.0.get_followers(i64::MAX, 0, db).await?;
4456

4557
let followers = followers
@@ -51,6 +63,16 @@ impl ApubUser {
5163
Ok(followers)
5264
}
5365

66+
pub async fn add_follower(
67+
&self,
68+
follower_id: i32,
69+
instance: &InstanceHandle,
70+
) -> Result<(), AppError> {
71+
let db = instance.database();
72+
self.0.add_follower(follower_id, db).await?;
73+
Ok(())
74+
}
75+
5476
pub fn followers_url(&self) -> Result<Url, AppError> {
5577
Url::parse(&self.0.followers_url).map_err(Into::into)
5678
}
@@ -59,6 +81,10 @@ impl ApubUser {
5981
&self.0.activity_pub_id
6082
}
6183

84+
pub fn local_id(&self) -> i32 {
85+
self.0.id
86+
}
87+
6288
fn activity_pub_id_as_url(&self) -> Result<Url, AppError> {
6389
Ok(Url::parse(self.activity_pub_id())?)
6490
}
@@ -81,6 +107,10 @@ impl ApubUser {
81107
let activity_id = format!("https://{hostname}/activity/{uuid}", uuid = Uuid::new_v4());
82108
let activity_id = Url::parse(&activity_id)?;
83109
let follow = Follow::new(follower, following, activity_id);
110+
tracing::debug!(
111+
"Sending follow activity to user inboc {}",
112+
other.shared_inbox_or_inbox()
113+
);
84114
self.send(
85115
follow,
86116
vec![other.shared_inbox_or_inbox()],
@@ -126,16 +156,21 @@ impl ApubObject for ApubUser {
126156
data: &Self::DataType,
127157
) -> Result<Option<Self>, Self::Error> {
128158
let db = data.database();
129-
let user = User::by_activity_pub_id(object_id.as_str(), db).await.ok();
159+
let user = User::by_activity_pub_id(object_id.as_str(), db).await?;
130160
Ok(user.map(ApubUser))
131161
}
132162

133163
async fn into_apub(self, _data: &Self::DataType) -> Result<Self::ApubType, Self::Error> {
134164
Ok(Person {
135165
kind: Default::default(),
136166
id: ObjectId::new(self.activity_pub_id_as_url()?),
137-
inbox: self.inbox(),
138167
public_key: self.public_key()?,
168+
email: self.0.email,
169+
username: self.0.username,
170+
outbox: Url::parse(&self.0.outbox_url)?,
171+
domain: self.0.domain,
172+
inbox: Url::parse(&self.0.inbox_url)?,
173+
followers: Url::parse(&self.0.followers_url)?,
139174
})
140175
}
141176

@@ -156,7 +191,25 @@ impl ApubObject for ApubUser {
156191
let db = data.database();
157192
let id = Url::from(apub.id);
158193
let user = User::by_activity_pub_id(id.as_str(), db).await?;
159-
Ok(ApubUser(user))
194+
if let Some(user) = user {
195+
Ok(ApubUser(user))
196+
} else {
197+
let user = CreateUser {
198+
username: apub.username,
199+
email: apub.email,
200+
private_key: None,
201+
public_key: apub.public_key.public_key_pem,
202+
activity_pub_id: id.to_string(),
203+
outbox_url: apub.outbox.to_string(),
204+
inbox_url: apub.inbox.to_string(),
205+
domain: apub.domain,
206+
followers_url: apub.followers.to_string(),
207+
is_local: false,
208+
};
209+
210+
let user = User::create(user, db).await?;
211+
Ok(ApubUser(user))
212+
}
160213
}
161214
}
162215

@@ -165,6 +218,6 @@ impl Actor for ApubUser {
165218
&self.0.public_key
166219
}
167220
fn inbox(&self) -> Url {
168-
self.activity_pub_id_as_url().expect("Invalid inbox url")
221+
Url::from_str(&self.0.inbox_url).expect("Invalid inbox url")
169222
}
170223
}

crates/gill-api/src/error.rs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,3 @@ impl AppError {
2424
(StatusCode::NOT_FOUND, format!("{}", self.0)).into_response()
2525
}
2626
}
27-
28-
29-

crates/gill-api/src/instance.rs

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,14 @@
1-
21
use crate::error::AppError;
32
use crate::oauth::{oauth_client, AppState};
43
use crate::{api, apub, view};
54

6-
7-
8-
9-
use activitypub_federation::{
10-
InstanceSettings, LocalInstance, UrlVerifier,
11-
};
5+
use activitypub_federation::{InstanceSettings, LocalInstance, UrlVerifier};
126
use async_session::MemoryStore;
137
use axum::async_trait;
148

15-
16-
17-
189
use axum::response::IntoResponse;
1910

20-
11+
use axum::routing::get;
2112
use axum::{Extension, Router};
2213
use gill_ipc::listener::IPCListener;
2314
use http::StatusCode;
@@ -30,9 +21,8 @@ use std::sync::Arc;
3021
use tower_http::services::ServeDir;
3122
use tower_http::trace::TraceLayer;
3223

33-
34-
use url::Url;
3524
use gill_settings::SETTINGS;
25+
use url::Url;
3626

3727
use crate::syntax::{load_syntax, load_theme};
3828

@@ -95,13 +85,17 @@ impl Instance {
9585
oauth_client,
9686
syntax_set,
9787
theme,
98-
instance: instance.clone()
88+
instance: instance.clone(),
9989
};
10090

10191
let app = Router::new()
92+
.route(
93+
"/.well-known/webfinger",
94+
get(crate::webfinger::webfinger).with_state(app_state.clone()),
95+
)
10296
.nest("/api/v1/", api::router())
10397
.nest_service("/apub", apub::router(instance))
104-
.nest_service("/", view::router(app_state))
98+
.nest_service("/", view::router(app_state.clone()))
10599
.nest_service("/assets/", serve_dir)
106100
.layer(TraceLayer::new_for_http())
107101
.layer(Extension(db))

crates/gill-api/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
pub mod api;
22
pub mod apub;
3-
pub mod webfinger;
43
pub mod error;
54
pub mod instance;
65
pub mod oauth;
76
pub mod syntax;
8-
pub mod view;
7+
pub mod view;
8+
pub mod webfinger;

0 commit comments

Comments
 (0)