feat: allow specifying base domain

This commit is contained in:
əlemi 2024-03-20 08:56:35 +01:00
parent d60c29d4cb
commit 5386c7ba7a
Signed by: alemi
GPG key ID: A4895B84D311642C
7 changed files with 81 additions and 51 deletions

View file

@ -1,14 +1,12 @@
use std::{ops::Deref, sync::Arc};
use axum::{extract::{Path, State}, http::StatusCode, Json};
use sea_orm::{DatabaseConnection, EntityTrait};
use sea_orm::EntityTrait;
use crate::{activitystream::Base, model::activity};
use crate::{activitystream::Base, model::activity, server::Context};
pub async fn view(State(db) : State<Arc<DatabaseConnection>>, Path(id): Path<String>) -> Result<Json<serde_json::Value>, StatusCode> {
pub async fn view(State(ctx) : State<Context>, Path(id): Path<String>) -> Result<Json<serde_json::Value>, StatusCode> {
let uri = format!("http://localhost:3000/activities/{id}");
match activity::Entity::find_by_id(uri).one(db.deref()).await {
match activity::Entity::find_by_id(uri).one(ctx.db()).await {
Ok(Some(activity)) => Ok(Json(activity.underlying_json_object())),
Ok(None) => Err(StatusCode::NOT_FOUND),
Err(e) => {

View file

@ -2,22 +2,13 @@ pub mod user;
pub mod object;
pub mod activity;
use std::{ops::Deref, sync::Arc};
use axum::{extract::State, http::StatusCode, Json};
use sea_orm::{DatabaseConnection, EntityTrait, IntoActiveModel};
use sea_orm::{EntityTrait, IntoActiveModel};
use crate::{activitystream::{object::{ObjectType, activity::{Activity, ActivityType}}, Base, BaseType, Node}, model};
use crate::{activitystream::{object::{activity::{Activity, ActivityType}, ObjectType}, Base, BaseType, Node}, model, server::Context};
pub fn uri_id(entity: &str, id: String) -> String {
if id.starts_with("http") { id } else { format!("http://localhost:3000/{entity}/{id}") }
}
pub fn id_uri(id: &str) -> &str {
id.split('/').last().unwrap_or("")
}
#[derive(Debug, serde::Deserialize)]
// TODO i don't really like how pleroma/mastodon do it actually, maybe change this?
pub struct Page {
@ -25,7 +16,7 @@ pub struct Page {
pub max_id: Option<String>,
}
pub async fn inbox(State(db) : State<Arc<DatabaseConnection>>, Json(object): Json<serde_json::Value>) -> Result<Json<serde_json::Value>, StatusCode> {
pub async fn inbox(State(ctx) : State<Context>, Json(object): Json<serde_json::Value>) -> Result<Json<serde_json::Value>, StatusCode> {
match object.base_type() {
None => { Err(StatusCode::BAD_REQUEST) },
Some(BaseType::Link(_x)) => Err(StatusCode::UNPROCESSABLE_ENTITY), // we could but not yet
@ -44,10 +35,10 @@ pub async fn inbox(State(db) : State<Arc<DatabaseConnection>>, Json(object): Jso
return Err(StatusCode::UNPROCESSABLE_ENTITY);
};
model::object::Entity::insert(obj_entity.into_active_model())
.exec(db.deref())
.exec(ctx.db())
.await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
model::activity::Entity::insert(activity_entity.into_active_model())
.exec(db.deref())
.exec(ctx.db())
.await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(serde_json::Value::Null)) // TODO hmmmmmmmmmmm not the best value to return....
},
@ -56,6 +47,6 @@ pub async fn inbox(State(db) : State<Arc<DatabaseConnection>>, Json(object): Jso
}
}
pub async fn outbox(State(_db): State<Arc<DatabaseConnection>>) -> Result<Json<serde_json::Value>, StatusCode> {
pub async fn outbox(State(_db): State<Context>) -> Result<Json<serde_json::Value>, StatusCode> {
todo!()
}

View file

@ -1,14 +1,11 @@
use std::{ops::Deref, sync::Arc};
use axum::{extract::{Path, State}, http::StatusCode, Json};
use sea_orm::{DatabaseConnection, EntityTrait};
use sea_orm::EntityTrait;
use crate::{activitystream::Base, model::object};
use crate::{activitystream::Base, model::object, server::Context};
pub async fn view(State(db) : State<Arc<DatabaseConnection>>, Path(id): Path<String>) -> Result<Json<serde_json::Value>, StatusCode> {
let uri = format!("http://localhost:3000/objects/{id}");
match object::Entity::find_by_id(uri).one(db.deref()).await {
pub async fn view(State(ctx) : State<Context>, Path(id): Path<String>) -> Result<Json<serde_json::Value>, StatusCode> {
match object::Entity::find_by_id(ctx.uri("objects", id)).one(ctx.db()).await {
Ok(Some(object)) => Ok(Json(object.underlying_json_object())),
Ok(None) => Err(StatusCode::NOT_FOUND),
Err(e) => {

View file

@ -3,14 +3,14 @@ use std::sync::Arc;
use axum::{extract::{Path, Query, State}, http::StatusCode, Json};
use sea_orm::{ColumnTrait, Condition, DatabaseConnection, EntityTrait, IntoActiveModel, Order, QueryFilter, QueryOrder, QuerySelect};
use crate::{activitystream::{self, object::{activity::{Activity, ActivityType}, collection::{page::CollectionPageMut, CollectionMut, CollectionType}, ObjectType}, Base, BaseMut, BaseType, Node}, model::{self, activity, object, user}};
use crate::{activitystream::{self, object::{activity::{Activity, ActivityType}, collection::{page::CollectionPageMut, CollectionMut, CollectionType}, ObjectType}, Base, BaseMut, BaseType, Node}, model::{self, activity, object, user}, server::Context};
pub async fn list(State(_db) : State<Arc<DatabaseConnection>>) -> Result<Json<serde_json::Value>, StatusCode> {
todo!()
}
pub async fn view(State(db) : State<Arc<DatabaseConnection>>, Path(id): Path<String>) -> Result<Json<serde_json::Value>, StatusCode> {
match user::Entity::find_by_id(super::uri_id("users", id)).one(&*db).await {
pub async fn view(State(ctx) : State<Context>, Path(id): Path<String>) -> Result<Json<serde_json::Value>, StatusCode> {
match user::Entity::find_by_id(ctx.uri("users", id)).one(ctx.db()).await {
Ok(Some(user)) => Ok(Json(user.underlying_json_object())),
Ok(None) => Err(StatusCode::NOT_FOUND),
Err(e) => {
@ -21,7 +21,7 @@ pub async fn view(State(db) : State<Arc<DatabaseConnection>>, Path(id): Path<Str
}
pub async fn outbox(
State(db): State<Arc<DatabaseConnection>>,
State(ctx): State<Context>,
Path(id): Path<String>,
Query(page): Query<super::Page>,
) -> Result<Json<serde_json::Value>, StatusCode> {
@ -29,8 +29,8 @@ pub async fn outbox(
// find requested recent post, to filter based on its date (use now() as fallback)
let before = if let Some(before) = page.max_id {
match model::activity::Entity::find_by_id(super::uri_id("activities", before))
.one(&*db).await
match model::activity::Entity::find_by_id(ctx.uri("activities", before))
.one(ctx.db()).await
{
Ok(None) => return Err(StatusCode::NOT_FOUND),
Ok(Some(x)) => x.published,
@ -45,11 +45,11 @@ pub async fn outbox(
.filter(Condition::all().add(activity::Column::Published.lt(before)))
.order_by(activity::Column::Published, Order::Desc)
.limit(20) // TODO allow customizing, with boundaries
.all(&*db).await
.all(ctx.db()).await
{
Err(_e) => Err(StatusCode::INTERNAL_SERVER_ERROR),
Ok(items) => {
let next = super::id_uri(&items.last().unwrap().id).to_string();
let next = ctx.id(items.last().map(|x| x.id.as_str()).unwrap_or("").to_string());
let items = items
.into_iter()
.map(|i| i.underlying_json_object())
@ -76,7 +76,7 @@ pub async fn outbox(
}
pub async fn inbox(
State(db): State<Arc<DatabaseConnection>>,
State(ctx): State<Context>,
Path(_id): Path<String>,
Json(object): Json<serde_json::Value>
) -> Result<Json<serde_json::Value>, StatusCode> {
@ -98,10 +98,10 @@ pub async fn inbox(
return Err(StatusCode::UNPROCESSABLE_ENTITY);
};
object::Entity::insert(obj_entity.into_active_model())
.exec(&*db)
.exec(ctx.db())
.await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
activity::Entity::insert(activity_entity.into_active_model())
.exec(&*db)
.exec(ctx.db())
.await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(serde_json::Value::Null)) // TODO hmmmmmmmmmmm not the best value to return....
},

View file

@ -21,6 +21,10 @@ struct CliArgs {
/// database connection uri
database: String,
#[arg(short, long, default_value = "http://localhost:3000")]
/// instance base domain, for AP ids
domain: String,
#[arg(long, default_value_t=false)]
/// run with debug level tracing
debug: bool,
@ -66,13 +70,13 @@ async fn main() {
.await.expect("error connecting to db");
match args.command {
CliCommand::Serve => server::serve(db)
CliCommand::Serve => server::serve(db, args.domain)
.await,
CliCommand::Migrate => migrations::Migrator::up(&db, None)
.await.expect("error applying migrations"),
CliCommand::Faker => model::faker(&db)
CliCommand::Faker => model::faker(&db, args.domain)
.await.expect("error creating fake entities"),
CliCommand::Fetch { uri, save } => fetch(&db, &uri, save)

View file

@ -6,11 +6,11 @@ pub mod activity;
#[error("missing required field: '{0}'")]
pub struct FieldError(pub &'static str);
pub async fn faker(db: &sea_orm::DatabaseConnection) -> Result<(), sea_orm::DbErr> {
pub async fn faker(db: &sea_orm::DatabaseConnection, domain: String) -> Result<(), sea_orm::DbErr> {
use sea_orm::EntityTrait;
user::Entity::insert(user::ActiveModel {
id: sea_orm::Set("http://localhost:3000/users/root".into()),
id: sea_orm::Set(format!("{domain}/users/root")),
name: sea_orm::Set("root".into()),
actor_type: sea_orm::Set(super::activitystream::object::actor::ActorType::Person),
}).exec(db).await?;
@ -19,20 +19,20 @@ pub async fn faker(db: &sea_orm::DatabaseConnection) -> Result<(), sea_orm::DbEr
let oid = uuid::Uuid::new_v4();
let aid = uuid::Uuid::new_v4();
object::Entity::insert(object::ActiveModel {
id: sea_orm::Set(format!("http://localhost:3000/objects/{oid}")),
id: sea_orm::Set(format!("{domain}/objects/{oid}")),
name: sea_orm::Set(None),
object_type: sea_orm::Set(crate::activitystream::object::ObjectType::Note),
attributed_to: sea_orm::Set(Some("http://localhost:3000/users/root".into())),
attributed_to: sea_orm::Set(Some(format!("{domain}/users/root"))),
summary: sea_orm::Set(None),
content: sea_orm::Set(Some(format!("Hello world! {i}"))),
content: sea_orm::Set(Some(format!("[{i}] Tic(k). Quasiparticle of intensive multiplicity. Tics (or ticks) are intrinsically several components of autonomously numbering anorganic populations, propagating by contagion between segmentary divisions in the order of nature. Ticks - as nonqualitative differentially-decomposable counting marks - each designate a multitude comprehended as a singular variation in tic(k)-density."))),
published: sea_orm::Set(chrono::Utc::now() - std::time::Duration::from_secs(60*i)),
}).exec(db).await?;
activity::Entity::insert(activity::ActiveModel {
id: sea_orm::Set(format!("http://localhost:3000/activities/{aid}")),
id: sea_orm::Set(format!("{domain}/activities/{aid}")),
activity_type: sea_orm::Set(crate::activitystream::object::activity::ActivityType::Create),
actor: sea_orm::Set("http://localhost:3000/users/root".into()),
object: sea_orm::Set(Some(format!("http://localhost:3000/objects/{oid}"))),
actor: sea_orm::Set(format!("{domain}/users/root")),
object: sea_orm::Set(Some(format!("{domain}/objects/{oid}"))),
target: sea_orm::Set(None),
published: sea_orm::Set(chrono::Utc::now() - std::time::Duration::from_secs(60*i)),
}).exec(db).await?;

View file

@ -4,7 +4,47 @@ use axum::{routing::{get, post}, Router};
use sea_orm::DatabaseConnection;
use crate::activitypub as ap;
pub async fn serve(db: DatabaseConnection) {
#[derive(Clone)]
pub struct Context(Arc<ContextInner>);
struct ContextInner {
db: DatabaseConnection,
domain: String,
}
impl Context {
pub fn new(db: DatabaseConnection, mut domain: String) -> Self {
if !domain.starts_with("http") {
domain = format!("https://{domain}");
}
if domain.ends_with('/') {
domain.replace_range(domain.len()-1.., "");
}
Context(Arc::new(ContextInner { db, domain }))
}
pub fn db(&self) -> &DatabaseConnection {
&self.0.db
}
pub fn uri(&self, entity: &str, id: String) -> String {
if id.starts_with("http") { id } else {
format!("{}/{}/{}", self.0.domain, entity, id)
}
}
pub fn id(&self, id: String) -> String {
if id.starts_with(&self.0.domain) {
let mut out = id.replace(&self.0.domain, "");
if out.ends_with('/') {
out.replace_range(out.len()-1.., "");
}
out
} else {
id
}
}
}
pub async fn serve(db: DatabaseConnection, domain: String) {
// build our application with a single route
let app = Router::new()
// core server inbox/outbox, maybe for feeds? TODO do we need these?
@ -17,7 +57,7 @@ pub async fn serve(db: DatabaseConnection) {
// specific object routes
.route("/activities/:id", get(ap::activity::view))
.route("/objects/:id", get(ap::object::view))
.with_state(Arc::new(db));
.with_state(Context::new(db, domain));
// run our app with hyper, listening globally on port 3000
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();