aboutsummaryrefslogtreecommitdiff
path: root/internal/target/telegram.go
diff options
context:
space:
mode:
author2026-03-14 00:44:19 +0300
committer2026-03-14 00:44:19 +0300
commite5d6f4c02b757c83244ba5e04fead08623a27299 (patch)
tree5b5babb9887cafa3dbc165928dc2b0fd65265bda /internal/target/telegram.go
downloadpose-e5d6f4c02b757c83244ba5e04fead08623a27299.tar.gz
pose-e5d6f4c02b757c83244ba5e04fead08623a27299.tar.bz2
pose-e5d6f4c02b757c83244ba5e04fead08623a27299.tar.xz
pose-e5d6f4c02b757c83244ba5e04fead08623a27299.zip
начальный коммитHEADmaster
Diffstat (limited to 'internal/target/telegram.go')
-rw-r--r--internal/target/telegram.go112
1 files changed, 112 insertions, 0 deletions
diff --git a/internal/target/telegram.go b/internal/target/telegram.go
new file mode 100644
index 0000000..f4d4aff
--- /dev/null
+++ b/internal/target/telegram.go
@@ -0,0 +1,112 @@
+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
+}