mods: Add search and refactor mods

This commit is contained in:
Hamcha 2017-05-09 15:47:49 +02:00
parent 76c4a672fc
commit c514fb4678
5 changed files with 251 additions and 61 deletions

View file

@ -1,6 +1,7 @@
package main package main
import ( import (
"context"
"flag" "flag"
"log" "log"
"math/rand" "math/rand"
@ -55,6 +56,10 @@ var mods = map[string]Mod{
OnInit: initremind, OnInit: initremind,
OnMessage: remind, OnMessage: remind,
}, },
"search": {
OnInit: initsearch,
OnMessage: search,
},
} }
func initmods() { func initmods() {
@ -97,6 +102,8 @@ var sourcesans *string
var proverbi *string var proverbi *string
var talktoken *string var talktoken *string
var gapifile *string var gapifile *string
var gapikey *string
var gapiCtx context.Context
func main() { func main() {
brokerAddr := flag.String("broker", "localhost:7314", "Broker address:port") 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)") remindpath = flag.String("remindpath", "reminders.json", "Path to reminder db (JSON)")
proverbi = flag.String("proverbi", "proverbi.txt", "Path to proverbi pairs (separated by /)") proverbi = flag.String("proverbi", "proverbi.txt", "Path to proverbi pairs (separated by /)")
talktoken = flag.String("apiai", "@apiai.token", "api.ai token") 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)") disable := flag.String("disable", "", "Blacklist mods (separated by comma)")
enable := flag.String("enable", "", "Whitelist mods (separated by comma)") enable := flag.String("enable", "", "Whitelist mods (separated by comma)")
flag.Parse() flag.Parse()
gapiCtx = context.Background()
if *disable != "" { if *disable != "" {
for _, modname := range strings.Split(*disable, ",") { for _, modname := range strings.Split(*disable, ",") {
modname = strings.TrimSpace(modname) modname = strings.TrimSpace(modname)

View file

@ -25,9 +25,13 @@ var metaobjects = []string{
func metafora(broker *tg.Broker, update tg.APIMessage) { func metafora(broker *tg.Broker, update tg.APIMessage) {
if isCommand(update, "metafora") { if isCommand(update, "metafora") {
n := rand.Intn(len(metaactions)) broker.SendTextMessage(update.Chat, metaforaAPI(), nil)
m := rand.Intn(len(metaobjects))
broker.SendTextMessage(update.Chat, metaactions[n]+" "+metaobjects[m], nil)
return return
} }
} }
func metaforaAPI() string {
n := rand.Intn(len(metaactions))
m := rand.Intn(len(metaobjects))
return metaactions[n] + " " + metaobjects[m]
}

144
mods/search.go Normal file
View file

@ -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, "<b>ERRORE!</b> @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<b>%s</b>", 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
}

View file

@ -24,14 +24,12 @@ type QResponse struct {
ID string `json:"id"` ID string `json:"id"`
Timestamp time.Time `json:"timestamp"` Timestamp time.Time `json:"timestamp"`
Result struct { Result struct {
Source string `json:"source"` Source string `json:"source"`
ResolvedQuery string `json:"resolvedQuery"` ResolvedQuery string `json:"resolvedQuery"`
Action string `json:"action"` Action string `json:"action"`
ActionIncomplete bool `json:"actionIncomplete"` ActionIncomplete bool `json:"actionIncomplete"`
Parameters struct { Parameters map[string]string `json:"parameters"`
Name string `json:"name"` Contexts []struct {
} `json:"parameters"`
Contexts []struct {
Name string `json:"name"` Name string `json:"name"`
Parameters struct { Parameters struct {
Name string `json:"name"` Name string `json:"name"`
@ -116,6 +114,8 @@ func talk(broker *tg.Broker, update tg.APIMessage) {
return return
} }
broker.SendChatAction(update.Chat, tg.ActionTyping)
req.Header.Add("Authorization", "Bearer "+*talktoken) req.Header.Add("Authorization", "Bearer "+*talktoken)
req.Header.Set("Content-Type", "application/json; charset=utf-8") 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] == '#' { if reply[0] == '#' {
//TODO Find a better way? //TODO Find a better way?
if strings.HasPrefix(reply, "#metafora") { if strings.HasPrefix(reply, "#metafora") {
message := "/metafora" reply = metaforaAPI()
update.Text = &message
metafora(broker, update)
} }
return 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, "<b>ERRORE!</b> @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, "<b>ERRORE!</b> @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<b>%s</b>", 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, "<b>ERRORE!</b> @hamcha controlla la console!", &update.MessageID)
}
}
broker.SendTextMessage(update.Chat, reply, &update.MessageID) broker.SendTextMessage(update.Chat, reply, &update.MessageID)
} }

View file

@ -2,6 +2,7 @@ package main
import ( import (
"encoding/json" "encoding/json"
"fmt"
"log" "log"
"net/http" "net/http"
"net/url" "net/url"
@ -25,10 +26,6 @@ func viaggi(broker *tg.Broker, update tg.APIMessage) {
usage := func() { usage := func() {
broker.SendTextMessage(update.Chat, "Formato: /viaggi <i>&lt;PARTENZA&gt;</i> -> <i>&lt;DESTINAZIONE&gt;</i>", &update.MessageID) broker.SendTextMessage(update.Chat, "Formato: /viaggi <i>&lt;PARTENZA&gt;</i> -> <i>&lt;DESTINAZIONE&gt;</i>", &update.MessageID)
} }
oops := func(err error) {
log.Println("[viaggi] GET error:" + err.Error())
broker.SendTextMessage(update.Chat, "<b>ERRORE!</b> @hamcha controlla la console!", &update.MessageID)
}
parts := strings.SplitN(*(update.Text), " ", 2) parts := strings.SplitN(*(update.Text), " ", 2)
if len(parts) < 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[1] = strings.Replace(msgs[1], " ", "-", -1)
msgs[2] = strings.Replace(msgs[2], ",", "", -1) msgs[2] = strings.Replace(msgs[2], ",", "", -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 out, err := viaggiAPI(msgs[1], msgs[2])
err = json.NewDecoder(resp.Body).Decode(&outjson)
if err != nil { 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) log.Println("[viaggi] " + err.Error())
return
}
var moreeco Romeroute
var lesstim Romeroute
if len(outjson.Routes) < 1 {
// Should never happen
log.Println("[viaggi] No routes found (??)")
broker.SendTextMessage(update.Chat, "<b>ERRORE!</b> @hamcha controlla la console!", &update.MessageID) broker.SendTextMessage(update.Chat, "<b>ERRORE!</b> @hamcha controlla la console!", &update.MessageID)
return return
} }
// Calculate cheapest and fastest broker.SendTextMessage(update.Chat, out, &update.MessageID)
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 <b>"+outjson.Places[0].Name+
"</b> a <b>"+outjson.Places[1].Name+"</b>"+
"\n\n"+
"Piu economico: <b>"+moreeco.Name+"</b> ("+parseData(moreeco)+")"+
"\n"+
"Piu veloce: <b>"+lesstim.Name+"</b> ("+parseData(lesstim)+")"+
"\n\n"+
"<a href=\"http://www.rome2rio.com/it/s/"+src+"/"+dst+"\">Maggiori informazioni</a>",
&update.MessageID)
} }
} }
@ -138,3 +96,41 @@ type Romejson struct {
Places []Romeplace Places []Romeplace
Routes []Romeroute 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 <b>%s</b> a <b>%s</b>\n\nPiu economico: <b>%s</b> (%s)\nPiu veloce: <b>%s</b> (%s)\n\n<a href=\"http://www.rome2rio.com/it/s/%s/%s\">Maggiori informazioni</a>", outjson.Places[0].Name, outjson.Places[1].Name, moreeco.Name, parseData(moreeco), lesstim.Name, parseData(lesstim), src, dst), nil
}