Skip to content

Instantly share code, notes, and snippets.

@1lann
Last active October 20, 2015 14:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save 1lann/19020796bdfa30fe9d51 to your computer and use it in GitHub Desktop.
Save 1lann/19020796bdfa30fe9d51 to your computer and use it in GitHub Desktop.
Chatbox backend for remote services.
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