mirror of
https://git.asonix.dog/asonix/relay.git
synced 2024-11-22 01:21:06 +00:00
Basic relay functionality
This commit is contained in:
parent
aa6b5daaf0
commit
0cbe679e65
11 changed files with 391 additions and 37 deletions
1
.env
1
.env
|
@ -1 +1,2 @@
|
||||||
DATABASE_URL=postgres://ap_actix:ap_actix@localhost:5432/ap_actix
|
DATABASE_URL=postgres://ap_actix:ap_actix@localhost:5432/ap_actix
|
||||||
|
HOSTNAME=localhost:8080
|
||||||
|
|
10
Cargo.lock
generated
10
Cargo.lock
generated
|
@ -374,6 +374,7 @@ dependencies = [
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"ttl_cache",
|
"ttl_cache",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -2117,6 +2118,15 @@ dependencies = [
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uuid"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9fde2f6a4bea1d6e007c4ad38c6839fa71cbb63b6dbf5b595aa38dc9b1093c11"
|
||||||
|
dependencies = [
|
||||||
|
"rand",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vcpkg"
|
name = "vcpkg"
|
||||||
version = "0.2.8"
|
version = "0.2.8"
|
||||||
|
|
|
@ -23,3 +23,4 @@ serde_json = "1.0"
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
tokio = { version = "0.2.13", features = ["sync"] }
|
tokio = { version = "0.2.13", features = ["sync"] }
|
||||||
ttl_cache = "0.5.1"
|
ttl_cache = "0.5.1"
|
||||||
|
uuid = { version = "0.8", features = ["v4"] }
|
||||||
|
|
|
@ -3,7 +3,7 @@ CREATE TABLE listeners (
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
actor_id TEXT UNIQUE NOT NULL,
|
actor_id TEXT UNIQUE NOT NULL,
|
||||||
created_at TIMESTAMP NOT NULL,
|
created_at TIMESTAMP NOT NULL,
|
||||||
updated_at TIMESTAMP NOT NULL
|
updated_at TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX listeners_actor_id_index ON listeners(actor_id);
|
CREATE INDEX listeners_actor_id_index ON listeners(actor_id);
|
||||||
|
|
|
@ -3,7 +3,7 @@ CREATE TABLE blocks (
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
domain_name TEXT UNIQUE NOT NULL,
|
domain_name TEXT UNIQUE NOT NULL,
|
||||||
created_at TIMESTAMP NOT NULL,
|
created_at TIMESTAMP NOT NULL,
|
||||||
updated_at TIMESTAMP NOT NULL
|
updated_at TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX blocks_domain_name_index ON blocks(domain_name);
|
CREATE INDEX blocks_domain_name_index ON blocks(domain_name);
|
||||||
|
|
|
@ -3,7 +3,7 @@ CREATE TABLE whitelists (
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
domain_name TEXT UNIQUE NOT NULL,
|
domain_name TEXT UNIQUE NOT NULL,
|
||||||
created_at TIMESTAMP NOT NULL,
|
created_at TIMESTAMP NOT NULL,
|
||||||
updated_at TIMESTAMP NOT NULL
|
updated_at TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX whitelists_domain_name_index ON whitelists(domain_name);
|
CREATE INDEX whitelists_domain_name_index ON whitelists(domain_name);
|
||||||
|
|
30
src/apub.rs
30
src/apub.rs
|
@ -3,6 +3,7 @@ use activitystreams::{
|
||||||
primitives::XsdAnyUri,
|
primitives::XsdAnyUri,
|
||||||
PropRefs,
|
PropRefs,
|
||||||
};
|
};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PropRefs)]
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PropRefs)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
@ -12,6 +13,9 @@ pub struct AnyExistingObject {
|
||||||
|
|
||||||
#[serde(rename = "type")]
|
#[serde(rename = "type")]
|
||||||
pub kind: String,
|
pub kind: String,
|
||||||
|
|
||||||
|
#[serde(flatten)]
|
||||||
|
ext: HashMap<String, serde_json::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
@ -22,6 +26,7 @@ pub enum ValidTypes {
|
||||||
Delete,
|
Delete,
|
||||||
Follow,
|
Follow,
|
||||||
Undo,
|
Undo,
|
||||||
|
Update,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
@ -43,6 +48,9 @@ pub struct AcceptedObjects {
|
||||||
pub actor: XsdAnyUri,
|
pub actor: XsdAnyUri,
|
||||||
|
|
||||||
pub object: ValidObjects,
|
pub object: ValidObjects,
|
||||||
|
|
||||||
|
#[serde(flatten)]
|
||||||
|
ext: HashMap<String, serde_json::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
@ -71,6 +79,28 @@ impl ValidObjects {
|
||||||
ValidObjects::Object(ref obj) => &obj.id,
|
ValidObjects::Object(ref obj) => &obj.id,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_kind(&self, query_kind: &str) -> bool {
|
||||||
|
match self {
|
||||||
|
ValidObjects::Id(_) => false,
|
||||||
|
ValidObjects::Object(AnyExistingObject { kind, .. }) => kind == query_kind,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn child_object_is_actor(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
ValidObjects::Id(_) => false,
|
||||||
|
ValidObjects::Object(AnyExistingObject { ext, .. }) => {
|
||||||
|
if let Some(o) = ext.get("object") {
|
||||||
|
if let Ok(s) = serde_json::from_value::<String>(o.clone()) {
|
||||||
|
return s.ends_with("/actor");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AcceptedActors {
|
impl AcceptedActors {
|
||||||
|
|
238
src/inbox.rs
238
src/inbox.rs
|
@ -1,16 +1,18 @@
|
||||||
use activitystreams::{
|
use activitystreams::{
|
||||||
activity::apub::{Accept, Follow},
|
activity::apub::{Accept, Announce, Follow, Undo},
|
||||||
|
context,
|
||||||
primitives::XsdAnyUri,
|
primitives::XsdAnyUri,
|
||||||
};
|
};
|
||||||
use actix::Addr;
|
use actix::Addr;
|
||||||
use actix_web::{client::Client, web, Responder};
|
use actix_web::{client::Client, web, HttpResponse};
|
||||||
use futures::join;
|
use futures::join;
|
||||||
use log::{error, info};
|
use log::error;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
apub::{AcceptedActors, AcceptedObjects, ValidTypes},
|
apub::{AcceptedActors, AcceptedObjects, ValidTypes},
|
||||||
db_actor::{DbActor, DbQuery, Pool},
|
db_actor::{DbActor, DbQuery, Pool},
|
||||||
state::State,
|
state::{State, UrlKind},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug, thiserror::Error)]
|
#[derive(Clone, Debug, thiserror::Error)]
|
||||||
|
@ -22,28 +24,146 @@ pub async fn inbox(
|
||||||
state: web::Data<State>,
|
state: web::Data<State>,
|
||||||
client: web::Data<Client>,
|
client: web::Data<Client>,
|
||||||
input: web::Json<AcceptedObjects>,
|
input: web::Json<AcceptedObjects>,
|
||||||
) -> Result<impl Responder, MyError> {
|
) -> Result<HttpResponse, MyError> {
|
||||||
let input = input.into_inner();
|
let input = input.into_inner();
|
||||||
|
|
||||||
let actor = fetch_actor(state.clone(), client, &input.actor).await?;
|
let actor = fetch_actor(state.clone(), &client, &input.actor).await?;
|
||||||
|
|
||||||
match input.kind {
|
match input.kind {
|
||||||
ValidTypes::Announce => (),
|
ValidTypes::Announce | ValidTypes::Create => {
|
||||||
ValidTypes::Create => (),
|
handle_relay(state, client, input, actor).await
|
||||||
ValidTypes::Delete => (),
|
}
|
||||||
ValidTypes::Follow => return handle_follow(db_actor, state, input, actor).await,
|
ValidTypes::Follow => handle_follow(db_actor, state, client, input, actor).await,
|
||||||
ValidTypes::Undo => (),
|
ValidTypes::Delete | ValidTypes::Update => {
|
||||||
|
handle_forward(state, client, input, actor).await
|
||||||
|
}
|
||||||
|
ValidTypes::Undo => handle_undo(db_actor, state, client, input, actor).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn response<T>(item: T) -> HttpResponse
|
||||||
|
where
|
||||||
|
T: serde::ser::Serialize,
|
||||||
|
{
|
||||||
|
HttpResponse::Accepted()
|
||||||
|
.content_type("application/activity+json")
|
||||||
|
.json(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_undo(
|
||||||
|
db_actor: web::Data<Addr<DbActor>>,
|
||||||
|
state: web::Data<State>,
|
||||||
|
client: web::Data<Client>,
|
||||||
|
input: AcceptedObjects,
|
||||||
|
actor: AcceptedActors,
|
||||||
|
) -> Result<HttpResponse, MyError> {
|
||||||
|
if !input.object.is_kind("Follow") {
|
||||||
|
return Err(MyError);
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(MyError)
|
let inbox = actor.inbox().to_owned();
|
||||||
|
|
||||||
|
let state2 = state.clone().into_inner();
|
||||||
|
db_actor.do_send(DbQuery(move |pool: Pool| {
|
||||||
|
let inbox = inbox.clone();
|
||||||
|
|
||||||
|
async move {
|
||||||
|
let conn = pool.get().await?;
|
||||||
|
|
||||||
|
state2.remove_listener(&conn, &inbox).await.map_err(|e| {
|
||||||
|
error!("Error removing listener, {}", e);
|
||||||
|
e
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
let mut undo = Undo::default();
|
||||||
|
let mut follow = Follow::default();
|
||||||
|
|
||||||
|
follow
|
||||||
|
.object_props
|
||||||
|
.set_id(state.generate_url(UrlKind::Activity))?;
|
||||||
|
follow
|
||||||
|
.follow_props
|
||||||
|
.set_actor_xsd_any_uri(actor.id.clone())?
|
||||||
|
.set_object_xsd_any_uri(actor.id.clone())?;
|
||||||
|
|
||||||
|
undo.object_props
|
||||||
|
.set_id(state.generate_url(UrlKind::Activity))?
|
||||||
|
.set_many_to_xsd_any_uris(vec![actor.id.clone()])?
|
||||||
|
.set_context_xsd_any_uri(context())?;
|
||||||
|
undo.undo_props
|
||||||
|
.set_object_object_box(follow)?
|
||||||
|
.set_actor_xsd_any_uri(state.generate_url(UrlKind::Actor))?;
|
||||||
|
|
||||||
|
if input.object.child_object_is_actor() {
|
||||||
|
let undo2 = undo.clone();
|
||||||
|
let client = client.into_inner();
|
||||||
|
actix::Arbiter::spawn(async move {
|
||||||
|
let _ = deliver(&client, actor.id, &undo2).await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(response(undo))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_forward(
|
||||||
|
state: web::Data<State>,
|
||||||
|
client: web::Data<Client>,
|
||||||
|
input: AcceptedObjects,
|
||||||
|
actor: AcceptedActors,
|
||||||
|
) -> Result<HttpResponse, MyError> {
|
||||||
|
let object_id = input.object.id();
|
||||||
|
|
||||||
|
let inboxes = get_inboxes(&state, &actor, &object_id).await?;
|
||||||
|
|
||||||
|
deliver_many(client, inboxes, input);
|
||||||
|
|
||||||
|
Ok(response(HashMap::<(), ()>::new()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_relay(
|
||||||
|
state: web::Data<State>,
|
||||||
|
client: web::Data<Client>,
|
||||||
|
input: AcceptedObjects,
|
||||||
|
actor: AcceptedActors,
|
||||||
|
) -> Result<HttpResponse, MyError> {
|
||||||
|
let object_id = input.object.id();
|
||||||
|
|
||||||
|
if state.is_cached(object_id).await {
|
||||||
|
return Err(MyError);
|
||||||
|
}
|
||||||
|
|
||||||
|
let activity_id: XsdAnyUri = state.generate_url(UrlKind::Activity).parse()?;
|
||||||
|
|
||||||
|
let mut announce = Announce::default();
|
||||||
|
announce
|
||||||
|
.object_props
|
||||||
|
.set_context_xsd_any_uri(context())?
|
||||||
|
.set_many_to_xsd_any_uris(vec![state.generate_url(UrlKind::Followers)])?
|
||||||
|
.set_id(activity_id.clone())?;
|
||||||
|
|
||||||
|
announce
|
||||||
|
.announce_props
|
||||||
|
.set_object_xsd_any_uri(object_id.clone())?
|
||||||
|
.set_actor_xsd_any_uri(state.generate_url(UrlKind::Actor))?;
|
||||||
|
|
||||||
|
let inboxes = get_inboxes(&state, &actor, &object_id).await?;
|
||||||
|
|
||||||
|
deliver_many(client, inboxes, announce);
|
||||||
|
|
||||||
|
state.cache(object_id.to_owned(), activity_id).await;
|
||||||
|
|
||||||
|
Ok(response(HashMap::<(), ()>::new()))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_follow(
|
async fn handle_follow(
|
||||||
db_actor: web::Data<Addr<DbActor>>,
|
db_actor: web::Data<Addr<DbActor>>,
|
||||||
state: web::Data<State>,
|
state: web::Data<State>,
|
||||||
|
client: web::Data<Client>,
|
||||||
input: AcceptedObjects,
|
input: AcceptedObjects,
|
||||||
actor: AcceptedActors,
|
actor: AcceptedActors,
|
||||||
) -> Result<web::Json<Accept>, MyError> {
|
) -> Result<HttpResponse, MyError> {
|
||||||
let (is_listener, is_blocked, is_whitelisted) = join!(
|
let (is_listener, is_blocked, is_whitelisted) = join!(
|
||||||
state.is_listener(&actor.id),
|
state.is_listener(&actor.id),
|
||||||
state.is_blocked(&actor.id),
|
state.is_blocked(&actor.id),
|
||||||
|
@ -61,44 +181,55 @@ async fn handle_follow(
|
||||||
}
|
}
|
||||||
|
|
||||||
if !is_listener {
|
if !is_listener {
|
||||||
let state = state.into_inner();
|
let state = state.clone().into_inner();
|
||||||
|
|
||||||
let actor = actor.clone();
|
let inbox = actor.inbox().to_owned();
|
||||||
db_actor.do_send(DbQuery(move |pool: Pool| {
|
db_actor.do_send(DbQuery(move |pool: Pool| {
|
||||||
let actor_id = actor.id.clone();
|
let inbox = inbox.clone();
|
||||||
let state = state.clone();
|
let state = state.clone();
|
||||||
|
|
||||||
async move {
|
async move {
|
||||||
let conn = pool.get().await?;
|
let conn = pool.get().await?;
|
||||||
|
|
||||||
state.add_listener(&conn, actor_id).await
|
state.add_listener(&conn, inbox).await.map_err(|e| {
|
||||||
|
error!("Error adding listener, {}", e);
|
||||||
|
e
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let actor_inbox = actor.inbox().clone();
|
||||||
|
|
||||||
let mut accept = Accept::default();
|
let mut accept = Accept::default();
|
||||||
let mut follow = Follow::default();
|
let mut follow = Follow::default();
|
||||||
follow.object_props.set_id(input.id)?;
|
follow.object_props.set_id(input.id)?;
|
||||||
follow
|
follow
|
||||||
.follow_props
|
.follow_props
|
||||||
.set_object_xsd_any_uri(format!("https://{}/actor", "localhost"))?
|
.set_object_xsd_any_uri(state.generate_url(UrlKind::Actor))?
|
||||||
.set_actor_xsd_any_uri(actor.id.clone())?;
|
.set_actor_xsd_any_uri(actor.id.clone())?;
|
||||||
|
|
||||||
accept
|
accept
|
||||||
.object_props
|
.object_props
|
||||||
.set_id(format!("https://{}/activities/{}", "localhost", "1"))?
|
.set_id(state.generate_url(UrlKind::Activity))?
|
||||||
.set_many_to_xsd_any_uris(vec![actor.id])?;
|
.set_many_to_xsd_any_uris(vec![actor.id])?;
|
||||||
accept
|
accept
|
||||||
.accept_props
|
.accept_props
|
||||||
.set_object_object_box(follow)?
|
.set_object_object_box(follow)?
|
||||||
.set_actor_xsd_any_uri(format!("https://{}/actor", "localhost"))?;
|
.set_actor_xsd_any_uri(state.generate_url(UrlKind::Actor))?;
|
||||||
|
|
||||||
Ok(web::Json(accept))
|
let client = client.into_inner();
|
||||||
|
let accept2 = accept.clone();
|
||||||
|
actix::Arbiter::spawn(async move {
|
||||||
|
let _ = deliver(&client, actor_inbox, &accept2).await;
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(response(accept))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_actor(
|
async fn fetch_actor(
|
||||||
state: web::Data<State>,
|
state: web::Data<State>,
|
||||||
client: web::Data<Client>,
|
client: &web::Data<Client>,
|
||||||
actor_id: &XsdAnyUri,
|
actor_id: &XsdAnyUri,
|
||||||
) -> Result<AcceptedActors, MyError> {
|
) -> Result<AcceptedActors, MyError> {
|
||||||
if let Some(actor) = state.get_actor(actor_id).await {
|
if let Some(actor) = state.get_actor(actor_id).await {
|
||||||
|
@ -111,13 +242,13 @@ async fn fetch_actor(
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
error!("Couldn't send request for actor, {}", e);
|
error!("Couldn't send request to {} for actor, {}", actor_id, e);
|
||||||
MyError
|
MyError
|
||||||
})?
|
})?
|
||||||
.json()
|
.json()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
error!("Coudn't fetch actor, {}", e);
|
error!("Coudn't fetch actor from {}, {}", actor_id, e);
|
||||||
MyError
|
MyError
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
@ -126,6 +257,65 @@ async fn fetch_actor(
|
||||||
Ok(actor)
|
Ok(actor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn deliver_many<T>(client: web::Data<Client>, inboxes: Vec<XsdAnyUri>, item: T)
|
||||||
|
where
|
||||||
|
T: serde::ser::Serialize + 'static,
|
||||||
|
{
|
||||||
|
let client = client.into_inner();
|
||||||
|
|
||||||
|
actix::Arbiter::spawn(async move {
|
||||||
|
use futures::stream::StreamExt;
|
||||||
|
|
||||||
|
let client = client.clone();
|
||||||
|
let mut unordered = futures::stream::FuturesUnordered::new();
|
||||||
|
|
||||||
|
for inbox in inboxes {
|
||||||
|
unordered.push(deliver(&client, inbox, &item));
|
||||||
|
}
|
||||||
|
|
||||||
|
while let Some(_) = unordered.next().await {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn deliver<T>(
|
||||||
|
client: &std::sync::Arc<Client>,
|
||||||
|
inbox: XsdAnyUri,
|
||||||
|
item: &T,
|
||||||
|
) -> Result<(), MyError>
|
||||||
|
where
|
||||||
|
T: serde::ser::Serialize,
|
||||||
|
{
|
||||||
|
let res = client
|
||||||
|
.post(inbox.as_str())
|
||||||
|
.header("Accept", "application/activity+json")
|
||||||
|
.header("Content-Type", "application/activity+json")
|
||||||
|
.send_json(item)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
error!("Couldn't send deliver request to {}, {}", inbox, e);
|
||||||
|
MyError
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if !res.status().is_success() {
|
||||||
|
error!("Invalid response status from {}, {}", inbox, res.status());
|
||||||
|
return Err(MyError);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_inboxes(
|
||||||
|
state: &web::Data<State>,
|
||||||
|
actor: &AcceptedActors,
|
||||||
|
object_id: &XsdAnyUri,
|
||||||
|
) -> Result<Vec<XsdAnyUri>, MyError> {
|
||||||
|
let domain = object_id.as_url().host().ok_or(MyError)?.to_string();
|
||||||
|
|
||||||
|
let inbox = actor.inbox();
|
||||||
|
|
||||||
|
Ok(state.listeners_without(&inbox, &domain).await)
|
||||||
|
}
|
||||||
|
|
||||||
impl actix_web::error::ResponseError for MyError {}
|
impl actix_web::error::ResponseError for MyError {}
|
||||||
|
|
||||||
impl From<std::convert::Infallible> for MyError {
|
impl From<std::convert::Infallible> for MyError {
|
||||||
|
|
45
src/main.rs
45
src/main.rs
|
@ -1,5 +1,6 @@
|
||||||
#![feature(drain_filter)]
|
#![feature(drain_filter)]
|
||||||
use actix_web::{client::Client, web, App, HttpServer, Responder};
|
use activitystreams::{actor::apub::Application, context, endpoint::EndpointProperties};
|
||||||
|
use actix_web::{client::Client, middleware::Logger, web, App, HttpServer, Responder};
|
||||||
use bb8_postgres::tokio_postgres;
|
use bb8_postgres::tokio_postgres;
|
||||||
|
|
||||||
mod apub;
|
mod apub;
|
||||||
|
@ -8,12 +9,42 @@ mod inbox;
|
||||||
mod label;
|
mod label;
|
||||||
mod state;
|
mod state;
|
||||||
|
|
||||||
use self::{db_actor::DbActor, label::ArbiterLabelFactory, state::State};
|
use self::{
|
||||||
|
db_actor::DbActor,
|
||||||
|
inbox::MyError,
|
||||||
|
label::ArbiterLabelFactory,
|
||||||
|
state::{State, UrlKind},
|
||||||
|
};
|
||||||
|
|
||||||
async fn index() -> impl Responder {
|
async fn index() -> impl Responder {
|
||||||
"hewwo, mr obama"
|
"hewwo, mr obama"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn actor_route(state: web::Data<State>) -> Result<impl Responder, MyError> {
|
||||||
|
let mut application = Application::default();
|
||||||
|
let mut endpoint = EndpointProperties::default();
|
||||||
|
|
||||||
|
endpoint.set_shared_inbox(format!("https://{}/inbox", "localhost"))?;
|
||||||
|
|
||||||
|
application
|
||||||
|
.object_props
|
||||||
|
.set_id(state.generate_url(UrlKind::Actor))?
|
||||||
|
.set_summary_xsd_string("AodeRelay bot")?
|
||||||
|
.set_name_xsd_string("AodeRelay")?
|
||||||
|
.set_url_xsd_any_uri(state.generate_url(UrlKind::Actor))?
|
||||||
|
.set_context_xsd_any_uri(context())?;
|
||||||
|
|
||||||
|
application
|
||||||
|
.ap_actor_props
|
||||||
|
.set_preferred_username("relay")?
|
||||||
|
.set_followers(state.generate_url(UrlKind::Followers))?
|
||||||
|
.set_following(state.generate_url(UrlKind::Following))?
|
||||||
|
.set_inbox(state.generate_url(UrlKind::Inbox))?
|
||||||
|
.set_endpoints(endpoint)?;
|
||||||
|
|
||||||
|
Ok(inbox::response(application))
|
||||||
|
}
|
||||||
|
|
||||||
#[actix_rt::main]
|
#[actix_rt::main]
|
||||||
async fn main() -> Result<(), anyhow::Error> {
|
async fn main() -> Result<(), anyhow::Error> {
|
||||||
dotenv::dotenv().ok();
|
dotenv::dotenv().ok();
|
||||||
|
@ -21,13 +52,19 @@ async fn main() -> Result<(), anyhow::Error> {
|
||||||
pretty_env_logger::init();
|
pretty_env_logger::init();
|
||||||
|
|
||||||
let pg_config: tokio_postgres::Config = std::env::var("DATABASE_URL")?.parse()?;
|
let pg_config: tokio_postgres::Config = std::env::var("DATABASE_URL")?.parse()?;
|
||||||
|
let hostname: String = std::env::var("HOSTNAME")?;
|
||||||
|
let use_whitelist = std::env::var("USE_WHITELIST").is_ok();
|
||||||
|
let use_https = std::env::var("USE_HTTPS").is_ok();
|
||||||
|
|
||||||
let arbiter_labeler = ArbiterLabelFactory::new();
|
let arbiter_labeler = ArbiterLabelFactory::new();
|
||||||
|
|
||||||
let db_actor = DbActor::new(pg_config.clone());
|
let db_actor = DbActor::new(pg_config.clone());
|
||||||
arbiter_labeler.clone().set_label();
|
arbiter_labeler.clone().set_label();
|
||||||
|
|
||||||
let state: State = db_actor
|
let state: State = db_actor
|
||||||
.send(db_actor::DbQuery(|pool| State::hydrate(false, pool)))
|
.send(db_actor::DbQuery(move |pool| {
|
||||||
|
State::hydrate(use_https, use_whitelist, hostname, pool)
|
||||||
|
}))
|
||||||
.await?
|
.await?
|
||||||
.await??;
|
.await??;
|
||||||
|
|
||||||
|
@ -37,11 +74,13 @@ async fn main() -> Result<(), anyhow::Error> {
|
||||||
let client = Client::default();
|
let client = Client::default();
|
||||||
|
|
||||||
App::new()
|
App::new()
|
||||||
|
.wrap(Logger::default())
|
||||||
.data(actor)
|
.data(actor)
|
||||||
.data(state.clone())
|
.data(state.clone())
|
||||||
.data(client)
|
.data(client)
|
||||||
.service(web::resource("/").route(web::get().to(index)))
|
.service(web::resource("/").route(web::get().to(index)))
|
||||||
.service(web::resource("/inbox").route(web::post().to(inbox::inbox)))
|
.service(web::resource("/inbox").route(web::post().to(inbox::inbox)))
|
||||||
|
.service(web::resource("/actor").route(web::get().to(actor_route)))
|
||||||
})
|
})
|
||||||
.bind("127.0.0.1:8080")?
|
.bind("127.0.0.1:8080")?
|
||||||
.run()
|
.run()
|
||||||
|
|
|
@ -3,7 +3,7 @@ table! {
|
||||||
id -> Uuid,
|
id -> Uuid,
|
||||||
domain_name -> Text,
|
domain_name -> Text,
|
||||||
created_at -> Timestamp,
|
created_at -> Timestamp,
|
||||||
updated_at -> Timestamp,
|
updated_at -> Nullable<Timestamp>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ table! {
|
||||||
id -> Uuid,
|
id -> Uuid,
|
||||||
actor_id -> Text,
|
actor_id -> Text,
|
||||||
created_at -> Timestamp,
|
created_at -> Timestamp,
|
||||||
updated_at -> Timestamp,
|
updated_at -> Nullable<Timestamp>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ table! {
|
||||||
id -> Uuid,
|
id -> Uuid,
|
||||||
domain_name -> Text,
|
domain_name -> Text,
|
||||||
created_at -> Timestamp,
|
created_at -> Timestamp,
|
||||||
updated_at -> Timestamp,
|
updated_at -> Nullable<Timestamp>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
91
src/state.rs
91
src/state.rs
|
@ -6,11 +6,14 @@ use lru::LruCache;
|
||||||
use std::{collections::HashSet, sync::Arc};
|
use std::{collections::HashSet, sync::Arc};
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
use ttl_cache::TtlCache;
|
use ttl_cache::TtlCache;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{apub::AcceptedActors, db_actor::Pool};
|
use crate::{apub::AcceptedActors, db_actor::Pool};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct State {
|
pub struct State {
|
||||||
|
use_https: bool,
|
||||||
|
hostname: String,
|
||||||
whitelist_enabled: bool,
|
whitelist_enabled: bool,
|
||||||
actor_cache: Arc<RwLock<TtlCache<XsdAnyUri, AcceptedActors>>>,
|
actor_cache: Arc<RwLock<TtlCache<XsdAnyUri, AcceptedActors>>>,
|
||||||
actor_id_cache: Arc<RwLock<LruCache<XsdAnyUri, XsdAnyUri>>>,
|
actor_id_cache: Arc<RwLock<LruCache<XsdAnyUri, XsdAnyUri>>>,
|
||||||
|
@ -19,11 +22,69 @@ pub struct State {
|
||||||
listeners: Arc<RwLock<HashSet<XsdAnyUri>>>,
|
listeners: Arc<RwLock<HashSet<XsdAnyUri>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum UrlKind {
|
||||||
|
Activity,
|
||||||
|
Actor,
|
||||||
|
Followers,
|
||||||
|
Following,
|
||||||
|
Inbox,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, thiserror::Error)]
|
#[derive(Clone, Debug, thiserror::Error)]
|
||||||
#[error("No host present in URI")]
|
#[error("No host present in URI")]
|
||||||
pub struct HostError;
|
pub struct HostError;
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
|
pub fn generate_url(&self, kind: UrlKind) -> String {
|
||||||
|
let scheme = if self.use_https { "https" } else { "http" };
|
||||||
|
|
||||||
|
match kind {
|
||||||
|
UrlKind::Activity => {
|
||||||
|
format!("{}://{}/activity/{}", scheme, self.hostname, Uuid::new_v4())
|
||||||
|
}
|
||||||
|
UrlKind::Actor => format!("{}://{}/actor", scheme, self.hostname),
|
||||||
|
UrlKind::Followers => format!("{}://{}/followers", scheme, self.hostname),
|
||||||
|
UrlKind::Following => format!("{}://{}/following", scheme, self.hostname),
|
||||||
|
UrlKind::Inbox => format!("{}://{}/inbox", scheme, self.hostname),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn remove_listener(&self, client: &Client, inbox: &XsdAnyUri) -> Result<(), Error> {
|
||||||
|
let hs = self.listeners.clone();
|
||||||
|
|
||||||
|
log::info!("DELETE FROM listeners WHERE actor_id = {};", inbox.as_str());
|
||||||
|
client
|
||||||
|
.execute(
|
||||||
|
"DELETE FROM listeners WHERE actor_id = $1::TEXT;",
|
||||||
|
&[&inbox.as_str()],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut write_guard = hs.write().await;
|
||||||
|
write_guard.remove(inbox);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn listeners_without(&self, inbox: &XsdAnyUri, domain: &str) -> Vec<XsdAnyUri> {
|
||||||
|
let hs = self.listeners.clone();
|
||||||
|
|
||||||
|
let read_guard = hs.read().await;
|
||||||
|
|
||||||
|
read_guard
|
||||||
|
.iter()
|
||||||
|
.filter_map(|listener| {
|
||||||
|
if let Some(host) = listener.as_url().host() {
|
||||||
|
if listener != inbox && host.to_string() != domain {
|
||||||
|
return Some(listener.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn is_whitelisted(&self, actor_id: &XsdAnyUri) -> bool {
|
pub async fn is_whitelisted(&self, actor_id: &XsdAnyUri) -> bool {
|
||||||
if !self.whitelist_enabled {
|
if !self.whitelist_enabled {
|
||||||
return true;
|
return true;
|
||||||
|
@ -94,9 +155,13 @@ impl State {
|
||||||
return Err(HostError.into());
|
return Err(HostError.into());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
log::info!(
|
||||||
|
"INSERT INTO blocks (domain_name, created_at) VALUES ($1::TEXT, 'now'); [{}]",
|
||||||
|
host.to_string()
|
||||||
|
);
|
||||||
client
|
client
|
||||||
.execute(
|
.execute(
|
||||||
"INSERT INTO blocks (actor_id, created_at) VALUES ($1::TEXT, now);",
|
"INSERT INTO blocks (domain_name, created_at) VALUES ($1::TEXT, 'now');",
|
||||||
&[&host.to_string()],
|
&[&host.to_string()],
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -116,9 +181,13 @@ impl State {
|
||||||
return Err(HostError.into());
|
return Err(HostError.into());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
log::info!(
|
||||||
|
"INSERT INTO whitelists (domain_name, created_at) VALUES ($1::TEXT, 'now'); [{}]",
|
||||||
|
host.to_string()
|
||||||
|
);
|
||||||
client
|
client
|
||||||
.execute(
|
.execute(
|
||||||
"INSERT INTO whitelists (actor_id, created_at) VALUES ($1::TEXT, now);",
|
"INSERT INTO whitelists (domain_name, created_at) VALUES ($1::TEXT, 'now');",
|
||||||
&[&host.to_string()],
|
&[&host.to_string()],
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -132,9 +201,13 @@ impl State {
|
||||||
pub async fn add_listener(&self, client: &Client, listener: XsdAnyUri) -> Result<(), Error> {
|
pub async fn add_listener(&self, client: &Client, listener: XsdAnyUri) -> Result<(), Error> {
|
||||||
let listeners = self.listeners.clone();
|
let listeners = self.listeners.clone();
|
||||||
|
|
||||||
|
log::info!(
|
||||||
|
"INSERT INTO listeners (actor_id, created_at) VALUES ($1::TEXT, 'now'); [{}]",
|
||||||
|
listener.as_str(),
|
||||||
|
);
|
||||||
client
|
client
|
||||||
.execute(
|
.execute(
|
||||||
"INSERT INTO listeners (actor_id, created_at) VALUES ($1::TEXT, now);",
|
"INSERT INTO listeners (actor_id, created_at) VALUES ($1::TEXT, 'now');",
|
||||||
&[&listener.as_str()],
|
&[&listener.as_str()],
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -145,7 +218,12 @@ impl State {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn hydrate(whitelist_enabled: bool, pool: Pool) -> Result<Self, Error> {
|
pub async fn hydrate(
|
||||||
|
use_https: bool,
|
||||||
|
whitelist_enabled: bool,
|
||||||
|
hostname: String,
|
||||||
|
pool: Pool,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
let pool1 = pool.clone();
|
let pool1 = pool.clone();
|
||||||
let pool2 = pool.clone();
|
let pool2 = pool.clone();
|
||||||
|
|
||||||
|
@ -170,7 +248,9 @@ impl State {
|
||||||
let (blocks, whitelists, listeners) = try_join!(f1, f2, f3)?;
|
let (blocks, whitelists, listeners) = try_join!(f1, f2, f3)?;
|
||||||
|
|
||||||
Ok(State {
|
Ok(State {
|
||||||
|
use_https,
|
||||||
whitelist_enabled,
|
whitelist_enabled,
|
||||||
|
hostname,
|
||||||
actor_cache: Arc::new(RwLock::new(TtlCache::new(1024 * 8))),
|
actor_cache: Arc::new(RwLock::new(TtlCache::new(1024 * 8))),
|
||||||
actor_id_cache: Arc::new(RwLock::new(LruCache::new(1024 * 8))),
|
actor_id_cache: Arc::new(RwLock::new(LruCache::new(1024 * 8))),
|
||||||
blocks: Arc::new(RwLock::new(blocks)),
|
blocks: Arc::new(RwLock::new(blocks)),
|
||||||
|
@ -181,12 +261,14 @@ impl State {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn hydrate_blocks(client: &Client) -> Result<HashSet<String>, Error> {
|
pub async fn hydrate_blocks(client: &Client) -> Result<HashSet<String>, Error> {
|
||||||
|
log::info!("SELECT domain_name FROM blocks");
|
||||||
let rows = client.query("SELECT domain_name FROM blocks", &[]).await?;
|
let rows = client.query("SELECT domain_name FROM blocks", &[]).await?;
|
||||||
|
|
||||||
parse_rows(rows)
|
parse_rows(rows)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn hydrate_whitelists(client: &Client) -> Result<HashSet<String>, Error> {
|
pub async fn hydrate_whitelists(client: &Client) -> Result<HashSet<String>, Error> {
|
||||||
|
log::info!("SELECT domain_name FROM whitelists");
|
||||||
let rows = client
|
let rows = client
|
||||||
.query("SELECT domain_name FROM whitelists", &[])
|
.query("SELECT domain_name FROM whitelists", &[])
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -195,6 +277,7 @@ pub async fn hydrate_whitelists(client: &Client) -> Result<HashSet<String>, Erro
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn hydrate_listeners(client: &Client) -> Result<HashSet<XsdAnyUri>, Error> {
|
pub async fn hydrate_listeners(client: &Client) -> Result<HashSet<XsdAnyUri>, Error> {
|
||||||
|
log::info!("SELECT actor_id FROM listeners");
|
||||||
let rows = client.query("SELECT actor_id FROM listeners", &[]).await?;
|
let rows = client.query("SELECT actor_id FROM listeners", &[]).await?;
|
||||||
|
|
||||||
parse_rows(rows)
|
parse_rows(rows)
|
||||||
|
|
Loading…
Reference in a new issue