2023-06-23 04:08:00 +03:00
|
|
|
package core
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"errors"
|
2023-07-23 04:35:28 +03:00
|
|
|
"sort"
|
2023-06-23 04:08:00 +03:00
|
|
|
"strconv"
|
2023-06-28 22:20:19 +03:00
|
|
|
"time"
|
2023-06-23 04:08:00 +03:00
|
|
|
|
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
|
|
|
)
|
|
|
|
|
|
2024-05-06 01:13:07 +03:00
|
|
|
var ErrCaptcha = errors.New("captcha detected")
|
|
|
|
|
var ErrSearchTimeout = errors.New("timeout. Cannot find element on page")
|
2023-06-25 20:41:47 +03:00
|
|
|
|
2023-06-23 04:08:00 +03:00
|
|
|
type SearchResult struct {
|
|
|
|
|
Rank int `json:"rank"`
|
|
|
|
|
URL string `json:"url"`
|
|
|
|
|
Title string `json:"title"`
|
|
|
|
|
Description string `json:"description"`
|
2024-05-06 01:13:07 +03:00
|
|
|
Ad bool `json:"ad"`
|
2023-06-23 04:08:00 +03:00
|
|
|
}
|
|
|
|
|
|
2025-04-10 16:59:31 +08:00
|
|
|
func DeduplicateResults(results []SearchResult) []SearchResult {
|
|
|
|
|
unique := make(map[string]bool)
|
|
|
|
|
var deduped []SearchResult
|
|
|
|
|
|
|
|
|
|
for _, result := range results {
|
|
|
|
|
if result.URL == "" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if !unique[result.URL] {
|
|
|
|
|
unique[result.URL] = true
|
|
|
|
|
deduped = append(deduped, result)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sort.Slice(deduped, func(i, j int) bool {
|
|
|
|
|
return deduped[i].Rank < deduped[j].Rank
|
|
|
|
|
})
|
|
|
|
|
return deduped
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-23 04:35:28 +03:00
|
|
|
func ConvertSearchResultsMap(searchResultsMap map[string]SearchResult) *[]SearchResult {
|
|
|
|
|
searchResults := []SearchResult{}
|
|
|
|
|
|
|
|
|
|
for _, v := range searchResultsMap {
|
|
|
|
|
searchResults = append(searchResults, v)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sort.Slice(searchResults, func(i, j int) bool {
|
|
|
|
|
return searchResults[i].Rank < searchResults[j].Rank
|
|
|
|
|
})
|
|
|
|
|
return &searchResults
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-23 04:08:00 +03:00
|
|
|
type Query struct {
|
2026-04-01 17:38:13 +03:00
|
|
|
Text string
|
|
|
|
|
LangCode string // eg. EN, ES, RU...
|
|
|
|
|
DateInterval string // format: YYYYMMDD..YYYMMDD - 20181010..20231010
|
|
|
|
|
Filetype string // File extension to search.
|
|
|
|
|
Site string // Search site
|
|
|
|
|
Limit int // Limit the number of results
|
|
|
|
|
Start int // Search offset for pagination (Google uses 0, 10, 20...)
|
|
|
|
|
Filter bool // Filter duplicates (google) (false: include similar, true: hide similar)
|
|
|
|
|
Answers bool // Include question and answers from SERP page to results with negative indexes
|
|
|
|
|
ProxyURL string // Proxy URL for raw requests
|
|
|
|
|
ProxyOverride string // Request-scoped proxy override: tag or direct
|
|
|
|
|
Insecure bool // Allow insecure TLS connections
|
2023-06-23 04:08:00 +03:00
|
|
|
}
|
|
|
|
|
|
2026-03-17 01:54:25 +03:00
|
|
|
func ComputePagination(start int, pageSize int) (int, int, error) {
|
|
|
|
|
if pageSize <= 0 {
|
|
|
|
|
return 0, 0, errors.New("pageSize must be > 0")
|
|
|
|
|
}
|
|
|
|
|
if start < 0 {
|
|
|
|
|
return 0, 0, errors.New("start must be >= 0")
|
|
|
|
|
}
|
|
|
|
|
return start / pageSize, start % pageSize, nil
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-23 04:08:00 +03:00
|
|
|
func (q Query) IsEmpty() bool {
|
|
|
|
|
if q.Site == "" && q.Filetype == "" && q.Text == "" {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-06 01:13:07 +03:00
|
|
|
func (searchQuery *Query) InitFromContext(reqCtx *fiber.Ctx) error {
|
|
|
|
|
searchQuery.Text = reqCtx.Query("text")
|
|
|
|
|
searchQuery.LangCode = reqCtx.Query("lang")
|
|
|
|
|
searchQuery.DateInterval = reqCtx.Query("date")
|
|
|
|
|
searchQuery.Filetype = reqCtx.Query("file")
|
|
|
|
|
searchQuery.Site = reqCtx.Query("site")
|
2023-06-23 04:08:00 +03:00
|
|
|
|
2024-05-06 01:13:07 +03:00
|
|
|
limit, err := strconv.Atoi(reqCtx.Query("limit", "25"))
|
2023-06-23 04:08:00 +03:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2024-05-06 01:13:07 +03:00
|
|
|
searchQuery.Limit = limit
|
2023-06-23 04:08:00 +03:00
|
|
|
|
2026-03-17 01:54:25 +03:00
|
|
|
start, err := strconv.Atoi(reqCtx.Query("start", "0"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if start < 0 {
|
|
|
|
|
return errors.New("start must be >= 0")
|
|
|
|
|
}
|
|
|
|
|
searchQuery.Start = start
|
|
|
|
|
|
|
|
|
|
searchQuery.Filter, err = strconv.ParseBool(reqCtx.Query("filter", "1"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-06 01:13:07 +03:00
|
|
|
searchQuery.Answers, err = strconv.ParseBool(reqCtx.Query("answers", "0"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
2023-06-23 04:08:00 +03:00
|
|
|
}
|
|
|
|
|
|
2026-04-01 17:38:13 +03:00
|
|
|
searchQuery.ProxyOverride, err = NormalizeProxyRequestOverride(reqCtx.Get("X-Use-Proxy"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-06 01:13:07 +03:00
|
|
|
if searchQuery.IsEmpty() {
|
|
|
|
|
return errors.New("Query cannot be empty")
|
|
|
|
|
}
|
2023-06-23 04:08:00 +03:00
|
|
|
return nil
|
|
|
|
|
}
|
2023-06-28 22:20:19 +03:00
|
|
|
|
|
|
|
|
type SearchEngineOptions struct {
|
2023-06-30 22:04:26 +03:00
|
|
|
RateRequests int `mapstructure:"rate_requests"`
|
|
|
|
|
RateTime int64 `mapstructure:"rate_seconds"`
|
|
|
|
|
RateBurst int `mapstructure:"rate_burst"`
|
|
|
|
|
SelectorTimeout int64 `mapstructure:"selector_timeout"` // CSS selector timeout in seconds
|
2024-05-06 01:13:07 +03:00
|
|
|
IsSolveCaptcha bool `mapstructure:"captcha"`
|
2023-06-28 22:20:19 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (o *SearchEngineOptions) Init() {
|
|
|
|
|
if o.RateRequests == 0 {
|
2023-06-30 22:04:26 +03:00
|
|
|
o.RateRequests = 6
|
2023-06-28 22:20:19 +03:00
|
|
|
}
|
|
|
|
|
if o.RateTime == 0 {
|
2023-06-30 22:04:26 +03:00
|
|
|
o.RateTime = 60
|
2023-06-28 22:20:19 +03:00
|
|
|
}
|
|
|
|
|
if o.RateBurst == 0 {
|
|
|
|
|
o.RateBurst = 1
|
|
|
|
|
}
|
|
|
|
|
if o.SelectorTimeout == 0 {
|
2023-06-30 22:04:26 +03:00
|
|
|
o.SelectorTimeout = 5
|
2023-06-28 22:20:19 +03:00
|
|
|
}
|
|
|
|
|
}
|
2023-06-30 22:04:26 +03:00
|
|
|
|
|
|
|
|
func (o *SearchEngineOptions) GetRatelimit() time.Duration {
|
|
|
|
|
return (time.Duration(o.RateTime) * time.Second) / time.Duration(o.RateRequests)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (o *SearchEngineOptions) GetSelectorTimeout() time.Duration {
|
|
|
|
|
return time.Duration(o.SelectorTimeout) * time.Second
|
|
|
|
|
}
|