Fix golint errors and gofmt

This commit is contained in:
Elara 2021-12-08 13:18:14 -08:00
parent c8bec472be
commit 254155d442
10 changed files with 144 additions and 21 deletions

View File

@ -44,6 +44,8 @@ func NewCalcCard(query, _ string) Card {
return &CalcCard{query: query, useFunc: true} return &CalcCard{query: query, useFunc: true}
} }
// Matches determines whether a query matches the requirements
// fot CalcCard and determines which function to run with what arguments
func (cc *CalcCard) Matches() bool { func (cc *CalcCard) Matches() bool {
// If query has solve prefix // If query has solve prefix
if strings.HasPrefix(cc.query, "solve") { if strings.HasPrefix(cc.query, "solve") {
@ -106,6 +108,7 @@ func (cc *CalcCard) Matches() bool {
return false return false
} }
// StripKey removes all key words related to CalcCard
func (cc *CalcCard) StripKey() string { func (cc *CalcCard) StripKey() string {
// Compile regex for words to be removed // Compile regex for words to be removed
trimRgx := regexp.MustCompile(`^(.*?)solve|integrate|integral(?: of)?|diff|differentiate|derivative(?: of)?|derive|calculate(.*)$`) trimRgx := regexp.MustCompile(`^(.*?)solve|integrate|integral(?: of)?|diff|differentiate|derivative(?: of)?|derive|calculate(.*)$`)
@ -113,6 +116,7 @@ func (cc *CalcCard) StripKey() string {
return trimRgx.ReplaceAllString(cc.query, "${1}${2}") return trimRgx.ReplaceAllString(cc.query, "${1}${2}")
} }
// Content returns the solveRenderScript with the given input
func (cc *CalcCard) Content() template.HTML { func (cc *CalcCard) Content() template.HTML {
var input string var input string
// If function is being used // If function is being used
@ -130,22 +134,29 @@ func (cc *CalcCard) Content() template.HTML {
)) ))
} }
// Footer returns empty string because CalcCard has no footer
func (cc *CalcCard) Footer() template.HTML { func (cc *CalcCard) Footer() template.HTML {
return "" return ""
} }
// Returned always returns true since CalcCard is a frontend
// card and it is thus impossible to determine whether
// the query gets an answer
func (cc *CalcCard) Returned() bool { func (cc *CalcCard) Returned() bool {
return true return true
} }
// RunQuery returns nil as CalcCard is a frontend card
func (cc *CalcCard) RunQuery() error { func (cc *CalcCard) RunQuery() error {
return nil return nil
} }
// Title for CalcCard is "Calculation"
func (cc *CalcCard) Title() string { func (cc *CalcCard) Title() string {
return "Calculation" return "Calculation"
} }
// Head returns the extra head tags required for CalcCard
func (cc *CalcCard) Head() template.HTML { func (cc *CalcCard) Head() template.HTML {
return calcExtraHead return calcExtraHead
} }

View File

@ -1,13 +1,10 @@
package cards package cards
import ( import (
"errors"
"html/template" "html/template"
"sort" "sort"
) )
var ErrCardRegistered = errors.New("card with that name has already been registered")
// cardRegistration represents a card that has been registered // cardRegistration represents a card that has been registered
type cardRegistration struct { type cardRegistration struct {
name string name string
@ -79,7 +76,7 @@ func GetCard(query, userAgent string) Card {
for _, cardReg := range cards { for _, cardReg := range cards {
// Create new card // Create new card
card := cardReg.newFn(query, userAgent) card := cardReg.newFn(query, userAgent)
// If card matches, return it // If card matches, return it
if card.Matches() { if card.Matches() {
return card return card
} }

View File

@ -39,7 +39,7 @@ type DDGInstAns struct {
AnswerType string AnswerType string
} }
// NewDDGCard isa NewCardFunc that creates a new DDGCard // NewDDGCard is a NewCardFunc that creates a new DDGCard
func NewDDGCard(query, userAgent string) Card { func NewDDGCard(query, userAgent string) Card {
return &DDGCard{ return &DDGCard{
query: url.QueryEscape(query), query: url.QueryEscape(query),
@ -68,7 +68,7 @@ func (ddg *DDGCard) RunQuery() error {
return err return err
} }
// Decode response into repsonse struct // Decode response into response struct
err = json.NewDecoder(res.Body).Decode(&ddg.resp) err = json.NewDecoder(res.Body).Decode(&ddg.resp)
if err != nil { if err != nil {
return err return err
@ -77,30 +77,38 @@ func (ddg *DDGCard) RunQuery() error {
return nil return nil
} }
// Returned checks if abstract is empty.
// If it is, query returned no result.
func (ddg *DDGCard) Returned() bool { func (ddg *DDGCard) Returned() bool {
// Value was returned if abstract is not empty // Value was returned if abstract is not empty
return ddg.resp.Abstract != "" return ddg.resp.Abstract != ""
} }
// Matches always returns true as there are no keys
func (ddg *DDGCard) Matches() bool { func (ddg *DDGCard) Matches() bool {
// Everything matches since there are no keys
return true return true
} }
// StripKey returns the query as there are no keys
func (ddg *DDGCard) StripKey() string { func (ddg *DDGCard) StripKey() string {
// No key to strip, so return query // No key to strip, so return query
return ddg.query return ddg.query
} }
// Title returns the instant answer heading
func (ddg *DDGCard) Title() string { func (ddg *DDGCard) Title() string {
return ddg.resp.Heading return ddg.resp.Heading
} }
// Content returns the instant answer abstract with
// DuckDuckGo attibution
func (ddg *DDGCard) Content() template.HTML { func (ddg *DDGCard) Content() template.HTML {
// Return abstract with attribution // Return abstract with attribution
return template.HTML(ddg.resp.Abstract + fmt.Sprintf(ddgAttribution, ddg.query)) return template.HTML(ddg.resp.Abstract + fmt.Sprintf(ddgAttribution, ddg.query))
} }
// Footer returns a footer containing URL and name of source
// gotten from instant answer API
func (ddg *DDGCard) Footer() template.HTML { func (ddg *DDGCard) Footer() template.HTML {
// Return footer with abstract url and source // Return footer with abstract url and source
return template.HTML(fmt.Sprintf( return template.HTML(fmt.Sprintf(
@ -110,6 +118,8 @@ func (ddg *DDGCard) Footer() template.HTML {
)) ))
} }
// Head returns an empty string as no extra tags are reuired
// for DDGCard
func (ddg *DDGCard) Head() template.HTML { func (ddg *DDGCard) Head() template.HTML {
return "" return ""
} }

View File

@ -166,16 +166,21 @@ func (mc *MetaweatherCard) RunQuery() error {
return nil return nil
} }
// Returned checks whether no location was found or response
// said not found.
func (mc *MetaweatherCard) Returned() bool { func (mc *MetaweatherCard) Returned() bool {
return len(mc.location) > 0 && mc.resp.Detail != "Not found." return len(mc.location) > 0 && mc.resp.Detail != "Not found."
} }
// Matches determines whether the query matches the keys for
// MetaweatherCard
func (mc *MetaweatherCard) Matches() bool { func (mc *MetaweatherCard) Matches() bool {
return strings.HasPrefix(mc.query, "weather in") || return strings.HasPrefix(mc.query, "weather in") ||
strings.HasPrefix(mc.query, "weather") || strings.HasPrefix(mc.query, "weather") ||
strings.HasSuffix(mc.query, "weather") strings.HasSuffix(mc.query, "weather")
} }
// StripKey removes keys related to MetaweatherCard
func (mc *MetaweatherCard) StripKey() string { func (mc *MetaweatherCard) StripKey() string {
query := strings.TrimPrefix(mc.query, "weather in") query := strings.TrimPrefix(mc.query, "weather in")
query = strings.TrimPrefix(query, "weather") query = strings.TrimPrefix(query, "weather")
@ -183,6 +188,7 @@ func (mc *MetaweatherCard) StripKey() string {
return strings.TrimSpace(query) return strings.TrimSpace(query)
} }
// Content returns metaweatherContent with all information added
func (mc *MetaweatherCard) Content() template.HTML { func (mc *MetaweatherCard) Content() template.HTML {
return template.HTML(fmt.Sprintf( return template.HTML(fmt.Sprintf(
metaweatherContent, metaweatherContent,
@ -213,15 +219,18 @@ func (mc *MetaweatherCard) Content() template.HTML {
)) ))
} }
// Title of MetaweatherCard is "Weather"
func (mc *MetaweatherCard) Title() string { func (mc *MetaweatherCard) Title() string {
return "Weather" return "Weather"
} }
// Footer returns a footer with a link to metaweather
func (mc *MetaweatherCard) Footer() template.HTML { func (mc *MetaweatherCard) Footer() template.HTML {
// Return footer with link to metaweather
return `<div class="card-footer"><a class="card-footer-item" href="https://www.metaweather.com/">Metaweather</a></div>` return `<div class="card-footer"><a class="card-footer-item" href="https://www.metaweather.com/">Metaweather</a></div>`
} }
// Head returns an empty string as no extra head tags
// are required for MetaweatherCard
func (mc *MetaweatherCard) Head() template.HTML { func (mc *MetaweatherCard) Head() template.HTML {
return "" return ""
} }

View File

@ -43,12 +43,14 @@ func NewPlotCard(query, _ string) Card {
return &PlotCard{query: query} return &PlotCard{query: query}
} }
// Matches checks if the query matches the rules for PlotCard
func (pc *PlotCard) Matches() bool { func (pc *PlotCard) Matches() bool {
return strings.HasPrefix(pc.query, "plot") || return strings.HasPrefix(pc.query, "plot") ||
strings.HasPrefix(pc.query, "graph") || strings.HasPrefix(pc.query, "graph") ||
strings.HasPrefix(pc.query, "draw") strings.HasPrefix(pc.query, "draw")
} }
// StripKey removes all keys related to PlotCard
func (pc *PlotCard) StripKey() string { func (pc *PlotCard) StripKey() string {
query := strings.TrimPrefix(pc.query, "plot") query := strings.TrimPrefix(pc.query, "plot")
query = strings.TrimPrefix(query, "graph") query = strings.TrimPrefix(query, "graph")
@ -56,6 +58,7 @@ func (pc *PlotCard) StripKey() string {
return strings.TrimSpace(query) return strings.TrimSpace(query)
} }
// Content returns plot script with given input
func (pc *PlotCard) Content() template.HTML { func (pc *PlotCard) Content() template.HTML {
return template.HTML(fmt.Sprintf( return template.HTML(fmt.Sprintf(
plotScript, plotScript,
@ -63,27 +66,28 @@ func (pc *PlotCard) Content() template.HTML {
)) ))
} }
// Since this card is frontend, this cannot be checked. // Returned will alwats return true because
// Therefore, it will always return true. // this card is frontend, and this cannot be checked.
func (pc *PlotCard) Returned() bool { func (pc *PlotCard) Returned() bool {
return true return true
} }
// Title generates a title formatted as "Plot (<eqation>)"
func (pc *PlotCard) Title() string { func (pc *PlotCard) Title() string {
return "Plot (" + pc.StripKey() + ")" return "Plot (" + pc.StripKey() + ")"
} }
// Head returns extra head tags for PlotCard
func (pc *PlotCard) Head() template.HTML { func (pc *PlotCard) Head() template.HTML {
return plotExtraHead return plotExtraHead
} }
// Footer returns an empty string as PlotCard has no footer
func (pc *PlotCard) Footer() template.HTML { func (pc *PlotCard) Footer() template.HTML {
return "" return ""
} }
// RunQuery returns nil as PlotCard is a frontend card
func (pc *PlotCard) RunQuery() error { func (pc *PlotCard) RunQuery() error {
return nil return nil
} }

View File

@ -66,6 +66,7 @@ type TmplContext struct {
Card cards.Card Card cards.Card
} }
// ErrTmplContext represents context passed to an error template
type ErrTmplContext struct { type ErrTmplContext struct {
BaseContext BaseContext
Error string Error string
@ -74,6 +75,7 @@ type ErrTmplContext struct {
Status int Status int
} }
// BaseContext is the common context between all templates
type BaseContext struct { type BaseContext struct {
LoadTime time.Duration LoadTime time.Duration
} }
@ -252,7 +254,7 @@ func main() {
// Join address and port from config // Join address and port from config
addr := net.JoinHostPort(viper.GetString("server.addr"), viper.GetString("server.port")) addr := net.JoinHostPort(viper.GetString("server.addr"), viper.GetString("server.port"))
// Log server starting with address // Log server starting with address
log.Info().Str("address", addr).Msg("Starting HTTP server") log.Info().Str("address", addr).Msg("Starting HTTP server")
// Start server // Start server

View File

@ -9,6 +9,7 @@ import (
var bingURL = urlMustParse("https://www.bing.com/search?count=10") var bingURL = urlMustParse("https://www.bing.com/search?count=10")
// Bing represents the Bing search engine
type Bing struct { type Bing struct {
keyword string keyword string
userAgent string userAgent string
@ -18,29 +19,37 @@ type Bing struct {
baseSel *goquery.Selection baseSel *goquery.Selection
} }
// SetKeyword sets the keyword for searching
func (b *Bing) SetKeyword(keyword string) { func (b *Bing) SetKeyword(keyword string) {
b.keyword = keyword b.keyword = keyword
} }
// SetPage sets the page number for searching
func (b *Bing) SetPage(page int) { func (b *Bing) SetPage(page int) {
b.first = page * 10 b.first = page * 10
} }
// SetUserAgent sets the user agent to use for the request
func (b *Bing) SetUserAgent(ua string) { func (b *Bing) SetUserAgent(ua string) {
b.userAgent = ua b.userAgent = ua
} }
// Init runs requests for Bing search engine
func (b *Bing) Init() error { func (b *Bing) Init() error {
// Copy URL so it can be changed
initURL := copyURL(bingURL) initURL := copyURL(bingURL)
query := initURL.Query() query := initURL.Query()
// Set query
query.Set("q", b.keyword) query.Set("q", b.keyword)
if b.first > 0 { if b.first > 0 {
query.Set("first", strconv.Itoa(b.first)) query.Set("first", strconv.Itoa(b.first))
} else { } else {
query.Set("first", "1") query.Set("first", "1")
} }
// Update URL query parameters
initURL.RawQuery = query.Encode() initURL.RawQuery = query.Encode()
// Create new request for modified URL
req, err := http.NewRequest( req, err := http.NewRequest(
http.MethodGet, http.MethodGet,
initURL.String(), initURL.String(),
@ -49,17 +58,21 @@ func (b *Bing) Init() error {
if err != nil { if err != nil {
return err return err
} }
// If no user agent, use default
if b.userAgent == "" { if b.userAgent == "" {
b.userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36" b.userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36"
} }
// Set request user agent
req.Header.Set("User-Agent", b.userAgent) req.Header.Set("User-Agent", b.userAgent)
// Perform request
res, err := http.DefaultClient.Do(req) res, err := http.DefaultClient.Do(req)
if err != nil { if err != nil {
return err return err
} }
defer res.Body.Close() defer res.Body.Close()
// Create new goquery document
doc, err := goquery.NewDocumentFromReader(res.Body) doc, err := goquery.NewDocumentFromReader(res.Body)
if err != nil { if err != nil {
return err return err
@ -70,6 +83,7 @@ func (b *Bing) Init() error {
return nil return nil
} }
// Each runs eachCb with the index of each search result
func (b *Bing) Each(eachCb func(int) error) error { func (b *Bing) Each(eachCb func(int) error) error {
for i := 0; i < b.baseSel.Length(); i++ { for i := 0; i < b.baseSel.Length(); i++ {
err := eachCb(i) err := eachCb(i)
@ -80,18 +94,22 @@ func (b *Bing) Each(eachCb func(int) error) error {
return nil return nil
} }
// Title returns the title of the search result corresponding to i
func (b *Bing) Title(i int) (string, error) { func (b *Bing) Title(i int) (string, error) {
return get(b.baseSel, i).ChildrenFiltered("h2").Children().First().Text(), nil return get(b.baseSel, i).ChildrenFiltered("h2").Children().First().Text(), nil
} }
// Link returns the link to the search result corresponding to i
func (b *Bing) Link(i int) (string, error) { func (b *Bing) Link(i int) (string, error) {
return get(b.baseSel, i).ChildrenFiltered("h2").Children().First().AttrOr("href", ""), nil return get(b.baseSel, i).ChildrenFiltered("h2").Children().First().AttrOr("href", ""), nil
} }
// Desc returns the description of the search result corresponding to i
func (b *Bing) Desc(i int) (string, error) { func (b *Bing) Desc(i int) (string, error) {
return get(b.baseSel, i).ChildrenFiltered(".b_caption").Children().Last().Text(), nil return get(b.baseSel, i).ChildrenFiltered(".b_caption").Children().Last().Text(), nil
} }
// Name returns "bing"
func (b *Bing) Name() string { func (b *Bing) Name() string {
return "bing" return "bing"
} }

View File

@ -12,6 +12,7 @@ var ddgURL = urlMustParse("https://html.duckduckgo.com/html")
const uddgPrefix = "//duckduckgo.com/l/?uddg=" const uddgPrefix = "//duckduckgo.com/l/?uddg="
// DDG represents the DuckDuckGo search engine
type DDG struct { type DDG struct {
keyword string keyword string
userAgent string userAgent string
@ -21,28 +22,37 @@ type DDG struct {
baseSel *goquery.Selection baseSel *goquery.Selection
} }
// SetKeyword sets the keyword for searching
func (d *DDG) SetKeyword(keyword string) { func (d *DDG) SetKeyword(keyword string) {
d.keyword = keyword d.keyword = keyword
} }
// SetPage sets the page number for searching
func (d *DDG) SetPage(page int) { func (d *DDG) SetPage(page int) {
d.page = page * 30 d.page = page * 30
} }
// SetUserAgent sets the user agent for the request
func (d *DDG) SetUserAgent(ua string) { func (d *DDG) SetUserAgent(ua string) {
d.userAgent = "Opera/9.80 (Windows NT 5.1; U; zh-tw) Presto/2.8.131 Version/11.10" //ua d.userAgent = ua
} }
// Init runs requests for the DuckDuckGo search engine
func (d *DDG) Init() error { func (d *DDG) Init() error {
// Copy URL so that it can be changed
initURL := copyURL(ddgURL) initURL := copyURL(ddgURL)
// Get query parameters
query := initURL.Query() query := initURL.Query()
// Set query
query.Set("q", d.keyword) query.Set("q", d.keyword)
if d.page > 0 { if d.page > 0 {
query.Set("s", strconv.Itoa(d.page)) query.Set("s", strconv.Itoa(d.page))
query.Set("dc", strconv.Itoa(d.page+1)) query.Set("dc", strconv.Itoa(d.page+1))
} }
// Update URL query
initURL.RawQuery = query.Encode() initURL.RawQuery = query.Encode()
// Create new request for modified URL
req, err := http.NewRequest( req, err := http.NewRequest(
http.MethodGet, http.MethodGet,
initURL.String(), initURL.String(),
@ -51,17 +61,21 @@ func (d *DDG) Init() error {
if err != nil { if err != nil {
return err return err
} }
// If user agent empty, use default
if d.userAgent == "" { if d.userAgent == "" {
d.userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36" d.userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36"
} }
// Set user agent of request
req.Header.Set("User-Agent", d.userAgent) req.Header.Set("User-Agent", d.userAgent)
// Perform request
res, err := http.DefaultClient.Do(req) res, err := http.DefaultClient.Do(req)
if err != nil { if err != nil {
return err return err
} }
defer res.Body.Close() defer res.Body.Close()
// Create goquery document from reader
doc, err := goquery.NewDocumentFromReader(res.Body) doc, err := goquery.NewDocumentFromReader(res.Body)
if err != nil { if err != nil {
return err return err
@ -72,6 +86,7 @@ func (d *DDG) Init() error {
return nil return nil
} }
// Each runs eachCb with the index of each search result
func (d *DDG) Each(eachCb func(int) error) error { func (d *DDG) Each(eachCb func(int) error) error {
for i := 0; i < d.baseSel.Length(); i++ { for i := 0; i < d.baseSel.Length(); i++ {
err := eachCb(i) err := eachCb(i)
@ -82,10 +97,12 @@ func (d *DDG) Each(eachCb func(int) error) error {
return nil return nil
} }
// Title returns the title of the search result corresponding to i
func (d *DDG) Title(i int) (string, error) { func (d *DDG) Title(i int) (string, error) {
return strings.TrimSpace(get(d.baseSel, i).Children().First().ChildrenFiltered("h2").Text()), nil return strings.TrimSpace(get(d.baseSel, i).Children().First().ChildrenFiltered("h2").Text()), nil
} }
// Link returns the link to the search result corresponding to i
func (d *DDG) Link(i int) (string, error) { func (d *DDG) Link(i int) (string, error) {
link := get(d.baseSel, i).Children().First().ChildrenFiltered("a").AttrOr("href", "") link := get(d.baseSel, i).Children().First().ChildrenFiltered("a").AttrOr("href", "")
if strings.HasPrefix(link, uddgPrefix) { if strings.HasPrefix(link, uddgPrefix) {
@ -94,10 +111,12 @@ func (d *DDG) Link(i int) (string, error) {
return link, nil return link, nil
} }
// Desc returns the description of the search result corresponding to i
func (d *DDG) Desc(i int) (string, error) { func (d *DDG) Desc(i int) (string, error) {
return get(d.baseSel, i).Children().First().ChildrenFiltered("a").Text(), nil return get(d.baseSel, i).Children().First().ChildrenFiltered("a").Text(), nil
} }
// Name returns "ddg"
func (d *DDG) Name() string { func (d *DDG) Name() string {
return "ddg" return "ddg"
} }

View File

@ -10,6 +10,7 @@ import (
var googleURL = urlMustParse("https://www.google.com/search") var googleURL = urlMustParse("https://www.google.com/search")
// Google represents the Google search engine
type Google struct { type Google struct {
keyword string keyword string
userAgent string userAgent string
@ -19,24 +20,35 @@ type Google struct {
baseSel *goquery.Selection baseSel *goquery.Selection
} }
// SetKeyword sets the keyword for searching
func (g *Google) SetKeyword(keyword string) { func (g *Google) SetKeyword(keyword string) {
g.keyword = keyword g.keyword = keyword
} }
// SetPage sets the page number for searching
func (g *Google) SetPage(page int) { func (g *Google) SetPage(page int) {
g.page = page * 10 g.page = page * 10
} }
// SetUserAgent sets the user agent for the request
func (g *Google) SetUserAgent(ua string) { func (g *Google) SetUserAgent(ua string) {
g.userAgent = ua g.userAgent = ua
} }
// Init runs requests for the Google search engine
func (g *Google) Init() error { func (g *Google) Init() error {
// Copy URL so that it can be changed
initURL := copyURL(googleURL) initURL := copyURL(googleURL)
// Get query parameters
query := initURL.Query() query := initURL.Query()
// Set query
query.Set("q", g.keyword) query.Set("q", g.keyword)
// Set starting result (page number)
query.Set("start", strconv.Itoa(g.page)) query.Set("start", strconv.Itoa(g.page))
// Update URL query
initURL.RawQuery = query.Encode() initURL.RawQuery = query.Encode()
// Create new request for modified URL
req, err := http.NewRequest( req, err := http.NewRequest(
http.MethodGet, http.MethodGet,
initURL.String(), initURL.String(),
@ -45,17 +57,21 @@ func (g *Google) Init() error {
if err != nil { if err != nil {
return err return err
} }
// If user agent empty, use default
if g.userAgent == "" { if g.userAgent == "" {
g.userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36" g.userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36"
} }
// Set user agent of request
req.Header.Set("User-Agent", g.userAgent) req.Header.Set("User-Agent", g.userAgent)
// Perform request
res, err := http.DefaultClient.Do(req) res, err := http.DefaultClient.Do(req)
if err != nil { if err != nil {
return err return err
} }
defer res.Body.Close() defer res.Body.Close()
// Create goquery document from reader
doc, err := goquery.NewDocumentFromReader(res.Body) doc, err := goquery.NewDocumentFromReader(res.Body)
if err != nil { if err != nil {
return err return err
@ -66,6 +82,7 @@ func (g *Google) Init() error {
return nil return nil
} }
// Each runs eachCb with the index of each search result
func (g *Google) Each(eachCb func(int) error) error { func (g *Google) Each(eachCb func(int) error) error {
for i := 0; i < g.baseSel.Length(); i++ { for i := 0; i < g.baseSel.Length(); i++ {
err := eachCb(i) err := eachCb(i)
@ -76,31 +93,38 @@ func (g *Google) Each(eachCb func(int) error) error {
return nil return nil
} }
// Title returns the title of the search result corresponding to i
func (g *Google) Title(i int) (string, error) { func (g *Google) Title(i int) (string, error) {
return get(g.baseSel, i).Text(), nil return get(g.baseSel, i).Text(), nil
} }
// Link returns the link to the search result corresponding to i
func (g *Google) Link(i int) (string, error) { func (g *Google) Link(i int) (string, error) {
return get(g.baseSel, i).Parent().AttrOr("href", ""), nil return get(g.baseSel, i).Parent().AttrOr("href", ""), nil
} }
// Desc returns the description of the search result corresponding to i
func (g *Google) Desc(i int) (string, error) { func (g *Google) Desc(i int) (string, error) {
return get(g.baseSel, i).Parent().Parent().Next().Text(), nil return get(g.baseSel, i).Parent().Parent().Next().Text(), nil
} }
// Name returns "google"
func (g *Google) Name() string { func (g *Google) Name() string {
return "google" return "google"
} }
// get gets an element and given index from given selection
func get(sel *goquery.Selection, i int) *goquery.Selection { func get(sel *goquery.Selection, i int) *goquery.Selection {
return sel.Slice(i, i+1) return sel.Slice(i, i+1)
} }
// Parse url ignoring error
func urlMustParse(urlStr string) *url.URL { func urlMustParse(urlStr string) *url.URL {
out, _ := url.Parse(urlStr) out, _ := url.Parse(urlStr)
return out return out
} }
// copyURL makes a copy of the url and returns it
func copyURL(orig *url.URL) *url.URL { func copyURL(orig *url.URL) *url.URL {
newURL := new(url.URL) newURL := new(url.URL)
*newURL = *orig *newURL = *orig

View File

@ -13,6 +13,7 @@ func init() {
http.DefaultClient.Timeout = 5 * time.Second http.DefaultClient.Timeout = 5 * time.Second
} }
// Result represents a search result
type Result struct { type Result struct {
Title string Title string
Link string Link string
@ -21,6 +22,7 @@ type Result struct {
Rank int Rank int
} }
// Engine represents a search engine for web results (not images, shopping, erc.)
type Engine interface { type Engine interface {
// Set search keyword for engine // Set search keyword for engine
SetKeyword(string) SetKeyword(string)
@ -51,74 +53,95 @@ type Engine interface {
Name() string Name() string
} }
// Options represents search options
type Options struct { type Options struct {
Keyword string Keyword string
UserAgent string UserAgent string
Page int Page int
} }
// Search searches the given engines concurrently and returns the results
func Search(opts Options, engines ...Engine) ([]*Result, error) { func Search(opts Options, engines ...Engine) ([]*Result, error) {
var outMtx sync.Mutex var outMtx sync.Mutex
var out []*Result var out []*Result
// Create new error group
wg := errgroup.Group{} wg := errgroup.Group{}
// For every engine
for index, engine := range engines { for index, engine := range engines {
// Copy index and engine (for goroutine)
curIndex, curEngine := index, engine curIndex, curEngine := index, engine
wg.Go(func() error { wg.Go(func() error {
// Set options
curEngine.SetKeyword(opts.Keyword) curEngine.SetKeyword(opts.Keyword)
curEngine.SetUserAgent(opts.UserAgent) curEngine.SetUserAgent(opts.UserAgent)
curEngine.SetPage(opts.Page) curEngine.SetPage(opts.Page)
// Attempt to init engine
if err := curEngine.Init(); err != nil { if err := curEngine.Init(); err != nil {
return err return err
} }
// For each result
err := curEngine.Each(func(i int) error { err := curEngine.Each(func(i int) error {
// Get result link
link, err := curEngine.Link(i) link, err := curEngine.Link(i)
if err != nil { if err != nil {
return err return err
} }
// Calculate result rank
rank := (curIndex * 100) + i rank := (curIndex * 100) + i
// Check if result exists
index, exists := linkExists(out, link) index, exists := linkExists(out, link)
// If result already exists
if exists { if exists {
// Add engine to the existing result
out[index].Engines = append(out[index].Engines, curEngine.Name()) out[index].Engines = append(out[index].Engines, curEngine.Name())
// If the rank is higher than the old one, update it
if rank < out[index].Rank { if rank < out[index].Rank {
out[index].Rank = rank out[index].Rank = rank
} }
return nil return nil
} }
// Get result title
title, err := curEngine.Title(i) title, err := curEngine.Title(i)
if err != nil { if err != nil {
return err return err
} }
// Get result description
desc, err := curEngine.Desc(i) desc, err := curEngine.Desc(i)
if err != nil { if err != nil {
return err return err
} }
// If title, link, or description empty, ignore
if title == "" || link == "" || desc == "" { if title == "" || link == "" || desc == "" {
return nil return nil
} }
// If length of description, truncate
if len(desc) > 500 { if len(desc) > 500 {
desc = desc[:500] + "..." desc = desc[:500] + "..."
} }
// Create result struct
result := &Result{ result := &Result{
Title: title, Title: title,
Link: link, Link: link,
Desc: desc, Desc: desc,
Rank: rank, Rank: rank,
Engines: []string{curEngine.Name()},
} }
result.Engines = append(result.Engines, curEngine.Name())
// Lock out mutex
outMtx.Lock() outMtx.Lock()
// Add result to slice
out = append(out, result) out = append(out, result)
// Unlock out mutex
outMtx.Unlock() outMtx.Unlock()
return nil return nil
@ -127,6 +150,7 @@ func Search(opts Options, engines ...Engine) ([]*Result, error) {
return err return err
} }
// Sort slice by rank
sort.Slice(out, func(i, j int) bool { sort.Slice(out, func(i, j int) bool {
return out[i].Rank < out[j].Rank return out[i].Rank < out[j].Rank
}) })
@ -134,6 +158,7 @@ func Search(opts Options, engines ...Engine) ([]*Result, error) {
}) })
} }
// Wait for error group
if err := wg.Wait(); err != nil { if err := wg.Wait(); err != nil {
return out, err return out, err
} }
@ -141,9 +166,13 @@ func Search(opts Options, engines ...Engine) ([]*Result, error) {
return out, nil return out, nil
} }
// linkExists checks if a link exists in the results
func linkExists(results []*Result, link string) (int, bool) { func linkExists(results []*Result, link string) (int, bool) {
// For every result
for index, result := range results { for index, result := range results {
// If link is the same as provided
if result.Link == link { if result.Link == link {
// Return index with true
return index, true return index, true
} }
} }