gourami

[UNMAINTAINED] Activitypub server in Rust
Log | Files | Refs | README | LICENSE

commit 338a863b9ad808139728fa9b6f483d272638aecb
parent 5390555cb6a90e0dce51579179134524f380a12a
Author: alex wennerberg <alex@alexwennerberg.com>
Date:   Sat, 16 May 2020 14:27:29 -0500

Run Cargo Fmt

Diffstat:
M.gitignore | 3+++
Mdocs/ADMIN_GUIDE.md | 14+++++++-------
Msrc/ap.rs | 166+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Msrc/db/mod.rs | 4++--
Msrc/db/note.rs | 59++++++++++++++++++++++++++++-------------------------------
Msrc/db/server_mutuals.rs | 4++--
Msrc/error.rs | 2+-
Msrc/lib.rs | 45++++++++++++++++++++++++++-------------------
Msrc/main.rs | 20++++++++++++--------
Msrc/routes.rs | 64++++++++++++++++++++++++++++++++++++++++++++--------------------
10 files changed, 223 insertions(+), 158 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -7,3 +7,6 @@ warp-diesel-ructe-sample/ # env vars .env ansible/prod_dotenv +ansible/inventory.dev +ansible/inventory.prod + diff --git a/docs/ADMIN_GUIDE.md b/docs/ADMIN_GUIDE.md @@ -5,21 +5,21 @@ If you want to administer a Gourami server, you'll need a few technical skills: 1. Basic Linux sysadmin skills -- ability to set up a web server. 2. Basic SQL knowledge -- ability to query and insert records. Right now, Gourami does not have an admin interface or admin tools, so certain actions (such as resetting a user's password or deleting a post or account) will require manual SQL intervention. -## Installation and setup +## Deployment -// TODO +Once you've built Gourami, you'll have a standalone binary that runs the server. Nothing else is needed, aside from Sqlite, OpenSSL and a TLS cert! I put together an ansible playbook that you may find helpful in `ansible/`. If you are having trouble deploying Gourami, feel free to send me an email or open a GitHub issue. + +It should be relatively straightforward to deploy this on a Raspberry Pi too -- I plan on doing this at some point, and I'll put together a guide for it. ## Inviting users -Gourami is invite-only. Right now, you create an invite by adding a record to the invitation_keys table and sharing that key with the user you're inviting. +Gourami is invite-only. Right now, you create an invite by adding a record to the invitation_keys table and sharing that key with the user you're inviting. You'll have to do this for yourself as well. ## Connecting with other servers. Gourami uses ActivityPub to connect with other ActivityPub Actors. If you're familiar with ActivityPub, you should know that Gourami works somewhat differently than a service like Mastodon. -Gourami connects through the "neighborhood" timeline. This means that any post that a user on your server makes in the neighborhood timeline is sent to all servers you are connected with. You can connect with either a server or an individual ActivityPub actor, such as a Mastodon user, but be aware that that user will see all posts in your neighborhood timeline. - -You will only be considered "connected" to a remote server if you follow that server and that server follows you back. +Gourami connects through the "neighborhood" timeline. This means that any post that a user on your server makes in the neighborhood timeline is sent to all servers you are connected with. You can connect with either a server or an individual ActivityPub actor, such as a Mastodon user, but be aware that that user will see all posts in your neighborhood timeline. You will only be considered "connected" to a remote server if you follow that server and that server follows you back. Gourami doesn't implement unfollows yet, so you'll have to directly modify the database and communicate with the user / server you're unfollowing. @@ -42,7 +42,7 @@ You may want to customize parts of Gourami, such as the CSS format or server mes Gourami's ActivityPub implementation is somewhat opinionated and a little esoteric. Gourami is not Mastodon or Twitter and is not trying to be. Using it in that way may cause some frustration -- so just be aware that Gourami does things a little differently. I'm considering adding more 'traditional' activitypub functionality. -The server has a server actor. This is an ActivityPub actor of type "Organization" and is the only ActivityPub actor on the server. All requests go through this actor. This forces you to think of your server as a cohesive whole -- users or other servers can only follow an entire server, not individual users. I encourage you to think about how this would change the way you structure your community. +The server has a server actor. This is an ActivityPub actor of type "Organization" and is the only ActivityPub actor on the server. All requests go through this actor. This forces you to think of your server as a cohesive whole -- users or other servers can only follow an entire server, not individual users. I encourage you to think about how this would change the way you structure your community. The distinction between which user says something is done via a string at the beginning of the post content. Gourami will parse this string as the user, other services will not. Currently, deletes are not supported. diff --git a/src/ap.rs b/src/ap.rs @@ -1,8 +1,8 @@ -use crate::error::Error; use crate::db::conn::POOL; use crate::db::note::{Note, NoteInput, RemoteNoteInput}; -use crate::db::user::{NewRemoteUser, User}; use crate::db::server_mutuals::{NewServerMutual, ServerMutual}; +use crate::db::user::{NewRemoteUser, User}; +use crate::error::Error; use base64; use chrono::{Duration, Utc}; use diesel::insert_into; @@ -21,7 +21,7 @@ use std::env; use std::path::Path; fn domain_url() -> String { - if &env::var("SSL_ENABLED").unwrap() == "1" { + if &env::var("SSL_ENABLED").unwrap() == "1" { return format!("https://{}", &env::var("GOURAMI_DOMAIN").unwrap()); } return format!("http://{}", &env::var("GOURAMI_DOMAIN").unwrap()); @@ -32,7 +32,7 @@ pub struct ServerApData { pub key_id: String, pub domain: String, pub inbox: String, - pub public_key: String + pub public_key: String, } lazy_static! { @@ -56,7 +56,8 @@ fn generate_activity_id() -> String { } #[derive(Deserialize, Serialize)] -pub struct CreateNote { // Maybe use AP crate +pub struct CreateNote { + // Maybe use AP crate id: String, note: ApNote, actor: Actor, @@ -64,41 +65,41 @@ pub struct CreateNote { // Maybe use AP crate #[derive(Debug, Deserialize, Serialize)] pub struct Actor { - #[serde(rename = "@context")] - context: Value, + #[serde(rename = "@context")] + context: Value, id: String, name: Option<String>, summary: Option<String>, - #[serde(rename = "type")] + #[serde(rename = "type")] _type: String, - #[serde(rename = "preferredUsername")] + #[serde(rename = "preferredUsername")] preferred_username: String, inbox: String, - #[serde(rename = "publicKey")] - public_key: PublicKey + #[serde(rename = "publicKey")] + public_key: PublicKey, } #[derive(Debug, Deserialize, Serialize)] pub struct ApNote { content: String, - #[serde(rename = "attributedTo")] + #[serde(rename = "attributedTo")] attributed_to: String, url: String, summary: Option<String>, id: String, - #[serde(rename = "inReplyTo")] - in_reply_to: Option<String> + #[serde(rename = "inReplyTo")] + in_reply_to: Option<String>, } use regex::Regex; impl ApNote { fn get_remote_user_name(&self) -> Option<String> { - let re = Regex::new(r"^(.+?)(💬)").unwrap(); - match re.captures(&self.content) { - Some(t) => t.get(1).unwrap().as_str().parse().ok(), - None => None, - } + let re = Regex::new(r"^(.+?)(💬)").unwrap(); + match re.captures(&self.content) { + Some(t) => t.get(1).unwrap().as_str().parse().ok(), + None => None, + } } } @@ -106,7 +107,7 @@ impl ApNote { pub struct PublicKey { id: String, owner: String, - #[serde(rename = "publicKeyPem")] + #[serde(rename = "publicKeyPem")] public_key_pem: String, } @@ -124,27 +125,26 @@ fn send_to_outbox(activity: bool) { // activitystreams object fetch/store from db. db objects need to serialize/deserialize this object if get -> fetch from db if post -> put to db, send to inbox of followers send to inbox of followers } - #[derive(Deserialize)] pub struct WebFingerQuery { - resource: String + resource: String, } pub fn webfinger_json(query: WebFingerQuery) -> Value { // global -- single user json!({ - "aliases": [ - SERVER.global_id - ], - "links": [ - { - "href": SERVER.global_id, - "rel": "self", - "type": "application/activity+json" - } - ], - "subject": format!("acct:server@{}", SERVER.domain), - }) + "aliases": [ + SERVER.global_id + ], + "links": [ + { + "href": SERVER.global_id, + "rel": "self", + "type": "application/activity+json" + } + ], + "subject": format!("acct:server@{}", SERVER.domain), + }) } /// get the server user json @@ -167,12 +167,11 @@ pub fn server_actor_json() -> Actor { "owner": SERVER.global_id, "publicKeyPem": SERVER.public_key // TODO -- list server admin contact somewhere. summary or attachment - }})).unwrap() + }})) + .unwrap() } -pub fn process_create_note( - v: Value, -) -> Result<(), Box<dyn std::error::Error>> { +pub fn process_create_note(v: Value) -> Result<(), Box<dyn std::error::Error>> { // Actions usually associated with notes // maybe there's a cleaner way to do this. cant iterate over types // TODO inbox forwarding https://www.w3.org/TR/activitypub/#inbox-forwarding @@ -189,20 +188,22 @@ pub fn process_create_note( use crate::db::schema::users::dsl as u; // if user not in db, insert // - let remote_username = ap_note.get_remote_user_name().unwrap_or(ap_note.attributed_to); // TODO -- prevent usernames iwth colons - // strip out username + let remote_username = ap_note + .get_remote_user_name() + .unwrap_or(ap_note.attributed_to); // TODO -- prevent usernames iwth colons + // strip out username let new_user = NewRemoteUser { username: remote_username.clone(), remote_user: true, }; let new_user_id: i32 = conn.transaction(|| { - insert_into(u::users).values(&new_user).execute(conn).ok(); // TODO only check unique constraint error - // last insert id - u::users - .select(u::id) - .filter(u::username.eq(&remote_username)) - .first(conn) + insert_into(u::users).values(&new_user).execute(conn).ok(); // TODO only check unique constraint error + // last insert id + u::users + .select(u::id) + .filter(u::username.eq(&remote_username)) + .first(conn) })?; let new_remote_note = RemoteNoteInput { @@ -221,12 +222,16 @@ pub fn process_create_note( } pub async fn process_accept(v: Value) -> Result<(), Error> { - let actor_id: &str = v.get("actor").ok_or("No actor found")?.as_str().ok_or("Not a string")?; + let actor_id: &str = v + .get("actor") + .ok_or("No actor found")? + .as_str() + .ok_or("Not a string")?; set_mutual_accepted(actor_id); Ok(()) } -fn set_mutual_accepted (the_actor_id: &str) -> Result<(), Error>{ +fn set_mutual_accepted(the_actor_id: &str) -> Result<(), Error> { use crate::db::schema::server_mutuals::dsl::*; let conn = &POOL.get()?; diesel::update(server_mutuals) @@ -236,8 +241,8 @@ fn set_mutual_accepted (the_actor_id: &str) -> Result<(), Error>{ Ok(()) } -// TODO clean this up -fn set_mutual_followed_back (the_actor_id: &str) -> Result<(), Error> { +// TODO clean this up +fn set_mutual_followed_back(the_actor_id: &str) -> Result<(), Error> { use crate::db::schema::server_mutuals::dsl::*; let conn = &POOL.get()?; diesel::update(server_mutuals) @@ -250,8 +255,11 @@ fn set_mutual_followed_back (the_actor_id: &str) -> Result<(), Error> { fn should_accept(actor_id: &str) -> bool { use crate::db::schema::server_mutuals::dsl as s; let conn = &POOL.get().unwrap(); - let sent_req: bool = s::server_mutuals.select(s::actor_id) - .filter(s::actor_id.eq(actor_id)).first::<String>(conn).is_ok(); + let sent_req: bool = s::server_mutuals + .select(s::actor_id) + .filter(s::actor_id.eq(actor_id)) + .first::<String>(conn) + .is_ok(); sent_req } @@ -262,15 +270,17 @@ pub async fn process_follow(v: Value) -> Result<(), Error> { let sent_req = true; // should_accept(actor); if sent_req { set_mutual_followed_back(actor)?; - // send accept follow - let accept = json!({ + // send accept follow + let accept = json!({ "@context": "https://www.w3.org/ns/activitystreams", "id": generate_activity_id(), "type": "Accept", "actor": SERVER.global_id, "object": &v, }); - send_ap_message(&accept, actor_inbox.to_string()).await.unwrap(); + send_ap_message(&accept, actor_inbox.to_string()) + .await + .unwrap(); } Ok(()) // generate accept @@ -282,7 +292,9 @@ pub fn get_connected_remotes() -> Vec<ServerMutual> { let conn = &POOL.get().unwrap(); server_mutuals .filter(accepted.eq(true)) - .filter(followed_back.eq(true)).load(conn).unwrap() + .filter(followed_back.eq(true)) + .load(conn) + .unwrap() } pub async fn send_ap_message( @@ -294,9 +306,15 @@ pub async fn send_ap_message( let client = reqwest::Client::new(); let response = client .post(&destination) - .header("date", Utc::now().format("%a, %d %b %Y %H:%M:%S GMT").to_string()) //HTTP time format + .header( + "date", + Utc::now().format("%a, %d %b %Y %H:%M:%S GMT").to_string(), + ) //HTTP time format .body(msg) - .header("Content-Type", r#"application/ld+json; profile="https://www.w3.org/ns/activitystreams""#) + .header( + "Content-Type", + r#"application/ld+json; profile="https://www.w3.org/ns/activitystreams""#, + ) .http_sign_outgoing()? .send() .await?; @@ -306,8 +324,12 @@ pub async fn send_ap_message( pub async fn get_remote_actor(actor_id: &str) -> Result<Actor, Error> { debug!("Fetching remote actor {}", actor_id); let client = reqwest::Client::new(); - let res = client.get(actor_id) - .header("Accept", r#"application/ld+json; profile="https://www.w3.org/ns/activitystreams""#) + let res = client + .get(actor_id) + .header( + "Accept", + r#"application/ld+json; profile="https://www.w3.org/ns/activitystreams""#, + ) .send() .await?; let res: Actor = res.json().await?; @@ -337,12 +359,15 @@ fn generate_server_follow(remote_actor: &str, my_inbox_url: &str) -> Result<Valu }); use crate::db::schema::server_mutuals::dsl::*; // TODO use str instead of String - insert_into(server_mutuals).values(NewServerMutual{actor_id: remote_actor.to_owned(), inbox_url: my_inbox_url.to_owned()}).execute(conn)?; + insert_into(server_mutuals) + .values(NewServerMutual { + actor_id: remote_actor.to_owned(), + inbox_url: my_inbox_url.to_owned(), + }) + .execute(conn)?; Ok(res) - } - /// Generate an AP create message from a new note pub fn new_note_to_ap_message(note: &Note, user: &User) -> Value { // we need note, user. note noteinput but note obj @@ -405,9 +430,11 @@ impl HttpSignature for reqwest::RequestBuilder { } else { req.url().path().to_string() }; - let unsigned = config.begin_sign(req.method().as_str(), &path_and_query, bt).unwrap(); + let unsigned = config + .begin_sign(req.method().as_str(), &path_and_query, bt) + .unwrap(); let sig_header = unsigned - .sign(server_key_id,|signing_string| { + .sign(server_key_id, |signing_string| { let private_key = read_file(Path::new(&env::var("SIGNATURE_PRIVKEY").unwrap())); let key_pair = ring::signature::RsaKeyPair::from_pkcs8(&private_key.unwrap()).unwrap(); @@ -484,14 +511,17 @@ fn read_file(path: &std::path::Path) -> Result<Vec<u8>, MyError> { use warp::http; -pub async fn verify_ap_message(method: &str, path_and_query: &str, headers: BTreeMap<String, String>) -> Result<(), Error> { +pub async fn verify_ap_message( + method: &str, + path_and_query: &str, + headers: BTreeMap<String, String>, +) -> Result<(), Error> { // TODO -- case insensitivity? // mastodon doesnt use created filed let config = Config::default() .set_expiration(Duration::seconds(3600)) .dont_use_created_field(); - let unverified = config - .begin_verify(method, path_and_query, headers)?; + let unverified = config.begin_verify(method, path_and_query, headers)?; let actor: Actor = get_remote_actor(unverified.key_id()).await?; let res = unverified.verify(|signature, signing_string| { let public_key: &[u8] = actor.public_key.public_key_pem.as_bytes(); diff --git a/src/db/mod.rs b/src/db/mod.rs @@ -2,5 +2,5 @@ pub mod conn; pub mod note; pub mod notification; pub mod schema; -pub mod user; -pub mod server_mutuals; // TODO singular +pub mod server_mutuals; +pub mod user; // TODO singular diff --git a/src/db/note.rs b/src/db/note.rs @@ -1,11 +1,11 @@ use super::schema::notes; -use std::env; use crate::db::user::User; use ammonia; +use chrono::Utc; use maplit::hashset; use regex::Regex; use serde::{Deserialize, Serialize}; -use chrono::Utc; +use std::env; use crate::ap::SERVER; @@ -28,38 +28,35 @@ pub struct Note { pub remote_id: Option<String>, } - impl Note { - pub fn get_url(&self) -> String { - // TODO move domain url function + pub fn get_url(&self) -> String { + // TODO move domain url function format!("{}/note/{}", SERVER.global_id, self.id) } - // we make some modifications for outgoing notes - pub fn get_content_for_outgoing(&self, username: &str) -> String { - // remove first reply string - // username not user id - format!("{}:{}💬 {}", SERVER.domain, username, self.content) - } - - pub fn relative_timestamp(&self) -> String { - // Maybe use some fancy library here - let diff = Utc::now().naive_utc().signed_duration_since(self.created_time); - if diff.num_days() > 30 { - return format!("{}", self.created_time.date()); - } - else if diff.num_hours() > 24 { - return format!("{}d", diff.num_days()); - } - else if diff.num_minutes() > 60 { - return format!("{}h", diff.num_hours()); - } - else if diff.num_seconds() > 60 { - return format!("{}m", diff.num_minutes()); - } - else { - return format!("{}s", diff.num_seconds()); - } - } + // we make some modifications for outgoing notes + pub fn get_content_for_outgoing(&self, username: &str) -> String { + // remove first reply string + // username not user id + format!("{}:{}💬 {}", SERVER.domain, username, self.content) + } + + pub fn relative_timestamp(&self) -> String { + // Maybe use some fancy library here + let diff = Utc::now() + .naive_utc() + .signed_duration_since(self.created_time); + if diff.num_days() > 30 { + return format!("{}", self.created_time.date()); + } else if diff.num_hours() > 24 { + return format!("{}d", diff.num_days()); + } else if diff.num_minutes() > 60 { + return format!("{}h", diff.num_hours()); + } else if diff.num_seconds() > 60 { + return format!("{}m", diff.num_minutes()); + } else { + return format!("{}s", diff.num_seconds()); + } + } } /// Content in the DB is stored in plaintext (WILL BE) diff --git a/src/db/server_mutuals.rs b/src/db/server_mutuals.rs @@ -1,5 +1,5 @@ -use diesel::sqlite::SqliteConnection; use super::schema::server_mutuals; +use diesel::sqlite::SqliteConnection; #[derive(Queryable, PartialEq, Debug)] pub struct ServerMutual { @@ -12,7 +12,7 @@ pub struct ServerMutual { } #[derive(Insertable)] -#[table_name="server_mutuals"] +#[table_name = "server_mutuals"] pub struct NewServerMutual { pub actor_id: String, pub inbox_url: String, diff --git a/src/error.rs b/src/error.rs @@ -8,7 +8,7 @@ pub enum Error { DatabaseError(diesel::result::Error), PoolError(r2d2::Error), MiscError(String), // Just a temp - HttpSigError(http_signature_normalization::PrepareVerifyError) + HttpSigError(http_signature_normalization::PrepareVerifyError), } impl From<&str> for Error { diff --git a/src/lib.rs b/src/lib.rs @@ -10,9 +10,9 @@ extern crate log; extern crate lazy_static; use serde_json::Value; +use std::collections::BTreeMap; use std::convert::Infallible; use zxcvbn::zxcvbn; -use std::collections::BTreeMap; use warp::filters::path::FullPath; use warp::http; @@ -24,8 +24,8 @@ use askama::Template; use db::conn::POOL; use db::note; use db::note::{Note, NoteInput}; -use db::server_mutuals::{ServerMutual}; use db::notification::{NewNotification, NewNotificationViewer, Notification, NotificationViewer}; +use db::server_mutuals::ServerMutual; use db::user::{NewUser, RegistrationKey, User, Username}; use diesel::insert_into; use diesel::prelude::*; @@ -111,7 +111,7 @@ struct RegisterTemplate<'a> { struct ServerInfoTemplate<'a> { global: Global<'a>, users: Vec<User>, - server_mutuals: Vec<ServerMutual> + server_mutuals: Vec<ServerMutual>, } const PAGE_SIZE: i64 = 50; @@ -206,7 +206,11 @@ struct NewNoteRequest { } use tokio::sync::mpsc::UnboundedSender; -async fn handle_new_note_form(u: Option<User>, f: NewNoteRequest, sender: UnboundedSender<(Value, Vec<String>)>) -> Result<impl Reply, Rejection> { +async fn handle_new_note_form( + u: Option<User>, + f: NewNoteRequest, + sender: UnboundedSender<(Value, Vec<String>)>, +) -> Result<impl Reply, Rejection> { match u { Some(u) => { let n = new_note(&u, &f.note_input, f.neighborhood.is_some()).unwrap(); @@ -214,7 +218,8 @@ async fn handle_new_note_form(u: Option<User>, f: NewNoteRequest, sender: Unboun let nj = ap::new_note_to_ap_message(&n, &u); let destinations = ap::get_connected_remotes() .into_iter() - .map(|s| s.inbox_url).collect(); + .map(|s| s.inbox_url) + .collect(); sender.send((nj, destinations)).ok(); } let red_url: http::Uri = f.redirect_url.parse().unwrap(); @@ -248,9 +253,7 @@ pub fn new_note( }; let inserted_note: Note = conn.transaction(|| { insert_into(notes::notes).values(&new_note).execute(conn)?; - notes::notes - .order(notes::id.desc()) - .first(conn) + notes::notes.order(notes::id.desc()).first(conn) })?; // notify person u reply to if mentions.len() > 0 { @@ -269,7 +272,7 @@ pub fn new_note( for mention in mentions { // skip if you reply to yourself if auth_user.username == mention { - continue + continue; } // create reply notification // I thinks this may work but worry about multithreading @@ -364,7 +367,6 @@ fn do_register(form: RegisterForm, query_params: serde_json::Value) -> impl Repl email: &form.email, }; insert_into(users).values(new_user).execute(conn).unwrap(); - } } // database @@ -626,7 +628,7 @@ fn server_info_page(auth_user: Option<User>) -> impl Reply { render_template(&ServerInfoTemplate { global: Global::create(auth_user, "/server"), users: users, - server_mutuals: server_mutuals + server_mutuals: server_mutuals, }) } @@ -709,23 +711,28 @@ pub fn get_outbox() {} use warp::Buf; -pub async fn post_inbox(buf: impl Buf, headers: http::header::HeaderMap) -> Result<impl Reply, Infallible> { +pub async fn post_inbox( + buf: impl Buf, + headers: http::header::HeaderMap, +) -> Result<impl Reply, Infallible> { // TODO check if it is a create note message let message: Value = serde_json::from_slice(buf.bytes()).unwrap(); // TODO error handling debug!("received request {:?}", message); - let mut headersbtree: BTreeMap<String, String> = BTreeMap::new(); + let mut headersbtree: BTreeMap<String, String> = BTreeMap::new(); // convert to btree - for (k,v) in headers.iter() { + for (k, v) in headers.iter() { headersbtree.insert(k.as_str().to_owned(), v.to_str().unwrap().to_owned()); } - ap::verify_ap_message("POST","/inbox", headersbtree).await.unwrap(); // slash or empty string? + ap::verify_ap_message("POST", "/inbox", headersbtree) + .await + .unwrap(); // slash or empty string? let msg_type = message.get("type").unwrap().as_str().unwrap(); debug!("Received ActivityPub message of type {}", msg_type); // TODO improve logging match msg_type { - "Create" => ap::process_create_note(message).unwrap(), - "Follow" => ap::process_follow(message).await.unwrap(), - "Accept" => ap::process_accept(message).await.unwrap(), - _ => () + "Create" => ap::process_create_note(message).unwrap(), + "Follow" => ap::process_follow(message).await.unwrap(), + "Accept" => ap::process_accept(message).await.unwrap(), + _ => (), } // thtas it! Ok("ok!") diff --git a/src/main.rs b/src/main.rs @@ -1,7 +1,7 @@ use clap::{App, Arg, SubCommand}; -use gourami_social::routes::run_server; -use gourami_social::ap; use dotenv; +use gourami_social::ap; +use gourami_social::routes::run_server; #[tokio::main] async fn main() { @@ -31,12 +31,16 @@ async fn main() { if let Some(_) = matches.subcommand_matches("run") { run_server().await; } else if let Some(m) = matches.subcommand_matches("follow") { - let url = m.value_of("URL").unwrap(); - ap::whitelist_or_follow_remote_server(url, true).await.unwrap(); + let url = m.value_of("URL").unwrap(); + ap::whitelist_or_follow_remote_server(url, true) + .await + .unwrap(); } else if let Some(m) = matches.subcommand_matches("whitelist") { - let url = m.value_of("URL").unwrap(); - ap::whitelist_or_follow_remote_server(url, false).await.unwrap(); + let url = m.value_of("URL").unwrap(); + ap::whitelist_or_follow_remote_server(url, false) + .await + .unwrap(); } - // reset password - // follow remote + // reset password + // follow remote } diff --git a/src/routes.rs b/src/routes.rs @@ -1,12 +1,14 @@ use crate::session; use crate::*; use env_logger; -use warp::{header, body, body::form, body::json, filters::cookie, filters::query::query, path, reply}; +use http::header::{HeaderName, HeaderValue, CONTENT_TYPE}; use serde::de::DeserializeOwned; -use warp::reject::{self, Rejection}; use std::time::Duration; +use warp::reject::{self, Rejection}; use warp::reply::Response; -use http::header::{HeaderName, HeaderValue, CONTENT_TYPE}; +use warp::{ + body, body::form, body::json, filters::cookie, filters::query::query, header, path, reply, +}; pub async fn run_server() { // NOT TESTED YET @@ -40,14 +42,24 @@ pub async fn run_server() { let actor_json = warp::path::end() // In practice, the headers may not follow the spec // https://www.w3.org/TR/activitypub/#retrieving-objects - .and(header::exact_ignore_case("accept", r#"application/ld+json; profile="https://www.w3.org/ns/activitystreams""#) - .or(header::exact_ignore_case("accept", r#"application/ld+json"#)) - .or(header::exact_ignore_case("accept", r#"profile="https://www.w3.org/ns/activitystreams""#)) - .or(header::exact_ignore_case("accept", "application/json") + .and( + header::exact_ignore_case( + "accept", + r#"application/ld+json; profile="https://www.w3.org/ns/activitystreams""#, ) + .or(header::exact_ignore_case( + "accept", + r#"application/ld+json"#, + )) + .or(header::exact_ignore_case( + "accept", + r#"profile="https://www.w3.org/ns/activitystreams""#, + )) + .or(header::exact_ignore_case("accept", "application/json")), ) - // TODO content type - .map(|_| reply::json(&ap::server_actor_json()) // how do async work + // TODO content type + .map( + |_| reply::json(&ap::server_actor_json()), // how do async work ); let home = warp::path::end() @@ -106,20 +118,23 @@ pub async fn run_server() { .and(with_sender) .and_then(handle_new_note_form); - let delete_note = path("delete_note").and(private_session_filter()).and(form()).map( - |u: Option<User>, f: DeleteNoteRequest| match u { + let delete_note = path("delete_note") + .and(private_session_filter()) + .and(form()) + .map(|u: Option<User>, f: DeleteNoteRequest| match u { Some(u) => { delete_note(f.note_id).unwrap(); // TODO fix unwrap let red_url: http::Uri = f.redirect_url.parse().unwrap(); redirect(red_url) } None => redirect(http::Uri::from_static("error")), - }, - ); + }); // couldn't figure out how to get this folder to render on root properly let robots = warp::path("robots.txt").and(warp::fs::file("./static/robots.txt")); - let static_files = warp::path("static").and(warp::fs::dir("./static")).or(robots); + let static_files = warp::path("static") + .and(warp::fs::dir("./static")) + .or(robots); // activityPub stuff // This stuff should filter based on the application headers @@ -127,16 +142,25 @@ pub async fn run_server() { // POST // TODO -- setup proper replies - // force content type to be application/ld+json; profile="https://www.w3.org/ns/activitystreams let post_server_inbox = path!("inbox") .and(body::aggregate()) - .and(header::exact_ignore_case("content-type", r#"application/ld+json; profile="https://www.w3.org/ns/activitystreams""#) - .or(header::exact_ignore_case("content-type", r#"application/ld+json"#)) - .or(header::exact_ignore_case("content-type", r#"profile="https://www.w3.org/ns/activitystreams""#))) + .and( + header::exact_ignore_case( + "content-type", + r#"application/ld+json; profile="https://www.w3.org/ns/activitystreams""#, + ) + .or(header::exact_ignore_case( + "content-type", + r#"application/ld+json"#, + )) + .or(header::exact_ignore_case( + "content-type", + r#"profile="https://www.w3.org/ns/activitystreams""#, + )), + ) .and(header::headers_cloned()) - .and_then(|buf,_, headers| async move { - post_inbox(buf, headers).await}); + .and_then(|buf, _, headers| async move { post_inbox(buf, headers).await }); let get_server_outbox = path!("outbox").map(get_outbox);