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 sqlx::Sqlite;
use std::fmt::Debug;
use tracing::{event, Level};
use tracing::{Level, event};
#[derive(Debug, Clone)]
pub enum ActivityType {
@ -104,7 +104,7 @@ impl<'a> Outbox<'a> {
pub async fn post<T: Serialize + Debug>(&self, activity: OutgoingActivity<T>) {
event!(Level::INFO, ?activity, "activity in outbox");
let raw = RawActivity {
context: "https://www.w3.org/ns/activitystreams".to_string(),
id: activity.req.id.clone(),
@ -127,9 +127,11 @@ impl<'a> Outbox<'a> {
.await
.unwrap();
event!(Level::DEBUG,
outbox_res, activity = activity.req.id,
"got response for outbox dispatch"
event!(
Level::DEBUG,
outbox_res,
activity = activity.req.id,
"got response for outbox dispatch"
);
}

View file

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

View file

@ -1,21 +1,21 @@
use std::sync::mpsc;
use std::thread;
use tracing::{info, span, Level};
use tracing::{Level, info, span};
#[derive(Debug)]
pub enum QueueMessage {
Heartbeat
Heartbeat,
}
pub struct RequestQueue {
name: &'static str,
send: mpsc::Sender<QueueMessage>,
recv: mpsc::Receiver<QueueMessage>
recv: mpsc::Receiver<QueueMessage>,
}
#[derive(Clone)]
pub struct QueueHandle {
send: mpsc::Sender<QueueMessage>
send: mpsc::Sender<QueueMessage>,
}
impl QueueHandle {
@ -27,33 +27,29 @@ impl QueueHandle {
impl RequestQueue {
pub fn new(name: &'static str) -> Self {
let (send, recv) = mpsc::channel();
Self {
name,
send,
recv
}
Self { name, send, recv }
}
pub fn spawn(self) -> QueueHandle {
info!("starting up queue '{}'", self.name);
thread::spawn(move || {
info!("queue '{}' up", self.name);
let recv = self.recv;
while let Ok(req) = recv.recv() {
// FIXME: When we make this do async things we will need to add tokio and
// use proper async handled spans as the enter/drop won't work.
// See inbox.rs for how we handle that.
let s = span!(Level::INFO, "queue", queue_name = self.name);
let _enter = s.enter();
info!(?req, "got a message into the queue");
drop(_enter);
}
});
QueueHandle { send: self.send }
}
}

View file

@ -35,7 +35,6 @@ pub struct User {
display_name: String,
}
#[derive(Error, Debug)]
pub enum UserError {
#[error("user `{0}` not found")]
@ -67,7 +66,10 @@ impl User {
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!(
r#"
SELECT u.*, a.id as "actor_own_id", a.inbox, a.outbox
@ -77,10 +79,10 @@ impl User {
"#,
uuid
)
.fetch_one(conn)
.await
.map_err(|_| UserError::NotFound(uuid.to_string()))?;
.fetch_one(conn)
.await
.map_err(|_| UserError::NotFound(uuid.to_string()))?;
Ok(User {
id: user.id,
username: user.username,
@ -106,10 +108,10 @@ impl User {
"#,
username
)
.fetch_one(conn)
.await
.unwrap();
.fetch_one(conn)
.await
.unwrap();
User {
id: user.id,
username: user.username,

View file

@ -14,7 +14,7 @@ impl Config {
pub fn host(&self) -> &str {
&self.server.host
}
pub fn user_url(&self, user_uuid: &str) -> String {
format!("{}/users/{}", self.host(), user_uuid)
}
@ -26,7 +26,7 @@ impl Config {
pub fn followers_url(&self, user_uuid: &str) -> String {
format!("{}/followers", self.user_url(user_uuid))
}
pub fn following_url(&self, user_uuid: &str) -> String {
format!("{}/following", self.user_url(user_uuid))
}

View file

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

View file

@ -1,18 +1,18 @@
use crate::types_rewrite::api;
use crate::types_rewrite::ap;
use crate::types_rewrite::db;
use crate::types::ap;
use crate::types::api;
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 {
fn from(val: db::Actor) -> ap::Actor {
ap::Actor {
obj: Object {
context: as_context(),
id: val.id
id: val.id,
},
inbox: val.inbox,
outbox: val.outbox
outbox: val.outbox,
}
}
}
@ -22,7 +22,7 @@ impl From<ap::Actor> for db::Actor {
db::Actor {
id: val.obj.id,
inbox: val.inbox,
outbox: val.outbox
outbox: val.outbox,
}
}
}
@ -34,28 +34,51 @@ impl From<db::User> for api::Account {
username: val.username,
acct: val.acct,
display_name: val.display_name,
locked: false,
bot: false,
created_at: val.created_at.to_rfc3339(),
attribution_domains: vec![],
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(),
followers_count: 0,
following_count: 0,
statuses_count: 0,
last_status_at: val.posts.last_post_at.map(|ts| ts.to_rfc3339()),
emojis: vec![],
fields: vec![],
}
}
}
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 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>> {
NaiveDateTime::parse_from_str(&ts, SQLITE_TIME_FMT)
@ -14,8 +13,9 @@ fn parse_ts(ts: String) -> Option<DateTime<Utc>> {
pub async fn user_by_id(id: ObjectUuid, conn: &mut SqliteConnection) -> Result<db::User, DbError> {
info!("fetching user by uuid '{:?}' from the database", id);
let record = sqlx::query!(r#"
let record = sqlx::query!(
r#"
SELECT
u.id as "user_id",
u.username,
@ -30,46 +30,54 @@ pub async fn user_by_id(id: ObjectUuid, conn: &mut SqliteConnection) -> Result<d
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| DbError::FetchError(e.to_string()))?;
"#,
id.0
)
.fetch_one(&mut *conn)
.await
.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)
FROM "follow"
WHERE followed_id = ?1
"#, record.actor_id)
.fetch_one(&mut *conn)
.await
.map_err(|e| DbError::FetchError(e.to_string()))?;
"#,
record.actor_id
)
.fetch_one(&mut *conn)
.await
.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)
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| DbError::FetchError(e.to_string()))?
.and_then(|ts| {
info!("parsing timestamp {}", ts);
parse_ts(ts)
});
"#,
record.user_id
)
.fetch_one(&mut *conn)
.await
.map_err(|e| DbError::FetchError(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
outbox: record.outbox,
},
acct: record.acct,
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,
created_at: user_created,
url: record.url,
posts: db::UserPosts {
last_post_at
}
posts: db::UserPosts { 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 thiserror::Error;
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
use thiserror::Error;
use uuid::Uuid;
pub mod convert;
@ -12,10 +12,10 @@ pub enum DbError {
#[error("an unknown error occured when creating: {0}")]
CreationError(String),
#[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 {
ObjectContext::Str(AS_CONTEXT_RAW.to_string())
}
@ -33,6 +33,12 @@ pub struct ObjectUri(pub String);
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)]
pub struct ObjectUuid(pub String);
impl Default for ObjectUuid {
fn default() -> Self {
Self::new()
}
}
impl ObjectUuid {
pub fn new() -> Self {
Self(Uuid::new_v4().to_string())
@ -42,13 +48,13 @@ impl ObjectUuid {
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct Object {
#[serde(rename = "@context")]
context: ObjectContext,
id: ObjectUri,
pub context: ObjectContext,
pub id: ObjectUri,
}
pub mod db {
use chrono::{DateTime, Utc};
use super::*;
use chrono::{DateTime, Utc};
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct Actor {
@ -60,7 +66,7 @@ pub mod db {
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct UserPosts {
// User may have no posts
pub last_post_at: Option<DateTime<Utc>>
pub last_post_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Eq, PartialEq, Clone)]
@ -73,86 +79,210 @@ pub mod db {
pub remote: bool,
pub url: String,
pub created_at: DateTime<Utc>,
pub posts: UserPosts
pub posts: UserPosts,
}
}
pub mod ap {
use serde::{Serialize, Deserialize};
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)]
pub struct Actor {
#[serde(flatten)]
pub obj: Object,
pub inbox: String,
pub outbox: String,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Person {
#[serde(flatten)]
pub obj: Object,
pub following: String,
pub followers: String,
pub summary: String,
pub inbox: String,
pub outbox: String,
pub preferred_username: String,
pub name: String,
pub public_key: Option<UserKey>,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct UserKey {
pub id: String,
pub owner: String,
#[serde(rename = "publicKeyPem")]
pub public_key: String,
}
}
pub mod api {
use serde::{Serialize, Deserialize};
use super::*;
use serde::{Deserialize, Serialize};
// API will not really use actors so treat them as DB actors
// until we require specificity
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)]
pub struct Account {
pub id: ObjectUuid,
pub username: String,
pub acct: String,
pub display_name: String,
pub locked: bool,
pub bot: bool,
pub created_at: String,
pub attribution_domains: Vec<String>,
pub note: String,
pub url: String,
pub avatar: String,
pub avatar_static: String,
pub header: String,
pub header_static: String,
pub followers_count: i64,
pub following_count: i64,
pub statuses_count: i64,
pub last_status_at: Option<String>,
pub emojis: Vec<Emoji>,
pub fields: Vec<CustomField>,
}
@ -277,11 +407,11 @@ pub mod api {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ap_actor_to_db() {
let domain = "https://example.com";
let ap = ap::Actor {
obj: Object {
context: as_context(),
@ -292,11 +422,14 @@ mod tests {
};
let db: db::Actor = ap.into();
assert_eq!(db, db::Actor {
id: ObjectUri("https://example.com/users/sample".to_string()),
inbox: "https://example.com/users/sample/inbox".to_string(),
outbox: "https://example.com/users/sample/outbox".to_string(),
});
assert_eq!(
db,
db::Actor {
id: ObjectUri("https://example.com/users/sample".to_string()),
inbox: "https://example.com/users/sample/inbox".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)
}