Initial commit.
This commit is contained in:
+102
@@ -0,0 +1,102 @@
|
||||
package rss
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/mmcdole/gofeed"
|
||||
"github.com/prodigeris/html2text"
|
||||
"go.dev.pztrn.name/rsser/fixers"
|
||||
)
|
||||
|
||||
// Parse parses RSS feeds and produce output in outputs.
|
||||
func (r *RSS) Parse() {
|
||||
for _, feed := range r.cfg.RSS.Feeds {
|
||||
ctx, cancelFunc := context.WithTimeout(
|
||||
context.Background(),
|
||||
time.Second*time.Duration(r.cfg.RSS.HTTPClient.RequestTimeoutSeconds),
|
||||
)
|
||||
//revive:disable:defer
|
||||
defer cancelFunc()
|
||||
|
||||
parser := gofeed.NewParser()
|
||||
|
||||
parsedFeedData, err := parser.ParseURLWithContext(feed.URL, ctx)
|
||||
if err != nil {
|
||||
slog.Error("Failed to parse feed!", "name", feed.Name, "error", err.Error())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
feedURL, _ := url.Parse(feed.URL)
|
||||
|
||||
for _, item := range parsedFeedData.Items {
|
||||
slog.Info(
|
||||
"Processing feed item...",
|
||||
"domain", feedURL.Host,
|
||||
"guid", item.GUID,
|
||||
)
|
||||
|
||||
itemWasProcessed, err := r.checkItemWasProcessed(feedURL.Host, item.GUID)
|
||||
if err != nil {
|
||||
slog.Error(
|
||||
"Failed to check item processing status!",
|
||||
"domain", feedURL.Host,
|
||||
"guid", item.GUID,
|
||||
"error", err.Error(),
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if itemWasProcessed {
|
||||
slog.Info(
|
||||
"Feed item already processed, skipping.",
|
||||
"domain", feedURL.Host,
|
||||
"guid", item.GUID,
|
||||
)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
description, err := html2text.FromString(item.Description, html2text.Options{
|
||||
PrettyTables: true,
|
||||
})
|
||||
if err != nil {
|
||||
description = item.Description
|
||||
}
|
||||
|
||||
title, fixedDescription := fixers.Fix(feedURL.Host, item.Title, description)
|
||||
|
||||
for _, output := range r.outputs {
|
||||
if feed.Output == output.Name() {
|
||||
if err := output.Do(feed.Destination, feed.Name, item.Link, title, fixedDescription); err != nil {
|
||||
slog.Error(
|
||||
"Failed to process feed item with outputter!",
|
||||
"feed", feed.Name,
|
||||
"guid", item.GUID,
|
||||
"error", err.Error(),
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if err := r.addProcessedItem(feedURL.Host, item.GUID); err != nil {
|
||||
slog.Error(
|
||||
"Failed to save item processing status!",
|
||||
"domain", feedURL.Host,
|
||||
"guid", item.GUID,
|
||||
"error", err.Error(),
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package rss
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"go.dev.pztrn.name/rsser/configuration"
|
||||
"go.dev.pztrn.name/rsser/outputs"
|
||||
)
|
||||
|
||||
var (
|
||||
errNoOutputs = errors.New("no outputs")
|
||||
errRSS = errors.New("RSS controller")
|
||||
)
|
||||
|
||||
// RSS is an RSS feeds parsing controlling structure.
|
||||
type RSS struct {
|
||||
cfg *configuration.Config
|
||||
httpClient *http.Client
|
||||
outputs []outputs.Output
|
||||
}
|
||||
|
||||
// New creates new RSS parsing instance.
|
||||
func New(cfg *configuration.Config, outs []outputs.Output) (*RSS, error) {
|
||||
rss := &RSS{
|
||||
cfg: cfg,
|
||||
outputs: outs,
|
||||
}
|
||||
|
||||
if err := rss.initialize(); err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", errRSS, err)
|
||||
}
|
||||
|
||||
return rss, nil
|
||||
}
|
||||
|
||||
func (r *RSS) initialize() error {
|
||||
if len(r.outputs) == 0 {
|
||||
return fmt.Errorf("initialize: %w", errNoOutputs)
|
||||
}
|
||||
|
||||
slog.Info(
|
||||
"HTTP client configuration.",
|
||||
"request_timeout_seconds", r.cfg.RSS.HTTPClient.RequestTimeoutSeconds,
|
||||
)
|
||||
|
||||
r.httpClient = &http.Client{
|
||||
Timeout: time.Second * time.Duration(r.cfg.RSS.HTTPClient.RequestTimeoutSeconds),
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package rss
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var processedItemsDir string
|
||||
|
||||
//nolint:gochecknoinits
|
||||
func init() {
|
||||
userHomeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
panic("Failed to get user's home directory: " + err.Error())
|
||||
}
|
||||
|
||||
processedItemsDir = strings.Replace("~/.rsser", "~", userHomeDir, 1)
|
||||
|
||||
if err := os.MkdirAll(processedItemsDir, 0o755); err != nil {
|
||||
panic("Failed to create ~/.rsser: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RSS) addProcessedItem(domain, guid string) error {
|
||||
file, err := os.OpenFile(
|
||||
filepath.Join(processedItemsDir, domain),
|
||||
os.O_APPEND|os.O_CREATE|os.O_WRONLY,
|
||||
0o644,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("add processed item: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if _, err := file.WriteString(guid + "\n"); err != nil {
|
||||
return fmt.Errorf("add processed item: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RSS) checkItemWasProcessed(domain, guid string) (bool, error) {
|
||||
fileDataBytes, err := os.ReadFile(filepath.Join(processedItemsDir, domain))
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return false, fmt.Errorf("check item processing: %w", err)
|
||||
}
|
||||
|
||||
fileData := string(fileDataBytes)
|
||||
|
||||
for line := range strings.Lines(fileData) {
|
||||
if line == guid+"\n" {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
Reference in New Issue
Block a user