summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorrtkay123 <dev@kanjala.com>2025-07-15 08:42:19 +0200
committerrtkay123 <dev@kanjala.com>2025-07-15 08:42:19 +0200
commita69c24e561c8ae16dc730f7713f8d8da0bd25e0e (patch)
tree32878bf97b1adf2da14c8e3da7265c8937b89650 /src
parenta64eb6b08f2f8d22cf129fba39e1bb2c66bb3fad (diff)
downloadsellershut-a69c24e561c8ae16dc730f7713f8d8da0bd25e0e.tar.bz2
sellershut-a69c24e561c8ae16dc730f7713f8d8da0bd25e0e.zip
feat: persist with sqlx
Diffstat (limited to 'src')
-rw-r--r--src/entity/user.rs64
-rw-r--r--src/main.rs48
-rw-r--r--src/server.rs14
-rw-r--r--src/server/routes.rs14
-rw-r--r--src/server/routes/users.rs4
-rw-r--r--src/server/routes/users/get_outbox.rs77
-rw-r--r--src/server/routes/users/get_user.rs49
-rw-r--r--src/server/routes/users/webfinger.rs23
-rw-r--r--src/state.rs17
9 files changed, 259 insertions, 51 deletions
diff --git a/src/entity/user.rs b/src/entity/user.rs
index e136cb3..da22f00 100644
--- a/src/entity/user.rs
+++ b/src/entity/user.rs
@@ -8,36 +8,77 @@ use activitypub_federation::{
};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
+use stack_up::Services;
use tracing::trace;
use url::Url;
+use uuid::Uuid;
use crate::{error::AppError, state::AppHandle};
#[derive(PartialEq, Clone, Debug)]
pub(crate) struct User {
+ pub id: String,
pub username: String,
pub ap_id: ObjectId<User>,
pub private_key: Option<String>,
pub public_key: String,
pub inbox: Url,
+ pub outbox: Option<Url>,
}
-impl User {
- pub fn new(username: &str) -> Result<Self, AppError> {
- trace!("creating a new user");
- let keys = generate_actor_keypair()?;
- let stub = &format!("http://localhost/users/{username}");
+pub struct DbUser {
+ pub id: String,
+ pub username: String,
+ pub ap_id: String,
+ pub private_key: Option<String>,
+ pub public_key: String,
+ pub inbox: String,
+ pub outbox: Option<String>,
+ pub local: bool,
+}
+impl TryFrom<DbUser> for User {
+ type Error = AppError;
+ fn try_from(value: DbUser) -> Result<Self, Self::Error> {
Ok(Self {
- username: username.to_owned(),
- ap_id: Url::parse(stub)?.into(),
- private_key: Some(keys.private_key),
- public_key: keys.public_key,
- inbox: Url::parse(&format!("{stub}/inbox"))?,
+ id: value.id,
+ username: value.username,
+ ap_id: Url::parse(&value.ap_id)?.into(),
+ private_key: value.private_key,
+ public_key: value.public_key,
+ inbox: Url::parse(&value.inbox)?,
+ outbox: match value.outbox {
+ Some(ref url) => Some(Url::parse(url)?),
+ None => None,
+ },
})
}
}
+impl User {
+ pub async fn new(username: &str, services: &Services) -> Result<Self, AppError> {
+ trace!("creating keypair for new user");
+ let keys = generate_actor_keypair()?;
+ let stub = &format!("http://localhost/users/{username}");
+ let id = Uuid::now_v7();
+
+ trace!(id = ?id, "creating a new user");
+ let user = sqlx::query_as!(
+ DbUser,
+ "insert into account (id, username, ap_id, private_key, public_key, inbox, outbox, local) values ($1, $2, $3, $4, $5, $6, $7, $8) returning *",
+ id,
+ username,
+ stub,
+ keys.private_key,
+ keys.public_key,
+ &format!("{stub}/inbox"),
+ &format!("{stub}/outbox"),
+ true
+ ).fetch_one(&services.postgres).await?;
+ Self::try_from(user)
+ }
+}
+
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Person {
@@ -47,6 +88,8 @@ pub struct Person {
id: ObjectId<User>,
inbox: Url,
public_key: PublicKey,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ outbox: Option<Url>,
}
#[async_trait]
@@ -85,6 +128,7 @@ impl Object for User {
id: self.ap_id.clone(),
inbox: self.inbox.clone(),
public_key: self.public_key(),
+ outbox: self.outbox.clone(),
})
}
diff --git a/src/main.rs b/src/main.rs
index 1e5b259..68aedc4 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -3,19 +3,53 @@ mod error;
mod server;
mod state;
-use stack_up::{Monitoring, tracing::Tracing};
+use std::net::{Ipv6Addr, SocketAddr};
+
+use clap::Parser;
+use stack_up::{Configuration, Services, tracing::Tracing};
use crate::{error::AppError, state::AppState};
+use tracing::{error, info};
+
+/// sellershut
+#[derive(Parser, Debug)]
+#[command(version, about, long_about = None)]
+struct Args {
+ /// Path to config file
+ #[arg(short, long)]
+ config_file: Option<std::path::PathBuf>,
+}
#[tokio::main]
async fn main() -> Result<(), AppError> {
- let _tracing = Tracing::builder().build(&Monitoring {
- log_level: "trace".into(),
- });
+ let args = Args::parse();
+ let config = include_str!("../sellershut.toml");
+
+ let mut config = config::Config::builder()
+ .add_source(config::File::from_str(config, config::FileFormat::Toml));
+
+ if let Some(cf) = args.config_file.as_ref().and_then(|v| v.to_str()) {
+ config = config.add_source(config::File::new(cf, config::FileFormat::Toml));
+ };
+
+ let mut config: Configuration = config.build()?.try_deserialize()?;
+ config.application.name = env!("CARGO_CRATE_NAME").into();
+ config.application.version = env!("CARGO_PKG_VERSION").into();
+
+ let _tracing = Tracing::builder().build(&config.monitoring);
+
+ let services = Services::builder()
+ .postgres(&config.database)
+ .await
+ .inspect_err(|e| error!("database: {e}"))?
+ .build();
+
+ let state = AppState::new(services, &config).await?;
+
+ let addr = SocketAddr::from((Ipv6Addr::UNSPECIFIED, config.application.port));
- let state = AppState::new().await?;
- let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await?;
- tracing::debug!("listening on {}", listener.local_addr()?);
+ let listener = tokio::net::TcpListener::bind(addr).await?;
+ info!(port = addr.port(), "serving api");
axum::serve(listener, server::router(state)).await?;
Ok(())
diff --git a/src/server.rs b/src/server.rs
index bb2b4d0..85a3e81 100644
--- a/src/server.rs
+++ b/src/server.rs
@@ -13,3 +13,17 @@ pub fn router(state: FederationConfig<AppHandle>) -> Router {
.layer(TraceLayer::new_for_http())
.layer(FederationMiddleware::new(state))
}
+
+#[cfg(test)]
+pub(crate) fn test_config() -> stack_up::Configuration {
+ use stack_up::Configuration;
+
+ let config_path = "sellershut.toml";
+
+ let config = config::Config::builder()
+ .add_source(config::File::new(config_path, config::FileFormat::Toml))
+ .build()
+ .unwrap();
+
+ config.try_deserialize::<Configuration>().unwrap()
+}
diff --git a/src/server/routes.rs b/src/server/routes.rs
index 7751c95..85cc100 100644
--- a/src/server/routes.rs
+++ b/src/server/routes.rs
@@ -14,13 +14,19 @@ mod tests {
body::Body,
http::{Request, StatusCode},
};
+ use sqlx::PgPool;
+ use stack_up::Services;
use tower::ServiceExt;
- use crate::{server, state::AppState};
+ use crate::{
+ server::{self, test_config},
+ state::AppState,
+ };
- #[tokio::test]
- async fn health_check() {
- let state = AppState::new().await.unwrap();
+ #[sqlx::test]
+ async fn health_check(pool: PgPool) {
+ let services = Services { postgres: pool };
+ let state = AppState::new(services, &test_config()).await.unwrap();
let app = server::router(state);
let response = app
diff --git a/src/server/routes/users.rs b/src/server/routes/users.rs
index 2ef49b2..d3ce446 100644
--- a/src/server/routes/users.rs
+++ b/src/server/routes/users.rs
@@ -1,3 +1,4 @@
+pub mod get_outbox;
pub mod get_user;
pub mod webfinger;
@@ -5,6 +6,7 @@ use axum::{Router, routing::get};
pub fn users_router() -> Router {
Router::new()
- .route("/users/{usernme}", get(get_user::http_get_user))
+ .route("/users/{username}", get(get_user::http_get_user))
+ .route("/users/{username}/outbox", get(get_outbox::http_get_outbox))
.route("/.well-known/webfinger", get(webfinger::webfinger))
}
diff --git a/src/server/routes/users/get_outbox.rs b/src/server/routes/users/get_outbox.rs
new file mode 100644
index 0000000..d5a4af5
--- /dev/null
+++ b/src/server/routes/users/get_outbox.rs
@@ -0,0 +1,77 @@
+use activitypub_federation::{
+ axum::json::FederationJson, config::Data, protocol::context::WithContext, traits::Object,
+};
+use axum::{debug_handler, extract::Path, http::StatusCode, response::IntoResponse};
+
+use crate::{error::AppError, state::AppHandle};
+
+#[debug_handler]
+pub async fn http_get_outbox(
+ Path(name): Path<String>,
+ data: Data<AppHandle>,
+) -> Result<impl IntoResponse, AppError> {
+ if let Some(a) = super::get_user::read_user(&name, &data).await? {
+ let json_user = a.into_json(&data).await?;
+ Ok((
+ StatusCode::OK,
+ FederationJson(WithContext::new_default(json_user)),
+ )
+ .into_response())
+ } else {
+ Ok((StatusCode::NOT_FOUND, "").into_response())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use axum::{
+ body::Body,
+ http::{Request, StatusCode},
+ };
+ use sqlx::PgPool;
+ use stack_up::Services;
+ use tower::ServiceExt;
+
+ use crate::{
+ server::{self, test_config},
+ state::AppState,
+ };
+
+ #[sqlx::test]
+ async fn get_user(pool: PgPool) {
+ let services = Services { postgres: pool };
+ let state = AppState::new(services, &test_config()).await.unwrap();
+ let app = server::router(state);
+
+ let response = app
+ .oneshot(
+ Request::builder()
+ .uri("/users/sellershut")
+ .body(Body::empty())
+ .unwrap(),
+ )
+ .await
+ .unwrap();
+
+ assert_eq!(response.status(), StatusCode::OK);
+ }
+
+ #[sqlx::test]
+ async fn get_user_not_found(pool: PgPool) {
+ let services = Services { postgres: pool };
+ let state = AppState::new(services, &test_config()).await.unwrap();
+ let app = server::router(state);
+
+ let response = app
+ .oneshot(
+ Request::builder()
+ .uri("/users/selut")
+ .body(Body::empty())
+ .unwrap(),
+ )
+ .await
+ .unwrap();
+
+ assert_eq!(response.status(), StatusCode::NOT_FOUND);
+ }
+}
diff --git a/src/server/routes/users/get_user.rs b/src/server/routes/users/get_user.rs
index d86cc26..4079731 100644
--- a/src/server/routes/users/get_user.rs
+++ b/src/server/routes/users/get_user.rs
@@ -2,6 +2,7 @@ use activitypub_federation::{
axum::json::FederationJson, config::Data, protocol::context::WithContext, traits::Object,
};
use axum::{debug_handler, extract::Path, http::StatusCode, response::IntoResponse};
+use tracing::trace;
use crate::{error::AppError, state::AppHandle};
@@ -10,7 +11,7 @@ pub async fn http_get_user(
Path(name): Path<String>,
data: Data<AppHandle>,
) -> Result<impl IntoResponse, AppError> {
- if let Some(a) = read_user(&name, &data).await {
+ if let Some(a) = read_user(&name, &data).await? {
let json_user = a.into_json(&data).await?;
Ok((
StatusCode::OK,
@@ -22,11 +23,26 @@ pub async fn http_get_user(
}
}
-pub async fn read_user(name: &str, data: &Data<AppHandle>) -> Option<crate::entity::user::User> {
- let read = data.users.read().await;
- read.iter()
- .find(|value| value.username.eq(&name))
- .map(ToOwned::to_owned)
+pub async fn read_user(
+ name: &str,
+ data: &Data<AppHandle>,
+) -> Result<Option<crate::entity::user::User>, AppError> {
+ trace!(username = name, "getting user");
+ let read = sqlx::query_as!(
+ crate::entity::user::DbUser,
+ "select * from account where username = $1 and local = $2",
+ name,
+ true
+ )
+ .fetch_optional(&data.services.postgres)
+ .await?;
+
+ let user = read.into_iter().find(|value| value.username.eq(&name));
+ let user = match user {
+ Some(user) => Some(crate::entity::user::User::try_from(user)?),
+ None => None,
+ };
+ Ok(user)
}
#[cfg(test)]
@@ -35,13 +51,19 @@ mod tests {
body::Body,
http::{Request, StatusCode},
};
+ use sqlx::PgPool;
+ use stack_up::Services;
use tower::ServiceExt;
- use crate::{server, state::AppState};
+ use crate::{
+ server::{self, test_config},
+ state::AppState,
+ };
- #[tokio::test]
- async fn get_user() {
- let state = AppState::new().await.unwrap();
+ #[sqlx::test]
+ async fn get_user(pool: PgPool) {
+ let services = Services { postgres: pool };
+ let state = AppState::new(services, &test_config()).await.unwrap();
let app = server::router(state);
let response = app
@@ -57,9 +79,10 @@ mod tests {
assert_eq!(response.status(), StatusCode::OK);
}
- #[tokio::test]
- async fn get_user_not_found() {
- let state = AppState::new().await.unwrap();
+ #[sqlx::test]
+ async fn get_user_not_found(pool: PgPool) {
+ let services = Services { postgres: pool };
+ let state = AppState::new(services, &test_config()).await.unwrap();
let app = server::router(state);
let response = app
diff --git a/src/server/routes/users/webfinger.rs b/src/server/routes/users/webfinger.rs
index 22975c2..8efeda6 100644
--- a/src/server/routes/users/webfinger.rs
+++ b/src/server/routes/users/webfinger.rs
@@ -17,7 +17,7 @@ pub async fn webfinger(
data: Data<AppHandle>,
) -> Result<impl IntoResponse, AppError> {
let name = extract_webfinger_name(&query.resource, &data)?;
- if let Some(db_user) = read_user(name, &data).await {
+ if let Some(db_user) = read_user(name, &data).await? {
Ok((
StatusCode::OK,
Json(build_webfinger_response(
@@ -37,13 +37,19 @@ mod tests {
body::Body,
http::{Request, StatusCode},
};
+ use sqlx::PgPool;
+ use stack_up::Services;
use tower::ServiceExt;
- use crate::{server, state::AppState};
+ use crate::{
+ server::{self, test_config},
+ state::AppState,
+ };
- #[tokio::test]
- async fn webfinger_ok() {
- let state = AppState::new().await.unwrap();
+ #[sqlx::test]
+ async fn webfinger_ok(pool: PgPool) {
+ let services = Services { postgres: pool };
+ let state = AppState::new(services, &test_config()).await.unwrap();
let app = server::router(state);
let response = app
@@ -59,9 +65,10 @@ mod tests {
assert_eq!(response.status(), StatusCode::OK);
}
- #[tokio::test]
- async fn webfinger_err() {
- let state = AppState::new().await.unwrap();
+ #[sqlx::test]
+ async fn webfinger_err(pool: PgPool) {
+ let services = Services { postgres: pool };
+ let state = AppState::new(services, &test_config()).await.unwrap();
let app = server::router(state);
let response = app
diff --git a/src/state.rs b/src/state.rs
index d7c9136..5ced62e 100644
--- a/src/state.rs
+++ b/src/state.rs
@@ -1,7 +1,7 @@
use std::{ops::Deref, sync::Arc};
use activitypub_federation::config::FederationConfig;
-use tokio::sync::RwLock;
+use stack_up::{Configuration, Environment, Services};
use crate::{entity::user::User, error::AppError};
@@ -17,23 +17,24 @@ impl Deref for AppHandle {
}
pub struct AppState {
- pub users: RwLock<Vec<User>>,
+ pub services: Services,
}
impl AppState {
- pub async fn new() -> Result<FederationConfig<AppHandle>, AppError> {
- let user = User::new("sellershut")?;
+ pub async fn new(
+ services: Services,
+ configuration: &Configuration,
+ ) -> Result<FederationConfig<AppHandle>, AppError> {
+ let user = User::new("sellershut", &services).await?;
let domain = "localhost";
let config = FederationConfig::builder()
.domain(domain)
.signed_fetch_actor(&user)
- .app_data(AppHandle(Arc::new(Self {
- users: RwLock::new(vec![user]),
- })))
+ .app_data(AppHandle(Arc::new(Self { services })))
// .url_verifier(Box::new(MyUrlVerifier()))
// TODO: could change this to env variable?
- .debug(cfg!(debug_assertions))
+ .debug(configuration.application.env == Environment::Development)
.build()
.await?;