1203 lines
36 KiB
Go
1203 lines
36 KiB
Go
package rpc
|
||
|
||
import (
|
||
"context"
|
||
"net/url"
|
||
"strings"
|
||
"time"
|
||
"unicode/utf8"
|
||
|
||
"github.com/iamxvbaba/td/tg"
|
||
"github.com/iamxvbaba/td/tgerr"
|
||
|
||
"telesrv/internal/domain"
|
||
"telesrv/internal/store"
|
||
)
|
||
|
||
const (
|
||
botInlineQueryTTL = 25 * time.Second
|
||
botInlineCacheMaxTTL = 5 * time.Minute
|
||
botInlineCacheMaxEntries = 256
|
||
)
|
||
|
||
func botInlineDisabledErr() error { return tgerr.New(400, "BOT_INLINE_DISABLED") }
|
||
func botInlineGeoNotAllowedErr() error { return tgerr.New(400, "BOT_INLINE_GEO_NOT_ALLOWED") }
|
||
func botWebviewDisabledErr() error { return tgerr.New(400, "BOT_WEBVIEW_DISABLED") }
|
||
func queryIDInvalidErr() error { return tgerr.New(400, "QUERY_ID_INVALID") }
|
||
func queryIDEmptyErr() error { return tgerr.New(400, "QUERY_ID_EMPTY") }
|
||
func resultIDEmptyErr() error { return tgerr.New(400, "RESULT_ID_EMPTY") }
|
||
func resultIDInvalidErr() error { return tgerr.New(400, "RESULT_ID_INVALID") }
|
||
func resultIDDuplicateErr() error { return tgerr.New(400, "RESULT_ID_DUPLICATE") }
|
||
func resultTypeInvalidErr() error { return tgerr.New(400, "RESULT_TYPE_INVALID") }
|
||
func resultsTooMuchErr() error { return tgerr.New(400, "RESULTS_TOO_MUCH") }
|
||
func sendMessageTypeInvalidErr() error { return tgerr.New(400, "SEND_MESSAGE_TYPE_INVALID") }
|
||
func nextOffsetInvalidErr() error { return tgerr.New(400, "NEXT_OFFSET_INVALID") }
|
||
func startParamEmptyErr() error { return tgerr.New(400, "START_PARAM_EMPTY") }
|
||
func switchPmTextEmptyErr() error { return tgerr.New(400, "SWITCH_PM_TEXT_EMPTY") }
|
||
func switchWebviewInvalidErr() error { return tgerr.New(400, "SWITCH_WEBVIEW_URL_INVALID") }
|
||
func inlineResultExpiredErr() error { return tgerr.New(400, "INLINE_RESULT_EXPIRED") }
|
||
func webDocumentInvalidErr() error { return tgerr.New(400, "WEBDOCUMENT_INVALID") }
|
||
func webDocumentMimeInvalidErr() error { return tgerr.New(400, "WEBDOCUMENT_MIME_INVALID") }
|
||
func webDocumentSizeTooBigErr() error { return tgerr.New(400, "WEBDOCUMENT_SIZE_TOO_BIG") }
|
||
func webDocumentURLEmptyErr() error { return tgerr.New(400, "WEBDOCUMENT_URL_EMPTY") }
|
||
func webDocumentURLInvalidErr() error { return tgerr.New(400, "WEBDOCUMENT_URL_INVALID") }
|
||
|
||
func (r *Router) onMessagesGetInlineBotResults(ctx context.Context, req *tg.MessagesGetInlineBotResultsRequest) (*tg.MessagesBotResults, error) {
|
||
userID, _, err := r.currentUserID(ctx)
|
||
if err != nil {
|
||
return nil, internalErr()
|
||
}
|
||
if userID == 0 || r.deps.Bots == nil {
|
||
return nil, botInvalidErr()
|
||
}
|
||
if len(req.Offset) > domain.MaxBotInlineNextOffsetLen {
|
||
return nil, nextOffsetInvalidErr()
|
||
}
|
||
bot, found, err := r.userFromInput(ctx, userID, req.Bot)
|
||
if err != nil {
|
||
return nil, internalErr()
|
||
}
|
||
if !found || !bot.Bot {
|
||
return nil, botInvalidErr()
|
||
}
|
||
profile, found, err := r.deps.Bots.BotInfo(ctx, bot.ID)
|
||
if err != nil {
|
||
return nil, internalErr()
|
||
}
|
||
if !found || profile.InlinePlaceholder == "" {
|
||
return nil, botInlineDisabledErr()
|
||
}
|
||
var queryGeo *domain.MessageGeoPoint
|
||
if geo, ok := req.GetGeoPointAsNotEmpty(); ok {
|
||
if !profile.InlineGeo {
|
||
return nil, botInlineGeoNotAllowedErr()
|
||
}
|
||
queryGeo, err = domainGeoPointFromInput(geo)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
cacheKey := inlineCacheKey{
|
||
botUserID: bot.ID,
|
||
userID: userID,
|
||
peer: peer,
|
||
query: req.Query,
|
||
offset: req.Offset,
|
||
}
|
||
if queryGeo != nil {
|
||
cacheKey.hasGeo = true
|
||
cacheKey.geoLat = queryGeo.Lat
|
||
cacheKey.geoLong = queryGeo.Long
|
||
cacheKey.geoAccuracy = queryGeo.AccuracyRadius
|
||
}
|
||
now := r.clock.Now()
|
||
if cached, ok := r.inlines.cachedContext(ctx, now, cacheKey); ok {
|
||
results := r.inlines.registerCachedContext(ctx, now, bot.ID, userID, peer, cached)
|
||
return r.tgBotInlineResults(ctx, userID, results), nil
|
||
}
|
||
queryID, pending := r.inlines.registerWithCacheKeyContext(ctx, now, bot.ID, userID, peer, cacheKey)
|
||
defer r.inlines.deregisterIfUnansweredContext(ctx, queryID)
|
||
|
||
peerType := tgInlineQueryPeerType(ctx, r, userID, bot.ID, peer)
|
||
update := &tg.UpdateBotInlineQuery{
|
||
QueryID: queryID,
|
||
UserID: userID,
|
||
Query: req.Query,
|
||
Offset: req.Offset,
|
||
PeerType: peerType,
|
||
}
|
||
if queryGeo != nil {
|
||
update.SetGeo(tgGeoPoint(*queryGeo))
|
||
}
|
||
r.pushUserMessage(ctx, bot.ID, "push bot inline query", &tg.Updates{
|
||
Updates: []tg.UpdateClass{update},
|
||
Date: int(now.Unix()),
|
||
})
|
||
push := store.BotInlineQueryPush{
|
||
QueryID: queryID,
|
||
BotUserID: bot.ID,
|
||
UserID: userID,
|
||
Query: req.Query,
|
||
Offset: req.Offset,
|
||
PeerType: storeInlineQueryPeerType(peerType),
|
||
Date: int(now.Unix()),
|
||
}
|
||
if queryGeo != nil {
|
||
geo := *queryGeo
|
||
push.Geo = &geo
|
||
}
|
||
r.publishInlineBotQuery(ctx, push)
|
||
|
||
waitCtx, cancel := context.WithTimeout(ctx, botInlineQueryTTL)
|
||
defer cancel()
|
||
results, err := r.awaitInlineBotResults(waitCtx, pending, userID, queryID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return r.tgBotInlineResults(ctx, userID, results), nil
|
||
}
|
||
|
||
func (r *Router) onMessagesSetInlineBotResults(ctx context.Context, req *tg.MessagesSetInlineBotResultsRequest) (bool, error) {
|
||
botID, err := r.callerBotID(ctx)
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
results, err := r.domainInlineResultsFromTG(ctx, botID, req)
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
if !r.inlines.resolveContext(ctx, r.clock.Now(), botID, req.QueryID, results) {
|
||
return false, queryIDInvalidErr()
|
||
}
|
||
return true, nil
|
||
}
|
||
|
||
func (r *Router) onMessagesSendInlineBotResult(ctx context.Context, req *tg.MessagesSendInlineBotResultRequest) (tg.UpdatesClass, error) {
|
||
if req.QueryID == 0 {
|
||
return nil, queryIDEmptyErr()
|
||
}
|
||
if req.ID == "" {
|
||
return nil, resultIDEmptyErr()
|
||
}
|
||
if req.RandomID == 0 {
|
||
return nil, randomIDEmptyErr()
|
||
}
|
||
if req.ScheduleDate != 0 && !scheduleDateIsImmediate(req.ScheduleDate, int(r.clock.Now().Unix())) {
|
||
return nil, scheduleDateInvalidErr()
|
||
}
|
||
if req.QuickReplyShortcut != nil {
|
||
return nil, shortcutInvalidErr()
|
||
}
|
||
if req.AllowPaidStars < 0 {
|
||
return nil, starsAmountInvalidErr()
|
||
}
|
||
if req.AllowPaidStars > 0 {
|
||
return nil, paymentUnsupportedErr()
|
||
}
|
||
userID, _, err := r.currentUserID(ctx)
|
||
if err != nil {
|
||
return nil, internalErr()
|
||
}
|
||
if userID == 0 {
|
||
return nil, peerIDInvalidErr()
|
||
}
|
||
if err := r.checkSendRateLimit(ctx, userID, 1); err != nil {
|
||
return nil, err
|
||
}
|
||
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if peer.Type != domain.PeerTypeUser && peer.Type != domain.PeerTypeChannel {
|
||
return nil, peerIDInvalidErr()
|
||
}
|
||
results, result, ok := r.inlines.resultForSendContext(ctx, r.clock.Now(), userID, req.QueryID, req.ID)
|
||
if !ok {
|
||
return nil, inlineResultExpiredErr()
|
||
}
|
||
if !r.inlineResultsAllowPeer(ctx, userID, results, peer) {
|
||
return nil, peerIDInvalidErr()
|
||
}
|
||
media := cloneInlineMedia(result.Media)
|
||
if result.MediaAuto {
|
||
media, err = r.domainInlineExternalContentMedia(ctx, result)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
r.resolveInlineContactMediaForSender(ctx, userID, media)
|
||
updates, duplicate, err := r.sendOutgoing(ctx, userID, peer, outgoingSend{
|
||
randomID: req.RandomID,
|
||
message: result.Message,
|
||
entities: tgMessageEntities(result.Entities),
|
||
media: media,
|
||
silent: req.Silent,
|
||
replyToInput: req.ReplyTo,
|
||
sendAsInput: req.SendAs,
|
||
clearDraft: req.ClearDraft,
|
||
replyMarkup: result.ReplyMarkup,
|
||
viaBotID: results.BotUserID,
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if !duplicate {
|
||
r.pushInlineBotSendFeedback(ctx, userID, results, result, updates)
|
||
r.inlines.consumeContext(ctx, req.QueryID)
|
||
}
|
||
return updates, nil
|
||
}
|
||
|
||
func (r *Router) inlineResultsAllowPeer(ctx context.Context, userID int64, results domain.BotInlineResults, peer domain.Peer) bool {
|
||
if results.Peer.Type != "" || results.Peer.ID != 0 {
|
||
return results.Peer == peer
|
||
}
|
||
if len(results.PeerTypes) == 0 {
|
||
return true
|
||
}
|
||
actual := storeInlineQueryPeerType(tgInlineQueryPeerType(ctx, r, userID, results.BotUserID, peer))
|
||
if actual == "" {
|
||
return false
|
||
}
|
||
for _, allowed := range results.PeerTypes {
|
||
if allowed == actual {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func (r *Router) awaitInlineBotResults(ctx context.Context, pending *pendingInlineQuery, userID, queryID int64) (domain.BotInlineResults, error) {
|
||
ticker := time.NewTicker(50 * time.Millisecond)
|
||
defer ticker.Stop()
|
||
for {
|
||
select {
|
||
case results := <-pending.ch:
|
||
return results, nil
|
||
case <-ticker.C:
|
||
if results, ok := r.inlines.resultsForQueryContext(ctx, r.clock.Now(), userID, queryID); ok {
|
||
return results, nil
|
||
}
|
||
case <-ctx.Done():
|
||
return domain.BotInlineResults{}, botResponseTimeoutErr()
|
||
}
|
||
}
|
||
}
|
||
|
||
func (r *Router) domainInlineResultsFromTG(ctx context.Context, botID int64, req *tg.MessagesSetInlineBotResultsRequest) (domain.BotInlineResults, error) {
|
||
if len(req.Results) > domain.MaxBotInlineResults {
|
||
return domain.BotInlineResults{}, resultsTooMuchErr()
|
||
}
|
||
if len(req.NextOffset) > domain.MaxBotInlineNextOffsetLen {
|
||
return domain.BotInlineResults{}, nextOffsetInvalidErr()
|
||
}
|
||
switchPM, err := domainInlineSwitchPM(req)
|
||
if err != nil {
|
||
return domain.BotInlineResults{}, err
|
||
}
|
||
switchWeb, err := domainInlineSwitchWebView(req)
|
||
if err != nil {
|
||
return domain.BotInlineResults{}, err
|
||
}
|
||
seen := make(map[string]struct{}, len(req.Results))
|
||
out := domain.BotInlineResults{
|
||
BotUserID: botID,
|
||
QueryID: req.QueryID,
|
||
Gallery: req.Gallery,
|
||
Private: req.Private,
|
||
CacheTime: req.CacheTime,
|
||
NextOffset: req.NextOffset,
|
||
SwitchPM: switchPM,
|
||
SwitchWeb: switchWeb,
|
||
Results: make([]domain.BotInlineResult, 0, len(req.Results)),
|
||
}
|
||
for _, raw := range req.Results {
|
||
item, err := r.domainInlineResultFromTG(ctx, botID, raw)
|
||
if err != nil {
|
||
return domain.BotInlineResults{}, err
|
||
}
|
||
if err := r.prepareTelegramLoginMarkup(ctx, botID, item.ReplyMarkup); err != nil {
|
||
return domain.BotInlineResults{}, replyMarkupErr(err)
|
||
}
|
||
if _, ok := seen[item.ID]; ok {
|
||
return domain.BotInlineResults{}, resultIDDuplicateErr()
|
||
}
|
||
seen[item.ID] = struct{}{}
|
||
out.Results = append(out.Results, item)
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
func domainInlineSwitchWebView(req *tg.MessagesSetInlineBotResultsRequest) (*domain.BotInlineSwitchWebView, error) {
|
||
switchWeb, ok := req.GetSwitchWebview()
|
||
if !ok {
|
||
return nil, nil
|
||
}
|
||
if strings.TrimSpace(switchWeb.Text) == "" || utf8.RuneCountInString(switchWeb.Text) > domain.MaxBotInlineSwitchTextLen {
|
||
return nil, buttonInvalidErr()
|
||
}
|
||
if err := validateInlineWebURL(switchWeb.URL, true); err != nil {
|
||
return nil, switchWebviewInvalidErr()
|
||
}
|
||
return &domain.BotInlineSwitchWebView{
|
||
Text: switchWeb.Text,
|
||
URL: switchWeb.URL,
|
||
}, nil
|
||
}
|
||
|
||
func domainInlineSwitchPM(req *tg.MessagesSetInlineBotResultsRequest) (*domain.BotInlineSwitchPM, error) {
|
||
switchPM, ok := req.GetSwitchPm()
|
||
if !ok {
|
||
return nil, nil
|
||
}
|
||
if switchPM.Text == "" {
|
||
return nil, switchPmTextEmptyErr()
|
||
}
|
||
if utf8.RuneCountInString(switchPM.Text) > domain.MaxBotInlineSwitchTextLen {
|
||
return nil, buttonInvalidErr()
|
||
}
|
||
if switchPM.StartParam == "" {
|
||
return nil, startParamEmptyErr()
|
||
}
|
||
if !validInlineStartParam(switchPM.StartParam) {
|
||
return nil, startParamInvalidErr()
|
||
}
|
||
return &domain.BotInlineSwitchPM{
|
||
Text: switchPM.Text,
|
||
StartParam: switchPM.StartParam,
|
||
}, nil
|
||
}
|
||
|
||
func validInlineStartParam(in string) bool {
|
||
if utf8.RuneCountInString(in) > domain.MaxStartParamLen {
|
||
return false
|
||
}
|
||
for _, r := range in {
|
||
if r >= 'a' && r <= 'z' {
|
||
continue
|
||
}
|
||
if r >= 'A' && r <= 'Z' {
|
||
continue
|
||
}
|
||
if r >= '0' && r <= '9' {
|
||
continue
|
||
}
|
||
if r == '_' || r == '-' {
|
||
continue
|
||
}
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
func (r *Router) domainInlineResultFromTG(ctx context.Context, botID int64, raw tg.InputBotInlineResultClass) (domain.BotInlineResult, error) {
|
||
switch result := raw.(type) {
|
||
case *tg.InputBotInlineResult:
|
||
if result.ID == "" {
|
||
return domain.BotInlineResult{}, resultIDEmptyErr()
|
||
}
|
||
if len(result.ID) > domain.MaxBotInlineResultIDLen {
|
||
return domain.BotInlineResult{}, resultIDInvalidErr()
|
||
}
|
||
if result.Type == "" {
|
||
return domain.BotInlineResult{}, resultTypeInvalidErr()
|
||
}
|
||
switch msg := result.SendMessage.(type) {
|
||
case *tg.InputBotInlineMessageText:
|
||
if result.Type != "article" {
|
||
return domain.BotInlineResult{}, resultTypeInvalidErr()
|
||
}
|
||
resultURL, thumb, content, err := domainInlineResultWebPreview(result)
|
||
if err != nil {
|
||
return domain.BotInlineResult{}, err
|
||
}
|
||
message, entities, markup, noWebpage, err := domainInlineTextMessage(botID, msg)
|
||
if err != nil {
|
||
return domain.BotInlineResult{}, err
|
||
}
|
||
return domain.BotInlineResult{
|
||
ID: result.ID,
|
||
Type: result.Type,
|
||
Title: result.Title,
|
||
Description: result.Description,
|
||
URL: resultURL,
|
||
Thumb: thumb,
|
||
Content: content,
|
||
Message: message,
|
||
Entities: entities,
|
||
ReplyMarkup: markup,
|
||
NoWebpage: noWebpage,
|
||
}, nil
|
||
case *tg.InputBotInlineMessageMediaAuto:
|
||
if !validInlineExternalContentResultType(result.Type) {
|
||
return domain.BotInlineResult{}, resultTypeInvalidErr()
|
||
}
|
||
resultURL, thumb, content, err := domainInlineResultWebPreview(result)
|
||
if err != nil {
|
||
return domain.BotInlineResult{}, err
|
||
}
|
||
if content == nil {
|
||
return domain.BotInlineResult{}, webDocumentInvalidErr()
|
||
}
|
||
if !inlineExternalContentMimeAllowed(result.Type, content.MimeType) {
|
||
return domain.BotInlineResult{}, mediaInvalidErr()
|
||
}
|
||
message, entities, markup, err := domainInlineMediaAutoMessage(botID, msg)
|
||
if err != nil {
|
||
return domain.BotInlineResult{}, err
|
||
}
|
||
return domain.BotInlineResult{
|
||
ID: result.ID,
|
||
Type: result.Type,
|
||
Title: result.Title,
|
||
Description: result.Description,
|
||
URL: resultURL,
|
||
Thumb: thumb,
|
||
Content: content,
|
||
Message: message,
|
||
Entities: entities,
|
||
ReplyMarkup: markup,
|
||
MediaAuto: true,
|
||
}, nil
|
||
case *tg.InputBotInlineMessageMediaGeo:
|
||
if result.Type != "geo" {
|
||
return domain.BotInlineResult{}, resultTypeInvalidErr()
|
||
}
|
||
if inlineResultHasWebPreview(result) {
|
||
return domain.BotInlineResult{}, webDocumentInvalidErr()
|
||
}
|
||
media, markup, err := domainInlineGeoMessage(msg)
|
||
if err != nil {
|
||
return domain.BotInlineResult{}, err
|
||
}
|
||
return domain.BotInlineResult{
|
||
ID: result.ID,
|
||
Type: result.Type,
|
||
Title: result.Title,
|
||
Description: result.Description,
|
||
ReplyMarkup: markup,
|
||
Media: media,
|
||
}, nil
|
||
case *tg.InputBotInlineMessageMediaVenue:
|
||
if result.Type != "venue" {
|
||
return domain.BotInlineResult{}, resultTypeInvalidErr()
|
||
}
|
||
if inlineResultHasWebPreview(result) {
|
||
return domain.BotInlineResult{}, webDocumentInvalidErr()
|
||
}
|
||
media, markup, err := domainInlineVenueMessage(msg)
|
||
if err != nil {
|
||
return domain.BotInlineResult{}, err
|
||
}
|
||
return domain.BotInlineResult{
|
||
ID: result.ID,
|
||
Type: result.Type,
|
||
Title: result.Title,
|
||
Description: result.Description,
|
||
ReplyMarkup: markup,
|
||
Media: media,
|
||
}, nil
|
||
case *tg.InputBotInlineMessageMediaContact:
|
||
if result.Type != "contact" {
|
||
return domain.BotInlineResult{}, resultTypeInvalidErr()
|
||
}
|
||
if inlineResultHasWebPreview(result) {
|
||
return domain.BotInlineResult{}, webDocumentInvalidErr()
|
||
}
|
||
media, markup, err := domainInlineContactMessage(msg)
|
||
if err != nil {
|
||
return domain.BotInlineResult{}, err
|
||
}
|
||
return domain.BotInlineResult{
|
||
ID: result.ID,
|
||
Type: result.Type,
|
||
Title: result.Title,
|
||
Description: result.Description,
|
||
ReplyMarkup: markup,
|
||
Media: media,
|
||
}, nil
|
||
default:
|
||
return domain.BotInlineResult{}, sendMessageTypeInvalidErr()
|
||
}
|
||
case *tg.InputBotInlineResultPhoto:
|
||
if result.ID == "" {
|
||
return domain.BotInlineResult{}, resultIDEmptyErr()
|
||
}
|
||
if len(result.ID) > domain.MaxBotInlineResultIDLen {
|
||
return domain.BotInlineResult{}, resultIDInvalidErr()
|
||
}
|
||
if result.Type != "photo" {
|
||
return domain.BotInlineResult{}, resultTypeInvalidErr()
|
||
}
|
||
media, err := r.domainInlinePhotoMedia(ctx, result.Photo)
|
||
if err != nil {
|
||
return domain.BotInlineResult{}, err
|
||
}
|
||
message, entities, markup, err := domainInlineMediaAutoMessage(botID, result.SendMessage)
|
||
if err != nil {
|
||
return domain.BotInlineResult{}, err
|
||
}
|
||
return domain.BotInlineResult{
|
||
ID: result.ID,
|
||
Type: result.Type,
|
||
Message: message,
|
||
Entities: entities,
|
||
ReplyMarkup: markup,
|
||
Media: media,
|
||
}, nil
|
||
case *tg.InputBotInlineResultDocument:
|
||
if result.ID == "" {
|
||
return domain.BotInlineResult{}, resultIDEmptyErr()
|
||
}
|
||
if len(result.ID) > domain.MaxBotInlineResultIDLen {
|
||
return domain.BotInlineResult{}, resultIDInvalidErr()
|
||
}
|
||
if !validInlineDocumentResultType(result.Type) {
|
||
return domain.BotInlineResult{}, resultTypeInvalidErr()
|
||
}
|
||
media, err := r.domainInlineDocumentMedia(ctx, result.Document)
|
||
if err != nil {
|
||
return domain.BotInlineResult{}, err
|
||
}
|
||
message, entities, markup, err := domainInlineMediaAutoMessage(botID, result.SendMessage)
|
||
if err != nil {
|
||
return domain.BotInlineResult{}, err
|
||
}
|
||
return domain.BotInlineResult{
|
||
ID: result.ID,
|
||
Type: result.Type,
|
||
Title: result.Title,
|
||
Description: result.Description,
|
||
Message: message,
|
||
Entities: entities,
|
||
ReplyMarkup: markup,
|
||
Media: media,
|
||
}, nil
|
||
default:
|
||
return domain.BotInlineResult{}, resultTypeInvalidErr()
|
||
}
|
||
}
|
||
|
||
func domainInlineResultWebPreview(result *tg.InputBotInlineResult) (string, *domain.BotInlineWebDocument, *domain.BotInlineWebDocument, error) {
|
||
var resultURL string
|
||
if v, ok := result.GetURL(); ok {
|
||
if err := validateInlineWebURL(v, false); err != nil {
|
||
return "", nil, nil, err
|
||
}
|
||
resultURL = v
|
||
}
|
||
var thumb *domain.BotInlineWebDocument
|
||
if raw, ok := result.GetThumb(); ok {
|
||
var err error
|
||
thumb, err = domainInlineWebDocument(raw)
|
||
if err != nil {
|
||
return "", nil, nil, err
|
||
}
|
||
}
|
||
var content *domain.BotInlineWebDocument
|
||
if raw, ok := result.GetContent(); ok {
|
||
var err error
|
||
content, err = domainInlineWebDocument(raw)
|
||
if err != nil {
|
||
return "", nil, nil, err
|
||
}
|
||
}
|
||
return resultURL, thumb, content, nil
|
||
}
|
||
|
||
func inlineResultHasWebPreview(result *tg.InputBotInlineResult) bool {
|
||
if _, ok := result.GetURL(); ok {
|
||
return true
|
||
}
|
||
if _, ok := result.GetThumb(); ok {
|
||
return true
|
||
}
|
||
if _, ok := result.GetContent(); ok {
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
func domainInlineWebDocument(input tg.InputWebDocument) (*domain.BotInlineWebDocument, error) {
|
||
if err := validateInlineWebURL(input.URL, true); err != nil {
|
||
return nil, err
|
||
}
|
||
if input.Size <= 0 {
|
||
return nil, webDocumentInvalidErr()
|
||
}
|
||
if input.Size > domain.MaxBotInlineWebSize {
|
||
return nil, webDocumentSizeTooBigErr()
|
||
}
|
||
if !validInlineWebMime(input.MimeType) {
|
||
return nil, webDocumentMimeInvalidErr()
|
||
}
|
||
return &domain.BotInlineWebDocument{
|
||
URL: input.URL,
|
||
AccessHash: randomNonZeroInt64(),
|
||
Size: input.Size,
|
||
MimeType: input.MimeType,
|
||
Attributes: domainDocumentAttributes(input.Attributes),
|
||
}, nil
|
||
}
|
||
|
||
func validateInlineWebURL(raw string, emptyIsURLSpecific bool) error {
|
||
if raw == "" {
|
||
if emptyIsURLSpecific {
|
||
return webDocumentURLEmptyErr()
|
||
}
|
||
return webDocumentURLInvalidErr()
|
||
}
|
||
if len(raw) > domain.MaxBotInlineWebURLLen || strings.TrimSpace(raw) != raw {
|
||
return webDocumentURLInvalidErr()
|
||
}
|
||
u, err := url.Parse(raw)
|
||
if err != nil || u.Scheme == "" || u.Host == "" || u.User != nil {
|
||
return webDocumentURLInvalidErr()
|
||
}
|
||
if strings.ToLower(u.Scheme) != "https" {
|
||
return webDocumentURLInvalidErr()
|
||
}
|
||
if strings.ContainsAny(u.Host, " \t\r\n") {
|
||
return webDocumentURLInvalidErr()
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func validInlineWebMime(in string) bool {
|
||
if in == "" || len(in) > domain.MaxBotInlineWebMimeLen || strings.TrimSpace(in) != in {
|
||
return false
|
||
}
|
||
slash := strings.IndexByte(in, '/')
|
||
if slash <= 0 || slash == len(in)-1 {
|
||
return false
|
||
}
|
||
for _, r := range in {
|
||
if r <= 32 || r == 127 {
|
||
return false
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
|
||
func domainInlineTextMessage(botID int64, msg *tg.InputBotInlineMessageText) (string, []domain.MessageEntity, *domain.MessageReplyMarkup, bool, error) {
|
||
if msg.Message == "" {
|
||
return "", nil, nil, false, messageEmptyErr()
|
||
}
|
||
if utf8.RuneCountInString(msg.Message) > maxSendMessageTextLength {
|
||
return "", nil, nil, false, messageTooLongErr()
|
||
}
|
||
if len(msg.Entities) > maxMessageEntityCount {
|
||
return "", nil, nil, false, entitiesTooLongErr()
|
||
}
|
||
markup, err := domainReplyMarkupForSender(msg.ReplyMarkup, true)
|
||
if err != nil {
|
||
return "", nil, nil, false, replyMarkupErr(err)
|
||
}
|
||
return msg.Message, domainMessageEntitiesForViewer(botID, msg.Entities), markup, msg.NoWebpage, nil
|
||
}
|
||
|
||
func domainInlineMediaAutoMessage(botID int64, raw tg.InputBotInlineMessageClass) (string, []domain.MessageEntity, *domain.MessageReplyMarkup, error) {
|
||
msg, ok := raw.(*tg.InputBotInlineMessageMediaAuto)
|
||
if !ok {
|
||
return "", nil, nil, sendMessageTypeInvalidErr()
|
||
}
|
||
if utf8.RuneCountInString(msg.Message) > maxSendMessageTextLength {
|
||
return "", nil, nil, messageTooLongErr()
|
||
}
|
||
if len(msg.Entities) > maxMessageEntityCount {
|
||
return "", nil, nil, entitiesTooLongErr()
|
||
}
|
||
markup, err := domainReplyMarkupForSender(msg.ReplyMarkup, true)
|
||
if err != nil {
|
||
return "", nil, nil, replyMarkupErr(err)
|
||
}
|
||
return msg.Message, domainMessageEntitiesForViewer(botID, msg.Entities), markup, nil
|
||
}
|
||
|
||
func domainInlineGeoMessage(msg *tg.InputBotInlineMessageMediaGeo) (*domain.MessageMedia, *domain.MessageReplyMarkup, error) {
|
||
if _, ok := msg.GetHeading(); ok {
|
||
return nil, nil, mediaInvalidErr()
|
||
}
|
||
if _, ok := msg.GetPeriod(); ok {
|
||
return nil, nil, mediaInvalidErr()
|
||
}
|
||
if _, ok := msg.GetProximityNotificationRadius(); ok {
|
||
return nil, nil, mediaInvalidErr()
|
||
}
|
||
geo, err := domainGeoPointFromInput(msg.GeoPoint)
|
||
if err != nil {
|
||
return nil, nil, err
|
||
}
|
||
markup, err := domainReplyMarkupForSender(msg.ReplyMarkup, true)
|
||
if err != nil {
|
||
return nil, nil, replyMarkupErr(err)
|
||
}
|
||
return &domain.MessageMedia{Kind: domain.MessageMediaKindGeo, Geo: geo}, markup, nil
|
||
}
|
||
|
||
func domainInlineVenueMessage(msg *tg.InputBotInlineMessageMediaVenue) (*domain.MessageMedia, *domain.MessageReplyMarkup, error) {
|
||
geo, err := domainGeoPointFromInput(msg.GeoPoint)
|
||
if err != nil {
|
||
return nil, nil, err
|
||
}
|
||
if strings.TrimSpace(msg.Title) == "" {
|
||
return nil, nil, mediaEmptyErr()
|
||
}
|
||
if utf8.RuneCountInString(msg.Title) > maxVenueTitleLength ||
|
||
utf8.RuneCountInString(msg.Address) > maxVenueAddressLength ||
|
||
utf8.RuneCountInString(msg.Provider) > maxVenueProviderLength ||
|
||
utf8.RuneCountInString(msg.VenueID) > maxVenueIDLength ||
|
||
utf8.RuneCountInString(msg.VenueType) > maxVenueIDLength {
|
||
return nil, nil, mediaInvalidErr()
|
||
}
|
||
markup, err := domainReplyMarkupForSender(msg.ReplyMarkup, true)
|
||
if err != nil {
|
||
return nil, nil, replyMarkupErr(err)
|
||
}
|
||
return &domain.MessageMedia{Kind: domain.MessageMediaKindVenue, Venue: &domain.MessageVenue{
|
||
Geo: *geo,
|
||
Title: msg.Title,
|
||
Address: msg.Address,
|
||
Provider: msg.Provider,
|
||
VenueID: msg.VenueID,
|
||
VenueType: msg.VenueType,
|
||
}}, markup, nil
|
||
}
|
||
|
||
func domainInlineContactMessage(msg *tg.InputBotInlineMessageMediaContact) (*domain.MessageMedia, *domain.MessageReplyMarkup, error) {
|
||
if !validContactInput(msg.PhoneNumber, msg.FirstName, msg.LastName, "", 0) || utf8.RuneCountInString(msg.Vcard) > maxContactVcardLength {
|
||
return nil, nil, mediaInvalidErr()
|
||
}
|
||
if strings.TrimSpace(msg.PhoneNumber) == "" && strings.TrimSpace(msg.FirstName) == "" && strings.TrimSpace(msg.LastName) == "" && strings.TrimSpace(msg.Vcard) == "" {
|
||
return nil, nil, mediaEmptyErr()
|
||
}
|
||
markup, err := domainReplyMarkupForSender(msg.ReplyMarkup, true)
|
||
if err != nil {
|
||
return nil, nil, replyMarkupErr(err)
|
||
}
|
||
return &domain.MessageMedia{Kind: domain.MessageMediaKindContact, Contact: &domain.MessageContact{
|
||
PhoneNumber: msg.PhoneNumber,
|
||
FirstName: msg.FirstName,
|
||
LastName: msg.LastName,
|
||
Vcard: msg.Vcard,
|
||
}}, markup, nil
|
||
}
|
||
|
||
func (r *Router) resolveInlineContactMediaForSender(ctx context.Context, userID int64, media *domain.MessageMedia) {
|
||
if media == nil || media.Kind != domain.MessageMediaKindContact || media.Contact == nil || media.Contact.UserID != 0 {
|
||
return
|
||
}
|
||
media.Contact.UserID = r.messageContactUserID(ctx, userID, media.Contact.PhoneNumber)
|
||
}
|
||
|
||
func (r *Router) domainInlinePhotoMedia(ctx context.Context, input tg.InputPhotoClass) (*domain.MessageMedia, error) {
|
||
if r.deps.Files == nil {
|
||
return nil, mediaInvalidErr()
|
||
}
|
||
photoID, ok := inputPhotoID(input)
|
||
if !ok {
|
||
return nil, photoInvalidErr()
|
||
}
|
||
photo, found, err := r.deps.Files.GetPhoto(ctx, photoID)
|
||
if err != nil {
|
||
return nil, internalErr()
|
||
}
|
||
if !found {
|
||
return nil, photoInvalidErr()
|
||
}
|
||
return &domain.MessageMedia{Kind: domain.MessageMediaKindPhoto, Photo: &photo}, nil
|
||
}
|
||
|
||
func (r *Router) domainInlineDocumentMedia(ctx context.Context, input tg.InputDocumentClass) (*domain.MessageMedia, error) {
|
||
if r.deps.Files == nil {
|
||
return nil, mediaInvalidErr()
|
||
}
|
||
docIDs, ok := inputDocumentCandidateIDs(input)
|
||
if !ok {
|
||
return nil, mediaInvalidErr()
|
||
}
|
||
for _, docID := range docIDs {
|
||
doc, found, err := r.deps.Files.GetDocument(ctx, docID)
|
||
if err != nil {
|
||
return nil, internalErr()
|
||
}
|
||
if found {
|
||
return messageMediaFromDocument(doc, false, 0), nil
|
||
}
|
||
}
|
||
return nil, mediaInvalidErr()
|
||
}
|
||
|
||
func validInlineDocumentResultType(t string) bool {
|
||
switch t {
|
||
case "file", "gif", "video", "audio", "voice", "sticker":
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
|
||
func validInlineExternalContentResultType(t string) bool {
|
||
return t == "photo" || validInlineDocumentResultType(t)
|
||
}
|
||
|
||
func (r *Router) domainInlineExternalContentMedia(ctx context.Context, result domain.BotInlineResult) (*domain.MessageMedia, error) {
|
||
if r.deps.Files == nil || result.Content == nil {
|
||
return nil, mediaInvalidErr()
|
||
}
|
||
document, data, mime, err := r.registeredInlineWebDocumentBytes(ctx, result.Content.URL, result.Content.AccessHash)
|
||
if err != nil {
|
||
return nil, mediaInvalidErr()
|
||
}
|
||
document.MimeType = mime
|
||
document.Size = len(data)
|
||
if !inlineExternalContentMimeAllowed(result.Type, mime) {
|
||
return nil, mediaInvalidErr()
|
||
}
|
||
if result.Type == "photo" {
|
||
photo, err := r.deps.Files.CreatePhotoFromBytes(ctx, data)
|
||
if err != nil {
|
||
return nil, photoInvalidErr()
|
||
}
|
||
return &domain.MessageMedia{Kind: domain.MessageMediaKindPhoto, Photo: &photo}, nil
|
||
}
|
||
spec := inlineExternalContentDocumentSpec(result.Type, document)
|
||
doc, err := r.deps.Files.CreateDocumentFromBytes(ctx, data, spec)
|
||
if err != nil {
|
||
return nil, documentInvalidErr()
|
||
}
|
||
return messageMediaFromDocument(doc, false, 0), nil
|
||
}
|
||
|
||
func inlineExternalContentMimeAllowed(resultType, mime string) bool {
|
||
mime = strings.ToLower(strings.TrimSpace(mime))
|
||
if !validInlineWebMime(mime) {
|
||
return false
|
||
}
|
||
switch resultType {
|
||
case "photo":
|
||
return strings.HasPrefix(mime, "image/") && mime != "image/gif"
|
||
case "gif":
|
||
return mime == "image/gif" || mime == "video/mp4" || mime == "image/webp"
|
||
case "video":
|
||
return strings.HasPrefix(mime, "video/")
|
||
case "audio", "voice":
|
||
return strings.HasPrefix(mime, "audio/")
|
||
case "sticker":
|
||
return strings.HasPrefix(mime, "image/") || strings.HasPrefix(mime, "application/x-tg")
|
||
case "file":
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
|
||
func inlineExternalContentDocumentSpec(resultType string, document domain.BotInlineWebDocument) domain.DocumentSpec {
|
||
mime := strings.ToLower(strings.TrimSpace(document.MimeType))
|
||
attrs := append([]domain.DocumentAttribute(nil), document.Attributes...)
|
||
w, h := inlineExternalContentDimensions(attrs)
|
||
switch resultType {
|
||
case "gif":
|
||
attrs = ensureDocumentAttribute(attrs, domain.DocumentAttribute{Kind: domain.DocAttrAnimated})
|
||
attrs = ensureImageSizeAttribute(attrs, w, h)
|
||
case "video":
|
||
attrs = ensureVideoAttribute(attrs, w, h)
|
||
case "audio":
|
||
attrs = ensureAudioAttribute(attrs, false)
|
||
case "voice":
|
||
attrs = ensureAudioAttribute(attrs, true)
|
||
case "sticker":
|
||
attrs = ensureImageSizeAttribute(attrs, w, h)
|
||
attrs = ensureDocumentAttribute(attrs, domain.DocumentAttribute{Kind: domain.DocAttrSticker})
|
||
}
|
||
attrs = ensureFilenameAttribute(attrs, inlineExternalContentFilename(resultType, mime))
|
||
return domain.DocumentSpec{MimeType: mime, Attributes: attrs}
|
||
}
|
||
|
||
func inlineExternalContentDimensions(attrs []domain.DocumentAttribute) (int, int) {
|
||
for _, attr := range attrs {
|
||
if (attr.Kind == domain.DocAttrImageSize || attr.Kind == domain.DocAttrVideo) && attr.W > 0 && attr.H > 0 {
|
||
return attr.W, attr.H
|
||
}
|
||
}
|
||
return 0, 0
|
||
}
|
||
|
||
func ensureImageSizeAttribute(attrs []domain.DocumentAttribute, w, h int) []domain.DocumentAttribute {
|
||
if hasDocumentAttribute(attrs, domain.DocAttrImageSize) || w <= 0 || h <= 0 {
|
||
return attrs
|
||
}
|
||
return append(attrs, domain.DocumentAttribute{Kind: domain.DocAttrImageSize, W: w, H: h})
|
||
}
|
||
|
||
func ensureVideoAttribute(attrs []domain.DocumentAttribute, w, h int) []domain.DocumentAttribute {
|
||
if hasDocumentAttribute(attrs, domain.DocAttrVideo) {
|
||
return attrs
|
||
}
|
||
return append(attrs, domain.DocumentAttribute{Kind: domain.DocAttrVideo, W: w, H: h, SupportsStreaming: true})
|
||
}
|
||
|
||
func ensureAudioAttribute(attrs []domain.DocumentAttribute, voice bool) []domain.DocumentAttribute {
|
||
for i := range attrs {
|
||
if attrs[i].Kind == domain.DocAttrAudio {
|
||
if voice {
|
||
attrs[i].Voice = true
|
||
}
|
||
return attrs
|
||
}
|
||
}
|
||
return append(attrs, domain.DocumentAttribute{Kind: domain.DocAttrAudio, Voice: voice})
|
||
}
|
||
|
||
func ensureFilenameAttribute(attrs []domain.DocumentAttribute, name string) []domain.DocumentAttribute {
|
||
for i := range attrs {
|
||
if attrs[i].Kind == domain.DocAttrFilename {
|
||
if strings.TrimSpace(attrs[i].FileName) == "" {
|
||
attrs[i].FileName = name
|
||
}
|
||
return attrs
|
||
}
|
||
}
|
||
return append(attrs, domain.DocumentAttribute{Kind: domain.DocAttrFilename, FileName: name})
|
||
}
|
||
|
||
func ensureDocumentAttribute(attrs []domain.DocumentAttribute, attr domain.DocumentAttribute) []domain.DocumentAttribute {
|
||
if hasDocumentAttribute(attrs, attr.Kind) {
|
||
return attrs
|
||
}
|
||
return append(attrs, attr)
|
||
}
|
||
|
||
func hasDocumentAttribute(attrs []domain.DocumentAttribute, kind domain.DocumentAttributeKind) bool {
|
||
for _, attr := range attrs {
|
||
if attr.Kind == kind {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func inlineExternalContentFilename(resultType, mime string) string {
|
||
switch resultType {
|
||
case "gif":
|
||
if mime == "video/mp4" {
|
||
return "animation.mp4"
|
||
}
|
||
return "animation.gif"
|
||
case "video":
|
||
return "video.mp4"
|
||
case "audio":
|
||
return "audio.mp3"
|
||
case "voice":
|
||
return "audio.ogg"
|
||
case "sticker":
|
||
return "sticker.webp"
|
||
default:
|
||
if slash := strings.LastIndexByte(mime, '/'); slash >= 0 && slash < len(mime)-1 {
|
||
ext := mime[slash+1:]
|
||
if ext == "jpeg" {
|
||
ext = "jpg"
|
||
}
|
||
if validInlineFilenameExt(ext) {
|
||
return "file." + ext
|
||
}
|
||
}
|
||
return "file"
|
||
}
|
||
}
|
||
|
||
func validInlineFilenameExt(ext string) bool {
|
||
if ext == "" || len(ext) > 16 {
|
||
return false
|
||
}
|
||
for _, r := range ext {
|
||
if r >= 'a' && r <= 'z' {
|
||
continue
|
||
}
|
||
if r >= '0' && r <= '9' {
|
||
continue
|
||
}
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
func (r *Router) tgBotInlineResults(ctx context.Context, viewerUserID int64, in domain.BotInlineResults) *tg.MessagesBotResults {
|
||
out := &tg.MessagesBotResults{
|
||
Gallery: in.Gallery,
|
||
QueryID: in.QueryID,
|
||
Results: make([]tg.BotInlineResultClass, 0, len(in.Results)),
|
||
CacheTime: in.CacheTime,
|
||
}
|
||
if in.NextOffset != "" {
|
||
out.SetNextOffset(in.NextOffset)
|
||
}
|
||
if in.SwitchPM != nil {
|
||
out.SetSwitchPm(tg.InlineBotSwitchPM{
|
||
Text: in.SwitchPM.Text,
|
||
StartParam: in.SwitchPM.StartParam,
|
||
})
|
||
}
|
||
if in.SwitchWeb != nil {
|
||
out.SetSwitchWebview(tg.InlineBotWebView{
|
||
Text: in.SwitchWeb.Text,
|
||
URL: in.SwitchWeb.URL,
|
||
})
|
||
}
|
||
for _, item := range in.Results {
|
||
out.Results = append(out.Results, tgBotInlineResult(item))
|
||
}
|
||
if r.deps.Users != nil && in.BotUserID != 0 {
|
||
if u, found, err := r.deps.Users.ByID(ctx, viewerUserID, in.BotUserID); err == nil && found {
|
||
out.Users = append(out.Users, r.tgUser(u))
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
func tgBotInlineResult(in domain.BotInlineResult) tg.BotInlineResultClass {
|
||
if in.Media != nil && !in.Media.IsZero() {
|
||
msg := tgBotInlineMediaMessage(in)
|
||
out := &tg.BotInlineResult{
|
||
ID: in.ID,
|
||
Type: in.Type,
|
||
Title: in.Title,
|
||
Description: in.Description,
|
||
SendMessage: msg,
|
||
}
|
||
if in.Media.Kind == domain.MessageMediaKindGeo ||
|
||
in.Media.Kind == domain.MessageMediaKindVenue ||
|
||
in.Media.Kind == domain.MessageMediaKindContact {
|
||
return out
|
||
}
|
||
mediaMsg := &tg.BotInlineMessageMediaAuto{
|
||
Message: in.Message,
|
||
Entities: tgMessageEntities(in.Entities),
|
||
}
|
||
if markup := tgReplyMarkup(in.ReplyMarkup); markup != nil {
|
||
mediaMsg.SetReplyMarkup(markup)
|
||
}
|
||
mediaOut := &tg.BotInlineMediaResult{
|
||
ID: in.ID,
|
||
Type: in.Type,
|
||
Title: in.Title,
|
||
Description: in.Description,
|
||
SendMessage: mediaMsg,
|
||
}
|
||
if in.Media.Kind == domain.MessageMediaKindPhoto && in.Media.Photo != nil {
|
||
mediaOut.SetPhoto(tgPhoto(*in.Media.Photo))
|
||
}
|
||
if in.Media.Kind == domain.MessageMediaKindDocument && in.Media.Document != nil {
|
||
mediaOut.SetDocument(tgDocument(*in.Media.Document))
|
||
}
|
||
return mediaOut
|
||
}
|
||
if in.MediaAuto {
|
||
msg := &tg.BotInlineMessageMediaAuto{
|
||
Message: in.Message,
|
||
Entities: tgMessageEntities(in.Entities),
|
||
}
|
||
if markup := tgReplyMarkup(in.ReplyMarkup); markup != nil {
|
||
msg.SetReplyMarkup(markup)
|
||
}
|
||
out := &tg.BotInlineResult{
|
||
ID: in.ID,
|
||
Type: in.Type,
|
||
Title: in.Title,
|
||
Description: in.Description,
|
||
SendMessage: msg,
|
||
}
|
||
if in.URL != "" {
|
||
out.SetURL(in.URL)
|
||
}
|
||
if in.Thumb != nil {
|
||
out.SetThumb(tgInlineWebDocument(*in.Thumb))
|
||
}
|
||
if in.Content != nil {
|
||
out.SetContent(tgInlineWebDocument(*in.Content))
|
||
}
|
||
return out
|
||
}
|
||
msg := &tg.BotInlineMessageText{
|
||
NoWebpage: in.NoWebpage,
|
||
Message: in.Message,
|
||
Entities: tgMessageEntities(in.Entities),
|
||
}
|
||
if markup := tgReplyMarkup(in.ReplyMarkup); markup != nil {
|
||
msg.SetReplyMarkup(markup)
|
||
}
|
||
out := &tg.BotInlineResult{
|
||
ID: in.ID,
|
||
Type: in.Type,
|
||
Title: in.Title,
|
||
Description: in.Description,
|
||
SendMessage: msg,
|
||
}
|
||
if in.URL != "" {
|
||
out.SetURL(in.URL)
|
||
}
|
||
if in.Thumb != nil {
|
||
out.SetThumb(tgInlineWebDocument(*in.Thumb))
|
||
}
|
||
if in.Content != nil {
|
||
out.SetContent(tgInlineWebDocument(*in.Content))
|
||
}
|
||
return out
|
||
}
|
||
|
||
func tgInlineWebDocument(in domain.BotInlineWebDocument) tg.WebDocumentClass {
|
||
return &tg.WebDocument{
|
||
URL: in.URL,
|
||
AccessHash: in.AccessHash,
|
||
Size: in.Size,
|
||
MimeType: in.MimeType,
|
||
Attributes: tgDocumentAttributes(in.MimeType, in.Attributes),
|
||
}
|
||
}
|
||
|
||
func tgBotInlineMediaMessage(in domain.BotInlineResult) tg.BotInlineMessageClass {
|
||
switch {
|
||
case in.Media.Kind == domain.MessageMediaKindGeo && in.Media.Geo != nil:
|
||
msg := &tg.BotInlineMessageMediaGeo{Geo: tgGeoPoint(*in.Media.Geo)}
|
||
if markup := tgReplyMarkup(in.ReplyMarkup); markup != nil {
|
||
msg.SetReplyMarkup(markup)
|
||
}
|
||
return msg
|
||
case in.Media.Kind == domain.MessageMediaKindVenue && in.Media.Venue != nil:
|
||
msg := &tg.BotInlineMessageMediaVenue{
|
||
Geo: tgGeoPoint(in.Media.Venue.Geo),
|
||
Title: in.Media.Venue.Title,
|
||
Address: in.Media.Venue.Address,
|
||
Provider: in.Media.Venue.Provider,
|
||
VenueID: in.Media.Venue.VenueID,
|
||
VenueType: in.Media.Venue.VenueType,
|
||
}
|
||
if markup := tgReplyMarkup(in.ReplyMarkup); markup != nil {
|
||
msg.SetReplyMarkup(markup)
|
||
}
|
||
return msg
|
||
case in.Media.Kind == domain.MessageMediaKindContact && in.Media.Contact != nil:
|
||
msg := &tg.BotInlineMessageMediaContact{
|
||
PhoneNumber: in.Media.Contact.PhoneNumber,
|
||
FirstName: in.Media.Contact.FirstName,
|
||
LastName: in.Media.Contact.LastName,
|
||
Vcard: in.Media.Contact.Vcard,
|
||
}
|
||
if markup := tgReplyMarkup(in.ReplyMarkup); markup != nil {
|
||
msg.SetReplyMarkup(markup)
|
||
}
|
||
return msg
|
||
default:
|
||
return &tg.BotInlineMessageMediaAuto{
|
||
Message: in.Message,
|
||
Entities: tgMessageEntities(in.Entities),
|
||
}
|
||
}
|
||
}
|
||
|
||
func tgInlineQueryPeerType(ctx context.Context, r *Router, userID, botID int64, peer domain.Peer) tg.InlineQueryPeerTypeClass {
|
||
switch peer.Type {
|
||
case domain.PeerTypeUser:
|
||
if peer.ID == botID {
|
||
return &tg.InlineQueryPeerTypeSameBotPM{}
|
||
}
|
||
if r.userIsBot(ctx, peer.ID) {
|
||
return &tg.InlineQueryPeerTypeBotPM{}
|
||
}
|
||
return &tg.InlineQueryPeerTypePM{}
|
||
case domain.PeerTypeChannel:
|
||
if r.deps.Channels != nil {
|
||
// 只读 Megagroup 标志:走轻量 ResolveChannel,省 dialog/读态/boost 查询。
|
||
if view, err := r.deps.Channels.ResolveChannel(ctx, userID, peer.ID); err == nil && !view.Channel.Megagroup {
|
||
return &tg.InlineQueryPeerTypeBroadcast{}
|
||
}
|
||
}
|
||
return &tg.InlineQueryPeerTypeMegagroup{}
|
||
default:
|
||
return &tg.InlineQueryPeerTypePM{}
|
||
}
|
||
}
|