package telegram import ( "bytes" "context" "encoding/json" "fmt" "io" "mime/multipart" "net/http" "net/url" "path/filepath" "strings" "time" ) 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, "") } 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) 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"` }