Extend the database model and add conversions
This commit is contained in:
parent
0969bff603
commit
131312b33e
8 changed files with 439 additions and 87 deletions
36
Cargo.lock
generated
36
Cargo.lock
generated
|
@ -138,6 +138,12 @@ version = "1.0.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.3"
|
||||
|
@ -208,6 +214,19 @@ dependencies = [
|
|||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "0.99.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustc_version",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.3"
|
||||
|
@ -478,8 +497,10 @@ name = "hermit"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"derive_more",
|
||||
"futures",
|
||||
"openssl",
|
||||
"rand",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
@ -1056,6 +1077,15 @@ dependencies = [
|
|||
"winreg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
|
||||
dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.10"
|
||||
|
@ -1101,6 +1131,12 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2333e6df6d6598f2b1974829f853c2b4c5f4a6e503c10af918081aa6f8564e1"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.137"
|
||||
|
|
|
@ -16,3 +16,5 @@ url = { version = '*', features = [ "serde" ] }
|
|||
sqlx = { version = '*', features = [ "postgres", "runtime-tokio-native-tls" ] }
|
||||
openssl = '*'
|
||||
tokio-stream = { version = '*', features = [ "sync" ] }
|
||||
derive_more = '*'
|
||||
rand = '*'
|
||||
|
|
|
@ -3,38 +3,82 @@
|
|||
use futures::prelude::*;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{ Id, Activity, err, Result, Error, sign, ctx::Context };
|
||||
use crate::{ Id, Activity, err, Result, Error, sign, ctx::Context, db::{Post, Privacy} };
|
||||
|
||||
/// Represents the creation of an object.
|
||||
///
|
||||
/// Valid object types: [`Note`].
|
||||
#[derive(Clone, Serialize)]
|
||||
pub enum Create {
|
||||
Note { object: Note }
|
||||
Note {
|
||||
id: Id,
|
||||
actor: Id,
|
||||
object: Note,
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Create> for Activity { fn from (a: Create) -> Activity { Activity::Create (a) } }
|
||||
impl TryFrom<Create> for Post {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from (value: Create) -> Result<Post> {
|
||||
let privacy = Privacy::infer(&value);
|
||||
match value {
|
||||
Create::Note { id: create, object, actor } => {
|
||||
let Note { content, id, .. } = object;
|
||||
Ok (Post {
|
||||
author: actor,
|
||||
content,
|
||||
privacy,
|
||||
create,
|
||||
id,
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Invalid {}
|
||||
|
||||
/// A follow request.
|
||||
///
|
||||
/// Valid object types: [`Actor`].
|
||||
#[derive(Clone, Serialize)]
|
||||
pub enum Follow {
|
||||
Actor { object: Actor }
|
||||
Actor {
|
||||
id: Id,
|
||||
object: Actor,
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Follow> for Activity { fn from (a: Follow) -> Activity { Activity::Follow (a) } }
|
||||
|
||||
/// Signal that a [follow request][Follow] has been accepted.
|
||||
///
|
||||
/// Valid object types: [`Follow`].
|
||||
#[derive(Clone, Serialize)]
|
||||
pub enum Accept {
|
||||
Follow { object: Follow }
|
||||
Follow {
|
||||
id: Id,
|
||||
object: Follow,
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Accept> for Activity { fn from (a: Accept) -> Activity { Activity::Accept (a) } }
|
||||
|
||||
/// An entity that publishes activities.
|
||||
#[derive(Clone, Serialize, sqlx::FromRow)]
|
||||
#[derive(Clone, Serialize, sqlx::FromRow, sqlx::Type)]
|
||||
pub struct Actor {
|
||||
id: Id,
|
||||
}
|
||||
|
||||
/// Represents a [`Post`] for federation purposes.
|
||||
#[derive(Clone, Serialize)]
|
||||
pub struct Note {
|
||||
id: Id,
|
||||
content: Option<String>,
|
||||
}
|
||||
|
||||
impl From<Post> for Note {
|
||||
fn from (Post { id, content, .. }: Post) -> Note {
|
||||
Note { id, content }
|
||||
}
|
||||
}
|
||||
|
||||
impl Activity {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
//! Hermit instance configuration.
|
||||
|
||||
use crate::Id;
|
||||
use crate::{ Id, db };
|
||||
|
||||
use std::collections::HashMap as Map;
|
||||
|
||||
|
@ -15,17 +15,20 @@ pub struct Config {
|
|||
pub rules: Vec<rule::Rule>,
|
||||
/// Notification configuration for each local actor.
|
||||
pub notify: Map<Id, Notify>,
|
||||
/// Configuration related to the database.
|
||||
pub db_config: db::Config,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Create a new default config.
|
||||
pub fn new (hostname: impl ToString) -> Config {
|
||||
let (notify, rules) = def();
|
||||
let (notify, rules, db_config) = def();
|
||||
Config {
|
||||
host: hostname.to_string(),
|
||||
port: 6969,
|
||||
notify,
|
||||
rules,
|
||||
db_config,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
204
src/db/mod.rs
204
src/db/mod.rs
|
@ -1,20 +1,54 @@
|
|||
//! Database abstraction layer used by Hermit.
|
||||
|
||||
use std::borrow::Borrow;
|
||||
use std::{marker::PhantomData, pin::Pin};
|
||||
use std::pin::Pin;
|
||||
|
||||
use crate::ap::Note;
|
||||
use crate::{ Id, Result, ap::Actor, err };
|
||||
use futures::prelude::*;
|
||||
use sqlx::{Executor, pool::PoolConnection, database::HasArguments};
|
||||
|
||||
use sqlx::{pool::PoolConnection, database::HasArguments};
|
||||
use sqlx::{ FromRow, Either::Right };
|
||||
|
||||
pub (crate) use self::data::*;
|
||||
|
||||
/// `const ()` but in Rust
|
||||
fn void <T> (_: T) -> () { () }
|
||||
|
||||
pub(crate) type Database = sqlx::Postgres;
|
||||
/// The type of database.
|
||||
pub (crate) type Database = sqlx::Postgres;
|
||||
|
||||
/// Specifies how to connect to the database.
|
||||
pub struct Config {}
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Config {
|
||||
/// The host of the database server.
|
||||
///
|
||||
/// Defaults to `localhost`.
|
||||
pub host: String,
|
||||
/// The port on which the database server accepts connections.
|
||||
///
|
||||
/// Defaults to `5432`.
|
||||
pub port: u16,
|
||||
/// The user used to perform database actions on the server's behalf.
|
||||
///
|
||||
/// Defaults to `hermit`.
|
||||
pub user: String,
|
||||
/// The password used to log in to the database user.
|
||||
///
|
||||
/// Defaults to `hermit-does-not-imply-lonely`
|
||||
pub pass: String,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Config {
|
||||
host: "localhost".into(),
|
||||
user: "hermit".into(),
|
||||
pass: "hermit-does-not-imply-lonely".into(),
|
||||
port: 5432,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A database client.
|
||||
///
|
||||
|
@ -32,11 +66,14 @@ impl Client {
|
|||
todo!()
|
||||
}
|
||||
|
||||
/// Perform a query and stream the matching rows back.
|
||||
pub async fn query <'e, T: 'e> (&self, query: Query<'e, T>) -> Pin<Box<dyn Stream<Item = Result<T>> + Send + 'e>> {
|
||||
let Query (q, f) = query;
|
||||
let stream = q
|
||||
.fetch_many(&self.pool)
|
||||
.filter_map(async move |r| {
|
||||
// `async move` works here because `f` is a function pointer,
|
||||
// which are `Copy`.
|
||||
match r.map_err(err) {
|
||||
Ok (Right (row)) => Some (f(row)),
|
||||
Err (error) => Some (Err (error)),
|
||||
|
@ -47,6 +84,8 @@ impl Client {
|
|||
Box::pin (stream)
|
||||
}
|
||||
|
||||
/// Get a single item from the database. If multiple items are matched by the query,
|
||||
/// this function discards all but the first one.
|
||||
pub async fn get <'q, T: 'q> (&self, query: Query<'q, T>) -> Result<Option<T>> {
|
||||
self.query(query)
|
||||
.await
|
||||
|
@ -69,20 +108,56 @@ impl Client {
|
|||
})
|
||||
.await
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
type Q<'a> = sqlx::query::Query<'a, Database, <Database as HasArguments<'a>>::Arguments>;
|
||||
type Row = <Database as sqlx::Database>::Row;
|
||||
|
||||
/// Represents a query awaiting execution.
|
||||
pub struct Query <'a, T> (Q<'a>, fn (Row) -> Result<T>);
|
||||
|
||||
impl<'a, T> Query<'a, T> {
|
||||
|
||||
/// Map over the inner [`Q`].
|
||||
fn mapq (self, f: fn (Q<'a>) -> Q<'a>) -> Query<'a, T> {
|
||||
let Query (q, g) = self;
|
||||
Query (f(q), g)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Force user to pick between incoming relations and outgoing relations.
|
||||
pub struct Choice <'a, T> {
|
||||
/// The incomplete query
|
||||
query: Query<'a, T>,
|
||||
/// The function that completes in the case of "incoming".
|
||||
i: fn (Q<'a>) -> Q<'a>,
|
||||
/// The function that completes in the case of "outgoing".
|
||||
o: fn (Q<'a>) -> Q<'a>,
|
||||
}
|
||||
|
||||
impl<'a, T> Choice<'a, T> {
|
||||
|
||||
/// Get the relations that are "incoming".
|
||||
pub fn incoming (self) -> Query<'a, T> {
|
||||
self.query.mapq(self.i)
|
||||
}
|
||||
|
||||
/// Get the relations that are "outgoing".
|
||||
pub fn outgoing (self) -> Query<'a, T> {
|
||||
self.query.mapq(self.o)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Generate a query that gets an [`Actor`] by its [`Id`].
|
||||
pub fn get_actor <'a> (id: &'a Id) -> Query<'a, Actor> {
|
||||
|
||||
// Prepare a query
|
||||
let query = sqlx::query("select * from actors where id = $1")
|
||||
.bind(id);
|
||||
.bind(id.borrow());
|
||||
|
||||
// Return an sql query which will result in a series of rows,
|
||||
// and a decoder function that will translate each row to a
|
||||
|
@ -93,3 +168,122 @@ pub fn get_actor <'a> (id: &'a Id) -> Query<'a, Actor> {
|
|||
})
|
||||
|
||||
}
|
||||
|
||||
/// Construct a query that gets a [`Note`] by id.
|
||||
pub fn get_note <'a> (id: &'a Id) -> Query<'a, Note> {
|
||||
|
||||
// Prepare a query
|
||||
let query = sqlx::query("select * from posts where id = $1")
|
||||
.bind(id.borrow());
|
||||
|
||||
// Return an sql query which will result in a series of rows,
|
||||
// and a decoder function that will translate each row to a
|
||||
// value of type `Actor`.
|
||||
Query (query, |row: Row| {
|
||||
let data = Post::from_row(&row)?.into();
|
||||
Ok (data)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
/// Generate a [`Choice`] of sets of follow requests. The [outgoing] set refers to
|
||||
/// follow requests where the `origin` matches the given id, whereas the [incoming]
|
||||
/// set refers to the requests where the `target` matches the id.
|
||||
///
|
||||
/// [outgoing]: Choice::outgoing
|
||||
/// [incoming]: Choice::incoming
|
||||
pub fn get_follow_reqs <'a> (id: &'a Id) -> Choice<'a, FollowRequest> {
|
||||
|
||||
// Prepare an SQL query
|
||||
let query = sqlx::query("select * from followreqs where $2 = $1")
|
||||
.bind(id);
|
||||
|
||||
// Create an inner query object
|
||||
let query = Query (query, |row: Row| {
|
||||
let data = FollowRequest::from_row(&row)?;
|
||||
Ok (data)
|
||||
});
|
||||
|
||||
// Return an incomplete query object, with two functions to bind
|
||||
// the final parameter: `i` for the "incoming" relations and `o`
|
||||
// for the "outgoing" relations.
|
||||
//
|
||||
// Note that closures that are `FnOnce` and do not capture from
|
||||
// the environment are coercable to function pointers, which is
|
||||
// what we're exploiting here to cut down on boilerplate a bit.
|
||||
Choice {
|
||||
i: |q| q.bind("target"),
|
||||
o: |q| q.bind("origin"),
|
||||
query,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pub mod data {
|
||||
|
||||
//! Data types stored as records in the database. Sometimes the
|
||||
//! federation model and the database model don't *quite* match
|
||||
//! and in that case conversions between the two are needed.
|
||||
|
||||
use sqlx::{ Type, FromRow };
|
||||
use crate::{Id, ap::{Note, Create}, Result, Error};
|
||||
|
||||
/// Encodes the status of a follow request.
|
||||
#[derive(Clone, Debug, Type)]
|
||||
pub enum FollowRequestStatus {
|
||||
/// The follow request was accepted.
|
||||
Accepted,
|
||||
/// The follow request was rejected.
|
||||
Rejected,
|
||||
/// The follow request has neither been accepted nor rejected.
|
||||
Pending,
|
||||
}
|
||||
|
||||
/// Represents a follow request record in the database.
|
||||
#[derive(Clone, Debug, FromRow, Type)]
|
||||
pub struct FollowRequest {
|
||||
/// The status of the follow request (whether it is accepted, rejected
|
||||
/// or still pending).
|
||||
pub status: FollowRequestStatus,
|
||||
/// The actor that sent the request.
|
||||
pub origin: Id,
|
||||
/// The actor the request was sent to.
|
||||
pub target: Id,
|
||||
}
|
||||
|
||||
/// A social media post.
|
||||
#[derive(Clone, Debug, FromRow, Type)]
|
||||
pub struct Post {
|
||||
/// The id of the post.
|
||||
pub id: Id,
|
||||
/// The id of the [`Actor`] that created the post.
|
||||
pub author: Id,
|
||||
/// The id of the associated [`Create`] activity.
|
||||
pub create: Id,
|
||||
/// The content of the post.
|
||||
pub content: Option<String>,
|
||||
/// The post's privacy scope.
|
||||
pub privacy: Privacy,
|
||||
}
|
||||
|
||||
/// Represents a privacy scope.
|
||||
#[derive(Clone, Debug, Type)]
|
||||
pub enum Privacy {
|
||||
/// Visible on timelines etc.
|
||||
Public,
|
||||
/// Called "Unlisted" on Mastodon
|
||||
Hidden,
|
||||
/// Called "Followers-only" on Mastodon
|
||||
Locked,
|
||||
/// A DM.
|
||||
Direct,
|
||||
}
|
||||
|
||||
impl Privacy {
|
||||
/// Infer the privacy scope from the activity.
|
||||
pub fn infer (_: &Create) -> Privacy {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
30
src/error.rs
Normal file
30
src/error.rs
Normal file
|
@ -0,0 +1,30 @@
|
|||
//! Defines the [`Error`] type, [`Result`] shorthand and [`err`] utility function to convert
|
||||
//! compatible errors to `Error`.
|
||||
|
||||
use derive_more::From;
|
||||
|
||||
use crate::ap;
|
||||
|
||||
/// 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, From)]
|
||||
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),
|
||||
/// An error with parsing a [`url`].
|
||||
Url (url::ParseError),
|
||||
/// An error in converting between models.
|
||||
Invalid (ap::Invalid),
|
||||
}
|
||||
|
||||
/// Trivial conversion function for use in `map_err` functions.
|
||||
pub (crate) fn err (e: impl Into<Error>) -> Error { e.into() }
|
170
src/lib.rs
170
src/lib.rs
|
@ -1,10 +1,11 @@
|
|||
#![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.
|
||||
#![feature(generic_associated_types)]
|
||||
|
||||
use derive_more::From;
|
||||
use serde::Serialize;
|
||||
|
||||
// Expose the `Id` type in the crate root
|
||||
|
@ -17,8 +18,12 @@ 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)]
|
||||
#[derive(Clone, Serialize, From)]
|
||||
pub enum Activity {
|
||||
/// Create a post.
|
||||
Create (ap::Create),
|
||||
|
@ -28,68 +33,38 @@ pub enum Activity {
|
|||
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 std::str::FromStr;
|
||||
|
||||
use reqwest::Url;
|
||||
use serde::{ Deserialize, Serialize };
|
||||
use sqlx::database::{HasArguments, HasValueRef};
|
||||
use url::ParseError;
|
||||
use sqlx::database::{ HasArguments, HasValueRef };
|
||||
|
||||
use crate::db;
|
||||
|
||||
/// An ActivityPub identifier.
|
||||
#[derive(PartialEq, Eq, Clone, Serialize, Deserialize)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct Id (reqwest::Url);
|
||||
|
||||
impl FromStr for Id {
|
||||
type Err = url::ParseError;
|
||||
type Err = crate::Error;
|
||||
|
||||
fn from_str (s: &str) -> Result<Self, Self::Err> {
|
||||
s.parse().map(Id)
|
||||
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 {
|
||||
write!(f, "{}", self.0.to_string())
|
||||
self.0.as_str().fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -116,12 +91,14 @@ mod id {
|
|||
|
||||
mod ctx {
|
||||
|
||||
use std::sync::Arc;
|
||||
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::{self, Actor}, Activity, Id, err };
|
||||
use crate::{ conf::Config, db, Result, sign::Sign, ap, Activity, Id, err };
|
||||
|
||||
/// The context of a thread/task.
|
||||
///
|
||||
|
@ -151,12 +128,13 @@ mod ctx {
|
|||
|
||||
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(json)
|
||||
.dereference_activity(json)
|
||||
.await
|
||||
}
|
||||
|
||||
|
@ -191,16 +169,28 @@ mod ctx {
|
|||
&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!()
|
||||
}
|
||||
|
||||
pub fn id_gen (&self) -> IdGenerator<StdRng> {
|
||||
IdGenerator {
|
||||
hostname: self.config.host.clone(),
|
||||
rng: StdRng::from_entropy(),
|
||||
db: self.client.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// A type that provides dereferencing facilities for [`Activity`] data.
|
||||
|
@ -214,18 +204,21 @@ mod ctx {
|
|||
where
|
||||
S: Sign
|
||||
{
|
||||
|
||||
/// Perform the dereferencing.
|
||||
pub async fn dereference (&self, json: Value) -> Result<Activity> {
|
||||
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 database client.
|
||||
fn db_client (&self) -> &db::Client {
|
||||
&self.db
|
||||
}
|
||||
|
||||
|
||||
/// Get the inner web client.
|
||||
fn web_client (&self) -> &reqwest::Client {
|
||||
&self.web
|
||||
}
|
||||
|
@ -233,27 +226,37 @@ mod ctx {
|
|||
/// 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 id = Id::new(&url);
|
||||
|
||||
let value = client
|
||||
.execute(req)
|
||||
.await?
|
||||
.json()
|
||||
.await?;
|
||||
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
|
||||
};
|
||||
|
||||
Ok (value)
|
||||
|
||||
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
|
||||
|
@ -266,6 +269,12 @@ mod ctx {
|
|||
.map(Some)
|
||||
}
|
||||
|
||||
if let Some (data) = self.db.get(db::get_note(&id)).await? {
|
||||
return to_value(data)
|
||||
.map_err(err)
|
||||
.map(Some)
|
||||
}
|
||||
|
||||
todo!()
|
||||
|
||||
}
|
||||
|
@ -281,8 +290,41 @@ mod ctx {
|
|||
_ => return Err (todo!()),
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const ID_LEN: usize = 64;
|
||||
|
||||
/// Generates [`Id`]s.
|
||||
pub struct IdGenerator <R> {
|
||||
hostname: String,
|
||||
db: db::Client,
|
||||
rng: R,
|
||||
}
|
||||
|
||||
impl<R> IdGenerator<R>
|
||||
where
|
||||
R: Rng
|
||||
{
|
||||
pub fn gen (&mut self, prefix: &str) -> Option<Id> {
|
||||
|
||||
let IdGenerator { rng, db, hostname } = self;
|
||||
|
||||
// Generate a random suffix
|
||||
let suffix = {
|
||||
let mut buf = [0; ID_LEN];
|
||||
rng.fill(&mut buf);
|
||||
base64::encode_block(&buf)
|
||||
.replace("=", "-")
|
||||
.replace("+", "_")
|
||||
};
|
||||
|
||||
let id = format!("https://{hostname}/{prefix}/{suffix}").parse().ok()?;
|
||||
|
||||
Some (id)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
13
src/main.rs
13
src/main.rs
|
@ -1,4 +1,3 @@
|
|||
|
||||
use std::sync::Arc;
|
||||
|
||||
use hermit::{ Context, Error, db, sign, Activity, };
|
||||
|
@ -12,7 +11,7 @@ use tokio_stream::wrappers::ReceiverStream;
|
|||
mod web;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main () {
|
||||
async fn main () -> Result<(), Error> {
|
||||
|
||||
// Set up the context for each task.
|
||||
let ctx = {
|
||||
|
@ -21,16 +20,16 @@ async fn main () {
|
|||
let hostname = "dev.riley.lgbt";
|
||||
|
||||
// Establish a connection to the database.
|
||||
let client = db::Client::new(db::Config {}).await.unwrap();
|
||||
let client = db::Client::new(db::Config::default()).await?;
|
||||
|
||||
// Generate the config from the hostname.
|
||||
let config = Config::new(&hostname);
|
||||
|
||||
// Use an instance-wide signing key (for now).
|
||||
let signer = sign::Key::load(
|
||||
format!("https://{hostname}/key/main").parse().unwrap(),
|
||||
format!("https://{hostname}/key/main").parse()?,
|
||||
"private_key.pem"
|
||||
).map(Arc::new).unwrap();
|
||||
).map(Arc::new)?;
|
||||
|
||||
Context {
|
||||
signer,
|
||||
|
@ -85,7 +84,9 @@ async fn main () {
|
|||
ctx.run (task::Ctrl {
|
||||
rx: ctrl_rx,
|
||||
tx: ctrl_tx,
|
||||
})
|
||||
});
|
||||
|
||||
Ok (())
|
||||
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue