chore: import clean Agent Desk snapshot (3/7)
This commit is contained in:
137
internal/telegram/client.go
Normal file
137
internal/telegram/client.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package telegram
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const defaultBaseURL = "https://api.telegram.org"
|
||||
|
||||
type Client struct {
|
||||
token string
|
||||
baseURL string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
func NewClient(token string) *Client {
|
||||
return &Client{
|
||||
token: strings.TrimSpace(token),
|
||||
baseURL: defaultBaseURL,
|
||||
httpClient: &http.Client{Timeout: 15 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) SetBaseURL(url string) {
|
||||
if strings.TrimSpace(url) != "" {
|
||||
c.baseURL = strings.TrimRight(strings.TrimSpace(url), "/")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) GetMe(ctx context.Context) (*User, error) {
|
||||
var resp APIResponse[User]
|
||||
if err := c.doRequest(ctx, "getMe", nil, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !resp.OK {
|
||||
return nil, fmt.Errorf("telegram getMe failed (%d): %s", resp.ErrorCode, resp.Description)
|
||||
}
|
||||
return &resp.Result, nil
|
||||
}
|
||||
|
||||
func (c *Client) SetWebhook(ctx context.Context, req SetWebhookRequest) error {
|
||||
var resp APIResponse[bool]
|
||||
if err := c.doRequest(ctx, "setWebhook", req, &resp); err != nil {
|
||||
return err
|
||||
}
|
||||
if !resp.OK {
|
||||
return fmt.Errorf("telegram setWebhook failed (%d): %s", resp.ErrorCode, resp.Description)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) DeleteWebhook(ctx context.Context) error {
|
||||
var resp APIResponse[bool]
|
||||
if err := c.doRequest(ctx, "deleteWebhook", nil, &resp); err != nil {
|
||||
return err
|
||||
}
|
||||
if !resp.OK {
|
||||
return fmt.Errorf("telegram deleteWebhook failed (%d): %s", resp.ErrorCode, resp.Description)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) GetWebhookInfo(ctx context.Context) (*WebhookInfo, error) {
|
||||
var resp APIResponse[WebhookInfo]
|
||||
if err := c.doRequest(ctx, "getWebhookInfo", nil, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !resp.OK {
|
||||
return nil, fmt.Errorf("telegram getWebhookInfo failed (%d): %s", resp.ErrorCode, resp.Description)
|
||||
}
|
||||
return &resp.Result, nil
|
||||
}
|
||||
|
||||
func (c *Client) SendMessage(ctx context.Context, req SendMessageRequest) (*Message, error) {
|
||||
if strings.TrimSpace(req.Text) == "" {
|
||||
return nil, fmt.Errorf("telegram message text is required")
|
||||
}
|
||||
if req.ChatID == 0 {
|
||||
return nil, fmt.Errorf("telegram chat_id is required")
|
||||
}
|
||||
|
||||
var resp APIResponse[Message]
|
||||
if err := c.doRequest(ctx, "sendMessage", req, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !resp.OK {
|
||||
return nil, fmt.Errorf("telegram sendMessage failed (%d): %s", resp.ErrorCode, resp.Description)
|
||||
}
|
||||
return &resp.Result, nil
|
||||
}
|
||||
|
||||
func (c *Client) doRequest(ctx context.Context, method string, payload any, result any) error {
|
||||
if c.token == "" {
|
||||
return fmt.Errorf("telegram bot token is required")
|
||||
}
|
||||
|
||||
endpoint := fmt.Sprintf("%s/bot%s/%s", c.baseURL, c.token, method)
|
||||
|
||||
var bodyReader io.Reader
|
||||
if payload != nil {
|
||||
bodyBytes, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal telegram request failed: %w", err)
|
||||
}
|
||||
bodyReader = bytes.NewBuffer(bodyBytes)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bodyReader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create telegram request failed: %w", err)
|
||||
}
|
||||
if payload != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
res, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("telegram http request failed: %w", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
bodyBytes, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read telegram response failed: %w", err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(bodyBytes, result); err != nil {
|
||||
return fmt.Errorf("unmarshal telegram response failed: %w (body: %s)", err, string(bodyBytes))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
124
internal/telegram/client_test.go
Normal file
124
internal/telegram/client_test.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package telegram
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTelegramClient_GetMe(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/bot123456:ABC-DEF/getMe" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(APIResponse[User]{
|
||||
OK: true,
|
||||
Result: User{
|
||||
ID: 987654321,
|
||||
IsBot: true,
|
||||
FirstName: "Crove Desk Bot",
|
||||
Username: "crove_desk_bot",
|
||||
},
|
||||
})
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
client := NewClient("123456:ABC-DEF")
|
||||
client.SetBaseURL(ts.URL)
|
||||
|
||||
user, err := client.GetMe(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("GetMe failed: %v", err)
|
||||
}
|
||||
if user.Username != "crove_desk_bot" || !user.IsBot {
|
||||
t.Fatalf("unexpected user: %+v", user)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTelegramClient_SendMessage(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/bot123456:ABC-DEF/sendMessage" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
var req SendMessageRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if req.ChatID != 112233 || req.Text != "Xin chào từ Crove Desk!" {
|
||||
http.Error(w, "invalid params", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(APIResponse[Message]{
|
||||
OK: true,
|
||||
Result: Message{
|
||||
MessageID: 101,
|
||||
Chat: Chat{
|
||||
ID: 112233,
|
||||
Type: "private",
|
||||
},
|
||||
Text: req.Text,
|
||||
},
|
||||
})
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
client := NewClient("123456:ABC-DEF")
|
||||
client.SetBaseURL(ts.URL)
|
||||
|
||||
msg, err := client.SendMessage(context.Background(), SendMessageRequest{
|
||||
ChatID: 112233,
|
||||
Text: "Xin chào từ Crove Desk!",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("SendMessage failed: %v", err)
|
||||
}
|
||||
if msg.MessageID != 101 || msg.Text != "Xin chào từ Crove Desk!" {
|
||||
t.Fatalf("unexpected sent message: %+v", msg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTelegramClient_SetAndDeleteWebhook(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/bot123456:ABC-DEF/setWebhook":
|
||||
_ = json.NewEncoder(w).Encode(APIResponse[bool]{OK: true, Result: true})
|
||||
case "/bot123456:ABC-DEF/deleteWebhook":
|
||||
_ = json.NewEncoder(w).Encode(APIResponse[bool]{OK: true, Result: true})
|
||||
case "/bot123456:ABC-DEF/getWebhookInfo":
|
||||
_ = json.NewEncoder(w).Encode(APIResponse[WebhookInfo]{
|
||||
OK: true,
|
||||
Result: WebhookInfo{
|
||||
URL: "https://desk.crove.com/api/third/telegram/webhook/ch_123",
|
||||
},
|
||||
})
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
client := NewClient("123456:ABC-DEF")
|
||||
client.SetBaseURL(ts.URL)
|
||||
|
||||
err := client.SetWebhook(context.Background(), SetWebhookRequest{
|
||||
URL: "https://desk.crove.com/api/third/telegram/webhook/ch_123",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("SetWebhook failed: %v", err)
|
||||
}
|
||||
|
||||
info, err := client.GetWebhookInfo(context.Background())
|
||||
if err != nil || info.URL != "https://desk.crove.com/api/third/telegram/webhook/ch_123" {
|
||||
t.Fatalf("GetWebhookInfo failed: %+v, err: %v", info, err)
|
||||
}
|
||||
|
||||
err = client.DeleteWebhook(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("DeleteWebhook failed: %v", err)
|
||||
}
|
||||
}
|
||||
70
internal/telegram/types.go
Normal file
70
internal/telegram/types.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package telegram
|
||||
|
||||
// Update represents an incoming update from Telegram Bot Webhook.
|
||||
type Update struct {
|
||||
UpdateID int64 `json:"update_id"`
|
||||
Message *Message `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// Message represents a Telegram message.
|
||||
type Message struct {
|
||||
MessageID int64 `json:"message_id"`
|
||||
From *User `json:"from,omitempty"`
|
||||
Chat Chat `json:"chat"`
|
||||
Date int64 `json:"date"`
|
||||
Text string `json:"text,omitempty"`
|
||||
Caption string `json:"caption,omitempty"`
|
||||
}
|
||||
|
||||
// User represents a Telegram user or bot.
|
||||
type User struct {
|
||||
ID int64 `json:"id"`
|
||||
IsBot bool `json:"is_bot"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
LanguageCode string `json:"language_code,omitempty"`
|
||||
}
|
||||
|
||||
// Chat represents a Telegram chat (private, group, supergroup, channel).
|
||||
type Chat struct {
|
||||
ID int64 `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
FirstName string `json:"first_name,omitempty"`
|
||||
LastName string `json:"last_name,omitempty"`
|
||||
}
|
||||
|
||||
// SendMessageRequest represents payload for Telegram sendMessage API.
|
||||
type SendMessageRequest struct {
|
||||
ChatID int64 `json:"chat_id"`
|
||||
Text string `json:"text"`
|
||||
ParseMode string `json:"parse_mode,omitempty"`
|
||||
ReplyToMessageID int64 `json:"reply_to_message_id,omitempty"`
|
||||
DisableWebPagePreview bool `json:"disable_web_page_preview,omitempty"`
|
||||
}
|
||||
|
||||
// APIResponse represents standard Telegram Bot API response envelope.
|
||||
type APIResponse[T any] struct {
|
||||
OK bool `json:"ok"`
|
||||
Result T `json:"result,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
ErrorCode int `json:"error_code,omitempty"`
|
||||
}
|
||||
|
||||
// SetWebhookRequest represents payload for Telegram setWebhook API.
|
||||
type SetWebhookRequest struct {
|
||||
URL string `json:"url"`
|
||||
SecretToken string `json:"secret_token,omitempty"`
|
||||
AllowedUpdates []string `json:"allowed_updates,omitempty"`
|
||||
}
|
||||
|
||||
// WebhookInfo represents current Telegram webhook status.
|
||||
type WebhookInfo struct {
|
||||
URL string `json:"url"`
|
||||
HasCustomCertificate 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"`
|
||||
}
|
||||
Reference in New Issue
Block a user