rss2hook.go 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. // rss2hook is a simple utility which will make HTTP POST
  2. // requests to remote web-hooks or execute commands when
  3. // new items appear in an RSS feed.
  4. //
  5. // Steve
  6. // poesty
  7. //
  8. package main
  9. import (
  10. "bytes"
  11. "encoding/json"
  12. "flag"
  13. "io"
  14. "log"
  15. "net/http"
  16. "os"
  17. "os/exec"
  18. "os/signal"
  19. "reflect"
  20. "syscall"
  21. "time"
  22. "github.com/SlyMarbo/rss"
  23. "github.com/robfig/cron"
  24. )
  25. // RSSEntry describes a single RSS feed and the corresponding hook
  26. type RSSEntry struct {
  27. // The URL of the RSS/Atom feed
  28. Url string `json:"url,omitempty"`
  29. // The hook method: webhook/command
  30. Method string `json:"method,omitempty"`
  31. // The retry count
  32. Retry int `json:"retry,omitempty"`
  33. // The hook end-point
  34. Hook string `json:"hook,omitempty"`
  35. }
  36. // Loaded contains the loaded feeds + hooks, as read from the specified
  37. // configuration file
  38. var Loaded []RSSEntry
  39. // Timeout is the (global) timeout we use when loading remote RSS
  40. // feeds.
  41. var Timeout time.Duration
  42. // loadConfig loads the named configuration file and populates our
  43. // `Loaded` list of RSS-feeds & Webhook addresses
  44. func loadConfig(filename string) {
  45. file, err := os.Open(filename)
  46. if err != nil {
  47. log.Fatalf("loadConfig: Error opening %s - %s\n", filename, err.Error())
  48. }
  49. defer file.Close()
  50. err = json.NewDecoder(file).Decode(&Loaded)
  51. if err != nil {
  52. log.Fatalf("loadConfig: Error decoding %s - %s\n", filename, err.Error())
  53. }
  54. log.Printf("Loaded %d feeds\n", len(Loaded))
  55. }
  56. // checkFeeds is our work-horse.
  57. //
  58. // For each available feed it looks for new entries, and when founds
  59. // triggers `notify` upon the resulting entry
  60. func checkFeeds(feeds []rss.Feed) {
  61. //
  62. // For each thing we're monitoring
  63. //
  64. for i, feed := range feeds {
  65. // skip empty feed, probably because:
  66. // 1. feed initialization failed
  67. // 2. retry count exceeded
  68. if reflect.ValueOf(feed).IsZero() {
  69. continue
  70. }
  71. // Fetch the feed-contents
  72. err := feed.Update()
  73. if err != nil {
  74. log.Printf("checkFeeds: Error fetching %s - %s\n",
  75. Loaded[i].Url, err.Error())
  76. if Loaded[i].Retry--; Loaded[i].Retry == 0 {
  77. log.Printf("checkFeeds: Temporally diable feed - %s\n", Loaded[i].Url)
  78. feeds[i] = rss.Feed{}
  79. }
  80. // log.Printf("checkFeeds: refresh time - %s\n", feed.Refresh.Format(time.RFC3339))
  81. continue
  82. }
  83. if feed.Unread != 0 {
  84. notify(i, feed.Items[0])
  85. feed.Unread = 0
  86. }
  87. }
  88. }
  89. // notify submits the specified item to the remote webhook
  90. // or execute custom commands.
  91. // The RSS-item is submitted as a JSON-object.
  92. func notify(i int, item *rss.Item) {
  93. if Loaded[i].Method == "command" {
  94. cmd := exec.Command(Loaded[i].Hook)
  95. err := cmd.Start()
  96. if err != nil {
  97. log.Fatalf("notify: Failed to start command: %s\n", err.Error())
  98. }
  99. } else if Loaded[i].Method == "webhook" {
  100. // We'll post the item as a JSON object.
  101. // So first of all encode it.
  102. jsonValue, err := json.Marshal(item)
  103. if err != nil {
  104. log.Fatalf("notify: Failed to encode JSON:%s\n", err.Error())
  105. }
  106. //
  107. // Post to the specified hook URL.
  108. //
  109. res, err := http.Post(Loaded[i].Hook,
  110. "application/json",
  111. bytes.NewBuffer(jsonValue))
  112. if err != nil {
  113. log.Printf("notify: Failed to POST to %s - %s\n",
  114. Loaded[i].Hook, err.Error())
  115. return
  116. }
  117. //
  118. // OK now we've submitted the post.
  119. //
  120. // We should retrieve the status-code + body, if the status-code
  121. // is "odd" then we'll show them.
  122. //
  123. defer res.Body.Close()
  124. _, err = io.ReadAll(res.Body)
  125. if err != nil {
  126. log.Printf("notify: Failed to read response from %s - %s\n",
  127. Loaded[i].Hook, err.Error())
  128. return
  129. }
  130. status := res.StatusCode
  131. if status != 200 {
  132. log.Printf("notify: Warning - Status code was not 200: %d\n", status)
  133. }
  134. }
  135. }
  136. // main is our entry-point
  137. func main() {
  138. // Parse the command-line flags
  139. config := flag.String("config", "config.json", "The path to the configuration-file to read")
  140. timeout := flag.Duration("timeout", 5*time.Second, "The timeout used for fetching the remote feeds")
  141. schedule := flag.String("schedule", "@every 5m", "The cron schedule for fetching the remote feeds")
  142. flag.Parse()
  143. // Setup the default timeout and TTL
  144. rss.DefaultRefreshInterval = 10 * time.Second
  145. rss.DefaultFetchFunc = func(url string) (*http.Response, error) {
  146. client := &http.Client{Timeout: *timeout}
  147. return client.Get(url)
  148. }
  149. //
  150. // Load the configuration file
  151. //
  152. loadConfig(*config)
  153. //
  154. // Show the things we're monitoring
  155. //
  156. // for _, ent := range Loaded {
  157. // log.Printf("Monitoring feed %s\nPosting to %s\n\n",
  158. // ent.Feed, ent.Hook)
  159. // }
  160. //
  161. // Make the initial scan of feeds immediately to avoid waiting too
  162. // long for the first time.
  163. //
  164. feeds := make([]rss.Feed, len(Loaded))
  165. for i, ent := range Loaded {
  166. go func(i int, ent RSSEntry) {
  167. feed, err := rss.Fetch(ent.Url)
  168. if err != nil {
  169. log.Printf("main: Error fetching %s - %s\n",
  170. ent.Url, err.Error())
  171. return
  172. }
  173. // feed.Unread = 0
  174. feeds[i] = *feed
  175. }(i, ent)
  176. }
  177. //
  178. // Now repeat that every five minutes or in custom schedule.
  179. //
  180. c := cron.New()
  181. c.AddFunc(*schedule, func() { checkFeeds(feeds) })
  182. c.Start()
  183. //
  184. // Now we can loop waiting to be terminated via ctrl-c, etc.
  185. //
  186. sigs := make(chan os.Signal, 1)
  187. done := make(chan bool, 1)
  188. signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
  189. go func() {
  190. <-sigs
  191. done <- true
  192. }()
  193. <-done
  194. }