Last active
October 20, 2015 14:41
-
-
Save 1lann/19020796bdfa30fe9d51 to your computer and use it in GitHub Desktop.
Chatbox backend for remote services.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"encoding/json" | |
"encoding/xml" | |
"errors" | |
"github.com/1lann/ccserialize" | |
"github.com/gorilla/mux" | |
"github.com/vincent-petithory/countries" | |
"io/ioutil" | |
"log" | |
"net/http" | |
"net/url" | |
"regexp" | |
"strconv" | |
"strings" | |
"time" | |
) | |
const wolframAlphaAppId = "API KEY HERE" | |
const openWeatherMapAppId = "API KEY HERE" | |
const dateFormat = "3:04 PM on Monday, 2 January 2006" | |
var ( | |
ErrUnreachable = errors.New("Could not connect to external service.") | |
ErrParseError = errors.New("Received unexpected response from external service.") | |
ErrServerError = errors.New("A server error occurred while attempting to serve your query.") | |
ErrQueryInvalid = errors.New("Sorry, what you were looking for in your query could not be found.") | |
ErrAmbigious = errors.New("Sorry, your query was too ambigious. Try to be more specific.") | |
ErrNotFound = errors.New("Sorry, what you were looking for in your query could not be found.") | |
) | |
var timezones map[string]*time.Location = make(map[string]*time.Location) | |
func main() { | |
r := mux.NewRouter() | |
r.HandleFunc("/query", queryHandler).Methods("POST") | |
http.Handle("/", r) | |
log.Println("Hosting...") | |
log.Fatal(http.ListenAndServe(":9108", nil)) | |
} | |
func errorResponse(errorMessage string) string { | |
return ccserialize.Serialize(struct { | |
Error string | |
}{errorMessage}) | |
} | |
func queryHandler(resp http.ResponseWriter, request *http.Request) { | |
err := request.ParseForm() | |
if err != nil { | |
resp.Write([]byte(errorResponse("Invalid form data."))) | |
return | |
} | |
queryType := request.PostForm.Get("query_type") | |
if queryType == "" { | |
resp.Write([]byte(errorResponse("Did not specify a query type."))) | |
return | |
} | |
serializedResp := errorResponse("Invalid query type.") | |
switch queryType { | |
case "weather": | |
location := request.PostForm.Get("location") | |
if location == "" { | |
serializedResp = "A location is required" | |
break | |
} | |
time := request.PostForm.Get("time") | |
if time == "" { | |
serializedResp = "A time is required" | |
break | |
} | |
log.Println("weather: " + location + " during " + time) | |
serializedResp, err = handleWeatherQuery(location, time) | |
if err != nil { | |
serializedResp = errorResponse(err.Error()) | |
break | |
} | |
case "time": | |
location := request.PostForm.Get("location") | |
if location == "" { | |
serializedResp = errorResponse("A location is required.") | |
break | |
} | |
log.Println("time: " + location) | |
serializedResp, err = handleTimeQuery(location) | |
if err != nil { | |
serializedResp = errorResponse(err.Error()) | |
break | |
} | |
case "definition": | |
word := request.PostForm.Get("word") | |
if word == "" { | |
serializedResp = errorResponse("A word to define is required.") | |
break | |
} | |
log.Println("definition: " + word) | |
serializedResp, err = handleDefinitionQuery(word) | |
if err != nil { | |
serializedResp = errorResponse(err.Error()) | |
break | |
} | |
case "conversion": | |
conversion := request.PostForm.Get("conversion") | |
if conversion == "" { | |
serializedResp = errorResponse("A conversion query is required.") | |
break | |
} | |
log.Println("conversion: " + conversion) | |
serializedResp, err = handleConversionQuery(conversion) | |
if err != nil { | |
serializedResp = errorResponse(err.Error()) | |
break | |
} | |
case "wolframalpha": | |
query := request.PostForm.Get("query") | |
if query == "" { | |
serializedResp = errorResponse("A query is required.") | |
break | |
} | |
log.Println("wolframalpha: " + query) | |
serializedResp, err = handleGeneralQuery(query) | |
if err != nil { | |
serializedResp = errorResponse(err.Error()) | |
break | |
} | |
} | |
log.Println(serializedResp) | |
resp.Write([]byte(serializedResp)) | |
} | |
func handleWeatherQuery(location string, time string) (string, error) { | |
weatherData, err := makeOpenWeatherMapRequest(location) | |
if err != nil { | |
return "", err | |
} | |
if len(weatherData.DailyWeather) < 2 { | |
return "", ErrServerError | |
} | |
if time == "tomorrow" { | |
forecast := weatherData.DailyWeather[1] | |
description := forecast.Weather[0].Description | |
if description == "sky is clear" { | |
description = "clear skies" | |
} | |
forecastDescription := "Tomorrow's forecast for " + | |
weatherData.City.Name + ", " + weatherData.City.Country + " is " + | |
strconv.FormatFloat(forecast.Temperature.Day-273.15, 'f', 1, 64) + | |
"C with " + strings.ToLower(description) + "." | |
return ccserialize.Serialize(struct { | |
Success string | |
}{forecastDescription}), nil | |
} else { | |
forecast := weatherData.DailyWeather[0] | |
description := forecast.Weather[0].Description | |
if description == "sky is clear" { | |
description = "clear skies" | |
} | |
forecastDescription := "Today's weather for " + | |
weatherData.City.Name + ", " + weatherData.City.Country + " is " + | |
strconv.FormatFloat(forecast.Temperature.Day-273.15, 'f', 1, 64) + | |
"C with " + strings.ToLower(description) + "." | |
return ccserialize.Serialize(struct { | |
Success string | |
}{forecastDescription}), nil | |
} | |
} | |
func handleTimeQuery(location string) (string, error) { | |
if timeLocation, exists := timezones[location]; exists { | |
formattedDate := time.Now().In(timeLocation).Format(dateFormat) | |
responseLine := "It's currently " + formattedDate + " in " + | |
timeLocation.String() + "." | |
return ccserialize.Serialize(struct { | |
Success string | |
}{responseLine}), nil | |
} | |
result := struct { | |
Pod []struct { | |
Id string `xml:"id,attr"` | |
Plaintext string `xml:"subpod>plaintext"` | |
} `xml:"pod"` | |
}{} | |
err := makeWolframAlphaRequest("time zone of "+location, &result) | |
if err != nil { | |
return "", err | |
} | |
var resolvedLocation string | |
var zoneOffset int | |
for _, pod := range result.Pod { | |
if pod.Id == "Input" { | |
r := regexp.MustCompile("^(.+) \\| time zone$") | |
matches := r.FindStringSubmatch(pod.Plaintext) | |
if len(matches) < 2 { | |
resolvedLocation = pod.Plaintext | |
break | |
} | |
resolvedLocation = matches[1] | |
} else if pod.Id == "CurrentTimeDifferences:TimeZoneData" { | |
r := regexp.MustCompile("from UTC \\| (.+) \\(") | |
matches := r.FindStringSubmatch(pod.Plaintext) | |
if len(matches) < 2 { | |
return "", ErrQueryInvalid | |
} | |
if matches[1] == "+" { | |
zoneOffset = 0 | |
continue | |
} | |
durationString := strings.Replace(matches[1], "hours", "h", -1) | |
durationString = strings.Replace(durationString, "hour", "h", -1) | |
durationString = strings.Replace(durationString, "minutes", "m", -1) | |
durationString = strings.Replace(durationString, "minute", "m", -1) | |
durationString = strings.Replace(durationString, " ", "", -1) | |
duration, err := time.ParseDuration(durationString) | |
if err != nil { | |
log.Println("Parse duration error:", err) | |
return "", ErrParseError | |
} | |
zoneOffset = int(duration.Seconds()) | |
} | |
} | |
if resolvedLocation == "" { | |
return "", ErrQueryInvalid | |
} | |
timeLocation := time.FixedZone(resolvedLocation, zoneOffset) | |
timezones[location] = timeLocation | |
formattedDate := time.Now().In(timeLocation).Format(dateFormat) | |
responseLine := "It's currently " + formattedDate + " in " + | |
resolvedLocation + "." | |
return ccserialize.Serialize(struct { | |
Success string | |
}{responseLine}), nil | |
} | |
func handleGeneralQuery(query string) (string, error) { | |
result := struct { | |
Pod []struct { | |
Title string `xml:"title,attr"` | |
Plaintext string `xml:"subpod>plaintext"` | |
} `xml:"pod"` | |
}{} | |
err := makeWolframAlphaRequest(query, &result) | |
if err != nil { | |
return "", err | |
} | |
for _, pod := range result.Pod { | |
if pod.Title == "Result" || pod.Title == "Response" { | |
return ccserialize.Serialize(struct { | |
Success string | |
}{strings.Replace(pod.Plaintext, "\n", " ` ", -1)}), nil | |
} | |
} | |
if len(result.Pod) > 1 { | |
return ccserialize.Serialize(struct { | |
Success string | |
}{strings.Replace(result.Pod[1].Plaintext, "\n", " ` ", -1)}), nil | |
} | |
return "", ErrQueryInvalid | |
} | |
func handleConversionQuery(query string) (string, error) { | |
result := struct { | |
Pod []struct { | |
Id string `xml:"id,attr"` | |
Plaintext string `xml:"subpod>plaintext"` | |
} `xml:"pod"` | |
}{} | |
err := makeWolframAlphaRequest(query, &result) | |
if err != nil { | |
return "", err | |
} | |
for _, pod := range result.Pod { | |
if pod.Id == "Result" { | |
return ccserialize.Serialize(struct { | |
Success string | |
}{pod.Plaintext}), nil | |
} | |
} | |
return "", ErrQueryInvalid | |
} | |
// func isWordType(word string) bool { | |
// if word == "pronoun" || word == "proverb" || word == "noun" || | |
// word == "verb" || word == "adjective" || word == "adverb" || | |
// word == "article" || word == "determiner" || word == "conjunction" || | |
// word == "preposition" { | |
// return true | |
// } | |
// return false | |
// } | |
func handleDefinitionQuery(word string) (string, error) { | |
result := struct { | |
Pod []struct { | |
Id string `xml:"id,attr"` | |
Plaintext string `xml:"subpod>plaintext"` | |
} `xml:"pod"` | |
}{} | |
err := makeWolframAlphaRequest("word "+word, &result) | |
if err != nil { | |
return "", err | |
} | |
var rawDefinition string | |
for _, pod := range result.Pod { | |
if pod.Id == "Definition:WordData" { | |
rawDefinition = pod.Plaintext | |
break | |
} else if strings.Contains(pod.Id, "Definition") { | |
return ccserialize.Serialize(struct { | |
Success string | |
}{pod.Plaintext}), nil | |
} | |
} | |
if rawDefinition == "" { | |
if len(result.Pod) < 2 { | |
return "", ErrQueryInvalid | |
} | |
return ccserialize.Serialize(struct { | |
Success string | |
}{result.Pod[1].Plaintext}), nil | |
} | |
var definition string | |
lines := strings.Split(rawDefinition, "\n") | |
for _, line := range lines { | |
components := strings.Split(line, " | ") | |
if len(components) < 2 { | |
break | |
} | |
if _, err := strconv.Atoi(components[0]); err == nil { | |
// Has a number prefix. | |
definition = definition + components[0] + ". (" + components[1] + ") " + | |
components[2] + ". ` " | |
} else { | |
definition = definition + "(" + components[0] + ") " + | |
components[1] + "." | |
} | |
} | |
return ccserialize.Serialize(struct { | |
Success string | |
}{definition}), nil | |
} | |
type LocationWeather struct { | |
City struct { | |
Name string `json:"name"` | |
Country string `json:"country"` | |
} `json:"city"` | |
DailyWeather []struct { | |
Temperature struct { | |
Day float64 `json:"day"` | |
Min float64 `json:"min"` | |
} `json:"temp"` | |
Weather []struct { | |
Description string `json:"description"` | |
} `json:"weather"` | |
} `json:"list"` | |
} | |
func makeOpenWeatherMapRequest(location string) (LocationWeather, error) { | |
resp, err := http.Get( | |
"http://api.openweathermap.org/data/2.5/forecast/daily?q=" + | |
url.QueryEscape(location) + "&type=accurate&mode=json&appid=" + | |
openWeatherMapAppId) | |
if err != nil { | |
if resp.StatusCode == 404 { | |
return LocationWeather{}, ErrNotFound | |
} | |
log.Println("Weather request error:", err) | |
return LocationWeather{}, ErrUnreachable | |
} | |
defer resp.Body.Close() | |
data, err := ioutil.ReadAll(resp.Body) | |
if err != nil { | |
return LocationWeather{}, ErrParseError | |
} | |
// Check for errors | |
checkCode := struct { | |
Code string `json:"cod"` | |
}{} | |
err = json.Unmarshal(data, &checkCode) | |
if err != nil { | |
return LocationWeather{}, ErrParseError | |
} | |
if checkCode.Code == "404" { | |
return LocationWeather{}, ErrNotFound | |
} | |
locationWeatherData := LocationWeather{} | |
err = json.Unmarshal(data, &locationWeatherData) | |
if err != nil { | |
return LocationWeather{}, ErrParseError | |
} | |
if country, found := | |
countries.Countries[locationWeatherData.City.Country]; found { | |
locationWeatherData.City.Country = country. | |
ISO3166OneEnglishShortNameReadingOrder | |
} | |
return locationWeatherData, nil | |
} | |
func makeWolframAlphaRequest(query string, wrapper interface{}) error { | |
resp, err := http.Get("http://api.wolframalpha.com/v2/query?input=" + | |
url.QueryEscape(query) + "&appid=" + wolframAlphaAppId) | |
if err != nil { | |
return ErrUnreachable | |
} | |
defer resp.Body.Close() | |
data, err := ioutil.ReadAll(resp.Body) | |
if err != nil { | |
return ErrParseError | |
} | |
// Check for errors | |
queryResult := struct { | |
Success string `xml:"success,attr"` | |
}{} | |
err = xml.Unmarshal(data, &queryResult) | |
if err != nil { | |
return ErrParseError | |
} | |
if queryResult.Success != "true" { | |
return ErrQueryInvalid | |
} | |
assumptionResult := struct { | |
Assumptions struct { | |
Count string `xml:"count,attr"` | |
} `xml:"assumptions"` | |
}{} | |
err = xml.Unmarshal(data, &assumptionResult) | |
if err != nil { | |
return err | |
} | |
// if assumptionResult.Assumptions.Count != "" { | |
// return ErrAmbigious | |
// } | |
err = xml.Unmarshal(data, wrapper) | |
if err != nil { | |
return ErrParseError | |
} | |
return nil | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment