added full gifts support
This commit is contained in:
parent
ea5b86de8f
commit
354128c106
35 changed files with 1502 additions and 103 deletions
102
internal/app/bots/gifbot.go
Normal file
102
internal/app/bots/gifbot.go
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
package bots
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
// HandlesInlineBot reports whether botUserID is a built-in bot this service
|
||||
// answers messages.getInlineBotResults for synchronously
|
||||
// (rpc.ServiceBotInlineResults). Deliberately separate from HandlesBot (see
|
||||
// that interface's doc comment in internal/rpc/deps.go) -- @gif gets inline
|
||||
// queries only, not private-message/callback dispatch.
|
||||
func (s *Service) HandlesInlineBot(botUserID int64) bool {
|
||||
return s != nil && botUserID == domain.GifBotUserID
|
||||
}
|
||||
|
||||
// OnInlineQuery serves @gif's admin-curated catalog as inline gif results,
|
||||
// ordered by title relevance to query (see rankGifCatalogEntries).
|
||||
//
|
||||
// offset/paging is not implemented: the catalog is bounded by
|
||||
// MaxGifCatalogEntries, which is MaxBotInlineResults, so one response always
|
||||
// carries all of it.
|
||||
//
|
||||
// Note TDesktop only ever calls this with a non-empty query: its GIF tab has
|
||||
// no trending panel, and GifsListWidget::searchForGifs returns early on an
|
||||
// empty string (chat_helpers/gifs_list_widget.cpp), showing saved GIFs alone.
|
||||
// An empty query is still handled here for clients that do ask for one.
|
||||
func (s *Service) OnInlineQuery(ctx context.Context, botUserID, _ int64, query, _ string) (domain.BotInlineResults, bool, error) {
|
||||
if s == nil || botUserID != domain.GifBotUserID {
|
||||
return domain.BotInlineResults{}, false, nil
|
||||
}
|
||||
if s.gifCatalog == nil {
|
||||
return domain.BotInlineResults{Gallery: true}, true, nil
|
||||
}
|
||||
entries, err := s.gifCatalog.ListGifCatalog(ctx, true)
|
||||
if err != nil {
|
||||
return domain.BotInlineResults{}, false, err
|
||||
}
|
||||
entries = rankGifCatalogEntries(entries, query)
|
||||
if len(entries) == 0 {
|
||||
return domain.BotInlineResults{Gallery: true}, true, nil
|
||||
}
|
||||
ids := make([]int64, len(entries))
|
||||
for i, e := range entries {
|
||||
ids[i] = e.DocumentID
|
||||
}
|
||||
docs, err := s.gifCatalog.GetDocuments(ctx, ids)
|
||||
if err != nil {
|
||||
return domain.BotInlineResults{}, false, err
|
||||
}
|
||||
byID := make(map[int64]domain.Document, len(docs))
|
||||
for _, d := range docs {
|
||||
byID[d.ID] = d
|
||||
}
|
||||
results := make([]domain.BotInlineResult, 0, len(entries))
|
||||
for _, e := range entries {
|
||||
doc, ok := byID[e.DocumentID]
|
||||
if !ok {
|
||||
// Catalog entry outlived its document (shouldn't happen -- documents
|
||||
// are never deleted -- but skip rather than surface a broken result).
|
||||
continue
|
||||
}
|
||||
docCopy := doc
|
||||
results = append(results, domain.BotInlineResult{
|
||||
ID: strconv.FormatInt(e.ID, 10),
|
||||
Type: "gif",
|
||||
Title: e.Title,
|
||||
Media: &domain.MessageMedia{Kind: domain.MessageMediaKindDocument, Document: &docCopy},
|
||||
})
|
||||
}
|
||||
return domain.BotInlineResults{Gallery: true, Results: results}, true, nil
|
||||
}
|
||||
|
||||
// rankGifCatalogEntries orders title matches first but never drops the rest.
|
||||
//
|
||||
// A real @gif searches a huge third-party index, so "no match" there means the
|
||||
// query genuinely found nothing. A self-hosted catalog is a handful of curated
|
||||
// files instead: filtering it down to exact title matches would leave the
|
||||
// picker empty for almost every word an operator's users type, which reads as
|
||||
// "the feature is broken" rather than "no results". Showing the whole catalog
|
||||
// with the closest titles first keeps every query useful while still honouring
|
||||
// the search term. Order within each group stays the admin-set
|
||||
// (sort_order, id) order the store already applied.
|
||||
func rankGifCatalogEntries(entries []domain.GifCatalogEntry, query string) []domain.GifCatalogEntry {
|
||||
query = strings.TrimSpace(strings.ToLower(query))
|
||||
if query == "" {
|
||||
return entries
|
||||
}
|
||||
matched := make([]domain.GifCatalogEntry, 0, len(entries))
|
||||
rest := make([]domain.GifCatalogEntry, 0, len(entries))
|
||||
for _, e := range entries {
|
||||
if strings.Contains(strings.ToLower(e.Title), query) {
|
||||
matched = append(matched, e)
|
||||
} else {
|
||||
rest = append(rest, e)
|
||||
}
|
||||
}
|
||||
return append(matched, rest...)
|
||||
}
|
||||
|
|
@ -44,6 +44,14 @@ type userStickerSetInstaller interface {
|
|||
InstallUserStickerSet(ctx context.Context, userID int64, setID int64, kind domain.StickerSetKind, archived bool, installedDate int) error
|
||||
}
|
||||
|
||||
// gifCatalogSource is the built-in @gif inline bot's read-only view of the
|
||||
// admin-curated GIF catalog (app/files.Service satisfies it as-is: it already
|
||||
// exposes GetDocuments for the sticker-set responder above).
|
||||
type gifCatalogSource interface {
|
||||
ListGifCatalog(ctx context.Context, onlyEnabled bool) ([]domain.GifCatalogEntry, error)
|
||||
GetDocuments(ctx context.Context, ids []int64) ([]domain.Document, error)
|
||||
}
|
||||
|
||||
type aiChatGenerator interface {
|
||||
GenerateTextStream(ctx context.Context, req domain.AITextGenerationRequest, emit func(domain.AIComposeText) error) (domain.AIComposeText, error)
|
||||
}
|
||||
|
|
@ -110,6 +118,7 @@ type Service struct {
|
|||
verification verificationApplications
|
||||
customVerification customVerifications
|
||||
verifierTargets verifierBotTargets
|
||||
gifCatalog gifCatalogSource
|
||||
telegramLogin *telegramloginapp.Service
|
||||
hooks RouterHooks
|
||||
textDrafts TextDraftPusher
|
||||
|
|
@ -207,6 +216,19 @@ func WithUserStickerSets(c userStickerSetInstaller) Option {
|
|||
}
|
||||
}
|
||||
|
||||
// WithGifCatalogSource injects the read-only catalog access used by the
|
||||
// built-in @gif inline bot. Without it, HandlesInlineBot still claims
|
||||
// GifBotUserID but OnInlineQuery reports handled=true with zero results
|
||||
// rather than erroring -- an unconfigured catalog is "nothing to show", not a
|
||||
// bot failure.
|
||||
func WithGifCatalogSource(c gifCatalogSource) Option {
|
||||
return func(s *Service) {
|
||||
if c != nil {
|
||||
s.gifCatalog = c
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithAIChatGenerator 注入内置 @ChatBot 使用的 AI 文本生成器。
|
||||
func WithAIChatGenerator(g aiChatGenerator) Option {
|
||||
return func(s *Service) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue