189 lines
4.1 KiB
Go
189 lines
4.1 KiB
Go
package models
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
)
|
|
|
|
// Search represents a parsed search query with inclusions and exclusions.
|
|
type Search struct {
|
|
Includes []string
|
|
Excludes []string
|
|
}
|
|
|
|
// ParseSearchString parses a user search query and supports "quoted phrases" and -negations.
|
|
func ParseSearchString(input string) *Search {
|
|
var result = new(Search)
|
|
|
|
var (
|
|
negate bool
|
|
phrase bool
|
|
buf = []rune{}
|
|
commit = func() {
|
|
var text = strings.TrimSpace(string(buf))
|
|
if len(text) == 0 {
|
|
return
|
|
}
|
|
if negate {
|
|
result.Excludes = append(result.Excludes, text)
|
|
negate = false
|
|
} else {
|
|
result.Includes = append(result.Includes, text)
|
|
}
|
|
buf = []rune{}
|
|
}
|
|
)
|
|
|
|
for _, char := range input {
|
|
// Inside a quoted phrase?
|
|
if phrase {
|
|
if char == '"' {
|
|
// End of quoted phrase.
|
|
commit()
|
|
phrase = false
|
|
continue
|
|
}
|
|
buf = append(buf, char)
|
|
continue
|
|
}
|
|
|
|
// Start a quoted phrase?
|
|
if char == '"' {
|
|
phrase = true
|
|
continue
|
|
}
|
|
|
|
// Negation indicator?
|
|
if len(buf) == 0 && char == '-' {
|
|
negate = true
|
|
continue
|
|
}
|
|
|
|
// End of a word?
|
|
if char == ' ' {
|
|
commit()
|
|
continue
|
|
}
|
|
|
|
buf = append(buf, char)
|
|
}
|
|
|
|
// Last word?
|
|
commit()
|
|
|
|
return result
|
|
}
|
|
|
|
// ForumSearchFilters apply additional filters specific to searching the forum.
|
|
type ForumSearchFilters struct {
|
|
UserID uint64
|
|
ThreadsOnly bool
|
|
}
|
|
|
|
// SearchForum searches the forum.
|
|
func SearchForum(user *User, search *Search, filters ForumSearchFilters, pager *Pagination) ([]*Comment, error) {
|
|
var (
|
|
coms = []*Comment{}
|
|
query = (&Comment{}).Preload()
|
|
blockedUserIDs = BlockedUserIDs(user)
|
|
wheres = []string{"table_name = 'threads'"}
|
|
placeholders = []interface{}{}
|
|
)
|
|
|
|
// Hide explicit forum if user hasn't opted into it.
|
|
if !user.Explicit && !user.IsAdmin {
|
|
wheres = append(wheres, "forums.explicit = false")
|
|
}
|
|
|
|
// Circle membership.
|
|
if !user.IsInnerCircle() {
|
|
wheres = append(wheres, "forums.inner_circle is not true")
|
|
}
|
|
|
|
// Private forums.
|
|
if !user.IsAdmin {
|
|
wheres = append(wheres, "forums.private is not true")
|
|
}
|
|
|
|
// Blocked users?
|
|
if len(blockedUserIDs) > 0 {
|
|
wheres = append(wheres, "comments.user_id NOT IN ?")
|
|
placeholders = append(placeholders, blockedUserIDs)
|
|
}
|
|
|
|
// Don't show comments from banned or disabled accounts.
|
|
wheres = append(wheres, `
|
|
EXISTS (
|
|
SELECT 1
|
|
FROM users
|
|
WHERE users.id = comments.user_id
|
|
AND users.status = 'active'
|
|
)
|
|
`)
|
|
|
|
// Apply their search terms.
|
|
if filters.UserID > 0 {
|
|
wheres = append(wheres, "comments.user_id = ?")
|
|
placeholders = append(placeholders, filters.UserID)
|
|
}
|
|
if filters.ThreadsOnly {
|
|
wheres = append(wheres, "comments.id = threads.comment_id")
|
|
}
|
|
for _, term := range search.Includes {
|
|
var ilike = "%" + strings.ToLower(term) + "%"
|
|
wheres = append(wheres, "(comments.message ILIKE ? OR threads.title ILIKE ?)")
|
|
placeholders = append(placeholders, ilike, ilike)
|
|
}
|
|
for _, term := range search.Excludes {
|
|
var ilike = "%" + strings.ToLower(term) + "%"
|
|
wheres = append(wheres, "(comments.message NOT ILIKE ? AND threads.title NOT ILIKE ?)")
|
|
placeholders = append(placeholders, ilike, ilike)
|
|
}
|
|
|
|
query = query.Joins(
|
|
"JOIN threads ON (comments.table_id = threads.id)",
|
|
).Joins(
|
|
"JOIN forums ON (threads.forum_id = forums.id)",
|
|
).Where(
|
|
strings.Join(wheres, " AND "),
|
|
placeholders...,
|
|
).Order(pager.Sort)
|
|
|
|
query.Model(&Comment{}).Count(&pager.Total)
|
|
res := query.Offset(pager.GetOffset()).Limit(pager.PerPage).Find(&coms)
|
|
if res.Error != nil {
|
|
return nil, res.Error
|
|
}
|
|
|
|
// Inject user relationships into all comments now.
|
|
SetUserRelationshipsInComments(user, coms)
|
|
|
|
return coms, nil
|
|
}
|
|
|
|
// Equals inspects if the search result matches, to help with unit tests.
|
|
func (s Search) Equals(other Search) bool {
|
|
if len(s.Includes) != len(other.Includes) || len(s.Excludes) != len(other.Excludes) {
|
|
return false
|
|
}
|
|
|
|
for i, v := range s.Includes {
|
|
if other.Includes[i] != v {
|
|
return false
|
|
}
|
|
}
|
|
|
|
for i, v := range s.Excludes {
|
|
if other.Excludes[i] != v {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (s Search) String() string {
|
|
b, _ := json.Marshal(s)
|
|
return string(b)
|
|
}
|