A whole bunch of different refactors

This commit is contained in:
Riley Apeldoorn 2024-05-01 18:47:02 +02:00
parent 09cf289b75
commit 8d350e8cd9
10 changed files with 273 additions and 300 deletions

View file

@ -1,59 +1,73 @@
#![feature(iterator_try_collect)] #![feature(iterator_try_collect)]
use puppy::{ use puppy::{
actor::Actor, actor::Actor,
data::{Bite, Profile}, auth::Verifier,
config::Config,
data::{schema, Bite, Profile},
post::Author, post::Author,
store::util::IterExt as _, store::{util::IterExt as _, Store},
Context, Context,
}; };
fn main() -> puppy::Result<()> { fn main() -> puppy::Result<()> {
// puppy::store::Store::nuke(".state")?; // puppy::store::Store::nuke(".state")?;
puppy::context(|cx| { let config = Config {
let db = cx.store(); ap_domain: "test.piss-on.me".to_string(),
println!("creating actors"); wf_domain: "test.piss-on.me".to_string(),
let riley = get_or_create_actor(&cx, "riley")?; state_dir: ".state".to_string(),
let linen = get_or_create_actor(&cx, "linen")?; port: 1312,
if true { };
println!("creating posts"); let verifier = Verifier::load(&config);
puppy::post::create_post(&cx, riley.key, "@linen <3")?; let db = Store::open(&config.state_dir, schema())?;
puppy::post::create_post(&cx, linen.key, "@riley <3")?; let cx = Context::new(config, db.clone(), verifier);
} println!("creating actors");
let riley = get_or_create_actor(&cx, "riley")?;
let linen = get_or_create_actor(&cx, "linen")?;
if true {
println!("creating posts");
puppy::post::create_post(&cx, riley.key, "@linen <3")?;
puppy::post::create_post(&cx, linen.key, "@riley <3")?;
}
if true { if true {
println!("making riley follow linen"); println!("making riley follow linen");
if !riley.follows(&cx, &linen)? {
cx.run(|tx| {
if !riley.follows(&tx, &linen)? {
println!("follow relation does not exist yet"); println!("follow relation does not exist yet");
if let Some(req) = linen if let Some(req) = linen
.pending_requests(&cx) .pending_requests(&tx)
.find_ok(|r| r.origin == riley.key)? .find_ok(|r| r.origin == riley.key)?
{ {
println!("accepting the pending follow request"); println!("accepting the pending follow request");
linen.do_accept_request(&cx, req)?; linen.do_accept_request(&cx, req)
} else { } else {
println!("no pending follow request; creating"); println!("no pending follow request; creating");
riley.do_follow_request(&cx, &linen)?; riley.do_follow_request(&cx, &linen).map(|_| ())
} }
} else { } else {
println!("riley already follows linen"); println!("riley already follows linen");
Ok(())
} }
} })?;
}
println!("\nPosts on the instance:"); println!("\nPosts on the instance:");
for post in puppy::post::fetch_timeline(&db, .., None)?.posts() { for post in puppy::post::fetch_timeline(&db, .., None)?.posts() {
let Author { ref handle, .. } = post.author; let Author { ref handle, .. } = post.author;
let content = post.content.content.as_ref().unwrap(); let content = post.content.content.as_ref().unwrap();
println!("- {:?} by {handle}:\n{content}", post.id) println!("- {:?} by {handle}:\n{content}", post.id)
} }
cx.run(|tx| {
println!("\nLinen's followers:"); println!("\nLinen's followers:");
for id in linen.followers(&cx).try_collect::<Vec<_>>()? { for id in linen.followers(&tx).try_collect::<Vec<_>>()? {
let Profile { account_name, .. } = db.get_mixin(id)?.unwrap(); let Profile { account_name, .. } = db.get_mixin(id)?.unwrap();
println!("- @{account_name} ({id})"); println!("- @{account_name} ({id})");
} }
println!("\nRiley's following:"); println!("\nRiley's following:");
for id in riley.following(&cx).try_collect::<Vec<_>>()? { for id in riley.following(&tx).try_collect::<Vec<_>>()? {
let Profile { account_name, .. } = db.get_mixin(id)?.unwrap(); let Profile { account_name, .. } = db.get_mixin(id)?.unwrap();
println!("- @{account_name} ({id})"); println!("- @{account_name} ({id})");
} }
@ -61,7 +75,7 @@ fn main() -> puppy::Result<()> {
if false { if false {
println!("Biting riley"); println!("Biting riley");
linen.do_bite(&cx, &riley)?; linen.do_bite(&cx, &riley)?;
for Bite { id, biter, .. } in riley.bites_suffered(&cx).try_collect::<Vec<_>>()? { for Bite { id, biter, .. } in riley.bites_suffered(&tx).try_collect::<Vec<_>>()? {
let Profile { account_name, .. } = db.get_mixin(biter)?.unwrap(); let Profile { account_name, .. } = db.get_mixin(biter)?.unwrap();
println!("riley was bitten by @{account_name} at {}", id.timestamp()); println!("riley was bitten by @{account_name} at {}", id.timestamp());
} }
@ -70,8 +84,8 @@ fn main() -> puppy::Result<()> {
}) })
} }
fn get_or_create_actor(cx: &Context<'_>, username: &str) -> puppy::Result<Actor> { fn get_or_create_actor(cx: &Context, username: &str) -> puppy::Result<Actor> {
let user = Actor::by_username(cx, username)?; let user = cx.run(|tx| Actor::by_username(tx, username))?;
match user { match user {
Some(key) => { Some(key) => {
println!("found '{username}' ({key:?})"); println!("found '{username}' ({key:?})");

View file

@ -11,13 +11,13 @@ pub mod ap {
config::Config, config::Config,
data::{Id, PrivateKey, PublicKey}, data::{Id, PrivateKey, PublicKey},
fetch::{signatures::Private, SigningKey}, fetch::{signatures::Private, SigningKey},
get_local_ap_object, Key, get_local_ap_object, Context, Key,
}; };
use serde_json::{to_string, Value}; use serde_json::{to_string, Value};
use crate::{respond, Response}; use crate::{respond, Response};
/// Proxy a request through the instance. /// Proxy a request through the instance.
pub async fn proxy(params: &[(&str, &str)]) -> Response { pub async fn proxy(cx: &Context, params: &[(&str, &str)]) -> Response {
// Extract our query parameters. // Extract our query parameters.
let Some(user) = params.iter().find_map(|(k, v)| (*k == "user").then_some(v)) else { let Some(user) = params.iter().find_map(|(k, v)| (*k == "user").then_some(v)) else {
return respond(400, Some("Expected `user` query param"), []); return respond(400, Some("Expected `user` query param"), []);
@ -27,15 +27,16 @@ pub mod ap {
}; };
// Look up the actor's key in the store (which is accessible through the puppy context). // Look up the actor's key in the store (which is accessible through the puppy context).
let signing_key = puppy::context::<_, puppy::Error>(|cx| try { let Ok(signing_key) = cx.run(|tx| {
let actor = Actor::by_username(&cx, user)?.unwrap(); let actor = Actor::by_username(&tx, user)?.unwrap();
let (PrivateKey { key_pem, .. }, PublicKey { key_id, .. }) = let (PrivateKey { key_pem, .. }, PublicKey { key_id, .. }) =
cx.store().get_mixin_many(actor.key)?; tx.get_mixin_many(actor.key)?;
let Id(owner) = cx.store().get_alias(actor.key)?.unwrap(); let Id(owner) = tx.get_alias(actor.key)?.unwrap();
let inner = Private::decode_pem(&key_pem); let inner = Private::decode_pem(&key_pem);
SigningKey { id: key_id, owner, inner } Ok(SigningKey { id: key_id, owner, inner })
}) }) else {
.unwrap(); panic!("failed to get signing key");
};
eprintln!("proxy: params: {params:?}"); eprintln!("proxy: params: {params:?}");
// Proxy the request through our fetcher. // Proxy the request through our fetcher.
@ -47,16 +48,16 @@ pub mod ap {
} }
/// Handle POSTs to actor inboxes. Requires request signature. /// Handle POSTs to actor inboxes. Requires request signature.
pub fn inbox(actor_id: &str, sig: Signer, body: Value, cfg: Config) -> Response { pub fn inbox(cx: &Context, actor_id: &str, sig: Signer, body: Value) -> Response {
todo!() todo!()
} }
/// Serve an ActivityPub object as json-ld. /// Serve an ActivityPub object as json-ld.
pub fn serve_object(object_ulid: &str) -> Response { pub fn serve_object(cx: &Context, object_ulid: &str) -> Response {
let Ok(parsed) = object_ulid.parse::<Key>() else { let Ok(parsed) = object_ulid.parse::<Key>() else {
return respond(400, Some("improperly formatted id"), []); return respond(400, Some("improperly formatted id"), []);
}; };
let result = puppy::context(|cx| get_local_ap_object(&cx, parsed)); let result = cx.run(|tx| get_local_ap_object(&tx, parsed));
let Ok(object) = result else { let Ok(object) = result else {
return respond(404, <Option<String>>::None, []); return respond(404, <Option<String>>::None, []);
}; };
@ -67,8 +68,8 @@ pub mod ap {
const AP_CONTENT_TYPE: (&str, &str) = ("content-type", "application/activity+json"); const AP_CONTENT_TYPE: (&str, &str) = ("content-type", "application/activity+json");
/// Serve the special actor used for signing requests. /// Serve the special actor used for signing requests.
pub fn serve_verifier_actor(cfg: Config) -> Response { pub fn serve_verifier_actor(cx: &Context) -> Response {
let body = Verifier::load(&cfg).to_json_ld(); let body = cx.verifier().to_json_ld();
let encoded = serde_json::to_vec(&body).unwrap(); let encoded = serde_json::to_vec(&body).unwrap();
respond(200, Some(encoded), [AP_CONTENT_TYPE]) respond(200, Some(encoded), [AP_CONTENT_TYPE])
} }
@ -77,26 +78,31 @@ pub mod ap {
pub mod wf { pub mod wf {
//! WebFinger endpoints and related stuff. //! WebFinger endpoints and related stuff.
use puppy::{config::Config, data::Username, Error}; use puppy::{data::Username, Context, Error};
use serde_json::json; use serde_json::json;
use crate::{respond, Response}; use crate::{respond, Response};
const WF_CONTENT_TYPE: (&str, &str) = ("content-type", "application/jrd+json"); const WF_CONTENT_TYPE: (&str, &str) = ("content-type", "application/jrd+json");
pub fn resolve(query: &[(&str, &str)], cfg: Config) -> Response { pub fn resolve(cx: &Context, query: &[(&str, &str)]) -> Response {
match query.iter().find_map(get_handle) { match query.iter().find_map(get_handle) {
// Serve JRDs for local actors. // Serve JRDs for local actors.
Some(handle) if cfg.wf_domain == handle.instance => { Some(handle) if cx.config().wf_domain == handle.instance => {
let id = puppy::context::<_, Error>(|cx| try { let id = {
let user = cx let user = cx
.store() .store()
.lookup(Username(handle.username.to_string()))? .lookup(Username(handle.username.to_string()))
.unwrap()
.unwrap(); .unwrap();
let id = cx.store().get_alias::<puppy::data::Id>(user)?.unwrap().0; let id = cx
.store()
.get_alias::<puppy::data::Id>(user)
.unwrap()
.unwrap()
.0;
id id
}) };
.unwrap();
let jrd = json!({ let jrd = json!({
"subject": format!("acct:{}@{}", handle.username, handle.instance), "subject": format!("acct:{}@{}", handle.username, handle.instance),
"links": [ "links": [

View file

@ -3,16 +3,15 @@
use std::convert::Infallible; use std::convert::Infallible;
use std::net::SocketAddr; use std::net::SocketAddr;
use http::request::Parts;
use http_body_util::{BodyExt as _, Full}; use http_body_util::{BodyExt as _, Full};
use hyper::body::Bytes; use hyper::body::Bytes;
use hyper::server::conn::http1; use hyper::server::conn::http1;
use hyper::service::service_fn; use hyper::service::service_fn;
use hyper_util::rt::TokioIo; use hyper_util::rt::TokioIo;
use puppy::auth::{SigError, Signer}; use puppy::auth::{self, SigError, Signer, Verifier};
use puppy::{auth::verify_signature, config::Config}; use puppy::store::Store;
use puppy::fetch::signatures::Signature; use puppy::{auth::verify_signature, config::Config, Context};
use serde_json::{from_slice, json, Value}; use serde_json::{from_slice, Value};
use tokio::net::TcpListener; use tokio::net::TcpListener;
#[tokio::main] #[tokio::main]
@ -20,13 +19,17 @@ async fn main() {
let config = Config { let config = Config {
ap_domain: "test.piss-on.me".to_string(), ap_domain: "test.piss-on.me".to_string(),
wf_domain: "test.piss-on.me".to_string(), wf_domain: "test.piss-on.me".to_string(),
state_dir: "state".to_string(),
port: 1312, port: 1312,
}; };
start(&config).await.unwrap(); let store = Store::open(&config.state_dir, puppy::data::schema()).unwrap();
let verifier = Verifier::load(&config);
let context = Context::new(config, store, verifier);
start(context).await.unwrap();
} }
pub async fn start(cfg: &Config) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { pub async fn start(context: Context) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let addr = SocketAddr::from(([127, 0, 0, 1], cfg.port)); let addr = SocketAddr::from(([127, 0, 0, 1], context.config().port));
let listener = TcpListener::bind(addr).await?; let listener = TcpListener::bind(addr).await?;
// We start a loop to continuously accept incoming connections // We start a loop to continuously accept incoming connections
@ -37,13 +40,13 @@ pub async fn start(cfg: &Config) -> Result<(), Box<dyn std::error::Error + Send
// `hyper::rt` IO traits. // `hyper::rt` IO traits.
let io = TokioIo::new(stream); let io = TokioIo::new(stream);
let cfg = cfg.clone(); let context = context.clone();
// Spawn a tokio task to serve multiple connections concurrently // Spawn a tokio task to serve multiple connections concurrently
tokio::task::spawn(async move { tokio::task::spawn(async move {
// Finally, we bind the incoming connection to our `hello` service // Finally, we bind the incoming connection to our `hello` service
if let Err(err) = http1::Builder::new() if let Err(err) = http1::Builder::new()
// `service_fn` converts our function in a `Service` // `service_fn` converts our function in a `Service`
.serve_connection(io, service_fn(|req| handle(req, cfg.clone()))) .serve_connection(io, service_fn(|req| handle(req, context.clone())))
.await .await
{ {
eprintln!("Error serving connection: {:?}", err); eprintln!("Error serving connection: {:?}", err);
@ -55,45 +58,6 @@ pub async fn start(cfg: &Config) -> Result<(), Box<dyn std::error::Error + Send
type Request = hyper::Request<hyper::body::Incoming>; type Request = hyper::Request<hyper::body::Incoming>;
type Response<T = Full<Bytes>> = hyper::Response<T>; type Response<T = Full<Bytes>> = hyper::Response<T>;
/// The request handler.
async fn handle(req: Request, cfg: Config) -> Result<Response, Infallible> {
// We need to fetch the entire body of the request for signature validation, because that involves making
// a digest of the request body in some cases.
let request = {
let (req, body) = req.into_parts();
let Ok(body) = body.collect().await.map(|b| b.to_bytes()) else {
return Ok(error::invalid_body("Could not get request body"));
};
http::Request::from_parts(req, body)
};
// Simplified representation of a request, so we can pattern match on it more easily in the dispatchers.
let req = make_req(&request);
eprintln!("{request:?}: open");
// We'll use the path to pick where specifically to send the request.
// Check request signature at the door. Even if it isn't needed for a particular endpoint, failing fast
// with a clear error message will save anyone trying to get *their* signatures implementation a major
// headache.
let res = match verify_signature(&request, &cfg).await {
// If the request was signed and the signature was accepted, they can access the protected endpoints.
Ok(Some(sig)) => dispatch_signed(req, sig, cfg).await,
// Unsigned requests can see a smaller subset of endpoints, most notably the verification actor.
Ok(None) => dispatch_public(req, cfg).await,
// If a signature was provided *but it turned out to be unverifiable*, show them the error message.
Err(err) => error::bad_signature(match err {
SigError::VerificationFailed { error } => format!("Verification failed: {error}"),
SigError::ParseSignature { error } => format!("Failed to parse signature: {error}"),
SigError::FailedToFetchKey { keyid } => format!("Failed to fetch {keyid}"),
}),
};
eprintln!(
"{} {}: done (status: {})",
request.method(),
request.uri(),
res.status()
);
Ok(res)
}
// A parsed HTTP request for easy handling. // A parsed HTTP request for easy handling.
struct Req<'a> { struct Req<'a> {
method: &'a Method, method: &'a Method,
@ -109,7 +73,7 @@ impl Req<'_> {
} }
} }
fn make_req<'x>(r: &'x http::Request<Bytes>) -> Req<'x> { fn simplify<'x>(r: &'x http::Request<Bytes>) -> Req<'x> {
let path: Vec<&str> = r let path: Vec<&str> = r
.uri() .uri()
.path() .path()
@ -136,37 +100,69 @@ use hyper::Method;
const POST: &Method = &Method::POST; const POST: &Method = &Method::POST;
const GET: &Method = &Method::GET; const GET: &Method = &Method::GET;
/// The request handler.
async fn handle(req: Request, cx: Context) -> Result<Response, Infallible> {
// We need to fetch the entire body of the request for signature validation, because that involves making
// a digest of the request body in some cases.
let request = {
let (req, body) = req.into_parts();
let Ok(body) = body.collect().await.map(|b| b.to_bytes()) else {
return Ok(error::invalid_body("Could not get request body"));
};
http::Request::from_parts(req, body)
};
// Simplified representation of a request, so we can pattern match on it more easily in the dispatchers.
let req = simplify(&request);
// We'll use the path to pick where specifically to send the request.
// Check request signature at the door. Even if it isn't needed for a particular endpoint, failing fast
// with a clear error message will save anyone trying to get *their* signatures implementation a major
// headache.
let res = match verify_signature(&cx, &request).await {
// If the request was signed and the signature was accepted, they can access the protected endpoints.
Ok(Some(sig)) => dispatch_signed(cx, req, sig).await,
// Unsigned requests can see a smaller subset of endpoints, most notably the verification actor.
Ok(None) => dispatch_public(cx, req).await,
// If a signature was provided *but it turned out to be unverifiable*, show them the error message.
Err(err) => error::bad_signature(match err {
SigError::VerificationFailed { error } => format!("Verification failed: {error}"),
SigError::ParseSignature { error } => format!("Failed to parse signature: {error}"),
SigError::FailedToFetchKey { keyid } => format!("Failed to fetch {keyid}"),
}),
};
Ok(res)
}
/// Handle a signed and verified request. /// Handle a signed and verified request.
/// ///
/// This function is where all requests to a protected endpoint have to go through. If the request /// This function is where all requests to a protected endpoint have to go through. If the request
/// was signed but does not target a protected endpoint, this function will fall back to the /// was signed but does not target a protected endpoint, this function will fall back to the
/// [`dispatch_public`] handler. /// [`dispatch_public`] handler.
async fn dispatch_signed(req: Req<'_>, sig: Signer, cfg: Config) -> Response { async fn dispatch_signed(cx: Context, req: Req<'_>, sig: Signer) -> Response {
eprintln!("Dispatching signed request"); eprintln!("Dispatching signed request");
match (req.method, req.path()) { match (req.method, req.path()) {
// Viewing ActivityPub objects requires a signed request, i.e. "authorized fetch". // Viewing ActivityPub objects requires a signed request, i.e. "authorized fetch".
// The one exception for this is `/s/request-verifier`, which is where the request // The one exception for this is `/s/request-verifier`, which is where the request
// verification actor lives. // verification actor lives.
(GET, ["o", ulid]) => api::ap::serve_object(ulid), (GET, ["o", ulid]) => api::ap::serve_object(&cx, ulid),
// POSTs to an actor's inbox need to be signed to prevent impersonation. // POSTs to an actor's inbox need to be signed to prevent impersonation.
(POST, ["o", ulid, "inbox"]) => with_json(&req.body, |json| { (POST, ["o", ulid, "inbox"]) => with_json(&req.body, |json| {
// We only handle the intermediate parsing of the json, full resolution of the // We only handle the intermediate parsing of the json, full resolution of the
// activity object will happen inside the inbox handler itself. // activity object will happen inside the inbox handler itself.
api::ap::inbox(ulid, sig, json, cfg) api::ap::inbox(&cx, ulid, sig, json)
}), }),
// Try the resources for which no signature is required as well. // Try the resources for which no signature is required as well.
_ => dispatch_public(req, cfg).await, _ => dispatch_public(cx, req).await,
} }
} }
/// Dispatch `req` to an unprotected endpoint. If the requested path does not exist, the /// Dispatch `req` to an unprotected endpoint. If the requested path does not exist, the
/// function will return a 404 response. /// function will return a 404 response.
async fn dispatch_public(req: Req<'_>, cfg: Config) -> Response { async fn dispatch_public(cx: Context, req: Req<'_>) -> Response {
eprintln!("Dispatching public request"); eprintln!("Dispatching public request");
match (req.method, req.path()) { match (req.method, req.path()) {
(GET, ["proxy"]) => api::ap::proxy(&req.params).await, (GET, ["proxy"]) => api::ap::proxy(&cx, &req.params).await,
(GET, [".well-known", "webfinger"]) => api::wf::resolve(&req.params, cfg), (GET, [".well-known", "webfinger"]) => api::wf::resolve(&cx, &req.params),
(GET, ["s", "request-verifier"]) => api::ap::serve_verifier_actor(cfg), (GET, auth::VERIFIER_MOUNT) => api::ap::serve_verifier_actor(&cx),
(m, p) => { (m, p) => {
eprintln!("404: {m} {p:?}"); eprintln!("404: {m} {p:?}");
error::not_found() error::not_found()

View file

@ -99,22 +99,12 @@ pub struct Private(rsa::RsaPrivateKey);
impl Private { impl Private {
/// Generate a new keypair. /// Generate a new keypair.
pub fn gen() -> (Private, Public) { pub fn gen() -> (Private, Public) {
println!("[!] gen");
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
let bits = 512; let bits = 512;
let private = RsaPrivateKey::new(&mut rng, bits).unwrap(); let private = RsaPrivateKey::new(&mut rng, bits).unwrap();
let public = private.to_public_key(); let public = private.to_public_key();
(Private(private), Public(public)) (Private(private), Public(public))
} }
pub fn tee(path: impl AsRef<Path>) -> (Private, Public) {
println!("[!] tee");
let (a, b) = Private::gen();
println!("[!] keygen complete");
a.0.write_pkcs8_pem_file(path, LineEnding::default())
.unwrap();
println!("[!] write finished");
(a, b)
}
/// Get the public counterpart to this key. /// Get the public counterpart to this key.
pub fn get_public(&self) -> Public { pub fn get_public(&self) -> Public {
Public(self.0.to_public_key()) Public(self.0.to_public_key())
@ -312,7 +302,8 @@ fn sign_rsa_sha256(signing_string: &str, key: &Private) -> Result<Vec<u8>, Strin
Ok(buf) Ok(buf)
} }
/// Default for when /// Maximum time difference between the creation time of the signature and the current time before the
/// signature will be rejected. This is a measure to increase the difficulty of a replay attack.
const EXPIRY_WINDOW: TimeDelta = TimeDelta::minutes(5); const EXPIRY_WINDOW: TimeDelta = TimeDelta::minutes(5);
/// Configuration for the behavior of the signing and verification routines. /// Configuration for the behavior of the signing and verification routines.

View file

@ -1,75 +1,55 @@
use std::sync::OnceLock;
use store::{Key, Store, Transaction}; use store::{Key, Store, Transaction};
use crate::{auth, config::Config, data::schema, Error, Result}; use crate::{auth::Verifier, config::Config, Result};
/// The context of a running ActivityPuppy. /// The context of a running ActivityPuppy.
/// ///
/// This type provides access to the data store and configuration. /// This type provides access to the data store and configuration.
pub struct Context<'c> { #[derive(Clone)]
pub(crate) config: &'c Config, pub struct Context {
pub(crate) db: Store, verifier: Option<Verifier>,
config: Config,
store: Store,
} }
impl Context<'_> { impl Context {
pub fn new(config: Config, store: Store, verifier: Verifier) -> Context {
Context {
verifier: Some(verifier),
config,
store,
}
}
/// Do a data store [transaction][store::Transaction]. /// Do a data store [transaction][store::Transaction].
pub fn run<T>(&self, f: impl FnOnce(&Transaction<'_>) -> Result<T>) -> Result<T> { pub fn run<T>(&self, f: impl FnOnce(&Transaction<'_>) -> Result<T>) -> Result<T> {
self.db.run(f) self.store.run(f)
} }
/// Access the store directly. /// Access the store directly.
pub fn store(&self) -> &Store { pub fn store(&self) -> &Store {
&self.db &self.store
} }
/// Access the configuration. /// Access the configuration.
pub fn config(&self) -> &Config { pub fn config(&self) -> &Config {
self.config &self.config
} }
/// Create an ActivityPub object ID from a key. /// Create an ActivityPub object ID from a key.
pub fn mk_url(&self, key: Key) -> String { pub fn mk_url(&self, key: Key) -> String {
format!("https://{}/o/{key}", self.config.ap_domain) format!("https://{}/o/{key}", self.config.ap_domain)
} }
/// Get the verification actor. /// Get the verification actor.
pub fn verifier(&self) -> &auth::Verifier { pub fn verifier(&self) -> &Verifier {
todo!() self.verifier.as_ref().unwrap()
} }
} }
/// The store, which we initialize only once this way.
///
/// This makes it so we don't have to thread everything down everywhere, we can just access the context
/// when we need it.
static STORE: OnceLock<Store> = OnceLock::new();
/// Load the puppy [`Context`] from anywhere!
///
/// This gives you access to the data store and the configuration, without having to thread it through every place.
// WARNING: don't use this within this crate. there's a clippy lint.
pub fn context<T, E>(f: impl FnOnce(Context<'_>) -> Result<T, E>) -> Result<T, E>
where
E: From<Error>,
{
let cfg = Config {
ap_domain: String::from("test.piss-on.me"),
wf_domain: String::from("test.piss-on.me"),
port: 1312,
};
let db = STORE
.get_or_try_init(|| Store::open(".state", schema()))
.map_err(Error::Store)?
.clone();
f(Context { db, config: &cfg })
}
#[cfg(test)]
use store::types::Schema;
/// Load a context for running tests in. /// Load a context for running tests in.
#[cfg(test)] #[cfg(test)]
pub fn test_context<T>( pub fn test_context<T>(
config: Config, config: Config,
schema: Schema, schema: store::types::Schema,
test: impl FnOnce(Context<'_>) -> Result<T>, test: impl FnOnce(Context) -> Result<T>,
) -> Result<T> { ) -> Result<T> {
Store::test(schema, |db| test(Context { config: &config, db })) Store::test(schema, |store| {
test(Context { config, store, verifier: None })
})
} }

View file

@ -1,6 +1,6 @@
//! Interactions between actors. //! Interactions between actors.
use store::{util::IterExt as _, Key, StoreError}; use store::{util::IterExt as _, Key, StoreError, Transaction};
use crate::{ use crate::{
actor::Actor, actor::Actor,
@ -29,15 +29,13 @@ impl Actor {
/// Makes `biter` bite `victim` and inserts the records into the database. /// Makes `biter` bite `victim` and inserts the records into the database.
pub fn do_bite(&self, cx: &Context, victim: &Actor) -> Result<Bite> { pub fn do_bite(&self, cx: &Context, victim: &Actor) -> Result<Bite> {
let bite = self.bite(victim); let bite = self.bite(victim);
cx.run(|tx| { cx.run(|tx| try { tx.create(bite) })?;
tx.create(bite)?; Ok(bite)
Ok(bite)
})
} }
/// Creates a follow request from `self` to `target`. /// Creates a follow request from `self` to `target`.
pub fn do_follow_request(&self, cx: &Context, target: &Actor) -> Result<FollowRequest> { pub fn do_follow_request(&self, cx: &Context, target: &Actor) -> Result<FollowRequest> {
let req = self.follow_request(target);
cx.run(|tx| { cx.run(|tx| {
let req = self.follow_request(target);
tx.create(req)?; tx.create(req)?;
tx.add_mixin(req.id, Status::Pending)?; tx.add_mixin(req.id, Status::Pending)?;
Ok(req) Ok(req)
@ -72,49 +70,48 @@ impl Actor {
self.key == req.target, self.key == req.target,
"only the target of a follow request may accept it" "only the target of a follow request may accept it"
}; };
cx.run(|tx| try { cx.run(|tx| try { tx.update(req.id, |_| Status::Rejected) })?;
tx.update(req.id, |_| Status::Rejected)?; Ok(())
})
} }
/// Get all pending follow request for `self`. /// Get all pending follow request for `self`.
pub fn pending_requests<'c>( pub fn pending_requests<'c>(
&self, &self,
cx: &'c Context, tx: &'c Transaction<'c>,
) -> impl Iterator<Item = Result<FollowRequest>> + 'c { ) -> impl Iterator<Item = Result<FollowRequest>> + 'c {
cx.store() tx.incoming::<FollowRequest>(self.key)
.incoming::<FollowRequest>(self.key)
.map_err(Error::Store) .map_err(Error::Store)
.filter_bind_results(|req| Ok(if req.is_pending(cx)? { Some(req) } else { None })) .filter_bind_results(|req| Ok(if req.is_pending(tx)? { Some(req) } else { None }))
} }
/// Get all nodes `self` is following. /// Get all nodes `self` is following.
pub fn following<'c>(&self, cx: &'c Context) -> impl Iterator<Item = Result<Key>> + 'c { pub fn following<'c>(&self, tx: &'c Transaction<'c>) -> impl Iterator<Item = Result<Key>> + 'c {
cx.store() tx.outgoing::<Follows>(self.key)
.outgoing::<Follows>(self.key)
.map_err(Error::Store) .map_err(Error::Store)
.map_ok(|a| a.followed) .map_ok(|a| a.followed)
} }
/// Get all followers of `self`. /// Get all followers of `self`.
pub fn followers<'c>(&self, cx: &'c Context) -> impl Iterator<Item = Result<Key>> + 'c { pub fn followers<'c>(&self, tx: &'c Transaction<'c>) -> impl Iterator<Item = Result<Key>> + 'c {
cx.store() tx.incoming::<Follows>(self.key)
.incoming::<Follows>(self.key)
.map_err(Error::Store) .map_err(Error::Store)
.map_ok(|a| a.follower) .map_ok(|a| a.follower)
} }
/// List all specific times `self` was bitten. /// List all specific times `self` was bitten.
pub fn bites_suffered<'c>(&self, cx: &'c Context) -> impl Iterator<Item = Result<Bite>> + 'c { pub fn bites_suffered<'c>(
cx.store().incoming::<Bite>(self.key).map_err(Error::Store) &self,
tx: &'c Transaction<'c>,
) -> impl Iterator<Item = Result<Bite>> + 'c {
tx.incoming::<Bite>(self.key).map_err(Error::Store)
} }
/// Check whether `self` follows `other`. /// Check whether `self` follows `other`.
pub fn follows(&self, cx: &Context, other: &Actor) -> Result<bool> { pub fn follows(&self, tx: &Transaction<'_>, other: &Actor) -> Result<bool> {
try { cx.db.exists::<Follows>(self.key, other.key)? } try { tx.exists::<Follows>(self.key, other.key)? }
} }
} }
impl FollowRequest { impl FollowRequest {
/// Determine if this follow request is pending. /// Determine if this follow request is pending.
pub fn is_pending(&self, cx: &Context) -> Result<bool> { pub fn is_pending(&self, tx: &Transaction<'_>) -> Result<bool> {
// The status is stored as a mixin, so we need to get it. // The status is stored as a mixin, so we need to get it.
let Some(st) = cx.db.get_mixin::<Status>(self.id)? else { let Some(st) = tx.get_mixin::<Status>(self.id)? else {
// If we don't have a status for a follow request, something is borked. // If we don't have a status for a follow request, something is borked.
return Err(StoreError::Missing.into()); return Err(StoreError::Missing.into());
}; };
@ -122,7 +119,7 @@ impl FollowRequest {
// relation already exists. // relation already exists.
debug_assert! { debug_assert! {
!(st == Status::Pending) !(st == Status::Pending)
|| cx.db.exists::<Follows>(self.origin, self.target).map(|x| !x)?, || tx.exists::<Follows>(self.origin, self.target).map(|x| !x)?,
"fr.is_pending -> !(fr.origin follows fr.target)" "fr.is_pending -> !(fr.origin follows fr.target)"
}; };
Ok(st == Status::Pending) Ok(st == Status::Pending)
@ -150,28 +147,31 @@ mod tests {
Config { Config {
ap_domain: String::from("unit-test.puppy.gay"), ap_domain: String::from("unit-test.puppy.gay"),
wf_domain: String::from("unit-test.puppy.gay"), wf_domain: String::from("unit-test.puppy.gay"),
state_dir: todo!(), // TODO: make this a temp dir
port: 0,
} }
} }
#[test] #[test]
fn create_fr() -> Result<()> { fn create_fr() -> Result<()> {
test_context(test_config(), schema(), |cx| try { test_context(test_config(), schema(), |cx| {
let db = cx.store();
let (alice, bob) = make_test_actors(&cx)?; let (alice, bob) = make_test_actors(&cx)?;
alice.do_follow_request(&cx, &bob)?; alice.do_follow_request(&cx, &bob)?;
assert!( assert!(
db.exists::<FollowRequest>(alice.key, bob.key)?, cx.store().exists::<FollowRequest>(alice.key, bob.key)?,
"(alice -> bob) ∈ follow-requested" "(alice -> bob) ∈ follow-requested"
); );
assert!( assert!(
!db.exists::<Follows>(alice.key, bob.key)?, !cx.store().exists::<Follows>(alice.key, bob.key)?,
"(alice -> bob) ∉ follows" "(alice -> bob) ∉ follows"
); );
let pending_for_bob = bob let pending_for_bob = cx.run(|tx| {
.pending_requests(&cx) bob.pending_requests(&tx)
.map_ok(|fr| fr.origin) .map_ok(|fr| fr.origin)
.try_collect::<Vec<_>>()?; .try_collect::<Vec<_>>()
})?;
assert_eq!(pending_for_bob, vec![alice.key], "bob.pending = {{alice}}"); assert_eq!(pending_for_bob, vec![alice.key], "bob.pending = {{alice}}");
Ok(())
}) })
} }
@ -192,16 +192,17 @@ mod tests {
"(bob -> alice) ∉ follows" "(bob -> alice) ∉ follows"
); );
let pending_for_bob: Vec<_> = bob.pending_requests(&cx).try_collect()?; cx.run(|tx| try {
assert!(pending_for_bob.is_empty(), "bob.pending = ∅"); let pending_for_bob: Vec<_> = bob.pending_requests(&tx).try_collect()?;
assert!(pending_for_bob.is_empty(), "bob.pending = ∅");
let followers_of_bob: Vec<_> = bob.followers(&cx).try_collect()?; let followers_of_bob: Vec<_> = bob.followers(&tx).try_collect()?;
assert_eq!( assert_eq!(
followers_of_bob, followers_of_bob,
vec![alice.key], vec![alice.key],
"bob.followers = {{alice}}" "bob.followers = {{alice}}"
); );
Ok(()) })
}) })
} }
@ -212,19 +213,21 @@ mod tests {
let req = alice.do_follow_request(&cx, &bob)?; let req = alice.do_follow_request(&cx, &bob)?;
bob.do_accept_request(&cx, req)?; bob.do_accept_request(&cx, req)?;
let followers_of_bob: Vec<_> = bob.followers(&cx).try_collect()?; cx.run(|tx| try {
assert_eq!( let followers_of_bob: Vec<_> = bob.followers(&tx).try_collect()?;
followers_of_bob, assert_eq!(
vec![alice.key], followers_of_bob,
"bob.followers = {{alice}}" vec![alice.key],
); "bob.followers = {{alice}}"
);
let following_of_alice: Vec<_> = alice.following(&cx).try_collect()?; let following_of_alice: Vec<_> = alice.following(&tx).try_collect()?;
assert_eq!( assert_eq!(
following_of_alice, following_of_alice,
vec![bob.key], vec![bob.key],
"alice.following = {{bob}}" "alice.following = {{bob}}"
); );
})?
}) })
} }
} }

View file

@ -12,12 +12,13 @@
// but that would make every type signature ever 100x more complicated, so we're not doing it. // but that would make every type signature ever 100x more complicated, so we're not doing it.
#![deny(clippy::disallowed_methods, clippy::disallowed_types)] #![deny(clippy::disallowed_methods, clippy::disallowed_types)]
pub use context::{context, Context}; pub use context::Context;
#[cfg(test)] #[cfg(test)]
pub use context::test_context; pub use context::test_context;
use data::{ActivityKind, Channel, Content, Create, Id, Object, ObjectKind, Profile, PublicKey}; use data::{ActivityKind, Channel, Content, Create, Id, Object, ObjectKind, Profile, PublicKey};
use store::Transaction;
pub use store::{self, Key, StoreError}; pub use store::{self, Key, StoreError};
pub use fetch; pub use fetch;
@ -30,21 +31,21 @@ mod interact;
/// Retrieve an ActivityPub object from the database. /// Retrieve an ActivityPub object from the database.
/// ///
/// Fails with `Error::Missing` if the required properties are not present. /// Fails with `Error::Missing` if the required properties are not present.
pub fn get_local_ap_object(cx: &Context<'_>, key: Key) -> Result<fetch::Object> { pub fn get_local_ap_object(tx: &Transaction<'_>, key: Key) -> Result<fetch::Object> {
let Some(obj) = cx.db.get_mixin::<Object>(key)? else { let Some(obj) = tx.get_mixin::<Object>(key)? else {
// We need this data in order to determine the object type. If the passed key does not // We need this data in order to determine the object type. If the passed key does not
// have this data, it must not be an ActivityPub object. // have this data, it must not be an ActivityPub object.
return Err(Error::MissingData { node: key, prop: "Object" }); return Err(Error::MissingData { node: key, prop: "Object" });
}; };
match obj.kind { match obj.kind {
ObjectKind::Actor => { ObjectKind::Actor => {
let Some(Profile { account_name, display_name, .. }) = cx.db.get_mixin(key)? else { let Some(Profile { account_name, display_name, .. }) = tx.get_mixin(key)? else {
return Err(Error::MissingData { node: key, prop: "Profile" }); return Err(Error::MissingData { node: key, prop: "Profile" });
}; };
let Some(Channel { inbox }) = cx.db.get_mixin(key)? else { let Some(Channel { inbox }) = tx.get_mixin(key)? else {
return Err(Error::MissingData { node: key, prop: "Channel" }); return Err(Error::MissingData { node: key, prop: "Channel" });
}; };
let Some(PublicKey { key_id, key_pem }) = cx.db.get_mixin(key)? else { let Some(PublicKey { key_id, key_pem }) = tx.get_mixin(key)? else {
return Err(Error::MissingData { node: key, prop: "PublicKey" }); return Err(Error::MissingData { node: key, prop: "PublicKey" });
}; };
Ok(fetch::Object::Actor(fetch::Actor { Ok(fetch::Object::Actor(fetch::Actor {
@ -60,19 +61,19 @@ pub fn get_local_ap_object(cx: &Context<'_>, key: Key) -> Result<fetch::Object>
})) }))
} }
ObjectKind::Activity(ActivityKind::Create) => { ObjectKind::Activity(ActivityKind::Create) => {
let Some(Create { object, actor, .. }) = cx.db.get_arrow(key)? else { let Some(Create { object, actor, .. }) = tx.get_arrow(key)? else {
panic!("expected a `Create`"); panic!("expected a `Create`");
}; };
let Id(actor) = cx.db.get_alias(actor)?.unwrap(); let Id(actor) = tx.get_alias(actor)?.unwrap();
Ok(fetch::Object::Activity(fetch::Activity { Ok(fetch::Object::Activity(fetch::Activity {
id: obj.id.0.into(), id: obj.id.0.into(),
actor: actor.into(), actor: actor.into(),
object: Box::new(get_local_ap_object(cx, object)?), object: Box::new(get_local_ap_object(tx, object)?),
kind: String::from("Create"), kind: String::from("Create"),
})) }))
} }
ObjectKind::Notelike(kind) => { ObjectKind::Notelike(kind) => {
let Some(Content { content, warning, .. }) = cx.db.get_mixin(key)? else { let Some(Content { content, warning, .. }) = tx.get_mixin(key)? else {
panic!() panic!()
}; };
Ok(fetch::Object::Object { Ok(fetch::Object::Object {
@ -105,9 +106,8 @@ pub mod actor {
impl Actor { impl Actor {
/// Get a local actor from the store by their username. /// Get a local actor from the store by their username.
pub fn by_username(cx: &Context, username: impl ToString) -> Result<Option<Actor>> { pub fn by_username(tx: &Transaction<'_>, username: impl ToString) -> Result<Option<Actor>> {
let maybe_key = cx let maybe_key = tx
.store()
.lookup(Username(username.to_string())) .lookup(Username(username.to_string()))
.map_err(Error::Store)?; .map_err(Error::Store)?;
// For now, we only have local actors. // For now, we only have local actors.
@ -125,8 +125,8 @@ pub mod actor {
cx.run(|tx| { cx.run(|tx| {
let username: Username = username.to_string().into(); let username: Username = username.to_string().into();
// Federation stuff // Federation stuff
mixin_ap_actor(tx, key, &cx.config.ap_domain, true)?; mixin_ap_actor(tx, key, &cx.config().ap_domain, true)?;
mixin_priv_key(tx, key, &cx.config.ap_domain)?; mixin_priv_key(tx, key, &cx.config().ap_domain)?;
// Social properties // Social properties
tx.add_alias(key, username.clone())?; tx.add_alias(key, username.clone())?;
tx.add_mixin(key, Profile { tx.add_mixin(key, Profile {
@ -195,6 +195,7 @@ pub mod config {
pub struct Config { pub struct Config {
pub ap_domain: String, pub ap_domain: String,
pub wf_domain: String, pub wf_domain: String,
pub state_dir: String,
pub port: u16, pub port: u16,
} }
} }
@ -203,8 +204,10 @@ pub mod auth {
use fetch::signatures::{Private, Public, Signature}; use fetch::signatures::{Private, Public, Signature};
use serde_json::{json, Value}; use serde_json::{json, Value};
use crate::config::Config; use crate::{config::Config, Context};
/// Checks request signatures.
#[derive(Clone)]
pub struct Verifier { pub struct Verifier {
actor_id: String, actor_id: String,
key_id: String, key_id: String,
@ -212,13 +215,20 @@ pub mod auth {
public: Public, public: Public,
} }
const VERIFIER_PATH: &str = "/s/request-verifier";
/// The path at which the request verification actor will present itself.
pub const VERIFIER_MOUNT: &[&str] = &["s", "request-verifier"];
impl Verifier { impl Verifier {
pub async fn get_public_key(&self, uri: &str) -> Result<fetch::Key, String> { /// Send a request to get the public key from an ID. This request will be signed with the
/// verifier actor's public key.
async fn fetch_public_key(&self, uri: &str) -> Result<fetch::Key<Public>, String> {
let json = fetch::resolve(&self.signing_key(), uri).await.unwrap(); let json = fetch::resolve(&self.signing_key(), uri).await.unwrap();
Ok(fetch::Key::from_json(dbg!(json)).unwrap()) Ok(fetch::Key::from_json(dbg!(json)).unwrap().upgrade())
} }
pub fn signing_key(&self) -> fetch::SigningKey { /// Get the key that the verification actor signs requests with.
fn signing_key(&self) -> fetch::SigningKey {
fetch::Key { fetch::Key {
id: self.key_id.clone(), id: self.key_id.clone(),
owner: self.actor_id.clone(), owner: self.actor_id.clone(),
@ -226,6 +236,7 @@ pub mod auth {
} }
} }
/// Get the JSON-LD representation of the verifier actor.
pub fn to_json_ld(&self) -> Value { pub fn to_json_ld(&self) -> Value {
json!({ json!({
"@context": [ "@context": [
@ -243,14 +254,15 @@ pub mod auth {
}) })
} }
/// Load the actor's verifier actor.
pub fn load(cfg: &Config) -> Verifier { pub fn load(cfg: &Config) -> Verifier {
println!("[*] loading private key"); println!("[*] loading private key");
let domain = &cfg.ap_domain; let Config { ap_domain, state_dir, .. } = cfg;
let private = Private::load(".state/fetcher.pem"); let private = Private::load(format!("{state_dir}/fetcher.pem"));
println!("* done loading private key"); println!("* done loading private key");
Verifier { Verifier {
actor_id: format!("https://{domain}/s/request-verifier"), actor_id: format!("https://{ap_domain}{VERIFIER_PATH}"),
key_id: format!("https://{domain}/s/request-verifier#sig-key"), key_id: format!("https://{ap_domain}{VERIFIER_PATH}#sig-key"),
public: private.get_public(), public: private.get_public(),
private, private,
} }
@ -269,21 +281,22 @@ pub mod auth {
VerificationFailed { error: String }, VerificationFailed { error: String },
} }
// TODO: make it so we don't have to know what an "http request" is in this crate.
/// Check the signature for a request. /// Check the signature for a request.
pub async fn verify_signature( pub async fn verify_signature(
cx: &Context,
req: &fetch::http::Request<impl AsRef<[u8]> + std::fmt::Debug>, req: &fetch::http::Request<impl AsRef<[u8]> + std::fmt::Debug>,
cfg: &Config,
) -> Result<Option<Signer>, SigError> { ) -> Result<Option<Signer>, SigError> {
println!(">>> starting signature verification for {req:#?}"); println!(">>> starting signature verification for {req:#?}");
if req.uri().path() == "/s/request-verifier" { if req.uri().path() == VERIFIER_PATH {
// Allow access to the request verifier actor without checking the signature. // HACK: Allow access to the request verifier actor without checking the signature.
return Ok(None); return Ok(None);
} }
println!(">>> not going for the verifier!"); println!(">>> not going for the verifier!");
if req.headers().get("signature").is_none() { if req.headers().get("signature").is_none() {
// Request is not signed! // Request is not signed.
return Ok(None); return Ok(None);
}; };
println!(">>> has signature"); println!(">>> has signature");
@ -299,8 +312,8 @@ pub mod auth {
println!(">>> signature is syntatically valid"); println!(">>> signature is syntatically valid");
// Fetch the public key using the verifier private key. // Fetch the public key using the verifier private key.
let verifier = Verifier::load(cfg); let verifier = cx.verifier();
let Ok(public_key) = verifier.get_public_key(sig.key_id()).await else { let Ok(public_key) = verifier.fetch_public_key(sig.key_id()).await else {
return Err(SigError::FailedToFetchKey { return Err(SigError::FailedToFetchKey {
keyid: sig.key_id().to_string(), keyid: sig.key_id().to_string(),
}); });
@ -308,14 +321,12 @@ pub mod auth {
println!(">>> public key fetched"); println!(">>> public key fetched");
// Verify the signature header on the request. // Verify the signature header on the request.
let public_key = public_key.upgrade();
println!(">>> upgraded");
if let Err(error) = public_key.verify(&sig) { if let Err(error) = public_key.verify(&sig) {
println!(">>> rejected"); println!(">>> rejected");
Err(SigError::VerificationFailed { error }) Err(SigError::VerificationFailed { error })
} else { } else {
println!(">>> request verified"); println!(">>> request verified");
Ok(Some(Signer { ap_id: public_key.owner.into() })) Ok(Some(Signer { ap_id: public_key.owner }))
} }
} }
} }

View file

@ -17,53 +17,6 @@ pub struct Post {
pub key: Key, pub key: Key,
} }
impl Post {
pub async fn federate(&self, cx: &Context<'_>) -> crate::Result<()> {
use fetch::{Activity, Object};
let post @ Object::Object { .. } = crate::get_local_ap_object(cx, self.key)? else {
todo!()
};
let author = cx
.db
.incoming::<AuthorOf>(self.key)
.map_ok(|a| a.author)
.next()
.unwrap()?;
let author_id = cx.db.get_alias::<data::Id>(author)?.unwrap();
let activity = Activity {
id: post.id().clone().into(),
object: Box::new(post),
kind: String::from("Create"),
actor: author_id.0.clone().into(),
};
let (private, public) = cx.db.get_mixin_many::<(PrivateKey, PublicKey)>(author)?;
let key = fetch::SigningKey {
id: public.key_id,
owner: author_id.0,
inner: Private::decode_pem(&private.key_pem),
};
// Insert the activity in the database.
cx.run(|tx| try {
let activity_key = Key::gen();
let id: data::Id = cx.mk_url(activity_key).into();
tx.add_mixin(activity_key, data::Object {
id: id.clone(),
kind: ObjectKind::Activity(data::ActivityKind::Create),
local: true,
})?;
tx.create(data::Create {
id: activity_key,
actor: author,
object: self.key,
})?;
tx.add_alias(activity_key, id)?;
})?;
// Send the requests.
fetch::deliver(&key, activity, "https://crimew.gay/users/ezri/inbox").await;
Ok(())
}
}
impl From<&str> for Content { impl From<&str> for Content {
fn from(value: &str) -> Self { fn from(value: &str) -> Self {
value.to_string().into() value.to_string().into()
@ -180,7 +133,7 @@ pub fn create_post(cx: &Context, author: Key, content: impl Into<Content>) -> cr
// Local stuff // Local stuff
mixin_post(tx, key, author, content)?; mixin_post(tx, key, author, content)?;
// Federation stuff // Federation stuff
let id = Id(format!("https://{}/o/{key}", cx.config.ap_domain)); let id = Id(cx.mk_url(key));
tx.add_alias(key, id.clone())?; tx.add_alias(key, id.clone())?;
tx.add_mixin(key, Object { tx.add_mixin(key, Object {
kind: ObjectKind::Notelike("Note".to_string()), kind: ObjectKind::Notelike("Note".to_string()),

View file

@ -217,6 +217,18 @@ impl Transaction<'_> {
{ {
op::between::<A>(self, a, b).map_ok(A::from) op::between::<A>(self, a, b).map_ok(A::from)
} }
/// Construct the arrow from its identifier.
pub fn get_arrow<A>(&self, key: Key) -> Result<Option<A>>
where
A: Arrow<Kind = Multi>,
{
let arrow = self
.open(crate::types::MULTIEDGE_HEADERS)
.get(key)?
.map(|v| Key::split(v.as_ref()))
.map(|(origin, target)| A::from(Multi { origin, target, identity: key }));
Ok(arrow)
}
} }
impl Batch { impl Batch {

View file

@ -128,6 +128,13 @@ impl Transaction<'_> {
{ {
op::join_on(self, iter.into_iter().map_ok(f)) op::join_on(self, iter.into_iter().map_ok(f))
} }
/// Get multiple mixins associated with the same key.
pub fn get_mixin_many<T>(&self, key: Key) -> Result<T>
where
T: GetMany,
{
T::get(self, key)
}
} }
impl Batch { impl Batch {