From c514fb46781f9c2b526ebbe4efd793a3dc02f3a5 Mon Sep 17 00:00:00 2001 From: Hamcha Date: Tue, 9 May 2017 15:47:49 +0200 Subject: [PATCH] mods: Add search and refactor mods --- mods/main.go | 12 +++- mods/metafora.go | 10 +++- mods/search.go | 144 +++++++++++++++++++++++++++++++++++++++++++++++ mods/talk.go | 58 +++++++++++++++---- mods/viaggi.go | 88 ++++++++++++++--------------- 5 files changed, 251 insertions(+), 61 deletions(-) create mode 100644 mods/search.go diff --git a/mods/main.go b/mods/main.go index 09902ad..3e57090 100644 --- a/mods/main.go +++ b/mods/main.go @@ -1,6 +1,7 @@ package main import ( + "context" "flag" "log" "math/rand" @@ -55,6 +56,10 @@ var mods = map[string]Mod{ OnInit: initremind, OnMessage: remind, }, + "search": { + OnInit: initsearch, + OnMessage: search, + }, } func initmods() { @@ -97,6 +102,8 @@ var sourcesans *string var proverbi *string var talktoken *string var gapifile *string +var gapikey *string +var gapiCtx context.Context func main() { brokerAddr := flag.String("broker", "localhost:7314", "Broker address:port") @@ -108,11 +115,14 @@ func main() { remindpath = flag.String("remindpath", "reminders.json", "Path to reminder db (JSON)") proverbi = flag.String("proverbi", "proverbi.txt", "Path to proverbi pairs (separated by /)") talktoken = flag.String("apiai", "@apiai.token", "api.ai token") - gapifile = flag.String("gapi", "gapi.json", "Google API Service Credentials file") + gapifile = flag.String("gapifile", "gapi.json", "Google API Service Credentials file (for STT)") + gapikey = flag.String("gapikey", "@gapi.key", "Google API key (for search/KG)") disable := flag.String("disable", "", "Blacklist mods (separated by comma)") enable := flag.String("enable", "", "Whitelist mods (separated by comma)") flag.Parse() + gapiCtx = context.Background() + if *disable != "" { for _, modname := range strings.Split(*disable, ",") { modname = strings.TrimSpace(modname) diff --git a/mods/metafora.go b/mods/metafora.go index bb5eec1..bd766ff 100644 --- a/mods/metafora.go +++ b/mods/metafora.go @@ -25,9 +25,13 @@ var metaobjects = []string{ func metafora(broker *tg.Broker, update tg.APIMessage) { if isCommand(update, "metafora") { - n := rand.Intn(len(metaactions)) - m := rand.Intn(len(metaobjects)) - broker.SendTextMessage(update.Chat, metaactions[n]+" "+metaobjects[m], nil) + broker.SendTextMessage(update.Chat, metaforaAPI(), nil) return } } + +func metaforaAPI() string { + n := rand.Intn(len(metaactions)) + m := rand.Intn(len(metaobjects)) + return metaactions[n] + " " + metaobjects[m] +} diff --git a/mods/search.go b/mods/search.go new file mode 100644 index 0000000..6782a88 --- /dev/null +++ b/mods/search.go @@ -0,0 +1,144 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "net/url" + "strings" + + "github.com/hamcha/clessy/tg" +) + +type SearchResult struct { + Name string + Description *string + Alternatives []string + None bool +} + +type SearchResultData struct { + ItemListElement []struct { + Result struct { + Name []struct { + Value string `json:"@value"` + Lang string `json:"@language"` + } `json:"name,omitempty"` + Description []struct { + Value string `json:"@value"` + Lang string `json:"@language"` + } `json:"description,omitempty"` + DetailedDescription []struct { + Value string `json:"articleBody"` + Lang string `json:"inLanguage"` + } `json:"detailedDescription,omitempty"` + } `json:"result"` + } `json:"itemListElement"` + Error struct { + Code int `json:"code"` + Message string `json:"message"` + } `json:"error,omitempty"` +} + +const MAX_SEARCH_ALTS = 2 + +func initsearch() { + if strings.HasPrefix(*gapikey, "@") { + data, err := ioutil.ReadFile((*gapikey)[1:]) + if err != nil { + panic(err) + } + *gapikey = strings.TrimSpace(string(data)) + } + if *gapikey == "" { + panic(fmt.Errorf("Missing GAPI key for search module (provide it or --disable search)")) + } +} + +func search(broker *tg.Broker, update tg.APIMessage) { + if isCommand(update, "search") { + parts := strings.SplitN(*(update.Text), " ", 2) + if len(parts) < 2 { + broker.SendTextMessage(update.Chat, "Non mi hai dato niente da cercare!", &update.MessageID) + return + } + broker.SendChatAction(update.Chat, tg.ActionTyping) + + result, err := searchAPI(parts[1]) + if err != nil { + log.Printf("[search] %s\n", err.Error()) + broker.SendTextMessage(update.Chat, "ERRORE! @hamcha controlla la console!", &update.MessageID) + return + } + + if result.None { + broker.SendTextMessage(update.Chat, "Non ho trovato nulla, mi spiace :(", &update.MessageID) + return + } + + resulttxt := fmt.Sprintf("Ecco la prima cosa che ho trovato:\n%s", result.Name) + if result.Description != nil { + resulttxt += "\n" + *(result.Description) + } + if len(result.Alternatives) > 0 { + resulttxt += "\nAlternativamente:\n - " + strings.Join(result.Alternatives, "\n - ") + } + + broker.SendTextMessage(update.Chat, resulttxt, &update.MessageID) + } +} + +func searchAPI(term string) (SearchResult, error) { + resp, err := http.Get("https://kgsearch.googleapis.com/v1/entities:search?indent=true&languages=it&languages=en&query=" + url.QueryEscape(term) + "&key=" + *gapikey) + if err != nil { + return SearchResult{}, err + } + defer resp.Body.Close() + + var res SearchResultData + err = json.NewDecoder(resp.Body).Decode(&res) + if err != nil { + return SearchResult{}, err + } + + if res.Error.Message != "" { + return SearchResult{}, fmt.Errorf("Request error: %d %s", res.Error.Code, res.Error.Message) + } + + if len(res.ItemListElement) < 1 { + return SearchResult{None: true}, nil + } + + out := SearchResult{None: false} + for _, item := range res.ItemListElement { + // Skip elements without a name + if item.Result.Name == nil || len(item.Result.Name) == 0 { + continue + } + namestr := item.Result.Name[0].Value + + // Check if it has a small description + if item.Result.Description != nil && len(item.Result.Description) > 0 { + namestr += " (" + item.Result.Description[0].Value + ")" + } + + // If we already had an item, add it to alternatives and find others + if out.Name != "" { + out.Alternatives = append(out.Alternatives, namestr) + if len(out.Alternatives) > MAX_SEARCH_ALTS { + break + } + continue + } + out.Name = namestr + + // Check for a description + if item.Result.DetailedDescription != nil || len(item.Result.DetailedDescription) > 0 { + out.Description = &(item.Result.DetailedDescription[0].Value) + } + } + + return out, nil +} diff --git a/mods/talk.go b/mods/talk.go index 4f20965..8e644b7 100644 --- a/mods/talk.go +++ b/mods/talk.go @@ -24,14 +24,12 @@ type QResponse struct { ID string `json:"id"` Timestamp time.Time `json:"timestamp"` Result struct { - Source string `json:"source"` - ResolvedQuery string `json:"resolvedQuery"` - Action string `json:"action"` - ActionIncomplete bool `json:"actionIncomplete"` - Parameters struct { - Name string `json:"name"` - } `json:"parameters"` - Contexts []struct { + Source string `json:"source"` + ResolvedQuery string `json:"resolvedQuery"` + Action string `json:"action"` + ActionIncomplete bool `json:"actionIncomplete"` + Parameters map[string]string `json:"parameters"` + Contexts []struct { Name string `json:"name"` Parameters struct { Name string `json:"name"` @@ -116,6 +114,8 @@ func talk(broker *tg.Broker, update tg.APIMessage) { return } + broker.SendChatAction(update.Chat, tg.ActionTyping) + req.Header.Add("Authorization", "Bearer "+*talktoken) req.Header.Set("Content-Type", "application/json; charset=utf-8") @@ -153,12 +153,48 @@ func talk(broker *tg.Broker, update tg.APIMessage) { if reply[0] == '#' { //TODO Find a better way? if strings.HasPrefix(reply, "#metafora") { - message := "/metafora" - update.Text = &message - metafora(broker, update) + reply = metaforaAPI() } return } + if record.Result.Action != "" && !record.Result.ActionIncomplete { + switch record.Result.Action { + case "viaggi": + // Check that both parameters are given + if record.Result.Parameters["from-city"] != "" && record.Result.Parameters["to-city"] != "" { + data, err := viaggiAPI(record.Result.Parameters["from-city"], record.Result.Parameters["to-city"]) + if err != nil { + log.Printf("[talk] viaggi failed:\n%s\n", record.Result.Action) + broker.SendTextMessage(update.Chat, "ERRORE! @hamcha controlla la console!", &update.MessageID) + return + } + reply += "\n" + data + } + case "search": + result, err := searchAPI(record.Result.Parameters["search-item"]) + if err != nil { + log.Printf("[talk] search failed:\n%s\n", record.Result.Action) + broker.SendTextMessage(update.Chat, "ERRORE! @hamcha controlla la console!", &update.MessageID) + return + } + if result.None { + broker.SendTextMessage(update.Chat, "Non ho trovato nulla, mi spiace :(", &update.MessageID) + return + } + + reply = fmt.Sprintf("%s\n%s", reply, result.Name) + if result.Description != nil { + reply += "\n" + *(result.Description) + } + if len(result.Alternatives) > 0 { + reply += "\nAlternativamente:\n - " + strings.Join(result.Alternatives, "\n - ") + } + default: + log.Printf("[talk] Unknown action called:\n%s\n", record.Result.Action) + broker.SendTextMessage(update.Chat, "ERRORE! @hamcha controlla la console!", &update.MessageID) + } + } + broker.SendTextMessage(update.Chat, reply, &update.MessageID) } diff --git a/mods/viaggi.go b/mods/viaggi.go index 93f1b13..0bd8d44 100644 --- a/mods/viaggi.go +++ b/mods/viaggi.go @@ -2,6 +2,7 @@ package main import ( "encoding/json" + "fmt" "log" "net/http" "net/url" @@ -25,10 +26,6 @@ func viaggi(broker *tg.Broker, update tg.APIMessage) { usage := func() { broker.SendTextMessage(update.Chat, "Formato: /viaggi <PARTENZA> -> <DESTINAZIONE>", &update.MessageID) } - oops := func(err error) { - log.Println("[viaggi] GET error:" + err.Error()) - broker.SendTextMessage(update.Chat, "ERRORE! @hamcha controlla la console!", &update.MessageID) - } parts := strings.SplitN(*(update.Text), " ", 2) if len(parts) < 2 { @@ -46,54 +43,15 @@ func viaggi(broker *tg.Broker, update tg.APIMessage) { msgs[1] = strings.Replace(msgs[1], " ", "-", -1) msgs[2] = strings.Replace(msgs[2], ",", "", -1) msgs[2] = strings.Replace(msgs[2], " ", "-", -1) - src := url.QueryEscape(msgs[1]) - dst := url.QueryEscape(msgs[2]) - url := viaggiurl + "&oName=" + src + "&dName=" + dst - resp, err := http.Get(url) - if err != nil { - oops(err) - return - } - defer resp.Body.Close() - var outjson Romejson - err = json.NewDecoder(resp.Body).Decode(&outjson) + out, err := viaggiAPI(msgs[1], msgs[2]) if err != nil { - broker.SendTextMessage(update.Chat, "Hm, Rome2rio non ha trovato nulla, mi spiace :(\nForse non hai scritto bene uno dei due posti?", &update.MessageID) - return - } - - var moreeco Romeroute - var lesstim Romeroute - if len(outjson.Routes) < 1 { - // Should never happen - log.Println("[viaggi] No routes found (??)") + log.Println("[viaggi] " + err.Error()) broker.SendTextMessage(update.Chat, "ERRORE! @hamcha controlla la console!", &update.MessageID) return } - // Calculate cheapest and fastest - moreeco = outjson.Routes[0] - lesstim = outjson.Routes[0] - for _, v := range outjson.Routes { - if v.IndicativePrice.Price < moreeco.IndicativePrice.Price { - moreeco = v - } - if v.Duration < lesstim.Duration { - lesstim = v - } - } - - broker.SendTextMessage(update.Chat, - "Viaggio da "+outjson.Places[0].Name+ - " a "+outjson.Places[1].Name+""+ - "\n\n"+ - "Piu economico: "+moreeco.Name+" ("+parseData(moreeco)+")"+ - "\n"+ - "Piu veloce: "+lesstim.Name+" ("+parseData(lesstim)+")"+ - "\n\n"+ - "Maggiori informazioni", - &update.MessageID) + broker.SendTextMessage(update.Chat, out, &update.MessageID) } } @@ -138,3 +96,41 @@ type Romejson struct { Places []Romeplace Routes []Romeroute } + +func viaggiAPI(from string, to string) (string, error) { + src := url.QueryEscape(from) + dst := url.QueryEscape(to) + url := viaggiurl + "&oName=" + src + "&dName=" + dst + resp, err := http.Get(url) + if err != nil { + return "", fmt.Errorf("GET error: %s", err.Error()) + } + defer resp.Body.Close() + + var outjson Romejson + err = json.NewDecoder(resp.Body).Decode(&outjson) + if err != nil { + return "Hm, Rome2rio non ha trovato nulla, mi spiace :(\nForse non hai scritto bene uno dei due posti?", nil + } + + var moreeco Romeroute + var lesstim Romeroute + if len(outjson.Routes) < 1 { + // Should never happen + return "", fmt.Errorf("No routes found (??)") + } + + // Calculate cheapest and fastest + moreeco = outjson.Routes[0] + lesstim = outjson.Routes[0] + for _, v := range outjson.Routes { + if v.IndicativePrice.Price < moreeco.IndicativePrice.Price { + moreeco = v + } + if v.Duration < lesstim.Duration { + lesstim = v + } + } + + return fmt.Sprintf("Viaggio da %s a %s\n\nPiu economico: %s (%s)\nPiu veloce: %s (%s)\n\nMaggiori informazioni", outjson.Places[0].Name, outjson.Places[1].Name, moreeco.Name, parseData(moreeco), lesstim.Name, parseData(lesstim), src, dst), nil +}