feat!: merge branch 'betterdb' into dev

reworked and improved db structure, more reliable inbox processing
This commit is contained in:
əlemi 2024-05-31 01:57:21 +02:00
commit 8c91b6c87a
Signed by: alemi
GPG key ID: A4895B84D311642C
101 changed files with 4392 additions and 3304 deletions

8
.tci
View file

@ -7,13 +7,13 @@ systemctl --user stop upub
echo "installing new binary" echo "installing new binary"
cp ./target/release/upub /opt/bin/upub cp ./target/release/upub /opt/bin/upub
echo "migrating database" echo "migrating database"
/opt/bin/upub --db "sqlite:///srv/tci/upub.db" --domain https://feditest.alemi.dev migrate /opt/bin/upub --db "sqlite:///srv/tci/upub.db" --domain https://upub.alemi.dev migrate
echo "restarting service" echo "restarting service"
systemctl --user start upub systemctl --user start upub
echo "rebuilding frontend" echo "rebuilding frontend"
cd web cd web
CARGO_BUILD_JOBS=1 /opt/bin/trunk build --release --public-url 'https://feditest.alemi.dev/web' CARGO_BUILD_JOBS=1 /opt/bin/trunk build --release --public-url 'https://upub.alemi.dev/web'
echo "deploying frontend" echo "deploying frontend"
rm /srv/http/feditest/web/* rm /srv/http/upub/web/*
mv ./dist/* /srv/http/feditest/web/ mv ./dist/* /srv/http/upub/web/
echo "done" echo "done"

697
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -13,7 +13,7 @@ all interactions happen with ActivityPub's client-server methods (basically POST
development is still active, so expect more stuff to come! since most fediverse software uses Mastodon's API, μpub plans to implement it as an optional feature, becoming eventually compatible with most existing frontends and mobile applications, but focus right now is on producing something specific to μpub needs development is still active, so expect more stuff to come! since most fediverse software uses Mastodon's API, μpub plans to implement it as an optional feature, becoming eventually compatible with most existing frontends and mobile applications, but focus right now is on producing something specific to μpub needs
a test instance is _usually_ available at [feditest.alemi.dev](https://feditest.alemi.dev) a test instance is _usually_ available at [upub.alemi.dev](https://upub.alemi.dev)
## about the database schema ## about the database schema
im going to be very real i tried to do migrations but its getting super messy so until further notice assume db to be volatile. next change may be a migration (easy!) or a whole db rebuild (aaaaaaaaaa...), so if you're not comfortable with either manually exporting/importing or dropping and starting from scratch, **you really shouldn't put upub in prod yet**! im going to be very real i tried to do migrations but its getting super messy so until further notice assume db to be volatile. next change may be a migration (easy!) or a whole db rebuild (aaaaaaaaaa...), so if you're not comfortable with either manually exporting/importing or dropping and starting from scratch, **you really shouldn't put upub in prod yet**!
@ -62,18 +62,19 @@ don't hesitate to get in touch, i'd be thrilled to showcase the project to you!
- [x] like, share, reply via frontend - [x] like, share, reply via frontend
- [x] backend config - [x] backend config
- [x] frontend config - [x] frontend config
- [x] optimize `addressing` database schema
- [ ] mentions, notifications - [ ] mentions, notifications
- [ ] hashtags
- [ ] public vs unlisted for discovery
- [ ] mastodon-like search bar - [ ] mastodon-like search bar
- [ ] polls - [ ] polls
- [ ] better editing via web frontend - [ ] better editing via web frontend
- [ ] remote media proxy - [ ] remote media proxy
- [ ] upload media - [ ] upload media
- [ ] hashtags
- [ ] public vs unlisted for discovery
- [ ] user fields - [ ] user fields
- [ ] lists - [ ] lists
- [ ] full mastodon api - [ ] full mastodon api
- [ ] optimize `addressing` database schema - [ ] get rid of internal ids from code
## what about the name? ## what about the name?
μpub (or simply `upub`) means "[micro](https://en.wikipedia.org/wiki/International_System_of_Units#Prefixes)-pub", but could also be read "upub", "you-pub" or "mu-pub" μpub (or simply `upub`) means "[micro](https://en.wikipedia.org/wiki/International_System_of_Units#Prefixes)-pub", but could also be read "upub", "you-pub" or "mu-pub"

View file

@ -357,49 +357,3 @@ pub fn set_maybe_value(obj: &mut serde_json::Value, key: &str, value: Option<ser
tracing::error!("error setting '{key}' on json Value: not an object"); tracing::error!("error setting '{key}' on json Value: not an object");
} }
} }
#[cfg(feature = "unstructured")]
pub(crate) trait InsertValue {
fn insert_node(&mut self, k: &str, v: crate::Node<serde_json::Value>);
fn insert_str(&mut self, k: &str, v: Option<&str>);
fn insert_float(&mut self, k: &str, f: Option<f64>);
fn insert_timestr(&mut self, k: &str, t: Option<chrono::DateTime<chrono::Utc>>);
}
#[cfg(feature = "unstructured")]
impl InsertValue for serde_json::Map<String, serde_json::Value> {
fn insert_node(&mut self, k: &str, node: crate::Node<serde_json::Value>) {
if !node.is_nothing() {
self.insert(k.to_string(), node.into());
}
}
fn insert_str(&mut self, k: &str, v: Option<&str>) {
if let Some(v) = v {
self.insert(
k.to_string(),
serde_json::Value::String(v.to_string()),
);
}
}
fn insert_float(&mut self, k: &str, v: Option<f64>) {
if let Some(v) = v {
if let Some(n) = serde_json::Number::from_f64(v) {
self.insert(
k.to_string(),
serde_json::Value::Number(n),
);
}
}
}
fn insert_timestr(&mut self, k: &str, t: Option<chrono::DateTime<chrono::Utc>>) {
if let Some(published) = t {
self.insert(
k.to_string(),
serde_json::Value::String(published.to_rfc3339()),
);
}
}
}

View file

@ -1,16 +1,17 @@
use crate::model::{addressing, config, credential, activity, object, user, Audience}; use crate::model::{addressing, config, credential, activity, object, actor, Audience};
use openssl::rsa::Rsa; use openssl::rsa::Rsa;
use sea_orm::IntoActiveModel; use sea_orm::{ActiveValue::NotSet, IntoActiveModel};
pub async fn faker(ctx: crate::server::Context, count: u64) -> Result<(), sea_orm::DbErr> { pub async fn faker(ctx: crate::server::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();
let db = ctx.db(); let db = ctx.db();
let key = Rsa::generate(2048).unwrap(); let key = Rsa::generate(2048).unwrap();
let test_user = user::Model { let test_user = actor::Model {
id: format!("{domain}/users/test"), internal: 42,
id: format!("{domain}/actors/test"),
name: Some("μpub".into()), name: Some("μpub".into()),
domain: clean_domain(domain), domain: clean_domain(domain),
preferred_username: "test".to_string(), preferred_username: "test".to_string(),
@ -19,24 +20,25 @@ pub async fn faker(ctx: crate::server::Context, count: u64) -> Result<(), sea_or
following_count: 0, following_count: 0,
followers: None, followers: None,
followers_count: 0, followers_count: 0,
statuses_count: count as i64, statuses_count: count as i32,
icon: Some("https://cdn.alemi.dev/social/circle-square.png".to_string()), icon: Some("https://cdn.alemi.dev/social/circle-square.png".to_string()),
image: Some("https://cdn.alemi.dev/social/someriver-xs.jpg".to_string()), image: Some("https://cdn.alemi.dev/social/someriver-xs.jpg".to_string()),
inbox: None, inbox: None,
shared_inbox: None, shared_inbox: None,
outbox: None, outbox: None,
actor_type: apb::ActorType::Person, actor_type: apb::ActorType::Person,
created: chrono::Utc::now(), published: chrono::Utc::now(),
updated: chrono::Utc::now(), updated: chrono::Utc::now(),
private_key: Some(std::str::from_utf8(&key.private_key_to_pem().unwrap()).unwrap().to_string()), private_key: Some(std::str::from_utf8(&key.private_key_to_pem().unwrap()).unwrap().to_string()),
// TODO generate a fresh one every time // TODO generate a fresh one every time
public_key: std::str::from_utf8(&key.public_key_to_pem().unwrap()).unwrap().to_string(), public_key: std::str::from_utf8(&key.public_key_to_pem().unwrap()).unwrap().to_string(),
}; };
user::Entity::insert(test_user.clone().into_active_model()).exec(db).await?; actor::Entity::insert(test_user.clone().into_active_model()).exec(db).await?;
config::Entity::insert(config::ActiveModel { config::Entity::insert(config::ActiveModel {
id: Set(test_user.id.clone()), internal: NotSet,
actor: Set(test_user.id.clone()),
accept_follow_requests: Set(true), accept_follow_requests: Set(true),
show_followers: Set(true), show_followers: Set(true),
show_following: Set(true), show_following: Set(true),
@ -45,8 +47,9 @@ pub async fn faker(ctx: crate::server::Context, count: u64) -> Result<(), sea_or
}).exec(db).await?; }).exec(db).await?;
credential::Entity::insert(credential::ActiveModel { credential::Entity::insert(credential::ActiveModel {
id: Set(test_user.id.clone()), internal: NotSet,
email: Set("mail@example.net".to_string()), actor: Set(test_user.id.clone()),
login: Set("mail@example.net".to_string()),
password: Set(sha256::digest("very-strong-password")), password: Set(sha256::digest("very-strong-password")),
}).exec(db).await?; }).exec(db).await?;
@ -57,28 +60,29 @@ pub async fn faker(ctx: crate::server::Context, count: u64) -> Result<(), sea_or
let aid = uuid::Uuid::new_v4(); let aid = uuid::Uuid::new_v4();
addressing::Entity::insert(addressing::ActiveModel { addressing::Entity::insert(addressing::ActiveModel {
actor: Set(apb::target::PUBLIC.to_string()), actor: Set(None),
server: Set("www.w3.org".to_string()), instance: Set(None),
activity: Set(Some(format!("{domain}/activities/{aid}"))), activity: Set(Some(42 + i)),
object: Set(Some(format!("{domain}/objects/{oid}"))), object: Set(Some(42 + i)),
published: Set(chrono::Utc::now()), published: Set(chrono::Utc::now()),
..Default::default() ..Default::default()
}).exec(db).await?; }).exec(db).await?;
object::Entity::insert(object::ActiveModel { object::Entity::insert(object::ActiveModel {
internal: Set(42 + i),
id: Set(format!("{domain}/objects/{oid}")), id: Set(format!("{domain}/objects/{oid}")),
name: Set(None), name: Set(None),
object_type: Set(apb::ObjectType::Note), object_type: Set(apb::ObjectType::Note),
attributed_to: Set(Some(format!("{domain}/users/test"))), attributed_to: Set(Some(format!("{domain}/actors/test"))),
summary: Set(None), summary: Set(None),
context: Set(Some(context.clone())), context: Set(Some(context.clone())),
in_reply_to: Set(None), in_reply_to: Set(None),
content: Set(Some(format!("[{i}] Tic(k). Quasiparticle of intensive multiplicity. Tics (or ticks) are intrinsically several components of autonomously numbering anorganic populations, propagating by contagion between segmentary divisions in the order of nature. Ticks - as nonqualitative differentially-decomposable counting marks - each designate a multitude comprehended as a singular variation in tic(k)-density."))), content: Set(Some(format!("[{i}] Tic(k). Quasiparticle of intensive multiplicity. Tics (or ticks) are intrinsically several components of autonomously numbering anorganic populations, propagating by contagion between segmentary divisions in the order of nature. Ticks - as nonqualitative differentially-decomposable counting marks - each designate a multitude comprehended as a singular variation in tic(k)-density."))),
published: Set(chrono::Utc::now() - std::time::Duration::from_secs(60*i)), published: Set(chrono::Utc::now() - std::time::Duration::from_secs(60*i as u64)),
updated: Set(None), updated: Set(chrono::Utc::now()),
comments: Set(0), replies: Set(0),
likes: Set(0), likes: Set(0),
shares: Set(0), announces: Set(0),
to: Set(Audience(vec![apb::target::PUBLIC.to_string()])), to: Set(Audience(vec![apb::target::PUBLIC.to_string()])),
bto: Set(Audience::default()), bto: Set(Audience::default()),
cc: Set(Audience(vec![])), cc: Set(Audience(vec![])),
@ -88,12 +92,13 @@ pub async fn faker(ctx: crate::server::Context, count: u64) -> Result<(), sea_or
}).exec(db).await?; }).exec(db).await?;
activity::Entity::insert(activity::ActiveModel { activity::Entity::insert(activity::ActiveModel {
internal: Set(42 + i),
id: Set(format!("{domain}/activities/{aid}")), id: Set(format!("{domain}/activities/{aid}")),
activity_type: Set(apb::ActivityType::Create), activity_type: Set(apb::ActivityType::Create),
actor: Set(format!("{domain}/users/test")), actor: Set(format!("{domain}/actors/test")),
object: Set(Some(format!("{domain}/objects/{oid}"))), object: Set(Some(format!("{domain}/objects/{oid}"))),
target: Set(None), target: Set(None),
published: Set(chrono::Utc::now() - std::time::Duration::from_secs(60*i)), published: Set(chrono::Utc::now() - std::time::Duration::from_secs(60*i as u64)),
to: Set(Audience(vec![apb::target::PUBLIC.to_string()])), to: Set(Audience(vec![apb::target::PUBLIC.to_string()])),
bto: Set(Audience::default()), bto: Set(Audience::default()),
cc: Set(Audience(vec![])), cc: Set(Audience(vec![])),

View file

@ -1,6 +1,6 @@
use sea_orm::{EntityTrait, IntoActiveModel}; use sea_orm::EntityTrait;
use crate::server::fetcher::Fetchable; 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: crate::server::Context, uri: String, save: bool) -> crate::Result<()> {
use apb::Base; use apb::Base;
@ -8,24 +8,23 @@ pub async fn fetch(ctx: crate::server::Context, uri: String, save: bool) -> crat
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.get().expect("node still empty after fetch?"); let obj = node.extract().expect("node still empty after fetch?");
let server = Context::server(&uri);
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::user::Entity::insert( crate::model::actor::Entity::insert(
crate::model::user::Model::new(obj).unwrap().into_active_model() crate::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(_))) => {
crate::model::activity::Entity::insert( ctx.insert_activity(obj, Some(server)).await.unwrap();
crate::model::activity::Model::new(obj).unwrap().into_active_model()
).exec(ctx.db()).await.unwrap();
}, },
Some(apb::BaseType::Object(apb::ObjectType::Note)) => { Some(apb::BaseType::Object(apb::ObjectType::Note)) => {
crate::model::object::Entity::insert( ctx.insert_object(obj, Some(server)).await.unwrap();
crate::model::object::Model::new(obj).unwrap().into_active_model()
).exec(ctx.db()).await.unwrap();
}, },
Some(apb::BaseType::Object(t)) => tracing::warn!("not implemented: {:?}", t), Some(apb::BaseType::Object(t)) => tracing::warn!("not implemented: {:?}", t),
Some(apb::BaseType::Link(_)) => tracing::error!("fetched another link?"), Some(apb::BaseType::Link(_)) => tracing::error!("fetched another link?"),
@ -33,7 +32,5 @@ pub async fn fetch(ctx: crate::server::Context, uri: String, save: bool) -> crat
} }
} }
println!("{}", serde_json::to_string_pretty(&obj).unwrap());
Ok(()) Ok(())
} }

View file

@ -11,13 +11,13 @@ pub async fn fix(ctx: crate::server::Context, likes: bool, shares: bool, replies
{ {
let mut stream = crate::model::like::Entity::find().stream(db).await?; let mut stream = crate::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.likes.clone(), store.get(&like.likes).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 = crate::model::object::ActiveModel {
id: sea_orm::Set(k.clone()), internal: sea_orm::Set(k),
likes: sea_orm::Set(v), likes: sea_orm::Set(v),
..Default::default() ..Default::default()
}; };
@ -34,16 +34,16 @@ 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::share::Entity::find().stream(db).await?; let mut stream = crate::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.shares.clone(), store.get(&share.shares).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 = crate::model::object::ActiveModel {
id: sea_orm::Set(k.clone()), internal: sea_orm::Set(k),
shares: 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) = crate::model::object::Entity::update(m)
@ -71,7 +71,7 @@ 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 = crate::model::object::ActiveModel {
id: sea_orm::Set(k.clone()), id: sea_orm::Set(k.clone()),
comments: 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) = crate::model::object::Entity::update(m)

View file

@ -104,7 +104,7 @@ pub async fn run(
).await?; ).await?;
match command { match command {
CliCommand::Faker { count } => CliCommand::Faker { count } =>
Ok(faker(ctx, count).await?), Ok(faker(ctx, count as i64).await?),
CliCommand::Fetch { uri, save } => CliCommand::Fetch { uri, save } =>
Ok(fetch(ctx, uri, save).await?), Ok(fetch(ctx, uri, save).await?),
CliCommand::Relay { actor, accept } => CliCommand::Relay { actor, accept } =>

View file

@ -1,19 +1,22 @@
use sea_orm::{ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter, QueryOrder}; 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<()> { 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 aid = ctx.aid(&uuid::Uuid::new_v4().to_string());
let mut activity_model = crate::model::activity::Model { let mut activity_model = crate::model::activity::ActiveModel {
id: aid.clone(), internal: NotSet,
activity_type: apb::ActivityType::Follow, id: Set(aid.clone()),
actor: ctx.base().to_string(), activity_type: Set(apb::ActivityType::Follow),
object: Some(actor.clone()), actor: Set(ctx.base().to_string()),
target: None, object: Set(Some(actor.clone())),
published: chrono::Utc::now(), target: Set(None),
to: crate::model::Audience(vec![actor.clone()]), published: Set(chrono::Utc::now()),
bto: crate::model::Audience::default(), to: Set(crate::model::Audience(vec![actor.clone()])),
cc: crate::model::Audience(vec![apb::target::PUBLIC.to_string()]), bto: Set(crate::model::Audience::default()),
bcc: crate::model::Audience::default(), cc: Set(crate::model::Audience(vec![apb::target::PUBLIC.to_string()])),
bcc: Set(crate::model::Audience::default()),
}; };
if accept { if accept {
@ -25,11 +28,11 @@ pub async fn relay(ctx: crate::server::Context, actor: String, accept: bool) ->
.one(ctx.db()) .one(ctx.db())
.await? .await?
.expect("no follow request to accept"); .expect("no follow request to accept");
activity_model.activity_type = apb::ActivityType::Accept(apb::AcceptType::Accept); activity_model.activity_type = Set(apb::ActivityType::Accept(apb::AcceptType::Accept));
activity_model.object = Some(follow_req.id); activity_model.object = Set(Some(follow_req.id));
}; };
crate::model::activity::Entity::insert(activity_model.into_active_model()) crate::model::activity::Entity::insert(activity_model)
.exec(ctx.db()).await?; .exec(ctx.db()).await?;
ctx.dispatch(ctx.base(), vec![actor, apb::target::PUBLIC.to_string()], &aid, None).await?; ctx.dispatch(ctx.base(), vec![actor, apb::target::PUBLIC.to_string()], &aid, None).await?;

View file

@ -1,5 +1,5 @@
use futures::TryStreamExt; use futures::TryStreamExt;
use sea_orm::{ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter}; use sea_orm::{ActiveValue::Set, ColumnTrait, EntityTrait, QueryFilter};
use crate::server::fetcher::Fetcher; use crate::server::fetcher::Fetcher;
@ -8,28 +8,35 @@ pub async fn update_users(ctx: crate::server::Context, days: i64) -> crate::Resu
let mut insertions = Vec::new(); let mut insertions = Vec::new();
{ {
let mut stream = crate::model::user::Entity::find() let mut stream = crate::model::actor::Entity::find()
.filter(crate::model::user::Column::Updated.lt(chrono::Utc::now() - chrono::Duration::days(days))) .filter(crate::model::actor::Column::Updated.lt(chrono::Utc::now() - chrono::Duration::days(days)))
.stream(ctx.db()) .stream(ctx.db())
.await?; .await?;
while let Some(user) = stream.try_next().await? { while let Some(user) = stream.try_next().await? {
if ctx.is_local(&user.id) { continue } if ctx.is_local(&user.id) { continue }
match ctx.pull_user(&user.id).await { 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(u) => { Ok(Err(e)) => tracing::warn!("could not update user {}: {e}", user.id),
insertions.push(u); Ok(Ok(doc)) => match crate::model::actor::ActiveModel::new(&doc) {
count += 1; Ok(mut u) => {
u.internal = Set(user.internal);
u.updated = Set(chrono::Utc::now());
insertions.push((user.id, u));
count += 1;
},
Err(e) => tracing::warn!("failed deserializing user '{}': {e}", user.id),
}, },
} }
} }
} }
for u in insertions { for (uid, user_model) in insertions {
tracing::info!("updating user {}", u.id); tracing::info!("updating user {}", uid);
crate::model::user::Entity::delete_by_id(&u.id).exec(ctx.db()).await?; crate::model::actor::Entity::update(user_model)
crate::model::user::Entity::insert(u.into_active_model()).exec(ctx.db()).await?; .exec(ctx.db())
.await?;
} }
tracing::info!("updated {count} users"); tracing::info!("updated {count} users");

View file

@ -70,6 +70,15 @@ pub struct SecurityConfig {
#[serde_inline_default(true)] #[serde_inline_default(true)]
pub show_reply_ids: bool, pub show_reply_ids: bool,
#[serde(default)]
pub allow_login_refresh: bool,
#[serde_inline_default(2)]
pub max_id_redirects: u32,
#[serde_inline_default(20)]
pub thread_crawl_depth: u32,
} }

View file

@ -28,6 +28,9 @@ pub enum UpubError {
#[error("invalid base64 string: {0:?}")] #[error("invalid base64 string: {0:?}")]
Base64(#[from] base64::DecodeError), Base64(#[from] base64::DecodeError),
#[error("type mismatch on object: expected {0:?}, found {1:?}")]
Mismatch(apb::ObjectType, apb::ObjectType),
// 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}")]
@ -62,6 +65,10 @@ impl UpubError {
pub fn internal_server_error() -> Self { pub fn internal_server_error() -> Self {
Self::Status(axum::http::StatusCode::INTERNAL_SERVER_ERROR) Self::Status(axum::http::StatusCode::INTERNAL_SERVER_ERROR)
} }
pub fn field(field: &'static str) -> Self {
Self::Field(crate::model::FieldError(field))
}
} }
pub type UpubResult<T> = Result<T, UpubError>; pub type UpubResult<T> = Result<T, UpubError>;
@ -74,6 +81,9 @@ impl From<axum::http::StatusCode> for UpubError {
impl axum::response::IntoResponse for UpubError { impl axum::response::IntoResponse for UpubError {
fn into_response(self) -> axum::response::Response { fn into_response(self) -> axum::response::Response {
// TODO it's kind of jank to hide this print down here, i should probably learn how spans work
// in tracing and use the library's features but ehhhh
tracing::debug!("emitting error response: {self:?}");
match self { match self {
UpubError::Redirect(to) => Redirect::to(&to).into_response(), UpubError::Redirect(to) => Redirect::to(&to).into_response(),
UpubError::Status(status) => status.into_response(), UpubError::Status(status) => status.into_response(),
@ -101,6 +111,15 @@ impl axum::response::IntoResponse for UpubError {
"description": format!("missing required field from request: '{}'", x.0), "description": format!("missing required field from request: '{}'", x.0),
})) }))
).into_response(), ).into_response(),
UpubError::Mismatch(expected, found) => (
axum::http::StatusCode::UNPROCESSABLE_ENTITY,
axum::Json(serde_json::json!({
"error": "type",
"expected": expected.as_ref().to_string(),
"found": found.as_ref().to_string(),
"description": self.to_string(),
}))
).into_response(),
_ => ( _ => (
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(serde_json::json!({ axum::Json(serde_json::json!({

View file

@ -1,8 +1,8 @@
pub mod server; // TODO there are some methods that i dont use yet, make it public so that ra shuts up mod server;
mod model; mod model;
mod routes; mod routes;
mod errors; pub mod errors;
mod config; mod config;
#[cfg(feature = "cli")] #[cfg(feature = "cli")]
@ -51,7 +51,11 @@ struct Args {
#[derive(Clone, Subcommand)] #[derive(Clone, Subcommand)]
enum Mode { enum Mode {
/// run fediverse server /// run fediverse server
Serve, 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,
@ -88,13 +92,14 @@ async fn main() {
let mut opts = ConnectOptions::new(&database); let mut opts = ConnectOptions::new(&database);
opts opts
.sqlx_logging(true)
.sqlx_logging_level(tracing::log::LevelFilter::Debug) .sqlx_logging_level(tracing::log::LevelFilter::Debug)
.max_connections(config.datasource.max_connections) .max_connections(config.datasource.max_connections)
.min_connections(config.datasource.min_connections) .min_connections(config.datasource.min_connections)
.acquire_timeout(std::time::Duration::from_secs(config.datasource.acquire_timeout_seconds)) .acquire_timeout(std::time::Duration::from_secs(config.datasource.acquire_timeout_seconds))
.connect_timeout(std::time::Duration::from_secs(config.datasource.connect_timeout_seconds)) .connect_timeout(std::time::Duration::from_secs(config.datasource.connect_timeout_seconds))
.sqlx_slow_statements_logging_settings( .sqlx_slow_statements_logging_settings(
if config.datasource.slow_query_warn_enable { tracing::log::LevelFilter::Warn } else { tracing::log::LevelFilter::Off }, if config.datasource.slow_query_warn_enable { tracing::log::LevelFilter::Warn } else { tracing::log::LevelFilter::Debug },
std::time::Duration::from_secs(config.datasource.slow_query_warn_seconds) std::time::Duration::from_secs(config.datasource.slow_query_warn_seconds)
); );
@ -114,7 +119,7 @@ async fn main() {
Mode::Config => println!("{}", toml::to_string_pretty(&config).expect("failed serializing config")), Mode::Config => println!("{}", toml::to_string_pretty(&config).expect("failed serializing config")),
Mode::Serve => { Mode::Serve { bind } => {
let ctx = server::Context::new(db, domain, config) let ctx = server::Context::new(db, domain, config)
.await.expect("failed creating server context"); .await.expect("failed creating server context");
@ -129,7 +134,7 @@ async fn main() {
.with_state(ctx); .with_state(ctx);
// run our app with hyper, listening locally on port 3000 // run our app with hyper, listening locally on port 3000
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000") let listener = tokio::net::TcpListener::bind(bind)
.await.expect("could not bind tcp socket"); .await.expect("could not bind tcp socket");
axum::serve(listener, router) axum::serve(listener, router)

View file

@ -1,169 +0,0 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Users::Table)
.col(
ColumnDef::new(Users::Id)
.string()
.not_null()
.primary_key()
)
.col(ColumnDef::new(Users::ActorType).string().not_null())
.col(ColumnDef::new(Users::Domain).string().not_null())
.col(ColumnDef::new(Users::Name).string().null())
.col(ColumnDef::new(Users::Summary).string().null())
.col(ColumnDef::new(Users::Image).string().null())
.col(ColumnDef::new(Users::Icon).string().null())
.col(ColumnDef::new(Users::PreferredUsername).string().not_null())
.col(ColumnDef::new(Users::Inbox).string().null())
.col(ColumnDef::new(Users::SharedInbox).string().null())
.col(ColumnDef::new(Users::Outbox).string().null())
.col(ColumnDef::new(Users::Following).string().null())
.col(ColumnDef::new(Users::Followers).string().null())
.col(ColumnDef::new(Users::FollowingCount).integer().not_null().default(0))
.col(ColumnDef::new(Users::FollowersCount).integer().not_null().default(0))
// .col(ColumnDef::new(Users::StatusesCount).integer().not_null().default(0))
.col(ColumnDef::new(Users::PublicKey).string().not_null())
.col(ColumnDef::new(Users::PrivateKey).string().null())
.col(ColumnDef::new(Users::Created).date_time().not_null())
.col(ColumnDef::new(Users::Updated).date_time().not_null())
.to_owned()
)
.await?;
manager
.create_table(
Table::create()
.table(Activities::Table)
.col(
ColumnDef::new(Activities::Id)
.string()
.not_null()
.primary_key()
)
.col(ColumnDef::new(Activities::ActivityType).string().not_null())
.col(ColumnDef::new(Activities::Actor).string().not_null())
.col(ColumnDef::new(Activities::Object).string().null())
.col(ColumnDef::new(Activities::Target).string().null())
.col(ColumnDef::new(Activities::To).json().null())
.col(ColumnDef::new(Activities::Bto).json().null())
.col(ColumnDef::new(Activities::Cc).json().null())
.col(ColumnDef::new(Activities::Bcc).json().null())
.col(ColumnDef::new(Activities::Published).date_time().not_null())
.to_owned()
).await?;
manager
.create_table(
Table::create()
.table(Objects::Table)
.col(
ColumnDef::new(Objects::Id)
.string()
.not_null()
.primary_key()
)
.col(ColumnDef::new(Objects::ObjectType).string().not_null())
.col(ColumnDef::new(Objects::AttributedTo).string().null())
.col(ColumnDef::new(Objects::Name).string().null())
.col(ColumnDef::new(Objects::Summary).string().null())
.col(ColumnDef::new(Objects::Content).string().null())
.col(ColumnDef::new(Objects::Likes).integer().not_null().default(0))
.col(ColumnDef::new(Objects::Shares).integer().not_null().default(0))
.col(ColumnDef::new(Objects::Comments).integer().not_null().default(0))
.col(ColumnDef::new(Objects::Context).string().null())
.col(ColumnDef::new(Objects::To).json().null())
.col(ColumnDef::new(Objects::Bto).json().null())
.col(ColumnDef::new(Objects::Cc).json().null())
.col(ColumnDef::new(Objects::Bcc).json().null())
.col(ColumnDef::new(Objects::Published).string().not_null())
.to_owned()
).await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Users::Table).to_owned())
.await?;
manager
.drop_table(Table::drop().table(Activities::Table).to_owned())
.await?;
manager
.drop_table(Table::drop().table(Objects::Table).to_owned())
.await?;
Ok(())
}
}
#[derive(DeriveIden)]
enum Users {
Table,
Id,
Domain,
ActorType,
Name,
Summary,
Image,
Icon,
PreferredUsername,
Inbox,
SharedInbox,
Outbox,
Following,
FollowingCount,
Followers,
FollowersCount,
// StatusesCount,
PublicKey,
PrivateKey,
Created,
Updated,
}
#[derive(DeriveIden)]
enum Activities {
Table,
Id,
ActivityType,
Actor,
Object,
Target,
Cc,
Bcc,
To,
Bto,
Published,
}
#[derive(DeriveIden)]
enum Objects {
Table,
Id,
ObjectType,
AttributedTo,
Name,
Summary,
Content,
Likes,
Shares,
Comments,
Context,
Cc,
Bcc,
To,
Bto,
Published,
}

View file

@ -1,44 +0,0 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Relations::Table)
.col(
ColumnDef::new(Relations::Id)
.integer()
.auto_increment()
.not_null()
.primary_key()
)
.col(ColumnDef::new(Relations::Follower).string().not_null())
.col(ColumnDef::new(Relations::Following).string().not_null())
.to_owned()
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Relations::Table).to_owned())
.await?;
Ok(())
}
}
#[derive(DeriveIden)]
enum Relations {
Table,
Id,
Follower,
Following,
}

View file

@ -1,80 +0,0 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Likes::Table)
.col(
ColumnDef::new(Likes::Id)
.integer()
.auto_increment()
.not_null()
.primary_key()
)
.col(ColumnDef::new(Likes::Actor).string().not_null())
.col(ColumnDef::new(Likes::Likes).string().not_null())
.col(ColumnDef::new(Likes::Date).date_time().not_null())
.to_owned()
)
.await?;
manager
.create_table(
Table::create()
.table(Shares::Table)
.col(
ColumnDef::new(Shares::Id)
.integer()
.auto_increment()
.not_null()
.primary_key()
)
.col(ColumnDef::new(Shares::Actor).string().not_null())
.col(ColumnDef::new(Shares::Shares).string().not_null())
.col(ColumnDef::new(Shares::Date).date_time().not_null())
.to_owned()
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Likes::Table).to_owned())
.await?;
manager
.drop_table(Table::drop().table(Shares::Table).to_owned())
.await?;
Ok(())
}
}
#[derive(DeriveIden)]
#[allow(clippy::enum_variant_names)]
enum Likes {
Table,
Id,
Actor,
Likes,
Date,
}
#[derive(DeriveIden)]
#[allow(clippy::enum_variant_names)]
enum Shares {
Table,
Id,
Actor,
Shares,
Date,
}

View file

@ -1,176 +0,0 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_index(
Index::create()
.name("user-domain-index")
.table(Users::Table)
.col(Users::Domain)
.to_owned()
)
.await?;
manager
.create_index(
Index::create()
.name("activities-published-descending-index")
.table(Activities::Table)
.col((Activities::Published, IndexOrder::Desc))
.to_owned()
)
.await?;
manager
.create_index(
Index::create()
.name("activities-actor-index")
.table(Activities::Table)
.col(Activities::Actor)
.to_owned()
)
.await?;
manager
.create_index(
Index::create()
.name("activities-object-index")
.table(Activities::Table)
.col(Activities::Object)
.to_owned()
).await?;
manager
.create_index(
Index::create()
.name("objects-attributed-to-index")
.table(Objects::Table)
.col(Objects::AttributedTo)
.to_owned()
).await?;
manager
.create_index(
Index::create()
.name("shares-actor-index")
.table(Shares::Table)
.col(Shares::Actor)
.to_owned()
).await?;
manager
.create_index(
Index::create()
.name("shares-shares-index")
.table(Shares::Table)
.col(Shares::Shares)
.to_owned()
).await?;
manager
.create_index(
Index::create()
.name("likes-actor-index")
.table(Likes::Table)
.col(Likes::Actor)
.to_owned()
).await?;
manager
.create_index(
Index::create()
.name("likes-likes-index")
.table(Likes::Table)
.col(Likes::Likes)
.to_owned()
).await?;
manager
.create_index(
Index::create()
.name("likes-actor-likes-index")
.table(Likes::Table)
.col(Likes::Actor)
.col(Likes::Likes)
.unique()
.to_owned()
).await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_index(Index::drop().name("user-domain-index").to_owned())
.await?;
manager
.drop_index(Index::drop().name("activities-published-descending-index").to_owned())
.await?;
manager
.drop_index(Index::drop().name("activities-actor-index").to_owned())
.await?;
manager
.drop_index(Index::drop().name("activities-object-index").to_owned())
.await?;
manager
.drop_index(Index::drop().name("objects-attributed-to-index").to_owned())
.await?;
manager
.drop_index(Index::drop().name("shares-actor-index").to_owned())
.await?;
manager
.drop_index(Index::drop().name("shares-shares-index").to_owned())
.await?;
manager
.drop_index(Index::drop().name("likes-actor-index").to_owned())
.await?;
manager
.drop_index(Index::drop().name("likes-likes-index").to_owned())
.await?;
manager
.drop_index(Index::drop().name("likes-actor-likes-index").to_owned())
.await?;
Ok(())
}
}
#[derive(DeriveIden)]
#[allow(clippy::enum_variant_names)]
enum Likes {
Table,
Actor,
Likes,
}
#[derive(DeriveIden)]
#[allow(clippy::enum_variant_names)]
enum Shares {
Table,
Actor,
Shares,
}
#[derive(DeriveIden)]
enum Users {
Table,
Domain,
}
#[derive(DeriveIden)]
enum Activities {
Table,
Actor,
Object,
Published,
}
#[derive(DeriveIden)]
enum Objects {
Table,
AttributedTo,
}

View file

@ -1,49 +0,0 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Configs::Table)
.col(
ColumnDef::new(Configs::Id)
.string()
.not_null()
.primary_key()
)
.col(ColumnDef::new(Configs::AcceptFollowRequests).boolean().not_null())
.col(ColumnDef::new(Configs::ShowFollowersCount).boolean().not_null())
.col(ColumnDef::new(Configs::ShowFollowingCount).boolean().not_null())
.col(ColumnDef::new(Configs::ShowFollowers).boolean().not_null())
.col(ColumnDef::new(Configs::ShowFollowing).boolean().not_null())
.to_owned()
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Configs::Table).to_owned())
.await?;
Ok(())
}
}
#[derive(DeriveIden)]
enum Configs {
Table,
Id,
AcceptFollowRequests,
ShowFollowersCount,
ShowFollowingCount,
ShowFollowers,
ShowFollowing,
}

View file

@ -1,67 +0,0 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Credentials::Table)
.col(
ColumnDef::new(Credentials::Id)
.string()
.not_null()
.primary_key()
)
.col(ColumnDef::new(Credentials::Email).string().not_null())
.col(ColumnDef::new(Credentials::Password).string().not_null())
.to_owned()
)
.await?;
manager
.create_table(
Table::create()
.table(Sessions::Table)
.col(
ColumnDef::new(Sessions::Id)
.string()
.not_null()
.primary_key()
)
.col(ColumnDef::new(Sessions::Actor).string().not_null())
.col(ColumnDef::new(Sessions::Expires).date_time().not_null())
.to_owned()
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Credentials::Table).to_owned())
.await?;
Ok(())
}
}
#[derive(DeriveIden)]
enum Credentials {
Table,
Id,
Email,
Password,
}
#[derive(DeriveIden)]
enum Sessions {
Table,
Id, // TODO here ID is the session "secret" but in Credentials it's the actor ID (String) ??? weird!!
Actor,
Expires,
}

View file

@ -1,110 +0,0 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Addressing::Table)
.col(
ColumnDef::new(Addressing::Id)
.integer()
.not_null()
.auto_increment()
.primary_key()
)
.col(ColumnDef::new(Addressing::Actor).string().not_null())
.col(ColumnDef::new(Addressing::Server).string().not_null())
.col(ColumnDef::new(Addressing::Activity).string().null())
.col(ColumnDef::new(Addressing::Object).string().null())
.col(ColumnDef::new(Addressing::Published).date_time().not_null())
.to_owned()
)
.await?;
// TODO these indexes may not be ordered, killing out timeline query performance
// it may be necessary to include datetime in the index itself? or maybe specify
// some ordering to use another type of indes?
manager
.create_index(
Index::create()
.name("addressing-actor-index")
.table(Addressing::Table)
.col(Addressing::Actor)
.to_owned()
)
.await?;
manager
.create_index(
Index::create()
.name("addressing-server-index")
.table(Addressing::Table)
.col(Addressing::Server)
.to_owned()
)
.await?;
manager
.create_index(
Index::create()
.name("addressing-activity-index")
.table(Addressing::Table)
.col(Addressing::Activity)
.to_owned()
)
.await?;
manager
.create_index(
Index::create()
.name("addressing-object-index")
.table(Addressing::Table)
.col(Addressing::Object)
.to_owned()
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Addressing::Table).to_owned())
.await?;
manager
.drop_index(Index::drop().name("addressing-actor-index").to_owned())
.await?;
manager
.drop_index(Index::drop().name("addressing-server-index").to_owned())
.await?;
manager
.drop_index(Index::drop().name("addressing-activity-index").to_owned())
.await?;
manager
.drop_index(Index::drop().name("addressing-object-index").to_owned())
.await?;
Ok(())
}
}
#[derive(DeriveIden)]
enum Addressing {
Table,
Id,
Actor,
Server,
Activity,
Object,
Published,
}

View file

@ -1,66 +0,0 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Deliveries::Table)
.col(
ColumnDef::new(Deliveries::Id)
.integer()
.not_null()
.auto_increment()
.primary_key()
)
.col(ColumnDef::new(Deliveries::Actor).string().not_null())
.col(ColumnDef::new(Deliveries::Target).string().not_null())
.col(ColumnDef::new(Deliveries::Activity).string().not_null())
.col(ColumnDef::new(Deliveries::Created).date_time().not_null())
.col(ColumnDef::new(Deliveries::NotBefore).date_time().not_null())
.col(ColumnDef::new(Deliveries::Attempt).integer().not_null())
.to_owned()
)
.await?;
manager
.create_index(
Index::create()
.name("deliveries-notbefore-index")
.table(Deliveries::Table)
.col((Deliveries::NotBefore, IndexOrder::Asc))
.to_owned()
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Deliveries::Table).to_owned())
.await?;
manager
.drop_index(Index::drop().name("deliveries-notbefore-index").to_owned())
.await?;
Ok(())
}
}
#[derive(DeriveIden)]
enum Deliveries {
Table,
Id,
Actor,
Target,
Activity,
Created,
NotBefore,
Attempt,
}

View file

@ -1,46 +0,0 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Application::Table)
.col(
ColumnDef::new(Application::Id)
.integer()
.not_null()
.auto_increment()
.primary_key()
)
.col(ColumnDef::new(Application::PrivateKey).string().not_null())
.col(ColumnDef::new(Application::PublicKey).string().not_null())
.col(ColumnDef::new(Application::Created).date_time().not_null())
.to_owned()
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Application::Table).to_owned())
.await?;
Ok(())
}
}
#[derive(DeriveIden)]
enum Application {
Table,
Id,
PrivateKey,
PublicKey,
Created,
}

View file

@ -1,72 +0,0 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(Users::Table)
.add_column(
ColumnDef::new(Users::StatusesCount)
.integer()
.not_null()
.default(0)
)
.to_owned()
)
.await?;
manager
.alter_table(
Table::alter()
.table(Objects::Table)
.add_column(
ColumnDef::new(Objects::InReplyTo)
.string()
.null()
)
.to_owned()
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(Users::Table)
.drop_column(Users::StatusesCount)
.to_owned()
)
.await?;
manager
.alter_table(
Table::alter()
.table(Objects::Table)
.drop_column(Objects::InReplyTo)
.to_owned()
)
.await?;
Ok(())
}
}
#[derive(DeriveIden)]
enum Users {
Table,
StatusesCount,
}
#[derive(DeriveIden)]
enum Objects {
Table,
InReplyTo,
}

View file

@ -1,66 +0,0 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Attachments::Table)
.col(
ColumnDef::new(Attachments::Id)
.integer()
.not_null()
.auto_increment()
.primary_key()
)
.col(ColumnDef::new(Attachments::Url).string().not_null())
.col(ColumnDef::new(Attachments::Object).string().not_null())
.col(ColumnDef::new(Attachments::DocumentType).string().not_null())
.col(ColumnDef::new(Attachments::Name).string().null())
.col(ColumnDef::new(Attachments::MediaType).string().not_null())
.col(ColumnDef::new(Attachments::Created).date_time().not_null())
.to_owned()
)
.await?;
manager
.create_index(
Index::create()
.name("attachment-object-index")
.table(Attachments::Table)
.col(Attachments::Object)
.to_owned()
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Attachments::Table).to_owned())
.await?;
manager
.drop_index(Index::drop().name("attachment-object-index").to_owned())
.await?;
Ok(())
}
}
#[derive(DeriveIden)]
enum Attachments {
Table,
Id,
Url,
Object,
DocumentType,
Name,
MediaType,
Created,
}

View file

@ -1,45 +0,0 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(Objects::Table)
.add_column(
ColumnDef::new(Objects::Sensitive)
.boolean()
.not_null()
.default(false)
)
.to_owned()
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(Objects::Table)
.drop_column(Objects::Sensitive)
.to_owned()
)
.await?;
Ok(())
}
}
#[derive(DeriveIden)]
enum Objects {
Table,
Sensitive,
}

View file

@ -1,43 +0,0 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Relays::Table)
.col(
ColumnDef::new(Relays::Id)
.string()
.not_null()
.primary_key()
)
.col(ColumnDef::new(Relays::Accepted).boolean().not_null().default(false))
.col(ColumnDef::new(Relays::Forwarding).boolean().not_null().default(false))
.to_owned()
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Relays::Table).to_owned())
.await?;
Ok(())
}
}
#[derive(DeriveIden)]
enum Relays {
Table,
Id,
Accepted,
Forwarding,
}

View file

@ -1,40 +0,0 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(Objects::Table)
.add_column(ColumnDef::new(Objects::Updated).date_time().null())
.to_owned()
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(Objects::Table)
.drop_column(Objects::Updated)
.to_owned()
)
.await?;
Ok(())
}
}
#[derive(DeriveIden)]
enum Objects {
Table,
Updated,
}

View file

@ -1,82 +0,0 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_index(Index::drop().name("addressing-actor-index").to_owned())
.await?;
manager
.create_index(
Index::create()
.name("addressing-actor-published-index")
.table(Addressing::Table)
.col(Addressing::Actor)
.col(Addressing::Published)
.to_owned()
)
.await?;
manager
.drop_index(Index::drop().name("addressing-server-index").to_owned())
.await?;
manager
.create_index(
Index::create()
.name("addressing-server-published-index")
.table(Addressing::Table)
.col(Addressing::Server)
.col(Addressing::Published)
.to_owned()
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_index(Index::drop().name("addressing-actor-published-index").to_owned())
.await?;
manager
.create_index(
Index::create()
.name("addressing-actor-index")
.table(Addressing::Table)
.col(Addressing::Actor)
.to_owned()
)
.await?;
manager
.drop_index(Index::drop().name("addressing-server-published-index").to_owned())
.await?;
manager
.create_index(
Index::create()
.name("addressing-server-index")
.table(Addressing::Table)
.col(Addressing::Server)
.to_owned()
)
.await?;
Ok(())
}
}
#[derive(DeriveIden)]
enum Addressing {
Table,
Actor,
Server,
Published,
}

View file

@ -0,0 +1,392 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveIden)]
pub enum Actors {
Table,
Internal,
Id,
Domain,
ActorType,
Name,
Summary,
Image,
Icon,
PreferredUsername,
Inbox,
SharedInbox,
Outbox,
Following,
FollowingCount,
Followers,
FollowersCount,
StatusesCount,
PublicKey,
PrivateKey,
Published,
Updated,
}
#[derive(DeriveIden)]
pub enum Activities {
Table,
Internal,
Id,
ActivityType,
Actor,
Object,
Target,
Cc,
Bcc,
To,
Bto,
Published,
}
#[derive(DeriveIden)]
pub enum Objects {
Table,
Internal,
Id,
ObjectType,
AttributedTo,
Name,
Summary,
Content,
Sensitive,
Url,
Likes,
Announces,
Replies,
Context,
InReplyTo,
Cc,
Bcc,
To,
Bto,
Published,
Updated,
}
#[derive(DeriveIden)]
pub enum Instances {
Table,
Internal,
Domain,
Name,
Software,
Version,
Icon,
DownSince,
Users,
Posts,
Published,
Updated,
}
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Instances::Table)
.comment("known other instances in the fediverse")
.col(
ColumnDef::new(Instances::Internal)
.big_integer()
.not_null()
.auto_increment()
.primary_key()
)
.col(ColumnDef::new(Instances::Domain).string().not_null().unique_key())
.col(ColumnDef::new(Instances::Name).string().null())
.col(ColumnDef::new(Instances::Software).string().null())
.col(ColumnDef::new(Instances::Version).string().null())
.col(ColumnDef::new(Instances::Icon).string().null())
.col(ColumnDef::new(Instances::DownSince).date_time().null())
.col(ColumnDef::new(Instances::Users).big_integer().null())
.col(ColumnDef::new(Instances::Posts).big_integer().null())
.col(ColumnDef::new(Instances::Published).date_time().not_null().default(Expr::current_timestamp()))
.col(ColumnDef::new(Instances::Updated).date_time().not_null().default(Expr::current_timestamp()))
.to_owned()
)
.await?;
manager
.create_index(Index::create().unique().name("index-instances-domain").table(Instances::Table).col(Instances::Domain).to_owned())
.await?;
manager
.create_table(
Table::create()
.table(Actors::Table)
.comment("main actors table, with users and applications")
.col(
ColumnDef::new(Actors::Internal)
.big_integer()
.not_null()
.primary_key()
.auto_increment()
)
.col(ColumnDef::new(Actors::Id).string().not_null().unique_key())
.col(ColumnDef::new(Actors::ActorType).string().not_null())
.col(ColumnDef::new(Actors::Domain).string().not_null())
// .foreign_key(
// ForeignKey::create()
// .name("fkey-actors-instances")
// .from(Actors::Table, Actors::Domain)
// .to(Instances::Table, Instances::Domain)
// .on_update(ForeignKeyAction::Cascade)
// )
.col(ColumnDef::new(Actors::Name).string().null())
.col(ColumnDef::new(Actors::Summary).string().null())
.col(ColumnDef::new(Actors::Image).string().null())
.col(ColumnDef::new(Actors::Icon).string().null())
.col(ColumnDef::new(Actors::PreferredUsername).string().not_null())
.col(ColumnDef::new(Actors::Inbox).string().null())
.col(ColumnDef::new(Actors::SharedInbox).string().null())
.col(ColumnDef::new(Actors::Outbox).string().null())
.col(ColumnDef::new(Actors::Following).string().null())
.col(ColumnDef::new(Actors::Followers).string().null())
.col(ColumnDef::new(Actors::FollowingCount).integer().not_null().default(0))
.col(ColumnDef::new(Actors::FollowersCount).integer().not_null().default(0))
.col(ColumnDef::new(Actors::StatusesCount).integer().not_null().default(0))
.col(ColumnDef::new(Actors::PublicKey).string().not_null())
.col(ColumnDef::new(Actors::PrivateKey).string().null())
.col(ColumnDef::new(Actors::Published).date_time().not_null().default(Expr::current_timestamp()))
.col(ColumnDef::new(Actors::Updated).date_time().not_null().default(Expr::current_timestamp()))
.to_owned()
)
.await?;
manager
.create_index(Index::create().unique().name("index-actors-id").table(Actors::Table).col(Actors::Id).to_owned())
.await?;
manager
.create_index(
Index::create()
.unique()
.name("index-actors-preferred-username-domain")
.table(Actors::Table)
.col(Actors::PreferredUsername)
.col(Actors::Domain)
.to_owned()
)
.await?;
manager
.create_index(Index::create().name("index-actors-domain").table(Actors::Table).col(Actors::Domain).to_owned())
.await?;
manager
.create_table(
Table::create()
.table(Objects::Table)
.comment("objects are all AP documents which are neither actors nor activities")
.col(
ColumnDef::new(Objects::Internal)
.big_integer()
.not_null()
.primary_key()
.auto_increment()
)
.col(ColumnDef::new(Objects::Id).string().not_null().unique_key())
.col(ColumnDef::new(Objects::ObjectType).string().not_null())
.col(ColumnDef::new(Objects::AttributedTo).string().null())
// .foreign_key(
// ForeignKey::create()
// .name("fkey-objects-attributed-to")
// .from(Objects::Table, Objects::AttributedTo)
// .to(Actors::Table, Actors::Internal)
// .on_update(ForeignKeyAction::Cascade)
// )
.col(ColumnDef::new(Objects::Name).string().null())
.col(ColumnDef::new(Objects::Summary).string().null())
.col(ColumnDef::new(Objects::Content).string().null())
.col(ColumnDef::new(Objects::Sensitive).boolean().not_null().default(false))
.col(ColumnDef::new(Objects::InReplyTo).string().null())
// .foreign_key(
// ForeignKey::create()
// .name("fkey-objects-in-reply-to")
// .from(Objects::Table, Objects::InReplyTo)
// .to(Objects::Table, Objects::Id)
// .on_update(ForeignKeyAction::Cascade)
// )
.col(ColumnDef::new(Objects::Url).string().null())
.col(ColumnDef::new(Objects::Likes).integer().not_null().default(0))
.col(ColumnDef::new(Objects::Announces).integer().not_null().default(0))
.col(ColumnDef::new(Objects::Replies).integer().not_null().default(0))
.col(ColumnDef::new(Objects::Context).string().null())
.col(ColumnDef::new(Objects::To).json().null())
.col(ColumnDef::new(Objects::Bto).json().null())
.col(ColumnDef::new(Objects::Cc).json().null())
.col(ColumnDef::new(Objects::Bcc).json().null())
.col(ColumnDef::new(Objects::Published).date_time().not_null().default(Expr::current_timestamp()))
.col(ColumnDef::new(Objects::Updated).date_time().not_null().default(Expr::current_timestamp()))
.to_owned()
).await?;
manager
.create_index(Index::create().unique().name("index-objects-id").table(Objects::Table).col(Objects::Id).to_owned())
.await?;
manager
.create_index(Index::create().name("index-objects-attributed-to").table(Objects::Table).col(Objects::AttributedTo).to_owned())
.await?;
manager
.create_index(Index::create().name("index-objects-in-reply-to").table(Objects::Table).col(Objects::InReplyTo).to_owned())
.await?;
manager
.create_index(Index::create().name("index-objects-content-text").table(Objects::Table).col(Objects::Content).full_text().to_owned())
.await?;
manager
.create_index(Index::create().name("index-objects-context").table(Objects::Table).col(Objects::Context).to_owned())
.await?;
manager
.create_table(
Table::create()
.table(Activities::Table)
.comment("all activities this instance ever received or generated")
.col(
ColumnDef::new(Activities::Internal)
.big_integer()
.not_null()
.primary_key()
.auto_increment()
)
.col(ColumnDef::new(Activities::Id).string().not_null().unique_key())
.col(ColumnDef::new(Activities::ActivityType).string().not_null())
.col(ColumnDef::new(Activities::Actor).string().not_null())
// .foreign_key(
// ForeignKey::create()
// .name("fkey-activities-actors")
// .from(Activities::Table, Activities::Actor)
// .to(Actors::Table, Actors::Id)
// .on_update(ForeignKeyAction::Cascade)
// )
.col(ColumnDef::new(Activities::Object).string().null())
// .foreign_key(
// ForeignKey::create()
// .name("fkey-activities-objects")
// .from(Activities::Table, Activities::Object)
// .to(Objects::Table, Objects::Internal)
// .on_update(ForeignKeyAction::Cascade)
// )
.col(ColumnDef::new(Activities::Target).string().null())
.col(ColumnDef::new(Activities::To).json().null())
.col(ColumnDef::new(Activities::Bto).json().null())
.col(ColumnDef::new(Activities::Cc).json().null())
.col(ColumnDef::new(Activities::Bcc).json().null())
.col(ColumnDef::new(Activities::Published).date_time().not_null().default(Expr::current_timestamp()))
.to_owned()
).await?;
manager
.create_index(Index::create().unique().name("index-activities-id").table(Activities::Table).col(Activities::Id).to_owned())
.await?;
manager
.create_index(Index::create().name("index-activities-actor").table(Activities::Table).col(Activities::Actor).to_owned())
.await?;
manager
.create_index(Index::create().name("activities-object-index").table(Activities::Table).col(Activities::Object).to_owned())
.await?;
manager
.create_index(Index::create().name("index-activities-published-descending").table(Activities::Table).col((Activities::Published, IndexOrder::Desc)).to_owned())
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Actors::Table).to_owned())
.await?;
manager
.drop_index(Index::drop().name("index-actors-id").table(Actors::Table).to_owned())
.await?;
manager
.drop_index(Index::drop().name("index-actors-preferred-username").table(Actors::Table).to_owned())
.await?;
manager
.drop_index(Index::drop().name("index-actors-domain").table(Actors::Table).to_owned())
.await?;
manager
.drop_table(Table::drop().table(Activities::Table).to_owned())
.await?;
manager
.drop_index(Index::drop().name("index-activities-id").table(Activities::Table).to_owned())
.await?;
manager
.drop_index(Index::drop().name("index-activities-actor").table(Activities::Table).to_owned())
.await?;
manager
.drop_index(Index::drop().name("activities-object-index").table(Activities::Table).to_owned())
.await?;
manager
.drop_index(Index::drop().name("index-activities-published-descending").table(Activities::Table).to_owned())
.await?;
manager
.drop_table(Table::drop().table(Objects::Table).to_owned())
.await?;
manager
.drop_index(Index::drop().name("index-objects-id").table(Objects::Table).to_owned())
.await?;
manager
.drop_index(Index::drop().name("index-objects-attributed-to").table(Objects::Table).to_owned())
.await?;
manager
.drop_index(Index::drop().name("index-objects-in-reply-to").table(Objects::Table).to_owned())
.await?;
manager
.drop_index(Index::drop().name("index-objects-content-text").table(Objects::Table).to_owned())
.await?;
manager
.drop_index(Index::drop().name("index-objects-context").table(Objects::Table).to_owned())
.await?;
manager
.drop_table(Table::drop().table(Instances::Table).to_owned())
.await?;
manager
.drop_index(Index::drop().name("index-instances-domain").table(Instances::Table).to_owned())
.await?;
Ok(())
}
}

View file

@ -0,0 +1,257 @@
use sea_orm_migration::prelude::*;
use super::m20240524_000001_create_actor_activity_object_tables::{Activities, Actors, Objects};
#[derive(DeriveIden)]
pub enum Relations {
Table,
Internal,
Follower,
Following,
Activity,
Accept,
}
#[derive(DeriveIden)]
#[allow(clippy::enum_variant_names)]
pub enum Likes {
Table,
Internal,
Actor,
Object,
Activity,
Published,
}
#[derive(DeriveIden)]
#[allow(clippy::enum_variant_names)]
pub enum Announces {
Table,
Internal,
Actor,
Object,
Published,
}
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Relations::Table)
.comment("follow relations between actors (applications too! for relays)")
.col(
ColumnDef::new(Relations::Internal)
.big_integer()
.not_null()
.primary_key()
.auto_increment()
)
.col(ColumnDef::new(Relations::Follower).big_integer().not_null())
.foreign_key(
ForeignKey::create()
.name("fkey-relations-follower")
.from(Relations::Table, Relations::Follower)
.to(Actors::Table, Actors::Internal)
.on_update(ForeignKeyAction::Cascade)
.on_delete(ForeignKeyAction::Cascade)
)
.col(ColumnDef::new(Relations::Following).big_integer().not_null())
.foreign_key(
ForeignKey::create()
.name("fkey-relations-following")
.from(Relations::Table, Relations::Following)
.to(Actors::Table, Actors::Internal)
.on_update(ForeignKeyAction::Cascade)
.on_delete(ForeignKeyAction::Cascade)
)
.col(ColumnDef::new(Relations::Accept).big_integer().null())
.foreign_key(
ForeignKey::create()
.name("fkey-relations-accept")
.from(Relations::Table, Relations::Accept)
.to(Activities::Table, Activities::Internal)
.on_update(ForeignKeyAction::Cascade)
)
.col(ColumnDef::new(Relations::Activity).big_integer().not_null())
.foreign_key(
ForeignKey::create()
.name("fkey-relations-activity")
.from(Relations::Table, Relations::Activity)
.to(Activities::Table, Activities::Internal)
.on_update(ForeignKeyAction::Cascade)
)
.to_owned()
)
.await?;
manager
.create_index(Index::create().name("index-relations-follower").table(Relations::Table).col(Relations::Follower).to_owned())
.await?;
manager
.create_index(Index::create().name("index-relations-following").table(Relations::Table).col(Relations::Following).to_owned())
.await?;
manager
.create_index(Index::create().name("index-relations-activity").table(Relations::Table).col(Relations::Activity).to_owned())
.await?;
manager
.create_table(
Table::create()
.table(Likes::Table)
.comment("all like events, joining actor to object")
.col(
ColumnDef::new(Likes::Internal)
.big_integer()
.not_null()
.primary_key()
.auto_increment()
)
.col(ColumnDef::new(Likes::Actor).big_integer().not_null())
.foreign_key(
ForeignKey::create()
.name("fkey-likes-actor")
.from(Likes::Table, Likes::Actor)
.to(Actors::Table, Actors::Internal)
.on_update(ForeignKeyAction::Cascade)
.on_delete(ForeignKeyAction::Cascade)
)
.col(ColumnDef::new(Likes::Object).big_integer().not_null())
.foreign_key(
ForeignKey::create()
.name("fkey-likes-object")
.from(Likes::Table, Likes::Object)
.to(Objects::Table, Objects::Internal)
.on_update(ForeignKeyAction::Cascade)
.on_delete(ForeignKeyAction::Cascade)
)
.col(ColumnDef::new(Likes::Activity).big_integer().not_null())
.foreign_key(
ForeignKey::create()
.name("fkey-likes-activity")
.from(Likes::Table, Likes::Activity)
.to(Activities::Table, Activities::Internal)
.on_update(ForeignKeyAction::Cascade)
.on_delete(ForeignKeyAction::Cascade)
)
.col(ColumnDef::new(Likes::Published).date_time().not_null().default(Expr::current_timestamp()))
.to_owned()
)
.await?;
manager
.create_index(Index::create().name("index-likes-actor").table(Likes::Table).col(Likes::Actor).to_owned())
.await?;
manager
.create_index(Index::create().name("index-likes-object").table(Likes::Table).col(Likes::Object).to_owned())
.await?;
manager
.create_index(
Index::create()
.unique()
.name("index-likes-actor-object")
.table(Likes::Table)
.col(Likes::Actor)
.col(Likes::Object)
.to_owned()
).await?;
manager
.create_table(
Table::create()
.table(Announces::Table)
.comment("all share/boost/reblog events, joining actor to object")
.col(
ColumnDef::new(Announces::Internal)
.big_integer()
.not_null()
.primary_key()
.auto_increment()
)
.col(ColumnDef::new(Announces::Actor).big_integer().not_null())
.foreign_key(
ForeignKey::create()
.name("fkey-announces-actor")
.from(Announces::Table, Announces::Actor)
.to(Actors::Table, Actors::Internal)
.on_update(ForeignKeyAction::Cascade)
.on_delete(ForeignKeyAction::Cascade)
)
.col(ColumnDef::new(Announces::Object).big_integer().not_null())
.foreign_key(
ForeignKey::create()
.name("fkey-announces-object")
.from(Announces::Table, Announces::Object)
.to(Objects::Table, Objects::Internal)
.on_update(ForeignKeyAction::Cascade)
.on_delete(ForeignKeyAction::Cascade)
)
.col(ColumnDef::new(Announces::Published).date_time().not_null().default(Expr::current_timestamp()))
.to_owned()
)
.await?;
manager
.create_index(Index::create().name("index-announces-actor").table(Announces::Table).col(Announces::Actor).to_owned())
.await?;
manager
.create_index(Index::create().name("index-announces-object").table(Announces::Table).col(Announces::Object).to_owned())
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Relations::Table).to_owned())
.await?;
manager
.drop_index(Index::drop().name("index-relations-follower").table(Relations::Table).to_owned())
.await?;
manager
.drop_index(Index::drop().name("index-relations-following").table(Relations::Table).to_owned())
.await?;
manager
.drop_index(Index::drop().name("index-relations-activity").table(Relations::Table).to_owned())
.await?;
manager
.drop_table(Table::drop().table(Likes::Table).to_owned())
.await?;
manager
.drop_index(Index::drop().name("index-likes-actor").table(Likes::Table).to_owned())
.await?;
manager
.drop_index(Index::drop().name("index-likes-object").table(Likes::Table).to_owned())
.await?;
manager
.drop_index(Index::drop().name("index-likes-actor-object").table(Likes::Table).to_owned())
.await?;
manager
.drop_table(Table::drop().table(Announces::Table).to_owned())
.await?;
manager
.drop_index(Index::drop().name("index-announces-actor").table(Announces::Table).to_owned())
.await?;
manager
.drop_index(Index::drop().name("index-announces-object").table(Announces::Table).to_owned())
.await?;
Ok(())
}
}

View file

@ -0,0 +1,171 @@
use sea_orm_migration::prelude::*;
use super::m20240524_000001_create_actor_activity_object_tables::Actors;
#[derive(DeriveIden)]
pub enum Configs {
Table,
Internal,
Actor,
AcceptFollowRequests,
ShowFollowersCount,
ShowFollowingCount,
ShowFollowers,
ShowFollowing,
}
#[derive(DeriveIden)]
pub enum Credentials {
Table,
Internal,
Actor,
Login,
Password,
}
#[derive(DeriveIden)]
pub enum Sessions {
Table,
Internal,
Actor,
Secret,
Expires,
}
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Configs::Table)
.comment("configuration for each local user")
.col(
ColumnDef::new(Configs::Internal)
.big_integer()
.not_null()
.primary_key()
.auto_increment()
)
.col(ColumnDef::new(Configs::Actor).string().not_null().unique_key())
.foreign_key(
ForeignKey::create()
.name("fkey-config-actor")
.from(Configs::Table, Configs::Actor)
.to(Actors::Table, Actors::Id)
.on_update(ForeignKeyAction::Cascade)
.on_delete(ForeignKeyAction::Cascade)
)
.col(ColumnDef::new(Configs::AcceptFollowRequests).boolean().not_null())
.col(ColumnDef::new(Configs::ShowFollowersCount).boolean().not_null())
.col(ColumnDef::new(Configs::ShowFollowingCount).boolean().not_null())
.col(ColumnDef::new(Configs::ShowFollowers).boolean().not_null())
.col(ColumnDef::new(Configs::ShowFollowing).boolean().not_null())
.to_owned()
)
.await?;
manager
.create_index(Index::create().unique().name("index-configs-actor").table(Configs::Table).col(Configs::Actor).to_owned())
.await?;
manager
.create_table(
Table::create()
.table(Credentials::Table)
.comment("simple login credentials to authenticate local users")
.col(
ColumnDef::new(Credentials::Internal)
.big_integer()
.not_null()
.primary_key()
.auto_increment()
)
.col(ColumnDef::new(Credentials::Actor).string().not_null().unique_key())
.foreign_key(
ForeignKey::create()
.name("fkey-credentials-actor")
.from(Credentials::Table, Credentials::Actor)
.to(Actors::Table, Actors::Id)
.on_update(ForeignKeyAction::Cascade)
.on_delete(ForeignKeyAction::Cascade)
)
.col(ColumnDef::new(Credentials::Login).string().not_null())
.col(ColumnDef::new(Credentials::Password).string().not_null())
.to_owned()
)
.await?;
manager
.create_index(Index::create().unique().name("index-credentials-actor").table(Credentials::Table).col(Credentials::Actor).to_owned())
.await?;
manager
.create_index(Index::create().name("index-credentials-login").table(Credentials::Table).col(Credentials::Login).to_owned())
.await?;
manager
.create_table(
Table::create()
.table(Sessions::Table)
.comment("authenticated sessions from local users")
.col(
ColumnDef::new(Sessions::Internal)
.big_integer()
.not_null()
.primary_key()
.auto_increment()
)
.col(ColumnDef::new(Sessions::Actor).string().not_null())
.foreign_key(
ForeignKey::create()
.name("fkey-sessions-actor")
.from(Sessions::Table, Sessions::Actor)
.to(Actors::Table, Actors::Id)
.on_update(ForeignKeyAction::Cascade)
.on_delete(ForeignKeyAction::Cascade)
)
.col(ColumnDef::new(Sessions::Secret).string().not_null())
.col(ColumnDef::new(Sessions::Expires).date_time().not_null())
.to_owned()
)
.await?;
manager
.create_index(Index::create().name("index-sessions-secret").table(Sessions::Table).col(Sessions::Secret).to_owned())
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Configs::Table).to_owned())
.await?;
manager
.drop_index(Index::drop().name("index-configs-actor").table(Configs::Table).to_owned())
.await?;
manager
.drop_table(Table::drop().table(Credentials::Table).to_owned())
.await?;
manager
.drop_index(Index::drop().name("index-credentials-login").table(Credentials::Table).to_owned())
.await?;
manager
.drop_table(Table::drop().table(Sessions::Table).to_owned())
.await?;
manager
.drop_index(Index::drop().name("index-sessions-secret").table(Sessions::Table).to_owned())
.await?;
Ok(())
}
}

View file

@ -0,0 +1,195 @@
use sea_orm_migration::prelude::*;
use super::m20240524_000001_create_actor_activity_object_tables::{Activities, Actors, Instances, Objects};
#[derive(DeriveIden)]
pub enum Addressing {
Table,
Internal,
Actor,
Instance,
Activity,
Object,
Published,
}
#[derive(DeriveIden)]
pub enum Deliveries {
Table,
Internal,
Actor,
Target,
Activity,
Published,
NotBefore,
Attempt,
}
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Addressing::Table)
.comment("this join table contains all addressing relations, serving effectively as permissions truth table")
.col(
ColumnDef::new(Addressing::Internal)
.big_integer()
.not_null()
.auto_increment()
.primary_key()
)
.col(ColumnDef::new(Addressing::Actor).big_integer().null())
.foreign_key(
ForeignKey::create()
.name("fkey-addressing-actor")
.from(Addressing::Table, Addressing::Actor)
.to(Actors::Table, Actors::Internal)
.on_update(ForeignKeyAction::Cascade)
)
.col(ColumnDef::new(Addressing::Instance).big_integer().null())
.foreign_key(
ForeignKey::create()
.name("fkey-addressing-instance")
.from(Addressing::Table, Addressing::Instance)
.to(Instances::Table, Instances::Internal)
.on_update(ForeignKeyAction::Cascade)
)
.col(ColumnDef::new(Addressing::Activity).big_integer().null())
.foreign_key(
ForeignKey::create()
.name("fkey-addressing-activity")
.from(Addressing::Table, Addressing::Activity)
.to(Activities::Table, Activities::Internal)
.on_update(ForeignKeyAction::Cascade)
)
.col(ColumnDef::new(Addressing::Object).big_integer().null())
.foreign_key(
ForeignKey::create()
.name("fkey-addressing-object")
.from(Addressing::Table, Addressing::Object)
.to(Objects::Table, Objects::Internal)
.on_update(ForeignKeyAction::Cascade)
)
.col(ColumnDef::new(Addressing::Published).date_time().not_null().default(Expr::current_timestamp()))
.to_owned()
)
.await?;
manager
.create_index(
Index::create()
.name("index-addressing-actor-published")
.table(Addressing::Table)
.col(Addressing::Actor)
.col(Addressing::Published)
.to_owned()
)
.await?;
manager
.create_index(
Index::create()
.name("index-addressing-instance-published")
.table(Addressing::Table)
.col(Addressing::Instance)
.col(Addressing::Published)
.to_owned()
)
.await?;
manager
.create_index(Index::create().name("index-addressing-activity").table(Addressing::Table).col(Addressing::Activity).to_owned())
.await?;
manager
.create_index(Index::create().name("index-addressing-object").table(Addressing::Table).col(Addressing::Object).to_owned())
.await?;
manager
.create_table(
Table::create()
.table(Deliveries::Table)
.comment("this table contains all enqueued outgoing delivery jobs")
.col(
ColumnDef::new(Deliveries::Internal)
.big_integer()
.not_null()
.auto_increment()
.primary_key()
)
.col(ColumnDef::new(Deliveries::Actor).string().not_null())
.foreign_key(
ForeignKey::create()
.name("fkey-deliveries-actor")
.from(Deliveries::Table, Deliveries::Actor)
.to(Actors::Table, Actors::Id)
.on_update(ForeignKeyAction::Cascade)
.on_delete(ForeignKeyAction::Cascade)
)
.col(ColumnDef::new(Deliveries::Target).string().not_null())
.col(ColumnDef::new(Deliveries::Activity).string().not_null())
.foreign_key(
ForeignKey::create()
.name("fkey-deliveries-activity")
.from(Deliveries::Table, Deliveries::Activity)
.to(Activities::Table, Activities::Id)
.on_update(ForeignKeyAction::Cascade)
.on_delete(ForeignKeyAction::Cascade)
)
.col(ColumnDef::new(Deliveries::Published).date_time().not_null().default(Expr::current_timestamp()))
.col(ColumnDef::new(Deliveries::NotBefore).date_time().not_null().default(Expr::current_timestamp()))
.col(ColumnDef::new(Deliveries::Attempt).integer().not_null().default(0))
.to_owned()
)
.await?;
manager
.create_index(
Index::create()
.name("index-deliveries-not-before")
.table(Deliveries::Table)
.col((Deliveries::NotBefore, IndexOrder::Asc))
.to_owned()
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Addressing::Table).to_owned())
.await?;
manager
.drop_index(Index::drop().name("index-addressing-actor").to_owned())
.await?;
manager
.drop_index(Index::drop().name("index-addressing-server").to_owned())
.await?;
manager
.drop_index(Index::drop().name("index-addressing-activity").to_owned())
.await?;
manager
.drop_index(Index::drop().name("index-addressing-object").to_owned())
.await?;
manager
.drop_table(Table::drop().table(Deliveries::Table).to_owned())
.await?;
manager
.drop_index(Index::drop().name("index-deliveries-not-before").to_owned())
.await?;
Ok(())
}
}

View file

@ -0,0 +1,207 @@
use sea_orm_migration::prelude::*;
use super::m20240524_000001_create_actor_activity_object_tables::Objects;
#[derive(DeriveIden)]
pub enum Attachments {
Table,
Internal,
DocumentType,
Url,
Object,
Name,
MediaType,
Published,
}
#[derive(DeriveIden)]
pub enum Mentions {
Table,
Internal,
Object,
Actor,
Published,
}
#[derive(DeriveIden)]
pub enum Hashtags {
Table,
Internal,
Object,
Name,
Published,
}
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Attachments::Table)
.comment("media attachments related to objects")
.col(
ColumnDef::new(Attachments::Internal)
.big_integer()
.not_null()
.primary_key()
.auto_increment()
)
.col(ColumnDef::new(Attachments::Url).string().not_null().unique_key())
.col(ColumnDef::new(Attachments::Object).big_integer().not_null())
.foreign_key(
ForeignKey::create()
.name("fkey-attachments-object")
.from(Attachments::Table, Attachments::Object)
.to(Objects::Table, Objects::Internal)
.on_update(ForeignKeyAction::Cascade)
.on_delete(ForeignKeyAction::Cascade)
)
.col(ColumnDef::new(Attachments::DocumentType).string().not_null())
.col(ColumnDef::new(Attachments::Name).string().null())
.col(ColumnDef::new(Attachments::MediaType).string().not_null())
.col(ColumnDef::new(Attachments::Published).date_time().not_null().default(Expr::current_timestamp()))
.to_owned()
)
.await?;
manager
.create_index(Index::create().name("index-attachment-object").table(Attachments::Table).col(Attachments::Object).to_owned())
.await?;
manager
.create_table(
Table::create()
.table(Mentions::Table)
.comment("join table relating posts to mentioned users")
.col(
ColumnDef::new(Mentions::Internal)
.big_integer()
.not_null()
.primary_key()
.auto_increment()
)
.col(ColumnDef::new(Mentions::Object).big_integer().not_null())
.foreign_key(
ForeignKey::create()
.name("fkey-mentions-object")
.from(Mentions::Table, Mentions::Object)
.to(Objects::Table, Objects::Internal)
.on_update(ForeignKeyAction::Cascade)
.on_delete(ForeignKeyAction::Cascade)
)
.col(ColumnDef::new(Mentions::Actor).string().not_null())
// .foreign_key(
// ForeignKey::create()
// .name("fkey-mentions-actor")
// .from(Mentions::Table, Mentions::Actor)
// .to(Actors::Table, Actors::Internal)
// .on_update(ForeignKeyAction::Cascade)
// .on_delete(ForeignKeyAction::Cascade)
// )
.col(ColumnDef::new(Mentions::Published).date_time().not_null().default(Expr::current_timestamp()))
.to_owned()
)
.await?;
manager
.create_index(Index::create().name("index-mentions-object").table(Mentions::Table).col(Mentions::Object).to_owned())
.await?;
manager
.create_index(
Index::create()
.name("index-mentions-actor-published")
.table(Mentions::Table)
.col(Mentions::Actor)
.col((Mentions::Published, IndexOrder::Desc))
.to_owned()
)
.await?;
manager
.create_table(
Table::create()
.table(Hashtags::Table)
.comment("join table relating posts to hashtags")
.col(
ColumnDef::new(Hashtags::Internal)
.big_integer()
.not_null()
.primary_key()
.auto_increment()
)
.col(ColumnDef::new(Hashtags::Object).big_integer().not_null())
.foreign_key(
ForeignKey::create()
.name("fkey-hashtags-object")
.from(Hashtags::Table, Hashtags::Object)
.to(Objects::Table, Objects::Internal)
.on_update(ForeignKeyAction::Cascade)
.on_delete(ForeignKeyAction::Cascade)
)
.col(ColumnDef::new(Hashtags::Name).string().not_null())
.col(ColumnDef::new(Hashtags::Published).date_time().not_null().default(Expr::current_timestamp()))
.to_owned()
)
.await?;
manager
.create_index(Index::create().name("index-hashtags-object").table(Hashtags::Table).col(Hashtags::Object).to_owned())
.await?;
manager
.create_index(
Index::create()
.name("index-hashtags-name-published")
.table(Hashtags::Table)
.col(Hashtags::Name)
.col((Hashtags::Published, IndexOrder::Desc))
.to_owned()
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Attachments::Table).to_owned())
.await?;
manager
.drop_index(Index::drop().name("index-attachment-object").to_owned())
.await?;
manager
.drop_table(Table::drop().table(Mentions::Table).to_owned())
.await?;
manager
.drop_index(Index::drop().name("index-mentions-object").table(Mentions::Table).to_owned())
.await?;
manager
.drop_index(Index::drop().name("index-mentions-actor-published").table(Mentions::Table).to_owned())
.await?;
manager
.drop_table(Table::drop().table(Hashtags::Table).to_owned())
.await?;
manager
.drop_index(Index::drop().name("index-hashtags-object").table(Hashtags::Table).to_owned())
.await?;
manager
.drop_index(Index::drop().name("index-hashtags-name-published").table(Hashtags::Table).to_owned())
.await?;
Ok(())
}
}

View file

@ -1,5 +1,7 @@
use sea_orm_migration::prelude::*; use sea_orm_migration::prelude::*;
use super::m20240524_000002_create_relations_likes_shares::Relations;
#[derive(DeriveMigrationName)] #[derive(DeriveMigrationName)]
pub struct Migration; pub struct Migration;
@ -7,34 +9,23 @@ pub struct Migration;
impl MigrationTrait for Migration { impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager manager
.alter_table( .create_index(
Table::alter() Index::create()
.table(Objects::Table) .unique()
.add_column(ColumnDef::new(Objects::Url).string().null()) .name("index-relations-follower-following")
.table(Relations::Table)
.col(Relations::Following)
.col(Relations::Follower)
.to_owned() .to_owned()
) )
.await?; .await?;
Ok(()) Ok(())
} }
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager manager
.alter_table( .drop_index(Index::drop().name("index-relations-follower-following").table(Relations::Table).to_owned())
Table::alter()
.table(Objects::Table)
.drop_column(Objects::Url)
.to_owned()
)
.await?; .await?;
Ok(()) Ok(())
} }
} }
#[derive(DeriveIden)]
enum Objects {
Table,
Url,
}

View file

@ -1,21 +1,11 @@
use sea_orm_migration::prelude::*; use sea_orm_migration::prelude::*;
mod m20240316_000001_create_table; mod m20240524_000001_create_actor_activity_object_tables;
mod m20240322_000001_create_relations; mod m20240524_000002_create_relations_likes_shares;
mod m20240322_000002_add_likes_shares; mod m20240524_000003_create_users_auth_and_config;
mod m20240322_000003_add_indexes; mod m20240524_000004_create_addressing_deliveries;
mod m20240323_000001_add_user_configs; mod m20240524_000005_create_attachments_tags_mentions;
mod m20240323_000002_add_simple_credentials; mod m20240529_000001_add_relation_unique_index;
mod m20240324_000001_add_addressing;
mod m20240325_000001_add_deliveries;
mod m20240325_000002_add_system_key;
mod m20240418_000001_add_statuses_and_reply_to;
mod m20240421_000001_add_attachments;
mod m20240424_000001_add_sensitive_field;
mod m20240429_000001_add_relays_table;
mod m20240502_000001_add_object_updated;
mod m20240512_000001_add_url_to_objects;
mod m20240520_000001_add_published_to_addressing_actor_index;
pub struct Migrator; pub struct Migrator;
@ -23,22 +13,12 @@ pub struct Migrator;
impl MigratorTrait for Migrator { impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> { fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![ vec![
Box::new(m20240316_000001_create_table::Migration), Box::new(m20240524_000001_create_actor_activity_object_tables::Migration),
Box::new(m20240322_000001_create_relations::Migration), Box::new(m20240524_000002_create_relations_likes_shares::Migration),
Box::new(m20240322_000002_add_likes_shares::Migration), Box::new(m20240524_000003_create_users_auth_and_config::Migration),
Box::new(m20240322_000003_add_indexes::Migration), Box::new(m20240524_000004_create_addressing_deliveries::Migration),
Box::new(m20240323_000001_add_user_configs::Migration), Box::new(m20240524_000005_create_attachments_tags_mentions::Migration),
Box::new(m20240323_000002_add_simple_credentials::Migration), Box::new(m20240529_000001_add_relation_unique_index::Migration),
Box::new(m20240324_000001_add_addressing::Migration),
Box::new(m20240325_000001_add_deliveries::Migration),
Box::new(m20240325_000002_add_system_key::Migration),
Box::new(m20240418_000001_add_statuses_and_reply_to::Migration),
Box::new(m20240421_000001_add_attachments::Migration),
Box::new(m20240424_000001_add_sensitive_field::Migration),
Box::new(m20240429_000001_add_relays_table::Migration),
Box::new(m20240502_000001_add_object_updated::Migration),
Box::new(m20240512_000001_add_url_to_objects::Migration),
Box::new(m20240520_000001_add_published_to_addressing_actor_index::Migration),
] ]
} }
} }

View file

@ -1,46 +1,113 @@
use apb::{ActivityMut, BaseMut, ObjectMut}; use apb::{ActivityMut, ActivityType, BaseMut, ObjectMut};
use sea_orm::entity::prelude::*; use sea_orm::{entity::prelude::*, QuerySelect, SelectColumns};
use crate::routes::activitypub::jsonld::LD; use crate::{model::Audience, errors::UpubError, routes::activitypub::jsonld::LD};
use super::Audience;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "activities")] #[sea_orm(table_name = "activities")]
pub struct Model { pub struct Model {
#[sea_orm(primary_key)] #[sea_orm(primary_key)]
pub internal: i64,
#[sea_orm(unique)]
pub id: String, pub id: String,
pub activity_type: ActivityType,
pub activity_type: apb::ActivityType,
pub actor: String, pub actor: String,
pub object: Option<String>, pub object: Option<String>,
pub target: Option<String>,
pub target: Option<String>, // TODO relates to USER maybe??
pub cc: Audience,
pub bcc: Audience,
pub to: Audience, pub to: Audience,
pub bto: Audience, pub bto: Audience,
pub cc: Audience,
pub bcc: Audience,
pub published: ChronoDateTimeUtc, pub published: ChronoDateTimeUtc,
}
// TODO: origin, result, instrument #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::actor::Entity",
from = "Column::Actor",
to = "super::actor::Column::Id",
on_update = "Cascade",
on_delete = "NoAction"
)]
Actors,
#[sea_orm(has_many = "super::addressing::Entity")]
Addressing,
#[sea_orm(has_many = "super::delivery::Entity")]
Deliveries,
#[sea_orm(
belongs_to = "super::object::Entity",
from = "Column::Object",
to = "super::object::Column::Id",
on_update = "Cascade",
on_delete = "NoAction"
)]
Objects,
}
impl Related<super::actor::Entity> for Entity {
fn to() -> RelationDef {
Relation::Actors.def()
}
}
impl Related<super::addressing::Entity> for Entity {
fn to() -> RelationDef {
Relation::Addressing.def()
}
}
impl Related<super::delivery::Entity> for Entity {
fn to() -> RelationDef {
Relation::Deliveries.def()
}
}
impl Related<super::object::Entity> for Entity {
fn to() -> RelationDef {
Relation::Objects.def()
}
}
impl ActiveModelBehavior for ActiveModel {}
impl Entity {
pub fn find_by_ap_id(id: &str) -> Select<Entity> {
Entity::find().filter(Column::Id.eq(id))
}
pub async fn ap_to_internal(id: &str, db: &DatabaseConnection) -> crate::Result<i64> {
Entity::find()
.filter(Column::Id.eq(id))
.select_only()
.select_column(Column::Internal)
.into_tuple::<i64>()
.one(db)
.await?
.ok_or_else(UpubError::not_found)
}
}
impl ActiveModel {
//#[deprecated = "should remove this, get models thru normalizer"]
pub fn new(activity: &impl apb::Activity) -> Result<Self, super::FieldError> {
Ok(ActiveModel {
internal: sea_orm::ActiveValue::NotSet,
id: sea_orm::ActiveValue::Set(activity.id().ok_or(super::FieldError("id"))?.to_string()),
activity_type: sea_orm::ActiveValue::Set(activity.activity_type().ok_or(super::FieldError("type"))?),
actor: sea_orm::ActiveValue::Set(activity.actor().id().ok_or(super::FieldError("actor"))?),
object: sea_orm::ActiveValue::Set(activity.object().id()),
target: sea_orm::ActiveValue::Set(activity.target().id()),
published: sea_orm::ActiveValue::Set(activity.published().unwrap_or(chrono::Utc::now())),
to: sea_orm::ActiveValue::Set(activity.to().into()),
bto: sea_orm::ActiveValue::Set(activity.bto().into()),
cc: sea_orm::ActiveValue::Set(activity.cc().into()),
bcc: sea_orm::ActiveValue::Set(activity.bcc().into()),
})
}
} }
impl Model { impl Model {
pub fn new(activity: &impl apb::Activity) -> Result<Self, super::FieldError> {
Ok(Model {
id: activity.id().ok_or(super::FieldError("id"))?.to_string(),
activity_type: activity.activity_type().ok_or(super::FieldError("type"))?,
actor: activity.actor().id().ok_or(super::FieldError("actor"))?,
object: activity.object().id(),
target: activity.target().id(),
published: activity.published().unwrap_or(chrono::Utc::now()),
to: activity.to().into(),
bto: activity.bto().into(),
cc: activity.cc().into(),
bcc: activity.bcc().into(),
})
}
pub fn ap(self) -> serde_json::Value { pub fn ap(self) -> serde_json::Value {
serde_json::Value::new_object() serde_json::Value::new_object()
.set_id(Some(&self.id)) .set_id(Some(&self.id))
@ -56,49 +123,6 @@ impl Model {
} }
} }
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::Actor",
to = "super::user::Column::Id"
)]
User,
#[sea_orm(
belongs_to = "super::object::Entity",
from = "Column::Object",
to = "super::object::Column::Id"
)]
Object,
#[sea_orm(has_many = "super::addressing::Entity")]
Addressing,
#[sea_orm(has_many = "super::delivery::Entity")]
Delivery,
}
impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::User.def()
}
}
impl Related<super::object::Entity> for Entity {
fn to() -> RelationDef {
Relation::Object.def()
}
}
impl Related<super::addressing::Entity> for Entity {
fn to() -> RelationDef {
Relation::Addressing.def()
}
}
impl ActiveModelBehavior for ActiveModel {}
impl apb::target::Addressed for Model { impl apb::target::Addressed for Model {
fn addressed(&self) -> Vec<String> { fn addressed(&self) -> Vec<String> {
let mut to : Vec<String> = self.to.0.clone(); let mut to : Vec<String> = self.to.0.clone();

242
src/model/actor.rs Normal file
View file

@ -0,0 +1,242 @@
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};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "actors")]
pub struct Model {
#[sea_orm(primary_key)]
pub internal: i64,
#[sea_orm(unique)]
pub id: String,
pub actor_type: ActorType,
pub domain: String,
pub name: Option<String>,
pub summary: Option<String>,
pub image: Option<String>,
pub icon: Option<String>,
pub preferred_username: String,
pub inbox: Option<String>,
pub shared_inbox: Option<String>,
pub outbox: Option<String>,
pub following: Option<String>,
pub followers: Option<String>,
pub following_count: i32,
pub followers_count: i32,
pub statuses_count: i32,
pub public_key: String,
pub private_key: Option<String>,
pub published: ChronoDateTimeUtc,
pub updated: ChronoDateTimeUtc,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::activity::Entity")]
Activities,
#[sea_orm(has_many = "super::addressing::Entity")]
Addressing,
#[sea_orm(has_many = "super::announce::Entity")]
Announces,
#[sea_orm(has_many = "super::config::Entity")]
Configs,
#[sea_orm(has_many = "super::credential::Entity")]
Credentials,
#[sea_orm(has_many = "super::delivery::Entity")]
Deliveries,
#[sea_orm(
belongs_to = "super::instance::Entity",
from = "Column::Domain",
to = "super::instance::Column::Domain",
on_update = "Cascade",
on_delete = "NoAction"
)]
Instances,
#[sea_orm(has_many = "super::like::Entity")]
Likes,
#[sea_orm(has_many = "super::mention::Entity")]
Mentions,
#[sea_orm(has_many = "super::object::Entity")]
Objects,
#[sea_orm(has_many = "super::relation::Entity")]
Relations,
#[sea_orm(has_many = "super::session::Entity")]
Sessions,
}
impl Related<super::activity::Entity> for Entity {
fn to() -> RelationDef {
Relation::Activities.def()
}
}
impl Related<super::addressing::Entity> for Entity {
fn to() -> RelationDef {
Relation::Addressing.def()
}
}
impl Related<super::announce::Entity> for Entity {
fn to() -> RelationDef {
Relation::Announces.def()
}
}
impl Related<super::config::Entity> for Entity {
fn to() -> RelationDef {
Relation::Configs.def()
}
}
impl Related<super::credential::Entity> for Entity {
fn to() -> RelationDef {
Relation::Credentials.def()
}
}
impl Related<super::delivery::Entity> for Entity {
fn to() -> RelationDef {
Relation::Deliveries.def()
}
}
impl Related<super::instance::Entity> for Entity {
fn to() -> RelationDef {
Relation::Instances.def()
}
}
impl Related<super::like::Entity> for Entity {
fn to() -> RelationDef {
Relation::Likes.def()
}
}
impl Related<super::mention::Entity> for Entity {
fn to() -> RelationDef {
Relation::Mentions.def()
}
}
impl Related<super::object::Entity> for Entity {
fn to() -> RelationDef {
Relation::Objects.def()
}
}
impl Related<super::relation::Entity> for Entity {
fn to() -> RelationDef {
Relation::Relations.def()
}
}
impl Related<super::session::Entity> for Entity {
fn to() -> RelationDef {
Relation::Sessions.def()
}
}
impl ActiveModelBehavior for ActiveModel {}
impl Entity {
pub fn find_by_ap_id(id: &str) -> Select<Entity> {
Entity::find().filter(Column::Id.eq(id))
}
pub fn delete_by_ap_id(id: &str) -> sea_orm::DeleteMany<Entity> {
Entity::delete_many().filter(Column::Id.eq(id))
}
pub async fn ap_to_internal(id: &str, db: &DatabaseConnection) -> crate::Result<i64> {
Entity::find()
.filter(Column::Id.eq(id))
.select_only()
.select_column(Column::Internal)
.into_tuple::<i64>()
.one(db)
.await?
.ok_or_else(UpubError::not_found)
}
}
impl ActiveModel {
pub fn new(object: &impl Actor) -> Result<Self, super::FieldError> {
let ap_id = object.id().ok_or(super::FieldError("id"))?.to_string();
let (domain, fallback_preferred_username) = split_user_id(&ap_id);
Ok(ActiveModel {
internal: sea_orm::ActiveValue::NotSet,
domain: sea_orm::ActiveValue::Set(domain),
id: sea_orm::ActiveValue::Set(ap_id),
preferred_username: sea_orm::ActiveValue::Set(object.preferred_username().unwrap_or(&fallback_preferred_username).to_string()),
actor_type: sea_orm::ActiveValue::Set(object.actor_type().ok_or(super::FieldError("type"))?),
name: sea_orm::ActiveValue::Set(object.name().map(|x| x.to_string())),
summary: sea_orm::ActiveValue::Set(object.summary().map(|x| x.to_string())),
icon: sea_orm::ActiveValue::Set(object.icon().get().and_then(|x| x.url().id())),
image: sea_orm::ActiveValue::Set(object.image().get().and_then(|x| x.url().id())),
inbox: sea_orm::ActiveValue::Set(object.inbox().id()),
outbox: sea_orm::ActiveValue::Set(object.outbox().id()),
shared_inbox: sea_orm::ActiveValue::Set(object.endpoints().get().and_then(|x| Some(x.shared_inbox()?.to_string()))),
followers: sea_orm::ActiveValue::Set(object.followers().id()),
following: sea_orm::ActiveValue::Set(object.following().id()),
published: sea_orm::ActiveValue::Set(object.published().unwrap_or(chrono::Utc::now())),
updated: sea_orm::ActiveValue::Set(chrono::Utc::now()),
following_count: sea_orm::ActiveValue::Set(object.following_count().unwrap_or(0) as i32),
followers_count: sea_orm::ActiveValue::Set(object.followers_count().unwrap_or(0) as i32),
statuses_count: sea_orm::ActiveValue::Set(object.statuses_count().unwrap_or(0) as i32),
public_key: sea_orm::ActiveValue::Set(object.public_key().get().ok_or(super::FieldError("publicKey"))?.public_key_pem().to_string()),
private_key: sea_orm::ActiveValue::Set(None), // there's no way to transport privkey over AP json, must come from DB
})
}
}
impl Model {
pub fn ap(self) -> serde_json::Value {
serde_json::Value::new_object()
.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()
.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()
.set_document_type(Some(apb::DocumentType::Image))
.set_url(apb::Node::link(i.clone()))
)))
.set_published(Some(self.published))
.set_preferred_username(Some(&self.preferred_username))
.set_statuses_count(Some(self.statuses_count as u64))
.set_followers_count(Some(self.followers_count as u64))
.set_following_count(Some(self.following_count as u64))
.set_inbox(apb::Node::maybe_link(self.inbox))
.set_outbox(apb::Node::maybe_link(self.outbox))
.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()
.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()
.set_shared_inbox(self.shared_inbox.as_deref())
))
.set_discoverable(Some(true))
}
}
fn split_user_id(id: &str) -> (String, String) {
let clean = id
.replace("http://", "")
.replace("https://", "");
let mut splits = clean.split('/');
let first = splits.next().unwrap_or("");
let last = splits.last().unwrap_or(first);
(first.to_string(), last.to_string())
}

View file

@ -7,53 +7,71 @@ use crate::routes::activitypub::jsonld::LD;
#[sea_orm(table_name = "addressing")] #[sea_orm(table_name = "addressing")]
pub struct Model { pub struct Model {
#[sea_orm(primary_key)] #[sea_orm(primary_key)]
pub id: i64, pub internal: i64,
pub actor: String, pub actor: Option<i64>,
pub server: String, pub instance: Option<i64>,
pub activity: Option<String>, pub activity: Option<i64>,
pub object: Option<String>, pub object: Option<i64>,
pub published: ChronoDateTimeUtc, pub published: ChronoDateTimeUtc,
} }
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation { pub enum Relation {
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::Actor",
to = "super::user::Column::Id"
)]
User,
#[sea_orm( #[sea_orm(
belongs_to = "super::activity::Entity", belongs_to = "super::activity::Entity",
from = "Column::Activity", from = "Column::Activity",
to = "super::activity::Column::Id" to = "super::activity::Column::Internal",
on_update = "Cascade",
on_delete = "NoAction"
)] )]
Activity, Activities,
#[sea_orm(
belongs_to = "super::actor::Entity",
from = "Column::Actor",
to = "super::actor::Column::Internal",
on_update = "Cascade",
on_delete = "NoAction"
)]
Actors,
#[sea_orm(
belongs_to = "super::instance::Entity",
from = "Column::Instance",
to = "super::instance::Column::Internal",
on_update = "Cascade",
on_delete = "NoAction"
)]
Instances,
#[sea_orm( #[sea_orm(
belongs_to = "super::object::Entity", belongs_to = "super::object::Entity",
from = "Column::Object", from = "Column::Object",
to = "super::object::Column::Id" to = "super::object::Column::Internal",
on_update = "Cascade",
on_delete = "NoAction"
)] )]
Object, Objects,
}
impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::User.def()
}
} }
impl Related<super::activity::Entity> for Entity { impl Related<super::activity::Entity> for Entity {
fn to() -> RelationDef { fn to() -> RelationDef {
Relation::Activity.def() Relation::Activities.def()
}
}
impl Related<super::actor::Entity> for Entity {
fn to() -> RelationDef {
Relation::Actors.def()
}
}
impl Related<super::instance::Entity> for Entity {
fn to() -> RelationDef {
Relation::Instances.def()
} }
} }
impl Related<super::object::Entity> for Entity { impl Related<super::object::Entity> for Entity {
fn to() -> RelationDef { fn to() -> RelationDef {
Relation::Object.def() Relation::Objects.def()
} }
} }
@ -79,12 +97,12 @@ pub enum Event {
impl Event { impl Event {
pub fn id(&self) -> &str { pub fn internal(&self) -> i64 {
match self { match self {
Event::Tombstone => "", Event::Tombstone => 0,
Event::Activity(x) => x.id.as_str(), Event::Activity(x) => x.internal,
Event::StrayObject { object, liked: _ } => object.id.as_str(), Event::StrayObject { object, liked: _ } => object.internal,
Event::DeepActivity { activity: _, liked: _, object } => object.id.as_str(), Event::DeepActivity { activity: _, liked: _, object } => object.internal,
} }
} }
@ -136,12 +154,12 @@ impl FromQueryResult for Event {
impl Entity { impl Entity {
pub fn find_addressed(uid: Option<&str>) -> Select<Entity> { pub fn find_addressed(uid: Option<i64>) -> Select<Entity> {
let mut select = Entity::find() let mut select = Entity::find()
.distinct() .distinct()
.select_only() .select_only()
.join(sea_orm::JoinType::LeftJoin, Relation::Object.def()) .join(sea_orm::JoinType::LeftJoin, Relation::Objects.def())
.join(sea_orm::JoinType::LeftJoin, Relation::Activity.def()) .join(sea_orm::JoinType::LeftJoin, Relation::Activities.def())
.filter( .filter(
// TODO ghetto double inner join because i want to filter out tombstones // TODO ghetto double inner join because i want to filter out tombstones
Condition::any() Condition::any()
@ -151,12 +169,11 @@ impl Entity {
.order_by(Column::Published, Order::Desc); .order_by(Column::Published, Order::Desc);
if let Some(uid) = uid { if let Some(uid) = uid {
let uid = uid.to_string();
select = select select = select
.join( .join(
sea_orm::JoinType::LeftJoin, sea_orm::JoinType::LeftJoin,
crate::model::object::Relation::Like.def() crate::model::object::Relation::Likes.def()
.on_condition(move |_l, _r| crate::model::like::Column::Actor.eq(uid.clone()).into_condition()), .on_condition(move |_l, _r| crate::model::like::Column::Actor.eq(uid).into_condition()),
) )
.select_column_as(crate::model::like::Column::Actor, format!("{}{}", crate::model::like::Entity.table_name(), crate::model::like::Column::Actor.to_string())); .select_column_as(crate::model::like::Column::Actor, format!("{}{}", crate::model::like::Entity.table_name(), crate::model::like::Column::Actor.to_string()));
} }

45
src/model/announce.rs Normal file
View file

@ -0,0 +1,45 @@
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "announces")]
pub struct Model {
#[sea_orm(primary_key)]
pub internal: i64,
pub actor: i64,
pub object: i64,
pub published: ChronoDateTimeUtc,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::actor::Entity",
from = "Column::Actor",
to = "super::actor::Column::Internal",
on_update = "Cascade",
on_delete = "Cascade"
)]
Actors,
#[sea_orm(
belongs_to = "super::object::Entity",
from = "Column::Object",
to = "super::object::Column::Internal",
on_update = "Cascade",
on_delete = "Cascade"
)]
Objects,
}
impl Related<super::actor::Entity> for Entity {
fn to() -> RelationDef {
Relation::Actors.def()
}
}
impl Related<super::object::Entity> for Entity {
fn to() -> RelationDef {
Relation::Objects.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View file

@ -1,18 +0,0 @@
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "application")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i64,
pub private_key: String,
pub public_key: String,
pub created: ChronoDateTimeUtc,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

View file

@ -1,4 +1,4 @@
use apb::{DocumentMut, ObjectMut}; use apb::{DocumentMut, DocumentType, ObjectMut};
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
use crate::routes::activitypub::jsonld::LD; use crate::routes::activitypub::jsonld::LD;
@ -9,16 +9,36 @@ use super::addressing::Event;
#[sea_orm(table_name = "attachments")] #[sea_orm(table_name = "attachments")]
pub struct Model { pub struct Model {
#[sea_orm(primary_key)] #[sea_orm(primary_key)]
pub id: i64, pub internal: i64,
#[sea_orm(unique)]
pub url: String, pub url: String,
pub object: String, pub object: i64,
pub document_type: apb::DocumentType, pub document_type: DocumentType,
pub name: Option<String>, pub name: Option<String>,
pub media_type: String, pub media_type: String,
pub created: ChronoDateTimeUtc, pub published: ChronoDateTimeUtc,
} }
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::object::Entity",
from = "Column::Object",
to = "super::object::Column::Internal",
on_update = "Cascade",
on_delete = "Cascade"
)]
Objects,
}
impl Related<super::object::Entity> for Entity {
fn to() -> RelationDef {
Relation::Objects.def()
}
}
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() serde_json::Value::new_object()
@ -26,37 +46,18 @@ impl Model {
.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))
.set_name(self.name.as_deref()) .set_name(self.name.as_deref())
.set_published(Some(self.created)) .set_published(Some(self.published))
} }
} }
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::object::Entity",
from = "Column::Object",
to = "super::object::Column::Id"
)]
Object,
}
impl Related<super::object::Entity> for Entity {
fn to() -> RelationDef {
Relation::Object.def()
}
}
impl ActiveModelBehavior for ActiveModel {}
#[axum::async_trait] #[axum::async_trait]
pub trait BatchFillable { pub trait BatchFillable {
async fn load_attachments_batch(&self, db: &DatabaseConnection) -> Result<std::collections::BTreeMap<String, Vec<Model>>, DbErr>; async fn load_attachments_batch(&self, db: &DatabaseConnection) -> Result<std::collections::BTreeMap<i64, Vec<Model>>, DbErr>;
} }
#[axum::async_trait] #[axum::async_trait]
impl BatchFillable for &[Event] { impl BatchFillable for &[Event] {
async fn load_attachments_batch(&self, db: &DatabaseConnection) -> Result<std::collections::BTreeMap<String, Vec<Model>>, DbErr> { async fn load_attachments_batch(&self, db: &DatabaseConnection) -> Result<std::collections::BTreeMap<i64, Vec<Model>>, DbErr> {
let objects : Vec<crate::model::object::Model> = self let objects : Vec<crate::model::object::Model> = self
.iter() .iter()
.filter_map(|x| match x { .filter_map(|x| match x {
@ -69,12 +70,11 @@ impl BatchFillable for &[Event] {
let attachments = objects.load_many(Entity, db).await?; let attachments = objects.load_many(Entity, db).await?;
let mut out : std::collections::BTreeMap<String, Vec<Model>> = std::collections::BTreeMap::new(); let mut out : std::collections::BTreeMap<i64, Vec<Model>> = std::collections::BTreeMap::new();
for attach in attachments.into_iter().flatten() { for attach in attachments.into_iter().flatten() {
if out.contains_key(&attach.object) { match out.entry(attach.object) {
out.get_mut(&attach.object).expect("contains but get failed?").push(attach); std::collections::btree_map::Entry::Vacant(a) => { a.insert(vec![attach]); },
} else { std::collections::btree_map::Entry::Occupied(mut e) => { e.get_mut().push(attach); },
out.insert(attach.object.clone(), vec![attach]);
} }
} }
@ -84,14 +84,14 @@ impl BatchFillable for &[Event] {
#[axum::async_trait] #[axum::async_trait]
impl BatchFillable for Vec<Event> { impl BatchFillable for Vec<Event> {
async fn load_attachments_batch(&self, db: &DatabaseConnection) -> Result<std::collections::BTreeMap<String, Vec<Model>>, DbErr> { async fn load_attachments_batch(&self, db: &DatabaseConnection) -> Result<std::collections::BTreeMap<i64, Vec<Model>>, DbErr> {
self.as_slice().load_attachments_batch(db).await self.as_slice().load_attachments_batch(db).await
} }
} }
#[axum::async_trait] #[axum::async_trait]
impl BatchFillable for Event { impl BatchFillable for Event {
async fn load_attachments_batch(&self, db: &DatabaseConnection) -> Result<std::collections::BTreeMap<String, Vec<Model>>, DbErr> { async fn load_attachments_batch(&self, db: &DatabaseConnection) -> Result<std::collections::BTreeMap<i64, Vec<Model>>, DbErr> {
let x = vec![self.clone()]; // TODO wasteful clone and vec![] but ehhh convenient let x = vec![self.clone()]; // TODO wasteful clone and vec![] but ehhh convenient
x.load_attachments_batch(db).await x.load_attachments_batch(db).await
} }

View file

@ -4,7 +4,9 @@ use sea_orm::entity::prelude::*;
#[sea_orm(table_name = "configs")] #[sea_orm(table_name = "configs")]
pub struct Model { pub struct Model {
#[sea_orm(primary_key)] #[sea_orm(primary_key)]
pub id: String, pub internal: i64,
#[sea_orm(unique)]
pub actor: String,
pub accept_follow_requests: bool, pub accept_follow_requests: bool,
pub show_followers_count: bool, pub show_followers_count: bool,
pub show_following_count: bool, pub show_following_count: bool,
@ -15,7 +17,7 @@ pub struct Model {
impl Default for Model { impl Default for Model {
fn default() -> Self { fn default() -> Self {
Model { Model {
id: "".to_string(), internal: 0, actor: "".into(),
accept_follow_requests: true, accept_follow_requests: true,
show_following_count: true, show_following_count: true,
show_following: true, show_following: true,
@ -28,16 +30,18 @@ impl Default for Model {
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation { pub enum Relation {
#[sea_orm( #[sea_orm(
belongs_to = "super::user::Entity", belongs_to = "super::actor::Entity",
from = "Column::Id", from = "Column::Actor",
to = "super::user::Column::Id" to = "super::actor::Column::Id",
on_update = "Cascade",
on_delete = "Cascade"
)] )]
User, Actors,
} }
impl Related<super::user::Entity> for Entity { impl Related<super::actor::Entity> for Entity {
fn to() -> RelationDef { fn to() -> RelationDef {
Relation::User.def() Relation::Actors.def()
} }
} }

View file

@ -4,24 +4,28 @@ use sea_orm::entity::prelude::*;
#[sea_orm(table_name = "credentials")] #[sea_orm(table_name = "credentials")]
pub struct Model { pub struct Model {
#[sea_orm(primary_key)] #[sea_orm(primary_key)]
pub id: String, pub internal: i64,
pub email: String, #[sea_orm(unique)]
pub actor: String,
pub login: String,
pub password: String, pub password: String,
} }
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation { pub enum Relation {
#[sea_orm( #[sea_orm(
belongs_to = "super::user::Entity", belongs_to = "super::actor::Entity",
from = "Column::Id", from = "Column::Actor",
to = "super::user::Column::Id" to = "super::actor::Column::Id",
on_update = "Cascade",
on_delete = "Cascade"
)] )]
User, Actors,
} }
impl Related<super::user::Entity> for Entity { impl Related<super::actor::Entity> for Entity {
fn to() -> RelationDef { fn to() -> RelationDef {
Relation::User.def() Relation::Actors.def()
} }
} }

View file

@ -4,11 +4,11 @@ use sea_orm::entity::prelude::*;
#[sea_orm(table_name = "deliveries")] #[sea_orm(table_name = "deliveries")]
pub struct Model { pub struct Model {
#[sea_orm(primary_key)] #[sea_orm(primary_key)]
pub id: i64, pub internal: i64,
pub actor: String, pub actor: String,
pub target: String, pub target: String,
pub activity: String, pub activity: String,
pub created: ChronoDateTimeUtc, pub published: ChronoDateTimeUtc,
pub not_before: ChronoDateTimeUtc, pub not_before: ChronoDateTimeUtc,
pub attempt: i32, pub attempt: i32,
} }
@ -18,14 +18,30 @@ pub enum Relation {
#[sea_orm( #[sea_orm(
belongs_to = "super::activity::Entity", belongs_to = "super::activity::Entity",
from = "Column::Activity", from = "Column::Activity",
to = "super::activity::Column::Id" to = "super::activity::Column::Id",
on_update = "Cascade",
on_delete = "Cascade"
)] )]
Activity, Activities,
#[sea_orm(
belongs_to = "super::actor::Entity",
from = "Column::Actor",
to = "super::actor::Column::Id",
on_update = "Cascade",
on_delete = "Cascade"
)]
Actors,
} }
impl Related<super::activity::Entity> for Entity { impl Related<super::activity::Entity> for Entity {
fn to() -> RelationDef { fn to() -> RelationDef {
Relation::Activity.def() Relation::Activities.def()
}
}
impl Related<super::actor::Entity> for Entity {
fn to() -> RelationDef {
Relation::Actors.def()
} }
} }
@ -45,6 +61,6 @@ impl Model {
} }
pub fn expired(&self) -> bool { pub fn expired(&self) -> bool {
chrono::Utc::now() - self.created > chrono::Duration::days(7) chrono::Utc::now() - self.published > chrono::Duration::days(7)
} }
} }

View file

@ -1,28 +1,30 @@
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "shares")] #[sea_orm(table_name = "hashtags")]
pub struct Model { pub struct Model {
#[sea_orm(primary_key)] #[sea_orm(primary_key)]
pub id: i64, pub internal: i64,
pub actor: String, pub object: i64,
pub shares: String, pub name: String,
pub date: ChronoDateTimeUtc, pub published: ChronoDateTimeUtc,
} }
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation { pub enum Relation {
#[sea_orm( #[sea_orm(
belongs_to = "super::object::Entity", belongs_to = "super::object::Entity",
from = "Column::Shares", from = "Column::Object",
to = "super::object::Column::Id", to = "super::object::Column::Internal",
on_update = "Cascade",
on_delete = "Cascade"
)] )]
Object Objects,
} }
impl Related<super::object::Entity> for Entity { impl Related<super::object::Entity> for Entity {
fn to() -> RelationDef { fn to() -> RelationDef {
Relation::Object.def() Relation::Objects.def()
} }
} }

70
src/model/instance.rs Normal file
View file

@ -0,0 +1,70 @@
use nodeinfo::NodeInfoOwned;
use sea_orm::{entity::prelude::*, QuerySelect, SelectColumns};
use crate::errors::UpubError;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "instances")]
pub struct Model {
#[sea_orm(primary_key)]
pub internal: i64,
#[sea_orm(unique)]
pub domain: String,
pub name: Option<String>,
pub software: Option<String>,
pub version: Option<String>,
pub icon: Option<String>,
pub down_since: Option<ChronoDateTimeUtc>,
pub users: Option<i64>,
pub posts: Option<i64>,
pub published: ChronoDateTimeUtc,
pub updated: ChronoDateTimeUtc,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::actor::Entity")]
Actors,
#[sea_orm(has_many = "super::addressing::Entity")]
Addressing,
}
impl Related<super::actor::Entity> for Entity {
fn to() -> RelationDef {
Relation::Actors.def()
}
}
impl Related<super::addressing::Entity> for Entity {
fn to() -> RelationDef {
Relation::Addressing.def()
}
}
impl ActiveModelBehavior for ActiveModel {}
impl Entity {
pub fn find_by_domain(domain: &str) -> Select<Entity> {
Entity::find().filter(Column::Domain.eq(domain))
}
pub async fn domain_to_internal(domain: &str, db: &DatabaseConnection) -> crate::Result<i64> {
Entity::find()
.filter(Column::Domain.eq(domain))
.select_only()
.select_column(Column::Internal)
.into_tuple::<i64>()
.one(db)
.await?
.ok_or_else(UpubError::not_found)
}
pub async fn nodeinfo(domain: &str) -> crate::Result<NodeInfoOwned> {
Ok(
reqwest::get(format!("https://{domain}/nodeinfo/2.0.json"))
.await?
.json()
.await?
)
}
}

View file

@ -4,26 +4,63 @@ use sea_orm::entity::prelude::*;
#[sea_orm(table_name = "likes")] #[sea_orm(table_name = "likes")]
pub struct Model { pub struct Model {
#[sea_orm(primary_key)] #[sea_orm(primary_key)]
pub id: i64, pub internal: i64,
pub actor: String, pub actor: i64,
pub likes: String, pub object: i64,
pub date: ChronoDateTimeUtc, pub activity: i64,
pub published: ChronoDateTimeUtc,
} }
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation { pub enum Relation {
#[sea_orm( #[sea_orm(
belongs_to = "super::object::Entity", belongs_to = "super::activity::Entity",
from = "Column::Likes", from = "Column::Activity",
to = "super::object::Column::Id", to = "super::activity::Column::Internal",
on_update = "Cascade",
on_delete = "Cascade"
)] )]
Object Activities,
#[sea_orm(
belongs_to = "super::actor::Entity",
from = "Column::Actor",
to = "super::actor::Column::Internal",
on_update = "Cascade",
on_delete = "Cascade"
)]
Actors,
#[sea_orm(
belongs_to = "super::object::Entity",
from = "Column::Object",
to = "super::object::Column::Internal",
on_update = "Cascade",
on_delete = "Cascade"
)]
Objects,
}
impl Related<super::activity::Entity> for Entity {
fn to() -> RelationDef {
Relation::Activities.def()
}
}
impl Related<super::actor::Entity> for Entity {
fn to() -> RelationDef {
Relation::Actors.def()
}
} }
impl Related<super::object::Entity> for Entity { impl Related<super::object::Entity> for Entity {
fn to() -> RelationDef { fn to() -> RelationDef {
Relation::Object.def() Relation::Objects.def()
} }
} }
impl ActiveModelBehavior for ActiveModel {} impl ActiveModelBehavior for ActiveModel {}
impl Entity {
pub fn find_by_uid_oid(uid: i64, oid: i64) -> Select<Entity> {
Entity::find().filter(Column::Actor.eq(uid)).filter(Column::Object.eq(oid))
}
}

45
src/model/mention.rs Normal file
View file

@ -0,0 +1,45 @@
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "mentions")]
pub struct Model {
#[sea_orm(primary_key)]
pub internal: i64,
pub object: i64,
pub actor: String,
pub published: ChronoDateTimeUtc,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::actor::Entity",
from = "Column::Actor",
to = "super::actor::Column::Id",
on_update = "Cascade",
on_delete = "Cascade"
)]
Actors,
#[sea_orm(
belongs_to = "super::object::Entity",
from = "Column::Object",
to = "super::object::Column::Internal",
on_update = "Cascade",
on_delete = "Cascade"
)]
Objects,
}
impl Related<super::actor::Entity> for Entity {
fn to() -> RelationDef {
Relation::Actors.def()
}
}
impl Related<super::object::Entity> for Entity {
fn to() -> RelationDef {
Relation::Objects.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View file

@ -1,18 +1,23 @@
pub mod actor;
pub mod object; pub mod object;
pub mod activity; pub mod activity;
pub mod user;
pub mod config;
pub mod relay; pub mod config;
pub mod relation;
pub mod addressing;
pub mod share;
pub mod like;
pub mod credential; pub mod credential;
pub mod session; pub mod session;
pub mod instance;
pub mod delivery; pub mod delivery;
pub mod relation;
pub mod announce;
pub mod like;
pub mod hashtag;
pub mod mention;
pub mod attachment; pub mod attachment;
pub mod application;
pub mod addressing;
#[derive(Debug, Clone, thiserror::Error)] #[derive(Debug, Clone, thiserror::Error)]
#[error("missing required field: '{0}'")] #[error("missing required field: '{0}'")]

View file

@ -1,7 +1,7 @@
use apb::{BaseMut, Collection, CollectionMut, ObjectMut}; use apb::{BaseMut, Collection, CollectionMut, ObjectMut, ObjectType};
use sea_orm::entity::prelude::*; use sea_orm::{entity::prelude::*, QuerySelect, SelectColumns};
use crate::routes::activitypub::jsonld::LD; use crate::{errors::UpubError, routes::activitypub::jsonld::LD};
use super::Audience; use super::Audience;
@ -9,57 +9,181 @@ use super::Audience;
#[sea_orm(table_name = "objects")] #[sea_orm(table_name = "objects")]
pub struct Model { pub struct Model {
#[sea_orm(primary_key)] #[sea_orm(primary_key)]
pub internal: i64,
#[sea_orm(unique)]
pub id: String, pub id: String,
pub object_type: apb::ObjectType, pub object_type: ObjectType,
pub attributed_to: Option<String>, pub attributed_to: Option<String>,
pub name: Option<String>, pub name: Option<String>,
pub summary: Option<String>, pub summary: Option<String>,
pub content: Option<String>, pub content: Option<String>,
pub likes: i64, pub sensitive: bool,
pub shares: i64,
pub comments: i64,
pub context: Option<String>,
pub in_reply_to: Option<String>, pub in_reply_to: Option<String>,
pub cc: Audience, pub url: Option<String>,
pub bcc: Audience, pub likes: i32,
pub announces: i32,
pub replies: i32,
pub context: Option<String>,
pub to: Audience, pub to: Audience,
pub bto: Audience, pub bto: Audience,
pub url: Option<String>, pub cc: Audience,
pub bcc: Audience,
pub published: ChronoDateTimeUtc, pub published: ChronoDateTimeUtc,
pub updated: Option<ChronoDateTimeUtc>, pub updated: ChronoDateTimeUtc,
}
pub sensitive: bool, #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::activity::Entity")]
Activities,
#[sea_orm(
belongs_to = "super::actor::Entity",
from = "Column::AttributedTo",
to = "super::actor::Column::Id",
on_update = "Cascade",
on_delete = "NoAction"
)]
Actors,
#[sea_orm(has_many = "super::addressing::Entity")]
Addressing,
#[sea_orm(has_many = "super::announce::Entity")]
Announces,
#[sea_orm(has_many = "super::attachment::Entity")]
Attachments,
#[sea_orm(has_many = "super::hashtag::Entity")]
Hashtags,
#[sea_orm(has_many = "super::like::Entity")]
Likes,
#[sea_orm(has_many = "super::mention::Entity")]
Mentions,
#[sea_orm(
belongs_to = "Entity",
from = "Column::InReplyTo",
to = "Column::Id",
on_update = "Cascade",
on_delete = "NoAction"
)]
Objects,
}
impl Related<super::activity::Entity> for Entity {
fn to() -> RelationDef {
Relation::Activities.def()
}
}
impl Related<super::actor::Entity> for Entity {
fn to() -> RelationDef {
Relation::Actors.def()
}
}
impl Related<super::addressing::Entity> for Entity {
fn to() -> RelationDef {
Relation::Addressing.def()
}
}
impl Related<super::announce::Entity> for Entity {
fn to() -> RelationDef {
Relation::Announces.def()
}
}
impl Related<super::attachment::Entity> for Entity {
fn to() -> RelationDef {
Relation::Attachments.def()
}
}
impl Related<super::hashtag::Entity> for Entity {
fn to() -> RelationDef {
Relation::Hashtags.def()
}
}
impl Related<super::like::Entity> for Entity {
fn to() -> RelationDef {
Relation::Likes.def()
}
}
impl Related<super::mention::Entity> for Entity {
fn to() -> RelationDef {
Relation::Mentions.def()
}
}
impl Related<Entity> for Entity {
fn to() -> RelationDef {
Relation::Objects.def()
}
}
impl ActiveModelBehavior for ActiveModel {}
impl Entity {
pub fn find_by_ap_id(id: &str) -> Select<Entity> {
Entity::find().filter(Column::Id.eq(id))
}
pub fn delete_by_ap_id(id: &str) -> sea_orm::DeleteMany<Entity> {
Entity::delete_many().filter(Column::Id.eq(id))
}
pub async fn ap_to_internal(id: &str, db: &DatabaseConnection) -> crate::Result<i64> {
Entity::find()
.filter(Column::Id.eq(id))
.select_only()
.select_column(Column::Internal)
.into_tuple::<i64>()
.one(db)
.await?
.ok_or_else(UpubError::not_found)
}
}
impl ActiveModel {
pub fn new(object: &impl apb::Object) -> Result<Self, super::FieldError> {
let t = object.object_type().ok_or(super::FieldError("type"))?;
if matches!(t,
apb::ObjectType::Activity(_)
| apb::ObjectType::Actor(_)
| apb::ObjectType::Collection(_)
| apb::ObjectType::Document(_)
) {
return Err(super::FieldError("type"));
}
Ok(ActiveModel {
internal: sea_orm::ActiveValue::NotSet,
id: sea_orm::ActiveValue::Set(object.id().ok_or(super::FieldError("id"))?.to_string()),
object_type: sea_orm::ActiveValue::Set(t),
attributed_to: sea_orm::ActiveValue::Set(object.attributed_to().id()),
name: sea_orm::ActiveValue::Set(object.name().map(|x| x.to_string())),
summary: sea_orm::ActiveValue::Set(object.summary().map(|x| x.to_string())),
content: sea_orm::ActiveValue::Set(object.content().map(|x| x.to_string())),
context: sea_orm::ActiveValue::Set(object.context().id()),
in_reply_to: sea_orm::ActiveValue::Set(object.in_reply_to().id()),
published: sea_orm::ActiveValue::Set(object.published().unwrap_or_else(chrono::Utc::now)),
updated: sea_orm::ActiveValue::Set(object.updated().unwrap_or_else(chrono::Utc::now)),
url: sea_orm::ActiveValue::Set(object.url().id()),
replies: sea_orm::ActiveValue::Set(object.replies().get()
.map_or(0, |x| x.total_items().unwrap_or(0)) as i32),
likes: sea_orm::ActiveValue::Set(object.likes().get()
.map_or(0, |x| x.total_items().unwrap_or(0)) as i32),
announces: sea_orm::ActiveValue::Set(object.shares().get()
.map_or(0, |x| x.total_items().unwrap_or(0)) as i32),
to: sea_orm::ActiveValue::Set(object.to().into()),
bto: sea_orm::ActiveValue::Set(object.bto().into()),
cc: sea_orm::ActiveValue::Set(object.cc().into()),
bcc: sea_orm::ActiveValue::Set(object.bcc().into()),
sensitive: sea_orm::ActiveValue::Set(object.sensitive().unwrap_or(false)),
})
}
} }
impl Model { impl Model {
pub fn new(object: &impl apb::Object) -> Result<Self, super::FieldError> {
Ok(Model {
id: object.id().ok_or(super::FieldError("id"))?.to_string(),
object_type: object.object_type().ok_or(super::FieldError("type"))?,
attributed_to: object.attributed_to().id(),
name: object.name().map(|x| x.to_string()),
summary: object.summary().map(|x| x.to_string()),
content: object.content().map(|x| x.to_string()),
context: object.context().id(),
in_reply_to: object.in_reply_to().id(),
published: object.published().ok_or(super::FieldError("published"))?,
updated: object.updated(),
url: object.url().id(),
comments: object.replies().get()
.map_or(0, |x| x.total_items().unwrap_or(0)) as i64,
likes: object.likes().get()
.map_or(0, |x| x.total_items().unwrap_or(0)) as i64,
shares: object.shares().get()
.map_or(0, |x| x.total_items().unwrap_or(0)) as i64,
to: object.to().into(),
bto: object.bto().into(),
cc: object.cc().into(),
bcc: object.bcc().into(),
sensitive: object.sensitive().unwrap_or(false),
})
}
pub fn ap(self) -> serde_json::Value { pub fn ap(self) -> serde_json::Value {
serde_json::Value::new_object() serde_json::Value::new_object()
.set_id(Some(&self.id)) .set_id(Some(&self.id))
@ -72,7 +196,7 @@ impl Model {
.set_conversation(apb::Node::maybe_link(self.context.clone())) // duplicate context for mastodon .set_conversation(apb::Node::maybe_link(self.context.clone())) // duplicate context for mastodon
.set_in_reply_to(apb::Node::maybe_link(self.in_reply_to.clone())) .set_in_reply_to(apb::Node::maybe_link(self.in_reply_to.clone()))
.set_published(Some(self.published)) .set_published(Some(self.published))
.set_updated(self.updated) .set_updated(Some(self.updated))
.set_to(apb::Node::links(self.to.0.clone())) .set_to(apb::Node::links(self.to.0.clone()))
.set_bto(apb::Node::Empty) .set_bto(apb::Node::Empty)
.set_cc(apb::Node::links(self.cc.0.clone())) .set_cc(apb::Node::links(self.cc.0.clone()))
@ -82,7 +206,7 @@ impl Model {
.set_shares(apb::Node::object( .set_shares(apb::Node::object(
serde_json::Value::new_object() serde_json::Value::new_object()
.set_collection_type(Some(apb::CollectionType::OrderedCollection)) .set_collection_type(Some(apb::CollectionType::OrderedCollection))
.set_total_items(Some(self.shares 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() serde_json::Value::new_object()
@ -92,74 +216,11 @@ impl Model {
.set_replies(apb::Node::object( .set_replies(apb::Node::object(
serde_json::Value::new_object() serde_json::Value::new_object()
.set_collection_type(Some(apb::CollectionType::OrderedCollection)) .set_collection_type(Some(apb::CollectionType::OrderedCollection))
.set_total_items(Some(self.comments as u64)) .set_total_items(Some(self.replies as u64))
)) ))
} }
} }
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::activity::Entity")]
Activity,
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::AttributedTo",
to = "super::user::Column::Id",
)]
User,
#[sea_orm(has_many = "super::addressing::Entity")]
Addressing,
#[sea_orm(has_many = "super::attachment::Entity")]
Attachment,
#[sea_orm(has_many = "super::like::Entity")]
Like,
#[sea_orm(has_many = "super::share::Entity")]
Share,
}
impl Related<super::activity::Entity> for Entity {
fn to() -> RelationDef {
Relation::Activity.def()
}
}
impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::User.def()
}
}
impl Related<super::addressing::Entity> for Entity {
fn to() -> RelationDef {
Relation::Addressing.def()
}
}
impl Related<super::attachment::Entity> for Entity {
fn to() -> RelationDef {
Relation::Attachment.def()
}
}
impl Related<super::like::Entity> for Entity {
fn to() -> RelationDef {
Relation::Like.def()
}
}
impl Related<super::share::Entity> for Entity {
fn to() -> RelationDef {
Relation::Share.def()
}
}
impl ActiveModelBehavior for ActiveModel {}
impl apb::target::Addressed for Model { impl apb::target::Addressed for Model {
fn addressed(&self) -> Vec<String> { fn addressed(&self) -> Vec<String> {
let mut to : Vec<String> = self.to.0.clone(); let mut to : Vec<String> = self.to.0.clone();

View file

@ -1,16 +1,119 @@
use sea_orm::entity::prelude::*; use sea_orm::{entity::prelude::*, QuerySelect, SelectColumns};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "relations")] #[sea_orm(table_name = "relations")]
pub struct Model { pub struct Model {
#[sea_orm(primary_key)] #[sea_orm(primary_key)]
pub id: i64, pub internal: i64,
pub follower: String, pub follower: i64,
pub following: String, pub following: i64,
pub accept: Option<i64>,
pub activity: i64,
} }
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {} pub enum Relation {
// TODO how to represent this User-to-User relation in sea orm?? #[sea_orm(
belongs_to = "super::activity::Entity",
from = "Column::Accept",
to = "super::activity::Column::Internal",
on_update = "Cascade",
on_delete = "NoAction"
)]
ActivitiesAccept,
#[sea_orm(
belongs_to = "super::activity::Entity",
from = "Column::Activity",
to = "super::activity::Column::Internal",
on_update = "Cascade",
on_delete = "NoAction"
)]
ActivitiesFollow,
#[sea_orm(
belongs_to = "super::actor::Entity",
from = "Column::Follower",
to = "super::actor::Column::Internal",
on_update = "Cascade",
on_delete = "Cascade"
)]
ActorsFollower,
#[sea_orm(
belongs_to = "super::actor::Entity",
from = "Column::Following",
to = "super::actor::Column::Internal",
on_update = "Cascade",
on_delete = "Cascade"
)]
ActorsFollowing,
}
impl Related<super::actor::Entity> for Entity {
fn to() -> RelationDef {
Relation::ActorsFollowing.def()
}
}
impl Related<super::activity::Entity> for Entity {
fn to() -> RelationDef {
Relation::ActivitiesFollow.def()
}
}
impl ActiveModelBehavior for ActiveModel {} impl ActiveModelBehavior for ActiveModel {}
impl Entity {
// TODO this is 2 queries!!! can it be optimized down to 1?
pub async fn followers(uid: &str, db: &DatabaseConnection) -> crate::Result<Vec<String>> {
let internal_id = super::actor::Entity::ap_to_internal(uid, db).await?;
let out = Entity::find()
.join(
sea_orm::JoinType::InnerJoin,
Entity::belongs_to(super::actor::Entity)
.from(Column::Follower)
.to(super::actor::Column::Internal)
.into()
)
.filter(Column::Accept.is_not_null())
.filter(Column::Following.eq(internal_id))
.select_only()
.select_column(super::actor::Column::Id)
.into_tuple::<String>()
.all(db)
.await?;
Ok(out)
}
// TODO this is 2 queries!!! can it be optimized down to 1?
pub async fn following(uid: &str, db: &DatabaseConnection) -> crate::Result<Vec<String>> {
let internal_id = super::actor::Entity::ap_to_internal(uid, db).await?;
let out = Entity::find()
.join(
sea_orm::JoinType::InnerJoin,
Entity::belongs_to(super::actor::Entity)
.from(Column::Following)
.to(super::actor::Column::Internal)
.into()
)
.filter(Column::Accept.is_not_null())
.filter(Column::Follower.eq(internal_id))
.select_only()
.select_column(super::actor::Column::Id)
.into_tuple::<String>()
.all(db)
.await?;
Ok(out)
}
// TODO this is 3 queries!!! can it be optimized down to 1?
pub fn is_following(follower: i64, following: i64) -> sea_orm::Selector<sea_orm::SelectGetableTuple<i64>> {
Entity::find()
.filter(Column::Accept.is_not_null())
.filter(Column::Follower.eq(follower))
.filter(Column::Following.eq(following))
.select_only()
.select_column(Column::Internal)
.into_tuple::<i64>()
}
}

View file

@ -1,16 +0,0 @@
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "relays")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: String,
pub accepted: bool,
pub forwarding: bool,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
// TODO how to represent this User-to-User relation in sea orm??
impl ActiveModelBehavior for ActiveModel {}

View file

@ -4,24 +4,27 @@ use sea_orm::entity::prelude::*;
#[sea_orm(table_name = "sessions")] #[sea_orm(table_name = "sessions")]
pub struct Model { pub struct Model {
#[sea_orm(primary_key)] #[sea_orm(primary_key)]
pub id: String, pub internal: i64,
pub actor: String, pub actor: String,
pub secret: String,
pub expires: ChronoDateTimeUtc, pub expires: ChronoDateTimeUtc,
} }
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation { pub enum Relation {
#[sea_orm( #[sea_orm(
belongs_to = "super::user::Entity", belongs_to = "super::actor::Entity",
from = "Column::Actor", from = "Column::Actor",
to = "super::user::Column::Id" to = "super::actor::Column::Id",
on_update = "Cascade",
on_delete = "Cascade"
)] )]
User, Actors,
} }
impl Related<super::user::Entity> for Entity { impl Related<super::actor::Entity> for Entity {
fn to() -> RelationDef { fn to() -> RelationDef {
Relation::User.def() Relation::Actors.def()
} }
} }

View file

@ -1,176 +0,0 @@
use sea_orm::entity::prelude::*;
use apb::{Actor, ActorMut, ActorType, BaseMut, DocumentMut, Endpoints, EndpointsMut, Object, ObjectMut, PublicKey, PublicKeyMut};
use crate::routes::activitypub::jsonld::LD;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "users")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: String,
pub domain: String,
pub actor_type: ActorType,
pub preferred_username: String,
pub name: Option<String>,
pub summary: Option<String>,
pub image: Option<String>,
pub icon: Option<String>,
pub inbox: Option<String>,
pub shared_inbox: Option<String>,
pub outbox: Option<String>,
pub following: Option<String>,
pub followers: Option<String>,
pub following_count: i64,
pub followers_count: i64,
pub statuses_count: i64,
pub public_key: String,
pub private_key: Option<String>,
pub created: ChronoDateTimeUtc,
pub updated: ChronoDateTimeUtc,
// TODO these are also suggested
// pub liked: Option<String>,
// pub streams: Option<String>,
}
impl Model {
pub fn new(object: &impl Actor) -> Result<Self, super::FieldError> {
let ap_id = object.id().ok_or(super::FieldError("id"))?.to_string();
let (domain, fallback_preferred_username) = split_user_id(&ap_id);
Ok(Model {
id: ap_id,
domain,
preferred_username: object.preferred_username().unwrap_or(&fallback_preferred_username).to_string(),
actor_type: object.actor_type().ok_or(super::FieldError("type"))?,
name: object.name().map(|x| x.to_string()),
summary: object.summary().map(|x| x.to_string()),
icon: object.icon().get().and_then(|x| x.url().id()),
image: object.image().get().and_then(|x| x.url().id()),
inbox: object.inbox().id(),
outbox: object.outbox().id(),
shared_inbox: object.endpoints().get().and_then(|x| Some(x.shared_inbox()?.to_string())),
followers: object.followers().id(),
following: object.following().id(),
created: object.published().unwrap_or(chrono::Utc::now()),
updated: chrono::Utc::now(),
following_count: object.following_count().unwrap_or(0) as i64,
followers_count: object.followers_count().unwrap_or(0) as i64,
statuses_count: object.statuses_count().unwrap_or(0) as i64,
public_key: object.public_key().get().ok_or(super::FieldError("publicKey"))?.public_key_pem().to_string(),
private_key: None, // there's no way to transport privkey over AP json, must come from DB
})
}
pub fn ap(self) -> serde_json::Value {
serde_json::Value::new_object()
.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()
.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()
.set_document_type(Some(apb::DocumentType::Image))
.set_url(apb::Node::link(i.clone()))
)))
.set_published(Some(self.created))
.set_preferred_username(Some(&self.preferred_username))
.set_statuses_count(Some(self.statuses_count as u64))
.set_followers_count(Some(self.followers_count as u64))
.set_following_count(Some(self.following_count as u64))
.set_inbox(apb::Node::maybe_link(self.inbox))
.set_outbox(apb::Node::maybe_link(self.outbox))
.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()
.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()
.set_shared_inbox(self.shared_inbox.as_deref())
))
.set_discoverable(Some(true))
}
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::activity::Entity")]
Activity,
#[sea_orm(has_many = "super::object::Entity")]
Object,
#[sea_orm(has_one = "super::config::Entity")]
Config,
#[sea_orm(has_one = "super::credential::Entity")]
Credential,
#[sea_orm(has_many = "super::session::Entity")]
Session,
#[sea_orm(has_many = "super::addressing::Entity")]
Addressing,
}
impl Related<super::activity::Entity> for Entity {
fn to() -> RelationDef {
Relation::Activity.def()
}
}
impl Related<super::object::Entity> for Entity {
fn to() -> RelationDef {
Relation::Object.def()
}
}
impl Related<super::config::Entity> for Entity {
fn to() -> RelationDef {
Relation::Config.def()
}
}
impl Related<super::credential::Entity> for Entity {
fn to() -> RelationDef {
Relation::Credential.def()
}
}
impl Related<super::session::Entity> for Entity {
fn to() -> RelationDef {
Relation::Session.def()
}
}
impl Related<super::addressing::Entity> for Entity {
fn to() -> RelationDef {
Relation::Addressing.def()
}
}
impl ActiveModelBehavior for ActiveModel {}
fn split_user_id(id: &str) -> (String, String) {
let clean = id
.replace("http://", "")
.replace("https://", "");
let mut splits = clean.split('/');
let first = splits.next().unwrap_or("");
let last = splits.last().unwrap_or(first);
(first.to_string(), last.to_string())
}

View file

@ -27,7 +27,7 @@ pub async fn view(
.ok_or_else(UpubError::not_found)?; .ok_or_else(UpubError::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.id()); let attach = attachments.remove(&row.internal());
Ok(JsonLD(row.ap(attach).ld_context())) Ok(JsonLD(row.ap(attach).ld_context()))
} }

View file

@ -2,7 +2,7 @@ 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 crate::{errors::UpubError, server::{auth::{AuthIdentity, Identity}, fetcher::Fetcher, Context}, url}; use crate::{errors::UpubError, server::{auth::AuthIdentity, fetcher::Fetcher, Context}, url};
use super::{jsonld::LD, JsonLD}; use super::{jsonld::LD, JsonLD};
@ -26,14 +26,14 @@ pub async fn view(
.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(url!(ctx, "/inbox")))
.set_outbox(apb::Node::link(url!(ctx, "/outbox"))) .set_outbox(apb::Node::link(url!(ctx, "/outbox")))
.set_published(Some(ctx.app().created)) .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() serde_json::Value::new_object()
.set_id(Some(&url!(ctx, "#main-key"))) .set_id(Some(&url!(ctx, "#main-key")))
.set_owner(Some(&url!(ctx, ""))) .set_owner(Some(&url!(ctx, "")))
.set_public_key_pem(&ctx.app().public_key) .set_public_key_pem(&ctx.actor().public_key)
)) ))
.ld_context() .ld_context()
).into_response()) ).into_response())
@ -50,7 +50,7 @@ pub async fn proxy_get(
AuthIdentity(auth): AuthIdentity, AuthIdentity(auth): AuthIdentity,
) -> crate::Result<Json<serde_json::Value>> { ) -> crate::Result<Json<serde_json::Value>> {
// only local users can request fetches // only local users can request fetches
if !ctx.cfg().security.allow_public_debugger && !matches!(auth, Identity::Local(_)) { if !ctx.cfg().security.allow_public_debugger && !auth.is_local() {
return Err(UpubError::unauthorized()); return Err(UpubError::unauthorized());
} }
Ok(Json( Ok(Json(
@ -59,7 +59,7 @@ pub async fn proxy_get(
&query.id, &query.id,
None, None,
ctx.base(), ctx.base(),
&ctx.app().private_key, ctx.pkey(),
&format!("{}+proxy", ctx.domain()), &format!("{}+proxy", ctx.domain()),
) )
.await? .await?
@ -74,7 +74,7 @@ pub async fn proxy_form(
Form(query): Form<FetchPath>, Form(query): Form<FetchPath>,
) -> crate::Result<Json<serde_json::Value>> { ) -> crate::Result<Json<serde_json::Value>> {
// only local users can request fetches // only local users can request fetches
if !ctx.cfg().security.allow_public_debugger && !matches!(auth, Identity::Local(_)) { if !ctx.cfg().security.allow_public_debugger && auth.is_local() {
return Err(UpubError::unauthorized()); return Err(UpubError::unauthorized());
} }
Ok(Json( Ok(Json(
@ -83,7 +83,7 @@ pub async fn proxy_form(
&query.id, &query.id,
None, None,
ctx.base(), ctx.base(),
&ctx.app().private_key, ctx.pkey(),
&format!("{}+proxy", ctx.domain()), &format!("{}+proxy", ctx.domain()),
) )
.await? .await?

View file

@ -1,6 +1,6 @@
use axum::{http::StatusCode, extract::State, Json}; use axum::{http::StatusCode, extract::State, Json};
use rand::Rng; use rand::Rng;
use sea_orm::{ColumnTrait, Condition, EntityTrait, QueryFilter}; use sea_orm::{ActiveValue::{Set, NotSet}, ColumnTrait, Condition, EntityTrait, QueryFilter};
use crate::{errors::UpubError, model, server::{admin::Administrable, Context}}; use crate::{errors::UpubError, model, server::{admin::Administrable, Context}};
@ -18,6 +18,15 @@ pub struct AuthSuccess {
expires: chrono::DateTime<chrono::Utc>, expires: chrono::DateTime<chrono::Utc>,
} }
fn token() -> String {
// TODO should probably use crypto-safe rng
rand::thread_rng()
.sample_iter(&rand::distributions::Alphanumeric)
.take(128)
.map(char::from)
.collect()
}
pub async fn login( pub async fn login(
State(ctx): State<Context>, State(ctx): State<Context>,
Json(login): Json<LoginForm> Json(login): Json<LoginForm>
@ -25,24 +34,20 @@ pub async fn login(
// TODO salt the pwd // TODO salt the pwd
match model::credential::Entity::find() match model::credential::Entity::find()
.filter(Condition::all() .filter(Condition::all()
.add(model::credential::Column::Email.eq(login.email)) .add(model::credential::Column::Login.eq(login.email))
.add(model::credential::Column::Password.eq(sha256::digest(login.password))) .add(model::credential::Column::Password.eq(sha256::digest(login.password)))
) )
.one(ctx.db()) .one(ctx.db())
.await? .await?
{ {
Some(x) => { Some(x) => {
// TODO should probably use crypto-safe rng let token = token();
let token : String = rand::thread_rng()
.sample_iter(&rand::distributions::Alphanumeric)
.take(128)
.map(char::from)
.collect();
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( model::session::Entity::insert(
model::session::ActiveModel { model::session::ActiveModel {
id: sea_orm::ActiveValue::Set(token.clone()), internal: sea_orm::ActiveValue::NotSet,
actor: sea_orm::ActiveValue::Set(x.id.clone()), secret: sea_orm::ActiveValue::Set(token.clone()),
actor: sea_orm::ActiveValue::Set(x.actor.clone()),
expires: sea_orm::ActiveValue::Set(expires), expires: sea_orm::ActiveValue::Set(expires),
} }
) )
@ -50,13 +55,52 @@ pub async fn login(
.await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; .await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(AuthSuccess { Ok(Json(AuthSuccess {
token, expires, token, expires,
user: x.id user: x.actor
})) }))
}, },
None => Err(UpubError::unauthorized()), None => Err(UpubError::unauthorized()),
} }
} }
#[derive(Debug, Clone, serde::Deserialize)]
pub struct RefreshForm {
token: String,
}
pub async fn refresh(
State(ctx): State<Context>,
Json(login): Json<RefreshForm>
) -> crate::Result<Json<AuthSuccess>> {
if !ctx.cfg().security.allow_login_refresh {
return Err(UpubError::forbidden());
}
let prev = model::session::Entity::find()
.filter(model::session::Column::Secret.eq(login.token))
.one(ctx.db())
.await?
.ok_or_else(UpubError::unauthorized)?;
if prev.expires > chrono::Utc::now() {
return Ok(Json(AuthSuccess { token: prev.secret, user: prev.actor, expires: prev.expires }));
}
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 {
internal: NotSet,
actor: Set(user.clone()),
secret: Set(token.clone()),
expires: Set(expires),
};
model::session::Entity::insert(new_session)
.exec(ctx.db())
.await?;
Ok(Json(AuthSuccess { token, expires, user }))
}
#[derive(Debug, Clone, serde::Deserialize)] #[derive(Debug, Clone, serde::Deserialize)]
pub struct RegisterForm { pub struct RegisterForm {
username: String, username: String,

View file

@ -9,7 +9,7 @@ pub async fn get(
AuthIdentity(auth): AuthIdentity, AuthIdentity(auth): AuthIdentity,
) -> crate::Result<JsonLD<serde_json::Value>> { ) -> crate::Result<JsonLD<serde_json::Value>> {
let local_context_id = url!(ctx, "/context/{id}"); let local_context_id = url!(ctx, "/context/{id}");
let context = ctx.context_id(&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())
.filter(auth.filter_condition()) .filter(auth.filter_condition())
@ -26,7 +26,7 @@ pub async fn page(
Query(page): Query<Pagination>, Query(page): Query<Pagination>,
AuthIdentity(auth): AuthIdentity, AuthIdentity(auth): AuthIdentity,
) -> crate::Result<JsonLD<serde_json::Value>> { ) -> crate::Result<JsonLD<serde_json::Value>> {
let context = ctx.context_id(&id); let context = ctx.oid(&id);
crate::server::builders::paginate( crate::server::builders::paginate(
url!(ctx, "/context/{id}/page"), url!(ctx, "/context/{id}/page"),
@ -36,6 +36,7 @@ pub async fn page(
ctx.db(), ctx.db(),
page, page,
auth.my_id(), auth.my_id(),
false,
) )
.await .await
} }

View file

@ -20,11 +20,12 @@ pub async fn page(
) -> crate::Result<JsonLD<serde_json::Value>> { ) -> crate::Result<JsonLD<serde_json::Value>> {
crate::server::builders::paginate( crate::server::builders::paginate(
url!(ctx, "/inbox/page"), url!(ctx, "/inbox/page"),
crate::model::addressing::Column::Actor.eq(apb::target::PUBLIC) crate::model::addressing::Column::Actor.is_null()
.into_condition(), .into_condition(),
ctx.db(), ctx.db(),
page, page,
auth.my_id(), auth.my_id(),
false,
) )
.await .await
} }
@ -41,7 +42,7 @@ pub async fn post(
AuthIdentity(auth): AuthIdentity, AuthIdentity(auth): AuthIdentity,
Json(activity): Json<serde_json::Value> Json(activity): Json<serde_json::Value>
) -> crate::Result<()> { ) -> crate::Result<()> {
let Identity::Remote(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!
// we basically received a delete for a user we can't fetch and verify, meaning remote // we basically received a delete for a user we can't fetch and verify, meaning remote
@ -63,8 +64,7 @@ pub async fn post(
return Err(UpubError::bad_request()); return Err(UpubError::bad_request());
}; };
// TODO add whitelist of relays if server != Context::server(&actor) {
if !server.ends_with(&Context::server(&actor)) {
return Err(UpubError::unauthorized()); return Err(UpubError::unauthorized());
} }

View file

@ -11,7 +11,7 @@ pub mod well_known;
pub mod jsonld; pub mod jsonld;
pub use jsonld::JsonLD; pub use jsonld::JsonLD;
use axum::{http::StatusCode, response::IntoResponse, routing::{get, 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;
@ -35,8 +35,9 @@ impl ActivityPubRouter for Router<crate::server::Context> {
.route("/outbox", get(ap::outbox::get)) .route("/outbox", get(ap::outbox::get))
.route("/outbox/page", get(ap::outbox::page)) .route("/outbox/page", get(ap::outbox::page))
// AUTH routes // AUTH routes
.route("/auth", post(ap::auth::login))
.route("/auth", put(ap::auth::register)) .route("/auth", put(ap::auth::register))
.route("/auth", post(ap::auth::login))
.route("/auth", patch(ap::auth::refresh))
// .well-known and discovery // .well-known and discovery
.route("/.well-known/webfinger", get(ap::well_known::webfinger)) .route("/.well-known/webfinger", get(ap::well_known::webfinger))
.route("/.well-known/host-meta", get(ap::well_known::host_meta)) .route("/.well-known/host-meta", get(ap::well_known::host_meta))
@ -44,17 +45,17 @@ impl ActivityPubRouter for Router<crate::server::Context> {
.route("/.well-known/oauth-authorization-server", get(ap::well_known::oauth_authorization_server)) .route("/.well-known/oauth-authorization-server", get(ap::well_known::oauth_authorization_server))
.route("/nodeinfo/:version", get(ap::well_known::nodeinfo)) .route("/nodeinfo/:version", get(ap::well_known::nodeinfo))
// actor routes // actor routes
.route("/users/:id", get(ap::user::view)) .route("/actors/:id", get(ap::user::view))
.route("/users/:id/inbox", post(ap::user::inbox::post)) .route("/actors/:id/inbox", post(ap::user::inbox::post))
.route("/users/:id/inbox", get(ap::user::inbox::get)) .route("/actors/:id/inbox", get(ap::user::inbox::get))
.route("/users/:id/inbox/page", get(ap::user::inbox::page)) .route("/actors/:id/inbox/page", get(ap::user::inbox::page))
.route("/users/:id/outbox", post(ap::user::outbox::post)) .route("/actors/:id/outbox", post(ap::user::outbox::post))
.route("/users/:id/outbox", get(ap::user::outbox::get)) .route("/actors/:id/outbox", get(ap::user::outbox::get))
.route("/users/:id/outbox/page", get(ap::user::outbox::page)) .route("/actors/:id/outbox/page", get(ap::user::outbox::page))
.route("/users/:id/followers", get(ap::user::following::get::<false>)) .route("/actors/:id/followers", get(ap::user::following::get::<false>))
.route("/users/:id/followers/page", get(ap::user::following::page::<false>)) .route("/actors/:id/followers/page", get(ap::user::following::page::<false>))
.route("/users/:id/following", get(ap::user::following::get::<true>)) .route("/actors/:id/following", get(ap::user::following::get::<true>))
.route("/users/:id/following/page", get(ap::user::following::page::<true>)) .route("/actors/:id/following/page", get(ap::user::following::page::<true>))
// activities // activities
.route("/activities/:id", get(ap::activity::view)) .route("/activities/:id", get(ap::activity::view))
// context // context

View file

@ -2,7 +2,7 @@ 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, EntityTrait, ModelTrait, QueryFilter, QuerySelect, SelectColumns}; use sea_orm::{ColumnTrait, ModelTrait, QueryFilter, QuerySelect, SelectColumns};
use crate::{errors::UpubError, model::{self, addressing::Event}, server::{auth::AuthIdentity, fetcher::Fetcher, Context}}; use crate::{errors::UpubError, model::{self, addressing::Event}, server::{auth::AuthIdentity, fetcher::Fetcher, Context}};
@ -62,7 +62,7 @@ pub async fn view(
// .set_id(Some(&crate::url!(ctx, "/objects/{id}/replies"))) // .set_id(Some(&crate::url!(ctx, "/objects/{id}/replies")))
// .set_first(apb::Node::link(crate::url!(ctx, "/objects/{id}/replies/page"))) // .set_first(apb::Node::link(crate::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.comments 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

@ -42,6 +42,7 @@ pub async fn page(
ctx.db(), ctx.db(),
page, page,
auth.my_id(), auth.my_id(),
false,
) )
.await .await
} }

View file

@ -1,4 +1,5 @@
use axum::{extract::{Query, State}, http::StatusCode, Json}; use axum::{extract::{Query, State}, http::StatusCode, Json};
use sea_orm::{ColumnTrait, Condition};
use crate::{errors::UpubError, routes::activitypub::{CreationResult, JsonLD, Pagination}, server::{auth::AuthIdentity, Context}, url}; use crate::{errors::UpubError, routes::activitypub::{CreationResult, JsonLD, Pagination}, server::{auth::AuthIdentity, Context}, url};
@ -13,10 +14,13 @@ pub async fn page(
) -> crate::Result<JsonLD<serde_json::Value>> { ) -> crate::Result<JsonLD<serde_json::Value>> {
crate::server::builders::paginate( crate::server::builders::paginate(
url!(ctx, "/outbox/page"), url!(ctx, "/outbox/page"),
auth.filter_condition(), // TODO filter local only stuff Condition::all()
.add(auth.filter_condition())
.add(crate::model::actor::Column::Domain.eq(ctx.domain().to_string())),
ctx.db(), ctx.db(),
page, page,
auth.my_id(), auth.my_id(),
true,
) )
.await .await
} }

View file

@ -17,7 +17,7 @@ pub async fn get<const OUTGOING: bool>(
0 0
}); });
crate::server::builders::collection(&url!(ctx, "/users/{id}/{follow___}"), Some(count)) crate::server::builders::collection(&url!(ctx, "/actors/{id}/{follow___}"), Some(count))
} }
pub async fn page<const OUTGOING: bool>( pub async fn page<const OUTGOING: bool>(
@ -40,7 +40,7 @@ pub async fn page<const OUTGOING: bool>(
.await?; .await?;
crate::server::builders::collection_page( crate::server::builders::collection_page(
&url!(ctx, "/users/{id}/{follow___}/page"), &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

@ -10,9 +10,9 @@ pub async fn get(
) -> crate::Result<JsonLD<serde_json::Value>> { ) -> crate::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(user) => if ctx.uid(&id) == user { Identity::Local { id: user, .. } => if ctx.uid(&id) == user {
crate::server::builders::collection(&url!(ctx, "/users/{id}/inbox"), None) crate::server::builders::collection(&url!(ctx, "/actors/{id}/inbox"), None)
} else { } else {
Err(StatusCode::FORBIDDEN.into()) Err(StatusCode::FORBIDDEN.into())
}, },
@ -25,7 +25,7 @@ pub async fn page(
AuthIdentity(auth): AuthIdentity, AuthIdentity(auth): AuthIdentity,
Query(page): Query<Pagination>, Query(page): Query<Pagination>,
) -> crate::Result<JsonLD<serde_json::Value>> { ) -> crate::Result<JsonLD<serde_json::Value>> {
let Identity::Local(uid) = &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(UpubError::forbidden());
}; };
@ -34,14 +34,15 @@ pub async fn page(
} }
crate::server::builders::paginate( crate::server::builders::paginate(
url!(ctx, "/users/{id}/inbox/page"), url!(ctx, "/actors/{id}/inbox/page"),
Condition::any() Condition::any()
.add(model::addressing::Column::Actor.eq(uid)) .add(model::addressing::Column::Actor.eq(*internal))
.add(model::object::Column::AttributedTo.eq(uid)) .add(model::object::Column::AttributedTo.eq(uid))
.add(model::activity::Column::Actor.eq(uid)), .add(model::activity::Column::Actor.eq(uid)),
ctx.db(), ctx.db(),
page, page,
auth.my_id(), auth.my_id(),
false,
) )
.await .await
} }

View file

@ -5,10 +5,9 @@ pub mod outbox;
pub mod following; pub mod following;
use axum::extract::{Path, Query, State}; use axum::extract::{Path, Query, State};
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QuerySelect, SelectColumns};
use apb::{ActorMut, EndpointsMut, Node}; use apb::{ActorMut, EndpointsMut, Node, ObjectMut};
use crate::{errors::UpubError, model::{self, user}, server::{auth::AuthIdentity, fetcher::Fetcher, Context}, url}; use crate::{errors::UpubError, model, server::{auth::AuthIdentity, builders::AnyQuery, fetcher::Fetcher, Context}, url};
use super::{jsonld::LD, JsonLD, TryFetch}; use super::{jsonld::LD, JsonLD, TryFetch};
@ -30,48 +29,31 @@ pub async fn view(
ctx.fetch_user(&uid).await?; ctx.fetch_user(&uid).await?;
} }
} }
let internal_uid = model::actor::Entity::ap_to_internal(&uid, ctx.db()).await?;
let (followed_by_me, following_me) = match auth.my_id() { let (followed_by_me, following_me) = match auth.my_id() {
None => (None, None), None => (None, None),
Some(my_id) => { Some(my_id) => {
// TODO these two queries are fast because of indexes but still are 2 subqueries for each // TODO these two queries are fast because of indexes but still are 2 subqueries for each
// user GET, not even parallelized... should really add these as joins on the main query, so // user GET, not even parallelized... should maybe add these as joins on the main query? so
// that it's one roundtrip only // that it's one roundtrip only
let followed_by_me = model::relation::Entity::find() let followed_by_me = model::relation::Entity::is_following(my_id, internal_uid).any(ctx.db()).await?;
.filter(model::relation::Column::Follower.eq(my_id)) let following_me = model::relation::Entity::is_following(internal_uid, my_id).any(ctx.db()).await?;
.filter(model::relation::Column::Following.eq(&uid)) (Some(followed_by_me), Some(following_me))
.select_only()
.select_column(model::relation::Column::Follower)
.into_tuple::<String>()
.one(ctx.db())
.await?
.map(|_| true);
let following_me = model::relation::Entity::find()
.filter(model::relation::Column::Following.eq(my_id))
.filter(model::relation::Column::Follower.eq(&uid))
.select_only()
.select_column(model::relation::Column::Follower)
.into_tuple::<String>()
.one(ctx.db())
.await?
.map(|_| true);
(followed_by_me, following_me)
}, },
}; };
match user::Entity::find_by_id(&uid) match model::actor::Entity::find_by_ap_id(&uid)
.find_also_related(model::config::Entity) .find_also_related(model::config::Entity)
.one(ctx.db()).await? .one(ctx.db()).await?
{ {
// 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, "/users/{id}/inbox"))) .set_inbox(Node::link(url!(ctx, "/actors/{id}/inbox")))
.set_outbox(Node::link(url!(ctx, "/users/{id}/outbox"))) .set_outbox(Node::link(url!(ctx, "/actors/{id}/outbox")))
.set_following(Node::link(url!(ctx, "/users/{id}/following"))) .set_following(Node::link(url!(ctx, "/actors/{id}/following")))
.set_followers(Node::link(url!(ctx, "/users/{id}/followers"))) .set_followers(Node::link(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(
@ -88,6 +70,10 @@ pub async fn view(
user = user.set_following_count(None); user = user.set_following_count(None);
} }
if let Some(ref fe) = ctx.cfg().instance.frontend {
user = user.set_url(Node::link(format!("{fe}/actors/{id}")));
}
Ok(JsonLD(user.ld_context())) Ok(JsonLD(user.ld_context()))
}, },
// remote user // remote user

View file

@ -8,7 +8,7 @@ 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>> { ) -> crate::Result<JsonLD<serde_json::Value>> {
crate::server::builders::collection(&url!(ctx, "/users/{id}/outbox"), None) crate::server::builders::collection(&url!(ctx, "/actors/{id}/outbox"), None)
} }
pub async fn page( pub async fn page(
@ -19,7 +19,7 @@ pub async fn page(
) -> crate::Result<JsonLD<serde_json::Value>> { ) -> crate::Result<JsonLD<serde_json::Value>> {
let uid = ctx.uid(&id); let uid = ctx.uid(&id);
crate::server::builders::paginate( crate::server::builders::paginate(
url!(ctx, "/users/{id}/outbox/page"), url!(ctx, "/actors/{id}/outbox/page"),
Condition::all() Condition::all()
.add(auth.filter_condition()) .add(auth.filter_condition())
.add( .add(
@ -30,6 +30,7 @@ pub async fn page(
ctx.db(), ctx.db(),
page, page,
auth.my_id(), auth.my_id(),
false,
) )
.await .await
} }
@ -42,8 +43,8 @@ pub async fn post(
) -> Result<CreationResult, UpubError> { ) -> Result<CreationResult, UpubError> {
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()),
Identity::Local(uid) => if ctx.uid(&id) == uid { Identity::Local { id: uid, .. } => if ctx.uid(&id) == uid {
tracing::debug!("processing new local activity: {}", serde_json::to_string(&activity).unwrap_or_default()); tracing::debug!("processing new local activity: {}", serde_json::to_string(&activity).unwrap_or_default());
match activity.base_type() { match activity.base_type() {
None => Err(StatusCode::BAD_REQUEST.into()), None => Err(StatusCode::BAD_REQUEST.into()),

View file

@ -35,7 +35,7 @@ pub async fn nodeinfo_discovery(State(ctx): State<Context>) -> Json<NodeInfoDisc
pub async fn nodeinfo(State(ctx): State<Context>, Path(version): Path<String>) -> Result<Json<nodeinfo::NodeInfoOwned>, StatusCode> { pub async fn nodeinfo(State(ctx): State<Context>, Path(version): Path<String>) -> Result<Json<nodeinfo::NodeInfoOwned>, StatusCode> {
// TODO it's unsustainable to count these every time, especially comments since it's a complex // TODO it's unsustainable to count these every time, especially comments since it's a complex
// filter! keep these numbers caches somewhere, maybe db, so that we can just look them up // filter! keep these numbers caches somewhere, maybe db, so that we can just look them up
let total_users = model::user::Entity::find().count(ctx.db()).await.ok(); let total_users = model::actor::Entity::find().count(ctx.db()).await.ok();
let total_posts = None; let total_posts = None;
let total_comments = None; let total_comments = None;
let (software, version) = match version.as_str() { let (software, version) = match version.as_str() {
@ -102,77 +102,37 @@ pub async fn webfinger(State(ctx): State<Context>, Query(query): Query<Webfinger
.replace("acct:", "") .replace("acct:", "")
.split_once('@') .split_once('@')
{ {
if domain == ctx.domain() { let usr = model::actor::Entity::find()
if user == ctx.domain() { .filter(model::actor::Column::PreferredUsername.eq(user))
// we fetch with our domain as user, they are checking us back, this is a special edge case .filter(model::actor::Column::Domain.eq(domain))
Ok(JsonRD(JsonResourceDescriptor { .one(ctx.db())
subject: format!("acct:{user}@{domain}"), .await?
aliases: vec![ctx.base().to_string()], .ok_or_else(UpubError::not_found)?;
links: vec![
JsonResourceDescriptorLink {
rel: "self".to_string(),
link_type: Some("application/ld+json".to_string()),
href: Some(ctx.base().to_string()),
properties: jrd::Map::default(),
titles: jrd::Map::default(),
},
],
expires: None,
properties: jrd::Map::default(),
}))
} else {
// local user
let uid = ctx.uid(user);
let usr = model::user::Entity::find_by_id(uid)
.one(ctx.db())
.await?
.ok_or_else(UpubError::not_found)?;
Ok(JsonRD(JsonResourceDescriptor {
subject: format!("acct:{user}@{domain}"),
aliases: vec![usr.id.clone()],
links: vec![
JsonResourceDescriptorLink {
rel: "self".to_string(),
link_type: Some("application/ld+json".to_string()),
href: Some(usr.id),
properties: jrd::Map::default(),
titles: jrd::Map::default(),
},
],
expires: None,
properties: jrd::Map::default(),
}))
}
let expires = if domain == ctx.domain() {
// TODO configurable webfinger TTL, also 30 days may be too much???
Some(chrono::Utc::now() + chrono::Duration::days(30))
} else { } else {
// remote user // we are no authority on local users, this info should be considered already outdated,
let usr = model::user::Entity::find() // but can still be relevant, for example for our frontend
.filter(model::user::Column::PreferredUsername.eq(user)) Some(chrono::Utc::now())
.filter(model::user::Column::Domain.eq(domain)) };
.one(ctx.db())
.await? Ok(JsonRD(JsonResourceDescriptor {
.ok_or_else(UpubError::not_found)?; subject: format!("acct:{user}@{domain}"),
aliases: vec![usr.id.clone()],
Ok(JsonRD(JsonResourceDescriptor { links: vec![
subject: format!("acct:{user}@{domain}"), JsonResourceDescriptorLink {
aliases: vec![usr.id.clone()], rel: "self".to_string(),
links: vec![ link_type: Some("application/ld+json".to_string()),
JsonResourceDescriptorLink { href: Some(usr.id),
rel: "self".to_string(), properties: jrd::Map::default(),
link_type: Some("application/ld+json".to_string()), titles: jrd::Map::default(),
href: Some(usr.id), },
properties: jrd::Map::default(), ],
titles: jrd::Map::default(), properties: jrd::Map::default(),
}, expires,
], }))
properties: jrd::Map::default(),
// we are no authority on local users, this info should be considered already outdated,
// but can still be relevant, for example for our frontend
expires: Some(chrono::Utc::now()),
}))
}
} else { } else {
Err(StatusCode::UNPROCESSABLE_ENTITY.into()) Err(StatusCode::UNPROCESSABLE_ENTITY.into())
} }

View file

@ -1,6 +1,5 @@
use axum::{extract::{Path, State}, http::StatusCode, Json}; use axum::{extract::{Path, State}, http::StatusCode, Json};
use mastodon_async_entities::account::{Account, AccountId}; use mastodon_async_entities::account::{Account, AccountId};
use sea_orm::EntityTrait;
use crate::{model, server::{auth::AuthIdentity, Context}}; use crate::{model, server::{auth::AuthIdentity, Context}};
@ -9,7 +8,7 @@ pub async fn view(
AuthIdentity(_auth): AuthIdentity, AuthIdentity(_auth): AuthIdentity,
Path(id): Path<String> Path(id): Path<String>
) -> Result<Json<Account>, StatusCode> { ) -> Result<Json<Account>, StatusCode> {
match model::user::Entity::find_by_id(ctx.uid(&id)) match model::actor::Entity::find_by_ap_id(&ctx.uid(&id))
.find_also_related(model::config::Entity) .find_also_related(model::config::Entity)
.one(ctx.db()) .one(ctx.db())
.await .await
@ -21,7 +20,7 @@ pub async fn view(
acct: x.preferred_username.clone(), acct: x.preferred_username.clone(),
avatar: x.icon.as_deref().unwrap_or("").to_string(), avatar: x.icon.as_deref().unwrap_or("").to_string(),
avatar_static: x.icon.unwrap_or_default(), avatar_static: x.icon.unwrap_or_default(),
created_at: time::OffsetDateTime::from_unix_timestamp(x.created.timestamp()).unwrap(), created_at: time::OffsetDateTime::from_unix_timestamp(x.published.timestamp()).unwrap(),
display_name: x.name.unwrap_or_default(), display_name: x.name.unwrap_or_default(),
// TODO hide these maybe // TODO hide these maybe
followers_count: x.followers_count as u64, followers_count: x.followers_count as u64,

130
src/server/addresser.rs Normal file
View file

@ -0,0 +1,130 @@
use sea_orm::{ActiveValue::{NotSet, Set}, EntityTrait};
use crate::model;
use super::{fetcher::Fetcher, Context};
#[axum::async_trait]
pub trait Addresser {
async fn expand_addressing(&self, targets: Vec<String>) -> crate::Result<Vec<String>>;
async fn address_to(&self, aid: Option<i64>, oid: Option<i64>, targets: &[String]) -> crate::Result<()>;
async fn deliver_to(&self, aid: &str, from: &str, targets: &[String]) -> crate::Result<()>;
//#[deprecated = "should probably directly invoke address_to() since we most likely have internal ids at this point"]
async fn dispatch(&self, uid: &str, activity_targets: Vec<String>, aid: &str, oid: Option<&str>) -> crate::Result<()>;
}
#[axum::async_trait]
impl Addresser for super::Context {
async fn expand_addressing(&self, targets: Vec<String>) -> crate::Result<Vec<String>> {
let mut out = Vec::new();
for target in targets {
if target.ends_with("/followers") {
let target_id = target.replace("/followers", "");
let mut followers = model::relation::Entity::followers(&target_id, self.db()).await?;
if followers.is_empty() { // stuff with zero addressing will never be seen again!!! TODO
followers.push(target_id);
}
for follower in followers {
out.push(follower);
}
} else {
out.push(target);
}
}
Ok(out)
}
async fn address_to(&self, aid: Option<i64>, oid: Option<i64>, targets: &[String]) -> crate::Result<()> {
// TODO address_to became kind of expensive, with these two selects right away and then another
// select for each target we're addressing to... can this be improved??
let local_activity = if let Some(x) = aid { self.is_local_internal_activity(x).await.unwrap_or(false) } else { false };
let local_object = if let Some(x) = oid { self.is_local_internal_object(x).await.unwrap_or(false) } else { false };
let mut addressing = Vec::new();
for target in targets
.iter()
.filter(|to| !to.is_empty())
.filter(|to| !to.ends_with("/followers"))
.filter(|to| local_activity || local_object || to.as_str() == apb::target::PUBLIC || self.is_local(to))
{
let (server, actor) = if target == apb::target::PUBLIC { (None, None) } else {
match (
model::instance::Entity::domain_to_internal(&Context::server(target), self.db()).await,
model::actor::Entity::ap_to_internal(target, self.db()).await,
) {
(Ok(server), Ok(actor)) => (Some(server), Some(actor)),
(Err(e), Ok(_)) => { tracing::error!("failed resolving domain: {e}"); continue; },
(Ok(_), Err(e)) => { tracing::error!("failed resolving actor: {e}"); continue; },
(Err(es), Err(ea)) => { tracing::error!("failed resolving domain ({es}) and actor ({ea})"); continue; },
}
};
addressing.push(
model::addressing::ActiveModel {
internal: NotSet,
instance: Set(server),
actor: Set(actor),
activity: Set(aid),
object: Set(oid),
published: Set(chrono::Utc::now()),
}
);
}
if !addressing.is_empty() {
model::addressing::Entity::insert_many(addressing)
.exec(self.db())
.await?;
}
Ok(())
}
async fn deliver_to(&self, aid: &str, from: &str, targets: &[String]) -> crate::Result<()> {
let mut deliveries = Vec::new();
for target in targets.iter()
.filter(|to| !to.is_empty())
.filter(|to| Context::server(to) != self.domain())
.filter(|to| to != &apb::target::PUBLIC)
{
// TODO fetch concurrently
match self.fetch_user(target).await {
Ok(model::actor::Model { inbox: Some(inbox), .. }) => deliveries.push(
model::delivery::ActiveModel {
internal: sea_orm::ActiveValue::NotSet,
actor: Set(from.to_string()),
// TODO we should resolve each user by id and check its inbox because we can't assume
// it's /actors/{id}/inbox for every software, but oh well it's waaaaay easier now
target: Set(inbox),
activity: Set(aid.to_string()),
published: Set(chrono::Utc::now()),
not_before: Set(chrono::Utc::now()),
attempt: Set(0),
}
),
Ok(_) => tracing::error!("resolved target but missing inbox: '{target}', skipping delivery"),
Err(e) => tracing::error!("failed resolving target inbox: {e}, skipping delivery to '{target}'"),
}
}
if !deliveries.is_empty() {
model::delivery::Entity::insert_many(deliveries)
.exec(self.db())
.await?;
}
self.dispatcher().wakeup();
Ok(())
}
//#[deprecated = "should probably directly invoke address_to() since we most likely have internal ids at this point"]
async fn dispatch(&self, uid: &str, activity_targets: Vec<String>, aid: &str, oid: Option<&str>) -> crate::Result<()> {
let addressed = self.expand_addressing(activity_targets).await?;
let internal_aid = model::activity::Entity::ap_to_internal(aid, self.db()).await?;
let internal_oid = if let Some(o) = oid { Some(model::object::Entity::ap_to_internal(o, self.db()).await?) } else { None };
self.address_to(Some(internal_aid), internal_oid, &addressed).await?;
self.deliver_to(aid, uid, &addressed).await?;
Ok(())
}
}

View file

@ -1,4 +1,4 @@
use sea_orm::{EntityTrait, IntoActiveModel}; use sea_orm::{ActiveValue::{Set, NotSet}, EntityTrait};
#[axum::async_trait] #[axum::async_trait]
pub trait Administrable { pub trait Administrable {
@ -28,52 +28,56 @@ impl Administrable for super::Context {
let ap_id = self.uid(&username); let ap_id = self.uid(&username);
let db = self.db(); let db = self.db();
let domain = self.domain().to_string(); let domain = self.domain().to_string();
let user_model = crate::model::user::Model { let user_model = crate::model::actor::ActiveModel {
id: ap_id.clone(), internal: NotSet,
name: display_name, id: Set(ap_id.clone()),
domain, summary, name: Set(display_name),
preferred_username: username.clone(), domain: Set(domain),
following: None, summary: Set(summary),
following_count: 0, preferred_username: Set(username.clone()),
followers: None, following: Set(None),
followers_count: 0, following_count: Set(0),
statuses_count: 0, followers: Set(None),
icon: avatar_url, followers_count: Set(0),
image: banner_url, statuses_count: Set(0),
inbox: None, icon: Set(avatar_url),
shared_inbox: None, image: Set(banner_url),
outbox: None, inbox: Set(None),
actor_type: apb::ActorType::Person, shared_inbox: Set(None),
created: chrono::Utc::now(), outbox: Set(None),
updated: chrono::Utc::now(), actor_type: Set(apb::ActorType::Person),
private_key: Some(std::str::from_utf8(&key.private_key_to_pem().unwrap()).unwrap().to_string()), published: Set(chrono::Utc::now()),
public_key: std::str::from_utf8(&key.public_key_to_pem().unwrap()).unwrap().to_string(), updated: Set(chrono::Utc::now()),
private_key: Set(Some(std::str::from_utf8(&key.private_key_to_pem().unwrap()).unwrap().to_string())),
public_key: Set(std::str::from_utf8(&key.public_key_to_pem().unwrap()).unwrap().to_string()),
}; };
crate::model::user::Entity::insert(user_model.into_active_model()) crate::model::actor::Entity::insert(user_model)
.exec(db) .exec(db)
.await?; .await?;
let config_model = crate::model::config::Model { let config_model = crate::model::config::ActiveModel {
id: ap_id.clone(), internal: NotSet,
accept_follow_requests: true, actor: Set(ap_id.clone()),
show_followers_count: true, accept_follow_requests: Set(true),
show_following_count: true, show_followers_count: Set(true),
show_followers: false, show_following_count: Set(true),
show_following: false, show_followers: Set(false),
show_following: Set(false),
}; };
crate::model::config::Entity::insert(config_model.into_active_model()) crate::model::config::Entity::insert(config_model)
.exec(db) .exec(db)
.await?; .await?;
let credentials_model = crate::model::credential::Model { let credentials_model = crate::model::credential::ActiveModel {
id: ap_id, internal: NotSet,
email: username, actor: Set(ap_id),
password, login: Set(username),
password: Set(sha256::digest(password)),
}; };
crate::model::credential::Entity::insert(credentials_model.into_active_model()) crate::model::credential::Entity::insert(credentials_model)
.exec(db) .exec(db)
.await?; .await?;

View file

@ -9,63 +9,58 @@ use super::{fetcher::Fetcher, httpsign::HttpSignature};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Identity { pub enum Identity {
Anonymous, Anonymous,
Local(String), Remote {
Remote(String), domain: String,
internal: i64,
},
Local {
id: String,
internal: i64,
},
} }
impl Identity { impl Identity {
pub fn filter_condition(&self) -> Condition { pub fn filter_condition(&self) -> Condition {
let base_cond = Condition::any().add(model::addressing::Column::Actor.eq(apb::target::PUBLIC)); let base_cond = Condition::any().add(model::addressing::Column::Actor.is_null());
match self { match self {
Identity::Anonymous => base_cond, Identity::Anonymous => base_cond,
Identity::Remote(server) => base_cond.add(model::addressing::Column::Server.eq(server)), Identity::Remote { internal, .. } => base_cond.add(model::addressing::Column::Instance.eq(*internal)),
// TODO should we allow all users on same server to see? or just specific user?? // TODO should we allow all users on same server to see? or just specific user??
Identity::Local(uid) => base_cond Identity::Local { id, internal } => base_cond
.add(model::addressing::Column::Actor.eq(uid)) .add(model::addressing::Column::Actor.eq(*internal))
.add(model::activity::Column::Actor.eq(uid)) .add(model::activity::Column::Actor.eq(id))
.add(model::object::Column::AttributedTo.eq(uid)), .add(model::object::Column::AttributedTo.eq(id)),
} }
} }
pub fn my_id(&self) -> Option<&str> { pub fn my_id(&self) -> Option<i64> {
match self { match self {
Identity::Local(x) => Some(x.as_str()), Identity::Local { internal, .. } => Some(*internal),
_ => None, _ => None,
} }
} }
pub fn is(&self, id: &str) -> bool { pub fn is(&self, uid: &str) -> bool {
match self { match self {
Identity::Anonymous => false, Identity::Anonymous => false,
Identity::Remote(_) => false, // TODO per-actor server auth should check this Identity::Remote { .. } => false, // TODO per-actor server auth should check this
Identity::Local(uid) => uid.as_str() == id Identity::Local { id, .. } => id.as_str() == uid
} }
} }
#[allow(unused)]
pub fn is_anon(&self) -> bool { pub fn is_anon(&self) -> bool {
matches!(self, Self::Anonymous) matches!(self, Self::Anonymous)
} }
#[allow(unused)]
pub fn is_local(&self) -> bool { pub fn is_local(&self) -> bool {
matches!(self, Self::Local(_)) matches!(self, Self::Local { .. })
} }
#[allow(unused)]
pub fn is_remote(&self) -> bool { pub fn is_remote(&self) -> bool {
matches!(self, Self::Remote(_)) matches!(self, Self::Remote { .. })
}
pub fn is_local_user(&self, uid: &str) -> bool {
match self {
Self::Local(x) => x == uid,
_ => false,
}
}
pub fn is_remote_server(&self, uid: &str) -> bool {
match self {
Self::Remote(x) => x == uid,
_ => false,
}
} }
} }
@ -90,13 +85,19 @@ where
.unwrap_or(""); .unwrap_or("");
if auth_header.starts_with("Bearer ") { if auth_header.starts_with("Bearer ") {
match model::session::Entity::find_by_id(auth_header.replace("Bearer ", "")) match model::session::Entity::find()
.filter(model::session::Column::Secret.eq(auth_header.replace("Bearer ", "")))
.filter(model::session::Column::Expires.gt(chrono::Utc::now())) .filter(model::session::Column::Expires.gt(chrono::Utc::now()))
.one(ctx.db()) .one(ctx.db())
.await .await
{ {
Ok(Some(x)) => identity = Identity::Local(x.actor),
Ok(None) => return Err(UpubError::unauthorized()), Ok(None) => return Err(UpubError::unauthorized()),
Ok(Some(x)) => {
// TODO could we store both actor ap id and internal id in session? to avoid this extra
// lookup on *every* local authed request we receive...
let internal = model::actor::Entity::ap_to_internal(&x.actor, ctx.db()).await?;
identity = Identity::Local { id: x.actor, internal };
},
Err(e) => { Err(e) => {
tracing::error!("failed querying user session: {e}"); tracing::error!("failed querying user session: {e}");
return Err(UpubError::internal_server_error()) return Err(UpubError::internal_server_error())
@ -122,7 +123,13 @@ where
.build_from_parts(parts) .build_from_parts(parts)
.verify(&user.public_key) .verify(&user.public_key)
{ {
Ok(true) => identity = Identity::Remote(Context::server(&user_id)), Ok(true) => {
// TODO can we avoid this extra db rountrip made on each server fetch?
let domain = Context::server(&user_id);
// TODO this will fail because we never fetch and insert into instance oops
let internal = model::instance::Entity::domain_to_internal(&domain, ctx.db()).await?;
identity = Identity::Remote { domain, internal };
},
Ok(false) => tracing::warn!("invalid signature: {http_signature:?}"), Ok(false) => tracing::warn!("invalid signature: {http_signature:?}"),
Err(e) => tracing::error!("error verifying signature: {e}"), Err(e) => tracing::error!("error verifying signature: {e}"),
}, },

View file

@ -1,5 +1,5 @@
use apb::{BaseMut, CollectionMut, CollectionPageMut}; use apb::{BaseMut, CollectionMut, CollectionPageMut};
use sea_orm::{Condition, DatabaseConnection, QueryFilter, QuerySelect}; use sea_orm::{Condition, DatabaseConnection, QueryFilter, QuerySelect, RelationTrait};
use crate::{model::{addressing::Event, attachment::BatchFillable}, routes::activitypub::{jsonld::LD, JsonLD, Pagination}}; use crate::{model::{addressing::Event, attachment::BatchFillable}, routes::activitypub::{jsonld::LD, JsonLD, Pagination}};
@ -8,12 +8,20 @@ pub async fn paginate(
filter: Condition, filter: Condition,
db: &DatabaseConnection, db: &DatabaseConnection,
page: Pagination, page: Pagination,
my_id: Option<&str>, my_id: Option<i64>,
with_users: bool, // TODO ewww too many arguments for this weird function...
) -> crate::Result<JsonLD<serde_json::Value>> { ) -> crate::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 items = crate::model::addressing::Entity::find_addressed(my_id) let mut select = crate::model::addressing::Entity::find_addressed(my_id);
if with_users {
select = select
.join(sea_orm::JoinType::InnerJoin, crate::model::activity::Relation::Actors.def());
}
let items = select
.filter(filter) .filter(filter)
// TODO also limit to only local activities // TODO also limit to only local activities
.limit(limit) .limit(limit)
@ -27,7 +35,7 @@ pub async fn paginate(
let items : Vec<serde_json::Value> = items let items : Vec<serde_json::Value> = items
.into_iter() .into_iter()
.map(|item| { .map(|item| {
let attach = attachments.remove(item.id()); let attach = attachments.remove(&item.internal());
item.ap(attach) item.ap(attach)
}) })
.collect(); .collect();
@ -63,3 +71,22 @@ pub fn collection(id: &str, total_items: Option<u64>) -> crate::Result<JsonLD<se
.ld_context() .ld_context()
)) ))
} }
#[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())
}
}

View file

@ -1,12 +1,11 @@
use std::{collections::BTreeSet, sync::Arc}; use std::{collections::BTreeSet, sync::Arc};
use openssl::rsa::Rsa; use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, QuerySelect, SelectColumns};
use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, QuerySelect, SelectColumns, Set};
use crate::{config::Config, model, server::fetcher::Fetcher}; use crate::{config::Config, errors::UpubError, model};
use uriproxy::UriClass; use uriproxy::UriClass;
use super::dispatcher::Dispatcher; use super::{builders::AnyQuery, dispatcher::Dispatcher};
#[derive(Clone)] #[derive(Clone)]
@ -19,8 +18,16 @@ struct ContextInner {
base_url: String, base_url: String,
dispatcher: Dispatcher, dispatcher: Dispatcher,
// TODO keep these pre-parsed // TODO keep these pre-parsed
app: model::application::Model, actor: model::actor::Model,
relays: BTreeSet<String>, instance: model::instance::Model,
pkey: String,
#[allow(unused)] relay: Relays,
}
#[allow(unused)]
pub struct Relays {
sources: BTreeSet<String>,
sinks: BTreeSet<String>,
} }
#[macro_export] #[macro_export]
@ -46,42 +53,37 @@ impl Context {
for _ in 0..1 { // TODO customize delivery workers amount for _ in 0..1 { // TODO customize delivery workers amount
dispatcher.spawn(db.clone(), domain.clone(), 30); // TODO ew don't do it this deep and secretly!! dispatcher.spawn(db.clone(), domain.clone(), 30); // TODO ew don't do it this deep and secretly!!
} }
let app = match model::application::Entity::find().one(&db).await? { let base_url = format!("{}{}", protocol, domain);
Some(model) => model,
None => { let (actor, instance) = super::init::application(domain.clone(), base_url.clone(), &db).await?;
tracing::info!("generating application keys");
let rsa = Rsa::generate(2048)?; // TODO maybe we could provide a more descriptive error...
let privk = std::str::from_utf8(&rsa.private_key_to_pem()?)?.to_string(); let pkey = actor.private_key.as_deref().ok_or_else(UpubError::internal_server_error)?.to_string();
let pubk = std::str::from_utf8(&rsa.public_key_to_pem()?)?.to_string();
let system = model::application::ActiveModel { let relay_sinks = model::relation::Entity::followers(&actor.id, &db).await?;
id: sea_orm::ActiveValue::NotSet, let relay_sources = model::relation::Entity::following(&actor.id, &db).await?;
private_key: sea_orm::ActiveValue::Set(privk.clone()),
public_key: sea_orm::ActiveValue::Set(pubk.clone()), let relay = Relays {
created: sea_orm::ActiveValue::Set(chrono::Utc::now()), sources: BTreeSet::from_iter(relay_sources),
}; sinks: BTreeSet::from_iter(relay_sinks),
model::application::Entity::insert(system).exec(&db).await?;
// sqlite doesn't resurn last inserted id so we're better off just querying again, it's just one time
model::application::Entity::find().one(&db).await?.expect("could not find app config just inserted")
}
}; };
let relays = model::relay::Entity::find()
.select_only()
.select_column(model::relay::Column::Id)
.filter(model::relay::Column::Accepted.eq(true))
.into_tuple::<String>()
.all(&db)
.await?;
Ok(Context(Arc::new(ContextInner { Ok(Context(Arc::new(ContextInner {
base_url: format!("{}{}", protocol, domain), base_url, db, domain, protocol, actor, instance, dispatcher, config, pkey, relay,
db, domain, protocol, app, dispatcher, config,
relays: BTreeSet::from_iter(relays.into_iter()),
}))) })))
} }
pub fn app(&self) -> &model::application::Model { pub fn actor(&self) -> &model::actor::Model {
&self.0.app &self.0.actor
}
#[allow(unused)]
pub fn instance(&self) -> &model::instance::Model {
&self.0.instance
}
pub fn pkey(&self) -> &str {
&self.0.pkey
} }
pub fn db(&self) -> &DatabaseConnection { pub fn db(&self) -> &DatabaseConnection {
@ -104,9 +106,13 @@ impl Context {
&self.0.base_url &self.0.base_url
} }
pub fn dispatcher(&self) -> &Dispatcher {
&self.0.dispatcher
}
/// get full user id uri /// get full user id uri
pub fn uid(&self, id: &str) -> String { pub fn uid(&self, id: &str) -> String {
uriproxy::uri(self.base(), UriClass::User, id) uriproxy::uri(self.base(), UriClass::Actor, id)
} }
/// get full object id uri /// get full object id uri
@ -119,14 +125,6 @@ impl Context {
uriproxy::uri(self.base(), UriClass::Activity, id) uriproxy::uri(self.base(), UriClass::Activity, id)
} }
// TODO remove this!!
pub fn context_id(&self, id: &str) -> String {
if id.starts_with("tag:") {
return id.to_string();
}
uriproxy::uri(self.base(), UriClass::Context, id)
}
/// get bare id, which is uuid for local stuff and +{uri|base64} for remote stuff /// get bare id, which is uuid for local stuff and +{uri|base64} for remote stuff
pub fn id(&self, full_id: &str) -> String { pub fn id(&self, full_id: &str) -> String {
if self.is_local(full_id) { if self.is_local(full_id) {
@ -150,100 +148,39 @@ impl Context {
id.starts_with(self.base()) id.starts_with(self.base())
} }
pub async fn expand_addressing(&self, targets: Vec<String>) -> crate::Result<Vec<String>> { pub async fn is_local_internal_object(&self, internal: i64) -> crate::Result<bool> {
let mut out = Vec::new(); model::object::Entity::find()
for target in targets { .filter(model::object::Column::Internal.eq(internal))
if target.ends_with("/followers") { .select_only()
let target_id = target.replace("/followers", ""); .select_column(model::object::Column::Internal)
model::relation::Entity::find() .into_tuple::<i64>()
.filter(model::relation::Column::Following.eq(target_id)) .any(self.db())
.select_only() .await
.select_column(model::relation::Column::Follower)
.into_tuple::<String>()
.all(self.db())
.await?
.into_iter()
.for_each(|x| out.push(x));
} else {
out.push(target);
}
}
Ok(out)
} }
pub async fn address_to(&self, aid: Option<&str>, oid: Option<&str>, targets: &[String]) -> crate::Result<()> { pub async fn is_local_internal_activity(&self, internal: i64) -> crate::Result<bool> {
let local_activity = aid.map(|x| self.is_local(x)).unwrap_or(false); model::activity::Entity::find()
let local_object = oid.map(|x| self.is_local(x)).unwrap_or(false); .filter(model::activity::Column::Internal.eq(internal))
let addressings : Vec<model::addressing::ActiveModel> = targets .select_only()
.iter() .select_column(model::activity::Column::Internal)
.filter(|to| !to.is_empty()) .into_tuple::<i64>()
.filter(|to| !to.ends_with("/followers")) .any(self.db())
.filter(|to| local_activity || local_object || to.as_str() == apb::target::PUBLIC || self.is_local(to)) .await
.map(|to| model::addressing::ActiveModel {
id: sea_orm::ActiveValue::NotSet,
server: Set(Context::server(to)),
actor: Set(to.to_string()),
activity: Set(aid.map(|x| x.to_string())),
object: Set(oid.map(|x| x.to_string())),
published: Set(chrono::Utc::now()),
})
.collect();
if !addressings.is_empty() {
model::addressing::Entity::insert_many(addressings)
.exec(self.db())
.await?;
}
Ok(())
} }
pub async fn deliver_to(&self, aid: &str, from: &str, targets: &[String]) -> crate::Result<()> { #[allow(unused)]
let mut deliveries = Vec::new(); pub async fn is_local_internal_actor(&self, internal: i64) -> crate::Result<bool> {
for target in targets.iter() model::actor::Entity::find()
.filter(|to| !to.is_empty()) .filter(model::actor::Column::Internal.eq(internal))
.filter(|to| Context::server(to) != self.domain()) .select_only()
.filter(|to| to != &apb::target::PUBLIC) .select_column(model::actor::Column::Internal)
{ .into_tuple::<i64>()
// TODO fetch concurrently .any(self.db())
match self.fetch_user(target).await { .await
Ok(model::user::Model { inbox: Some(inbox), .. }) => deliveries.push(
model::delivery::ActiveModel {
id: sea_orm::ActiveValue::NotSet,
actor: Set(from.to_string()),
// TODO we should resolve each user by id and check its inbox because we can't assume
// it's /users/{id}/inbox for every software, but oh well it's waaaaay easier now
target: Set(inbox),
activity: Set(aid.to_string()),
created: Set(chrono::Utc::now()),
not_before: Set(chrono::Utc::now()),
attempt: Set(0),
}
),
Ok(_) => tracing::error!("resolved target but missing inbox: '{target}', skipping delivery"),
Err(e) => tracing::error!("failed resolving target inbox: {e}, skipping delivery to '{target}'"),
}
}
if !deliveries.is_empty() {
model::delivery::Entity::insert_many(deliveries)
.exec(self.db())
.await?;
}
self.0.dispatcher.wakeup();
Ok(())
}
pub async fn dispatch(&self, uid: &str, activity_targets: Vec<String>, aid: &str, oid: Option<&str>) -> crate::Result<()> {
let addressed = self.expand_addressing(activity_targets).await?;
self.address_to(Some(aid), oid, &addressed).await?;
self.deliver_to(aid, uid, &addressed).await?;
Ok(())
} }
#[allow(unused)]
pub fn is_relay(&self, id: &str) -> bool { pub fn is_relay(&self, id: &str) -> bool {
self.0.relays.contains(id) self.0.relay.sources.contains(id) || self.0.relay.sinks.contains(id)
} }
} }

View file

@ -54,7 +54,7 @@ async fn worker(db: &DatabaseConnection, domain: &str, poll_interval: u64, waker
}; };
let del_row = model::delivery::ActiveModel { let del_row = model::delivery::ActiveModel {
id: sea_orm::ActiveValue::Set(delivery.id), internal: sea_orm::ActiveValue::Set(delivery.internal),
..Default::default() ..Default::default()
}; };
let del = model::delivery::Entity::delete(del_row) let del = model::delivery::Entity::delete(del_row)
@ -72,7 +72,7 @@ async fn worker(db: &DatabaseConnection, domain: &str, poll_interval: u64, waker
tracing::info!("delivering {} to {}", delivery.activity, delivery.target); tracing::info!("delivering {} to {}", delivery.activity, delivery.target);
let payload = match model::activity::Entity::find_by_id(&delivery.activity) let payload = match model::activity::Entity::find_by_ap_id(&delivery.activity)
.find_also_related(model::object::Entity) .find_also_related(model::object::Entity)
.one(db) .one(db)
.await? // TODO probably should not fail here and at least re-insert the delivery .await? // TODO probably should not fail here and at least re-insert the delivery
@ -99,24 +99,19 @@ async fn worker(db: &DatabaseConnection, domain: &str, poll_interval: u64, waker
}, },
}; };
let key = if delivery.actor == format!("https://{domain}") { let Some(actor) = model::actor::Entity::find_by_ap_id(&delivery.actor)
let Some(model::application::Model { private_key: key, .. }) = model::application::Entity::find() .one(db)
.one(db).await? .await?
else { else {
tracing::error!("no private key configured for application"); tracing::error!("abandoning delivery of {} from non existant actor: {}", delivery.activity, delivery.actor);
continue; continue;
};
key
} else {
let Some(model::user::Model{ private_key: Some(key), .. }) = model::user::Entity::find_by_id(&delivery.actor)
.one(db).await?
else {
tracing::error!("can not dispatch activity for user without private key: {}", delivery.actor);
continue;
};
key
}; };
let Some(key) = actor.private_key
else {
tracing::error!("abandoning delivery of {} from actor without private key: {}", delivery.activity, delivery.actor);
continue;
};
if let Err(e) = Context::request( if let Err(e) = Context::request(
Method::POST, &delivery.target, Method::POST, &delivery.target,
@ -125,12 +120,12 @@ async fn worker(db: &DatabaseConnection, domain: &str, poll_interval: u64, waker
).await { ).await {
tracing::warn!("failed delivery of {} to {} : {e}", delivery.activity, delivery.target); tracing::warn!("failed delivery of {} to {} : {e}", delivery.activity, delivery.target);
let new_delivery = model::delivery::ActiveModel { let new_delivery = model::delivery::ActiveModel {
id: sea_orm::ActiveValue::NotSet, internal: sea_orm::ActiveValue::NotSet,
not_before: sea_orm::ActiveValue::Set(delivery.next_delivery()), not_before: sea_orm::ActiveValue::Set(delivery.next_delivery()),
actor: sea_orm::ActiveValue::Set(delivery.actor), actor: sea_orm::ActiveValue::Set(delivery.actor),
target: sea_orm::ActiveValue::Set(delivery.target), target: sea_orm::ActiveValue::Set(delivery.target),
activity: sea_orm::ActiveValue::Set(delivery.activity), activity: sea_orm::ActiveValue::Set(delivery.activity),
created: sea_orm::ActiveValue::Set(delivery.created), published: sea_orm::ActiveValue::Set(delivery.published),
attempt: sea_orm::ActiveValue::Set(delivery.attempt + 1), attempt: sea_orm::ActiveValue::Set(delivery.attempt + 1),
}; };
model::delivery::Entity::insert(new_delivery).exec(db).await?; model::delivery::Entity::insert(new_delivery).exec(db).await?;

View file

@ -1,26 +1,69 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use apb::{target::Addressed, Activity, Base, Collection, CollectionPage, Link, Object}; use apb::{target::Addressed, Activity, Actor, ActorMut, Base, Collection, Object};
use base64::Engine; use base64::Engine;
use reqwest::{header::{ACCEPT, CONTENT_TYPE, USER_AGENT}, Method, Response}; use reqwest::{header::{ACCEPT, CONTENT_TYPE, USER_AGENT}, Method, Response};
use sea_orm::{sea_query::Expr, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter}; use sea_orm::{EntityTrait, IntoActiveModel, NotSet};
use crate::{errors::UpubError, model, VERSION}; use crate::{errors::UpubError, model, VERSION};
use super::{httpsign::HttpSignature, normalizer::Normalizer, Context}; use super::{addresser::Addresser, httpsign::HttpSignature, normalizer::Normalizer, Context};
#[derive(Debug, Clone)]
pub enum PullResult<T> {
Actor(T),
Activity(T),
Object(T),
}
impl PullResult<serde_json::Value> {
pub fn actor(self) -> crate::Result<serde_json::Value> {
match self {
Self::Actor(x) => Ok(x),
Self::Activity(x) => Err(UpubError::Mismatch(apb::ObjectType::Actor(apb::ActorType::Person), x.object_type().unwrap_or(apb::ObjectType::Activity(apb::ActivityType::Activity)))),
Self::Object(x) => Err(UpubError::Mismatch(apb::ObjectType::Actor(apb::ActorType::Person), x.object_type().unwrap_or(apb::ObjectType::Object))),
}
}
pub fn activity(self) -> crate::Result<serde_json::Value> {
match self {
Self::Actor(x) => Err(UpubError::Mismatch(apb::ObjectType::Activity(apb::ActivityType::Activity), x.object_type().unwrap_or(apb::ObjectType::Actor(apb::ActorType::Person)))),
Self::Activity(x) => Ok(x),
Self::Object(x) => Err(UpubError::Mismatch(apb::ObjectType::Activity(apb::ActivityType::Activity), x.object_type().unwrap_or(apb::ObjectType::Object))),
}
}
pub fn object(self) -> crate::Result<serde_json::Value> {
match self {
Self::Actor(x) => Err(UpubError::Mismatch(apb::ObjectType::Object, x.object_type().unwrap_or(apb::ObjectType::Actor(apb::ActorType::Person)))),
Self::Activity(x) => Err(UpubError::Mismatch(apb::ObjectType::Object, x.object_type().unwrap_or(apb::ObjectType::Activity(apb::ActivityType::Activity)))),
Self::Object(x) => Ok(x),
}
}
}
#[axum::async_trait] #[axum::async_trait]
pub trait Fetcher { pub trait Fetcher {
async fn pull(&self, id: &str) -> crate::Result<PullResult<serde_json::Value>> { self.pull_r(id, 0).await }
async fn pull_r(&self, id: &str, depth: u32) -> crate::Result<PullResult<serde_json::Value>>;
async fn webfinger(&self, user: &str, host: &str) -> crate::Result<String>; async fn webfinger(&self, user: &str, host: &str) -> crate::Result<String>;
async fn fetch_user(&self, id: &str) -> crate::Result<model::user::Model>; async fn fetch_domain(&self, domain: &str) -> crate::Result<model::instance::Model>;
async fn pull_user(&self, id: &str) -> crate::Result<model::user::Model>;
async fn fetch_object(&self, id: &str) -> crate::Result<model::object::Model>; async fn fetch_user(&self, id: &str) -> crate::Result<model::actor::Model>;
async fn pull_object(&self, id: &str) -> crate::Result<model::object::Model>; async fn resolve_user(&self, actor: serde_json::Value) -> crate::Result<model::actor::Model>;
async fn fetch_activity(&self, id: &str) -> crate::Result<model::activity::Model>; async fn fetch_activity(&self, id: &str) -> crate::Result<model::activity::Model>;
async fn pull_activity(&self, id: &str) -> crate::Result<model::activity::Model>; async fn resolve_activity(&self, activity: serde_json::Value) -> crate::Result<model::activity::Model>;
async fn fetch_object(&self, id: &str) -> crate::Result<model::object::Model> { self.fetch_object_r(id, 0).await }
#[allow(unused)] async fn resolve_object(&self, object: serde_json::Value) -> crate::Result<model::object::Model> { self.resolve_object_r(object, 0).await }
async fn fetch_object_r(&self, id: &str, depth: u32) -> crate::Result<model::object::Model>;
async fn resolve_object_r(&self, object: serde_json::Value, depth: u32) -> crate::Result<model::object::Model>;
async fn fetch_thread(&self, id: &str) -> crate::Result<()>; async fn fetch_thread(&self, id: &str) -> crate::Result<()>;
@ -83,6 +126,36 @@ pub trait Fetcher {
#[axum::async_trait] #[axum::async_trait]
impl Fetcher for Context { impl Fetcher for Context {
async fn pull_r(&self, id: &str, depth: u32) -> crate::Result<PullResult<serde_json::Value>> {
let _domain = self.fetch_domain(&Context::server(id)).await?;
let document = Self::request(
Method::GET, id, None,
&format!("https://{}", self.domain()), self.pkey(), self.domain(),
)
.await?
.json::<serde_json::Value>()
.await?;
let doc_id = document.id().ok_or_else(|| UpubError::field("id"))?;
if id != doc_id {
if depth >= self.cfg().security.max_id_redirects {
return Err(UpubError::unprocessable());
}
return self.pull(doc_id).await;
}
match document.object_type() {
None => Err(UpubError::bad_request()),
Some(apb::ObjectType::Collection(_)) => Err(UpubError::unprocessable()),
Some(apb::ObjectType::Tombstone) => Err(UpubError::not_found()),
Some(apb::ObjectType::Activity(_)) => Ok(PullResult::Activity(document)),
Some(apb::ObjectType::Actor(_)) => Ok(PullResult::Actor(document)),
_ => Ok(PullResult::Object(document)),
}
}
async fn webfinger(&self, user: &str, host: &str) -> crate::Result<String> { async fn webfinger(&self, user: &str, host: &str) -> crate::Result<String> {
let subject = format!("acct:{user}@{host}"); let subject = format!("acct:{user}@{host}");
let webfinger_uri = format!("https://{host}/.well-known/webfinger?resource={subject}"); let webfinger_uri = format!("https://{host}/.well-known/webfinger?resource={subject}");
@ -114,82 +187,125 @@ impl Fetcher for Context {
Err(UpubError::not_found()) Err(UpubError::not_found())
} }
async fn fetch_domain(&self, domain: &str) -> crate::Result<model::instance::Model> {
async fn fetch_user(&self, id: &str) -> crate::Result<model::user::Model> { if let Some(x) = model::instance::Entity::find_by_domain(domain).one(self.db()).await? {
if let Some(x) = model::user::Entity::find_by_id(id).one(self.db()).await? {
return Ok(x); // already in db, easy return Ok(x); // already in db, easy
} }
let user_model = self.pull_user(id).await?; let mut instance_model = model::instance::Model {
internal: 0,
domain: domain.to_string(),
name: None,
software: None,
down_since: None,
icon: None,
version: None,
users: None,
posts: None,
published: chrono::Utc::now(),
updated: chrono::Utc::now(),
};
// TODO this may fail: while fetching, remote server may fetch our service actor. if let Ok(res) = Self::request(
// if it does so with http signature, we will fetch that actor in background Method::GET, &format!("https://{domain}"), None, &format!("https://{}", self.domain()), self.pkey(), self.domain(),
// meaning that, once we reach here, it's already inserted and returns an UNIQUE error ).await {
model::user::Entity::insert(user_model.clone().into_active_model()) if let Ok(actor) = res.json::<serde_json::Value>().await {
.exec(self.db()).await?; if let Some(name) = actor.name() {
instance_model.name = Some(name.to_string());
}
if let Some(icon) = actor.icon().id() {
instance_model.icon = Some(icon);
}
}
}
Ok(user_model) if let Ok(nodeinfo) = model::instance::Entity::nodeinfo(domain).await {
instance_model.software = Some(nodeinfo.software.name);
instance_model.version = nodeinfo.software.version;
instance_model.users = nodeinfo.usage.users.and_then(|x| x.total);
instance_model.posts = nodeinfo.usage.local_posts;
}
let mut active_model = instance_model.clone().into_active_model();
active_model.internal = NotSet;
model::instance::Entity::insert(active_model).exec(self.db()).await?;
let internal = model::instance::Entity::domain_to_internal(domain, self.db()).await?;
instance_model.internal = internal;
Ok(instance_model)
} }
async fn pull_user(&self, id: &str) -> crate::Result<model::user::Model> { async fn resolve_user(&self, mut document: serde_json::Value) -> crate::Result<model::actor::Model> {
let user = Self::request( let id = document.id().ok_or_else(|| UpubError::field("id"))?.to_string();
Method::GET, id, None, &format!("https://{}", self.domain()), &self.app().private_key, self.domain(),
).await?.json::<serde_json::Value>().await?;
let mut user_model = model::user::Model::new(&user)?;
// TODO try fetching these numbers from audience/generator fields to avoid making 2 more GETs // TODO try fetching these numbers from audience/generator fields to avoid making 2 more GETs every time
if let Some(followers_url) = &user_model.followers { if let Some(followers_url) = &document.followers().id() {
let req = Self::request( let req = Self::request(
Method::GET, followers_url, None, Method::GET, followers_url, None,
&format!("https://{}", self.domain()), &self.app().private_key, self.domain(), &format!("https://{}", self.domain()), self.pkey(), self.domain(),
).await; ).await;
if let Ok(res) = req { if let Ok(res) = req {
if let Ok(user_followers) = res.json::<serde_json::Value>().await { if let Ok(user_followers) = res.json::<serde_json::Value>().await {
if let Some(total) = user_followers.total_items() { if let Some(total) = user_followers.total_items() {
user_model.followers_count = total as i64; document = document.set_followers_count(Some(total));
} }
} }
} }
} }
if let Some(following_url) = &user_model.following { if let Some(following_url) = &document.following().id() {
let req = Self::request( let req = Self::request(
Method::GET, following_url, None, Method::GET, following_url, None,
&format!("https://{}", self.domain()), &self.app().private_key, self.domain(), &format!("https://{}", self.domain()), self.pkey(), self.domain(),
).await; ).await;
if let Ok(res) = req { if let Ok(res) = req {
if let Ok(user_following) = res.json::<serde_json::Value>().await { if let Ok(user_following) = res.json::<serde_json::Value>().await {
if let Some(total) = user_following.total_items() { if let Some(total) = user_following.total_items() {
user_model.following_count = total as i64; document = document.set_following_count(Some(total));
} }
} }
} }
} }
Ok(user_model) let user_model = model::actor::ActiveModel::new(&document)?;
// TODO this may fail: while fetching, remote server may fetch our service actor.
// if it does so with http signature, we will fetch that actor in background
// meaning that, once we reach here, it's already inserted and returns an UNIQUE error
model::actor::Entity::insert(user_model).exec(self.db()).await?;
// TODO fetch it back to get the internal id
Ok(
model::actor::Entity::find_by_ap_id(&id)
.one(self.db())
.await?
.ok_or_else(UpubError::internal_server_error)?
)
} }
async fn fetch_activity(&self, id: &str) -> crate::Result<model::activity::Model> { async fn fetch_user(&self, id: &str) -> crate::Result<model::actor::Model> {
if let Some(x) = model::activity::Entity::find_by_id(id).one(self.db()).await? { if let Some(x) = model::actor::Entity::find_by_ap_id(id).one(self.db()).await? {
return Ok(x); // already in db, easy return Ok(x); // already in db, easy
} }
let activity_model = self.pull_activity(id).await?; let document = self.pull(id).await?.actor()?;
model::activity::Entity::insert(activity_model.clone().into_active_model()) self.resolve_user(document).await
.exec(self.db()).await?;
let addressed = activity_model.addressed();
let expanded_addresses = self.expand_addressing(addressed).await?;
self.address_to(Some(&activity_model.id), None, &expanded_addresses).await?;
Ok(activity_model)
} }
async fn pull_activity(&self, id: &str) -> crate::Result<model::activity::Model> { async fn fetch_activity(&self, id: &str) -> crate::Result<model::activity::Model> {
let activity = Self::request( if let Some(x) = model::activity::Entity::find_by_ap_id(id).one(self.db()).await? {
Method::GET, id, None, &format!("https://{}", self.domain()), &self.app().private_key, self.domain(), return Ok(x); // already in db, easy
).await?.json::<serde_json::Value>().await?; }
let activity = self.pull(id).await?.activity()?;
self.resolve_activity(activity).await
}
async fn resolve_activity(&self, activity: serde_json::Value) -> crate::Result<model::activity::Model> {
let id = activity.id().ok_or_else(|| UpubError::field("id"))?.to_string();
if let Some(activity_actor) = activity.actor().id() { if let Some(activity_actor) = activity.actor().id() {
if let Err(e) = self.fetch_user(&activity_actor).await { if let Err(e) = self.fetch_user(&activity_actor).await {
@ -203,121 +319,64 @@ impl Fetcher for Context {
} }
} }
let activity_model = model::activity::Model::new(&activity)?; let activity_model = self.insert_activity(activity, Some(Context::server(&id))).await?;
let addressed = activity_model.addressed();
let expanded_addresses = self.expand_addressing(addressed).await?;
self.address_to(Some(activity_model.internal), None, &expanded_addresses).await?;
Ok(activity_model) Ok(activity_model)
} }
async fn fetch_thread(&self, id: &str) -> crate::Result<()> { async fn fetch_thread(&self, _id: &str) -> crate::Result<()> {
crawl_replies(self, id, 0).await // crawl_replies(self, id, 0).await
todo!()
} }
async fn fetch_object(&self, id: &str) -> crate::Result<model::object::Model> { async fn fetch_object_r(&self, id: &str, depth: u32) -> crate::Result<model::object::Model> {
fetch_object_inner(self, id, 0).await if let Some(x) = model::object::Entity::find_by_ap_id(id).one(self.db()).await? {
} return Ok(x); // already in db, easy
async fn pull_object(&self, id: &str) -> crate::Result<model::object::Model> {
let object = Context::request(
Method::GET, id, None, &format!("https://{}", self.domain()), &self.app().private_key, self.domain(),
).await?.json::<serde_json::Value>().await?;
Ok(model::object::Model::new(&object)?)
}
}
#[async_recursion::async_recursion]
async fn crawl_replies(ctx: &Context, id: &str, depth: usize) -> crate::Result<()> {
tracing::info!("crawling replies of '{id}'");
let object = Context::request(
Method::GET, id, None, &format!("https://{}", ctx.domain()), &ctx.app().private_key, ctx.domain(),
).await?.json::<serde_json::Value>().await?;
let object_model = model::object::Model::new(&object)?;
match model::object::Entity::insert(object_model.into_active_model())
.exec(ctx.db()).await
{
Ok(_) => {},
Err(sea_orm::DbErr::RecordNotInserted) => {},
Err(sea_orm::DbErr::Exec(_)) => {}, // ughhh bad fix for sqlite
Err(e) => return Err(e.into()),
}
if depth > 16 {
tracing::warn!("stopping thread crawling: too deep!");
return Ok(());
}
let mut page_url = match object.replies().get() {
Some(serde_json::Value::String(x)) => {
let replies = Context::request(
Method::GET, x, None, &format!("https://{}", ctx.domain()), &ctx.app().private_key, ctx.domain(),
).await?.json::<serde_json::Value>().await?;
replies.first().id()
},
Some(serde_json::Value::Object(x)) => {
let obj = serde_json::Value::Object(x.clone()); // lol putting it back, TODO!
obj.first().id()
},
_ => return Ok(()),
};
while let Some(ref url) = page_url {
let replies = Context::request(
Method::GET, url, None, &format!("https://{}", ctx.domain()), &ctx.app().private_key, ctx.domain(),
).await?.json::<serde_json::Value>().await?;
for reply in replies.items() {
// TODO right now it crawls one by one, could be made in parallel but would be quite more
// abusive, so i'll keep it like this while i try it out
crawl_replies(ctx, reply.href(), depth + 1).await?;
} }
page_url = replies.next().id(); let object = self.pull(id).await?.object()?;
self.resolve_object_r(object, depth).await
} }
Ok(()) async fn resolve_object_r(&self, object: serde_json::Value, depth: u32) -> crate::Result<model::object::Model> {
} let id = object.id().ok_or_else(|| UpubError::field("id"))?.to_string();
#[async_recursion::async_recursion] if let Some(oid) = object.id() {
async fn fetch_object_inner(ctx: &Context, id: &str, depth: usize) -> crate::Result<model::object::Model> { if oid != id {
if let Some(x) = model::object::Entity::find_by_id(id).one(ctx.db()).await? { if let Some(x) = model::object::Entity::find_by_ap_id(oid).one(self.db()).await? {
return Ok(x); // already in db, easy return Ok(x); // already in db, but with id different that given url
} }
let object = Context::request(
Method::GET, id, None, &format!("https://{}", ctx.domain()), &ctx.app().private_key, ctx.domain(),
).await?.json::<serde_json::Value>().await?;
if let Some(oid) = object.id() {
if oid != id {
if let Some(x) = model::object::Entity::find_by_id(oid).one(ctx.db()).await? {
return Ok(x); // already in db, but with id different that given url
} }
} }
}
if let Some(attributed_to) = object.attributed_to().id() { if let Some(attributed_to) = object.attributed_to().id() {
if let Err(e) = ctx.fetch_user(&attributed_to).await { if let Err(e) = self.fetch_user(&attributed_to).await {
tracing::warn!("could not get actor of fetched object: {e}"); tracing::warn!("could not get actor of fetched object: {e}");
}
} }
}
let addressed = object.addressed(); let addressed = object.addressed();
if let Some(reply) = object.in_reply_to().id() { if let Some(reply) = object.in_reply_to().id() {
if depth <= 16 { if depth <= self.cfg().security.thread_crawl_depth {
fetch_object_inner(ctx, &reply, depth + 1).await?; self.fetch_object_r(&reply, depth + 1).await?;
} else { } else {
tracing::warn!("thread deeper than 16, giving up fetching more replies"); tracing::warn!("thread deeper than {}, giving up fetching more replies", self.cfg().security.thread_crawl_depth);
}
} }
let object_model = self.insert_object(object, None).await?;
let expanded_addresses = self.expand_addressing(addressed).await?;
self.address_to(None, Some(object_model.internal), &expanded_addresses).await?;
Ok(object_model)
} }
let object_model = ctx.insert_object(object, None).await?;
let expanded_addresses = ctx.expand_addressing(addressed).await?;
ctx.address_to(None, Some(&object_model.id), &expanded_addresses).await?;
Ok(object_model)
} }
#[axum::async_trait] #[axum::async_trait]
@ -329,9 +388,7 @@ pub trait Fetchable : Sync + Send {
impl Fetchable for apb::Node<serde_json::Value> { impl Fetchable for apb::Node<serde_json::Value> {
async fn fetch(&mut self, ctx: &crate::server::Context) -> crate::Result<&mut Self> { async fn fetch(&mut self, ctx: &crate::server::Context) -> crate::Result<&mut Self> {
if let apb::Node::Link(uri) = self { if let apb::Node::Link(uri) = self {
let from = format!("{}{}", ctx.protocol(), ctx.domain()); // TODO helper to avoid this? *self = Context::request(Method::GET, uri.href(), None, ctx.base(), ctx.pkey(), ctx.domain())
let pkey = &ctx.app().private_key;
*self = Context::request(Method::GET, uri.href(), None, &from, pkey, ctx.domain())
.await? .await?
.json::<serde_json::Value>() .json::<serde_json::Value>()
.await? .await?
@ -341,3 +398,56 @@ impl Fetchable for apb::Node<serde_json::Value> {
Ok(self) Ok(self)
} }
} }
// #[async_recursion::async_recursion]
// async fn crawl_replies(ctx: &Context, id: &str, depth: usize) -> crate::Result<()> {
// tracing::info!("crawling replies of '{id}'");
// let object = Context::request(
// Method::GET, id, None, &format!("https://{}", ctx.domain()), &ctx.app().private_key, ctx.domain(),
// ).await?.json::<serde_json::Value>().await?;
//
// let object_model = model::object::Model::new(&object)?;
// match model::object::Entity::insert(object_model.into_active_model())
// .exec(ctx.db()).await
// {
// Ok(_) => {},
// Err(sea_orm::DbErr::RecordNotInserted) => {},
// Err(sea_orm::DbErr::Exec(_)) => {}, // ughhh bad fix for sqlite
// Err(e) => return Err(e.into()),
// }
//
// if depth > 16 {
// tracing::warn!("stopping thread crawling: too deep!");
// return Ok(());
// }
//
// let mut page_url = match object.replies().get() {
// Some(serde_json::Value::String(x)) => {
// let replies = Context::request(
// Method::GET, x, None, &format!("https://{}", ctx.domain()), &ctx.app().private_key, ctx.domain(),
// ).await?.json::<serde_json::Value>().await?;
// replies.first().id()
// },
// Some(serde_json::Value::Object(x)) => {
// let obj = serde_json::Value::Object(x.clone()); // lol putting it back, TODO!
// obj.first().id()
// },
// _ => return Ok(()),
// };
//
// while let Some(ref url) = page_url {
// let replies = Context::request(
// Method::GET, url, None, &format!("https://{}", ctx.domain()), &ctx.app().private_key, ctx.domain(),
// ).await?.json::<serde_json::Value>().await?;
//
// for reply in replies.items() {
// // TODO right now it crawls one by one, could be made in parallel but would be quite more
// // abusive, so i'll keep it like this while i try it out
// crawl_replies(ctx, reply.href(), depth + 1).await?;
// }
//
// page_url = replies.next().id();
// }
//
// Ok(())
// }

View file

@ -1,10 +1,10 @@
use apb::{target::Addressed, Activity, Base, Object}; use apb::{target::Addressed, Activity, Base, Object};
use reqwest::StatusCode; use reqwest::StatusCode;
use sea_orm::{sea_query::Expr, ActiveModelTrait, ColumnTrait, Condition, EntityTrait, IntoActiveModel, QueryFilter, QuerySelect, SelectColumns, Set}; use sea_orm::{sea_query::Expr, ActiveValue::{Set, NotSet}, ColumnTrait, EntityTrait, QueryFilter, QuerySelect, SelectColumns};
use crate::{errors::{LoggableError, UpubError}, model::{self, FieldError}, server::normalizer::Normalizer}; use crate::{errors::{LoggableError, UpubError}, model, server::{addresser::Addresser, builders::AnyQuery, normalizer::Normalizer}};
use super::{fetcher::Fetcher, Context}; use super::{fetcher::{Fetcher, PullResult}, side_effects::SideEffects, Context};
#[axum::async_trait] #[axum::async_trait]
@ -13,250 +13,233 @@ impl apb::server::Inbox for Context {
type Activity = serde_json::Value; type Activity = serde_json::Value;
async fn create(&self, server: String, activity: serde_json::Value) -> crate::Result<()> { async fn create(&self, server: String, activity: serde_json::Value) -> crate::Result<()> {
let activity_model = model::activity::Model::new(&activity)?;
let aid = activity_model.id.clone();
let Some(object_node) = activity.object().extract() else { let Some(object_node) = activity.object().extract() else {
// TODO we could process non-embedded activities or arrays but im lazy rn // TODO we could process non-embedded activities or arrays but im lazy rn
tracing::error!("refusing to process activity without embedded object: {}", serde_json::to_string_pretty(&activity).unwrap()); tracing::error!("refusing to process activity without embedded object: {}", serde_json::to_string_pretty(&activity).unwrap());
return Err(UpubError::unprocessable()); return Err(UpubError::unprocessable());
}; };
if let Some(reply) = object_node.in_reply_to().id() {
if let Err(e) = self.fetch_object(&reply).await {
tracing::warn!("failed fetching replies for received object: {e}");
}
}
let activity_model = self.insert_activity(activity, Some(server.clone())).await?;
let object_model = self.insert_object(object_node, Some(server)).await?; let object_model = self.insert_object(object_node, Some(server)).await?;
let expanded_addressing = self.expand_addressing(activity.addressed()).await?; let expanded_addressing = self.expand_addressing(activity_model.addressed()).await?;
self.address_to(Some(&aid), Some(&object_model.id), &expanded_addressing).await?; self.address_to(Some(activity_model.internal), Some(object_model.internal), &expanded_addressing).await?;
tracing::info!("{} posted {}", aid, object_model.id); tracing::info!("{} posted {}", activity_model.actor, object_model.id);
Ok(()) Ok(())
} }
async fn like(&self, _: String, activity: serde_json::Value) -> crate::Result<()> { async fn like(&self, server: String, activity: serde_json::Value) -> crate::Result<()> {
let aid = activity.id().ok_or(UpubError::bad_request())?;
let uid = activity.actor().id().ok_or(UpubError::bad_request())?; let uid = activity.actor().id().ok_or(UpubError::bad_request())?;
let internal_uid = model::actor::Entity::ap_to_internal(&uid, self.db()).await?;
let object_uri = activity.object().id().ok_or(UpubError::bad_request())?; let object_uri = activity.object().id().ok_or(UpubError::bad_request())?;
let obj = self.fetch_object(&object_uri).await?; let obj = self.fetch_object(&object_uri).await?;
let oid = obj.id; if model::like::Entity::find_by_uid_oid(internal_uid, obj.internal)
let like = model::like::ActiveModel { .any(self.db())
id: sea_orm::ActiveValue::NotSet, .await?
actor: sea_orm::Set(uid.clone()), {
likes: sea_orm::Set(oid.clone()), return Err(UpubError::not_modified());
date: sea_orm::Set(activity.published().unwrap_or(chrono::Utc::now())),
};
match model::like::Entity::insert(like).exec(self.db()).await {
Err(sea_orm::DbErr::RecordNotInserted) => Err(UpubError::not_modified()),
Err(sea_orm::DbErr::Exec(_)) => Err(UpubError::not_modified()), // bad fix for sqlite
Err(e) => {
tracing::error!("unexpected error procesing like from {uid} to {oid}: {e}");
Err(UpubError::internal_server_error())
}
Ok(_) => {
let activity_model = model::activity::Model::new(&activity)?.into_active_model();
model::activity::Entity::insert(activity_model)
.exec(self.db())
.await?;
let mut expanded_addressing = self.expand_addressing(activity.addressed()).await?;
if expanded_addressing.is_empty() { // WHY MASTODON!!!!!!!
expanded_addressing.push(
model::object::Entity::find_by_id(&oid)
.select_only()
.select_column(model::object::Column::AttributedTo)
.into_tuple::<String>()
.one(self.db())
.await?
.ok_or_else(UpubError::not_found)?
);
}
self.address_to(Some(aid), None, &expanded_addressing).await?;
model::object::Entity::update_many()
.col_expr(model::object::Column::Likes, Expr::col(model::object::Column::Likes).add(1))
.filter(model::object::Column::Id.eq(oid.clone()))
.exec(self.db())
.await?;
tracing::info!("{} liked {}", uid, oid);
Ok(())
},
} }
let activity_model = self.insert_activity(activity, Some(server)).await?;
self.process_like(internal_uid, obj.internal, activity_model.internal, activity_model.published).await?;
let mut expanded_addressing = self.expand_addressing(activity_model.addressed()).await?;
if expanded_addressing.is_empty() { // WHY MASTODON!!!!!!!
expanded_addressing.push(
model::object::Entity::find_by_id(obj.internal)
.select_only()
.select_column(model::object::Column::AttributedTo)
.into_tuple::<String>()
.one(self.db())
.await?
.ok_or_else(UpubError::not_found)?
);
}
self.address_to(Some(activity_model.internal), None, &expanded_addressing).await?;
tracing::info!("{} liked {}", uid, obj.id);
Ok(())
} }
async fn follow(&self, _: String, activity: serde_json::Value) -> crate::Result<()> { async fn follow(&self, _: String, activity: serde_json::Value) -> crate::Result<()> {
let activity_model = model::activity::Model::new(&activity)?; let aid = activity.id().ok_or_else(UpubError::bad_request)?.to_string();
let aid = activity_model.id.clone(); let source_actor = activity.actor().id().ok_or_else(UpubError::bad_request)?;
let target_user_uri = activity_model.object let source_actor_internal = model::actor::Entity::ap_to_internal(&source_actor, self.db()).await?;
.as_deref() let target_actor = activity.object().id().ok_or_else(UpubError::bad_request)?;
.ok_or_else(UpubError::bad_request)? let usr = self.fetch_user(&target_actor).await?;
.to_string(); let activity_model = model::activity::ActiveModel::new(&activity)?;
let usr = self.fetch_user(&target_user_uri).await?; model::activity::Entity::insert(activity_model)
let target_user_id = usr.id; .exec(self.db()).await?;
tracing::info!("{} wants to follow {}", activity_model.actor, target_user_id); let internal_aid = model::activity::Entity::ap_to_internal(&aid, self.db()).await?;
model::activity::Entity::insert(activity_model.into_active_model()) let relation_model = model::relation::ActiveModel {
internal: NotSet,
accept: Set(None),
activity: Set(internal_aid),
follower: Set(source_actor_internal),
following: Set(usr.internal),
};
model::relation::Entity::insert(relation_model)
.exec(self.db()).await?; .exec(self.db()).await?;
let mut expanded_addressing = self.expand_addressing(activity.addressed()).await?; let mut expanded_addressing = self.expand_addressing(activity.addressed()).await?;
if !expanded_addressing.contains(&target_user_id) { if !expanded_addressing.contains(&target_actor) {
expanded_addressing.push(target_user_id); expanded_addressing.push(target_actor);
} }
self.address_to(Some(&aid), None, &expanded_addressing).await?; self.address_to(Some(internal_aid), None, &expanded_addressing).await?;
tracing::info!("{} wants to follow {}", source_actor, usr.id);
Ok(()) Ok(())
} }
async fn accept(&self, _: String, activity: serde_json::Value) -> crate::Result<()> { async fn accept(&self, _: String, activity: serde_json::Value) -> crate::Result<()> {
// TODO what about TentativeAccept // TODO what about TentativeAccept
let activity_model = model::activity::Model::new(&activity)?; let aid = activity.id().ok_or_else(UpubError::bad_request)?.to_string();
let target_actor = activity.actor().id().ok_or_else(UpubError::bad_request)?;
if let Some(mut r) = model::relay::Entity::find_by_id(&activity_model.actor) let follow_request_id = activity.object().id().ok_or_else(UpubError::bad_request)?;
let follow_activity = model::activity::Entity::find_by_ap_id(&follow_request_id)
.one(self.db()) .one(self.db())
.await? .await?
{ .ok_or_else(UpubError::not_found)?;
r.accepted = true;
model::relay::Entity::update(r.into_active_model()).exec(self.db()).await?;
model::activity::Entity::insert(activity_model.clone().into_active_model())
.exec(self.db())
.await?;
tracing::info!("relay {} is now broadcasting to us", activity_model.actor);
return Ok(());
}
let Some(follow_request_id) = &activity_model.object else { if follow_activity.object.unwrap_or("".into()) != target_actor {
return Err(UpubError::bad_request());
};
let Some(follow_activity) = model::activity::Entity::find_by_id(follow_request_id)
.one(self.db()).await?
else {
return Err(UpubError::not_found());
};
if follow_activity.object.unwrap_or("".into()) != activity_model.actor {
return Err(UpubError::forbidden()); return Err(UpubError::forbidden());
} }
tracing::info!("{} accepted follow request by {}", activity_model.actor, follow_activity.actor); let activity_model = model::activity::ActiveModel::new(&activity)?;
model::activity::Entity::insert(activity_model)
.exec(self.db())
.await?;
let accept_internal_id = model::activity::Entity::ap_to_internal(&aid, self.db()).await?;
model::activity::Entity::insert(activity_model.clone().into_active_model()) model::actor::Entity::update_many()
.exec(self.db())
.await?;
model::user::Entity::update_many()
.col_expr( .col_expr(
model::user::Column::FollowingCount, model::actor::Column::FollowingCount,
Expr::col(model::user::Column::FollowingCount).add(1) Expr::col(model::actor::Column::FollowingCount).add(1)
) )
.filter(model::user::Column::Id.eq(&follow_activity.actor)) .filter(model::actor::Column::Id.eq(&follow_activity.actor))
.exec(self.db()) .exec(self.db())
.await?; .await?;
model::relation::Entity::insert( model::actor::Entity::update_many()
model::relation::ActiveModel { .col_expr(
follower: Set(follow_activity.actor.clone()), model::actor::Column::FollowersCount,
following: Set(activity_model.actor), Expr::col(model::actor::Column::FollowersCount).add(1)
..Default::default() )
} .filter(model::actor::Column::Id.eq(&follow_activity.actor))
).exec(self.db()).await?; .exec(self.db())
.await?;
model::relation::Entity::update_many()
.col_expr(model::relation::Column::Accept, Expr::value(Some(accept_internal_id)))
.filter(model::relation::Column::Activity.eq(follow_activity.internal))
.exec(self.db()).await?;
tracing::info!("{} accepted follow request by {}", target_actor, follow_activity.actor);
let mut expanded_addressing = self.expand_addressing(activity.addressed()).await?; let mut expanded_addressing = self.expand_addressing(activity.addressed()).await?;
if !expanded_addressing.contains(&follow_activity.actor) { if !expanded_addressing.contains(&follow_activity.actor) {
expanded_addressing.push(follow_activity.actor); expanded_addressing.push(follow_activity.actor);
} }
self.address_to(Some(&activity_model.id), None, &expanded_addressing).await?; self.address_to(Some(accept_internal_id), None, &expanded_addressing).await?;
Ok(()) Ok(())
} }
async fn reject(&self, _: String, activity: serde_json::Value) -> crate::Result<()> { async fn reject(&self, _: String, activity: serde_json::Value) -> crate::Result<()> {
// TODO what about TentativeReject? // TODO what about TentativeReject?
let activity_model = model::activity::Model::new(&activity)?; let aid = activity.id().ok_or_else(UpubError::bad_request)?.to_string();
let Some(follow_request_id) = &activity_model.object else { let uid = activity.actor().id().ok_or_else(UpubError::bad_request)?;
return Err(UpubError::bad_request()); let follow_request_id = activity.object().id().ok_or_else(UpubError::bad_request)?;
}; let follow_activity = model::activity::Entity::find_by_ap_id(&follow_request_id)
let Some(follow_activity) = model::activity::Entity::find_by_id(follow_request_id) .one(self.db())
.one(self.db()).await? .await?
else { .ok_or_else(UpubError::not_found)?;
return Err(UpubError::not_found());
}; if follow_activity.object.unwrap_or("".into()) != uid {
if follow_activity.object.unwrap_or("".into()) != activity_model.actor {
return Err(UpubError::forbidden()); return Err(UpubError::forbidden());
} }
tracing::info!("{} rejected follow request by {}", activity_model.actor, follow_activity.actor); let activity_model = model::activity::ActiveModel::new(&activity)?;
model::activity::Entity::insert(activity_model)
model::activity::Entity::insert(activity_model.clone().into_active_model())
.exec(self.db()) .exec(self.db())
.await?; .await?;
let internal_aid = model::activity::Entity::ap_to_internal(&aid, self.db()).await?;
model::relation::Entity::delete_many()
.filter(model::relation::Column::Activity.eq(internal_aid))
.exec(self.db())
.await?;
tracing::info!("{} rejected follow request by {}", uid, follow_activity.actor);
let mut expanded_addressing = self.expand_addressing(activity.addressed()).await?; let mut expanded_addressing = self.expand_addressing(activity.addressed()).await?;
if !expanded_addressing.contains(&follow_activity.actor) { if !expanded_addressing.contains(&follow_activity.actor) {
expanded_addressing.push(follow_activity.actor); expanded_addressing.push(follow_activity.actor);
} }
self.address_to(Some(&activity_model.id), None, &expanded_addressing).await?;
self.address_to(Some(internal_aid), None, &expanded_addressing).await?;
Ok(()) Ok(())
} }
async fn delete(&self, _: String, activity: serde_json::Value) -> crate::Result<()> { async fn delete(&self, _: String, activity: serde_json::Value) -> crate::Result<()> {
// TODO verify the signature before just deleting lmao let oid = activity.object().id().ok_or_else(UpubError::bad_request)?;
let oid = activity.object().id().ok_or(UpubError::bad_request())?; model::actor::Entity::delete_by_ap_id(&oid).exec(self.db()).await.info_failed("failed deleting from users");
tracing::debug!("deleting '{oid}'"); // this is so spammy wtf! model::object::Entity::delete_by_ap_id(&oid).exec(self.db()).await.info_failed("failed deleting from objects");
// TODO maybe we should keep the tombstone? tracing::debug!("deleted '{oid}'");
model::user::Entity::delete_by_id(&oid).exec(self.db()).await.info_failed("failed deleting from users");
model::activity::Entity::delete_by_id(&oid).exec(self.db()).await.info_failed("failed deleting from activities");
model::object::Entity::delete_by_id(&oid).exec(self.db()).await.info_failed("failed deleting from objects");
Ok(()) Ok(())
} }
async fn update(&self, server: String, activity: serde_json::Value) -> crate::Result<()> { async fn update(&self, _server: String, activity: serde_json::Value) -> crate::Result<()> {
let activity_model = model::activity::Model::new(&activity)?; let uid = activity.actor().id().ok_or_else(UpubError::bad_request)?;
let aid = activity_model.id.clone(); let aid = activity.id().ok_or_else(UpubError::bad_request)?;
let Some(object_node) = activity.object().extract() else { let Some(object_node) = activity.object().extract() else {
// TODO we could process non-embedded activities or arrays but im lazy rn // TODO we could process non-embedded activities or arrays but im lazy rn
tracing::error!("refusing to process activity without embedded object: {}", serde_json::to_string_pretty(&activity).unwrap()); tracing::error!("refusing to process activity without embedded object: {}", serde_json::to_string_pretty(&activity).unwrap());
return Err(UpubError::unprocessable()); return Err(UpubError::unprocessable());
}; };
let Some(oid) = object_node.id().map(|x| x.to_string()) else { let oid = object_node.id().ok_or_else(UpubError::bad_request)?.to_string();
return Err(UpubError::bad_request());
};
// make sure we're allowed to edit this object
if let Some(object_author) = object_node.attributed_to().id() {
if server != Context::server(&object_author) {
return Err(UpubError::forbidden());
}
} else if server != Context::server(&oid) {
return Err(UpubError::forbidden());
};
match object_node.object_type() {
Some(apb::ObjectType::Actor(_)) => {
// TODO oof here is an example of the weakness of this model, we have to go all the way
// back up to serde_json::Value because impl Object != impl Actor
let actor_model = model::user::Model::new(&object_node)?;
let mut update_model = actor_model.into_active_model();
update_model.updated = sea_orm::Set(chrono::Utc::now());
update_model.reset(model::user::Column::Name);
update_model.reset(model::user::Column::Summary);
update_model.reset(model::user::Column::Image);
update_model.reset(model::user::Column::Icon);
model::user::Entity::update(update_model)
.exec(self.db()).await?;
},
Some(apb::ObjectType::Note) => {
let object_model = model::object::Model::new(&object_node)?;
let mut update_model = object_model.into_active_model();
update_model.updated = sea_orm::Set(Some(chrono::Utc::now()));
update_model.reset(model::object::Column::Name);
update_model.reset(model::object::Column::Summary);
update_model.reset(model::object::Column::Content);
update_model.reset(model::object::Column::Sensitive);
model::object::Entity::update(update_model)
.exec(self.db()).await?;
},
Some(t) => tracing::warn!("no side effects implemented for update type {t:?}"),
None => tracing::warn!("empty type on embedded updated object"),
}
tracing::info!("{} updated {}", aid, oid); let activity_model = model::activity::ActiveModel::new(&activity)?;
model::activity::Entity::insert(activity_model.into_active_model()) model::activity::Entity::insert(activity_model)
.exec(self.db()) .exec(self.db())
.await?; .await?;
let internal_aid = model::activity::Entity::ap_to_internal(aid, self.db()).await?;
let internal_oid = match object_node.object_type().ok_or_else(UpubError::bad_request)? {
apb::ObjectType::Actor(_) => {
let internal_uid = model::actor::Entity::ap_to_internal(&oid, self.db()).await?;
let mut actor_model = model::actor::ActiveModel::new(&object_node)?;
actor_model.internal = Set(internal_uid);
actor_model.updated = Set(chrono::Utc::now());
model::actor::Entity::update(actor_model)
.exec(self.db())
.await?;
Some(internal_uid)
},
apb::ObjectType::Note => {
let internal_oid = model::object::Entity::ap_to_internal(&oid, self.db()).await?;
let mut object_model = model::object::ActiveModel::new(&object_node)?;
object_model.internal = Set(internal_oid);
object_model.updated = Set(chrono::Utc::now());
model::object::Entity::update(object_model)
.exec(self.db())
.await?;
Some(internal_oid)
},
t => {
tracing::warn!("no side effects implemented for update type {t:?}");
None
},
};
tracing::info!("{} updated {}", uid, oid);
let expanded_addressing = self.expand_addressing(activity.addressed()).await?; let expanded_addressing = self.expand_addressing(activity.addressed()).await?;
self.address_to(Some(&aid), Some(&oid), &expanded_addressing).await?; self.address_to(Some(internal_aid), internal_oid, &expanded_addressing).await?;
Ok(()) Ok(())
} }
async fn undo(&self, server: String, activity: serde_json::Value) -> crate::Result<()> { async fn undo(&self, server: String, activity: serde_json::Value) -> crate::Result<()> {
let uid = activity.actor().id().ok_or_else(UpubError::bad_request)?; let uid = activity.actor().id().ok_or_else(UpubError::bad_request)?;
let internal_uid = model::actor::Entity::ap_to_internal(&uid, self.db()).await?;
// TODO in theory we could work with just object_id but right now only accept embedded // TODO in theory we could work with just object_id but right now only accept embedded
let undone_activity = activity.object().extract().ok_or_else(UpubError::bad_request)?; let undone_activity = activity.object().extract().ok_or_else(UpubError::bad_request)?;
let undone_aid = undone_activity.id().ok_or_else(UpubError::bad_request)?;
let undone_object_uri = undone_activity.object().id().ok_or_else(UpubError::bad_request)?;
let activity_type = undone_activity.activity_type().ok_or_else(UpubError::bad_request)?;
let undone_activity_author = undone_activity.actor().id().ok_or_else(UpubError::bad_request)?; let undone_activity_author = undone_activity.actor().id().ok_or_else(UpubError::bad_request)?;
// can't undo activities from remote actors! // can't undo activities from remote actors!
@ -264,83 +247,70 @@ impl apb::server::Inbox for Context {
return Err(UpubError::forbidden()); return Err(UpubError::forbidden());
}; };
let obj = self.fetch_object(&undone_object_uri).await?; let activity_model = self.insert_activity(activity.clone(), Some(server)).await?;
let undone_object_id = obj.id;
match activity_type { let targets = self.expand_addressing(activity.addressed()).await?;
apb::ActivityType::Like => { self.process_undo(internal_uid, activity).await?;
model::like::Entity::delete_many()
.filter(
Condition::all()
.add(model::like::Column::Actor.eq(&uid))
.add(model::like::Column::Likes.eq(&undone_object_id))
)
.exec(self.db())
.await?;
model::object::Entity::update_many()
.filter(model::object::Column::Id.eq(&undone_object_id))
.col_expr(model::object::Column::Likes, Expr::col(model::object::Column::Likes).sub(1))
.exec(self.db())
.await?;
},
apb::ActivityType::Follow => {
model::relation::Entity::delete_many()
.filter(
Condition::all()
.add(model::relation::Column::Follower.eq(&uid))
.add(model::relation::Column::Following.eq(&undone_object_id))
)
.exec(self.db())
.await?;
},
_ => {
tracing::error!("received 'Undo' for unimplemented activity: {}", serde_json::to_string_pretty(&activity).unwrap());
return Err(StatusCode::NOT_IMPLEMENTED.into());
},
}
model::activity::Entity::delete_by_id(undone_aid).exec(self.db()).await?; self.address_to(Some(activity_model.internal), None, &targets).await?;
Ok(()) Ok(())
} }
async fn announce(&self, _: String, activity: serde_json::Value) -> crate::Result<()> { async fn announce(&self, server: String, activity: serde_json::Value) -> crate::Result<()> {
let activity_model = model::activity::Model::new(&activity)?; let uid = activity.actor().id().ok_or_else(|| UpubError::field("actor"))?;
let Some(object_uri) = &activity_model.object else { let actor = self.fetch_user(&uid).await?;
return Err(FieldError("object").into()); let internal_uid = model::actor::Entity::ap_to_internal(&uid, self.db()).await?;
}; let announced_id = activity.object().id().ok_or_else(|| UpubError::field("object"))?;
let obj = self.fetch_object(object_uri).await?;
let oid = obj.id; match self.pull(&announced_id).await? {
PullResult::Actor(_) => Err(UpubError::unprocessable()),
PullResult::Object(object) => {
let object_model = self.resolve_object(object).await?;
let activity_model = self.insert_activity(activity.clone(), Some(server.clone())).await?;
// relays send us activities as Announce, but we don't really want to count those towards the // relays send us objects as Announce, but we don't really want to count those towards the
// total shares count of an object, so just fetch the object and be done with it // total shares count of an object, so just fetch the object and be done with it
if self.is_relay(&activity_model.actor) { if !matches!(actor.actor_type, apb::ActorType::Person) {
tracing::info!("relay {} broadcasted {}", activity_model.actor, oid); tracing::info!("relay {} broadcasted {}", activity_model.actor, announced_id);
return Ok(()) return Ok(())
}
let share = model::announce::ActiveModel {
internal: NotSet,
actor: Set(internal_uid),
object: Set(object_model.internal),
published: Set(activity.published().unwrap_or(chrono::Utc::now())),
};
let expanded_addressing = self.expand_addressing(activity.addressed()).await?;
self.address_to(Some(activity_model.internal), None, &expanded_addressing).await?;
model::announce::Entity::insert(share)
.exec(self.db()).await?;
model::object::Entity::update_many()
.col_expr(model::object::Column::Announces, Expr::col(model::object::Column::Announces).add(1))
.filter(model::object::Column::Internal.eq(object_model.internal))
.exec(self.db())
.await?;
tracing::info!("{} shared {}", activity_model.actor, announced_id);
Ok(())
},
PullResult::Activity(activity) => {
// groups update all members of other things that happen inside, process those
let server = Context::server(activity.id().unwrap_or_default());
match activity.activity_type().ok_or_else(UpubError::bad_request)? {
apb::ActivityType::Like | apb::ActivityType::EmojiReact => Ok(self.like(server, activity).await?),
apb::ActivityType::Create => Ok(self.create(server, activity).await?),
apb::ActivityType::Undo => Ok(self.undo(server, activity).await?),
apb::ActivityType::Delete => Ok(self.delete(server, activity).await?),
apb::ActivityType::Update => Ok(self.update(server, activity).await?),
x => {
tracing::warn!("ignoring unhandled announced activity of type {x:?}");
Err(StatusCode::NOT_IMPLEMENTED.into())
},
}
},
} }
let share = model::share::ActiveModel {
id: sea_orm::ActiveValue::NotSet,
actor: sea_orm::Set(activity_model.actor.clone()),
shares: sea_orm::Set(oid.clone()),
date: sea_orm::Set(activity.published().unwrap_or(chrono::Utc::now())),
};
let expanded_addressing = self.expand_addressing(activity.addressed()).await?;
self.address_to(Some(&activity_model.id), None, &expanded_addressing).await?;
model::share::Entity::insert(share)
.exec(self.db()).await?;
model::activity::Entity::insert(activity_model.clone().into_active_model())
.exec(self.db())
.await?;
model::object::Entity::update_many()
.col_expr(model::object::Column::Shares, Expr::col(model::object::Column::Shares).add(1))
.filter(model::object::Column::Id.eq(oid.clone()))
.exec(self.db())
.await?;
tracing::info!("{} shared {}", activity_model.actor, oid);
Ok(())
} }
} }

71
src/server/init.rs Normal file
View file

@ -0,0 +1,71 @@
use openssl::rsa::Rsa;
use sea_orm::{ActiveValue::{NotSet, Set}, DatabaseConnection, EntityTrait};
use crate::model;
pub async fn application(
domain: String,
base_url: String,
db: &DatabaseConnection
) -> crate::Result<(model::actor::Model, model::instance::Model)> {
Ok((
match model::actor::Entity::find_by_ap_id(&base_url).one(db).await? {
Some(model) => model,
None => {
tracing::info!("generating application keys");
let rsa = Rsa::generate(2048)?;
let privk = std::str::from_utf8(&rsa.private_key_to_pem()?)?.to_string();
let pubk = std::str::from_utf8(&rsa.public_key_to_pem()?)?.to_string();
let system = model::actor::ActiveModel {
internal: NotSet,
id: Set(base_url.clone()),
domain: Set(domain.clone()),
preferred_username: Set(domain.clone()),
actor_type: Set(apb::ActorType::Application),
private_key: Set(Some(privk)),
public_key: Set(pubk),
following: Set(None),
following_count: Set(0),
followers: Set(None),
followers_count: Set(0),
statuses_count: Set(0),
summary: Set(Some("micro social network, federated".to_string())),
name: Set(Some("μpub".to_string())),
image: Set(None),
icon: Set(Some("https://cdn.alemi.dev/social/circle-square.png".to_string())),
inbox: Set(Some(format!("{base_url}/inbox"))),
shared_inbox: Set(Some(format!("{base_url}/inbox"))),
outbox: Set(Some(format!("{base_url}/outbox"))),
published: Set(chrono::Utc::now()),
updated: Set(chrono::Utc::now()),
};
model::actor::Entity::insert(system).exec(db).await?;
// sqlite doesn't resurn last inserted id so we're better off just querying again, it's just one time
model::actor::Entity::find().one(db).await?.expect("could not find app actor just inserted")
}
},
match model::instance::Entity::find_by_domain(&domain).one(db).await? {
Some(model) => model,
None => {
tracing::info!("generating instance counters");
let system = model::instance::ActiveModel {
internal: NotSet,
domain: Set(domain.clone()),
down_since: Set(None),
software: Set(Some("upub".to_string())),
version: Set(Some(crate::VERSION.to_string())),
name: Set(None),
icon: Set(None),
users: Set(Some(0)),
posts: Set(Some(0)),
published: Set(chrono::Utc::now()),
updated: Set(chrono::Utc::now()),
};
model::instance::Entity::insert(system).exec(db).await?;
// sqlite doesn't resurn last inserted id so we're better off just querying again, it's just one time
model::instance::Entity::find().one(db).await?.expect("could not find app instance just inserted")
}
}
))
}

View file

@ -1,12 +1,15 @@
pub mod addresser;
pub mod admin; pub mod admin;
pub mod context; pub mod context;
pub mod dispatcher; pub mod dispatcher;
pub mod fetcher; pub mod fetcher;
pub mod inbox; pub mod inbox;
pub mod init;
pub mod outbox; pub mod outbox;
pub mod auth; pub mod auth;
pub mod builders; pub mod builders;
pub mod httpsign; pub mod httpsign;
pub mod normalizer; pub mod normalizer;
pub mod side_effects;
pub use context::Context; pub use context::Context;

View file

@ -1,34 +1,33 @@
use apb::{Node, Base, Object, Document}; use apb::{Node, Base, Object, Document};
use sea_orm::{sea_query::Expr, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter, Set}; use sea_orm::{sea_query::Expr, ActiveValue::{NotSet, Set}, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter};
use crate::{errors::UpubError, model, server::Context}; use crate::{errors::UpubError, model, server::Context};
use super::fetcher::Fetcher;
#[axum::async_trait] #[axum::async_trait]
pub trait Normalizer { pub trait Normalizer {
async fn insert_object(&self, obj: impl apb::Object, server: Option<String>) -> crate::Result<model::object::Model>; async fn insert_object(&self, obj: impl apb::Object, server: Option<String>) -> crate::Result<model::object::Model>;
async fn insert_activity(&self, act: impl apb::Activity, server: Option<String>) -> crate::Result<model::activity::Model>;
} }
#[axum::async_trait] #[axum::async_trait]
impl Normalizer for super::Context { impl Normalizer for super::Context {
async fn insert_object(&self, object_node: impl apb::Object, server: Option<String>) -> crate::Result<model::object::Model> { async fn insert_object(&self, object_node: impl apb::Object, server: Option<String>) -> crate::Result<model::object::Model> {
let mut object_model = model::object::Model::new(&object_node)?; let oid = object_node.id().ok_or_else(UpubError::bad_request)?.to_string();
let oid = object_model.id.clone(); let uid = object_node.attributed_to().id();
let uid = object_model.attributed_to.clone(); let mut object_model = model::object::ActiveModel::new(&object_node)?;
if let Some(server) = server { if let Some(server) = server {
// make sure we're allowed to create this object // make sure we're allowed to create this object
if let Some(object_author) = &object_model.attributed_to { if let Set(Some(object_author)) = &object_model.attributed_to {
if server != Context::server(object_author) { if server != Context::server(object_author) {
return Err(UpubError::forbidden()); return Err(UpubError::forbidden());
} }
} else if server != Context::server(&object_model.id) { } else if server != Context::server(&oid) {
return Err(UpubError::forbidden()); return Err(UpubError::forbidden());
}; };
} }
// make sure content only contains a safe subset of html // make sure content only contains a safe subset of html
if let Some(content) = object_model.content { if let Set(Some(content)) = object_model.content {
object_model.content = Some(mdhtml::safe_html(&content)); object_model.content = Set(Some(mdhtml::safe_html(&content)));
} }
// fix context for remote posts // fix context for remote posts
@ -37,33 +36,32 @@ impl Normalizer for super::Context {
// > btw! also if any link is broken or we get rate limited, the whole insertion fails which is // > btw! also if any link is broken or we get rate limited, the whole insertion fails which is
// > kind of dumb. there should be a job system so this can be done in waves. or maybe there's // > kind of dumb. there should be a job system so this can be done in waves. or maybe there's
// > some whole other way to do this?? im thinking but misskey aaaa!! TODO // > some whole other way to do this?? im thinking but misskey aaaa!! TODO
if let Some(ref reply) = object_model.in_reply_to { if let Set(Some(ref reply)) = object_model.in_reply_to {
if let Some(o) = model::object::Entity::find_by_id(reply).one(self.db()).await? { if let Some(o) = model::object::Entity::find_by_ap_id(reply).one(self.db()).await? {
object_model.context = o.context; object_model.context = Set(o.context);
} else { } else {
object_model.context = None; // TODO to be filled by some other task object_model.context = Set(None); // TODO to be filled by some other task
} }
} else { } else {
object_model.context = Some(object_model.id.clone()); object_model.context = Set(Some(oid.clone()));
} }
model::object::Entity::insert(object_model.clone().into_active_model()).exec(self.db()).await?; model::object::Entity::insert(object_model.clone().into_active_model()).exec(self.db()).await?;
let object = model::object::Entity::find_by_ap_id(&oid).one(self.db()).await?.ok_or_else(UpubError::internal_server_error)?;
// update replies counter // update replies counter
if let Some(ref in_reply_to) = object_model.in_reply_to { if let Set(Some(ref in_reply_to)) = object_model.in_reply_to {
if self.fetch_object(in_reply_to).await.is_ok() { model::object::Entity::update_many()
model::object::Entity::update_many() .filter(model::object::Column::Id.eq(in_reply_to))
.filter(model::object::Column::Id.eq(in_reply_to)) .col_expr(model::object::Column::Replies, Expr::col(model::object::Column::Replies).add(1))
.col_expr(model::object::Column::Comments, Expr::col(model::object::Column::Comments).add(1)) .exec(self.db())
.exec(self.db()) .await?;
.await?;
}
} }
// update statuses counter // update statuses counter
if let Some(object_author) = uid { if let Some(object_author) = uid {
model::user::Entity::update_many() model::actor::Entity::update_many()
.col_expr(model::user::Column::StatusesCount, Expr::col(model::user::Column::StatusesCount).add(1)) .col_expr(model::actor::Column::StatusesCount, Expr::col(model::actor::Column::StatusesCount).add(1))
.filter(model::user::Column::Id.eq(&object_author)) .filter(model::actor::Column::Id.eq(&object_author))
.exec(self.db()) .exec(self.db())
.await?; .await?;
} }
@ -76,22 +74,22 @@ impl Normalizer for super::Context {
continue continue
}, },
Node::Link(l) => model::attachment::ActiveModel { Node::Link(l) => model::attachment::ActiveModel {
id: sea_orm::ActiveValue::NotSet, internal: sea_orm::ActiveValue::NotSet,
url: Set(l.href().to_string()), url: Set(l.href().to_string()),
object: Set(oid.clone()), object: Set(object.internal),
document_type: Set(apb::DocumentType::Page), document_type: Set(apb::DocumentType::Page),
name: Set(l.link_name().map(|x| x.to_string())), name: Set(l.link_name().map(|x| x.to_string())),
media_type: Set(l.link_media_type().unwrap_or("link").to_string()), media_type: Set(l.link_media_type().unwrap_or("link").to_string()),
created: Set(chrono::Utc::now()), published: Set(chrono::Utc::now()),
}, },
Node::Object(o) => model::attachment::ActiveModel { Node::Object(o) => model::attachment::ActiveModel {
id: sea_orm::ActiveValue::NotSet, internal: sea_orm::ActiveValue::NotSet,
url: Set(o.url().id().unwrap_or_else(|| o.id().map(|x| x.to_string()).unwrap_or_default())), url: Set(o.url().id().unwrap_or_else(|| o.id().map(|x| x.to_string()).unwrap_or_default())),
object: Set(oid.clone()), object: Set(object.internal),
document_type: Set(o.as_document().map_or(apb::DocumentType::Document, |x| x.document_type().unwrap_or(apb::DocumentType::Page))), document_type: Set(o.as_document().map_or(apb::DocumentType::Document, |x| x.document_type().unwrap_or(apb::DocumentType::Page))),
name: Set(o.name().map(|x| x.to_string())), name: Set(o.name().map(|x| x.to_string())),
media_type: Set(o.media_type().unwrap_or("link").to_string()), media_type: Set(o.media_type().unwrap_or("link").to_string()),
created: Set(o.published().unwrap_or_else(chrono::Utc::now)), published: Set(o.published().unwrap_or_else(chrono::Utc::now)),
}, },
}; };
model::attachment::Entity::insert(attachment_model) model::attachment::Entity::insert(attachment_model)
@ -113,19 +111,50 @@ impl Normalizer for super::Context {
}; };
let attachment_model = model::attachment::ActiveModel { let attachment_model = model::attachment::ActiveModel {
id: sea_orm::ActiveValue::NotSet, internal: sea_orm::ActiveValue::NotSet,
url: Set(img.url().id().unwrap_or_else(|| img.id().map(|x| x.to_string()).unwrap_or_default())), url: Set(img.url().id().unwrap_or_else(|| img.id().map(|x| x.to_string()).unwrap_or_default())),
object: Set(oid.clone()), object: Set(object.internal),
document_type: Set(img.as_document().map_or(apb::DocumentType::Document, |x| x.document_type().unwrap_or(apb::DocumentType::Page))), document_type: Set(img.as_document().map_or(apb::DocumentType::Document, |x| x.document_type().unwrap_or(apb::DocumentType::Page))),
name: Set(img.name().map(|x| x.to_string())), name: Set(img.name().map(|x| x.to_string())),
media_type: Set(img.media_type().unwrap_or(media_type.as_deref().unwrap_or("link")).to_string()), media_type: Set(img.media_type().unwrap_or(media_type.as_deref().unwrap_or("link")).to_string()),
created: Set(img.published().unwrap_or_else(chrono::Utc::now)), published: Set(img.published().unwrap_or_else(chrono::Utc::now)),
}; };
model::attachment::Entity::insert(attachment_model) model::attachment::Entity::insert(attachment_model)
.exec(self.db()) .exec(self.db())
.await?; .await?;
} }
Ok(object_model) Ok(object)
}
async fn insert_activity(&self, activity: impl apb::Activity, server: Option<String>) -> crate::Result<model::activity::Model> {
let mut activity_model = model::activity::Model {
internal: 0,
id: activity.id().ok_or_else(|| UpubError::field("id"))?.to_string(),
activity_type: activity.activity_type().ok_or_else(|| UpubError::field("type"))?,
actor: activity.actor().id().ok_or_else(|| UpubError::field("actor"))?,
object: activity.object().id(),
target: activity.target().id(),
published: activity.published().unwrap_or(chrono::Utc::now()),
to: activity.to().into(),
bto: activity.bto().into(),
cc: activity.cc().into(),
bcc: activity.bcc().into(),
};
if let Some(server) = server {
if Context::server(&activity_model.actor) != server
|| Context::server(&activity_model.id) != server {
return Err(UpubError::forbidden());
}
}
let mut active_model = activity_model.clone().into_active_model();
active_model.internal = NotSet;
model::activity::Entity::insert(active_model)
.exec(self.db())
.await?;
let internal = model::activity::Entity::ap_to_internal(&activity_model.id, self.db()).await?;
activity_model.internal = internal;
Ok(activity_model)
} }
} }

View file

@ -1,10 +1,10 @@
use apb::{target::Addressed, Activity, ActivityMut, ActorMut, BaseMut, Node, Object, ObjectMut, PublicKeyMut}; use apb::{target::Addressed, Activity, ActivityMut, Base, BaseMut, Node, Object, ObjectMut};
use reqwest::StatusCode; use reqwest::StatusCode;
use sea_orm::{sea_query::Expr, ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter, QuerySelect, SelectColumns, Set}; 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, routes::activitypub::jsonld::LD};
use super::{fetcher::Fetcher, normalizer::Normalizer, Context}; use super::{addresser::Addresser, builders::AnyQuery, fetcher::Fetcher, normalizer::Normalizer, side_effects::SideEffects, Context};
#[axum::async_trait] #[axum::async_trait]
@ -14,61 +14,16 @@ impl apb::server::Outbox for Context {
type Activity = serde_json::Value; type Activity = serde_json::Value;
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> {
// TODO regex hell, here i come... self.create(
let re = regex::Regex::new(r"@(.+)@([^ ]+)").expect("failed compiling regex pattern"); uid,
let raw_oid = uuid::Uuid::new_v4().to_string(); serde_json::Value::new_object()
let oid = self.oid(&raw_oid); .set_activity_type(Some(apb::ActivityType::Create))
let aid = self.aid(&uuid::Uuid::new_v4().to_string()); .set_to(object.to())
let activity_targets = object.addressed(); .set_bto(object.bto())
.set_cc(object.cc())
let mut content = object.content().map(|x| x.to_string()); .set_bcc(object.bcc())
if let Some(c) = content { .set_object(Node::object(object))
let mut tmp = mdhtml::safe_markdown(&c); ).await
for (full, [user, domain]) in re.captures_iter(&tmp.clone()).map(|x| x.extract()) {
if let Ok(Some(uid)) = model::user::Entity::find()
.filter(model::user::Column::PreferredUsername.eq(user))
.filter(model::user::Column::Domain.eq(domain))
.select_only()
.select_column(model::user::Column::Id)
.into_tuple::<String>()
.one(self.db())
.await
{
tmp = tmp.replacen(full, &format!("<a href=\"{uid}\" class=\"u-url mention\">@{user}</a>"), 1);
}
}
content = Some(tmp);
}
let object_model = self.insert_object(
object
.set_id(Some(&oid))
.set_attributed_to(Node::link(uid.clone()))
.set_published(Some(chrono::Utc::now()))
.set_content(content.as_deref())
.set_url(Node::maybe_link(self.cfg().instance.frontend.as_ref().map(|x| format!("{x}/objects/{raw_oid}")))),
Some(self.domain().to_string()),
).await?;
let activity_model = model::activity::Model {
id: aid.clone(),
activity_type: apb::ActivityType::Create,
actor: uid.clone(),
object: Some(oid.clone()),
target: None,
cc: object_model.cc.clone(),
bcc: object_model.bcc.clone(),
to: object_model.to.clone(),
bto: object_model.bto.clone(),
published: object_model.published,
};
model::activity::Entity::insert(activity_model.into_active_model())
.exec(self.db()).await?;
self.dispatch(&uid, activity_targets, &aid, Some(&oid)).await?;
Ok(aid)
} }
async fn create(&self, uid: String, activity: serde_json::Value) -> crate::Result<String> { async fn create(&self, uid: String, activity: serde_json::Value) -> crate::Result<String> {
@ -81,28 +36,49 @@ impl apb::server::Outbox for Context {
let aid = self.aid(&uuid::Uuid::new_v4().to_string()); let aid = self.aid(&uuid::Uuid::new_v4().to_string());
let activity_targets = activity.addressed(); let activity_targets = activity.addressed();
if let Some(reply) = object.in_reply_to().id() {
self.fetch_object(&reply).await?;
}
// TODO regex hell here i come...
let re = regex::Regex::new(r"@(.+)@([^ ]+)").expect("failed compiling regex pattern");
let mut content = object.content().map(|x| x.to_string());
if let Some(c) = content {
let mut tmp = mdhtml::safe_markdown(&c);
for (full, [user, domain]) in re.captures_iter(&tmp.clone()).map(|x| x.extract()) {
if let Ok(Some(uid)) = model::actor::Entity::find()
.filter(model::actor::Column::PreferredUsername.eq(user))
.filter(model::actor::Column::Domain.eq(domain))
.select_only()
.select_column(model::actor::Column::Id)
.into_tuple::<String>()
.one(self.db())
.await
{
tmp = tmp.replacen(full, &format!("<a href=\"{uid}\" class=\"u-url mention\">@{user}</a>"), 1);
}
}
content = Some(tmp);
}
self.insert_object( self.insert_object(
object object
.set_id(Some(&oid)) .set_id(Some(&oid))
.set_attributed_to(Node::link(uid.clone())) .set_attributed_to(Node::link(uid.clone()))
.set_published(Some(chrono::Utc::now())) .set_published(Some(chrono::Utc::now()))
.set_to(activity.to()) .set_content(content.as_deref())
.set_bto(activity.bto()) .set_url(Node::maybe_link(self.cfg().instance.frontend.as_ref().map(|x| format!("{x}/objects/{raw_oid}")))),
.set_cc(activity.cc())
.set_bcc(activity.bcc()),
Some(self.domain().to_string()), Some(self.domain().to_string()),
).await?; ).await?;
let activity_model = model::activity::Model::new( self.insert_activity(
&activity activity
.set_id(Some(&aid)) .set_id(Some(&aid))
.set_actor(Node::link(uid.clone())) .set_actor(Node::link(uid.clone()))
.set_published(Some(chrono::Utc::now()))
.set_object(Node::link(oid.clone())) .set_object(Node::link(oid.clone()))
)?; .set_published(Some(chrono::Utc::now())),
Some(self.domain().to_string()),
model::activity::Entity::insert(activity_model.into_active_model()) ).await?;
.exec(self.db()).await?;
self.dispatch(&uid, activity_targets, &aid, Some(&oid)).await?; self.dispatch(&uid, activity_targets, &aid, Some(&oid)).await?;
Ok(aid) Ok(aid)
@ -112,28 +88,26 @@ impl apb::server::Outbox for Context {
let aid = self.aid(&uuid::Uuid::new_v4().to_string()); let aid = self.aid(&uuid::Uuid::new_v4().to_string());
let activity_targets = activity.addressed(); let activity_targets = activity.addressed();
let oid = activity.object().id().ok_or_else(UpubError::bad_request)?; let oid = activity.object().id().ok_or_else(UpubError::bad_request)?;
self.fetch_object(&oid).await?; let obj_model = self.fetch_object(&oid).await?;
let activity_model = model::activity::Model::new(
&activity
.set_id(Some(&aid))
.set_published(Some(chrono::Utc::now()))
.set_actor(Node::link(uid.clone()))
)?;
let like_model = model::like::ActiveModel { let internal_uid = model::actor::Entity::ap_to_internal(&uid, self.db()).await?;
actor: Set(uid.clone()),
likes: Set(oid.clone()), if model::like::Entity::find_by_uid_oid(internal_uid, obj_model.internal)
date: Set(chrono::Utc::now()), .any(self.db())
..Default::default() .await?
}; {
model::like::Entity::insert(like_model).exec(self.db()).await?; return Err(UpubError::not_modified());
model::activity::Entity::insert(activity_model.into_active_model()) }
.exec(self.db()).await?;
model::object::Entity::update_many() let activity_model = self.insert_activity(
.col_expr(model::object::Column::Likes, Expr::col(model::object::Column::Likes).add(1)) activity
.filter(model::object::Column::Id.eq(oid)) .set_id(Some(&aid))
.exec(self.db()) .set_actor(Node::link(uid.clone()))
.await?; .set_published(Some(chrono::Utc::now())),
Some(self.domain().to_string()),
).await?;
self.process_like(internal_uid, obj_model.internal, activity_model.internal, chrono::Utc::now()).await?;
self.dispatch(&uid, activity_targets, &aid, None).await?; self.dispatch(&uid, activity_targets, &aid, None).await?;
@ -143,17 +117,32 @@ impl apb::server::Outbox for Context {
async fn follow(&self, uid: String, activity: serde_json::Value) -> crate::Result<String> { async fn follow(&self, uid: String, activity: serde_json::Value) -> crate::Result<String> {
let aid = self.aid(&uuid::Uuid::new_v4().to_string()); let aid = self.aid(&uuid::Uuid::new_v4().to_string());
let activity_targets = activity.addressed(); let activity_targets = activity.addressed();
if activity.object().id().is_none() { let target = activity.object().id().ok_or_else(UpubError::bad_request)?;
return Err(UpubError::bad_request());
}
let activity_model = model::activity::Model::new( let activity_model = model::activity::ActiveModel::new(
&activity &activity
.set_id(Some(&aid)) .set_id(Some(&aid))
.set_actor(Node::link(uid.clone())) .set_actor(Node::link(uid.clone()))
.set_published(Some(chrono::Utc::now())) .set_published(Some(chrono::Utc::now()))
)?; )?;
model::activity::Entity::insert(activity_model.into_active_model())
let follower_internal = model::actor::Entity::ap_to_internal(&uid, self.db()).await?;
let following_internal = model::actor::Entity::ap_to_internal(&target, self.db()).await?;
model::activity::Entity::insert(activity_model)
.exec(self.db()).await?;
let internal_aid = model::activity::Entity::ap_to_internal(&aid, self.db()).await?;
let relation_model = model::relation::ActiveModel {
internal: NotSet,
follower: Set(follower_internal),
following: Set(following_internal),
activity: Set(internal_aid),
accept: Set(None),
};
model::relation::Entity::insert(relation_model)
.exec(self.db()).await?; .exec(self.db()).await?;
self.dispatch(&uid, activity_targets, &aid, None).await?; self.dispatch(&uid, activity_targets, &aid, None).await?;
@ -164,39 +153,19 @@ impl apb::server::Outbox for Context {
async fn accept(&self, uid: String, activity: serde_json::Value) -> crate::Result<String> { async fn accept(&self, uid: String, activity: serde_json::Value) -> crate::Result<String> {
let aid = self.aid(&uuid::Uuid::new_v4().to_string()); let aid = self.aid(&uuid::Uuid::new_v4().to_string());
let activity_targets = activity.addressed(); let activity_targets = activity.addressed();
if activity.object().id().is_none() { let accepted_id = activity.object().id().ok_or_else(UpubError::bad_request)?;
return Err(UpubError::bad_request()); let accepted_activity = model::activity::Entity::find_by_ap_id(&accepted_id)
}
let Some(accepted_id) = activity.object().id() else {
return Err(UpubError::bad_request());
};
let Some(accepted_activity) = model::activity::Entity::find_by_id(accepted_id)
.one(self.db()).await? .one(self.db()).await?
else { .ok_or_else(UpubError::not_found)?;
return Err(UpubError::not_found());
};
match accepted_activity.activity_type { if accepted_activity.activity_type != apb::ActivityType::Follow {
apb::ActivityType::Follow => { return Err(UpubError::bad_request());
model::user::Entity::update_many() }
.col_expr( if uid != accepted_activity.object.ok_or_else(UpubError::bad_request)? {
model::user::Column::FollowersCount, return Err(UpubError::forbidden());
Expr::col(model::user::Column::FollowersCount).add(1)
)
.filter(model::user::Column::Id.eq(&uid))
.exec(self.db())
.await?;
model::relation::Entity::insert(
model::relation::ActiveModel {
follower: Set(accepted_activity.actor), following: Set(uid.clone()),
..Default::default()
}
).exec(self.db()).await?;
},
t => tracing::warn!("no side effects implemented for accepting {t:?}"),
} }
let activity_model = model::activity::Model::new( let activity_model = model::activity::ActiveModel::new(
&activity &activity
.set_id(Some(&aid)) .set_id(Some(&aid))
.set_actor(Node::link(uid.clone())) .set_actor(Node::link(uid.clone()))
@ -205,54 +174,92 @@ impl apb::server::Outbox for Context {
model::activity::Entity::insert(activity_model.into_active_model()) model::activity::Entity::insert(activity_model.into_active_model())
.exec(self.db()).await?; .exec(self.db()).await?;
let internal_aid = model::activity::Entity::ap_to_internal(&aid, self.db()).await?;
match accepted_activity.activity_type {
apb::ActivityType::Follow => {
model::actor::Entity::update_many()
.col_expr(
model::actor::Column::FollowersCount,
Expr::col(model::actor::Column::FollowersCount).add(1)
)
.filter(model::actor::Column::Id.eq(&uid))
.exec(self.db())
.await?;
model::relation::Entity::update_many()
.filter(model::relation::Column::Activity.eq(accepted_activity.internal))
.col_expr(model::relation::Column::Accept, Expr::value(Some(internal_aid)))
.exec(self.db()).await?;
},
t => tracing::error!("no side effects implemented for accepting {t:?}"),
}
self.dispatch(&uid, activity_targets, &aid, None).await?; self.dispatch(&uid, activity_targets, &aid, None).await?;
Ok(aid) Ok(aid)
} }
async fn reject(&self, _uid: String, _activity: serde_json::Value) -> crate::Result<String> { async fn reject(&self, uid: String, activity: serde_json::Value) -> crate::Result<String> {
todo!() let aid = self.aid(&uuid::Uuid::new_v4().to_string());
let activity_targets = activity.addressed();
let rejected_id = activity.object().id().ok_or_else(UpubError::bad_request)?;
let rejected_activity = model::activity::Entity::find_by_ap_id(&rejected_id)
.one(self.db()).await?
.ok_or_else(UpubError::not_found)?;
if rejected_activity.activity_type != apb::ActivityType::Follow {
return Err(UpubError::bad_request());
}
if uid != rejected_activity.object.ok_or_else(UpubError::bad_request)? {
return Err(UpubError::forbidden());
}
let activity_model = model::activity::ActiveModel::new(
&activity
.set_id(Some(&aid))
.set_actor(Node::link(uid.clone()))
.set_published(Some(chrono::Utc::now()))
)?;
model::activity::Entity::insert(activity_model)
.exec(self.db()).await?;
let internal_aid = model::activity::Entity::ap_to_internal(&aid, self.db()).await?;
model::relation::Entity::delete_many()
.filter(model::relation::Column::Activity.eq(internal_aid))
.exec(self.db())
.await?;
self.dispatch(&uid, activity_targets, &aid, None).await?;
Ok(aid)
} }
async fn undo(&self, uid: String, activity: serde_json::Value) -> crate::Result<String> { async fn undo(&self, uid: String, activity: serde_json::Value) -> crate::Result<String> {
let aid = self.aid(&uuid::Uuid::new_v4().to_string()); let aid = self.aid(&uuid::Uuid::new_v4().to_string());
let activity_targets = activity.addressed(); let internal_uid = model::actor::Entity::ap_to_internal(&uid, self.db()).await?;
let old_aid = activity.object().id().ok_or_else(UpubError::bad_request)?; let old_aid = activity.object().id().ok_or_else(UpubError::bad_request)?;
let old_activity = model::activity::Entity::find_by_id(old_aid) let old_activity = model::activity::Entity::find_by_ap_id(&old_aid)
.one(self.db()) .one(self.db())
.await? .await?
.ok_or_else(UpubError::not_found)?; .ok_or_else(UpubError::not_found)?;
if old_activity.actor != uid { if old_activity.actor != uid {
return Err(UpubError::forbidden()); return Err(UpubError::forbidden());
} }
match old_activity.activity_type {
apb::ActivityType::Like => { let activity_model = self.insert_activity(
model::like::Entity::delete_many() activity.clone()
.filter(model::like::Column::Actor.eq(old_activity.actor))
.filter(model::like::Column::Likes.eq(old_activity.object.unwrap_or("".into())))
.exec(self.db())
.await?;
},
apb::ActivityType::Follow => {
model::relation::Entity::delete_many()
.filter(model::relation::Column::Follower.eq(old_activity.actor))
.filter(model::relation::Column::Following.eq(old_activity.object.unwrap_or("".into())))
.exec(self.db())
.await?;
},
t => tracing::warn!("extra side effects for activity {t:?} not implemented"),
}
let activity_model = model::activity::Model::new(
&activity
.set_id(Some(&aid)) .set_id(Some(&aid))
.set_actor(Node::link(uid.clone())) .set_actor(Node::link(uid.clone()))
.set_published(Some(chrono::Utc::now())) .set_published(Some(chrono::Utc::now())),
)?; Some(self.domain().to_string())
model::activity::Entity::insert(activity_model.into_active_model()) ).await?;
.exec(self.db())
.await?;
self.dispatch(&uid, activity_targets, &aid, None).await?; let targets = self.expand_addressing(activity.addressed()).await?;
self.process_undo(internal_uid, activity).await?;
self.address_to(Some(activity_model.internal), None, &targets).await?;
self.deliver_to(&activity_model.id, &uid, &targets).await?;
Ok(aid) Ok(aid)
} }
@ -261,34 +268,29 @@ impl apb::server::Outbox for Context {
let aid = self.aid(&uuid::Uuid::new_v4().to_string()); let aid = self.aid(&uuid::Uuid::new_v4().to_string());
let oid = activity.object().id().ok_or_else(UpubError::bad_request)?; let oid = activity.object().id().ok_or_else(UpubError::bad_request)?;
let object = model::object::Entity::find_by_id(&oid) let object = model::object::Entity::find_by_ap_id(&oid)
.one(self.db()) .one(self.db())
.await? .await?
.ok_or_else(UpubError::not_found)?; .ok_or_else(UpubError::not_found)?;
let Some(author_id) = object.attributed_to else { if uid != object.attributed_to.ok_or_else(UpubError::forbidden)? {
// can't change local objects attributed to nobody // can't change objects of others, and objects from noone count as others
return Err(UpubError::forbidden())
};
if author_id != uid {
// can't change objects of others
return Err(UpubError::forbidden()); return Err(UpubError::forbidden());
} }
let addressed = activity.addressed(); let addressed = activity.addressed();
let activity_model = model::activity::Model::new( let activity_model = model::activity::ActiveModel::new(
&activity &activity
.set_id(Some(&aid)) .set_id(Some(&aid))
.set_actor(Node::link(uid.clone())) .set_actor(Node::link(uid.clone()))
.set_published(Some(chrono::Utc::now())) .set_published(Some(chrono::Utc::now()))
)?; )?;
model::object::Entity::delete_by_id(&oid) model::activity::Entity::insert(activity_model)
.exec(self.db()) .exec(self.db())
.await?; .await?;
model::activity::Entity::insert(activity_model.into_active_model()) model::object::Entity::delete_by_ap_id(&oid)
.exec(self.db()) .exec(self.db())
.await?; .await?;
@ -300,18 +302,22 @@ impl apb::server::Outbox for Context {
async fn update(&self, uid: String, activity: serde_json::Value) -> crate::Result<String> { async fn update(&self, uid: String, activity: serde_json::Value) -> crate::Result<String> {
let aid = self.aid(&uuid::Uuid::new_v4().to_string()); let aid = self.aid(&uuid::Uuid::new_v4().to_string());
let object_node = activity.object().extract().ok_or_else(UpubError::bad_request)?; let object_node = activity.object().extract().ok_or_else(UpubError::bad_request)?;
let addressed = activity.addressed();
let target = object_node.id().ok_or_else(UpubError::bad_request)?.to_string();
let activity_model = model::activity::ActiveModel::new(
&activity
.set_id(Some(&aid))
.set_actor(Node::link(uid.clone()))
.set_published(Some(chrono::Utc::now()))
)?;
model::activity::Entity::insert(activity_model)
.exec(self.db()).await?;
match object_node.object_type() { match object_node.object_type() {
Some(apb::ObjectType::Actor(_)) => { Some(apb::ObjectType::Actor(_)) => {
let mut actor_model = model::user::Model::new( let old_actor_model = model::actor::Entity::find_by_ap_id(&target)
&object_node
// TODO must set these, but we will ignore them
.set_actor_type(Some(apb::ActorType::Person))
.set_public_key(apb::Node::object(
serde_json::Value::new_object().set_public_key_pem("")
))
)?;
let old_actor_model = model::user::Entity::find_by_id(&actor_model.id)
.one(self.db()) .one(self.db())
.await? .await?
.ok_or_else(UpubError::not_found)?; .ok_or_else(UpubError::not_found)?;
@ -321,66 +327,61 @@ impl apb::server::Outbox for Context {
return Err(UpubError::forbidden()); return Err(UpubError::forbidden());
} }
if actor_model.name.is_none() { actor_model.name = old_actor_model.name } let mut new_actor_model = model::actor::ActiveModel {
if actor_model.summary.is_none() { actor_model.summary = old_actor_model.summary } internal: Unchanged(old_actor_model.internal),
if actor_model.image.is_none() { actor_model.image = old_actor_model.image } ..Default::default()
if actor_model.icon.is_none() { actor_model.icon = old_actor_model.icon } };
let mut update_model = actor_model.into_active_model(); if let Some(name) = object_node.name() {
update_model.updated = sea_orm::Set(chrono::Utc::now()); new_actor_model.name = Set(Some(name.to_string()));
update_model.reset(model::user::Column::Name); }
update_model.reset(model::user::Column::Summary); if let Some(summary) = object_node.summary() {
update_model.reset(model::user::Column::Image); new_actor_model.summary = Set(Some(summary.to_string()));
update_model.reset(model::user::Column::Icon); }
if let Some(image) = object_node.image().id() {
new_actor_model.image = Set(Some(image));
}
if let Some(icon) = object_node.icon().id() {
new_actor_model.icon = Set(Some(icon));
}
new_actor_model.updated = Set(chrono::Utc::now());
model::user::Entity::update(update_model) model::actor::Entity::update(new_actor_model)
.exec(self.db()).await?; .exec(self.db()).await?;
}, },
Some(apb::ObjectType::Note) => { Some(apb::ObjectType::Note) => {
let mut object_model = model::object::Model::new( let old_object_model = model::object::Entity::find_by_ap_id(&target)
&object_node.set_published(Some(chrono::Utc::now()))
)?;
let old_object_model = model::object::Entity::find_by_id(&object_model.id)
.one(self.db()) .one(self.db())
.await? .await?
.ok_or_else(UpubError::not_found)?; .ok_or_else(UpubError::not_found)?;
// can't change local objects attributed to nobody if uid != old_object_model.attributed_to.ok_or_else(UpubError::forbidden)? {
let author_id = old_object_model.attributed_to.ok_or_else(UpubError::forbidden)?;
if author_id != uid {
// can't change objects of others // can't change objects of others
return Err(UpubError::forbidden()); return Err(UpubError::forbidden());
} }
if object_model.name.is_none() { object_model.name = old_object_model.name } let mut new_object_model = model::object::ActiveModel {
if object_model.summary.is_none() { object_model.summary = old_object_model.summary } internal: Unchanged(old_object_model.internal),
if object_model.content.is_none() { object_model.content = old_object_model.content } ..Default::default()
};
let mut update_model = object_model.into_active_model(); if let Some(name) = object_node.name() {
update_model.updated = sea_orm::Set(Some(chrono::Utc::now())); new_object_model.name = Set(Some(name.to_string()));
update_model.reset(model::object::Column::Name); }
update_model.reset(model::object::Column::Summary); if let Some(summary) = object_node.summary() {
update_model.reset(model::object::Column::Content); new_object_model.summary = Set(Some(summary.to_string()));
update_model.reset(model::object::Column::Sensitive); }
if let Some(content) = object_node.content() {
new_object_model.content = Set(Some(content.to_string()));
}
new_object_model.updated = Set(chrono::Utc::now());
model::object::Entity::update(update_model) model::object::Entity::update(new_object_model)
.exec(self.db()).await?; .exec(self.db()).await?;
}, },
_ => return Err(UpubError::Status(StatusCode::NOT_IMPLEMENTED)), _ => return Err(UpubError::Status(StatusCode::NOT_IMPLEMENTED)),
} }
let addressed = activity.addressed();
let activity_model = model::activity::Model::new(
&activity
.set_id(Some(&aid))
.set_actor(Node::link(uid.clone()))
.set_published(Some(chrono::Utc::now()))
)?;
model::activity::Entity::insert(activity_model.into_active_model())
.exec(self.db()).await?;
self.dispatch(&uid, addressed, &aid, None).await?; self.dispatch(&uid, addressed, &aid, None).await?;
Ok(aid) Ok(aid)
@ -390,26 +391,28 @@ impl apb::server::Outbox for Context {
let aid = self.aid(&uuid::Uuid::new_v4().to_string()); let aid = self.aid(&uuid::Uuid::new_v4().to_string());
let activity_targets = activity.addressed(); let activity_targets = activity.addressed();
let oid = activity.object().id().ok_or_else(UpubError::bad_request)?; let oid = activity.object().id().ok_or_else(UpubError::bad_request)?;
self.fetch_object(&oid).await?; let obj = self.fetch_object(&oid).await?;
let activity_model = model::activity::Model::new( let internal_uid = model::actor::Entity::ap_to_internal(&uid, self.db()).await?;
let activity_model = model::activity::ActiveModel::new(
&activity &activity
.set_id(Some(&aid)) .set_id(Some(&aid))
.set_published(Some(chrono::Utc::now()))
.set_actor(Node::link(uid.clone())) .set_actor(Node::link(uid.clone()))
.set_published(Some(chrono::Utc::now()))
)?; )?;
let share_model = model::share::ActiveModel { let share_model = model::announce::ActiveModel {
actor: Set(uid.clone()), internal: NotSet,
shares: Set(oid.clone()), actor: Set(internal_uid),
date: Set(chrono::Utc::now()), object: Set(obj.internal),
..Default::default() published: Set(chrono::Utc::now()),
}; };
model::share::Entity::insert(share_model).exec(self.db()).await?; model::activity::Entity::insert(activity_model)
model::activity::Entity::insert(activity_model.into_active_model())
.exec(self.db()).await?; .exec(self.db()).await?;
model::announce::Entity::insert(share_model).exec(self.db()).await?;
model::object::Entity::update_many() model::object::Entity::update_many()
.col_expr(model::object::Column::Shares, Expr::col(model::object::Column::Shares).add(1)) .col_expr(model::object::Column::Announces, Expr::col(model::object::Column::Announces).add(1))
.filter(model::object::Column::Id.eq(oid)) .filter(model::object::Column::Internal.eq(obj.internal))
.exec(self.db()) .exec(self.db())
.await?; .await?;

View file

@ -0,0 +1,79 @@
use reqwest::StatusCode;
use sea_orm::{sea_query::Expr, ActiveValue::{NotSet, Set}, ColumnTrait, Condition, EntityTrait, QueryFilter};
use crate::{errors::UpubError, model};
#[axum::async_trait]
pub trait SideEffects {
async fn process_like(&self, who: i64, what: i64, with: i64, when: chrono::DateTime<chrono::Utc>) -> crate::Result<()>;
async fn process_undo(&self, who: i64, activity: impl apb::Activity) -> crate::Result<()>;
}
#[axum::async_trait]
impl SideEffects for super::Context {
async fn process_like(&self, who: i64, what: i64, with: i64, when: chrono::DateTime<chrono::Utc>) -> crate::Result<()> {
let like = model::like::ActiveModel {
internal: NotSet,
actor: Set(who),
object: Set(what),
activity: Set(with),
published: Set(when),
};
model::like::Entity::insert(like).exec(self.db()).await?;
model::object::Entity::update_many()
.col_expr(model::object::Column::Likes, Expr::col(model::object::Column::Likes).add(1))
.filter(model::object::Column::Internal.eq(what))
.exec(self.db())
.await?;
Ok(())
}
async fn process_undo(&self, who: i64, activity: impl apb::Activity) -> crate::Result<()> {
let undone_object_id = activity.object().id().ok_or_else(UpubError::bad_request)?;
match activity.activity_type() {
Some(apb::ActivityType::Like) => {
let internal_oid = model::object::Entity::ap_to_internal(&undone_object_id, self.db()).await?;
model::like::Entity::delete_many()
.filter(
Condition::all()
.add(model::like::Column::Actor.eq(who))
.add(model::like::Column::Object.eq(internal_oid))
)
.exec(self.db())
.await?;
model::object::Entity::update_many()
.filter(model::object::Column::Internal.eq(internal_oid))
.col_expr(model::object::Column::Likes, Expr::col(model::object::Column::Likes).sub(1))
.exec(self.db())
.await?;
},
Some(apb::ActivityType::Follow) => {
let undone_aid = activity.object().id().ok_or_else(UpubError::bad_request)?;
let internal_aid = model::activity::Entity::ap_to_internal(&undone_aid, self.db()).await?;
model::relation::Entity::delete_many()
.filter(model::relation::Column::Activity.eq(internal_aid))
.exec(self.db())
.await?;
model::actor::Entity::update_many()
.filter(model::actor::Column::Internal.eq(who))
.col_expr(model::actor::Column::FollowingCount, Expr::col(model::actor::Column::FollowingCount).sub(1))
.exec(self.db())
.await?;
model::actor::Entity::update_many()
.filter(model::actor::Column::Id.eq(&undone_object_id))
.col_expr(model::actor::Column::FollowersCount, Expr::col(model::actor::Column::FollowersCount).sub(1))
.exec(self.db())
.await?;
},
t => {
tracing::error!("received 'Undo' for unimplemented activity type: {t:?}");
return Err(StatusCode::NOT_IMPLEMENTED.into());
},
}
Ok(())
}
}

View file

@ -23,37 +23,44 @@ impl TokenSink for Sink {
) { return TokenSinkResult::Continue } // skip this tag ) { return TokenSinkResult::Continue } // skip this tag
self.0.push('<'); self.0.push('<');
if !tag.self_closing && matches!(tag.kind, TagKind::EndTag) { if !tag.self_closing && matches!(tag.kind, TagKind::EndTag) {
self.0.push('/'); self.0.push('/');
} }
self.0.push_str(tag.name.as_ref()); self.0.push_str(tag.name.as_ref());
match tag.name.as_ref() { if !matches!(tag.kind, TagKind::EndTag) {
"img" => for attr in tag.attrs { match tag.name.as_ref() {
match attr.name.local.as_ref() { "img" => for attr in tag.attrs {
"src" => self.0.push_str(&format!(" src=\"{}\"", attr.value.as_ref())),
"title" => self.0.push_str(&format!(" title=\"{}\"", attr.value.as_ref())),
"alt" => self.0.push_str(&format!(" alt=\"{}\"", attr.value.as_ref())),
_ => {},
}
},
"a" => {
for attr in tag.attrs {
match attr.name.local.as_ref() { match attr.name.local.as_ref() {
"href" => self.0.push_str(&format!(" href=\"{}\"", attr.value.as_ref())), "src" => self.0.push_str(&format!(" src=\"{}\"", attr.value.as_ref())),
"title" => self.0.push_str(&format!(" title=\"{}\"", attr.value.as_ref())), "title" => self.0.push_str(&format!(" title=\"{}\"", attr.value.as_ref())),
"alt" => self.0.push_str(&format!(" alt=\"{}\"", attr.value.as_ref())),
_ => {}, _ => {},
} }
} },
self.0.push_str(" rel=\"nofollow noreferrer\" target=\"_blank\""); "a" => {
}, let any_attr = !tag.attrs.is_empty();
_ => {}, for attr in tag.attrs {
match attr.name.local.as_ref() {
"href" => self.0.push_str(&format!(" href=\"{}\"", attr.value.as_ref())),
"title" => self.0.push_str(&format!(" title=\"{}\"", attr.value.as_ref())),
_ => {},
}
}
if any_attr {
self.0.push_str(" rel=\"nofollow noreferrer\" target=\"_blank\"");
}
},
_ => {},
}
} }
if tag.self_closing { if tag.self_closing {
self.0.push('/'); self.0.push('/');
} }
self.0.push('>'); self.0.push('>');
}, },
Token::CharacterTokens(txt) => self.0.push_str(txt.as_ref()), Token::CharacterTokens(txt) => self.0.push_str(txt.as_ref()),

View file

@ -2,7 +2,7 @@ use base64::Engine;
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub enum UriClass { pub enum UriClass {
User, Actor,
Object, Object,
Activity, Activity,
Context, Context,
@ -11,7 +11,7 @@ pub enum UriClass {
impl AsRef<str> for UriClass { impl AsRef<str> for UriClass {
fn as_ref(&self) -> &str { fn as_ref(&self) -> &str {
match self { match self {
Self::User => "users", Self::Actor => "actors",
Self::Object => "objects", Self::Object => "objects",
Self::Activity => "activities", Self::Activity => "activities",
Self::Context => "context", Self::Context => "context",
@ -38,10 +38,10 @@ pub fn uri(base: &str, entity: UriClass, id: &str) -> String {
/// decompose local id constructed by uri() fn /// decompose local id constructed by uri() fn
pub fn decompose_id(full_id: &str) -> String { pub fn decompose_id(full_id: &str) -> String {
full_id // https://example.org/users/test/followers/page?offset=42 full_id // https://example.org/actors/test/followers/page?offset=42
.replace("https://", "") .replace("https://", "")
.replace("http://", "") .replace("http://", "")
.split('/') // ['example.org', 'users', 'test', 'followers', 'page?offset=42' ] .split('/') // ['example.org', 'actors', 'test', 'followers', 'page?offset=42' ]
.nth(2) // 'test' .nth(2) // 'test'
.unwrap_or("") .unwrap_or("")
.to_string() .to_string()

View file

@ -9,7 +9,7 @@
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:title" content="upub"> <meta property="og:title" content="upub">
<meta property="og:description" content="micro social network, federated" /> <meta property="og:description" content="micro social network, federated" />
<meta property="og:url" content="https://feditest.alemi.dev/web" /> <meta property="og:url" content="https://upub.alemi.dev/web" />
<meta property="og:site_name" content="upub" /> <meta property="og:site_name" content="upub" />
<link rel="preload" href="https://cdn.alemi.dev/web/font/FiraCode-Regular.woff2" as="font" type="font/woff2" crossorigin="anonymous"> <link rel="preload" href="https://cdn.alemi.dev/web/font/FiraCode-Regular.woff2" as="font" type="font/woff2" crossorigin="anonymous">

View file

@ -1,19 +1,16 @@
use leptos::*; use leptos::*;
use leptos_router::*; use leptos_router::*;
use reqwest::Method;
use crate::prelude::*; use crate::prelude::*;
use leptos_use::{storage::use_local_storage, use_cookie, use_cookie_with_options, utils::{FromToStringCodec, JsonCodec}, UseCookieOptions}; use leptos_use::{storage::use_local_storage, use_cookie, utils::{FromToStringCodec, JsonCodec}};
#[component] #[component]
pub fn App() -> impl IntoView { pub fn App() -> impl IntoView {
let (token, set_token) = use_cookie_with_options::<String, FromToStringCodec>( let (token, set_token) = use_cookie::<String, FromToStringCodec>("token");
"token",
UseCookieOptions::default()
.max_age(1000 * 60 * 60 * 6)
);
let (config, set_config, _) = use_local_storage::<crate::Config, JsonCodec>("config");
let (userid, set_userid) = use_cookie::<String, FromToStringCodec>("user_id"); let (userid, set_userid) = use_cookie::<String, FromToStringCodec>("user_id");
let (config, set_config, _) = use_local_storage::<crate::Config, JsonCodec>("config");
let auth = Auth { token, userid }; let auth = Auth { token, userid };
provide_context(auth); provide_context(auth);
@ -22,10 +19,12 @@ pub fn App() -> impl IntoView {
let username = auth.userid.get_untracked() let username = auth.userid.get_untracked()
.map(|x| x.split('/').last().unwrap_or_default().to_string()) .map(|x| x.split('/').last().unwrap_or_default().to_string())
.unwrap_or_default(); .unwrap_or_default();
let home_tl = Timeline::new(format!("{URL_BASE}/users/{username}/inbox/page")); let home_tl = Timeline::new(format!("{URL_BASE}/actors/{username}/inbox/page"));
let user_tl = Timeline::new(format!("{URL_BASE}/actors/{username}/outbox/page"));
let server_tl = Timeline::new(format!("{URL_BASE}/inbox/page")); let server_tl = Timeline::new(format!("{URL_BASE}/inbox/page"));
let user_tl = Timeline::new(format!("{URL_BASE}/users/{username}/outbox/page")); let local_tl = Timeline::new(format!("{URL_BASE}/outbox/page"));
let context_tl = Timeline::new(format!("{URL_BASE}/outbox/page"));
let context_tl = Timeline::new(format!("{URL_BASE}/outbox/page")); // TODO ehhh
let reply_controls = ReplyControls::default(); let reply_controls = ReplyControls::default();
provide_context(reply_controls); provide_context(reply_controls);
@ -35,26 +34,42 @@ pub fn App() -> impl IntoView {
let (menu, set_menu) = create_signal(screen_width <= 786); let (menu, set_menu) = create_signal(screen_width <= 786);
let (advanced, set_advanced) = create_signal(false); let (advanced, set_advanced) = create_signal(false);
spawn_local(async move { let title_target = move || if auth.present() { "/web/home" } else { "/web/server" };
if let Err(e) = server_tl.more(auth).await {
tracing::error!("error populating timeline: {e}");
}
});
let auth_present = auth.token.get_untracked().is_some(); // skip helper to use get_untracked if let Some(tok) = token.get_untracked() {
if auth_present {
spawn_local(async move { spawn_local(async move {
if let Err(e) = home_tl.more(auth).await { // refresh token first, or verify that we're still authed
tracing::error!("error populating timeline: {e}"); match reqwest::Client::new()
.request(Method::PATCH, format!("{URL_BASE}/auth"))
.json(&serde_json::json!({"token": tok}))
.send()
.await
{
Err(e) => tracing::error!("could not refresh token: {e}"),
Ok(res) => match res.error_for_status() {
Err(e) => tracing::error!("server rejected refresh: {e}"),
Ok(doc) => match doc.json::<AuthResponse>().await {
Err(e) => tracing::error!("failed parsing auth response: {e}"),
Ok(auth) => {
set_token.set(Some(auth.token));
set_userid.set(Some(auth.user));
},
}
}
} }
});
}
let title_target = if auth_present { "/web/home" } else { "/web/server" }; server_tl.more(auth);
local_tl.more(auth);
if auth.token.get_untracked().is_some() { home_tl.more(auth) };
})
} else {
server_tl.more(auth);
local_tl.more(auth);
}
view! { view! {
<nav class="w-100 mt-1 mb-1 pb-s"> <nav class="w-100 mt-1 mb-1 pb-s">
<code class="color ml-3" ><a class="upub-title" href={title_target} >μpub</a></code> <code class="color ml-3" ><a class="upub-title" href=title_target >μpub</a></code>
<small class="ml-1 mr-1 hidden-on-tiny" ><a class="clean" href="/web/server" >micro social network, federated</a></small> <small class="ml-1 mr-1 hidden-on-tiny" ><a class="clean" href="/web/server" >micro social network, federated</a></small>
/* TODO kinda jank with the float but whatever, will do for now */ /* TODO kinda jank with the float but whatever, will do for now */
<input type="submit" class="mr-2 rev" on:click=move |_| set_menu.set(!menu.get()) value="menu" style="float: right" /> <input type="submit" class="mr-2 rev" on:click=move |_| set_menu.set(!menu.get()) value="menu" style="float: right" />
@ -96,36 +111,34 @@ pub fn App() -> impl IntoView {
// in a sense it's what we want: refreshing the home tl is main purpose, but also // in a sense it's what we want: refreshing the home tl is main purpose, but also
// server tl may contain stuff we can no longer see, or otherwise we may now be // server tl may contain stuff we can no longer see, or otherwise we may now be
// entitled to see new posts. so while being ugly it's techically correct ig? // entitled to see new posts. so while being ugly it's techically correct ig?
{move || { <main>
view! { <Routes>
<main> <Route path="/web" view=move ||
<Routes> if auth.present() {
<Route path="/web" view=move || view! { <Redirect path="/web/home" /> }
if auth.present() { } else {
view! { <Redirect path="/web/home" /> } view! { <Redirect path="/web/server" /> }
} else { }
view! { <Redirect path="/web/server" /> } />
}
/>
<Route path="/web/home" view=move || view! { <TimelinePage name="home" tl=home_tl /> } /> <Route path="/web/home" view=move || view! { <TimelinePage name="home" tl=home_tl /> } />
<Route path="/web/server" view=move || view! { <TimelinePage name="server" tl=server_tl /> } /> <Route path="/web/server" view=move || view! { <TimelinePage name="server" tl=server_tl /> } />
<Route path="/web/local" view=move || view! { <TimelinePage name="local" tl=local_tl /> } />
<Route path="/web/about" view=AboutPage /> <Route path="/web/about" view=AboutPage />
<Route path="/web/config" view=move || view! { <ConfigPage setter=set_config /> } /> <Route path="/web/config" view=move || view! { <ConfigPage setter=set_config /> } />
<Route path="/web/config/dev" view=DebugPage /> <Route path="/web/config/dev" view=DebugPage />
<Route path="/web/users/:id" view=move || view! { <UserPage tl=user_tl /> } /> <Route path="/web/actors/:id" view=move || view! { <UserPage tl=user_tl /> } />
<Route path="/web/objects/:id" view=move || view! { <ObjectPage tl=context_tl /> } /> <Route path="/web/objects/:id" view=move || view! { <ObjectPage tl=context_tl /> } />
// <Route path="/web/activities/:id" view=move || view! { <ActivityPage tl=context_tl /> } />
<Route path="/web/search" view=SearchPage /> <Route path="/web/search" view=SearchPage />
<Route path="/web/register" view=RegisterPage /> <Route path="/web/register" view=RegisterPage />
<Route path="/" view=move || view! { <Redirect path="/web" /> } /> <Route path="/" view=move || view! { <Redirect path="/web" /> } />
</Routes> </Routes>
</main> </main>
}
}}
</Router> </Router>
</div> </div>
</div> </div>

View file

@ -39,6 +39,6 @@ impl AuthToken for Auth {
} }
fn outbox(&self) -> String { fn outbox(&self) -> String {
format!("{URL_BASE}/users/{}/outbox", self.username()) format!("{URL_BASE}/actors/{}/outbox", self.username())
} }
} }

View file

@ -7,11 +7,14 @@ use apb::{target::Addressed, Base, Activity, Object};
#[component] #[component]
pub fn ActivityLine(activity: crate::Object) -> impl IntoView { pub fn ActivityLine(activity: crate::Object) -> impl IntoView {
let object_id = activity.object().id().unwrap_or_default(); let object_id = activity.object().id().unwrap_or_default();
let activity_url = activity.id().map(|x| view! {
<sup><small><a class="clean ml-s" href={x.to_string()} target="_blank">""</a></small></sup>
});
let actor_id = activity.actor().id().unwrap_or_default(); let actor_id = activity.actor().id().unwrap_or_default();
let actor = CACHE.get_or(&actor_id, serde_json::Value::String(actor_id.clone()).into()); let actor = CACHE.get_or(&actor_id, serde_json::Value::String(actor_id.clone()).into());
let kind = activity.activity_type().unwrap_or(apb::ActivityType::Activity); let kind = activity.activity_type().unwrap_or(apb::ActivityType::Activity);
let href = match kind { let href = match kind {
apb::ActivityType::Follow => Uri::web(U::User, &object_id), apb::ActivityType::Follow => Uri::web(U::Actor, &object_id),
// TODO for update check what's being updated // TODO for update check what's being updated
_ => Uri::web(U::Object, &object_id), _ => Uri::web(U::Object, &object_id),
}; };
@ -25,6 +28,7 @@ pub fn ActivityLine(activity: crate::Object) -> impl IntoView {
<a class="upub-title clean" title={object_id} href={href} > <a class="upub-title clean" title={object_id} href={href} >
{kind.as_ref().to_string()} {kind.as_ref().to_string()}
</a> </a>
{activity_url}
<PrivacyMarker addressed=activity.addressed() /> <PrivacyMarker addressed=activity.addressed() />
</code> </code>
</span> </span>
@ -36,6 +40,7 @@ pub fn ActivityLine(activity: crate::Object) -> impl IntoView {
pub fn Item( pub fn Item(
item: crate::Object, item: crate::Object,
#[prop(optional)] sep: bool, #[prop(optional)] sep: bool,
#[prop(optional)] replies: bool,
) -> impl IntoView { ) -> impl IntoView {
let config = use_context::<Signal<crate::Config>>().expect("missing config context"); let config = use_context::<Signal<crate::Config>>().expect("missing config context");
let id = item.id().unwrap_or_default().to_string(); let id = item.id().unwrap_or_default().to_string();
@ -55,7 +60,7 @@ pub fn Item(
Some(apb::ObjectType::Activity(t)) => (move || { Some(apb::ObjectType::Activity(t)) => (move || {
if config.get().filters.visible(apb::ObjectType::Activity(t)) { if config.get().filters.visible(apb::ObjectType::Activity(t)) {
let object_id = item.object().id().unwrap_or_default(); let object_id = item.object().id().unwrap_or_default();
if !config.get().filters.replies && CACHE.get(&object_id).map(|x| x.in_reply_to().id().is_some()).unwrap_or(false) { if !replies && !config.get().filters.replies && CACHE.get(&object_id).map(|x| x.in_reply_to().id().is_some()).unwrap_or(false) {
None None
} else { } else {
let object = match t { let object = match t {

View file

@ -14,16 +14,12 @@ pub fn LoginBox(
view! { view! {
<div> <div>
<div class="w-100" class:hidden=move || !auth.present() > <div class="w-100" class:hidden=move || !auth.present() >
"hi "<a href={move || Uri::web(U::User, &auth.username() )} >{move || auth.username() }</a> "hi "<a href={move || Uri::web(U::Actor, &auth.username() )} >{move || auth.username() }</a>
<input style="float:right" type="submit" value="logout" on:click=move |_| { <input style="float:right" type="submit" value="logout" on:click=move |_| {
token_tx.set(None); token_tx.set(None);
home_tl.reset(format!("{URL_BASE}/outbox/page")); home_tl.reset(format!("{URL_BASE}/outbox/page"));
server_tl.reset(format!("{URL_BASE}/inbox/page")); server_tl.reset(format!("{URL_BASE}/inbox/page"));
spawn_local(async move { server_tl.more(auth);
if let Err(e) = server_tl.more(auth).await {
logging::error!("failed refreshing server timeline: {e}");
}
});
} /> } />
</div> </div>
<div class:hidden=move || auth.present() > <div class:hidden=move || auth.present() >
@ -49,19 +45,11 @@ pub fn LoginBox(
userid_tx.set(Some(auth_response.user)); userid_tx.set(Some(auth_response.user));
token_tx.set(Some(auth_response.token)); token_tx.set(Some(auth_response.token));
// reset home feed and point it to our user's inbox // reset home feed and point it to our user's inbox
home_tl.reset(format!("{URL_BASE}/users/{}/inbox/page", username)); home_tl.reset(format!("{URL_BASE}/actors/{}/inbox/page", username));
spawn_local(async move { home_tl.more(auth);
if let Err(e) = home_tl.more(auth).await {
tracing::error!("failed refreshing home timeline: {e}");
}
});
// reset server feed: there may be more content now that we're authed // reset server feed: there may be more content now that we're authed
server_tl.reset(format!("{URL_BASE}/inbox/page")); server_tl.reset(format!("{URL_BASE}/inbox/page"));
spawn_local(async move { server_tl.more(auth);
if let Err(e) = server_tl.more(auth).await {
tracing::error!("failed refreshing server timeline: {e}");
}
});
}); });
} > } >
<table class="w-100 align"> <table class="w-100 align">
@ -91,8 +79,8 @@ struct LoginForm {
#[derive(Debug, Clone, serde::Deserialize)] #[derive(Debug, Clone, serde::Deserialize)]
struct AuthResponse { pub struct AuthResponse {
token: String, pub token: String,
user: String, pub user: String,
expires: chrono::DateTime<chrono::Utc>, pub expires: chrono::DateTime<chrono::Utc>,
} }

View file

@ -39,6 +39,7 @@ pub fn Navigator() -> impl IntoView {
<table class="align w-100"> <table class="align w-100">
<tr><td colspan="2"><a href="/web/home"><input class="w-100" type="submit" class:hidden=move || !auth.present() value="home timeline" /></a></td></tr> <tr><td colspan="2"><a href="/web/home"><input class="w-100" type="submit" class:hidden=move || !auth.present() value="home timeline" /></a></td></tr>
<tr><td colspan="2"><a href="/web/server"><input class="w-100" type="submit" value="server timeline" /></a></td></tr> <tr><td colspan="2"><a href="/web/server"><input class="w-100" type="submit" value="server timeline" /></a></td></tr>
<tr><td colspan="2"><a href="/web/local"><input class="w-100" type="submit" value="local timeline" /></a></td></tr>
<tr> <tr>
<td class="w-50"><a href="/web/about"><input class="w-100" type="submit" value="about" /></a></td> <td class="w-50"><a href="/web/about"><input class="w-100" type="submit" value="about" /></a></td>
<td class="w-50"><a href="/web/config"><input class="w-100" type="submit" value="config" /></a></td> <td class="w-50"><a href="/web/config"><input class="w-100" type="submit" value="config" /></a></td>

View file

@ -236,7 +236,7 @@ pub fn LikeButton(
let cc = if private { apb::Node::Empty } else { let cc = if private { apb::Node::Empty } else {
apb::Node::links(vec![ apb::Node::links(vec![
apb::target::PUBLIC.to_string(), apb::target::PUBLIC.to_string(),
format!("{URL_BASE}/users/{}/followers", auth.username()) format!("{URL_BASE}/actors/{}/followers", auth.username())
]) ])
}; };
let payload = serde_json::Value::Object(serde_json::Map::default()) let payload = serde_json::Value::Object(serde_json::Map::default())
@ -312,7 +312,7 @@ pub fn RepostButton(n: u64, target: String) -> impl IntoView {
if !clicked.get() { return; } if !clicked.get() { return; }
set_clicked.set(false); set_clicked.set(false);
let to = apb::Node::links(vec![apb::target::PUBLIC.to_string()]); let to = apb::Node::links(vec![apb::target::PUBLIC.to_string()]);
let cc = apb::Node::links(vec![format!("{URL_BASE}/users/{}/followers", auth.username())]); let cc = apb::Node::links(vec![format!("{URL_BASE}/actors/{}/followers", auth.username())]);
let payload = serde_json::Value::Object(serde_json::Map::default()) let payload = serde_json::Value::Object(serde_json::Map::default())
.set_activity_type(Some(apb::ActivityType::Announce)) .set_activity_type(Some(apb::ActivityType::Announce))
.set_object(apb::Node::link(target.clone())) .set_object(apb::Node::link(target.clone()))

View file

@ -1,7 +1,6 @@
use apb::{ActivityMut, Base, BaseMut, Object, ObjectMut}; use apb::{ActivityMut, Base, BaseMut, Object, ObjectMut};
use leptos::*; use leptos::*;
use leptos_use::DebounceOptions;
use crate::{prelude::*, WEBFINGER}; use crate::{prelude::*, WEBFINGER};
#[derive(Debug, Clone, Copy, Default)] #[derive(Debug, Clone, Copy, Default)]
@ -87,7 +86,7 @@ pub fn PostBox(advanced: WriteSignal<bool>) -> impl IntoView {
mentions.get() mentions.get()
.map(|x| x.into_iter().map(|u| match CACHE.get(&u) { .map(|x| x.into_iter().map(|u| match CACHE.get(&u) {
Some(u) => view! { <span class="nowrap"><span class="emoji mr-s ml-s">"📨"</span><ActorStrip object=u /></span> }.into_view(), Some(u) => view! { <span class="nowrap"><span class="emoji mr-s ml-s">"📨"</span><ActorStrip object=u /></span> }.into_view(),
None => view! { <span class="nowrap"><span class="emoji mr-s ml-s">"📨"</span><a href={Uri::web(U::User, &u)}>{u}</a></span> }.into_view(), None => view! { <span class="nowrap"><span class="emoji mr-s ml-s">"📨"</span><a href={Uri::web(U::Actor, &u)}>{u}</a></span> }.into_view(),
}) })
.collect_view()) .collect_view())
} }
@ -116,10 +115,11 @@ pub fn PostBox(advanced: WriteSignal<bool>) -> impl IntoView {
let mut cc_vec = Vec::new(); let mut cc_vec = Vec::new();
let mut to_vec = Vec::new(); let mut to_vec = Vec::new();
if get_checked(followers_ref) { if get_checked(followers_ref) {
cc_vec.push(format!("{URL_BASE}/users/{}/followers", auth.username())); cc_vec.push(format!("{URL_BASE}/actors/{}/followers", auth.username()));
} }
if get_checked(public_ref) { if get_checked(public_ref) {
cc_vec.push(apb::target::PUBLIC.to_string()); cc_vec.push(apb::target::PUBLIC.to_string());
cc_vec.push(format!("{URL_BASE}/actors/{}/followers", auth.username()));
} }
if let Some(r) = reply.reply_to.get() { if let Some(r) = reply.reply_to.get() {
if let Some(au) = post_author(&r) { if let Some(au) = post_author(&r) {
@ -241,7 +241,7 @@ pub fn AdvancedPostBox(advanced: WriteSignal<bool>) -> impl IntoView {
<td class="w-66"><input class="w-100" type="text" node_ref=bto_ref title="bto" placeholder="bto" /></td> <td class="w-66"><input class="w-100" type="text" node_ref=bto_ref title="bto" placeholder="bto" /></td>
</tr> </tr>
<tr> <tr>
<td class="w-33"><input class="w-100" type="text" node_ref=cc_ref title="cc" placeholder="cc" value=format!("{URL_BASE}/users/{}/followers", auth.username()) /></td> <td class="w-33"><input class="w-100" type="text" node_ref=cc_ref title="cc" placeholder="cc" value=format!("{URL_BASE}/actors/{}/followers", auth.username()) /></td>
<td class="w-33"><input class="w-100" type="text" node_ref=bcc_ref title="bcc" placeholder="bcc" /></td> <td class="w-33"><input class="w-100" type="text" node_ref=bcc_ref title="bcc" placeholder="bcc" /></td>
</tr> </tr>
</table> </table>
@ -285,7 +285,7 @@ pub fn AdvancedPostBox(advanced: WriteSignal<bool>) -> impl IntoView {
apb::Node::maybe_link(object_id) apb::Node::maybe_link(object_id)
} }
); );
let target_url = format!("{URL_BASE}/users/{}/outbox", auth.username()); let target_url = format!("{URL_BASE}/actors/{}/outbox", auth.username());
match Http::post(&target_url, &payload, auth).await { match Http::post(&target_url, &payload, auth).await {
Err(e) => set_error.set(Some(e.to_string())), Err(e) => set_error.set(Some(e.to_string())),
Ok(()) => set_error.set(None), Ok(()) => set_error.set(None),

View file

@ -2,7 +2,7 @@ use std::{collections::BTreeSet, pin::Pin, sync::Arc};
use apb::{Activity, ActivityMut, Base, Object}; use apb::{Activity, ActivityMut, Base, Object};
use leptos::*; use leptos::*;
use leptos_use::{signal_debounced, signal_throttled, use_display_media, use_document_visibility, use_element_size, use_infinite_scroll_with_options, use_scroll, use_scroll_with_options, use_window, use_window_scroll, UseDisplayMediaReturn, UseElementSizeReturn, UseInfiniteScrollOptions, UseScrollOptions, UseScrollReturn}; use leptos_use::{signal_throttled, use_element_size, use_window_scroll, UseElementSizeReturn};
use crate::prelude::*; use crate::prelude::*;
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
@ -36,14 +36,21 @@ impl Timeline {
self.over.set(false); self.over.set(false);
} }
pub async fn more(&self, auth: Auth) -> reqwest::Result<()> { pub fn more(&self, auth: Auth) {
self.loading.set(true); if self.loading.get_untracked() { return }
let res = self.more_inner(auth).await; if self.over.get_untracked() { return }
self.loading.set(false); let _self = *self;
res spawn_local(async move {
_self.loading.set(true);
let res = _self.load_more(auth).await;
_self.loading.set(false);
if let Err(e) = res {
tracing::error!("failed loading posts for timeline: {e}");
}
});
} }
async fn more_inner(&self, auth: Auth) -> reqwest::Result<()> { pub async fn load_more(&self, auth: Auth) -> reqwest::Result<()> {
use apb::{Collection, CollectionPage}; use apb::{Collection, CollectionPage};
let feed_url = self.next.get_untracked(); let feed_url = self.next.get_untracked();
@ -97,7 +104,7 @@ pub fn TimelineRepliesRecursive(tl: Timeline, root: String) -> impl IntoView {
let oid = obj.id().unwrap_or_default().to_string(); let oid = obj.id().unwrap_or_default().to_string();
view! { view! {
<div class="context depth-r"> <div class="context depth-r">
<Item item=obj /> <Item item=obj replies=true />
<div class="depth-r"> <div class="depth-r">
<TimelineRepliesRecursive tl=tl root=oid /> <TimelineRepliesRecursive tl=tl root=oid />
</div> </div>
@ -119,13 +126,7 @@ pub fn TimelineReplies(tl: Timeline, root: String) -> impl IntoView {
<div class="center mt-1 mb-1" class:hidden=tl.over > <div class="center mt-1 mb-1" class:hidden=tl.over >
<button type="button" <button type="button"
prop:disabled=tl.loading prop:disabled=tl.loading
on:click=move |_| { on:click=move |_| tl.more(auth)
spawn_local(async move {
if let Err(e) = tl.more(auth).await {
tracing::error!("error fetching more items for timeline: {e}");
}
})
}
> >
{move || if tl.loading.get() { {move || if tl.loading.get() {
view! { "loading"<span class="dots"></span> }.into_view() view! { "loading"<span class="dots"></span> }.into_view()
@ -150,11 +151,9 @@ pub fn TimelineFeed(tl: Timeline) -> impl IntoView {
let _auto_loader = create_local_resource( let _auto_loader = create_local_resource(
move || (scroll_debounced.get(), height.get()), move || (scroll_debounced.get(), height.get()),
move |(s, h)| async move { move |(s, h)| async move {
if !config.get().infinite_scroll { return } if !config.get_untracked().infinite_scroll { return }
if !tl.loading.get() && h - s < view_height { if h - s < view_height {
if let Err(e) = tl.more(auth).await { tl.more(auth);
tracing::error!("auto load failed: {e}");
}
} }
}, },
); );
@ -179,7 +178,7 @@ pub fn TimelineFeed(tl: Timeline) -> impl IntoView {
<div class="center mt-1 mb-1" class:hidden=tl.over > <div class="center mt-1 mb-1" class:hidden=tl.over >
<button type="button" <button type="button"
prop:disabled=tl.loading prop:disabled=tl.loading
on:click=move |_| load_more(tl, auth) on:click=move |_| tl.more(auth)
> >
{move || if tl.loading.get() { {move || if tl.loading.get() {
view! { "loading"<span class="dots"></span> }.into_view() view! { "loading"<span class="dots"></span> }.into_view()
@ -189,16 +188,6 @@ pub fn TimelineFeed(tl: Timeline) -> impl IntoView {
} }
} }
fn load_more(tl: Timeline, auth: Auth) {
if !tl.loading.get() {
spawn_local(async move {
if let Err(e) = tl.more(auth).await {
tracing::error!("error fetching more items for timeline: {e}");
}
})
}
}
async fn process_activities(activities: Vec<serde_json::Value>, auth: Auth) -> Vec<String> { async fn process_activities(activities: Vec<serde_json::Value>, auth: Auth) -> Vec<String> {
let mut sub_tasks : Vec<Pin<Box<dyn futures::Future<Output = ()>>>> = Vec::new(); let mut sub_tasks : Vec<Pin<Box<dyn futures::Future<Output = ()>>>> = Vec::new();
let mut gonna_fetch = BTreeSet::new(); let mut gonna_fetch = BTreeSet::new();
@ -222,7 +211,7 @@ async fn process_activities(activities: Vec<serde_json::Value>, auth: Auth) -> V
if let Some(object_id) = activity.object().id() { if let Some(object_id) = activity.object().id() {
if !gonna_fetch.contains(&object_id) { if !gonna_fetch.contains(&object_id) {
let fetch_kind = match activity_type { let fetch_kind = match activity_type {
apb::ActivityType::Follow => U::User, apb::ActivityType::Follow => U::Actor,
_ => U::Object, _ => U::Object,
}; };
gonna_fetch.insert(object_id.clone()); gonna_fetch.insert(object_id.clone());
@ -246,20 +235,20 @@ async fn process_activities(activities: Vec<serde_json::Value>, auth: Auth) -> V
if let Some(uid) = activity.attributed_to().id() { if let Some(uid) = activity.attributed_to().id() {
if CACHE.get(&uid).is_none() && !gonna_fetch.contains(&uid) { if CACHE.get(&uid).is_none() && !gonna_fetch.contains(&uid) {
gonna_fetch.insert(uid.clone()); gonna_fetch.insert(uid.clone());
sub_tasks.push(Box::pin(fetch_and_update(U::User, uid, auth))); sub_tasks.push(Box::pin(fetch_and_update(U::Actor, uid, auth)));
} }
} }
if let Some(uid) = activity.actor().id() { if let Some(uid) = activity.actor().id() {
if CACHE.get(&uid).is_none() && !gonna_fetch.contains(&uid) { if CACHE.get(&uid).is_none() && !gonna_fetch.contains(&uid) {
gonna_fetch.insert(uid.clone()); gonna_fetch.insert(uid.clone());
sub_tasks.push(Box::pin(fetch_and_update(U::User, uid, auth))); sub_tasks.push(Box::pin(fetch_and_update(U::Actor, uid, auth)));
} }
} }
} }
for user in actors_seen { for user in actors_seen {
sub_tasks.push(Box::pin(fetch_and_update(U::User, user, auth))); sub_tasks.push(Box::pin(fetch_and_update(U::Actor, user, auth)));
} }
futures::future::join_all(sub_tasks).await; futures::future::join_all(sub_tasks).await;
@ -280,9 +269,9 @@ async fn fetch_and_update_with_user(kind: U, id: String, auth: Auth) {
if let Some(actor_id) = match kind { if let Some(actor_id) = match kind {
U::Object => obj.attributed_to().id(), U::Object => obj.attributed_to().id(),
U::Activity => obj.actor().id(), U::Activity => obj.actor().id(),
U::User | U::Context => None, U::Actor | U::Context => None,
} { } {
fetch_and_update(U::User, actor_id, auth).await; fetch_and_update(U::Actor, actor_id, auth).await;
} }
} }
} }

View file

@ -10,7 +10,7 @@ pub fn ActorStrip(object: crate::Object) -> impl IntoView {
let domain = object.id().unwrap_or_default().replace("https://", "").split('/').next().unwrap_or_default().to_string(); let domain = object.id().unwrap_or_default().replace("https://", "").split('/').next().unwrap_or_default().to_string();
let avatar = object.icon().get().map(|x| x.url().id().unwrap_or(DEFAULT_AVATAR_URL.into())).unwrap_or(DEFAULT_AVATAR_URL.into()); let avatar = object.icon().get().map(|x| x.url().id().unwrap_or(DEFAULT_AVATAR_URL.into())).unwrap_or(DEFAULT_AVATAR_URL.into());
view! { view! {
<a href={Uri::web(U::User, &actor_id)} class="clean hover"> <a href={Uri::web(U::Actor, &actor_id)} class="clean hover">
<img src={avatar} class="avatar inline mr-s" /><b>{username}</b><small>@{domain}</small> <img src={avatar} class="avatar inline mr-s" /><b>{username}</b><small>@{domain}</small>
</a> </a>
} }
@ -20,11 +20,11 @@ pub fn ActorStrip(object: crate::Object) -> impl IntoView {
pub fn ActorBanner(object: crate::Object) -> impl IntoView { pub fn ActorBanner(object: crate::Object) -> impl IntoView {
match object.as_ref() { match object.as_ref() {
serde_json::Value::String(id) => view! { serde_json::Value::String(id) => view! {
<div><b>?</b>" "<a class="clean hover" href={Uri::web(U::User, id)}>{Uri::pretty(id)}</a></div> <div><b>?</b>" "<a class="clean hover" href={Uri::web(U::Actor, id)}>{Uri::pretty(id)}</a></div>
}, },
serde_json::Value::Object(_) => { serde_json::Value::Object(_) => {
let uid = object.id().unwrap_or_default().to_string(); let uid = object.id().unwrap_or_default().to_string();
let uri = Uri::web(U::User, &uid); let uri = Uri::web(U::Actor, &uid);
let avatar_url = object.icon().get().map(|x| x.url().id().unwrap_or(DEFAULT_AVATAR_URL.into())).unwrap_or(DEFAULT_AVATAR_URL.into()); let avatar_url = object.icon().get().map(|x| x.url().id().unwrap_or(DEFAULT_AVATAR_URL.into())).unwrap_or(DEFAULT_AVATAR_URL.into());
let display_name = object.name().unwrap_or_default().to_string(); let display_name = object.name().unwrap_or_default().to_string();
let username = object.preferred_username().unwrap_or_default().to_string(); let username = object.preferred_username().unwrap_or_default().to_string();

View file

@ -10,7 +10,7 @@ pub use auth::Auth;
pub mod prelude; pub mod prelude;
pub const URL_BASE: &str = "https://feditest.alemi.dev"; pub const URL_BASE: &str = "https://upub.alemi.dev";
pub const URL_PREFIX: &str = "/web"; pub const URL_PREFIX: &str = "/web";
pub const URL_SENSITIVE: &str = "https://cdn.alemi.dev/social/nsfw.png"; pub const URL_SENSITIVE: &str = "https://cdn.alemi.dev/social/nsfw.png";
pub const DEFAULT_AVATAR_URL: &str = "https://cdn.alemi.dev/social/gradient.png"; pub const DEFAULT_AVATAR_URL: &str = "https://cdn.alemi.dev/social/gradient.png";
@ -99,7 +99,7 @@ impl WebfingerCache {
Ok(res) => match res.error_for_status() { Ok(res) => match res.error_for_status() {
Ok(res) => match res.json::<jrd::JsonResourceDescriptor>().await { Ok(res) => match res.json::<jrd::JsonResourceDescriptor>().await {
Ok(doc) => { Ok(doc) => {
if let Some(uid) = doc.links.into_iter().find(|x| x.rel == "self").map(|x| x.href).flatten() { if let Some(uid) = doc.links.into_iter().find(|x| x.rel == "self").and_then(|x| x.href) {
self.0.insert(query, LookupStatus::Found(uid)); self.0.insert(query, LookupStatus::Found(uid));
} else { } else {
self.0.insert(query, LookupStatus::NotFound); self.0.insert(query, LookupStatus::NotFound);

View file

@ -84,7 +84,7 @@ pub fn DebugPage() -> impl IntoView {
" raw :: " " raw :: "
<a href={move|| Uri::web(U::Object, &text.get())} >obj</a> <a href={move|| Uri::web(U::Object, &text.get())} >obj</a>
" :: " " :: "
<a href={move|| Uri::web(U::User, &text.get())} >usr</a> <a href={move|| Uri::web(U::Actor, &text.get())} >usr</a>
" :: " " :: "
<a href=move || cached_query().0 target="_blank" rel="nofollow noreferrer">ext</a> <a href=move || cached_query().0 target="_blank" rel="nofollow noreferrer">ext</a>
" :: " " :: "

View file

@ -12,8 +12,13 @@ pub fn ObjectPage(tl: Timeline) -> impl IntoView {
let auth = use_context::<Auth>().expect("missing auth context"); let auth = use_context::<Auth>().expect("missing auth context");
let id = params.get().get("id").cloned().unwrap_or_default(); let id = params.get().get("id").cloned().unwrap_or_default();
let uid = uriproxy::uri(URL_BASE, uriproxy::UriClass::Object, &id); let uid = uriproxy::uri(URL_BASE, uriproxy::UriClass::Object, &id);
let object = create_local_resource(move || params.get().get("id").cloned().unwrap_or_default(), move |oid| { let object = create_local_resource(
async move { move || params.get().get("id").cloned().unwrap_or_default(),
move |oid| async move {
let tl_url = format!("{}/page", Uri::api(U::Context, &oid, false));
if !tl.next.get().starts_with(&tl_url) {
tl.reset(tl_url);
}
match CACHE.get(&Uri::full(U::Object, &oid)) { match CACHE.get(&Uri::full(U::Object, &oid)) {
Some(x) => Some(x.clone()), Some(x) => Some(x.clone()),
None => { None => {
@ -21,9 +26,9 @@ pub fn ObjectPage(tl: Timeline) -> impl IntoView {
let obj = Arc::new(obj); let obj = Arc::new(obj);
if let Some(author) = obj.attributed_to().id() { if let Some(author) = obj.attributed_to().id() {
if let Ok(user) = Http::fetch::<serde_json::Value>( if let Ok(user) = Http::fetch::<serde_json::Value>(
&Uri::api(U::User, &author, true), auth &Uri::api(U::Actor, &author, true), auth
).await { ).await {
CACHE.put(Uri::full(U::User, &author), Arc::new(user)); CACHE.put(Uri::full(U::Actor, &author), Arc::new(user));
} }
} }
CACHE.put(Uri::full(U::Object, &oid), obj.clone()); CACHE.put(Uri::full(U::Object, &oid), obj.clone());
@ -31,7 +36,7 @@ pub fn ObjectPage(tl: Timeline) -> impl IntoView {
} }
} }
} }
}); );
view! { view! {
<div> <div>
<Breadcrumb back=true > <Breadcrumb back=true >
@ -41,11 +46,7 @@ pub fn ObjectPage(tl: Timeline) -> impl IntoView {
class:hidden=move || tl.is_empty() class:hidden=move || tl.is_empty()
on:click=move |_| { on:click=move |_| {
tl.reset(tl.next.get().split('?').next().unwrap_or_default().to_string()); tl.reset(tl.next.get().split('?').next().unwrap_or_default().to_string());
spawn_local(async move { tl.more(auth);
if let Err(e) = tl.more(auth).await {
tracing::error!("error fetching more items for timeline: {e}");
}
})
}><span class="emoji"> }><span class="emoji">
"\u{1f5d8}" "\u{1f5d8}"
</span></a> </span></a>
@ -59,10 +60,6 @@ pub fn ObjectPage(tl: Timeline) -> impl IntoView {
}, },
Some(Some(o)) => { Some(Some(o)) => {
let object = o.clone(); let object = o.clone();
let tl_url = format!("{}/page", Uri::api(U::Context, &o.context().id().unwrap_or_default(), false));
if !tl.next.get().starts_with(&tl_url) {
tl.reset(tl_url);
}
view!{ view!{
<Object object=object /> <Object object=object />
<div class="ml-1 mr-1 mt-2"> <div class="ml-1 mr-1 mt-2">

View file

@ -19,13 +19,13 @@ pub fn RegisterPage() -> impl IntoView {
<form on:submit=move|ev| { <form on:submit=move|ev| {
ev.prevent_default(); ev.prevent_default();
logging::log!("registering new user..."); logging::log!("registering new user...");
let email = username_ref.get().map(|x| x.value()).unwrap_or("".into()); let _email = username_ref.get().map(|x| x.value()).unwrap_or("".into());
let password = password_ref.get().map(|x| x.value()).unwrap_or("".into()); let _password = password_ref.get().map(|x| x.value()).unwrap_or("".into());
spawn_local(async move { spawn_local(async move {
match Http::request::<()>( match Http::request::<()>(
Method::PUT, &format!("{URL_BASE}/auth"), None, auth Method::PUT, &format!("{URL_BASE}/auth"), None, auth
).await { ).await {
Ok(x) => {}, Ok(_x) => {},
Err(e) => set_error.set(Some( Err(e) => set_error.set(Some(
view! { <blockquote>{e.to_string()}</blockquote> } view! { <blockquote>{e.to_string()}</blockquote> }
)), )),

View file

@ -11,7 +11,7 @@ pub fn SearchPage() -> impl IntoView {
let user = create_local_resource( let user = create_local_resource(
move || use_query_map().get().get("q").cloned().unwrap_or_default(), move || use_query_map().get().get("q").cloned().unwrap_or_default(),
move |q| { move |q| {
let user_fetch = Uri::api(U::User, &q, true); let user_fetch = Uri::api(U::Actor, &q, true);
async move { Some(Arc::new(Http::fetch::<serde_json::Value>(&user_fetch, auth).await.ok()?)) } async move { Some(Arc::new(Http::fetch::<serde_json::Value>(&user_fetch, auth).await.ok()?)) }
} }
); );

View file

@ -10,11 +10,7 @@ pub fn TimelinePage(name: &'static str, tl: Timeline) -> impl IntoView {
{name} {name}
<a class="clean ml-1" href="#" on:click=move |_| { <a class="clean ml-1" href="#" on:click=move |_| {
tl.reset(tl.next.get().split('?').next().unwrap_or_default().to_string()); tl.reset(tl.next.get().split('?').next().unwrap_or_default().to_string());
spawn_local(async move { tl.more(auth);
if let Err(e) = tl.more(auth).await {
tracing::error!("error fetching more items for timeline: {e}");
}
})
}><span class="emoji"> }><span class="emoji">
"\u{1f5d8}" "\u{1f5d8}"
</span></a> </span></a>

Some files were not shown because too many files have changed in this diff Show more