plugin.go 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. // This file is subject to a 1-clause BSD license.
  2. // Its contents can be found in the enclosed LICENSE file.
  3. // Package knmi provides commands to do current weather lookups.
  4. package knmi
  5. import (
  6. "encoding/xml"
  7. "log"
  8. "os/exec"
  9. "path/filepath"
  10. "sync"
  11. "notabug.org/mouz/bot/app/util"
  12. "notabug.org/mouz/bot/irc"
  13. "notabug.org/mouz/bot/irc/cmd"
  14. "notabug.org/mouz/bot/irc/proto"
  15. "notabug.org/mouz/bot/plugins"
  16. )
  17. func init() { plugins.Register(&plugin{}) }
  18. type plugin struct {
  19. m sync.Mutex
  20. cmd *cmd.Set
  21. config struct {
  22. ForecastURL string
  23. }
  24. }
  25. // Load initializes the module and loads any internal resources
  26. // which may be required.
  27. func (p *plugin) Load(prof irc.Profile) error {
  28. p.cmd = cmd.New(prof.CommandPrefix(), nil)
  29. p.cmd.Bind(TextForecastName, false, p.cmdForecast)
  30. file := filepath.Join(prof.Root(), "weather.cfg")
  31. return util.ReadFile(file, &p.config, false)
  32. }
  33. // Unload cleans the module up and unloads any internal resources.
  34. func (p *plugin) Unload(prof irc.Profile) error {
  35. p.config.ForecastURL = ""
  36. return nil
  37. }
  38. // Dispatch sends the given, incoming IRC message to the plugin for
  39. // processing as it sees fit.
  40. func (p *plugin) Dispatch(w irc.ResponseWriter, r *irc.Request) {
  41. if len(p.config.ForecastURL) == 0 {
  42. log.Println("[knmi] ForecastURL not in weather.cfg")
  43. _ = proto.PrivMsg(w, r.Target, TextNoForecast)
  44. return
  45. }
  46. p.cmd.Dispatch(w, r)
  47. }
  48. // cmdForecast yields the weather forecast.
  49. func (p *plugin) cmdForecast(w irc.ResponseWriter, r *irc.Request, params cmd.ParamList) {
  50. p.m.Lock()
  51. defer p.m.Unlock()
  52. data, err := p.fetch(p.config.ForecastURL)
  53. if err != nil {
  54. log.Println("[knmi]", err)
  55. return
  56. }
  57. err = p.sendResult(w, r, data)
  58. if err != nil {
  59. log.Println("[knmi]", err)
  60. return
  61. }
  62. }
  63. // fetch fetches the given URL using curl.
  64. // It returns curl's standard output or an error.
  65. func (p *plugin) fetch(URL string) ([]byte, error) {
  66. // using dirty hack so ftp URL does not need its own full module
  67. cmd := exec.Command("curl", URL)
  68. stdout, err := cmd.Output()
  69. if err != nil {
  70. return nil, err
  71. }
  72. return stdout, nil
  73. }
  74. // sendResult formats the result end sends it back to the user.
  75. func (p *plugin) sendResult(w irc.ResponseWriter, r *irc.Request, data []byte) error {
  76. forecast := knmiForecast{}
  77. err := xml.Unmarshal(data, &forecast)
  78. if err != nil {
  79. return err
  80. }
  81. report := forecast.Data.Location.Block[1].FieldContent
  82. return proto.PrivMsg(w, r.Target, report)
  83. }
  84. // knmiForecast represents the response when the request for the forecast was
  85. // succesful.
  86. //
  87. // The struct is generated using https://github.com/miku/zek via
  88. // https://www.onlinetool.io/xmltogo/ using as input a copy of
  89. // ftp://ftp.knmi.nl/pub_weerberichten/basisverwachting.xml
  90. type knmiForecast struct {
  91. XMLName xml.Name `xml:"report"`
  92. Text string `xml:",chardata"`
  93. Metadata struct {
  94. Text string `xml:",chardata"`
  95. ReportInfo struct {
  96. Text string `xml:",chardata"`
  97. ReportID string `xml:"report_id"`
  98. ReportLanguage string `xml:"report_language"`
  99. ReportTimeCoordinate string `xml:"report_time_coordinate"`
  100. ReportDtgIssued string `xml:"report_dtg_issued"`
  101. ReportStartValidTime string `xml:"report_start_valid_time"`
  102. ReportEndValidTime string `xml:"report_end_valid_time"`
  103. ReportIssuedBy string `xml:"report_issued_by"`
  104. ReportForecasterID string `xml:"report_forecaster_id"`
  105. ReportAdditionalInformation string `xml:"report_additional_information"`
  106. ReportDisclaimer string `xml:"report_disclaimer"`
  107. ReportCredit string `xml:"report_credit"`
  108. ReportCreditLogo string `xml:"report_credit_logo"`
  109. } `xml:"report_info"`
  110. ReportLocations struct {
  111. Text string `xml:",chardata"`
  112. ReportLocation struct {
  113. Text string `xml:",chardata"`
  114. LocationID string `xml:"location_id"`
  115. LocationDescr string `xml:"location_descr"`
  116. } `xml:"report_location"`
  117. } `xml:"report_locations"`
  118. ReportValidPeriods struct {
  119. Text string `xml:",chardata"`
  120. ReportValidPeriod []struct {
  121. Text string `xml:",chardata"`
  122. ValidID string `xml:"valid_id"`
  123. ValidStart string `xml:"valid_start"`
  124. ValidEnd string `xml:"valid_end"`
  125. ValidDescr string `xml:"valid_descr"`
  126. } `xml:"report_valid_period"`
  127. } `xml:"report_valid_periods"`
  128. ReportFields struct {
  129. Text string `xml:",chardata"`
  130. ReportField []struct {
  131. Text string `xml:",chardata"`
  132. FieldID string `xml:"field_id"`
  133. FieldDescr string `xml:"field_descr"`
  134. } `xml:"report_field"`
  135. } `xml:"report_fields"`
  136. } `xml:"metadata"`
  137. Data struct {
  138. Text string `xml:",chardata"`
  139. Location struct {
  140. Text string `xml:",chardata"`
  141. LocationID string `xml:"location_id"`
  142. Block []struct {
  143. Text string `xml:",chardata"`
  144. FieldID string `xml:"field_id"`
  145. ValidID string `xml:"valid_id"`
  146. FieldContent string `xml:"field_content"`
  147. } `xml:"block"`
  148. } `xml:"location"`
  149. } `xml:"data"`
  150. }