chore: BIG refactor into smaller crates

hopefully this makes lsp more responsive? because it wont need to
recompile everything every time, but idk really
This commit is contained in:
əlemi 2024-05-31 04:07:39 +02:00
parent 8c91b6c87a
commit 5b592874cb
Signed by: alemi
GPG key ID: A4895B84D311642C
88 changed files with 589 additions and 429 deletions

113
Cargo.lock generated
View file

@ -4,9 +4,9 @@ version = 3
[[package]] [[package]]
name = "addr2line" name = "addr2line"
version = "0.21.0" version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678"
dependencies = [ dependencies = [
"gimli", "gimli",
] ]
@ -310,9 +310,9 @@ dependencies = [
[[package]] [[package]]
name = "backtrace" name = "backtrace"
version = "0.3.71" version = "0.3.72"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" checksum = "17c6a35df3749d2e8bb1b7b21a976d82b15548788d2735b9d82f329268f71a11"
dependencies = [ dependencies = [
"addr2line", "addr2line",
"cc", "cc",
@ -1275,9 +1275,9 @@ dependencies = [
[[package]] [[package]]
name = "gimli" name = "gimli"
version = "0.28.1" version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd"
[[package]] [[package]]
name = "glob" name = "glob"
@ -1727,9 +1727,9 @@ dependencies = [
[[package]] [[package]]
name = "hyper-util" name = "hyper-util"
version = "0.1.4" version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d8d52be92d09acc2e01dddb7fde3ad983fc6489c7db4837e605bc3fca4cb63e" checksum = "7b875924a60b96e5d7b9ae7b066540b1dd1cbd90d1828f54c92e02a283351c56"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures-channel", "futures-channel",
@ -2280,11 +2280,10 @@ dependencies = [
[[package]] [[package]]
name = "native-tls" name = "native-tls"
version = "0.2.11" version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466"
dependencies = [ dependencies = [
"lazy_static",
"libc", "libc",
"log", "log",
"openssl", "openssl",
@ -2407,9 +2406,9 @@ dependencies = [
[[package]] [[package]]
name = "object" name = "object"
version = "0.32.2" version = "0.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" checksum = "b8ec7ab813848ba4522158d5517a6093db1ded27575b070f4177b8d12b41db5e"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@ -4358,9 +4357,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.37.0" version = "1.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"bytes", "bytes",
@ -4377,9 +4376,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio-macros" name = "tokio-macros"
version = "2.2.0" version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -4462,7 +4461,7 @@ dependencies = [
"serde", "serde",
"serde_spanned", "serde_spanned",
"toml_datetime", "toml_datetime",
"winnow 0.6.8", "winnow 0.6.9",
] ]
[[package]] [[package]]
@ -4683,38 +4682,92 @@ name = "upub"
version = "0.2.0" version = "0.2.0"
dependencies = [ dependencies = [
"apb", "apb",
"async-recursion",
"axum", "axum",
"base64 0.22.1", "base64 0.22.1",
"chrono", "chrono",
"clap",
"futures",
"jrd", "jrd",
"mastodon-async-entities",
"mdhtml", "mdhtml",
"nodeinfo", "nodeinfo",
"openssl", "openssl",
"rand",
"regex", "regex",
"reqwest", "reqwest",
"sea-orm", "sea-orm",
"sea-orm-migration",
"serde", "serde",
"serde-inline-default", "serde-inline-default",
"serde_default", "serde_default",
"serde_json", "serde_json",
"sha256", "sha256",
"thiserror", "thiserror",
"time",
"tokio", "tokio",
"toml", "toml",
"tower-http",
"tracing", "tracing",
"tracing-subscriber",
"uriproxy", "uriproxy",
"uuid", "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]] [[package]]
name = "upub-web" name = "upub-web"
version = "0.1.0" version = "0.1.0"
@ -4729,14 +4782,12 @@ dependencies = [
"leptos", "leptos",
"leptos-use", "leptos-use",
"leptos_router", "leptos_router",
"log",
"mdhtml", "mdhtml",
"reqwest", "reqwest",
"serde", "serde",
"serde-inline-default", "serde-inline-default",
"serde_default", "serde_default",
"serde_json", "serde_json",
"thiserror",
"tld", "tld",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
@ -5177,9 +5228,9 @@ dependencies = [
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.6.8" version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3c52e9c97a68071b23e836c9380edae937f17b9c4667bd021973efc689f618d" checksum = "86c949fede1d13936a99f14fafd3e76fd642b556dd2ce96287fbe2e0151bfac6"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]

View file

@ -1,8 +1,17 @@
[workspace] [workspace]
members = ["apb", "web", "utils/mdhtml", "utils/uriproxy"] members = [
"apb",
"upub/core",
"upub/cli",
"upub/migrations",
"upub/routes",
"web",
"utils/mdhtml",
"utils/uriproxy"
]
[package] [package]
name = "upub" name = "upub-bin"
version = "0.2.0" version = "0.2.0"
edition = "2021" edition = "2021"
authors = [ "alemi <me@alemi.dev>" ] authors = [ "alemi <me@alemi.dev>" ]
@ -12,46 +21,25 @@ keywords = ["activitypub", "activitystreams", "json"]
repository = "https://git.alemi.dev/upub.git" repository = "https://git.alemi.dev/upub.git"
readme = "README.md" 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] [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" toml = "0.8"
mdhtml = { path = "utils/mdhtml", features = ["markdown"] }
uriproxy = { path = "utils/uriproxy" }
jrd = "0.1"
tracing = "0.1" tracing = "0.1"
tracing-subscriber = "0.3" tracing-subscriber = "0.3"
sea-orm = "0.12"
clap = { version = "4.5", features = ["derive"] } clap = { version = "4.5", features = ["derive"] }
futures = "0.3"
tokio = { version = "1.35", features = ["full"] } # TODO slim this down 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"] } upub = { path = "upub/core" }
axum = "0.7" upub-cli = { path = "upub/cli", optional = true }
tower-http = { version = "0.5", features = ["cors", "trace"] } upub-migrations = { path = "upub/migrations", optional = true }
apb = { path = "apb", features = ["unstructured", "orm", "activitypub-fe", "activitypub-counters", "litepub", "ostatus", "toot"] } upub-routes = { path = "upub/routes", optional = true }
# 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"
[features] [features]
default = ["mastodon", "migrations", "cli"] default = ["serve", "migrate", "cli"]
cli = [] serve = ["dep:upub-routes"]
migrations = ["dep:sea-orm-migration"] migrate = ["dep:upub-migrations"]
mastodon = ["dep:mastodon-async-entities", "dep:time"] cli = ["dep:upub-cli"]

View file

@ -125,3 +125,8 @@ pub use types::{
tombstone::{Tombstone, TombstoneMut}, tombstone::{Tombstone, TombstoneMut},
}, },
}; };
#[cfg(feature = "unstructured")]
pub fn new() -> serde_json::Value {
serde_json::Value::Object(serde_json::Map::default())
}

View file

@ -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 std::path::PathBuf;
use config::Config;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use sea_orm::{ConnectOptions, Database}; use sea_orm::{ConnectOptions, Database};
pub use errors::UpubResult as Result; #[cfg(feature = "cli")]
use tower_http::{cors::CorsLayer, trace::TraceLayer}; 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)] #[derive(Parser)]
/// all names were taken /// all names were taken
@ -50,17 +38,10 @@ struct Args {
#[derive(Clone, Subcommand)] #[derive(Clone, Subcommand)]
enum Mode { 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 /// print current or default configuration
Config, Config,
#[cfg(feature = "migrations")] #[cfg(feature = "migrate")]
/// apply database migrations /// apply database migrations
Migrate, Migrate,
@ -71,6 +52,14 @@ enum Mode {
/// task to run /// task to run
command: cli::CliCommand, 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] #[tokio::main]
@ -83,7 +72,7 @@ async fn main() {
.with_max_level(if args.debug { tracing::Level::DEBUG } else { tracing::Level::INFO }) .with_max_level(if args.debug { tracing::Level::DEBUG } else { tracing::Level::INFO })
.init(); .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 database = args.database.unwrap_or(config.datasource.connection_string.clone());
let domain = args.domain.unwrap_or(config.instance.domain.clone()); let domain = args.domain.unwrap_or(config.instance.domain.clone());
@ -106,40 +95,28 @@ async fn main() {
let db = Database::connect(opts) let db = Database::connect(opts)
.await.expect("error connecting to db"); .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 { match args.command {
#[cfg(feature = "migrations")] Mode::Config => println!("{}", toml::to_string_pretty(&config).expect("failed serializing config")),
#[cfg(feature = "migrate")]
Mode::Migrate => Mode::Migrate =>
migrations::Migrator::up(&db, None) migrations::Migrator::up(ctx.db(), None)
.await.expect("error applying migrations"), .await.expect("error applying migrations"),
#[cfg(feature = "cli")] #[cfg(feature = "cli")]
Mode::Cli { command } => Mode::Cli { command } =>
cli::run(command, db, domain, config) cli::run(ctx, command)
.await.expect("failed running cli task"), .await.expect("failed running cli task"),
Mode::Config => println!("{}", toml::to_string_pretty(&config).expect("failed serializing config")), #[cfg(feature = "serve")]
Mode::Serve { bind } =>
Mode::Serve { bind } => { routes::serve(ctx, bind)
let ctx = server::Context::new(db, domain, config) .await.expect("failed serving api routes"),
.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")
},
} }
} }

View file

@ -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(())
}

View file

@ -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<crate::server::Context> {}
}

24
upub/cli/Cargo.toml Normal file
View file

@ -0,0 +1,24 @@
[package]
name = "upub-cli"
version = "0.2.0"
edition = "2021"
authors = [ "alemi <me@alemi.dev>" ]
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"

1
upub/cli/README.md Normal file
View file

@ -0,0 +1 @@
# upub cli

View file

@ -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 openssl::rsa::Rsa;
use sea_orm::{ActiveValue::NotSet, IntoActiveModel}; 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}; use sea_orm::{EntityTrait, Set};
let domain = ctx.domain(); let domain = ctx.domain();

View file

@ -1,23 +1,22 @@
use sea_orm::EntityTrait; use sea_orm::EntityTrait;
use upub::server::{fetcher::Fetchable, normalizer::Normalizer};
use crate::server::{fetcher::Fetchable, normalizer::Normalizer, Context}; pub async fn fetch(ctx: upub::Context, uri: String, save: bool) -> upub::Result<()> {
pub async fn fetch(ctx: crate::server::Context, uri: String, save: bool) -> crate::Result<()> {
use apb::Base; use apb::Base;
let mut node = apb::Node::link(uri.to_string()); let mut node = apb::Node::link(uri.to_string());
node.fetch(&ctx).await?; node.fetch(&ctx).await?;
let obj = node.extract().expect("node still empty after fetch?"); 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()); println!("{}", serde_json::to_string_pretty(&obj).unwrap());
if save { if save {
match obj.base_type() { match obj.base_type() {
Some(apb::BaseType::Object(apb::ObjectType::Actor(_))) => { Some(apb::BaseType::Object(apb::ObjectType::Actor(_))) => {
crate::model::actor::Entity::insert( upub::model::actor::Entity::insert(
crate::model::actor::ActiveModel::new(&obj).unwrap() upub::model::actor::ActiveModel::new(&obj).unwrap()
).exec(ctx.db()).await.unwrap(); ).exec(ctx.db()).await.unwrap();
}, },
Some(apb::BaseType::Object(apb::ObjectType::Activity(_))) => { Some(apb::BaseType::Object(apb::ObjectType::Activity(_))) => {

View file

@ -1,7 +1,6 @@
use sea_orm::EntityTrait; use sea_orm::EntityTrait;
pub async fn fix(ctx: upub::Context, likes: bool, shares: bool, replies: bool) -> upub::Result<()> {
pub async fn fix(ctx: crate::server::Context, likes: bool, shares: bool, replies: bool) -> crate::Result<()> {
use futures::TryStreamExt; use futures::TryStreamExt;
let db = ctx.db(); 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..."); tracing::info!("fixing likes...");
let mut store = std::collections::HashMap::new(); 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? { while let Some(like) = stream.try_next().await? {
store.insert(like.object, store.get(&like.object).unwrap_or(&0) + 1); store.insert(like.object, store.get(&like.object).unwrap_or(&0) + 1);
} }
} }
for (k, v) in store { for (k, v) in store {
let m = crate::model::object::ActiveModel { let m = upub::model::object::ActiveModel {
internal: sea_orm::Set(k), internal: sea_orm::Set(k),
likes: sea_orm::Set(v), likes: sea_orm::Set(v),
..Default::default() ..Default::default()
}; };
if let Err(e) = crate::model::object::Entity::update(m) if let Err(e) = upub::model::object::Entity::update(m)
.exec(db) .exec(db)
.await .await
{ {
@ -34,19 +33,19 @@ pub async fn fix(ctx: crate::server::Context, likes: bool, shares: bool, replies
tracing::info!("fixing shares..."); tracing::info!("fixing shares...");
let mut store = std::collections::HashMap::new(); 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? { while let Some(share) = stream.try_next().await? {
store.insert(share.object, store.get(&share.object).unwrap_or(&0) + 1); store.insert(share.object, store.get(&share.object).unwrap_or(&0) + 1);
} }
} }
for (k, v) in store { for (k, v) in store {
let m = crate::model::object::ActiveModel { let m = upub::model::object::ActiveModel {
internal: sea_orm::Set(k), internal: sea_orm::Set(k),
announces: sea_orm::Set(v), announces: sea_orm::Set(v),
..Default::default() ..Default::default()
}; };
if let Err(e) = crate::model::object::Entity::update(m) if let Err(e) = upub::model::object::Entity::update(m)
.exec(db) .exec(db)
.await .await
{ {
@ -59,7 +58,7 @@ pub async fn fix(ctx: crate::server::Context, likes: bool, shares: bool, replies
tracing::info!("fixing replies..."); tracing::info!("fixing replies...");
let mut store = std::collections::HashMap::new(); 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? { while let Some(object) = stream.try_next().await? {
if let Some(reply) = object.in_reply_to { if let Some(reply) = object.in_reply_to {
let before = store.get(&reply).unwrap_or(&0); 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 { for (k, v) in store {
let m = crate::model::object::ActiveModel { let m = upub::model::object::ActiveModel {
id: sea_orm::Set(k.clone()), id: sea_orm::Set(k.clone()),
replies: sea_orm::Set(v), replies: sea_orm::Set(v),
..Default::default() ..Default::default()
}; };
if let Err(e) = crate::model::object::Entity::update(m) if let Err(e) = upub::model::object::Entity::update(m)
.exec(db) .exec(db)
.await .await
{ {

View file

@ -93,15 +93,7 @@ pub enum CliCommand {
} }
} }
pub async fn run( pub async fn run(ctx: upub::Context, command: CliCommand) -> upub::Result<()> {
command: CliCommand,
db: sea_orm::DatabaseConnection,
domain: String,
config: crate::config::Config,
) -> crate::Result<()> {
let ctx = crate::server::Context::new(
db, domain, config,
).await?;
match command { match command {
CliCommand::Faker { count } => CliCommand::Faker { count } =>
Ok(faker(ctx, count as i64).await?), Ok(faker(ctx, count as i64).await?),

View file

@ -1,14 +1,14 @@
use crate::server::admin::Administrable; use upub::server::admin::Administrable;
pub async fn register( pub async fn register(
ctx: crate::server::Context, ctx: upub::Context,
username: String, username: String,
password: String, password: String,
display_name: Option<String>, display_name: Option<String>,
summary: Option<String>, summary: Option<String>,
avatar_url: Option<String>, avatar_url: Option<String>,
banner_url: Option<String>, banner_url: Option<String>,
) -> crate::Result<()> { ) -> upub::Result<()> {
ctx.register_user( ctx.register_user(
username.clone(), username.clone(),
password, password,

41
upub/cli/src/relay.rs Normal file
View file

@ -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(())
}

View file

@ -1,15 +1,15 @@
use futures::TryStreamExt; use futures::TryStreamExt;
use sea_orm::{ActiveValue::Set, ColumnTrait, EntityTrait, QueryFilter}; 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 count = 0;
let mut insertions = Vec::new(); let mut insertions = Vec::new();
{ {
let mut stream = crate::model::actor::Entity::find() let mut stream = upub::model::actor::Entity::find()
.filter(crate::model::actor::Column::Updated.lt(chrono::Utc::now() - chrono::Duration::days(days))) .filter(upub::model::actor::Column::Updated.lt(chrono::Utc::now() - chrono::Duration::days(days)))
.stream(ctx.db()) .stream(ctx.db())
.await?; .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()) { match ctx.pull(&user.id).await.map(|x| x.actor()) {
Err(e) => tracing::warn!("could not update user {}: {e}", user.id), Err(e) => tracing::warn!("could not update user {}: {e}", user.id),
Ok(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) => { Ok(mut u) => {
u.internal = Set(user.internal); u.internal = Set(user.internal);
u.updated = Set(chrono::Utc::now()); 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 { for (uid, user_model) in insertions {
tracing::info!("updating user {}", uid); tracing::info!("updating user {}", uid);
crate::model::actor::Entity::update(user_model) upub::model::actor::Entity::update(user_model)
.exec(ctx.db()) .exec(ctx.db())
.await?; .await?;
} }

36
upub/core/Cargo.toml Normal file
View file

@ -0,0 +1,36 @@
[package]
name = "upub"
version = "0.2.0"
edition = "2021"
authors = [ "alemi <me@alemi.dev>" ]
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" }

1
upub/core/README.md Normal file
View file

@ -0,0 +1 @@
# upub core

View file

@ -31,6 +31,9 @@ pub enum UpubError {
#[error("type mismatch on object: expected {0:?}, found {1:?}")] #[error("type mismatch on object: expected {0:?}, found {1:?}")]
Mismatch(apb::ObjectType, apb::ObjectType), 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 // 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!!!! // keep the type hints on the return type, still what the hell!!!!
#[error("redirecting to {0}")] #[error("redirecting to {0}")]

19
upub/core/src/ext.rs Normal file
View file

@ -0,0 +1,19 @@
#[axum::async_trait]
pub trait AnyQuery {
async fn any(self, db: &sea_orm::DatabaseConnection) -> crate::Result<bool>;
}
#[axum::async_trait]
impl<T : sea_orm::EntityTrait> AnyQuery for sea_orm::Select<T> {
async fn any(self, db: &sea_orm::DatabaseConnection) -> crate::Result<bool> {
Ok(self.one(db).await?.is_some())
}
}
#[axum::async_trait]
impl<T : sea_orm::SelectorTrait + Send> AnyQuery for sea_orm::Selector<T> {
async fn any(self, db: &sea_orm::DatabaseConnection) -> crate::Result<bool> {
Ok(self.one(db).await?.is_some())
}
}

12
upub/core/src/lib.rs Normal file
View file

@ -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");

View file

@ -1,7 +1,7 @@
use apb::{ActivityMut, ActivityType, BaseMut, ObjectMut}; use apb::{ActivityMut, ActivityType, BaseMut, ObjectMut};
use sea_orm::{entity::prelude::*, QuerySelect, SelectColumns}; 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)] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "activities")] #[sea_orm(table_name = "activities")]
@ -109,7 +109,7 @@ impl ActiveModel {
impl Model { impl Model {
pub fn ap(self) -> serde_json::Value { pub fn ap(self) -> serde_json::Value {
serde_json::Value::new_object() apb::new()
.set_id(Some(&self.id)) .set_id(Some(&self.id))
.set_activity_type(Some(self.activity_type)) .set_activity_type(Some(self.activity_type))
.set_actor(apb::Node::link(self.actor)) .set_actor(apb::Node::link(self.actor))

View file

@ -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 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)] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "actors")] #[sea_orm(table_name = "actors")]
@ -193,18 +193,18 @@ impl ActiveModel {
impl Model { impl Model {
pub fn ap(self) -> serde_json::Value { pub fn ap(self) -> serde_json::Value {
serde_json::Value::new_object() apb::new()
.set_id(Some(&self.id)) .set_id(Some(&self.id))
.set_actor_type(Some(self.actor_type)) .set_actor_type(Some(self.actor_type))
.set_name(self.name.as_deref()) .set_name(self.name.as_deref())
.set_summary(self.summary.as_deref()) .set_summary(self.summary.as_deref())
.set_icon(apb::Node::maybe_object(self.icon.map(|i| .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_document_type(Some(apb::DocumentType::Image))
.set_url(apb::Node::link(i.clone())) .set_url(apb::Node::link(i.clone()))
))) )))
.set_image(apb::Node::maybe_object(self.image.map(|i| .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_document_type(Some(apb::DocumentType::Image))
.set_url(apb::Node::link(i.clone())) .set_url(apb::Node::link(i.clone()))
))) )))
@ -218,13 +218,13 @@ impl Model {
.set_following(apb::Node::maybe_link(self.following)) .set_following(apb::Node::maybe_link(self.following))
.set_followers(apb::Node::maybe_link(self.followers)) .set_followers(apb::Node::maybe_link(self.followers))
.set_public_key(apb::Node::object( .set_public_key(apb::Node::object(
serde_json::Value::new_object() apb::new()
.set_id(Some(&format!("{}#main-key", self.id))) .set_id(Some(&format!("{}#main-key", self.id)))
.set_owner(Some(&self.id)) .set_owner(Some(&self.id))
.set_public_key_pem(&self.public_key) .set_public_key_pem(&self.public_key)
)) ))
.set_endpoints(apb::Node::object( .set_endpoints(apb::Node::object(
serde_json::Value::new_object() apb::new()
.set_shared_inbox(self.shared_inbox.as_deref()) .set_shared_inbox(self.shared_inbox.as_deref())
)) ))
.set_discoverable(Some(true)) .set_discoverable(Some(true))

View file

@ -1,8 +1,6 @@
use apb::{ActivityMut, ObjectMut}; use apb::{ActivityMut, ObjectMut};
use sea_orm::{entity::prelude::*, sea_query::IntoCondition, Condition, FromQueryResult, Iterable, Order, QueryOrder, QuerySelect, SelectColumns}; 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)] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "addressing")] #[sea_orm(table_name = "addressing")]
pub struct Model { pub struct Model {
@ -121,17 +119,17 @@ impl Event {
.set_attachment(attachment) .set_attachment(attachment)
.set_liked_by_me(if liked.is_some() { Some(true) } else { None }) .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_activity_type(Some(apb::ActivityType::Activity))
.set_object(apb::Node::object( .set_object(apb::Node::object(
object.ap() object.ap()
.set_attachment(attachment) .set_attachment(attachment)
.set_liked_by_me(if liked.is_some() { Some(true) } else { None }) .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_activity_type(Some(apb::ActivityType::Activity))
.set_object(apb::Node::object( .set_object(apb::Node::object(
serde_json::Value::new_object() apb::new()
.set_object_type(Some(apb::ObjectType::Tombstone)) .set_object_type(Some(apb::ObjectType::Tombstone))
)), )),
} }

View file

@ -1,8 +1,6 @@
use apb::{DocumentMut, DocumentType, ObjectMut}; use apb::{DocumentMut, DocumentType, ObjectMut};
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
use crate::routes::activitypub::jsonld::LD;
use super::addressing::Event; use super::addressing::Event;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
@ -41,7 +39,7 @@ impl ActiveModelBehavior for ActiveModel {}
impl Model { impl Model {
pub fn ap(self) -> serde_json::Value { pub fn ap(self) -> serde_json::Value {
serde_json::Value::new_object() apb::new()
.set_url(apb::Node::link(self.url)) .set_url(apb::Node::link(self.url))
.set_document_type(Some(self.document_type)) .set_document_type(Some(self.document_type))
.set_media_type(Some(&self.media_type)) .set_media_type(Some(&self.media_type))

View file

@ -1,7 +1,7 @@
use apb::{BaseMut, Collection, CollectionMut, ObjectMut, ObjectType}; use apb::{BaseMut, Collection, CollectionMut, ObjectMut, ObjectType};
use sea_orm::{entity::prelude::*, QuerySelect, SelectColumns}; use sea_orm::{entity::prelude::*, QuerySelect, SelectColumns};
use crate::{errors::UpubError, routes::activitypub::jsonld::LD}; use crate::errors::UpubError;
use super::Audience; use super::Audience;
@ -185,7 +185,7 @@ impl ActiveModel {
impl Model { impl Model {
pub fn ap(self) -> serde_json::Value { pub fn ap(self) -> serde_json::Value {
serde_json::Value::new_object() apb::new()
.set_id(Some(&self.id)) .set_id(Some(&self.id))
.set_object_type(Some(self.object_type)) .set_object_type(Some(self.object_type))
.set_attributed_to(apb::Node::maybe_link(self.attributed_to)) .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_url(apb::Node::maybe_link(self.url))
.set_sensitive(Some(self.sensitive)) .set_sensitive(Some(self.sensitive))
.set_shares(apb::Node::object( .set_shares(apb::Node::object(
serde_json::Value::new_object() apb::new()
.set_collection_type(Some(apb::CollectionType::OrderedCollection)) .set_collection_type(Some(apb::CollectionType::OrderedCollection))
.set_total_items(Some(self.announces as u64)) .set_total_items(Some(self.announces as u64))
)) ))
.set_likes(apb::Node::object( .set_likes(apb::Node::object(
serde_json::Value::new_object() apb::new()
.set_collection_type(Some(apb::CollectionType::OrderedCollection)) .set_collection_type(Some(apb::CollectionType::OrderedCollection))
.set_total_items(Some(self.likes as u64)) .set_total_items(Some(self.likes as u64))
)) ))
.set_replies(apb::Node::object( .set_replies(apb::Node::object(
serde_json::Value::new_object() apb::new()
.set_collection_type(Some(apb::CollectionType::OrderedCollection)) .set_collection_type(Some(apb::CollectionType::OrderedCollection))
.set_total_items(Some(self.replies as u64)) .set_total_items(Some(self.replies as u64))
)) ))

View file

@ -2,10 +2,10 @@ use std::{collections::BTreeSet, sync::Arc};
use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, QuerySelect, SelectColumns}; 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 uriproxy::UriClass;
use super::{builders::AnyQuery, dispatcher::Dispatcher}; use super::dispatcher::Dispatcher;
#[derive(Clone)] #[derive(Clone)]

View file

@ -3,7 +3,7 @@ use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, Order, QueryFilter,
use tokio::{sync::broadcast, task::JoinHandle}; use tokio::{sync::broadcast, task::JoinHandle};
use apb::{ActivityMut, Node}; 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 { pub struct Dispatcher {
waker: broadcast::Sender<()>, waker: broadcast::Sender<()>,

View file

@ -2,7 +2,7 @@ use apb::{target::Addressed, Activity, Base, Object};
use reqwest::StatusCode; use reqwest::StatusCode;
use sea_orm::{sea_query::Expr, ActiveValue::{Set, NotSet}, ColumnTrait, EntityTrait, QueryFilter, QuerySelect, SelectColumns}; 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}; use super::{fetcher::{Fetcher, PullResult}, side_effects::SideEffects, Context};

View file

@ -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 apb::Object;
use axum::response::{IntoResponse, Response};
pub trait LD { pub trait LD {
fn ld_context(self) -> Self; 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 { impl LD for serde_json::Value {
@ -51,15 +43,3 @@ impl LD for serde_json::Value {
self 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<T>(pub T);
impl<T: serde::Serialize> IntoResponse for JsonLD<T> {
fn into_response(self) -> Response {
(
[("Content-Type", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"")],
axum::Json(self.0)
).into_response()
}
}

View file

@ -7,9 +7,9 @@ pub mod inbox;
pub mod init; pub mod init;
pub mod outbox; pub mod outbox;
pub mod auth; pub mod auth;
pub mod builders;
pub mod httpsign; pub mod httpsign;
pub mod normalizer; pub mod normalizer;
pub mod side_effects; pub mod side_effects;
pub mod jsonld;
pub use context::Context; pub use context::Context;

View file

@ -2,9 +2,9 @@ use apb::{target::Addressed, Activity, ActivityMut, Base, BaseMut, Node, Object,
use reqwest::StatusCode; use reqwest::StatusCode;
use sea_orm::{sea_query::Expr, ActiveValue::{Set, NotSet, Unchanged}, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter, QuerySelect, SelectColumns}; 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] #[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<String> { async fn create_note(&self, uid: String, object: serde_json::Value) -> crate::Result<String> {
self.create( self.create(
uid, uid,
serde_json::Value::new_object() apb::new()
.set_activity_type(Some(apb::ActivityType::Create)) .set_activity_type(Some(apb::ActivityType::Create))
.set_to(object.to()) .set_to(object.to())
.set_bto(object.bto()) .set_bto(object.bto())

View file

@ -0,0 +1,14 @@
[package]
name = "upub-migrations"
version = "0.2.0"
edition = "2021"
authors = [ "alemi <me@alemi.dev>" ]
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"

View file

@ -0,0 +1 @@
# upub migrations

View file

@ -22,3 +22,5 @@ impl MigratorTrait for Migrator {
] ]
} }
} }
pub use sea_orm_migration::MigratorTrait;

32
upub/routes/Cargo.toml Normal file
View file

@ -0,0 +1,32 @@
[package]
name = "upub-routes"
version = "0.2.0"
edition = "2021"
authors = [ "alemi <me@alemi.dev>" ]
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 }

1
upub/routes/README.md Normal file
View file

@ -0,0 +1 @@
# upub routes

View file

@ -1,20 +1,22 @@
use axum::extract::{Path, Query, State}; use axum::extract::{Path, Query, State};
use sea_orm::{ColumnTrait, QueryFilter}; 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( pub async fn view(
State(ctx): State<Context>, State(ctx): State<Context>,
Path(id): Path<String>, Path(id): Path<String>,
AuthIdentity(auth): AuthIdentity, AuthIdentity(auth): AuthIdentity,
Query(query): Query<TryFetch>, Query(query): Query<TryFetch>,
) -> crate::Result<JsonLD<serde_json::Value>> { ) -> upub::Result<JsonLD<serde_json::Value>> {
let aid = ctx.aid(&id); let aid = ctx.aid(&id);
if auth.is_local() && query.fetch && !ctx.is_local(&aid) { if auth.is_local() && query.fetch && !ctx.is_local(&aid) {
let obj = ctx.fetch_activity(&aid).await?; let obj = ctx.fetch_activity(&aid).await?;
if obj.id != aid { 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::<Event>() .into_model::<Event>()
.one(ctx.db()) .one(ctx.db())
.await? .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 mut attachments = row.load_attachments_batch(ctx.db()).await?;
let attach = attachments.remove(&row.internal()); let attach = attachments.remove(&row.internal());

View file

@ -1,16 +1,15 @@
use apb::{ActorMut, BaseMut, ObjectMut, PublicKeyMut}; use apb::{ActorMut, BaseMut, ObjectMut, PublicKeyMut};
use axum::{extract::{Query, State}, http::HeaderMap, response::{IntoResponse, Redirect, Response}, Form, Json}; use axum::{extract::{Query, State}, http::HeaderMap, response::{IntoResponse, Redirect, Response}, Form, Json};
use reqwest::Method; 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 crate::builders::JsonLD;
use super::{jsonld::LD, JsonLD};
pub async fn view( pub async fn view(
headers: HeaderMap, headers: HeaderMap,
State(ctx): State<Context>, State(ctx): State<Context>,
) -> crate::Result<Response> { ) -> upub::Result<Response> {
if let Some(accept) = headers.get("Accept") { if let Some(accept) = headers.get("Accept") {
if let Ok(accept) = accept.to_str() { if let Ok(accept) = accept.to_str() {
if accept.contains("text/html") && !accept.contains("application/ld+json") { if accept.contains("text/html") && !accept.contains("application/ld+json") {
@ -19,20 +18,20 @@ pub async fn view(
} }
} }
Ok(JsonLD( Ok(JsonLD(
serde_json::Value::new_object() apb::new()
.set_id(Some(&url!(ctx, ""))) .set_id(Some(&upub::url!(ctx, "")))
.set_actor_type(Some(apb::ActorType::Application)) .set_actor_type(Some(apb::ActorType::Application))
.set_name(Some(&ctx.cfg().instance.name)) .set_name(Some(&ctx.cfg().instance.name))
.set_summary(Some(&ctx.cfg().instance.description)) .set_summary(Some(&ctx.cfg().instance.description))
.set_inbox(apb::Node::link(url!(ctx, "/inbox"))) .set_inbox(apb::Node::link(upub::url!(ctx, "/inbox")))
.set_outbox(apb::Node::link(url!(ctx, "/outbox"))) .set_outbox(apb::Node::link(upub::url!(ctx, "/outbox")))
.set_published(Some(ctx.actor().published)) .set_published(Some(ctx.actor().published))
.set_endpoints(apb::Node::Empty) .set_endpoints(apb::Node::Empty)
.set_preferred_username(Some(ctx.domain())) .set_preferred_username(Some(ctx.domain()))
.set_public_key(apb::Node::object( .set_public_key(apb::Node::object(
serde_json::Value::new_object() apb::new()
.set_id(Some(&url!(ctx, "#main-key"))) .set_id(Some(&upub::url!(ctx, "#main-key")))
.set_owner(Some(&url!(ctx, ""))) .set_owner(Some(&upub::url!(ctx, "")))
.set_public_key_pem(&ctx.actor().public_key) .set_public_key_pem(&ctx.actor().public_key)
)) ))
.ld_context() .ld_context()
@ -48,10 +47,10 @@ pub async fn proxy_get(
State(ctx): State<Context>, State(ctx): State<Context>,
Query(query): Query<FetchPath>, Query(query): Query<FetchPath>,
AuthIdentity(auth): AuthIdentity, AuthIdentity(auth): AuthIdentity,
) -> crate::Result<Json<serde_json::Value>> { ) -> upub::Result<Json<serde_json::Value>> {
// only local users can request fetches // only local users can request fetches
if !ctx.cfg().security.allow_public_debugger && !auth.is_local() { if !ctx.cfg().security.allow_public_debugger && !auth.is_local() {
return Err(UpubError::unauthorized()); return Err(upub::Error::unauthorized());
} }
Ok(Json( Ok(Json(
Context::request( Context::request(
@ -72,10 +71,10 @@ pub async fn proxy_form(
State(ctx): State<Context>, State(ctx): State<Context>,
AuthIdentity(auth): AuthIdentity, AuthIdentity(auth): AuthIdentity,
Form(query): Form<FetchPath>, Form(query): Form<FetchPath>,
) -> crate::Result<Json<serde_json::Value>> { ) -> upub::Result<Json<serde_json::Value>> {
// only local users can request fetches // only local users can request fetches
if !ctx.cfg().security.allow_public_debugger && auth.is_local() { if !ctx.cfg().security.allow_public_debugger && auth.is_local() {
return Err(UpubError::unauthorized()); return Err(upub::Error::unauthorized());
} }
Ok(Json( Ok(Json(
Context::request( Context::request(

View file

@ -1,8 +1,7 @@
use axum::{http::StatusCode, extract::State, Json}; use axum::{http::StatusCode, extract::State, Json};
use rand::Rng; use rand::Rng;
use sea_orm::{ActiveValue::{Set, NotSet}, ColumnTrait, Condition, EntityTrait, QueryFilter}; use sea_orm::{ActiveValue::{Set, NotSet}, ColumnTrait, Condition, EntityTrait, QueryFilter};
use upub::{server::admin::Administrable, Context};
use crate::{errors::UpubError, model, server::{admin::Administrable, Context}};
#[derive(Debug, Clone, serde::Deserialize)] #[derive(Debug, Clone, serde::Deserialize)]
@ -30,12 +29,12 @@ fn token() -> String {
pub async fn login( pub async fn login(
State(ctx): State<Context>, State(ctx): State<Context>,
Json(login): Json<LoginForm> Json(login): Json<LoginForm>
) -> crate::Result<Json<AuthSuccess>> { ) -> upub::Result<Json<AuthSuccess>> {
// TODO salt the pwd // TODO salt the pwd
match model::credential::Entity::find() match upub::model::credential::Entity::find()
.filter(Condition::all() .filter(Condition::all()
.add(model::credential::Column::Login.eq(login.email)) .add(upub::model::credential::Column::Login.eq(login.email))
.add(model::credential::Column::Password.eq(sha256::digest(login.password))) .add(upub::model::credential::Column::Password.eq(sha256::digest(login.password)))
) )
.one(ctx.db()) .one(ctx.db())
.await? .await?
@ -43,8 +42,8 @@ pub async fn login(
Some(x) => { Some(x) => {
let token = token(); let token = token();
let expires = chrono::Utc::now() + std::time::Duration::from_secs(3600 * 6); let expires = chrono::Utc::now() + std::time::Duration::from_secs(3600 * 6);
model::session::Entity::insert( upub::model::session::Entity::insert(
model::session::ActiveModel { upub::model::session::ActiveModel {
internal: sea_orm::ActiveValue::NotSet, internal: sea_orm::ActiveValue::NotSet,
secret: sea_orm::ActiveValue::Set(token.clone()), secret: sea_orm::ActiveValue::Set(token.clone()),
actor: sea_orm::ActiveValue::Set(x.actor.clone()), actor: sea_orm::ActiveValue::Set(x.actor.clone()),
@ -58,7 +57,7 @@ pub async fn login(
user: x.actor user: x.actor
})) }))
}, },
None => Err(UpubError::unauthorized()), None => Err(upub::Error::unauthorized()),
} }
} }
@ -70,16 +69,16 @@ pub struct RefreshForm {
pub async fn refresh( pub async fn refresh(
State(ctx): State<Context>, State(ctx): State<Context>,
Json(login): Json<RefreshForm> Json(login): Json<RefreshForm>
) -> crate::Result<Json<AuthSuccess>> { ) -> upub::Result<Json<AuthSuccess>> {
if !ctx.cfg().security.allow_login_refresh { if !ctx.cfg().security.allow_login_refresh {
return Err(UpubError::forbidden()); return Err(upub::Error::forbidden());
} }
let prev = model::session::Entity::find() let prev = upub::model::session::Entity::find()
.filter(model::session::Column::Secret.eq(login.token)) .filter(upub::model::session::Column::Secret.eq(login.token))
.one(ctx.db()) .one(ctx.db())
.await? .await?
.ok_or_else(UpubError::unauthorized)?; .ok_or_else(upub::Error::unauthorized)?;
if prev.expires > chrono::Utc::now() { if prev.expires > chrono::Utc::now() {
return Ok(Json(AuthSuccess { token: prev.secret, user: prev.actor, expires: prev.expires })); 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 token = token();
let expires = chrono::Utc::now() + std::time::Duration::from_secs(3600 * 6); let expires = chrono::Utc::now() + std::time::Duration::from_secs(3600 * 6);
let user = prev.actor; let user = prev.actor;
let new_session = model::session::ActiveModel { let new_session = upub::model::session::ActiveModel {
internal: NotSet, internal: NotSet,
actor: Set(user.clone()), actor: Set(user.clone()),
secret: Set(token.clone()), secret: Set(token.clone()),
expires: Set(expires), expires: Set(expires),
}; };
model::session::Entity::insert(new_session) upub::model::session::Entity::insert(new_session)
.exec(ctx.db()) .exec(ctx.db())
.await?; .await?;
@ -114,9 +113,9 @@ pub struct RegisterForm {
pub async fn register( pub async fn register(
State(ctx): State<Context>, State(ctx): State<Context>,
Json(registration): Json<RegisterForm> Json(registration): Json<RegisterForm>
) -> crate::Result<Json<String>> { ) -> upub::Result<Json<String>> {
if !ctx.cfg().security.allow_registration { if !ctx.cfg().security.allow_registration {
return Err(UpubError::forbidden()); return Err(upub::Error::forbidden());
} }
ctx.register_user( ctx.register_user(

View file

@ -1,14 +1,17 @@
use axum::extract::{Path, Query, State}; use axum::extract::{Path, Query, State};
use sea_orm::{ColumnTrait, Condition, PaginatorTrait, QueryFilter}; 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( pub async fn get(
State(ctx): State<Context>, State(ctx): State<Context>,
Path(id): Path<String>, Path(id): Path<String>,
AuthIdentity(auth): AuthIdentity, AuthIdentity(auth): AuthIdentity,
) -> crate::Result<JsonLD<serde_json::Value>> { ) -> upub::Result<JsonLD<serde_json::Value>> {
let local_context_id = url!(ctx, "/context/{id}"); let local_context_id = upub::url!(ctx, "/context/{id}");
let context = ctx.oid(&id); let context = ctx.oid(&id);
let count = model::addressing::Entity::find_addressed(auth.my_id()) let count = model::addressing::Entity::find_addressed(auth.my_id())
@ -17,7 +20,7 @@ pub async fn get(
.count(ctx.db()) .count(ctx.db())
.await?; .await?;
crate::server::builders::collection(&local_context_id, Some(count)) crate::builders::collection(&local_context_id, Some(count))
} }
pub async fn page( pub async fn page(
@ -25,11 +28,11 @@ pub async fn page(
Path(id): Path<String>, Path(id): Path<String>,
Query(page): Query<Pagination>, Query(page): Query<Pagination>,
AuthIdentity(auth): AuthIdentity, AuthIdentity(auth): AuthIdentity,
) -> crate::Result<JsonLD<serde_json::Value>> { ) -> upub::Result<JsonLD<serde_json::Value>> {
let context = ctx.oid(&id); let context = ctx.oid(&id);
crate::server::builders::paginate( crate::builders::paginate(
url!(ctx, "/context/{id}/page"), upub::url!(ctx, "/context/{id}/page"),
Condition::all() Condition::all()
.add(auth.filter_condition()) .add(auth.filter_condition())
.add(model::object::Column::Context.eq(context)), .add(model::object::Column::Context.eq(context)),

View file

@ -1,26 +1,27 @@
use apb::{server::Inbox, Activity, ActivityType}; use apb::{server::Inbox, Activity, ActivityType};
use axum::{extract::{Query, State}, http::StatusCode, Json}; use axum::{extract::{Query, State}, http::StatusCode, Json};
use sea_orm::{sea_query::IntoCondition, ColumnTrait}; 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( pub async fn get(
State(ctx): State<Context>, State(ctx): State<Context>,
) -> crate::Result<JsonLD<serde_json::Value>> { ) -> upub::Result<JsonLD<serde_json::Value>> {
crate::server::builders::collection(&url!(ctx, "/inbox"), None) crate::builders::collection(&upub::url!(ctx, "/inbox"), None)
} }
pub async fn page( pub async fn page(
State(ctx): State<Context>, State(ctx): State<Context>,
AuthIdentity(auth): AuthIdentity, AuthIdentity(auth): AuthIdentity,
Query(page): Query<Pagination>, Query(page): Query<Pagination>,
) -> crate::Result<JsonLD<serde_json::Value>> { ) -> upub::Result<JsonLD<serde_json::Value>> {
crate::server::builders::paginate( crate::builders::paginate(
url!(ctx, "/inbox/page"), upub::url!(ctx, "/inbox/page"),
crate::model::addressing::Column::Actor.is_null() upub::model::addressing::Column::Actor.is_null()
.into_condition(), .into_condition(),
ctx.db(), ctx.db(),
page, page,
@ -41,7 +42,7 @@ pub async fn post(
State(ctx): State<Context>, State(ctx): State<Context>,
AuthIdentity(auth): AuthIdentity, AuthIdentity(auth): AuthIdentity,
Json(activity): Json<serde_json::Value> Json(activity): Json<serde_json::Value>
) -> crate::Result<()> { ) -> upub::Result<()> {
let Identity::Remote { domain: server, .. } = auth else { let Identity::Remote { domain: server, .. } = auth else {
if activity.activity_type() == Some(ActivityType::Delete) { if activity.activity_type() == Some(ActivityType::Delete) {
// this is spammy af, ignore them! // this is spammy af, ignore them!
@ -54,24 +55,24 @@ pub async fn post(
} }
tracing::warn!("refusing unauthorized activity: {}", pretty_json!(activity)); tracing::warn!("refusing unauthorized activity: {}", pretty_json!(activity));
if matches!(auth, Identity::Anonymous) { if matches!(auth, Identity::Anonymous) {
return Err(UpubError::unauthorized()); return Err(upub::Error::unauthorized());
} else { } else {
return Err(UpubError::forbidden()); return Err(upub::Error::forbidden());
} }
}; };
let Some(actor) = activity.actor().id() else { let Some(actor) = activity.actor().id() else {
return Err(UpubError::bad_request()); return Err(upub::Error::bad_request());
}; };
if server != Context::server(&actor) { 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()); 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? // 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 => { ActivityType::Activity => {
tracing::warn!("skipping unprocessable base activity: {}", pretty_json!(activity)); tracing::warn!("skipping unprocessable base activity: {}", pretty_json!(activity));
Err(StatusCode::UNPROCESSABLE_ENTITY.into()) // won't ingest useless stuff Err(StatusCode::UNPROCESSABLE_ENTITY.into()) // won't ingest useless stuff

View file

@ -8,18 +8,15 @@ pub mod application;
pub mod auth; pub mod auth;
pub mod well_known; pub mod well_known;
pub mod jsonld;
pub use jsonld::JsonLD;
use axum::{http::StatusCode, response::IntoResponse, routing::{get, patch, post, put}, Router}; use axum::{http::StatusCode, response::IntoResponse, routing::{get, patch, post, put}, Router};
pub trait ActivityPubRouter { pub trait ActivityPubRouter {
fn ap_routes(self) -> Self; fn ap_routes(self) -> Self;
} }
impl ActivityPubRouter for Router<crate::server::Context> { impl ActivityPubRouter for Router<upub::Context> {
fn ap_routes(self) -> Self { fn ap_routes(self) -> Self {
use crate::routes::activitypub as ap; // TODO use self ? use crate::activitypub as ap; // TODO use self ?
self self
// core server inbox/outbox, maybe for feeds? TODO do we need these? // core server inbox/outbox, maybe for feeds? TODO do we need these?

View file

@ -3,23 +3,24 @@ pub mod replies;
use apb::{CollectionMut, ObjectMut}; use apb::{CollectionMut, ObjectMut};
use axum::extract::{Path, Query, State}; use axum::extract::{Path, Query, State};
use sea_orm::{ColumnTrait, ModelTrait, QueryFilter, QuerySelect, SelectColumns}; 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( pub async fn view(
State(ctx): State<Context>, State(ctx): State<Context>,
Path(id): Path<String>, Path(id): Path<String>,
AuthIdentity(auth): AuthIdentity, AuthIdentity(auth): AuthIdentity,
Query(query): Query<TryFetch>, Query(query): Query<TryFetch>,
) -> crate::Result<JsonLD<serde_json::Value>> { ) -> upub::Result<JsonLD<serde_json::Value>> {
let oid = ctx.oid(&id); let oid = ctx.oid(&id);
if auth.is_local() && query.fetch && !ctx.is_local(&oid) { if auth.is_local() && query.fetch && !ctx.is_local(&oid) {
let obj = ctx.fetch_object(&oid).await?; let obj = ctx.fetch_object(&oid).await?;
// some implementations serve statuses on different urls than their AP id // some implementations serve statuses on different urls than their AP id
if obj.id != oid { 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::<Event>() .into_model::<Event>()
.one(ctx.db()) .one(ctx.db())
.await? .await?
.ok_or_else(UpubError::not_found)?; .ok_or_else(upub::Error::not_found)?;
let object = match item { let object = match item {
Event::Tombstone => return Err(UpubError::not_found()), Event::Tombstone => return Err(upub::Error::not_found()),
Event::Activity(_) => return Err(UpubError::not_found()), Event::Activity(_) => return Err(upub::Error::not_found()),
Event::StrayObject { liked: _, object } => object, Event::StrayObject { liked: _, object } => object,
Event::DeepActivity { activity: _, liked: _, object } => object, Event::DeepActivity { activity: _, liked: _, object } => object,
}; };
@ -58,9 +59,9 @@ pub async fn view(
.await?; .await?;
replies = apb::Node::object( replies = apb::Node::object(
serde_json::Value::new_object() apb::new()
// .set_id(Some(&crate::url!(ctx, "/objects/{id}/replies"))) // .set_id(Some(&upub::url!(ctx, "/objects/{id}/replies")))
// .set_first(apb::Node::link(crate::url!(ctx, "/objects/{id}/replies/page"))) // .set_first(apb::Node::link(upub::url!(ctx, "/objects/{id}/replies/page")))
.set_collection_type(Some(apb::CollectionType::Collection)) .set_collection_type(Some(apb::CollectionType::Collection))
.set_total_items(Some(object.replies as u64)) .set_total_items(Some(object.replies as u64))
.set_items(apb::Node::links(replies_ids)) .set_items(apb::Node::links(replies_ids))

View file

@ -1,15 +1,16 @@
use axum::extract::{Path, Query, State}; use axum::extract::{Path, Query, State};
use sea_orm::{ColumnTrait, Condition, PaginatorTrait, QueryFilter}; 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( pub async fn get(
State(ctx): State<Context>, State(ctx): State<Context>,
Path(id): Path<String>, Path(id): Path<String>,
AuthIdentity(auth): AuthIdentity, AuthIdentity(auth): AuthIdentity,
Query(q): Query<TryFetch>, Query(q): Query<TryFetch>,
) -> crate::Result<JsonLD<serde_json::Value>> { ) -> upub::Result<JsonLD<serde_json::Value>> {
let replies_id = url!(ctx, "/objects/{id}/replies"); let replies_id = upub::url!(ctx, "/objects/{id}/replies");
let oid = ctx.oid(&id); let oid = ctx.oid(&id);
if auth.is_local() && q.fetch { if auth.is_local() && q.fetch {
@ -22,7 +23,7 @@ pub async fn get(
.count(ctx.db()) .count(ctx.db())
.await?; .await?;
crate::server::builders::collection(&replies_id, Some(count)) crate::builders::collection(&replies_id, Some(count))
} }
pub async fn page( pub async fn page(
@ -30,11 +31,11 @@ pub async fn page(
Path(id): Path<String>, Path(id): Path<String>,
Query(page): Query<Pagination>, Query(page): Query<Pagination>,
AuthIdentity(auth): AuthIdentity, AuthIdentity(auth): AuthIdentity,
) -> crate::Result<JsonLD<serde_json::Value>> { ) -> upub::Result<JsonLD<serde_json::Value>> {
let page_id = url!(ctx, "/objects/{id}/replies/page"); let page_id = upub::url!(ctx, "/objects/{id}/replies/page");
let oid = ctx.oid(&id); let oid = ctx.oid(&id);
crate::server::builders::paginate( crate::builders::paginate(
page_id, page_id,
Condition::all() Condition::all()
.add(auth.filter_condition()) .add(auth.filter_condition())

View file

@ -1,22 +1,23 @@
use axum::{extract::{Query, State}, http::StatusCode, Json}; use axum::{extract::{Query, State}, http::StatusCode, Json};
use sea_orm::{ColumnTrait, Condition}; 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<Context>) -> crate::Result<JsonLD<serde_json::Value>> { pub async fn get(State(ctx): State<Context>) -> upub::Result<JsonLD<serde_json::Value>> {
crate::server::builders::collection(&url!(ctx, "/outbox"), None) crate::builders::collection(&upub::url!(ctx, "/outbox"), None)
} }
pub async fn page( pub async fn page(
State(ctx): State<Context>, State(ctx): State<Context>,
Query(page): Query<Pagination>, Query(page): Query<Pagination>,
AuthIdentity(auth): AuthIdentity, AuthIdentity(auth): AuthIdentity,
) -> crate::Result<JsonLD<serde_json::Value>> { ) -> upub::Result<JsonLD<serde_json::Value>> {
crate::server::builders::paginate( crate::builders::paginate(
url!(ctx, "/outbox/page"), upub::url!(ctx, "/outbox/page"),
Condition::all() Condition::all()
.add(auth.filter_condition()) .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(), ctx.db(),
page, page,
auth.my_id(), auth.my_id(),
@ -29,7 +30,7 @@ pub async fn post(
State(_ctx): State<Context>, State(_ctx): State<Context>,
AuthIdentity(_auth): AuthIdentity, AuthIdentity(_auth): AuthIdentity,
Json(_activity): Json<serde_json::Value>, Json(_activity): Json<serde_json::Value>,
) -> Result<CreationResult, UpubError> { ) -> upub::Result<CreationResult> {
// TODO administrative actions may be carried out against this outbox? // TODO administrative actions may be carried out against this outbox?
Err(StatusCode::NOT_IMPLEMENTED.into()) Err(StatusCode::NOT_IMPLEMENTED.into())
} }

View file

@ -1,15 +1,16 @@
use axum::extract::{Path, Query, State}; use axum::extract::{Path, Query, State};
use sea_orm::{ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QuerySelect, SelectColumns}; 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<const OUTGOING: bool>( pub async fn get<const OUTGOING: bool>(
State(ctx): State<Context>, State(ctx): State<Context>,
Path(id): Path<String>, Path(id): Path<String>,
) -> crate::Result<JsonLD<serde_json::Value>> { ) -> upub::Result<JsonLD<serde_json::Value>> {
let follow___ = if OUTGOING { "following" } else { "followers" }; let follow___ = if OUTGOING { "following" } else { "followers" };
use upub::model::relation::Column::{Follower, Following};
let count = model::relation::Entity::find() let count = model::relation::Entity::find()
.filter(if OUTGOING { Follower } else { Following }.eq(ctx.uid(&id))) .filter(if OUTGOING { Follower } else { Following }.eq(ctx.uid(&id)))
.count(ctx.db()).await.unwrap_or_else(|e| { .count(ctx.db()).await.unwrap_or_else(|e| {
@ -17,18 +18,19 @@ pub async fn get<const OUTGOING: bool>(
0 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<const OUTGOING: bool>( pub async fn page<const OUTGOING: bool>(
State(ctx): State<Context>, State(ctx): State<Context>,
Path(id): Path<String>, Path(id): Path<String>,
Query(page): Query<Pagination>, Query(page): Query<Pagination>,
) -> crate::Result<JsonLD<serde_json::Value>> { ) -> upub::Result<JsonLD<serde_json::Value>> {
let follow___ = if OUTGOING { "following" } else { "followers" }; let follow___ = if OUTGOING { "following" } else { "followers" };
let limit = page.batch.unwrap_or(20).min(50); let limit = page.batch.unwrap_or(20).min(50);
let offset = page.offset.unwrap_or(0); let offset = page.offset.unwrap_or(0);
use upub::model::relation::Column::{Follower, Following};
let following = model::relation::Entity::find() let following = model::relation::Entity::find()
.filter(if OUTGOING { Follower } else { Following }.eq(ctx.uid(&id))) .filter(if OUTGOING { Follower } else { Following }.eq(ctx.uid(&id)))
.select_only() .select_only()
@ -39,8 +41,8 @@ pub async fn page<const OUTGOING: bool>(
.all(ctx.db()) .all(ctx.db())
.await?; .await?;
crate::server::builders::collection_page( crate::builders::collection_page(
&url!(ctx, "/actors/{id}/{follow___}/page"), &upub::url!(ctx, "/actors/{id}/{follow___}/page"),
offset, limit, offset, limit,
following.into_iter().map(serde_json::Value::String).collect() following.into_iter().map(serde_json::Value::String).collect()
) )

View file

@ -1,18 +1,20 @@
use axum::{extract::{Path, Query, State}, http::StatusCode, Json}; use axum::{extract::{Path, Query, State}, http::StatusCode, Json};
use sea_orm::{ColumnTrait, Condition}; 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( pub async fn get(
State(ctx): State<Context>, State(ctx): State<Context>,
Path(id): Path<String>, Path(id): Path<String>,
AuthIdentity(auth): AuthIdentity, AuthIdentity(auth): AuthIdentity,
) -> crate::Result<JsonLD<serde_json::Value>> { ) -> upub::Result<JsonLD<serde_json::Value>> {
match auth { match auth {
Identity::Anonymous => Err(StatusCode::FORBIDDEN.into()), Identity::Anonymous => Err(StatusCode::FORBIDDEN.into()),
Identity::Remote { .. } => Err(StatusCode::FORBIDDEN.into()), Identity::Remote { .. } => Err(StatusCode::FORBIDDEN.into()),
Identity::Local { id: user, .. } => if ctx.uid(&id) == user { 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 { } else {
Err(StatusCode::FORBIDDEN.into()) Err(StatusCode::FORBIDDEN.into())
}, },
@ -24,17 +26,17 @@ pub async fn page(
Path(id): Path<String>, Path(id): Path<String>,
AuthIdentity(auth): AuthIdentity, AuthIdentity(auth): AuthIdentity,
Query(page): Query<Pagination>, Query(page): Query<Pagination>,
) -> crate::Result<JsonLD<serde_json::Value>> { ) -> upub::Result<JsonLD<serde_json::Value>> {
let Identity::Local { id: uid, internal } = &auth else { let Identity::Local { id: uid, internal } = &auth else {
// local inbox is only for local users // local inbox is only for local users
return Err(UpubError::forbidden()); return Err(upub::Error::forbidden());
}; };
if uid != &ctx.uid(&id) { if uid != &ctx.uid(&id) {
return Err(UpubError::forbidden()); return Err(upub::Error::forbidden());
} }
crate::server::builders::paginate( crate::builders::paginate(
url!(ctx, "/actors/{id}/inbox/page"), upub::url!(ctx, "/actors/{id}/inbox/page"),
Condition::any() Condition::any()
.add(model::addressing::Column::Actor.eq(*internal)) .add(model::addressing::Column::Actor.eq(*internal))
.add(model::object::Column::AttributedTo.eq(uid)) .add(model::object::Column::AttributedTo.eq(uid))
@ -52,7 +54,7 @@ pub async fn post(
Path(_id): Path<String>, Path(_id): Path<String>,
AuthIdentity(_auth): AuthIdentity, AuthIdentity(_auth): AuthIdentity,
Json(activity): Json<serde_json::Value>, Json(activity): Json<serde_json::Value>,
) -> Result<(), UpubError> { ) -> Result<(), upub::Error> {
// POSTing to user inboxes is effectively the same as POSTing to the main inbox // 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 super::super::inbox::post(State(ctx), AuthIdentity(_auth), Json(activity)).await
} }

View file

@ -7,9 +7,11 @@ pub mod following;
use axum::extract::{Path, Query, State}; use axum::extract::{Path, Query, State};
use apb::{ActorMut, EndpointsMut, Node, ObjectMut}; 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( pub async fn view(
@ -17,7 +19,7 @@ pub async fn view(
AuthIdentity(auth): AuthIdentity, AuthIdentity(auth): AuthIdentity,
Path(id): Path<String>, Path(id): Path<String>,
Query(query): Query<TryFetch>, Query(query): Query<TryFetch>,
) -> crate::Result<JsonLD<serde_json::Value>> { ) -> upub::Result<JsonLD<serde_json::Value>> {
let mut uid = ctx.uid(&id); let mut uid = ctx.uid(&id);
if auth.is_local() { if auth.is_local() {
if id.starts_with('@') { if id.starts_with('@') {
@ -50,16 +52,16 @@ pub async fn view(
// local user // local user
Some((user_model, Some(cfg))) => { Some((user_model, Some(cfg))) => {
let mut user = user_model.ap() let mut user = user_model.ap()
.set_inbox(Node::link(url!(ctx, "/actors/{id}/inbox"))) .set_inbox(Node::link(upub::url!(ctx, "/actors/{id}/inbox")))
.set_outbox(Node::link(url!(ctx, "/actors/{id}/outbox"))) .set_outbox(Node::link(upub::url!(ctx, "/actors/{id}/outbox")))
.set_following(Node::link(url!(ctx, "/actors/{id}/following"))) .set_following(Node::link(upub::url!(ctx, "/actors/{id}/following")))
.set_followers(Node::link(url!(ctx, "/actors/{id}/followers"))) .set_followers(Node::link(upub::url!(ctx, "/actors/{id}/followers")))
.set_following_me(following_me) .set_following_me(following_me)
.set_followed_by_me(followed_by_me) .set_followed_by_me(followed_by_me)
.set_endpoints(Node::object( .set_endpoints(Node::object(
serde_json::Value::new_object() apb::new()
.set_shared_inbox(Some(&url!(ctx, "/inbox"))) .set_shared_inbox(Some(&upub::url!(ctx, "/inbox")))
.set_proxy_url(Some(&url!(ctx, "/proxy"))) .set_proxy_url(Some(&upub::url!(ctx, "/proxy")))
)); ));
if !auth.is(&uid) && !cfg.show_followers_count { if !auth.is(&uid) && !cfg.show_followers_count {
@ -83,7 +85,7 @@ pub async fn view(
.set_followed_by_me(followed_by_me) .set_followed_by_me(followed_by_me)
.ld_context() .ld_context()
)), )),
None => Err(UpubError::not_found()), None => Err(upub::Error::not_found()),
} }
} }

View file

@ -2,13 +2,15 @@ use axum::{extract::{Path, Query, State}, http::StatusCode, Json};
use sea_orm::{ColumnTrait, Condition}; use sea_orm::{ColumnTrait, Condition};
use apb::{server::Outbox, AcceptType, ActivityType, Base, BaseType, ObjectType, RejectType}; 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( pub async fn get(
State(ctx): State<Context>, State(ctx): State<Context>,
Path(id): Path<String>, Path(id): Path<String>,
) -> crate::Result<JsonLD<serde_json::Value>> { ) -> upub::Result<JsonLD<serde_json::Value>> {
crate::server::builders::collection(&url!(ctx, "/actors/{id}/outbox"), None) crate::builders::collection(&upub::url!(ctx, "/actors/{id}/outbox"), None)
} }
pub async fn page( pub async fn page(
@ -16,10 +18,10 @@ pub async fn page(
Path(id): Path<String>, Path(id): Path<String>,
Query(page): Query<Pagination>, Query(page): Query<Pagination>,
AuthIdentity(auth): AuthIdentity, AuthIdentity(auth): AuthIdentity,
) -> crate::Result<JsonLD<serde_json::Value>> { ) -> upub::Result<JsonLD<serde_json::Value>> {
let uid = ctx.uid(&id); let uid = ctx.uid(&id);
crate::server::builders::paginate( crate::builders::paginate(
url!(ctx, "/actors/{id}/outbox/page"), upub::url!(ctx, "/actors/{id}/outbox/page"),
Condition::all() Condition::all()
.add(auth.filter_condition()) .add(auth.filter_condition())
.add( .add(
@ -40,7 +42,7 @@ pub async fn post(
Path(id): Path<String>, Path(id): Path<String>,
AuthIdentity(auth): AuthIdentity, AuthIdentity(auth): AuthIdentity,
Json(activity): Json<serde_json::Value>, Json(activity): Json<serde_json::Value>,
) -> Result<CreationResult, UpubError> { ) -> upub::Result<CreationResult> {
match auth { match auth {
Identity::Anonymous => Err(StatusCode::UNAUTHORIZED.into()), Identity::Anonymous => Err(StatusCode::UNAUTHORIZED.into()),
Identity::Remote { .. } => Err(StatusCode::NOT_IMPLEMENTED.into()), Identity::Remote { .. } => Err(StatusCode::NOT_IMPLEMENTED.into()),

View file

@ -1,8 +1,7 @@
use axum::{extract::{Path, Query, State}, http::StatusCode, response::{IntoResponse, Response}, Json}; use axum::{extract::{Path, Query, State}, http::StatusCode, response::{IntoResponse, Response}, Json};
use jrd::{JsonResourceDescriptor, JsonResourceDescriptorLink}; use jrd::{JsonResourceDescriptor, JsonResourceDescriptorLink};
use sea_orm::{ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter}; use sea_orm::{ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter};
use upub::{model, Context};
use crate::{errors::UpubError, model, server::Context, url, VERSION};
#[derive(serde::Serialize)] #[derive(serde::Serialize)]
pub struct NodeInfoDiscovery { pub struct NodeInfoDiscovery {
@ -20,11 +19,11 @@ pub async fn nodeinfo_discovery(State(ctx): State<Context>) -> Json<NodeInfoDisc
links: vec![ links: vec![
NodeInfoDiscoveryRel { NodeInfoDiscoveryRel {
rel: "http://nodeinfo.diaspora.software/ns/schema/2.0".into(), rel: "http://nodeinfo.diaspora.software/ns/schema/2.0".into(),
href: crate::url!(ctx, "/nodeinfo/2.0.json"), href: upub::url!(ctx, "/nodeinfo/2.0.json"),
}, },
NodeInfoDiscoveryRel { NodeInfoDiscoveryRel {
rel: "http://nodeinfo.diaspora.software/ns/schema/2.1".into(), rel: "http://nodeinfo.diaspora.software/ns/schema/2.1".into(),
href: crate::url!(ctx, "/nodeinfo/2.1.json"), href: upub::url!(ctx, "/nodeinfo/2.1.json"),
}, },
], ],
}) })
@ -42,7 +41,7 @@ pub async fn nodeinfo(State(ctx): State<Context>, Path(version): Path<String>) -
"2.0.json" | "2.0" => ( "2.0.json" | "2.0" => (
nodeinfo::types::Software { nodeinfo::types::Software {
name: "μpub".to_string(), name: "μpub".to_string(),
version: Some(VERSION.into()), version: Some(upub::VERSION.into()),
repository: None, repository: None,
homepage: None, homepage: None,
}, },
@ -51,7 +50,7 @@ pub async fn nodeinfo(State(ctx): State<Context>, Path(version): Path<String>) -
"2.1.json" | "2.1" => ( "2.1.json" | "2.1" => (
nodeinfo::types::Software { nodeinfo::types::Software {
name: "μpub".to_string(), name: "μpub".to_string(),
version: Some(VERSION.into()), version: Some(upub::VERSION.into()),
repository: Some("https://git.alemi.dev/upub.git/".into()), repository: Some("https://git.alemi.dev/upub.git/".into()),
homepage: None, homepage: None,
}, },
@ -96,7 +95,7 @@ impl<T: serde::Serialize> IntoResponse for JsonRD<T> {
} }
} }
pub async fn webfinger(State(ctx): State<Context>, Query(query): Query<WebfingerQuery>) -> crate::Result<JsonRD<JsonResourceDescriptor>> { pub async fn webfinger(State(ctx): State<Context>, Query(query): Query<WebfingerQuery>) -> upub::Result<JsonRD<JsonResourceDescriptor>> {
if let Some((user, domain)) = query if let Some((user, domain)) = query
.resource .resource
.replace("acct:", "") .replace("acct:", "")
@ -107,7 +106,7 @@ pub async fn webfinger(State(ctx): State<Context>, Query(query): Query<Webfinger
.filter(model::actor::Column::Domain.eq(domain)) .filter(model::actor::Column::Domain.eq(domain))
.one(ctx.db()) .one(ctx.db())
.await? .await?
.ok_or_else(UpubError::not_found)?; .ok_or_else(upub::Error::not_found)?;
let expires = if domain == ctx.domain() { let expires = if domain == ctx.domain() {
// TODO configurable webfinger TTL, also 30 days may be too much??? // TODO configurable webfinger TTL, also 30 days may be too much???
@ -163,10 +162,10 @@ pub struct OauthAuthorizationServerResponse {
authorization_response_iss_parameter_supported: bool, authorization_response_iss_parameter_supported: bool,
} }
pub async fn oauth_authorization_server(State(ctx): State<Context>) -> crate::Result<Json<OauthAuthorizationServerResponse>> { pub async fn oauth_authorization_server(State(ctx): State<Context>) -> upub::Result<Json<OauthAuthorizationServerResponse>> {
Ok(Json(OauthAuthorizationServerResponse { Ok(Json(OauthAuthorizationServerResponse {
issuer: url!(ctx, ""), issuer: upub::url!(ctx, ""),
authorization_endpoint: url!(ctx, "/auth"), authorization_endpoint: upub::url!(ctx, "/auth"),
token_endpoint: "".to_string(), token_endpoint: "".to_string(),
scopes_supported: vec![ scopes_supported: vec![
"read:account".to_string(), "read:account".to_string(),

View file

@ -1,7 +1,9 @@
use apb::{BaseMut, CollectionMut, CollectionPageMut}; use apb::{BaseMut, CollectionMut, CollectionPageMut};
use sea_orm::{Condition, DatabaseConnection, QueryFilter, QuerySelect, RelationTrait}; 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( pub async fn paginate(
id: String, id: String,
@ -10,15 +12,15 @@ pub async fn paginate(
page: Pagination, page: Pagination,
my_id: Option<i64>, my_id: Option<i64>,
with_users: bool, // TODO ewww too many arguments for this weird function... with_users: bool, // TODO ewww too many arguments for this weird function...
) -> crate::Result<JsonLD<serde_json::Value>> { ) -> upub::Result<JsonLD<serde_json::Value>> {
let limit = page.batch.unwrap_or(20).min(50); let limit = page.batch.unwrap_or(20).min(50);
let offset = page.offset.unwrap_or(0); 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 { if with_users {
select = select 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 let items = select
@ -43,14 +45,14 @@ pub async fn paginate(
collection_page(&id, offset, limit, items) collection_page(&id, offset, limit, items)
} }
pub fn collection_page(id: &str, offset: u64, limit: u64, items: Vec<serde_json::Value>) -> crate::Result<JsonLD<serde_json::Value>> { pub fn collection_page(id: &str, offset: u64, limit: u64, items: Vec<serde_json::Value>) -> upub::Result<JsonLD<serde_json::Value>> {
let next = if items.len() < limit as usize { let next = if items.len() < limit as usize {
apb::Node::Empty apb::Node::Empty
} else { } else {
apb::Node::link(format!("{id}?offset={}", offset+limit)) apb::Node::link(format!("{id}?offset={}", offset+limit))
}; };
Ok(JsonLD( Ok(JsonLD(
serde_json::Value::new_object() apb::new()
.set_id(Some(&format!("{id}?offset={offset}"))) .set_id(Some(&format!("{id}?offset={offset}")))
.set_collection_type(Some(apb::CollectionType::OrderedCollectionPage)) .set_collection_type(Some(apb::CollectionType::OrderedCollectionPage))
.set_part_of(apb::Node::link(id.replace("/page", ""))) .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<serde_json:
} }
pub fn collection(id: &str, total_items: Option<u64>) -> crate::Result<JsonLD<serde_json::Value>> { pub fn collection(id: &str, total_items: Option<u64>) -> upub::Result<JsonLD<serde_json::Value>> {
Ok(JsonLD( Ok(JsonLD(
serde_json::Value::new_object() apb::new()
.set_id(Some(id)) .set_id(Some(id))
.set_collection_type(Some(apb::CollectionType::OrderedCollection)) .set_collection_type(Some(apb::CollectionType::OrderedCollection))
.set_first(apb::Node::link(format!("{id}/page"))) .set_first(apb::Node::link(format!("{id}/page")))
@ -72,21 +74,14 @@ pub fn collection(id: &str, total_items: Option<u64>) -> crate::Result<JsonLD<se
)) ))
} }
#[axum::async_trait] // got this from https://github.com/kitsune-soc/kitsune/blob/b023a12b687dd9a274233a5a9950f2de5e192344/kitsune/src/http/responder.rs
pub trait AnyQuery { // i was trying to do it with middlewares but this is way cleaner
async fn any(self, db: &sea_orm::DatabaseConnection) -> crate::Result<bool>; pub struct JsonLD<T>(pub T);
} impl<T: serde::Serialize> IntoResponse for JsonLD<T> {
fn into_response(self) -> Response {
#[axum::async_trait] (
impl<T : sea_orm::EntityTrait> AnyQuery for sea_orm::Select<T> { [("Content-Type", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"")],
async fn any(self, db: &sea_orm::DatabaseConnection) -> crate::Result<bool> { axum::Json(self.0)
Ok(self.one(db).await?.is_some()) ).into_response()
}
}
#[axum::async_trait]
impl<T : sea_orm::SelectorTrait + Send> AnyQuery for sea_orm::Selector<T> {
async fn any(self, db: &sea_orm::DatabaseConnection) -> crate::Result<bool> {
Ok(self.one(db).await?.is_some())
} }
} }

39
upub/routes/src/lib.rs Normal file
View file

@ -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<upub::Context> {}
}
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(())
}

View file

@ -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 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
log = "0.4"
tracing = "0.1" tracing = "0.1"
tracing-subscriber = "0.3" tracing-subscriber = "0.3"
tracing-subscriber-wasm = "0.1" tracing-subscriber-wasm = "0.1"
console_error_panic_hook = "0.1" console_error_panic_hook = "0.1"
thiserror = "1"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
serde_default = "0.1" serde_default = "0.1"
@ -26,13 +24,13 @@ dashmap = "5.5"
leptos = { version = "0.6", features = ["csr", "tracing"] } leptos = { version = "0.6", features = ["csr", "tracing"] }
leptos_router = { version = "0.6", features = ["csr"] } leptos_router = { version = "0.6", features = ["csr"] }
leptos-use = { version = "0.10", features = ["serde"] } leptos-use = { version = "0.10", features = ["serde"] }
web-sys = { version = "0.3", features = ["Screen"] }
reqwest = { version = "0.12", features = ["json"] } reqwest = { version = "0.12", features = ["json"] }
apb = { path = "../apb", features = ["unstructured", "activitypub-fe", "activitypub-counters", "litepub"] } apb = { path = "../apb", features = ["unstructured", "activitypub-fe", "activitypub-counters", "litepub"] }
uriproxy = { path = "../utils/uriproxy/" } uriproxy = { path = "../utils/uriproxy/" }
mdhtml = { path = "../utils/mdhtml/" }
futures = "0.3.30" futures = "0.3.30"
lazy_static = "1.4" lazy_static = "1.4"
chrono = { version = "0.4", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] }
web-sys = { version = "0.3", features = ["Screen"] }
mdhtml = { path = "../utils/mdhtml/" }
jrd = "0.1" jrd = "0.1"
tld = "2.35" tld = "2.35"

0
web/README.md Normal file
View file