(new): rich message support
Golang lint / lint (pull_request) Successful in 1m20s
Golang lint / lint (push) Successful in 4m8s

(fix): runtime reliability
(tests): regression coverage
(doc): v1.1 release notes
This commit is contained in:
2026-08-12 16:34:44 +03:00
parent 48ddf66540
commit f03a081ed6
83 changed files with 6122 additions and 1925 deletions
+3 -3
View File
@@ -236,7 +236,7 @@ func (r TelegramRequest[R, P]) doRequest(ctx context.Context, api *API) (R, erro
req.Body = io.NopCloser(buf)
req.ContentLength = int64(len(reqData))
api.logger.Debugln("REQ", url, string(reqData))
api.logger.Debugln("REQ", url, redactRequestLog(reqData))
resp, err := api.client.Do(req)
if err != nil {
return zero, fmt.Errorf("HTTP request failed: %w", err)
@@ -248,7 +248,7 @@ func (r TelegramRequest[R, P]) doRequest(ctx context.Context, api *API) (R, erro
return zero, fmt.Errorf("failed to read response body: %w", err)
}
api.logger.Debugln("RES", r.method, string(respData))
api.logger.Debugln("RES", responseLogSummary(r.method, len(respData)))
response, err := parseBody[R](respData)
if err != nil {
@@ -269,7 +269,7 @@ func (r TelegramRequest[R, P]) doRequest(ctx context.Context, api *API) (R, erro
// Apply cooldown to global or chat-specific limiter
if api.Limiter != nil {
if r.chatID > 0 {
if r.chatID != 0 {
api.Limiter.SetChatLock(r.chatID, after)
} else {
api.Limiter.SetGlobalLock(after)
+94
View File
@@ -64,6 +64,100 @@ func TestInputRichMessageContentMarshal(t *testing.T) {
}
}
func TestInputRichMessageMediaMarshal(t *testing.T) {
message := InputRichMessage{
HTML: `<video src="tg://video?id=intro"></video>`,
Media: []InputRichMessageMedia{{
ID: "intro",
Media: InputMedia{Type: InputMediaTypeVideo, Media: "attach://intro"},
}},
}
data, err := json.Marshal(message)
if err != nil {
t.Fatalf("Marshal returned error: %v", err)
}
var got struct {
Media []struct {
ID string `json:"id"`
Media InputMedia `json:"media"`
} `json:"media"`
}
if err := json.Unmarshal(data, &got); err != nil {
t.Fatalf("Unmarshal returned error: %v", err)
}
if len(got.Media) != 1 || got.Media[0].ID != "intro" {
t.Fatalf("unexpected media: %+v", got.Media)
}
if got.Media[0].Media.Type != InputMediaTypeVideo || got.Media[0].Media.Media != "attach://intro" {
t.Fatalf("unexpected embedded media: %+v", got.Media[0].Media)
}
}
func TestEphemeralMethodsMarshalReceiverUserID(t *testing.T) {
cases := []struct {
name string
params any
}{
{"edit text", EditEphemeralMessageText{ChatID: 1, ReceiverUserID: 2, EphemeralMessageID: 3, Text: "updated"}},
{"edit media", EditEphemeralMessageMedia{ChatID: 1, ReceiverUserID: 2, EphemeralMessageID: 3, Media: InputMedia{Type: InputMediaTypePhoto, Media: "photo-id"}}},
{"edit caption", EditEphemeralMessageCaption{ChatID: 1, ReceiverUserID: 2, EphemeralMessageID: 3, Caption: "updated"}},
{"edit markup", EditEphemeralMessageReplyMarkup{ChatID: 1, ReceiverUserID: 2, EphemeralMessageID: 3}},
{"delete", DeleteEphemeralMessage{ChatID: 1, ReceiverUserID: 2, EphemeralMessageID: 3}},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
data, err := json.Marshal(tt.params)
if err != nil {
t.Fatalf("Marshal returned error: %v", err)
}
var fields map[string]json.RawMessage
if err := json.Unmarshal(data, &fields); err != nil {
t.Fatalf("Unmarshal returned error: %v", err)
}
if _, ok := fields["receiver_user_id"]; !ok {
t.Fatalf("receiver_user_id is missing from %s", data)
}
if _, ok := fields["reciever_user_id"]; ok {
t.Fatalf("misspelled receiver_user_id is present in %s", data)
}
})
}
}
func TestEphemeralSendParametersMarshal(t *testing.T) {
cases := []struct {
name string
params any
}{
{"message", SendMessage{ChatID: 1, Text: "text", ReceiverUserID: 2, CallbackQueryID: "callback"}},
{"animation", SendAnimation{ChatID: 1, Animation: "animation", ReceiverUserID: 2, CallbackQueryID: "callback"}},
{"audio", SendAudio{ChatID: 1, Audio: "audio", ReceiverUserID: 2, CallbackQueryID: "callback"}},
{"document", SendDocument{ChatID: 1, Document: "document", ReceiverUserID: 2, CallbackQueryID: "callback"}},
{"photo", SendPhoto{ChatID: 1, Photo: "photo", ReceiverUserID: 2, CallbackQueryID: "callback"}},
{"sticker", SendSticker{ChatID: 1, Sticker: "sticker", ReceiverUserID: 2, CallbackQueryID: "callback"}},
{"video", SendVideo{ChatID: 1, Video: "video", ReceiverUserID: 2, CallbackQueryID: "callback"}},
{"video note", SendVideoNote{ChatID: 1, VideoNote: "video-note", ReceiverUserID: 2, CallbackQueryID: "callback"}},
{"voice", SendVoice{ChatID: 1, Voice: "voice", ReceiverUserID: 2, CallbackQueryID: "callback"}},
{"contact", SendContact{ChatID: 1, PhoneNumber: "+10000000000", FirstName: "A", ReceiverUserID: 2, CallbackQueryID: "callback"}},
{"location", SendLocation{ChatID: 1, Latitude: 1, Longitude: 2, ReceiverUserID: 2, CallbackQueryID: "callback"}},
{"venue", SendVenue{ChatID: 1, Latitude: 1, Longitude: 2, Title: "Venue", Address: "Address", ReceiverUserID: 2, CallbackQueryID: "callback"}},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
data, err := json.Marshal(tt.params)
if err != nil {
t.Fatalf("Marshal returned error: %v", err)
}
if !strings.Contains(string(data), `"receiver_user_id":2`) || !strings.Contains(string(data), `"callback_query_id":"callback"`) {
t.Fatalf("missing ephemeral parameters in %s", data)
}
})
}
}
func TestInputPollOptionMediaLinkMarshal(t *testing.T) {
media := InputPollOptionMedia{Type: "link", URL: "https://example.com"}
data, err := json.Marshal(media)
+35 -1
View File
@@ -10,6 +10,10 @@ type SendPhoto struct {
ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"`
DirectMessagesTopicID int `json:"direct_messages_topic_id,omitempty"`
// ReceiverUserID identifies the user who can see the ephemeral message.
ReceiverUserID int64 `json:"receiver_user_id,omitempty"` // Since: Bot API 10.2
// CallbackQueryID identifies the callback query that triggered an ephemeral response.
CallbackQueryID string `json:"callback_query_id,omitempty"` // Since: Bot API 10.2
Photo string `json:"photo"`
Caption string `json:"caption,omitempty"`
@@ -53,6 +57,10 @@ type SendAudio struct {
ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"`
DirectMessagesTopicID int `json:"direct_messages_topic_id,omitempty"`
// ReceiverUserID identifies the user who can see the ephemeral message.
ReceiverUserID int64 `json:"receiver_user_id,omitempty"` // Since: Bot API 10.2
// CallbackQueryID identifies the callback query that triggered an ephemeral response.
CallbackQueryID string `json:"callback_query_id,omitempty"` // Since: Bot API 10.2
Audio string `json:"audio"`
Caption string `json:"caption,omitempty"`
@@ -98,6 +106,10 @@ type SendDocument struct {
ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"`
DirectMessagesTopicID int `json:"direct_messages_topic_id,omitempty"`
// ReceiverUserID identifies the user who can see the ephemeral message.
ReceiverUserID int64 `json:"receiver_user_id,omitempty"` // Since: Bot API 10.2
// CallbackQueryID identifies the callback query that triggered an ephemeral response.
CallbackQueryID string `json:"callback_query_id,omitempty"` // Since: Bot API 10.2
Document string `json:"document"`
Thumbnail string `json:"thumbnail,omitempty"`
@@ -141,6 +153,10 @@ type SendVideo struct {
ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"`
DirectMessagesTopicID int `json:"direct_messages_topic_id,omitempty"`
// ReceiverUserID identifies the user who can see the ephemeral message.
ReceiverUserID int64 `json:"receiver_user_id,omitempty"` // Since: Bot API 10.2
// CallbackQueryID identifies the callback query that triggered an ephemeral response.
CallbackQueryID string `json:"callback_query_id,omitempty"` // Since: Bot API 10.2
Video string `json:"video"`
Thumbnail string `json:"thumbnail,omitempty"`
@@ -192,6 +208,10 @@ type SendAnimation struct {
ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"`
DirectMessagesTopicID int `json:"direct_messages_topic_id,omitempty"`
// ReceiverUserID identifies the user who can see the ephemeral message.
ReceiverUserID int64 `json:"receiver_user_id,omitempty"` // Since: Bot API 10.2
// CallbackQueryID identifies the callback query that triggered an ephemeral response.
CallbackQueryID string `json:"callback_query_id,omitempty"` // Since: Bot API 10.2
Animation string `json:"animation"`
Thumbnail string `json:"thumbnail,omitempty"`
@@ -239,6 +259,10 @@ type SendVoice struct {
ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"`
DirectMessagesTopicID int `json:"direct_messages_topic_id,omitempty"`
// ReceiverUserID identifies the user who can see the ephemeral message.
ReceiverUserID int64 `json:"receiver_user_id,omitempty"` // Since: Bot API 10.2
// CallbackQueryID identifies the callback query that triggered an ephemeral response.
CallbackQueryID string `json:"callback_query_id,omitempty"` // Since: Bot API 10.2
Voice string `json:"voice"`
Caption string `json:"caption,omitempty"`
@@ -280,6 +304,10 @@ type SendVideoNote struct {
ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"`
DirectMessagesTopicID int `json:"direct_messages_topic_id,omitempty"`
// ReceiverUserID identifies the user who can see the ephemeral message.
ReceiverUserID int64 `json:"receiver_user_id,omitempty"` // Since: Bot API 10.2
// CallbackQueryID identifies the callback query that triggered an ephemeral response.
CallbackQueryID string `json:"callback_query_id,omitempty"` // Since: Bot API 10.2
VideoNote string `json:"video_note"`
Thumbnail string `json:"thumbnail,omitempty"`
@@ -397,7 +425,13 @@ type SendLivePhoto struct {
MessageThreadID int `json:"message_thread_id,omitempty"`
DirectMessagesTopicID int `json:"direct_messages_topic_id,omitempty"`
LivePhoto string `json:"live_photo"`
// ReceiverUserID identifies the user who can see the ephemeral message.
ReceiverUserID int64 `json:"receiver_user_id,omitempty"` // Since: Bot API 10.2
// CallbackQueryID identifies the callback query that triggered an ephemeral response.
CallbackQueryID string `json:"callback_query_id,omitempty"` // Since: Bot API 10.2
LivePhoto string `json:"live_photo"`
// Photo contains or identifies the associated photo.
Photo string `json:"photo"`
Caption string `json:"caption,omitempty"`
ParseMode ParseMode `json:"parse_mode,omitempty"`
CaptionEntities []MessageEntity `json:"caption_entities,omitempty"`
+27 -12
View File
@@ -112,9 +112,13 @@ type PaidMediaInfo struct {
type PaidMediaType string
const (
PaidMediaPreviewType PaidMediaType = "preview"
PaidMediaPhotoType PaidMediaType = "photo"
PaidMediaVideoType PaidMediaType = "video"
// PaidMediaPreviewType identifies a paid-media preview.
PaidMediaPreviewType PaidMediaType = "preview"
// PaidMediaPhotoType identifies a paid photo.
PaidMediaPhotoType PaidMediaType = "photo"
// PaidMediaVideoType identifies a paid video.
PaidMediaVideoType PaidMediaType = "video"
// PaidMediaLivePhotoType identifies a paid live photo.
PaidMediaLivePhotoType PaidMediaType = "live_photo" // Since: Bot API 10.0
)
@@ -172,7 +176,8 @@ type PollOption struct {
type InputPollOptionMedia struct {
Type string `json:"type"`
Media string `json:"media,omitempty"`
URL string `json:"url,omitempty"` // Since: Bot API 10.1; for type "link"
// URL contains the HTTP URL.
URL string `json:"url,omitempty"` // Since: Bot API 10.1; for type "link"
}
// InputPollOption contains information about one answer option in a poll to be sent.
@@ -226,8 +231,8 @@ const (
// See https://core.telegram.org/bots/api#pollanswer
type PollAnswer struct {
PollID string `json:"poll_id"`
VoterChat Chat `json:"voter_chat"` // Since: Bot API 6.8
User User `json:"user"`
VoterChat Chat `json:"voter_chat,omitempty"` // Since: Bot API 6.8
User User `json:"user,omitempty"` // FIXME: Pointer in v2
OptionIDs []int `json:"option_ids"`
OptionPersistentIDs []string `json:"option_persistent_ids"` // Since: Bot API 9.6
}
@@ -264,15 +269,17 @@ type Poll struct {
// Since: Bot API 10.1
// See https://core.telegram.org/bots/api#link
type Link struct {
// URL contains the HTTP URL.
URL string `json:"url"`
}
// PollMedia represents media attached to a poll.
// Since: Bot API 10.0
type PollMedia struct {
Animation *Animation `json:"animation,omitempty"`
Audio *Audio `json:"audio,omitempty"`
Document *Document `json:"document,omitempty"`
Animation *Animation `json:"animation,omitempty"`
Audio *Audio `json:"audio,omitempty"`
Document *Document `json:"document,omitempty"`
// Link contains link media attached to the poll.
Link *Link `json:"link,omitempty"` // Since: Bot API 10.1
LivePhoto *LivePhoto `json:"live_photo,omitempty"`
Location *Location `json:"location,omitempty"`
@@ -352,10 +359,18 @@ const (
InputMediaTypeVideo InputMediaType = "video"
// InputMediaTypeAudio is an audio file.
InputMediaTypeAudio InputMediaType = "audio"
// InputMediaTypeVoiceNote is a voice message.
//
// Since: Bot API 10.2
InputMediaTypeVoiceNote InputMediaType = "voice_note"
InputMediaTypeSticker InputMediaType = "sticker"
InputMediaTypeLocation InputMediaType = "location"
InputMediaTypeVenue InputMediaType = "venue"
// InputMediaTypeSticker is a sticker.
InputMediaTypeSticker InputMediaType = "sticker"
// InputMediaTypeLocation is a location.
InputMediaTypeLocation InputMediaType = "location"
// InputMediaTypeVenue is a venue.
InputMediaTypeVenue InputMediaType = "venue"
// InputMediaTypeLivePhoto is a live photo.
InputMediaTypeLivePhoto InputMediaType = "live_photo" // Since: Bot API 10.0
)
+2
View File
@@ -6,6 +6,8 @@ package tgapi
type BotCommand struct {
Command string `json:"command"`
Description string `json:"description"`
// IsEphemeral marks the command as visible only in ephemeral command contexts.
IsEphemeral bool `json:"is_ephemeral,omitempty"` // Since: Bot API 10.2
}
// BotCommandScopeType indicates the type of a command scope.
+7 -3
View File
@@ -498,8 +498,10 @@ const (
// Since: Bot API 10.1
// See https://core.telegram.org/bots/api#answerchatjoinrequestquery
type AnswerChatJoinRequestQuery struct {
ChatJoinRequestQueryID string `json:"chat_join_request_query_id"`
Result ChatJoinRequestQueryResult `json:"result"`
// ChatJoinRequestQueryID identifies the chat join request query.
ChatJoinRequestQueryID string `json:"chat_join_request_query_id"`
// Result contains the decision for the join request query.
Result ChatJoinRequestQueryResult `json:"result"`
}
// AnswerChatJoinRequestQuery processes a received chat join request query.
@@ -524,8 +526,10 @@ func (api *API) AnswerChatJoinRequestQueryWithContext(ctx context.Context, param
// Since: Bot API 10.1
// See https://core.telegram.org/bots/api#sendchatjoinrequestwebapp
type SendChatJoinRequestWebApp struct {
// ChatJoinRequestQueryID identifies the chat join request query.
ChatJoinRequestQueryID string `json:"chat_join_request_query_id"`
WebAppURL string `json:"web_app_url"`
// WebAppURL is the HTTPS URL of the Mini App to open.
WebAppURL string `json:"web_app_url"`
}
// SendChatJoinRequestWebApp shows a Mini App to the user before deciding a
+27 -1
View File
@@ -52,7 +52,6 @@ type ChatFullInfo struct {
PersonalChat *Chat `json:"personal_chat,omitempty"`
ParentChat *Chat `json:"parent_chat,omitempty"` // Since: Bot API 9.2
GuardBot *User `json:"guard_bot,omitempty"` // Since: Bot API 10.1; visible to chat administrators only
AvailableReaction []ReactionType `json:"available_reaction,omitempty"`
@@ -92,6 +91,10 @@ type ChatFullInfo struct {
FirstProfileAudio *Audio `json:"first_profile_audio,omitempty"` // Since: Bot API 9.4
UniqueGiftColors *UniqueGiftColors `json:"unique_gift_colors,omitempty"` // Since: Bot API 9.3
PaidMessageStarCount *int `json:"paid_message_star_count,omitempty"` // Since: Bot API 9.3
// GuardBot contains the guard bot visible to chat administrators.
GuardBot *User `json:"guard_bot,omitempty"` // Since: Bot API 10.1; visible to chat administrators only
// Community contains information about the affected community.
Community *Community `json:"community,omitempty"` // Since: Bot API 10.2
}
// ChatPhoto represents a chat photo.
@@ -318,3 +321,26 @@ type ChatBoostRemoved struct {
RemoveDate int `json:"remove_date"`
Source ChatBoostSource `json:"source"`
}
// Community represents a group of chats.
//
// Since: Bot API 10.2
type Community struct {
// ID uniquely identifies the value within its containing object.
ID int64 `json:"id"`
// Name is the user-facing or reference name of the value.
Name string `json:"name"`
}
// CommunityChatAdded describes a service message about a chat joining a community.
//
// Since: Bot API 10.2
type CommunityChatAdded struct {
// Community contains information about the affected community.
Community Community `json:"community"`
}
// CommunityChatRemoved describes a service message about a chat leaving a community.
//
// Since: Bot API 10.2
type CommunityChatRemoved struct{}
+5
View File
@@ -14,6 +14,11 @@ var ErrPoolQueueFull = errors.New("worker pool queue full")
// ErrPoolStopped reports that a request was submitted after the worker pool stopped.
var ErrPoolStopped = errors.New("worker pool stopped")
// ErrRichMessageDraftUploadUnsupported reports a direct file upload attempted for a rich draft.
//
// Since: Bot API 10.2
var ErrRichMessageDraftUploadUnsupported = errors.New("sendRichMessageDraft does not support direct file uploads")
// ResponseError reports an unsuccessful Telegram API response.
type ResponseError struct {
Code int
+1
View File
@@ -20,6 +20,7 @@ type InlineQueryResultsButton struct {
// Since: Bot API 10.1
// See https://core.telegram.org/bots/api#inputrichmessagecontent
type InputRichMessageContent struct {
// RichMessage contains structured rich-message content.
RichMessage InputRichMessage `json:"rich_message"`
}
+57
View File
@@ -0,0 +1,57 @@
package tgapi
import (
"encoding/json"
"fmt"
"strings"
)
const redactedLogValue = "<REDACTED>"
var sensitiveLogFields = map[string]struct{}{
"callback_data": {},
"credentials": {},
"data": {},
"invoice_payload": {},
"payload": {},
"provider_data": {},
"provider_token": {},
"secret": {},
"secret_token": {},
"token": {},
"web_app_query_id": {},
}
func redactRequestLog(data []byte) string {
var value any
if err := json.Unmarshal(data, &value); err != nil {
return fmt.Sprintf("<invalid JSON omitted: %d bytes>", len(data))
}
redactLogValue(value)
redacted, err := json.Marshal(value)
if err != nil {
return fmt.Sprintf("<unavailable JSON omitted: %d bytes>", len(data))
}
return string(redacted)
}
func redactLogValue(value any) {
switch value := value.(type) {
case map[string]any:
for key, item := range value {
if _, sensitive := sensitiveLogFields[strings.ToLower(key)]; sensitive {
value[key] = redactedLogValue
continue
}
redactLogValue(item)
}
case []any:
for _, item := range value {
redactLogValue(item)
}
}
}
func responseLogSummary(method string, size int) string {
return fmt.Sprintf("method=%s bytes=%d body=omitted", method, size)
}
+39
View File
@@ -0,0 +1,39 @@
package tgapi
import (
"strings"
"testing"
)
func TestRedactRequestLogRemovesSensitiveValues(t *testing.T) {
const input = `{"secret_token":"webhook-secret","provider_token":"payment-token","nested":{"data":"passport-data","callback_data":"callback-secret"},"chat_id":42}`
got := redactRequestLog([]byte(input))
for _, secret := range []string{"webhook-secret", "payment-token", "passport-data", "callback-secret"} {
if strings.Contains(got, secret) {
t.Errorf("redacted request contains %q: %s", secret, got)
}
}
if !strings.Contains(got, `"chat_id":42`) {
t.Errorf("redacted request lost non-sensitive field: %s", got)
}
}
func TestRedactRequestLogOmitsInvalidJSON(t *testing.T) {
const secret = "not-json-secret"
got := redactRequestLog([]byte(secret))
if strings.Contains(got, secret) {
t.Fatalf("invalid JSON was logged verbatim: %s", got)
}
}
func TestResponseLogSummaryNeverContainsBody(t *testing.T) {
const token = "managed-bot-token"
got := responseLogSummary("getManagedBotToken", len(token))
if strings.Contains(got, token) {
t.Fatalf("response summary contains response body: %s", got)
}
if !strings.Contains(got, "body=omitted") {
t.Fatalf("response summary does not explain omission: %s", got)
}
}
+216 -17
View File
@@ -10,6 +10,10 @@ type SendMessage struct {
ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"`
DirectMessagesTopicID int64 `json:"direct_messages_topic_id,omitempty"`
// ReceiverUserID identifies the user who can see the ephemeral message.
ReceiverUserID int64 `json:"receiver_user_id,omitempty"` // Since: Bot API 10.2
// CallbackQueryID identifies the callback query that triggered an ephemeral response.
CallbackQueryID string `json:"callback_query_id,omitempty"` // Since: Bot API 10.2
Text string `json:"text"`
ParseMode ParseMode `json:"parse_mode,omitempty"`
@@ -29,7 +33,7 @@ type SendMessage struct {
// Since: Bot API 1.0
// See https://core.telegram.org/bots/api#sendmessage
func (api *API) SendMessage(params SendMessage) (Message, error) {
req := NewRequestWithChatID[Message, SendMessage]("sendMessage", params, params.ChatID)
req := NewRequestWithChatID[Message]("sendMessage", params, params.ChatID)
return req.Do(api)
}
@@ -38,7 +42,7 @@ func (api *API) SendMessage(params SendMessage) (Message, error) {
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendmessage
func (api *API) SendMessageWithContext(ctx context.Context, params SendMessage) (Message, error) {
req := NewRequestWithChatID[Message, SendMessage]("sendMessage", params, params.ChatID)
req := NewRequestWithChatID[Message]("sendMessage", params, params.ChatID)
return req.DoWithContext(ctx, api)
}
@@ -200,6 +204,10 @@ type SendLocation struct {
ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"`
DirectMessagesTopicID int `json:"direct_messages_topic_id,omitempty"`
// ReceiverUserID identifies the user who can see the ephemeral message.
ReceiverUserID int64 `json:"receiver_user_id,omitempty"` // Since: Bot API 10.2
// CallbackQueryID identifies the callback query that triggered an ephemeral response.
CallbackQueryID string `json:"callback_query_id,omitempty"` // Since: Bot API 10.2
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
@@ -243,6 +251,10 @@ type SendVenue struct {
ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"`
DirectMessagesTopicID int `json:"direct_messages_topic_id,omitempty"`
// ReceiverUserID identifies the user who can see the ephemeral message.
ReceiverUserID int64 `json:"receiver_user_id,omitempty"` // Since: Bot API 10.2
// CallbackQueryID identifies the callback query that triggered an ephemeral response.
CallbackQueryID string `json:"callback_query_id,omitempty"` // Since: Bot API 10.2
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
@@ -288,6 +300,10 @@ type SendContact struct {
ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"`
DirectMessagesTopicID int `json:"direct_messages_topic_id,omitempty"`
// ReceiverUserID identifies the user who can see the ephemeral message.
ReceiverUserID int64 `json:"receiver_user_id,omitempty"` // Since: Bot API 10.2
// CallbackQueryID identifies the callback query that triggered an ephemeral response.
CallbackQueryID string `json:"callback_query_id,omitempty"` // Since: Bot API 10.2
PhoneNumber string `json:"phone_number"`
FirstName string `json:"first_name"`
@@ -551,8 +567,9 @@ type EditMessageText struct {
ParseMode ParseMode `json:"parse_mode,omitempty"`
Entities []MessageEntity `json:"entities,omitempty"`
LinkPreviewOptions *LinkPreviewOptions `json:"link_preview_options,omitempty"`
RichMessage *InputRichMessage `json:"rich_message,omitempty"` // Since: Bot API 10.1; required if Text is not specified
ReplyMarkup *ReplyMarkup `json:"reply_markup,omitempty"`
// RichMessage contains structured rich-message content.
RichMessage *InputRichMessage `json:"rich_message,omitempty"` // Since: Bot API 10.1; required if Text is not specified
ReplyMarkup *ReplyMarkup `json:"reply_markup,omitempty"`
}
// EditMessageText edits text messages.
@@ -1095,19 +1112,31 @@ func (api *API) DeleteMessageReactionWithContext(ctx context.Context, params Del
// Since: Bot API 10.1
// See https://core.telegram.org/bots/api#sendrichmessage
type SendRichMessage struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"`
ChatID int64 `json:"chat_id"`
MessageThreadID int64 `json:"message_thread_id,omitempty"`
DirectMessagesTopicID int64 `json:"direct_messages_topic_id,omitempty"`
// BusinessConnectionID identifies the business connection used to send the message.
BusinessConnectionID string `json:"business_connection_id,omitempty"`
// ChatID identifies the target chat.
ChatID int64 `json:"chat_id"`
// MessageThreadID identifies the target message thread.
MessageThreadID int64 `json:"message_thread_id,omitempty"`
// DirectMessagesTopicID identifies the target direct-messages topic.
DirectMessagesTopicID int64 `json:"direct_messages_topic_id,omitempty"`
RichMessage InputRichMessage `json:"rich_message"`
DisableNotification bool `json:"disable_notification,omitempty"`
ProtectContent bool `json:"protect_content,omitempty"`
AllowPaidBroadcast bool `json:"allow_paid_broadcast,omitempty"`
MessageEffectID string `json:"message_effect_id,omitempty"`
// RichMessage contains structured rich-message content.
RichMessage InputRichMessage `json:"rich_message"`
// DisableNotification requests delivery without a notification sound.
DisableNotification bool `json:"disable_notification,omitempty"`
// ProtectContent prevents forwarding and saving the sent content.
ProtectContent bool `json:"protect_content,omitempty"`
// AllowPaidBroadcast permits high-throughput delivery using paid broadcast capacity.
AllowPaidBroadcast bool `json:"allow_paid_broadcast,omitempty"`
// MessageEffectID identifies the message effect to apply.
MessageEffectID string `json:"message_effect_id,omitempty"`
// SuggestedPostParameters contains parameters for a suggested channel post.
SuggestedPostParameters *SuggestedPostParameters `json:"suggested_post_parameters,omitempty"`
ReplyParameters *ReplyParameters `json:"reply_parameters,omitempty"`
ReplyMarkup *ReplyMarkup `json:"reply_markup,omitempty"`
// ReplyParameters describes the message being replied to.
ReplyParameters *ReplyParameters `json:"reply_parameters,omitempty"`
// ReplyMarkup defines the message's inline keyboard.
ReplyMarkup *ReplyMarkup `json:"reply_markup,omitempty"`
}
// SendRichMessage sends a rich formatted message.
@@ -1131,11 +1160,14 @@ func (api *API) SendRichMessageWithContext(ctx context.Context, params SendRichM
// Since: Bot API 10.1
// See https://core.telegram.org/bots/api#sendrichmessagedraft
type SendRichMessageDraft struct {
ChatID int64 `json:"chat_id"`
// ChatID identifies the target chat.
ChatID int64 `json:"chat_id"`
// MessageThreadID identifies the target message thread.
MessageThreadID int64 `json:"message_thread_id,omitempty"`
// DraftID must be non-zero; changes to drafts with the same identifier are animated.
DraftID int64 `json:"draft_id"`
DraftID int64 `json:"draft_id"`
// RichMessage contains structured rich-message content.
RichMessage InputRichMessage `json:"rich_message"`
}
@@ -1158,3 +1190,170 @@ func (api *API) SendRichMessageDraftWithContext(ctx context.Context, params Send
req := NewRequestWithChatID[bool]("sendRichMessageDraft", params, params.ChatID)
return req.DoWithContext(ctx, api)
}
// EditEphemeralMessageText holds parameters for editing an ephemeral text message.
//
// Since: Bot API 10.2
type EditEphemeralMessageText struct {
// ChatID identifies the target chat.
ChatID int64 `json:"chat_id"`
// ReceiverUserID identifies the user who can see the ephemeral message.
ReceiverUserID int64 `json:"receiver_user_id"`
// EphemeralMessageID identifies the ephemeral message.
EphemeralMessageID int64 `json:"ephemeral_message_id"`
// Text contains the formatted or plain text content.
Text string `json:"text"`
// ParseMode selects the formatting syntax used by the text or caption.
ParseMode ParseMode `json:"parse_mode,omitempty"`
// Entities describes explicit formatting entities in Text.
Entities []MessageEntity `json:"entities,omitempty"`
// LinkPreviewOptions controls link preview generation for Text.
LinkPreviewOptions *LinkPreviewOptions `json:"link_preview_options,omitempty"`
// ReplyMarkup defines the message's inline keyboard.
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
}
// EditEphemeralMessageText edits an ephemeral text message.
//
// Since: Bot API 10.2
func (api *API) EditEphemeralMessageText(params EditEphemeralMessageText) (bool, error) {
req := NewRequestWithChatID[bool]("editEphemeralMessageText", params, params.ChatID)
return req.Do(api)
}
// EditEphemeralMessageTextWithContext is the context-aware variant of EditEphemeralMessageText.
//
// Since: Bot API 10.2
func (api *API) EditEphemeralMessageTextWithContext(ctx context.Context, params EditEphemeralMessageText) (bool, error) {
req := NewRequestWithChatID[bool]("editEphemeralMessageText", params, params.ChatID)
return req.DoWithContext(ctx, api)
}
// EditEphemeralMessageMedia holds parameters for editing ephemeral message media.
// New files cannot be uploaded; use a file ID or URL.
//
// Since: Bot API 10.2
type EditEphemeralMessageMedia struct {
// ChatID identifies the target chat.
ChatID int64 `json:"chat_id"`
// ReceiverUserID identifies the user who can see the ephemeral message.
ReceiverUserID int64 `json:"receiver_user_id"`
// EphemeralMessageID identifies the ephemeral message.
EphemeralMessageID int64 `json:"ephemeral_message_id"`
// Media contains or identifies media associated with the value.
Media InputMedia `json:"media"`
// ReplyMarkup defines the message's inline keyboard.
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
}
// EditEphemeralMessageMedia edits the media of an ephemeral message.
//
// Since: Bot API 10.2
func (api *API) EditEphemeralMessageMedia(params EditEphemeralMessageMedia) (bool, error) {
req := NewRequestWithChatID[bool]("editEphemeralMessageMedia", params, params.ChatID)
return req.Do(api)
}
// EditEphemeralMessageMediaWithContext is the context-aware variant of EditEphemeralMessageMedia.
//
// Since: Bot API 10.2
func (api *API) EditEphemeralMessageMediaWithContext(ctx context.Context, params EditEphemeralMessageMedia) (bool, error) {
req := NewRequestWithChatID[bool]("editEphemeralMessageMedia", params, params.ChatID)
return req.DoWithContext(ctx, api)
}
// EditEphemeralMessageCaption holds parameters for editing an ephemeral message caption.
//
// Since: Bot API 10.2
type EditEphemeralMessageCaption struct {
// ChatID identifies the target chat.
ChatID int64 `json:"chat_id"`
// ReceiverUserID identifies the user who can see the ephemeral message.
ReceiverUserID int64 `json:"receiver_user_id"`
// EphemeralMessageID identifies the ephemeral message.
EphemeralMessageID int64 `json:"ephemeral_message_id"`
// Caption contains the media or block caption.
Caption string `json:"caption,omitempty"`
// ParseMode selects the formatting syntax used by the text or caption.
ParseMode ParseMode `json:"parse_mode,omitempty"`
// CaptionEntities describes formatting entities in Caption.
CaptionEntities []MessageEntity `json:"caption_entities,omitempty"`
// ReplyMarkup defines the message's inline keyboard.
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
}
// EditEphemeralMessageCaption edits an ephemeral message caption.
//
// Since: Bot API 10.2
func (api *API) EditEphemeralMessageCaption(params EditEphemeralMessageCaption) (bool, error) {
req := NewRequestWithChatID[bool]("editEphemeralMessageCaption", params, params.ChatID)
return req.Do(api)
}
// EditEphemeralMessageCaptionWithContext is the context-aware variant of EditEphemeralMessageCaption.
//
// Since: Bot API 10.2
func (api *API) EditEphemeralMessageCaptionWithContext(ctx context.Context, params EditEphemeralMessageCaption) (bool, error) {
req := NewRequestWithChatID[bool]("editEphemeralMessageCaption", params, params.ChatID)
return req.DoWithContext(ctx, api)
}
// EditEphemeralMessageReplyMarkup holds parameters for editing an ephemeral message's inline keyboard.
//
// Since: Bot API 10.2
type EditEphemeralMessageReplyMarkup struct {
// ChatID identifies the target chat.
ChatID int64 `json:"chat_id"`
// ReceiverUserID identifies the user who can see the ephemeral message.
ReceiverUserID int64 `json:"receiver_user_id"`
// EphemeralMessageID identifies the ephemeral message.
EphemeralMessageID int64 `json:"ephemeral_message_id"`
// ReplyMarkup defines the message's inline keyboard.
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
}
// EditEphemeralMessageReplyMarkup edits an ephemeral message's inline keyboard.
//
// Since: Bot API 10.2
func (api *API) EditEphemeralMessageReplyMarkup(params EditEphemeralMessageReplyMarkup) (bool, error) {
req := NewRequestWithChatID[bool]("editEphemeralMessageReplyMarkup", params, params.ChatID)
return req.Do(api)
}
// EditEphemeralMessageReplyMarkupWithContext is the context-aware variant of EditEphemeralMessageReplyMarkup.
//
// Since: Bot API 10.2
func (api *API) EditEphemeralMessageReplyMarkupWithContext(ctx context.Context, params EditEphemeralMessageReplyMarkup) (bool, error) {
req := NewRequestWithChatID[bool]("editEphemeralMessageReplyMarkup", params, params.ChatID)
return req.DoWithContext(ctx, api)
}
// DeleteEphemeralMessage holds parameters for deleting an ephemeral message.
//
// Since: Bot API 10.2
type DeleteEphemeralMessage struct {
// ChatID identifies the target chat.
ChatID int64 `json:"chat_id"`
// ReceiverUserID identifies the user who can see the ephemeral message.
ReceiverUserID int64 `json:"receiver_user_id"`
// EphemeralMessageID identifies the ephemeral message.
EphemeralMessageID int64 `json:"ephemeral_message_id"`
}
// DeleteEphemeralMessage deletes an ephemeral message.
//
// Since: Bot API 10.2
func (api *API) DeleteEphemeralMessage(params DeleteEphemeralMessage) (bool, error) {
req := NewRequestWithChatID[bool]("deleteEphemeralMessage", params, params.ChatID)
return req.Do(api)
}
// DeleteEphemeralMessageWithContext is the context-aware variant of DeleteEphemeralMessage.
//
// Since: Bot API 10.2
func (api *API) DeleteEphemeralMessageWithContext(ctx context.Context, params DeleteEphemeralMessage) (bool, error) {
req := NewRequestWithChatID[bool]("deleteEphemeralMessage", params, params.ChatID)
return req.DoWithContext(ctx, api)
}
+53 -17
View File
@@ -23,10 +23,14 @@ type DirectMessageTopic struct {
type MessageOriginType string
const (
MessageOriginUserType = "user"
// MessageOriginUserType identifies a known user origin.
MessageOriginUserType = "user"
// MessageOriginHiddenUserType identifies a hidden user origin.
MessageOriginHiddenUserType = "hidden_user"
MessageOriginChatType = "chat"
MessageOriginChannel = "channel"
// MessageOriginChatType identifies a chat origin.
MessageOriginChatType = "chat"
// MessageOriginChannel identifies a channel origin.
MessageOriginChannel = "channel"
)
// MessageOrigin describes the origin of a message.
@@ -115,10 +119,14 @@ type Message struct {
DirectMessageTopic *DirectMessageTopic `json:"direct_message_topic,omitempty"` // Since: Bot API 9.2
From *User `json:"from,omitempty"`
SenderChat *Chat `json:"sender_chat,omitempty"` // Since: Bot API 5.0
SenderBoostCount int `json:"sender_boost_count,omitempty"` // Since: Bot API 7.1
SenderBusinessBot *User `json:"sender_business_bot,omitempty"` // Since: Bot API 7.2
SenderTag string `json:"sender_tag,omitempty"` // Since: Bot API 9.5
SenderChat *Chat `json:"sender_chat,omitempty"` // Since: Bot API 5.0
SenderBoostCount int `json:"sender_boost_count,omitempty"` // Since: Bot API 7.1
SenderBusinessBot *User `json:"sender_business_bot,omitempty"` // Since: Bot API 7.2
SenderTag string `json:"sender_tag,omitempty"` // Since: Bot API 9.5
// ReceiverUser identifies the user who can see the ephemeral message.
ReceiverUser *User `json:"receiver_user,omitempty"` // Since: Bot API 10.2
// EphemeralMessageID identifies the ephemeral message.
EphemeralMessageID int64 `json:"ephemeral_message_id,omitempty"` // Since: Bot API 10.2
Date int `json:"date"`
GuestQueryID string `json:"guest_query_id,omitempty"` // Since: Bot API 10.0
BusinessConnectionID string `json:"business_connection_id,omitempty"` // Since: Bot API 7.2
@@ -151,6 +159,7 @@ type Message struct {
SuggestedPostInfo *SuggestedPostInfo `json:"suggested_post_info,omitempty"` // Since: Bot API 9.1
EffectID string `json:"effect_id,omitempty"` // Since: Bot API 7.4
// RichMessage contains structured rich-message content.
RichMessage *RichMessage `json:"rich_message,omitempty"` // Since: Bot API 10.1
Animation *Animation `json:"animation,omitempty"` // Since: Bot API 4.0
Audio *Audio `json:"audio,omitempty"`
@@ -206,8 +215,12 @@ type Message struct {
BoostAdded *ChatBoostAdded `json:"boost_added,omitempty"` // Since: Bot API 7.1
ChatBackgroundSet *ChatBackground `json:"chat_background_set,omitempty"` // Since: Bot API 7.5
ChecklistTaskDone *ChecklistTaskDone `json:"checklist_task_done,omitempty"` // Since: Bot API 9.1
ChecklistTasksAdded *ChecklistTasksAdded `json:"checklist_tasks_added,omitempty"` // Since: Bot API 9.1
ChecklistTaskDone *ChecklistTaskDone `json:"checklist_task_done,omitempty"` // Since: Bot API 9.1
ChecklistTasksAdded *ChecklistTasksAdded `json:"checklist_tasks_added,omitempty"` // Since: Bot API 9.1
// CommunityChatAdded describes a community chat addition service message.
CommunityChatAdded *CommunityChatAdded `json:"community_chat_added,omitempty"` // Since: Bot API 10.2
// CommunityChatRemoved describes a community chat removal service message.
CommunityChatRemoved *CommunityChatRemoved `json:"community_chat_removed,omitempty"` // Since: Bot API 10.2
DirectMessagePriceChanged *DirectMessagePriceChanged `json:"direct_message_price_changed,omitempty"` // Since: Bot API 9.1
PaidMessagePriceChanged *PaidMessagePriceChanged `json:"paid_message_price_changed,omitempty"` // Since: Bot API 9.x
ForumTopicCreated *ForumTopicCreated `json:"forum_topic_created,omitempty"` // Since: Bot API 6.3
@@ -395,8 +408,10 @@ type MessageEntity struct {
// Since: Bot API 7.0
// See https://core.telegram.org/bots/api#replyparameters
type ReplyParameters struct {
MessageID int `json:"message_id"`
MessageID int `json:"message_id,omitempty"`
ChatID int64 `json:"chat_id,omitempty"`
// EphemeralMessageID identifies the ephemeral message.
EphemeralMessageID int64 `json:"ephemeral_message_id,omitempty"` // Since: Bot API 10.2
AllowSendingWithoutReply bool `json:"allow_sending_without_reply,omitempty"`
Quote string `json:"quote,omitempty"`
@@ -750,18 +765,39 @@ type SentGuestMessage struct {
InlineMessageID string `json:"inline_message_id"`
}
// RichMessage Rich formatted message.
// RichMessage represents a received rich-formatted message.
// Since: Bot API 10.1
type RichMessage struct {
// Blocks contains the nested rich-message blocks.
Blocks []RichBlock `json:"blocks"`
IsRTL bool `json:"is_rtl,omitempty"`
// IsRTL requests right-to-left rich-message layout.
IsRTL bool `json:"is_rtl,omitempty"`
}
// InputRichMessage Describes a rich message to be sent. Exactly one of the fields html or markdown must be used.
// InputRichMessageMedia describes media embedded in outgoing rich-message HTML or Markdown.
//
// Since: Bot API 10.2
type InputRichMessageMedia struct {
// ID uniquely identifies the value within its containing object.
ID string `json:"id"`
// Media contains or identifies media associated with the value.
Media InputMedia `json:"media"`
}
// InputRichMessage describes a rich message to be sent. Exactly one of HTML, Markdown, or Blocks must be used.
//
// Since: Bot API 10.1
type InputRichMessage struct {
HTML string `json:"html,omitempty"`
Markdown string `json:"markdown,omitempty"`
IsRTL bool `json:"is_rtl,omitempty"`
SkipEntityDetection bool `json:"skip_entity_detection,omitempty"`
// Blocks contains the nested rich-message blocks.
Blocks []InputRichBlock `json:"blocks,omitempty"` // Since: Bot API 10.2
// HTML contains rich-message content in Telegram HTML syntax.
HTML string `json:"html,omitempty"`
// Markdown contains rich-message content in Telegram Markdown syntax.
Markdown string `json:"markdown,omitempty"`
// Media contains or identifies media associated with the value.
Media []InputRichMessageMedia `json:"media,omitempty"` // Since: Bot API 10.2
// IsRTL requests right-to-left rich-message layout.
IsRTL bool `json:"is_rtl,omitempty"`
// SkipEntityDetection disables automatic detection of links, mentions, hashtags, commands, phone numbers, and bank cards.
SkipEntityDetection bool `json:"skip_entity_detection,omitempty"`
}
+4 -4
View File
@@ -79,7 +79,7 @@ func (api *API) ReplaceManagedBotTokenWithContext(ctx context.Context, params Re
// Returns true on success.
// See https://core.telegram.org/bots/api#logout
func (api *API) LogOut() (bool, error) {
req := NewRequest[bool, EmptyParams]("logOut", NoParams)
req := NewRequest[bool]("logOut", NoParams)
return req.Do(api)
}
@@ -87,7 +87,7 @@ func (api *API) LogOut() (bool, error) {
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#logout
func (api *API) LogOutWithContext(ctx context.Context) (bool, error) {
req := NewRequest[bool, EmptyParams]("logOut", NoParams)
req := NewRequest[bool]("logOut", NoParams)
return req.DoWithContext(ctx, api)
}
@@ -95,7 +95,7 @@ func (api *API) LogOutWithContext(ctx context.Context) (bool, error) {
// Returns true on success.
// See https://core.telegram.org/bots/api#close
func (api *API) CloseRemote() (bool, error) {
req := NewRequest[bool, EmptyParams]("close", NoParams)
req := NewRequest[bool]("close", NoParams)
return req.Do(api)
}
@@ -103,7 +103,7 @@ func (api *API) CloseRemote() (bool, error) {
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#close
func (api *API) CloseRemoteWithContext(ctx context.Context) (bool, error) {
req := NewRequest[bool, EmptyParams]("close", NoParams)
req := NewRequest[bool]("close", NoParams)
return req.DoWithContext(ctx, api)
}
+25 -12
View File
@@ -20,19 +20,32 @@ type PassportFile struct {
type PassportElementType string
const (
PassportPersonalDetailsType PassportElementType = "personal_details"
PassportPassportType PassportElementType = "passport"
PassportDriverLicenseType PassportElementType = "driver_license"
PassportIdentityCardType PassportElementType = "identity_card"
PassportInternalPassportType PassportElementType = "internal_passport"
PassportAddressType PassportElementType = "address"
PassportUtilityBillType PassportElementType = "utility_bill"
PassportBankStatementType PassportElementType = "bank_statement"
PassportRentalAgreementType PassportElementType = "rental_agreement"
PassportPassportRegistrationType PassportElementType = "passport_registration"
// PassportPersonalDetailsType identifies personal details.
PassportPersonalDetailsType PassportElementType = "personal_details"
// PassportPassportType identifies an international passport.
PassportPassportType PassportElementType = "passport"
// PassportDriverLicenseType identifies a driver license.
PassportDriverLicenseType PassportElementType = "driver_license"
// PassportIdentityCardType identifies an identity card.
PassportIdentityCardType PassportElementType = "identity_card"
// PassportInternalPassportType identifies an internal passport.
PassportInternalPassportType PassportElementType = "internal_passport"
// PassportAddressType identifies a residential address.
PassportAddressType PassportElementType = "address"
// PassportUtilityBillType identifies a utility bill.
PassportUtilityBillType PassportElementType = "utility_bill"
// PassportBankStatementType identifies a bank statement.
PassportBankStatementType PassportElementType = "bank_statement"
// PassportRentalAgreementType identifies a rental agreement.
PassportRentalAgreementType PassportElementType = "rental_agreement"
// PassportPassportRegistrationType identifies a passport registration.
PassportPassportRegistrationType PassportElementType = "passport_registration"
// PassportTemporaryRegistrationType identifies a temporary registration.
PassportTemporaryRegistrationType PassportElementType = "temporary_registration"
PassportPhoneNumberType PassportElementType = "phone_number"
PassportEmailType PassportElementType = "email"
// PassportPhoneNumberType identifies a phone number.
PassportPhoneNumberType PassportElementType = "phone_number"
// PassportEmailType identifies an email address.
PassportEmailType PassportElementType = "email"
)
// EncryptedPassportElement contains information about documents or other Telegram Passport elements.
+611
View File
@@ -0,0 +1,611 @@
package tgapi
import "encoding/json"
// RichBlock is a block in a structured rich message.
//
// Since: Bot API 10.1
type RichBlock interface {
isRichBlock()
}
// RichBlockCaption is the caption of a media block or container.
//
// Since: Bot API 10.1
type RichBlockCaption struct {
// Text contains the formatted or plain text content.
Text RichText
// Credit contains attribution displayed with the block.
Credit RichText
}
// MarshalJSON implements json.Marshaler.
//
// Since: Bot API 10.1
func (c RichBlockCaption) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Text RichText `json:"text"`
Credit RichText `json:"credit,omitempty"`
}{c.Text, c.Credit})
}
// UnmarshalJSON implements json.Unmarshaler.
//
// Since: Bot API 10.1
func (c *RichBlockCaption) UnmarshalJSON(data []byte) error {
var raw struct {
Text json.RawMessage `json:"text"`
Credit json.RawMessage `json:"credit"`
}
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
text, err := parseOptRichText(raw.Text)
if err != nil {
return err
}
credit, err := parseOptRichText(raw.Credit)
if err != nil {
return err
}
*c = RichBlockCaption{text, credit}
return nil
}
// RichBlockListItem is a single list item. Label is the ready-to-display
// visible marker ("1.", "c.", "vii.", "•"): the server renders it itself
// when parsing html/markdown.
//
// Since: Bot API 10.1
type RichBlockListItem struct {
// Label contains the list-item label.
Label string
// Blocks contains the nested rich-message blocks.
Blocks []RichBlock
// HasCheckbox reports whether the list item includes a checkbox.
HasCheckbox bool
// IsChecked reports whether the list-item checkbox is checked.
IsChecked bool
Value int // for ordered lists: numeric value of the marker
Type RichBlockListItemType // for ordered lists: "a", "A", "i", "I" or "1"
}
// MarshalJSON implements json.Marshaler.
//
// Since: Bot API 10.1
func (i RichBlockListItem) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Label string `json:"label"`
Blocks []RichBlock `json:"blocks"`
HasCheckbox bool `json:"has_checkbox,omitempty"`
IsChecked bool `json:"is_checked,omitempty"`
Value int `json:"value,omitempty"`
Type RichBlockListItemType `json:"type,omitempty"`
}{i.Label, i.Blocks, i.HasCheckbox, i.IsChecked, i.Value, i.Type})
}
// UnmarshalJSON implements json.Unmarshaler.
//
// Since: Bot API 10.1
func (i *RichBlockListItem) UnmarshalJSON(data []byte) error {
var raw struct {
Label string `json:"label"`
Blocks json.RawMessage `json:"blocks"`
HasCheckbox bool `json:"has_checkbox"`
IsChecked bool `json:"is_checked"`
Value int `json:"value"`
Type RichBlockListItemType `json:"type"`
}
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
blocks, err := unmarshalRichBlocks(raw.Blocks)
if err != nil {
return err
}
*i = RichBlockListItem{raw.Label, blocks, raw.HasCheckbox, raw.IsChecked, raw.Value, raw.Type}
return nil
}
// RichBlockTableCell is a table cell. An empty Text means an invisible cell.
//
// Since: Bot API 10.1
type RichBlockTableCell struct {
// Text contains the formatted or plain text content.
Text RichText
// IsHeader marks the table cell as a header cell.
IsHeader bool
// ColSpan is the number of table columns spanned by the cell.
ColSpan int
// RowSpan is the number of table rows spanned by the cell.
RowSpan int
Align string // "left", "center" or "right"
VAlign string // "top", "middle" or "bottom"
}
// MarshalJSON implements json.Marshaler.
//
// Since: Bot API 10.1
func (c RichBlockTableCell) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Text RichText `json:"text,omitempty"`
IsHeader bool `json:"is_header,omitempty"`
Colspan int `json:"colspan,omitempty"`
Rowspan int `json:"rowspan,omitempty"`
Align string `json:"align,omitempty"`
VAlign string `json:"valign,omitempty"`
}{c.Text, c.IsHeader, c.ColSpan, c.RowSpan, c.Align, c.VAlign})
}
// UnmarshalJSON implements json.Unmarshaler.
//
// Since: Bot API 10.1
func (c *RichBlockTableCell) UnmarshalJSON(data []byte) error {
var raw struct {
Text json.RawMessage `json:"text"`
IsHeader bool `json:"is_header"`
Colspan int `json:"colspan"`
Rowspan int `json:"rowspan"`
Align string `json:"align"`
VAlign string `json:"valign"`
}
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
text, err := parseOptRichText(raw.Text)
if err != nil {
return err
}
*c = RichBlockTableCell{text, raw.IsHeader, raw.Colspan, raw.Rowspan, raw.Align, raw.VAlign}
return nil
}
// RichBlockWrap covers all blocks that have only a text field.
//
// Since: Bot API 10.1
type RichBlockWrap struct {
// Tag identifies the rich-text formatting wrapper.
Tag string
// Text contains the formatted or plain text content.
Text RichText
}
func (RichBlockWrap) isRichBlock() {}
// MarshalJSON implements json.Marshaler.
//
// Since: Bot API 10.1
func (b RichBlockWrap) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Text RichText `json:"text"`
}{b.Tag, b.Text})
}
var richBlockWrapTags = map[string]bool{
"paragraph": true, "footer": true, "thinking": true,
}
// RichBlockSectionHeading is a section heading block.
//
// Since: Bot API 10.1
type RichBlockSectionHeading struct {
// Text contains the formatted or plain text content.
Text RichText
Size int // 1-6, 1 is the largest
}
func (RichBlockSectionHeading) isRichBlock() {}
// MarshalJSON implements json.Marshaler.
//
// Since: Bot API 10.1
func (b RichBlockSectionHeading) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Text RichText `json:"text"`
Size int `json:"size"`
}{"heading", b.Text, b.Size})
}
// RichBlockPreformatted is a preformatted code block.
//
// Since: Bot API 10.1
type RichBlockPreformatted struct {
// Text contains the formatted or plain text content.
Text RichText
// Language identifies the programming language used for syntax highlighting.
Language string
}
func (RichBlockPreformatted) isRichBlock() {}
// MarshalJSON implements json.Marshaler.
//
// Since: Bot API 10.1
func (b RichBlockPreformatted) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Text RichText `json:"text"`
Language string `json:"language,omitempty"`
}{"pre", b.Text, b.Language})
}
// RichBlockQuotation is a block quotation with block-level content
// (officially RichBlockBlockQuotation).
//
// Since: Bot API 10.1
type RichBlockQuotation struct {
// Blocks contains the nested rich-message blocks.
Blocks []RichBlock
// Credit contains attribution displayed with the block.
Credit RichText
}
func (RichBlockQuotation) isRichBlock() {}
// MarshalJSON implements json.Marshaler.
//
// Since: Bot API 10.1
func (b RichBlockQuotation) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Blocks []RichBlock `json:"blocks"`
Credit RichText `json:"credit,omitempty"`
}{"blockquote", b.Blocks, b.Credit})
}
// RichBlockPullQuotation is a pull quotation with inline content.
//
// Since: Bot API 10.1
type RichBlockPullQuotation struct {
// Text contains the formatted or plain text content.
Text RichText
// Credit contains attribution displayed with the block.
Credit RichText
}
func (RichBlockPullQuotation) isRichBlock() {}
// MarshalJSON implements json.Marshaler.
//
// Since: Bot API 10.1
func (b RichBlockPullQuotation) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Text RichText `json:"text"`
Credit RichText `json:"credit,omitempty"`
}{"pullquote", b.Text, b.Credit})
}
// RichBlockList is a list block.
//
// Since: Bot API 10.1
type RichBlockList struct {
// Items contains the list items.
Items []RichBlockListItem
}
func (RichBlockList) isRichBlock() {}
// MarshalJSON implements json.Marshaler.
//
// Since: Bot API 10.1
func (b RichBlockList) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Items []RichBlockListItem `json:"items"`
}{"list", b.Items})
}
// RichBlockCollage is a collage of media blocks.
//
// Since: Bot API 10.1
type RichBlockCollage struct {
// Blocks contains the nested rich-message blocks.
Blocks []RichBlock
// Caption contains the media or block caption.
Caption *RichBlockCaption
}
func (RichBlockCollage) isRichBlock() {}
// MarshalJSON implements json.Marshaler.
//
// Since: Bot API 10.1
func (b RichBlockCollage) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Blocks []RichBlock `json:"blocks"`
Caption *RichBlockCaption `json:"caption,omitempty"`
}{"collage", b.Blocks, b.Caption})
}
// RichBlockSlideshow is a slideshow of media blocks.
//
// Since: Bot API 10.1
type RichBlockSlideshow struct {
// Blocks contains the nested rich-message blocks.
Blocks []RichBlock
// Caption contains the media or block caption.
Caption *RichBlockCaption
}
func (RichBlockSlideshow) isRichBlock() {}
// MarshalJSON implements json.Marshaler.
//
// Since: Bot API 10.1
func (b RichBlockSlideshow) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Blocks []RichBlock `json:"blocks"`
Caption *RichBlockCaption `json:"caption,omitempty"`
}{"slideshow", b.Blocks, b.Caption})
}
// RichBlockDetails is an expandable block with an inline summary.
//
// Since: Bot API 10.1
type RichBlockDetails struct {
// Summary contains the visible summary of a details block.
Summary RichText
// Blocks contains the nested rich-message blocks.
Blocks []RichBlock
// IsOpen requests the details block to be expanded initially.
IsOpen bool
}
func (RichBlockDetails) isRichBlock() {}
// MarshalJSON implements json.Marshaler.
//
// Since: Bot API 10.1
func (b RichBlockDetails) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Summary RichText `json:"summary"`
Blocks []RichBlock `json:"blocks"`
IsOpen bool `json:"is_open,omitempty"`
}{"details", b.Summary, b.Blocks, b.IsOpen})
}
// RichBlockTable is a table block.
//
// Since: Bot API 10.1
type RichBlockTable struct {
// Cells contains the table rows and cells.
Cells [][]RichBlockTableCell
// IsBordered requests visible table borders.
IsBordered bool
// IsStriped requests alternating table row styling.
IsStriped bool
// Caption contains the media or block caption.
Caption RichText
}
func (RichBlockTable) isRichBlock() {}
// MarshalJSON implements json.Marshaler.
//
// Since: Bot API 10.1
func (b RichBlockTable) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Cells [][]RichBlockTableCell `json:"cells"`
IsBordered bool `json:"is_bordered,omitempty"`
IsStriped bool `json:"is_striped,omitempty"`
Caption RichText `json:"caption,omitempty"`
}{"table", b.Cells, b.IsBordered, b.IsStriped, b.Caption})
}
// RichBlockMap is a location map block.
//
// Since: Bot API 10.1
type RichBlockMap struct {
// Location contains the map location.
Location Location
Zoom int // 13-20
// Width is the requested media or map width in pixels.
Width int
// Height is the requested media or map height in pixels.
Height int
// Caption contains the media or block caption.
Caption *RichBlockCaption
}
func (RichBlockMap) isRichBlock() {}
// MarshalJSON implements json.Marshaler.
//
// Since: Bot API 10.1
func (b RichBlockMap) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Location Location `json:"location"`
Zoom int `json:"zoom"`
Width int `json:"width"`
Height int `json:"height"`
Caption *RichBlockCaption `json:"caption,omitempty"`
}{"map", b.Location, b.Zoom, b.Width, b.Height, b.Caption})
}
// RichBlockPhoto is a photo block.
//
// Since: Bot API 10.1
type RichBlockPhoto struct {
// Photo contains or identifies the associated photo.
Photo []PhotoSize
// HasSpoiler reports whether the media is covered by a spoiler.
HasSpoiler bool
// Caption contains the media or block caption.
Caption *RichBlockCaption
}
func (RichBlockPhoto) isRichBlock() {}
// MarshalJSON implements json.Marshaler.
//
// Since: Bot API 10.1
func (b RichBlockPhoto) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Photo []PhotoSize `json:"photo"`
HasSpoiler bool `json:"has_spoiler,omitempty"`
Caption *RichBlockCaption `json:"caption,omitempty"`
}{"photo", b.Photo, b.HasSpoiler, b.Caption})
}
// RichBlockVideo is a video block.
//
// Since: Bot API 10.1
type RichBlockVideo struct {
// Video contains the video rendered by the block.
Video Video
// HasSpoiler reports whether the media is covered by a spoiler.
HasSpoiler bool
// Caption contains the media or block caption.
Caption *RichBlockCaption
}
func (RichBlockVideo) isRichBlock() {}
// MarshalJSON implements json.Marshaler.
//
// Since: Bot API 10.1
func (b RichBlockVideo) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Video Video `json:"video"`
HasSpoiler bool `json:"has_spoiler,omitempty"`
Caption *RichBlockCaption `json:"caption,omitempty"`
}{"video", b.Video, b.HasSpoiler, b.Caption})
}
// RichBlockAudio is an audio block.
//
// Since: Bot API 10.1
type RichBlockAudio struct {
// Audio contains the audio rendered by the block.
Audio Audio
// Caption contains the media or block caption.
Caption *RichBlockCaption
}
func (RichBlockAudio) isRichBlock() {}
// MarshalJSON implements json.Marshaler.
//
// Since: Bot API 10.1
func (b RichBlockAudio) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Audio Audio `json:"audio"`
Caption *RichBlockCaption `json:"caption,omitempty"`
}{"audio", b.Audio, b.Caption})
}
// RichBlockAnimation is an animation block.
//
// Since: Bot API 10.1
type RichBlockAnimation struct {
// Animation contains the animation rendered by the block.
Animation Animation
// HasSpoiler reports whether the media is covered by a spoiler.
HasSpoiler bool
// Caption contains the media or block caption.
Caption *RichBlockCaption
}
func (RichBlockAnimation) isRichBlock() {}
// MarshalJSON implements json.Marshaler.
//
// Since: Bot API 10.1
func (b RichBlockAnimation) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Animation Animation `json:"animation"`
HasSpoiler bool `json:"has_spoiler,omitempty"`
Caption *RichBlockCaption `json:"caption,omitempty"`
}{"animation", b.Animation, b.HasSpoiler, b.Caption})
}
// RichBlockVoiceNote is a voice note block.
//
// Since: Bot API 10.1
type RichBlockVoiceNote struct {
// VoiceNote contains the voice note rendered by the block.
VoiceNote Voice
// Caption contains the media or block caption.
Caption *RichBlockCaption
}
func (RichBlockVoiceNote) isRichBlock() {}
// MarshalJSON implements json.Marshaler.
//
// Since: Bot API 10.1
func (b RichBlockVoiceNote) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
VoiceNote Voice `json:"voice_note"`
Caption *RichBlockCaption `json:"caption,omitempty"`
}{"voice_note", b.VoiceNote, b.Caption})
}
// RichBlockDivider is a horizontal divider block.
//
// Since: Bot API 10.1
type RichBlockDivider struct{}
func (RichBlockDivider) isRichBlock() {}
// MarshalJSON implements json.Marshaler.
//
// Since: Bot API 10.1
func (b RichBlockDivider) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
}{"divider"})
}
// RichBlockMathematicalExpression is a block-level mathematical expression.
//
// Since: Bot API 10.1
type RichBlockMathematicalExpression struct {
// Expression contains the mathematical expression source.
Expression string
}
func (RichBlockMathematicalExpression) isRichBlock() {}
// MarshalJSON implements json.Marshaler.
//
// Since: Bot API 10.1
func (b RichBlockMathematicalExpression) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Expression string `json:"expression"`
}{"mathematical_expression", b.Expression})
}
// RichBlockAnchor is a named anchor block that anchor links can point to.
//
// Since: Bot API 10.1
type RichBlockAnchor struct {
// Name is the user-facing or reference name of the value.
Name string
}
func (RichBlockAnchor) isRichBlock() {}
// MarshalJSON implements json.Marshaler.
//
// Since: Bot API 10.1
func (b RichBlockAnchor) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Name string `json:"name"`
}{"anchor", b.Name})
}
+427
View File
@@ -0,0 +1,427 @@
package tgapi
// InputRichType identifies the JSON type of an input rich block.
//
// Since: Bot API 10.2
type InputRichType string
const (
// InputRichTypeParagraph identifies a paragraph block.
InputRichTypeParagraph InputRichType = "paragraph"
// InputRichTypeSectionHeading identifies a section-heading block.
InputRichTypeSectionHeading InputRichType = "heading"
// InputRichTypePre identifies a preformatted block.
InputRichTypePre InputRichType = "pre"
// InputRichTypeFooter identifies a footer block.
InputRichTypeFooter InputRichType = "footer"
// InputRichTypeDivider identifies a divider block.
InputRichTypeDivider InputRichType = "divider"
// InputRichTypeMathematicalExpression identifies a mathematical-expression block.
InputRichTypeMathematicalExpression InputRichType = "mathematical_expression"
// InputRichTypeAnchor identifies an anchor block.
InputRichTypeAnchor InputRichType = "anchor"
// InputRichTypeList identifies a list block.
InputRichTypeList InputRichType = "list"
// InputRichTypeBlockQuotation identifies a block-quotation block.
InputRichTypeBlockQuotation InputRichType = "blockquote"
// InputRichTypePullQuotation identifies a pull-quotation block.
InputRichTypePullQuotation InputRichType = "pullquote"
// InputRichTypeCollage identifies a collage block.
InputRichTypeCollage InputRichType = "collage"
// InputRichTypeSlideshow identifies a slideshow block.
InputRichTypeSlideshow InputRichType = "slideshow"
// InputRichTypeTable identifies a table block.
InputRichTypeTable InputRichType = "table"
// InputRichTypeDetails identifies an expandable details block.
InputRichTypeDetails InputRichType = "details"
// InputRichTypeMap identifies a map block.
InputRichTypeMap InputRichType = "map"
// InputRichTypeAnimation identifies an animation block.
InputRichTypeAnimation InputRichType = "animation"
// InputRichTypeAudio identifies an audio block.
InputRichTypeAudio InputRichType = "audio"
// InputRichTypePhoto identifies a photo block.
InputRichTypePhoto InputRichType = "photo"
// InputRichTypeVideo identifies a video block.
InputRichTypeVideo InputRichType = "video"
// InputRichTypeVoiceNote identifies a voice-note block.
InputRichTypeVoiceNote InputRichType = "voice_note"
// InputRichTypeThinking identifies a thinking block.
InputRichTypeThinking InputRichType = "thinking"
)
// InputRichBlock represents a block available to format an outgoing rich message.
//
// Since: Bot API 10.2
type InputRichBlock interface {
isInputRichBlock()
}
// InputRichBlockParagraph is a text paragraph corresponding to the HTML <p> tag.
//
// Since: Bot API 10.2
type InputRichBlockParagraph struct {
// Type is the Bot API type discriminator.
Type InputRichType `json:"type"`
// Text contains the formatted or plain text content.
Text RichText `json:"text"`
}
func (InputRichBlockParagraph) isInputRichBlock() {}
// InputRichBlockSectionHeading is a section heading corresponding to an HTML <h1> through <h6> tag.
//
// Since: Bot API 10.2
type InputRichBlockSectionHeading struct {
// Type is the Bot API type discriminator.
Type InputRichType `json:"type"`
// Text contains the formatted or plain text content.
Text RichText `json:"text"`
// Size selects the section heading level from 1 through 6.
Size uint8 `json:"size"`
}
func (InputRichBlockSectionHeading) isInputRichBlock() {}
// InputRichBlockPreformatted is a preformatted text block corresponding to nested HTML <pre> and <code> tags.
//
// Since: Bot API 10.2
type InputRichBlockPreformatted struct {
// Type is the Bot API type discriminator.
Type InputRichType `json:"type"`
// Text contains the formatted or plain text content.
Text RichText `json:"text"`
// Language identifies the programming language used for syntax highlighting.
Language string `json:"language,omitempty"`
}
func (InputRichBlockPreformatted) isInputRichBlock() {}
// InputRichBlockFooter is a footer corresponding to the HTML <footer> tag.
//
// Since: Bot API 10.2
type InputRichBlockFooter struct {
// Type is the Bot API type discriminator.
Type InputRichType `json:"type"`
// Text contains the formatted or plain text content.
Text RichText `json:"text"`
}
func (InputRichBlockFooter) isInputRichBlock() {}
// InputRichBlockDivider is a divider corresponding to the HTML <hr/> tag.
//
// Since: Bot API 10.2
type InputRichBlockDivider struct {
// Type is the Bot API type discriminator.
Type InputRichType `json:"type"`
}
func (InputRichBlockDivider) isInputRichBlock() {}
// InputRichBlockMath is a block containing a mathematical expression in LaTeX format,
// corresponding to the custom HTML <tg-math-block> tag.
//
// Since: Bot API 10.2
type InputRichBlockMath struct {
// Type is the Bot API type discriminator.
Type InputRichType `json:"type"`
// Expression contains the mathematical expression source.
Expression string `json:"expression"`
}
func (InputRichBlockMath) isInputRichBlock() {}
// InputRichBlockAnchor is a block containing an anchor corresponding to an HTML <a> tag with a name attribute.
//
// Since: Bot API 10.2
type InputRichBlockAnchor struct {
// Type is the Bot API type discriminator.
Type InputRichType `json:"type"`
// Name is the user-facing or reference name of the value.
Name string `json:"name"`
}
func (InputRichBlockAnchor) isInputRichBlock() {}
// RichBlockListItemType identifies an ordered-list label style.
//
// Since: Bot API 10.2
type RichBlockListItemType string
const (
// InputRichBlockListItemTypeLower uses lowercase letters.
InputRichBlockListItemTypeLower RichBlockListItemType = "a"
// InputRichBlockListItemTypeUpper uses uppercase letters.
InputRichBlockListItemTypeUpper RichBlockListItemType = "A"
// InputRichBlockListItemTypeRomanLow uses lowercase Roman numerals.
InputRichBlockListItemTypeRomanLow RichBlockListItemType = "i"
// InputRichBlockListItemTypeRomanUpper uses uppercase Roman numerals.
InputRichBlockListItemTypeRomanUpper RichBlockListItemType = "I"
// InputRichBlockListItemTypeDecimal uses decimal numbers.
InputRichBlockListItemTypeDecimal RichBlockListItemType = "1"
)
// InputRichBlockListItem represents an item in an input rich-message list.
//
// Since: Bot API 10.2
type InputRichBlockListItem struct {
// Blocks contains the nested rich-message blocks.
Blocks []InputRichBlock `json:"blocks"`
// HasCheckbox reports whether the list item includes a checkbox.
HasCheckbox bool `json:"has_checkbox,omitempty"`
// IsChecked reports whether the list-item checkbox is checked.
IsChecked bool `json:"is_checked,omitempty"`
// Value sets the numeric marker value for an ordered list item.
Value int `json:"value,omitempty"`
// Type is the Bot API type discriminator.
Type RichBlockListItemType `json:"type,omitempty"`
}
// NewInputRichBlockListItem creates a list item containing blocks.
//
// Since: Bot API 10.2
func NewInputRichBlockListItem(blocks ...InputRichBlock) *InputRichBlockListItem {
return &InputRichBlockListItem{Blocks: blocks}
}
// SetCheckbox configures whether the list item has a checkbox.
//
// Since: Bot API 10.2
func (i *InputRichBlockListItem) SetCheckbox(hasCheckbox bool) *InputRichBlockListItem {
i.HasCheckbox = hasCheckbox
return i
}
// Check marks the list item's checkbox as checked.
//
// Since: Bot API 10.2
func (i *InputRichBlockListItem) Check() *InputRichBlockListItem {
i.IsChecked = true
return i
}
// SetValue sets the numeric value of an ordered-list item.
//
// Since: Bot API 10.2
func (i *InputRichBlockListItem) SetValue(val int) *InputRichBlockListItem {
i.Value = val
return i
}
// SetType sets the label style of an ordered-list item.
//
// Since: Bot API 10.2
func (i *InputRichBlockListItem) SetType(t RichBlockListItemType) *InputRichBlockListItem {
i.Type = t
return i
}
// InputRichBlockList is a list of input rich-message blocks.
//
// Since: Bot API 10.2
type InputRichBlockList struct {
// Type is the Bot API type discriminator.
Type InputRichType `json:"type"`
// Items contains the list items.
Items []InputRichBlockListItem `json:"items"`
}
func (InputRichBlockList) isInputRichBlock() {}
// InputRichBlockBlockQuotation is a block quotation in an input rich message.
//
// Since: Bot API 10.2
type InputRichBlockBlockQuotation struct {
// Type is the Bot API type discriminator.
Type InputRichType `json:"type"`
// Blocks contains the nested rich-message blocks.
Blocks []InputRichBlock `json:"blocks"`
// Credit contains attribution displayed with the block.
Credit *RichText `json:"credit,omitempty"`
}
func (InputRichBlockBlockQuotation) isInputRichBlock() {}
// InputRichBlockPullQuotation is a centered quotation in an input rich message.
//
// Since: Bot API 10.2
type InputRichBlockPullQuotation struct {
// Type is the Bot API type discriminator.
Type InputRichType `json:"type"`
// Text contains the formatted or plain text content.
Text RichText `json:"text"`
// Credit contains attribution displayed with the block.
Credit *RichText `json:"credit,omitempty"`
}
func (InputRichBlockPullQuotation) isInputRichBlock() {}
// InputRichBlockCollage is a collage in an input rich message.
//
// Since: Bot API 10.2
type InputRichBlockCollage struct {
// Type is the Bot API type discriminator.
Type InputRichType `json:"type"`
// Blocks contains the nested rich-message blocks.
Blocks []InputRichBlock `json:"blocks"`
// Caption contains the media or block caption.
Caption *RichBlockCaption `json:"caption,omitempty"`
}
func (InputRichBlockCollage) isInputRichBlock() {}
// InputRichBlockSlideshow is a slideshow in an input rich message.
//
// Since: Bot API 10.2
type InputRichBlockSlideshow struct {
// Type is the Bot API type discriminator.
Type InputRichType `json:"type"`
// Blocks contains the nested rich-message blocks.
Blocks []InputRichBlock `json:"blocks"`
// Caption contains the media or block caption.
Caption *RichBlockCaption `json:"caption,omitempty"`
}
func (InputRichBlockSlideshow) isInputRichBlock() {}
// InputRichBlockTable is a table in an input rich message.
//
// Since: Bot API 10.2
type InputRichBlockTable struct {
// Type is the Bot API type discriminator.
Type InputRichType `json:"type"`
// Cells contains the table rows and cells.
Cells [][]RichBlockTableCell `json:"cells"`
// IsBordered requests visible table borders.
IsBordered bool `json:"is_bordered,omitempty"`
// IsStriped requests alternating table row styling.
IsStriped bool `json:"is_striped,omitempty"`
// Caption contains the media or block caption.
Caption *RichText `json:"caption,omitempty"`
}
func (InputRichBlockTable) isInputRichBlock() {}
// InputRichBlockDetails is an expandable block in an input rich message.
//
// Since: Bot API 10.2
type InputRichBlockDetails struct {
// Type is the Bot API type discriminator.
Type InputRichType `json:"type"`
// Summary contains the visible summary of a details block.
Summary RichText `json:"summary"`
// Blocks contains the nested rich-message blocks.
Blocks []InputRichBlock `json:"blocks"`
// IsOpen requests the details block to be expanded initially.
IsOpen bool `json:"is_open,omitempty"`
}
func (InputRichBlockDetails) isInputRichBlock() {}
// InputRichBlockMap is a location map in an input rich message.
//
// Since: Bot API 10.2
type InputRichBlockMap struct {
// Type is the Bot API type discriminator.
Type InputRichType `json:"type"`
// Location contains the map location.
Location Location `json:"location"`
// Zoom sets the map zoom level.
Zoom uint8 `json:"zoom,omitempty"`
// Width is the requested media or map width in pixels.
Width uint16 `json:"width,omitempty"`
// Height is the requested media or map height in pixels.
Height uint16 `json:"height,omitempty"`
// Caption contains the media or block caption.
Caption *RichBlockCaption `json:"caption,omitempty"`
}
func (InputRichBlockMap) isInputRichBlock() {}
// InputRichBlockAnimation is an animation block corresponding to the HTML <video> tag.
// The animation caption is ignored; use Caption instead.
//
// Since: Bot API 10.2
type InputRichBlockAnimation struct {
// Type is the Bot API type discriminator.
Type InputRichType `json:"type"`
// Animation contains the animation rendered by the block.
Animation InputMedia `json:"animation"`
// Caption contains the media or block caption.
Caption *RichBlockCaption `json:"caption,omitempty"`
}
func (InputRichBlockAnimation) isInputRichBlock() {}
// InputRichBlockAudio is a music-file block corresponding to the HTML <audio> tag.
// The audio caption is ignored; use Caption instead.
//
// Since: Bot API 10.2
type InputRichBlockAudio struct {
// Type is the Bot API type discriminator.
Type InputRichType `json:"type"`
// Audio contains the audio rendered by the block.
Audio InputMedia `json:"audio"`
// Caption contains the media or block caption.
Caption *RichBlockCaption `json:"caption,omitempty"`
}
func (InputRichBlockAudio) isInputRichBlock() {}
// InputRichBlockPhoto is a photo block corresponding to the HTML <img> tag.
// The photo caption is ignored; use Caption instead.
//
// Since: Bot API 10.2
type InputRichBlockPhoto struct {
// Type is the Bot API type discriminator.
Type InputRichType `json:"type"`
// Photo contains or identifies the associated photo.
Photo InputMedia `json:"photo"`
// Caption contains the media or block caption.
Caption *RichBlockCaption `json:"caption,omitempty"`
}
func (InputRichBlockPhoto) isInputRichBlock() {}
// InputRichBlockVideo is a video block corresponding to the HTML <video> tag.
// The video caption is ignored; use Caption instead.
//
// Since: Bot API 10.2
type InputRichBlockVideo struct {
// Type is the Bot API type discriminator.
Type InputRichType `json:"type"`
// Video contains the video rendered by the block.
Video InputMedia `json:"video"`
// Caption contains the media or block caption.
Caption *RichBlockCaption `json:"caption,omitempty"`
}
func (InputRichBlockVideo) isInputRichBlock() {}
// InputRichBlockVoiceNote is a voice-note block corresponding to the HTML <audio> tag.
// The voice-note caption is ignored; use Caption instead.
//
// Since: Bot API 10.2
type InputRichBlockVoiceNote struct {
// Type is the Bot API type discriminator.
Type InputRichType `json:"type"`
// VoiceNote contains the voice note rendered by the block.
VoiceNote InputMedia `json:"voice_note"`
// Caption contains the media or block caption.
Caption *RichBlockCaption `json:"caption,omitempty"`
}
func (InputRichBlockVoiceNote) isInputRichBlock() {}
// InputRichBlockThinking is a block for displaying a thinking state.
//
// Since: Bot API 10.2
type InputRichBlockThinking struct {
// Type is the Bot API type discriminator.
Type InputRichType `json:"type"`
// Text contains the formatted or plain text content.
Text RichText `json:"text"`
}
func (InputRichBlockThinking) isInputRichBlock() {}
+65
View File
@@ -0,0 +1,65 @@
package tgapi
import (
"encoding/json"
"testing"
)
func TestInputRichMediaBlocksMarshal(t *testing.T) {
caption := RichBlockCaption{Text: RichTextPlain("caption")}
cases := []struct {
name string
block InputRichBlock
blockType InputRichType
mediaKey string
mediaType InputMediaType
}{
{"animation", InputRichBlockAnimation{Type: InputRichTypeAnimation, Animation: InputMedia{Type: InputMediaTypeAnimation, Media: "animation-id"}, Caption: &caption}, InputRichTypeAnimation, "animation", InputMediaTypeAnimation},
{"audio", InputRichBlockAudio{Type: InputRichTypeAudio, Audio: InputMedia{Type: InputMediaTypeAudio, Media: "audio-id"}, Caption: &caption}, InputRichTypeAudio, "audio", InputMediaTypeAudio},
{"photo", InputRichBlockPhoto{Type: InputRichTypePhoto, Photo: InputMedia{Type: InputMediaTypePhoto, Media: "photo-id"}, Caption: &caption}, InputRichTypePhoto, "photo", InputMediaTypePhoto},
{"video", InputRichBlockVideo{Type: InputRichTypeVideo, Video: InputMedia{Type: InputMediaTypeVideo, Media: "video-id"}, Caption: &caption}, InputRichTypeVideo, "video", InputMediaTypeVideo},
{"voice note", InputRichBlockVoiceNote{Type: InputRichTypeVoiceNote, VoiceNote: InputMedia{Type: InputMediaTypeVoiceNote, Media: "voice-id"}, Caption: &caption}, InputRichTypeVoiceNote, "voice_note", InputMediaTypeVoiceNote},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
data, err := json.Marshal(InputRichMessage{Blocks: []InputRichBlock{tt.block}})
if err != nil {
t.Fatalf("Marshal returned error: %v", err)
}
var message struct {
Blocks []map[string]json.RawMessage `json:"blocks"`
}
if err := json.Unmarshal(data, &message); err != nil {
t.Fatalf("Unmarshal returned error: %v", err)
}
if len(message.Blocks) != 1 {
t.Fatalf("got %d blocks, want 1", len(message.Blocks))
}
var blockType InputRichType
if err := json.Unmarshal(message.Blocks[0]["type"], &blockType); err != nil {
t.Fatalf("unmarshal block type: %v", err)
}
if blockType != tt.blockType {
t.Errorf("block type = %q, want %q", blockType, tt.blockType)
}
var media InputMedia
if err := json.Unmarshal(message.Blocks[0][tt.mediaKey], &media); err != nil {
t.Fatalf("unmarshal %s: %v", tt.mediaKey, err)
}
if media.Type != tt.mediaType {
t.Errorf("media type = %q, want %q", media.Type, tt.mediaType)
}
if message.Blocks[0]["caption"] == nil {
t.Error("caption is missing")
}
})
}
}
func TestInputRichBlockMapImplementsInputRichBlock(t *testing.T) {
var _ InputRichBlock = InputRichBlockMap{}
}
+155 -209
View File
@@ -1,51 +1,44 @@
package tgapi
import (
"encoding/json"
"fmt"
)
// Rich messages (Bot API 10.1), receive side: the RichText*/RichBlock* types
// mirror what the server sends in Message.rich_message, plus their parsers.
// These types intentionally have no constructors: sending goes only through
// InputRichMessage (html/markdown), and HTML generation lives in tgfmt
// (rich.go). Names follow the API objects; the exception is
// RichBlockQuotation (officially RichBlockBlockQuotation, the double Block
// is dropped).
import "encoding/json"
// RichText is a node of the rich formatted text tree: a plain string, an
// array, or one of the typed objects below.
//
// Since: Bot API 10.1
type RichText interface {
isRichText()
}
// ---------------------------------------------------------------------------
// Base forms: string and array
// ---------------------------------------------------------------------------
// RichTextPlain is a plain text leaf.
//
// Since: Bot API 10.1
type RichTextPlain string
func (RichTextPlain) isRichText() {}
// RichTextArray is a concatenation of rich text nodes.
//
// Since: Bot API 10.1
type RichTextArray []RichText
func (RichTextArray) isRichText() {}
// ---------------------------------------------------------------------------
// Nodes with only a text field. There are 9; only the tag differs.
// bold italic underline strikethrough spoiler subscript superscript marked code
// ---------------------------------------------------------------------------
// RichTextWrap covers all "pure" wrapper nodes with a single type.
//
// Since: Bot API 10.1
type RichTextWrap struct {
Tag string // "bold", "italic", ...
// Tag identifies the rich-text formatting wrapper.
Tag string
// Text contains the formatted or plain text content.
Text RichText
}
func (RichTextWrap) isRichText() {}
// MarshalJSON implements json.Marshaler.
//
// Since: Bot API 10.1
func (w RichTextWrap) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
@@ -59,17 +52,21 @@ var richTextWrapTags = map[string]bool{
"superscript": true, "marked": true, "code": true,
}
// ---------------------------------------------------------------------------
// Nodes with text + one extra string field.
// ---------------------------------------------------------------------------
// RichTextURL is rich text linking to a URL.
//
// Since: Bot API 10.1
type RichTextURL struct {
// Text contains the formatted or plain text content.
Text RichText
URL string
// URL contains the HTTP URL.
URL string
}
func (RichTextURL) isRichText() {}
// MarshalJSON implements json.Marshaler.
//
// Since: Bot API 10.1
func (v RichTextURL) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
@@ -79,12 +76,20 @@ func (v RichTextURL) MarshalJSON() ([]byte, error) {
}
// RichTextEmailAddress is rich text linking to an email address.
//
// Since: Bot API 10.1
type RichTextEmailAddress struct {
Text RichText
// Text contains the formatted or plain text content.
Text RichText
// EmailAddress is the email address associated with the text.
EmailAddress string
}
func (RichTextEmailAddress) isRichText() {}
// MarshalJSON implements json.Marshaler.
//
// Since: Bot API 10.1
func (v RichTextEmailAddress) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
@@ -94,12 +99,20 @@ func (v RichTextEmailAddress) MarshalJSON() ([]byte, error) {
}
// RichTextPhoneNumber is rich text linking to a phone number.
//
// Since: Bot API 10.1
type RichTextPhoneNumber struct {
Text RichText
// Text contains the formatted or plain text content.
Text RichText
// PhoneNumber is the phone number associated with the text.
PhoneNumber string
}
func (RichTextPhoneNumber) isRichText() {}
// MarshalJSON implements json.Marshaler.
//
// Since: Bot API 10.1
func (v RichTextPhoneNumber) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
@@ -109,12 +122,20 @@ func (v RichTextPhoneNumber) MarshalJSON() ([]byte, error) {
}
// RichTextBankCardNumber is rich text marked as a bank card number.
//
// Since: Bot API 10.1
type RichTextBankCardNumber struct {
Text RichText
// Text contains the formatted or plain text content.
Text RichText
// BankCardNumber is the bank card number associated with the text.
BankCardNumber string
}
func (RichTextBankCardNumber) isRichText() {}
// MarshalJSON implements json.Marshaler.
//
// Since: Bot API 10.1
func (v RichTextBankCardNumber) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
@@ -124,12 +145,20 @@ func (v RichTextBankCardNumber) MarshalJSON() ([]byte, error) {
}
// RichTextMention is rich text mentioning a user by username.
//
// Since: Bot API 10.1
type RichTextMention struct {
Text RichText
// Text contains the formatted or plain text content.
Text RichText
// Username is the username associated with the mention.
Username string
}
func (RichTextMention) isRichText() {}
// MarshalJSON implements json.Marshaler.
//
// Since: Bot API 10.1
func (v RichTextMention) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
@@ -139,12 +168,20 @@ func (v RichTextMention) MarshalJSON() ([]byte, error) {
}
// RichTextHashtag is rich text marked as a hashtag.
//
// Since: Bot API 10.1
type RichTextHashtag struct {
Text RichText
// Text contains the formatted or plain text content.
Text RichText
// Hashtag is the hashtag associated with the text.
Hashtag string
}
func (RichTextHashtag) isRichText() {}
// MarshalJSON implements json.Marshaler.
//
// Since: Bot API 10.1
func (v RichTextHashtag) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
@@ -154,12 +191,20 @@ func (v RichTextHashtag) MarshalJSON() ([]byte, error) {
}
// RichTextCashtag is rich text marked as a cashtag.
//
// Since: Bot API 10.1
type RichTextCashtag struct {
Text RichText
// Text contains the formatted or plain text content.
Text RichText
// Cashtag is the cashtag associated with the text.
Cashtag string
}
func (RichTextCashtag) isRichText() {}
// MarshalJSON implements json.Marshaler.
//
// Since: Bot API 10.1
func (v RichTextCashtag) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
@@ -169,12 +214,20 @@ func (v RichTextCashtag) MarshalJSON() ([]byte, error) {
}
// RichTextBotCommand is rich text marked as a bot command.
//
// Since: Bot API 10.1
type RichTextBotCommand struct {
Text RichText
// Text contains the formatted or plain text content.
Text RichText
// BotCommand is the bot command associated with the text.
BotCommand string
}
func (RichTextBotCommand) isRichText() {}
// MarshalJSON implements json.Marshaler.
//
// Since: Bot API 10.1
func (v RichTextBotCommand) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
@@ -184,12 +237,20 @@ func (v RichTextBotCommand) MarshalJSON() ([]byte, error) {
}
// RichTextAnchorLink is rich text linking to a named anchor in the same message.
//
// Since: Bot API 10.1
type RichTextAnchorLink struct {
Text RichText
// Text contains the formatted or plain text content.
Text RichText
// AnchorName names the anchor targeted by the link.
AnchorName string
}
func (RichTextAnchorLink) isRichText() {}
// MarshalJSON implements json.Marshaler.
//
// Since: Bot API 10.1
func (v RichTextAnchorLink) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
@@ -199,12 +260,20 @@ func (v RichTextAnchorLink) MarshalJSON() ([]byte, error) {
}
// RichTextReference is rich text marked as a named reference target.
//
// Since: Bot API 10.1
type RichTextReference struct {
// Text contains the formatted or plain text content.
Text RichText
// Name is the user-facing or reference name of the value.
Name string
}
func (RichTextReference) isRichText() {}
// MarshalJSON implements json.Marshaler.
//
// Since: Bot API 10.1
func (v RichTextReference) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
@@ -214,12 +283,20 @@ func (v RichTextReference) MarshalJSON() ([]byte, error) {
}
// RichTextReferenceLink is rich text linking to a named reference.
//
// Since: Bot API 10.1
type RichTextReferenceLink struct {
Text RichText
// Text contains the formatted or plain text content.
Text RichText
// ReferenceName names the reference targeted by the link.
ReferenceName string
}
func (RichTextReferenceLink) isRichText() {}
// MarshalJSON implements json.Marshaler.
//
// Since: Bot API 10.1
func (v RichTextReferenceLink) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
@@ -228,18 +305,23 @@ func (v RichTextReferenceLink) MarshalJSON() ([]byte, error) {
}{"reference_link", v.Text, v.ReferenceName})
}
// ---------------------------------------------------------------------------
// Nodes with text + multiple/non-string fields.
// ---------------------------------------------------------------------------
// RichTextDateTime is rich text bound to a point in time with a display format.
//
// Since: Bot API 10.1
type RichTextDateTime struct {
Text RichText
UnixTime int64
// Text contains the formatted or plain text content.
Text RichText
// UnixTime is the Unix timestamp associated with the text.
UnixTime int64
// DateTimeFormat controls how the associated Unix time is displayed.
DateTimeFormat string
}
func (RichTextDateTime) isRichText() {}
// MarshalJSON implements json.Marshaler.
//
// Since: Bot API 10.1
func (v RichTextDateTime) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
@@ -250,12 +332,20 @@ func (v RichTextDateTime) MarshalJSON() ([]byte, error) {
}
// RichTextTextMention is rich text mentioning a user without a username.
//
// Since: Bot API 10.1
type RichTextTextMention struct {
// Text contains the formatted or plain text content.
Text RichText
// User contains the user associated with the value.
User User
}
func (RichTextTextMention) isRichText() {}
// MarshalJSON implements json.Marshaler.
//
// Since: Bot API 10.1
func (v RichTextTextMention) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
@@ -264,17 +354,21 @@ func (v RichTextTextMention) MarshalJSON() ([]byte, error) {
}{"text_mention", v.Text, v.User})
}
// ---------------------------------------------------------------------------
// LEAVES: no text field.
// ---------------------------------------------------------------------------
// RichTextCustomEmoji is a custom emoji leaf with alternative text.
//
// Since: Bot API 10.1
type RichTextCustomEmoji struct {
CustomEmojiID string
// CustomEmojiID identifies the custom emoji.
CustomEmojiID string
// AlternativeText is shown when the custom emoji can't be rendered.
AlternativeText string
}
func (RichTextCustomEmoji) isRichText() {}
// MarshalJSON implements json.Marshaler.
//
// Since: Bot API 10.1
func (v RichTextCustomEmoji) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
@@ -284,11 +378,18 @@ func (v RichTextCustomEmoji) MarshalJSON() ([]byte, error) {
}
// RichTextMathematicalExpression is an inline mathematical expression leaf.
//
// Since: Bot API 10.1
type RichTextMathematicalExpression struct {
// Expression contains the mathematical expression source.
Expression string
}
func (RichTextMathematicalExpression) isRichText() {}
// MarshalJSON implements json.Marshaler.
//
// Since: Bot API 10.1
func (v RichTextMathematicalExpression) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
@@ -297,176 +398,21 @@ func (v RichTextMathematicalExpression) MarshalJSON() ([]byte, error) {
}
// RichTextAnchor is a named anchor leaf that anchor links can point to.
//
// Since: Bot API 10.1
type RichTextAnchor struct {
// Name is the user-facing or reference name of the value.
Name string
}
func (RichTextAnchor) isRichText() {}
// MarshalJSON implements json.Marshaler.
//
// Since: Bot API 10.1
func (v RichTextAnchor) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Name string `json:"name"`
}{"anchor", v.Name})
}
// ---------------------------------------------------------------------------
// JSON -> RichText parsing
// ---------------------------------------------------------------------------
// UnmarshalRichText parses a RichText tree from JSON: a string, an array, or
// a typed object. Unknown object types that carry a text field are preserved
// as RichTextWrap for forward compatibility.
func UnmarshalRichText(data []byte) (RichText, error) {
// 1. string
var s string
if err := json.Unmarshal(data, &s); err == nil {
return RichTextPlain(s), nil
}
// 2. array
var raw []json.RawMessage
if err := json.Unmarshal(data, &raw); err == nil {
arr := make(RichTextArray, len(raw))
for i, it := range raw {
rt, err := UnmarshalRichText(it)
if err != nil {
return nil, err
}
arr[i] = rt
}
return arr, nil
}
// 3. object -> dispatch on type, grabbing the raw text along the way
var head struct {
Type string `json:"type"`
Text json.RawMessage `json:"text"`
}
if err := json.Unmarshal(data, &head); err != nil {
return nil, fmt.Errorf("richtext: not a string, array or object: %w", err)
}
// Recursively parse the nested text, if any.
var inner RichText
if len(head.Text) > 0 {
var err error
if inner, err = UnmarshalRichText(head.Text); err != nil {
return nil, fmt.Errorf("richtext %q: bad text: %w", head.Type, err)
}
}
if richTextWrapTags[head.Type] {
return RichTextWrap{Tag: head.Type, Text: inner}, nil
}
switch head.Type {
case "url":
var v struct {
URL string `json:"url"`
}
if err := json.Unmarshal(data, &v); err != nil {
return nil, err
}
return RichTextURL{inner, v.URL}, nil
case "email_address":
var v struct {
V string `json:"email_address"`
}
_ = json.Unmarshal(data, &v)
return RichTextEmailAddress{inner, v.V}, nil
case "phone_number":
var v struct {
V string `json:"phone_number"`
}
_ = json.Unmarshal(data, &v)
return RichTextPhoneNumber{inner, v.V}, nil
case "bank_card_number":
var v struct {
V string `json:"bank_card_number"`
}
_ = json.Unmarshal(data, &v)
return RichTextBankCardNumber{inner, v.V}, nil
case "mention":
var v struct {
V string `json:"username"`
}
_ = json.Unmarshal(data, &v)
return RichTextMention{inner, v.V}, nil
case "hashtag":
var v struct {
V string `json:"hashtag"`
}
_ = json.Unmarshal(data, &v)
return RichTextHashtag{inner, v.V}, nil
case "cashtag":
var v struct {
V string `json:"cashtag"`
}
_ = json.Unmarshal(data, &v)
return RichTextCashtag{inner, v.V}, nil
case "bot_command":
var v struct {
V string `json:"bot_command"`
}
_ = json.Unmarshal(data, &v)
return RichTextBotCommand{inner, v.V}, nil
case "anchor_link":
var v struct {
V string `json:"anchor_name"`
}
_ = json.Unmarshal(data, &v)
return RichTextAnchorLink{inner, v.V}, nil
case "reference":
var v struct {
V string `json:"name"`
}
_ = json.Unmarshal(data, &v)
return RichTextReference{inner, v.V}, nil
case "reference_link":
var v struct {
V string `json:"reference_name"`
}
_ = json.Unmarshal(data, &v)
return RichTextReferenceLink{inner, v.V}, nil
case "date_time":
var v struct {
UnixTime int64 `json:"unix_time"`
DateTimeFormat string `json:"date_time_format"`
}
_ = json.Unmarshal(data, &v)
return RichTextDateTime{inner, v.UnixTime, v.DateTimeFormat}, nil
case "text_mention":
var v struct {
User User `json:"user"`
}
_ = json.Unmarshal(data, &v)
return RichTextTextMention{inner, v.User}, nil
// --- leaves without text ---
case "custom_emoji":
var v struct {
ID string `json:"custom_emoji_id"`
Alt string `json:"alternative_text"`
}
_ = json.Unmarshal(data, &v)
return RichTextCustomEmoji{v.ID, v.Alt}, nil
case "mathematical_expression":
var v struct {
Expression string `json:"expression"`
}
_ = json.Unmarshal(data, &v)
return RichTextMathematicalExpression{v.Expression}, nil
case "anchor":
var v struct {
Name string `json:"name"`
}
_ = json.Unmarshal(data, &v)
return RichTextAnchor{v.Name}, nil
default:
// forward-compat: keep an unknown tag with a text field as
// RichTextWrap; without text it is an error (the shape cannot be guessed).
if inner != nil {
return RichTextWrap{Tag: head.Type, Text: inner}, nil
}
return nil, fmt.Errorf("richtext: unknown type %q", head.Type)
}
}
@@ -68,3 +68,18 @@ func TestRichTextLeafHasNoText(t *testing.T) {
t.Fatalf("anchor must not have text field: %s", b)
}
}
func TestUnmarshalRichTextRejectsInvalidValues(t *testing.T) {
tests := []string{
`null`,
`{"type":"date_time","text":"now","unix_time":"soon"}`,
`{"type":"custom_emoji","custom_emoji_id":42}`,
}
for _, raw := range tests {
t.Run(raw, func(t *testing.T) {
if _, err := UnmarshalRichText([]byte(raw)); err == nil {
t.Fatal("expected malformed rich text to be rejected")
}
})
}
}
+514
View File
@@ -0,0 +1,514 @@
package tgapi
import (
"bytes"
"encoding/json"
"fmt"
)
// UnmarshalRichText parses a RichText tree from JSON: a string, an array, or
// a typed object. Unknown object types that carry a text field are preserved
// as RichTextWrap so their nested text remains usable; unmodeled fields are
// discarded.
//
// Since: Bot API 10.1
func UnmarshalRichText(data []byte) (RichText, error) {
if bytes.Equal(bytes.TrimSpace(data), []byte("null")) {
return nil, fmt.Errorf("richtext: null is not a rich text value")
}
// 1. string
var s string
if err := json.Unmarshal(data, &s); err == nil {
return RichTextPlain(s), nil
}
// 2. array
var raw []json.RawMessage
if err := json.Unmarshal(data, &raw); err == nil {
arr := make(RichTextArray, len(raw))
for i, it := range raw {
rt, err := UnmarshalRichText(it)
if err != nil {
return nil, err
}
arr[i] = rt
}
return arr, nil
}
// 3. object -> dispatch on type, grabbing the raw text along the way
var head struct {
Type string `json:"type"`
Text json.RawMessage `json:"text"`
}
if err := json.Unmarshal(data, &head); err != nil {
return nil, fmt.Errorf("richtext: not a string, array or object: %w", err)
}
// Recursively parse the nested text, if any.
var inner RichText
if len(head.Text) > 0 {
var err error
if inner, err = UnmarshalRichText(head.Text); err != nil {
return nil, fmt.Errorf("richtext %q: bad text: %w", head.Type, err)
}
}
if richTextWrapTags[head.Type] {
return RichTextWrap{Tag: head.Type, Text: inner}, nil
}
switch head.Type {
case "url":
var v struct {
URL string `json:"url"`
}
if err := json.Unmarshal(data, &v); err != nil {
return nil, err
}
return RichTextURL{inner, v.URL}, nil
case "email_address":
var v struct {
V string `json:"email_address"`
}
if err := json.Unmarshal(data, &v); err != nil {
return nil, err
}
return RichTextEmailAddress{inner, v.V}, nil
case "phone_number":
var v struct {
V string `json:"phone_number"`
}
if err := json.Unmarshal(data, &v); err != nil {
return nil, err
}
return RichTextPhoneNumber{inner, v.V}, nil
case "bank_card_number":
var v struct {
V string `json:"bank_card_number"`
}
if err := json.Unmarshal(data, &v); err != nil {
return nil, err
}
return RichTextBankCardNumber{inner, v.V}, nil
case "mention":
var v struct {
V string `json:"username"`
}
if err := json.Unmarshal(data, &v); err != nil {
return nil, err
}
return RichTextMention{inner, v.V}, nil
case "hashtag":
var v struct {
V string `json:"hashtag"`
}
if err := json.Unmarshal(data, &v); err != nil {
return nil, err
}
return RichTextHashtag{inner, v.V}, nil
case "cashtag":
var v struct {
V string `json:"cashtag"`
}
if err := json.Unmarshal(data, &v); err != nil {
return nil, err
}
return RichTextCashtag{inner, v.V}, nil
case "bot_command":
var v struct {
V string `json:"bot_command"`
}
if err := json.Unmarshal(data, &v); err != nil {
return nil, err
}
return RichTextBotCommand{inner, v.V}, nil
case "anchor_link":
var v struct {
V string `json:"anchor_name"`
}
if err := json.Unmarshal(data, &v); err != nil {
return nil, err
}
return RichTextAnchorLink{inner, v.V}, nil
case "reference":
var v struct {
V string `json:"name"`
}
if err := json.Unmarshal(data, &v); err != nil {
return nil, err
}
return RichTextReference{inner, v.V}, nil
case "reference_link":
var v struct {
V string `json:"reference_name"`
}
if err := json.Unmarshal(data, &v); err != nil {
return nil, err
}
return RichTextReferenceLink{inner, v.V}, nil
case "date_time":
var v struct {
UnixTime int64 `json:"unix_time"`
DateTimeFormat string `json:"date_time_format"`
}
if err := json.Unmarshal(data, &v); err != nil {
return nil, err
}
return RichTextDateTime{inner, v.UnixTime, v.DateTimeFormat}, nil
case "text_mention":
var v struct {
User User `json:"user"`
}
if err := json.Unmarshal(data, &v); err != nil {
return nil, err
}
return RichTextTextMention{inner, v.User}, nil
// --- leaves without text ---
case "custom_emoji":
var v struct {
ID string `json:"custom_emoji_id"`
Alt string `json:"alternative_text"`
}
if err := json.Unmarshal(data, &v); err != nil {
return nil, err
}
return RichTextCustomEmoji{v.ID, v.Alt}, nil
case "mathematical_expression":
var v struct {
Expression string `json:"expression"`
}
if err := json.Unmarshal(data, &v); err != nil {
return nil, err
}
return RichTextMathematicalExpression{v.Expression}, nil
case "anchor":
var v struct {
Name string `json:"name"`
}
if err := json.Unmarshal(data, &v); err != nil {
return nil, err
}
return RichTextAnchor{v.Name}, nil
default:
// forward-compat: keep an unknown tag with a text field as
// RichTextWrap; without text it is an error (the shape cannot be guessed).
if inner != nil {
return RichTextWrap{Tag: head.Type, Text: inner}, nil
}
return nil, fmt.Errorf("richtext: unknown type %q", head.Type)
}
}
// UnmarshalRichBlock parses a single RichBlock from JSON, dispatching on the
// type tag. Unknown types that carry a text field are decoded as RichBlockWrap
// so their nested text remains usable; unmodeled fields are discarded.
//
// Since: Bot API 10.1
func UnmarshalRichBlock(data []byte) (RichBlock, error) {
var head struct {
Type string `json:"type"`
Text json.RawMessage `json:"text"`
}
if err := json.Unmarshal(data, &head); err != nil {
return nil, fmt.Errorf("richblock: %w", err)
}
if richBlockWrapTags[head.Type] {
text, err := parseOptRichText(head.Text)
if err != nil {
return nil, fmt.Errorf("richblock %q: text: %w", head.Type, err)
}
return RichBlockWrap{Tag: head.Type, Text: text}, nil
}
switch head.Type {
case "heading":
var v struct {
Size int `json:"size"`
}
if err := json.Unmarshal(data, &v); err != nil {
return nil, err
}
text, err := parseOptRichText(head.Text)
if err != nil {
return nil, fmt.Errorf("richblock %q: text: %w", head.Type, err)
}
return RichBlockSectionHeading{text, v.Size}, nil
case "pre":
var v struct {
Language string `json:"language"`
}
if err := json.Unmarshal(data, &v); err != nil {
return nil, err
}
text, err := parseOptRichText(head.Text)
if err != nil {
return nil, fmt.Errorf("richblock %q: text: %w", head.Type, err)
}
return RichBlockPreformatted{text, v.Language}, nil
case "blockquote":
var raw struct {
Blocks json.RawMessage `json:"blocks"`
Credit json.RawMessage `json:"credit"`
}
if err := json.Unmarshal(data, &raw); err != nil {
return nil, err
}
blocks, err := unmarshalRichBlocks(raw.Blocks)
if err != nil {
return nil, err
}
credit, err := parseOptRichText(raw.Credit)
if err != nil {
return nil, fmt.Errorf("richblock %q: credit: %w", head.Type, err)
}
return RichBlockQuotation{blocks, credit}, nil
case "pullquote":
var raw struct {
Credit json.RawMessage `json:"credit"`
}
if err := json.Unmarshal(data, &raw); err != nil {
return nil, err
}
text, err := parseOptRichText(head.Text)
if err != nil {
return nil, fmt.Errorf("richblock %q: text: %w", head.Type, err)
}
credit, err := parseOptRichText(raw.Credit)
if err != nil {
return nil, fmt.Errorf("richblock %q: credit: %w", head.Type, err)
}
return RichBlockPullQuotation{text, credit}, nil
case "list":
var v struct {
Items []RichBlockListItem `json:"items"`
}
if err := json.Unmarshal(data, &v); err != nil {
return nil, err
}
return RichBlockList{v.Items}, nil
case "collage":
var raw struct {
Blocks json.RawMessage `json:"blocks"`
Caption *RichBlockCaption `json:"caption"`
}
if err := json.Unmarshal(data, &raw); err != nil {
return nil, err
}
blocks, err := unmarshalRichBlocks(raw.Blocks)
if err != nil {
return nil, err
}
return RichBlockCollage{blocks, raw.Caption}, nil
case "slideshow":
var raw struct {
Blocks json.RawMessage `json:"blocks"`
Caption *RichBlockCaption `json:"caption"`
}
if err := json.Unmarshal(data, &raw); err != nil {
return nil, err
}
blocks, err := unmarshalRichBlocks(raw.Blocks)
if err != nil {
return nil, err
}
return RichBlockSlideshow{blocks, raw.Caption}, nil
case "details":
var raw struct {
Summary json.RawMessage `json:"summary"`
Blocks json.RawMessage `json:"blocks"`
IsOpen bool `json:"is_open"`
}
if err := json.Unmarshal(data, &raw); err != nil {
return nil, err
}
summary, err := parseOptRichText(raw.Summary)
if err != nil {
return nil, fmt.Errorf("richblock %q: summary: %w", head.Type, err)
}
blocks, err := unmarshalRichBlocks(raw.Blocks)
if err != nil {
return nil, err
}
return RichBlockDetails{summary, blocks, raw.IsOpen}, nil
case "table":
var raw struct {
Cells [][]RichBlockTableCell `json:"cells"`
IsBordered bool `json:"is_bordered"`
IsStriped bool `json:"is_striped"`
Caption json.RawMessage `json:"caption"`
}
if err := json.Unmarshal(data, &raw); err != nil {
return nil, err
}
caption, err := parseOptRichText(raw.Caption)
if err != nil {
return nil, fmt.Errorf("richblock %q: caption: %w", head.Type, err)
}
return RichBlockTable{raw.Cells, raw.IsBordered, raw.IsStriped, caption}, nil
case "map":
var v struct {
Location Location `json:"location"`
Zoom int `json:"zoom"`
Width int `json:"width"`
Height int `json:"height"`
Caption *RichBlockCaption `json:"caption"`
}
if err := json.Unmarshal(data, &v); err != nil {
return nil, err
}
return RichBlockMap{v.Location, v.Zoom, v.Width, v.Height, v.Caption}, nil
case "photo":
var v struct {
Photo []PhotoSize `json:"photo"`
HasSpoiler bool `json:"has_spoiler"`
Caption *RichBlockCaption `json:"caption"`
}
if err := json.Unmarshal(data, &v); err != nil {
return nil, err
}
return RichBlockPhoto{v.Photo, v.HasSpoiler, v.Caption}, nil
case "video":
var v struct {
Video Video `json:"video"`
HasSpoiler bool `json:"has_spoiler"`
Caption *RichBlockCaption `json:"caption"`
}
if err := json.Unmarshal(data, &v); err != nil {
return nil, err
}
return RichBlockVideo{v.Video, v.HasSpoiler, v.Caption}, nil
case "audio":
var v struct {
Audio Audio `json:"audio"`
Caption *RichBlockCaption `json:"caption"`
}
if err := json.Unmarshal(data, &v); err != nil {
return nil, err
}
return RichBlockAudio{v.Audio, v.Caption}, nil
case "animation":
var v struct {
Animation Animation `json:"animation"`
HasSpoiler bool `json:"has_spoiler"`
Caption *RichBlockCaption `json:"caption"`
}
if err := json.Unmarshal(data, &v); err != nil {
return nil, err
}
return RichBlockAnimation{v.Animation, v.HasSpoiler, v.Caption}, nil
case "voice_note":
var v struct {
VoiceNote Voice `json:"voice_note"`
Caption *RichBlockCaption `json:"caption"`
}
if err := json.Unmarshal(data, &v); err != nil {
return nil, err
}
return RichBlockVoiceNote{v.VoiceNote, v.Caption}, nil
case "divider":
return RichBlockDivider{}, nil
case "mathematical_expression":
var v struct {
Expression string `json:"expression"`
}
if err := json.Unmarshal(data, &v); err != nil {
return nil, err
}
return RichBlockMathematicalExpression{v.Expression}, nil
case "anchor":
var v struct {
Name string `json:"name"`
}
if err := json.Unmarshal(data, &v); err != nil {
return nil, err
}
return RichBlockAnchor{v.Name}, nil
default:
// forward-compat: unknown type with text -> RichBlockWrap, without text -> error.
if text, err := parseOptRichText(head.Text); err == nil && text != nil {
return RichBlockWrap{Tag: head.Type, Text: text}, nil
}
return nil, fmt.Errorf("richblock: unknown type %q", head.Type)
}
}
// UnmarshalRichMessage parses a root RichMessage from JSON.
//
// Since: Bot API 10.1
func UnmarshalRichMessage(data []byte) (RichMessage, error) {
var raw struct {
Blocks json.RawMessage `json:"blocks"`
IsRTL bool `json:"is_rtl"`
}
if err := json.Unmarshal(data, &raw); err != nil {
return RichMessage{}, fmt.Errorf("richmessage: %w", err)
}
blocks, err := unmarshalRichBlocks(raw.Blocks)
if err != nil {
return RichMessage{}, err
}
return RichMessage{blocks, raw.IsRTL}, nil
}
// UnmarshalJSON implements json.Unmarshaler.
//
// Since: Bot API 10.1
func (m *RichMessage) UnmarshalJSON(data []byte) error {
parsed, err := UnmarshalRichMessage(data)
if err != nil {
return err
}
*m = parsed
return nil
}
// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------
// Optional RichText fields treat absent and null values as nil.
func parseOptRichText(raw json.RawMessage) (RichText, error) {
if len(raw) == 0 || string(raw) == "null" {
return nil, nil
}
return UnmarshalRichText(raw)
}
func unmarshalRichBlocks(raw json.RawMessage) ([]RichBlock, error) {
if len(raw) == 0 || string(raw) == "null" {
return nil, nil
}
var raws []json.RawMessage
if err := json.Unmarshal(raw, &raws); err != nil {
return nil, err
}
blocks := make([]RichBlock, len(raws))
for i, r := range raws {
b, err := UnmarshalRichBlock(r)
if err != nil {
return nil, err
}
blocks[i] = b
}
return blocks, nil
}
@@ -92,11 +92,11 @@ func TestRichBlockRoundtrip(t *testing.T) {
{Text: RichTextPlain("Score"), IsHeader: true, VAlign: "middle"},
},
{
{Text: RichTextPlain("Alice"), Colspan: 2},
{Text: RichTextPlain("Alice"), ColSpan: 2},
},
{
{}, // invisible cell
{Text: RichTextPlain("42"), Rowspan: 2},
{Text: RichTextPlain("42"), RowSpan: 2},
},
},
IsBordered: true,
@@ -251,3 +251,18 @@ func TestRichBlockUnknownTypeWithoutTextIsError(t *testing.T) {
t.Fatal("expected error for unknown type without text")
}
}
func TestUnmarshalRichBlockRejectsMalformedFields(t *testing.T) {
tests := []string{
`{"type":"heading","size":"large","text":"hello"}`,
`{"type":"blockquote","blocks":[],"credit":{"type":"date_time","text":"now","unix_time":"soon"}}`,
`{"type":"table","cells":[],"caption":{"type":"date_time","text":"now","unix_time":"soon"}}`,
}
for _, raw := range tests {
t.Run(raw, func(t *testing.T) {
if _, err := UnmarshalRichBlock([]byte(raw)); err == nil {
t.Fatal("expected malformed rich block to be rejected")
}
})
}
}
-738
View File
@@ -1,738 +0,0 @@
package tgapi
import (
"encoding/json"
"fmt"
)
// RichBlock is a block in a structured rich message.
type RichBlock interface {
isRichBlock()
}
// ---------------------------------------------------------------------------
// Helper types
// ---------------------------------------------------------------------------
// RichBlockCaption is the caption of a media block or container.
type RichBlockCaption struct {
Text RichText
Credit RichText
}
func (c RichBlockCaption) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Text RichText `json:"text"`
Credit RichText `json:"credit,omitempty"`
}{c.Text, c.Credit})
}
func (c *RichBlockCaption) UnmarshalJSON(data []byte) error {
var raw struct {
Text json.RawMessage `json:"text"`
Credit json.RawMessage `json:"credit"`
}
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
text, err := parseOptRichText(raw.Text)
if err != nil {
return err
}
credit, err := parseOptRichText(raw.Credit)
if err != nil {
return err
}
*c = RichBlockCaption{text, credit}
return nil
}
// RichBlockListItem is a single list item. Label is the ready-to-display
// visible marker ("1.", "c.", "vii.", "•"): the server renders it itself
// when parsing html/markdown.
type RichBlockListItem struct {
Label string
Blocks []RichBlock
HasCheckbox bool
IsChecked bool
Value int // for ordered lists: numeric value of the marker
Type string // for ordered lists: "a", "A", "i", "I" or "1"
}
func (i RichBlockListItem) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Label string `json:"label"`
Blocks []RichBlock `json:"blocks"`
HasCheckbox bool `json:"has_checkbox,omitempty"`
IsChecked bool `json:"is_checked,omitempty"`
Value int `json:"value,omitempty"`
Type string `json:"type,omitempty"`
}{i.Label, i.Blocks, i.HasCheckbox, i.IsChecked, i.Value, i.Type})
}
func (i *RichBlockListItem) UnmarshalJSON(data []byte) error {
var raw struct {
Label string `json:"label"`
Blocks json.RawMessage `json:"blocks"`
HasCheckbox bool `json:"has_checkbox"`
IsChecked bool `json:"is_checked"`
Value int `json:"value"`
Type string `json:"type"`
}
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
blocks, err := unmarshalRichBlocks(raw.Blocks)
if err != nil {
return err
}
*i = RichBlockListItem{raw.Label, blocks, raw.HasCheckbox, raw.IsChecked, raw.Value, raw.Type}
return nil
}
// RichBlockTableCell is a table cell. An empty Text means an invisible cell.
type RichBlockTableCell struct {
Text RichText
IsHeader bool
Colspan int
Rowspan int
Align string // "left", "center" or "right"
VAlign string // "top", "middle" or "bottom"
}
func (c RichBlockTableCell) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Text RichText `json:"text,omitempty"`
IsHeader bool `json:"is_header,omitempty"`
Colspan int `json:"colspan,omitempty"`
Rowspan int `json:"rowspan,omitempty"`
Align string `json:"align,omitempty"`
VAlign string `json:"valign,omitempty"`
}{c.Text, c.IsHeader, c.Colspan, c.Rowspan, c.Align, c.VAlign})
}
func (c *RichBlockTableCell) UnmarshalJSON(data []byte) error {
var raw struct {
Text json.RawMessage `json:"text"`
IsHeader bool `json:"is_header"`
Colspan int `json:"colspan"`
Rowspan int `json:"rowspan"`
Align string `json:"align"`
VAlign string `json:"valign"`
}
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
text, err := parseOptRichText(raw.Text)
if err != nil {
return err
}
*c = RichBlockTableCell{text, raw.IsHeader, raw.Colspan, raw.Rowspan, raw.Align, raw.VAlign}
return nil
}
// ---------------------------------------------------------------------------
// RichBlockWrap: pure text blocks — paragraph, footer, thinking.
// ---------------------------------------------------------------------------
// RichBlockWrap covers all blocks that have only a text field.
type RichBlockWrap struct {
Tag string
Text RichText
}
func (RichBlockWrap) isRichBlock() {}
func (b RichBlockWrap) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Text RichText `json:"text"`
}{b.Tag, b.Text})
}
var richBlockWrapTags = map[string]bool{
"paragraph": true, "footer": true, "thinking": true,
}
// ---------------------------------------------------------------------------
// Section heading: text + size
// ---------------------------------------------------------------------------
// RichBlockSectionHeading is a section heading block.
type RichBlockSectionHeading struct {
Text RichText
Size int // 1-6, 1 is the largest
}
func (RichBlockSectionHeading) isRichBlock() {}
func (b RichBlockSectionHeading) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Text RichText `json:"text"`
Size int `json:"size"`
}{"heading", b.Text, b.Size})
}
// ---------------------------------------------------------------------------
// Block with text + language
// ---------------------------------------------------------------------------
// RichBlockPreformatted is a preformatted code block.
type RichBlockPreformatted struct {
Text RichText
Language string
}
func (RichBlockPreformatted) isRichBlock() {}
func (b RichBlockPreformatted) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Text RichText `json:"text"`
Language string `json:"language,omitempty"`
}{"pre", b.Text, b.Language})
}
// ---------------------------------------------------------------------------
// Quotations
// ---------------------------------------------------------------------------
// RichBlockQuotation is a block quotation with block-level content
// (officially RichBlockBlockQuotation).
type RichBlockQuotation struct {
Blocks []RichBlock
Credit RichText
}
func (RichBlockQuotation) isRichBlock() {}
func (b RichBlockQuotation) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Blocks []RichBlock `json:"blocks"`
Credit RichText `json:"credit,omitempty"`
}{"blockquote", b.Blocks, b.Credit})
}
// RichBlockPullQuotation is a pull quotation with inline content.
type RichBlockPullQuotation struct {
Text RichText
Credit RichText
}
func (RichBlockPullQuotation) isRichBlock() {}
func (b RichBlockPullQuotation) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Text RichText `json:"text"`
Credit RichText `json:"credit,omitempty"`
}{"pullquote", b.Text, b.Credit})
}
// ---------------------------------------------------------------------------
// List
// ---------------------------------------------------------------------------
// RichBlockList is a list block.
type RichBlockList struct {
Items []RichBlockListItem
}
func (RichBlockList) isRichBlock() {}
func (b RichBlockList) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Items []RichBlockListItem `json:"items"`
}{"list", b.Items})
}
// ---------------------------------------------------------------------------
// Containers with blocks []RichBlock + caption
// ---------------------------------------------------------------------------
// RichBlockCollage is a collage of media blocks.
type RichBlockCollage struct {
Blocks []RichBlock
Caption *RichBlockCaption
}
func (RichBlockCollage) isRichBlock() {}
func (b RichBlockCollage) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Blocks []RichBlock `json:"blocks"`
Caption *RichBlockCaption `json:"caption,omitempty"`
}{"collage", b.Blocks, b.Caption})
}
// RichBlockSlideshow is a slideshow of media blocks.
type RichBlockSlideshow struct {
Blocks []RichBlock
Caption *RichBlockCaption
}
func (RichBlockSlideshow) isRichBlock() {}
func (b RichBlockSlideshow) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Blocks []RichBlock `json:"blocks"`
Caption *RichBlockCaption `json:"caption,omitempty"`
}{"slideshow", b.Blocks, b.Caption})
}
// ---------------------------------------------------------------------------
// Details — expandable block
// ---------------------------------------------------------------------------
// RichBlockDetails is an expandable block with an inline summary.
type RichBlockDetails struct {
Summary RichText
Blocks []RichBlock
IsOpen bool
}
func (RichBlockDetails) isRichBlock() {}
func (b RichBlockDetails) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Summary RichText `json:"summary"`
Blocks []RichBlock `json:"blocks"`
IsOpen bool `json:"is_open,omitempty"`
}{"details", b.Summary, b.Blocks, b.IsOpen})
}
// ---------------------------------------------------------------------------
// Table
// ---------------------------------------------------------------------------
// RichBlockTable is a table block.
type RichBlockTable struct {
Cells [][]RichBlockTableCell
IsBordered bool
IsStriped bool
Caption RichText
}
func (RichBlockTable) isRichBlock() {}
func (b RichBlockTable) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Cells [][]RichBlockTableCell `json:"cells"`
IsBordered bool `json:"is_bordered,omitempty"`
IsStriped bool `json:"is_striped,omitempty"`
Caption RichText `json:"caption,omitempty"`
}{"table", b.Cells, b.IsBordered, b.IsStriped, b.Caption})
}
// ---------------------------------------------------------------------------
// Map
// ---------------------------------------------------------------------------
// RichBlockMap is a location map block.
type RichBlockMap struct {
Location Location
Zoom int // 13-20
Width int
Height int
Caption *RichBlockCaption
}
func (RichBlockMap) isRichBlock() {}
func (b RichBlockMap) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Location Location `json:"location"`
Zoom int `json:"zoom"`
Width int `json:"width"`
Height int `json:"height"`
Caption *RichBlockCaption `json:"caption,omitempty"`
}{"map", b.Location, b.Zoom, b.Width, b.Height, b.Caption})
}
// ---------------------------------------------------------------------------
// Media blocks
// ---------------------------------------------------------------------------
// RichBlockPhoto is a photo block.
type RichBlockPhoto struct {
Photo []PhotoSize
HasSpoiler bool
Caption *RichBlockCaption
}
func (RichBlockPhoto) isRichBlock() {}
func (b RichBlockPhoto) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Photo []PhotoSize `json:"photo"`
HasSpoiler bool `json:"has_spoiler,omitempty"`
Caption *RichBlockCaption `json:"caption,omitempty"`
}{"photo", b.Photo, b.HasSpoiler, b.Caption})
}
// RichBlockVideo is a video block.
type RichBlockVideo struct {
Video Video
HasSpoiler bool
Caption *RichBlockCaption
}
func (RichBlockVideo) isRichBlock() {}
func (b RichBlockVideo) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Video Video `json:"video"`
HasSpoiler bool `json:"has_spoiler,omitempty"`
Caption *RichBlockCaption `json:"caption,omitempty"`
}{"video", b.Video, b.HasSpoiler, b.Caption})
}
// RichBlockAudio is an audio block.
type RichBlockAudio struct {
Audio Audio
Caption *RichBlockCaption
}
func (RichBlockAudio) isRichBlock() {}
func (b RichBlockAudio) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Audio Audio `json:"audio"`
Caption *RichBlockCaption `json:"caption,omitempty"`
}{"audio", b.Audio, b.Caption})
}
// RichBlockAnimation is an animation block.
type RichBlockAnimation struct {
Animation Animation
HasSpoiler bool
Caption *RichBlockCaption
}
func (RichBlockAnimation) isRichBlock() {}
func (b RichBlockAnimation) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Animation Animation `json:"animation"`
HasSpoiler bool `json:"has_spoiler,omitempty"`
Caption *RichBlockCaption `json:"caption,omitempty"`
}{"animation", b.Animation, b.HasSpoiler, b.Caption})
}
// RichBlockVoiceNote is a voice note block.
type RichBlockVoiceNote struct {
VoiceNote Voice
Caption *RichBlockCaption
}
func (RichBlockVoiceNote) isRichBlock() {}
func (b RichBlockVoiceNote) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
VoiceNote Voice `json:"voice_note"`
Caption *RichBlockCaption `json:"caption,omitempty"`
}{"voice_note", b.VoiceNote, b.Caption})
}
// ---------------------------------------------------------------------------
// Leaves without nested content
// ---------------------------------------------------------------------------
// RichBlockDivider is a horizontal divider block.
type RichBlockDivider struct{}
func (RichBlockDivider) isRichBlock() {}
func (b RichBlockDivider) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
}{"divider"})
}
// RichBlockMathematicalExpression is a block-level mathematical expression.
type RichBlockMathematicalExpression struct {
Expression string
}
func (RichBlockMathematicalExpression) isRichBlock() {}
func (b RichBlockMathematicalExpression) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Expression string `json:"expression"`
}{"mathematical_expression", b.Expression})
}
// RichBlockAnchor is a named anchor block that anchor links can point to.
type RichBlockAnchor struct {
Name string
}
func (RichBlockAnchor) isRichBlock() {}
func (b RichBlockAnchor) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Type string `json:"type"`
Name string `json:"name"`
}{"anchor", b.Name})
}
// ---------------------------------------------------------------------------
// JSON -> RichBlock parsing
// ---------------------------------------------------------------------------
// UnmarshalRichBlock parses a single RichBlock from JSON, dispatching on the
// type tag. Unknown types that carry a text field are preserved as
// RichBlockWrap for forward compatibility.
func UnmarshalRichBlock(data []byte) (RichBlock, error) {
var head struct {
Type string `json:"type"`
Text json.RawMessage `json:"text"`
}
if err := json.Unmarshal(data, &head); err != nil {
return nil, fmt.Errorf("richblock: %w", err)
}
if richBlockWrapTags[head.Type] {
text, err := parseOptRichText(head.Text)
if err != nil {
return nil, fmt.Errorf("richblock %q: text: %w", head.Type, err)
}
return RichBlockWrap{Tag: head.Type, Text: text}, nil
}
switch head.Type {
case "heading":
var v struct {
Size int `json:"size"`
}
_ = json.Unmarshal(data, &v)
text, _ := parseOptRichText(head.Text)
return RichBlockSectionHeading{text, v.Size}, nil
case "pre":
var v struct {
Language string `json:"language"`
}
_ = json.Unmarshal(data, &v)
text, _ := parseOptRichText(head.Text)
return RichBlockPreformatted{text, v.Language}, nil
case "blockquote":
var raw struct {
Blocks json.RawMessage `json:"blocks"`
Credit json.RawMessage `json:"credit"`
}
_ = json.Unmarshal(data, &raw)
blocks, err := unmarshalRichBlocks(raw.Blocks)
if err != nil {
return nil, err
}
credit, _ := parseOptRichText(raw.Credit)
return RichBlockQuotation{blocks, credit}, nil
case "pullquote":
var raw struct {
Credit json.RawMessage `json:"credit"`
}
_ = json.Unmarshal(data, &raw)
text, _ := parseOptRichText(head.Text)
credit, _ := parseOptRichText(raw.Credit)
return RichBlockPullQuotation{text, credit}, nil
case "list":
var v struct {
Items []RichBlockListItem `json:"items"`
}
if err := json.Unmarshal(data, &v); err != nil {
return nil, err
}
return RichBlockList{v.Items}, nil
case "collage":
var raw struct {
Blocks json.RawMessage `json:"blocks"`
Caption *RichBlockCaption `json:"caption"`
}
if err := json.Unmarshal(data, &raw); err != nil {
return nil, err
}
blocks, err := unmarshalRichBlocks(raw.Blocks)
if err != nil {
return nil, err
}
return RichBlockCollage{blocks, raw.Caption}, nil
case "slideshow":
var raw struct {
Blocks json.RawMessage `json:"blocks"`
Caption *RichBlockCaption `json:"caption"`
}
if err := json.Unmarshal(data, &raw); err != nil {
return nil, err
}
blocks, err := unmarshalRichBlocks(raw.Blocks)
if err != nil {
return nil, err
}
return RichBlockSlideshow{blocks, raw.Caption}, nil
case "details":
var raw struct {
Summary json.RawMessage `json:"summary"`
Blocks json.RawMessage `json:"blocks"`
IsOpen bool `json:"is_open"`
}
_ = json.Unmarshal(data, &raw)
summary, _ := parseOptRichText(raw.Summary)
blocks, err := unmarshalRichBlocks(raw.Blocks)
if err != nil {
return nil, err
}
return RichBlockDetails{summary, blocks, raw.IsOpen}, nil
case "table":
var raw struct {
Cells [][]RichBlockTableCell `json:"cells"`
IsBordered bool `json:"is_bordered"`
IsStriped bool `json:"is_striped"`
Caption json.RawMessage `json:"caption"`
}
if err := json.Unmarshal(data, &raw); err != nil {
return nil, err
}
caption, _ := parseOptRichText(raw.Caption)
return RichBlockTable{raw.Cells, raw.IsBordered, raw.IsStriped, caption}, nil
case "map":
var v struct {
Location Location `json:"location"`
Zoom int `json:"zoom"`
Width int `json:"width"`
Height int `json:"height"`
Caption *RichBlockCaption `json:"caption"`
}
_ = json.Unmarshal(data, &v)
return RichBlockMap{v.Location, v.Zoom, v.Width, v.Height, v.Caption}, nil
case "photo":
var v struct {
Photo []PhotoSize `json:"photo"`
HasSpoiler bool `json:"has_spoiler"`
Caption *RichBlockCaption `json:"caption"`
}
_ = json.Unmarshal(data, &v)
return RichBlockPhoto{v.Photo, v.HasSpoiler, v.Caption}, nil
case "video":
var v struct {
Video Video `json:"video"`
HasSpoiler bool `json:"has_spoiler"`
Caption *RichBlockCaption `json:"caption"`
}
_ = json.Unmarshal(data, &v)
return RichBlockVideo{v.Video, v.HasSpoiler, v.Caption}, nil
case "audio":
var v struct {
Audio Audio `json:"audio"`
Caption *RichBlockCaption `json:"caption"`
}
_ = json.Unmarshal(data, &v)
return RichBlockAudio{v.Audio, v.Caption}, nil
case "animation":
var v struct {
Animation Animation `json:"animation"`
HasSpoiler bool `json:"has_spoiler"`
Caption *RichBlockCaption `json:"caption"`
}
_ = json.Unmarshal(data, &v)
return RichBlockAnimation{v.Animation, v.HasSpoiler, v.Caption}, nil
case "voice_note":
var v struct {
VoiceNote Voice `json:"voice_note"`
Caption *RichBlockCaption `json:"caption"`
}
_ = json.Unmarshal(data, &v)
return RichBlockVoiceNote{v.VoiceNote, v.Caption}, nil
case "divider":
return RichBlockDivider{}, nil
case "mathematical_expression":
var v struct {
Expression string `json:"expression"`
}
_ = json.Unmarshal(data, &v)
return RichBlockMathematicalExpression{v.Expression}, nil
case "anchor":
var v struct {
Name string `json:"name"`
}
_ = json.Unmarshal(data, &v)
return RichBlockAnchor{v.Name}, nil
default:
// forward-compat: unknown type with text -> RichBlockWrap, without text -> error.
if text, err := parseOptRichText(head.Text); err == nil && text != nil {
return RichBlockWrap{Tag: head.Type, Text: text}, nil
}
return nil, fmt.Errorf("richblock: unknown type %q", head.Type)
}
}
// UnmarshalRichMessage parses a root RichMessage from JSON.
func UnmarshalRichMessage(data []byte) (RichMessage, error) {
var raw struct {
Blocks json.RawMessage `json:"blocks"`
IsRTL bool `json:"is_rtl"`
}
if err := json.Unmarshal(data, &raw); err != nil {
return RichMessage{}, fmt.Errorf("richmessage: %w", err)
}
blocks, err := unmarshalRichBlocks(raw.Blocks)
if err != nil {
return RichMessage{}, err
}
return RichMessage{blocks, raw.IsRTL}, nil
}
// UnmarshalJSON parses the blocks through UnmarshalRichBlock: the Blocks
// field is interface-typed, so the standard unmarshaler cannot handle it.
func (m *RichMessage) UnmarshalJSON(data []byte) error {
parsed, err := UnmarshalRichMessage(data)
if err != nil {
return err
}
*m = parsed
return nil
}
// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------
// parseOptRichText parses an optional RichText field: absent and null yield nil.
func parseOptRichText(raw json.RawMessage) (RichText, error) {
if len(raw) == 0 || string(raw) == "null" {
return nil, nil
}
return UnmarshalRichText(raw)
}
func unmarshalRichBlocks(raw json.RawMessage) ([]RichBlock, error) {
if len(raw) == 0 || string(raw) == "null" {
return nil, nil
}
var raws []json.RawMessage
if err := json.Unmarshal(raw, &raws); err != nil {
return nil, err
}
blocks := make([]RichBlock, len(raws))
for i, r := range raws {
b, err := UnmarshalRichBlock(r)
if err != nil {
return nil, err
}
blocks[i] = b
}
return blocks, nil
}
+4
View File
@@ -10,6 +10,10 @@ type SendSticker struct {
ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"`
DirectMessagesTopicID int `json:"direct_messages_topic_id,omitempty"`
// ReceiverUserID identifies the user who can see the ephemeral message.
ReceiverUserID int64 `json:"receiver_user_id,omitempty"` // Since: Bot API 10.2
// CallbackQueryID identifies the callback query that triggered an ephemeral response.
CallbackQueryID string `json:"callback_query_id,omitempty"` // Since: Bot API 10.2
Sticker string `json:"sticker"`
Emoji string `json:"emoji,omitempty"`
+46 -4
View File
@@ -63,6 +63,11 @@ const (
// UpdateTypeGuestMessage is a guest message update.
UpdateTypeGuestMessage UpdateType = "guest_message"
// UpdateTypeSubscription is a bot subscription update.
//
// Since: Bot API 10.2
UpdateTypeSubscription UpdateType = "subscription"
)
// Update represents an incoming update from Telegram.
@@ -101,6 +106,8 @@ type Update struct {
RemovedChatBoost *ChatBoostRemoved `json:"removed_chat_boost,omitempty"` // Since: Bot API 7.0
ManagedBot *ManagedBotUpdated `json:"managed_bot,omitempty"` // Since: Bot API 9.6
// Subscription contains a bot subscription update.
Subscription *BotSubscriptionUpdated `json:"subscription,omitempty"` // Since: Bot API 10.2
}
// UnmarshalJSON decodes an update and derives its Type from the populated payload field.
@@ -168,6 +175,8 @@ func (u *Update) UnmarshalJSON(data []byte) error {
u.Type = UpdateTypeRemovedChatBoost
case u.ManagedBot != nil:
u.Type = UpdateTypeManagedBot
case u.Subscription != nil:
u.Type = UpdateTypeSubscription
default:
u.Type = UpdateTypeUnknown
}
@@ -557,8 +566,11 @@ type WriteAccessAllowed struct {
type BackgroundFillType string
const (
BackgroundFillSolidType BackgroundFillType = "solid"
BackgroundFillGradientType BackgroundFillType = "gradient"
// BackgroundFillSolidType identifies a solid fill.
BackgroundFillSolidType BackgroundFillType = "solid"
// BackgroundFillGradientType identifies a two-color gradient.
BackgroundFillGradientType BackgroundFillType = "gradient"
// BackgroundFillFreeformGradientType identifies a freeform gradient.
BackgroundFillFreeformGradientType BackgroundFillType = "freeform_gradient"
)
@@ -581,9 +593,13 @@ type BackgroundFill struct {
type BackgroundTypeType string
const (
BackgroundTypeFillType BackgroundTypeType = "fill"
// BackgroundTypeFillType identifies a generated fill.
BackgroundTypeFillType BackgroundTypeType = "fill"
// BackgroundTypeWallpaperType identifies a wallpaper.
BackgroundTypeWallpaperType BackgroundTypeType = "wallpaper"
BackgroundTypePatternType BackgroundTypeType = "pattern"
// BackgroundTypePatternType identifies a pattern.
BackgroundTypePatternType BackgroundTypeType = "pattern"
// BackgroundTypeChatThemeType identifies a chat theme.
BackgroundTypeChatThemeType BackgroundTypeType = "chat_theme"
)
@@ -604,3 +620,29 @@ type BackgroundType struct {
ThemeName string `json:"theme_name,omitempty"`
}
// BotSubscriptionState identifies the state of a user's subscription to the bot.
//
// Since: Bot API 10.2
type BotSubscriptionState string
const (
// BotSubscriptionCanceledState indicates that the user canceled the subscription.
BotSubscriptionCanceledState BotSubscriptionState = "canceled"
// BotSubscriptionActiveState indicates that the user re-enabled the subscription.
BotSubscriptionActiveState BotSubscriptionState = "active"
// BotSubscriptionFailedState indicates that subscription payment failed.
BotSubscriptionFailedState BotSubscriptionState = "failed"
)
// BotSubscriptionUpdated describes a change to a user's payment subscription to the bot.
//
// Since: Bot API 10.2
type BotSubscriptionUpdated struct {
// User contains the user associated with the value.
User User `json:"user"`
// InvoicePayload contains the bot-defined subscription invoice payload.
InvoicePayload string `json:"invoice_payload"`
// State is the new subscription state.
State BotSubscriptionState `json:"state"`
}
+15
View File
@@ -72,6 +72,18 @@ func TestUpdateUnmarshalSetsType(t *testing.T) {
}`,
want: UpdateTypeManagedBot,
},
{
name: "subscription",
body: `{
"update_id": 6,
"subscription": {
"user": {"id": 13, "is_bot": false, "first_name": "Subscriber"},
"invoice_payload": "monthly",
"state": "active"
}
}`,
want: UpdateTypeSubscription,
},
}
for _, tt := range tests {
@@ -89,6 +101,9 @@ func TestUpdateUnmarshalSetsType(t *testing.T) {
if tt.want == UpdateTypeManagedBot && update.ManagedBot.Bot.ID != 12 {
t.Fatalf("unexpected managed bot id: got %d want %d", update.ManagedBot.Bot.ID, 12)
}
if tt.want == UpdateTypeSubscription && update.Subscription.User.ID != 13 {
t.Fatalf("unexpected subscription user id: got %d want %d", update.Subscription.User.ID, 13)
}
})
}
}
+11 -2
View File
@@ -61,6 +61,15 @@ func (f UploaderFile) SetType(t UploaderFileType) UploaderFile {
return f
}
// SetAttachName sets the multipart field name used by an attach:// reference.
// The name must match the suffix of the corresponding InputMedia.Media value.
//
// Since: Bot API 10.2
func (f UploaderFile) SetAttachName(name string) UploaderFile {
f.field = UploaderFileType(name)
return f
}
// Uploader is a Telegram Bot API client specialized for multipart file uploads.
//
// Use Uploader methods when you need to upload binary files directly
@@ -155,7 +164,7 @@ func (r UploaderRequest[R, P]) doRequest(ctx context.Context, up *Uploader) (R,
if err != nil {
return zero, err
}
up.logger.Debugln("UPLOADER RES", url, string(body))
up.logger.Debugln("UPLOADER RES", responseLogSummary(r.method, len(body)))
response, err := parseBody[R](body)
if err != nil {
@@ -167,7 +176,7 @@ func (r UploaderRequest[R, P]) doRequest(ctx context.Context, up *Uploader) (R,
after := *response.Parameters.RetryAfter
up.logger.Warnf("Rate limited, retry after %d seconds (chat: %d)", after, r.chatID)
if up.api.Limiter != nil {
if r.chatID > 0 {
if r.chatID != 0 {
up.api.Limiter.SetChatLock(r.chatID, after)
} else {
up.api.Limiter.SetGlobalLock(after)
+192
View File
@@ -1,6 +1,7 @@
package tgapi
import (
"context"
"errors"
"fmt"
"io"
@@ -105,6 +106,17 @@ func TestUploaderEncodesJSONFieldsAndLeavesAcceptEncodingToHTTPTransport(t *test
}
}
func TestUploaderRejectsDirectRichMessageDraftUpload(t *testing.T) {
uploader := &Uploader{}
_, err := uploader.SendRichMessageDraft(
SendRichMessageDraft{ChatID: 42, DraftID: 1},
NewUploaderFile("photo.jpg", []byte("photo")),
)
if !errors.Is(err, ErrRichMessageDraftUploadUnsupported) {
t.Fatalf("expected ErrRichMessageDraftUploadUnsupported, got %v", err)
}
}
func TestUploaderSurfacesResponseErrorForTelegramFailure(t *testing.T) {
const responseBody = `{"ok":false,"error_code":400,"description":"Bad Request: chat not found"}`
@@ -177,6 +189,141 @@ func TestNewUploaderFileDetectsFileTypeCaseInsensitively(t *testing.T) {
}
}
func TestUploaderSendLivePhotoUsesRequiredMultipartFields(t *testing.T) {
tests := []struct {
name string
send func(*Uploader) (Message, error)
}{
{
name: "background context",
send: func(uploader *Uploader) (Message, error) {
return uploader.SendLivePhoto(
UploadLivePhoto{ChatID: 42},
NewUploaderFile("live.mp4", []byte("video")),
NewUploaderFile("photo.jpg", []byte("image")),
)
},
},
{
name: "explicit context",
send: func(uploader *Uploader) (Message, error) {
return uploader.SendLivePhotoWithContext(
context.Background(),
UploadLivePhoto{ChatID: 42},
NewUploaderFile("live.mp4", []byte("video")),
NewUploaderFile("photo.jpg", []byte("image")),
)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var (
gotPath string
gotFiles map[string]multipartFile
parseErr error
)
client := &http.Client{
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
gotPath = req.URL.Path
gotFiles, parseErr = readMultipartFiles(req)
return &http.Response{
StatusCode: http.StatusOK,
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: io.NopCloser(strings.NewReader(`{"ok":true,"result":{"message_id":5,"date":1}}`)),
}, nil
}),
}
api := NewAPI(
NewAPIOpts("token").
SetAPIURL("https://example.test").
SetHTTPClient(client),
)
defer func() {
if err := api.Close(); err != nil {
t.Errorf("Close API returned error: %v", err)
}
}()
uploader := NewUploader(api)
defer func() {
if err := uploader.Close(); err != nil {
t.Errorf("Close uploader returned error: %v", err)
}
}()
if _, err := tt.send(uploader); err != nil {
t.Fatalf("SendLivePhoto returned error: %v", err)
}
if parseErr != nil {
t.Fatalf("multipart parse failed: %v", parseErr)
}
if gotPath != "/bottoken/sendLivePhoto" {
t.Fatalf("unexpected request path: %q", gotPath)
}
assertMultipartFile(t, gotFiles, "live_photo", "live.mp4", "video")
assertMultipartFile(t, gotFiles, "photo", "photo.jpg", "image")
})
}
}
func TestPrepareMultipartRichMessageUsesAttachName(t *testing.T) {
params := SendRichMessage{
ChatID: 42,
RichMessage: InputRichMessage{Blocks: []InputRichBlock{
InputRichBlockAnimation{
Type: InputRichTypeAnimation,
Animation: InputMedia{Type: InputMediaTypeAnimation, Media: "attach://animation"},
},
}},
}
buf, contentType, err := prepareMultipart(
[]UploaderFile{NewUploaderFile("animation.mp4", []byte("animation")).SetAttachName("animation")},
params,
)
if err != nil {
t.Fatalf("prepareMultipart returned error: %v", err)
}
_, contentTypeParams, err := mime.ParseMediaType(contentType)
if err != nil {
t.Fatalf("ParseMediaType returned error: %v", err)
}
reader := multipart.NewReader(buf, contentTypeParams["boundary"])
parts := make(map[string]string)
var fileData []byte
for {
part, err := reader.NextPart()
if err == io.EOF {
break
}
if err != nil {
t.Fatalf("NextPart returned error: %v", err)
}
data, err := io.ReadAll(part)
if err != nil {
t.Fatalf("ReadAll returned error: %v", err)
}
if part.FileName() != "" {
if part.FormName() != "animation" {
t.Errorf("file form name = %q, want animation", part.FormName())
}
fileData = data
continue
}
parts[part.FormName()] = string(data)
}
if string(fileData) != "animation" {
t.Errorf("file data = %q, want animation", fileData)
}
if got := parts["rich_message"]; !strings.Contains(got, `"media":"attach://animation"`) {
t.Errorf("rich_message = %s, want attach reference", got)
}
}
func readMultipartRequest(req *http.Request) (map[string]string, string, []byte, error) {
_, params, err := mime.ParseMediaType(req.Header.Get("Content-Type"))
if err != nil {
@@ -209,3 +356,48 @@ func readMultipartRequest(req *http.Request) (map[string]string, string, []byte,
fields[part.FormName()] = string(data)
}
}
type multipartFile struct {
name string
data string
}
func readMultipartFiles(req *http.Request) (map[string]multipartFile, error) {
_, params, err := mime.ParseMediaType(req.Header.Get("Content-Type"))
if err != nil {
return nil, err
}
reader := multipart.NewReader(req.Body, params["boundary"])
files := make(map[string]multipartFile)
for {
part, err := reader.NextPart()
if err == io.EOF {
return files, nil
}
if err != nil {
return nil, err
}
if part.FileName() == "" {
continue
}
data, err := io.ReadAll(part)
if err != nil {
return nil, err
}
files[part.FormName()] = multipartFile{name: part.FileName(), data: string(data)}
}
}
func assertMultipartFile(t *testing.T, files map[string]multipartFile, field, name, data string) {
t.Helper()
file, ok := files[field]
if !ok {
t.Fatalf("multipart field %q is missing", field)
}
if file.name != name {
t.Errorf("multipart field %q filename = %q, want %q", field, file.name, name)
}
if file.data != data {
t.Errorf("multipart field %q data = %q, want %q", field, file.data, data)
}
}
+89 -8
View File
@@ -2,6 +2,45 @@ package tgapi
import "context"
// SendRichMessage uploads files referenced by attach:// names in params.RichMessage
// and sends the rich message.
//
// Since: Bot API 10.2
func (u *Uploader) SendRichMessage(params SendRichMessage, files ...UploaderFile) (Message, error) {
req := NewUploaderRequestWithChatID[Message]("sendRichMessage", params, params.ChatID, files...)
return req.Do(u)
}
// SendRichMessageWithContext uploads files referenced by attach:// names in params.RichMessage
// and sends the rich message using ctx for cancellation and deadlines.
//
// Since: Bot API 10.2
func (u *Uploader) SendRichMessageWithContext(ctx context.Context, params SendRichMessage, files ...UploaderFile) (Message, error) {
req := NewUploaderRequestWithChatID[Message]("sendRichMessage", params, params.ChatID, files...)
return req.DoWithContext(ctx, u)
}
// SendRichMessageDraft streams a rich-message draft without direct file uploads.
// It returns ErrRichMessageDraftUploadUnsupported when files is non-empty.
//
// Since: Bot API 10.2
func (u *Uploader) SendRichMessageDraft(params SendRichMessageDraft, files ...UploaderFile) (bool, error) {
if len(files) > 0 {
return false, ErrRichMessageDraftUploadUnsupported
}
return u.api.SendRichMessageDraft(params)
}
// SendRichMessageDraftWithContext is the context-aware variant of SendRichMessageDraft.
//
// Since: Bot API 10.2
func (u *Uploader) SendRichMessageDraftWithContext(ctx context.Context, params SendRichMessageDraft, files ...UploaderFile) (bool, error) {
if len(files) > 0 {
return false, ErrRichMessageDraftUploadUnsupported
}
return u.api.SendRichMessageDraftWithContext(ctx, params)
}
// UploadPhoto holds parameters for uploading a photo using the Uploader.
// Since: Bot API 1.0
// See https://core.telegram.org/bots/api#sendphoto
@@ -10,6 +49,10 @@ type UploadPhoto struct {
ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"`
DirectMessagesTopicID int `json:"direct_messages_topic_id,omitempty"`
// ReceiverUserID identifies the user who can see the ephemeral message.
ReceiverUserID int64 `json:"receiver_user_id,omitempty"` // Since: Bot API 10.2
// CallbackQueryID identifies the callback query that triggered an ephemeral response.
CallbackQueryID string `json:"callback_query_id,omitempty"` // Since: Bot API 10.2
Caption string `json:"caption,omitempty"`
ParseMode ParseMode `json:"parse_mode,omitempty"`
@@ -53,6 +96,10 @@ type UploadAudio struct {
ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"`
DirectMessagesTopicID int `json:"direct_messages_topic_id,omitempty"`
// ReceiverUserID identifies the user who can see the ephemeral message.
ReceiverUserID int64 `json:"receiver_user_id,omitempty"` // Since: Bot API 10.2
// CallbackQueryID identifies the callback query that triggered an ephemeral response.
CallbackQueryID string `json:"callback_query_id,omitempty"` // Since: Bot API 10.2
Caption string `json:"caption,omitempty"`
ParseMode ParseMode `json:"parse_mode,omitempty"`
@@ -98,6 +145,10 @@ type UploadDocument struct {
ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"`
DirectMessagesTopicID int `json:"direct_messages_topic_id,omitempty"`
// ReceiverUserID identifies the user who can see the ephemeral message.
ReceiverUserID int64 `json:"receiver_user_id,omitempty"` // Since: Bot API 10.2
// CallbackQueryID identifies the callback query that triggered an ephemeral response.
CallbackQueryID string `json:"callback_query_id,omitempty"` // Since: Bot API 10.2
Caption string `json:"caption,omitempty"`
ParseMode ParseMode `json:"parse_mode,omitempty"`
@@ -140,6 +191,10 @@ type UploadVideo struct {
ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"`
DirectMessagesTopicID int `json:"direct_messages_topic_id,omitempty"`
// ReceiverUserID identifies the user who can see the ephemeral message.
ReceiverUserID int64 `json:"receiver_user_id,omitempty"` // Since: Bot API 10.2
// CallbackQueryID identifies the callback query that triggered an ephemeral response.
CallbackQueryID string `json:"callback_query_id,omitempty"` // Since: Bot API 10.2
Duration int `json:"duration,omitempty"`
Width int `json:"width,omitempty"`
@@ -189,6 +244,10 @@ type UploadAnimation struct {
ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"`
DirectMessagesTopicID int `json:"direct_messages_topic_id,omitempty"`
// ReceiverUserID identifies the user who can see the ephemeral message.
ReceiverUserID int64 `json:"receiver_user_id,omitempty"` // Since: Bot API 10.2
// CallbackQueryID identifies the callback query that triggered an ephemeral response.
CallbackQueryID string `json:"callback_query_id,omitempty"` // Since: Bot API 10.2
Duration int `json:"duration,omitempty"`
Width int `json:"width,omitempty"`
@@ -236,6 +295,10 @@ type UploadVoice struct {
ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"`
DirectMessagesTopicID int `json:"direct_messages_topic_id,omitempty"`
// ReceiverUserID identifies the user who can see the ephemeral message.
ReceiverUserID int64 `json:"receiver_user_id,omitempty"` // Since: Bot API 10.2
// CallbackQueryID identifies the callback query that triggered an ephemeral response.
CallbackQueryID string `json:"callback_query_id,omitempty"` // Since: Bot API 10.2
Caption string `json:"caption,omitempty"`
ParseMode ParseMode `json:"parse_mode,omitempty"`
@@ -278,6 +341,10 @@ type UploadVideoNote struct {
ChatID int64 `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"`
DirectMessagesTopicID int `json:"direct_messages_topic_id,omitempty"`
// ReceiverUserID identifies the user who can see the ephemeral message.
ReceiverUserID int64 `json:"receiver_user_id,omitempty"` // Since: Bot API 10.2
// CallbackQueryID identifies the callback query that triggered an ephemeral response.
CallbackQueryID string `json:"callback_query_id,omitempty"` // Since: Bot API 10.2
Duration int `json:"duration,omitempty"`
Length int `json:"length,omitempty"`
@@ -375,6 +442,10 @@ type UploadLivePhoto struct {
MessageThreadID int `json:"message_thread_id,omitempty"`
DirectMessagesTopicID int `json:"direct_messages_topic_id,omitempty"`
// ReceiverUserID identifies the user who can see the ephemeral message.
ReceiverUserID int64 `json:"receiver_user_id,omitempty"` // Since: Bot API 10.2
// CallbackQueryID identifies the callback query that triggered an ephemeral response.
CallbackQueryID string `json:"callback_query_id,omitempty"` // Since: Bot API 10.2
Caption string `json:"caption,omitempty"`
ParseMode ParseMode `json:"parse_mode,omitempty"`
CaptionEntities []MessageEntity `json:"caption_entities,omitempty"`
@@ -391,20 +462,30 @@ type UploadLivePhoto struct {
ReplyMarkup *ReplyMarkup `json:"reply_markup,omitempty"`
}
// SendLivePhoto uploads a live photo via multipart and sends it as a message.
// SendLivePhoto uploads a live-photo video and its static image via multipart.
// livePhoto is sent in the live_photo field and photo in the photo field.
// Since: Bot API 10.0
// file is the live photo file to upload.
// See https://core.telegram.org/bots/api#sendlivephoto
func (u *Uploader) SendLivePhoto(params UploadLivePhoto, file UploaderFile) (Message, error) {
req := NewUploaderRequestWithChatID[Message]("sendLivePhoto", params, params.ChatID, file.SetType(UploaderLivePhotoType))
func (u *Uploader) SendLivePhoto(params UploadLivePhoto, livePhoto, photo UploaderFile) (Message, error) {
req := NewUploaderRequestWithChatID[Message](
"sendLivePhoto", params, params.ChatID,
livePhoto.SetType(UploaderLivePhotoType),
photo.SetType(UploaderPhotoType),
)
return req.Do(u)
}
// SendLivePhotoWithContext is the context-aware variant of SendLivePhoto.
// SendLivePhotoWithContext uploads a live-photo video and its static image via
// multipart using ctx for cancellation and deadlines.
// Since: Bot API 10.0
// It executes the same request but uses ctx for cancellation and deadlines.
// See https://core.telegram.org/bots/api#sendlivephoto
func (u *Uploader) SendLivePhotoWithContext(ctx context.Context, params UploadLivePhoto, file UploaderFile) (Message, error) {
req := NewUploaderRequestWithChatID[Message]("sendLivePhoto", params, params.ChatID, file.SetType(UploaderLivePhotoType))
func (u *Uploader) SendLivePhotoWithContext(
ctx context.Context, params UploadLivePhoto, livePhoto, photo UploaderFile,
) (Message, error) {
req := NewUploaderRequestWithChatID[Message](
"sendLivePhoto", params, params.ChatID,
livePhoto.SetType(UploaderLivePhotoType),
photo.SetType(UploaderPhotoType),
)
return req.DoWithContext(ctx, u)
}