hermit/src/lib.rs

303 lines
7.7 KiB
Rust

#![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 <T, E = Error> = std::result::Result<T, E>;
/// 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<sqlx::Error> for Error {
fn from (e: sqlx::Error) -> Self { Error::Sqlx (e) }
}
impl From<reqwest::Error> for Error {
fn from (e: reqwest::Error) -> Self { Error::Http (e) }
}
impl From<serde_json::Error> for Error {
fn from (e: serde_json::Error) -> Self { Error::Json (e) }
}
impl From<openssl::error::ErrorStack> 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>) -> 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<Self, Self::Err> {
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<db::Database> for Id {
fn type_info () -> <db::Database as sqlx::Database>::TypeInfo {
String::type_info()
}
}
impl<'q> sqlx::Encode<'q, db::Database> for Id {
fn encode_by_ref(&self, buf: &mut <db::Database as HasArguments<'q>>::ArgumentBuffer) -> sqlx::encode::IsNull {
self.0.to_string().encode_by_ref(buf)
}
}
impl<'r> sqlx::Decode<'r, db::Database> for Id {
fn decode(value: <db::Database as HasValueRef<'r>>::ValueRef) -> Result<Self, sqlx::error::BoxDynError> {
<String as sqlx::Decode<db::Database>>::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 <S> {
/// The configuration.
pub config: Config,
/// The signing key used by actions running within this context.
pub signer: Arc<S>,
/// A handle to the database.
pub client: db::Client,
}
impl<S> Clone for Context<S> {
fn clone (&self) -> Context<S> {
Context {
config: self.config.clone(),
signer: self.signer.clone(),
client: self.client.clone(),
}
}
}
impl<S> Context<S> {
pub async fn dereference (&self, json: Value) -> Result<Activity>
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<T>
where
F: FnOnce (&'a mut db::Client) -> O,
O: Future<Output = Result<T>> + 'a,
{
f(&mut self.client).await
}
/// Get all actors on the instance.
pub fn actors (&self) -> impl Iterator<Item = ap::Actor> + '_ {
None.into_iter()
}
/// Get a dereferencer.
fn dereferencer (&self) -> Dereferencer<S>
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<Activity>) -> Result<()> {
let act = act.into();
todo!()
}
}
/// A type that provides dereferencing facilities for [`Activity`] data.
pub struct Dereferencer <S> {
web: reqwest::Client,
db: db::Client,
signer: Arc<S>,
}
impl<S> Dereferencer<S>
where
S: Sign
{
/// Perform the dereferencing.
pub async fn dereference (&self, json: Value) -> Result<Activity> {
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<Value> {
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<Option<Value>> {
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<ap::Create> {
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<url::Url>;
}
impl<T> IntoUrl for T where T: ToString {
fn into_url (self) -> Option<url::Url> {
self.to_string()
.parse()
.ok()
}
}