diff --git a/Cargo.lock b/Cargo.lock index fe3d61d..fd43ed1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -110,6 +110,48 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "askama" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75363874b771be265f4ffe307ca705ef6f3baa19011c149da8674a87f1b75c4" +dependencies = [ + "askama_derive", + "itoa", + "percent-encoding", + "serde", + "serde_json", +] + +[[package]] +name = "askama_derive" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "129397200fe83088e8a68407a8e2b1f826cf0086b21ccdb866a722c8bcd3a94f" +dependencies = [ + "askama_parser", + "basic-toml", + "memchr", + "proc-macro2", + "quote", + "rustc-hash", + "serde", + "serde_derive", + "syn 2.0.100", +] + +[[package]] +name = "askama_parser" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6ab5630b3d5eaf232620167977f95eb51f3432fc76852328774afbd242d4358" +dependencies = [ + "memchr", + "serde", + "serde_derive", + "winnow", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -206,6 +248,15 @@ version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" +[[package]] +name = "basic-toml" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" +dependencies = [ + "serde", +] + [[package]] name = "binascii" version = "0.1.4" @@ -2156,6 +2207,7 @@ dependencies = [ name = "server" version = "0.1.0" dependencies = [ + "askama", "chrono", "main", "rand 0.8.5", diff --git a/ferri-cli/src/main.rs b/ferri-cli/src/main.rs index 36969c8..b67ad9e 100644 --- a/ferri-cli/src/main.rs +++ b/ferri-cli/src/main.rs @@ -55,9 +55,9 @@ async fn main() { r#" INSERT INTO user ( id, acct, url, remote, username, - actor_id, display_name, created_at + actor_id, display_name, created_at, icon_url ) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9) "#, "9b9d497b-2731-435f-a929-e609ca69dac9", "amy", @@ -66,7 +66,8 @@ async fn main() { "amy", "https://ferri.amy.mov/users/9b9d497b-2731-435f-a929-e609ca69dac9", "amy", - ts + ts, + "https://ferri.amy.mov/assets/pfp.png" ) .execute(&mut *conn) .await diff --git a/ferri-main/src/types/convert.rs b/ferri-main/src/types/convert.rs index 5453cd4..c473954 100644 --- a/ferri-main/src/types/convert.rs +++ b/ferri-main/src/types/convert.rs @@ -44,10 +44,10 @@ impl From for api::Account { note: "".to_string(), url: val.url, - avatar: "https://ferri.amy.mov/assets/pfp.png".to_string(), - avatar_static: "https://ferri.amy.mov/assets/pfp.png".to_string(), - header: "https://ferri.amy.mov/assets/pfp.png".to_string(), - header_static: "https://ferri.amy.mov/assets/pfp.png".to_string(), + avatar: val.icon_url.clone(), + avatar_static: val.icon_url.clone(), + header: val.icon_url.clone(), + header_static: val.icon_url, followers_count: 0, following_count: 0, @@ -79,6 +79,42 @@ impl From for ap::Person { owner: format!("https://ferri.amy.mov/users/{}", val.id.0), public_key: include_str!("../../../public.pem").to_string(), }), + icon: None } } } + +impl From for api::Status { + fn from(value: db::Post) -> api::Status { + api::Status { + id: value.id, + created_at: value.created_at.to_rfc3339(), + in_reply_to_id: None, + in_reply_to_account_id: None, + sensitive: false, + spoiler_text: String::new(), + visibility: "Public".to_string(), + language: "en-GB".to_string(), + uri: value.uri.clone(), + url: value.uri.0.to_string(), + replies_count: 0, + reblogs_count: 0, + favourites_count: 0, + favourited: false, + reblogged: false, + muted: false, + bookmarked: false, + content: value.content, + reblog: None, + application: None, + account: value.user.into(), + media_attachments: vec![], + mentions: vec![], + tags: vec![], + emojis: vec![], + card: None, + poll: None + } + } +} + diff --git a/ferri-main/src/types/get.rs b/ferri-main/src/types/get.rs index d4197e6..18627f4 100644 --- a/ferri-main/src/types/get.rs +++ b/ferri-main/src/types/get.rs @@ -1,14 +1,28 @@ use crate::types::{DbError, ObjectUri, ObjectUuid, db}; use chrono::{DateTime, NaiveDateTime, Utc}; use sqlx::SqliteConnection; -use tracing::info; +use tracing::{info, error}; const SQLITE_TIME_FMT: &str = "%Y-%m-%d %H:%M:%S"; fn parse_ts(ts: String) -> Option> { - NaiveDateTime::parse_from_str(&ts, SQLITE_TIME_FMT) - .ok() - .map(|nt| nt.and_utc()) + // Depending on how the TS is queried it may be naive (so get it back to utc) + // or it may have a timezone associated with it + let dt = NaiveDateTime::parse_from_str(&ts, SQLITE_TIME_FMT) + .map(|ndt| { + ndt.and_utc() + }) + .or_else(|_| { + DateTime::parse_from_rfc3339(&ts) + .map(|dt| dt.to_utc()) + }); + + if let Err(err) = dt { + error!("could not parse datetime {} ({}), db weirdness", ts, err); + return None + } + + Some(dt.unwrap()) } pub async fn user_by_id(id: ObjectUuid, conn: &mut SqliteConnection) -> Result { @@ -26,7 +40,8 @@ pub async fn user_by_id(id: ObjectUuid, conn: &mut SqliteConnection) -> Result Result Result Result { + info!("fetching user by actor_uri '{:?}' from the database", uri); + + let record = sqlx::query!( + r#" + SELECT + u.id as "user_id", + u.username, + u.actor_id, + u.display_name, + a.inbox, + a.outbox, + u.url, + u.acct, + u.remote, + u.created_at, + u.icon_url + FROM "user" u + INNER JOIN "actor" a ON u.actor_id = a.id + WHERE u.actor_id = ?1 + "#, + uri.0 + ) + .fetch_one(&mut *conn) + .await + .map_err(|e| DbError::FetchError(e.to_string()))?; + + let follower_count = sqlx::query_scalar!( + r#" + SELECT COUNT(follower_id) + FROM "follow" + WHERE followed_id = ?1 + "#, + record.actor_id + ) + .fetch_one(&mut *conn) + .await + .map_err(|e| DbError::FetchError(e.to_string()))?; + + let last_post_at = sqlx::query_scalar!( + r#" + SELECT datetime(p.created_at) + FROM post p + WHERE p.user_id = ?1 + ORDER BY datetime(p.created_at) DESC + LIMIT 1 + "#, + record.user_id + ) + .fetch_optional(&mut *conn) + .await + .map_err(|e| DbError::FetchError(e.to_string()))? + .flatten() + .and_then(|ts| { + info!("parsing timestamp {}", ts); + parse_ts(ts) + }); + + let user_created = parse_ts(record.created_at).expect("no db corruption"); + + info!("user {:?} has {} followers", record.user_id, follower_count); + info!("user {:?} last posted {:?}", record.user_id, last_post_at); + + Ok(db::User { + id: ObjectUuid(record.user_id), + actor: db::Actor { + id: ObjectUri(record.actor_id), + inbox: record.inbox, + outbox: record.outbox, + }, + acct: record.acct, + remote: record.remote, + username: record.username, + display_name: record.display_name, + created_at: user_created, + url: record.url, + posts: db::UserPosts { last_post_at }, + icon_url: record.icon_url + }) +} + +pub async fn posts_for_user_id( + id: ObjectUuid, + conn: &mut SqliteConnection +) -> Result, DbError> { + let mut out = vec![]; + let posts = sqlx::query!(r#" + SELECT + p.id as "post_id", u.id as "user_id", + p.content, p.uri as "post_uri", u.username, u.display_name, + u.actor_id, p.created_at as "post_created", + p.boosted_post_id, a.inbox, a.outbox, u.created_at as "user_created", + u.acct, u.remote, u.url as "user_url", u.icon_url + FROM post p + INNER JOIN user u on p.user_id = u.id + INNER JOIN actor a ON u.actor_id = a.id + WHERE p.user_id = ? + "#, id.0) + .fetch_all(&mut *conn) + .await + .unwrap(); + + for record in posts { + let user_created = parse_ts(record.user_created) + .expect("no db corruption"); + + out.push(db::Post { + id: ObjectUuid(record.post_id), + uri: ObjectUri(record.post_uri), + user: db::User { + id: ObjectUuid(record.user_id), + actor: db::Actor { + id: ObjectUri(record.actor_id), + inbox: record.inbox, + outbox: record.outbox, + }, + acct: record.acct, + remote: record.remote, + username: record.username, + display_name: record.display_name, + created_at: user_created, + url: record.user_url, + icon_url: record.icon_url, + posts: db::UserPosts { + last_post_at: None + } + }, + content: record.content, + created_at: parse_ts(record.post_created).unwrap(), + boosted_post: None + }) + } + + Ok(out) +} diff --git a/ferri-main/src/types/make.rs b/ferri-main/src/types/make.rs index 36c5696..5383d98 100644 --- a/ferri-main/src/types/make.rs +++ b/ferri-main/src/types/make.rs @@ -5,8 +5,9 @@ pub async fn new_user(user: db::User, conn: &mut SqliteConnection) -> Result Result), } +impl Default for ObjectContext { + fn default() -> Self { + ObjectContext::Str(String::new()) + } +} + #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] pub struct ObjectUri(pub String); @@ -48,6 +54,7 @@ impl ObjectUuid { #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] pub struct Object { #[serde(rename = "@context")] + #[serde(default)] pub context: ObjectContext, pub id: ObjectUri, } @@ -79,9 +86,20 @@ pub mod db { pub remote: bool, pub url: String, pub created_at: DateTime, + pub icon_url: String, pub posts: UserPosts, } + + #[derive(Debug, Eq, PartialEq, Clone)] + pub struct Post { + pub id: ObjectUuid, + pub uri: ObjectUri, + pub user: User, + pub content: String, + pub created_at: DateTime, + pub boosted_post: Option + } } pub mod ap { @@ -103,6 +121,8 @@ pub mod ap { pub struct MinimalActivity { #[serde(flatten)] pub obj: Object, + + #[serde(rename = "type")] pub ty: ActivityType, } @@ -123,6 +143,7 @@ pub mod ap { #[serde(flatten)] pub obj: Object, + #[serde(rename = "type")] pub ty: ActivityType, pub object: Post, @@ -139,6 +160,7 @@ pub mod ap { #[serde(flatten)] pub obj: Object, + #[serde(rename = "type")] pub ty: ActivityType, pub object: String, @@ -149,7 +171,8 @@ pub mod ap { pub struct AcceptActivity { #[serde(flatten)] pub obj: Object, - + + #[serde(rename = "type")] pub ty: ActivityType, pub object: String, @@ -161,6 +184,7 @@ pub mod ap { #[serde(flatten)] pub obj: Object, + #[serde(rename = "type")] pub ty: ActivityType, pub actor: String, @@ -175,6 +199,7 @@ pub mod ap { #[serde(flatten)] pub obj: Object, + #[serde(rename = "type")] pub ty: ActivityType, #[serde(rename = "published")] @@ -196,6 +221,25 @@ pub mod ap { pub outbox: String, } + #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] + pub enum IconType { + Image + } + + #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] + pub struct PersonIcon { + #[serde(rename = "type")] + pub ty: IconType, + pub url: String, + + #[serde(default)] + pub summary: String, + #[serde(default)] + pub width: i64, + #[serde(default)] + pub height: i64 + } + #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct Person { @@ -213,6 +257,8 @@ pub mod ap { pub name: String, pub public_key: Option, + + pub icon: Option } #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] @@ -257,6 +303,37 @@ pub mod api { pub links: Vec, } + #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] + pub struct Status { + pub id: ObjectUuid, + pub created_at: String, + pub in_reply_to_id: Option, + pub in_reply_to_account_id: Option, + pub sensitive: bool, + pub spoiler_text: String, + pub visibility: String, + pub language: String, + pub uri: ObjectUri, + pub url: String, + pub replies_count: i64, + pub reblogs_count: i64, + pub favourites_count: i64, + pub favourited: bool, + pub reblogged: bool, + pub muted: bool, + pub bookmarked: bool, + pub content: String, + pub reblog: Option>, + pub application: Option<()>, + pub account: Account, + pub media_attachments: Vec>, + pub mentions: Vec>, + pub tags: Vec>, + pub emojis: Vec>, + pub card: Option<()>, + pub poll: Option<()>, + } + #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] pub struct Account { pub id: ObjectUuid, diff --git a/ferri-server/Cargo.toml b/ferri-server/Cargo.toml index a378685..11d5807 100644 --- a/ferri-server/Cargo.toml +++ b/ferri-server/Cargo.toml @@ -18,4 +18,5 @@ tracing = { workspace = true } tracing-subscriber = { workspace = true } thiserror = { workspace = true } serde_json = { workspace = true } -serde = { workspace = true } \ No newline at end of file +serde = { workspace = true } +askama = "0.14.0" diff --git a/ferri-server/src/endpoints/admin/mod.rs b/ferri-server/src/endpoints/admin/mod.rs new file mode 100644 index 0000000..51e6ec7 --- /dev/null +++ b/ferri-server/src/endpoints/admin/mod.rs @@ -0,0 +1,20 @@ +use rocket::{get, post, response::content::RawHtml}; +use askama::Template; + +#[derive(Template)] +#[template(path = "index.html")] +struct IndexTemplate { + val: String +} + +#[post("/clicked")] +pub async fn button_clicked() -> RawHtml { + let tmpl = IndexTemplate { val: "clicked".to_string() }; + RawHtml(tmpl.render().unwrap()) +} + +#[get("/")] +pub async fn index() -> RawHtml { + let tmpl = IndexTemplate { val: "test".to_string() }; + RawHtml(tmpl.render().unwrap()) +} diff --git a/ferri-server/src/endpoints/api/apps.rs b/ferri-server/src/endpoints/api/apps.rs index 7b566d2..a798f0c 100644 --- a/ferri-server/src/endpoints/api/apps.rs +++ b/ferri-server/src/endpoints/api/apps.rs @@ -27,8 +27,8 @@ pub async fn new_app( VALUES (?1, ?2, ?3) "#, app.client_name, - app.scopes, - secret + secret, + app.scopes ) .execute(&mut **db) .await diff --git a/ferri-server/src/endpoints/api/mod.rs b/ferri-server/src/endpoints/api/mod.rs index 02fe1da..cccade5 100644 --- a/ferri-server/src/endpoints/api/mod.rs +++ b/ferri-server/src/endpoints/api/mod.rs @@ -4,3 +4,4 @@ pub mod preferences; pub mod status; pub mod timeline; pub mod user; +pub mod search; diff --git a/ferri-server/src/endpoints/api/search.rs b/ferri-server/src/endpoints/api/search.rs new file mode 100644 index 0000000..c009c0f --- /dev/null +++ b/ferri-server/src/endpoints/api/search.rs @@ -0,0 +1,88 @@ +use rocket::{ + get, serde::json::Json, FromFormField, State, +}; +use main::types::{api, get}; +use rocket_db_pools::Connection; +use serde::{Deserialize, Serialize}; +use tracing::{info, error}; + +use crate::{http_wrapper::HttpWrapper, AuthenticatedUser, Db}; + +#[derive(Serialize, Deserialize, FromFormField, Debug)] +#[serde(rename_all = "lowercase")] +pub enum SearchType { + Accounts, + Hashtags, + Statuses, + All +} + +impl Default for SearchType { + fn default() -> Self { + Self::All + } +} + +#[derive(Serialize, Deserialize)] +pub struct SearchResults { + statuses: Vec, + accounts: Vec, + hashtags: Vec<()> +} + +#[get("/search?&")] +pub async fn search( + q: &str, + r#type: SearchType, + helpers: &State, + mut db: Connection, + user: AuthenticatedUser +) -> Json { + let ty = r#type; + info!("search for {} (ty: {:?})", q, ty); + + let key_id = "https://ferri.amy.mov/users/9b9d497b-2731-435f-a929-e609ca69dac9#main-key"; + let http = HttpWrapper::new(&helpers.http, key_id); + + let mut accounts = vec![]; + let mut statuses = vec![]; + + match ty { + SearchType::Accounts => { + let person = { + let res = http.get_person(q).await; + if let Err(e) = res { + error!("could not load user {}: {}", q, e.to_string()); + None + } else { + Some(res.unwrap()) + } + }; + + let user = get::user_by_actor_uri(person.unwrap().obj.id, &mut **db) + .await + .unwrap(); + + accounts.push(user.into()) + }, + SearchType::Statuses => { + if q == "me" { + let st = get::posts_for_user_id(user.id, &mut **db) + .await + .unwrap(); + + for status in st.into_iter() { + statuses.push(status.into()); + } + } + }, + SearchType::Hashtags => todo!(), + SearchType::All => todo!(), + } + + Json(SearchResults { + statuses, + accounts, + hashtags: vec![], + }) +} diff --git a/ferri-server/src/endpoints/api/status.rs b/ferri-server/src/endpoints/api/status.rs index 8e7694d..90b766c 100644 --- a/ferri-server/src/endpoints/api/status.rs +++ b/ferri-server/src/endpoints/api/status.rs @@ -43,7 +43,7 @@ async fn create_status( http: &HttpClient, status: &Status, ) -> TimelineStatus { - let user = ap::User::from_id(&user.id, &mut **db).await.unwrap(); + let user = ap::User::from_id(&user.id.0, &mut **db).await.unwrap(); let outbox = ap::Outbox::for_user(user.clone(), http); let post_id = ap::new_id(); diff --git a/ferri-server/src/endpoints/api/timeline.rs b/ferri-server/src/endpoints/api/timeline.rs index f9b868c..57be00f 100644 --- a/ferri-server/src/endpoints/api/timeline.rs +++ b/ferri-server/src/endpoints/api/timeline.rs @@ -35,11 +35,8 @@ pub struct TimelineStatus { #[get("/timelines/home")] pub async fn home( mut db: Connection, - helpers: &State, _user: AuthenticatedUser, ) -> Json> { - let config = &helpers.config; - #[derive(sqlx::FromRow, Debug)] struct Post { is_boost_source: bool, @@ -51,6 +48,8 @@ pub async fn home( boosted_post_id: Option, display_name: String, username: String, + icon_url: String, + user_url: String } // FIXME: query! can't cope with this. returns a type error @@ -75,7 +74,7 @@ pub async fn home( ) SELECT is_boost_source, p.id as "post_id", u.id as "user_id", p.content, p.uri as "post_uri", u.username, u.display_name, - u.actor_id, p.created_at, p.boosted_post_id + u.actor_id, p.created_at, p.boosted_post_id, u.icon_url, u.url as "user_url" FROM get_home_timeline_with_boosts JOIN post p ON p.id = get_home_timeline_with_boosts.id JOIN user u ON u.id = p.user_id; @@ -90,7 +89,6 @@ pub async fn home( for record in posts.iter() { let mut boost: Option> = None; if let Some(ref boosted_id) = record.boosted_post_id { - let user_uri = config.user_url(&record.user_id); let record = posts.iter().find(|p| &p.post_id == boosted_id).unwrap(); boost = Some(Box::new(TimelineStatus { @@ -123,11 +121,11 @@ pub async fn home( created_at: "2025-04-10T22:12:09Z".to_string(), attribution_domains: vec![], note: "".to_string(), - url: user_uri, - avatar: "https://ferri.amy.mov/assets/pfp.png".to_string(), - avatar_static: "https://ferri.amy.mov/assets/pfp.png".to_string(), - header: "https://ferri.amy.mov/assets/pfp.png".to_string(), - header_static: "https://ferri.amy.mov/assets/pfp.png".to_string(), + url: record.user_url.clone(), + avatar: record.icon_url.clone(), + avatar_static: record.icon_url.clone(), + header: record.icon_url.clone(), + header_static: record.icon_url.clone(), followers_count: 1, following_count: 1, statuses_count: 1, @@ -137,7 +135,6 @@ pub async fn home( } if !record.is_boost_source { - let user_uri = config.user_web_url(&record.username); out.push(TimelineStatus { id: record.post_id.clone(), created_at: record.created_at.clone(), @@ -168,11 +165,11 @@ pub async fn home( created_at: "2025-04-10T22:12:09Z".to_string(), attribution_domains: vec![], note: "".to_string(), - url: user_uri, - avatar: "https://ferri.amy.mov/assets/pfp.png".to_string(), - avatar_static: "https://ferri.amy.mov/assets/pfp.png".to_string(), - header: "https://ferri.amy.mov/assets/pfp.png".to_string(), - header_static: "https://ferri.amy.mov/assets/pfp.png".to_string(), + url: record.user_url.clone(), + avatar: record.icon_url.clone(), + avatar_static: record.icon_url.clone(), + header: record.icon_url.clone(), + header_static: record.icon_url.clone(), followers_count: 1, following_count: 1, statuses_count: 1, diff --git a/ferri-server/src/endpoints/api/user.rs b/ferri-server/src/endpoints/api/user.rs index 2c174a4..c07b749 100644 --- a/ferri-server/src/endpoints/api/user.rs +++ b/ferri-server/src/endpoints/api/user.rs @@ -66,7 +66,7 @@ pub async fn new_follow( ) -> Result<(), NotFound> { 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.0, &mut **db).await; let followed = ap::User::from_id(uuid, &mut **db) .await diff --git a/ferri-server/src/endpoints/inbox.rs b/ferri-server/src/endpoints/inbox.rs index 8145340..9750187 100644 --- a/ferri-server/src/endpoints/inbox.rs +++ b/ferri-server/src/endpoints/inbox.rs @@ -57,6 +57,9 @@ async fn create_user( }; let url = format!("https://ferri.amy.mov/{}", acct); + let icon_url = user.icon.as_ref().map(|ic| ic.url.clone()).unwrap_or( + "https://ferri.amy.mov/assets/pfp.png".to_string() + ); let uuid = Uuid::new_v4().to_string(); // FIXME: Pull from user @@ -65,10 +68,10 @@ async fn create_user( r#" INSERT INTO user ( id, acct, url, remote, username, - actor_id, display_name, created_at + actor_id, display_name, created_at, icon_url ) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8) - ON CONFLICT(actor_id) DO NOTHING; + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9) + ON CONFLICT(actor_id) DO NOTHING; "#, uuid, acct, @@ -77,7 +80,8 @@ async fn create_user( user.preferred_username, actor, user.name, - ts + ts, + icon_url ) .execute(conn) .await @@ -170,6 +174,9 @@ async fn resolve_actor<'a>( remote: remote_info.is_remote, url: remote_info.web_url, created_at: main::ap::now(), + icon_url: person.icon.map(|ic| ic.url).unwrap_or( + "https://ferri.amy.mov/assets/pfp.png".to_string() + ), posts: db::UserPosts { last_post_at: None }, }; @@ -327,6 +334,8 @@ async fn handle_boost_activity<'a>( ); let user_id = actor_user.id(); + info!("inserting post with id {} uri {}", base_id, uri); + sqlx::query!( " INSERT INTO post (id, uri, user_id, content, created_at, boosted_post_id) diff --git a/ferri-server/src/endpoints/mod.rs b/ferri-server/src/endpoints/mod.rs index 50f374c..74f42d3 100644 --- a/ferri-server/src/endpoints/mod.rs +++ b/ferri-server/src/endpoints/mod.rs @@ -4,6 +4,8 @@ pub mod oauth; pub mod user; pub mod api; +pub mod admin; + pub mod custom; pub mod inbox; pub mod well_known; diff --git a/ferri-server/src/endpoints/oauth.rs b/ferri-server/src/endpoints/oauth.rs index 6d0391e..853c04f 100644 --- a/ferri-server/src/endpoints/oauth.rs +++ b/ferri-server/src/endpoints/oauth.rs @@ -1,23 +1,40 @@ use crate::Db; +use askama::Template; +use tracing::error; + use rocket::{ FromForm, form::Form, get, post, - response::Redirect, + response::status::BadRequest, + response::content::RawHtml, serde::{Deserialize, Serialize, json::Json}, }; + use rocket_db_pools::Connection; -#[get("/oauth/authorize?&&&")] -pub async fn authorize( - client_id: &str, - scope: &str, - redirect_uri: &str, - response_type: &str, +struct AuthorizeClient { + id: String +} + +#[derive(Template)] +#[template(path = "authorize.html")] +struct AuthorizeTemplate { + client: AuthorizeClient, + scopes: Vec, + scope_raw: String, + redirect_uri: String, + user_id: String +} + +#[post("/oauth/accept?&&")] +pub async fn accept( mut db: Connection, -) -> Redirect { - // For now, we will always authorize the request and assign it to an admin user - let user_id = "9b9d497b-2731-435f-a929-e609ca69dac9"; + id: &str, + client_id: &str, + scope: &str +) -> RawHtml { + let user_id = id; 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 @@ -52,7 +69,37 @@ pub async fn authorize( .await .unwrap(); - Redirect::temporary(format!("{}?code={}", redirect_uri, code)) + // HACK: Until we are storing oauth stuff more properly we will hardcode phanpy + RawHtml(format!(r#" + + "#, "https://phanpy.social?code=", code)) +} + +#[get("/oauth/authorize?&&&")] +pub async fn authorize( + client_id: &str, + scope: &str, + redirect_uri: &str, + response_type: &str +) -> Result, BadRequest> { + if response_type != "code" { + error!("unknown response type {}", response_type); + return Err( + BadRequest(format!("unknown response type {}", response_type)) + ) + } + + let tmpl = AuthorizeTemplate { + client: AuthorizeClient { + id: client_id.to_string() + }, + scope_raw: scope.to_string(), + scopes: scope.split(" ").map(|s| s.to_string()).collect(), + redirect_uri: redirect_uri.to_string(), + user_id: "9b9d497b-2731-435f-a929-e609ca69dac9".to_string() + }; + + Ok(RawHtml(tmpl.render().unwrap())) } #[derive(Serialize, Deserialize, Debug)] diff --git a/ferri-server/src/http_wrapper.rs b/ferri-server/src/http_wrapper.rs index 050ea49..38c51be 100644 --- a/ferri-server/src/http_wrapper.rs +++ b/ferri-server/src/http_wrapper.rs @@ -2,7 +2,7 @@ use crate::http::HttpClient; use main::types::ap; use std::fmt::Debug; use thiserror::Error; -use tracing::{Level, error, event}; +use tracing::{Level, error, event, info}; pub struct HttpWrapper<'a> { client: &'a HttpClient, @@ -54,6 +54,7 @@ impl<'a> HttpWrapper<'a> { } let raw_body = raw_body.unwrap(); + info!("raw body {}", raw_body); let decoded = serde_json::from_str::(&raw_body); if let Err(e) = decoded { diff --git a/ferri-server/src/lib.rs b/ferri-server/src/lib.rs index d373a7e..04b1e4d 100644 --- a/ferri-server/src/lib.rs +++ b/ferri-server/src/lib.rs @@ -1,11 +1,11 @@ use endpoints::{ api::{self, timeline}, - custom, inbox, oauth, user, well_known, + admin, custom, inbox, oauth, user, well_known, }; use tracing_subscriber::fmt; -use main::ap; +use main::{ap, types::{ObjectUri, ObjectUuid}}; use main::ap::http; use main::config::Config; @@ -36,9 +36,9 @@ async fn activity_endpoint(_activity: String) {} #[derive(Debug)] pub struct AuthenticatedUser { pub username: String, - pub id: String, + pub id: ObjectUuid, pub token: String, - pub actor_id: String, + pub actor_id: ObjectUri, } #[derive(Debug)] @@ -72,9 +72,9 @@ impl<'a> FromRequest<'a> for AuthenticatedUser { if let Ok(auth) = auth { return Outcome::Success(AuthenticatedUser { token: auth.token, - id: auth.id, + id: ObjectUuid(auth.id), username: auth.display_name, - actor_id: auth.actor_id, + actor_id: ObjectUri(auth.actor_id), }); } } @@ -122,6 +122,13 @@ pub fn launch(cfg: Config) -> Rocket { .attach(Db::init()) .attach(cors::CORS) .mount("/assets", rocket::fs::FileServer::from("./assets")) + .mount( + "/admin", + routes![ + admin::index, + admin::button_clicked + ] + ) .mount( "/", routes![ @@ -133,6 +140,7 @@ pub fn launch(cfg: Config) -> Rocket { user::following, user::post, oauth::authorize, + oauth::accept, oauth::new_token, cors::options_req, activity_endpoint, @@ -142,7 +150,10 @@ pub fn launch(cfg: Config) -> Rocket { user_profile, ], ) - .mount("/api/v2", routes![api::instance::instance]) + .mount("/api/v2", routes![ + api::instance::instance, + api::search::search, + ]) .mount( "/api/v1", routes![ diff --git a/ferri-server/templates/authorize.html b/ferri-server/templates/authorize.html new file mode 100644 index 0000000..bba0a95 --- /dev/null +++ b/ferri-server/templates/authorize.html @@ -0,0 +1,46 @@ + + + + + + + + + Ferri Test + + + + + + + +
+

Authorization request

+ + + App '{{ client.id }}' would like to access the following scopes in your account + + +
    + {% for scope in scopes %} +
  • {{ scope }}
  • + {% endfor %} +
+ + +
+ + diff --git a/ferri-server/templates/index.html b/ferri-server/templates/index.html new file mode 100644 index 0000000..f912bcf --- /dev/null +++ b/ferri-server/templates/index.html @@ -0,0 +1,19 @@ + + + + + + + + + Ferri Test + + + + + + + + diff --git a/migrations/20250410121119_add_user.sql b/migrations/20250410121119_add_user.sql index 2dbadf1..10a280c 100644 --- a/migrations/20250410121119_add_user.sql +++ b/migrations/20250410121119_add_user.sql @@ -9,6 +9,7 @@ CREATE TABLE IF NOT EXISTS user username TEXT NOT NULL, actor_id TEXT NOT NULL UNIQUE, display_name TEXT NOT NULL, + icon_url TEXT NOT NULL, FOREIGN KEY(actor_id) REFERENCES actor(id) );