package botapi import ( "encoding/json" "errors" "net/url" "strconv" "strings" "unicode/utf8" "telesrv/internal/domain" "telesrv/internal/store" ) func inlineResultFromAPI(raw string) (domain.BotInlineResult, error) { if strings.TrimSpace(raw) == "" { return domain.BotInlineResult{}, errors.New("RESULT_ID_INVALID") } var payload apiInlineResult if err := json.Unmarshal([]byte(raw), &payload); err != nil { return domain.BotInlineResult{}, errors.New("RESULT_TYPE_INVALID") } if payload.ID == "" { return domain.BotInlineResult{}, errors.New("RESULT_ID_EMPTY") } if len(payload.ID) > domain.MaxBotInlineResultIDLen { return domain.BotInlineResult{}, errors.New("RESULT_ID_INVALID") } if payload.Type != "article" { return domain.BotInlineResult{}, errors.New("RESULT_TYPE_INVALID") } if payload.URL != "" && !validBotAPIHTTPSURL(payload.URL) { return domain.BotInlineResult{}, errors.New("BUTTON_URL_INVALID") } message, entities, noWebpage, err := inputTextMessageContentFromAPI(payload) if err != nil { return domain.BotInlineResult{}, err } markup, err := inlineReplyMarkupFromAPI(payload.ReplyMarkup) if err != nil { return domain.BotInlineResult{}, err } return domain.BotInlineResult{ ID: payload.ID, Type: payload.Type, Title: payload.Title, Description: payload.Description, URL: payload.URL, Message: message, Entities: entities, ReplyMarkup: markup, NoWebpage: noWebpage, }, nil } func inputTextMessageContentFromAPI(payload apiInlineResult) (string, []domain.MessageEntity, bool, error) { var content apiInputTextMessageContent if len(payload.InputMessageContent) > 0 && string(payload.InputMessageContent) != "null" { if err := json.Unmarshal(payload.InputMessageContent, &content); err != nil { return "", nil, false, errors.New("MESSAGE_EMPTY") } } else if payload.MessageText != "" { content.MessageText = payload.MessageText } if content.ParseMode != "" { return "", nil, false, errors.New("ENTITY_PARSE_UNSUPPORTED") } message := content.MessageText if message == "" { return "", nil, false, errors.New("MESSAGE_EMPTY") } if utf8.RuneCountInString(message) > domain.MaxMessageTextLength { return "", nil, false, errors.New("MESSAGE_TOO_LONG") } entities, err := messageEntitiesFromAPI(content.Entities) if err != nil { return "", nil, false, err } noWebpage := content.DisableWebPagePreview || content.LinkPreviewOptions.IsDisabled return message, entities, noWebpage, nil } func messageEntitiesFromAPI(in []apiMessageEntity) ([]domain.MessageEntity, error) { if len(in) == 0 { return nil, nil } if len(in) > domain.MaxMessageEntityCount { return nil, errors.New("ENTITIES_TOO_LONG") } out := make([]domain.MessageEntity, 0, len(in)) for _, entity := range in { if entity.Offset < 0 || entity.Length <= 0 { return nil, errors.New("ENTITY_BOUNDS_INVALID") } mapped, ok := apiEntityType(entity.Type) if !ok { return nil, errors.New("ENTITY_TYPE_UNSUPPORTED") } item := domain.MessageEntity{ Type: mapped, Offset: entity.Offset, Length: entity.Length, URL: entity.URL, Language: entity.Language, } if entity.User != nil { item.UserID = entity.User.ID } if entity.CustomEmojiID != "" { id, err := strconv.ParseInt(entity.CustomEmojiID, 10, 64) if err != nil || id <= 0 { return nil, errors.New("ENTITY_TYPE_UNSUPPORTED") } item.DocumentID = id } out = append(out, item) } return out, nil } func apiEntityType(in string) (domain.MessageEntityType, bool) { switch in { case "bold": return domain.MessageEntityBold, true case "italic": return domain.MessageEntityItalic, true case "underline": return domain.MessageEntityUnderline, true case "strikethrough": return domain.MessageEntityStrike, true case "code": return domain.MessageEntityCode, true case "pre": return domain.MessageEntityPre, true case "text_link": return domain.MessageEntityTextURL, true case "text_mention": return domain.MessageEntityMentionName, true case "spoiler": return domain.MessageEntitySpoiler, true case "blockquote": return domain.MessageEntityBlockquote, true case "custom_emoji": return domain.MessageEntityCustomEmoji, true case "mention": return domain.MessageEntityMention, true case "hashtag": return domain.MessageEntityHashtag, true case "cashtag": return domain.MessageEntityCashtag, true case "bot_command": return domain.MessageEntityBotCommand, true case "url": return domain.MessageEntityURL, true case "email": return domain.MessageEntityEmail, true case "phone_number": return domain.MessageEntityPhone, true default: return "", false } } func replyMarkupFromAPI(raw json.RawMessage) (*domain.MessageReplyMarkup, error) { if len(raw) == 0 || string(raw) == "null" { return nil, nil } var shape map[string]json.RawMessage if err := json.Unmarshal(raw, &shape); err != nil { return nil, errors.New("BUTTON_INVALID") } constructors := 0 for _, key := range []string{"inline_keyboard", "keyboard", "remove_keyboard", "force_reply"} { if _, ok := shape[key]; ok { constructors++ } } if constructors != 1 { return nil, errors.New("BUTTON_INVALID") } if _, ok := shape["inline_keyboard"]; ok { return inlineKeyboardMarkupFromAPI(raw) } if _, ok := shape["keyboard"]; ok { return replyKeyboardMarkupFromAPI(raw) } if _, ok := shape["remove_keyboard"]; ok { var payload apiReplyKeyboardRemove if err := json.Unmarshal(raw, &payload); err != nil || !payload.RemoveKeyboard { return nil, errors.New("BUTTON_INVALID") } out := &domain.MessageReplyMarkup{Type: domain.MessageReplyMarkupHide, Selective: payload.Selective} if err := domain.ValidateReplyMarkup(out); err != nil { return nil, replyMarkupErrFromDomain(err) } return out, nil } var payload apiForceReply if err := json.Unmarshal(raw, &payload); err != nil || !payload.ForceReply { return nil, errors.New("BUTTON_INVALID") } out := &domain.MessageReplyMarkup{ Type: domain.MessageReplyMarkupForceReply, SingleUse: true, Selective: payload.Selective, Placeholder: payload.InputFieldPlaceholder, } if err := domain.ValidateReplyMarkup(out); err != nil { return nil, replyMarkupErrFromDomain(err) } return out, nil } func inlineReplyMarkupFromAPI(raw json.RawMessage) (*domain.MessageReplyMarkup, error) { markup, err := replyMarkupFromAPI(raw) if err != nil || markup == nil { return markup, err } if markup.Kind() != domain.MessageReplyMarkupInline { return nil, errors.New("BUTTON_INVALID") } return markup, nil } func inlineKeyboardMarkupFromAPI(raw json.RawMessage) (*domain.MessageReplyMarkup, error) { var payload apiInlineKeyboardMarkup if err := json.Unmarshal(raw, &payload); err != nil { return nil, errors.New("BUTTON_INVALID") } if len(payload.InlineKeyboard) == 0 { return nil, nil } out := &domain.MessageReplyMarkup{Type: domain.MessageReplyMarkupInline, Inline: make([][]domain.MarkupButton, 0, len(payload.InlineKeyboard))} for _, row := range payload.InlineKeyboard { domainRow := make([]domain.MarkupButton, 0, len(row)) for _, button := range row { item, err := markupButtonFromAPI(button) if err != nil { return nil, err } domainRow = append(domainRow, item) } out.Inline = append(out.Inline, domainRow) } if err := domain.ValidateReplyMarkup(out); err != nil { return nil, replyMarkupErrFromDomain(err) } if out.IsZero() { return nil, nil } return out, nil } func replyKeyboardMarkupFromAPI(raw json.RawMessage) (*domain.MessageReplyMarkup, error) { var payload apiReplyKeyboardMarkup if err := json.Unmarshal(raw, &payload); err != nil || len(payload.Keyboard) == 0 { return nil, errors.New("BUTTON_INVALID") } out := &domain.MessageReplyMarkup{ Type: domain.MessageReplyMarkupKeyboard, Keyboard: make([][]domain.MarkupButton, 0, len(payload.Keyboard)), Resize: payload.ResizeKeyboard, SingleUse: payload.OneTimeKeyboard, Selective: payload.Selective, Persistent: payload.IsPersistent, Placeholder: payload.InputFieldPlaceholder, } for _, row := range payload.Keyboard { domainRow := make([]domain.MarkupButton, 0, len(row)) for _, button := range row { if button.Text == "" { return nil, errors.New("BUTTON_INVALID") } if button.Unsupported { return nil, errors.New("BUTTON_TYPE_INVALID") } style, icon, err := markupButtonDecorationFromAPI(button.Style, button.IconCustomEmojiID, button.IconCustomEmojiIDSet) if err != nil { return nil, err } item := domain.MarkupButton{Type: domain.MarkupButtonText, Text: button.Text, Style: style, IconCustomEmojiID: icon} switch button.Kind { case "request_contact": item.Type = domain.MarkupButtonRequestPhone case "request_location": item.Type = domain.MarkupButtonRequestLocation case "request_poll": item.Type, item.PollType = domain.MarkupButtonRequestPoll, button.PollType case "request_users": item.Type, item.ButtonID, item.RequestPeerType = domain.MarkupButtonRequestPeer, button.RequestID, "user" item.MaxQuantity, item.NameRequested, item.UsernameRequested, item.PhotoRequested = button.MaxQuantity, button.RequestName, button.RequestUsername, button.RequestPhoto item.RequestPeerFilter = button.RequestPeerFilter case "request_chat": item.Type, item.ButtonID = domain.MarkupButtonRequestPeer, button.RequestID if button.ChatIsChannel { item.RequestPeerType = "broadcast" } else { item.RequestPeerType = "chat" } item.MaxQuantity, item.NameRequested, item.UsernameRequested, item.PhotoRequested = 1, button.RequestTitle, button.RequestUsername, button.RequestPhoto item.RequestPeerFilter = button.RequestPeerFilter case "web_app": item.Type, item.URL = domain.MarkupButtonSimpleWebView, button.WebAppURL } domainRow = append(domainRow, item) } out.Keyboard = append(out.Keyboard, domainRow) } if err := domain.ValidateReplyMarkup(out); err != nil { return nil, replyMarkupErrFromDomain(err) } return out, nil } func markupButtonFromAPI(button apiInlineKeyboardButton) (domain.MarkupButton, error) { if button.Unsupported { return domain.MarkupButton{}, errors.New("BUTTON_TYPE_INVALID") } constructors := 0 if button.URLSet { constructors++ } if button.CallbackDataSet { constructors++ } if button.WebAppSet { constructors++ } if button.SwitchInlineSet { constructors++ } if button.CopyTextSet { constructors++ } if constructors != 1 { return domain.MarkupButton{}, errors.New("BUTTON_INVALID") } style, icon, err := markupButtonDecorationFromAPI(button.Style, button.IconCustomEmojiID, button.IconCustomEmojiIDSet) if err != nil { return domain.MarkupButton{}, err } if button.URLSet { return domain.MarkupButton{Type: domain.MarkupButtonURL, Text: button.Text, URL: button.URL, Style: style, IconCustomEmojiID: icon}, nil } if button.CallbackDataSet { if button.CallbackData == "" || len([]byte(button.CallbackData)) > domain.MaxCallbackDataLen { return domain.MarkupButton{}, errors.New("BUTTON_DATA_INVALID") } return domain.MarkupButton{Type: domain.MarkupButtonCallback, Text: button.Text, Data: []byte(button.CallbackData), Style: style, IconCustomEmojiID: icon}, nil } if button.WebAppSet { return domain.MarkupButton{Type: domain.MarkupButtonWebView, Text: button.Text, URL: button.WebAppURL, Style: style, IconCustomEmojiID: icon}, nil } if button.SwitchInlineSet { return domain.MarkupButton{Type: domain.MarkupButtonSwitchInline, Text: button.Text, Query: button.SwitchInlineQuery, SamePeer: button.SwitchInlineSamePeer, PeerTypes: append([]string(nil), button.SwitchInlinePeerTypes...), Style: style, IconCustomEmojiID: icon}, nil } if button.CopyTextSet { return domain.MarkupButton{Type: domain.MarkupButtonCopy, Text: button.Text, CopyText: button.CopyText, Style: style, IconCustomEmojiID: icon}, nil } return domain.MarkupButton{}, errors.New("BUTTON_INVALID") } func markupButtonDecorationFromAPI(rawStyle, rawIcon string, iconSet bool) (domain.MarkupButtonStyle, int64, error) { style := domain.MarkupButtonStyle(strings.TrimSpace(rawStyle)) switch style { case "", domain.MarkupButtonStylePrimary, domain.MarkupButtonStyleDanger, domain.MarkupButtonStyleSuccess: default: return "", 0, errors.New("BUTTON_INVALID") } if !iconSet { return style, 0, nil } icon, err := strconv.ParseInt(strings.TrimSpace(rawIcon), 10, 64) if err != nil || icon <= 0 { return "", 0, errors.New("BUTTON_INVALID") } return style, icon, nil } func replyMarkupErrFromDomain(err error) error { switch { case errors.Is(err, domain.ErrButtonURLInvalid): return errors.New("BUTTON_URL_INVALID") case errors.Is(err, domain.ErrButtonDataInvalid): return errors.New("BUTTON_DATA_INVALID") default: return errors.New("BUTTON_INVALID") } } func preparedPeerTypesFromAPI(values map[string]string) []string { out := make([]string, 0, 5) if apiBool(values["allow_user_chats"]) { out = append(out, store.InlineQueryPeerTypePM) } if apiBool(values["allow_bot_chats"]) { out = append(out, store.InlineQueryPeerTypeBotPM) } if apiBool(values["allow_group_chats"]) { out = append(out, store.InlineQueryPeerTypeChat, store.InlineQueryPeerTypeMegagroup) } if apiBool(values["allow_channel_chats"]) { out = append(out, store.InlineQueryPeerTypeBroadcast) } return out } func apiBool(raw string) bool { v, err := strconv.ParseBool(strings.TrimSpace(raw)) return err == nil && v } func validBotAPIHTTPSURL(raw string) bool { if raw == "" || len(raw) > domain.MaxBotInlineWebURLLen || strings.TrimSpace(raw) != raw { return false } parsed, err := url.Parse(raw) if err != nil || parsed.User != nil || parsed.Host == "" || strings.ToLower(parsed.Scheme) != "https" { return false } return !strings.ContainsAny(parsed.Host, " \t\r\n") } type apiInlineResult struct { Type string `json:"type"` ID string `json:"id"` Title string `json:"title"` Description string `json:"description"` URL string `json:"url"` MessageText string `json:"message_text"` InputMessageContent json.RawMessage `json:"input_message_content"` ReplyMarkup json.RawMessage `json:"reply_markup"` } type apiInputTextMessageContent struct { MessageText string `json:"message_text"` ParseMode string `json:"parse_mode"` Entities []apiMessageEntity `json:"entities"` DisableWebPagePreview bool `json:"disable_web_page_preview"` LinkPreviewOptions struct { IsDisabled bool `json:"is_disabled"` } `json:"link_preview_options"` } type apiMessageEntity struct { Type string `json:"type"` Offset int `json:"offset"` Length int `json:"length"` URL string `json:"url"` User *struct { ID int64 `json:"id"` } `json:"user"` Language string `json:"language"` CustomEmojiID string `json:"custom_emoji_id"` } type apiInlineKeyboardMarkup struct { InlineKeyboard [][]apiInlineKeyboardButton `json:"inline_keyboard"` } type apiReplyKeyboardMarkup struct { Keyboard [][]apiKeyboardButton `json:"keyboard"` IsPersistent bool `json:"is_persistent"` ResizeKeyboard bool `json:"resize_keyboard"` OneTimeKeyboard bool `json:"one_time_keyboard"` InputFieldPlaceholder string `json:"input_field_placeholder"` Selective bool `json:"selective"` } type apiKeyboardButton struct { Text string Style string IconCustomEmojiID string IconCustomEmojiIDSet bool Unsupported bool Kind string PollType string RequestID int MaxQuantity int RequestName bool RequestUsername bool RequestPhoto bool RequestTitle bool ChatIsChannel bool WebAppURL string RequestPeerFilter *domain.BotRequestPeerFilter } type apiChatAdministratorRights struct { IsAnonymous bool `json:"is_anonymous"` CanManageChat bool `json:"can_manage_chat"` CanDeleteMessages bool `json:"can_delete_messages"` CanManageVideoChats bool `json:"can_manage_video_chats"` CanRestrictMembers bool `json:"can_restrict_members"` CanPromoteMembers bool `json:"can_promote_members"` CanChangeInfo bool `json:"can_change_info"` CanInviteUsers bool `json:"can_invite_users"` CanPostStories bool `json:"can_post_stories"` CanEditStories bool `json:"can_edit_stories"` CanDeleteStories bool `json:"can_delete_stories"` CanPostMessages bool `json:"can_post_messages"` CanEditMessages bool `json:"can_edit_messages"` CanPinMessages bool `json:"can_pin_messages"` CanManageTopics bool `json:"can_manage_topics"` CanManageDirectMessages bool `json:"can_manage_direct_messages"` } func domainRequestAdminRights(in *apiChatAdministratorRights) *domain.BotRequestAdminRights { if in == nil { return nil } return &domain.BotRequestAdminRights{ Anonymous: in.IsAnonymous, ManageChat: in.CanManageChat, DeleteMessages: in.CanDeleteMessages, ManageVideoChats: in.CanManageVideoChats, RestrictMembers: in.CanRestrictMembers, PromoteMembers: in.CanPromoteMembers, ChangeInfo: in.CanChangeInfo, InviteUsers: in.CanInviteUsers, PostStories: in.CanPostStories, EditStories: in.CanEditStories, DeleteStories: in.CanDeleteStories, PostMessages: in.CanPostMessages, EditMessages: in.CanEditMessages, PinMessages: in.CanPinMessages, ManageTopics: in.CanManageTopics, ManageDirectMessages: in.CanManageDirectMessages, } } func (b *apiKeyboardButton) UnmarshalJSON(data []byte) error { trimmed := strings.TrimSpace(string(data)) if strings.HasPrefix(trimmed, "\"") { b.Kind = "text" return json.Unmarshal([]byte(trimmed), &b.Text) } var fields map[string]json.RawMessage if err := json.Unmarshal(data, &fields); err != nil { return err } text, ok := fields["text"] if !ok || json.Unmarshal(text, &b.Text) != nil { return errors.New("invalid keyboard button text") } if raw, ok := fields["style"]; ok { if err := json.Unmarshal(raw, &b.Style); err != nil { return err } } if raw, ok := fields["icon_custom_emoji_id"]; ok { b.IconCustomEmojiIDSet = true if err := json.Unmarshal(raw, &b.IconCustomEmojiID); err != nil { return err } } actions := 0 if raw, ok := fields["request_contact"]; ok { var enabled bool if json.Unmarshal(raw, &enabled) != nil || !enabled { b.Unsupported = true } else { b.Kind = "request_contact" actions++ } } if raw, ok := fields["request_location"]; ok { var enabled bool if json.Unmarshal(raw, &enabled) != nil || !enabled { b.Unsupported = true } else { b.Kind = "request_location" actions++ } } if raw, ok := fields["request_poll"]; ok { var poll struct { Type string `json:"type"` } if json.Unmarshal(raw, &poll) != nil { b.Unsupported = true } else { b.Kind, b.PollType = "request_poll", poll.Type actions++ } } if raw, ok := fields["request_users"]; ok { var request struct { RequestID int `json:"request_id"` UserIsBot *bool `json:"user_is_bot"` UserIsPremium *bool `json:"user_is_premium"` MaxQuantity int `json:"max_quantity"` RequestName bool `json:"request_name"` RequestUsername bool `json:"request_username"` RequestPhoto bool `json:"request_photo"` } if json.Unmarshal(raw, &request) != nil || request.RequestID == 0 { b.Unsupported = true } else { b.Kind, b.RequestID, b.MaxQuantity, b.RequestName, b.RequestUsername, b.RequestPhoto = "request_users", request.RequestID, request.MaxQuantity, request.RequestName, request.RequestUsername, request.RequestPhoto b.RequestPeerFilter = &domain.BotRequestPeerFilter{} if request.UserIsBot != nil { b.RequestPeerFilter.UserIsBotSet, b.RequestPeerFilter.UserIsBot = true, *request.UserIsBot } if request.UserIsPremium != nil { b.RequestPeerFilter.UserIsPremiumSet, b.RequestPeerFilter.UserIsPremium = true, *request.UserIsPremium } if b.MaxQuantity == 0 { b.MaxQuantity = 1 } actions++ } } if raw, ok := fields["request_chat"]; ok { var request struct { RequestID int `json:"request_id"` ChatIsChannel bool `json:"chat_is_channel"` ChatIsForum *bool `json:"chat_is_forum"` ChatHasUsername *bool `json:"chat_has_username"` ChatIsCreated bool `json:"chat_is_created"` UserAdministratorRights *apiChatAdministratorRights `json:"user_administrator_rights"` BotAdministratorRights *apiChatAdministratorRights `json:"bot_administrator_rights"` BotIsMember bool `json:"bot_is_member"` RequestTitle bool `json:"request_title"` RequestUsername bool `json:"request_username"` RequestPhoto bool `json:"request_photo"` } if json.Unmarshal(raw, &request) != nil || request.RequestID == 0 || (request.ChatIsChannel && (request.ChatIsForum != nil || request.BotIsMember)) { b.Unsupported = true } else { b.Kind, b.RequestID, b.ChatIsChannel, b.RequestTitle, b.RequestUsername, b.RequestPhoto = "request_chat", request.RequestID, request.ChatIsChannel, request.RequestTitle, request.RequestUsername, request.RequestPhoto b.RequestPeerFilter = &domain.BotRequestPeerFilter{ ChatIsCreated: request.ChatIsCreated, BotIsMember: request.BotIsMember, UserAdminRights: domainRequestAdminRights(request.UserAdministratorRights), BotAdminRights: domainRequestAdminRights(request.BotAdministratorRights), } if request.ChatIsForum != nil { b.RequestPeerFilter.ChatIsForumSet, b.RequestPeerFilter.ChatIsForum = true, *request.ChatIsForum } if request.ChatHasUsername != nil { b.RequestPeerFilter.ChatHasUsernameSet, b.RequestPeerFilter.ChatHasUsername = true, *request.ChatHasUsername } actions++ } } if raw, ok := fields["web_app"]; ok { var app struct { URL string `json:"url"` } if json.Unmarshal(raw, &app) != nil { b.Unsupported = true } else { b.Kind, b.WebAppURL = "web_app", app.URL actions++ } } if actions == 0 { b.Kind = "text" } if actions > 1 { b.Unsupported = true } for key := range fields { switch key { case "text", "style", "icon_custom_emoji_id", "request_contact", "request_location", "request_poll", "request_users", "request_chat", "web_app": default: b.Unsupported = true } } return nil } type apiReplyKeyboardRemove struct { RemoveKeyboard bool `json:"remove_keyboard"` Selective bool `json:"selective"` } type apiForceReply struct { ForceReply bool `json:"force_reply"` InputFieldPlaceholder string `json:"input_field_placeholder"` Selective bool `json:"selective"` } type apiInlineKeyboardButton struct { Text string URL string URLSet bool CallbackData string CallbackDataSet bool Style string IconCustomEmojiID string IconCustomEmojiIDSet bool Unsupported bool WebAppURL string WebAppSet bool SwitchInlineQuery string SwitchInlineSet bool SwitchInlineSamePeer bool SwitchInlinePeerTypes []string CopyText string CopyTextSet bool } func (b *apiInlineKeyboardButton) UnmarshalJSON(data []byte) error { var fields map[string]json.RawMessage if err := json.Unmarshal(data, &fields); err != nil { return err } text, ok := fields["text"] if !ok || json.Unmarshal(text, &b.Text) != nil { return errors.New("invalid inline keyboard button text") } if raw, ok := fields["url"]; ok { b.URLSet = true if err := json.Unmarshal(raw, &b.URL); err != nil { return err } } if raw, ok := fields["callback_data"]; ok { b.CallbackDataSet = true if err := json.Unmarshal(raw, &b.CallbackData); err != nil { return err } } if raw, ok := fields["web_app"]; ok { b.WebAppSet = true var app struct { URL string `json:"url"` } if json.Unmarshal(raw, &app) != nil { return errors.New("invalid web app") } b.WebAppURL = app.URL } switchActions := 0 if raw, ok := fields["switch_inline_query"]; ok { switchActions++ b.SwitchInlineSet = true if json.Unmarshal(raw, &b.SwitchInlineQuery) != nil { return errors.New("invalid switch inline query") } } if raw, ok := fields["switch_inline_query_current_chat"]; ok { switchActions++ b.SwitchInlineSet, b.SwitchInlineSamePeer = true, true if json.Unmarshal(raw, &b.SwitchInlineQuery) != nil { return errors.New("invalid switch inline query") } } if raw, ok := fields["switch_inline_query_chosen_chat"]; ok { switchActions++ b.SwitchInlineSet = true var chosen struct { Query string `json:"query"` AllowUserChats bool `json:"allow_user_chats"` AllowBotChats bool `json:"allow_bot_chats"` AllowGroupChats bool `json:"allow_group_chats"` AllowChannelChats bool `json:"allow_channel_chats"` } if json.Unmarshal(raw, &chosen) != nil { return errors.New("invalid switch inline query") } b.SwitchInlineQuery = chosen.Query if chosen.AllowUserChats { b.SwitchInlinePeerTypes = append(b.SwitchInlinePeerTypes, store.InlineQueryPeerTypePM) } if chosen.AllowBotChats { b.SwitchInlinePeerTypes = append(b.SwitchInlinePeerTypes, store.InlineQueryPeerTypeBotPM) } if chosen.AllowGroupChats { b.SwitchInlinePeerTypes = append(b.SwitchInlinePeerTypes, store.InlineQueryPeerTypeChat, store.InlineQueryPeerTypeMegagroup) } if chosen.AllowChannelChats { b.SwitchInlinePeerTypes = append(b.SwitchInlinePeerTypes, store.InlineQueryPeerTypeBroadcast) } } if switchActions > 1 { b.Unsupported = true } if raw, ok := fields["copy_text"]; ok { b.CopyTextSet = true var copy struct { Text string `json:"text"` } if json.Unmarshal(raw, ©) != nil { return errors.New("invalid copy text") } b.CopyText = copy.Text } if raw, ok := fields["style"]; ok { if err := json.Unmarshal(raw, &b.Style); err != nil { return err } } if raw, ok := fields["icon_custom_emoji_id"]; ok { b.IconCustomEmojiIDSet = true if err := json.Unmarshal(raw, &b.IconCustomEmojiID); err != nil { return err } } for key := range fields { switch key { case "text", "url", "callback_data", "web_app", "switch_inline_query", "switch_inline_query_current_chat", "switch_inline_query_chosen_chat", "copy_text", "style", "icon_custom_emoji_id": default: b.Unsupported = true } } return nil }