forecast.go 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. // This file is subject to a 1-clause BSD license.
  2. // Its contents can be found in the enclosed LICENSE file.
  3. package weather
  4. import (
  5. "fmt"
  6. "strings"
  7. "time"
  8. "github.com/monkeybird/autimaat/app/util"
  9. "github.com/monkeybird/autimaat/irc"
  10. "github.com/monkeybird/autimaat/irc/cmd"
  11. "github.com/monkeybird/autimaat/irc/proto"
  12. )
  13. const ForecastURL = "https://api.wunderground.com/api/%s/forecast/lang:%s/q/%s.json"
  14. // cmdCurrentWeather yields weather forecast data for a given location.
  15. func (p *plugin) cmdForecast(w irc.ResponseWriter, r *irc.Request, params cmd.ParamList) {
  16. p.m.Lock()
  17. defer p.m.Unlock()
  18. if len(p.config.WundergroundApiKey) == 0 {
  19. proto.PrivMsg(w, r.Target, TextNoWeather)
  20. return
  21. }
  22. loc := newLocation(r)
  23. key := strings.ToLower(loc.String())
  24. if fr, ok := p.forecastCache[key]; ok {
  25. // If the cached result is younger than the timeout, print its
  26. // contents for the user and exit. Otherwise, consider it stale,
  27. // delete it and re-fetch.
  28. if time.Since(fr.Timestamp) <= CacheTimeout {
  29. sendForecast(w, r, fr, loc)
  30. return
  31. }
  32. delete(p.currentWeatherCache, key)
  33. }
  34. var resp forecastResponse
  35. resp.Timestamp = time.Now()
  36. if !p.fetch(ForecastURL, key, &resp) {
  37. return
  38. }
  39. // It is possible we received location suggestions, instead of weather
  40. // data. Present these suggestions to the user and exit. Do not cache
  41. // the response.
  42. if len(resp.Response.Results) > 0 {
  43. sendLocations(w, r, resp.Response.Results)
  44. return
  45. }
  46. sendForecast(w, r, &resp, loc)
  47. p.forecastCache[key] = &resp
  48. }
  49. // sendCurrentWeather formats a response for the user who invoked the
  50. // weather request and sends it back to them.
  51. func sendForecast(w irc.ResponseWriter, r *irc.Request, fr *forecastResponse, loc *location) {
  52. location := util.Bold(loc.City)
  53. if len(loc.Country) > 0 {
  54. if len(loc.State) > 0 {
  55. location += fmt.Sprintf(" (%s, %s)", loc.Country, loc.State)
  56. } else {
  57. location += fmt.Sprintf(" (%s)", loc.Country)
  58. }
  59. }
  60. if len(fr.Forecast.TextForecast.ForecastDay) == 0 {
  61. proto.PrivMsg(w, r.SenderName, TextNoResult, r.SenderName)
  62. return
  63. }
  64. proto.PrivMsg(w, r.SenderName, TextForecastDisplay, location)
  65. for _, v := range fr.Forecast.TextForecast.ForecastDay {
  66. proto.PrivMsg(w, r.SenderName, "%s: %s", util.Bold(v.Title), v.Text)
  67. }
  68. }
  69. // forecastResponse defines an API response.
  70. type forecastResponse struct {
  71. Timestamp time.Time
  72. // This is filled if an ambiguous location name is provided to
  73. // the API. It will contain location suggestions for specific
  74. // places.
  75. Response struct {
  76. Results []location `json:"results"`
  77. } `json:"response"`
  78. // This defines actual forecast data for a specific location.
  79. // It will be empty if the Response.Results field is not.
  80. Forecast struct {
  81. TextForecast struct {
  82. ForecastDay []struct {
  83. Title string `json:"title"`
  84. Text string `json:"fcttext_metric"`
  85. } `json:"forecastday"`
  86. } `json:"txt_forecast"`
  87. } `json:"forecast"`
  88. }