From 9a38fb25bf8402282e2cf89e0afa80825695fe36 Mon Sep 17 00:00:00 2001 From: silverpill Date: Sat, 16 Jul 2022 01:49:27 +0000 Subject: [PATCH] Move AP ID parsers to activitypub::identifiers module --- src/activitypub/fetcher/helpers.rs | 4 +- src/activitypub/handlers/accept_follow.rs | 8 +- src/activitypub/handlers/announce.rs | 5 +- src/activitypub/handlers/create_note.rs | 30 +++--- src/activitypub/handlers/follow.rs | 8 +- src/activitypub/handlers/like.rs | 5 +- src/activitypub/handlers/reject_follow.rs | 8 +- src/activitypub/handlers/undo_follow.rs | 7 +- src/activitypub/handlers/update_note.rs | 4 +- src/activitypub/identifiers.rs | 109 ++++++++++++++++++++++ src/activitypub/receiver.rs | 85 ----------------- 11 files changed, 156 insertions(+), 117 deletions(-) diff --git a/src/activitypub/fetcher/helpers.rs b/src/activitypub/fetcher/helpers.rs index 00d5621..6073068 100644 --- a/src/activitypub/fetcher/helpers.rs +++ b/src/activitypub/fetcher/helpers.rs @@ -9,7 +9,7 @@ use crate::activitypub::handlers::{ create_note::handle_note, update_person::update_remote_profile, }; -use crate::activitypub::receiver::parse_object_id; +use crate::activitypub::identifiers::parse_local_object_id; use crate::config::{Config, Instance}; use crate::errors::{DatabaseError, HttpError, ValidationError}; use crate::models::posts::queries::get_post_by_object_id; @@ -193,7 +193,7 @@ pub async fn import_post( loop { let object_id = match maybe_object_id_to_fetch { Some(object_id) => { - if parse_object_id(&instance.url(), &object_id).is_ok() { + if parse_local_object_id(&instance.url(), &object_id).is_ok() { // Object is a local post assert!(objects.len() > 0); break; diff --git a/src/activitypub/handlers/accept_follow.rs b/src/activitypub/handlers/accept_follow.rs index 279ef36..f0af9c0 100644 --- a/src/activitypub/handlers/accept_follow.rs +++ b/src/activitypub/handlers/accept_follow.rs @@ -2,7 +2,8 @@ use tokio_postgres::GenericClient; use crate::activitypub::{ activity::Activity, - receiver::{find_object_id, parse_object_id}, + identifiers::parse_local_object_id, + receiver::find_object_id, vocabulary::FOLLOW, }; use crate::config::Config; @@ -21,7 +22,10 @@ pub async fn handle_accept_follow( ) -> HandlerResult { let actor_profile = get_profile_by_actor_id(db_client, &activity.actor).await?; let object_id = find_object_id(&activity.object)?; - let follow_request_id = parse_object_id(&config.instance_url(), &object_id)?; + let follow_request_id = parse_local_object_id( + &config.instance_url(), + &object_id, + )?; let follow_request = get_follow_request_by_id(db_client, &follow_request_id).await?; if follow_request.target_id != actor_profile.id { return Err(ValidationError("actor is not a target").into()); diff --git a/src/activitypub/handlers/announce.rs b/src/activitypub/handlers/announce.rs index 1500a4c..d36b277 100644 --- a/src/activitypub/handlers/announce.rs +++ b/src/activitypub/handlers/announce.rs @@ -3,7 +3,8 @@ use tokio_postgres::GenericClient; use crate::activitypub::{ activity::Activity, fetcher::helpers::{get_or_import_profile_by_actor_id, import_post}, - receiver::{find_object_id, parse_object_id}, + identifiers::parse_local_object_id, + receiver::find_object_id, vocabulary::NOTE, }; use crate::config::Config; @@ -30,7 +31,7 @@ pub async fn handle_announce( &activity.actor, ).await?; let object_id = find_object_id(&activity.object)?; - let post_id = match parse_object_id(&config.instance_url(), &object_id) { + let post_id = match parse_local_object_id(&config.instance_url(), &object_id) { Ok(post_id) => post_id, Err(_) => { // Try to get remote post diff --git a/src/activitypub/handlers/create_note.rs b/src/activitypub/handlers/create_note.rs index 38880d6..44df487 100644 --- a/src/activitypub/handlers/create_note.rs +++ b/src/activitypub/handlers/create_note.rs @@ -4,21 +4,19 @@ use std::path::Path; use tokio_postgres::GenericClient; use uuid::Uuid; -use crate::activitypub::activity::{Attachment, Object}; -use crate::activitypub::constants::AP_PUBLIC; -use crate::activitypub::fetcher::fetchers::fetch_file; -use crate::activitypub::fetcher::helpers::{ - get_or_import_profile_by_actor_id, - import_profile_by_actor_address, - ImportError, +use crate::activitypub::{ + activity::{Attachment, Object}, + constants::AP_PUBLIC, + fetcher::fetchers::fetch_file, + fetcher::helpers::{ + get_or_import_profile_by_actor_id, + import_profile_by_actor_address, + ImportError, + }, + identifiers::{parse_local_actor_id, parse_local_object_id}, + receiver::{parse_array, parse_property_value}, + vocabulary::{DOCUMENT, HASHTAG, IMAGE, MENTION, NOTE}, }; -use crate::activitypub::receiver::{ - parse_actor_id, - parse_array, - parse_object_id, - parse_property_value, -}; -use crate::activitypub::vocabulary::{DOCUMENT, HASHTAG, IMAGE, MENTION, NOTE}; use crate::config::Instance; use crate::errors::{DatabaseError, ValidationError}; use crate::models::attachments::queries::create_attachment; @@ -163,7 +161,7 @@ pub async fn handle_note( } else if tag.tag_type == MENTION { // Try to find profile by actor ID. if let Some(href) = tag.href { - if let Ok(username) = parse_actor_id(&instance.url(), &href) { + if let Ok(username) = parse_local_actor_id(&instance.url(), &href) { let user = get_user_by_name(db_client, &username).await?; if !mentions.contains(&user.id) { mentions.push(user.id); @@ -245,7 +243,7 @@ pub async fn handle_note( }; let in_reply_to_id = match object.in_reply_to { Some(object_id) => { - match parse_object_id(&instance.url(), &object_id) { + match parse_local_object_id(&instance.url(), &object_id) { Ok(post_id) => { // Local post let post = get_post_by_id(db_client, &post_id).await?; diff --git a/src/activitypub/handlers/follow.rs b/src/activitypub/handlers/follow.rs index 6c67ca6..2d87093 100644 --- a/src/activitypub/handlers/follow.rs +++ b/src/activitypub/handlers/follow.rs @@ -4,7 +4,8 @@ use crate::activitypub::{ activity::Activity, builders::accept_follow::prepare_accept_follow, fetcher::helpers::{get_or_import_profile_by_actor_id, ImportError}, - receiver::{find_object_id, parse_actor_id}, + identifiers::parse_local_actor_id, + receiver::find_object_id, vocabulary::PERSON, }; use crate::config::Config; @@ -27,7 +28,10 @@ pub async fn handle_follow( let source_actor = source_profile.actor_json .ok_or(ImportError::LocalObject)?; let target_actor_id = find_object_id(&activity.object)?; - let target_username = parse_actor_id(&config.instance_url(), &target_actor_id)?; + let target_username = parse_local_actor_id( + &config.instance_url(), + &target_actor_id, + )?; let target_user = get_user_by_name(db_client, &target_username).await?; match follow(db_client, &source_profile.id, &target_user.profile.id).await { Ok(_) => (), diff --git a/src/activitypub/handlers/like.rs b/src/activitypub/handlers/like.rs index f6b4aaa..6557617 100644 --- a/src/activitypub/handlers/like.rs +++ b/src/activitypub/handlers/like.rs @@ -3,7 +3,8 @@ use tokio_postgres::GenericClient; use crate::activitypub::{ activity::Activity, fetcher::helpers::get_or_import_profile_by_actor_id, - receiver::{find_object_id, parse_object_id}, + identifiers::parse_local_object_id, + receiver::find_object_id, vocabulary::NOTE, }; use crate::config::Config; @@ -24,7 +25,7 @@ pub async fn handle_like( &activity.actor, ).await?; let object_id = find_object_id(&activity.object)?; - let post_id = match parse_object_id(&config.instance_url(), &object_id) { + let post_id = match parse_local_object_id(&config.instance_url(), &object_id) { Ok(post_id) => post_id, Err(_) => { let post = match get_post_by_object_id(db_client, &object_id).await { diff --git a/src/activitypub/handlers/reject_follow.rs b/src/activitypub/handlers/reject_follow.rs index 6cda8fb..1cc43a2 100644 --- a/src/activitypub/handlers/reject_follow.rs +++ b/src/activitypub/handlers/reject_follow.rs @@ -2,7 +2,8 @@ use tokio_postgres::GenericClient; use crate::activitypub::{ activity::Activity, - receiver::{find_object_id, parse_object_id}, + identifiers::parse_local_object_id, + receiver::find_object_id, vocabulary::FOLLOW, }; use crate::config::Config; @@ -21,7 +22,10 @@ pub async fn handle_reject_follow( ) -> HandlerResult { let actor_profile = get_profile_by_actor_id(db_client, &activity.actor).await?; let object_id = find_object_id(&activity.object)?; - let follow_request_id = parse_object_id(&config.instance_url(), &object_id)?; + let follow_request_id = parse_local_object_id( + &config.instance_url(), + &object_id, + )?; let follow_request = get_follow_request_by_id(db_client, &follow_request_id).await?; if follow_request.target_id != actor_profile.id { return Err(ValidationError("actor is not a target").into()); diff --git a/src/activitypub/handlers/undo_follow.rs b/src/activitypub/handlers/undo_follow.rs index e82e825..371f3d0 100644 --- a/src/activitypub/handlers/undo_follow.rs +++ b/src/activitypub/handlers/undo_follow.rs @@ -2,7 +2,7 @@ use tokio_postgres::GenericClient; use crate::activitypub::{ activity::{Activity, Object}, - receiver::parse_actor_id, + identifiers::parse_local_actor_id, vocabulary::FOLLOW, }; use crate::config::Config; @@ -24,7 +24,10 @@ pub async fn handle_undo_follow( let source_profile = get_profile_by_actor_id(db_client, &activity.actor).await?; let target_actor_id = object.object .ok_or(ValidationError("invalid object"))?; - let target_username = parse_actor_id(&config.instance_url(), &target_actor_id)?; + let target_username = parse_local_actor_id( + &config.instance_url(), + &target_actor_id, + )?; // acct equals username if profile is local let target_profile = get_profile_by_acct(db_client, &target_username).await?; match unfollow(db_client, &source_profile.id, &target_profile.id).await { diff --git a/src/activitypub/handlers/update_note.rs b/src/activitypub/handlers/update_note.rs index a182070..3b93336 100644 --- a/src/activitypub/handlers/update_note.rs +++ b/src/activitypub/handlers/update_note.rs @@ -2,7 +2,7 @@ use chrono::Utc; use tokio_postgres::GenericClient; use crate::activitypub::activity::Object; -use crate::activitypub::receiver::parse_object_id; +use crate::activitypub::identifiers::parse_local_object_id; use crate::activitypub::vocabulary::NOTE; use crate::errors::DatabaseError; use crate::models::posts::queries::{ @@ -18,7 +18,7 @@ pub async fn handle_update_note( instance_url: &str, object: Object, ) -> HandlerResult { - let post_id = match parse_object_id(instance_url, &object.id) { + let post_id = match parse_local_object_id(instance_url, &object.id) { Ok(post_id) => post_id, Err(_) => { let post = match get_post_by_object_id(db_client, &object.id).await { diff --git a/src/activitypub/identifiers.rs b/src/activitypub/identifiers.rs index 44467f1..6728132 100644 --- a/src/activitypub/identifiers.rs +++ b/src/activitypub/identifiers.rs @@ -1,5 +1,8 @@ +use regex::Regex; use uuid::Uuid; +use crate::errors::ValidationError; + pub enum LocalActorCollection { Inbox, Outbox, @@ -57,3 +60,109 @@ pub fn local_instance_actor_id(instance_url: &str) -> String { pub fn local_object_id(instance_url: &str, internal_object_id: &Uuid) -> String { format!("{}/objects/{}", instance_url, internal_object_id) } + +pub fn parse_local_actor_id( + instance_url: &str, + actor_id: &str, +) -> Result { + let url_regexp_str = format!( + "^{}/users/(?P[0-9a-z_]+)$", + instance_url.replace('.', r"\."), + ); + let url_regexp = Regex::new(&url_regexp_str) + .map_err(|_| ValidationError("error"))?; + let url_caps = url_regexp.captures(actor_id) + .ok_or(ValidationError("invalid actor ID"))?; + let username = url_caps.name("username") + .ok_or(ValidationError("invalid actor ID"))? + .as_str() + .to_owned(); + Ok(username) +} + +pub fn parse_local_object_id( + instance_url: &str, + object_id: &str, +) -> Result { + let url_regexp_str = format!( + "^{}/objects/(?P[0-9a-f-]+)$", + instance_url.replace('.', r"\."), + ); + let url_regexp = Regex::new(&url_regexp_str) + .map_err(|_| ValidationError("error"))?; + let url_caps = url_regexp.captures(object_id) + .ok_or(ValidationError("invalid object ID"))?; + let internal_object_id: Uuid = url_caps.name("uuid") + .ok_or(ValidationError("invalid object ID"))? + .as_str().parse() + .map_err(|_| ValidationError("invalid object ID"))?; + Ok(internal_object_id) +} + +#[cfg(test)] +mod tests { + use crate::utils::id::new_uuid; + use super::*; + + const INSTANCE_URL: &str = "https://example.org"; + + #[test] + fn test_parse_local_actor_id() { + let username = parse_local_actor_id( + INSTANCE_URL, + "https://example.org/users/test", + ).unwrap(); + assert_eq!(username, "test".to_string()); + } + + #[test] + fn test_parse_local_actor_id_wrong_path() { + let error = parse_local_actor_id( + INSTANCE_URL, + "https://example.org/user/test", + ).unwrap_err(); + assert_eq!(error.to_string(), "invalid actor ID"); + } + + #[test] + fn test_parse_local_actor_id_invalid_username() { + let error = parse_local_actor_id( + INSTANCE_URL, + "https://example.org/users/tes-t", + ).unwrap_err(); + assert_eq!(error.to_string(), "invalid actor ID"); + } + + #[test] + fn test_parse_local_actor_id_invalid_instance_url() { + let error = parse_local_actor_id( + INSTANCE_URL, + "https://example.gov/users/test", + ).unwrap_err(); + assert_eq!(error.to_string(), "invalid actor ID"); + } + + #[test] + fn test_parse_local_object_id() { + let expected_uuid = new_uuid(); + let object_id = format!( + "https://example.org/objects/{}", + expected_uuid, + ); + let internal_object_id = parse_local_object_id( + INSTANCE_URL, + &object_id, + ).unwrap(); + assert_eq!(internal_object_id, expected_uuid); + } + + #[test] + fn test_parse_local_object_id_invalid_uuid() { + let object_id = "https://example.org/objects/1234"; + let error = parse_local_object_id( + INSTANCE_URL, + object_id, + ).unwrap_err(); + assert_eq!(error.to_string(), "invalid object ID"); + } +} diff --git a/src/activitypub/receiver.rs b/src/activitypub/receiver.rs index 85ffc8a..aab3abc 100644 --- a/src/activitypub/receiver.rs +++ b/src/activitypub/receiver.rs @@ -1,9 +1,7 @@ use actix_web::HttpRequest; -use regex::Regex; use serde::de::DeserializeOwned; use serde_json::Value; use tokio_postgres::GenericClient; -use uuid::Uuid; use crate::config::Config; use crate::errors::{ConversionError, HttpError, ValidationError}; @@ -24,44 +22,6 @@ use super::handlers::{ }; use super::vocabulary::*; -pub fn parse_actor_id( - instance_url: &str, - actor_id: &str, -) -> Result { - let url_regexp_str = format!( - "^{}/users/(?P[0-9a-z_]+)$", - instance_url.replace('.', r"\."), - ); - let url_regexp = Regex::new(&url_regexp_str) - .map_err(|_| ValidationError("error"))?; - let url_caps = url_regexp.captures(actor_id) - .ok_or(ValidationError("invalid actor ID"))?; - let username = url_caps.name("username") - .ok_or(ValidationError("invalid actor ID"))? - .as_str() - .to_owned(); - Ok(username) -} - -pub fn parse_object_id( - instance_url: &str, - object_id: &str, -) -> Result { - let url_regexp_str = format!( - "^{}/objects/(?P[0-9a-f-]+)$", - instance_url.replace('.', r"\."), - ); - let url_regexp = Regex::new(&url_regexp_str) - .map_err(|_| ValidationError("error"))?; - let url_caps = url_regexp.captures(object_id) - .ok_or(ValidationError("invalid object ID"))?; - let internal_object_id: Uuid = url_caps.name("uuid") - .ok_or(ValidationError("invalid object ID"))? - .as_str().parse() - .map_err(|_| ValidationError("invalid object ID"))?; - Ok(internal_object_id) -} - /// Transforms arbitrary property value into array of strings pub fn parse_array(value: &Value) -> Result, ConversionError> { let result = match value { @@ -252,53 +212,8 @@ pub async fn receive_activity( #[cfg(test)] mod tests { use serde_json::json; - use crate::utils::id::new_uuid; use super::*; - const INSTANCE_URL: &str = "https://example.org"; - - #[test] - fn test_parse_actor_id() { - let username = parse_actor_id(INSTANCE_URL, "https://example.org/users/test").unwrap(); - assert_eq!(username, "test".to_string()); - } - - #[test] - fn test_parse_actor_id_wrong_path() { - let error = parse_actor_id(INSTANCE_URL, "https://example.org/user/test").unwrap_err(); - assert_eq!(error.to_string(), "invalid actor ID"); - } - - #[test] - fn test_parse_actor_id_invalid_username() { - let error = parse_actor_id(INSTANCE_URL, "https://example.org/users/tes-t").unwrap_err(); - assert_eq!(error.to_string(), "invalid actor ID"); - } - - #[test] - fn test_parse_actor_id_invalid_instance_url() { - let error = parse_actor_id(INSTANCE_URL, "https://example.gov/users/test").unwrap_err(); - assert_eq!(error.to_string(), "invalid actor ID"); - } - - #[test] - fn test_parse_object_id() { - let expected_uuid = new_uuid(); - let object_id = format!( - "https://example.org/objects/{}", - expected_uuid, - ); - let internal_object_id = parse_object_id(INSTANCE_URL, &object_id).unwrap(); - assert_eq!(internal_object_id, expected_uuid); - } - - #[test] - fn test_parse_object_id_invalid_uuid() { - let object_id = "https://example.org/objects/1234"; - let error = parse_object_id(INSTANCE_URL, object_id).unwrap_err(); - assert_eq!(error.to_string(), "invalid object ID"); - } - #[test] fn test_parse_array_with_string() { let value = json!("test");