372 lines
10 KiB
Go
372 lines
10 KiB
Go
package telegram
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"net/url"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type PhotoUpload struct {
|
|
Filename string
|
|
Data []byte
|
|
Caption string
|
|
}
|
|
|
|
type Client struct {
|
|
token string
|
|
baseURL string
|
|
fileBaseURL string
|
|
httpClient *http.Client
|
|
}
|
|
|
|
func NewClient(token string) *Client {
|
|
return &Client{
|
|
token: token,
|
|
baseURL: "https://api.telegram.org/bot" + token,
|
|
fileBaseURL: "https://api.telegram.org/file/bot" + token,
|
|
httpClient: &http.Client{Timeout: 90 * time.Second},
|
|
}
|
|
}
|
|
|
|
func (c *Client) redactError(err error) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("%s", c.redact(err.Error()))
|
|
}
|
|
|
|
func (c *Client) redact(text string) string {
|
|
if c.token == "" {
|
|
return text
|
|
}
|
|
return strings.ReplaceAll(text, c.token, "<telegram-token>")
|
|
}
|
|
|
|
func (c *Client) GetUpdates(ctx context.Context, offset int, timeoutSeconds int) ([]Update, error) {
|
|
params := map[string]any{
|
|
"offset": offset,
|
|
"timeout": timeoutSeconds,
|
|
"allowed_updates": []string{"message", "callback_query"},
|
|
}
|
|
var updates []Update
|
|
if err := c.postJSON(ctx, "getUpdates", params, &updates); err != nil {
|
|
return nil, err
|
|
}
|
|
return updates, nil
|
|
}
|
|
|
|
func (c *Client) SendMessage(ctx context.Context, chatID int64, text string, opts SendMessageOptions) (Message, error) {
|
|
params := map[string]any{
|
|
"chat_id": chatID,
|
|
"text": text,
|
|
}
|
|
if opts.ParseMode != "" {
|
|
params["parse_mode"] = opts.ParseMode
|
|
}
|
|
if opts.ReplyMarkup != nil {
|
|
params["reply_markup"] = opts.ReplyMarkup
|
|
}
|
|
var message Message
|
|
if err := c.postJSON(ctx, "sendMessage", params, &message); err != nil {
|
|
return Message{}, err
|
|
}
|
|
return message, nil
|
|
}
|
|
|
|
func (c *Client) SendChatAction(ctx context.Context, chatID int64, action string) error {
|
|
params := map[string]any{
|
|
"chat_id": chatID,
|
|
"action": action,
|
|
}
|
|
var ok bool
|
|
return c.postJSON(ctx, "sendChatAction", params, &ok)
|
|
}
|
|
|
|
func (c *Client) SendMessageDraft(ctx context.Context, chatID int64, draftID int64, text string) error {
|
|
params := map[string]any{
|
|
"chat_id": chatID,
|
|
"draft_id": draftID,
|
|
"text": text,
|
|
}
|
|
var ok bool
|
|
return c.postJSON(ctx, "sendMessageDraft", params, &ok)
|
|
}
|
|
|
|
func (c *Client) EditMessageText(ctx context.Context, chatID int64, messageID int, text string, opts EditMessageTextOptions) (Message, error) {
|
|
params := map[string]any{
|
|
"chat_id": chatID,
|
|
"message_id": messageID,
|
|
"text": text,
|
|
}
|
|
if opts.ParseMode != "" {
|
|
params["parse_mode"] = opts.ParseMode
|
|
}
|
|
if opts.ReplyMarkup != nil {
|
|
params["reply_markup"] = opts.ReplyMarkup
|
|
}
|
|
var message Message
|
|
if err := c.postJSON(ctx, "editMessageText", params, &message); err != nil {
|
|
return Message{}, err
|
|
}
|
|
return message, nil
|
|
}
|
|
|
|
func (c *Client) PinChatMessage(ctx context.Context, chatID int64, messageID int, disableNotification bool) error {
|
|
params := map[string]any{
|
|
"chat_id": chatID,
|
|
"message_id": messageID,
|
|
"disable_notification": disableNotification,
|
|
}
|
|
var ignored bool
|
|
return c.postJSON(ctx, "pinChatMessage", params, &ignored)
|
|
}
|
|
|
|
func (c *Client) AnswerCallbackQuery(ctx context.Context, callbackQueryID, text string) error {
|
|
params := map[string]any{
|
|
"callback_query_id": callbackQueryID,
|
|
}
|
|
if text != "" {
|
|
params["text"] = text
|
|
}
|
|
var ignored bool
|
|
return c.postJSON(ctx, "answerCallbackQuery", params, &ignored)
|
|
}
|
|
|
|
func (c *Client) GetFile(ctx context.Context, fileID string) (File, error) {
|
|
var file File
|
|
if err := c.postJSON(ctx, "getFile", map[string]any{"file_id": fileID}, &file); err != nil {
|
|
return File{}, err
|
|
}
|
|
return file, nil
|
|
}
|
|
|
|
func (c *Client) DownloadFile(ctx context.Context, filePath string) ([]byte, error) {
|
|
u := c.fileBaseURL + "/" + url.PathEscape(filePath)
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, c.redactError(err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
return nil, fmt.Errorf("download file: telegram returned %s", resp.Status)
|
|
}
|
|
return io.ReadAll(resp.Body)
|
|
}
|
|
|
|
func (c *Client) SendPhotoBytes(ctx context.Context, chatID int64, filename string, data []byte, caption string) (Message, error) {
|
|
var body bytes.Buffer
|
|
writer := multipart.NewWriter(&body)
|
|
if err := writer.WriteField("chat_id", fmt.Sprint(chatID)); err != nil {
|
|
return Message{}, err
|
|
}
|
|
if caption != "" {
|
|
if err := writer.WriteField("caption", caption); err != nil {
|
|
return Message{}, err
|
|
}
|
|
}
|
|
part, err := writer.CreateFormFile("photo", filepath.Base(filename))
|
|
if err != nil {
|
|
return Message{}, err
|
|
}
|
|
if _, err := part.Write(data); err != nil {
|
|
return Message{}, err
|
|
}
|
|
if err := writer.Close(); err != nil {
|
|
return Message{}, err
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/sendPhoto", &body)
|
|
if err != nil {
|
|
return Message{}, err
|
|
}
|
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return Message{}, c.redactError(err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
payload, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
|
|
return Message{}, fmt.Errorf("sendPhoto: telegram returned %s: %s", resp.Status, string(payload))
|
|
}
|
|
var decoded apiResponse[Message]
|
|
if err := json.NewDecoder(resp.Body).Decode(&decoded); err != nil {
|
|
return Message{}, err
|
|
}
|
|
if !decoded.OK {
|
|
return Message{}, fmt.Errorf("sendPhoto: telegram error %d: %s", decoded.ErrorCode, decoded.Description)
|
|
}
|
|
return decoded.Result, nil
|
|
}
|
|
|
|
func (c *Client) SendPhotoGroupBytes(ctx context.Context, chatID int64, photos []PhotoUpload) ([]Message, error) {
|
|
if len(photos) == 0 {
|
|
return nil, nil
|
|
}
|
|
if len(photos) == 1 {
|
|
message, err := c.SendPhotoBytes(ctx, chatID, photos[0].Filename, photos[0].Data, photos[0].Caption)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return []Message{message}, nil
|
|
}
|
|
|
|
var body bytes.Buffer
|
|
writer := multipart.NewWriter(&body)
|
|
if err := writer.WriteField("chat_id", fmt.Sprint(chatID)); err != nil {
|
|
return nil, err
|
|
}
|
|
media := make([]map[string]string, 0, len(photos))
|
|
for i, photo := range photos {
|
|
name := fmt.Sprintf("photo%d", i)
|
|
entry := map[string]string{
|
|
"type": "photo",
|
|
"media": "attach://" + name,
|
|
}
|
|
if photo.Caption != "" {
|
|
entry["caption"] = photo.Caption
|
|
}
|
|
media = append(media, entry)
|
|
}
|
|
mediaJSON, err := json.Marshal(media)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := writer.WriteField("media", string(mediaJSON)); err != nil {
|
|
return nil, err
|
|
}
|
|
for i, photo := range photos {
|
|
name := fmt.Sprintf("photo%d", i)
|
|
part, err := writer.CreateFormFile(name, filepath.Base(photo.Filename))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := part.Write(photo.Data); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if err := writer.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/sendMediaGroup", &body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, c.redactError(err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
payload, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
|
|
return nil, fmt.Errorf("sendMediaGroup: telegram returned %s: %s", resp.Status, string(payload))
|
|
}
|
|
var decoded apiResponse[[]Message]
|
|
if err := json.NewDecoder(resp.Body).Decode(&decoded); err != nil {
|
|
return nil, err
|
|
}
|
|
if !decoded.OK {
|
|
return nil, fmt.Errorf("sendMediaGroup: telegram error %d: %s", decoded.ErrorCode, decoded.Description)
|
|
}
|
|
return decoded.Result, nil
|
|
}
|
|
|
|
func (c *Client) SendDocumentBytes(ctx context.Context, chatID int64, filename string, data []byte, caption string) (Message, error) {
|
|
var body bytes.Buffer
|
|
writer := multipart.NewWriter(&body)
|
|
if err := writer.WriteField("chat_id", fmt.Sprint(chatID)); err != nil {
|
|
return Message{}, err
|
|
}
|
|
if caption != "" {
|
|
if err := writer.WriteField("caption", caption); err != nil {
|
|
return Message{}, err
|
|
}
|
|
}
|
|
part, err := writer.CreateFormFile("document", filepath.Base(filename))
|
|
if err != nil {
|
|
return Message{}, err
|
|
}
|
|
if _, err := part.Write(data); err != nil {
|
|
return Message{}, err
|
|
}
|
|
if err := writer.Close(); err != nil {
|
|
return Message{}, err
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/sendDocument", &body)
|
|
if err != nil {
|
|
return Message{}, err
|
|
}
|
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return Message{}, c.redactError(err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
payload, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
|
|
return Message{}, fmt.Errorf("sendDocument: telegram returned %s: %s", resp.Status, string(payload))
|
|
}
|
|
var decoded apiResponse[Message]
|
|
if err := json.NewDecoder(resp.Body).Decode(&decoded); err != nil {
|
|
return Message{}, err
|
|
}
|
|
if !decoded.OK {
|
|
return Message{}, fmt.Errorf("sendDocument: telegram error %d: %s", decoded.ErrorCode, decoded.Description)
|
|
}
|
|
return decoded.Result, nil
|
|
}
|
|
|
|
func (c *Client) postJSON(ctx context.Context, method string, params any, result any) error {
|
|
body, err := json.Marshal(params)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/"+method, 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 c.redactError(err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
payload, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
|
|
return fmt.Errorf("%s: telegram returned %s: %s", method, resp.Status, string(payload))
|
|
}
|
|
var decoded apiResponse[json.RawMessage]
|
|
if err := json.NewDecoder(resp.Body).Decode(&decoded); err != nil {
|
|
return err
|
|
}
|
|
if !decoded.OK {
|
|
return fmt.Errorf("%s: telegram error %d: %s", method, decoded.ErrorCode, decoded.Description)
|
|
}
|
|
if result == nil {
|
|
return nil
|
|
}
|
|
return json.Unmarshal(decoded.Result, result)
|
|
}
|
|
|
|
type apiResponse[T any] struct {
|
|
OK bool `json:"ok"`
|
|
Result T `json:"result"`
|
|
ErrorCode int `json:"error_code,omitempty"`
|
|
Description string `json:"description,omitempty"`
|
|
}
|