(new): rich message support
(fix): runtime reliability (tests): regression coverage (doc): v1.1 release notes
This commit is contained in:
+3
-3
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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{}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
@@ -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() {}
|
||||
@@ -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{}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user