package target import ( "context" "errors" "fmt" "io" "log/slog" "net/http" "net/url" "os" "time" "github.com/microcosm-cc/bluemonday" cm "go.neonxp.ru/conf/model" "go.neonxp.ru/pose/internal/model" ) var ( ErrNoToken = errors.New("no api token") ErrNoGroup = errors.New("no group") ) const telegramRequestTimeout = 30 * time.Second const telegramMaxItemsChan = 32 type Telegram struct { logger *slog.Logger apiToken string group string client *http.Client policy *bluemonday.Policy } func NewTelegram(cfg cm.Group, logger *slog.Logger) (*Telegram, error) { token := cfg.Get("token").StringExt("", os.LookupEnv) if token == "" { return nil, ErrNoToken } group := cfg.Get("group").StringExt("", os.LookupEnv) if group == "" { return nil, ErrNoGroup } pol := bluemonday.NewPolicy() pol.AllowAttrs("href").OnElements("a") pol.AllowAttrs("class").OnElements("span") pol.AllowElements("p", "br", "b", "strong", "i", "em", "u", "ins", "s", "strike", "del", "code", "pre", "blockquote") return &Telegram{ logger: logger, apiToken: token, group: group, client: &http.Client{Timeout: telegramRequestTimeout}, policy: pol, }, nil } func (t *Telegram) Send(ctx context.Context) chan<- model.Item { ch := make(chan model.Item, telegramMaxItemsChan) go func() { defer close(ch) for { select { case <-ctx.Done(): return case item := <-ch: if err := t.sendMessage(item); err != nil { t.logger.ErrorContext(ctx, "failed send feed item to telegram", slog.Any("err", err)) continue } t.logger.InfoContext(ctx, "send item to telegram", slog.String("id", item.ID)) } } }() return ch } func (t *Telegram) sendMessage(it model.Item) error { sendMessageURL := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", t.apiToken) message := it.BuildMessage() message = t.policy.Sanitize(message) message = processHTML(message) params := url.Values{} params.Set("chat_id", t.group) params.Set("text", message) params.Set("parse_mode", "HTML") resp, err := t.client.PostForm(sendMessageURL, params) if err != nil { return err } if resp.StatusCode < 200 || resp.StatusCode >= 300 { msg, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("failed read error body: %w", err) } return fmt.Errorf( "invalid status code %d (%s): %s", resp.StatusCode, resp.Status, string(msg), ) } return nil }