diff --git a/docs/openapi.yaml b/docs/openapi.yaml index c8b7eec..851496d 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -887,6 +887,10 @@ paths: type: array items: $ref: '#/components/schemas/Status' + hashtags: + type: array + items: + $ref: '#/components/schemas/Tag' components: securitySchemes: diff --git a/src/mastodon_api/search/helpers.rs b/src/mastodon_api/search/helpers.rs index c921772..792221a 100644 --- a/src/mastodon_api/search/helpers.rs +++ b/src/mastodon_api/search/helpers.rs @@ -14,6 +14,7 @@ use crate::errors::{ValidationError, HttpError}; use crate::ethereum::identity::DidPkh; use crate::mastodon_api::accounts::types::Account; use crate::mastodon_api::statuses::helpers::build_status_list; +use crate::mastodon_api::statuses::types::Tag; use crate::models::posts::helpers::can_view_post; use crate::models::posts::types::Post; use crate::models::profiles::queries::{ @@ -22,12 +23,14 @@ use crate::models::profiles::queries::{ search_profile_by_wallet_address, }; use crate::models::profiles::types::DbActorProfile; +use crate::models::tags::queries::search_tags; use crate::models::users::types::User; use crate::utils::currencies::{validate_wallet_address, Currency}; use super::types::SearchResults; enum SearchQuery { ProfileQuery(String, Option), + TagQuery(String), Url(String), WalletAddress(String), Did(DidPkh), @@ -38,17 +41,28 @@ fn parse_profile_query(query: &str) -> Result<(String, Option), ValidationError> { // See also: USERNAME_RE in models::profiles::validators - let acct_regexp = Regex::new(r"^(@|!)?(?P[\w\.-]+)(@(?P[\w\.-]+))?$").unwrap(); - let acct_caps = acct_regexp.captures(query) + let acct_query_re = + Regex::new(r"^(@|!)?(?P[\w\.-]+)(@(?P[\w\.-]+))?$").unwrap(); + let acct_query_caps = acct_query_re.captures(query) .ok_or(ValidationError("invalid profile query"))?; - let username = acct_caps.name("user") + let username = acct_query_caps.name("user") .ok_or(ValidationError("invalid profile query"))? .as_str().to_string(); - let maybe_instance = acct_caps.name("instance") + let maybe_instance = acct_query_caps.name("instance") .map(|val| val.as_str().to_string()); Ok((username, maybe_instance)) } +fn parse_tag_query(query: &str) -> Result { + let tag_query_re = Regex::new(r"^#(?P\w+)$").unwrap(); + let tag_query_caps = tag_query_re.captures(query) + .ok_or(ValidationError("invalid tag query"))?; + let tag = tag_query_caps.name("tag") + .ok_or(ValidationError("invalid tag query"))? + .as_str().to_string(); + Ok(tag) +} + fn parse_search_query(search_query: &str) -> SearchQuery { let search_query = search_query.trim(); // DID is a valid URI so it should be tried before Url::parse @@ -65,14 +79,13 @@ fn parse_search_query(search_query: &str) -> SearchQuery { ).is_ok() { return SearchQuery::WalletAddress(search_query.to_string()); }; - match parse_profile_query(search_query) { - Ok((username, instance)) => { - SearchQuery::ProfileQuery(username, instance) - }, - Err(_) => { - SearchQuery::Unknown - }, - } + if let Ok(tag) = parse_tag_query(search_query) { + return SearchQuery::TagQuery(tag); + }; + if let Ok((username, maybe_instance)) = parse_profile_query(search_query) { + return SearchQuery::ProfileQuery(username, maybe_instance); + }; + SearchQuery::Unknown } async fn search_profiles_or_import( @@ -146,13 +159,21 @@ pub async fn search( ) -> Result { let mut profiles = vec![]; let mut posts = vec![]; + let mut tags = vec![]; match parse_search_query(search_query) { - SearchQuery::ProfileQuery(username, instance) => { + SearchQuery::ProfileQuery(username, maybe_instance) => { profiles = search_profiles_or_import( config, db_client, username, - instance, + maybe_instance, + limit, + ).await?; + }, + SearchQuery::TagQuery(tag) => { + tags = search_tags( + db_client, + &tag, limit, ).await?; }, @@ -192,7 +213,10 @@ pub async fn search( Some(current_user), posts, ).await?; - Ok(SearchResults { accounts, statuses }) + let hashtags = tags.into_iter() + .map(Tag::from_tag_name) + .collect(); + Ok(SearchResults { accounts, statuses, hashtags }) } pub async fn search_profiles_only( @@ -236,4 +260,12 @@ mod tests { assert_eq!(username, "group"); assert_eq!(maybe_instance.as_deref(), Some("example.com")); } + + #[test] + fn test_parse_tag_query() { + let query = "#Activity"; + let tag = parse_tag_query(query).unwrap(); + + assert_eq!(tag, "Activity"); + } } diff --git a/src/mastodon_api/search/types.rs b/src/mastodon_api/search/types.rs index fec7a3a..7c4830b 100644 --- a/src/mastodon_api/search/types.rs +++ b/src/mastodon_api/search/types.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use crate::mastodon_api::accounts::types::Account; -use crate::mastodon_api::statuses::types::Status; +use crate::mastodon_api::statuses::types::{Status, Tag}; fn default_limit() -> i64 { 20 } @@ -18,4 +18,5 @@ pub struct SearchQueryParams { pub struct SearchResults { pub accounts: Vec, pub statuses: Vec, + pub hashtags: Vec, } diff --git a/src/mastodon_api/statuses/types.rs b/src/mastodon_api/statuses/types.rs index fa311cb..f9ef0ff 100644 --- a/src/mastodon_api/statuses/types.rs +++ b/src/mastodon_api/statuses/types.rs @@ -38,7 +38,7 @@ pub struct Tag { } impl Tag { - fn from_tag_name(tag_name: String) -> Self { + pub fn from_tag_name(tag_name: String) -> Self { Tag { name: tag_name, // TODO: add link to tag page diff --git a/src/models/mod.rs b/src/models/mod.rs index 75fa897..8ec7ed6 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -9,4 +9,5 @@ pub mod profiles; pub mod reactions; pub mod relationships; pub mod subscriptions; +pub mod tags; pub mod users; diff --git a/src/models/tags/mod.rs b/src/models/tags/mod.rs new file mode 100644 index 0000000..84c032e --- /dev/null +++ b/src/models/tags/mod.rs @@ -0,0 +1 @@ +pub mod queries; diff --git a/src/models/tags/queries.rs b/src/models/tags/queries.rs new file mode 100644 index 0000000..61a4571 --- /dev/null +++ b/src/models/tags/queries.rs @@ -0,0 +1,24 @@ +use tokio_postgres::GenericClient; + +use crate::errors::DatabaseError; + +pub async fn search_tags( + db_client: &impl GenericClient, + search_query: &str, + limit: i64, +) -> Result, DatabaseError> { + let db_search_query = format!("%{}%", search_query); + let rows = db_client.query( + " + SELECT tag_name + FROM tag + WHERE tag_name ILIKE $1 + LIMIT $2 + ", + &[&db_search_query, &limit], + ).await?; + let tags: Vec = rows.iter() + .map(|row| row.try_get("tag_name")) + .collect::>()?; + Ok(tags) +}