361 lines
9.2 KiB
Rust
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()
|
|
}
|
|
}
|
|
|