Enable hashtag search
This commit is contained in:
parent
c488d5b5d4
commit
a8dae2a621
7 changed files with 80 additions and 17 deletions
|
@ -887,6 +887,10 @@ paths:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/Status'
|
$ref: '#/components/schemas/Status'
|
||||||
|
hashtags:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/Tag'
|
||||||
|
|
||||||
components:
|
components:
|
||||||
securitySchemes:
|
securitySchemes:
|
||||||
|
|
|
@ -14,6 +14,7 @@ use crate::errors::{ValidationError, HttpError};
|
||||||
use crate::ethereum::identity::DidPkh;
|
use crate::ethereum::identity::DidPkh;
|
||||||
use crate::mastodon_api::accounts::types::Account;
|
use crate::mastodon_api::accounts::types::Account;
|
||||||
use crate::mastodon_api::statuses::helpers::build_status_list;
|
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::helpers::can_view_post;
|
||||||
use crate::models::posts::types::Post;
|
use crate::models::posts::types::Post;
|
||||||
use crate::models::profiles::queries::{
|
use crate::models::profiles::queries::{
|
||||||
|
@ -22,12 +23,14 @@ use crate::models::profiles::queries::{
|
||||||
search_profile_by_wallet_address,
|
search_profile_by_wallet_address,
|
||||||
};
|
};
|
||||||
use crate::models::profiles::types::DbActorProfile;
|
use crate::models::profiles::types::DbActorProfile;
|
||||||
|
use crate::models::tags::queries::search_tags;
|
||||||
use crate::models::users::types::User;
|
use crate::models::users::types::User;
|
||||||
use crate::utils::currencies::{validate_wallet_address, Currency};
|
use crate::utils::currencies::{validate_wallet_address, Currency};
|
||||||
use super::types::SearchResults;
|
use super::types::SearchResults;
|
||||||
|
|
||||||
enum SearchQuery {
|
enum SearchQuery {
|
||||||
ProfileQuery(String, Option<String>),
|
ProfileQuery(String, Option<String>),
|
||||||
|
TagQuery(String),
|
||||||
Url(String),
|
Url(String),
|
||||||
WalletAddress(String),
|
WalletAddress(String),
|
||||||
Did(DidPkh),
|
Did(DidPkh),
|
||||||
|
@ -38,17 +41,28 @@ fn parse_profile_query(query: &str) ->
|
||||||
Result<(String, Option<String>), ValidationError>
|
Result<(String, Option<String>), ValidationError>
|
||||||
{
|
{
|
||||||
// See also: USERNAME_RE in models::profiles::validators
|
// See also: USERNAME_RE in models::profiles::validators
|
||||||
let acct_regexp = Regex::new(r"^(@|!)?(?P<user>[\w\.-]+)(@(?P<instance>[\w\.-]+))?$").unwrap();
|
let acct_query_re =
|
||||||
let acct_caps = acct_regexp.captures(query)
|
Regex::new(r"^(@|!)?(?P<user>[\w\.-]+)(@(?P<instance>[\w\.-]+))?$").unwrap();
|
||||||
|
let acct_query_caps = acct_query_re.captures(query)
|
||||||
.ok_or(ValidationError("invalid profile 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"))?
|
.ok_or(ValidationError("invalid profile query"))?
|
||||||
.as_str().to_string();
|
.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());
|
.map(|val| val.as_str().to_string());
|
||||||
Ok((username, maybe_instance))
|
Ok((username, maybe_instance))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_tag_query(query: &str) -> Result<String, ValidationError> {
|
||||||
|
let tag_query_re = Regex::new(r"^#(?P<tag>\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 {
|
fn parse_search_query(search_query: &str) -> SearchQuery {
|
||||||
let search_query = search_query.trim();
|
let search_query = search_query.trim();
|
||||||
// DID is a valid URI so it should be tried before Url::parse
|
// 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() {
|
).is_ok() {
|
||||||
return SearchQuery::WalletAddress(search_query.to_string());
|
return SearchQuery::WalletAddress(search_query.to_string());
|
||||||
};
|
};
|
||||||
match parse_profile_query(search_query) {
|
if let Ok(tag) = parse_tag_query(search_query) {
|
||||||
Ok((username, instance)) => {
|
return SearchQuery::TagQuery(tag);
|
||||||
SearchQuery::ProfileQuery(username, instance)
|
};
|
||||||
},
|
if let Ok((username, maybe_instance)) = parse_profile_query(search_query) {
|
||||||
Err(_) => {
|
return SearchQuery::ProfileQuery(username, maybe_instance);
|
||||||
SearchQuery::Unknown
|
};
|
||||||
},
|
SearchQuery::Unknown
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn search_profiles_or_import(
|
async fn search_profiles_or_import(
|
||||||
|
@ -146,13 +159,21 @@ pub async fn search(
|
||||||
) -> Result<SearchResults, HttpError> {
|
) -> Result<SearchResults, HttpError> {
|
||||||
let mut profiles = vec![];
|
let mut profiles = vec![];
|
||||||
let mut posts = vec![];
|
let mut posts = vec![];
|
||||||
|
let mut tags = vec![];
|
||||||
match parse_search_query(search_query) {
|
match parse_search_query(search_query) {
|
||||||
SearchQuery::ProfileQuery(username, instance) => {
|
SearchQuery::ProfileQuery(username, maybe_instance) => {
|
||||||
profiles = search_profiles_or_import(
|
profiles = search_profiles_or_import(
|
||||||
config,
|
config,
|
||||||
db_client,
|
db_client,
|
||||||
username,
|
username,
|
||||||
instance,
|
maybe_instance,
|
||||||
|
limit,
|
||||||
|
).await?;
|
||||||
|
},
|
||||||
|
SearchQuery::TagQuery(tag) => {
|
||||||
|
tags = search_tags(
|
||||||
|
db_client,
|
||||||
|
&tag,
|
||||||
limit,
|
limit,
|
||||||
).await?;
|
).await?;
|
||||||
},
|
},
|
||||||
|
@ -192,7 +213,10 @@ pub async fn search(
|
||||||
Some(current_user),
|
Some(current_user),
|
||||||
posts,
|
posts,
|
||||||
).await?;
|
).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(
|
pub async fn search_profiles_only(
|
||||||
|
@ -236,4 +260,12 @@ mod tests {
|
||||||
assert_eq!(username, "group");
|
assert_eq!(username, "group");
|
||||||
assert_eq!(maybe_instance.as_deref(), Some("example.com"));
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::mastodon_api::accounts::types::Account;
|
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 }
|
fn default_limit() -> i64 { 20 }
|
||||||
|
|
||||||
|
@ -18,4 +18,5 @@ pub struct SearchQueryParams {
|
||||||
pub struct SearchResults {
|
pub struct SearchResults {
|
||||||
pub accounts: Vec<Account>,
|
pub accounts: Vec<Account>,
|
||||||
pub statuses: Vec<Status>,
|
pub statuses: Vec<Status>,
|
||||||
|
pub hashtags: Vec<Tag>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,7 @@ pub struct Tag {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Tag {
|
impl Tag {
|
||||||
fn from_tag_name(tag_name: String) -> Self {
|
pub fn from_tag_name(tag_name: String) -> Self {
|
||||||
Tag {
|
Tag {
|
||||||
name: tag_name,
|
name: tag_name,
|
||||||
// TODO: add link to tag page
|
// TODO: add link to tag page
|
||||||
|
|
|
@ -9,4 +9,5 @@ pub mod profiles;
|
||||||
pub mod reactions;
|
pub mod reactions;
|
||||||
pub mod relationships;
|
pub mod relationships;
|
||||||
pub mod subscriptions;
|
pub mod subscriptions;
|
||||||
|
pub mod tags;
|
||||||
pub mod users;
|
pub mod users;
|
||||||
|
|
1
src/models/tags/mod.rs
Normal file
1
src/models/tags/mod.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub mod queries;
|
24
src/models/tags/queries.rs
Normal file
24
src/models/tags/queries.rs
Normal file
|
@ -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<Vec<String>, 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<String> = rows.iter()
|
||||||
|
.map(|row| row.try_get("tag_name"))
|
||||||
|
.collect::<Result<_, _>>()?;
|
||||||
|
Ok(tags)
|
||||||
|
}
|
Loading…
Reference in a new issue