296 lines
8 KiB
Go
296 lines
8 KiB
Go
package rpc
|
|
|
|
import (
|
|
"context"
|
|
"crypto/md5"
|
|
"encoding/binary"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/iamxvbaba/td/tg"
|
|
|
|
"telesrv/internal/compat/tdesktop"
|
|
"telesrv/internal/domain"
|
|
)
|
|
|
|
func (r *Router) onMessagesGetTopReactions(ctx context.Context, req *tg.MessagesGetTopReactionsRequest) (tg.MessagesReactionsClass, error) {
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
if req.Limit < 0 || req.Limit > maxSearchResultsLimit {
|
|
return nil, limitInvalidErr()
|
|
}
|
|
limit := req.Limit
|
|
if limit == 0 {
|
|
limit = defaultTopReactionsLimit
|
|
}
|
|
reactions := []domain.MessageReaction{}
|
|
if r.deps.Channels != nil {
|
|
var err error
|
|
reactions, err = r.deps.Channels.TopReactions(ctx, userID, limit)
|
|
if err != nil {
|
|
return nil, channelInvalidErr(err)
|
|
}
|
|
}
|
|
return messagesReactionsFromDomain(r.reactionsWithCatalogFallback(ctx, reactions, limit), req.Hash), nil
|
|
}
|
|
|
|
func (r *Router) onMessagesGetRecentReactions(ctx context.Context, req *tg.MessagesGetRecentReactionsRequest) (tg.MessagesReactionsClass, error) {
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
if req.Limit < 0 || req.Limit > maxSearchResultsLimit {
|
|
return nil, limitInvalidErr()
|
|
}
|
|
if r.deps.Channels == nil {
|
|
return messagesReactionsEmpty(req.Hash), nil
|
|
}
|
|
reactions, err := r.deps.Channels.RecentReactions(ctx, userID, req.Limit)
|
|
if err != nil {
|
|
return nil, channelInvalidErr(err)
|
|
}
|
|
return messagesReactionsFromDomain(reactions, req.Hash), nil
|
|
}
|
|
|
|
func (r *Router) onMessagesClearRecentReactions(ctx context.Context) (bool, error) {
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return false, internalErr()
|
|
}
|
|
if r.deps.Channels != nil {
|
|
if err := r.deps.Channels.ClearRecentReactions(ctx, userID); err != nil {
|
|
return false, channelInvalidErr(err)
|
|
}
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func (r *Router) onMessagesGetSavedReactionTags(ctx context.Context, req *tg.MessagesGetSavedReactionTagsRequest) (tg.MessagesSavedReactionTagsClass, error) {
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
var savedPeer domain.Peer
|
|
if peer, ok := req.GetPeer(); ok && peer != nil {
|
|
savedPeer, err = r.checkedDomainPeerFromInputPeer(ctx, userID, peer)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if r.deps.Messages == nil {
|
|
return savedReactionTagsEmpty(req.Hash), nil
|
|
}
|
|
tags, err := r.deps.Messages.SavedReactionTags(ctx, userID, savedPeer, domain.MaxSavedReactionTags)
|
|
if err != nil {
|
|
return nil, messageReactionErr(err)
|
|
}
|
|
return savedReactionTagsFromDomain(tags, req.Hash, savedPeer.ID == 0), nil
|
|
}
|
|
|
|
func (r *Router) onMessagesGetDefaultTagReactions(ctx context.Context, hash int64) (tg.MessagesReactionsClass, error) {
|
|
if _, _, err := r.currentUserID(ctx); err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
return messagesReactionsFromDomain(
|
|
r.reactionsWithCatalogFallback(ctx, nil, domain.MaxChannelMessageReactionsPerUser),
|
|
hash,
|
|
), nil
|
|
}
|
|
|
|
func messagesReactionsEmpty(_ int64) tg.MessagesReactionsClass {
|
|
return &tg.MessagesReactions{
|
|
Hash: 0,
|
|
Reactions: []tg.ReactionClass{},
|
|
}
|
|
}
|
|
|
|
func messagesReactionsFromDomain(reactions []domain.MessageReaction, requestHash int64) tg.MessagesReactionsClass {
|
|
hash := messageReactionListHash(reactions)
|
|
if hash != 0 && requestHash == hash {
|
|
return &tg.MessagesReactionsNotModified{}
|
|
}
|
|
out := make([]tg.ReactionClass, 0, len(reactions))
|
|
for _, reaction := range reactions {
|
|
tgReaction := tgMessageReaction(reaction)
|
|
if tgReaction != nil {
|
|
out = append(out, tgReaction)
|
|
}
|
|
}
|
|
return &tg.MessagesReactions{
|
|
Hash: hash,
|
|
Reactions: out,
|
|
}
|
|
}
|
|
|
|
func savedReactionTagsEmpty(_ int64) tg.MessagesSavedReactionTagsClass {
|
|
return &tg.MessagesSavedReactionTags{
|
|
Tags: []tg.SavedReactionTag{},
|
|
Hash: 0,
|
|
}
|
|
}
|
|
|
|
func savedReactionTagsFromDomain(tags []domain.SavedReactionTag, requestHash int64, includeTitles bool) tg.MessagesSavedReactionTagsClass {
|
|
tags = append([]domain.SavedReactionTag(nil), tags...)
|
|
sort.SliceStable(tags, func(i, j int) bool {
|
|
if tags[i].Count != tags[j].Count {
|
|
return tags[i].Count > tags[j].Count
|
|
}
|
|
return savedReactionTagLongID(tags[i].Reaction) > savedReactionTagLongID(tags[j].Reaction)
|
|
})
|
|
hash := savedReactionTagListHash(tags, includeTitles)
|
|
if hash != 0 && requestHash == hash {
|
|
return &tg.MessagesSavedReactionTagsNotModified{}
|
|
}
|
|
out := make([]tg.SavedReactionTag, 0, len(tags))
|
|
for _, tag := range tags {
|
|
reaction := tgMessageReaction(tag.Reaction)
|
|
if reaction == nil {
|
|
continue
|
|
}
|
|
item := tg.SavedReactionTag{
|
|
Reaction: reaction,
|
|
Count: tag.Count,
|
|
}
|
|
if includeTitles && tag.Title != "" {
|
|
item.SetTitle(tag.Title)
|
|
}
|
|
out = append(out, item)
|
|
}
|
|
if len(out) == 0 {
|
|
return savedReactionTagsEmpty(requestHash)
|
|
}
|
|
return &tg.MessagesSavedReactionTags{
|
|
Tags: out,
|
|
Hash: hash,
|
|
}
|
|
}
|
|
|
|
func (r *Router) reactionsWithCatalogFallback(ctx context.Context, reactions []domain.MessageReaction, limit int) []domain.MessageReaction {
|
|
return mergeReactionCatalogFallback(reactions, r.availableReactionCatalog(ctx, limit), limit)
|
|
}
|
|
|
|
func (r *Router) availableReactionCatalog(ctx context.Context, limit int) []domain.MessageReaction {
|
|
if limit <= 0 {
|
|
return nil
|
|
}
|
|
if r.deps.Files != nil {
|
|
catalog, err := r.deps.Files.ListAvailableReactions(ctx)
|
|
if err == nil {
|
|
out := make([]domain.MessageReaction, 0, min(limit, len(catalog)))
|
|
for _, item := range catalog {
|
|
emoticon := strings.TrimSpace(item.Reaction)
|
|
if item.Inactive || emoticon == "" {
|
|
continue
|
|
}
|
|
out = append(out, domain.MessageReaction{Type: domain.MessageReactionEmoji, Emoticon: emoticon})
|
|
if len(out) >= limit {
|
|
return out
|
|
}
|
|
}
|
|
if len(out) > 0 {
|
|
return out
|
|
}
|
|
}
|
|
}
|
|
return staticReactionCatalog()
|
|
}
|
|
|
|
func staticReactionCatalog() []domain.MessageReaction {
|
|
emoticons := tdesktop.DefaultReactionEmoticons()
|
|
out := make([]domain.MessageReaction, 0, len(emoticons))
|
|
for _, emoticon := range emoticons {
|
|
out = append(out, domain.MessageReaction{Type: domain.MessageReactionEmoji, Emoticon: emoticon})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func mergeReactionCatalogFallback(reactions, fallback []domain.MessageReaction, limit int) []domain.MessageReaction {
|
|
if limit <= 0 {
|
|
return []domain.MessageReaction{}
|
|
}
|
|
if limit > domain.MaxTopMessageReactions {
|
|
limit = domain.MaxTopMessageReactions
|
|
}
|
|
out := make([]domain.MessageReaction, 0, limit)
|
|
seen := make(map[string]struct{}, limit)
|
|
for _, reaction := range reactions {
|
|
if !reaction.Valid() {
|
|
continue
|
|
}
|
|
key := reaction.Key()
|
|
if _, ok := seen[key]; ok {
|
|
continue
|
|
}
|
|
out = append(out, reaction)
|
|
seen[key] = struct{}{}
|
|
if len(out) >= limit {
|
|
return out
|
|
}
|
|
}
|
|
for _, reaction := range fallback {
|
|
if !reaction.Valid() {
|
|
continue
|
|
}
|
|
key := reaction.Key()
|
|
if _, ok := seen[key]; ok {
|
|
continue
|
|
}
|
|
out = append(out, reaction)
|
|
seen[key] = struct{}{}
|
|
if len(out) >= limit {
|
|
return out
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func messageReactionListHash(reactions []domain.MessageReaction) int64 {
|
|
if len(reactions) == 0 {
|
|
return 0
|
|
}
|
|
var hash uint64
|
|
for _, reaction := range reactions {
|
|
hash = telegramListHashNext(hash, savedReactionTagLongID(reaction))
|
|
}
|
|
return int64(hash)
|
|
}
|
|
|
|
func savedReactionTagListHash(tags []domain.SavedReactionTag, includeTitles bool) int64 {
|
|
if len(tags) == 0 {
|
|
return 0
|
|
}
|
|
var hash uint64
|
|
for _, tag := range tags {
|
|
hash = telegramListHashNext(hash, savedReactionTagLongID(tag.Reaction))
|
|
if includeTitles && tag.Title != "" {
|
|
hash = telegramListHashNext(hash, md5LongID(tag.Title))
|
|
}
|
|
hash = telegramListHashNext(hash, uint64(tag.Count))
|
|
}
|
|
return int64(hash)
|
|
}
|
|
|
|
func savedReactionTagLongID(reaction domain.MessageReaction) uint64 {
|
|
switch reaction.Type {
|
|
case domain.MessageReactionEmoji:
|
|
return md5LongID(strings.ReplaceAll(reaction.Emoticon, "\ufe0f", ""))
|
|
case domain.MessageReactionCustomEmoji:
|
|
return uint64(reaction.DocumentID)
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
func md5LongID(value string) uint64 {
|
|
sum := md5.Sum([]byte(value))
|
|
return binary.BigEndian.Uint64(sum[:8])
|
|
}
|
|
|
|
func telegramListHashNext(hash, id uint64) uint64 {
|
|
hash ^= hash >> 21
|
|
hash ^= hash << 35
|
|
hash ^= hash >> 4
|
|
return hash + id
|
|
}
|