#![feature(async_closure)] //! # The Hermit ActivityPub server //! //! This library contains the types and trait impls that make up the ActivityPub //! support and database interaction for the Hermit ActivityPub server. #![feature(generic_associated_types)] use serde::Serialize; // Expose the `Id` type in the crate root pub use id::Id; pub use ctx::Context; // Module imports pub mod conf; pub mod sign; pub mod db; pub mod ap; /// The Activity supertype used in abstractions over any kind of activity. #[derive(Clone, Serialize)] pub enum Activity { /// Create a post. Create (ap::Create), /// Request to follow an actor. Follow (ap::Follow), /// Accept a follow request. Accept (ap::Accept), } /// A result type that defaults to using [`Error`] as the second type /// parameter. pub type Result = std::result::Result; /// Errors generated within Hermit. #[derive(Debug)] pub enum Error { /// [`reqwest`] errors. Http (reqwest::Error), /// [`serde_json`] errors. Json (serde_json::Error), /// [`sqlx`] errors. Sqlx (sqlx::Error), /// A cryptography error from [`openssl`]. OpenSSL (openssl::error::ErrorStack), } impl From for Error { fn from (e: sqlx::Error) -> Self { Error::Sqlx (e) } } impl From for Error { fn from (e: reqwest::Error) -> Self { Error::Http (e) } } impl From for Error { fn from (e: serde_json::Error) -> Self { Error::Json (e) } } impl From for Error { fn from (e: openssl::error::ErrorStack) -> Self { Error::OpenSSL (e) } } /// Trivial conversion function for use in `map_err` functions. pub (crate) fn err (e: impl Into) -> Error { e.into() } mod id { use std::{str::FromStr, error::Error}; use serde::{ Deserialize, Serialize }; use sqlx::database::{HasArguments, HasValueRef}; use url::ParseError; use crate::db; /// An ActivityPub identifier. #[derive(PartialEq, Eq, Clone, Serialize, Deserialize)] #[serde(transparent)] pub struct Id (reqwest::Url); impl FromStr for Id { type Err = url::ParseError; fn from_str (s: &str) -> Result { s.parse().map(Id) } } impl std::fmt::Display for Id { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0.to_string()) } } impl sqlx::Type for Id { fn type_info () -> ::TypeInfo { String::type_info() } } impl<'q> sqlx::Encode<'q, db::Database> for Id { fn encode_by_ref(&self, buf: &mut >::ArgumentBuffer) -> sqlx::encode::IsNull { self.0.to_string().encode_by_ref(buf) } } impl<'r> sqlx::Decode<'r, db::Database> for Id { fn decode(value: >::ValueRef) -> Result { >::decode(value) .map(|s| s.parse().expect("Failed to parse ID as URL")) .map(Id) } } } mod ctx { use std::sync::Arc; use futures::prelude::*; use serde_json::{Value, to_value}; use crate::{ conf::Config, db, Result, sign::Sign, ap::{self, Actor}, Activity, Id, err }; /// The context of a thread/task. /// /// The intended usage pattern is to create a single [`Context`] per /// thread/async task and to propagate updates to the [`Config`] using /// message-passing style between the tasks. The library provides no /// such functionality. Live-reloading is implemented by the program /// itself. pub struct Context { /// The configuration. pub config: Config, /// The signing key used by actions running within this context. pub signer: Arc, /// A handle to the database. pub client: db::Client, } impl Clone for Context { fn clone (&self) -> Context { Context { config: self.config.clone(), signer: self.signer.clone(), client: self.client.clone(), } } } impl Context { pub async fn dereference (&self, json: Value) -> Result where S: Sign { self.dereferencer() .dereference(json) .await } /// Attempt an action within the context of the database. pub async fn with_db <'a, F, O, T> (&'a mut self, f: F) -> Result where F: FnOnce (&'a mut db::Client) -> O, O: Future> + 'a, { f(&mut self.client).await } /// Get all actors on the instance. pub fn actors (&self) -> impl Iterator + '_ { None.into_iter() } /// Get a dereferencer. fn dereferencer (&self) -> Dereferencer where S: Sign { Dereferencer { web: reqwest::Client::new(), signer: self.signer.clone(), db: self.client.clone(), } } /// Access the inner [`Sign`] provider. pub fn signer (&self) -> &S { &self.signer } pub fn config (&self) -> &Config { &self.config } /// Conjure an activity "from thin air" as though it were posted through a client. pub (crate) async fn conjure (&self, act: impl Into) -> Result<()> { let act = act.into(); todo!() } } /// A type that provides dereferencing facilities for [`Activity`] data. pub struct Dereferencer { web: reqwest::Client, db: db::Client, signer: Arc, } impl Dereferencer where S: Sign { /// Perform the dereferencing. pub async fn dereference (&self, json: Value) -> Result { match json["type"].as_str() { Some ("Create") => self.deref_create(json).await.map(Activity::Create), _ => todo!() } } fn db_client (&self) -> &db::Client { &self.db } fn web_client (&self) -> &reqwest::Client { &self.web } /// Fetch a JSON value. pub async fn fetch (&self, url: impl crate::IntoUrl) -> Result { let client = self.web_client(); let url = match url.into_url() { Some (url) => url, None => todo!(), }; let req = { let mut r = client.get(url).build()?; self.signer.sign(&mut r)?; r }; let value = client .execute(req) .await? .json() .await?; Ok (value) } /// Fetch a value from the database by trying all the ActivityPub /// records. async fn db_fetch (&self, id: Id) -> Result> { if let Some (data) = self.db.get(db::get_actor(&id)).await? { return to_value(data) .map_err(err) .map(Some) } todo!() } /// Attempt to dereference to a [`Create`](ap::Create) activity. async fn deref_create (&self, json: Value) -> Result { let json = if let Value::String (url) = json { self.fetch(url).await? } else { json }; match json["object"]["type"].as_str() { Some ("Note" | "Article") => todo!(), //Ok (act::Create::Note { id }), _ => return Err (todo!()), } } } } /// Types that can be mapped to a [`Url`](url::Url). pub trait IntoUrl { /// Perform the conversion. fn into_url (self) -> Option; } impl IntoUrl for T where T: ToString { fn into_url (self) -> Option { self.to_string() .parse() .ok() } }