303 lines
7.7 KiB
Rust
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()
|
|
}
|
|
}
|
|
|