gourami

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

commit f326dba07da5f0ba3f5274c65587d93d1d22be04
parent df7cf075fd81e1e101f0eb94e9a0fc1e9be80068
Author: alex wennerberg <alex@alexwennerberg.com>
Date:   Thu, 30 Apr 2020 16:37:26 -0500

Very WIP, sloppy activitypub implementation

Diffstat:
MCargo.lock | 20++++++++++++++++----
MCargo.toml | 4++--
MTODO | 3+++
Msrc/ap.rs | 49+++++++++++++++++++++++++------------------------
Msrc/db/note.rs | 4++--
Msrc/lib.rs | 49++++++++++++++++++++++++++++++++++++++++++++-----
Msrc/routes.rs | 19++++++++++---------
7 files changed, 102 insertions(+), 46 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -2,9 +2,9 @@ # It is not intended for manual editing. [[package]] name = "activitystreams" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae98a55a86fc3150f278b225644cd46b5359f4d75067eae6dc3a52b409c537fb" +checksum = "dd5b29a0f2c64cc56f2b79ec29cab68a9dab3b714d811a55668d072f18a8638e" dependencies = [ "activitystreams-derive", "chrono", @@ -17,9 +17,9 @@ dependencies = [ [[package]] name = "activitystreams-derive" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20d0384ae423a1df266f216e351ce9b40e8d369467d9242c086121154b4327dd" +checksum = "985d3ca1ee226e83f4118e0235bc11d9fce39c4eec8d53739a21b01dd0b3f30f" dependencies = [ "proc-macro2 1.0.10", "quote 1.0.3", @@ -1125,6 +1125,16 @@ dependencies = [ ] [[package]] +name = "num_cpus" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] name = "opaque-debug" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1608,6 +1618,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "serde", + "serde_json", "serde_urlencoded", "time 0.1.43", "tokio", @@ -2081,6 +2092,7 @@ dependencies = [ "lazy_static", "memchr", "mio", + "num_cpus", "pin-project-lite", "slab", "tokio-macros", diff --git a/Cargo.toml b/Cargo.toml @@ -5,7 +5,7 @@ authors = ["alex wennerberg <alex@alexwennerberg.com>"] edition = "2018" [dependencies] -activitystreams = "0.5.0" +activitystreams = "0.6.0" askama = "0.8.0" bcrypt = "0.7.0" cookie = "0.13.3" @@ -15,7 +15,7 @@ env_logger = "0.7.1" lazy_static = "1.4.0" log = "0.4.8" rand = "0.7.3" -reqwest = "0.10.4" +reqwest = {version="0.10.4",features=["json", "blocking"]} serde = { version = "1.0.106", features = ["derive"] } serde_json = "1.0.51" tokio = { version = "0.2.18", features = ["macros"] } diff --git a/TODO b/TODO @@ -1,5 +1,8 @@ CORE FEATURES +Change to GPL v3 license due to dependencies + + Proper threading - Render all DESCENDENTS (allow for replies to multiple items...) diff --git a/src/ap.rs b/src/ap.rs @@ -8,14 +8,15 @@ /// as I can make it! use serde_json::json; -use activitystreams::activity::{Accept, Activity, Announce, Create, Delete, Follow, Reject}; -use activitystreams::BaseBox; -use log::debug; -use serde_json::{Value, Error}; -use serde_json::from_str; +use serde_json::{Value}; use crate::db::note::{NoteInput, RemoteNoteInput}; +use warp::{Reply, Filter, Rejection}; +lazy_static! { + // const SERVER_ACTOR = "gourami.social" +} + enum Action { CreateNote, DoNothing, @@ -27,7 +28,7 @@ fn categorize_input_message(v: Value) -> Action { Action::DoNothing } -fn parse_create_note(v: Value) -> Result<RemoteNoteInput, Box<dyn std::error::Error>>{ +pub fn parse_create_note(v: Value) -> Result<RemoteNoteInput, 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 @@ -57,7 +58,24 @@ fn parse_create_note(v: Value) -> Result<RemoteNoteInput, Box<dyn std::error::Er /// Generate an AP create message from a new note -fn new_note_to_ap_message(note_input: NoteInput) { +pub fn new_note_to_ap_message(note_input: &NoteInput) -> Value{ + json!({ + "id": "someid", + "type": "Create", + "actor": "my_server/actor", // get from DEPLOY_URL + "published": "now", + "to": [ + "destination.server" + ], + "object": { + "id": "unique id", + "type": "note", + "url": "abc", + "inReplyTo": "none", + "attributedTo": "joe", + "content": "whats up", + } + }) } // /// used to send to others @@ -133,20 +151,3 @@ mod tests { }) } } - -pub fn post_inbox(message: Value) { - // TODO check if it is a create note message - parse_create_note(message); - // insert new note into database - // thtas it! -} - -pub fn get_outbox() {} - -pub fn post_outbox(message: Value) {} - -// TODO figure out how to follow mastodon -// -pub fn user_followers(user_name: String) {} - -pub fn user_following(user_name: String) {} diff --git a/src/db/note.rs b/src/db/note.rs @@ -46,10 +46,10 @@ fn remove_unnacceptable_html(input_text: &str) -> String { #[derive(Insertable, Clone, Debug)] #[table_name = "notes"] -pub struct NoteInput<'a> { +pub struct NoteInput { //pub id: i32, //unsigned? pub user_id: i32, - pub content: &'a str, + pub content: String, pub in_reply_to: Option<i32>, pub neighborhood: bool, } diff --git a/src/lib.rs b/src/lib.rs @@ -4,6 +4,7 @@ extern crate diesel; #[macro_use] extern crate lazy_static; #[macro_use] extern crate maplit; +use serde_json::{Value}; use std::convert::Infallible; use zxcvbn::zxcvbn; @@ -133,7 +134,25 @@ struct NewNoteRequest { neighborhood: Option<String>, // "on" } -fn new_note(auth_user: User, note_input: &str, neighborhood: bool,) -> Result<(), Box<dyn std::error::Error>> { +async fn handle_new_note_form(u: Option<User>, f: NewNoteRequest) -> Result<impl Reply, Rejection> { + match u { + Some(u) => { + let n = new_note(u, &f.note_input, f.neighborhood.is_some()).unwrap(); + send_note(&n).await.unwrap(); // TODO error handling + let red_url: http::Uri = f.redirect_url.parse().unwrap(); + Ok(redirect(red_url))}, + None => Ok(redirect(http::Uri::from_static("error")))} +} + +async fn send_note(note: &NoteInput) -> Result<(), reqwest::Error> { + let nj = ap::new_note_to_ap_message(note); + let client = reqwest::Client::new(); + client.post("http://localhost:3030/inbox.json") + .json(&nj) + .send().await?; + Ok(()) +} +pub fn new_note(auth_user: User, note_input: &str, neighborhood: bool,) -> Result<NoteInput, Box<dyn std::error::Error>> { use db::schema::notes::dsl as notes; // create activitypub activity object // TODO -- micropub? @@ -144,10 +163,10 @@ fn new_note(auth_user: User, note_input: &str, neighborhood: bool,) -> Result<() let new_note = NoteInput{ user_id: auth_user.id, in_reply_to: reply, - content: &parsed_note_text, + content: parsed_note_text, neighborhood: neighborhood }; - insert_into(notes::notes).values(new_note).execute(conn)?; + insert_into(notes::notes).values(&new_note).execute(conn)?; // notify person u reply to if let Some(r_id) = reply { use db::schema::notifications::dsl as notifs; @@ -181,12 +200,11 @@ fn new_note(auth_user: User, note_input: &str, neighborhood: bool,) -> Result<() insert_into(nv::notification_viewers).values(new_nv).execute(conn)?; } - // ap::generate_ap(ap::Activity::create_note); // generate activitypub object from post request // send to outbox // add notification // if request made from web form - Ok(()) + Ok(new_note) } // ActivityPub outbox @@ -540,6 +558,27 @@ fn render_user_edit_page(user: Option<User>, user_name: String) -> impl Reply { } +pub fn get_outbox() {} + +pub fn post_outbox(message: Value) {} + +// TODO figure out how to follow mastodon +// +pub fn user_followers(user_name: String) {} + +pub fn user_following(user_name: String) {} + +pub fn post_inbox(message: Value) -> impl Reply { + // TODO check if it is a create note message + use db::schema::notes::dsl::*; + let conn = &POOL.get().unwrap(); + let mynote = ap::parse_create_note(message).unwrap(); + insert_into(notes).values(&mynote).execute(conn).unwrap(); + // insert new note into database + // thtas it! + Ok("ok!") +} + #[derive(Deserialize)] struct EditForm { redirect_url: String, diff --git a/src/routes.rs b/src/routes.rs @@ -79,12 +79,7 @@ pub async fn run_server() { .and(session_filter()) .and(form()) // Verbose -- see if you can refactor - .map(|u: Option<User>, f: NewNoteRequest| match u { - Some(u) => { - new_note(u, &f.note_input, f.neighborhood.is_some()).unwrap(); - let red_url: http::Uri = f.redirect_url.parse().unwrap(); - redirect(red_url)}, - None => redirect(http::Uri::from_static("error"))}); + .and_then(handle_new_note_form); let delete_note = path("delete_note") .and(session_filter()) @@ -107,14 +102,14 @@ pub async fn run_server() { // TODO -- setup proper replies let post_server_inbox = path!("inbox.json" ) .and(json()) - .map(ap::post_inbox); + .map(post_inbox); let post_server_outbox = path!("outbox.json" ) .and(json()) - .map(ap::post_outbox); + .map(post_outbox); let get_server_outbox = path!("outbox.json" ) - .map(ap::get_outbox); + .map(get_outbox); // https://github.com/seanmonstar/warp/issues/42 -- how to set up diesel // TODO set content length limit @@ -124,6 +119,7 @@ pub async fn run_server() { // let api_filter = session::create_session_filter(&POOL.get()); let html_renders = home.or(login_page).or(register_page).or(user_page).or(note_page).or(server_info_page).or(notification_page).or(user_edit_page).or(neighborhood); let forms = do_register.or(do_login).or(do_logout).or(create_note).or(delete_note).or(edit_user); + let api_post = post_server_inbox; // let api // catch all for any other paths @@ -132,6 +128,11 @@ pub async fn run_server() { 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(static_files) .with(warp::log("server")) .recover(handle_rejection)