Files
telegram-gateway/telegram.go
Gabriel Radureau 515b407db4
All checks were successful
Docker Build / build-and-push-image (push) Successful in 56s
docs: align ADR path references to doc/adr (singular)
Mirror of factory#8 path correction. Updates Gitea URLs in AUTH.md /
HOWTO_ADD_BOT.md and the '// Voir factory/...' header comments in code.
2026-05-09 14:26:12 +02:00

204 lines
5.7 KiB
Go

package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"time"
)
const telegramAPIBase = "https://api.telegram.org"
type TelegramClient struct {
httpClient *http.Client
apiBase string
}
func NewTelegramClient() *TelegramClient {
return &TelegramClient{
httpClient: &http.Client{Timeout: 10 * time.Second},
apiBase: telegramAPIBase,
}
}
type SendMessageParams struct {
ChatID int64 `json:"chat_id"`
Text string `json:"text"`
}
type apiResponse struct {
OK bool `json:"ok"`
Description string `json:"description,omitempty"`
Result json.RawMessage `json:"result,omitempty"`
ErrorCode int `json:"error_code,omitempty"`
}
func (c *TelegramClient) SendMessage(ctx context.Context, token string, params SendMessageParams) error {
body, err := json.Marshal(params)
if err != nil {
return err
}
endpoint := fmt.Sprintf("%s/bot%s/sendMessage", c.apiBase, token)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
var ar apiResponse
if jerr := json.Unmarshal(respBody, &ar); jerr != nil {
return fmt.Errorf("decode telegram response (status %d): %w", resp.StatusCode, jerr)
}
if !ar.OK {
return fmt.Errorf("telegram api error (code=%d): %s", ar.ErrorCode, ar.Description)
}
return nil
}
type SetWebhookParams struct {
URL string `json:"url"`
SecretToken string `json:"secret_token,omitempty"`
DropPendingUpdates bool `json:"drop_pending_updates,omitempty"`
AllowedUpdates []string `json:"allowed_updates,omitempty"`
MaxConnections int `json:"max_connections,omitempty"`
}
func (c *TelegramClient) SetWebhook(ctx context.Context, token string, params SetWebhookParams) error {
body, err := json.Marshal(params)
if err != nil {
return err
}
endpoint := fmt.Sprintf("%s/bot%s/setWebhook", c.apiBase, token)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
var ar apiResponse
if jerr := json.Unmarshal(respBody, &ar); jerr != nil {
return fmt.Errorf("decode telegram response (status %d): %w", resp.StatusCode, jerr)
}
if !ar.OK {
return fmt.Errorf("telegram setWebhook error (code=%d): %s", ar.ErrorCode, ar.Description)
}
return nil
}
func (c *TelegramClient) DeleteWebhook(ctx context.Context, token string) error {
endpoint := fmt.Sprintf("%s/bot%s/deleteWebhook", c.apiBase, token)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, nil)
if err != nil {
return err
}
resp, err := c.httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
var ar apiResponse
if jerr := json.Unmarshal(respBody, &ar); jerr != nil {
return fmt.Errorf("decode telegram response (status %d): %w", resp.StatusCode, jerr)
}
if !ar.OK {
return fmt.Errorf("telegram deleteWebhook error (code=%d): %s", ar.ErrorCode, ar.Description)
}
return nil
}
type WebhookInfo struct {
URL string `json:"url"`
HasCustomCert bool `json:"has_custom_certificate"`
PendingUpdateCount int `json:"pending_update_count"`
LastErrorDate int64 `json:"last_error_date,omitempty"`
LastErrorMessage string `json:"last_error_message,omitempty"`
}
func (c *TelegramClient) GetWebhookInfo(ctx context.Context, token string) (*WebhookInfo, error) {
endpoint := fmt.Sprintf("%s/bot%s/getWebhookInfo", c.apiBase, token)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
var ar apiResponse
if jerr := json.Unmarshal(respBody, &ar); jerr != nil {
return nil, fmt.Errorf("decode telegram response: %w", jerr)
}
if !ar.OK {
return nil, fmt.Errorf("telegram getWebhookInfo error: %s", ar.Description)
}
var info WebhookInfo
if jerr := json.Unmarshal(ar.Result, &info); jerr != nil {
return nil, fmt.Errorf("decode webhook info: %w", jerr)
}
return &info, nil
}
type DeleteMessageParams struct {
ChatID int64 `json:"chat_id"`
MessageID int64 `json:"message_id"`
}
// DeleteMessage removes a message from a chat. Used as best-effort replay
// defense after a successful /auth (we delete the message that contained
// the secret). See factory/doc/adr/20260509-telegram-gateway-auth.md.
func (c *TelegramClient) DeleteMessage(ctx context.Context, token string, chatID, messageID int64) error {
body, err := json.Marshal(DeleteMessageParams{ChatID: chatID, MessageID: messageID})
if err != nil {
return err
}
endpoint := fmt.Sprintf("%s/bot%s/deleteMessage", c.apiBase, token)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
var ar apiResponse
if jerr := json.Unmarshal(respBody, &ar); jerr != nil {
return fmt.Errorf("decode telegram response (status %d): %w", resp.StatusCode, jerr)
}
if !ar.OK {
return fmt.Errorf("telegram deleteMessage error (code=%d): %s", ar.ErrorCode, ar.Description)
}
return nil
}
// botBuildURL is exposed for tests; not used directly.
var _ = url.Parse