gourami

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

commit df6bd711aef66db1ec25486217dd338eef5231bb
parent c17ccb570b71e38f2cd8a82f79549436fb692d57
Author: alex wennerberg <alex@alexwennerberg.com>
Date:   Sat,  9 May 2020 20:11:05 -0500

Janky, WIP server follow / accept working

Diffstat:
MCargo.lock | 27+++++++++++++++++++++++++++
MCargo.toml | 1+
Mdocs/ADMIN_GUIDE.md | 3+++
Msample_env | 1+
Msrc/ap.rs | 312+++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Msrc/db/mod.rs | 1+
Msrc/db/server_mutuals.rs | 15++++++++++++---
Msrc/db/user.rs | 1+
Msrc/lib.rs | 33+++++++++++++++++++++++----------
Msrc/main.rs | 16++++++++++++++--
Msrc/routes.rs | 14+++++++-------
Mtemplates/server_info.html | 3+++
12 files changed, 309 insertions(+), 118 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -1,6 +1,32 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. [[package]] +name = "activitystreams" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5b29a0f2c64cc56f2b79ec29cab68a9dab3b714d811a55668d072f18a8638e" +dependencies = [ + "activitystreams-derive", + "chrono", + "mime 0.3.16", + "serde", + "serde_json", + "thiserror", + "url", +] + +[[package]] +name = "activitystreams-derive" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39ba5929399e9f921055bac76dd8f47419fa5b6b6da1ac4c1e82b94ed0ac7b4" +dependencies = [ + "proc-macro2 1.0.10", + "quote 1.0.3", + "syn 1.0.17", +] + +[[package]] name = "aho-corasick" version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -603,6 +629,7 @@ dependencies = [ name = "gourami_social" version = "0.1.0" dependencies = [ + "activitystreams", "ammonia", "askama", "base64 0.12.1", diff --git a/Cargo.toml b/Cargo.toml @@ -5,6 +5,7 @@ authors = ["alex wennerberg <alex@alexwennerberg.com>"] edition = "2018" [dependencies] +activitystreams = "0.6.0" ammonia = "3.1.0" askama = "0.8.0" base64 = "0.12.1" diff --git a/docs/ADMIN_GUIDE.md b/docs/ADMIN_GUIDE.md @@ -32,6 +32,9 @@ I would recommend following basic Linux syadmin best practices: disable password ## Federation -- the "neighborhood" +Use the admin command follow to follow a server. +That server must accept your follow, then follow you back in order to be in the neighborhood. There is no one-way following in gourami, we (ab)use the AP standard to force mutual follows + ## Passwordless local deployment Don't do this on the public internet, it is a bad idea and will only lead to ruin! Seriously, don't do it. diff --git a/sample_env b/sample_env @@ -8,3 +8,4 @@ export GOURAMI_DOMAIN="localhost:3030" # used for http signatures export SIGNATURE_PRIVKEY="local/private.pk8" export SIGNATURE_PUBKEY="local/public.der" +export SIGNATURE_PUBKEY_PEM="local/public.pem" diff --git a/src/ap.rs b/src/ap.rs @@ -1,21 +1,23 @@ -use ring::digest; -use ring::signature::UnparsedPublicKey; -use std::path::Path; -use base64; use crate::db::conn::POOL; use crate::db::note::{NoteInput, RemoteNoteInput}; use crate::db::user::{NewRemoteUser, User}; +use crate::db::server_mutuals::{NewServerMutual, ServerMutual}; +use base64; +use chrono::{Duration, Utc}; use diesel::insert_into; -use openssl::rsa::Rsa; use diesel::prelude::*; use diesel::sqlite::SqliteConnection; +use http_signature_normalization::Config; +use openssl::rsa::Rsa; +use reqwest::Request; +use ring::digest; +use ring::signature::UnparsedPublicKey; +use serde::{Deserialize, Serialize}; use serde_json::json; use serde_json::Value; -use std::env; use std::collections::BTreeMap; -use reqwest::Request; -use chrono::{Duration, Utc}; -use http_signature_normalization::{Config}; +use std::env; +use std::path::Path; /// Users don't follow users in Gourami. Instead the server does hte following /// There are a number of reasons for this: @@ -26,8 +28,10 @@ use http_signature_normalization::{Config}; /// This is a somewhat eccentric activitypub implementation, but it is as consistent with the spec /// as I can make it! use std::fs; -lazy_static! { - // const SERVER_ACTOR = "gourami.social" + +// TODO figure out how to get static working +fn server_actor() -> String { + return format!("http://{}/actor", &env::var("GOURAMI_DOMAIN").unwrap()); } // ActivityPub outbox @@ -35,36 +39,41 @@ 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 } -fn verify_incoming_message() { +// build something like this +struct ActivityPubMessage { } +fn verify_incoming_message() {} + enum Action { CreateNote, DoNothing, // DeleteNote } + /// get the server user json pub fn server_actor_json() -> Value { // TODO figure out how to get lazy static working + // TODO use ap library let DOMAIN: &str = &env::var("GOURAMI_DOMAIN").unwrap(); - let SERVER_ACTOR: &str = &format!("http://{}/actor", DOMAIN); - let SERVER_INBOX: &str = &format!("http://{}/inbox", DOMAIN); - let SERVER_KEY_ID: &str = &format!("{}#key", SERVER_ACTOR); - let SERVER_PUBLIC_KEY: &str = &fs::read_to_string(env::var("SIGNATURE_PUBKEY").unwrap()).unwrap(); + let server_inbox: &str = &format!("http://{}/inbox", DOMAIN); + let SERVER_KEY_ID: &str = &format!("{}#key", server_actor()); + let SERVER_PUBLIC_KEY: &str = + &fs::read_to_string(env::var("SIGNATURE_PUBKEY_PEM").unwrap()).unwrap(); json!({ "@context": [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1" ], - "id": SERVER_ACTOR, + "id": server_actor(), "type": "Organization", // application? "preferredUsername": DOMAIN, // think about it - "inbox": SERVER_INBOX, + "inbox": server_inbox, "publicKey": { "id": SERVER_KEY_ID, - "owner": SERVER_ACTOR, + "owner": server_actor(), "publicKeyPem": SERVER_PUBLIC_KEY }}) } @@ -74,12 +83,13 @@ fn categorize_input_message(v: Value) -> Action { } pub fn process_create_note( - conn: &SqliteConnection, 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 + // + let conn = &POOL.get().unwrap(); let object = v.get("object").ok_or("No object found")?; let _type = object.get("type").ok_or("No object type found")?; let content = object @@ -116,14 +126,14 @@ pub fn process_create_note( username: String::from(remote_creator), remote_url: Some(String::from(remote_creator)), }; - // + insert_into(u::users).values(&new_user).execute(conn).ok(); // TODO only check unique constraint error + // last insert id let new_user_id: i32 = u::users .select(u::id) .filter(u::remote_url.eq(remote_creator)) - .first(conn) - .unwrap(); + .first(conn)?; let new_remote_note = RemoteNoteInput { content: String::from(content), @@ -143,37 +153,110 @@ pub fn process_create_note( return Ok(()); } +pub async fn process_accept(v: Value) -> Result<(), reqwest::Error> { + let actor: &str = v.get("actor").unwrap().as_str().unwrap(); + let remote_actor: Value = reqwest::get(actor).await?.json().await?; + let actor_inbox = remote_actor.get("inbox").unwrap().as_str().unwrap(); + set_mutual_accepted(actor_inbox); + Ok(()) +} + +fn set_mutual_accepted (actor_inbox: &str) { + use crate::db::schema::server_mutuals::dsl::*; + let conn = &POOL.get().unwrap(); + diesel::update(server_mutuals) + .filter(inbox_url.eq(actor_inbox)) + .set(accepted.eq(true)) + .execute(conn).unwrap(); +} + +// TODO clean this up +fn set_mutual_followed_back (actor_inbox: &str) { + use crate::db::schema::server_mutuals::dsl::*; + let conn = &POOL.get().unwrap(); + diesel::update(server_mutuals) + .filter(inbox_url.eq(actor_inbox)) + .set(followed_back.eq(true)) + .execute(conn).unwrap(); +} + +fn should_accept(actor_inbox: &str) -> bool { + use crate::db::schema::server_mutuals::dsl::*; + let conn = &POOL.get().unwrap(); + let sent_req: bool = server_mutuals.select(inbox_url).filter(inbox_url.eq(actor_inbox)).first::<String>(conn).is_ok(); + sent_req +} + +pub async fn process_follow(v: Value) -> Result<(), reqwest::Error> { + let actor: &str = v.get("actor").unwrap().as_str().unwrap(); + let remote_actor: Value = reqwest::get(actor).await?.json().await?; + let actor_inbox = remote_actor.get("inbox").unwrap().as_str().unwrap(); + let sent_req = should_accept(actor_inbox); + debug!("Should server accept the request? {}", sent_req); + if sent_req { + set_mutual_followed_back(actor_inbox); + // send accept follow + let accept = json!({ + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://my-example.com/my-first-accept", + "type": "Accept", + "actor": server_actor(), + "object": &v, + }); + send_ap_message(&accept, vec![actor_inbox.to_string()]).await.unwrap(); + } + Ok(()) + // generate accept +} + pub fn get_destinations() -> Vec<String> { // maybe lazy static this use crate::db::schema::server_mutuals::dsl::*; let conn = &POOL.get().unwrap(); - server_mutuals.select(inbox_url).load(conn).unwrap() + server_mutuals.select(inbox_url) + .filter(accepted.eq(true)) + .filter(followed_back.eq(true)).load(conn).unwrap() } pub async fn send_ap_message( ap_message: &Value, - destinations: Vec<String>, + destinations: Vec<String>, // really vec of URLs ) -> Result<(), reqwest::Error> { // Right now we have only once delivery for destination in destinations { let client = reqwest::Client::new(); - client.post(&destination).json(&ap_message).send().await?; + client + .post(&destination) + .header("date", Utc::now().to_rfc2822()) + .json(&ap_message) + .header("Content-Type", "application/activity+json") + .send() + .await?; } Ok(()) } -fn follow_remote_server(remote_url: String) { - // create follow request +pub async fn follow_remote_server(remote_url: &str) -> Result<(), reqwest::Error> { + let remote_actor: Value = reqwest::get(remote_url).await?.json().await?; + let inbox_url = remote_actor.get("inbox").unwrap().as_str().unwrap(); + let msg = generate_server_follow(inbox_url); + send_ap_message(&msg, vec![inbox_url.to_owned()]).await?; + Ok(()) } -fn generate_server_follow(remote_url: String) -> Value { - json!({ +fn generate_server_follow(remote_url: &str) -> Value { + let conn = &POOL.get().unwrap(); + let res = json!({ "@context": "https://www.w3.org/ns/activitystreams", "id": "https://my-example.com/my-first-follow", "type": "Follow", - "actor": "https://my-example.com/actor", + "actor": server_actor(), "object": remote_url, - }) + }); + use crate::db::schema::server_mutuals::dsl::*; + insert_into(server_mutuals).values(NewServerMutual{inbox_url: remote_url.to_owned()}).execute(conn).unwrap(); + res + } /// Generate an AP create message from a new note @@ -220,9 +303,12 @@ impl HttpSignature for reqwest::RequestBuilder { fn http_sign_outgoing(self) -> Result<reqwest::RequestBuilder, Box<dyn std::error::Error>> { // try and remove clone here let req = self.try_clone().unwrap().build().unwrap(); - let config = Config::default().set_expiration(Duration::seconds(3600)).dont_use_created_field(); - // let server_key_id = - let server_key_id: &str = &format!("http://{}/actor#key", &env::var("GOURAMI_DOMAIN").unwrap()); + let config = Config::default() + .set_expiration(Duration::seconds(3600)) + .dont_use_created_field(); + // let server_key_id = + let server_key_id: &str = + &format!("http://{}/actor#key", &env::var("GOURAMI_DOMAIN").unwrap()); let mut bt = std::collections::BTreeMap::new(); for (k, v) in req.headers().iter() { bt.insert(k.as_str().to_owned(), v.to_str()?.to_owned()); @@ -234,18 +320,27 @@ impl HttpSignature for reqwest::RequestBuilder { }; let unsigned = config.begin_sign(req.method().as_str(), &path_and_query, bt)?; println!("{:?}", &unsigned); - let sig_header = unsigned.sign(server_key_id.to_owned(), |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(); - let rng = ring::rand::SystemRandom::new(); - let mut signature = vec![0; key_pair.public_modulus_len()]; - key_pair.sign(&ring::signature::RSA_PKCS1_SHA256, &rng, signing_string.as_bytes(), &mut signature).unwrap(); - // let digest = digest::digest(&digest::SHA256, &signing_string.as_bytes()); - println!("{:?}", &signing_string); - let hexencode = base64::encode(&signature); - Ok(hexencode) as Result<_, Box<dyn std::error::Error>> - })? - .signature_header(); + let sig_header = unsigned + .sign(server_key_id.to_owned(), |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(); + let rng = ring::rand::SystemRandom::new(); + let mut signature = vec![0; key_pair.public_modulus_len()]; + key_pair + .sign( + &ring::signature::RSA_PKCS1_SHA256, + &rng, + signing_string.as_bytes(), + &mut signature, + ) + .unwrap(); + // let digest = digest::digest(&digest::SHA256, &signing_string.as_bytes()); + println!("{:?}", &signing_string); + let hexencode = base64::encode(&signature); + Ok(hexencode) as Result<_, Box<dyn std::error::Error>> + })? + .signature_header(); // this SHOULD be OK // host and date? println!("{:?}", &sig_header); @@ -255,70 +350,84 @@ impl HttpSignature for reqwest::RequestBuilder { } } -fn sign_and_verify_rsa(private_key_path: &std::path::Path, -public_key_path: &std::path::Path) --> Result<(), MyError> { +fn sign_and_verify_rsa( + private_key_path: &std::path::Path, + public_key_path: &std::path::Path, +) -> Result<(), MyError> { use ring::{rand, signature}; -// Create an `RsaKeyPair` from the DER-encoded bytes. This example uses -// a 2048-bit key, but larger keys are also supported. -let private_key_der = read_file(private_key_path)?; -let key_pair = signature::RsaKeyPair::from_pkcs8(&private_key_der) -.map_err(|_| MyError::BadPrivateKey)?; - -// Sign the message "hello, world", using PKCS#1 v1.5 padding and the -// SHA256 digest algorithm. -const MESSAGE: &'static [u8] = b"hello, world"; -let rng = rand::SystemRandom::new(); -let mut signature = vec![0; key_pair.public_modulus_len()]; -key_pair.sign(&signature::RSA_PKCS1_SHA256, &rng, MESSAGE, &mut signature) -.map_err(|_| MyError::OOM)?; - -// Verify the signature. -let public_key = -signature::UnparsedPublicKey::new(&signature::RSA_PKCS1_2048_8192_SHA256, - read_file(public_key_path)?); -public_key.verify(MESSAGE, &signature) -.map_err(|_| MyError::BadSignature) + // Create an `RsaKeyPair` from the DER-encoded bytes. This example uses + // a 2048-bit key, but larger keys are also supported. + let private_key_der = read_file(private_key_path)?; + let key_pair = + signature::RsaKeyPair::from_pkcs8(&private_key_der).map_err(|_| MyError::BadPrivateKey)?; + + // Sign the message "hello, world", using PKCS#1 v1.5 padding and the + // SHA256 digest algorithm. + const MESSAGE: &'static [u8] = b"hello, world"; + let rng = rand::SystemRandom::new(); + let mut signature = vec![0; key_pair.public_modulus_len()]; + key_pair + .sign(&signature::RSA_PKCS1_SHA256, &rng, MESSAGE, &mut signature) + .map_err(|_| MyError::OOM)?; + + // Verify the signature. + let public_key = signature::UnparsedPublicKey::new( + &signature::RSA_PKCS1_2048_8192_SHA256, + read_file(public_key_path)?, + ); + public_key + .verify(MESSAGE, &signature) + .map_err(|_| MyError::BadSignature) } #[derive(Debug)] enum MyError { -IO(std::io::Error), -BadPrivateKey, -OOM, -BadSignature, + IO(std::io::Error), + BadPrivateKey, + OOM, + BadSignature, } fn read_file(path: &std::path::Path) -> Result<Vec<u8>, MyError> { -use std::io::Read; + use std::io::Read; -let mut file = std::fs::File::open(path).map_err(|e| MyError::IO(e))?; -let mut contents: Vec<u8> = Vec::new(); -file.read_to_end(&mut contents).map_err(|e| MyError::IO(e))?; -Ok(contents) + let mut file = std::fs::File::open(path).map_err(|e| MyError::IO(e))?; + let mut contents: Vec<u8> = Vec::new(); + file.read_to_end(&mut contents) + .map_err(|e| MyError::IO(e))?; + Ok(contents) } -fn verify_ap_message(method: &str, path_and_query: &str, headers: BTreeMap<String,String>) { + +fn verify_ap_message(method: &str, path_and_query: &str, headers: BTreeMap<String, String>) { // TODO -- case insensitivity? // mastodon doesnt use created filed - let config = Config::default().set_expiration(Duration::seconds(3600)).dont_use_created_field(); - println!("{:?}", config); - println!("{:?}", headers); - let unverified = config.begin_verify(method, path_and_query, headers).unwrap(); + let config = Config::default() + .set_expiration(Duration::seconds(3600)) + .dont_use_created_field(); + let unverified = config + .begin_verify(method, path_and_query, headers) + .unwrap(); let res = unverified.verify(|signature, signing_string| { - println!("{:?}", signature); - println!("{:?}", signing_string); - let res: Value = reqwest::blocking::get(unverified.key_id()).unwrap().json().unwrap(); - let public_key: &[u8]= res.get("publicKey").unwrap().get("publicKeyPem").unwrap().as_str().unwrap().as_bytes(); + let res: Value = reqwest::blocking::get(unverified.key_id()) + .unwrap() + .json() + .unwrap(); + let public_key: &[u8] = res + .get("publicKey") + .unwrap() + .get("publicKeyPem") + .unwrap() + .as_str() + .unwrap() + .as_bytes(); // let public_key = &read_file(Path::new(&env::var("SIGNATURE_PUBKEY").unwrap())).unwrap(); let r = Rsa::public_key_from_pem(public_key).unwrap(); let public_key = r.public_key_to_der_pkcs1().unwrap(); let key = UnparsedPublicKey::new(&ring::signature::RSA_PKCS1_2048_8192_SHA256, &public_key); let hexdecode = base64::decode(signature).unwrap(); key.verify(signing_string.as_bytes(), &hexdecode).unwrap(); - println!("{:?}", hexdecode); - true}); - println!("{:?}", res); - // verify unverified + true + }); println!("{:?}", unverified); } @@ -337,14 +446,24 @@ mod tests { #[test] fn test_verify_rsa() { - sign_and_verify_rsa(Path::new(&env::var("SIGNATURE_PRIVKEY").unwrap()), Path::new(&env::var("SIGNATURE_PUBKEY").unwrap())).unwrap() + sign_and_verify_rsa( + Path::new(&env::var("SIGNATURE_PRIVKEY").unwrap()), + Path::new(&env::var("SIGNATURE_PUBKEY").unwrap()), + ) + .unwrap() } #[test] fn test_verify_ap_message() { let mut headers = BTreeMap::new(); - headers.insert("Content-Type".to_owned(), "application/activity+json".to_owned()); - headers.insert("date".to_owned(), "Fri, 08 May 2020 00:42:41 +0000".to_owned()); + headers.insert( + "Content-Type".to_owned(), + "application/activity+json".to_owned(), + ); + headers.insert( + "date".to_owned(), + "Fri, 08 May 2020 00:42:41 +0000".to_owned(), + ); let sample = "keyId=\"http://localhost:3030/actor#key\",algorithm=\"hs2019\",headers=\"(request-target) content-type date\",signature=\"YCJ7bwIX8y6rJ9Be31wm4ZkiBqper4vGydPHc/avBRE7D7SpIfWO+aA00VQcHlAGYjNRLEWiA5SkpszW3wnAs5JzuRWK01pELsEluYyE54/ou+rc06DxPt9beb9mIrbPs9EByN6epkYAGuKna8xoE7qsjhpfz5Q0SfNP3qS10uLaP5/puFCxMVgDIb3wMiJz1WiCzWZ26e5Wujoea8l5HS37V4xYhqicXmTvU1SzEiC+Qsn3RteWTesItAEDID5CFOhFizkSvgYVNjpTMwbLf1QiqyfgctVQIYt4fuQSTlcdKjhpS1cAxKTJg5hFQ9vjo1Qm1NP6XBALcRWpAIw5SA==\""; headers.insert("signature".to_owned(), sample.to_owned()); verify_ap_message("post", "/inbox", headers); @@ -359,7 +478,8 @@ mod tests { .header("date", Utc::now().to_rfc2822()) .json(&body) .header("Content-Type", "application/activity+json") - .http_sign_outgoing().unwrap(); + .http_sign_outgoing() + .unwrap(); } #[test] diff --git a/src/db/mod.rs b/src/db/mod.rs @@ -3,3 +3,4 @@ pub mod note; pub mod notification; pub mod schema; pub mod user; +pub mod server_mutuals; // TODO singular diff --git a/src/db/server_mutuals.rs b/src/db/server_mutuals.rs @@ -1,8 +1,17 @@ use diesel::sqlite::SqliteConnection; +use super::schema::server_mutuals; -#[derive(Queryable, Debug)] -struct ServerMutuals { +#[derive(Queryable, PartialEq, Debug)] +pub struct ServerMutual { id: i32, + accepted: bool, + followed_back: bool, inbox_url: String, - outbox_url: String, + outbox_url: String, // not implemented yet +} + +#[derive(Insertable)] +#[table_name="server_mutuals"] +pub struct NewServerMutual { + pub inbox_url: String, } diff --git a/src/db/user.rs b/src/db/user.rs @@ -65,6 +65,7 @@ impl User { pub fn authenticate(conn: &SqliteConnection, user: &str, pass: &str) -> Option<Self> { use crate::db::schema::users::dsl::*; // TODO -- allow email login as well + debug!("Authenticating user {}", user); let user = match users.filter(username.eq(user)).first::<Self>(conn) { Ok(user) => user, Err(e) => { diff --git a/src/lib.rs b/src/lib.rs @@ -31,7 +31,7 @@ use hyper; use serde::Deserialize; use session::Session; -mod ap; +pub mod ap; mod db; pub mod routes; mod session; @@ -110,7 +110,7 @@ struct ServerInfoTemplate<'a> { users: Vec<User>, } -const PAGE_SIZE: i64 = 50; +const PAGE_SIZE: i64 = 50; struct Global<'a> { // variables used on all pages w header @@ -576,9 +576,10 @@ fn render_timeline( header.has_more = true; } render_template(&TimelineTemplate { - global: header, - notes: n, - })}, + global: header, + notes: n, + }) + } _ => render_template(&ErrorTemplate { global: header, error_message: "Could not fetch notes", @@ -620,7 +621,7 @@ fn server_info_page(auth_user: Option<User>) -> impl Reply { let users = get_users().unwrap(); render_template(&ServerInfoTemplate { global: Global::create(auth_user, "/server"), - users: users + users: users, }) } @@ -701,10 +702,18 @@ pub fn get_outbox() {} // pub fn user_following(user_name: String) {} -pub fn post_inbox(message: Value) -> impl Reply { +pub async fn post_inbox(message: Value) -> Result<impl Reply, Infallible> { // TODO check if it is a create note message - let conn = &POOL.get().unwrap(); - ap::process_create_note(conn, message).unwrap(); + // + // ap::verify_ap_message("POST","/index") + 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(), + _ => () + } // thtas it! Ok("ok!") } @@ -723,7 +732,11 @@ fn edit_user(user: Option<User>, user_name: String, f: EditForm) -> impl Reply { if u.username == user_name || u.admin { use db::schema::users::dsl::*; diesel::update(users.find(u.id)) - .set((bio.eq(&f.bio), email.eq(&f.email), show_email.eq(&f.show_email.is_some()))) + .set(( + bio.eq(&f.bio), + email.eq(&f.email), + show_email.eq(&f.show_email.is_some()), + )) .execute(conn) .unwrap(); } diff --git a/src/main.rs b/src/main.rs @@ -1,5 +1,6 @@ use clap::{App, Arg, SubCommand}; use gourami_social::routes::run_server; +use gourami_social::ap; #[tokio::main] async fn main() { @@ -8,12 +9,23 @@ async fn main() { .author("Alex Wennerberg <alex@alexwennerberg.com>") .about("Gourami server and admin tools") .subcommand(App::new("run").about("Run server")) - .subcommand(App::new("admin").about("Admin Tools")) + .subcommand(App::new("admin").about("Admin Tools") + .subcommand(App::new("follow") + .arg(Arg::with_name("URL") + .help("url of the remote server to follow") + .required(true) + .index(1) + ) + ) + ) .get_matches(); if let Some(m) = matches.subcommand_matches("run") { run_server().await; } else if let Some(m) = matches.subcommand_matches("admin") { - // write admin commands here + if let Some(m) = m.subcommand_matches("follow") { + let url = m.value_of("URL").unwrap(); + ap::follow_remote_server(url).await.unwrap(); + } // reset password // follow remote } diff --git a/src/routes.rs b/src/routes.rs @@ -1,7 +1,7 @@ use crate::session; use crate::*; use env_logger; -use warp::{reply, body::form, body::json, filters::cookie, filters::query::query, path}; +use warp::{body::form, body::json, filters::cookie, filters::query::query, path, reply}; // I had trouble decoupling routes from server -- couldnt figure out the return type pub async fn run_server() { @@ -89,9 +89,9 @@ pub async fn run_server() { // TODO -- setup proper replies let server_actor = path!("actor").map(|| reply::json(&ap::server_actor_json())); - let post_server_inbox = path!("inbox").and(json()).map(post_inbox); - - let post_server_inbox = path!("inbox").and(json()).map(post_inbox); + let post_server_inbox = path!("inbox") + .and(json()) + .and_then(post_inbox); let get_server_outbox = path!("outbox").map(get_outbox); @@ -126,9 +126,9 @@ pub async fn run_server() { .or(warp::post() .and(warp::body::content_length_limit(1024 * 32)) .and(forms)) - // .or(warp::post() - // .and(warp::body::content_length_limit(1024 * 64)) - // .and(api_post)) + .or(warp::post() + .and(warp::body::content_length_limit(1024 * 64)) + .and(api_post)) .or(static_files) .with(warp::log("server")) .recover(handle_rejection) diff --git a/templates/server_info.html b/templates/server_info.html @@ -2,6 +2,7 @@ {% block content %} <div class="text-block padded"> Welcome to Gourami, an intentionally small, private, minimalist, invite-only social network. See the <a href="https://github.com/alexwennerberg/gourami/blob/master/docs/USER_GUIDE.md">user guide</a> for more info or look at the <a href="https://github.com/alexwennerberg/gourami">project page</a>! Here is a list of users on this server -- maybe introduce yourself to someone you don't know! By default your email is not shared, but you can share it to other users on this instance on your edit user page. + <br> <table> <tr> @@ -17,3 +18,5 @@ </div> </table> {% endblock %} + +The neighborhood is a collection of users or servers that our server is connected to. This is a two-way share. Here's a list of servers we are connected with: ...