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"
cp ./target/release/upub /opt/bin/upub
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"
systemctl --user start upub
echo "rebuilding frontend"
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"
rm /srv/http/feditest/web/*
mv ./dist/* /srv/http/feditest/web/
rm /srv/http/upub/web/*
mv ./dist/* /srv/http/upub/web/
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
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
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] backend config
- [x] frontend config
- [x] optimize `addressing` database schema
- [ ] mentions, notifications
- [ ] hashtags
- [ ] public vs unlisted for discovery
- [ ] mastodon-like search bar
- [ ] polls
- [ ] better editing via web frontend
- [ ] remote media proxy
- [ ] upload media
- [ ] hashtags
- [ ] public vs unlisted for discovery
- [ ] user fields
- [ ] lists
- [ ] full mastodon api
- [ ] optimize `addressing` database schema
- [ ] get rid of internal ids from code
## 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"

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");
}
}
#[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 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};
let domain = ctx.domain();
let db = ctx.db();
let key = Rsa::generate(2048).unwrap();
let test_user = user::Model {
id: format!("{domain}/users/test"),
let test_user = actor::Model {
internal: 42,
id: format!("{domain}/actors/test"),
name: Some("μpub".into()),
domain: clean_domain(domain),
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,
followers: None,
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()),
image: Some("https://cdn.alemi.dev/social/someriver-xs.jpg".to_string()),
inbox: None,
shared_inbox: None,
outbox: None,
actor_type: apb::ActorType::Person,
created: chrono::Utc::now(),
published: chrono::Utc::now(),
updated: chrono::Utc::now(),
private_key: Some(std::str::from_utf8(&key.private_key_to_pem().unwrap()).unwrap().to_string()),
// TODO generate a fresh one every time
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 {
id: Set(test_user.id.clone()),
internal: NotSet,
actor: Set(test_user.id.clone()),
accept_follow_requests: Set(true),
show_followers: 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?;
credential::Entity::insert(credential::ActiveModel {
id: Set(test_user.id.clone()),
email: Set("mail@example.net".to_string()),
internal: NotSet,
actor: Set(test_user.id.clone()),
login: Set("mail@example.net".to_string()),
password: Set(sha256::digest("very-strong-password")),
}).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();
addressing::Entity::insert(addressing::ActiveModel {
actor: Set(apb::target::PUBLIC.to_string()),
server: Set("www.w3.org".to_string()),
activity: Set(Some(format!("{domain}/activities/{aid}"))),
object: Set(Some(format!("{domain}/objects/{oid}"))),
actor: Set(None),
instance: Set(None),
activity: Set(Some(42 + i)),
object: Set(Some(42 + i)),
published: Set(chrono::Utc::now()),
..Default::default()
}).exec(db).await?;
object::Entity::insert(object::ActiveModel {
internal: Set(42 + i),
id: Set(format!("{domain}/objects/{oid}")),
name: Set(None),
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),
context: Set(Some(context.clone())),
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."))),
published: Set(chrono::Utc::now() - std::time::Duration::from_secs(60*i)),
updated: Set(None),
comments: Set(0),
published: Set(chrono::Utc::now() - std::time::Duration::from_secs(60*i as u64)),
updated: Set(chrono::Utc::now()),
replies: Set(0),
likes: Set(0),
shares: Set(0),
announces: Set(0),
to: Set(Audience(vec![apb::target::PUBLIC.to_string()])),
bto: Set(Audience::default()),
cc: Set(Audience(vec![])),
@ -88,12 +92,13 @@ pub async fn faker(ctx: crate::server::Context, count: u64) -> Result<(), sea_or
}).exec(db).await?;
activity::Entity::insert(activity::ActiveModel {
internal: Set(42 + i),
id: Set(format!("{domain}/activities/{aid}")),
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}"))),
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()])),
bto: Set(Audience::default()),
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<()> {
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());
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 {
match obj.base_type() {
Some(apb::BaseType::Object(apb::ObjectType::Actor(_))) => {
crate::model::user::Entity::insert(
crate::model::user::Model::new(obj).unwrap().into_active_model()
crate::model::actor::Entity::insert(
crate::model::actor::ActiveModel::new(&obj).unwrap()
).exec(ctx.db()).await.unwrap();
},
Some(apb::BaseType::Object(apb::ObjectType::Activity(_))) => {
crate::model::activity::Entity::insert(
crate::model::activity::Model::new(obj).unwrap().into_active_model()
).exec(ctx.db()).await.unwrap();
ctx.insert_activity(obj, Some(server)).await.unwrap();
},
Some(apb::BaseType::Object(apb::ObjectType::Note)) => {
crate::model::object::Entity::insert(
crate::model::object::Model::new(obj).unwrap().into_active_model()
).exec(ctx.db()).await.unwrap();
ctx.insert_object(obj, Some(server)).await.unwrap();
},
Some(apb::BaseType::Object(t)) => tracing::warn!("not implemented: {:?}", t),
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(())
}

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?;
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 {
let m = crate::model::object::ActiveModel {
id: sea_orm::Set(k.clone()),
internal: sea_orm::Set(k),
likes: sea_orm::Set(v),
..Default::default()
};
@ -34,16 +34,16 @@ pub async fn fix(ctx: crate::server::Context, likes: bool, shares: bool, replies
tracing::info!("fixing shares...");
let mut store = std::collections::HashMap::new();
{
let mut stream = crate::model::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? {
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 {
let m = crate::model::object::ActiveModel {
id: sea_orm::Set(k.clone()),
shares: sea_orm::Set(v),
internal: sea_orm::Set(k),
announces: sea_orm::Set(v),
..Default::default()
};
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 {
let m = crate::model::object::ActiveModel {
id: sea_orm::Set(k.clone()),
comments: sea_orm::Set(v),
replies: sea_orm::Set(v),
..Default::default()
};
if let Err(e) = crate::model::object::Entity::update(m)

View file

@ -104,7 +104,7 @@ pub async fn run(
).await?;
match command {
CliCommand::Faker { count } =>
Ok(faker(ctx, count).await?),
Ok(faker(ctx, count as i64).await?),
CliCommand::Fetch { uri, save } =>
Ok(fetch(ctx, uri, save).await?),
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<()> {
let aid = ctx.aid(&uuid::Uuid::new_v4().to_string());
let mut activity_model = crate::model::activity::Model {
id: aid.clone(),
activity_type: apb::ActivityType::Follow,
actor: ctx.base().to_string(),
object: Some(actor.clone()),
target: None,
published: chrono::Utc::now(),
to: crate::model::Audience(vec![actor.clone()]),
bto: crate::model::Audience::default(),
cc: crate::model::Audience(vec![apb::target::PUBLIC.to_string()]),
bcc: crate::model::Audience::default(),
let mut activity_model = crate::model::activity::ActiveModel {
internal: NotSet,
id: Set(aid.clone()),
activity_type: Set(apb::ActivityType::Follow),
actor: Set(ctx.base().to_string()),
object: Set(Some(actor.clone())),
target: Set(None),
published: Set(chrono::Utc::now()),
to: Set(crate::model::Audience(vec![actor.clone()])),
bto: Set(crate::model::Audience::default()),
cc: Set(crate::model::Audience(vec![apb::target::PUBLIC.to_string()])),
bcc: Set(crate::model::Audience::default()),
};
if accept {
@ -25,11 +28,11 @@ pub async fn relay(ctx: crate::server::Context, actor: String, accept: bool) ->
.one(ctx.db())
.await?
.expect("no follow request to accept");
activity_model.activity_type = apb::ActivityType::Accept(apb::AcceptType::Accept);
activity_model.object = Some(follow_req.id);
activity_model.activity_type = Set(apb::ActivityType::Accept(apb::AcceptType::Accept));
activity_model.object = Set(Some(follow_req.id));
};
crate::model::activity::Entity::insert(activity_model.into_active_model())
crate::model::activity::Entity::insert(activity_model)
.exec(ctx.db()).await?;
ctx.dispatch(ctx.base(), vec![actor, apb::target::PUBLIC.to_string()], &aid, None).await?;

View file

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

View file

@ -70,6 +70,15 @@ pub struct SecurityConfig {
#[serde_inline_default(true)]
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:?}")]
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
// keep the type hints on the return type, still what the hell!!!!
#[error("redirecting to {0}")]
@ -62,6 +65,10 @@ impl UpubError {
pub fn internal_server_error() -> Self {
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>;
@ -74,6 +81,9 @@ impl From<axum::http::StatusCode> for UpubError {
impl axum::response::IntoResponse for UpubError {
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 {
UpubError::Redirect(to) => Redirect::to(&to).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),
}))
).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,
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 routes;
mod errors;
pub mod errors;
mod config;
#[cfg(feature = "cli")]
@ -51,7 +51,11 @@ struct Args {
#[derive(Clone, Subcommand)]
enum Mode {
/// 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
Config,
@ -88,13 +92,14 @@ async fn main() {
let mut opts = ConnectOptions::new(&database);
opts
.sqlx_logging(true)
.sqlx_logging_level(tracing::log::LevelFilter::Debug)
.max_connections(config.datasource.max_connections)
.min_connections(config.datasource.min_connections)
.acquire_timeout(std::time::Duration::from_secs(config.datasource.acquire_timeout_seconds))
.connect_timeout(std::time::Duration::from_secs(config.datasource.connect_timeout_seconds))
.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)
);
@ -114,7 +119,7 @@ async fn main() {
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)
.await.expect("failed creating server context");
@ -129,7 +134,7 @@ async fn main() {
.with_state(ctx);
// 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");
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 super::m20240524_000002_create_relations_likes_shares::Relations;
#[derive(DeriveMigrationName)]
pub struct Migration;
@ -7,34 +9,23 @@ pub struct Migration;
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::Url).string().null())
.create_index(
Index::create()
.unique()
.name("index-relations-follower-following")
.table(Relations::Table)
.col(Relations::Following)
.col(Relations::Follower)
.to_owned()
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(Objects::Table)
.drop_column(Objects::Url)
.to_owned()
)
.drop_index(Index::drop().name("index-relations-follower-following").table(Relations::Table).to_owned())
.await?;
Ok(())
}
}
#[derive(DeriveIden)]
enum Objects {
Table,
Url,
}

View file

@ -1,21 +1,11 @@
use sea_orm_migration::prelude::*;
mod m20240316_000001_create_table;
mod m20240322_000001_create_relations;
mod m20240322_000002_add_likes_shares;
mod m20240322_000003_add_indexes;
mod m20240323_000001_add_user_configs;
mod m20240323_000002_add_simple_credentials;
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;
mod m20240524_000001_create_actor_activity_object_tables;
mod m20240524_000002_create_relations_likes_shares;
mod m20240524_000003_create_users_auth_and_config;
mod m20240524_000004_create_addressing_deliveries;
mod m20240524_000005_create_attachments_tags_mentions;
mod m20240529_000001_add_relation_unique_index;
pub struct Migrator;
@ -23,22 +13,12 @@ pub struct Migrator;
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![
Box::new(m20240316_000001_create_table::Migration),
Box::new(m20240322_000001_create_relations::Migration),
Box::new(m20240322_000002_add_likes_shares::Migration),
Box::new(m20240322_000003_add_indexes::Migration),
Box::new(m20240323_000001_add_user_configs::Migration),
Box::new(m20240323_000002_add_simple_credentials::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),
Box::new(m20240524_000001_create_actor_activity_object_tables::Migration),
Box::new(m20240524_000002_create_relations_likes_shares::Migration),
Box::new(m20240524_000003_create_users_auth_and_config::Migration),
Box::new(m20240524_000004_create_addressing_deliveries::Migration),
Box::new(m20240524_000005_create_attachments_tags_mentions::Migration),
Box::new(m20240529_000001_add_relation_unique_index::Migration),
]
}
}

View file

@ -1,46 +1,113 @@
use apb::{ActivityMut, BaseMut, ObjectMut};
use sea_orm::entity::prelude::*;
use apb::{ActivityMut, ActivityType, BaseMut, ObjectMut};
use sea_orm::{entity::prelude::*, QuerySelect, SelectColumns};
use crate::routes::activitypub::jsonld::LD;
use super::Audience;
use crate::{model::Audience, errors::UpubError, routes::activitypub::jsonld::LD};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "activities")]
pub struct Model {
#[sea_orm(primary_key)]
pub internal: i64,
#[sea_orm(unique)]
pub id: String,
pub activity_type: apb::ActivityType,
pub activity_type: ActivityType,
pub actor: String,
pub object: Option<String>,
pub target: Option<String>, // TODO relates to USER maybe??
pub cc: Audience,
pub bcc: Audience,
pub target: Option<String>,
pub to: Audience,
pub bto: Audience,
pub cc: Audience,
pub bcc: Audience,
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 {
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 {
serde_json::Value::new_object()
.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 {
fn addressed(&self) -> Vec<String> {
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")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i64,
pub actor: String,
pub server: String,
pub activity: Option<String>,
pub object: Option<String>,
pub internal: i64,
pub actor: Option<i64>,
pub instance: Option<i64>,
pub activity: Option<i64>,
pub object: Option<i64>,
pub published: ChronoDateTimeUtc,
}
#[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::activity::Entity",
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(
belongs_to = "super::object::Entity",
from = "Column::Object",
to = "super::object::Column::Id"
to = "super::object::Column::Internal",
on_update = "Cascade",
on_delete = "NoAction"
)]
Object,
}
impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::User.def()
}
Objects,
}
impl Related<super::activity::Entity> for Entity {
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 {
fn to() -> RelationDef {
Relation::Object.def()
Relation::Objects.def()
}
}
@ -79,12 +97,12 @@ pub enum Event {
impl Event {
pub fn id(&self) -> &str {
pub fn internal(&self) -> i64 {
match self {
Event::Tombstone => "",
Event::Activity(x) => x.id.as_str(),
Event::StrayObject { object, liked: _ } => object.id.as_str(),
Event::DeepActivity { activity: _, liked: _, object } => object.id.as_str(),
Event::Tombstone => 0,
Event::Activity(x) => x.internal,
Event::StrayObject { object, liked: _ } => object.internal,
Event::DeepActivity { activity: _, liked: _, object } => object.internal,
}
}
@ -136,12 +154,12 @@ impl FromQueryResult for Event {
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()
.distinct()
.select_only()
.join(sea_orm::JoinType::LeftJoin, Relation::Object.def())
.join(sea_orm::JoinType::LeftJoin, Relation::Activity.def())
.join(sea_orm::JoinType::LeftJoin, Relation::Objects.def())
.join(sea_orm::JoinType::LeftJoin, Relation::Activities.def())
.filter(
// TODO ghetto double inner join because i want to filter out tombstones
Condition::any()
@ -151,12 +169,11 @@ impl Entity {
.order_by(Column::Published, Order::Desc);
if let Some(uid) = uid {
let uid = uid.to_string();
select = select
.join(
sea_orm::JoinType::LeftJoin,
crate::model::object::Relation::Like.def()
.on_condition(move |_l, _r| crate::model::like::Column::Actor.eq(uid.clone()).into_condition()),
crate::model::object::Relation::Likes.def()
.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()));
}

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 crate::routes::activitypub::jsonld::LD;
@ -9,16 +9,36 @@ use super::addressing::Event;
#[sea_orm(table_name = "attachments")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i64,
pub internal: i64,
#[sea_orm(unique)]
pub url: String,
pub object: String,
pub document_type: apb::DocumentType,
pub object: i64,
pub document_type: DocumentType,
pub name: Option<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 {
pub fn ap(self) -> serde_json::Value {
serde_json::Value::new_object()
@ -26,37 +46,18 @@ impl Model {
.set_document_type(Some(self.document_type))
.set_media_type(Some(&self.media_type))
.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]
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]
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
.iter()
.filter_map(|x| match x {
@ -69,12 +70,11 @@ impl BatchFillable for &[Event] {
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() {
if out.contains_key(&attach.object) {
out.get_mut(&attach.object).expect("contains but get failed?").push(attach);
} else {
out.insert(attach.object.clone(), vec![attach]);
match out.entry(attach.object) {
std::collections::btree_map::Entry::Vacant(a) => { a.insert(vec![attach]); },
std::collections::btree_map::Entry::Occupied(mut e) => { e.get_mut().push(attach); },
}
}
@ -84,14 +84,14 @@ impl BatchFillable for &[Event] {
#[axum::async_trait]
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
}
}
#[axum::async_trait]
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
x.load_attachments_batch(db).await
}

View file

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

View file

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

View file

@ -4,11 +4,11 @@ use sea_orm::entity::prelude::*;
#[sea_orm(table_name = "deliveries")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i64,
pub internal: i64,
pub actor: String,
pub target: String,
pub activity: String,
pub created: ChronoDateTimeUtc,
pub published: ChronoDateTimeUtc,
pub not_before: ChronoDateTimeUtc,
pub attempt: i32,
}
@ -18,14 +18,30 @@ pub enum Relation {
#[sea_orm(
belongs_to = "super::activity::Entity",
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 {
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 {
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::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "shares")]
#[sea_orm(table_name = "hashtags")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i64,
pub actor: String,
pub shares: String,
pub date: ChronoDateTimeUtc,
pub internal: i64,
pub object: i64,
pub name: String,
pub published: ChronoDateTimeUtc,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::object::Entity",
from = "Column::Shares",
to = "super::object::Column::Id",
from = "Column::Object",
to = "super::object::Column::Internal",
on_update = "Cascade",
on_delete = "Cascade"
)]
Object
Objects,
}
impl Related<super::object::Entity> for Entity {
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")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i64,
pub actor: String,
pub likes: String,
pub date: ChronoDateTimeUtc,
pub internal: i64,
pub actor: i64,
pub object: i64,
pub activity: i64,
pub published: ChronoDateTimeUtc,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::object::Entity",
from = "Column::Likes",
to = "super::object::Column::Id",
belongs_to = "super::activity::Entity",
from = "Column::Activity",
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 {
fn to() -> RelationDef {
Relation::Object.def()
Relation::Objects.def()
}
}
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 activity;
pub mod user;
pub mod config;
pub mod relay;
pub mod relation;
pub mod addressing;
pub mod share;
pub mod like;
pub mod config;
pub mod credential;
pub mod session;
pub mod instance;
pub mod delivery;
pub mod relation;
pub mod announce;
pub mod like;
pub mod hashtag;
pub mod mention;
pub mod attachment;
pub mod application;
pub mod addressing;
#[derive(Debug, Clone, thiserror::Error)]
#[error("missing required field: '{0}'")]

View file

@ -1,7 +1,7 @@
use apb::{BaseMut, Collection, CollectionMut, ObjectMut};
use sea_orm::entity::prelude::*;
use apb::{BaseMut, Collection, CollectionMut, ObjectMut, ObjectType};
use sea_orm::{entity::prelude::*, QuerySelect, SelectColumns};
use crate::routes::activitypub::jsonld::LD;
use crate::{errors::UpubError, routes::activitypub::jsonld::LD};
use super::Audience;
@ -9,57 +9,181 @@ use super::Audience;
#[sea_orm(table_name = "objects")]
pub struct Model {
#[sea_orm(primary_key)]
pub internal: i64,
#[sea_orm(unique)]
pub id: String,
pub object_type: apb::ObjectType,
pub object_type: ObjectType,
pub attributed_to: Option<String>,
pub name: Option<String>,
pub summary: Option<String>,
pub content: Option<String>,
pub likes: i64,
pub shares: i64,
pub comments: i64,
pub context: Option<String>,
pub sensitive: bool,
pub in_reply_to: Option<String>,
pub cc: Audience,
pub bcc: Audience,
pub url: Option<String>,
pub likes: i32,
pub announces: i32,
pub replies: i32,
pub context: Option<String>,
pub to: Audience,
pub bto: Audience,
pub url: Option<String>,
pub cc: Audience,
pub bcc: Audience,
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 {
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 {
serde_json::Value::new_object()
.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_in_reply_to(apb::Node::maybe_link(self.in_reply_to.clone()))
.set_published(Some(self.published))
.set_updated(self.updated)
.set_updated(Some(self.updated))
.set_to(apb::Node::links(self.to.0.clone()))
.set_bto(apb::Node::Empty)
.set_cc(apb::Node::links(self.cc.0.clone()))
@ -82,7 +206,7 @@ impl Model {
.set_shares(apb::Node::object(
serde_json::Value::new_object()
.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(
serde_json::Value::new_object()
@ -92,74 +216,11 @@ impl Model {
.set_replies(apb::Node::object(
serde_json::Value::new_object()
.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 {
fn addressed(&self) -> Vec<String> {
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)]
#[sea_orm(table_name = "relations")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i64,
pub follower: String,
pub following: String,
pub internal: i64,
pub follower: i64,
pub following: i64,
pub accept: Option<i64>,
pub activity: i64,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
// TODO how to represent this User-to-User relation in sea orm??
pub enum Relation {
#[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 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")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: String,
pub internal: i64,
pub actor: String,
pub secret: String,
pub expires: ChronoDateTimeUtc,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::user::Entity",
belongs_to = "super::actor::Entity",
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 {
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)?;
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()))
}

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 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};
@ -26,14 +26,14 @@ pub async fn view(
.set_summary(Some(&ctx.cfg().instance.description))
.set_inbox(apb::Node::link(url!(ctx, "/inbox")))
.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_preferred_username(Some(ctx.domain()))
.set_public_key(apb::Node::object(
serde_json::Value::new_object()
.set_id(Some(&url!(ctx, "#main-key")))
.set_owner(Some(&url!(ctx, "")))
.set_public_key_pem(&ctx.app().public_key)
.set_public_key_pem(&ctx.actor().public_key)
))
.ld_context()
).into_response())
@ -50,7 +50,7 @@ pub async fn proxy_get(
AuthIdentity(auth): AuthIdentity,
) -> crate::Result<Json<serde_json::Value>> {
// 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());
}
Ok(Json(
@ -59,7 +59,7 @@ pub async fn proxy_get(
&query.id,
None,
ctx.base(),
&ctx.app().private_key,
ctx.pkey(),
&format!("{}+proxy", ctx.domain()),
)
.await?
@ -74,7 +74,7 @@ pub async fn proxy_form(
Form(query): Form<FetchPath>,
) -> crate::Result<Json<serde_json::Value>> {
// 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());
}
Ok(Json(
@ -83,7 +83,7 @@ pub async fn proxy_form(
&query.id,
None,
ctx.base(),
&ctx.app().private_key,
ctx.pkey(),
&format!("{}+proxy", ctx.domain()),
)
.await?

View file

@ -1,6 +1,6 @@
use axum::{http::StatusCode, extract::State, Json};
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}};
@ -18,6 +18,15 @@ pub struct AuthSuccess {
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(
State(ctx): State<Context>,
Json(login): Json<LoginForm>
@ -25,24 +34,20 @@ pub async fn login(
// TODO salt the pwd
match model::credential::Entity::find()
.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)))
)
.one(ctx.db())
.await?
{
Some(x) => {
// TODO should probably use crypto-safe rng
let token : String = rand::thread_rng()
.sample_iter(&rand::distributions::Alphanumeric)
.take(128)
.map(char::from)
.collect();
let token = token();
let expires = chrono::Utc::now() + std::time::Duration::from_secs(3600 * 6);
model::session::Entity::insert(
model::session::ActiveModel {
id: sea_orm::ActiveValue::Set(token.clone()),
actor: sea_orm::ActiveValue::Set(x.id.clone()),
internal: sea_orm::ActiveValue::NotSet,
secret: sea_orm::ActiveValue::Set(token.clone()),
actor: sea_orm::ActiveValue::Set(x.actor.clone()),
expires: sea_orm::ActiveValue::Set(expires),
}
)
@ -50,13 +55,52 @@ pub async fn login(
.await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(AuthSuccess {
token, expires,
user: x.id
user: x.actor
}))
},
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)]
pub struct RegisterForm {
username: String,

View file

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

View file

@ -20,11 +20,12 @@ pub async fn page(
) -> crate::Result<JsonLD<serde_json::Value>> {
crate::server::builders::paginate(
url!(ctx, "/inbox/page"),
crate::model::addressing::Column::Actor.eq(apb::target::PUBLIC)
crate::model::addressing::Column::Actor.is_null()
.into_condition(),
ctx.db(),
page,
auth.my_id(),
false,
)
.await
}
@ -41,7 +42,7 @@ pub async fn post(
AuthIdentity(auth): AuthIdentity,
Json(activity): Json<serde_json::Value>
) -> crate::Result<()> {
let Identity::Remote(server) = auth else {
let Identity::Remote { domain: server, .. } = auth else {
if activity.activity_type() == Some(ActivityType::Delete) {
// this is spammy af, ignore them!
// 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());
};
// TODO add whitelist of relays
if !server.ends_with(&Context::server(&actor)) {
if server != Context::server(&actor) {
return Err(UpubError::unauthorized());
}

View file

@ -11,7 +11,7 @@ pub mod well_known;
pub mod 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 {
fn ap_routes(self) -> Self;
@ -35,8 +35,9 @@ impl ActivityPubRouter for Router<crate::server::Context> {
.route("/outbox", get(ap::outbox::get))
.route("/outbox/page", get(ap::outbox::page))
// AUTH routes
.route("/auth", post(ap::auth::login))
.route("/auth", put(ap::auth::register))
.route("/auth", post(ap::auth::login))
.route("/auth", patch(ap::auth::refresh))
// .well-known and discovery
.route("/.well-known/webfinger", get(ap::well_known::webfinger))
.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("/nodeinfo/:version", get(ap::well_known::nodeinfo))
// actor routes
.route("/users/:id", get(ap::user::view))
.route("/users/:id/inbox", post(ap::user::inbox::post))
.route("/users/:id/inbox", get(ap::user::inbox::get))
.route("/users/:id/inbox/page", get(ap::user::inbox::page))
.route("/users/:id/outbox", post(ap::user::outbox::post))
.route("/users/:id/outbox", get(ap::user::outbox::get))
.route("/users/:id/outbox/page", get(ap::user::outbox::page))
.route("/users/:id/followers", get(ap::user::following::get::<false>))
.route("/users/:id/followers/page", get(ap::user::following::page::<false>))
.route("/users/:id/following", get(ap::user::following::get::<true>))
.route("/users/:id/following/page", get(ap::user::following::page::<true>))
.route("/actors/:id", get(ap::user::view))
.route("/actors/:id/inbox", post(ap::user::inbox::post))
.route("/actors/:id/inbox", get(ap::user::inbox::get))
.route("/actors/:id/inbox/page", get(ap::user::inbox::page))
.route("/actors/:id/outbox", post(ap::user::outbox::post))
.route("/actors/:id/outbox", get(ap::user::outbox::get))
.route("/actors/:id/outbox/page", get(ap::user::outbox::page))
.route("/actors/:id/followers", get(ap::user::following::get::<false>))
.route("/actors/:id/followers/page", get(ap::user::following::page::<false>))
.route("/actors/:id/following", get(ap::user::following::get::<true>))
.route("/actors/:id/following/page", get(ap::user::following::page::<true>))
// activities
.route("/activities/:id", get(ap::activity::view))
// context

View file

@ -2,7 +2,7 @@ pub mod replies;
use apb::{CollectionMut, ObjectMut};
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}};
@ -62,7 +62,7 @@ pub async fn view(
// .set_id(Some(&crate::url!(ctx, "/objects/{id}/replies")))
// .set_first(apb::Node::link(crate::url!(ctx, "/objects/{id}/replies/page")))
.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))
);
}

View file

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

View file

@ -1,4 +1,5 @@
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};
@ -13,10 +14,13 @@ pub async fn page(
) -> crate::Result<JsonLD<serde_json::Value>> {
crate::server::builders::paginate(
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(),
page,
auth.my_id(),
true,
)
.await
}

View file

@ -17,7 +17,7 @@ pub async fn get<const OUTGOING: bool>(
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>(
@ -40,7 +40,7 @@ pub async fn page<const OUTGOING: bool>(
.await?;
crate::server::builders::collection_page(
&url!(ctx, "/users/{id}/{follow___}/page"),
&url!(ctx, "/actors/{id}/{follow___}/page"),
offset, limit,
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>> {
match auth {
Identity::Anonymous => Err(StatusCode::FORBIDDEN.into()),
Identity::Remote(_) => Err(StatusCode::FORBIDDEN.into()),
Identity::Local(user) => if ctx.uid(&id) == user {
crate::server::builders::collection(&url!(ctx, "/users/{id}/inbox"), None)
Identity::Remote { .. } => Err(StatusCode::FORBIDDEN.into()),
Identity::Local { id: user, .. } => if ctx.uid(&id) == user {
crate::server::builders::collection(&url!(ctx, "/actors/{id}/inbox"), None)
} else {
Err(StatusCode::FORBIDDEN.into())
},
@ -25,7 +25,7 @@ pub async fn page(
AuthIdentity(auth): AuthIdentity,
Query(page): Query<Pagination>,
) -> 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
return Err(UpubError::forbidden());
};
@ -34,14 +34,15 @@ pub async fn page(
}
crate::server::builders::paginate(
url!(ctx, "/users/{id}/inbox/page"),
url!(ctx, "/actors/{id}/inbox/page"),
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::activity::Column::Actor.eq(uid)),
ctx.db(),
page,
auth.my_id(),
false,
)
.await
}

View file

@ -5,10 +5,9 @@ pub mod outbox;
pub mod following;
use axum::extract::{Path, Query, State};
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QuerySelect, SelectColumns};
use apb::{ActorMut, EndpointsMut, Node};
use crate::{errors::UpubError, model::{self, user}, server::{auth::AuthIdentity, fetcher::Fetcher, Context}, url};
use apb::{ActorMut, EndpointsMut, Node, ObjectMut};
use crate::{errors::UpubError, model, server::{auth::AuthIdentity, builders::AnyQuery, fetcher::Fetcher, Context}, url};
use super::{jsonld::LD, JsonLD, TryFetch};
@ -30,48 +29,31 @@ pub async fn view(
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() {
None => (None, None),
Some(my_id) => {
// 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
let followed_by_me = model::relation::Entity::find()
.filter(model::relation::Column::Follower.eq(my_id))
.filter(model::relation::Column::Following.eq(&uid))
.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)
let followed_by_me = model::relation::Entity::is_following(my_id, internal_uid).any(ctx.db()).await?;
let following_me = model::relation::Entity::is_following(internal_uid, my_id).any(ctx.db()).await?;
(Some(followed_by_me), Some(following_me))
},
};
match user::Entity::find_by_id(&uid)
match model::actor::Entity::find_by_ap_id(&uid)
.find_also_related(model::config::Entity)
.one(ctx.db()).await?
{
// local user
Some((user_model, Some(cfg))) => {
let mut user = user_model.ap()
.set_inbox(Node::link(url!(ctx, "/users/{id}/inbox")))
.set_outbox(Node::link(url!(ctx, "/users/{id}/outbox")))
.set_following(Node::link(url!(ctx, "/users/{id}/following")))
.set_followers(Node::link(url!(ctx, "/users/{id}/followers")))
.set_inbox(Node::link(url!(ctx, "/actors/{id}/inbox")))
.set_outbox(Node::link(url!(ctx, "/actors/{id}/outbox")))
.set_following(Node::link(url!(ctx, "/actors/{id}/following")))
.set_followers(Node::link(url!(ctx, "/actors/{id}/followers")))
.set_following_me(following_me)
.set_followed_by_me(followed_by_me)
.set_endpoints(Node::object(
@ -88,6 +70,10 @@ pub async fn view(
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()))
},
// remote user

View file

@ -8,7 +8,7 @@ pub async fn get(
State(ctx): State<Context>,
Path(id): Path<String>,
) -> 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(
@ -19,7 +19,7 @@ pub async fn page(
) -> crate::Result<JsonLD<serde_json::Value>> {
let uid = ctx.uid(&id);
crate::server::builders::paginate(
url!(ctx, "/users/{id}/outbox/page"),
url!(ctx, "/actors/{id}/outbox/page"),
Condition::all()
.add(auth.filter_condition())
.add(
@ -30,6 +30,7 @@ pub async fn page(
ctx.db(),
page,
auth.my_id(),
false,
)
.await
}
@ -42,8 +43,8 @@ pub async fn post(
) -> Result<CreationResult, UpubError> {
match auth {
Identity::Anonymous => Err(StatusCode::UNAUTHORIZED.into()),
Identity::Remote(_) => Err(StatusCode::NOT_IMPLEMENTED.into()),
Identity::Local(uid) => if ctx.uid(&id) == uid {
Identity::Remote { .. } => Err(StatusCode::NOT_IMPLEMENTED.into()),
Identity::Local { id: uid, .. } => if ctx.uid(&id) == uid {
tracing::debug!("processing new local activity: {}", serde_json::to_string(&activity).unwrap_or_default());
match activity.base_type() {
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> {
// 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
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_comments = None;
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:", "")
.split_once('@')
{
if domain == ctx.domain() {
if user == ctx.domain() {
// we fetch with our domain as user, they are checking us back, this is a special edge case
Ok(JsonRD(JsonResourceDescriptor {
subject: format!("acct:{user}@{domain}"),
aliases: vec![ctx.base().to_string()],
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)
let usr = model::actor::Entity::find()
.filter(model::actor::Column::PreferredUsername.eq(user))
.filter(model::actor::Column::Domain.eq(domain))
.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 {
// remote user
let usr = model::user::Entity::find()
.filter(model::user::Column::PreferredUsername.eq(user))
.filter(model::user::Column::Domain.eq(domain))
.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(),
},
],
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()),
Some(chrono::Utc::now())
};
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(),
},
],
properties: jrd::Map::default(),
expires,
}))
}
} else {
Err(StatusCode::UNPROCESSABLE_ENTITY.into())
}

View file

@ -1,6 +1,5 @@
use axum::{extract::{Path, State}, http::StatusCode, Json};
use mastodon_async_entities::account::{Account, AccountId};
use sea_orm::EntityTrait;
use crate::{model, server::{auth::AuthIdentity, Context}};
@ -9,7 +8,7 @@ pub async fn view(
AuthIdentity(_auth): AuthIdentity,
Path(id): Path<String>
) -> 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)
.one(ctx.db())
.await
@ -21,7 +20,7 @@ pub async fn view(
acct: x.preferred_username.clone(),
avatar: x.icon.as_deref().unwrap_or("").to_string(),
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(),
// TODO hide these maybe
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]
pub trait Administrable {
@ -28,52 +28,56 @@ impl Administrable for super::Context {
let ap_id = self.uid(&username);
let db = self.db();
let domain = self.domain().to_string();
let user_model = crate::model::user::Model {
id: ap_id.clone(),
name: display_name,
domain, summary,
preferred_username: username.clone(),
following: None,
following_count: 0,
followers: None,
followers_count: 0,
statuses_count: 0,
icon: avatar_url,
image: banner_url,
inbox: None,
shared_inbox: None,
outbox: None,
actor_type: apb::ActorType::Person,
created: chrono::Utc::now(),
updated: chrono::Utc::now(),
private_key: Some(std::str::from_utf8(&key.private_key_to_pem().unwrap()).unwrap().to_string()),
public_key: std::str::from_utf8(&key.public_key_to_pem().unwrap()).unwrap().to_string(),
let user_model = crate::model::actor::ActiveModel {
internal: NotSet,
id: Set(ap_id.clone()),
name: Set(display_name),
domain: Set(domain),
summary: Set(summary),
preferred_username: Set(username.clone()),
following: Set(None),
following_count: Set(0),
followers: Set(None),
followers_count: Set(0),
statuses_count: Set(0),
icon: Set(avatar_url),
image: Set(banner_url),
inbox: Set(None),
shared_inbox: Set(None),
outbox: Set(None),
actor_type: Set(apb::ActorType::Person),
published: Set(chrono::Utc::now()),
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)
.await?;
let config_model = crate::model::config::Model {
id: ap_id.clone(),
accept_follow_requests: true,
show_followers_count: true,
show_following_count: true,
show_followers: false,
show_following: false,
let config_model = crate::model::config::ActiveModel {
internal: NotSet,
actor: Set(ap_id.clone()),
accept_follow_requests: Set(true),
show_followers_count: Set(true),
show_following_count: Set(true),
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)
.await?;
let credentials_model = crate::model::credential::Model {
id: ap_id,
email: username,
password,
let credentials_model = crate::model::credential::ActiveModel {
internal: NotSet,
actor: Set(ap_id),
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)
.await?;

View file

@ -9,63 +9,58 @@ use super::{fetcher::Fetcher, httpsign::HttpSignature};
#[derive(Debug, Clone)]
pub enum Identity {
Anonymous,
Local(String),
Remote(String),
Remote {
domain: String,
internal: i64,
},
Local {
id: String,
internal: i64,
},
}
impl Identity {
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 {
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??
Identity::Local(uid) => base_cond
.add(model::addressing::Column::Actor.eq(uid))
.add(model::activity::Column::Actor.eq(uid))
.add(model::object::Column::AttributedTo.eq(uid)),
Identity::Local { id, internal } => base_cond
.add(model::addressing::Column::Actor.eq(*internal))
.add(model::activity::Column::Actor.eq(id))
.add(model::object::Column::AttributedTo.eq(id)),
}
}
pub fn my_id(&self) -> Option<&str> {
pub fn my_id(&self) -> Option<i64> {
match self {
Identity::Local(x) => Some(x.as_str()),
Identity::Local { internal, .. } => Some(*internal),
_ => None,
}
}
pub fn is(&self, id: &str) -> bool {
pub fn is(&self, uid: &str) -> bool {
match self {
Identity::Anonymous => false,
Identity::Remote(_) => false, // TODO per-actor server auth should check this
Identity::Local(uid) => uid.as_str() == id
Identity::Remote { .. } => false, // TODO per-actor server auth should check this
Identity::Local { id, .. } => id.as_str() == uid
}
}
#[allow(unused)]
pub fn is_anon(&self) -> bool {
matches!(self, Self::Anonymous)
}
#[allow(unused)]
pub fn is_local(&self) -> bool {
matches!(self, Self::Local(_))
matches!(self, Self::Local { .. })
}
#[allow(unused)]
pub fn is_remote(&self) -> bool {
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,
}
matches!(self, Self::Remote { .. })
}
}
@ -90,13 +85,19 @@ where
.unwrap_or("");
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()))
.one(ctx.db())
.await
{
Ok(Some(x)) => identity = Identity::Local(x.actor),
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) => {
tracing::error!("failed querying user session: {e}");
return Err(UpubError::internal_server_error())
@ -122,7 +123,13 @@ where
.build_from_parts(parts)
.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:?}"),
Err(e) => tracing::error!("error verifying signature: {e}"),
},

View file

@ -1,5 +1,5 @@
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}};
@ -8,12 +8,20 @@ pub async fn paginate(
filter: Condition,
db: &DatabaseConnection,
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>> {
let limit = page.batch.unwrap_or(20).min(50);
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)
// TODO also limit to only local activities
.limit(limit)
@ -27,7 +35,7 @@ pub async fn paginate(
let items : Vec<serde_json::Value> = items
.into_iter()
.map(|item| {
let attach = attachments.remove(item.id());
let attach = attachments.remove(&item.internal());
item.ap(attach)
})
.collect();
@ -63,3 +71,22 @@ pub fn collection(id: &str, total_items: Option<u64>) -> crate::Result<JsonLD<se
.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 openssl::rsa::Rsa;
use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, QuerySelect, SelectColumns, Set};
use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, QuerySelect, SelectColumns};
use crate::{config::Config, model, server::fetcher::Fetcher};
use crate::{config::Config, errors::UpubError, model};
use uriproxy::UriClass;
use super::dispatcher::Dispatcher;
use super::{builders::AnyQuery, dispatcher::Dispatcher};
#[derive(Clone)]
@ -19,8 +18,16 @@ struct ContextInner {
base_url: String,
dispatcher: Dispatcher,
// TODO keep these pre-parsed
app: model::application::Model,
relays: BTreeSet<String>,
actor: model::actor::Model,
instance: model::instance::Model,
pkey: String,
#[allow(unused)] relay: Relays,
}
#[allow(unused)]
pub struct Relays {
sources: BTreeSet<String>,
sinks: BTreeSet<String>,
}
#[macro_export]
@ -46,42 +53,37 @@ impl Context {
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!!
}
let app = match model::application::Entity::find().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::application::ActiveModel {
id: sea_orm::ActiveValue::NotSet,
private_key: sea_orm::ActiveValue::Set(privk.clone()),
public_key: sea_orm::ActiveValue::Set(pubk.clone()),
created: sea_orm::ActiveValue::Set(chrono::Utc::now()),
};
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 base_url = format!("{}{}", protocol, domain);
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?;
let (actor, instance) = super::init::application(domain.clone(), base_url.clone(), &db).await?;
// TODO maybe we could provide a more descriptive error...
let pkey = actor.private_key.as_deref().ok_or_else(UpubError::internal_server_error)?.to_string();
let relay_sinks = model::relation::Entity::followers(&actor.id, &db).await?;
let relay_sources = model::relation::Entity::following(&actor.id, &db).await?;
let relay = Relays {
sources: BTreeSet::from_iter(relay_sources),
sinks: BTreeSet::from_iter(relay_sinks),
};
Ok(Context(Arc::new(ContextInner {
base_url: format!("{}{}", protocol, domain),
db, domain, protocol, app, dispatcher, config,
relays: BTreeSet::from_iter(relays.into_iter()),
base_url, db, domain, protocol, actor, instance, dispatcher, config, pkey, relay,
})))
}
pub fn app(&self) -> &model::application::Model {
&self.0.app
pub fn actor(&self) -> &model::actor::Model {
&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 {
@ -104,9 +106,13 @@ impl Context {
&self.0.base_url
}
pub fn dispatcher(&self) -> &Dispatcher {
&self.0.dispatcher
}
/// get full user id uri
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
@ -119,14 +125,6 @@ impl Context {
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
pub fn id(&self, full_id: &str) -> String {
if self.is_local(full_id) {
@ -150,100 +148,39 @@ impl Context {
id.starts_with(self.base())
}
pub 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", "");
model::relation::Entity::find()
.filter(model::relation::Column::Following.eq(target_id))
pub async fn is_local_internal_object(&self, internal: i64) -> crate::Result<bool> {
model::object::Entity::find()
.filter(model::object::Column::Internal.eq(internal))
.select_only()
.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)
.select_column(model::object::Column::Internal)
.into_tuple::<i64>()
.any(self.db())
.await
}
pub async fn address_to(&self, aid: Option<&str>, oid: Option<&str>, targets: &[String]) -> crate::Result<()> {
let local_activity = aid.map(|x| self.is_local(x)).unwrap_or(false);
let local_object = oid.map(|x| self.is_local(x)).unwrap_or(false);
let addressings : Vec<model::addressing::ActiveModel> = 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))
.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?;
pub async fn is_local_internal_activity(&self, internal: i64) -> crate::Result<bool> {
model::activity::Entity::find()
.filter(model::activity::Column::Internal.eq(internal))
.select_only()
.select_column(model::activity::Column::Internal)
.into_tuple::<i64>()
.any(self.db())
.await
}
Ok(())
}
pub 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::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 async fn is_local_internal_actor(&self, internal: i64) -> crate::Result<bool> {
model::actor::Entity::find()
.filter(model::actor::Column::Internal.eq(internal))
.select_only()
.select_column(model::actor::Column::Internal)
.into_tuple::<i64>()
.any(self.db())
.await
}
#[allow(unused)]
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 {
id: sea_orm::ActiveValue::Set(delivery.id),
internal: sea_orm::ActiveValue::Set(delivery.internal),
..Default::default()
};
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);
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)
.one(db)
.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(model::application::Model { private_key: key, .. }) = model::application::Entity::find()
.one(db).await?
let Some(actor) = model::actor::Entity::find_by_ap_id(&delivery.actor)
.one(db)
.await?
else {
tracing::error!("no private key configured for application");
tracing::error!("abandoning delivery of {} from non existant actor: {}", delivery.activity, delivery.actor);
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(
Method::POST, &delivery.target,
@ -125,12 +120,12 @@ async fn worker(db: &DatabaseConnection, domain: &str, poll_interval: u64, waker
).await {
tracing::warn!("failed delivery of {} to {} : {e}", delivery.activity, delivery.target);
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()),
actor: sea_orm::ActiveValue::Set(delivery.actor),
target: sea_orm::ActiveValue::Set(delivery.target),
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),
};
model::delivery::Entity::insert(new_delivery).exec(db).await?;

View file

@ -1,26 +1,69 @@
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 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 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]
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 fetch_user(&self, id: &str) -> crate::Result<model::user::Model>;
async fn pull_user(&self, id: &str) -> crate::Result<model::user::Model>;
async fn fetch_domain(&self, domain: &str) -> crate::Result<model::instance::Model>;
async fn fetch_object(&self, id: &str) -> crate::Result<model::object::Model>;
async fn pull_object(&self, id: &str) -> crate::Result<model::object::Model>;
async fn fetch_user(&self, id: &str) -> crate::Result<model::actor::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 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<()>;
@ -83,6 +126,36 @@ pub trait Fetcher {
#[axum::async_trait]
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> {
let subject = format!("acct:{user}@{host}");
let webfinger_uri = format!("https://{host}/.well-known/webfinger?resource={subject}");
@ -114,82 +187,125 @@ impl Fetcher for Context {
Err(UpubError::not_found())
}
async fn fetch_user(&self, id: &str) -> crate::Result<model::user::Model> {
if let Some(x) = model::user::Entity::find_by_id(id).one(self.db()).await? {
async fn fetch_domain(&self, domain: &str) -> crate::Result<model::instance::Model> {
if let Some(x) = model::instance::Entity::find_by_domain(domain).one(self.db()).await? {
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 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::user::Entity::insert(user_model.clone().into_active_model())
.exec(self.db()).await?;
Ok(user_model)
if let Ok(res) = Self::request(
Method::GET, &format!("https://{domain}"), None, &format!("https://{}", self.domain()), self.pkey(), self.domain(),
).await {
if let Ok(actor) = res.json::<serde_json::Value>().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);
}
}
}
async fn pull_user(&self, id: &str) -> crate::Result<model::user::Model> {
let user = Self::request(
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)?;
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;
}
// TODO try fetching these numbers from audience/generator fields to avoid making 2 more GETs
if let Some(followers_url) = &user_model.followers {
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 resolve_user(&self, mut document: serde_json::Value) -> crate::Result<model::actor::Model> {
let id = document.id().ok_or_else(|| UpubError::field("id"))?.to_string();
// TODO try fetching these numbers from audience/generator fields to avoid making 2 more GETs every time
if let Some(followers_url) = &document.followers().id() {
let req = Self::request(
Method::GET, followers_url, None,
&format!("https://{}", self.domain()), &self.app().private_key, self.domain(),
&format!("https://{}", self.domain()), self.pkey(), self.domain(),
).await;
if let Ok(res) = req {
if let Ok(user_followers) = res.json::<serde_json::Value>().await {
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(
Method::GET, following_url, None,
&format!("https://{}", self.domain()), &self.app().private_key, self.domain(),
&format!("https://{}", self.domain()), self.pkey(), self.domain(),
).await;
if let Ok(res) = req {
if let Ok(user_following) = res.json::<serde_json::Value>().await {
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> {
if let Some(x) = model::activity::Entity::find_by_id(id).one(self.db()).await? {
async fn fetch_user(&self, id: &str) -> crate::Result<model::actor::Model> {
if let Some(x) = model::actor::Entity::find_by_ap_id(id).one(self.db()).await? {
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())
.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)
self.resolve_user(document).await
}
async fn pull_activity(&self, id: &str) -> crate::Result<model::activity::Model> {
let activity = Self::request(
Method::GET, id, None, &format!("https://{}", self.domain()), &self.app().private_key, self.domain(),
).await?.json::<serde_json::Value>().await?;
async fn fetch_activity(&self, id: &str) -> crate::Result<model::activity::Model> {
if let Some(x) = model::activity::Entity::find_by_ap_id(id).one(self.db()).await? {
return Ok(x); // already in db, easy
}
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 Err(e) = self.fetch_user(&activity_actor).await {
@ -203,101 +319,43 @@ 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)
}
async fn fetch_thread(&self, id: &str) -> crate::Result<()> {
crawl_replies(self, id, 0).await
async fn fetch_thread(&self, _id: &str) -> crate::Result<()> {
// crawl_replies(self, id, 0).await
todo!()
}
async fn fetch_object(&self, id: &str) -> crate::Result<model::object::Model> {
fetch_object_inner(self, id, 0).await
}
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();
}
Ok(())
}
#[async_recursion::async_recursion]
async fn fetch_object_inner(ctx: &Context, id: &str, depth: usize) -> crate::Result<model::object::Model> {
if let Some(x) = model::object::Entity::find_by_id(id).one(ctx.db()).await? {
async fn fetch_object_r(&self, id: &str, depth: u32) -> crate::Result<model::object::Model> {
if let Some(x) = model::object::Entity::find_by_ap_id(id).one(self.db()).await? {
return Ok(x); // already in db, easy
}
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 = self.pull(id).await?.object()?;
self.resolve_object_r(object, depth).await
}
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();
if let Some(oid) = object.id() {
if oid != id {
if let Some(x) = model::object::Entity::find_by_id(oid).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, but with id different that given url
}
}
}
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}");
}
}
@ -305,19 +363,20 @@ async fn fetch_object_inner(ctx: &Context, id: &str, depth: usize) -> crate::Res
let addressed = object.addressed();
if let Some(reply) = object.in_reply_to().id() {
if depth <= 16 {
fetch_object_inner(ctx, &reply, depth + 1).await?;
if depth <= self.cfg().security.thread_crawl_depth {
self.fetch_object_r(&reply, depth + 1).await?;
} 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 = ctx.insert_object(object, None).await?;
let object_model = self.insert_object(object, None).await?;
let expanded_addresses = ctx.expand_addressing(addressed).await?;
ctx.address_to(None, Some(&object_model.id), &expanded_addresses).await?;
let expanded_addresses = self.expand_addressing(addressed).await?;
self.address_to(None, Some(object_model.internal), &expanded_addresses).await?;
Ok(object_model)
}
}
#[axum::async_trait]
@ -329,9 +388,7 @@ pub trait Fetchable : Sync + Send {
impl Fetchable for apb::Node<serde_json::Value> {
async fn fetch(&mut self, ctx: &crate::server::Context) -> crate::Result<&mut Self> {
if let apb::Node::Link(uri) = self {
let from = format!("{}{}", ctx.protocol(), ctx.domain()); // TODO helper to avoid this?
let pkey = &ctx.app().private_key;
*self = Context::request(Method::GET, uri.href(), None, &from, pkey, ctx.domain())
*self = Context::request(Method::GET, uri.href(), None, ctx.base(), ctx.pkey(), ctx.domain())
.await?
.json::<serde_json::Value>()
.await?
@ -341,3 +398,56 @@ impl Fetchable for apb::Node<serde_json::Value> {
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 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]
@ -13,48 +13,42 @@ impl apb::server::Inbox for Context {
type Activity = serde_json::Value;
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 {
// 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());
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 expanded_addressing = self.expand_addressing(activity.addressed()).await?;
self.address_to(Some(&aid), Some(&object_model.id), &expanded_addressing).await?;
tracing::info!("{} posted {}", aid, object_model.id);
let expanded_addressing = self.expand_addressing(activity_model.addressed()).await?;
self.address_to(Some(activity_model.internal), Some(object_model.internal), &expanded_addressing).await?;
tracing::info!("{} posted {}", activity_model.actor, object_model.id);
Ok(())
}
async fn like(&self, _: String, activity: serde_json::Value) -> crate::Result<()> {
let aid = activity.id().ok_or(UpubError::bad_request())?;
async fn like(&self, server: String, activity: serde_json::Value) -> crate::Result<()> {
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 obj = self.fetch_object(&object_uri).await?;
let oid = obj.id;
let like = model::like::ActiveModel {
id: sea_orm::ActiveValue::NotSet,
actor: sea_orm::Set(uid.clone()),
likes: sea_orm::Set(oid.clone()),
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())
if model::like::Entity::find_by_uid_oid(internal_uid, obj.internal)
.any(self.db())
.await?
{
return Err(UpubError::not_modified());
}
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?;
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(&oid)
model::object::Entity::find_by_id(obj.internal)
.select_only()
.select_column(model::object::Column::AttributedTo)
.into_tuple::<String>()
@ -63,200 +57,189 @@ impl apb::server::Inbox for Context {
.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);
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<()> {
let activity_model = model::activity::Model::new(&activity)?;
let aid = activity_model.id.clone();
let target_user_uri = activity_model.object
.as_deref()
.ok_or_else(UpubError::bad_request)?
.to_string();
let usr = self.fetch_user(&target_user_uri).await?;
let target_user_id = usr.id;
tracing::info!("{} wants to follow {}", activity_model.actor, target_user_id);
model::activity::Entity::insert(activity_model.into_active_model())
let aid = activity.id().ok_or_else(UpubError::bad_request)?.to_string();
let source_actor = activity.actor().id().ok_or_else(UpubError::bad_request)?;
let source_actor_internal = model::actor::Entity::ap_to_internal(&source_actor, self.db()).await?;
let target_actor = activity.object().id().ok_or_else(UpubError::bad_request)?;
let usr = self.fetch_user(&target_actor).await?;
let activity_model = model::activity::ActiveModel::new(&activity)?;
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,
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?;
let mut expanded_addressing = self.expand_addressing(activity.addressed()).await?;
if !expanded_addressing.contains(&target_user_id) {
expanded_addressing.push(target_user_id);
if !expanded_addressing.contains(&target_actor) {
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(())
}
async fn accept(&self, _: String, activity: serde_json::Value) -> crate::Result<()> {
// TODO what about TentativeAccept
let activity_model = model::activity::Model::new(&activity)?;
if let Some(mut r) = model::relay::Entity::find_by_id(&activity_model.actor)
let aid = activity.id().ok_or_else(UpubError::bad_request)?.to_string();
let target_actor = activity.actor().id().ok_or_else(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)
.one(self.db())
.await?
{
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(());
}
.ok_or_else(UpubError::not_found)?;
let Some(follow_request_id) = &activity_model.object else {
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 {
if follow_activity.object.unwrap_or("".into()) != target_actor {
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())
.exec(self.db())
.await?;
model::user::Entity::update_many()
model::actor::Entity::update_many()
.col_expr(
model::user::Column::FollowingCount,
Expr::col(model::user::Column::FollowingCount).add(1)
model::actor::Column::FollowingCount,
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())
.await?;
model::relation::Entity::insert(
model::relation::ActiveModel {
follower: Set(follow_activity.actor.clone()),
following: Set(activity_model.actor),
..Default::default()
}
).exec(self.db()).await?;
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(&follow_activity.actor))
.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?;
if !expanded_addressing.contains(&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(())
}
async fn reject(&self, _: String, activity: serde_json::Value) -> crate::Result<()> {
// TODO what about TentativeReject?
let activity_model = model::activity::Model::new(&activity)?;
let Some(follow_request_id) = &activity_model.object else {
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 {
let aid = activity.id().ok_or_else(UpubError::bad_request)?.to_string();
let uid = activity.actor().id().ok_or_else(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)
.one(self.db())
.await?
.ok_or_else(UpubError::not_found)?;
if follow_activity.object.unwrap_or("".into()) != uid {
return Err(UpubError::forbidden());
}
tracing::info!("{} rejected follow request by {}", activity_model.actor, follow_activity.actor);
model::activity::Entity::insert(activity_model.clone().into_active_model())
let activity_model = model::activity::ActiveModel::new(&activity)?;
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?;
tracing::info!("{} rejected follow request by {}", uid, follow_activity.actor);
let mut expanded_addressing = self.expand_addressing(activity.addressed()).await?;
if !expanded_addressing.contains(&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(())
}
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(UpubError::bad_request())?;
tracing::debug!("deleting '{oid}'"); // this is so spammy wtf!
// TODO maybe we should keep the tombstone?
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");
let oid = activity.object().id().ok_or_else(UpubError::bad_request)?;
model::actor::Entity::delete_by_ap_id(&oid).exec(self.db()).await.info_failed("failed deleting from users");
model::object::Entity::delete_by_ap_id(&oid).exec(self.db()).await.info_failed("failed deleting from objects");
tracing::debug!("deleted '{oid}'");
Ok(())
}
async fn update(&self, server: String, activity: serde_json::Value) -> crate::Result<()> {
let activity_model = model::activity::Model::new(&activity)?;
let aid = activity_model.id.clone();
async fn update(&self, _server: String, activity: serde_json::Value) -> crate::Result<()> {
let uid = activity.actor().id().ok_or_else(UpubError::bad_request)?;
let aid = activity.id().ok_or_else(UpubError::bad_request)?;
let Some(object_node) = activity.object().extract() else {
// 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());
return Err(UpubError::unprocessable());
};
let Some(oid) = object_node.id().map(|x| x.to_string()) else {
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"),
}
let oid = object_node.id().ok_or_else(UpubError::bad_request)?.to_string();
tracing::info!("{} updated {}", aid, oid);
model::activity::Entity::insert(activity_model.into_active_model())
let activity_model = model::activity::ActiveModel::new(&activity)?;
model::activity::Entity::insert(activity_model)
.exec(self.db())
.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?;
self.address_to(Some(&aid), Some(&oid), &expanded_addressing).await?;
self.address_to(Some(internal_aid), internal_oid, &expanded_addressing).await?;
Ok(())
}
async fn undo(&self, server: String, activity: serde_json::Value) -> crate::Result<()> {
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
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)?;
// can't undo activities from remote actors!
@ -264,83 +247,70 @@ impl apb::server::Inbox for Context {
return Err(UpubError::forbidden());
};
let obj = self.fetch_object(&undone_object_uri).await?;
let undone_object_id = obj.id;
let activity_model = self.insert_activity(activity.clone(), Some(server)).await?;
match activity_type {
apb::ActivityType::Like => {
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());
},
}
let targets = self.expand_addressing(activity.addressed()).await?;
self.process_undo(internal_uid, activity).await?;
model::activity::Entity::delete_by_id(undone_aid).exec(self.db()).await?;
self.address_to(Some(activity_model.internal), None, &targets).await?;
Ok(())
}
async fn announce(&self, _: String, activity: serde_json::Value) -> crate::Result<()> {
let activity_model = model::activity::Model::new(&activity)?;
let Some(object_uri) = &activity_model.object else {
return Err(FieldError("object").into());
};
let obj = self.fetch_object(object_uri).await?;
let oid = obj.id;
async fn announce(&self, server: String, activity: serde_json::Value) -> crate::Result<()> {
let uid = activity.actor().id().ok_or_else(|| UpubError::field("actor"))?;
let actor = self.fetch_user(&uid).await?;
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"))?;
// relays send us activities as Announce, but we don't really want to count those towards the
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 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
if self.is_relay(&activity_model.actor) {
tracing::info!("relay {} broadcasted {}", activity_model.actor, oid);
if !matches!(actor.actor_type, apb::ActorType::Person) {
tracing::info!("relay {} broadcasted {}", activity_model.actor, announced_id);
return Ok(())
}
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 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.id), None, &expanded_addressing).await?;
model::share::Entity::insert(share)
self.address_to(Some(activity_model.internal), None, &expanded_addressing).await?;
model::announce::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()))
.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, oid);
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())
},
}
},
}
}
}

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 context;
pub mod dispatcher;
pub mod fetcher;
pub mod inbox;
pub mod init;
pub mod outbox;
pub mod auth;
pub mod builders;
pub mod httpsign;
pub mod normalizer;
pub mod side_effects;
pub use context::Context;

View file

@ -1,34 +1,33 @@
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 super::fetcher::Fetcher;
#[axum::async_trait]
pub trait Normalizer {
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]
impl Normalizer for super::Context {
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_model.id.clone();
let uid = object_model.attributed_to.clone();
let oid = object_node.id().ok_or_else(UpubError::bad_request)?.to_string();
let uid = object_node.attributed_to().id();
let mut object_model = model::object::ActiveModel::new(&object_node)?;
if let Some(server) = server {
// 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) {
return Err(UpubError::forbidden());
}
} else if server != Context::server(&object_model.id) {
} else if server != Context::server(&oid) {
return Err(UpubError::forbidden());
};
}
// make sure content only contains a safe subset of html
if let Some(content) = object_model.content {
object_model.content = Some(mdhtml::safe_html(&content));
if let Set(Some(content)) = object_model.content {
object_model.content = Set(Some(mdhtml::safe_html(&content)));
}
// 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
// > 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
if let Some(ref reply) = object_model.in_reply_to {
if let Some(o) = model::object::Entity::find_by_id(reply).one(self.db()).await? {
object_model.context = o.context;
if let Set(Some(ref reply)) = object_model.in_reply_to {
if let Some(o) = model::object::Entity::find_by_ap_id(reply).one(self.db()).await? {
object_model.context = Set(o.context);
} 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 {
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?;
let object = model::object::Entity::find_by_ap_id(&oid).one(self.db()).await?.ok_or_else(UpubError::internal_server_error)?;
// update replies counter
if let Some(ref in_reply_to) = object_model.in_reply_to {
if self.fetch_object(in_reply_to).await.is_ok() {
if let Set(Some(ref in_reply_to)) = object_model.in_reply_to {
model::object::Entity::update_many()
.filter(model::object::Column::Id.eq(in_reply_to))
.col_expr(model::object::Column::Comments, Expr::col(model::object::Column::Comments).add(1))
.col_expr(model::object::Column::Replies, Expr::col(model::object::Column::Replies).add(1))
.exec(self.db())
.await?;
}
}
// update statuses counter
if let Some(object_author) = uid {
model::user::Entity::update_many()
.col_expr(model::user::Column::StatusesCount, Expr::col(model::user::Column::StatusesCount).add(1))
.filter(model::user::Column::Id.eq(&object_author))
model::actor::Entity::update_many()
.col_expr(model::actor::Column::StatusesCount, Expr::col(model::actor::Column::StatusesCount).add(1))
.filter(model::actor::Column::Id.eq(&object_author))
.exec(self.db())
.await?;
}
@ -76,22 +74,22 @@ impl Normalizer for super::Context {
continue
},
Node::Link(l) => model::attachment::ActiveModel {
id: sea_orm::ActiveValue::NotSet,
internal: sea_orm::ActiveValue::NotSet,
url: Set(l.href().to_string()),
object: Set(oid.clone()),
object: Set(object.internal),
document_type: Set(apb::DocumentType::Page),
name: Set(l.link_name().map(|x| x.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 {
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())),
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))),
name: Set(o.name().map(|x| x.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)
@ -113,19 +111,50 @@ impl Normalizer for super::Context {
};
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())),
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))),
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()),
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)
.exec(self.db())
.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 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 super::{fetcher::Fetcher, normalizer::Normalizer, Context};
use super::{addresser::Addresser, builders::AnyQuery, fetcher::Fetcher, normalizer::Normalizer, side_effects::SideEffects, Context};
#[axum::async_trait]
@ -14,61 +14,16 @@ impl apb::server::Outbox for Context {
type Activity = serde_json::Value;
async fn create_note(&self, uid: String, object: serde_json::Value) -> crate::Result<String> {
// TODO regex hell, here i come...
let re = regex::Regex::new(r"@(.+)@([^ ]+)").expect("failed compiling regex pattern");
let raw_oid = uuid::Uuid::new_v4().to_string();
let oid = self.oid(&raw_oid);
let aid = self.aid(&uuid::Uuid::new_v4().to_string());
let activity_targets = object.addressed();
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::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)
self.create(
uid,
serde_json::Value::new_object()
.set_activity_type(Some(apb::ActivityType::Create))
.set_to(object.to())
.set_bto(object.bto())
.set_cc(object.cc())
.set_bcc(object.bcc())
.set_object(Node::object(object))
).await
}
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 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(
object
.set_id(Some(&oid))
.set_attributed_to(Node::link(uid.clone()))
.set_published(Some(chrono::Utc::now()))
.set_to(activity.to())
.set_bto(activity.bto())
.set_cc(activity.cc())
.set_bcc(activity.bcc()),
.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::new(
&activity
self.insert_activity(
activity
.set_id(Some(&aid))
.set_actor(Node::link(uid.clone()))
.set_published(Some(chrono::Utc::now()))
.set_object(Node::link(oid.clone()))
)?;
model::activity::Entity::insert(activity_model.into_active_model())
.exec(self.db()).await?;
.set_published(Some(chrono::Utc::now())),
Some(self.domain().to_string()),
).await?;
self.dispatch(&uid, activity_targets, &aid, Some(&oid)).await?;
Ok(aid)
@ -112,28 +88,26 @@ impl apb::server::Outbox for Context {
let aid = self.aid(&uuid::Uuid::new_v4().to_string());
let activity_targets = activity.addressed();
let oid = activity.object().id().ok_or_else(UpubError::bad_request)?;
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 obj_model = self.fetch_object(&oid).await?;
let like_model = model::like::ActiveModel {
actor: Set(uid.clone()),
likes: Set(oid.clone()),
date: Set(chrono::Utc::now()),
..Default::default()
};
model::like::Entity::insert(like_model).exec(self.db()).await?;
model::activity::Entity::insert(activity_model.into_active_model())
.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::Id.eq(oid))
.exec(self.db())
.await?;
let internal_uid = model::actor::Entity::ap_to_internal(&uid, self.db()).await?;
if model::like::Entity::find_by_uid_oid(internal_uid, obj_model.internal)
.any(self.db())
.await?
{
return Err(UpubError::not_modified());
}
let activity_model = self.insert_activity(
activity
.set_id(Some(&aid))
.set_actor(Node::link(uid.clone()))
.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?;
@ -143,17 +117,32 @@ impl apb::server::Outbox for Context {
async fn follow(&self, uid: String, activity: serde_json::Value) -> crate::Result<String> {
let aid = self.aid(&uuid::Uuid::new_v4().to_string());
let activity_targets = activity.addressed();
if activity.object().id().is_none() {
return Err(UpubError::bad_request());
}
let target = activity.object().id().ok_or_else(UpubError::bad_request)?;
let activity_model = model::activity::Model::new(
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.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?;
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> {
let aid = self.aid(&uuid::Uuid::new_v4().to_string());
let activity_targets = activity.addressed();
if activity.object().id().is_none() {
return Err(UpubError::bad_request());
}
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)
let accepted_id = activity.object().id().ok_or_else(UpubError::bad_request)?;
let accepted_activity = model::activity::Entity::find_by_ap_id(&accepted_id)
.one(self.db()).await?
else {
return Err(UpubError::not_found());
};
.ok_or_else(UpubError::not_found)?;
match accepted_activity.activity_type {
apb::ActivityType::Follow => {
model::user::Entity::update_many()
.col_expr(
model::user::Column::FollowersCount,
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()
if accepted_activity.activity_type != apb::ActivityType::Follow {
return Err(UpubError::bad_request());
}
).exec(self.db()).await?;
},
t => tracing::warn!("no side effects implemented for accepting {t:?}"),
if uid != accepted_activity.object.ok_or_else(UpubError::bad_request)? {
return Err(UpubError::forbidden());
}
let activity_model = model::activity::Model::new(
let activity_model = model::activity::ActiveModel::new(
&activity
.set_id(Some(&aid))
.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())
.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?;
Ok(aid)
}
async fn reject(&self, _uid: String, _activity: serde_json::Value) -> crate::Result<String> {
todo!()
async fn reject(&self, uid: String, activity: serde_json::Value) -> crate::Result<String> {
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> {
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_activity = model::activity::Entity::find_by_id(old_aid)
let old_activity = model::activity::Entity::find_by_ap_id(&old_aid)
.one(self.db())
.await?
.ok_or_else(UpubError::not_found)?;
if old_activity.actor != uid {
return Err(UpubError::forbidden());
}
match old_activity.activity_type {
apb::ActivityType::Like => {
model::like::Entity::delete_many()
.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
let activity_model = self.insert_activity(
activity.clone()
.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?;
.set_published(Some(chrono::Utc::now())),
Some(self.domain().to_string())
).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)
}
@ -261,34 +268,29 @@ impl apb::server::Outbox for Context {
let aid = self.aid(&uuid::Uuid::new_v4().to_string());
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())
.await?
.ok_or_else(UpubError::not_found)?;
let Some(author_id) = object.attributed_to else {
// can't change local objects attributed to nobody
return Err(UpubError::forbidden())
};
if author_id != uid {
// can't change objects of others
if uid != object.attributed_to.ok_or_else(UpubError::forbidden)? {
// can't change objects of others, and objects from noone count as others
return Err(UpubError::forbidden());
}
let addressed = activity.addressed();
let activity_model = model::activity::Model::new(
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::object::Entity::delete_by_id(&oid)
model::activity::Entity::insert(activity_model)
.exec(self.db())
.await?;
model::activity::Entity::insert(activity_model.into_active_model())
model::object::Entity::delete_by_ap_id(&oid)
.exec(self.db())
.await?;
@ -300,18 +302,22 @@ impl apb::server::Outbox for Context {
async fn update(&self, uid: String, activity: serde_json::Value) -> crate::Result<String> {
let aid = self.aid(&uuid::Uuid::new_v4().to_string());
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() {
Some(apb::ObjectType::Actor(_)) => {
let mut actor_model = model::user::Model::new(
&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)
let old_actor_model = model::actor::Entity::find_by_ap_id(&target)
.one(self.db())
.await?
.ok_or_else(UpubError::not_found)?;
@ -321,66 +327,61 @@ impl apb::server::Outbox for Context {
return Err(UpubError::forbidden());
}
if actor_model.name.is_none() { actor_model.name = old_actor_model.name }
if actor_model.summary.is_none() { actor_model.summary = old_actor_model.summary }
if actor_model.image.is_none() { actor_model.image = old_actor_model.image }
if actor_model.icon.is_none() { actor_model.icon = old_actor_model.icon }
let mut new_actor_model = model::actor::ActiveModel {
internal: Unchanged(old_actor_model.internal),
..Default::default()
};
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);
if let Some(name) = object_node.name() {
new_actor_model.name = Set(Some(name.to_string()));
}
if let Some(summary) = object_node.summary() {
new_actor_model.summary = Set(Some(summary.to_string()));
}
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?;
},
Some(apb::ObjectType::Note) => {
let mut object_model = model::object::Model::new(
&object_node.set_published(Some(chrono::Utc::now()))
)?;
let old_object_model = model::object::Entity::find_by_id(&object_model.id)
let old_object_model = model::object::Entity::find_by_ap_id(&target)
.one(self.db())
.await?
.ok_or_else(UpubError::not_found)?;
// can't change local objects attributed to nobody
let author_id = old_object_model.attributed_to.ok_or_else(UpubError::forbidden)?;
if author_id != uid {
if uid != old_object_model.attributed_to.ok_or_else(UpubError::forbidden)? {
// can't change objects of others
return Err(UpubError::forbidden());
}
if object_model.name.is_none() { object_model.name = old_object_model.name }
if object_model.summary.is_none() { object_model.summary = old_object_model.summary }
if object_model.content.is_none() { object_model.content = old_object_model.content }
let mut new_object_model = model::object::ActiveModel {
internal: Unchanged(old_object_model.internal),
..Default::default()
};
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);
if let Some(name) = object_node.name() {
new_object_model.name = Set(Some(name.to_string()));
}
if let Some(summary) = object_node.summary() {
new_object_model.summary = Set(Some(summary.to_string()));
}
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?;
},
_ => 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?;
Ok(aid)
@ -390,26 +391,28 @@ impl apb::server::Outbox for Context {
let aid = self.aid(&uuid::Uuid::new_v4().to_string());
let activity_targets = activity.addressed();
let oid = activity.object().id().ok_or_else(UpubError::bad_request)?;
self.fetch_object(&oid).await?;
let activity_model = model::activity::Model::new(
let obj = self.fetch_object(&oid).await?;
let internal_uid = model::actor::Entity::ap_to_internal(&uid, self.db()).await?;
let activity_model = model::activity::ActiveModel::new(
&activity
.set_id(Some(&aid))
.set_published(Some(chrono::Utc::now()))
.set_actor(Node::link(uid.clone()))
.set_published(Some(chrono::Utc::now()))
)?;
let share_model = model::share::ActiveModel {
actor: Set(uid.clone()),
shares: Set(oid.clone()),
date: Set(chrono::Utc::now()),
..Default::default()
let share_model = model::announce::ActiveModel {
internal: NotSet,
actor: Set(internal_uid),
object: Set(obj.internal),
published: Set(chrono::Utc::now()),
};
model::share::Entity::insert(share_model).exec(self.db()).await?;
model::activity::Entity::insert(activity_model.into_active_model())
model::activity::Entity::insert(activity_model)
.exec(self.db()).await?;
model::announce::Entity::insert(share_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))
.col_expr(model::object::Column::Announces, Expr::col(model::object::Column::Announces).add(1))
.filter(model::object::Column::Internal.eq(obj.internal))
.exec(self.db())
.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,12 +23,14 @@ impl TokenSink for Sink {
) { return TokenSinkResult::Continue } // skip this tag
self.0.push('<');
if !tag.self_closing && matches!(tag.kind, TagKind::EndTag) {
self.0.push('/');
}
self.0.push_str(tag.name.as_ref());
if !matches!(tag.kind, TagKind::EndTag) {
match tag.name.as_ref() {
"img" => for attr in tag.attrs {
match attr.name.local.as_ref() {
@ -39,6 +41,7 @@ impl TokenSink for Sink {
}
},
"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())),
@ -46,14 +49,18 @@ impl TokenSink for Sink {
_ => {},
}
}
if any_attr {
self.0.push_str(" rel=\"nofollow noreferrer\" target=\"_blank\"");
}
},
_ => {},
}
}
if tag.self_closing {
self.0.push('/');
}
self.0.push('>');
},
Token::CharacterTokens(txt) => self.0.push_str(txt.as_ref()),

View file

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

View file

@ -9,7 +9,7 @@
<meta property="og:type" content="website" />
<meta property="og:title" content="upub">
<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" />
<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_router::*;
use reqwest::Method;
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]
pub fn App() -> impl IntoView {
let (token, set_token) = use_cookie_with_options::<String, FromToStringCodec>(
"token",
UseCookieOptions::default()
.max_age(1000 * 60 * 60 * 6)
);
let (config, set_config, _) = use_local_storage::<crate::Config, JsonCodec>("config");
let (token, set_token) = use_cookie::<String, FromToStringCodec>("token");
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 };
provide_context(auth);
@ -22,10 +19,12 @@ pub fn App() -> impl IntoView {
let username = auth.userid.get_untracked()
.map(|x| x.split('/').last().unwrap_or_default().to_string())
.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 user_tl = Timeline::new(format!("{URL_BASE}/users/{username}/outbox/page"));
let context_tl = Timeline::new(format!("{URL_BASE}/outbox/page"));
let local_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();
provide_context(reply_controls);
@ -35,26 +34,42 @@ pub fn App() -> impl IntoView {
let (menu, set_menu) = create_signal(screen_width <= 786);
let (advanced, set_advanced) = create_signal(false);
spawn_local(async move {
if let Err(e) = server_tl.more(auth).await {
tracing::error!("error populating timeline: {e}");
}
});
let title_target = move || if auth.present() { "/web/home" } else { "/web/server" };
let auth_present = auth.token.get_untracked().is_some(); // skip helper to use get_untracked
if auth_present {
if let Some(tok) = token.get_untracked() {
spawn_local(async move {
if let Err(e) = home_tl.more(auth).await {
tracing::error!("error populating timeline: {e}");
// refresh token first, or verify that we're still authed
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! {
<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>
/* 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" />
@ -96,8 +111,6 @@ pub fn App() -> impl IntoView {
// 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
// entitled to see new posts. so while being ugly it's techically correct ig?
{move || {
view! {
<main>
<Routes>
<Route path="/web" view=move ||
@ -110,13 +123,15 @@ pub fn App() -> impl IntoView {
<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/local" view=move || view! { <TimelinePage name="local" tl=local_tl /> } />
<Route path="/web/about" view=AboutPage />
<Route path="/web/config" view=move || view! { <ConfigPage setter=set_config /> } />
<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/activities/:id" view=move || view! { <ActivityPage tl=context_tl /> } />
<Route path="/web/search" view=SearchPage />
<Route path="/web/register" view=RegisterPage />
@ -124,8 +139,6 @@ pub fn App() -> impl IntoView {
<Route path="/" view=move || view! { <Redirect path="/web" /> } />
</Routes>
</main>
}
}}
</Router>
</div>
</div>

View file

@ -39,6 +39,6 @@ impl AuthToken for Auth {
}
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]
pub fn ActivityLine(activity: crate::Object) -> impl IntoView {
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 = CACHE.get_or(&actor_id, serde_json::Value::String(actor_id.clone()).into());
let kind = activity.activity_type().unwrap_or(apb::ActivityType::Activity);
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
_ => 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} >
{kind.as_ref().to_string()}
</a>
{activity_url}
<PrivacyMarker addressed=activity.addressed() />
</code>
</span>
@ -36,6 +40,7 @@ pub fn ActivityLine(activity: crate::Object) -> impl IntoView {
pub fn Item(
item: crate::Object,
#[prop(optional)] sep: bool,
#[prop(optional)] replies: bool,
) -> impl IntoView {
let config = use_context::<Signal<crate::Config>>().expect("missing config context");
let id = item.id().unwrap_or_default().to_string();
@ -55,7 +60,7 @@ pub fn Item(
Some(apb::ObjectType::Activity(t)) => (move || {
if config.get().filters.visible(apb::ObjectType::Activity(t)) {
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
} else {
let object = match t {

View file

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

View file

@ -39,6 +39,7 @@ pub fn Navigator() -> impl IntoView {
<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/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>
<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>

View file

@ -236,7 +236,7 @@ pub fn LikeButton(
let cc = if private { apb::Node::Empty } else {
apb::Node::links(vec![
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())
@ -312,7 +312,7 @@ pub fn RepostButton(n: u64, target: String) -> impl IntoView {
if !clicked.get() { return; }
set_clicked.set(false);
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())
.set_activity_type(Some(apb::ActivityType::Announce))
.set_object(apb::Node::link(target.clone()))

View file

@ -1,7 +1,6 @@
use apb::{ActivityMut, Base, BaseMut, Object, ObjectMut};
use leptos::*;
use leptos_use::DebounceOptions;
use crate::{prelude::*, WEBFINGER};
#[derive(Debug, Clone, Copy, Default)]
@ -87,7 +86,7 @@ pub fn PostBox(advanced: WriteSignal<bool>) -> impl IntoView {
mentions.get()
.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(),
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())
}
@ -116,10 +115,11 @@ pub fn PostBox(advanced: WriteSignal<bool>) -> impl IntoView {
let mut cc_vec = Vec::new();
let mut to_vec = Vec::new();
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) {
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(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>
</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>
</tr>
</table>
@ -285,7 +285,7 @@ pub fn AdvancedPostBox(advanced: WriteSignal<bool>) -> impl IntoView {
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 {
Err(e) => set_error.set(Some(e.to_string())),
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 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::*;
#[derive(Debug, Clone, Copy)]
@ -36,14 +36,21 @@ impl Timeline {
self.over.set(false);
}
pub async fn more(&self, auth: Auth) -> reqwest::Result<()> {
self.loading.set(true);
let res = self.more_inner(auth).await;
self.loading.set(false);
res
pub fn more(&self, auth: Auth) {
if self.loading.get_untracked() { return }
if self.over.get_untracked() { return }
let _self = *self;
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};
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();
view! {
<div class="context depth-r">
<Item item=obj />
<Item item=obj replies=true />
<div class="depth-r">
<TimelineRepliesRecursive tl=tl root=oid />
</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 >
<button type="button"
prop:disabled=tl.loading
on:click=move |_| {
spawn_local(async move {
if let Err(e) = tl.more(auth).await {
tracing::error!("error fetching more items for timeline: {e}");
}
})
}
on:click=move |_| tl.more(auth)
>
{move || if tl.loading.get() {
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(
move || (scroll_debounced.get(), height.get()),
move |(s, h)| async move {
if !config.get().infinite_scroll { return }
if !tl.loading.get() && h - s < view_height {
if let Err(e) = tl.more(auth).await {
tracing::error!("auto load failed: {e}");
}
if !config.get_untracked().infinite_scroll { return }
if h - s < view_height {
tl.more(auth);
}
},
);
@ -179,7 +178,7 @@ pub fn TimelineFeed(tl: Timeline) -> impl IntoView {
<div class="center mt-1 mb-1" class:hidden=tl.over >
<button type="button"
prop:disabled=tl.loading
on:click=move |_| load_more(tl, auth)
on:click=move |_| tl.more(auth)
>
{move || if tl.loading.get() {
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> {
let mut sub_tasks : Vec<Pin<Box<dyn futures::Future<Output = ()>>>> = Vec::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 !gonna_fetch.contains(&object_id) {
let fetch_kind = match activity_type {
apb::ActivityType::Follow => U::User,
apb::ActivityType::Follow => U::Actor,
_ => U::Object,
};
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 CACHE.get(&uid).is_none() && !gonna_fetch.contains(&uid) {
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 CACHE.get(&uid).is_none() && !gonna_fetch.contains(&uid) {
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 {
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;
@ -280,9 +269,9 @@ async fn fetch_and_update_with_user(kind: U, id: String, auth: Auth) {
if let Some(actor_id) = match kind {
U::Object => obj.attributed_to().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 avatar = object.icon().get().map(|x| x.url().id().unwrap_or(DEFAULT_AVATAR_URL.into())).unwrap_or(DEFAULT_AVATAR_URL.into());
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>
</a>
}
@ -20,11 +20,11 @@ pub fn ActorStrip(object: crate::Object) -> impl IntoView {
pub fn ActorBanner(object: crate::Object) -> impl IntoView {
match object.as_ref() {
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(_) => {
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 display_name = object.name().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 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_SENSITIVE: &str = "https://cdn.alemi.dev/social/nsfw.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.json::<jrd::JsonResourceDescriptor>().await {
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));
} else {
self.0.insert(query, LookupStatus::NotFound);

View file

@ -84,7 +84,7 @@ pub fn DebugPage() -> impl IntoView {
" raw :: "
<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>
" :: "

View file

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

View file

@ -19,13 +19,13 @@ pub fn RegisterPage() -> impl IntoView {
<form on:submit=move|ev| {
ev.prevent_default();
logging::log!("registering new user...");
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 _email = username_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 {
match Http::request::<()>(
Method::PUT, &format!("{URL_BASE}/auth"), None, auth
).await {
Ok(x) => {},
Ok(_x) => {},
Err(e) => set_error.set(Some(
view! { <blockquote>{e.to_string()}</blockquote> }
)),

View file

@ -11,7 +11,7 @@ pub fn SearchPage() -> impl IntoView {
let user = create_local_resource(
move || use_query_map().get().get("q").cloned().unwrap_or_default(),
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()?)) }
}
);

View file

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

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