浏览代码

Initial rewrite

poesty 9 月之前
父节点
当前提交
c1a086bc00
共有 5 个文件被更改,包括 130 次插入323 次删除
  1. 14 0
      config.json
  2. 2 5
      go.mod
  3. 4 94
      go.sum
  4. 110 196
      rss2hook.go
  5. 0 28
      sample.cfg

+ 14 - 0
config.json

@@ -0,0 +1,14 @@
+[
+    {
+        "url": "https://lorem-rss.herokuapp.com/feed?unit=second",
+        "method": "webhook",
+        "retry": 5,
+        "hook": "http://localhost:8080/"
+    },
+    {
+        "url": "https://lorem-rss.herokuapp.com/feed?unit=minute&interval=1",
+        "method": "command",
+        "retry": 5,
+        "hook": "ping"
+    }
+]

+ 2 - 5
go.mod

@@ -1,11 +1,8 @@
-module github.com/skx/rss2hook
+module git.qunn.eu/poesty/rss2hook
 
 go 1.12
 
 require (
-	github.com/PuerkitoBio/goquery v1.8.1 // indirect
-	github.com/andybalholm/cascadia v1.3.2 // indirect
-	github.com/mmcdole/gofeed v1.2.1
+	github.com/SlyMarbo/rss v1.0.5
 	github.com/robfig/cron v1.2.0
-	golang.org/x/net v0.14.0 // indirect
 )

+ 4 - 94
go.sum

@@ -1,96 +1,6 @@
-github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
-github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI=
-github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
-github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
-github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
-github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
-github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
-github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
-github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
-github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
-github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
-github.com/mmcdole/gofeed v1.2.1 h1:tPbFN+mfOLcM1kDF1x2c/N68ChbdBatkppdzf/vDe1s=
-github.com/mmcdole/gofeed v1.2.1/go.mod h1:2wVInNpgmC85q16QTTuwbuKxtKkHLCDDtf0dCmnrNr4=
-github.com/mmcdole/goxpp v1.1.0 h1:WwslZNF7KNAXTFuzRtn/OKZxFLJAAyOA9w82mDz2ZGI=
-github.com/mmcdole/goxpp v1.1.0/go.mod h1:v+25+lT2ViuQ7mVxcncQ8ch1URund48oH+jhjiwEgS8=
-github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
-github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
-github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/SlyMarbo/rss v1.0.5 h1:DPcZ4aOXXHJ5yNLXY1q/57frIixMmAvTtLxDE3fsMEI=
+github.com/SlyMarbo/rss v1.0.5/go.mod h1:w6Bhn1BZs91q4OlEnJVZEUNRJmlbFmV7BkAlgCN8ofM=
+github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394 h1:OYA+5W64v3OgClL+IrOD63t4i/RW7RqrAVl9LTZ9UqQ=
+github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394/go.mod h1:Q8n74mJTIgjX4RBBcHnJ05h//6/k6foqmgE45jTQtxg=
 github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
 github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
-github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
-github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
-github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
-github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
-github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
-github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-github.com/urfave/cli v1.22.3/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
-github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
-golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
-golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
-golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
-golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
-golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
-golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
-golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
-golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
-golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
-golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
-golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
-golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
-golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
-golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
-golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
-golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
-golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
-gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

+ 110 - 196
rss2hook.go

@@ -1,40 +1,41 @@
 // rss2hook is a simple utility which will make HTTP POST
-// requests to remote web-hooks when new items appear in an RSS feed.
+// requests to remote web-hooks or execute commands when
+// new items appear in an RSS feed.
 //
 // Steve
+// poesty
 //
 
 package main
 
 import (
-	"bufio"
 	"bytes"
-	"crypto/sha1"
-	"encoding/hex"
 	"encoding/json"
 	"flag"
-	"fmt"
-	"io/ioutil"
+	"io"
+	"log"
 	"net/http"
 	"os"
+	"os/exec"
 	"os/signal"
-	"regexp"
-	"strings"
+	"reflect"
 	"syscall"
 	"time"
 
-	"github.com/mmcdole/gofeed"
+	"github.com/SlyMarbo/rss"
 	"github.com/robfig/cron"
 )
 
 // RSSEntry describes a single RSS feed and the corresponding hook
-// to POST to.
 type RSSEntry struct {
 	// The URL of the RSS/Atom feed
-	feed string
-
-	// The end-point to make the webhook request to.
-	hook string
+	Url string `json:"url,omitempty"`
+	// The hook method: webhook/command
+	Method string `json:"method,omitempty"`
+	// The retry count
+	Retry int `json:"retry,omitempty"`
+	// The hook end-point
+	Hook string `json:"hook,omitempty"`
 }
 
 // Loaded contains the loaded feeds + hooks, as read from the specified
@@ -50,223 +51,124 @@ var Timeout time.Duration
 func loadConfig(filename string) {
 	file, err := os.Open(filename)
 	if err != nil {
-		fmt.Printf("Error opening %s - %s\n", filename, err.Error())
-		return
+		log.Fatalf("loadConfig: Error opening %s - %s\n", filename, err.Error())
 	}
 	defer file.Close()
 
-	//
-	// Process it line by line.
-	//
-	scanner := bufio.NewScanner(file)
-	for scanner.Scan() {
-
-		// Get the next line, and strip leading/trailing space
-		tmp := scanner.Text()
-		tmp = strings.TrimSpace(tmp)
-
-		//
-		// Skip lines that begin with a comment.
-		//
-		if (tmp != "") && (!strings.HasPrefix(tmp, "#")) {
-
-			//
-			// Otherwise find the feed + post-point
-			//
-			parser := regexp.MustCompile("^(.+?)=([^=].+)")
-			match := parser.FindStringSubmatch(tmp)
-
-			//
-			// OK we found a suitable entry.
-			//
-			if len(match) == 3 {
-
-				feed := strings.TrimSpace(match[1])
-				hook := strings.TrimSpace(match[2])
-
-				// Append the new entry to our list
-				entry := RSSEntry{feed: feed, hook: hook}
-				Loaded = append(Loaded, entry)
-			}
-
-		}
-	}
-
-}
-
-// fetchFeed fetches the contents of the specified URL.
-func fetchFeed(url string) (string, error) {
-
-	// Ensure we setup a timeout for our fetch
-	client := &http.Client{Timeout: Timeout}
-
-	// We'll only make a GET request
-	req, err := http.NewRequest("GET", url, nil)
-	if err != nil {
-		return "", err
-	}
-
-	// We ensure we identify ourself.
-	req.Header.Set("User-Agent", "rss2email (https://github.com/skx/rss2email)")
-
-	// Make the request
-	resp, err := client.Do(req)
+	err = json.NewDecoder(file).Decode(&Loaded)
 	if err != nil {
-		return "", err
+		log.Fatalf("loadConfig: Error decoding %s - %s\n", filename, err.Error())
 	}
-	defer resp.Body.Close()
-
-	// Read the body returned
-	output, err := ioutil.ReadAll(resp.Body)
-	if err != nil {
-		return "", err
-	}
-	return string(output), nil
-}
-
-// isNew returns TRUE if this feed-item hasn't been notified about
-// previously.
-func isNew(parent string, item *gofeed.Item) bool {
-
-	hasher := sha1.New()
-	hasher.Write([]byte(parent))
-	hasher.Write([]byte(item.GUID))
-	hashBytes := hasher.Sum(nil)
-
-	// Hexadecimal conversion
-	hexSha1 := hex.EncodeToString(hashBytes)
-
-	if _, err := os.Stat(os.Getenv("HOME") + "/.rss2hook/seen/" + hexSha1); os.IsNotExist(err) {
-		return true
-	}
-	return false
-}
-
-// recordSeen ensures that we won't re-announce a given feed-item.
-func recordSeen(parent string, item *gofeed.Item) {
-
-	hasher := sha1.New()
-	hasher.Write([]byte(parent))
-	hasher.Write([]byte(item.GUID))
-	hashBytes := hasher.Sum(nil)
-
-	// Hexadecimal conversion
-	hexSha1 := hex.EncodeToString(hashBytes)
-
-	dir := os.Getenv("HOME") + "/.rss2hook/seen"
-	os.MkdirAll(dir, os.ModePerm)
-
-	_ = ioutil.WriteFile(dir+"/"+hexSha1, []byte(item.Link), 0644)
 
+	log.Printf("Loaded %d feeds\n", len(Loaded))
 }
 
 // checkFeeds is our work-horse.
 //
 // For each available feed it looks for new entries, and when founds
 // triggers `notify` upon the resulting entry
-func checkFeeds() {
-
+func checkFeeds(feeds []rss.Feed) {
 	//
 	// For each thing we're monitoring
 	//
-	for _, monitor := range Loaded {
-
-		// Fetch the feed-contents
-		content, err := fetchFeed(monitor.feed)
+	for i, feed := range feeds {
 
-		if err != nil {
-			fmt.Printf("Error fetching %s - %s\n",
-				monitor.feed, err.Error())
+		// skip empty feed, probably because:
+		// 1. feed initialization failed
+		// 2. retry count exceeded
+		if reflect.ValueOf(feed).IsZero() {
 			continue
 		}
 
-		// Now parse the feed contents into a set of items
-		fp := gofeed.NewParser()
-		feed, err := fp.ParseString(content)
-		if err != nil {
-			fmt.Printf("Error parsing %s contents: %s\n", monitor.feed, err.Error())
-			continue
-		}
+		// Fetch the feed-contents
+		err := feed.Update()
 
-		// For each entry in the feed
-		for _, i := range feed.Items {
+		if err != nil {
+			log.Printf("checkFeeds: Error fetching %s - %s\n",
+				Loaded[i].Url, err.Error())
 
-			// If we've not already notified about this one.
-			if isNew(monitor.feed, i) {
+			if Loaded[i].Retry--; Loaded[i].Retry == 0 {
+				log.Printf("checkFeeds: Temporally diable feed - %s\n", Loaded[i].Url)
+				feeds[i] = rss.Feed{}
+			}
+			// log.Printf("checkFeeds: refresh time - %s\n", feed.Refresh.Format(time.RFC3339))
 
-				// Trigger the notification
-				err := notify(monitor.hook, i)
+			continue
+		}
 
-				// and if that notification succeeded
-				// then record this item as having been
-				// processed successfully.
-				if err == nil {
-					recordSeen(monitor.feed, i)
-				}
-			}
+		if feed.Unread != 0 {
+			notify(i, feed.Items[0])
+			feed.Unread = 0
 		}
 	}
 }
 
-// notify actually submits the specified item to the remote webhook.
-//
+// notify submits the specified item to the remote webhook
+// or execute custom commands.
 // The RSS-item is submitted as a JSON-object.
-func notify(hook string, item *gofeed.Item) error {
-
-	// We'll post the item as a JSON object.
-	// So first of all encode it.
-	jsonValue, err := json.Marshal(item)
-	if err != nil {
-		fmt.Printf("notify: Failed to encode JSON:%s\n", err.Error())
-		return err
-	}
+func notify(i int, item *rss.Item) {
+	if Loaded[i].Method == "command" {
+		cmd := exec.Command(Loaded[i].Hook)
+		err := cmd.Start()
+		if err != nil {
+			log.Fatalf("notify: Failed to start command: %s\n", err.Error())
+		}
+	} else if Loaded[i].Method == "webhook" {
+		// We'll post the item as a JSON object.
+		// So first of all encode it.
+		jsonValue, err := json.Marshal(item)
+		if err != nil {
+			log.Fatalf("notify: Failed to encode JSON:%s\n", err.Error())
+		}
 
-	//
-	// Post to the specified hook URL.
-	//
-	res, err := http.Post(hook,
-		"application/json",
-		bytes.NewBuffer(jsonValue))
+		//
+		// Post to the specified hook URL.
+		//
+		res, err := http.Post(Loaded[i].Hook,
+			"application/json",
+			bytes.NewBuffer(jsonValue))
 
-	if err != nil {
-		fmt.Printf("notify: Failed to POST to %s - %s\n",
-			hook, err.Error())
-		return err
-	}
+		if err != nil {
+			log.Printf("notify: Failed to POST to %s - %s\n",
+				Loaded[i].Hook, err.Error())
+			return
+		}
 
-	//
-	// OK now we've submitted the post.
-	//
-	// We should retrieve the status-code + body, if the status-code
-	// is "odd" then we'll show them.
-	//
-	defer res.Body.Close()
-	_, err = ioutil.ReadAll(res.Body)
-	if err != nil {
-		return err
-	}
-	status := res.StatusCode
+		//
+		// OK now we've submitted the post.
+		//
+		// We should retrieve the status-code + body, if the status-code
+		// is "odd" then we'll show them.
+		//
+		defer res.Body.Close()
+		_, err = io.ReadAll(res.Body)
+		if err != nil {
+			log.Printf("notify: Failed to read response from %s - %s\n",
+				Loaded[i].Hook, err.Error())
+			return
+		}
+		status := res.StatusCode
 
-	if status != 200 {
-		fmt.Printf("notify: Warning - Status code was not 200: %d\n", status)
+		if status != 200 {
+			log.Printf("notify: Warning - Status code was not 200: %d\n", status)
+		}
 	}
-	return nil
 }
 
 // main is our entry-point
 func main() {
 
 	// Parse the command-line flags
-	config := flag.String("config", "", "The path to the configuration-file to read")
+	config := flag.String("config", "config.json", "The path to the configuration-file to read")
 	timeout := flag.Duration("timeout", 5*time.Second, "The timeout used for fetching the remote feeds")
+	schedule := flag.String("schedule", "@every 5m", "The cron schedule for fetching the remote feeds")
 	flag.Parse()
 
-	// Setup the default timeout.
-	Timeout = *timeout
-
-	if *config == "" {
-		fmt.Printf("Please specify a configuration-file to read\n")
-		return
+	// Setup the default timeout and TTL
+	rss.DefaultRefreshInterval = 10 * time.Second
+	rss.DefaultFetchFunc = func(url string) (*http.Response, error) {
+		client := &http.Client{Timeout: *timeout}
+		return client.Get(url)
 	}
 
 	//
@@ -277,22 +179,34 @@ func main() {
 	//
 	// Show the things we're monitoring
 	//
-	for _, ent := range Loaded {
-		fmt.Printf("Monitoring feed %s\nPosting to %s\n\n",
-			ent.feed, ent.hook)
-	}
+	// for _, ent := range Loaded {
+	// 	log.Printf("Monitoring feed %s\nPosting to %s\n\n",
+	// 		ent.Feed, ent.Hook)
+	// }
 
 	//
 	// Make the initial scan of feeds immediately to avoid waiting too
 	// long for the first time.
 	//
-	checkFeeds()
+	feeds := make([]rss.Feed, len(Loaded))
+	for i, ent := range Loaded {
+		go func(i int, ent RSSEntry) {
+			feed, err := rss.Fetch(ent.Url)
+			if err != nil {
+				log.Printf("main: Error fetching %s - %s\n",
+					ent.Url, err.Error())
+				return
+			}
+			// feed.Unread = 0
+			feeds[i] = *feed
+		}(i, ent)
+	}
 
 	//
-	// Now repeat that every five minutes.
+	// Now repeat that every five minutes or in custom schedule.
 	//
 	c := cron.New()
-	c.AddFunc("@every 5m", func() { checkFeeds() })
+	c.AddFunc(*schedule, func() { checkFeeds(feeds) })
 	c.Start()
 
 	//

+ 0 - 28
sample.cfg

@@ -1,28 +0,0 @@
-#
-# This is the sample configuration file for rss2hook.
-#
-# rss2hook is designed to make a HTTP-POST to a webhook
-# when a new RSS item appears in a feed.
-#
-# There are two things to specify:
-#
-#   * The URL of the RSS feed to monitor.
-#
-#   * The corresponding end-point to make the POST request to.
-#
-# In this configuration-file they're specified as pairs like so:
-#
-#   RSS = HOOK
-#
-
-
-#
-# The following example reads from my blog, and posts to the sample
-# webhook-handler as included in the repository:
-#
-https://blog.steve.fi/index.rss = http://localhost:8080/
-
-#
-# We have a second feed here, containing news stories from the BBC
-#
-http://feeds.bbci.co.uk/news/rss.xml = http://localhost:8080/