website/pkg/controller/account/search.go
2024-10-19 13:07:17 -07:00

261 lines
7.6 KiB
Go

package account
import (
"fmt"
"net/http"
"strconv"
"code.nonshy.com/nonshy/website/pkg/config"
"code.nonshy.com/nonshy/website/pkg/controller/chat"
"code.nonshy.com/nonshy/website/pkg/geoip"
"code.nonshy.com/nonshy/website/pkg/log"
"code.nonshy.com/nonshy/website/pkg/models"
"code.nonshy.com/nonshy/website/pkg/session"
"code.nonshy.com/nonshy/website/pkg/spam"
"code.nonshy.com/nonshy/website/pkg/templates"
"code.nonshy.com/nonshy/website/pkg/worker"
)
// Search controller.
func Search() http.HandlerFunc {
tmpl := templates.Must("account/search.html")
// Whitelist for ordering options.
var sortWhitelist = []string{
"last_login_at desc",
"created_at desc",
"certified_at desc",
"username",
"username desc",
"lower(name)",
"lower(name) desc",
"distance",
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Search filters.
var (
isCertified = r.FormValue("certified")
username = r.FormValue("name") // username search
searchTerm = r.FormValue("search") // profile text search
citySearch = r.FormValue("wcs")
gender = r.FormValue("gender")
orientation = r.FormValue("orientation")
maritalStatus = r.FormValue("marital_status")
hereFor = r.FormValue("here_for")
friendSearch = r.FormValue("friends") == "true"
likedSearch = r.FormValue("liked") == "true"
onChatSearch = r.FormValue("on_chat") == "true"
sort = r.FormValue("sort")
sortOK bool
)
ageMin, err1 := strconv.Atoi(r.FormValue("age_min"))
ageMax, err2 := strconv.Atoi(r.FormValue("age_max"))
if ageMin > ageMax && err1 == nil && err2 == nil {
ageMin, ageMax = ageMax, ageMin
}
rawSearch := models.ParseSearchString(searchTerm)
search, restricted := spam.RestrictSearchTerms(rawSearch)
// Get current user.
currentUser, err := session.CurrentUser(r)
if err != nil {
session.FlashError(w, r, "Couldn't get current user!")
templates.Redirect(w, "/")
return
}
// Report when search terms are restricted.
if restricted != nil {
// Admin users: allow the search anyway.
if currentUser.IsAdmin {
search = rawSearch
} else {
fb := &models.Feedback{
Intent: "report",
Subject: "Search Keyword Blacklist",
UserID: currentUser.ID,
TableName: "users",
TableID: currentUser.ID,
Message: fmt.Sprintf(
"A user has run a search on the Member Directory using search terms which are prohibited.\n\n"+
"Their search query was: %s",
searchTerm,
),
}
// Save the feedback.
if err := models.CreateFeedback(fb); err != nil {
log.Error("Couldn't save feedback from user updating their DOB: %s", err)
}
}
}
// Geolocation/Who's Nearby: if the current user uses GeoIP, update
// their coordinates now.
myLocation, err := models.RefreshGeoIP(currentUser.ID, r)
if err != nil {
log.Error("RefreshGeoIP: %s", err)
}
// Are they doing a Location search (from world city typeahead)?
var city *models.WorldCities
if citySearch != "" {
sort = "distance"
// Require the current user to have THEIR location set, for fairness.
if myLocation.Source == models.LocationSourceNone {
session.FlashError(w, r, "You must set your own location before you can search for others by their location.")
} else {
// Look up the coordinates of their search.
city, err = models.FindWorldCity(citySearch)
if err != nil {
session.FlashError(w, r, "Location search: no match was found for '%s', please use one of the exact search results from the type-ahead on the Location field.", citySearch)
citySearch = "" // null out their search
}
}
}
// Sort options.
for _, v := range sortWhitelist {
if sort == v {
sortOK = true
break
}
}
if !sortOK {
sort = "last_login_at desc"
}
// Real name for certified_at
if sort == "certified_at desc" {
sort = "certification_photos.updated_at desc"
}
// Default
if isCertified == "" {
isCertified = "true"
}
// Always filter for certified-only users unless the request specifically looked for non-certified.
// Searches for disabled/banned users (admin only) should also reveal ALL users including non-certified.
var certifiedOnly = true
if isCertified == "false" || isCertified == "all" || isCertified == "disabled" || isCertified == "banned" {
certifiedOnly = false
}
// Non-admin view: always hide non-certified profiles, they can be unsafe (fake profiles, scams if they won't certify)
if !currentUser.IsAdmin {
certifiedOnly = true
}
// Are we filtering for "On Chat?"
var inUsername = []string{}
if onChatSearch {
stats := chat.FilteredChatStatistics(currentUser)
inUsername = stats.Usernames
if len(inUsername) == 0 {
session.FlashError(w, r, "Notice: you wanted to filter by people currently on the chat room, but nobody is on chat at this time.")
inUsername = []string{"@"}
}
}
pager := &models.Pagination{
PerPage: config.PageSizeMemberSearch,
Sort: sort,
}
pager.ParsePage(r)
users, err := models.SearchUsers(currentUser, &models.UserSearch{
Username: username,
InUsername: inUsername,
Gender: gender,
Orientation: orientation,
MaritalStatus: maritalStatus,
HereFor: hereFor,
ProfileText: search,
Certified: certifiedOnly,
NearCity: city,
NotCertified: isCertified == "false",
ShyAccounts: isCertified == "shy",
IsBanned: isCertified == "banned",
IsDisabled: isCertified == "disabled",
IsAdmin: isCertified == "admin",
Friends: friendSearch,
Liked: likedSearch,
AgeMin: ageMin,
AgeMax: ageMax,
}, pager)
if err != nil {
session.FlashError(w, r, "An error has occurred: %s.", err)
}
// Who's Nearby feature, get some data.
insights, _ := geoip.GetRequestInsights(r)
// Collect usernames to map to chat online status.
var usernames = []string{}
var userIDs = []uint64{}
for _, user := range users {
usernames = append(usernames, user.Username)
userIDs = append(userIDs, user.ID)
}
// User IDs of these I have "Liked"
likedIDs, err := models.LikedIDs(currentUser, "users", userIDs)
if err != nil {
log.Error("LikedIDs: %s", err)
}
var vars = map[string]interface{}{
"Users": users,
"Pager": pager,
"Enum": config.ProfileEnums,
// Search filter values.
"Certified": isCertified,
"Gender": gender,
"Orientation": orientation,
"MaritalStatus": maritalStatus,
"HereFor": hereFor,
"EmailOrUsername": username,
"Search": searchTerm,
"City": citySearch,
"AgeMin": ageMin,
"AgeMax": ageMax,
"FriendSearch": friendSearch,
"LikedSearch": likedSearch,
"OnChatSearch": onChatSearch,
"Sort": sort,
// Restricted Search errors.
"RestrictedSearchError": restricted,
// Photo counts mapped to users
"PhotoCountMap": models.MapPhotoCounts(users),
// Map Shy Account badges for these results
"ShyMap": models.MapShyAccounts(users),
// Map friendships and likes to these users.
"FriendMap": models.MapFriends(currentUser, users),
"LikedMap": models.MapLikes(currentUser, "users", likedIDs),
// Users on the chat room map.
"UserOnChatMap": worker.GetChatStatistics().MapUsersOnline(usernames),
// Current user's location setting.
"MyLocation": myLocation,
"GeoIPInsights": insights,
"DistanceMap": models.MapDistances(currentUser, city, users),
}
if err := tmpl.Execute(w, r, vars); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
})
}