From 7cba618587db4a825f71e6d8f867fbc27ede491e Mon Sep 17 00:00:00 2001 From: Dessalines Date: Tue, 23 Apr 2019 15:05:50 -0700 Subject: [PATCH] Adding a search page - Fixes # 70 --- server/src/actions/comment_view.rs | 29 ++- server/src/actions/post_view.rs | 30 ++- server/src/lib.rs | 18 +- server/src/websocket_server/server.rs | 163 +++++++++++++++- ui/src/components/navbar.tsx | 2 +- ui/src/components/search.tsx | 259 ++++++++++++++++++++++++++ ui/src/index.tsx | 2 + ui/src/interfaces.ts | 21 ++- ui/src/services/WebSocketService.ts | 7 +- 9 files changed, 513 insertions(+), 18 deletions(-) create mode 100644 ui/src/components/search.tsx diff --git a/server/src/actions/comment_view.rs b/server/src/actions/comment_view.rs index 2e3ae058..85ddf587 100644 --- a/server/src/actions/comment_view.rs +++ b/server/src/actions/comment_view.rs @@ -3,7 +3,7 @@ use diesel::*; use diesel::result::Error; use diesel::dsl::*; use serde::{Deserialize, Serialize}; -use { SortType, limit_and_offset }; +use { SortType, limit_and_offset, fuzzy_search }; // The faked schema since diesel doesn't do views table! { @@ -60,6 +60,7 @@ impl CommentView { sort: &SortType, for_post_id: Option, for_creator_id: Option, + search_term: Option, my_user_id: Option, saved_only: bool, page: Option, @@ -86,6 +87,10 @@ impl CommentView { if let Some(for_post_id) = for_post_id { query = query.filter(post_id.eq(for_post_id)); }; + + if let Some(search_term) = search_term { + query = query.filter(content.ilike(fuzzy_search(&search_term))); + }; if saved_only { query = query.filter(saved.eq(true)); @@ -353,8 +358,26 @@ mod tests { saved: None, }; - let read_comment_views_no_user = CommentView::list(&conn, &SortType::New, Some(inserted_post.id), None, None, false, None, None).unwrap(); - let read_comment_views_with_user = CommentView::list(&conn, &SortType::New, Some(inserted_post.id), None, Some(inserted_user.id), false, None, None).unwrap(); + let read_comment_views_no_user = CommentView::list( + &conn, + &SortType::New, + Some(inserted_post.id), + None, + None, + None, + false, + None, + None).unwrap(); + let read_comment_views_with_user = CommentView::list( + &conn, + &SortType::New, + Some(inserted_post.id), + None, + None, + Some(inserted_user.id), + false, + None, + None).unwrap(); let like_removed = CommentLike::remove(&conn, &comment_like_form).unwrap(); let num_deleted = Comment::delete(&conn, inserted_comment.id).unwrap(); Post::delete(&conn, inserted_post.id).unwrap(); diff --git a/server/src/actions/post_view.rs b/server/src/actions/post_view.rs index ccad9317..e24b0ed2 100644 --- a/server/src/actions/post_view.rs +++ b/server/src/actions/post_view.rs @@ -3,7 +3,7 @@ use diesel::*; use diesel::result::Error; use diesel::dsl::*; use serde::{Deserialize, Serialize}; -use { SortType, limit_and_offset }; +use { SortType, limit_and_offset, fuzzy_search }; #[derive(EnumString,ToString,Debug, Serialize, Deserialize)] pub enum PostListingType { @@ -74,6 +74,7 @@ impl PostView { sort: &SortType, for_community_id: Option, for_creator_id: Option, + search_term: Option, my_user_id: Option, saved_only: bool, unread_only: bool, @@ -94,6 +95,10 @@ impl PostView { query = query.filter(creator_id.eq(for_creator_id)); }; + if let Some(search_term) = search_term { + query = query.filter(name.ilike(fuzzy_search(&search_term))); + }; + // TODO these are wrong, bc they'll only show saved for your logged in user, not theirs if saved_only { query = query.filter(saved.eq(true)); @@ -295,8 +300,27 @@ mod tests { }; - let read_post_listings_with_user = PostView::list(&conn, PostListingType::Community, &SortType::New, Some(inserted_community.id), None, Some(inserted_user.id), false, false, None, None).unwrap(); - let read_post_listings_no_user = PostView::list(&conn, PostListingType::Community, &SortType::New, Some(inserted_community.id), None, None, false, false, None, None).unwrap(); + let read_post_listings_with_user = PostView::list(&conn, + PostListingType::Community, + &SortType::New, Some(inserted_community.id), + None, + None, + Some(inserted_user.id), + false, + false, + None, + None).unwrap(); + let read_post_listings_no_user = PostView::list(&conn, + PostListingType::Community, + &SortType::New, + Some(inserted_community.id), + None, + None, + None, + false, + false, + None, + None).unwrap(); let read_post_listing_no_user = PostView::read(&conn, inserted_post.id, None).unwrap(); let read_post_listing_with_user = PostView::read(&conn, inserted_post.id, Some(inserted_user.id)).unwrap(); diff --git a/server/src/lib.rs b/server/src/lib.rs index 8347c23d..d8d7f152 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -97,6 +97,11 @@ pub enum SortType { Hot, New, TopDay, TopWeek, TopMonth, TopYear, TopAll } +#[derive(EnumString,ToString,Debug, Serialize, Deserialize)] +pub enum SearchType { + Both, Comments, Posts +} + pub fn to_datetime_utc(ndt: NaiveDateTime) -> DateTime { DateTime::::from_utc(ndt, Utc) } @@ -121,6 +126,11 @@ pub fn has_slurs(test: &str) -> bool { SLUR_REGEX.is_match(test) } +pub fn fuzzy_search(q: &str) -> String { + let replaced = q.replace(" ", "%"); + format!("%{}%", replaced) +} + pub fn limit_and_offset(page: Option, limit: Option) -> (i64, i64) { let page = page.unwrap_or(1); let limit = limit.unwrap_or(10); @@ -130,7 +140,7 @@ pub fn limit_and_offset(page: Option, limit: Option) -> (i64, i64) { #[cfg(test)] mod tests { - use {Settings, is_email_regex, remove_slurs, has_slurs}; + use {Settings, is_email_regex, remove_slurs, has_slurs, fuzzy_search}; #[test] fn test_api() { assert_eq!(Settings::get().api_endpoint(), "http://0.0.0.0/api/v1"); @@ -148,9 +158,15 @@ mod tests { assert!(has_slurs(&test)); assert!(!has_slurs(slur_free)); } + + #[test] fn test_fuzzy_search() { + let test = "This is a fuzzy search"; + assert_eq!(fuzzy_search(test), "%This%is%a%fuzzy%search%".to_string()); + } } + lazy_static! { static ref EMAIL_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$").unwrap(); static ref SLUR_REGEX: Regex = Regex::new(r"(fag(g|got|tard)?|maricos?|cock\s?sucker(s|ing)?|\bnig(\b|g?(a|er)?s?)\b|dindu(s?)|mudslime?s?|kikes?|mongoloids?|towel\s*heads?|\bspi(c|k)s?\b|\bchinks?|niglets?|beaners?|\bnips?\b|\bcoons?\b|jungle\s*bunn(y|ies?)|jigg?aboo?s?|\bpakis?\b|rag\s*heads?|gooks?|cunts?|bitch(es|ing|y)?|puss(y|ies?)|twats?|feminazis?|whor(es?|ing)|\bslut(s|t?y)?|\btrann?(y|ies?)|ladyboy(s?))").unwrap(); diff --git a/server/src/websocket_server/server.rs b/server/src/websocket_server/server.rs index c61756e1..d7b93416 100644 --- a/server/src/websocket_server/server.rs +++ b/server/src/websocket_server/server.rs @@ -12,7 +12,7 @@ use std::str::FromStr; use diesel::PgConnection; use failure::Error; -use {Crud, Joinable, Likeable, Followable, Bannable, Saveable, establish_connection, naive_now, naive_from_unix, SortType, has_slurs, remove_slurs}; +use {Crud, Joinable, Likeable, Followable, Bannable, Saveable, establish_connection, naive_now, naive_from_unix, SortType, SearchType, has_slurs, remove_slurs}; use actions::community::*; use actions::user::*; use actions::post::*; @@ -27,7 +27,7 @@ use actions::moderator::*; #[derive(EnumString,ToString,Debug)] pub enum UserOperation { - Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, SaveComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, SavePost, EditCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails, GetReplies, GetModlog, BanFromCommunity, AddModToCommunity, CreateSite, EditSite, GetSite, AddAdmin, BanUser + Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, SaveComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, SavePost, EditCommunity, FollowCommunity, GetFollowedCommunities, GetUserDetails, GetReplies, GetModlog, BanFromCommunity, AddModToCommunity, CreateSite, EditSite, GetSite, AddAdmin, BanUser, Search } #[derive(Fail, Debug)] @@ -458,6 +458,23 @@ pub struct GetRepliesResponse { replies: Vec, } +#[derive(Serialize, Deserialize)] +pub struct Search { + q: String, + type_: String, + community_id: Option, + sort: String, + page: Option, + limit: Option, +} + +#[derive(Serialize, Deserialize)] +pub struct SearchResponse { + op: String, + comments: Vec, + posts: Vec, +} + /// `ChatServer` manages chat rooms and responsible for coordinating chat /// session. implementation is super primitive pub struct ChatServer { @@ -500,6 +517,7 @@ impl ChatServer { Some(community_id), None, None, + None, false, false, None, @@ -703,6 +721,10 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result { + let search: Search = serde_json::from_str(data)?; + search.perform(chat, msg.id) + }, } } @@ -1106,7 +1128,7 @@ impl Perform for GetPost { chat.rooms.get_mut(&self.id).unwrap().insert(addr); - let comments = CommentView::list(&conn, &SortType::New, Some(self.id), None, user_id, false, None, Some(9999))?; + let comments = CommentView::list(&conn, &SortType::New, Some(self.id), None, None, user_id, false, None, Some(9999))?; let community = CommunityView::read(&conn, post_view.community_id, user_id)?; @@ -1537,7 +1559,17 @@ impl Perform for GetPosts { let type_ = PostListingType::from_str(&self.type_)?; let sort = SortType::from_str(&self.sort)?; - let posts = match PostView::list(&conn, type_, &sort, self.community_id, None, user_id, false, false, self.page, self.limit) { + let posts = match PostView::list(&conn, + type_, + &sort, + self.community_id, + None, + None, + user_id, + false, + false, + self.page, + self.limit) { Ok(posts) => posts, Err(_e) => { return Err(self.error("Couldn't get posts"))? @@ -2006,15 +2038,52 @@ impl Perform for GetUserDetails { let sort = SortType::from_str(&self.sort)?; let user_view = UserView::read(&conn, self.user_id)?; + // If its saved only, you don't care what creator it was let posts = if self.saved_only { - PostView::list(&conn, PostListingType::All, &sort, self.community_id, None, Some(self.user_id), self.saved_only, false, self.page, self.limit)? + PostView::list(&conn, + PostListingType::All, + &sort, + self.community_id, + None, + None, + Some(self.user_id), + self.saved_only, + false, + self.page, + self.limit)? } else { - PostView::list(&conn, PostListingType::All, &sort, self.community_id, Some(self.user_id), None, self.saved_only, false, self.page, self.limit)? + PostView::list(&conn, + PostListingType::All, + &sort, + self.community_id, + Some(self.user_id), + None, + None, + self.saved_only, + false, + self.page, + self.limit)? }; let comments = if self.saved_only { - CommentView::list(&conn, &sort, None, None, Some(self.user_id), self.saved_only, self.page, self.limit)? + CommentView::list(&conn, + &sort, + None, + None, + None, + Some(self.user_id), + self.saved_only, + self.page, + self.limit)? } else { - CommentView::list(&conn, &sort, None, Some(self.user_id), None, self.saved_only, self.page, self.limit)? + CommentView::list(&conn, + &sort, + None, + Some(self.user_id), + None, + None, + self.saved_only, + self.page, + self.limit)? }; let follows = CommunityFollowerView::for_user(&conn, self.user_id)?; @@ -2539,3 +2608,81 @@ impl Perform for BanUser { } } + +impl Perform for Search { + fn op_type(&self) -> UserOperation { + UserOperation::Search + } + + fn perform(&self, _chat: &mut ChatServer, _addr: usize) -> Result { + + let conn = establish_connection(); + + let sort = SortType::from_str(&self.sort)?; + let type_ = SearchType::from_str(&self.type_)?; + + let mut posts = Vec::new(); + let mut comments = Vec::new(); + + match type_ { + SearchType::Posts => { + posts = PostView::list(&conn, + PostListingType::All, + &sort, + self.community_id, + None, + Some(self.q.to_owned()), + None, + false, + false, + self.page, + self.limit)?; + }, + SearchType::Comments => { + comments = CommentView::list(&conn, + &sort, + None, + None, + Some(self.q.to_owned()), + None, + false, + self.page, + self.limit)?; + }, + SearchType::Both => { + posts = PostView::list(&conn, + PostListingType::All, + &sort, + self.community_id, + None, + Some(self.q.to_owned()), + None, + false, + false, + self.page, + self.limit)?; + comments = CommentView::list(&conn, + &sort, + None, + None, + Some(self.q.to_owned()), + None, + false, + self.page, + self.limit)?; + } + }; + + + // Return the jwt + Ok( + serde_json::to_string( + &SearchResponse { + op: self.op_type().to_string(), + comments: comments, + posts: posts, + } + )? + ) + } +} diff --git a/ui/src/components/navbar.tsx b/ui/src/components/navbar.tsx index 77ffa9af..fb8f5755 100644 --- a/ui/src/components/navbar.tsx +++ b/ui/src/components/navbar.tsx @@ -76,7 +76,7 @@ export class Navbar extends Component { Forums