hermit/src/lib.rs

361 lines
9.2 KiB
Rust

#![feature(async_closure)]
#![feature(generic_associated_types)]
//! # 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.
use derive_more::From;
use serde::Serialize;
// Expose the `Id` type in the crate root
pub use id::Id;
pub use ctx::{ Dereferencer, IdGenerator, Context };
// Module imports
pub mod conf;
pub mod sign;
pub mod db;
pub mod ap;
mod error;
pub use error::{ Result, Error };
use error::err;
/// The Activity supertype used in abstractions over any kind of activity.
#[derive(Clone, Serialize, From)]
pub enum Activity {
/// Create a post.
Create (ap::Create),
/// Request to follow an actor.
Follow (ap::Follow),
/// Accept a follow request.
Accept (ap::Accept),
}
mod id {
use std::str::FromStr;
use reqwest::Url;
use serde::{ Deserialize, Serialize };
use sqlx::database::{ HasArguments, HasValueRef };
use crate::db;
/// An ActivityPub identifier.
#[derive(PartialEq, Eq, Hash, Clone, Debug, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Id (reqwest::Url);
impl FromStr for Id {
type Err = crate::Error;
fn from_str (s: &str) -> Result<Self, Self::Err> {
s.parse().map(Id).map_err(crate::err)
}
}
impl Id {
pub (crate) fn new <'a> (url: &'a Url) -> Id {
Id (url.to_owned())
}
}
impl std::fmt::Display for Id {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.as_str().fmt(f)
}
}
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, pin::Pin};
use futures::prelude::*;
use openssl::base64;
use rand::{Rng, prelude::StdRng, SeedableRng};
use serde_json::{Value, to_value};
use crate::{ conf::Config, db, Result, sign::Sign, ap, Activity, Id, err, Error };
/// 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> {
/// Attempt to dereference the given json to an [`Activity`].
pub async fn dereference (&self, json: Value) -> Result<Activity>
where
S: Sign
{
self.dereferencer()
.dereference_activity(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
}
/// Access the current [`Config`].
pub fn config (&self) -> &Config {
&self.config
}
/// Conjure an activity "from thin air" as though it were posted through a client.
///
/// Sometimes an activity implies another activity, for example, an activity should
/// be emitted when accepting a follow request.
pub (crate) async fn conjure (&self, act: impl Into<Activity>) -> Result<()> {
let act = act.into();
todo!()
}
/// Get an [`IdGenerator`].
pub fn id_gen (&self) -> IdGenerator<'_, StdRng> {
IdGenerator {
hostname: &self.config.host,
rng: StdRng::from_entropy(),
db: self.client.clone(),
}
}
}
/// 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_activity (&self, json: Value) -> Result<Activity> {
match json["type"].as_str() {
Some ("Create") => self.deref_create(json).await.map(Activity::Create),
_ => todo!()
}
}
/// Get the inner web client.
fn web_client (&self) -> &reqwest::Client {
&self.web
}
/// Fetch a JSON value.
pub async fn fetch (&self, url: impl crate::IntoUrl) -> Result<Value> {
let url = match url.into_url() {
Some (url) => url,
None => todo!(),
};
let id = Id::new(&url);
if let None = self.db_fetch(id).await? {
let client = self.web_client();
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)
} else {
// Not finding anything is not considered a hard error.
Ok (Value::Null)
}
}
/// 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)
}
if let Some (data) = self.db.get(db::get_note(&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!()),
}
}
}
/// How many bytes to generate for an ID.
const ID_LEN: usize = 64;
/// Generates [`Id`]s.
pub struct IdGenerator <'h, R> {
hostname: &'h str,
db: db::Client,
rng: R,
}
impl<R> IdGenerator<'_, R>
where
R: Rng
{
/// Generate an [`Id`] with the given prefix.
pub async fn gen (&mut self, prefix: &str) -> Result<Id> {
let IdGenerator { rng, db, hostname } = self;
// Give up after 200 failed attempts.
let mut tries = 200;
loop {
// Generate a random suffix and encode it as base64.
let suffix = {
let mut buf = [0; ID_LEN];
rng.fill(&mut buf);
base64::encode_block(&buf)
.replace("=", "-")
.replace("+", "_")
};
// Create an actual `Id` with the generated garbage.
let id = format!("https://{hostname}/{prefix}/{suffix}").parse()?;
// Basic check to see if the given id exists. This reserves the id.
// If the id is already taken, it is consumed.
if let Some (id) = db.verify_unique(id).await? {
break Ok (id)
}
tries -= 1;
if tries == 0 {
// After our tries are exhausted, give up.
break Err (Error::Timeout)
}
}
}
}
}
/// 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()
}
}