current.go 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. // This file is subject to a 1-clause BSD license.
  2. // Its contents can be found in the enclosed LICENSE file.
  3. package owm
  4. import (
  5. "encoding/json"
  6. "fmt"
  7. "log"
  8. "math"
  9. "net/url"
  10. "strings"
  11. "time"
  12. "notabug.org/mouz/bot/app/util"
  13. "notabug.org/mouz/bot/irc"
  14. "notabug.org/mouz/bot/irc/cmd"
  15. "notabug.org/mouz/bot/irc/proto"
  16. )
  17. const currentWeatherURL = "https://api.openweathermap.org/data/2.5/weather?" +
  18. "units=%s&mode=json&lang=%s&appid=%s&q=%s"
  19. // cmdCurrentWeather yields current weather data for a given location.
  20. func (p *plugin) cmdCurrentWeather(w irc.ResponseWriter, r *irc.Request, params cmd.ParamList) {
  21. p.m.Lock()
  22. defer p.m.Unlock()
  23. if len(p.config.OwmAPIKey) == 0 {
  24. log.Println("[owm] API key not in configuration")
  25. _ = proto.PrivMsg(w, r.Target, TextNoWeather, r.SenderName)
  26. return
  27. }
  28. loc := getLocation(r)
  29. if cached, ok := p.currentWeatherCache[loc]; ok {
  30. // If the cached result is younger than the timeout, print its
  31. // contents for the user and exit. Otherwise, consider it stale,
  32. // delete it and re-fetch.
  33. if time.Since(cached.Timestamp) <= CacheTimeout {
  34. log.Println("[owm] using cached item for current weather")
  35. err := sendCurrentWeather(w, r, cached)
  36. if err != nil {
  37. log.Println("[owm]", err)
  38. }
  39. return
  40. }
  41. delete(p.currentWeatherCache, loc)
  42. }
  43. // Fetch new response.
  44. data, err := p.fetch(currentWeatherURL, loc)
  45. if err != nil {
  46. log.Println("[owm]", err)
  47. return
  48. }
  49. err = p.sendResult(w, r, data)
  50. if err != nil {
  51. log.Println("[owm]", err)
  52. return
  53. }
  54. }
  55. // sendResult sends the result back to the user. The result is either
  56. // an error message or a meaningful current weather report.
  57. func (p *plugin) sendResult(w irc.ResponseWriter, r *irc.Request, data []byte) error {
  58. // unmarshal into a generic map, just to be able to detect the kind
  59. // of data that came in from the weather server
  60. var js map[string]interface{}
  61. err := json.Unmarshal(data, &js)
  62. if err != nil {
  63. return err
  64. }
  65. if len(js) == 2 {
  66. // assume error message from weather server
  67. var oerr owmError
  68. err := json.Unmarshal(data, &oerr)
  69. if err != nil {
  70. return err
  71. }
  72. return sendError(w, r, &oerr)
  73. }
  74. // assume meaningful current weather response
  75. var owmcur owmCurrent
  76. owmcur.Timestamp = time.Now()
  77. err = json.Unmarshal(data, &owmcur)
  78. if err != nil {
  79. return err
  80. }
  81. loc := getLocation(r)
  82. p.currentWeatherCache[loc] = &owmcur
  83. return sendCurrentWeather(w, r, &owmcur)
  84. }
  85. // getLocation extracts the requested location
  86. func getLocation(r *irc.Request) string {
  87. l := ""
  88. for _, s := range r.Fields(1) {
  89. l += url.QueryEscape(strings.ToLower(s)) + " "
  90. }
  91. return strings.TrimSuffix(l, " ")
  92. }
  93. // sendError reports on an error message coming from
  94. // openweathermap.org back to the user
  95. func sendError(w irc.ResponseWriter, r *irc.Request, oErr *owmError) error {
  96. loc, _ := url.QueryUnescape(getLocation(r))
  97. str, ok := oErr.Code.(string)
  98. if ok && str == "404" {
  99. return proto.PrivMsg(w, r.Target, TextNotFound404, util.Bold(loc))
  100. } else {
  101. log.Printf("[owm] weather server returned error %+v %s [%s]\n", oErr.Code, oErr.Message, loc)
  102. }
  103. return nil
  104. }
  105. // sendCurrentWeather formats the current weather and sends it back to
  106. // the user
  107. func sendCurrentWeather(w irc.ResponseWriter, r *irc.Request, owmcur *owmCurrent) error {
  108. location := util.Bold(owmcur.Name)
  109. location += fmt.Sprintf(" (%s)", owmcur.Sys.Country)
  110. temp := fmt.Sprintf(TextTempExact, owmcur.Main.Temp)
  111. if owmcur.Main.TempMax-owmcur.Main.TempMin > 1 {
  112. temp = fmt.Sprintf(TextTempRange, owmcur.Main.TempMin, owmcur.Main.TempMax)
  113. }
  114. precip := ""
  115. if owmcur.Snow.OneH > 0 {
  116. precip = fmt.Sprintf(TextSnowFall, owmcur.Snow.OneH)
  117. } else if owmcur.Snow.ThreeH > 0 {
  118. precip = fmt.Sprintf(TextSnowFall, owmcur.Snow.ThreeH/3)
  119. } else if owmcur.Rain.OneH > 0 {
  120. precip = fmt.Sprintf(TextRainFall, owmcur.Rain.OneH)
  121. } else if owmcur.Rain.ThreeH > 0 {
  122. precip = fmt.Sprintf(TextRainFall, owmcur.Rain.ThreeH/3)
  123. }
  124. index := int(0.5+owmcur.Wind.Deg/(360/16)) % 16
  125. winddir := TextWindDirection[index]
  126. speedBf := math.Pow(float64(owmcur.Wind.Speed)/0.836, 0.666)
  127. loc, _ := time.LoadLocation(TextTimeZone)
  128. t := time.Unix(owmcur.Sys.Sunrise, 0).In(loc)
  129. sunrise := t.Format(TextTimeFormat)
  130. t = time.Unix(owmcur.Sys.Sunset, 0).In(loc)
  131. sunset := t.Format(TextTimeFormat)
  132. sunriseset := fmt.Sprintf(TextSunRiseSet, sunrise, sunset)
  133. return proto.PrivMsg(w, r.Target, TextCurrentWeatherDisplay,
  134. r.SenderName,
  135. location,
  136. temp,
  137. owmcur.Weather[0].Description,
  138. precip,
  139. owmcur.Main.Pressure,
  140. owmcur.Main.Humidity,
  141. owmcur.Wind.Speed,
  142. speedBf,
  143. winddir,
  144. sunriseset,
  145. )
  146. }
  147. // owmError defines the API response when the request for the
  148. // current weather was not succesful.
  149. type owmError struct {
  150. Code interface{} `json:"cod"`
  151. Message string `json:"message"`
  152. }
  153. // owmCurrent defines the API response when the request for the
  154. // current weather was succesful.
  155. type owmCurrent struct {
  156. Timestamp time.Time
  157. Coord coordinate `json:"coord"`
  158. Weather []weatherCondition `json:"weather"`
  159. Base string `json:"base"`
  160. Main mainWeather `json:"main"`
  161. Visibility int `json:"visibility"`
  162. Wind windData `json:"wind"`
  163. Clouds cloudiness `json:"clouds"`
  164. Rain rainVolume `json:"rain"`
  165. Snow snowVolume `json:"snow"`
  166. Dt int64 `json:"dt"`
  167. Sys sysData `json:"sys"`
  168. ID int `json:"id"`
  169. Name string `json:"name"`
  170. Cod int `json:"cod"`
  171. }
  172. type coordinate struct {
  173. Lon float32 `json:"lon"`
  174. Lat float32 `json:"lat"`
  175. }
  176. type weatherCondition struct {
  177. ID int `json:"id"`
  178. Main string `json:"main"`
  179. Description string `json:"description"`
  180. Icon string `json:"icon"`
  181. }
  182. type mainWeather struct {
  183. Temp float32 `json:"temp"`
  184. Pressure float32 `json:"pressure"`
  185. Humidity float32 `json:"humidity"`
  186. TempMin float32 `json:"temp_min"`
  187. TempMax float32 `json:"temp_max"`
  188. SeaLevel float32 `json:"sea_level"`
  189. GroundLevel float32 `json:"grnd_level"`
  190. }
  191. type windData struct {
  192. Speed float32 `json:"speed"`
  193. Deg float32 `json:"deg"`
  194. }
  195. type cloudiness struct {
  196. All int `json:"all"`
  197. }
  198. type rainVolume struct {
  199. OneH float32 `json:"1h"`
  200. ThreeH float32 `json:"3h"`
  201. }
  202. type snowVolume struct {
  203. OneH float32 `json:"1h"`
  204. ThreeH float32 `json:"3h"`
  205. }
  206. type sysData struct {
  207. Type int `json:"type"`
  208. ID int `json:"id"`
  209. Message float32 `json:"message"`
  210. Country string `json:"country"`
  211. Sunrise int64 `json:"sunrise"`
  212. Sunset int64 `json:"sunset"`
  213. }