Sync telesrv b96f2dd (feat(bot): complete keyboards callbacks and durable delivery). Skipped private docs and preserved public README files per sync rules; normalized the appearance seed log label for public naming.
799 lines
26 KiB
Go
799 lines
26 KiB
Go
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
|
|
}
|