diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..0e6fe52 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "sn-rss-to-tg", + "type": "go", + "request": "launch", + "mode": "debug", + "program": "." + } + ] +} diff --git a/go.mod b/go.mod index 345b1d6..3bc30c3 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module gitlab.com/ekzyis/stackernews_rss-to-tg go 1.20 require ( + github.com/dustin/go-humanize v1.0.1 // indirect github.com/joho/godotenv v1.5.1 // indirect github.com/namsral/flag v1.7.4-pre // indirect ) diff --git a/go.sum b/go.sum index 4c16f8b..1120682 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/namsral/flag v1.7.4-pre h1:b2ScHhoCUkbsq0d2C15Mv+VU8bl8hAXV8arnWiOHNZs= diff --git a/main.go b/main.go index da29a2c..1e87853 100644 --- a/main.go +++ b/main.go @@ -1,4 +1,53 @@ package main -func main() { +import ( + "log" + "time" +) + +func WaitUntilNextUpdate() { + now := time.Now() + dur := now.Truncate(time.Hour).Add(time.Hour).Sub(now) + log.Println("sleeping for", dur.Round(time.Second)) + time.Sleep(dur) +} + +func contains(rss *Rss, a Item) bool { + if rss == nil { + return false + } + for _, x := range rss.Channel.Items { + if x.Title == a.Title { + return true + } + } + return false +} + +func main() { + var oldRss *Rss + for { + rss, err := FetchStackerNewsRssFeed() + if err != nil { + log.Println(err) + WaitUntilNextUpdate() + continue + } + + for _, item := range rss.Channel.Items { + // TODO: We ignore errors during sending items to telegram. + // This means these items will be missed since they will be skipped in the next run + if contains(oldRss, item) { + continue + } + err := SendItemToTelegram(&item) + if err != nil { + log.Println(err) + continue + } + } + + oldRss = rss + WaitUntilNextUpdate() + } } diff --git a/rss.go b/rss.go new file mode 100644 index 0000000..348355d --- /dev/null +++ b/rss.go @@ -0,0 +1,69 @@ +package main + +import ( + "encoding/xml" + "fmt" + "log" + "net/http" + "time" +) + +type Item struct { + Guid string `xml:"guid"` + Title string `xml:"title"` + Link string `xml:"link"` + Description string `xml:"description"` + PubDate RssDate `xml:"pubDate"` +} + +type Channel struct { + Title string `xml:"title"` + Description string `xml:"description"` + Link string `xml:"link"` + Items []Item `xml:"item"` + LastBuildDate RssDate `xml:"lastBuildDate"` +} + +type Rss struct { + Channel Channel `xml:"channel"` +} + +type RssDate struct { + time.Time +} + +func (c *RssDate) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + var v string + dateFormat := "Mon, 02 Jan 2006 15:04:05 GMT" + d.DecodeElement(&v, &start) + parse, err := time.Parse(dateFormat, v) + if err != nil { + return err + } + *c = RssDate{parse} + return nil +} + +var ( + StackerNewsRssFeedUrl = "https://stacker.news/rss" +) + +func FetchStackerNewsRssFeed() (*Rss, error) { + resp, err := http.Get(StackerNewsRssFeedUrl) + if err != nil { + err = fmt.Errorf("error fetching RSS feed: %w\n", err) + log.Println(err) + return nil, err + } + defer resp.Body.Close() + + var rss Rss + err = xml.NewDecoder(resp.Body).Decode(&rss) + if err != nil { + err = fmt.Errorf("error decoding RSS feed XML: %w\n", err) + return nil, err + } + + return &rss, nil + +} diff --git a/tg.go b/tg.go index 5d0e173..e2f86a5 100644 --- a/tg.go +++ b/tg.go @@ -4,7 +4,9 @@ import ( "fmt" "log" "net/http" + "net/url" + "github.com/dustin/go-humanize" "github.com/joho/godotenv" "github.com/namsral/flag" ) @@ -30,14 +32,24 @@ func init() { } } -func SendTextToTelegram(text string) { - url := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage?chat_id=%d&text=%s", BotToken, ChatId, text) +func SendItemToTelegram(item *Item) error { + text := fmt.Sprintf("%s (%s)\n", item.Title, humanize.Time(item.PubDate.Time)) + linkItem := item.Link != item.Guid + if linkItem { + text += fmt.Sprintf("[Link](%s) ", item.Link) + } + text += fmt.Sprintf("[Comments](%s)", item.Guid) + return SendTextToTelegram(text) +} + +func SendTextToTelegram(text string) error { + url := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage?parse_mode=markdown&disable_web_page_preview=true&chat_id=%d&text=%s", BotToken, ChatId, url.QueryEscape(text)) resp, err := http.Get(url) if err != nil { err = fmt.Errorf("error during GET %s: %w", url, err) - log.Println(err) - return + return err } defer resp.Body.Close() log.Printf("GET %s: %d\n", url, resp.StatusCode) + return nil }