merged from gramsrv upstream

This commit is contained in:
onysd 2026-09-01 12:06:31 +03:00
parent 79c64ee916
commit 21a0856587
651 changed files with 54774 additions and 4590 deletions

View file

@ -3,6 +3,7 @@ package rpc
import (
"context"
"errors"
"sort"
"strings"
"unicode/utf8"
@ -116,7 +117,12 @@ func (r *Router) onMessagesSendMessage(ctx context.Context, req *tg.MessagesSend
if req.ClearDraft {
r.clearDraftAfterSend(ctx, userID, peer, replyTo)
}
return r.monoforumSendUpdates(ctx, userID, replay.channel.Channel, savedPeer, replay.channel), nil
updates, projectionErr := r.monoforumSendUpdatesStrict(ctx, userID, replay.channel.Channel, savedPeer, replay.channel)
if projectionErr != nil {
sendErr = projectionErr
return nil, projectionErr
}
return updates, nil
}
if err := r.checkSendRateLimit(ctx, userID, 1); err != nil {
sendErr = err
@ -325,7 +331,7 @@ func (r *Router) messageReplyFromInput(ctx context.Context, userID int64, peer d
replyPeer := peer
if inputPeer, ok := reply.GetReplyToPeerID(); ok {
parsed, err := r.checkedDomainPeerFromInputPeer(ctx, userID, inputPeer)
if err != nil || parsed != peer {
if err != nil {
return nil, replyMessageIDInvalidErr()
}
replyPeer = parsed
@ -340,6 +346,19 @@ func (r *Router) messageReplyFromInput(ctx context.Context, userID int64, peer d
if reply.ReplyToMsgID == 0 && topMsgID == 0 {
return nil, replyMessageIDInvalidErr()
}
// inputReplyToMessage.reply_to_peer_id is explicitly allowed to point to a
// different dialog. Private-source existence is checked transactionally by
// MessageStore; channel sources are validated here because they live in the
// channel store rather than message_boxes.
if replyPeer.Type == domain.PeerTypeChannel && reply.ReplyToMsgID > 0 {
if r.deps.Channels == nil {
return nil, replyMessageIDInvalidErr()
}
history, err := r.deps.Channels.GetMessages(ctx, userID, replyPeer.ID, []int{reply.ReplyToMsgID})
if err != nil || len(history.Messages) != 1 || history.Messages[0].ID != reply.ReplyToMsgID {
return nil, replyMessageIDInvalidErr()
}
}
quoteText, _ := reply.GetQuoteText()
if utf8.RuneCountInString(quoteText) > maxReplyQuoteLength {
return nil, limitInvalidErr()
@ -434,9 +453,13 @@ func (r *Router) mentionedUserIDsFromMessage(ctx context.Context, currentUserID
}
}
if identity != nil {
for _, username := range extractMentionUsernames(message, domain.MaxChannelMentionRecipients-len(out)) {
blocked := mentionScanBlockedSpansFromTGEntities(message, entities)
for _, username := range extractMentionUsernames(message, domain.MaxChannelMentionRecipients-len(out), blocked) {
user, found, err := identity.ResolveUsername(ctx, currentUserID, username)
if err != nil {
if isMentionResolveMiss(err) {
continue
}
return nil, internalErr()
}
if found {
@ -450,16 +473,29 @@ func (r *Router) mentionedUserIDsFromMessage(ctx context.Context, currentUserID
return out, nil
}
func extractMentionUsernames(message string, limit int) []string {
func isMentionResolveMiss(err error) bool {
return errors.Is(err, domain.ErrUsernameInvalid) || errors.Is(err, domain.ErrUsernameNotOccupied)
}
func extractMentionUsernames(message string, limit int, blocked []byteSpan) []string {
if limit <= 0 || message == "" {
return nil
}
blocked = mergeByteSpans(append(blocked, rawURLByteSpans(message)...))
blockIndex := 0
seen := make(map[string]struct{})
out := make([]string, 0)
for i := 0; i < len(message); i++ {
if message[i] != '@' {
continue
}
for blockIndex < len(blocked) && blocked[blockIndex].end <= i {
blockIndex++
}
if blockIndex < len(blocked) && blocked[blockIndex].start <= i && i < blocked[blockIndex].end {
i = blocked[blockIndex].end - 1
continue
}
if i > 0 && isUsernameByte(message[i-1]) {
continue
}
@ -484,6 +520,110 @@ func extractMentionUsernames(message string, limit int) []string {
return out
}
func mergeByteSpans(spans []byteSpan) []byteSpan {
if len(spans) == 0 {
return nil
}
out := spans[:0]
for _, span := range spans {
if span.start < 0 || span.end <= span.start {
continue
}
inserted := false
for i := range out {
if span.start < out[i].start {
out = append(out, byteSpan{})
copy(out[i+1:], out[i:])
out[i] = span
inserted = true
break
}
}
if !inserted {
out = append(out, span)
}
}
if len(out) == 0 {
return nil
}
merged := out[:1]
for _, span := range out[1:] {
last := &merged[len(merged)-1]
if span.start <= last.end {
if span.end > last.end {
last.end = span.end
}
continue
}
merged = append(merged, span)
}
return merged
}
func mentionScanBlockedSpansFromTGEntities(message string, entities []tg.MessageEntityClass) []byteSpan {
if len(entities) == 0 {
return nil
}
bounds := utf16ByteBoundaries(message)
var out []byteSpan
for _, entity := range entities {
switch entity.(type) {
case *tg.MessageEntityURL, *tg.MessageEntityTextURL:
default:
continue
}
if span, ok := byteSpanFromUTF16Bounds(bounds, entity.GetOffset(), entity.GetLength()); ok {
out = append(out, span)
}
}
return out
}
func mentionScanBlockedSpansFromDomainEntities(message string, entities []domain.MessageEntity) []byteSpan {
if len(entities) == 0 {
return nil
}
bounds := utf16ByteBoundaries(message)
var out []byteSpan
for _, entity := range entities {
if entity.Type != domain.MessageEntityURL && entity.Type != domain.MessageEntityTextURL {
continue
}
if span, ok := byteSpanFromUTF16Bounds(bounds, entity.Offset, entity.Length); ok {
out = append(out, span)
}
}
return out
}
func utf16ByteBoundaries(message string) []int {
total := utf16CodeUnitLen(message)
bounds := make([]int, total+1)
for i := range bounds {
bounds[i] = -1
}
unit := 0
bounds[0] = 0
for i, r := range message {
bounds[unit] = i
if r <= 0xFFFF {
unit++
} else {
unit += 2
}
bounds[unit] = i + utf8.RuneLen(r)
}
return bounds
}
func byteSpanFromUTF16Bounds(bounds []int, offset, length int) (byteSpan, bool) {
end := offset + length
if offset < 0 || length <= 0 || end > len(bounds)-1 || bounds[offset] < 0 || bounds[end] < 0 {
return byteSpan{}, false
}
return byteSpan{start: bounds[offset], end: bounds[end]}, true
}
func isUsernameByte(b byte) bool {
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9') || b == '_'
}
@ -543,63 +683,18 @@ func tgPrivateSendResultUpdates(res domain.SendPrivateTextResult, randomID int64
}
func (r *Router) usersForMessageUpdate(ctx context.Context, ownerUserID int64, msg domain.Message) []tg.UserClass {
seen := make(map[int64]struct{}, 2)
users := make([]tg.UserClass, 0, 2)
add := func(id int64) {
if id == 0 {
return
}
if _, ok := seen[id]; ok {
return
}
seen[id] = struct{}{}
switch {
case isSystemUserID(id):
if u, ok := domain.SystemUserByID(id); ok {
users = append(users, r.tgUser(u))
}
case id == ownerUserID:
if r.deps.Users == nil {
return
}
u, err := r.deps.Users.Self(ctx, ownerUserID)
if err == nil && u.ID != 0 {
users = append(users, r.tgSelfUser(u))
}
default:
if r.deps.Users == nil {
return
}
u, found, err := r.deps.Users.ByID(ctx, ownerUserID, id)
if err == nil && found {
users = append(users, r.tgUser(u))
}
}
}
if msg.From.Type == domain.PeerTypeUser {
add(msg.From.ID)
}
if msg.Peer.Type == domain.PeerTypeUser {
add(msg.Peer.ID)
}
if msg.Forward != nil && msg.Forward.From.Type == domain.PeerTypeUser {
add(msg.Forward.From.ID)
}
add(msg.ViaBotID)
if msg.ReplyTo != nil && msg.ReplyTo.Peer.Type == domain.PeerTypeUser {
add(msg.ReplyTo.Peer.ID)
}
if msg.Media != nil && msg.Media.Contact != nil {
add(msg.Media.Contact.UserID)
}
// A non-min User replaces the cached peer on iOS. Keep the complete
// username vector on synchronous message echoes instead of letting this
// response regress a previously hydrated profile to the legacy scalar.
r.applyUsernamesToPeerObjects(ctx, users, nil)
return users
return r.usersForMessageUpdates(ctx, ownerUserID, []domain.Message{msg})
}
func (r *Router) usersForMessageUpdateWithPreloaded(ctx context.Context, ownerUserID int64, msg domain.Message, preloaded []domain.User) []tg.UserClass {
return r.usersForMessageUpdatesWithPreloaded(ctx, ownerUserID, []domain.Message{msg}, preloaded)
}
func (r *Router) usersForMessageUpdates(ctx context.Context, ownerUserID int64, messages []domain.Message) []tg.UserClass {
return r.usersForMessageUpdatesWithPreloaded(ctx, ownerUserID, messages, nil)
}
func (r *Router) usersForMessageUpdatesWithPreloaded(ctx context.Context, ownerUserID int64, messages []domain.Message, preloaded []domain.User) []tg.UserClass {
seen := make(map[int64]struct{}, len(messages)*2)
ids := make([]int64, 0, len(messages)*2)
addID := func(id int64) {
@ -613,29 +708,30 @@ func (r *Router) usersForMessageUpdates(ctx context.Context, ownerUserID int64,
ids = append(ids, id)
}
for _, msg := range messages {
if msg.From.Type == domain.PeerTypeUser {
addID(msg.From.ID)
}
if msg.Peer.Type == domain.PeerTypeUser {
addID(msg.Peer.ID)
}
if msg.Forward != nil && msg.Forward.From.Type == domain.PeerTypeUser {
addID(msg.Forward.From.ID)
}
addID(msg.ViaBotID)
if msg.ReplyTo != nil && msg.ReplyTo.Peer.Type == domain.PeerTypeUser {
addID(msg.ReplyTo.Peer.ID)
}
if msg.Media != nil && msg.Media.Contact != nil {
addID(msg.Media.Contact.UserID)
for _, id := range appendMessageUserIDs(nil, make(map[int64]struct{}), msg) {
addID(id)
}
}
if len(ids) == 0 {
return nil
}
loaded := make(map[int64]domain.User, len(ids))
if r.deps.Users != nil {
if users, err := r.deps.Users.ByIDs(ctx, ownerUserID, ids); err == nil {
for _, user := range preloaded {
if user.ID != 0 {
loaded[user.ID] = user
}
}
missing := make([]int64, 0, len(ids))
for _, id := range ids {
if isSystemUserID(id) {
continue
}
if _, ok := loaded[id]; !ok {
missing = append(missing, id)
}
}
if r.deps.Users != nil && len(missing) > 0 {
if users, err := r.deps.Users.ByIDs(ctx, ownerUserID, missing); err == nil {
for _, user := range users {
loaded[user.ID] = user
}
@ -666,6 +762,46 @@ func (r *Router) chatsForMessageUpdate(ctx context.Context, ownerUserID int64, m
return r.chatsForMessageUpdates(ctx, ownerUserID, []domain.Message{msg})
}
func appendMessageUserIDs(ids []int64, seen map[int64]struct{}, msg domain.Message) []int64 {
add := func(id int64) {
if id == 0 {
return
}
if _, ok := seen[id]; ok {
return
}
seen[id] = struct{}{}
ids = append(ids, id)
}
for _, peer := range []domain.Peer{msg.From, msg.Peer} {
if peer.Type == domain.PeerTypeUser {
add(peer.ID)
}
}
if msg.Forward != nil && msg.Forward.From.Type == domain.PeerTypeUser {
add(msg.Forward.From.ID)
}
add(msg.ViaBotID)
if msg.ReplyTo != nil && msg.ReplyTo.Peer.Type == domain.PeerTypeUser {
add(msg.ReplyTo.Peer.ID)
}
if msg.Media != nil && msg.Media.Contact != nil {
add(msg.Media.Contact.UserID)
}
userRefs := make(map[int64]struct{})
channelRefs := make(map[int64]struct{})
collectMessagePeerRefs(msg, 0, userRefs, channelRefs)
extra := make([]int64, 0, len(userRefs))
for id := range userRefs {
extra = append(extra, id)
}
sort.Slice(extra, func(i, j int) bool { return extra[i] < extra[j] })
for _, id := range extra {
add(id)
}
return ids
}
func appendMessageChannelIDs(ids []int64, seen map[int64]struct{}, msg domain.Message) []int64 {
add := func(id int64) {
if id == 0 {
@ -677,11 +813,10 @@ func appendMessageChannelIDs(ids []int64, seen map[int64]struct{}, msg domain.Me
seen[id] = struct{}{}
ids = append(ids, id)
}
if msg.From.Type == domain.PeerTypeChannel {
add(msg.From.ID)
}
if msg.Peer.Type == domain.PeerTypeChannel {
add(msg.Peer.ID)
for _, peer := range []domain.Peer{msg.From, msg.Peer} {
if peer.Type == domain.PeerTypeChannel {
add(peer.ID)
}
}
if msg.Forward != nil && msg.Forward.From.Type == domain.PeerTypeChannel {
add(msg.Forward.From.ID)
@ -689,6 +824,17 @@ func appendMessageChannelIDs(ids []int64, seen map[int64]struct{}, msg domain.Me
if msg.ReplyTo != nil && msg.ReplyTo.Peer.Type == domain.PeerTypeChannel {
add(msg.ReplyTo.Peer.ID)
}
userRefs := make(map[int64]struct{})
channelRefs := make(map[int64]struct{})
collectMessagePeerRefs(msg, 0, userRefs, channelRefs)
extra := make([]int64, 0, len(channelRefs))
for id := range channelRefs {
extra = append(extra, id)
}
sort.Slice(extra, func(i, j int) bool { return extra[i] < extra[j] })
for _, id := range extra {
add(id)
}
return ids
}
@ -744,9 +890,13 @@ func (r *Router) mentionUserIDsFromDomain(ctx context.Context, currentUserID int
}
}
if identity, ok := r.deps.Users.(UserIdentityService); ok && identity != nil {
for _, username := range extractMentionUsernames(message, domain.MaxChannelMentionRecipients-len(out)) {
blocked := mentionScanBlockedSpansFromDomainEntities(message, entities)
for _, username := range extractMentionUsernames(message, domain.MaxChannelMentionRecipients-len(out), blocked) {
user, found, err := identity.ResolveUsername(ctx, currentUserID, username)
if err != nil {
if isMentionResolveMiss(err) {
continue
}
break
}
if found {