diff --git a/Cargo.lock b/Cargo.lock index 7dcf1e09..ba4d0a5e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" dependencies = [ "gimli", ] @@ -310,9 +310,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.71" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +checksum = "17c6a35df3749d2e8bb1b7b21a976d82b15548788d2735b9d82f329268f71a11" dependencies = [ "addr2line", "cc", @@ -1275,9 +1275,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" [[package]] name = "glob" @@ -1727,9 +1727,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d8d52be92d09acc2e01dddb7fde3ad983fc6489c7db4837e605bc3fca4cb63e" +checksum = "7b875924a60b96e5d7b9ae7b066540b1dd1cbd90d1828f54c92e02a283351c56" dependencies = [ "bytes", "futures-channel", @@ -2280,11 +2280,10 @@ dependencies = [ [[package]] name = "native-tls" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" dependencies = [ - "lazy_static", "libc", "log", "openssl", @@ -2407,9 +2406,9 @@ dependencies = [ [[package]] name = "object" -version = "0.32.2" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "b8ec7ab813848ba4522158d5517a6093db1ded27575b070f4177b8d12b41db5e" dependencies = [ "memchr", ] @@ -4358,9 +4357,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.37.0" +version = "1.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" dependencies = [ "backtrace", "bytes", @@ -4377,9 +4376,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", @@ -4462,7 +4461,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.8", + "winnow 0.6.9", ] [[package]] @@ -4683,38 +4682,92 @@ name = "upub" version = "0.2.0" dependencies = [ "apb", - "async-recursion", "axum", "base64 0.22.1", "chrono", - "clap", - "futures", "jrd", - "mastodon-async-entities", "mdhtml", "nodeinfo", "openssl", - "rand", "regex", "reqwest", "sea-orm", - "sea-orm-migration", "serde", "serde-inline-default", "serde_default", "serde_json", "sha256", "thiserror", - "time", "tokio", "toml", - "tower-http", "tracing", - "tracing-subscriber", "uriproxy", "uuid", ] +[[package]] +name = "upub-bin" +version = "0.2.0" +dependencies = [ + "clap", + "sea-orm", + "tokio", + "toml", + "tracing", + "tracing-subscriber", + "upub", + "upub-cli", + "upub-migrations", + "upub-routes", +] + +[[package]] +name = "upub-cli" +version = "0.2.0" +dependencies = [ + "apb", + "chrono", + "clap", + "futures", + "openssl", + "sea-orm", + "serde_json", + "sha256", + "tracing", + "upub", + "uuid", +] + +[[package]] +name = "upub-migrations" +version = "0.2.0" +dependencies = [ + "sea-orm-migration", +] + +[[package]] +name = "upub-routes" +version = "0.2.0" +dependencies = [ + "apb", + "axum", + "chrono", + "jrd", + "mastodon-async-entities", + "nodeinfo", + "rand", + "reqwest", + "sea-orm", + "serde", + "serde_json", + "sha256", + "time", + "tokio", + "tower-http", + "tracing", + "upub", +] + [[package]] name = "upub-web" version = "0.1.0" @@ -4729,14 +4782,12 @@ dependencies = [ "leptos", "leptos-use", "leptos_router", - "log", "mdhtml", "reqwest", "serde", "serde-inline-default", "serde_default", "serde_json", - "thiserror", "tld", "tracing", "tracing-subscriber", @@ -5177,9 +5228,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3c52e9c97a68071b23e836c9380edae937f17b9c4667bd021973efc689f618d" +checksum = "86c949fede1d13936a99f14fafd3e76fd642b556dd2ce96287fbe2e0151bfac6" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index 9fc40578..1ee9c272 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,17 @@ [workspace] -members = ["apb", "web", "utils/mdhtml", "utils/uriproxy"] +members = [ + "apb", + "upub/core", + "upub/cli", + "upub/migrations", + "upub/routes", + "web", + "utils/mdhtml", + "utils/uriproxy" +] [package] -name = "upub" +name = "upub-bin" version = "0.2.0" edition = "2021" authors = [ "alemi " ] @@ -12,46 +21,25 @@ keywords = ["activitypub", "activitystreams", "json"] repository = "https://git.alemi.dev/upub.git" readme = "README.md" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[[bin]] +name = "upub" +path = "main.rs" [dependencies] -thiserror = "1" -rand = "0.8" -sha256 = "1.5" -openssl = "0.10" # TODO handle pubkeys with a smaller crate -base64 = "0.22" -chrono = { version = "0.4", features = ["serde"] } -uuid = { version = "1.8", features = ["v4"] } -regex = "1.10" -serde = { version = "1", features = ["derive"] } -serde_json = "1" -serde_default = "0.1" -serde-inline-default = "0.2" toml = "0.8" -mdhtml = { path = "utils/mdhtml", features = ["markdown"] } -uriproxy = { path = "utils/uriproxy" } -jrd = "0.1" tracing = "0.1" tracing-subscriber = "0.3" +sea-orm = "0.12" clap = { version = "4.5", features = ["derive"] } -futures = "0.3" tokio = { version = "1.35", features = ["full"] } # TODO slim this down -sea-orm = { version = "0.12", features = ["macros", "sqlx-sqlite", "runtime-tokio-rustls"] } -reqwest = { version = "0.12", features = ["json"] } -axum = "0.7" -tower-http = { version = "0.5", features = ["cors", "trace"] } -apb = { path = "apb", features = ["unstructured", "orm", "activitypub-fe", "activitypub-counters", "litepub", "ostatus", "toot"] } -# nodeinfo = "0.0.2" # the version on crates.io doesn't re-export necessary types to build the struct!!! -nodeinfo = { git = "https://codeberg.org/thefederationinfo/nodeinfo-rs", rev = "e865094804" } -# migrations -sea-orm-migration = { version = "0.12", optional = true } -# mastodon -mastodon-async-entities = { version = "1.1.0", optional = true } -time = { version = "0.3", features = ["serde"], optional = true } -async-recursion = "1.1" + +upub = { path = "upub/core" } +upub-cli = { path = "upub/cli", optional = true } +upub-migrations = { path = "upub/migrations", optional = true } +upub-routes = { path = "upub/routes", optional = true } [features] -default = ["mastodon", "migrations", "cli"] -cli = [] -migrations = ["dep:sea-orm-migration"] -mastodon = ["dep:mastodon-async-entities", "dep:time"] +default = ["serve", "migrate", "cli"] +serve = ["dep:upub-routes"] +migrate = ["dep:upub-migrations"] +cli = ["dep:upub-cli"] diff --git a/apb/src/lib.rs b/apb/src/lib.rs index 6bacda54..fad080f4 100644 --- a/apb/src/lib.rs +++ b/apb/src/lib.rs @@ -125,3 +125,8 @@ pub use types::{ tombstone::{Tombstone, TombstoneMut}, }, }; + +#[cfg(feature = "unstructured")] +pub fn new() -> serde_json::Value { + serde_json::Value::Object(serde_json::Map::default()) +} diff --git a/src/main.rs b/main.rs similarity index 67% rename from src/main.rs rename to main.rs index af04db95..171d5595 100644 --- a/src/main.rs +++ b/main.rs @@ -1,28 +1,16 @@ -mod server; -mod model; -mod routes; - -pub mod errors; -mod config; - -#[cfg(feature = "cli")] -mod cli; - -#[cfg(feature = "migrations")] -mod migrations; - -#[cfg(feature = "migrations")] -use sea_orm_migration::MigratorTrait; use std::path::PathBuf; -use config::Config; - use clap::{Parser, Subcommand}; use sea_orm::{ConnectOptions, Database}; -pub use errors::UpubResult as Result; -use tower_http::{cors::CorsLayer, trace::TraceLayer}; +#[cfg(feature = "cli")] +use upub_cli as cli; + +#[cfg(feature = "migrate")] +use upub_migrations as migrations; + +#[cfg(feature = "serve")] +use upub_routes as routes; -pub const VERSION: &str = env!("CARGO_PKG_VERSION"); #[derive(Parser)] /// all names were taken @@ -50,17 +38,10 @@ struct Args { #[derive(Clone, Subcommand)] enum Mode { - /// run fediverse server - Serve { - #[arg(short, long, default_value="127.0.0.1:3000")] - /// addr to bind and serve onto - bind: String, - }, - /// print current or default configuration Config, - #[cfg(feature = "migrations")] + #[cfg(feature = "migrate")] /// apply database migrations Migrate, @@ -71,6 +52,14 @@ enum Mode { /// task to run command: cli::CliCommand, }, + + #[cfg(feature = "serve")] + /// run fediverse server + Serve { + #[arg(short, long, default_value="127.0.0.1:3000")] + /// addr to bind and serve onto + bind: String, + }, } #[tokio::main] @@ -83,7 +72,7 @@ async fn main() { .with_max_level(if args.debug { tracing::Level::DEBUG } else { tracing::Level::INFO }) .init(); - let config = Config::load(args.config); + let config = upub::Config::load(args.config); let database = args.database.unwrap_or(config.datasource.connection_string.clone()); let domain = args.domain.unwrap_or(config.instance.domain.clone()); @@ -106,40 +95,28 @@ async fn main() { let db = Database::connect(opts) .await.expect("error connecting to db"); + let ctx = upub::Context::new(db, domain, config.clone()) + .await.expect("failed creating server context"); + + #[cfg(feature = "migrate")] + use migrations::MigratorTrait; + match args.command { - #[cfg(feature = "migrations")] + Mode::Config => println!("{}", toml::to_string_pretty(&config).expect("failed serializing config")), + + #[cfg(feature = "migrate")] Mode::Migrate => - migrations::Migrator::up(&db, None) + migrations::Migrator::up(ctx.db(), None) .await.expect("error applying migrations"), #[cfg(feature = "cli")] Mode::Cli { command } => - cli::run(command, db, domain, config) + cli::run(ctx, command) .await.expect("failed running cli task"), - Mode::Config => println!("{}", toml::to_string_pretty(&config).expect("failed serializing config")), - - Mode::Serve { bind } => { - let ctx = server::Context::new(db, domain, config) - .await.expect("failed creating server context"); - - use routes::activitypub::ActivityPubRouter; - use routes::mastodon::MastodonRouter; - - let router = axum::Router::new() - .ap_routes() - .mastodon_routes() // no-op if mastodon feature is disabled - .layer(CorsLayer::permissive()) - .layer(TraceLayer::new_for_http()) - .with_state(ctx); - - // run our app with hyper, listening locally on port 3000 - let listener = tokio::net::TcpListener::bind(bind) - .await.expect("could not bind tcp socket"); - - axum::serve(listener, router) - .await - .expect("failed serving application") - }, + #[cfg(feature = "serve")] + Mode::Serve { bind } => + routes::serve(ctx, bind) + .await.expect("failed serving api routes"), } } diff --git a/src/cli/relay.rs b/src/cli/relay.rs deleted file mode 100644 index d618229e..00000000 --- a/src/cli/relay.rs +++ /dev/null @@ -1,41 +0,0 @@ -use sea_orm::{ActiveValue::{Set, NotSet}, ColumnTrait, EntityTrait, QueryFilter, QueryOrder}; - -use crate::server::addresser::Addresser; - -pub async fn relay(ctx: crate::server::Context, actor: String, accept: bool) -> crate::Result<()> { - let aid = ctx.aid(&uuid::Uuid::new_v4().to_string()); - - let mut activity_model = crate::model::activity::ActiveModel { - internal: NotSet, - id: Set(aid.clone()), - activity_type: Set(apb::ActivityType::Follow), - actor: Set(ctx.base().to_string()), - object: Set(Some(actor.clone())), - target: Set(None), - published: Set(chrono::Utc::now()), - to: Set(crate::model::Audience(vec![actor.clone()])), - bto: Set(crate::model::Audience::default()), - cc: Set(crate::model::Audience(vec![apb::target::PUBLIC.to_string()])), - bcc: Set(crate::model::Audience::default()), - }; - - if accept { - let follow_req = crate::model::activity::Entity::find() - .filter(crate::model::activity::Column::ActivityType.eq("Follow")) - .filter(crate::model::activity::Column::Actor.eq(&actor)) - .filter(crate::model::activity::Column::Object.eq(ctx.base())) - .order_by_desc(crate::model::activity::Column::Published) - .one(ctx.db()) - .await? - .expect("no follow request to accept"); - activity_model.activity_type = Set(apb::ActivityType::Accept(apb::AcceptType::Accept)); - activity_model.object = Set(Some(follow_req.id)); - }; - - crate::model::activity::Entity::insert(activity_model) - .exec(ctx.db()).await?; - - ctx.dispatch(ctx.base(), vec![actor, apb::target::PUBLIC.to_string()], &aid, None).await?; - - Ok(()) -} diff --git a/src/routes/mod.rs b/src/routes/mod.rs deleted file mode 100644 index bde0fbdf..00000000 --- a/src/routes/mod.rs +++ /dev/null @@ -1,16 +0,0 @@ -pub mod activitypub; - -#[cfg(feature = "web")] -pub mod web; - -#[cfg(feature = "mastodon")] -pub mod mastodon; - -#[cfg(not(feature = "mastodon"))] -pub mod mastodon { - pub trait MastodonRouter { - fn mastodon_routes(self) -> Self where Self: Sized { self } - } - - impl MastodonRouter for axum::Router {} -} diff --git a/upub/cli/Cargo.toml b/upub/cli/Cargo.toml new file mode 100644 index 00000000..ec11cdfd --- /dev/null +++ b/upub/cli/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "upub-cli" +version = "0.2.0" +edition = "2021" +authors = [ "alemi " ] +description = "cli maintenance tasks for upub" +license = "AGPL-3.0" +repository = "https://git.alemi.dev/upub.git" +readme = "README.md" + +[lib] + +[dependencies] +apb = { path = "../../apb/" } +upub = { path = "../core" } +tracing = "0.1" +serde_json = "1" +sha256 = "1.5" +uuid = { version = "1.8", features = ["v4"] } +chrono = { version = "0.4", features = ["serde"] } +openssl = "0.10" # TODO handle pubkeys with a smaller crate +clap = { version = "4.5", features = ["derive"] } +sea-orm = { version = "0.12", features = ["macros", "sqlx-sqlite", "runtime-tokio-rustls"] } +futures = "0.3" diff --git a/upub/cli/README.md b/upub/cli/README.md new file mode 100644 index 00000000..fa0aadef --- /dev/null +++ b/upub/cli/README.md @@ -0,0 +1 @@ +# upub cli diff --git a/src/cli/faker.rs b/upub/cli/src/faker.rs similarity index 95% rename from src/cli/faker.rs rename to upub/cli/src/faker.rs index 5def1640..be35635e 100644 --- a/src/cli/faker.rs +++ b/upub/cli/src/faker.rs @@ -1,8 +1,8 @@ -use crate::model::{addressing, config, credential, activity, object, actor, Audience}; +use upub::model::{addressing, config, credential, activity, object, actor, Audience}; use openssl::rsa::Rsa; use sea_orm::{ActiveValue::NotSet, IntoActiveModel}; -pub async fn faker(ctx: crate::server::Context, count: i64) -> Result<(), sea_orm::DbErr> { +pub async fn faker(ctx: upub::Context, count: i64) -> Result<(), sea_orm::DbErr> { use sea_orm::{EntityTrait, Set}; let domain = ctx.domain(); diff --git a/src/cli/fetch.rs b/upub/cli/src/fetch.rs similarity index 73% rename from src/cli/fetch.rs rename to upub/cli/src/fetch.rs index a589e6b3..f5af92da 100644 --- a/src/cli/fetch.rs +++ b/upub/cli/src/fetch.rs @@ -1,23 +1,22 @@ use sea_orm::EntityTrait; +use upub::server::{fetcher::Fetchable, normalizer::Normalizer}; -use crate::server::{fetcher::Fetchable, normalizer::Normalizer, Context}; - -pub async fn fetch(ctx: crate::server::Context, uri: String, save: bool) -> crate::Result<()> { +pub async fn fetch(ctx: upub::Context, uri: String, save: bool) -> upub::Result<()> { use apb::Base; let mut node = apb::Node::link(uri.to_string()); node.fetch(&ctx).await?; let obj = node.extract().expect("node still empty after fetch?"); - let server = Context::server(&uri); + let server = upub::Context::server(&uri); println!("{}", serde_json::to_string_pretty(&obj).unwrap()); if save { match obj.base_type() { Some(apb::BaseType::Object(apb::ObjectType::Actor(_))) => { - crate::model::actor::Entity::insert( - crate::model::actor::ActiveModel::new(&obj).unwrap() + upub::model::actor::Entity::insert( + upub::model::actor::ActiveModel::new(&obj).unwrap() ).exec(ctx.db()).await.unwrap(); }, Some(apb::BaseType::Object(apb::ObjectType::Activity(_))) => { diff --git a/src/cli/fix.rs b/upub/cli/src/fix.rs similarity index 70% rename from src/cli/fix.rs rename to upub/cli/src/fix.rs index 53ede8eb..a782a127 100644 --- a/src/cli/fix.rs +++ b/upub/cli/src/fix.rs @@ -1,7 +1,6 @@ use sea_orm::EntityTrait; - -pub async fn fix(ctx: crate::server::Context, likes: bool, shares: bool, replies: bool) -> crate::Result<()> { +pub async fn fix(ctx: upub::Context, likes: bool, shares: bool, replies: bool) -> upub::Result<()> { use futures::TryStreamExt; let db = ctx.db(); @@ -9,19 +8,19 @@ pub async fn fix(ctx: crate::server::Context, likes: bool, shares: bool, replies tracing::info!("fixing likes..."); let mut store = std::collections::HashMap::new(); { - let mut stream = crate::model::like::Entity::find().stream(db).await?; + let mut stream = upub::model::like::Entity::find().stream(db).await?; while let Some(like) = stream.try_next().await? { store.insert(like.object, store.get(&like.object).unwrap_or(&0) + 1); } } for (k, v) in store { - let m = crate::model::object::ActiveModel { + let m = upub::model::object::ActiveModel { internal: sea_orm::Set(k), likes: sea_orm::Set(v), ..Default::default() }; - if let Err(e) = crate::model::object::Entity::update(m) + if let Err(e) = upub::model::object::Entity::update(m) .exec(db) .await { @@ -34,19 +33,19 @@ pub async fn fix(ctx: crate::server::Context, likes: bool, shares: bool, replies tracing::info!("fixing shares..."); let mut store = std::collections::HashMap::new(); { - let mut stream = crate::model::announce::Entity::find().stream(db).await?; + let mut stream = upub::model::announce::Entity::find().stream(db).await?; while let Some(share) = stream.try_next().await? { store.insert(share.object, store.get(&share.object).unwrap_or(&0) + 1); } } for (k, v) in store { - let m = crate::model::object::ActiveModel { + let m = upub::model::object::ActiveModel { internal: sea_orm::Set(k), announces: sea_orm::Set(v), ..Default::default() }; - if let Err(e) = crate::model::object::Entity::update(m) + if let Err(e) = upub::model::object::Entity::update(m) .exec(db) .await { @@ -59,7 +58,7 @@ pub async fn fix(ctx: crate::server::Context, likes: bool, shares: bool, replies tracing::info!("fixing replies..."); let mut store = std::collections::HashMap::new(); { - let mut stream = crate::model::object::Entity::find().stream(db).await?; + let mut stream = upub::model::object::Entity::find().stream(db).await?; while let Some(object) = stream.try_next().await? { if let Some(reply) = object.in_reply_to { let before = store.get(&reply).unwrap_or(&0); @@ -69,12 +68,12 @@ pub async fn fix(ctx: crate::server::Context, likes: bool, shares: bool, replies } for (k, v) in store { - let m = crate::model::object::ActiveModel { + let m = upub::model::object::ActiveModel { id: sea_orm::Set(k.clone()), replies: sea_orm::Set(v), ..Default::default() }; - if let Err(e) = crate::model::object::Entity::update(m) + if let Err(e) = upub::model::object::Entity::update(m) .exec(db) .await { diff --git a/src/cli/mod.rs b/upub/cli/src/lib.rs similarity index 91% rename from src/cli/mod.rs rename to upub/cli/src/lib.rs index bcf8eeaa..2636f9bd 100644 --- a/src/cli/mod.rs +++ b/upub/cli/src/lib.rs @@ -93,15 +93,7 @@ pub enum CliCommand { } } -pub async fn run( - command: CliCommand, - db: sea_orm::DatabaseConnection, - domain: String, - config: crate::config::Config, -) -> crate::Result<()> { - let ctx = crate::server::Context::new( - db, domain, config, - ).await?; +pub async fn run(ctx: upub::Context, command: CliCommand) -> upub::Result<()> { match command { CliCommand::Faker { count } => Ok(faker(ctx, count as i64).await?), diff --git a/src/cli/register.rs b/upub/cli/src/register.rs similarity index 78% rename from src/cli/register.rs rename to upub/cli/src/register.rs index 7755338d..9ef54b09 100644 --- a/src/cli/register.rs +++ b/upub/cli/src/register.rs @@ -1,14 +1,14 @@ -use crate::server::admin::Administrable; +use upub::server::admin::Administrable; pub async fn register( - ctx: crate::server::Context, + ctx: upub::Context, username: String, password: String, display_name: Option, summary: Option, avatar_url: Option, banner_url: Option, -) -> crate::Result<()> { +) -> upub::Result<()> { ctx.register_user( username.clone(), password, diff --git a/upub/cli/src/relay.rs b/upub/cli/src/relay.rs new file mode 100644 index 00000000..94b8dc28 --- /dev/null +++ b/upub/cli/src/relay.rs @@ -0,0 +1,41 @@ +use sea_orm::{ActiveValue::{Set, NotSet}, ColumnTrait, EntityTrait, QueryFilter, QueryOrder}; + +use upub::server::addresser::Addresser; + +pub async fn relay(ctx: upub::Context, actor: String, accept: bool) -> upub::Result<()> { + let aid = ctx.aid(&uuid::Uuid::new_v4().to_string()); + + let mut activity_model = upub::model::activity::ActiveModel { + internal: NotSet, + id: Set(aid.clone()), + activity_type: Set(apb::ActivityType::Follow), + actor: Set(ctx.base().to_string()), + object: Set(Some(actor.clone())), + target: Set(None), + published: Set(chrono::Utc::now()), + to: Set(upub::model::Audience(vec![actor.clone()])), + bto: Set(upub::model::Audience::default()), + cc: Set(upub::model::Audience(vec![apb::target::PUBLIC.to_string()])), + bcc: Set(upub::model::Audience::default()), + }; + + if accept { + let follow_req = upub::model::activity::Entity::find() + .filter(upub::model::activity::Column::ActivityType.eq("Follow")) + .filter(upub::model::activity::Column::Actor.eq(&actor)) + .filter(upub::model::activity::Column::Object.eq(ctx.base())) + .order_by_desc(upub::model::activity::Column::Published) + .one(ctx.db()) + .await? + .expect("no follow request to accept"); + activity_model.activity_type = Set(apb::ActivityType::Accept(apb::AcceptType::Accept)); + activity_model.object = Set(Some(follow_req.id)); + }; + + upub::model::activity::Entity::insert(activity_model) + .exec(ctx.db()).await?; + + ctx.dispatch(ctx.base(), vec![actor, apb::target::PUBLIC.to_string()], &aid, None).await?; + + Ok(()) +} diff --git a/src/cli/update.rs b/upub/cli/src/update.rs similarity index 69% rename from src/cli/update.rs rename to upub/cli/src/update.rs index 9d757895..ca5f48b2 100644 --- a/src/cli/update.rs +++ b/upub/cli/src/update.rs @@ -1,15 +1,15 @@ use futures::TryStreamExt; use sea_orm::{ActiveValue::Set, ColumnTrait, EntityTrait, QueryFilter}; -use crate::server::fetcher::Fetcher; +use upub::server::fetcher::Fetcher; -pub async fn update_users(ctx: crate::server::Context, days: i64) -> crate::Result<()> { +pub async fn update_users(ctx: upub::Context, days: i64) -> upub::Result<()> { let mut count = 0; let mut insertions = Vec::new(); { - let mut stream = crate::model::actor::Entity::find() - .filter(crate::model::actor::Column::Updated.lt(chrono::Utc::now() - chrono::Duration::days(days))) + let mut stream = upub::model::actor::Entity::find() + .filter(upub::model::actor::Column::Updated.lt(chrono::Utc::now() - chrono::Duration::days(days))) .stream(ctx.db()) .await?; @@ -19,7 +19,7 @@ pub async fn update_users(ctx: crate::server::Context, days: i64) -> crate::Resu match ctx.pull(&user.id).await.map(|x| x.actor()) { Err(e) => tracing::warn!("could not update user {}: {e}", user.id), Ok(Err(e)) => tracing::warn!("could not update user {}: {e}", user.id), - Ok(Ok(doc)) => match crate::model::actor::ActiveModel::new(&doc) { + Ok(Ok(doc)) => match upub::model::actor::ActiveModel::new(&doc) { Ok(mut u) => { u.internal = Set(user.internal); u.updated = Set(chrono::Utc::now()); @@ -34,7 +34,7 @@ pub async fn update_users(ctx: crate::server::Context, days: i64) -> crate::Resu for (uid, user_model) in insertions { tracing::info!("updating user {}", uid); - crate::model::actor::Entity::update(user_model) + upub::model::actor::Entity::update(user_model) .exec(ctx.db()) .await?; } diff --git a/upub/core/Cargo.toml b/upub/core/Cargo.toml new file mode 100644 index 00000000..5a1d28cd --- /dev/null +++ b/upub/core/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "upub" +version = "0.2.0" +edition = "2021" +authors = [ "alemi " ] +description = "core inner workings of upub" +license = "AGPL-3.0" +repository = "https://git.alemi.dev/upub.git" +readme = "README.md" + +[lib] + +[dependencies] +thiserror = "1" +sha256 = "1.5" +openssl = "0.10" # TODO handle pubkeys with a smaller crate +base64 = "0.22" +chrono = { version = "0.4", features = ["serde"] } +uuid = { version = "1.8", features = ["v4"] } +regex = "1.10" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +serde_default = "0.1" +serde-inline-default = "0.2" +toml = "0.8" +mdhtml = { path = "../../utils/mdhtml", features = ["markdown"] } +uriproxy = { path = "../../utils/uriproxy" } +jrd = "0.1" +tracing = "0.1" +tokio = { version = "1.35", features = ["full"] } # TODO slim this down +sea-orm = { version = "0.12", features = ["macros", "sqlx-sqlite", "runtime-tokio-rustls"] } +reqwest = { version = "0.12", features = ["json"] } +axum = "0.7" +apb = { path = "../../apb", features = ["unstructured", "orm", "activitypub-fe", "activitypub-counters", "litepub", "ostatus", "toot"] } +# nodeinfo = "0.0.2" # the version on crates.io doesn't re-export necessary types to build the struct!!! +nodeinfo = { git = "https://codeberg.org/thefederationinfo/nodeinfo-rs", rev = "e865094804" } diff --git a/upub/core/README.md b/upub/core/README.md new file mode 100644 index 00000000..9663c6f0 --- /dev/null +++ b/upub/core/README.md @@ -0,0 +1 @@ +# upub core diff --git a/src/config.rs b/upub/core/src/config.rs similarity index 100% rename from src/config.rs rename to upub/core/src/config.rs diff --git a/src/errors.rs b/upub/core/src/errors.rs similarity index 98% rename from src/errors.rs rename to upub/core/src/errors.rs index 1d0862f7..6279a49a 100644 --- a/src/errors.rs +++ b/upub/core/src/errors.rs @@ -31,6 +31,9 @@ pub enum UpubError { #[error("type mismatch on object: expected {0:?}, found {1:?}")] Mismatch(apb::ObjectType, apb::ObjectType), + #[error("os I/O error: {0}")] + IO(#[from] std::io::Error), + // TODO this isn't really an error but i need to redirect from some routes so this allows me to // keep the type hints on the return type, still what the hell!!!! #[error("redirecting to {0}")] diff --git a/upub/core/src/ext.rs b/upub/core/src/ext.rs new file mode 100644 index 00000000..2ebe26e9 --- /dev/null +++ b/upub/core/src/ext.rs @@ -0,0 +1,19 @@ + +#[axum::async_trait] +pub trait AnyQuery { + async fn any(self, db: &sea_orm::DatabaseConnection) -> crate::Result; +} + +#[axum::async_trait] +impl AnyQuery for sea_orm::Select { + async fn any(self, db: &sea_orm::DatabaseConnection) -> crate::Result { + Ok(self.one(db).await?.is_some()) + } +} + +#[axum::async_trait] +impl AnyQuery for sea_orm::Selector { + async fn any(self, db: &sea_orm::DatabaseConnection) -> crate::Result { + Ok(self.one(db).await?.is_some()) + } +} diff --git a/upub/core/src/lib.rs b/upub/core/src/lib.rs new file mode 100644 index 00000000..d6bfc92f --- /dev/null +++ b/upub/core/src/lib.rs @@ -0,0 +1,12 @@ +pub mod config; +pub mod errors; +pub mod server; +pub mod model; +pub mod ext; + +pub use server::Context; +pub use config::Config; +pub use errors::UpubResult as Result; +pub use errors::UpubError as Error; + +pub const VERSION: &str = env!("CARGO_PKG_VERSION"); diff --git a/src/model/README.md b/upub/core/src/model/README.md similarity index 100% rename from src/model/README.md rename to upub/core/src/model/README.md diff --git a/src/model/activity.rs b/upub/core/src/model/activity.rs similarity index 96% rename from src/model/activity.rs rename to upub/core/src/model/activity.rs index 44d5aa7e..4b657ba8 100644 --- a/src/model/activity.rs +++ b/upub/core/src/model/activity.rs @@ -1,7 +1,7 @@ use apb::{ActivityMut, ActivityType, BaseMut, ObjectMut}; use sea_orm::{entity::prelude::*, QuerySelect, SelectColumns}; -use crate::{model::Audience, errors::UpubError, routes::activitypub::jsonld::LD}; +use crate::{model::Audience, errors::UpubError}; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[sea_orm(table_name = "activities")] @@ -109,7 +109,7 @@ impl ActiveModel { impl Model { pub fn ap(self) -> serde_json::Value { - serde_json::Value::new_object() + apb::new() .set_id(Some(&self.id)) .set_activity_type(Some(self.activity_type)) .set_actor(apb::Node::link(self.actor)) diff --git a/src/model/actor.rs b/upub/core/src/model/actor.rs similarity index 96% rename from src/model/actor.rs rename to upub/core/src/model/actor.rs index 92f2ed59..47853e3f 100644 --- a/src/model/actor.rs +++ b/upub/core/src/model/actor.rs @@ -2,7 +2,7 @@ use sea_orm::{entity::prelude::*, QuerySelect, SelectColumns}; use apb::{Actor, ActorMut, ActorType, BaseMut, DocumentMut, Endpoints, EndpointsMut, Object, ObjectMut, PublicKey, PublicKeyMut}; -use crate::{errors::UpubError, routes::activitypub::jsonld::LD}; +use crate::errors::UpubError; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[sea_orm(table_name = "actors")] @@ -193,18 +193,18 @@ impl ActiveModel { impl Model { pub fn ap(self) -> serde_json::Value { - serde_json::Value::new_object() + apb::new() .set_id(Some(&self.id)) .set_actor_type(Some(self.actor_type)) .set_name(self.name.as_deref()) .set_summary(self.summary.as_deref()) .set_icon(apb::Node::maybe_object(self.icon.map(|i| - serde_json::Value::new_object() + apb::new() .set_document_type(Some(apb::DocumentType::Image)) .set_url(apb::Node::link(i.clone())) ))) .set_image(apb::Node::maybe_object(self.image.map(|i| - serde_json::Value::new_object() + apb::new() .set_document_type(Some(apb::DocumentType::Image)) .set_url(apb::Node::link(i.clone())) ))) @@ -218,13 +218,13 @@ impl Model { .set_following(apb::Node::maybe_link(self.following)) .set_followers(apb::Node::maybe_link(self.followers)) .set_public_key(apb::Node::object( - serde_json::Value::new_object() + apb::new() .set_id(Some(&format!("{}#main-key", self.id))) .set_owner(Some(&self.id)) .set_public_key_pem(&self.public_key) )) .set_endpoints(apb::Node::object( - serde_json::Value::new_object() + apb::new() .set_shared_inbox(self.shared_inbox.as_deref()) )) .set_discoverable(Some(true)) diff --git a/src/model/addressing.rs b/upub/core/src/model/addressing.rs similarity index 96% rename from src/model/addressing.rs rename to upub/core/src/model/addressing.rs index 8bbd139c..0c2b295e 100644 --- a/src/model/addressing.rs +++ b/upub/core/src/model/addressing.rs @@ -1,8 +1,6 @@ use apb::{ActivityMut, ObjectMut}; use sea_orm::{entity::prelude::*, sea_query::IntoCondition, Condition, FromQueryResult, Iterable, Order, QueryOrder, QuerySelect, SelectColumns}; -use crate::routes::activitypub::jsonld::LD; - #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[sea_orm(table_name = "addressing")] pub struct Model { @@ -121,17 +119,17 @@ impl Event { .set_attachment(attachment) .set_liked_by_me(if liked.is_some() { Some(true) } else { None }) )), - Event::StrayObject { object, liked } => serde_json::Value::new_object() + Event::StrayObject { object, liked } => apb::new() .set_activity_type(Some(apb::ActivityType::Activity)) .set_object(apb::Node::object( object.ap() .set_attachment(attachment) .set_liked_by_me(if liked.is_some() { Some(true) } else { None }) )), - Event::Tombstone => serde_json::Value::new_object() + Event::Tombstone => apb::new() .set_activity_type(Some(apb::ActivityType::Activity)) .set_object(apb::Node::object( - serde_json::Value::new_object() + apb::new() .set_object_type(Some(apb::ObjectType::Tombstone)) )), } diff --git a/src/model/announce.rs b/upub/core/src/model/announce.rs similarity index 100% rename from src/model/announce.rs rename to upub/core/src/model/announce.rs diff --git a/src/model/attachment.rs b/upub/core/src/model/attachment.rs similarity index 97% rename from src/model/attachment.rs rename to upub/core/src/model/attachment.rs index 3956819d..19077f38 100644 --- a/src/model/attachment.rs +++ b/upub/core/src/model/attachment.rs @@ -1,8 +1,6 @@ use apb::{DocumentMut, DocumentType, ObjectMut}; use sea_orm::entity::prelude::*; -use crate::routes::activitypub::jsonld::LD; - use super::addressing::Event; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] @@ -41,7 +39,7 @@ impl ActiveModelBehavior for ActiveModel {} impl Model { pub fn ap(self) -> serde_json::Value { - serde_json::Value::new_object() + apb::new() .set_url(apb::Node::link(self.url)) .set_document_type(Some(self.document_type)) .set_media_type(Some(&self.media_type)) diff --git a/src/model/config.rs b/upub/core/src/model/config.rs similarity index 100% rename from src/model/config.rs rename to upub/core/src/model/config.rs diff --git a/src/model/credential.rs b/upub/core/src/model/credential.rs similarity index 100% rename from src/model/credential.rs rename to upub/core/src/model/credential.rs diff --git a/src/model/delivery.rs b/upub/core/src/model/delivery.rs similarity index 100% rename from src/model/delivery.rs rename to upub/core/src/model/delivery.rs diff --git a/src/model/hashtag.rs b/upub/core/src/model/hashtag.rs similarity index 100% rename from src/model/hashtag.rs rename to upub/core/src/model/hashtag.rs diff --git a/src/model/instance.rs b/upub/core/src/model/instance.rs similarity index 100% rename from src/model/instance.rs rename to upub/core/src/model/instance.rs diff --git a/src/model/like.rs b/upub/core/src/model/like.rs similarity index 100% rename from src/model/like.rs rename to upub/core/src/model/like.rs diff --git a/src/model/mention.rs b/upub/core/src/model/mention.rs similarity index 100% rename from src/model/mention.rs rename to upub/core/src/model/mention.rs diff --git a/src/model/mod.rs b/upub/core/src/model/mod.rs similarity index 100% rename from src/model/mod.rs rename to upub/core/src/model/mod.rs diff --git a/src/model/object.rs b/upub/core/src/model/object.rs similarity index 96% rename from src/model/object.rs rename to upub/core/src/model/object.rs index 4f296376..2c97b767 100644 --- a/src/model/object.rs +++ b/upub/core/src/model/object.rs @@ -1,7 +1,7 @@ use apb::{BaseMut, Collection, CollectionMut, ObjectMut, ObjectType}; use sea_orm::{entity::prelude::*, QuerySelect, SelectColumns}; -use crate::{errors::UpubError, routes::activitypub::jsonld::LD}; +use crate::errors::UpubError; use super::Audience; @@ -185,7 +185,7 @@ impl ActiveModel { impl Model { pub fn ap(self) -> serde_json::Value { - serde_json::Value::new_object() + apb::new() .set_id(Some(&self.id)) .set_object_type(Some(self.object_type)) .set_attributed_to(apb::Node::maybe_link(self.attributed_to)) @@ -204,17 +204,17 @@ impl Model { .set_url(apb::Node::maybe_link(self.url)) .set_sensitive(Some(self.sensitive)) .set_shares(apb::Node::object( - serde_json::Value::new_object() + apb::new() .set_collection_type(Some(apb::CollectionType::OrderedCollection)) .set_total_items(Some(self.announces as u64)) )) .set_likes(apb::Node::object( - serde_json::Value::new_object() + apb::new() .set_collection_type(Some(apb::CollectionType::OrderedCollection)) .set_total_items(Some(self.likes as u64)) )) .set_replies(apb::Node::object( - serde_json::Value::new_object() + apb::new() .set_collection_type(Some(apb::CollectionType::OrderedCollection)) .set_total_items(Some(self.replies as u64)) )) diff --git a/src/model/relation.rs b/upub/core/src/model/relation.rs similarity index 100% rename from src/model/relation.rs rename to upub/core/src/model/relation.rs diff --git a/src/model/session.rs b/upub/core/src/model/session.rs similarity index 100% rename from src/model/session.rs rename to upub/core/src/model/session.rs diff --git a/src/server/addresser.rs b/upub/core/src/server/addresser.rs similarity index 100% rename from src/server/addresser.rs rename to upub/core/src/server/addresser.rs diff --git a/src/server/admin.rs b/upub/core/src/server/admin.rs similarity index 100% rename from src/server/admin.rs rename to upub/core/src/server/admin.rs diff --git a/src/server/auth.rs b/upub/core/src/server/auth.rs similarity index 100% rename from src/server/auth.rs rename to upub/core/src/server/auth.rs diff --git a/src/server/context.rs b/upub/core/src/server/context.rs similarity index 97% rename from src/server/context.rs rename to upub/core/src/server/context.rs index 7e1796f1..fec21353 100644 --- a/src/server/context.rs +++ b/upub/core/src/server/context.rs @@ -2,10 +2,10 @@ use std::{collections::BTreeSet, sync::Arc}; use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, QuerySelect, SelectColumns}; -use crate::{config::Config, errors::UpubError, model}; +use crate::{config::Config, errors::UpubError, model, ext::AnyQuery}; use uriproxy::UriClass; -use super::{builders::AnyQuery, dispatcher::Dispatcher}; +use super::dispatcher::Dispatcher; #[derive(Clone)] diff --git a/src/server/dispatcher.rs b/upub/core/src/server/dispatcher.rs similarity index 97% rename from src/server/dispatcher.rs rename to upub/core/src/server/dispatcher.rs index e24620c0..6885c4f0 100644 --- a/src/server/dispatcher.rs +++ b/upub/core/src/server/dispatcher.rs @@ -3,7 +3,7 @@ use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, Order, QueryFilter, use tokio::{sync::broadcast, task::JoinHandle}; use apb::{ActivityMut, Node}; -use crate::{model, routes::activitypub::jsonld::LD, server::{fetcher::Fetcher, Context}}; +use crate::{model, Context, server::{fetcher::Fetcher, jsonld::LD}}; pub struct Dispatcher { waker: broadcast::Sender<()>, diff --git a/src/server/fetcher.rs b/upub/core/src/server/fetcher.rs similarity index 100% rename from src/server/fetcher.rs rename to upub/core/src/server/fetcher.rs diff --git a/src/server/httpsign.rs b/upub/core/src/server/httpsign.rs similarity index 100% rename from src/server/httpsign.rs rename to upub/core/src/server/httpsign.rs diff --git a/src/server/inbox.rs b/upub/core/src/server/inbox.rs similarity index 99% rename from src/server/inbox.rs rename to upub/core/src/server/inbox.rs index 12f74b8c..f00b5eb7 100644 --- a/src/server/inbox.rs +++ b/upub/core/src/server/inbox.rs @@ -2,7 +2,7 @@ use apb::{target::Addressed, Activity, Base, Object}; use reqwest::StatusCode; use sea_orm::{sea_query::Expr, ActiveValue::{Set, NotSet}, ColumnTrait, EntityTrait, QueryFilter, QuerySelect, SelectColumns}; -use crate::{errors::{LoggableError, UpubError}, model, server::{addresser::Addresser, builders::AnyQuery, normalizer::Normalizer}}; +use crate::{errors::{LoggableError, UpubError}, model, ext::AnyQuery, server::{addresser::Addresser, normalizer::Normalizer}}; use super::{fetcher::{Fetcher, PullResult}, side_effects::SideEffects, Context}; diff --git a/src/server/init.rs b/upub/core/src/server/init.rs similarity index 100% rename from src/server/init.rs rename to upub/core/src/server/init.rs diff --git a/src/routes/activitypub/jsonld.rs b/upub/core/src/server/jsonld.rs similarity index 73% rename from src/routes/activitypub/jsonld.rs rename to upub/core/src/server/jsonld.rs index 023c2611..9d1c8b57 100644 --- a/src/routes/activitypub/jsonld.rs +++ b/upub/core/src/server/jsonld.rs @@ -1,16 +1,8 @@ -// TODO -// move this file somewhere else -// it's not a route -// maybe under src/server/jsonld.rs ?? - use apb::Object; -use axum::response::{IntoResponse, Response}; + pub trait LD { fn ld_context(self) -> Self; - fn new_object() -> serde_json::Value { - serde_json::Value::Object(serde_json::Map::default()) - } } impl LD for serde_json::Value { @@ -51,15 +43,3 @@ impl LD for serde_json::Value { self } } - -// got this from https://github.com/kitsune-soc/kitsune/blob/b023a12b687dd9a274233a5a9950f2de5e192344/kitsune/src/http/responder.rs -// i was trying to do it with middlewares but this is way cleaner -pub struct JsonLD(pub T); -impl IntoResponse for JsonLD { - fn into_response(self) -> Response { - ( - [("Content-Type", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"")], - axum::Json(self.0) - ).into_response() - } -} diff --git a/src/server/mod.rs b/upub/core/src/server/mod.rs similarity index 92% rename from src/server/mod.rs rename to upub/core/src/server/mod.rs index 1083e67f..07af4454 100644 --- a/src/server/mod.rs +++ b/upub/core/src/server/mod.rs @@ -7,9 +7,9 @@ pub mod inbox; pub mod init; pub mod outbox; pub mod auth; -pub mod builders; pub mod httpsign; pub mod normalizer; pub mod side_effects; +pub mod jsonld; pub use context::Context; diff --git a/src/server/normalizer.rs b/upub/core/src/server/normalizer.rs similarity index 100% rename from src/server/normalizer.rs rename to upub/core/src/server/normalizer.rs diff --git a/src/server/outbox.rs b/upub/core/src/server/outbox.rs similarity index 98% rename from src/server/outbox.rs rename to upub/core/src/server/outbox.rs index d3b55cdb..23c0150c 100644 --- a/src/server/outbox.rs +++ b/upub/core/src/server/outbox.rs @@ -2,9 +2,9 @@ use apb::{target::Addressed, Activity, ActivityMut, Base, BaseMut, Node, Object, use reqwest::StatusCode; use sea_orm::{sea_query::Expr, ActiveValue::{Set, NotSet, Unchanged}, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter, QuerySelect, SelectColumns}; -use crate::{errors::UpubError, model, routes::activitypub::jsonld::LD}; +use crate::{errors::UpubError, model, ext::AnyQuery}; -use super::{addresser::Addresser, builders::AnyQuery, fetcher::Fetcher, normalizer::Normalizer, side_effects::SideEffects, Context}; +use super::{addresser::Addresser, fetcher::Fetcher, normalizer::Normalizer, side_effects::SideEffects, Context}; #[axum::async_trait] @@ -16,7 +16,7 @@ impl apb::server::Outbox for Context { async fn create_note(&self, uid: String, object: serde_json::Value) -> crate::Result { self.create( uid, - serde_json::Value::new_object() + apb::new() .set_activity_type(Some(apb::ActivityType::Create)) .set_to(object.to()) .set_bto(object.bto()) diff --git a/src/server/server.rs b/upub/core/src/server/server.rs similarity index 100% rename from src/server/server.rs rename to upub/core/src/server/server.rs diff --git a/src/server/side_effects.rs b/upub/core/src/server/side_effects.rs similarity index 100% rename from src/server/side_effects.rs rename to upub/core/src/server/side_effects.rs diff --git a/upub/migrations/Cargo.toml b/upub/migrations/Cargo.toml new file mode 100644 index 00000000..56341c90 --- /dev/null +++ b/upub/migrations/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "upub-migrations" +version = "0.2.0" +edition = "2021" +authors = [ "alemi " ] +description = "database migrations for upub" +license = "AGPL-3.0" +repository = "https://git.alemi.dev/upub.git" +readme = "README.md" + +[lib] + +[dependencies] +sea-orm-migration = "0.12" diff --git a/upub/migrations/README.md b/upub/migrations/README.md new file mode 100644 index 00000000..0cb53668 --- /dev/null +++ b/upub/migrations/README.md @@ -0,0 +1 @@ +# upub migrations diff --git a/src/migrations/README.md b/upub/migrations/src/README.md similarity index 100% rename from src/migrations/README.md rename to upub/migrations/src/README.md diff --git a/src/migrations/mod.rs b/upub/migrations/src/lib.rs similarity index 95% rename from src/migrations/mod.rs rename to upub/migrations/src/lib.rs index a8831e1b..698b3e05 100644 --- a/src/migrations/mod.rs +++ b/upub/migrations/src/lib.rs @@ -22,3 +22,5 @@ impl MigratorTrait for Migrator { ] } } + +pub use sea_orm_migration::MigratorTrait; diff --git a/src/migrations/m20240524_000001_create_actor_activity_object_tables.rs b/upub/migrations/src/m20240524_000001_create_actor_activity_object_tables.rs similarity index 100% rename from src/migrations/m20240524_000001_create_actor_activity_object_tables.rs rename to upub/migrations/src/m20240524_000001_create_actor_activity_object_tables.rs diff --git a/src/migrations/m20240524_000002_create_relations_likes_shares.rs b/upub/migrations/src/m20240524_000002_create_relations_likes_shares.rs similarity index 100% rename from src/migrations/m20240524_000002_create_relations_likes_shares.rs rename to upub/migrations/src/m20240524_000002_create_relations_likes_shares.rs diff --git a/src/migrations/m20240524_000003_create_users_auth_and_config.rs b/upub/migrations/src/m20240524_000003_create_users_auth_and_config.rs similarity index 100% rename from src/migrations/m20240524_000003_create_users_auth_and_config.rs rename to upub/migrations/src/m20240524_000003_create_users_auth_and_config.rs diff --git a/src/migrations/m20240524_000004_create_addressing_deliveries.rs b/upub/migrations/src/m20240524_000004_create_addressing_deliveries.rs similarity index 100% rename from src/migrations/m20240524_000004_create_addressing_deliveries.rs rename to upub/migrations/src/m20240524_000004_create_addressing_deliveries.rs diff --git a/src/migrations/m20240524_000005_create_attachments_tags_mentions.rs b/upub/migrations/src/m20240524_000005_create_attachments_tags_mentions.rs similarity index 100% rename from src/migrations/m20240524_000005_create_attachments_tags_mentions.rs rename to upub/migrations/src/m20240524_000005_create_attachments_tags_mentions.rs diff --git a/src/migrations/m20240529_000001_add_relation_unique_index.rs b/upub/migrations/src/m20240529_000001_add_relation_unique_index.rs similarity index 100% rename from src/migrations/m20240529_000001_add_relation_unique_index.rs rename to upub/migrations/src/m20240529_000001_add_relation_unique_index.rs diff --git a/upub/routes/Cargo.toml b/upub/routes/Cargo.toml new file mode 100644 index 00000000..c8a95b19 --- /dev/null +++ b/upub/routes/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "upub-routes" +version = "0.2.0" +edition = "2021" +authors = [ "alemi " ] +description = "api route definitions for upub" +license = "AGPL-3.0" +repository = "https://git.alemi.dev/upub.git" +readme = "README.md" + +[lib] + +[dependencies] +rand = "0.8" +sha256 = "1.5" +chrono = { version = "0.4", features = ["serde"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +upub = { path = "../core/" } +jrd = "0.1" +tracing = "0.1" +tokio = { version = "1.35", features = ["full"] } # TODO slim this down +reqwest = { version = "0.12", features = ["json"] } +axum = "0.7" +tower-http = { version = "0.5", features = ["cors", "trace"] } +apb = { path = "../../apb", features = ["unstructured", "orm", "activitypub-fe", "activitypub-counters", "litepub", "ostatus", "toot"] } +sea-orm = { version = "0.12", features = ["macros", "sqlx-sqlite", "runtime-tokio-rustls"] } +# nodeinfo = "0.0.2" # the version on crates.io doesn't re-export necessary types to build the struct!!! +nodeinfo = { git = "https://codeberg.org/thefederationinfo/nodeinfo-rs", rev = "e865094804" } +# mastodon +mastodon-async-entities = { version = "1.1.0", optional = true } +time = { version = "0.3", features = ["serde"], optional = true } diff --git a/upub/routes/README.md b/upub/routes/README.md new file mode 100644 index 00000000..dfc2c1d8 --- /dev/null +++ b/upub/routes/README.md @@ -0,0 +1 @@ +# upub routes diff --git a/src/routes/activitypub/README.md b/upub/routes/src/activitypub/README.md similarity index 100% rename from src/routes/activitypub/README.md rename to upub/routes/src/activitypub/README.md diff --git a/src/routes/activitypub/activity.rs b/upub/routes/src/activitypub/activity.rs similarity index 69% rename from src/routes/activitypub/activity.rs rename to upub/routes/src/activitypub/activity.rs index a7880bcb..99393502 100644 --- a/src/routes/activitypub/activity.rs +++ b/upub/routes/src/activitypub/activity.rs @@ -1,20 +1,22 @@ use axum::extract::{Path, Query, State}; use sea_orm::{ColumnTrait, QueryFilter}; -use crate::{errors::UpubError, model::{self, addressing::Event, attachment::BatchFillable}, server::{auth::AuthIdentity, fetcher::Fetcher, Context}}; +use upub::{model::{self, addressing::Event, attachment::BatchFillable}, server::{auth::AuthIdentity, fetcher::Fetcher, jsonld::LD}, Context}; -use super::{jsonld::LD, JsonLD, TryFetch}; +use crate::builders::JsonLD; + +use super::TryFetch; pub async fn view( State(ctx): State, Path(id): Path, AuthIdentity(auth): AuthIdentity, Query(query): Query, -) -> crate::Result> { +) -> upub::Result> { let aid = ctx.aid(&id); if auth.is_local() && query.fetch && !ctx.is_local(&aid) { let obj = ctx.fetch_activity(&aid).await?; if obj.id != aid { - return Err(UpubError::Redirect(obj.id)); + return Err(upub::Error::Redirect(obj.id)); } } @@ -24,7 +26,7 @@ pub async fn view( .into_model::() .one(ctx.db()) .await? - .ok_or_else(UpubError::not_found)?; + .ok_or_else(upub::Error::not_found)?; let mut attachments = row.load_attachments_batch(ctx.db()).await?; let attach = attachments.remove(&row.internal()); diff --git a/src/routes/activitypub/application.rs b/upub/routes/src/activitypub/application.rs similarity index 74% rename from src/routes/activitypub/application.rs rename to upub/routes/src/activitypub/application.rs index fdadcd28..6b9d8d64 100644 --- a/src/routes/activitypub/application.rs +++ b/upub/routes/src/activitypub/application.rs @@ -1,16 +1,15 @@ use apb::{ActorMut, BaseMut, ObjectMut, PublicKeyMut}; use axum::{extract::{Query, State}, http::HeaderMap, response::{IntoResponse, Redirect, Response}, Form, Json}; use reqwest::Method; +use upub::{server::{auth::AuthIdentity, fetcher::Fetcher, jsonld::LD}, Context}; -use crate::{errors::UpubError, server::{auth::AuthIdentity, fetcher::Fetcher, Context}, url}; - -use super::{jsonld::LD, JsonLD}; +use crate::builders::JsonLD; pub async fn view( headers: HeaderMap, State(ctx): State, -) -> crate::Result { +) -> upub::Result { if let Some(accept) = headers.get("Accept") { if let Ok(accept) = accept.to_str() { if accept.contains("text/html") && !accept.contains("application/ld+json") { @@ -19,20 +18,20 @@ pub async fn view( } } Ok(JsonLD( - serde_json::Value::new_object() - .set_id(Some(&url!(ctx, ""))) + apb::new() + .set_id(Some(&upub::url!(ctx, ""))) .set_actor_type(Some(apb::ActorType::Application)) .set_name(Some(&ctx.cfg().instance.name)) .set_summary(Some(&ctx.cfg().instance.description)) - .set_inbox(apb::Node::link(url!(ctx, "/inbox"))) - .set_outbox(apb::Node::link(url!(ctx, "/outbox"))) + .set_inbox(apb::Node::link(upub::url!(ctx, "/inbox"))) + .set_outbox(apb::Node::link(upub::url!(ctx, "/outbox"))) .set_published(Some(ctx.actor().published)) .set_endpoints(apb::Node::Empty) .set_preferred_username(Some(ctx.domain())) .set_public_key(apb::Node::object( - serde_json::Value::new_object() - .set_id(Some(&url!(ctx, "#main-key"))) - .set_owner(Some(&url!(ctx, ""))) + apb::new() + .set_id(Some(&upub::url!(ctx, "#main-key"))) + .set_owner(Some(&upub::url!(ctx, ""))) .set_public_key_pem(&ctx.actor().public_key) )) .ld_context() @@ -48,10 +47,10 @@ pub async fn proxy_get( State(ctx): State, Query(query): Query, AuthIdentity(auth): AuthIdentity, -) -> crate::Result> { +) -> upub::Result> { // only local users can request fetches if !ctx.cfg().security.allow_public_debugger && !auth.is_local() { - return Err(UpubError::unauthorized()); + return Err(upub::Error::unauthorized()); } Ok(Json( Context::request( @@ -72,10 +71,10 @@ pub async fn proxy_form( State(ctx): State, AuthIdentity(auth): AuthIdentity, Form(query): Form, -) -> crate::Result> { +) -> upub::Result> { // only local users can request fetches if !ctx.cfg().security.allow_public_debugger && auth.is_local() { - return Err(UpubError::unauthorized()); + return Err(upub::Error::unauthorized()); } Ok(Json( Context::request( diff --git a/src/routes/activitypub/auth.rs b/upub/routes/src/activitypub/auth.rs similarity index 75% rename from src/routes/activitypub/auth.rs rename to upub/routes/src/activitypub/auth.rs index 76dd5a8c..7dfece4d 100644 --- a/src/routes/activitypub/auth.rs +++ b/upub/routes/src/activitypub/auth.rs @@ -1,8 +1,7 @@ use axum::{http::StatusCode, extract::State, Json}; use rand::Rng; use sea_orm::{ActiveValue::{Set, NotSet}, ColumnTrait, Condition, EntityTrait, QueryFilter}; - -use crate::{errors::UpubError, model, server::{admin::Administrable, Context}}; +use upub::{server::admin::Administrable, Context}; #[derive(Debug, Clone, serde::Deserialize)] @@ -30,12 +29,12 @@ fn token() -> String { pub async fn login( State(ctx): State, Json(login): Json -) -> crate::Result> { +) -> upub::Result> { // TODO salt the pwd - match model::credential::Entity::find() + match upub::model::credential::Entity::find() .filter(Condition::all() - .add(model::credential::Column::Login.eq(login.email)) - .add(model::credential::Column::Password.eq(sha256::digest(login.password))) + .add(upub::model::credential::Column::Login.eq(login.email)) + .add(upub::model::credential::Column::Password.eq(sha256::digest(login.password))) ) .one(ctx.db()) .await? @@ -43,8 +42,8 @@ pub async fn login( Some(x) => { let token = token(); let expires = chrono::Utc::now() + std::time::Duration::from_secs(3600 * 6); - model::session::Entity::insert( - model::session::ActiveModel { + upub::model::session::Entity::insert( + upub::model::session::ActiveModel { internal: sea_orm::ActiveValue::NotSet, secret: sea_orm::ActiveValue::Set(token.clone()), actor: sea_orm::ActiveValue::Set(x.actor.clone()), @@ -58,7 +57,7 @@ pub async fn login( user: x.actor })) }, - None => Err(UpubError::unauthorized()), + None => Err(upub::Error::unauthorized()), } } @@ -70,16 +69,16 @@ pub struct RefreshForm { pub async fn refresh( State(ctx): State, Json(login): Json -) -> crate::Result> { +) -> upub::Result> { if !ctx.cfg().security.allow_login_refresh { - return Err(UpubError::forbidden()); + return Err(upub::Error::forbidden()); } - let prev = model::session::Entity::find() - .filter(model::session::Column::Secret.eq(login.token)) + let prev = upub::model::session::Entity::find() + .filter(upub::model::session::Column::Secret.eq(login.token)) .one(ctx.db()) .await? - .ok_or_else(UpubError::unauthorized)?; + .ok_or_else(upub::Error::unauthorized)?; if prev.expires > chrono::Utc::now() { return Ok(Json(AuthSuccess { token: prev.secret, user: prev.actor, expires: prev.expires })); @@ -88,13 +87,13 @@ pub async fn refresh( let token = token(); let expires = chrono::Utc::now() + std::time::Duration::from_secs(3600 * 6); let user = prev.actor; - let new_session = model::session::ActiveModel { + let new_session = upub::model::session::ActiveModel { internal: NotSet, actor: Set(user.clone()), secret: Set(token.clone()), expires: Set(expires), }; - model::session::Entity::insert(new_session) + upub::model::session::Entity::insert(new_session) .exec(ctx.db()) .await?; @@ -114,9 +113,9 @@ pub struct RegisterForm { pub async fn register( State(ctx): State, Json(registration): Json -) -> crate::Result> { +) -> upub::Result> { if !ctx.cfg().security.allow_registration { - return Err(UpubError::forbidden()); + return Err(upub::Error::forbidden()); } ctx.register_user( diff --git a/src/routes/activitypub/context.rs b/upub/routes/src/activitypub/context.rs similarity index 66% rename from src/routes/activitypub/context.rs rename to upub/routes/src/activitypub/context.rs index 360d21fe..5f7936e1 100644 --- a/src/routes/activitypub/context.rs +++ b/upub/routes/src/activitypub/context.rs @@ -1,14 +1,17 @@ use axum::extract::{Path, Query, State}; use sea_orm::{ColumnTrait, Condition, PaginatorTrait, QueryFilter}; +use upub::{model, server::auth::AuthIdentity, Context}; -use crate::{model, routes::activitypub::{JsonLD, Pagination}, server::{auth::AuthIdentity, Context}, url}; +use crate::builders::JsonLD; + +use super::Pagination; pub async fn get( State(ctx): State, Path(id): Path, AuthIdentity(auth): AuthIdentity, -) -> crate::Result> { - let local_context_id = url!(ctx, "/context/{id}"); +) -> upub::Result> { + let local_context_id = upub::url!(ctx, "/context/{id}"); let context = ctx.oid(&id); let count = model::addressing::Entity::find_addressed(auth.my_id()) @@ -17,7 +20,7 @@ pub async fn get( .count(ctx.db()) .await?; - crate::server::builders::collection(&local_context_id, Some(count)) + crate::builders::collection(&local_context_id, Some(count)) } pub async fn page( @@ -25,11 +28,11 @@ pub async fn page( Path(id): Path, Query(page): Query, AuthIdentity(auth): AuthIdentity, -) -> crate::Result> { +) -> upub::Result> { let context = ctx.oid(&id); - crate::server::builders::paginate( - url!(ctx, "/context/{id}/page"), + crate::builders::paginate( + upub::url!(ctx, "/context/{id}/page"), Condition::all() .add(auth.filter_condition()) .add(model::object::Column::Context.eq(context)), diff --git a/src/routes/activitypub/inbox.rs b/upub/routes/src/activitypub/inbox.rs similarity index 80% rename from src/routes/activitypub/inbox.rs rename to upub/routes/src/activitypub/inbox.rs index adb1eb38..f3365f38 100644 --- a/src/routes/activitypub/inbox.rs +++ b/upub/routes/src/activitypub/inbox.rs @@ -1,26 +1,27 @@ use apb::{server::Inbox, Activity, ActivityType}; use axum::{extract::{Query, State}, http::StatusCode, Json}; use sea_orm::{sea_query::IntoCondition, ColumnTrait}; +use upub::{server::auth::{AuthIdentity, Identity}, Context}; -use crate::{errors::UpubError, server::{auth::{AuthIdentity, Identity}, Context}, url}; +use crate::builders::JsonLD; -use super::{JsonLD, Pagination}; +use super::Pagination; pub async fn get( State(ctx): State, -) -> crate::Result> { - crate::server::builders::collection(&url!(ctx, "/inbox"), None) +) -> upub::Result> { + crate::builders::collection(&upub::url!(ctx, "/inbox"), None) } pub async fn page( State(ctx): State, AuthIdentity(auth): AuthIdentity, Query(page): Query, -) -> crate::Result> { - crate::server::builders::paginate( - url!(ctx, "/inbox/page"), - crate::model::addressing::Column::Actor.is_null() +) -> upub::Result> { + crate::builders::paginate( + upub::url!(ctx, "/inbox/page"), + upub::model::addressing::Column::Actor.is_null() .into_condition(), ctx.db(), page, @@ -41,7 +42,7 @@ pub async fn post( State(ctx): State, AuthIdentity(auth): AuthIdentity, Json(activity): Json -) -> crate::Result<()> { +) -> upub::Result<()> { let Identity::Remote { domain: server, .. } = auth else { if activity.activity_type() == Some(ActivityType::Delete) { // this is spammy af, ignore them! @@ -54,24 +55,24 @@ pub async fn post( } tracing::warn!("refusing unauthorized activity: {}", pretty_json!(activity)); if matches!(auth, Identity::Anonymous) { - return Err(UpubError::unauthorized()); + return Err(upub::Error::unauthorized()); } else { - return Err(UpubError::forbidden()); + return Err(upub::Error::forbidden()); } }; let Some(actor) = activity.actor().id() else { - return Err(UpubError::bad_request()); + return Err(upub::Error::bad_request()); }; if server != Context::server(&actor) { - return Err(UpubError::unauthorized()); + return Err(upub::Error::unauthorized()); } tracing::debug!("processing federated activity: '{}'", serde_json::to_string(&activity).unwrap_or_default()); // TODO we could process Links and bare Objects maybe, but probably out of AP spec? - match activity.activity_type().ok_or_else(UpubError::bad_request)? { + match activity.activity_type().ok_or_else(upub::Error::bad_request)? { ActivityType::Activity => { tracing::warn!("skipping unprocessable base activity: {}", pretty_json!(activity)); Err(StatusCode::UNPROCESSABLE_ENTITY.into()) // won't ingest useless stuff diff --git a/src/routes/activitypub/mod.rs b/upub/routes/src/activitypub/mod.rs similarity index 95% rename from src/routes/activitypub/mod.rs rename to upub/routes/src/activitypub/mod.rs index 808a47e0..11dbc47e 100644 --- a/src/routes/activitypub/mod.rs +++ b/upub/routes/src/activitypub/mod.rs @@ -8,18 +8,15 @@ pub mod application; pub mod auth; pub mod well_known; -pub mod jsonld; -pub use jsonld::JsonLD; - use axum::{http::StatusCode, response::IntoResponse, routing::{get, patch, post, put}, Router}; pub trait ActivityPubRouter { fn ap_routes(self) -> Self; } -impl ActivityPubRouter for Router { +impl ActivityPubRouter for Router { fn ap_routes(self) -> Self { - use crate::routes::activitypub as ap; // TODO use self ? + use crate::activitypub as ap; // TODO use self ? self // core server inbox/outbox, maybe for feeds? TODO do we need these? diff --git a/src/routes/activitypub/object/mod.rs b/upub/routes/src/activitypub/object/mod.rs similarity index 73% rename from src/routes/activitypub/object/mod.rs rename to upub/routes/src/activitypub/object/mod.rs index 445dd29c..ffb687bd 100644 --- a/src/routes/activitypub/object/mod.rs +++ b/upub/routes/src/activitypub/object/mod.rs @@ -3,23 +3,24 @@ pub mod replies; use apb::{CollectionMut, ObjectMut}; use axum::extract::{Path, Query, State}; use sea_orm::{ColumnTrait, ModelTrait, QueryFilter, QuerySelect, SelectColumns}; +use upub::{model::{self, addressing::Event}, server::{auth::AuthIdentity, fetcher::Fetcher, jsonld::LD}, Context}; -use crate::{errors::UpubError, model::{self, addressing::Event}, server::{auth::AuthIdentity, fetcher::Fetcher, Context}}; +use crate::builders::JsonLD; -use super::{jsonld::LD, JsonLD, TryFetch}; +use super::TryFetch; pub async fn view( State(ctx): State, Path(id): Path, AuthIdentity(auth): AuthIdentity, Query(query): Query, -) -> crate::Result> { +) -> upub::Result> { let oid = ctx.oid(&id); if auth.is_local() && query.fetch && !ctx.is_local(&oid) { let obj = ctx.fetch_object(&oid).await?; // some implementations serve statuses on different urls than their AP id if obj.id != oid { - return Err(UpubError::Redirect(crate::url!(ctx, "/objects/{}", ctx.id(&obj.id)))); + return Err(upub::Error::Redirect(upub::url!(ctx, "/objects/{}", ctx.id(&obj.id)))); } } @@ -29,11 +30,11 @@ pub async fn view( .into_model::() .one(ctx.db()) .await? - .ok_or_else(UpubError::not_found)?; + .ok_or_else(upub::Error::not_found)?; let object = match item { - Event::Tombstone => return Err(UpubError::not_found()), - Event::Activity(_) => return Err(UpubError::not_found()), + Event::Tombstone => return Err(upub::Error::not_found()), + Event::Activity(_) => return Err(upub::Error::not_found()), Event::StrayObject { liked: _, object } => object, Event::DeepActivity { activity: _, liked: _, object } => object, }; @@ -58,9 +59,9 @@ pub async fn view( .await?; replies = apb::Node::object( - serde_json::Value::new_object() - // .set_id(Some(&crate::url!(ctx, "/objects/{id}/replies"))) - // .set_first(apb::Node::link(crate::url!(ctx, "/objects/{id}/replies/page"))) + apb::new() + // .set_id(Some(&upub::url!(ctx, "/objects/{id}/replies"))) + // .set_first(apb::Node::link(upub::url!(ctx, "/objects/{id}/replies/page"))) .set_collection_type(Some(apb::CollectionType::Collection)) .set_total_items(Some(object.replies as u64)) .set_items(apb::Node::links(replies_ids)) diff --git a/src/routes/activitypub/object/replies.rs b/upub/routes/src/activitypub/object/replies.rs similarity index 66% rename from src/routes/activitypub/object/replies.rs rename to upub/routes/src/activitypub/object/replies.rs index b9514952..e8b16b12 100644 --- a/src/routes/activitypub/object/replies.rs +++ b/upub/routes/src/activitypub/object/replies.rs @@ -1,15 +1,16 @@ use axum::extract::{Path, Query, State}; use sea_orm::{ColumnTrait, Condition, PaginatorTrait, QueryFilter}; +use upub::{model, server::{auth::AuthIdentity, fetcher::Fetcher}, Context}; -use crate::{model, routes::activitypub::{JsonLD, Pagination, TryFetch}, server::{auth::AuthIdentity, fetcher::Fetcher, Context}, url}; +use crate::{activitypub::{Pagination, TryFetch}, builders::JsonLD}; pub async fn get( State(ctx): State, Path(id): Path, AuthIdentity(auth): AuthIdentity, Query(q): Query, -) -> crate::Result> { - let replies_id = url!(ctx, "/objects/{id}/replies"); +) -> upub::Result> { + let replies_id = upub::url!(ctx, "/objects/{id}/replies"); let oid = ctx.oid(&id); if auth.is_local() && q.fetch { @@ -22,7 +23,7 @@ pub async fn get( .count(ctx.db()) .await?; - crate::server::builders::collection(&replies_id, Some(count)) + crate::builders::collection(&replies_id, Some(count)) } pub async fn page( @@ -30,11 +31,11 @@ pub async fn page( Path(id): Path, Query(page): Query, AuthIdentity(auth): AuthIdentity, -) -> crate::Result> { - let page_id = url!(ctx, "/objects/{id}/replies/page"); +) -> upub::Result> { + let page_id = upub::url!(ctx, "/objects/{id}/replies/page"); let oid = ctx.oid(&id); - crate::server::builders::paginate( + crate::builders::paginate( page_id, Condition::all() .add(auth.filter_condition()) diff --git a/src/routes/activitypub/outbox.rs b/upub/routes/src/activitypub/outbox.rs similarity index 52% rename from src/routes/activitypub/outbox.rs rename to upub/routes/src/activitypub/outbox.rs index 3abd0b33..1c812034 100644 --- a/src/routes/activitypub/outbox.rs +++ b/upub/routes/src/activitypub/outbox.rs @@ -1,22 +1,23 @@ use axum::{extract::{Query, State}, http::StatusCode, Json}; use sea_orm::{ColumnTrait, Condition}; +use upub::{server::auth::AuthIdentity, Context}; -use crate::{errors::UpubError, routes::activitypub::{CreationResult, JsonLD, Pagination}, server::{auth::AuthIdentity, Context}, url}; +use crate::{activitypub::{CreationResult, Pagination}, builders::JsonLD}; -pub async fn get(State(ctx): State) -> crate::Result> { - crate::server::builders::collection(&url!(ctx, "/outbox"), None) +pub async fn get(State(ctx): State) -> upub::Result> { + crate::builders::collection(&upub::url!(ctx, "/outbox"), None) } pub async fn page( State(ctx): State, Query(page): Query, AuthIdentity(auth): AuthIdentity, -) -> crate::Result> { - crate::server::builders::paginate( - url!(ctx, "/outbox/page"), +) -> upub::Result> { + crate::builders::paginate( + upub::url!(ctx, "/outbox/page"), Condition::all() .add(auth.filter_condition()) - .add(crate::model::actor::Column::Domain.eq(ctx.domain().to_string())), + .add(upub::model::actor::Column::Domain.eq(ctx.domain().to_string())), ctx.db(), page, auth.my_id(), @@ -29,7 +30,7 @@ pub async fn post( State(_ctx): State, AuthIdentity(_auth): AuthIdentity, Json(_activity): Json, -) -> Result { +) -> upub::Result { // TODO administrative actions may be carried out against this outbox? Err(StatusCode::NOT_IMPLEMENTED.into()) } diff --git a/src/routes/activitypub/user/following.rs b/upub/routes/src/activitypub/user/following.rs similarity index 72% rename from src/routes/activitypub/user/following.rs rename to upub/routes/src/activitypub/user/following.rs index ae3bcb3b..57184dd8 100644 --- a/src/routes/activitypub/user/following.rs +++ b/upub/routes/src/activitypub/user/following.rs @@ -1,15 +1,16 @@ use axum::extract::{Path, Query, State}; use sea_orm::{ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QuerySelect, SelectColumns}; -use crate::{routes::activitypub::{JsonLD, Pagination}, model, server::Context, url}; +use upub::{model, Context}; -use model::relation::Column::{Following, Follower}; +use crate::{activitypub::Pagination, builders::JsonLD}; pub async fn get( State(ctx): State, Path(id): Path, -) -> crate::Result> { +) -> upub::Result> { let follow___ = if OUTGOING { "following" } else { "followers" }; + use upub::model::relation::Column::{Follower, Following}; let count = model::relation::Entity::find() .filter(if OUTGOING { Follower } else { Following }.eq(ctx.uid(&id))) .count(ctx.db()).await.unwrap_or_else(|e| { @@ -17,18 +18,19 @@ pub async fn get( 0 }); - crate::server::builders::collection(&url!(ctx, "/actors/{id}/{follow___}"), Some(count)) + crate::builders::collection(&upub::url!(ctx, "/actors/{id}/{follow___}"), Some(count)) } pub async fn page( State(ctx): State, Path(id): Path, Query(page): Query, -) -> crate::Result> { +) -> upub::Result> { let follow___ = if OUTGOING { "following" } else { "followers" }; let limit = page.batch.unwrap_or(20).min(50); let offset = page.offset.unwrap_or(0); + use upub::model::relation::Column::{Follower, Following}; let following = model::relation::Entity::find() .filter(if OUTGOING { Follower } else { Following }.eq(ctx.uid(&id))) .select_only() @@ -39,8 +41,8 @@ pub async fn page( .all(ctx.db()) .await?; - crate::server::builders::collection_page( - &url!(ctx, "/actors/{id}/{follow___}/page"), + crate::builders::collection_page( + &upub::url!(ctx, "/actors/{id}/{follow___}/page"), offset, limit, following.into_iter().map(serde_json::Value::String).collect() ) diff --git a/src/routes/activitypub/user/inbox.rs b/upub/routes/src/activitypub/user/inbox.rs similarity index 72% rename from src/routes/activitypub/user/inbox.rs rename to upub/routes/src/activitypub/user/inbox.rs index 07e9849d..2278e56f 100644 --- a/src/routes/activitypub/user/inbox.rs +++ b/upub/routes/src/activitypub/user/inbox.rs @@ -1,18 +1,20 @@ use axum::{extract::{Path, Query, State}, http::StatusCode, Json}; use sea_orm::{ColumnTrait, Condition}; -use crate::{errors::UpubError, model, routes::activitypub::{JsonLD, Pagination}, server::{auth::{AuthIdentity, Identity}, Context}, url}; +use upub::{model, server::auth::{AuthIdentity, Identity}, Context}; + +use crate::{activitypub::Pagination, builders::JsonLD}; pub async fn get( State(ctx): State, Path(id): Path, AuthIdentity(auth): AuthIdentity, -) -> crate::Result> { +) -> upub::Result> { match auth { Identity::Anonymous => Err(StatusCode::FORBIDDEN.into()), Identity::Remote { .. } => Err(StatusCode::FORBIDDEN.into()), Identity::Local { id: user, .. } => if ctx.uid(&id) == user { - crate::server::builders::collection(&url!(ctx, "/actors/{id}/inbox"), None) + crate::builders::collection(&upub::url!(ctx, "/actors/{id}/inbox"), None) } else { Err(StatusCode::FORBIDDEN.into()) }, @@ -24,17 +26,17 @@ pub async fn page( Path(id): Path, AuthIdentity(auth): AuthIdentity, Query(page): Query, -) -> crate::Result> { +) -> upub::Result> { let Identity::Local { id: uid, internal } = &auth else { // local inbox is only for local users - return Err(UpubError::forbidden()); + return Err(upub::Error::forbidden()); }; if uid != &ctx.uid(&id) { - return Err(UpubError::forbidden()); + return Err(upub::Error::forbidden()); } - crate::server::builders::paginate( - url!(ctx, "/actors/{id}/inbox/page"), + crate::builders::paginate( + upub::url!(ctx, "/actors/{id}/inbox/page"), Condition::any() .add(model::addressing::Column::Actor.eq(*internal)) .add(model::object::Column::AttributedTo.eq(uid)) @@ -52,7 +54,7 @@ pub async fn post( Path(_id): Path, AuthIdentity(_auth): AuthIdentity, Json(activity): Json, -) -> Result<(), UpubError> { +) -> Result<(), upub::Error> { // POSTing to user inboxes is effectively the same as POSTing to the main inbox super::super::inbox::post(State(ctx), AuthIdentity(_auth), Json(activity)).await } diff --git a/src/routes/activitypub/user/mod.rs b/upub/routes/src/activitypub/user/mod.rs similarity index 76% rename from src/routes/activitypub/user/mod.rs rename to upub/routes/src/activitypub/user/mod.rs index 4dfac26d..091d82ee 100644 --- a/src/routes/activitypub/user/mod.rs +++ b/upub/routes/src/activitypub/user/mod.rs @@ -7,9 +7,11 @@ pub mod following; use axum::extract::{Path, Query, State}; use apb::{ActorMut, EndpointsMut, Node, ObjectMut}; -use crate::{errors::UpubError, model, server::{auth::AuthIdentity, builders::AnyQuery, fetcher::Fetcher, Context}, url}; +use upub::{ext::AnyQuery, model, server::{auth::AuthIdentity, fetcher::Fetcher, jsonld::LD}, Context}; -use super::{jsonld::LD, JsonLD, TryFetch}; +use crate::builders::JsonLD; + +use super::TryFetch; pub async fn view( @@ -17,7 +19,7 @@ pub async fn view( AuthIdentity(auth): AuthIdentity, Path(id): Path, Query(query): Query, -) -> crate::Result> { +) -> upub::Result> { let mut uid = ctx.uid(&id); if auth.is_local() { if id.starts_with('@') { @@ -50,16 +52,16 @@ pub async fn view( // local user Some((user_model, Some(cfg))) => { let mut user = user_model.ap() - .set_inbox(Node::link(url!(ctx, "/actors/{id}/inbox"))) - .set_outbox(Node::link(url!(ctx, "/actors/{id}/outbox"))) - .set_following(Node::link(url!(ctx, "/actors/{id}/following"))) - .set_followers(Node::link(url!(ctx, "/actors/{id}/followers"))) + .set_inbox(Node::link(upub::url!(ctx, "/actors/{id}/inbox"))) + .set_outbox(Node::link(upub::url!(ctx, "/actors/{id}/outbox"))) + .set_following(Node::link(upub::url!(ctx, "/actors/{id}/following"))) + .set_followers(Node::link(upub::url!(ctx, "/actors/{id}/followers"))) .set_following_me(following_me) .set_followed_by_me(followed_by_me) .set_endpoints(Node::object( - serde_json::Value::new_object() - .set_shared_inbox(Some(&url!(ctx, "/inbox"))) - .set_proxy_url(Some(&url!(ctx, "/proxy"))) + apb::new() + .set_shared_inbox(Some(&upub::url!(ctx, "/inbox"))) + .set_proxy_url(Some(&upub::url!(ctx, "/proxy"))) )); if !auth.is(&uid) && !cfg.show_followers_count { @@ -83,7 +85,7 @@ pub async fn view( .set_followed_by_me(followed_by_me) .ld_context() )), - None => Err(UpubError::not_found()), + None => Err(upub::Error::not_found()), } } diff --git a/src/routes/activitypub/user/outbox.rs b/upub/routes/src/activitypub/user/outbox.rs similarity index 86% rename from src/routes/activitypub/user/outbox.rs rename to upub/routes/src/activitypub/user/outbox.rs index 057d142b..62b3d6eb 100644 --- a/src/routes/activitypub/user/outbox.rs +++ b/upub/routes/src/activitypub/user/outbox.rs @@ -2,13 +2,15 @@ use axum::{extract::{Path, Query, State}, http::StatusCode, Json}; use sea_orm::{ColumnTrait, Condition}; use apb::{server::Outbox, AcceptType, ActivityType, Base, BaseType, ObjectType, RejectType}; -use crate::{errors::UpubError, model, routes::activitypub::{CreationResult, JsonLD, Pagination}, server::{auth::{AuthIdentity, Identity}, Context}, url}; +use upub::{model, server::auth::{AuthIdentity, Identity}, Context}; + +use crate::{activitypub::{CreationResult, Pagination}, builders::JsonLD}; pub async fn get( State(ctx): State, Path(id): Path, -) -> crate::Result> { - crate::server::builders::collection(&url!(ctx, "/actors/{id}/outbox"), None) +) -> upub::Result> { + crate::builders::collection(&upub::url!(ctx, "/actors/{id}/outbox"), None) } pub async fn page( @@ -16,10 +18,10 @@ pub async fn page( Path(id): Path, Query(page): Query, AuthIdentity(auth): AuthIdentity, -) -> crate::Result> { +) -> upub::Result> { let uid = ctx.uid(&id); - crate::server::builders::paginate( - url!(ctx, "/actors/{id}/outbox/page"), + crate::builders::paginate( + upub::url!(ctx, "/actors/{id}/outbox/page"), Condition::all() .add(auth.filter_condition()) .add( @@ -40,7 +42,7 @@ pub async fn post( Path(id): Path, AuthIdentity(auth): AuthIdentity, Json(activity): Json, -) -> Result { +) -> upub::Result { match auth { Identity::Anonymous => Err(StatusCode::UNAUTHORIZED.into()), Identity::Remote { .. } => Err(StatusCode::NOT_IMPLEMENTED.into()), diff --git a/src/routes/activitypub/well_known.rs b/upub/routes/src/activitypub/well_known.rs similarity index 91% rename from src/routes/activitypub/well_known.rs rename to upub/routes/src/activitypub/well_known.rs index 4c493b7e..3d1477f0 100644 --- a/src/routes/activitypub/well_known.rs +++ b/upub/routes/src/activitypub/well_known.rs @@ -1,8 +1,7 @@ use axum::{extract::{Path, Query, State}, http::StatusCode, response::{IntoResponse, Response}, Json}; use jrd::{JsonResourceDescriptor, JsonResourceDescriptorLink}; use sea_orm::{ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter}; - -use crate::{errors::UpubError, model, server::Context, url, VERSION}; +use upub::{model, Context}; #[derive(serde::Serialize)] pub struct NodeInfoDiscovery { @@ -20,11 +19,11 @@ pub async fn nodeinfo_discovery(State(ctx): State) -> Json, Path(version): Path) - "2.0.json" | "2.0" => ( nodeinfo::types::Software { name: "μpub".to_string(), - version: Some(VERSION.into()), + version: Some(upub::VERSION.into()), repository: None, homepage: None, }, @@ -51,7 +50,7 @@ pub async fn nodeinfo(State(ctx): State, Path(version): Path) - "2.1.json" | "2.1" => ( nodeinfo::types::Software { name: "μpub".to_string(), - version: Some(VERSION.into()), + version: Some(upub::VERSION.into()), repository: Some("https://git.alemi.dev/upub.git/".into()), homepage: None, }, @@ -96,7 +95,7 @@ impl IntoResponse for JsonRD { } } -pub async fn webfinger(State(ctx): State, Query(query): Query) -> crate::Result> { +pub async fn webfinger(State(ctx): State, Query(query): Query) -> upub::Result> { if let Some((user, domain)) = query .resource .replace("acct:", "") @@ -107,7 +106,7 @@ pub async fn webfinger(State(ctx): State, Query(query): Query) -> crate::Result> { +pub async fn oauth_authorization_server(State(ctx): State) -> upub::Result> { Ok(Json(OauthAuthorizationServerResponse { - issuer: url!(ctx, ""), - authorization_endpoint: url!(ctx, "/auth"), + issuer: upub::url!(ctx, ""), + authorization_endpoint: upub::url!(ctx, "/auth"), token_endpoint: "".to_string(), scopes_supported: vec![ "read:account".to_string(), diff --git a/src/server/builders.rs b/upub/routes/src/builders.rs similarity index 59% rename from src/server/builders.rs rename to upub/routes/src/builders.rs index 0f8b6413..70e6cb64 100644 --- a/src/server/builders.rs +++ b/upub/routes/src/builders.rs @@ -1,7 +1,9 @@ use apb::{BaseMut, CollectionMut, CollectionPageMut}; use sea_orm::{Condition, DatabaseConnection, QueryFilter, QuerySelect, RelationTrait}; +use axum::response::{IntoResponse, Response}; -use crate::{model::{addressing::Event, attachment::BatchFillable}, routes::activitypub::{jsonld::LD, JsonLD, Pagination}}; +use upub::{model::{addressing::Event, attachment::BatchFillable}, server::jsonld::LD}; +use crate::activitypub::Pagination; pub async fn paginate( id: String, @@ -10,15 +12,15 @@ pub async fn paginate( page: Pagination, my_id: Option, with_users: bool, // TODO ewww too many arguments for this weird function... -) -> crate::Result> { +) -> upub::Result> { let limit = page.batch.unwrap_or(20).min(50); let offset = page.offset.unwrap_or(0); - let mut select = crate::model::addressing::Entity::find_addressed(my_id); + let mut select = upub::model::addressing::Entity::find_addressed(my_id); if with_users { select = select - .join(sea_orm::JoinType::InnerJoin, crate::model::activity::Relation::Actors.def()); + .join(sea_orm::JoinType::InnerJoin, upub::model::activity::Relation::Actors.def()); } let items = select @@ -43,14 +45,14 @@ pub async fn paginate( collection_page(&id, offset, limit, items) } -pub fn collection_page(id: &str, offset: u64, limit: u64, items: Vec) -> crate::Result> { +pub fn collection_page(id: &str, offset: u64, limit: u64, items: Vec) -> upub::Result> { let next = if items.len() < limit as usize { apb::Node::Empty } else { apb::Node::link(format!("{id}?offset={}", offset+limit)) }; Ok(JsonLD( - serde_json::Value::new_object() + apb::new() .set_id(Some(&format!("{id}?offset={offset}"))) .set_collection_type(Some(apb::CollectionType::OrderedCollectionPage)) .set_part_of(apb::Node::link(id.replace("/page", ""))) @@ -61,9 +63,9 @@ pub fn collection_page(id: &str, offset: u64, limit: u64, items: Vec) -> crate::Result> { +pub fn collection(id: &str, total_items: Option) -> upub::Result> { Ok(JsonLD( - serde_json::Value::new_object() + apb::new() .set_id(Some(id)) .set_collection_type(Some(apb::CollectionType::OrderedCollection)) .set_first(apb::Node::link(format!("{id}/page"))) @@ -72,21 +74,14 @@ pub fn collection(id: &str, total_items: Option) -> crate::Result crate::Result; -} - -#[axum::async_trait] -impl AnyQuery for sea_orm::Select { - async fn any(self, db: &sea_orm::DatabaseConnection) -> crate::Result { - Ok(self.one(db).await?.is_some()) - } -} - -#[axum::async_trait] -impl AnyQuery for sea_orm::Selector { - async fn any(self, db: &sea_orm::DatabaseConnection) -> crate::Result { - Ok(self.one(db).await?.is_some()) +// got this from https://github.com/kitsune-soc/kitsune/blob/b023a12b687dd9a274233a5a9950f2de5e192344/kitsune/src/http/responder.rs +// i was trying to do it with middlewares but this is way cleaner +pub struct JsonLD(pub T); +impl IntoResponse for JsonLD { + fn into_response(self) -> Response { + ( + [("Content-Type", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"")], + axum::Json(self.0) + ).into_response() } } diff --git a/upub/routes/src/lib.rs b/upub/routes/src/lib.rs new file mode 100644 index 00000000..9904917b --- /dev/null +++ b/upub/routes/src/lib.rs @@ -0,0 +1,39 @@ +pub mod activitypub; + +#[cfg(feature = "web")] +pub mod web; + +#[cfg(feature = "mastodon")] +pub mod mastodon; + +pub mod builders; + +#[cfg(not(feature = "mastodon"))] +pub mod mastodon { + pub trait MastodonRouter { + fn mastodon_routes(self) -> Self where Self: Sized { self } + } + + impl MastodonRouter for axum::Router {} +} + +pub async fn serve(ctx: upub::Context, bind: String) -> upub::Result<()> { + use activitypub::ActivityPubRouter; + use mastodon::MastodonRouter; + use tower_http::{cors::CorsLayer, trace::TraceLayer}; + + let router = axum::Router::new() + .ap_routes() + .mastodon_routes() // no-op if mastodon feature is disabled + .layer(CorsLayer::permissive()) + .layer(TraceLayer::new_for_http()) + .with_state(ctx); + + // run our app with hyper, listening locally on port 3000 + let listener = tokio::net::TcpListener::bind(bind) + .await.expect("could not bind tcp socket"); + + axum::serve(listener, router).await?; + + Ok(()) +} diff --git a/src/routes/mastodon/README.md b/upub/routes/src/mastodon/README.md similarity index 100% rename from src/routes/mastodon/README.md rename to upub/routes/src/mastodon/README.md diff --git a/src/routes/mastodon/accounts.rs b/upub/routes/src/mastodon/accounts.rs similarity index 100% rename from src/routes/mastodon/accounts.rs rename to upub/routes/src/mastodon/accounts.rs diff --git a/src/routes/mastodon/instance.rs b/upub/routes/src/mastodon/instance.rs similarity index 100% rename from src/routes/mastodon/instance.rs rename to upub/routes/src/mastodon/instance.rs diff --git a/src/routes/mastodon/mod.rs b/upub/routes/src/mastodon/mod.rs similarity index 100% rename from src/routes/mastodon/mod.rs rename to upub/routes/src/mastodon/mod.rs diff --git a/web/Cargo.toml b/web/Cargo.toml index 050b378d..ab9deaad 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -12,12 +12,10 @@ repository = "https://git.alemi.dev/upub.git" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -log = "0.4" tracing = "0.1" tracing-subscriber = "0.3" tracing-subscriber-wasm = "0.1" console_error_panic_hook = "0.1" -thiserror = "1" serde = { version = "1", features = ["derive"] } serde_json = "1" serde_default = "0.1" @@ -26,13 +24,13 @@ dashmap = "5.5" leptos = { version = "0.6", features = ["csr", "tracing"] } leptos_router = { version = "0.6", features = ["csr"] } leptos-use = { version = "0.10", features = ["serde"] } +web-sys = { version = "0.3", features = ["Screen"] } reqwest = { version = "0.12", features = ["json"] } apb = { path = "../apb", features = ["unstructured", "activitypub-fe", "activitypub-counters", "litepub"] } uriproxy = { path = "../utils/uriproxy/" } +mdhtml = { path = "../utils/mdhtml/" } futures = "0.3.30" lazy_static = "1.4" chrono = { version = "0.4", features = ["serde"] } -web-sys = { version = "0.3", features = ["Screen"] } -mdhtml = { path = "../utils/mdhtml/" } jrd = "0.1" tld = "2.35" diff --git a/web/README.md b/web/README.md new file mode 100644 index 00000000..e69de29b