Compare commits

...

2 commits

Author SHA1 Message Date
3719fae102
feat: logging; fixes; error handloing 2025-04-25 16:46:47 +01:00
9c7c2858cc
feat: auth basics 2025-04-24 20:21:59 +01:00
22 changed files with 414 additions and 106 deletions

18
Cargo.lock generated
View file

@ -1264,6 +1264,8 @@ dependencies = [
"serde",
"serde_json",
"sqlx",
"thiserror 2.0.12",
"tracing",
"url",
"uuid",
]
@ -2156,10 +2158,13 @@ version = "0.1.0"
dependencies = [
"chrono",
"main",
"rand 0.8.5",
"reqwest",
"rocket",
"rocket_db_pools",
"sqlx",
"tracing",
"tracing-subscriber",
"url",
"uuid",
]
@ -2739,6 +2744,16 @@ dependencies = [
"tracing-core",
]
[[package]]
name = "tracing-serde"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1"
dependencies = [
"serde",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.19"
@ -2749,12 +2764,15 @@ dependencies = [
"nu-ansi-term",
"once_cell",
"regex",
"serde",
"serde_json",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
"tracing-serde",
]
[[package]]

View file

@ -8,4 +8,11 @@ serde = "1.0.219"
rocket = { version = "0.5.1", features = ["json"] }
sqlx = { version = "0.7", features = [ "runtime-tokio", "sqlite", "macros" ], default-features = false }
uuid = { version = "1.16.0", features = ["v4"] }
chrono = "0.4.40"
chrono = "0.4.40"
rand = "0.8"
thiserror = "2.0.12"
tracing = "0.1.40"
tracing-appender = "0.2.3"
tracing-log = "0.2.0"
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json", "registry", "smallvec"] }

View file

@ -5,13 +5,15 @@ edition = "2024"
[dependencies]
serde = { workspace = true }
serde_json = "1.0.140"
sqlx = { workspace = true }
chrono = { workspace = true }
reqwest = { workspace = true }
uuid = { workspace = true }
rand = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
base64 = "0.22.1"
rsa = { version = "0.9.8", features = ["sha2"] }
rand = "0.8"
url = "2.5.4"
serde_json = "1.0.140"

View file

@ -1,8 +1,9 @@
use crate::ap::{Actor, User, http};
use chrono::{DateTime, Local};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::Sqlite;
use std::fmt::Debug;
use tracing::{event, Level};
#[derive(Debug, Clone)]
pub enum ActivityType {
@ -28,7 +29,7 @@ pub struct Activity<T: Serialize + Debug> {
pub id: String,
pub ty: ActivityType,
pub object: T,
pub published: DateTime<Local>,
pub published: DateTime<Utc>,
pub to: Vec<String>,
pub cc: Vec<String>,
}
@ -39,7 +40,7 @@ impl<T: Serialize + Debug + Default> Default for Activity<T> {
id: Default::default(),
ty: ActivityType::Unknown,
object: Default::default(),
published: Local::now(),
published: Utc::now(),
to: Default::default(),
cc: Default::default(),
}
@ -102,19 +103,18 @@ impl<'a> Outbox<'a> {
}
pub async fn post<T: Serialize + Debug>(&self, activity: OutgoingActivity<T>) {
dbg!(&activity);
event!(Level::INFO, ?activity, "activity in outbox");
let raw = RawActivity {
context: "https://www.w3.org/ns/activitystreams".to_string(),
id: activity.req.id,
id: activity.req.id.clone(),
ty: activity.req.ty.to_raw(),
actor: self.user.actor().id().to_string(),
object: activity.req.object,
published: activity.req.published.to_rfc3339(),
};
dbg!(&raw);
let follow_res = self
let outbox_res = self
.transport
.post(activity.to.inbox())
.activity()
@ -127,7 +127,10 @@ impl<'a> Outbox<'a> {
.await
.unwrap();
dbg!(follow_res);
event!(Level::DEBUG,
outbox_res, activity = activity.req.id,
"got response for outbox dispatch"
);
}
pub fn for_user(user: User, transport: &'a OutboxTransport) -> Outbox<'a> {

View file

@ -12,6 +12,7 @@ use rsa::{
use base64::prelude::*;
use chrono::Utc;
use tracing::{event, Level};
pub struct HttpClient {
client: reqwest::Client,
@ -59,7 +60,8 @@ impl RequestBuilder {
}
pub async fn send(self) -> Result<Response, reqwest::Error> {
dbg!(&self.inner);
event!(Level::DEBUG, ?self.inner, "sending an http request");
self.inner.send().await
}

View file

@ -12,7 +12,7 @@ pub use user::*;
mod post;
pub use post::*;
pub const AS_CONTEXT: &'static str = "https://www.w3.org/ns/activitystreams";
pub const AS_CONTEXT: &str = "https://www.w3.org/ns/activitystreams";
pub fn new_id() -> String {
Uuid::new_v4().to_string()

View file

@ -3,7 +3,7 @@ use chrono::{DateTime, Utc};
use serde::Serialize;
use sqlx::Sqlite;
const POST_TYPE: &'static str = "Post";
const POST_TYPE: &str = "Post";
#[derive(Clone)]
pub struct Post {

View file

@ -1,5 +1,6 @@
use sqlx::Sqlite;
use std::fmt::Debug;
use thiserror::Error;
#[derive(Debug, Clone)]
pub struct Actor {
@ -20,6 +21,10 @@ impl Actor {
pub fn inbox(&self) -> &str {
&self.inbox
}
pub fn outbox(&self) -> &str {
&self.outbox
}
}
#[derive(Debug, Clone)]
@ -30,6 +35,13 @@ pub struct User {
display_name: String,
}
#[derive(Error, Debug)]
pub enum UserError {
#[error("user `{0}` not found")]
NotFound(String),
}
impl User {
pub fn id(&self) -> &str {
&self.id
@ -55,7 +67,7 @@ impl User {
format!("https://ferri.amy.mov/users/{}", self.id())
}
pub async fn from_id(uuid: &str, conn: impl sqlx::Executor<'_, Database = Sqlite>) -> User {
pub async fn from_id(uuid: &str, conn: impl sqlx::Executor<'_, Database = Sqlite>) -> Result<User, UserError> {
let user = sqlx::query!(
r#"
SELECT u.*, a.id as "actor_own_id", a.inbox, a.outbox
@ -65,10 +77,11 @@ impl User {
"#,
uuid
)
.fetch_one(conn)
.await
.unwrap();
User {
.fetch_one(conn)
.await
.map_err(|_| UserError::NotFound(uuid.to_string()))?;
Ok(User {
id: user.id,
username: user.username,
actor: Actor {
@ -77,7 +90,7 @@ impl User {
outbox: user.outbox,
},
display_name: user.display_name,
}
})
}
pub async fn from_username(

View file

@ -1,2 +1,11 @@
pub mod ap;
pub mod config;
use rand::{Rng, distributions::Alphanumeric};
pub fn gen_token(len: usize) -> String {
rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(len)
.map(char::from)
.collect()
}

View file

@ -5,11 +5,14 @@ edition = "2024"
[dependencies]
main = { path = "../ferri-main/" }
rocket = { workspace = true }
rocket_db_pools = { version = "0.2.0", features = ["sqlx_sqlite"] }
url = "2.5.4"
rocket = { workspace = true }
reqwest = { workspace = true }
sqlx = { workspace = true }
uuid = { workspace = true }
chrono = { workspace = true }
url = "2.5.4"
rand = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }

View file

@ -1,14 +1,44 @@
use rocket::{form::Form, post, serde::json::Json};
use crate::Db;
use crate::types::oauth::{App, CredentialApplication};
use rocket_db_pools::Connection;
#[post("/apps", data = "<app>")]
pub async fn new_app(app: Form<App>) -> Json<CredentialApplication> {
pub async fn new_app(app: Form<App>, mut db: Connection<Db>) -> Json<CredentialApplication> {
let secret = main::gen_token(15);
// Abort when we encounter a duplicate
let is_app_present = sqlx::query!(
r#"
INSERT INTO app (client_id, client_secret, scopes)
VALUES (?1, ?2, ?3)
"#,
app.client_name,
app.scopes,
secret
)
.execute(&mut **db)
.await
.is_err();
let mut app: App = app.clone();
if is_app_present {
let existing_app = sqlx::query!("SELECT * FROM app WHERE client_id = ?1", app.client_name)
.fetch_one(&mut **db)
.await
.unwrap();
app.client_name = existing_app.client_id;
app.scopes = existing_app.scopes;
}
Json(CredentialApplication {
name: app.client_name.clone(),
scopes: app.scopes.clone(),
redirect_uris: app.redirect_uris.clone(),
client_id: format!("id-for-{}", app.client_name),
client_secret: format!("secret-for-{}", app.client_name),
client_id: app.client_name.clone(),
client_secret: secret,
})
}

View file

@ -1,17 +1,17 @@
use crate::timeline::TimelineStatus;
use chrono::Local;
use main::ap::{self, http::HttpClient};
use rocket::{
FromForm, State,
form::Form,
post,
get,
serde::{Deserialize, Serialize, json::Json},
};
use rocket_db_pools::Connection;
use uuid::Uuid;
use crate::api::user::CredentialAcount;
use crate::{AuthenticatedUser, Db, types::content};
use crate::{AuthenticatedUser, Db};
#[derive(Serialize, Deserialize, Debug, FromForm)]
#[serde(crate = "rocket::serde")]
@ -19,17 +19,36 @@ pub struct Status {
status: String,
}
#[derive(Serialize, Deserialize, Debug, FromForm)]
#[serde(crate = "rocket::serde")]
pub struct StatusContext {
ancestors: Vec<Status>,
descendants: Vec<Status>
}
#[get("/statuses/<status>/context")]
pub async fn status_context(
status: &str,
user: AuthenticatedUser,
mut db: Connection<Db>
) -> Json<StatusContext> {
Json(StatusContext {
ancestors: vec![],
descendants: vec![],
})
}
async fn create_status(
user: AuthenticatedUser,
mut db: Connection<Db>,
http: &HttpClient,
status: &Status,
) -> TimelineStatus {
let user = ap::User::from_id(&user.username, &mut **db).await;
let user = ap::User::from_id(&user.id, &mut **db).await.unwrap();
let outbox = ap::Outbox::for_user(user.clone(), http);
let post_id = ap::new_id();
let now = ap::new_ts();
let now = ap::now();
let post = ap::Post::from_parts(post_id, status.status.clone(), user.clone())
.to(format!("{}/followers", user.uri()))
@ -55,6 +74,7 @@ async fn create_status(
ty: ap::ActivityType::Create,
object: post.clone().to_ap(),
to: vec![format!("{}/followers", user.uri())],
published: now,
cc: vec!["https://www.w3.org/ns/activitystreams#Public".to_string()],
..Default::default()
};

View file

@ -1,4 +1,4 @@
use crate::{Db, endpoints::api::user::CredentialAcount};
use crate::{AuthenticatedUser, Db, endpoints::api::user::CredentialAcount};
use rocket::{
get,
serde::{Deserialize, Serialize, json::Json},
@ -32,7 +32,11 @@ pub struct TimelineStatus {
}
#[get("/timelines/home?<limit>")]
pub async fn home(mut db: Connection<Db>, limit: i64) -> Json<Vec<TimelineStatus>> {
pub async fn home(
mut db: Connection<Db>,
limit: i64,
user: AuthenticatedUser,
) -> Json<Vec<TimelineStatus>> {
let posts = sqlx::query!(
r#"
SELECT p.id as "post_id", u.id as "user_id", p.content, p.uri as "post_uri",

View file

@ -5,6 +5,7 @@ use rocket::{
};
use rocket_db_pools::Connection;
use uuid::Uuid;
use rocket::response::status::NotFound;
use crate::timeline::{TimelineAccount, TimelineStatus};
use crate::{AuthenticatedUser, Db, http::HttpClient};
@ -62,9 +63,12 @@ pub async fn new_follow(
http: &State<HttpClient>,
uuid: &str,
user: AuthenticatedUser,
) {
) -> Result<(), NotFound<String>> {
let follower = ap::User::from_actor_id(&user.actor_id, &mut **db).await;
let followed = ap::User::from_id(uuid, &mut **db).await;
let followed = ap::User::from_id(uuid, &mut **db)
.await
.map_err(|e| NotFound(e.to_string()))?;
let outbox = ap::Outbox::for_user(follower.clone(), http.inner());
@ -83,6 +87,8 @@ pub async fn new_follow(
req.save(&mut **db).await;
outbox.post(req).await;
Ok(())
}
#[get("/accounts/<uuid>")]
@ -90,10 +96,12 @@ pub async fn account(
mut db: Connection<Db>,
uuid: &str,
user: AuthenticatedUser,
) -> Json<TimelineAccount> {
let user = ap::User::from_id(uuid, &mut **db).await;
) -> Result<Json<TimelineAccount>, NotFound<String>> {
let user = ap::User::from_id(uuid, &mut **db)
.await
.map_err(|e| NotFound(e.to_string()))?;
let user_uri = format!("https://ferri.amy.mov/users/{}", user.username());
Json(CredentialAcount {
Ok(Json(CredentialAcount {
id: user.id().to_string(),
username: user.username().to_string(),
acct: user.username().to_string(),
@ -112,7 +120,7 @@ pub async fn account(
following_count: 1,
statuses_count: 1,
last_status_at: "2025-04-10T22:14:34Z".to_string(),
})
}))
}
#[get("/accounts/<uuid>/statuses?<limit>")]
@ -121,8 +129,10 @@ pub async fn statuses(
uuid: &str,
limit: Option<i64>,
user: AuthenticatedUser,
) -> Json<Vec<TimelineStatus>> {
let user = ap::User::from_id(uuid, &mut **db).await;
) -> Result<Json<Vec<TimelineStatus>>, NotFound<String>> {
let user = ap::User::from_id(uuid, &mut **db)
.await
.map_err(|e| NotFound(e.to_string()))?;
let uid = user.id();
let posts = sqlx::query!(
@ -181,5 +191,5 @@ pub async fn statuses(
});
}
Json(out)
Ok(Json(out))
}

View file

@ -118,7 +118,5 @@ pub async fn test(http: &State<HttpClient>) -> &'static str {
.await
.unwrap();
dbg!(follow);
"Hello, world!"
}

View file

@ -6,6 +6,7 @@ use rocket_db_pools::Connection;
use sqlx::Sqlite;
use url::Url;
use uuid::Uuid;
use tracing::{event, span, Level, debug, warn, info};
use crate::{
Db,
@ -14,12 +15,12 @@ use crate::{
};
fn handle_delete_activity(activity: activity::DeleteActivity) {
dbg!(activity);
warn!(?activity, "unimplemented delete activity");
}
async fn create_actor(
user: &Person,
actor: String,
actor: &str,
conn: impl sqlx::Executor<'_, Database = Sqlite>,
) {
sqlx::query!(
@ -39,7 +40,7 @@ async fn create_actor(
async fn create_user(
user: &Person,
actor: String,
actor: &str,
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
@ -84,7 +85,7 @@ async fn create_follow(
}
async fn handle_follow_activity(
followed_account: String,
followed_account: &str,
activity: activity::FollowActivity,
http: &HttpClient,
mut db: Connection<Db>,
@ -99,8 +100,8 @@ async fn handle_follow_activity(
.await
.unwrap();
create_actor(&user, activity.actor.clone(), &mut **db).await;
create_user(&user, activity.actor.clone(), &mut **db).await;
create_actor(&user, &activity.actor, &mut **db).await;
create_user(&user, &activity.actor, &mut **db).await;
create_follow(&activity, &mut **db).await;
let follower = ap::User::from_actor_id(&activity.actor, &mut **db).await;
@ -128,11 +129,17 @@ async fn handle_follow_activity(
}
async fn handle_like_activity(activity: activity::LikeActivity, mut db: Connection<Db>) {
warn!(?activity, "unimplemented like activity");
let target_post = sqlx::query!("SELECT * FROM post WHERE uri = ?1", activity.object)
.fetch_one(&mut **db)
.await
.unwrap();
dbg!(&target_post);
.await;
if let Ok(post) = target_post {
warn!(?post, "tried to like post");
} else {
warn!(post = ?activity.object, "could not find post");
}
}
async fn handle_create_activity(
@ -141,6 +148,8 @@ async fn handle_create_activity(
mut db: Connection<Db>,
) {
assert!(&activity.object.ty == "Note");
debug!("resolving user {}", activity.actor);
let user = http
.get(&activity.actor)
.activity()
@ -151,10 +160,14 @@ async fn handle_create_activity(
.await
.unwrap();
create_actor(&user, activity.actor.clone(), &mut **db).await;
create_user(&user, activity.actor.clone(), &mut **db).await;
debug!("creating actor {}", activity.actor);
create_actor(&user, &activity.actor, &mut **db).await;
debug!("creating user {}", activity.actor);
create_user(&user, &activity.actor, &mut **db).await;
let user = ap::User::from_actor_id(&activity.actor, &mut **db).await;
debug!("user created {:?}", user);
let user_id = user.id();
let now = Local::now().to_rfc3339();
@ -162,6 +175,8 @@ async fn handle_create_activity(
let post_id = Uuid::new_v4().to_string();
let uri = activity.id;
info!(post_id, "creating post");
sqlx::query!(
r#"
INSERT INTO post (id, uri, user_id, content, created_at)
@ -173,14 +188,19 @@ async fn handle_create_activity(
content,
now
)
.execute(&mut **db)
.await
.unwrap();
.execute(&mut **db)
.await
.unwrap();
}
#[post("/users/<user>/inbox", data = "<body>")]
pub async fn inbox(db: Connection<Db>, http: &State<HttpClient>, user: String, body: String) {
pub async fn inbox(db: Connection<Db>, http: &State<HttpClient>, user: &str, body: String) {
let min = serde_json::from_str::<activity::MinimalActivity>(&body).unwrap();
let inbox_span = span!(Level::INFO, "inbox-post", user_id = user);
let _enter = inbox_span.enter();
event!(Level::INFO, ?min, "received an activity");
match min.ty.as_str() {
"Delete" => {
let activity = serde_json::from_str::<activity::DeleteActivity>(&body).unwrap();
@ -198,11 +218,11 @@ pub async fn inbox(db: Connection<Db>, http: &State<HttpClient>, user: String, b
let activity = serde_json::from_str::<activity::LikeActivity>(&body).unwrap();
handle_like_activity(activity, db).await;
}
unknown => {
eprintln!("WARN: Unknown activity '{}' - {}", unknown, body);
act => {
warn!(act, body, "unknown activity");
}
}
dbg!(min);
println!("Body in inbox: {}", body);
debug!("body in inbox: {}", body);
drop(_enter)
}

View file

@ -1,8 +1,12 @@
use crate::Db;
use rocket::{
FromForm,
form::Form,
get, post,
response::Redirect,
serde::{Deserialize, Serialize, json::Json},
};
use rocket_db_pools::Connection;
#[get("/oauth/authorize?<client_id>&<scope>&<redirect_uri>&<response_type>")]
pub async fn authorize(
@ -10,11 +14,45 @@ pub async fn authorize(
scope: &str,
redirect_uri: &str,
response_type: &str,
mut db: Connection<Db>,
) -> Redirect {
Redirect::temporary(format!(
"{}?code=code-for-{}&state=state-for-{}",
redirect_uri, client_id, client_id
))
// For now, we will always authorize the request and assign it to an admin user
let user_id = "9b9d497b-2731-435f-a929-e609ca69dac9";
let code = main::gen_token(15);
// This will act as a token for the user, but we will in future say that it expires very shortly
// and can only be used for obtaining an access token etc
sqlx::query!(
r#"
INSERT INTO auth (token, user_id)
VALUES (?1, ?2)
"#,
code,
user_id
)
.execute(&mut **db)
.await
.unwrap();
let id_token = main::gen_token(10);
// Add an oauth entry for the `code` which /oauth/token will rewrite
sqlx::query!(
r#"
INSERT INTO oauth (id_token, client_id, expires_in, scope, access_token)
VALUES (?1, ?2, ?3, ?4, ?5)
"#,
id_token,
client_id,
3600,
scope,
code
)
.execute(&mut **db)
.await
.unwrap();
Redirect::temporary(format!("{}?code={}", redirect_uri, code))
}
#[derive(Serialize, Deserialize, Debug)]
@ -27,13 +65,62 @@ pub struct Token {
pub id_token: String,
}
#[post("/oauth/token")]
pub async fn new_token() -> Json<Token> {
#[derive(Deserialize, Debug, FromForm)]
#[serde(crate = "rocket::serde")]
pub struct NewTokenRequest {
client_id: String,
redirect_uri: String,
grant_type: String,
code: String,
client_secret: String,
}
#[post("/oauth/token", data = "<req>")]
pub async fn new_token(req: Form<NewTokenRequest>, mut db: Connection<Db>) -> Json<Token> {
let oauth = sqlx::query!("
SELECT o.*, a.*
FROM oauth o
INNER JOIN auth a ON a.token = ?2
WHERE o.access_token = ?1
", req.code, req.code)
.fetch_one(&mut **db)
.await
.unwrap();
let access_token = main::gen_token(15);
// Important: setup 'auth' first
sqlx::query!(
r#"
INSERT INTO auth (token, user_id)
VALUES (?1, ?2)
"#,
access_token,
oauth.user_id
)
.execute(&mut **db)
.await
.unwrap();
sqlx::query!(
"UPDATE oauth SET access_token = ?1 WHERE access_token = ?2",
access_token,
req.code
)
.execute(&mut **db)
.await
.unwrap();
sqlx::query!("DELETE FROM auth WHERE token = ?1", req.code)
.execute(&mut **db)
.await
.unwrap();
Json(Token {
access_token: "9b9d497b-2731-435f-a929-e609ca69dac9".to_string(),
access_token: access_token.to_string(),
token_type: "Bearer".to_string(),
expires_in: 3600,
scope: "read write follow push".to_string(),
id_token: "id-token".to_string(),
expires_in: oauth.expires_in,
scope: oauth.scope.to_string(),
id_token: oauth.id_token,
})
}

View file

@ -1,6 +1,7 @@
use main::ap;
use rocket::{get, http::ContentType, serde::json::Json};
use rocket_db_pools::Connection;
use rocket::response::status::NotFound;
use crate::{
Db,
@ -20,7 +21,6 @@ pub async fn inbox(user: String) -> Json<OrderedCollection> {
#[get("/users/<user>/outbox")]
pub async fn outbox(user: String) -> Json<OrderedCollection> {
dbg!(&user);
Json(OrderedCollection {
ty: "OrderedCollection".to_string(),
total_items: 0,
@ -29,8 +29,11 @@ pub async fn outbox(user: String) -> Json<OrderedCollection> {
}
#[get("/users/<uuid>/followers")]
pub async fn followers(mut db: Connection<Db>, uuid: &str) -> Json<OrderedCollection> {
let target = ap::User::from_id(uuid, &mut **db).await;
pub async fn followers(mut db: Connection<Db>, uuid: &str) -> Result<Json<OrderedCollection>, NotFound<String>> {
let target = ap::User::from_id(uuid, &mut **db)
.await
.map_err(|e| NotFound(e.to_string()))?;
let actor_id = target.actor_id();
let followers = sqlx::query!(
@ -44,19 +47,22 @@ pub async fn followers(mut db: Connection<Db>, uuid: &str) -> Json<OrderedCollec
.await
.unwrap();
Json(OrderedCollection {
Ok(Json(OrderedCollection {
ty: "OrderedCollection".to_string(),
total_items: 1,
ordered_items: followers
.into_iter()
.map(|f| f.follower_id)
.collect::<Vec<_>>(),
})
}))
}
#[get("/users/<uuid>/following")]
pub async fn following(mut db: Connection<Db>, uuid: &str) -> Json<OrderedCollection> {
let target = ap::User::from_id(uuid, &mut **db).await;
pub async fn following(mut db: Connection<Db>, uuid: &str) -> Result<Json<OrderedCollection>, NotFound<String>> {
let target = ap::User::from_id(uuid, &mut **db)
.await
.map_err(|e| NotFound(e.to_string()))?;
let actor_id = target.actor_id();
let following = sqlx::query!(
@ -70,14 +76,14 @@ pub async fn following(mut db: Connection<Db>, uuid: &str) -> Json<OrderedCollec
.await
.unwrap();
Json(OrderedCollection {
Ok(Json(OrderedCollection {
ty: "OrderedCollection".to_string(),
total_items: 1,
ordered_items: following
.into_iter()
.map(|f| f.followed_id)
.collect::<Vec<_>>(),
})
}))
}
#[get("/users/<uuid>/posts/<post>")]
@ -111,14 +117,17 @@ pub async fn post(
}
#[get("/users/<uuid>")]
pub async fn user(mut db: Connection<Db>, uuid: &str) -> (ContentType, Json<Person>) {
let user = ap::User::from_id(uuid, &mut **db).await;
(
pub async fn user(mut db: Connection<Db>, uuid: &str) -> Result<(ContentType, Json<Person>), NotFound<String>> {
let user = ap::User::from_id(uuid, &mut **db)
.await
.map_err(|e| NotFound(e.to_string()))?;
Ok((
activity_type(),
Json(Person {
context: "https://www.w3.org/ns/activitystreams".to_string(),
ty: "Person".to_string(),
id: user.id().to_string(),
id: format!("https://ferri.amy.mov/users/{}", user.id()),
name: user.username().to_string(),
preferred_username: user.display_name().to_string(),
followers: format!("https://ferri.amy.mov/users/{}/followers", uuid),
@ -132,5 +141,5 @@ pub async fn user(mut db: Connection<Db>, uuid: &str) -> (ContentType, Json<Pers
public_key: include_str!("../../../public.pem").to_string(),
}),
}),
)
))
}

View file

@ -1,6 +1,7 @@
use main::ap;
use rocket::{get, serde::json::Json};
use rocket_db_pools::Connection;
use tracing::info;
use crate::{
Db,
@ -20,7 +21,8 @@ pub async fn host_meta() -> &'static str {
// https://mastodon.social/.well-known/webfinger?resource=acct:gargron@mastodon.social
#[get("/.well-known/webfinger?<resource>")]
pub async fn webfinger(mut db: Connection<Db>, resource: &str) -> Json<WebfingerResponse> {
println!("Webfinger request for {}", resource);
info!(?resource, "incoming webfinger request");
let acct = resource.strip_prefix("acct:").unwrap();
let (user, _) = acct.split_once("@").unwrap();
let user = ap::User::from_username(user, &mut **db).await;
@ -29,13 +31,13 @@ pub async fn webfinger(mut db: Connection<Db>, resource: &str) -> Json<Webfinger
subject: resource.to_string(),
aliases: vec![
format!("https://ferri.amy.mov/users/{}", user.id()),
format!("https://ferri.amy.mov/@{}", user.username()),
format!("https://ferri.amy.mov/{}", user.username()),
],
links: vec![
Link {
rel: "http://webfinger.net/rel/profile-page".to_string(),
ty: Some("text/html".to_string()),
href: Some(format!("https://ferri.amy.mov/@{}", user.username())),
href: Some(format!("https://ferri.amy.mov/{}", user.username())),
},
Link {
rel: "self".to_string(),

View file

@ -2,15 +2,19 @@ use endpoints::{
api::{self, timeline},
custom, inbox, oauth, user, well_known,
};
use tracing::Level;
use tracing_subscriber::fmt;
use main::ap::http;
use main::config::Config;
use rocket::{
Build, Request, Rocket, build, get,
http::ContentType,
http::{ContentType, Status},
request::{FromRequest, Outcome},
routes,
};
use rocket_db_pools::{Database, sqlx};
use rocket_db_pools::{Connection, Database, sqlx};
mod cors;
mod endpoints;
@ -22,19 +26,20 @@ pub struct Db(sqlx::SqlitePool);
#[get("/")]
async fn user_profile(cfg: &rocket::State<Config>) -> (ContentType, &'static str) {
dbg!(cfg);
(ContentType::HTML, "<p>hello</p>")
}
#[get("/activities/<activity>")]
async fn activity_endpoint(activity: String) {
dbg!(activity);
}
#[derive(Debug)]
struct AuthenticatedUser {
username: String,
actor_id: String,
pub username: String,
pub id: String,
pub token: String,
pub actor_id: String,
}
#[derive(Debug)]
@ -48,19 +53,58 @@ enum LoginError {
impl<'a> FromRequest<'a> for AuthenticatedUser {
type Error = LoginError;
async fn from_request(request: &'a Request<'_>) -> Outcome<AuthenticatedUser, LoginError> {
let token = request.headers().get_one("Authorization").unwrap();
let token = token
.strip_prefix("Bearer")
.map(|s| s.trim())
.unwrap_or(token);
Outcome::Success(AuthenticatedUser {
username: token.to_string(),
actor_id: format!("https://ferri.amy.mov/users/{}", token),
})
let token = request.headers().get_one("Authorization");
if let Some(token) = token {
let token = token
.strip_prefix("Bearer")
.map(|s| s.trim())
.unwrap_or(token);
let mut conn = request.guard::<Connection<Db>>().await.unwrap();
let auth = sqlx::query!(
r#"
SELECT *
FROM auth a
INNER JOIN user u ON a.user_id = u.id
WHERE token = ?1
"#,
token
)
.fetch_one(&mut **conn)
.await;
if let Ok(auth) = auth {
return Outcome::Success(AuthenticatedUser {
token: auth.token,
id: auth.id,
username: auth.display_name,
actor_id: auth.actor_id,
});
}
}
Outcome::Forward(Status::Unauthorized)
}
}
pub fn launch(cfg: Config) -> Rocket<Build> {
let format = fmt::format()
.with_ansi(true)
.without_time()
.with_level(true)
.with_target(false)
.with_thread_names(false)
.with_source_location(false)
.compact();
tracing_subscriber::fmt()
.event_format(format)
.with_writer(std::io::stdout)
.init();
let http_client = http::HttpClient::new();
build()
.manage(cfg)
@ -92,6 +136,7 @@ pub fn launch(cfg: Config) -> Rocket<Build> {
.mount(
"/api/v1",
routes![
api::status::status_context,
api::status::new_status,
api::status::new_status_json,
api::user::new_follow,

View file

@ -3,7 +3,7 @@ use rocket::{
serde::{Deserialize, Serialize},
};
#[derive(Serialize, Deserialize, Debug, FromForm)]
#[derive(Serialize, Deserialize, Debug, FromForm, Clone)]
#[serde(crate = "rocket::serde")]
pub struct App {
pub client_name: String,

View file

@ -0,0 +1,26 @@
CREATE TABLE IF NOT EXISTS auth
(
token TEXT PRIMARY KEY NOT NULL,
user_id TEXT NOT NULL,
FOREIGN KEY(user_id) REFERENCES user(id)
);
CREATE TABLE IF NOT EXISTS app
(
client_id TEXT PRIMARY KEY NOT NULL,
client_secret TEXT NOT NULL,
scopes TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS oauth
(
id_token TEXT PRIMARY KEY NOT NULL,
client_id TEXT NOT NULL,
expires_in INTEGER NOT NULL,
scope TEXT NOT NULL,
access_token TEXT NOT NULL,
FOREIGN KEY(access_token) REFERENCES auth(token),
FOREIGN KEY(client_id) REFERENCES app(client_id)
);