diff options
Diffstat (limited to 'util')
-rw-r--r-- | util/Cargo.toml | 29 | ||||
-rw-r--r-- | util/src/error.rs | 95 | ||||
-rw-r--r-- | util/src/lib.rs | 123 | ||||
-rw-r--r-- | util/src/queue.rs | 66 |
4 files changed, 313 insertions, 0 deletions
diff --git a/util/Cargo.toml b/util/Cargo.toml new file mode 100644 index 0000000..0425849 --- /dev/null +++ b/util/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "kittybox-util" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[features] +fs = ["rand", "tokio", "tokio/fs"] + +[dependencies] +serde = { version = "^1.0.170", features = ["derive"] } +serde_json = "^1.0.64" +axum-core = "^0.3.4" +http = "^0.2.7" +async-trait = "^0.1.50" +futures-util = "^0.3.14" +uuid = "^1.3.3" +[dependencies.rand] +version = "^0.8.5" +optional = true +[dependencies.tokio] +version = "^1.16.1" +features = ["tracing"] +optional = true +[dependencies.sqlx] +version = "0.7" +features = ["json"] +optional = true \ No newline at end of file diff --git a/util/src/error.rs b/util/src/error.rs new file mode 100644 index 0000000..1c95020 --- /dev/null +++ b/util/src/error.rs @@ -0,0 +1,95 @@ +use serde::{Deserialize, Serialize}; +use http::StatusCode; +use axum_core::response::{Response, IntoResponse}; + +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)] +#[serde(rename_all = "snake_case")] +/// Kinds of errors that can happen within a Micropub operation. +pub enum ErrorType { + /// An erroneous attempt to create something that already exists. + AlreadyExists, + /// Current user is expressly forbidden from performing this action. + Forbidden, + /// The Micropub server experienced an internal error. + InternalServerError, + /// The request was invalid or malformed. + InvalidRequest, + /// The provided OAuth2 scopes were insufficient to allow performing this action. + InvalidScope, + /// There was no token or other means of authorization in the request. + NotAuthorized, + /// Whatever was requested was not found. + NotFound, + /// The request payload was of a type unsupported by the Micropub endpoint. + UnsupportedMediaType, +} + +/// Representation of the Micropub API error. +#[derive(Serialize, Deserialize, Debug)] +pub struct MicropubError { + /// General kind of an error that occured. + pub error: ErrorType, + /// A human-readable error description intended for application developers. + // TODO use Cow<'static, str> to save on heap allocations + pub error_description: String, +} + +impl std::error::Error for MicropubError {} + +impl std::fmt::Display for MicropubError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("Micropub error: ")?; + f.write_str(&self.error_description) + } +} + +impl From<serde_json::Error> for MicropubError { + fn from(err: serde_json::Error) -> Self { + use ErrorType::*; + Self { + error: InvalidRequest, + error_description: err.to_string(), + } + } +} + +impl MicropubError { + /// Create a new Micropub error. + pub fn new(error: ErrorType, error_description: &str) -> Self { + Self { + error, + error_description: error_description.to_owned(), + } + } +} + +impl From<&MicropubError> for StatusCode { + fn from(err: &MicropubError) -> Self { + use ErrorType::*; + match err.error { + AlreadyExists => StatusCode::CONFLICT, + Forbidden => StatusCode::FORBIDDEN, + InternalServerError => StatusCode::INTERNAL_SERVER_ERROR, + InvalidRequest => StatusCode::BAD_REQUEST, + InvalidScope => StatusCode::UNAUTHORIZED, + NotAuthorized => StatusCode::UNAUTHORIZED, + NotFound => StatusCode::NOT_FOUND, + UnsupportedMediaType => StatusCode::UNSUPPORTED_MEDIA_TYPE, + } + } +} +impl From<MicropubError> for StatusCode { + fn from(err: MicropubError) -> Self { + (&err).into() + } +} + +impl IntoResponse for MicropubError { + fn into_response(self) -> Response { + IntoResponse::into_response(( + StatusCode::from(&self), + [("Content-Type", "application/json")], + serde_json::to_string(&self).unwrap(), + )) + } +} diff --git a/util/src/lib.rs b/util/src/lib.rs new file mode 100644 index 0000000..c49bdf5 --- /dev/null +++ b/util/src/lib.rs @@ -0,0 +1,123 @@ +#![warn(missing_docs)] +//! Small things that couldn't fit elsewhere in Kittybox, yet may be +//! useful on their own or in multiple Kittybox crates. +//! +//! Some things are gated behind features, namely: +//! - `fs` - enables use of filesystem-related utilities +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Serialize, Deserialize)] +pub struct IndiewebEndpoints { + pub authorization_endpoint: String, + pub token_endpoint: String, + pub webmention: Option<String>, + pub microsub: Option<String>, +} + +/// Data structure representing a Micropub channel in the ?q=channels output. +#[derive(Serialize, Deserialize, PartialEq, Debug)] +#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] +pub struct MicropubChannel { + /// The channel's UID. It is usually also a publically accessible permalink URL. + pub uid: String, + /// The channel's user-friendly name used to recognize it in lists. + pub name: String, +} + +#[derive(Debug, Default)] +/// Common types of webmentions. +pub enum MentionType { + /// Corresponds to a `u-in-reply-to` link. + Reply, + /// Corresponds to a `u-like-of` link. + Like, + /// Corresponds to a `u-repost-of` link. + Repost, + /// Corresponds to a `u-bookmark-of` link. + Bookmark, + /// A plain link without MF2 annotations. + #[default] + Mention +} + +/// Common errors from the IndieWeb protocols that can be reused between modules. +pub mod error; +pub use error::{ErrorType, MicropubError}; + +/// Common data-types useful in creating smart authentication systems. +pub mod auth { + #[derive(PartialEq, Eq, Hash, Clone, Copy)] + pub enum EnrolledCredential { + /// An indicator that a password is enrolled. Passwords can be + /// used to recover from a lost token. + Password, + /// An indicator that one or more WebAuthn credentials were + /// enrolled. + WebAuthn + } +} + +/// A collection of traits for implementing a robust job queue. +pub mod queue; + +#[cfg(feature = "fs")] +/// Commonly-used operations with the file system in Kittybox's +/// underlying storage mechanisms. +pub mod fs { + use std::io::{self, Result}; + use std::path::{Path, PathBuf}; + use rand::{Rng, distributions::Alphanumeric}; + use tokio::fs; + + /// Create a temporary file named `temp.[a-zA-Z0-9]{length}` in + /// the given location and immediately open it. Returns the + /// filename and the corresponding file handle. It is the caller's + /// responsibility to clean up the temporary file when it is no + /// longer needed. + /// + /// Uses [`OpenOptions::create_new`][fs::OpenOptions::create_new] + /// to detect filename collisions, in which case it will + /// automatically retry until the operation succeeds. + /// + /// # Errors + /// + /// Returns the underlying [`io::Error`] if the operation fails + /// due to reasons other than filename collision. + pub async fn mktemp<T, B>(dir: T, basename: B, length: usize) -> Result<(PathBuf, fs::File)> + where + T: AsRef<Path>, + B: Into<Option<&'static str>> + { + let dir = dir.as_ref(); + let basename = basename.into().unwrap_or(""); + fs::create_dir_all(dir).await?; + + loop { + let filename = dir.join(format!( + "{}{}{}", + basename, + if basename.is_empty() { "" } else { "." }, + { + let string = rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(length) + .collect::<Vec<u8>>(); + String::from_utf8(string).unwrap() + } + )); + + match fs::OpenOptions::new() + .create_new(true) + .write(true) + .open(&filename) + .await + { + Ok(file) => return Ok((filename, file)), + Err(err) => match err.kind() { + io::ErrorKind::AlreadyExists => continue, + _ => return Err(err) + } + } + } + } +} diff --git a/util/src/queue.rs b/util/src/queue.rs new file mode 100644 index 0000000..c880597 --- /dev/null +++ b/util/src/queue.rs @@ -0,0 +1,66 @@ +use futures_util::Stream; +use std::pin::Pin; +use uuid::Uuid; + +#[async_trait::async_trait] +/// A job queue that can store and return jobs. +pub trait JobQueue<T: JobItem>: Send + Sync + Sized + Clone + 'static { + /// A type of job object that will be returned by the queue. + type Job: Job<T, Self>; + /// Error type that the queue can produce in its work. + type Error: std::error::Error + Send + Sync + Sized; + + /// Get one item from the job queue, if the job queue has pending + /// items available. + /// + /// # Errors + /// + /// Returns an error if a job queue failed in some way. Having no + /// items is not a failure, in which case `Ok(None)` is returned. + async fn get_one(&self) -> Result<Option<Self::Job>, Self::Error>; + /// Put an item into a job queue, returning its UUID. + async fn put(&self, item: &T) -> Result<Uuid, Self::Error>; + + /* + /// Check the amount of pending and stuck items in the job queue. + async fn len(&self) -> Result<(usize, usize), Self::Error>; + /// Returns whether the job queue has some pending items. + async fn is_empty(&self) -> Result<bool, Self::Error> { + Ok(self.len().await?.0 == 0) + } + /// Returns whether the job queue has some stuck items that + /// require manual cleanup. + async fn has_stuck(&self) -> Result<bool, Self::Error> { + Ok(self.len().await?.1 > 0) + } + */ + + /// Consume the job queue object and return a stream of unstuck + /// items from the job queue. + /// + /// Note that one item may be returned several times if it is not + /// marked as done. + async fn into_stream(self) -> Result<Pin<Box<dyn Stream<Item = Result<Self::Job, Self::Error>> + Send>>, Self::Error>; +} + +#[async_trait::async_trait] +/// A job description yielded from a job queue. +/// +/// # Implementors +/// +/// On [`Drop`], the job should be returned to a job queue. If your +/// job queue tracks attempts, the counter should be incremented by +/// one. +/// +/// Figuring how to do this asynchronously from a synchronous trait +/// is left as an exercise to the reader. +pub trait Job<T: JobItem, Q: JobQueue<T>>: Send + Sync + Sized { + /// Get the object describing the task itself. + fn job(&self) -> &T; + /// Mark the job as done and remove it from the job queue. + async fn done(self) -> Result<(), Q::Error>; +} + +/// An object describing the job itself, returned as part of a +/// [`Job`]. +pub trait JobItem: Send + Sync + Sized + std::fmt::Debug {} |