gourami

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

commit e776463873d00ac1c3064e5a66cadf956b0cf94e
parent ba0c15f6ab5c855644153bdd2c9f86bae74abb07
Author: alex wennerberg <alex@alexwennerberg.com>
Date:   Sun, 17 May 2020 14:10:53 -0500

AP bug fixes

Diffstat:
Msrc/ap.rs | 63+++++++++++++++++++++++++++++++++++++++++++++++++++------------
Msrc/db/note.rs | 20++++++++++----------
Msrc/db/schema.rs | 1-
Msrc/lib.rs | 7+++++--
Msrc/routes.rs | 27++++++++++++++-------------
Mtemplates/base.html | 3++-
6 files changed, 82 insertions(+), 39 deletions(-)

diff --git a/src/ap.rs b/src/ap.rs @@ -92,6 +92,7 @@ pub struct ApNote { } use regex::Regex; +use crate::db::note; impl ApNote { fn get_remote_user_name(&self) -> Option<String> { @@ -101,6 +102,33 @@ impl ApNote { None => None, } } + // TODO write unit tests + pub fn transform_incoming_content(&self) -> Result<(String, Option<i32>), Error>{ + let attr_re = Regex::new(r"^(.+?)(💬)").unwrap(); + let note_re = Regex::new(r"\B(📝|>>)(\d+)").unwrap(); + let new_content = attr_re.replace(&note::remove_unacceptable_html(&self.content), "") + .replace(&format!("{}:", SERVER.domain), ""); + let mut new_content = note_re.replace(&new_content, "").into_owned(); + + // prepend the appropriate note if in_reply_to + let conn = &POOL.get()?; + use crate::db::schema::notes::dsl as n; + let re = Regex::new(r"^(.+?)(💬)").unwrap(); + let mut reply_id: Option<i32> = None; + println!("{:?}", self.in_reply_to); + if let Some(reply) = self.in_reply_to.clone() { + reply_id = n::notes.select(n::id) + .filter(n::remote_id.eq(reply)).first(conn).ok(); + println!("{:?}", reply_id); + if let Some(r) = reply_id { + new_content = format!("📝{} {}", r, new_content); + } + } + // render html and stuff + Ok((note::parse_note_text(&new_content), reply_id)) + } + + } #[derive(Debug, Deserialize, Serialize)] @@ -203,6 +231,7 @@ pub fn process_create_note(v: Value, domain: &str) -> Result<(), Error> { if !should_accept(&create_note.actor) { return Err(Error::MiscError("Not someone we are following".to_owned())); } + let (content, in_reply_to) = ap_note.transform_incoming_content()?; let remote_username = ap_note .get_remote_user_name() .unwrap_or(ap_note.attributed_to); // TODO -- prevent usernames iwth colons @@ -222,13 +251,12 @@ pub fn process_create_note(v: Value, domain: &str) -> Result<(), Error> { })?; let new_remote_note = RemoteNoteInput { - content: ap_note.content, - in_reply_to: None, // TODO + content: content, + in_reply_to: in_reply_to, // TODO neighborhood: true, is_remote: true, user_id: new_user_id, remote_id: ap_note.id, - remote_url: ap_note.url, }; insert_into(n::notes) .values(&new_remote_note) @@ -387,15 +415,23 @@ fn generate_server_follow(remote_actor: &str, my_inbox_url: &str) -> Result<Valu } /// 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 - // Do a bunch of db queries to get the info I need - // +pub fn new_note_to_ap_message(note: &Note, user: &User) -> Result<Value, Error> { // prepend the username to the content // strip it out on receipt // use a field separator + let conn = &POOL.get()?; let content = note.get_content_for_outgoing(&user.username); - json!({ + let reply = note::get_reply(&content); + use crate::db::schema::notes::dsl as n; + let in_reply_to_url = match reply { + Some(id) => { + let reply_url: Option<String> = n::notes.select(n::remote_id).filter(n::id.eq(id)).first(conn).unwrap(); // ?? + reply_url + }, + None => None + }; + println!("{:?}", note); + let res = json!({ "@context": "https://www.w3.org/ns/activitystreams", "id": generate_activity_id(), "type": "Create", @@ -405,14 +441,17 @@ pub fn new_note_to_ap_message(note: &Note, user: &User) -> Value { "https://www.w3.org/ns/activitystreams#Public" ], // todo audience "object": { - "id": note.get_url(), // TODO generate + "id": note.remote_id, "type": "Note", "summary": "", // unused - "url": note.get_url(), + "url": note.remote_id, "attributedTo": SERVER.global_id, - "content": content + "content": content, + "inReplyTo": in_reply_to_url } - }) + }); + println!("{:?}", res); + Ok(res) } // /// used to send to others diff --git a/src/db/note.rs b/src/db/note.rs @@ -24,15 +24,15 @@ pub struct Note { pub created_time: chrono::NaiveDateTime, pub neighborhood: bool, pub is_remote: bool, - pub remote_url: Option<String>, pub remote_id: Option<String>, } +pub fn get_url(note_id: i32) -> String { + // TODO move domain url function + format!("{}/note/{}", SERVER.global_id, note_id) +} + impl Note { - 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 @@ -40,6 +40,7 @@ impl Note { format!("{}:{}💬 {}", SERVER.domain, username, self.content) } + pub fn relative_timestamp(&self) -> String { // Maybe use some fancy library here let diff = Utc::now() @@ -71,7 +72,7 @@ impl Note { /// This is currently very aggressive -- maybe we could loosen it a bit /// We probably want to allow microformats and some accessibiltiy tags /// Dont allow a so we cant have sneaky urls -- I'll do all the url parsing on my end. -fn remove_unnacceptable_html(input_text: &str) -> String { +pub fn remove_unacceptable_html(input_text: &str) -> String { let ok_tags = hashset!["br", "p", "span"]; let html_clean = ammonia::Builder::default() .tags(ok_tags) @@ -98,7 +99,6 @@ pub struct RemoteNoteInput { pub in_reply_to: Option<i32>, pub neighborhood: bool, pub is_remote: bool, - pub remote_url: String, pub remote_id: String, } @@ -112,7 +112,7 @@ pub fn get_reply(note_text: &str) -> Option<i32> { } pub fn get_mentions(note_text: &str) -> Vec<String> { - let re = Regex::new(r"\B(@)(\w+)").unwrap(); + let re = Regex::new(r"\B(@)(\S+)").unwrap(); re.captures_iter(note_text) .map(|c| String::from(&c[2])) .collect() @@ -124,7 +124,7 @@ pub fn get_mentions(note_text: &str) -> Vec<String> { pub fn parse_note_text(text: &str) -> String { // There shouldn't be any html tags in the db, but // Let's strip it out just in case - let html_clean = remove_unnacceptable_html(text); + let html_clean = remove_unacceptable_html(text); if text.len() == 0 { return String::new(); } @@ -144,7 +144,7 @@ pub fn parse_note_text(text: &str) -> String { let notes_parsed = note_regex .replace_all(&urls_parsed, &replace_str as &str) .to_string(); - let person_regex = Regex::new(r"\B(@)(\w+)").unwrap(); + let person_regex = Regex::new(r"\B(@)(\S+)").unwrap(); let replace_str = "<a href=\"/user/$2\">$0</a>"; let people_parsed = person_regex .replace_all(&notes_parsed, &replace_str as &str) diff --git a/src/db/schema.rs b/src/db/schema.rs @@ -7,7 +7,6 @@ table! { created_time -> Timestamp, neighborhood -> Bool, is_remote -> Bool, - remote_url -> Nullable<Varchar>, remote_id -> Nullable<Varchar>, } } diff --git a/src/lib.rs b/src/lib.rs @@ -220,7 +220,7 @@ async fn handle_new_note_form( .into_iter() .map(|s| s.inbox_url) .collect(); - sender.send((nj, destinations)).ok(); + sender.send((nj.unwrap(), destinations)).ok(); } let red_url: http::Uri = f.redirect_url.parse().unwrap(); Ok(redirect(red_url)) @@ -251,10 +251,13 @@ pub fn new_note( content: parsed_note_text, neighborhood: neighborhood, }; - let inserted_note: Note = conn.transaction(|| { + let mut inserted_note: Note = conn.transaction(|| { insert_into(notes::notes).values(&new_note).execute(conn)?; notes::notes.order(notes::id.desc()).first(conn) })?; + // add note url + diesel::update(notes::notes.filter(notes::id.eq(inserted_note.id))).set(notes::remote_id.eq(note::get_url(inserted_note.id))).execute(conn)?; + inserted_note.remote_id = Some(note::get_url(inserted_note.id)); // notify person u reply to if mentions.len() > 0 { let message = format!( diff --git a/src/routes.rs b/src/routes.rs @@ -42,22 +42,23 @@ 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( + // TODO content type + // TODO get interop with mastodon working + .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 + .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")), + ) .map( |_| reply::json(&ap::server_actor_json()), // how do async work ); @@ -98,7 +99,7 @@ pub async fn run_server() { .and(path("notifications")) .map(render_notifications); - let server_info_page = session_filter().and(path("server")).map(server_info_page); + let server_info_page = session_filter().and(path("server_info")).map(server_info_page); // auth functions let register_page = path("register").and(query()).map(register_page); diff --git a/templates/base.html b/templates/base.html @@ -23,7 +23,8 @@ <div class="title">🐟{{global.title}}/<wbr>{{global.page_title}}</div> </div> <div class="navlinks"> - <a href="/">local</a> <a href="/neighborhood">neighborhood</a> <a href="/server">server</a>{% if global.logged_in %} + <a href="/">local</a> <a href="/neighborhood">neighborhood</a> <a + href="/server_info">server</a>{% if global.logged_in %} <a href="/notifications">n({{global.unread_notifications}})</a> <a href="/user/{{global.me.username}}">@{{global.me.username}}</a>{% else %} <a href="login">login</a>{% endif %} </div> </div>