package app import ( "bytes" "context" "log/slog" "os" "regexp" "strings" "text/template" "time" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" "github.com/mmcdole/gofeed" "gitrepo.ru/neonxp/rss2world/templates" ) type App struct { config *Config telegram *tgbotapi.BotAPI templates *template.Template } func New(cfg *Config) (*App, error) { bot, err := tgbotapi.NewBotAPI(cfg.Telegram.BotToken) if err != nil { return nil, err } tpl, err := template.ParseFS(templates.Templates, "*.gotmpl") if err != nil { return nil, err } return &App{ config: cfg, telegram: bot, templates: tpl, }, nil } func (a *App) Run(ctx context.Context) error { ticker := time.NewTicker(a.config.RSS.CheckInterval) for { select { case <-ctx.Done(): return nil case <-ticker.C: if err := a.iteration(); err != nil { slog.ErrorContext(ctx, "failed iteration", slog.Any("error", err)) } } } } func (a *App) iteration() error { seq, err := a.readSeqFile() if os.IsNotExist(err) { seq = "" err = nil } if err != nil { return err } items, err := a.findNewItems(seq) if err != nil || len(items) == 0 { return err } for i := len(items) - 1; i >= 0; i-- { if err := a.processItem(items[i]); err != nil { return err } if err := a.writeSeqFile(items[i].GUID); err != nil { return err } } return nil } func (a *App) processItem(item *gofeed.Item) error { buf := bytes.NewBufferString("") item.Content = trimHtml(item.Description) if err := a.templates.ExecuteTemplate(buf, "telegram", item); err != nil { return err } str := buf.String() for _, group := range a.config.Telegram.TargetGroups { msg := tgbotapi.NewMessage(group, str) msg.ParseMode = tgbotapi.ModeHTML if _, err := a.telegram.Send(msg); err != nil { return err } } return nil } func (a *App) readSeqFile() (string, error) { seqVal, err := os.ReadFile(a.config.RSS.SeqFile) if err != nil { return "", err } return string(seqVal), nil } func (a *App) writeSeqFile(seqVal string) error { return os.WriteFile(a.config.RSS.SeqFile, []byte(seqVal), 0o644) } func (a *App) findNewItems(from string) ([]*gofeed.Item, error) { fp := gofeed.NewParser() feed, err := fp.ParseURL(a.config.RSS.URL) if err != nil { return nil, err } out := make([]*gofeed.Item, 0, len(feed.Items)) for _, item := range feed.Items { if item.GUID == from { break } out = append(out, item) } return out, nil } func trimHtml(src string) string { // Convert all HTML tags to lowercase re, _ := regexp.Compile("\\<[\\S\\s]+?\\>") src = re.ReplaceAllStringFunc(src, strings.ToLower) // Remove STYLE re, _ = regexp.Compile("\\") src = re.ReplaceAllString(src, "") // Remove SCRIPT re, _ = regexp.Compile("\\") src = re.ReplaceAllString(src, "") // Remove all HTML code in angle brackets and replace them with newline characters re, _ = regexp.Compile("\\<[\\S\\s]+?\\>") src = re.ReplaceAllString(src, "\n") // Remove consecutive newlines re, _ = regexp.Compile("\\s{2,}") src = re.ReplaceAllString(src, "\n") return strings.TrimSpace(src) }