categorisation for gifs

This commit is contained in:
onysd 2026-08-24 23:09:08 +03:00
parent 5e18bd3830
commit d0669bdc5e
15 changed files with 468 additions and 49 deletions

View file

@ -6,6 +6,7 @@ import (
"strings"
"telesrv/internal/domain"
"telesrv/internal/seed/catalog"
)
// HandlesInlineBot reports whether botUserID is a built-in bot this service
@ -39,7 +40,15 @@ func (s *Service) OnInlineQuery(ctx context.Context, botUserID, _ int64, query,
if err != nil {
return domain.BotInlineResults{}, false, err
}
entries = rankGifCatalogEntries(entries, query)
if category := gifCategoryFromQuery(query); category != "" {
// A category-icon tap, not a typed word (see gifCategoryFromQuery) --
// filter by domain.GifCatalogEntry.Category instead of ranking by
// title, since the query is an emoji/emoji-blob a title never
// contains.
entries = filterGifCatalogEntriesByCategory(entries, category)
} else {
entries = rankGifCatalogEntries(entries, query)
}
if len(entries) == 0 {
return domain.BotInlineResults{Gallery: true}, true, nil
}
@ -84,6 +93,66 @@ func (s *Service) OnInlineQuery(ctx context.Context, botUserID, _ int64, query,
// 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.
// gifCategoryFromQuery recognizes a GIF-picker category-icon tap and returns
// which domain.GifCatalogCategories entry it names, or "" for an ordinary
// typed search.
//
// The two clients encode a tap completely differently, and neither sends the
// category's name:
// - Android (StickerCategoriesListView.EmojiCategory.remote, EmojiView.java)
// concatenates the whole tapped group's emoticons with no separator and
// sends that as the query -- an exact match against
// strings.Join(group.Emoticons, "").
// - TDesktop (GifSectionsValue, stickers_list_footer.cpp) reads a *separate*
// fixed emoji list (app config's gif_search_emojies, defaulting to 10
// emoji including some this server never configured) and sends the
// single tapped emoji as the query -- an exact match against one
// Emoticons entry.
//
// Both are checked against the same internal/seed/catalog data (the source of
// truth messages.getEmojiGroups itself serves), so no client-side changes or
// server-side emoji-list duplication are needed.
func gifCategoryFromQuery(query string) string {
query = strings.TrimSpace(query)
if query == "" {
return ""
}
groups, _ := catalog.EmojiGroups()
for _, g := range groups {
if len(g.Emoticons) == 0 {
continue
}
if strings.Join(g.Emoticons, "") == query {
return g.Title
}
}
for _, g := range groups {
for _, e := range g.Emoticons {
if e == query {
return g.Title
}
}
}
return ""
}
// filterGifCatalogEntriesByCategory keeps only entries tagged with category,
// falling back to the full (enabled) catalog if none are tagged yet -- an
// operator who hasn't categorized anything should still see every GIF on a
// category tap, not an empty picker.
func filterGifCatalogEntriesByCategory(entries []domain.GifCatalogEntry, category string) []domain.GifCatalogEntry {
filtered := make([]domain.GifCatalogEntry, 0, len(entries))
for _, e := range entries {
if e.Category == category {
filtered = append(filtered, e)
}
}
if len(filtered) == 0 {
return entries
}
return filtered
}
func rankGifCatalogEntries(entries []domain.GifCatalogEntry, query string) []domain.GifCatalogEntry {
query = strings.TrimSpace(strings.ToLower(query))
if query == "" {