feat: more fixes; finish account api types for now; add some more useful fields to it

This commit is contained in:
nullishamy 2025-04-28 19:23:56 +01:00
parent 76fb8838c2
commit 62931ee20b
Signed by: amy
SSH key fingerprint: SHA256:WmV0uk6WgAQvDJlM8Ld4mFPHZo02CLXXP5VkwQ5xtyk
10 changed files with 168 additions and 65 deletions

View file

@ -47,17 +47,26 @@ async fn main() {
)
.execute(&mut *conn)
.await
.unwrap();
.unwrap();
let ts = main::ap::new_ts();
sqlx::query!(
r#"
INSERT INTO user (id, username, actor_id, display_name)
VALUES (?1, ?2, ?3, ?4)
INSERT INTO user (
id, acct, url, remote, username,
actor_id, display_name, created_at
)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
"#,
"9b9d497b-2731-435f-a929-e609ca69dac9",
"amy",
"https://ferri.amy.mov/@amy",
false,
"amy",
"https://ferri.amy.mov/users/9b9d497b-2731-435f-a929-e609ca69dac9",
"amy"
"amy",
ts
)
.execute(&mut *conn)
.await

View file

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

View file

@ -32,17 +32,17 @@ impl From<db::User> for api::Account {
api::Account {
id: val.id,
username: val.username,
acct: "FIXME_api::Account::acct".to_string(),
acct: val.acct,
display_name: val.display_name,
locked: false,
bot: false,
created_at: "FIXME_api::Account::created_at".to_string(),
created_at: val.created_at.to_rfc3339(),
attribution_domains: vec![],
note: "".to_string(),
url: "FIXME_api::Account::url".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(),
@ -52,7 +52,7 @@ impl From<db::User> for api::Account {
followers_count: 0,
following_count: 0,
statuses_count: 0,
last_status_at: "FIXME_api::Account::last_status_at".to_string(),
last_status_at: val.posts.last_post_at.map(|ts| ts.to_rfc3339()),
emojis: vec![],
fields: vec![],

View file

@ -0,0 +1,90 @@
use crate::types_rewrite::{ObjectUuid, ObjectUri, db};
use sqlx::SqliteConnection;
use thiserror::Error;
use tracing::info;
use chrono::{NaiveDateTime, DateTime, Utc};
const SQLITE_TIME_FMT: &'static str = "%Y-%m-%d %H:%M:%S";
#[derive(Debug, Error)]
pub enum FetchError {
#[error("an unknown error occured when fetching: {0}")]
Unknown(String)
}
fn parse_ts(ts: String) -> Option<DateTime<Utc>> {
NaiveDateTime::parse_from_str(&ts, SQLITE_TIME_FMT)
.ok()
.map(|nt| nt.and_utc())
}
pub async fn user_by_id(id: ObjectUuid, conn: &mut SqliteConnection) -> Result<db::User, FetchError> {
info!("fetching user by uuid '{:?}' from the database", id);
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
FROM "user" u
INNER JOIN "actor" a ON u.actor_id = a.id
WHERE u.id = ?1
"#, id.0)
.fetch_one(&mut *conn)
.await
.map_err(|e| FetchError::Unknown(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| FetchError::Unknown(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_one(&mut *conn)
.await
.map_err(|e| FetchError::Unknown(e.to_string()))?
.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", id, follower_count);
info!("user {:?} last posted {:?}", 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
}
})
}

View file

@ -1,7 +1,7 @@
use serde::{Serialize, Deserialize};
mod convert;
pub use convert::*;
pub mod convert;
pub mod fetch;
pub const AS_CONTEXT_RAW: &'static str = "https://www.w3.org/ns/activitystreams";
pub fn as_context() -> ObjectContext {
@ -16,10 +16,10 @@ pub enum ObjectContext {
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct ObjectUri(String);
pub struct ObjectUri(pub String);
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct ObjectUuid(String);
pub struct ObjectUuid(pub String);
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct Object {
@ -29,22 +29,34 @@ pub struct Object {
}
pub mod db {
use serde::{Serialize, Deserialize};
use chrono::{DateTime, Utc};
use super::*;
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
#[derive(Debug, Eq, PartialEq)]
pub struct Actor {
pub id: ObjectUri,
pub inbox: String,
pub outbox: String,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
#[derive(Debug, Eq, PartialEq)]
pub struct UserPosts {
// User may have no posts
pub last_post_at: Option<DateTime<Utc>>
}
#[derive(Debug, Eq, PartialEq)]
pub struct User {
pub id: ObjectUuid,
pub actor_id: ObjectUri,
pub actor: Actor,
pub username: String,
pub display_name: String
pub display_name: String,
pub acct: String,
pub remote: bool,
pub url: String,
pub created_at: DateTime<Utc>,
pub posts: UserPosts
}
}
@ -121,7 +133,7 @@ pub mod api {
pub followers_count: i64,
pub following_count: i64,
pub statuses_count: i64,
pub last_status_at: String,
pub last_status_at: Option<String>,
pub emojis: Vec<Emoji>,
pub fields: Vec<CustomField>,

View file

@ -45,6 +45,7 @@ pub async fn home(
u.username, u.display_name, u.actor_id, p.created_at, p.boosted_post_id
FROM post p
INNER JOIN user u on p.user_id = u.id
ORDER BY datetime(p.created_at) DESC
"#
)
.fetch_all(&mut **db)

View file

@ -1,4 +1,3 @@
use main::ap::http::HttpClient;
use rocket::{State, get, response::status};
use rocket_db_pools::Connection;
use main::ap;
@ -8,7 +7,7 @@ use uuid::Uuid;
use crate::{
Db,
types::{self, activity, content, webfinger},
types::{self, webfinger},
};
#[get("/finger/<account>")]
@ -86,44 +85,17 @@ pub async fn resolve_user(acct: &str, host: &str) -> types::Person {
}
#[get("/test")]
pub async fn test(http: &State<HttpClient>, outbound: &State<OutboundQueue>) -> &'static str {
pub async fn test(
outbound: &State<OutboundQueue>,
mut db: Connection<Db>
) -> &'static str {
use main::types_rewrite::{ObjectUuid, fetch, api};
outbound.0.send(ap::QueueMessage::Heartbeat);
let user = resolve_user("amy@fedi.amy.mov", "fedi.amy.mov").await;
let post = activity::CreateActivity {
id: "https://ferri.amy.mov/activities/amy/20".to_string(),
ty: "Create".to_string(),
actor: "https://ferri.amy.mov/users/amy".to_string(),
object: content::Post {
context: "https://www.w3.org/ns/activitystreams".to_string(),
id: "https://ferri.amy.mov/users/amy/posts/20".to_string(),
ty: "Note".to_string(),
content: "My first post".to_string(),
ts: "2025-04-10T10:48:11Z".to_string(),
to: vec!["https://ferri.amy.mov/users/amy/followers".to_string()],
cc: vec!["https://www.w3.org/ns/activitystreams#Public".to_string()],
attributed_to: None
},
ts: "2025-04-10T10:48:11Z".to_string(),
to: vec!["https://ferri.amy.mov/users/amy/followers".to_string()],
cc: vec![],
};
let key_id = "https://ferri.amy.mov/users/amy#main-key";
let follow = http
.post(user.inbox)
.json(&post)
.sign(key_id)
.activity()
.send()
.await
.unwrap()
.text()
.await
.unwrap();
dbg!(follow);
let id = ObjectUuid("9b9d497b-2731-435f-a929-e609ca69dac9".to_string());
let user= dbg!(fetch::user_by_id(id, &mut **db).await.unwrap());
let apu: api::Account = user.into();
dbg!(apu);
"Hello, world!"
}

View file

@ -50,19 +50,34 @@ async fn create_user(
let host = url.host_str().unwrap();
info!("creating user '{}'@'{}' ({:#?})", user.preferred_username, host, user);
let username = format!("{}@{}", user.preferred_username, host);
let (acct, remote) = if host != "ferri.amy.mov" {
(format!("{}@{}", user.preferred_username, host), true)
} else {
(user.preferred_username.clone(), false)
};
let url = format!("https://ferri.amy.mov/{}", acct);
let uuid = Uuid::new_v4().to_string();
// FIXME: Pull from user
let ts = main::ap::new_ts();
sqlx::query!(
r#"
INSERT INTO user (id, username, actor_id, display_name)
VALUES (?1, ?2, ?3, ?4)
INSERT INTO user (
id, acct, url, remote, username,
actor_id, display_name, created_at
)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
ON CONFLICT(actor_id) DO NOTHING;
"#,
uuid,
username,
acct,
url,
remote,
user.preferred_username,
actor,
user.name
user.name,
ts
)
.execute(conn)
.await

View file

@ -8,12 +8,12 @@ use rocket::{
};
use rocket_db_pools::Connection;
#[get("/oauth/authorize?<client_id>&<scope>&<redirect_uri>&<_response_type>")]
#[get("/oauth/authorize?<client_id>&<scope>&<redirect_uri>&<response_type>")]
pub async fn authorize(
client_id: &str,
scope: &str,
redirect_uri: &str,
_response_type: &str,
response_type: &str,
mut db: Connection<Db>,
) -> Redirect {
// For now, we will always authorize the request and assign it to an admin user

View file

@ -2,6 +2,10 @@ CREATE TABLE IF NOT EXISTS user
(
-- UUID
id TEXT PRIMARY KEY NOT NULL,
acct TEXT NOT NULL,
url TEXT NOT NULL,
created_at TEXT NOT NULL,
remote BOOLEAN NOT NULL,
username TEXT NOT NULL,
actor_id TEXT NOT NULL UNIQUE,
display_name TEXT NOT NULL,