make post federation work
This commit is contained in:
parent
f0d7d793ca
commit
288c181cc9
10 changed files with 202 additions and 83 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1236,6 +1236,7 @@ dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"cli-table",
|
"cli-table",
|
||||||
"puppy",
|
"puppy",
|
||||||
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
@ -6,3 +6,4 @@ edition = "2021"
|
||||||
puppy = { path = "../../lib/puppy" }
|
puppy = { path = "../../lib/puppy" }
|
||||||
clap = { version = "*", features = ["derive"] }
|
clap = { version = "*", features = ["derive"] }
|
||||||
cli-table = "*"
|
cli-table = "*"
|
||||||
|
tokio = { version = "*", features = ["full"] }
|
||||||
|
|
|
@ -1,16 +1,10 @@
|
||||||
//! Control program for the ActivityPub federated social media server.
|
//! Control program for the ActivityPub federated social media server.
|
||||||
#![feature(iterator_try_collect)]
|
#![feature(iterator_try_collect)]
|
||||||
|
|
||||||
use puppy::{
|
use puppy::{actor::Actor, config::Config, Context};
|
||||||
actor::Actor,
|
|
||||||
config::Config,
|
|
||||||
data::{Bite, Profile},
|
|
||||||
post::Author,
|
|
||||||
store::util::IterExt as _,
|
|
||||||
Context,
|
|
||||||
};
|
|
||||||
|
|
||||||
fn main() -> puppy::Result<()> {
|
#[tokio::main]
|
||||||
|
async fn main() -> puppy::Result<()> {
|
||||||
// puppy::store::Store::nuke(".state")?;
|
// puppy::store::Store::nuke(".state")?;
|
||||||
let config = Config {
|
let config = Config {
|
||||||
ap_domain: "test.piss-on.me".to_string(),
|
ap_domain: "test.piss-on.me".to_string(),
|
||||||
|
@ -19,68 +13,69 @@ fn main() -> puppy::Result<()> {
|
||||||
port: 1312,
|
port: 1312,
|
||||||
};
|
};
|
||||||
let cx = Context::load(config)?;
|
let cx = Context::load(config)?;
|
||||||
let db = cx.store();
|
|
||||||
println!("creating actors");
|
|
||||||
let riley = get_or_create_actor(&cx, "riley")?;
|
let riley = get_or_create_actor(&cx, "riley")?;
|
||||||
let linen = get_or_create_actor(&cx, "linen")?;
|
let post = puppy::post::create_post(&cx, riley.key, "i like boys")?;
|
||||||
if true {
|
puppy::post::federate_post(&cx, post).await
|
||||||
println!("creating posts");
|
|
||||||
puppy::post::create_post(&cx, riley.key, "@linen <3")?;
|
|
||||||
puppy::post::create_post(&cx, linen.key, "@riley <3")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if true {
|
// let linen = get_or_create_actor(&cx, "linen")?;
|
||||||
println!("making riley follow linen");
|
// if true {
|
||||||
cx.run(|tx| {
|
// println!("creating posts");
|
||||||
if !riley.follows(&tx, &linen)? {
|
// puppy::post::create_post(&cx, riley.key, "@linen <3")?;
|
||||||
println!("follow relation does not exist yet");
|
// puppy::post::create_post(&cx, linen.key, "@riley <3")?;
|
||||||
if let Some(req) = linen
|
// }
|
||||||
.pending_requests(&tx)
|
|
||||||
.find_ok(|r| r.origin == riley.key)?
|
|
||||||
{
|
|
||||||
println!("accepting the pending follow request");
|
|
||||||
linen.do_accept_request(&cx, req)
|
|
||||||
} else {
|
|
||||||
println!("no pending follow request; creating");
|
|
||||||
riley.do_follow_request(&cx, &linen).map(|_| ())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
println!("riley already follows linen");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("\nPosts on the instance:");
|
// if true {
|
||||||
for post in puppy::post::fetch_timeline(&db, .., None)?.posts() {
|
// println!("making riley follow linen");
|
||||||
let Author { ref handle, .. } = post.author;
|
// cx.run(|tx| {
|
||||||
let content = post.content.content.as_ref().unwrap();
|
// if !riley.follows(&tx, &linen)? {
|
||||||
println!("- {:?} by {handle}:\n{content}", post.id)
|
// println!("follow relation does not exist yet");
|
||||||
}
|
// if let Some(req) = linen
|
||||||
|
// .pending_requests(&tx)
|
||||||
|
// .find_ok(|r| r.origin == riley.key)?
|
||||||
|
// {
|
||||||
|
// println!("accepting the pending follow request");
|
||||||
|
// linen.do_accept_request(&cx, req)
|
||||||
|
// } else {
|
||||||
|
// println!("no pending follow request; creating");
|
||||||
|
// riley.do_follow_request(&cx, &linen).map(|_| ())
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// println!("riley already follows linen");
|
||||||
|
// Ok(())
|
||||||
|
// }
|
||||||
|
// })?;
|
||||||
|
// }
|
||||||
|
|
||||||
cx.run(|tx| {
|
// println!("\nPosts on the instance:");
|
||||||
println!("\nLinen's followers:");
|
// for post in puppy::post::fetch_timeline(&db, .., None)?.posts() {
|
||||||
for id in linen.followers(&tx).try_collect::<Vec<_>>()? {
|
// let Author { ref handle, .. } = post.author;
|
||||||
let Profile { account_name, .. } = db.get_mixin(id)?.unwrap();
|
// let content = post.content.content.as_ref().unwrap();
|
||||||
println!("- @{account_name} ({id})");
|
// println!("- {:?} by {handle}:\n{content}", post.id)
|
||||||
}
|
// }
|
||||||
|
|
||||||
println!("\nRiley's following:");
|
// cx.run(|tx| {
|
||||||
for id in riley.following(&tx).try_collect::<Vec<_>>()? {
|
// println!("\nLinen's followers:");
|
||||||
let Profile { account_name, .. } = db.get_mixin(id)?.unwrap();
|
// for id in linen.followers(&tx).try_collect::<Vec<_>>()? {
|
||||||
println!("- @{account_name} ({id})");
|
// let Profile { account_name, .. } = db.get_mixin(id)?.unwrap();
|
||||||
}
|
// println!("- @{account_name} ({id})");
|
||||||
|
// }
|
||||||
|
|
||||||
if false {
|
// println!("\nRiley's following:");
|
||||||
println!("Biting riley");
|
// for id in riley.following(&tx).try_collect::<Vec<_>>()? {
|
||||||
linen.do_bite(&cx, &riley)?;
|
// let Profile { account_name, .. } = db.get_mixin(id)?.unwrap();
|
||||||
for Bite { id, biter, .. } in riley.bites_suffered(&tx).try_collect::<Vec<_>>()? {
|
// println!("- @{account_name} ({id})");
|
||||||
let Profile { account_name, .. } = db.get_mixin(biter)?.unwrap();
|
// }
|
||||||
println!("riley was bitten by @{account_name} at {}", id.timestamp());
|
|
||||||
}
|
// if false {
|
||||||
}
|
// println!("Biting riley");
|
||||||
Ok(())
|
// linen.do_bite(&cx, &riley)?;
|
||||||
})
|
// for Bite { id, biter, .. } in riley.bites_suffered(&tx).try_collect::<Vec<_>>()? {
|
||||||
|
// let Profile { account_name, .. } = db.get_mixin(biter)?.unwrap();
|
||||||
|
// println!("riley was bitten by @{account_name} at {}", id.timestamp());
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// Ok(())
|
||||||
|
// })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_or_create_actor(cx: &Context, username: &str) -> puppy::Result<Actor> {
|
fn get_or_create_actor(cx: &Context, username: &str) -> puppy::Result<Actor> {
|
||||||
|
|
|
@ -228,6 +228,7 @@ async fn dispatch_public(
|
||||||
) -> Result<Response, Message> {
|
) -> Result<Response, Message> {
|
||||||
match (req.method, req.path()) {
|
match (req.method, req.path()) {
|
||||||
(GET, ["proxy"]) => ap::proxy(&cx, &req.params).await,
|
(GET, ["proxy"]) => ap::proxy(&cx, &req.params).await,
|
||||||
|
(GET, ["outbox"]) => ap::outbox(&cx, &req.params).await,
|
||||||
(GET, [".well-known", "webfinger"]) => wf::resolve(&cx, &req.params),
|
(GET, [".well-known", "webfinger"]) => wf::resolve(&cx, &req.params),
|
||||||
// TODO: nicer solution for this
|
// TODO: nicer solution for this
|
||||||
(GET, VERIFIER_MOUNT) => Ok(ap::serve_verifier_actor(&verifier)),
|
(GET, VERIFIER_MOUNT) => Ok(ap::serve_verifier_actor(&verifier)),
|
||||||
|
|
|
@ -38,6 +38,29 @@ pub async fn proxy(cx: &Context, params: &[(&str, &str)]) -> Result<Response, Me
|
||||||
Ok(resp.map(Bytes::from).map(Full::new).into())
|
Ok(resp.map(Bytes::from).map(Full::new).into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn outbox(cx: &Context, params: &[(&str, &str)]) -> Result<Response, Message> {
|
||||||
|
// Extract our query parameters.
|
||||||
|
let Some(user) = params.iter().find_map(|(k, v)| (*k == "user").then_some(v)) else {
|
||||||
|
fuck!(400: "expected `user` query param");
|
||||||
|
};
|
||||||
|
let Some(content) = params
|
||||||
|
.iter()
|
||||||
|
.find_map(|(k, v)| (*k == "content").then_some(v))
|
||||||
|
else {
|
||||||
|
fuck!(400: "expected `url` query param");
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(Some(actor)) = cx.run(|tx| Actor::by_username(&tx, user)) else {
|
||||||
|
fuck!(500: "failed actor by name {user}");
|
||||||
|
};
|
||||||
|
|
||||||
|
let post = puppy::post::create_post(&cx, actor.key, content.to_string()).unwrap();
|
||||||
|
puppy::post::federate_post(&cx, post).await.unwrap();
|
||||||
|
Ok(respond! {
|
||||||
|
code: 200
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Handle POSTs to actor inboxes. Requires request signature.
|
/// Handle POSTs to actor inboxes. Requires request signature.
|
||||||
pub fn inbox(cx: &Context, actor_id: &str, sig: Signer, body: Value) -> Response {
|
pub fn inbox(cx: &Context, actor_id: &str, sig: Signer, body: Value) -> Response {
|
||||||
todo!()
|
todo!()
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
|
use http::Method;
|
||||||
use http_body_util::BodyExt as _;
|
use http_body_util::BodyExt as _;
|
||||||
use reqwest::Body;
|
use reqwest::Body;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
@ -36,7 +37,28 @@ impl Client {
|
||||||
/// Note that in order for the request to be considered valid by most implementations, `key.owner`
|
/// Note that in order for the request to be considered valid by most implementations, `key.owner`
|
||||||
/// must equal `payload.actor`.
|
/// must equal `payload.actor`.
|
||||||
pub async fn deliver(&self, key: &SigningKey, payload: &Activity, inbox: &str) {
|
pub async fn deliver(&self, key: &SigningKey, payload: &Activity, inbox: &str) {
|
||||||
todo!()
|
let system = Subsystem::Delivery;
|
||||||
|
|
||||||
|
let body = serde_json::to_string(&payload.to_json_ld()).unwrap();
|
||||||
|
let mut req = system
|
||||||
|
.new_request(inbox)
|
||||||
|
.unwrap()
|
||||||
|
.method(Method::POST)
|
||||||
|
.header("content-type", ACTIVITYPUB_TYPE)
|
||||||
|
.body(body)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
key.sign_with_digest(Options::LEGACY, &mut req)
|
||||||
|
.map_err(FetchError::Sig)
|
||||||
|
.expect("signature generation to work")
|
||||||
|
.commit(&mut req);
|
||||||
|
|
||||||
|
let req = dbg!(req);
|
||||||
|
|
||||||
|
let request = req.map(Body::from).try_into().unwrap();
|
||||||
|
let response = self.inner.execute(request).await.unwrap();
|
||||||
|
let body = dbg!(response).text().await.unwrap();
|
||||||
|
dbg!(body);
|
||||||
}
|
}
|
||||||
/// A high-level function to resolve a single ActivityPub ID using a signed request.
|
/// A high-level function to resolve a single ActivityPub ID using a signed request.
|
||||||
pub async fn resolve(&self, key: &SigningKey, url: &str) -> Result<Value, FetchError> {
|
pub async fn resolve(&self, key: &SigningKey, url: &str) -> Result<Value, FetchError> {
|
||||||
|
|
|
@ -72,6 +72,7 @@ pub enum Object {
|
||||||
Other {
|
Other {
|
||||||
id: String,
|
id: String,
|
||||||
kind: String,
|
kind: String,
|
||||||
|
author: String,
|
||||||
content: Option<String>,
|
content: Option<String>,
|
||||||
summary: Option<String>,
|
summary: Option<String>,
|
||||||
},
|
},
|
||||||
|
@ -89,9 +90,19 @@ impl Object {
|
||||||
match self {
|
match self {
|
||||||
Object::Activity(a) => a.to_json_ld(),
|
Object::Activity(a) => a.to_json_ld(),
|
||||||
Object::Actor(a) => a.to_json_ld(),
|
Object::Actor(a) => a.to_json_ld(),
|
||||||
Object::Other { id, kind, content, summary } => json!({
|
Object::Other {
|
||||||
|
id,
|
||||||
|
kind,
|
||||||
|
content,
|
||||||
|
summary,
|
||||||
|
author,
|
||||||
|
} => json!({
|
||||||
|
"to": [
|
||||||
|
"https://www.w3.org/ns/activitystreams#Public",
|
||||||
|
],
|
||||||
"id": id.to_string(),
|
"id": id.to_string(),
|
||||||
"type": kind,
|
"type": kind,
|
||||||
|
"attributedTo": author,
|
||||||
"content": content,
|
"content": content,
|
||||||
"summary": summary,
|
"summary": summary,
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -184,7 +184,7 @@ impl SigningKey {
|
||||||
T: AsRef<[u8]>,
|
T: AsRef<[u8]>,
|
||||||
{
|
{
|
||||||
// Calculate and insert digest if it isn't there yet, otherwise do nothing.
|
// Calculate and insert digest if it isn't there yet, otherwise do nothing.
|
||||||
let digest = encode(sha256(req.body()));
|
let digest = format!("sha-256={}", encode(sha256(req.body())));
|
||||||
req.headers_mut()
|
req.headers_mut()
|
||||||
.entry("digest")
|
.entry("digest")
|
||||||
.or_insert_with(|| digest.try_into().unwrap());
|
.or_insert_with(|| digest.try_into().unwrap());
|
||||||
|
|
|
@ -16,11 +16,13 @@ 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, AuthorOf, Channel, Content, Create, Id, Object, ObjectKind, Profile, PublicKey,
|
||||||
|
};
|
||||||
|
|
||||||
use store::Transaction;
|
use store::Transaction;
|
||||||
pub use store::{self, Key, StoreError};
|
pub use store::{self, Key, StoreError};
|
||||||
pub use fetch;
|
pub use fetch::{self, FetchError};
|
||||||
|
|
||||||
mod context;
|
mod context;
|
||||||
pub mod data;
|
pub mod data;
|
||||||
|
@ -76,10 +78,17 @@ pub fn get_local_ap_object(tx: &Transaction<'_>, key: Key) -> Result<fetch::obje
|
||||||
let Some(Content { content, warning, .. }) = tx.get_mixin(key)? else {
|
let Some(Content { content, warning, .. }) = tx.get_mixin(key)? else {
|
||||||
panic!()
|
panic!()
|
||||||
};
|
};
|
||||||
|
let Some(AuthorOf { author, .. }) = tx.incoming(key).next().transpose()? else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
let Some(Id(author)) = tx.get_alias(author)? else {
|
||||||
|
todo!()
|
||||||
|
};
|
||||||
Ok(fetch::object::Object::Other {
|
Ok(fetch::object::Object::Other {
|
||||||
id: obj.id.0.clone().into(),
|
id: obj.id.0.clone().into(),
|
||||||
summary: warning,
|
summary: warning,
|
||||||
content,
|
content,
|
||||||
|
author,
|
||||||
kind,
|
kind,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -101,7 +110,6 @@ pub mod actor {
|
||||||
pub struct Actor {
|
pub struct Actor {
|
||||||
/// The key identifying the actor in the data store.
|
/// The key identifying the actor in the data store.
|
||||||
pub key: Key,
|
pub key: Key,
|
||||||
local: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Actor {
|
impl Actor {
|
||||||
|
@ -111,11 +119,7 @@ pub mod actor {
|
||||||
.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.
|
||||||
Ok(maybe_key.map(|key| Actor { key, local: true }))
|
Ok(maybe_key.map(|key| Actor { key }))
|
||||||
}
|
|
||||||
/// Check whether the actor is local or not.
|
|
||||||
pub fn is_local(self) -> bool {
|
|
||||||
self.local
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,7 +140,7 @@ pub mod actor {
|
||||||
about_string: None,
|
about_string: None,
|
||||||
about_fields: Vec::new(),
|
about_fields: Vec::new(),
|
||||||
})?;
|
})?;
|
||||||
Ok(Actor { key, local: true })
|
Ok(Actor { key })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -189,6 +193,8 @@ pub type Result<T, E = Error> = std::result::Result<T, E>;
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
/// An error internal to the store.
|
/// An error internal to the store.
|
||||||
Store(StoreError),
|
Store(StoreError),
|
||||||
|
/// An error generated by the [fetch] subsystem.
|
||||||
|
Fetch(FetchError),
|
||||||
/// Expected `node` to have some property that it doesn't have.
|
/// Expected `node` to have some property that it doesn't have.
|
||||||
MissingData {
|
MissingData {
|
||||||
/// The node that is missing the data.
|
/// The node that is missing the data.
|
||||||
|
|
|
@ -4,15 +4,22 @@ use std::ops::RangeBounds;
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use either::Either::{Left, Right};
|
use either::Either::{Left, Right};
|
||||||
use fetch::signatures::Private;
|
use fetch::{
|
||||||
|
object::{Activity, Object},
|
||||||
|
signatures::Private,
|
||||||
|
};
|
||||||
use store::{util::IterExt as _, Key, Store, StoreError, Transaction};
|
use store::{util::IterExt as _, Key, Store, StoreError, Transaction};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
data::{self, AuthorOf, Content, Id, Object, ObjectKind, PrivateKey, Profile, PublicKey},
|
actor::{get_signing_key, Actor},
|
||||||
Context,
|
data::{
|
||||||
|
self, ActivityKind, AuthorOf, Content, Create, Id, ObjectKind, PrivateKey, Profile,
|
||||||
|
PublicKey,
|
||||||
|
},
|
||||||
|
Context, Error,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub struct Post {
|
pub struct Post {
|
||||||
pub key: Key,
|
pub key: Key,
|
||||||
}
|
}
|
||||||
|
@ -125,7 +132,7 @@ pub fn fetch_timeline(
|
||||||
Ok(Timeline { items: posts })
|
Ok(Timeline { items: posts })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new post.
|
/// Create a new post entity.
|
||||||
pub fn create_post(cx: &Context, author: Key, content: impl Into<Content>) -> crate::Result<Post> {
|
pub fn create_post(cx: &Context, author: Key, content: impl Into<Content>) -> crate::Result<Post> {
|
||||||
let content = content.into();
|
let content = content.into();
|
||||||
cx.run(|tx| {
|
cx.run(|tx| {
|
||||||
|
@ -135,7 +142,7 @@ pub fn create_post(cx: &Context, author: Key, content: impl Into<Content>) -> cr
|
||||||
// Federation stuff
|
// Federation stuff
|
||||||
let id = Id(cx.mk_url(key));
|
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, data::Object {
|
||||||
kind: ObjectKind::Notelike("Note".to_string()),
|
kind: ObjectKind::Notelike("Note".to_string()),
|
||||||
local: true,
|
local: true,
|
||||||
id,
|
id,
|
||||||
|
@ -144,6 +151,58 @@ pub fn create_post(cx: &Context, author: Key, content: impl Into<Content>) -> cr
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn federate_post(cx: &Context, post: Post) -> crate::Result<()> {
|
||||||
|
// Obtain all the data we need to construct our activity
|
||||||
|
let (Content { content, warning }, url, author, signing_key) = cx.run(|tx| try {
|
||||||
|
let Some(AuthorOf { author, .. }) = tx.incoming(post.key).next().transpose()? else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
let signing_key = get_signing_key(tx, Actor { key: author })?;
|
||||||
|
let (c, data::Object { id, .. }) = tx.get_mixin_many(post.key)?;
|
||||||
|
(c, id, author, signing_key)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let activity_key = Key::gen();
|
||||||
|
|
||||||
|
// Insert a create activity into the database so we can serve it later
|
||||||
|
cx.run(|tx| try {
|
||||||
|
let id = Id(cx.mk_url(activity_key));
|
||||||
|
tx.add_alias(activity_key, id.clone())?;
|
||||||
|
tx.add_mixin(activity_key, data::Object {
|
||||||
|
kind: ObjectKind::Activity(ActivityKind::Create),
|
||||||
|
local: true,
|
||||||
|
id,
|
||||||
|
})?;
|
||||||
|
tx.create(Create {
|
||||||
|
id: activity_key,
|
||||||
|
actor: author,
|
||||||
|
object: post.key,
|
||||||
|
})?;
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Construct an ActivityPub message to send
|
||||||
|
let activity = Activity {
|
||||||
|
id: cx.mk_url(activity_key),
|
||||||
|
actor: signing_key.owner.clone(),
|
||||||
|
object: Box::new(Object::Other {
|
||||||
|
id: url.to_string(),
|
||||||
|
kind: "Note".to_string(),
|
||||||
|
author: cx.mk_url(author),
|
||||||
|
summary: warning,
|
||||||
|
content,
|
||||||
|
}),
|
||||||
|
kind: "Create".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch::deliver(
|
||||||
|
&signing_key,
|
||||||
|
activity,
|
||||||
|
"https://crimew.gay/users/ezri/inbox",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Add a post's mixins and predicates to an existing `node`.
|
/// Add a post's mixins and predicates to an existing `node`.
|
||||||
pub fn mixin_post(
|
pub fn mixin_post(
|
||||||
tx: &Transaction<'_>,
|
tx: &Transaction<'_>,
|
||||||
|
|
Loading…
Reference in a new issue