chore: refresh gramsrv public release

This commit is contained in:
A 2026-06-30 14:37:43 +08:00
parent 75cebe8dbf
commit 70b6820474
1274 changed files with 378751 additions and 59919 deletions

294
internal/botapi/inline.go Normal file
View file

@ -0,0 +1,294 @@
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 := replyMarkupFromAPI(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 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{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 markupButtonFromAPI(button apiInlineKeyboardButton) (domain.MarkupButton, error) {
if button.URL != "" {
return domain.MarkupButton{Type: domain.MarkupButtonURL, Text: button.Text, URL: button.URL}, nil
}
if button.CallbackData != nil {
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)}, nil
}
return domain.MarkupButton{}, errors.New("BUTTON_INVALID")
}
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 apiInlineKeyboardButton struct {
Text string `json:"text"`
URL string `json:"url"`
CallbackData *string `json:"callback_data"`
}