Projeto bot pontos
This commit is contained in:
165
backend/internal/adapters/telegram/client.go
Normal file
165
backend/internal/adapters/telegram/client.go
Normal file
@@ -0,0 +1,165 @@
|
||||
// Package telegram é um cliente mínimo da Bot API (long polling e envio de
|
||||
// mensagens), portado do Modelo. Não usa bibliotecas externas.
|
||||
package telegram
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"botpontos/internal/domain"
|
||||
)
|
||||
|
||||
const apiBase = "https://api.telegram.org"
|
||||
|
||||
type Client struct {
|
||||
token string
|
||||
http *http.Client
|
||||
base string
|
||||
}
|
||||
|
||||
// NewClient aceita token vazio: nesse caso Send devolve domain.ErrNotConfigured.
|
||||
func NewClient(token string) *Client {
|
||||
return &Client{token: strings.TrimSpace(token), http: &http.Client{Timeout: 40 * time.Second}, base: apiBase}
|
||||
}
|
||||
|
||||
func (c *Client) Configured() bool { return c.token != "" }
|
||||
|
||||
type Update struct {
|
||||
UpdateID int64 `json:"update_id"`
|
||||
Message *Message `json:"message"`
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
Chat Chat `json:"chat"`
|
||||
From *From `json:"from"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type Chat struct {
|
||||
ID int64 `json:"id"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type From struct {
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
// Name é o nome de exibição usado no cadastro.
|
||||
func (f *From) Name() string {
|
||||
if f == nil {
|
||||
return "Sem nome"
|
||||
}
|
||||
name := strings.TrimSpace(f.FirstName + " " + f.LastName)
|
||||
if name == "" {
|
||||
name = f.Username
|
||||
}
|
||||
if name == "" {
|
||||
name = "Sem nome"
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
type response[T any] struct {
|
||||
OK bool `json:"ok"`
|
||||
Result T `json:"result"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
func (r *response[T]) status() (bool, string) { return r.OK, r.Description }
|
||||
|
||||
type sendResult struct {
|
||||
MessageID int64 `json:"message_id"`
|
||||
}
|
||||
|
||||
// apiResponse é satisfeita por *response[T] de qualquer tipo.
|
||||
type apiResponse interface {
|
||||
status() (ok bool, description string)
|
||||
}
|
||||
|
||||
// GetUpdates faz long polling por até timeout segundos.
|
||||
func (c *Client) GetUpdates(ctx context.Context, offset int64, timeout int) ([]Update, error) {
|
||||
if !c.Configured() {
|
||||
return nil, domain.ErrNotConfigured
|
||||
}
|
||||
query := url.Values{}
|
||||
query.Set("timeout", strconv.Itoa(timeout))
|
||||
query.Set("offset", strconv.FormatInt(offset, 10))
|
||||
query.Set("allowed_updates", `["message"]`)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.endpoint("getUpdates")+"?"+query.Encode(), nil)
|
||||
if err != nil {
|
||||
return nil, c.redact(err)
|
||||
}
|
||||
var body response[[]Update]
|
||||
if err := c.do(req, &body); err != nil {
|
||||
return nil, fmt.Errorf("telegram getUpdates: %w", err)
|
||||
}
|
||||
return body.Result, nil
|
||||
}
|
||||
|
||||
// Send envia uma mensagem de texto e devolve o message_id.
|
||||
func (c *Client) Send(ctx context.Context, chatID int64, text string) (int64, error) {
|
||||
if !c.Configured() {
|
||||
return 0, domain.ErrNotConfigured
|
||||
}
|
||||
form := url.Values{}
|
||||
form.Set("chat_id", strconv.FormatInt(chatID, 10))
|
||||
form.Set("text", text)
|
||||
form.Set("disable_web_page_preview", "true")
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.endpoint("sendMessage"), strings.NewReader(form.Encode()))
|
||||
if err != nil {
|
||||
return 0, c.redact(err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
var body response[sendResult]
|
||||
if err := c.do(req, &body); err != nil {
|
||||
return 0, fmt.Errorf("telegram sendMessage: %w", err)
|
||||
}
|
||||
return body.Result.MessageID, nil
|
||||
}
|
||||
|
||||
func (c *Client) do(req *http.Request, out apiResponse) error {
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
return c.redact(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if err := json.NewDecoder(resp.Body).Decode(out); err != nil {
|
||||
return fmt.Errorf("HTTP %d com resposta ilegível", resp.StatusCode)
|
||||
}
|
||||
if ok, description := out.status(); !ok {
|
||||
if description == "" {
|
||||
description = fmt.Sprintf("HTTP %d", resp.StatusCode)
|
||||
}
|
||||
return errors.New(description)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) endpoint(method string) string {
|
||||
return c.base + "/bot" + c.token + "/" + method
|
||||
}
|
||||
|
||||
// redact evita que o token (que faz parte da URL) apareça em logs e auditoria.
|
||||
func (c *Client) redact(err error) error {
|
||||
var urlErr *url.Error
|
||||
if errors.As(err, &urlErr) {
|
||||
err = urlErr.Err
|
||||
}
|
||||
if c.token == "" {
|
||||
return err
|
||||
}
|
||||
return errors.New(strings.ReplaceAll(err.Error(), c.token, "<token>"))
|
||||
}
|
||||
Reference in New Issue
Block a user