1
0

rss2hook.go 5.0 KB

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