diff --git a/Cargo.toml b/Cargo.toml index 779f393..9129ba7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,6 @@ async-trait = "0.1.88" atproto = { path = "./atproto" } serde = "1.0.219" serde_json = "1.0.140" -thiserror = "2.0.12" tokio = { version = "1.45.0", features = ["macros", "rt-multi-thread"] } tracing = "0.1.41" tracing-subscriber = "0.3.19" diff --git a/atproto/Cargo.toml b/atproto/Cargo.toml index 033ff0d..7d56725 100644 --- a/atproto/Cargo.toml +++ b/atproto/Cargo.toml @@ -8,7 +8,5 @@ atrium-api = { version = "0.25.3", default-features = false } lazy-regex = "3.4.1" serde.workspace = true serde_json.workspace = true -time = { version = "0.3.41", features = ["parsing", "formatting"] } tracing-subscriber.workspace = true tracing.workspace = true -thiserror.workspace = true diff --git a/atproto/src/error.rs b/atproto/src/error.rs deleted file mode 100644 index 1db7106..0000000 --- a/atproto/src/error.rs +++ /dev/null @@ -1,31 +0,0 @@ -use thiserror::Error as ThisError; - -#[non_exhaustive] -#[derive(Debug, ThisError)] -pub enum Error { - #[error("Error while parsing")] - Parse { err: ParseError, object: String }, - #[error("Error while formatting")] - Format { err: FormatError, object: String }, -} - -#[non_exhaustive] -#[derive(Debug, ThisError)] -pub enum FormatError { - #[error("Time Parse Error: {0}")] - Datetime(#[from] time::error::Format), -} -#[non_exhaustive] -#[derive(Debug, ThisError)] -pub enum ParseError { - #[error("Time Parse Error: {0}")] - Datetime(#[from] time::error::Parse), - #[error("Length of parsed object too long, max: {max:?}, got: {got:?}.")] - Length { max: usize, got: usize }, - #[error("Currently Did is enforced, cannot use handle, {handle:?}")] - ForceDid { handle: String }, - #[error("Incorrectly formatted")] - Format, -} - -pub type Result = std::result::Result; diff --git a/atproto/src/lib.rs b/atproto/src/lib.rs index c3b56ee..a327896 100644 --- a/atproto/src/lib.rs +++ b/atproto/src/lib.rs @@ -1,3 +1,80 @@ +use lazy_regex::regex_captures; +use core::str::FromStr; +use std::fmt::{ + Display, Formatter, Result as FmtResult +}; + +pub use atrium_api::types::{ + Collection, + string::{ + AtIdentifier as Authority, + Datetime, + Did, + Nsid, + RecordKey, + Tid, + Handle, + } +}; + pub mod lexicons; -pub mod types; -pub mod error; + +pub struct Cid(String); + +impl Display for Cid { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + write!(f, "{}", self.0) + } +} + +pub struct StrongRef { + pub content: T, + pub cid: Cid, +} + +pub struct Uri { + whole: String, + // These fields could be useful in the future, + // so I'm leaving the code for them. + // authority: Authority, + // collection: Option, + // rkey: Option, +} + +impl FromStr for Uri { + type Err = &'static str; + fn from_str(uri: &str) -> Result { + if uri.len() > 8000 { + return Err("Uri too long") + } + + let Some(( + whole, unchecked_authority, unchecked_collection, unchecked_rkey + )) = regex_captures!( + r"/^at:\/\/([\w\.\-_~:]+)(?:\/([\w\.\-_~:]+)(?:)\/([\w\.\-_~:]+))?$/i", + uri, + ) else { + return Err("Invalid Uri"); + }; + + // This parsing is required, but the values don't need to be used yet. + // No compute cost to use them, just storage cost + let _authority = Authority::from_str(unchecked_authority)?; + + let _collection = if unchecked_collection.is_empty() { None } + else { Some(Nsid::new(unchecked_collection.to_string())?) }; + + let _rkey = if unchecked_rkey.is_empty() { None } + else { Some(RecordKey::new(unchecked_rkey.to_string())?) }; + + // Ok(Uri{ whole: whole.to_string(), authority, collection, rkey }) + Ok(Uri { whole: whole.to_string() }) + } +} + +impl Uri { + pub fn as_str(&self) -> &str { + self.whole.as_str() + } +} + diff --git a/atproto/src/types.rs b/atproto/src/types.rs deleted file mode 100644 index ca30ded..0000000 --- a/atproto/src/types.rs +++ /dev/null @@ -1,70 +0,0 @@ -use crate::error::{Error, ParseError}; - -macro_rules! basic_string_type { - ($name:ident, $regex:literal, $max_len:literal) => { - pub struct $name { value: String, } - - impl std::fmt::Display for $name { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.value) - } - } - - impl std::str::FromStr for $name { - type Err = Error; - fn from_str(s: &str) -> Result { - if s.len() > $max_len { - return Err(Error::Parse { - err: ParseError::Length { max: s.len(), got: $max_len }, - object: stringify!($name).to_string(), - }); - } - - if ! lazy_regex::regex_is_match!( - $regex, - s - ) { - return Err(Error::Parse { - err: ParseError::Format, - object: stringify!($name).to_string(), - }); - } - - Ok(Self { - value: s.to_string(), - }) - } - } - } -} - -mod did; -pub use did::Did; -mod cid; -pub use cid::Cid; -mod authority; -pub use authority::Authority; -mod datetime; -pub use datetime::Datetime; -mod record_key; -pub use record_key::RecordKey; -mod uri; -pub use uri::Uri; - -basic_string_type!(Handle, - r"^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$", - 253 -); -basic_string_type!(Nsid, - r"^[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(\.[a-zA-Z]([a-zA-Z0-9]{0,62})?)$", - 317 -); -basic_string_type!(Tid, - r"^[234567abcdefghij][234567abcdefghijklmnopqrstuvwxyz]{12}$", - 13 -); - -pub struct StrongRef { - content: T, - cid: Cid, -} diff --git a/atproto/src/types/authority.rs b/atproto/src/types/authority.rs deleted file mode 100644 index 1c76750..0000000 --- a/atproto/src/types/authority.rs +++ /dev/null @@ -1,38 +0,0 @@ -use crate::{ - types::{Did, Handle}, - error::{Error, ParseError}, -}; -use std::{ - fmt::{Display, Formatter, Result as FmtResult}, - str::FromStr, -}; - -pub enum Authority { - Did(Did), - Handle(Handle), -} - -impl Display for Authority { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - write!(f, "{}", match self { - Authority::Did(did) => did.to_string(), - Authority::Handle(handle) => handle.to_string(), - }) - } -} - -impl FromStr for Authority { - type Err = Error; - fn from_str(s: &str) -> Result { - if let Ok(did) = s.parse::() { - return Ok(Authority::Did(did)); - } - if let Ok(did) = s.parse::() { - return Ok(Authority::Handle(did)); - } - Err(Error::Parse { - err: ParseError::Format, - object: "Authority".to_string(), - }) - } -} diff --git a/atproto/src/types/cid.rs b/atproto/src/types/cid.rs deleted file mode 100644 index 3581ad2..0000000 --- a/atproto/src/types/cid.rs +++ /dev/null @@ -1,16 +0,0 @@ -use crate::error::Error; - -pub struct Cid { value: String, } - -impl std::fmt::Display for Cid { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.value) - } -} - -impl std::str::FromStr for Cid { - type Err = Error; - fn from_str(s: &str) -> Result { - Ok(Self { value: s.to_string() }) - } -} diff --git a/atproto/src/types/datetime.rs b/atproto/src/types/datetime.rs deleted file mode 100644 index 6dcf234..0000000 --- a/atproto/src/types/datetime.rs +++ /dev/null @@ -1,63 +0,0 @@ -use crate::error::{Error, ParseError, FormatError}; -use time::{ - UtcDateTime, - format_description::well_known::{Rfc3339, Iso8601}, -}; -use std::{ - fmt::{Display, Formatter, Result as FmtResult}, - str::FromStr, -}; -pub use time::{Date, Time}; - -pub struct Datetime { - time: UtcDateTime, - derived_string: String, -} - -impl Datetime { - pub fn now() -> Result { - Datetime::from_utc(UtcDateTime::now()) - } - - pub fn new(date: Date, time: Time) -> Result { - Datetime::from_utc(UtcDateTime::new(date, time)) - } - - fn from_utc(utc: UtcDateTime) -> Result { - Ok(Datetime { - time: utc, - derived_string: match utc.format(&Rfc3339) { - Ok(ds) => ds, - Err(e) => return Err(Error::Format { - err: FormatError::Datetime(e), - object: "Datetime".to_string(), - }), - }, - }) - } -} - -impl Display for Datetime { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - write!(f, "{}", self.derived_string) - } -} - -impl FromStr for Datetime { - type Err = Error; - - fn from_str(s: &str) -> Result { - // Parse as both Rfc3339 and Iso8601 to ensure intersection - let dt_rfc3339 = UtcDateTime::parse(s, &Rfc3339); - let dt_iso8601 = UtcDateTime::parse(s, &Iso8601::DEFAULT); - - let datetime = dt_iso8601 - .and(dt_rfc3339) - .map_err(|e| Error::Parse { - err: ParseError::Datetime(e), - object: "Datetime".to_string(), - })?; - - Ok(Datetime { time: datetime, derived_string: s.to_string() }) - } -} diff --git a/atproto/src/types/did.rs b/atproto/src/types/did.rs deleted file mode 100644 index 6c91db2..0000000 --- a/atproto/src/types/did.rs +++ /dev/null @@ -1,72 +0,0 @@ -use crate::error::{Error, ParseError}; -use std::{ - fmt::{Display, Formatter, Result as FmtResult}, - str::FromStr, -}; -use lazy_regex::regex_captures; - -enum DidMethod { - Web, - Plc, -} -impl Display for DidMethod { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - write!(f, "{}", match self { - DidMethod::Web => String::from("web"), - DidMethod::Plc => String::from("plc"), - }) - } -} -impl FromStr for DidMethod { - type Err = Error; - fn from_str(s: &str) -> Result { - match s { - "web" => Ok(DidMethod::Web), - "plc" => Ok(DidMethod::Plc), - _ => Err(Error::Parse { - err: ParseError::Format, - object: "DidMethod".to_string(), - }), - } - } -} - -pub struct Did { - method: DidMethod, - identifier: String, -} - -impl Display for Did { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - write!(f, "did:{}:{}", self.method, self.identifier) - } -} - -impl FromStr for Did { - type Err = Error; - fn from_str(s: &str) -> Result { - if s.len() > 2048 { - return Err(Error::Parse { - err: ParseError::Length { max: s.len(), got: 2048 }, - object: "Did".to_string(), - }); - } - - let Some(( - _whole, unchecked_method, identifier - )) = regex_captures!( - r"^did:([a-z]+):([a-zA-Z0-9._:%-]*[a-zA-Z0-9._-])$", - s, - ) else { - return Err(Error::Parse { - err: ParseError::Format, - object: "Did".to_string(), - }); - }; - - Ok(Self { - method: unchecked_method.parse::()?, - identifier: identifier.to_string(), - }) - } -} diff --git a/atproto/src/types/record_key.rs b/atproto/src/types/record_key.rs deleted file mode 100644 index 7368b53..0000000 --- a/atproto/src/types/record_key.rs +++ /dev/null @@ -1,63 +0,0 @@ -use crate::{ - types::{Nsid, Tid}, - error::{Error, ParseError}, -}; -use std::{ - fmt::{Display, Formatter, Result as FmtResult}, - str::FromStr, -}; -use lazy_regex::regex_is_match; - -pub enum RecordKey { - Tid(Tid), - Nsid(Nsid), - Literal(String), - Any(String), -} -impl Display for RecordKey { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - write!(f, "{}", match self { - RecordKey::Tid(tid) => tid.to_string(), - RecordKey::Nsid(nsid) => nsid.to_string(), - RecordKey::Literal(literal) => literal.to_string(), - RecordKey::Any(any) => any.to_string(), - }) - } -} -impl FromStr for RecordKey { - type Err = Error; - fn from_str(s: &str) -> Result { - if s.len() > 512 { - return Err(Error::Parse { - err: ParseError::Length { max: 512, got: s.len() }, - object: "RecordKey".to_string(), - }); - } - - if !( - regex_is_match!( - r"^[a-zA-Z0-9`.-_:~]+$", - s, - ) - && s != "." - && s != ".." - ) { - return Err(Error::Parse { - err: ParseError::Format, - object: "RecordKey".to_string(), - }); - } - - // Valid record key, now decide type - if s.starts_with("literal:") { - return Ok(RecordKey::Literal(s.to_string())); - } - if let Ok(tid) = s.parse::() { - return Ok(RecordKey::Tid(tid)); - } - if let Ok(nsid) = s.parse::() { - return Ok(RecordKey::Nsid(nsid)); - } - Ok(RecordKey::Any(s.to_string())) - } -} diff --git a/atproto/src/types/uri.rs b/atproto/src/types/uri.rs deleted file mode 100644 index 843ee2b..0000000 --- a/atproto/src/types/uri.rs +++ /dev/null @@ -1,79 +0,0 @@ -use crate::{ - types::{Did, Authority, Nsid, RecordKey}, - error::{Error, ParseError}, -}; -use std::{ - fmt::{Display, Formatter, Result as FmtResult}, - str::FromStr, -}; -use lazy_regex::regex_captures; - -pub struct Uri { - authority: Did, - collection: Option, - rkey: Option, -} - -impl Display for Uri { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - write!(f, "at://{}", self.authority)?; - - if let Some(collection) = &self.collection { - write!(f, "/{}", collection)?; - - if let Some(rkey) = &self.rkey { - write!(f, "/{}", rkey)?; - } - } - - Ok(()) - } -} - -impl FromStr for Uri { - type Err = Error; - fn from_str(s: &str) -> Result { - if s.len() > 8000 { - return Err(Error::Parse { - err: ParseError::Length { max: 8000, got: s.len() }, - object: "Did".to_string(), - }); - } - - let Some(( - _whole, unchecked_authority, unchecked_collection, unchecked_rkey - )) = regex_captures!( - r"/^at:\/\/([\w\.\-_~:]+)(?:\/([\w\.\-_~:]+)(?:)\/([\w\.\-_~:]+))?$/i", - s, - ) else { - return Err(Error::Parse { - err: ParseError::Format, - object: "Uri".to_string(), - }); - }; - - let did = match Authority::from_str(unchecked_authority)? { - Authority::Handle(h) => - return Err(Error::Parse { - err: ParseError::ForceDid { handle: h.to_string() }, - object: "Uri".to_string(), - }), - Authority::Did(d) => d, - }; - - let collection = if unchecked_collection.is_empty() { None } - else { Some(unchecked_collection.parse::()?) }; - - let rkey = if unchecked_rkey.is_empty() { None } - else { Some(unchecked_rkey.parse::()?) }; - - Ok(Uri { authority: did, collection, rkey }) - } -} - -impl Uri { - pub fn authority_as_did(&self) -> &Did { - &self.authority - } -} - diff --git a/db/Cargo.toml b/db/Cargo.toml index 13b55ae..dd1d0b9 100644 --- a/db/Cargo.toml +++ b/db/Cargo.toml @@ -4,8 +4,8 @@ version = "0.1.0" edition = "2024" [dependencies] +thiserror = "2.0.12" atproto.workspace = true async-trait.workspace = true -sqlx.workspace = true -thiserror.workspace = true +sqlx = { version = "0.8.6", features = ["postgres", "runtime-tokio"] } tokio.workspace = true diff --git a/db/migrations/20250612223204_initial_schema.sql b/db/migrations/20250612223204_initial_schema.sql deleted file mode 100644 index f52b19b..0000000 --- a/db/migrations/20250612223204_initial_schema.sql +++ /dev/null @@ -1,45 +0,0 @@ --- Add migration script here - -CREATE EXTENSION IF NOT EXISTS pg_trgm; - -CREATE TABLE actor ( - did VARCHAR PRIMARY KEY, - handle VARCHAR UNIQUE, - indexed_at VARCHAR NOT NULL -); -CREATE INDEX actor_handle_trgm_idx ON actor USING gist (handle gist_trgm_ops); - -CREATE TABLE session ( - uri VARCHAR PRIMARY KEY, - cid VARCHAR NOT NULL, - owner VARCHAR NOT NULL, - - content VARCHAR NOT NULL, - contentcid VARCHAR NOT NULL, - label VARCHAR, - -- Participants in participant - - created_at VARCHAR NOT NULL, - indexed_at VARCHAR NOT NULL, - sort_at VARCHAR GENERATED ALWAYS AS (LEAST(created_at,indexed_at)) STORED NOT NULL -); - -CREATE TABLE activity ( - uri VARCHAR PRIMARY KEY, - cid VARCHAR NOT NULL, - - session VARCHAR, - sessioncid VARCHAR, - -- Progress in progress - - performed_at VARCHAR, - created_at VARCHAR NOT NULL, - indexed_at VARCHAR NOT NULL, - sort_at VARCHAR GENERATED ALWAYS AS (LEAST(created_at,indexed_at)) STORED NOT NULL -); - -CREATE TABLE participant ( - participantdid VARCHAR NOT NULL, - session VARCHAR NOT NULL, - role VARCHAR NOT NULL -); diff --git a/db/src/connection.rs b/db/src/connection.rs index 71e9c58..1c66086 100644 --- a/db/src/connection.rs +++ b/db/src/connection.rs @@ -1,3 +1,15 @@ +use sqlx::{ + query, + Database, + Pool, + Postgres, + pool::PoolOptions, + postgres::{ + PgConnectOptions, + PgSslMode, + }, + Result, +}; use std::string::ToString; pub struct Db { @@ -27,6 +39,13 @@ pub struct Session { impl Db { async fn connect() -> Result { + let conn = PgConnectOptions::new() + .host("localhost") + .port(5432) + .username("postgres") + .password("062217") + .database("anisky") + .ssl_mode(PgSslMode::Disable); let pool = match PoolOptions::new().connect_with(conn).await { Ok(p) => p, diff --git a/db/src/error.rs b/db/src/error.rs deleted file mode 100644 index ed1b585..0000000 --- a/db/src/error.rs +++ /dev/null @@ -1,8 +0,0 @@ -#[non_exhaustive] -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error("Database Implementation Error: {0}")] - Backend(#[from] sqlx::Error), -} - -pub type Result = std::result::Result; diff --git a/db/src/interfaces/spoor.rs b/db/src/interfaces/spoor.rs index 4a27dcb..f11386e 100644 --- a/db/src/interfaces/spoor.rs +++ b/db/src/interfaces/spoor.rs @@ -1,4 +1,4 @@ -use crate::Error; +use crate::DbError; use atproto::{ Cid, Uri, @@ -56,15 +56,15 @@ pub enum Progress { pub async fn ingest_session( db: PgPool, session: Session -) -> Result<(), Error> { +) -> Result<(), DbError> { let mut transaction = db.begin().await?; write_session(&mut transaction, session).await?; - transaction.commit().await.map_err(Error::Backend) + transaction.commit().await.map_err(DbError::Backend) } async fn write_session( tr: &mut PgTransaction<'_>, session: Session -) -> Result<(), Error> { +) -> Result<(), DbError> { let (contenturi, contentcid): (Option, String) = match session.content { @@ -94,7 +94,7 @@ async fn write_session( async fn write_participant( tr: &mut PgTransaction<'_>, participant: Participant, sessionuri: Uri -) -> Result<(), Error> { +) -> Result<(), DbError> { let (participantType, user): (String, User) = match participant { Participant::Owner(user) => ("Owner".to_string(), user), Participant::Added(user) => ("Participant".to_string(), user), diff --git a/db/src/lib.rs b/db/src/lib.rs index 6683e08..c93205e 100644 --- a/db/src/lib.rs +++ b/db/src/lib.rs @@ -1,7 +1,12 @@ +use thiserror::Error; + pub mod interfaces; -pub mod error; - -pub use crate::error::Error; +#[non_exhaustive] +#[derive(Debug, Error)] +pub enum DbError { + #[error("Database Implementation Error: {0}")] + Backend(#[from] sqlx::Error), +} // pub struct db; diff --git a/flake.lock b/flake.lock index 69e701e..8f3aa7b 100644 --- a/flake.lock +++ b/flake.lock @@ -41,11 +41,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1749695868, - "narHash": "sha256-debjTLOyqqsYOUuUGQsAHskFXH5+Kx2t3dOo/FCoNRA=", + "lastModified": 1746585402, + "narHash": "sha256-Pf+ufu6bYNA1+KQKHnGMNEfTwpD9ZIcAeLoE2yPWIP0=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "55f914d5228b5c8120e9e0f9698ed5b7214d09cd", + "rev": "72dd969389583664f87aa348b3458f2813693617", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 48d94bf..943945b 100644 --- a/flake.nix +++ b/flake.nix @@ -42,7 +42,6 @@ packages = (with pkgs; [ # The package provided by our custom overlay. Includes cargo, Clippy, cargo-fmt, # rustdoc, rustfmt, and other tools. - sqlx-cli rustToolchain ]) ++ pkgs.lib.optionals pkgs.stdenv.isDarwin (with pkgs; [ libiconv ]); };