Initial commit.

This commit is contained in:
2026-08-05 12:06:46 +05:00
commit f37a1422e2
21 changed files with 1026 additions and 0 deletions
+102
View File
@@ -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
View File
@@ -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
}
+64
View File
@@ -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
}