plugin.go 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  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 provides commands to do current weather lookups,
  4. // as well as weather forecasts for specific locations.
  5. package weather
  6. import (
  7. "encoding/json"
  8. "fmt"
  9. "io/ioutil"
  10. "log"
  11. "net/http"
  12. "path/filepath"
  13. "sort"
  14. "strings"
  15. "sync"
  16. "time"
  17. "github.com/monkeybird/autimaat/app/util"
  18. "github.com/monkeybird/autimaat/irc"
  19. "github.com/monkeybird/autimaat/irc/cmd"
  20. "github.com/monkeybird/autimaat/irc/proto"
  21. "github.com/monkeybird/autimaat/plugins"
  22. )
  23. func init() { plugins.Register(&plugin{}) }
  24. // CacheTimeout defines the time after which a cache entry is
  25. // considered stale and it must be re-fetched.
  26. const CacheTimeout = time.Minute * 10
  27. // LookupTimeout defines the timeout after which a service request
  28. // is considered failed.
  29. const LookupTimeout = time.Second * 5
  30. type plugin struct {
  31. m sync.Mutex
  32. cmd *cmd.Set
  33. currentWeatherCache map[string]*currentWeatherResponse
  34. forecastCache map[string]*forecastResponse
  35. config struct {
  36. WundergroundApiKey string
  37. }
  38. }
  39. // Load initializes the module and loads any internal resources
  40. // which may be required.
  41. func (p *plugin) Load(prof irc.Profile) error {
  42. file := filepath.Join(prof.Root(), "weather.cfg")
  43. err := util.ReadFile(file, &p.config, false)
  44. if err != nil {
  45. return err
  46. }
  47. p.currentWeatherCache = make(map[string]*currentWeatherResponse)
  48. p.forecastCache = make(map[string]*forecastResponse)
  49. p.cmd = cmd.New(prof.CommandPrefix(), nil)
  50. p.cmd.Bind(TextCurrentWeatherName, false, p.cmdCurrentWeather).
  51. Add(TextLocation, true, cmd.RegAny)
  52. p.cmd.Bind(TextForecastName, false, p.cmdForecast).
  53. Add(TextLocation, true, cmd.RegAny)
  54. return nil
  55. }
  56. // Unload cleans the module up and unloads any internal resources.
  57. func (p *plugin) Unload(prof irc.Profile) error {
  58. p.config.WundergroundApiKey = ""
  59. return nil
  60. }
  61. // Dispatch sends the given, incoming IRC message to the plugin for
  62. // processing as it sees fit.
  63. func (p *plugin) Dispatch(w irc.ResponseWriter, r *irc.Request) {
  64. p.cmd.Dispatch(w, r)
  65. }
  66. // sendLocations sends location suggestions to the request's sender.
  67. func sendLocations(w irc.ResponseWriter, r *irc.Request, locs []location) {
  68. set := make([]string, 0, len(locs))
  69. // Add location descriptors to the set, provided they are unique.
  70. for _, l := range locs {
  71. value := fmt.Sprintf("%s %s %s", l.City, l.Country, l.State)
  72. if !hasString(set, value) {
  73. set = append(set, value)
  74. }
  75. }
  76. sort.Strings(set)
  77. proto.PrivMsg(w, r.Target, TextLocationsText,
  78. r.SenderName, strings.Join(set, ", "))
  79. }
  80. // hasString returnstrue if p contains a case-insensitive version of v,
  81. func hasString(p []string, v string) bool {
  82. for _, pv := range p {
  83. if strings.EqualFold(pv, v) {
  84. return true
  85. }
  86. }
  87. return false
  88. }
  89. // fetch fetches the given URL contents and unmarshals them into the
  90. // specified struct. This returns false if the fetch failed.
  91. func (p *plugin) fetch(serviceURL, query string, v interface{}) bool {
  92. // Fetch new response.
  93. url := fmt.Sprintf(
  94. serviceURL,
  95. p.config.WundergroundApiKey,
  96. TextLanguageISO,
  97. query,
  98. )
  99. resp, err := http.Get(url)
  100. if err != nil {
  101. log.Println("[weather] fetch: http.Get:", err)
  102. return false
  103. }
  104. data, err := ioutil.ReadAll(resp.Body)
  105. resp.Body.Close()
  106. if err != nil {
  107. log.Println("[weather] fetch: ioutil.ReadAll:", err)
  108. return false
  109. }
  110. //log.Println(string(data))
  111. err = json.Unmarshal(data, v)
  112. if err != nil {
  113. log.Println("[weather] fetch: json.Unmarshal:", err)
  114. return false
  115. }
  116. return true
  117. }