package app import ( "bytes" "context" "log/slog" "os" "strings" "text/template" "time" "git.neonxp.ru/posse/templates" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" "github.com/microcosm-cc/bluemonday" "github.com/mmcdole/gofeed" ) 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) if err := a.iteration(); err != nil { slog.ErrorContext(ctx, "failed iteration", slog.Any("error", err)) } 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("") p := bluemonday.Policy{} p.AllowStandardURLs() p.AllowElements("b", "i", "code", "u", "strike", "pre", "br", "a") p.AllowNoAttrs().Globally() p.AllowAttrs("href").OnElements("a") s := strings.ReplaceAll(item.Description, "", "") s = strings.ReplaceAll(s, "", "") s = strings.ReplaceAll(s, "", "") s = strings.ReplaceAll(s, "", "") s = strings.ReplaceAll(s, "
", "\n") s = strings.ReplaceAll(s, "<p>", "") s = strings.ReplaceAll(s, "</p>", "") item.Content = p.Sanitize(s) if err := a.templates.ExecuteTemplate(buf, "telegram", item); err != nil { return err } str := "" str2 := buf.String() for str != str2 { str = str2 str2 = strings.ReplaceAll(str, "\n\n", "\n") str2 = strings.ReplaceAll(str2, " ", " ") str2 = strings.Trim(str2, " \t\n") } for _, group := range a.config.Telegram.TargetGroups { switch { case item.Image != nil: msg := tgbotapi.NewPhoto(group, tgbotapi.FileURL(item.Image.URL)) msg.ParseMode = tgbotapi.ModeHTML msg.Caption = str if _, err := a.telegram.Send(msg); err != nil { return err } default: 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 }