mirror of
https://github.com/tiennm99/miti99bot.git
synced 2026-10-04 12:13:20 +00:00
Text and caption formatting now survives an alias. Bold, italic, code, links
and mentions are stored as entities beside the text and sent back with it, on
the /insert, bare-command and inline paths alike. This works because the text
is re-sent byte-identical, so the offsets the entities carry stay valid — the
earlier comment claiming otherwise was wrong. They go back as entities rather
than re-rendered markup, which avoids escaping and re-parsing content the user
never wrote as markup.
/aliases now lists one line per name with what it holds, so the list says what
each will send:
3 aliases:
/cheer — sticker
/clip — video
/greeting — text
That costs one store read per listed alias, since DocStore has no bulk get and
the kind lives in the document. The reads stop once the message is full, so the
cost is bounded by what fits in one reply rather than by how many aliases exist.
Every reply that names a command or an alias wraps it in <code>, so tapping it
copies something ready to send. The generic usage lines stay plain text: they
contain a literal <name> placeholder that HTML mode would swallow as a tag.
Replying to another bot's message gets its own refusal. Telegram delivers that
reply with the content stripped, so capture finds nothing and the format advice
read as if the wrong kind had been sent. Checked only after capture fails, so
this bot's own messages — which are readable — never reach it.
132 lines
4.2 KiB
Go
132 lines
4.2 KiB
Go
package alias
|
|
|
|
import (
|
|
"context"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/go-telegram/bot"
|
|
"github.com/go-telegram/bot/models"
|
|
|
|
"github.com/tiennm99/miti99bot/internal/log"
|
|
)
|
|
|
|
const (
|
|
// maxInlineResults is Telegram's cap on results per answerInlineQuery.
|
|
maxInlineResults = 50
|
|
|
|
// inlineCacheSeconds is how long Telegram may reuse an answer for the same
|
|
// query. Kept short because the namespace is shared and writable: a name
|
|
// saved now should show up in the picker within seconds, not minutes.
|
|
inlineCacheSeconds = 5
|
|
)
|
|
|
|
// handleInline answers "@botname <prefix>" with the matching aliases.
|
|
//
|
|
// This is the reason the module stores a file_id rather than bytes: every
|
|
// result below is a "Cached" inline type, which takes an id Telegram already
|
|
// holds. Nothing is uploaded, and the picker renders real previews.
|
|
func (s *state) handleInline(ctx context.Context, b *bot.Bot, update *models.Update) error {
|
|
query := update.InlineQuery
|
|
if query == nil {
|
|
return nil
|
|
}
|
|
|
|
names, err := s.store.List(ctx, "")
|
|
if err != nil {
|
|
log.Error("alias_inline_list", "err", err)
|
|
// Answer with nothing rather than leaving the client spinning. An empty
|
|
// answer is also what Telegram expects when a query has no matches.
|
|
return s.answer(ctx, b, query.ID, nil)
|
|
}
|
|
|
|
prefix := strings.ToLower(strings.TrimSpace(query.Query))
|
|
matches := make([]string, 0, len(names))
|
|
for _, name := range names {
|
|
if prefix == "" || strings.HasPrefix(name, prefix) {
|
|
matches = append(matches, name)
|
|
}
|
|
}
|
|
sort.Strings(matches)
|
|
if len(matches) > maxInlineResults {
|
|
matches = matches[:maxInlineResults]
|
|
}
|
|
|
|
results := make([]models.InlineQueryResult, 0, len(matches))
|
|
for _, name := range matches {
|
|
entry, found, err := s.get(ctx, name)
|
|
if err != nil {
|
|
// One unreadable record must not blank the whole picker.
|
|
log.Error("alias_inline_get", "name", name, "err", err)
|
|
continue
|
|
}
|
|
if !found {
|
|
continue // deleted between the List and this read
|
|
}
|
|
if r := inlineResult(name, entry); r != nil {
|
|
results = append(results, r)
|
|
}
|
|
}
|
|
return s.answer(ctx, b, query.ID, results)
|
|
}
|
|
|
|
func (s *state) answer(ctx context.Context, b *bot.Bot, queryID string, results []models.InlineQueryResult) error {
|
|
_, err := b.AnswerInlineQuery(ctx, &bot.AnswerInlineQueryParams{
|
|
InlineQueryID: queryID,
|
|
Results: results,
|
|
CacheTime: inlineCacheSeconds,
|
|
})
|
|
return err
|
|
}
|
|
|
|
// inlineResult maps one alias to the inline result type that carries it, or nil
|
|
// when the kind has no cached inline form.
|
|
//
|
|
// Telegram defines no InlineQueryResultCachedVideoNote, so a video-note alias
|
|
// simply does not appear in the picker — it stays reachable through /insert and
|
|
// its own /name. Returning nil rather than substituting another type is
|
|
// deliberate: sending a round video note as a plain video would change what the
|
|
// user saved.
|
|
func inlineResult(name string, a Alias) models.InlineQueryResult {
|
|
switch a.Kind {
|
|
case kindSticker:
|
|
// No Title field on this type — Telegram shows the sticker itself.
|
|
return &models.InlineQueryResultCachedSticker{ID: name, StickerFileID: a.FileID}
|
|
case kindPhoto:
|
|
return &models.InlineQueryResultCachedPhoto{
|
|
ID: name, PhotoFileID: a.FileID, Title: name, Caption: a.Text, CaptionEntities: a.Entities,
|
|
}
|
|
case kindAnimation:
|
|
return &models.InlineQueryResultCachedGif{
|
|
ID: name, GifFileID: a.FileID, Title: name, Caption: a.Text, CaptionEntities: a.Entities,
|
|
}
|
|
case kindVideo:
|
|
return &models.InlineQueryResultCachedVideo{
|
|
ID: name, VideoFileID: a.FileID, Title: name, Caption: a.Text, CaptionEntities: a.Entities,
|
|
}
|
|
case kindAudio:
|
|
return &models.InlineQueryResultCachedAudio{
|
|
ID: name, AudioFileID: a.FileID, Caption: a.Text, CaptionEntities: a.Entities,
|
|
}
|
|
case kindVoice:
|
|
return &models.InlineQueryResultCachedVoice{
|
|
ID: name, VoiceFileID: a.FileID, Title: name, Caption: a.Text, CaptionEntities: a.Entities,
|
|
}
|
|
case kindDocument:
|
|
return &models.InlineQueryResultCachedDocument{
|
|
ID: name, DocumentFileID: a.FileID, Title: name, Caption: a.Text, CaptionEntities: a.Entities,
|
|
}
|
|
case kindText:
|
|
return &models.InlineQueryResultArticle{
|
|
ID: name,
|
|
Title: name,
|
|
Description: a.Text,
|
|
InputMessageContent: &models.InputTextMessageContent{
|
|
MessageText: a.Text,
|
|
Entities: a.Entities,
|
|
},
|
|
}
|
|
}
|
|
return nil
|
|
}
|