chore: finish types refactor

This commit is contained in:
nullishamy 2025-04-29 22:18:30 +01:00
parent 5f346922f5
commit 77fba1a082
Signed by: amy
SSH key fingerprint: SHA256:WmV0uk6WgAQvDJlM8Ld4mFPHZo02CLXXP5VkwQ5xtyk
29 changed files with 611 additions and 608 deletions

View file

@ -3,7 +3,7 @@ use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::Sqlite; use sqlx::Sqlite;
use std::fmt::Debug; use std::fmt::Debug;
use tracing::{event, Level}; use tracing::{Level, event};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum ActivityType { pub enum ActivityType {
@ -127,8 +127,10 @@ impl<'a> Outbox<'a> {
.await .await
.unwrap(); .unwrap();
event!(Level::DEBUG, event!(
outbox_res, activity = activity.req.id, Level::DEBUG,
outbox_res,
activity = activity.req.id,
"got response for outbox dispatch" "got response for outbox dispatch"
); );
} }

View file

@ -12,7 +12,7 @@ use rsa::{
use base64::prelude::*; use base64::prelude::*;
use chrono::Utc; use chrono::Utc;
use tracing::{event, Level}; use tracing::{Level, event};
pub struct HttpClient { pub struct HttpClient {
client: reqwest::Client, client: reqwest::Client,

View file

@ -1,21 +1,21 @@
use std::sync::mpsc; use std::sync::mpsc;
use std::thread; use std::thread;
use tracing::{info, span, Level}; use tracing::{Level, info, span};
#[derive(Debug)] #[derive(Debug)]
pub enum QueueMessage { pub enum QueueMessage {
Heartbeat Heartbeat,
} }
pub struct RequestQueue { pub struct RequestQueue {
name: &'static str, name: &'static str,
send: mpsc::Sender<QueueMessage>, send: mpsc::Sender<QueueMessage>,
recv: mpsc::Receiver<QueueMessage> recv: mpsc::Receiver<QueueMessage>,
} }
#[derive(Clone)] #[derive(Clone)]
pub struct QueueHandle { pub struct QueueHandle {
send: mpsc::Sender<QueueMessage> send: mpsc::Sender<QueueMessage>,
} }
impl QueueHandle { impl QueueHandle {
@ -27,11 +27,7 @@ impl QueueHandle {
impl RequestQueue { impl RequestQueue {
pub fn new(name: &'static str) -> Self { pub fn new(name: &'static str) -> Self {
let (send, recv) = mpsc::channel(); let (send, recv) = mpsc::channel();
Self { Self { name, send, recv }
name,
send,
recv
}
} }
pub fn spawn(self) -> QueueHandle { pub fn spawn(self) -> QueueHandle {

View file

@ -35,7 +35,6 @@ pub struct User {
display_name: String, display_name: String,
} }
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum UserError { pub enum UserError {
#[error("user `{0}` not found")] #[error("user `{0}` not found")]
@ -67,7 +66,10 @@ impl User {
format!("https://ferri.amy.mov/users/{}", self.id()) format!("https://ferri.amy.mov/users/{}", self.id())
} }
pub async fn from_id(uuid: &str, conn: impl sqlx::Executor<'_, Database = Sqlite>) -> Result<User, UserError> { pub async fn from_id(
uuid: &str,
conn: impl sqlx::Executor<'_, Database = Sqlite>,
) -> Result<User, UserError> {
let user = sqlx::query!( let user = sqlx::query!(
r#" r#"
SELECT u.*, a.id as "actor_own_id", a.inbox, a.outbox SELECT u.*, a.id as "actor_own_id", a.inbox, a.outbox

View file

@ -1,6 +1,6 @@
pub mod ap; pub mod ap;
pub mod config; pub mod config;
pub mod types_rewrite; pub mod types;
use rand::{Rng, distributions::Alphanumeric}; use rand::{Rng, distributions::Alphanumeric};

View file

@ -1,18 +1,18 @@
use crate::types_rewrite::api; use crate::types::ap;
use crate::types_rewrite::ap; use crate::types::api;
use crate::types_rewrite::db; use crate::types::db;
use crate::types_rewrite::{Object, as_context}; use crate::types::{Object, ObjectUri, as_context};
impl From<db::Actor> for ap::Actor { impl From<db::Actor> for ap::Actor {
fn from(val: db::Actor) -> ap::Actor { fn from(val: db::Actor) -> ap::Actor {
ap::Actor { ap::Actor {
obj: Object { obj: Object {
context: as_context(), context: as_context(),
id: val.id id: val.id,
}, },
inbox: val.inbox, inbox: val.inbox,
outbox: val.outbox outbox: val.outbox,
} }
} }
} }
@ -22,7 +22,7 @@ impl From<ap::Actor> for db::Actor {
db::Actor { db::Actor {
id: val.obj.id, id: val.obj.id,
inbox: val.inbox, inbox: val.inbox,
outbox: val.outbox outbox: val.outbox,
} }
} }
} }
@ -59,3 +59,26 @@ impl From<db::User> for api::Account {
} }
} }
} }
impl From<db::User> for ap::Person {
fn from(val: db::User) -> ap::Person {
ap::Person {
obj: Object {
context: as_context(),
id: ObjectUri(format!("https://ferri.amy.mov/users/{}", val.id.0)),
},
following: format!("https://ferri.amy.mov/users/{}/following", val.id.0),
followers: format!("https://ferri.amy.mov/users/{}/followers", val.id.0),
summary: format!("ferri {}", val.username),
inbox: format!("https://ferri.amy.mov/users/{}/inbox", val.id.0),
outbox: format!("https://ferri.amy.mov/users/{}/outbox", val.id.0),
preferred_username: val.display_name,
name: val.username,
public_key: Some(ap::UserKey {
id: format!("https://ferri.amy.mov/users/{}#main-key", val.id.0),
owner: format!("https://ferri.amy.mov/users/{}", val.id.0),
public_key: include_str!("../../../public.pem").to_string(),
}),
}
}
}

View file

@ -1,10 +1,9 @@
use crate::types_rewrite::{ObjectUuid, ObjectUri, db}; use crate::types::{DbError, ObjectUri, ObjectUuid, db};
use chrono::{DateTime, NaiveDateTime, Utc};
use sqlx::SqliteConnection; use sqlx::SqliteConnection;
use tracing::info; use tracing::info;
use chrono::{NaiveDateTime, DateTime, Utc};
use crate::types_rewrite::DbError;
const SQLITE_TIME_FMT: &'static str = "%Y-%m-%d %H:%M:%S"; const SQLITE_TIME_FMT: &str = "%Y-%m-%d %H:%M:%S";
fn parse_ts(ts: String) -> Option<DateTime<Utc>> { fn parse_ts(ts: String) -> Option<DateTime<Utc>> {
NaiveDateTime::parse_from_str(&ts, SQLITE_TIME_FMT) NaiveDateTime::parse_from_str(&ts, SQLITE_TIME_FMT)
@ -15,7 +14,8 @@ fn parse_ts(ts: String) -> Option<DateTime<Utc>> {
pub async fn user_by_id(id: ObjectUuid, conn: &mut SqliteConnection) -> Result<db::User, DbError> { pub async fn user_by_id(id: ObjectUuid, conn: &mut SqliteConnection) -> Result<db::User, DbError> {
info!("fetching user by uuid '{:?}' from the database", id); info!("fetching user by uuid '{:?}' from the database", id);
let record = sqlx::query!(r#" let record = sqlx::query!(
r#"
SELECT SELECT
u.id as "user_id", u.id as "user_id",
u.username, u.username,
@ -30,27 +30,35 @@ pub async fn user_by_id(id: ObjectUuid, conn: &mut SqliteConnection) -> Result<d
FROM "user" u FROM "user" u
INNER JOIN "actor" a ON u.actor_id = a.id INNER JOIN "actor" a ON u.actor_id = a.id
WHERE u.id = ?1 WHERE u.id = ?1
"#, id.0) "#,
id.0
)
.fetch_one(&mut *conn) .fetch_one(&mut *conn)
.await .await
.map_err(|e| DbError::FetchError(e.to_string()))?; .map_err(|e| DbError::FetchError(e.to_string()))?;
let follower_count = sqlx::query_scalar!(r#" let follower_count = sqlx::query_scalar!(
r#"
SELECT COUNT(follower_id) SELECT COUNT(follower_id)
FROM "follow" FROM "follow"
WHERE followed_id = ?1 WHERE followed_id = ?1
"#, record.actor_id) "#,
record.actor_id
)
.fetch_one(&mut *conn) .fetch_one(&mut *conn)
.await .await
.map_err(|e| DbError::FetchError(e.to_string()))?; .map_err(|e| DbError::FetchError(e.to_string()))?;
let last_post_at = sqlx::query_scalar!(r#" let last_post_at = sqlx::query_scalar!(
r#"
SELECT datetime(p.created_at) SELECT datetime(p.created_at)
FROM post p FROM post p
WHERE p.user_id = ?1 WHERE p.user_id = ?1
ORDER BY datetime(p.created_at) DESC ORDER BY datetime(p.created_at) DESC
LIMIT 1 LIMIT 1
"#, record.user_id) "#,
record.user_id
)
.fetch_one(&mut *conn) .fetch_one(&mut *conn)
.await .await
.map_err(|e| DbError::FetchError(e.to_string()))? .map_err(|e| DbError::FetchError(e.to_string()))?
@ -69,7 +77,7 @@ pub async fn user_by_id(id: ObjectUuid, conn: &mut SqliteConnection) -> Result<d
actor: db::Actor { actor: db::Actor {
id: ObjectUri(record.actor_id), id: ObjectUri(record.actor_id),
inbox: record.inbox, inbox: record.inbox,
outbox: record.outbox outbox: record.outbox,
}, },
acct: record.acct, acct: record.acct,
remote: record.remote, remote: record.remote,
@ -77,8 +85,6 @@ pub async fn user_by_id(id: ObjectUuid, conn: &mut SqliteConnection) -> Result<d
display_name: record.display_name, display_name: record.display_name,
created_at: user_created, created_at: user_created,
url: record.url, url: record.url,
posts: db::UserPosts { posts: db::UserPosts { last_post_at },
last_post_at
}
}) })
} }

View file

@ -0,0 +1,45 @@
use crate::types::{DbError, db};
use sqlx::SqliteConnection;
pub async fn new_user(user: db::User, conn: &mut SqliteConnection) -> Result<db::User, DbError> {
let ts = user.created_at.to_rfc3339();
sqlx::query!(
r#"
INSERT INTO user (id, acct, url, created_at, remote, username, actor_id, display_name)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
"#,
user.id.0,
user.acct,
user.url,
ts,
user.remote,
user.username,
user.actor.id.0,
user.display_name
)
.execute(conn)
.await
.map_err(|e| DbError::CreationError(e.to_string()))?;
Ok(user)
}
pub async fn new_actor(
actor: db::Actor,
conn: &mut SqliteConnection,
) -> Result<db::Actor, DbError> {
sqlx::query!(
r#"
INSERT INTO actor (id, inbox, outbox)
VALUES (?1, ?2, ?3)
"#,
actor.id.0,
actor.inbox,
actor.outbox
)
.execute(conn)
.await
.map_err(|e| DbError::CreationError(e.to_string()))?;
Ok(actor)
}

View file

@ -1,6 +1,6 @@
use serde::{Serialize, Deserialize}; use serde::{Deserialize, Serialize};
use thiserror::Error;
use std::fmt::Debug; use std::fmt::Debug;
use thiserror::Error;
use uuid::Uuid; use uuid::Uuid;
pub mod convert; pub mod convert;
@ -12,10 +12,10 @@ pub enum DbError {
#[error("an unknown error occured when creating: {0}")] #[error("an unknown error occured when creating: {0}")]
CreationError(String), CreationError(String),
#[error("an unknown error occured when fetching: {0}")] #[error("an unknown error occured when fetching: {0}")]
FetchError(String) FetchError(String),
} }
pub const AS_CONTEXT_RAW: &'static str = "https://www.w3.org/ns/activitystreams"; pub const AS_CONTEXT_RAW: &str = "https://www.w3.org/ns/activitystreams";
pub fn as_context() -> ObjectContext { pub fn as_context() -> ObjectContext {
ObjectContext::Str(AS_CONTEXT_RAW.to_string()) ObjectContext::Str(AS_CONTEXT_RAW.to_string())
} }
@ -33,6 +33,12 @@ pub struct ObjectUri(pub String);
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)]
pub struct ObjectUuid(pub String); pub struct ObjectUuid(pub String);
impl Default for ObjectUuid {
fn default() -> Self {
Self::new()
}
}
impl ObjectUuid { impl ObjectUuid {
pub fn new() -> Self { pub fn new() -> Self {
Self(Uuid::new_v4().to_string()) Self(Uuid::new_v4().to_string())
@ -42,13 +48,13 @@ impl ObjectUuid {
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct Object { pub struct Object {
#[serde(rename = "@context")] #[serde(rename = "@context")]
context: ObjectContext, pub context: ObjectContext,
id: ObjectUri, pub id: ObjectUri,
} }
pub mod db { pub mod db {
use chrono::{DateTime, Utc};
use super::*; use super::*;
use chrono::{DateTime, Utc};
#[derive(Debug, Eq, PartialEq, Clone)] #[derive(Debug, Eq, PartialEq, Clone)]
pub struct Actor { pub struct Actor {
@ -60,7 +66,7 @@ pub mod db {
#[derive(Debug, Eq, PartialEq, Clone)] #[derive(Debug, Eq, PartialEq, Clone)]
pub struct UserPosts { pub struct UserPosts {
// User may have no posts // User may have no posts
pub last_post_at: Option<DateTime<Utc>> pub last_post_at: Option<DateTime<Utc>>,
} }
#[derive(Debug, Eq, PartialEq, Clone)] #[derive(Debug, Eq, PartialEq, Clone)]
@ -74,13 +80,112 @@ pub mod db {
pub url: String, pub url: String,
pub created_at: DateTime<Utc>, pub created_at: DateTime<Utc>,
pub posts: UserPosts pub posts: UserPosts,
} }
} }
pub mod ap { pub mod ap {
use serde::{Serialize, Deserialize};
use super::*; use super::*;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub enum ActivityType {
Create,
Note,
Delete,
Accept,
Announce,
Like,
Follow,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct MinimalActivity {
#[serde(flatten)]
pub obj: Object,
pub ty: ActivityType,
}
pub type DeleteActivity = BasicActivity;
pub type LikeActivity = BasicActivity;
#[derive(Serialize, Deserialize, Debug)]
pub struct BasicActivity {
#[serde(flatten)]
pub obj: Object,
pub object: String,
pub actor: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct CreateActivity {
#[serde(flatten)]
pub obj: Object,
pub ty: ActivityType,
pub object: Post,
pub actor: String,
pub to: Vec<String>,
pub cc: Vec<String>,
#[serde(rename = "published")]
pub ts: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct FollowActivity {
#[serde(flatten)]
pub obj: Object,
pub ty: ActivityType,
pub object: String,
pub actor: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct AcceptActivity {
#[serde(flatten)]
pub obj: Object,
pub ty: ActivityType,
pub object: String,
pub actor: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct BoostActivity {
#[serde(flatten)]
pub obj: Object,
pub ty: ActivityType,
pub actor: String,
pub published: String,
pub to: Vec<String>,
pub cc: Vec<String>,
pub object: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Post {
#[serde(flatten)]
pub obj: Object,
pub ty: ActivityType,
#[serde(rename = "published")]
pub ts: String,
pub content: String,
pub to: Vec<String>,
pub cc: Vec<String>,
#[serde(rename = "attributedTo")]
pub attributed_to: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct Actor { pub struct Actor {
@ -92,6 +197,7 @@ pub mod ap {
} }
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Person { pub struct Person {
#[serde(flatten)] #[serde(flatten)]
pub obj: Object, pub obj: Object,
@ -120,13 +226,37 @@ pub mod ap {
} }
pub mod api { pub mod api {
use serde::{Serialize, Deserialize};
use super::*; use super::*;
use serde::{Deserialize, Serialize};
// API will not really use actors so treat them as DB actors // API will not really use actors so treat them as DB actors
// until we require specificity // until we require specificity
pub type Actor = db::Actor; pub type Actor = db::Actor;
#[derive(Serialize, Deserialize, Debug)]
pub struct CredentialApplication {
pub name: String,
pub scopes: String,
pub redirect_uris: Vec<String>,
pub client_id: String,
pub client_secret: String,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct WebfingerLink {
pub rel: String,
#[serde(rename = "type")]
pub ty: Option<String>,
pub href: Option<String>,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct WebfingerHit {
pub subject: String,
pub aliases: Vec<String>,
pub links: Vec<WebfingerLink>,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct Account { pub struct Account {
pub id: ObjectUuid, pub id: ObjectUuid,
@ -293,10 +423,13 @@ mod tests {
let db: db::Actor = ap.into(); let db: db::Actor = ap.into();
assert_eq!(db, db::Actor { assert_eq!(
db,
db::Actor {
id: ObjectUri("https://example.com/users/sample".to_string()), id: ObjectUri("https://example.com/users/sample".to_string()),
inbox: "https://example.com/users/sample/inbox".to_string(), inbox: "https://example.com/users/sample/inbox".to_string(),
outbox: "https://example.com/users/sample/outbox".to_string(), outbox: "https://example.com/users/sample/outbox".to_string(),
}); }
);
} }
} }

View file

@ -1,30 +0,0 @@
use crate::types_rewrite::db;
use sqlx::SqliteConnection;
use crate::types_rewrite::DbError;
pub async fn new_user(user: db::User, conn: &mut SqliteConnection) -> Result<db::User, DbError> {
let ts = user.created_at.to_rfc3339();
sqlx::query!(r#"
INSERT INTO user (id, acct, url, created_at, remote, username, actor_id, display_name)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
"#, user.id.0, user.acct, user.url, ts,
user.remote, user.username, user.actor.id.0, user.display_name
)
.execute(conn)
.await
.map_err(|e| DbError::CreationError(e.to_string()))?;
Ok(user)
}
pub async fn new_actor(actor: db::Actor, conn: &mut SqliteConnection) -> Result<db::Actor, DbError> {
sqlx::query!(r#"
INSERT INTO actor (id, inbox, outbox)
VALUES (?1, ?2, ?3)
"#, actor.id.0, actor.inbox, actor.outbox)
.execute(conn)
.await
.map_err(|e| DbError::CreationError(e.to_string()))?;
Ok(actor)
}

View file

@ -1,11 +1,23 @@
use rocket::{form::Form, post, serde::json::Json}; use rocket::{form::Form, post, serde::json::Json};
use crate::Db; use crate::Db;
use crate::types::oauth::{App, CredentialApplication}; use main::types::api;
use rocket::FromForm;
use rocket_db_pools::Connection; use rocket_db_pools::Connection;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, FromForm, Clone)]
pub struct OauthApp {
pub client_name: String,
pub redirect_uris: Vec<String>,
pub scopes: String,
}
#[post("/apps", data = "<app>")] #[post("/apps", data = "<app>")]
pub async fn new_app(app: Form<App>, mut db: Connection<Db>) -> Json<CredentialApplication> { pub async fn new_app(
app: Form<OauthApp>,
mut db: Connection<Db>,
) -> Json<api::CredentialApplication> {
let secret = main::gen_token(15); let secret = main::gen_token(15);
// Abort when we encounter a duplicate // Abort when we encounter a duplicate
@ -22,7 +34,7 @@ pub async fn new_app(app: Form<App>, mut db: Connection<Db>) -> Json<CredentialA
.await .await
.is_err(); .is_err();
let mut app: App = app.clone(); let mut app: OauthApp = app.clone();
if is_app_present { if is_app_present {
let existing_app = sqlx::query!("SELECT * FROM app WHERE client_id = ?1", app.client_name) let existing_app = sqlx::query!("SELECT * FROM app WHERE client_id = ?1", app.client_name)
@ -34,7 +46,7 @@ pub async fn new_app(app: Form<App>, mut db: Connection<Db>) -> Json<CredentialA
app.scopes = existing_app.scopes; app.scopes = existing_app.scopes;
} }
Json(CredentialApplication { Json(api::CredentialApplication {
name: app.client_name.clone(), name: app.client_name.clone(),
scopes: app.scopes.clone(), scopes: app.scopes.clone(),
redirect_uris: app.redirect_uris.clone(), redirect_uris: app.redirect_uris.clone(),

View file

@ -1,14 +1,13 @@
use rocket::{get, serde::json::Json, State}; use rocket::{State, get, serde::json::Json};
use crate::Config; use main::types::api::{
use main::types_rewrite::api::{
Accounts, Configuration, Contact, Instance, MediaAttachments, Polls, Registrations, Statuses, Accounts, Configuration, Contact, Instance, MediaAttachments, Polls, Registrations, Statuses,
Thumbnail, Translation, Urls, Thumbnail, Translation, Urls,
}; };
#[get("/instance")] #[get("/instance")]
pub async fn instance(config: &State<Config>) -> Json<Instance> { pub async fn instance(helpers: &State<crate::Helpers>) -> Json<Instance> {
let config = &helpers.config;
Json(Instance { Json(Instance {
domain: config.host().to_string(), domain: config.host().to_string(),
title: "Ferri".to_string(), title: "Ferri".to_string(),

View file

@ -3,8 +3,7 @@ use main::ap::{self, http::HttpClient};
use rocket::{ use rocket::{
FromForm, State, FromForm, State,
form::Form, form::Form,
post, get, post,
get,
serde::{Deserialize, Serialize, json::Json}, serde::{Deserialize, Serialize, json::Json},
}; };
use rocket_db_pools::Connection; use rocket_db_pools::Connection;
@ -23,14 +22,14 @@ pub struct Status {
#[serde(crate = "rocket::serde")] #[serde(crate = "rocket::serde")]
pub struct StatusContext { pub struct StatusContext {
ancestors: Vec<Status>, ancestors: Vec<Status>,
descendants: Vec<Status> descendants: Vec<Status>,
} }
#[get("/statuses/<_status>/context")] #[get("/statuses/<_status>/context")]
pub async fn status_context( pub async fn status_context(
_status: &str, _status: &str,
_user: AuthenticatedUser, _user: AuthenticatedUser,
_db: Connection<Db> _db: Connection<Db>,
) -> Json<StatusContext> { ) -> Json<StatusContext> {
Json(StatusContext { Json(StatusContext {
ancestors: vec![], ancestors: vec![],
@ -56,7 +55,10 @@ async fn create_status(
post.save(&mut **db).await; post.save(&mut **db).await;
let actor = sqlx::query!("SELECT * FROM actor WHERE id = ?1", "https://fedi.amy.mov/users/9zkygethkdw60001") let actor = sqlx::query!(
"SELECT * FROM actor WHERE id = ?1",
"https://fedi.amy.mov/users/9zkygethkdw60001"
)
.fetch_one(&mut **db) .fetch_one(&mut **db)
.await .await
.unwrap(); .unwrap();
@ -73,11 +75,7 @@ async fn create_status(
..Default::default() ..Default::default()
}; };
let actor = ap::Actor::from_raw( let actor = ap::Actor::from_raw(actor.id.clone(), actor.inbox.clone(), actor.outbox.clone());
actor.id.clone(),
actor.inbox.clone(),
actor.outbox.clone(),
);
let req = ap::OutgoingActivity { let req = ap::OutgoingActivity {
req: activity, req: activity,
@ -134,19 +132,19 @@ async fn create_status(
#[post("/statuses", data = "<status>")] #[post("/statuses", data = "<status>")]
pub async fn new_status( pub async fn new_status(
db: Connection<Db>, db: Connection<Db>,
http: &State<HttpClient>, helpers: &State<crate::Helpers>,
status: Form<Status>, status: Form<Status>,
user: AuthenticatedUser, user: AuthenticatedUser,
) -> Json<TimelineStatus> { ) -> Json<TimelineStatus> {
Json(create_status(user, db, http.inner(), &status).await) Json(create_status(user, db, &helpers.http, &status).await)
} }
#[post("/statuses", data = "<status>", rank = 2)] #[post("/statuses", data = "<status>", rank = 2)]
pub async fn new_status_json( pub async fn new_status_json(
db: Connection<Db>, db: Connection<Db>,
http: &State<HttpClient>, helpers: &State<crate::Helpers>,
status: Json<Status>, status: Json<Status>,
user: AuthenticatedUser, user: AuthenticatedUser,
) -> Json<TimelineStatus> { ) -> Json<TimelineStatus> {
Json(create_status(user, db, http.inner(), &status).await) Json(create_status(user, db, &helpers.http, &status).await)
} }

View file

@ -1,7 +1,6 @@
use crate::{AuthenticatedUser, Db, endpoints::api::user::CredentialAcount, Config}; use crate::{AuthenticatedUser, Db, endpoints::api::user::CredentialAcount};
use rocket::{ use rocket::{
State, State, get,
get,
serde::{Deserialize, Serialize, json::Json}, serde::{Deserialize, Serialize, json::Json},
}; };
use rocket_db_pools::Connection; use rocket_db_pools::Connection;
@ -36,9 +35,11 @@ pub struct TimelineStatus {
#[get("/timelines/home")] #[get("/timelines/home")]
pub async fn home( pub async fn home(
mut db: Connection<Db>, mut db: Connection<Db>,
config: &State<Config>, helpers: &State<crate::Helpers>,
_user: AuthenticatedUser, _user: AuthenticatedUser,
) -> Json<Vec<TimelineStatus>> { ) -> Json<Vec<TimelineStatus>> {
let config = &helpers.config;
#[derive(sqlx::FromRow, Debug)] #[derive(sqlx::FromRow, Debug)]
struct Post { struct Post {
is_boost_source: bool, is_boost_source: bool,
@ -49,7 +50,7 @@ pub async fn home(
created_at: String, created_at: String,
boosted_post_id: Option<String>, boosted_post_id: Option<String>,
display_name: String, display_name: String,
username: String username: String,
} }
// FIXME: query! can't cope with this. returns a type error // FIXME: query! can't cope with this. returns a type error
@ -78,7 +79,7 @@ pub async fn home(
FROM get_home_timeline_with_boosts FROM get_home_timeline_with_boosts
JOIN post p ON p.id = get_home_timeline_with_boosts.id JOIN post p ON p.id = get_home_timeline_with_boosts.id
JOIN user u ON u.id = p.user_id; JOIN user u ON u.id = p.user_id;
"# "#,
) )
.bind("https://ferri.amy.mov/users/9b9d497b-2731-435f-a929-e609ca69dac9") .bind("https://ferri.amy.mov/users/9b9d497b-2731-435f-a929-e609ca69dac9")
.fetch_all(&mut **db) .fetch_all(&mut **db)

View file

@ -1,14 +1,14 @@
use main::ap; use main::ap;
use rocket::response::status::NotFound;
use rocket::{ use rocket::{
State, get, post, State, get, post,
serde::{Deserialize, Serialize, json::Json}, serde::{Deserialize, Serialize, json::Json},
}; };
use rocket_db_pools::Connection; use rocket_db_pools::Connection;
use uuid::Uuid; use uuid::Uuid;
use rocket::response::status::NotFound;
use crate::timeline::{TimelineAccount, TimelineStatus}; use crate::timeline::{TimelineAccount, TimelineStatus};
use crate::{AuthenticatedUser, Db, http::HttpClient}; use crate::{AuthenticatedUser, Db};
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(crate = "rocket::serde")] #[serde(crate = "rocket::serde")]
@ -60,17 +60,19 @@ pub async fn verify_credentials() -> Json<CredentialAcount> {
#[post("/accounts/<uuid>/follow")] #[post("/accounts/<uuid>/follow")]
pub async fn new_follow( pub async fn new_follow(
mut db: Connection<Db>, mut db: Connection<Db>,
http: &State<HttpClient>, helpers: &State<crate::Helpers>,
uuid: &str, uuid: &str,
user: AuthenticatedUser, user: AuthenticatedUser,
) -> Result<(), NotFound<String>> { ) -> Result<(), NotFound<String>> {
let http = &helpers.http;
let follower = ap::User::from_actor_id(&user.actor_id, &mut **db).await; let follower = ap::User::from_actor_id(&user.actor_id, &mut **db).await;
let followed = ap::User::from_id(uuid, &mut **db) let followed = ap::User::from_id(uuid, &mut **db)
.await .await
.map_err(|e| NotFound(e.to_string()))?; .map_err(|e| NotFound(e.to_string()))?;
let outbox = ap::Outbox::for_user(follower.clone(), http.inner()); let outbox = ap::Outbox::for_user(follower.clone(), http);
let activity = ap::Activity { let activity = ap::Activity {
id: format!("https://ferri.amy.mov/activities/{}", Uuid::new_v4()), id: format!("https://ferri.amy.mov/activities/{}", Uuid::new_v4()),

View file

@ -1,15 +1,9 @@
use crate::{Db, OutboundQueue};
use main::types::{ap, api};
use rocket::{State, get, response::status}; use rocket::{State, get, response::status};
use rocket_db_pools::Connection; use rocket_db_pools::Connection;
use main::ap;
use crate::OutboundQueue;
use uuid::Uuid; use uuid::Uuid;
use crate::{
Db,
types::{self, webfinger},
};
#[get("/finger/<account>")] #[get("/finger/<account>")]
pub async fn finger_account(mut db: Connection<Db>, account: &str) -> status::Accepted<String> { pub async fn finger_account(mut db: Connection<Db>, account: &str) -> status::Accepted<String> {
// user@host.com // user@host.com
@ -23,7 +17,7 @@ pub async fn finger_account(mut db: Connection<Db>, account: &str) -> status::Ac
VALUES (?1, ?2, ?3) VALUES (?1, ?2, ?3)
ON CONFLICT(id) DO NOTHING ON CONFLICT(id) DO NOTHING
"#, "#,
user.id, user.obj.id.0,
user.inbox, user.inbox,
user.outbox user.outbox
) )
@ -41,7 +35,7 @@ pub async fn finger_account(mut db: Connection<Db>, account: &str) -> status::Ac
"#, "#,
uuid, uuid,
username, username,
user.id, user.obj.id.0,
user.preferred_username user.preferred_username
) )
.execute(&mut **db) .execute(&mut **db)
@ -51,7 +45,7 @@ pub async fn finger_account(mut db: Connection<Db>, account: &str) -> status::Ac
status::Accepted(format!("https://ferri.amy.mov/users/{}", uuid)) status::Accepted(format!("https://ferri.amy.mov/users/{}", uuid))
} }
pub async fn resolve_user(acct: &str, host: &str) -> types::Person { pub async fn resolve_user(acct: &str, host: &str) -> ap::Person {
let client = reqwest::Client::new(); let client = reqwest::Client::new();
let url = format!( let url = format!(
"https://{}/.well-known/webfinger?resource=acct:{}", "https://{}/.well-known/webfinger?resource=acct:{}",
@ -62,7 +56,7 @@ pub async fn resolve_user(acct: &str, host: &str) -> types::Person {
.send() .send()
.await .await
.unwrap() .unwrap()
.json::<webfinger::WebfingerResponse>() .json::<api::WebfingerHit>()
.await .await
.unwrap(); .unwrap();
@ -79,21 +73,18 @@ pub async fn resolve_user(acct: &str, host: &str) -> types::Person {
.send() .send()
.await .await
.unwrap() .unwrap()
.json::<types::Person>() .json::<ap::Person>()
.await .await
.unwrap() .unwrap()
} }
#[get("/test")] #[get("/test")]
pub async fn test( pub async fn test(outbound: &State<OutboundQueue>, mut db: Connection<Db>) -> &'static str {
outbound: &State<OutboundQueue>, use main::types::{ObjectUuid, api, get};
mut db: Connection<Db> outbound.0.send(main::ap::QueueMessage::Heartbeat);
) -> &'static str {
use main::types_rewrite::{ObjectUuid, get, api};
outbound.0.send(ap::QueueMessage::Heartbeat);
let id = ObjectUuid("9b9d497b-2731-435f-a929-e609ca69dac9".to_string()); let id = ObjectUuid("9b9d497b-2731-435f-a929-e609ca69dac9".to_string());
let user= dbg!(get::user_by_id(id, &mut **db).await.unwrap()); let user = dbg!(get::user_by_id(id, &mut db).await.unwrap());
let apu: api::Account = user.into(); let apu: api::Account = user.into();
dbg!(apu); dbg!(apu);

View file

@ -1,24 +1,19 @@
use crate::http_wrapper::HttpWrapper;
use chrono::Local; use chrono::Local;
use tracing::Instrument;
use rocket::serde::json::serde_json; use rocket::serde::json::serde_json;
use rocket::{State, post}; use rocket::{State, post};
use rocket_db_pools::Connection; use rocket_db_pools::Connection;
use sqlx::SqliteConnection;
use sqlx::Sqlite; use sqlx::Sqlite;
use sqlx::SqliteConnection;
use tracing::Instrument;
use tracing::{Level, debug, error, event, info, span, warn};
use url::Url; use url::Url;
use uuid::Uuid; use uuid::Uuid;
use tracing::{event, span, Level, debug, warn, info, error};
use crate::http_wrapper::HttpWrapper;
use main::types_rewrite::{make, db, ObjectUuid, ObjectUri, self, ap}; use crate::Db;
use main::types::{DbError, ObjectUri, ObjectUuid, ap, db, make};
use crate::{ fn handle_delete_activity(activity: ap::DeleteActivity) {
Db,
http::HttpClient,
types::{content::Post, activity},
};
fn handle_delete_activity(activity: activity::DeleteActivity) {
warn!(?activity, "unimplemented delete activity"); warn!(?activity, "unimplemented delete activity");
} }
@ -48,9 +43,12 @@ async fn create_user(
conn: impl sqlx::Executor<'_, Database = Sqlite>, conn: impl sqlx::Executor<'_, Database = Sqlite>,
) { ) {
// HACK: Allow us to formulate a `user@host` username by assuming the actor is on the same host as the user // HACK: Allow us to formulate a `user@host` username by assuming the actor is on the same host as the user
let url = Url::parse(&actor).unwrap(); let url = Url::parse(actor).unwrap();
let host = url.host_str().unwrap(); let host = url.host_str().unwrap();
info!("creating user '{}'@'{}' ({:#?})", user.preferred_username, host, user); info!(
"creating user '{}'@'{}' ({:#?})",
user.preferred_username, host, user
);
let (acct, remote) = if host != "ferri.amy.mov" { let (acct, remote) = if host != "ferri.amy.mov" {
(format!("{}@{}", user.preferred_username, host), true) (format!("{}@{}", user.preferred_username, host), true)
@ -87,7 +85,7 @@ async fn create_user(
} }
async fn create_follow( async fn create_follow(
activity: &activity::FollowActivity, activity: &ap::FollowActivity,
conn: impl sqlx::Executor<'_, Database = Sqlite>, conn: impl sqlx::Executor<'_, Database = Sqlite>,
) { ) {
sqlx::query!( sqlx::query!(
@ -96,7 +94,7 @@ async fn create_follow(
VALUES ( ?1, ?2, ?3 ) VALUES ( ?1, ?2, ?3 )
ON CONFLICT(id) DO NOTHING; ON CONFLICT(id) DO NOTHING;
"#, "#,
activity.id, activity.obj.id.0,
activity.actor, activity.actor,
activity.object activity.object
) )
@ -112,7 +110,7 @@ struct RemoteInfo {
} }
fn get_remote_info(actor_url: &str, person: &ap::Person) -> RemoteInfo { fn get_remote_info(actor_url: &str, person: &ap::Person) -> RemoteInfo {
let url = Url::parse(&actor_url).unwrap(); let url = Url::parse(actor_url).unwrap();
let host = url.host_str().unwrap(); let host = url.host_str().unwrap();
let (acct, remote) = if host != "ferri.amy.mov" { let (acct, remote) = if host != "ferri.amy.mov" {
@ -126,22 +124,23 @@ fn get_remote_info(actor_url: &str, person: &ap::Person) -> RemoteInfo {
RemoteInfo { RemoteInfo {
acct: acct.to_string(), acct: acct.to_string(),
web_url: url, web_url: url,
is_remote: remote is_remote: remote,
} }
} }
async fn resolve_actor<'a>( async fn resolve_actor<'a>(
actor_url: &str, actor_url: &str,
http: &HttpWrapper<'a>, http: &HttpWrapper<'a>,
conn: &mut SqliteConnection conn: &mut SqliteConnection,
) -> Result<db::User, types_rewrite::DbError> { ) -> Result<db::User, DbError> {
let person = { let person = {
let res = http.get_person(&actor_url).await; let res = http.get_person(actor_url).await;
if let Err(e) = res { if let Err(e) = res {
error!("could not load user {}: {}", actor_url, e.to_string()); error!("could not load user {}: {}", actor_url, e.to_string());
return Err(types_rewrite::DbError::FetchError( return Err(DbError::FetchError(format!(
format!("could not load user {}: {}", actor_url, e.to_string()) "could not load user {}: {}",
)) actor_url, e
)));
} }
res.unwrap() res.unwrap()
@ -172,9 +171,7 @@ async fn resolve_actor<'a>(
url: remote_info.web_url, url: remote_info.web_url,
created_at: main::ap::now(), created_at: main::ap::now(),
posts: db::UserPosts { posts: db::UserPosts { last_post_at: None },
last_post_at: None
}
}; };
Ok(make::new_user(user.clone(), conn).await.unwrap_or(user)) Ok(make::new_user(user.clone(), conn).await.unwrap_or(user))
@ -182,25 +179,28 @@ async fn resolve_actor<'a>(
async fn handle_follow_activity<'a>( async fn handle_follow_activity<'a>(
followed_account: &str, followed_account: &str,
activity: activity::FollowActivity, activity: ap::FollowActivity,
http: HttpWrapper<'a>, http: HttpWrapper<'a>,
mut db: Connection<Db>, mut db: Connection<Db>,
) { ) {
let actor = resolve_actor(&activity.actor, &http, &mut **db) let actor = resolve_actor(&activity.actor, &http, &mut db)
.await.unwrap(); .await
.unwrap();
info!("{:?} follows {}", actor, followed_account); info!("{:?} follows {}", actor, followed_account);
create_follow(&activity, &mut **db).await; create_follow(&activity, &mut **db).await;
let follower = main::ap::User::from_actor_id(&activity.actor, &mut **db).await; let follower = main::ap::User::from_actor_id(&activity.actor, &mut **db).await;
let followed = main::ap::User::from_id(&followed_account, &mut **db).await.unwrap(); let followed = main::ap::User::from_id(followed_account, &mut **db)
.await
.unwrap();
let outbox = main::ap::Outbox::for_user(followed.clone(), http.client()); let outbox = main::ap::Outbox::for_user(followed.clone(), http.client());
let activity = main::ap::Activity { let activity = main::ap::Activity {
id: format!("https://ferri.amy.mov/activities/{}", Uuid::new_v4()), id: format!("https://ferri.amy.mov/activities/{}", Uuid::new_v4()),
ty: main::ap::ActivityType::Accept, ty: main::ap::ActivityType::Accept,
object: activity.id, object: activity.obj.id.0,
..Default::default() ..Default::default()
}; };
@ -217,7 +217,7 @@ async fn handle_follow_activity<'a>(
outbox.post(req).await; outbox.post(req).await;
} }
async fn handle_like_activity(activity: activity::LikeActivity, mut db: Connection<Db>) { async fn handle_like_activity(activity: ap::LikeActivity, mut db: Connection<Db>) {
warn!(?activity, "unimplemented like activity"); warn!(?activity, "unimplemented like activity");
let target_post = sqlx::query!("SELECT * FROM post WHERE uri = ?1", activity.object) let target_post = sqlx::query!("SELECT * FROM post WHERE uri = ?1", activity.object)
@ -232,17 +232,17 @@ async fn handle_like_activity(activity: activity::LikeActivity, mut db: Connecti
} }
async fn handle_boost_activity<'a>( async fn handle_boost_activity<'a>(
activity: activity::BoostActivity, activity: ap::BoostActivity,
http: HttpWrapper<'a>, http: HttpWrapper<'a>,
mut db: Connection<Db>, mut db: Connection<Db>,
) { ) {
let key_id = "https://ferri.amy.mov/users/amy#main-key"; let key_id = "https://ferri.amy.mov/users/9b9d497b-2731-435f-a929-e609ca69dac9#main-key";
dbg!(&activity); dbg!(&activity);
let post = http let post = http
.client() .client()
.get(&activity.object) .get(&activity.object)
.activity() .activity()
.sign(&key_id) .sign(key_id)
.send() .send()
.await .await
.unwrap() .unwrap()
@ -252,10 +252,10 @@ async fn handle_boost_activity<'a>(
info!("{}", post); info!("{}", post);
let post = serde_json::from_str::<Post>(&post); let post = serde_json::from_str::<ap::Post>(&post);
if let Err(e) = post { if let Err(e) = post {
error!(?e, "when decoding post"); error!(?e, "when decoding post");
return return;
} }
let post = post.unwrap(); let post = post.unwrap();
@ -265,15 +265,19 @@ async fn handle_boost_activity<'a>(
let post_user = http.get_person(&attribution).await; let post_user = http.get_person(&attribution).await;
if let Err(e) = post_user { if let Err(e) = post_user {
error!("could not load post_user {}: {}", attribution, e.to_string()); error!(
return "could not load post_user {}: {}",
attribution,
e.to_string()
);
return;
} }
let post_user = post_user.unwrap(); let post_user = post_user.unwrap();
let user = http.get_person(&activity.actor).await; let user = http.get_person(&activity.actor).await;
if let Err(e) = user { if let Err(e) = user {
error!("could not load actor {}: {}", activity.actor, e.to_string()); error!("could not load actor {}: {}", activity.actor, e.to_string());
return return;
} }
let user = user.unwrap(); let user = user.unwrap();
@ -300,40 +304,58 @@ async fn handle_boost_activity<'a>(
let attr_id = attributed_user.id(); let attr_id = attributed_user.id();
// HACK: ON CONFLICT is to avoid duplicate remote posts coming in // HACK: ON CONFLICT is to avoid duplicate remote posts coming in
// check this better in future // check this better in future
sqlx::query!(" sqlx::query!(
"
INSERT INTO post (id, uri, user_id, content, created_at) INSERT INTO post (id, uri, user_id, content, created_at)
VALUES (?1, ?2, ?3, ?4, ?5) VALUES (?1, ?2, ?3, ?4, ?5)
ON CONFLICT(uri) DO NOTHING ON CONFLICT(uri) DO NOTHING
", reblog_id, post.id, attr_id, post.content, post.ts) ",
reblog_id,
post.obj.id.0,
attr_id,
post.content,
post.ts
)
.execute(&mut **db) .execute(&mut **db)
.await .await
.unwrap(); .unwrap();
let uri = format!("https://ferri.amy.mov/users/{}/posts/{}", actor_user.id(), base_id); let uri = format!(
"https://ferri.amy.mov/users/{}/posts/{}",
actor_user.id(),
base_id
);
let user_id = actor_user.id(); let user_id = actor_user.id();
sqlx::query!(" sqlx::query!(
"
INSERT INTO post (id, uri, user_id, content, created_at, boosted_post_id) INSERT INTO post (id, uri, user_id, content, created_at, boosted_post_id)
VALUES (?1, ?2, ?3, ?4, ?5, ?6) VALUES (?1, ?2, ?3, ?4, ?5, ?6)
", base_id, uri, user_id, "", now, reblog_id) ",
base_id,
uri,
user_id,
"",
now,
reblog_id
)
.execute(&mut **db) .execute(&mut **db)
.await .await
.unwrap(); .unwrap();
} }
async fn handle_create_activity<'a>( async fn handle_create_activity<'a>(
activity: activity::CreateActivity, activity: ap::CreateActivity,
http: HttpWrapper<'a>, http: HttpWrapper<'a>,
mut db: Connection<Db>, mut db: Connection<Db>,
) { ) {
assert!(&activity.object.ty == "Note"); assert!(activity.object.ty == ap::ActivityType::Note);
debug!("resolving user {}", activity.actor); debug!("resolving user {}", activity.actor);
let user = http.get_person(&activity.actor).await; let user = http.get_person(&activity.actor).await;
if let Err(e) = user { if let Err(e) = user {
error!("could not load user {}: {}", activity.actor, e.to_string()); error!("could not load user {}: {}", activity.actor, e.to_string());
return return;
} }
let user = user.unwrap(); let user = user.unwrap();
@ -351,7 +373,7 @@ async fn handle_create_activity<'a>(
let now = Local::now().to_rfc3339(); let now = Local::now().to_rfc3339();
let content = activity.object.content.clone(); let content = activity.object.content.clone();
let post_id = Uuid::new_v4().to_string(); let post_id = Uuid::new_v4().to_string();
let uri = activity.id; let uri = activity.obj.id.0;
info!(post_id, "creating post"); info!(post_id, "creating post");
@ -372,46 +394,45 @@ async fn handle_create_activity<'a>(
} }
#[post("/users/<user>/inbox", data = "<body>")] #[post("/users/<user>/inbox", data = "<body>")]
pub async fn inbox(db: Connection<Db>, http: &State<HttpClient>, user: &str, body: String) { pub async fn inbox(db: Connection<Db>, helpers: &State<crate::Helpers>, user: &str, body: String) {
let min = serde_json::from_str::<activity::MinimalActivity>(&body).unwrap(); debug!("body in inbox: {}", body);
let min = serde_json::from_str::<ap::MinimalActivity>(&body).unwrap();
let inbox_span = span!(Level::INFO, "inbox-post", user_id = user); let inbox_span = span!(Level::INFO, "inbox-post", user_id = user);
async move { async move {
event!(Level::INFO, ?min, "received an activity"); event!(Level::INFO, ?min, "received an activity");
let key_id = "https://ferri.amy.mov/users/amy#main-key"; let key_id = "https://ferri.amy.mov/users/9b9d497b-2731-435f-a929-e609ca69dac9#main-key";
let wrapper = HttpWrapper::new(http.inner(), key_id); let wrapper = HttpWrapper::new(&helpers.http, key_id);
match min.ty.as_str() { match min.ty {
"Delete" => { ap::ActivityType::Delete => {
let activity = serde_json::from_str::<activity::DeleteActivity>(&body).unwrap(); let activity = serde_json::from_str::<ap::DeleteActivity>(&body).unwrap();
handle_delete_activity(activity); handle_delete_activity(activity);
} }
"Follow" => { ap::ActivityType::Follow => {
let activity = serde_json::from_str::<activity::FollowActivity>(&body).unwrap(); let activity = serde_json::from_str::<ap::FollowActivity>(&body).unwrap();
handle_follow_activity(user, activity, wrapper, db).await; handle_follow_activity(user, activity, wrapper, db).await;
} }
"Create" => { ap::ActivityType::Create => {
let activity = serde_json::from_str::<activity::CreateActivity>(&body).unwrap(); let activity = serde_json::from_str::<ap::CreateActivity>(&body).unwrap();
handle_create_activity(activity, wrapper, db).await; handle_create_activity(activity, wrapper, db).await;
} }
"Like" => { ap::ActivityType::Like => {
let activity = serde_json::from_str::<activity::LikeActivity>(&body).unwrap(); let activity = serde_json::from_str::<ap::LikeActivity>(&body).unwrap();
handle_like_activity(activity, db).await; handle_like_activity(activity, db).await;
} }
"Announce" => { ap::ActivityType::Announce => {
let activity = serde_json::from_str::<activity::BoostActivity>(&body).unwrap(); let activity = serde_json::from_str::<ap::BoostActivity>(&body).unwrap();
handle_boost_activity(activity, wrapper, db).await; handle_boost_activity(activity, wrapper, db).await;
} }
ap::ActivityType::Note => todo!(),
act => { ap::ActivityType::Accept => todo!(),
warn!(act, body, "unknown activity");
} }
} }
debug!("body in inbox: {}", body);
}
// Allow the span to be used inside the async code // Allow the span to be used inside the async code
// https://docs.rs/tracing/latest/tracing/span/struct.EnteredSpan.html#deref-methods-Span // https://docs.rs/tracing/latest/tracing/span/struct.EnteredSpan.html#deref-methods-Span
.instrument(inbox_span).await; .instrument(inbox_span)
.await;
} }

View file

@ -77,12 +77,16 @@ pub struct NewTokenRequest {
#[post("/oauth/token", data = "<req>")] #[post("/oauth/token", data = "<req>")]
pub async fn new_token(req: Form<NewTokenRequest>, mut db: Connection<Db>) -> Json<Token> { pub async fn new_token(req: Form<NewTokenRequest>, mut db: Connection<Db>) -> Json<Token> {
let oauth = sqlx::query!(" let oauth = sqlx::query!(
"
SELECT o.*, a.* SELECT o.*, a.*
FROM oauth o FROM oauth o
INNER JOIN auth a ON a.token = ?2 INNER JOIN auth a ON a.token = ?2
WHERE o.access_token = ?1 WHERE o.access_token = ?1
", req.code, req.code) ",
req.code,
req.code
)
.fetch_one(&mut **db) .fetch_one(&mut **db)
.await .await
.unwrap(); .unwrap();

View file

@ -1,16 +1,23 @@
use main::ap; use rocket::{
use rocket::{get, http::ContentType, serde::json::Json, State, Responder}; Responder, State, get,
use rocket_db_pools::Connection; http::ContentType,
use rocket::response::Redirect; response::{Redirect, status::NotFound},
use rocket::response::status::NotFound; serde::json::Json,
use crate::{
Config,
Db,
types::{OrderedCollection, Person, UserKey, content},
}; };
use rocket_db_pools::Connection;
use serde::{Deserialize, Serialize};
use main::types::{Object, ObjectUri, ObjectUuid, ap, as_context, get};
use super::activity_type; use super::activity_type;
use crate::Db;
#[derive(Serialize, Deserialize)]
pub struct OrderedCollection {
ty: String,
total_items: i64,
ordered_items: Vec<String>,
}
#[get("/users/<_user>/inbox")] #[get("/users/<_user>/inbox")]
pub async fn inbox(_user: String) -> Json<OrderedCollection> { pub async fn inbox(_user: String) -> Json<OrderedCollection> {
@ -31,8 +38,11 @@ pub async fn outbox(_user: String) -> Json<OrderedCollection> {
} }
#[get("/users/<uuid>/followers")] #[get("/users/<uuid>/followers")]
pub async fn followers(mut db: Connection<Db>, uuid: &str) -> Result<Json<OrderedCollection>, NotFound<String>> { pub async fn followers(
let target = ap::User::from_id(uuid, &mut **db) mut db: Connection<Db>,
uuid: &str,
) -> Result<Json<OrderedCollection>, NotFound<String>> {
let target = main::ap::User::from_id(uuid, &mut **db)
.await .await
.map_err(|e| NotFound(e.to_string()))?; .map_err(|e| NotFound(e.to_string()))?;
@ -60,8 +70,11 @@ pub async fn followers(mut db: Connection<Db>, uuid: &str) -> Result<Json<Ordere
} }
#[get("/users/<uuid>/following")] #[get("/users/<uuid>/following")]
pub async fn following(mut db: Connection<Db>, uuid: &str) -> Result<Json<OrderedCollection>, NotFound<String>> { pub async fn following(
let target = ap::User::from_id(uuid, &mut **db) mut db: Connection<Db>,
uuid: &str,
) -> Result<Json<OrderedCollection>, NotFound<String>> {
let target = main::ap::User::from_id(uuid, &mut **db)
.await .await
.map_err(|e| NotFound(e.to_string()))?; .map_err(|e| NotFound(e.to_string()))?;
@ -91,10 +104,11 @@ pub async fn following(mut db: Connection<Db>, uuid: &str) -> Result<Json<Ordere
#[get("/users/<uuid>/posts/<post>")] #[get("/users/<uuid>/posts/<post>")]
pub async fn post( pub async fn post(
mut db: Connection<Db>, mut db: Connection<Db>,
config: &State<Config>, helpers: &State<crate::Helpers>,
uuid: &str, uuid: &str,
post: String, post: String,
) -> (ContentType, Json<content::Post>) { ) -> (ContentType, Json<ap::Post>) {
let config = &helpers.config;
let post = sqlx::query!( let post = sqlx::query!(
r#" r#"
SELECT * FROM post WHERE id = ?1 SELECT * FROM post WHERE id = ?1
@ -107,11 +121,13 @@ pub async fn post(
( (
activity_type(), activity_type(),
Json(content::Post { Json(ap::Post {
context: "https://www.w3.org/ns/activitystreams".to_string(), obj: Object {
id: config.post_url(uuid, &post.id), context: as_context(),
id: ObjectUri(config.post_url(uuid, &post.id)),
},
attributed_to: Some(config.user_url(uuid)), attributed_to: Some(config.user_url(uuid)),
ty: "Note".to_string(), ty: ap::ActivityType::Note,
content: post.content, content: post.content,
ts: post.created_at, ts: post.created_at,
to: vec![config.followers_url(uuid)], to: vec![config.followers_url(uuid)],
@ -138,38 +154,17 @@ fn ap_ok<T, E>(t: T) -> Result<ActivityResponse<T>, E> {
#[get("/users/<uuid>")] #[get("/users/<uuid>")]
pub async fn user( pub async fn user(
mut db: Connection<Db>, mut db: Connection<Db>,
config: &State<Config>, uuid: &str,
uuid: &str ) -> Result<ActivityResponse<Json<ap::Person>>, UserFetchError> {
) -> Result<ActivityResponse<Json<Person>>, UserFetchError> {
if uuid == "amy" { if uuid == "amy" {
return Err( return Err(UserFetchError::Moved(Redirect::permanent(
UserFetchError::Moved( "https://ferri.amy.mov/users/9b9d497b-2731-435f-a929-e609ca69dac9",
Redirect::permanent("https://ferri.amy.mov/users/9b9d497b-2731-435f-a929-e609ca69dac9") )));
)
)
} }
let user = ap::User::from_id(uuid, &mut **db) let user = get::user_by_id(ObjectUuid(uuid.to_string()), &mut db)
.await .await
.map_err(|e| UserFetchError::NotFound(NotFound(e.to_string())))?; .map_err(|e| UserFetchError::NotFound(NotFound(e.to_string())))?;
let person = Person { ap_ok(Json(user.into()))
context: "https://www.w3.org/ns/activitystreams".to_string(),
ty: "Person".to_string(),
id: config.user_url(user.id()),
name: user.username().to_string(),
preferred_username: user.display_name().to_string(),
followers: config.followers_url(user.id()),
following: config.following_url(user.id()),
summary: format!("ferri {}", user.username()),
inbox: config.inbox_url(user.id()),
outbox: config.outbox_url(user.id()),
public_key: Some(UserKey {
id: format!("https://ferri.amy.mov/users/{}#main-key", uuid),
owner: config.user_url(user.id()),
public_key: include_str!("../../../public.pem").to_string(),
}),
};
ap_ok(Json(person))
} }

View file

@ -1,14 +1,10 @@
use crate::Db;
use main::ap; use main::ap;
use rocket::{get, serde::json::Json, State}; use main::types::api;
use rocket::{State, get, serde::json::Json};
use rocket_db_pools::Connection; use rocket_db_pools::Connection;
use tracing::info; use tracing::info;
use crate::{
Config,
Db,
types::webfinger::{Link, WebfingerResponse},
};
#[get("/.well-known/host-meta")] #[get("/.well-known/host-meta")]
pub async fn host_meta() -> &'static str { pub async fn host_meta() -> &'static str {
r#" r#"
@ -21,26 +17,31 @@ pub async fn host_meta() -> &'static str {
// https://mastodon.social/.well-known/webfinger?resource=acct:gargron@mastodon.social // https://mastodon.social/.well-known/webfinger?resource=acct:gargron@mastodon.social
#[get("/.well-known/webfinger?<resource>")] #[get("/.well-known/webfinger?<resource>")]
pub async fn webfinger(mut db: Connection<Db>, config: &State<Config>, resource: &str) -> Json<WebfingerResponse> { pub async fn webfinger(
mut db: Connection<Db>,
helpers: &State<crate::Helpers>,
resource: &str,
) -> Json<api::WebfingerHit> {
let config = &helpers.config;
info!(?resource, "incoming webfinger request"); info!(?resource, "incoming webfinger request");
let acct = resource.strip_prefix("acct:").unwrap(); let acct = resource.strip_prefix("acct:").unwrap();
let (user, _) = acct.split_once("@").unwrap(); let (user, _) = acct.split_once("@").unwrap();
let user = ap::User::from_username(user, &mut **db).await; let user = ap::User::from_username(user, &mut **db).await;
Json(WebfingerResponse { Json(api::WebfingerHit {
subject: resource.to_string(), subject: resource.to_string(),
aliases: vec![ aliases: vec![
config.user_url(user.id()), config.user_url(user.id()),
config.user_web_url(user.username()) config.user_web_url(user.username()),
], ],
links: vec![ links: vec![
Link { api::WebfingerLink {
rel: "http://webfinger.net/rel/profile-page".to_string(), rel: "http://webfinger.net/rel/profile-page".to_string(),
ty: Some("text/html".to_string()), ty: Some("text/html".to_string()),
href: Some(config.user_web_url(user.username())), href: Some(config.user_web_url(user.username())),
}, },
Link { api::WebfingerLink {
rel: "self".to_string(), rel: "self".to_string(),
ty: Some("application/activity+json".to_string()), ty: Some("application/activity+json".to_string()),
href: Some(config.user_url(user.id())), href: Some(config.user_url(user.id())),

View file

@ -1,12 +1,12 @@
use thiserror::Error;
use tracing::{error, event, Level};
use crate::http::HttpClient; use crate::http::HttpClient;
use main::types_rewrite::ap; use main::types::ap;
use std::fmt::Debug; use std::fmt::Debug;
use thiserror::Error;
use tracing::{Level, error, event};
pub struct HttpWrapper<'a> { pub struct HttpWrapper<'a> {
client: &'a HttpClient, client: &'a HttpClient,
key_id: &'a str key_id: &'a str,
} }
#[derive(Error, Debug)] #[derive(Error, Debug)]
@ -19,21 +19,23 @@ pub enum HttpError {
impl<'a> HttpWrapper<'a> { impl<'a> HttpWrapper<'a> {
pub fn new(client: &'a HttpClient, key_id: &'a str) -> HttpWrapper<'a> { pub fn new(client: &'a HttpClient, key_id: &'a str) -> HttpWrapper<'a> {
Self { Self { client, key_id }
client,
key_id
}
} }
pub fn client(&self) -> &'a HttpClient { pub fn client(&self) -> &'a HttpClient {
&self.client self.client
} }
async fn get<T : serde::de::DeserializeOwned + Debug>(&self, ty: &str, url: &str) -> Result<T, HttpError> { async fn get<T: serde::de::DeserializeOwned + Debug>(
&self,
ty: &str,
url: &str,
) -> Result<T, HttpError> {
let ty = ty.to_string(); let ty = ty.to_string();
event!(Level::INFO, url, "loading {}", ty); event!(Level::INFO, url, "loading {}", ty);
let http_result = self.client let http_result = self
.client
.get(url) .get(url)
.sign(self.key_id) .sign(self.key_id)
.activity() .activity()
@ -51,15 +53,15 @@ impl <'a> HttpWrapper<'a> {
return Err(HttpError::LoadFailure(ty, url.to_string())); return Err(HttpError::LoadFailure(ty, url.to_string()));
} }
let decoded = serde_json::from_str::<T>(&raw_body.unwrap()); let raw_body = raw_body.unwrap();
let decoded = serde_json::from_str::<T>(&raw_body);
if let Err(e) = decoded { if let Err(e) = decoded {
error!("could not parse {} for url {}: {:#?}", ty, url, e); error!(
return Err(HttpError::ParseFailure( "could not parse {} for url {}: {:#?} {}",
ty, ty, url, e, &raw_body
url.to_string(), );
e.to_string() return Err(HttpError::ParseFailure(ty, url.to_string(), e.to_string()));
));
} }
Ok(decoded.unwrap()) Ok(decoded.unwrap())

View file

@ -19,7 +19,6 @@ use rocket_db_pools::{Connection, Database, sqlx};
mod cors; mod cors;
mod endpoints; mod endpoints;
mod types;
mod http_wrapper; mod http_wrapper;
#[derive(Database)] #[derive(Database)]
@ -32,9 +31,7 @@ async fn user_profile() -> (ContentType, &'static str) {
} }
#[get("/activities/<_activity>")] #[get("/activities/<_activity>")]
async fn activity_endpoint(_activity: String) { async fn activity_endpoint(_activity: String) {}
}
#[derive(Debug)] #[derive(Debug)]
pub struct AuthenticatedUser { pub struct AuthenticatedUser {
@ -45,8 +42,7 @@ pub struct AuthenticatedUser {
} }
#[derive(Debug)] #[derive(Debug)]
pub enum LoginError { pub enum LoginError {}
}
#[rocket::async_trait] #[rocket::async_trait]
impl<'a> FromRequest<'a> for AuthenticatedUser { impl<'a> FromRequest<'a> for AuthenticatedUser {
@ -90,6 +86,11 @@ impl<'a> FromRequest<'a> for AuthenticatedUser {
pub struct OutboundQueue(pub ap::QueueHandle); pub struct OutboundQueue(pub ap::QueueHandle);
pub struct InboundQueue(pub ap::QueueHandle); pub struct InboundQueue(pub ap::QueueHandle);
pub struct Helpers {
http: http::HttpClient,
config: Config,
}
pub fn launch(cfg: Config) -> Rocket<Build> { pub fn launch(cfg: Config) -> Rocket<Build> {
let format = fmt::format() let format = fmt::format()
.with_ansi(true) .with_ansi(true)
@ -111,10 +112,11 @@ pub fn launch(cfg: Config) -> Rocket<Build> {
let inbound = ap::RequestQueue::new("inbound"); let inbound = ap::RequestQueue::new("inbound");
let inbound_handle = inbound.spawn(); let inbound_handle = inbound.spawn();
let http_client = http::HttpClient::new();
build() build()
.manage(cfg) .manage(Helpers {
.manage(http_client) config: cfg,
http: http::HttpClient::new(),
})
.manage(OutboundQueue(outbound_handle)) .manage(OutboundQueue(outbound_handle))
.manage(InboundQueue(inbound_handle)) .manage(InboundQueue(inbound_handle))
.attach(Db::init()) .attach(Db::init())

View file

@ -1,75 +0,0 @@
use rocket::serde::{Deserialize, Serialize};
use crate::types::content::Post;
#[derive(Serialize, Deserialize, Debug)]
#[serde(crate = "rocket::serde")]
pub struct MinimalActivity {
pub id: String,
#[serde(rename = "type")]
pub ty: String,
}
pub type DeleteActivity = BasicActivity;
pub type LikeActivity = BasicActivity;
#[derive(Serialize, Deserialize, Debug)]
#[serde(crate = "rocket::serde")]
pub struct BasicActivity {
pub id: String,
#[serde(rename = "type")]
pub ty: String,
pub object: String,
pub actor: String,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(crate = "rocket::serde")]
pub struct CreateActivity {
pub id: String,
#[serde(rename = "type")]
pub ty: String,
pub object: Post,
pub actor: String,
pub to: Vec<String>,
pub cc: Vec<String>,
#[serde(rename = "published")]
pub ts: String,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(crate = "rocket::serde")]
pub struct FollowActivity {
pub id: String,
#[serde(rename = "type")]
pub ty: String,
pub object: String,
pub actor: String,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(crate = "rocket::serde")]
pub struct AcceptActivity {
#[serde(rename = "type")]
pub ty: String,
pub object: String,
pub actor: String,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(crate = "rocket::serde")]
pub struct BoostActivity {
#[serde(rename = "type")]
pub ty: String,
pub id: String,
pub actor: String,
pub published: String,
pub to: Vec<String>,
pub cc: Vec<String>,
pub object: String,
}

View file

@ -1,22 +0,0 @@
use rocket::serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Default)]
#[serde(crate = "rocket::serde")]
#[deprecated]
pub struct Post {
// FIXME: This is because Masto sends an array but we don't care
#[serde(rename = "@context")]
#[serde(skip_deserializing)]
pub context: String,
pub id: String,
#[serde(rename = "type")]
pub ty: String,
#[serde(rename = "published")]
pub ts: String,
pub content: String,
pub to: Vec<String>,
pub cc: Vec<String>,
#[serde(rename = "attributedTo")]
pub attributed_to: Option<String>
}

View file

@ -1,61 +0,0 @@
pub mod activity;
pub mod content;
pub mod oauth;
pub mod webfinger;
use rocket::serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
#[serde(crate = "rocket::serde")]
#[deprecated]
pub struct UserKey {
pub id: String,
pub owner: String,
#[serde(rename = "publicKeyPem")]
pub public_key: String,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
#[serde(crate = "rocket::serde")]
#[deprecated]
pub struct Person {
// FIXME: This is because Masto sends an array but we don't care
#[serde(rename = "@context")]
#[serde(skip_deserializing)]
pub context: String,
pub id: String,
#[serde(rename = "type")]
pub ty: String,
pub following: String,
pub followers: String,
pub summary: String,
pub inbox: String,
pub outbox: String,
pub preferred_username: String,
#[serde(default)]
pub name: String,
pub public_key: Option<UserKey>,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
#[serde(crate = "rocket::serde")]
pub struct Object {
pub id: String,
#[serde(rename = "type")]
pub ty: String,
pub object: String,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
#[serde(crate = "rocket::serde")]
pub struct OrderedCollection {
#[serde(rename = "type")]
pub ty: String,
pub total_items: u64,
pub ordered_items: Vec<String>,
}

View file

@ -1,24 +0,0 @@
use rocket::{
FromForm,
serde::{Deserialize, Serialize},
};
#[derive(Serialize, Deserialize, Debug, FromForm, Clone)]
#[serde(crate = "rocket::serde")]
#[deprecated]
pub struct App {
pub client_name: String,
pub redirect_uris: Vec<String>,
pub scopes: String,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(crate = "rocket::serde")]
#[deprecated]
pub struct CredentialApplication {
pub name: String,
pub scopes: String,
pub redirect_uris: Vec<String>,
pub client_id: String,
pub client_secret: String,
}

View file

@ -1,20 +0,0 @@
use rocket::serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize, Debug)]
#[serde(crate = "rocket::serde")]
#[deprecated]
pub struct Link {
pub rel: String,
#[serde(rename = "type")]
pub ty: Option<String>,
pub href: Option<String>,
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(crate = "rocket::serde")]
#[deprecated]
pub struct WebfingerResponse {
pub subject: String,
pub aliases: Vec<String>,
pub links: Vec<Link>,
}