owpengram-server/internal/rpc/messages_helpers.go

603 lines
18 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package rpc
import (
"context"
"errors"
"github.com/iamxvbaba/td/tg"
"github.com/iamxvbaba/td/tgerr"
"telesrv/internal/domain"
)
func reportResultForOption(option string) (tg.ReportResultClass, error) {
switch option {
case "":
return &tg.ReportResultChooseOption{
Title: "Report",
Options: []tg.MessageReportOption{
{Text: "Scam or spam", Option: []byte("spam")},
{Text: "Violence", Option: []byte("violence")},
{Text: "Pornography", Option: []byte("pornography")},
{Text: "Child abuse", Option: []byte("child_abuse")},
{Text: "Illegal drugs", Option: []byte("illegal_drugs")},
{Text: "Personal details", Option: []byte("personal_details")},
{Text: "Copyright", Option: []byte("copyright")},
{Text: "Fake or impersonation", Option: []byte("fake")},
{Text: "Other", Option: []byte("other")},
},
}, nil
case "other":
return &tg.ReportResultAddComment{Optional: false, Option: []byte("other:comment")}, nil
case "spam", "violence", "pornography", "child_abuse", "illegal_drugs",
"personal_details", "copyright", "fake", "other:comment":
return &tg.ReportResultReported{}, nil
default:
return nil, tgerr.New(400, "OPTION_INVALID")
}
}
func moderationReasonForReportOption(option string) (domain.ModerationReason, bool) {
switch option {
case "spam":
return domain.ModerationReasonSpam, true
case "violence":
return domain.ModerationReasonViolence, true
case "pornography":
return domain.ModerationReasonPornography, true
case "child_abuse":
return domain.ModerationReasonChildAbuse, true
case "illegal_drugs":
return domain.ModerationReasonIllegalDrugs, true
case "personal_details":
return domain.ModerationReasonPersonalDetails, true
case "copyright":
return domain.ModerationReasonCopyright, true
case "fake":
return domain.ModerationReasonFake, true
case "other:comment":
return domain.ModerationReasonOther, true
default:
return "", false
}
}
func (r *Router) inputPeerForDomainPeer(ctx context.Context, currentUserID int64, peer domain.Peer) tg.InputPeerClass {
switch peer.Type {
case domain.PeerTypeUser:
if u, ok := domain.SystemUserByID(peer.ID); ok {
return &tg.InputPeerUser{UserID: u.ID, AccessHash: u.AccessHash}
}
switch {
case r.deps.Users == nil:
return nil
case peer.ID == currentUserID:
u, err := r.deps.Users.Self(ctx, currentUserID)
if err != nil || u.ID == 0 {
return nil
}
return &tg.InputPeerUser{UserID: u.ID, AccessHash: u.AccessHash}
default:
u, found, err := r.deps.Users.ByID(ctx, currentUserID, peer.ID)
if err != nil || !found {
return nil
}
return &tg.InputPeerUser{UserID: u.ID, AccessHash: u.AccessHash}
}
case domain.PeerTypeChannel:
if r.deps.Channels == nil || peer.ID == 0 {
return nil
}
// 只需 access_hash 解析 InputPeerChannel走轻量 ResolveChannel仅访问校验省 dialog/读态/boost
// 这 3 条额外查询。inputPeerFor 是各消息类 RPC 的通用 peer 解析器、调用极频,是 GetChannel 放大的主源头。
view, err := r.deps.Channels.ResolveChannel(ctx, currentUserID, peer.ID)
if err != nil || view.Channel.ID == 0 {
return nil
}
return &tg.InputPeerChannel{ChannelID: view.Channel.ID, AccessHash: view.Channel.AccessHash}
default:
return nil
}
}
func validateHistoryBounds(offsetID, addOffset, limit, maxID, minID int) error {
if offsetID < 0 || offsetID > domain.MaxMessageBoxID || maxID < 0 || maxID > domain.MaxMessageBoxID || minID < 0 || minID > domain.MaxMessageBoxID {
return messageIDInvalidErr()
}
if addOffset < -100 || addOffset > 100 || limit < 0 || limit > maxSearchResultsLimit {
return limitInvalidErr()
}
return nil
}
func (r *Router) savedHistoryChats(ctx context.Context, userID int64, hasParent bool, parent domain.Peer, peer tg.InputPeerClass) []tg.ChatClass {
if r.deps.Channels == nil {
return []tg.ChatClass{}
}
seen := make(map[int64]struct{}, 2)
out := make([]tg.ChatClass, 0, 2)
add := func(channelID int64) {
if channelID == 0 {
return
}
if _, ok := seen[channelID]; ok {
return
}
seen[channelID] = struct{}{}
view, err := r.deps.Channels.ResolveChannel(ctx, userID, channelID)
if err != nil || view.Channel.ID == 0 {
return
}
out = append(out, tgChannelChatForView(userID, view))
}
if hasParent && parent.Type == domain.PeerTypeChannel {
add(parent.ID)
}
if p, ok := r.domainPeerFromInputPeer(userID, peer); ok && p.Type == domain.PeerTypeChannel {
add(p.ID)
}
return out
}
func (r *Router) channelOnlineCount(ctx context.Context, userID, channelID int64) int {
if channelID == 0 || r.deps.Channels == nil || r.deps.Sessions == nil {
return 1
}
provider, ok := r.deps.Sessions.(OnlineUserProvider)
if !ok {
return 1
}
online := provider.OnlineChannelUserIDs(channelID, domain.MaxChannelRealtimeFanout)
candidates := make([]int64, 0, len(online)+1)
if userID != 0 {
candidates = append(candidates, userID)
}
candidates = append(candidates, online...)
active, err := r.deps.Channels.FilterActiveMemberIDs(ctx, channelID, candidates)
if err != nil {
return 1
}
return len(active)
}
func (r *Router) onMessagesSetTyping(ctx context.Context, req *tg.MessagesSetTypingRequest) (bool, error) {
if req == nil {
return false, inputRequestInvalidErr()
}
topMsgID, topMsgIDSet := req.GetTopMsgID()
if !topMsgIDSet && req.TopMsgID != 0 {
topMsgID, topMsgIDSet = req.TopMsgID, true
}
if topMsgIDSet {
switch {
case topMsgID <= 0:
topMsgID = 0
case topMsgID > domain.MaxMessageBoxID:
return false, msgIDInvalidErr()
}
}
userID, _, err := r.currentUserID(ctx)
if err != nil {
return false, internalErr()
}
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer)
if err != nil {
return false, err
}
if peer.Type != domain.PeerTypeUser || peer.ID == 0 || peer.ID == userID {
if peer.Type == domain.PeerTypeChannel && peer.ID != 0 && r.deps.Channels != nil {
action := req.Action
if action == nil {
action = &tg.SendMessageCancelAction{}
}
updates := &tg.Updates{
Updates: []tg.UpdateClass{&tg.UpdateChannelUserTyping{
ChannelID: peer.ID,
FromID: &tg.PeerUser{UserID: userID},
TopMsgID: topMsgID,
Action: action,
}},
Date: int(r.clock.Now().Unix()),
}
r.pushChannelViewerUpdates(ctx, 0, peer.ID, nil, func(int64) *tg.Updates {
return updates
})
}
return true, nil
}
action := req.Action
if action == nil {
action = &tg.SendMessageCancelAction{}
}
update := &tg.UpdateUserTyping{
UserID: userID,
TopMsgID: topMsgID,
Action: action,
}
updates := &tg.UpdateShort{
Update: update,
Date: int(r.clock.Now().Unix()),
}
r.pushTypingUpdate(ctx, peer.ID, updates)
return true, nil
}
func (r *Router) pushTypingUpdate(ctx context.Context, targetUserID int64, updates *tg.UpdateShort) {
// typing 是 transient不写 durable log未就绪的 session 直接跳过、不进 pending。
r.pushUserMessageTransient(ctx, targetUserID, "push typing update", updates)
}
func inputMessageBoxID(input tg.InputMessageClass) (int, bool) {
switch msg := input.(type) {
case *tg.InputMessageID:
return msg.ID, true
default:
return 0, false
}
}
func (r *Router) lookupOwnerMessage(ctx context.Context, userID int64, id int) (domain.Message, bool, error) {
filter := domain.MessageFilter{
MinID: id - 1,
Limit: 1,
}
if id < domain.MaxMessageBoxID {
filter.MaxID = id + 1
}
list, err := r.deps.Messages.Search(ctx, userID, filter)
if err != nil {
return domain.Message{}, false, err
}
if len(list.Messages) == 0 || list.Messages[0].ID != id {
return domain.Message{}, false, nil
}
return list.Messages[0], true, nil
}
func draftClearUpdate(peer domain.Peer, topMessageID, date int) *tg.UpdateDraftMessage {
peerTL := tgPeer(peer)
if peerTL == nil {
return nil
}
draft := &tg.DraftMessageEmpty{}
draft.SetDate(date)
update := &tg.UpdateDraftMessage{Peer: peerTL, Draft: draft}
if topMessageID > 0 {
update.SetTopMsgID(topMessageID)
}
return update
}
func draftReplyIsEmpty(reply tg.InputReplyToClass) bool {
if reply == nil {
return true
}
input, ok := reply.(*tg.InputReplyToMessage)
if !ok {
return false
}
topMsgID, hasTopMsgID := input.GetTopMsgID()
return input.ReplyToMsgID == 0 && hasTopMsgID && topMsgID > 0
}
func draftInputMedia(media tg.InputMediaClass) tg.InputMediaClass {
switch media.(type) {
case nil, *tg.InputMediaEmpty:
return nil
default:
return media
}
}
func (r *Router) searchGlobalChannelOffsetID(ctx context.Context, userID int64, peer tg.InputPeerClass) (int64, error) {
if peer == nil {
return 0, nil
}
switch peer.(type) {
case *tg.InputPeerEmpty:
return 0, nil
}
ref, ok := inputPeerChannelRef(peer)
if !ok {
return 0, nil
}
if ref.ID <= 0 {
return 0, peerIDInvalidErr()
}
if ref.CheckAccessHash && r.deps.Channels != nil {
view, err := r.deps.Channels.ResolveChannel(ctx, userID, ref.ID)
if err != nil {
return 0, channelInvalidErr(err)
}
if !inputChannelAccessHashMatches(ref, view.Channel) {
return 0, channelInvalidErr(domain.ErrChannelPrivate)
}
}
return ref.ID, nil
}
func (s forwardSource) userID() int64 {
if s.from.Type == domain.PeerTypeUser {
return s.from.ID
}
if s.forward != nil && s.forward.From.Type == domain.PeerTypeUser {
return s.forward.From.ID
}
return 0
}
func (r *Router) onMessagesGetOutboxReadDate(ctx context.Context, req *tg.MessagesGetOutboxReadDateRequest) (*tg.OutboxReadDate, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if userID == 0 || r.deps.Messages == nil {
return nil, messageIDInvalidErr()
}
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer)
if err != nil {
return nil, err
}
if peer.Type != domain.PeerTypeUser || peer.ID == 0 {
return nil, peerIDInvalidErr()
}
date, err := r.deps.Messages.GetOutboxReadDate(ctx, userID, domain.OutboxReadDateRequest{
OwnerUserID: userID,
Peer: peer,
ID: req.MsgID,
})
if err != nil {
return nil, messageReadDateErr(err)
}
return &tg.OutboxReadDate{Date: date}, nil
}
func (r *Router) onMessagesGetMessageReadParticipants(ctx context.Context, req *tg.MessagesGetMessageReadParticipantsRequest) ([]tg.ReadParticipantDate, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if req.MsgID <= 0 || req.MsgID > domain.MaxMessageBoxID {
return nil, messageIDInvalidErr()
}
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer)
if err != nil {
return nil, err
}
if peer.Type != domain.PeerTypeChannel || peer.ID == 0 {
return nil, peerIDInvalidErr()
}
if r.deps.Channels == nil {
return []tg.ReadParticipantDate{}, nil
}
res, err := r.deps.Channels.GetMessageReadParticipants(ctx, userID, domain.ChannelReadParticipantsRequest{
UserID: userID,
ChannelID: peer.ID,
MessageID: req.MsgID,
Limit: domain.MaxChannelReadParticipants,
Date: int(r.clock.Now().Unix()),
})
if err != nil {
if errors.Is(err, domain.ErrMessageIDInvalid) {
return nil, messageIDInvalidErr()
}
return nil, channelInvalidErr(err)
}
out := make([]tg.ReadParticipantDate, 0, len(res.Participants))
for _, p := range res.Participants {
if p.UserID == 0 {
continue
}
out = append(out, tg.ReadParticipantDate{UserID: p.UserID, Date: p.Date})
}
return out, nil
}
func messageReadDateErr(err error) error {
switch {
case errors.Is(err, domain.ErrMessageIDInvalid):
return messageIDInvalidErr()
case errors.Is(err, domain.ErrMessageNotReadYet):
return messageNotReadYetErr()
default:
return internalErr()
}
}
func forwardMessagesUnsupportedOptionErr(req *tg.MessagesForwardMessagesRequest) error {
switch {
case req.QuickReplyShortcut != nil:
return shortcutInvalidErr()
case req.Effect != 0:
return effectIDInvalidErr()
case req.VideoTimestamp != 0:
return mediaInvalidErr()
case req.AllowPaidStars < 0:
return starsAmountInvalidErr()
case req.AllowPaidFloodskip:
return paymentUnsupportedErr()
default:
return nil
}
}
func (r *Router) metrics() Metrics {
if r.deps.Metrics == nil {
return NopMetrics{}
}
return r.deps.Metrics
}
func (r *Router) domainFolderPeerFromInputPeer(ctx context.Context, userID int64, peer tg.InputPeerClass) (domain.Peer, int64, error) {
if inputPeerClassNil(peer) {
return domain.Peer{}, 0, peerIDInvalidErr()
}
switch p := peer.(type) {
case *tg.InputPeerUser:
return domain.Peer{Type: domain.PeerTypeUser, ID: p.UserID}, p.AccessHash, nil
case *tg.InputPeerChannel:
out := domain.Peer{Type: domain.PeerTypeChannel, ID: p.ChannelID}
if err := r.validateInputPeerChannelAccess(ctx, userID, peer, p.ChannelID); err != nil {
return domain.Peer{}, 0, err
}
return out, p.AccessHash, nil
case *tg.InputPeerChannelFromMessage:
out := domain.Peer{Type: domain.PeerTypeChannel, ID: p.ChannelID}
if p.ChannelID <= 0 {
return domain.Peer{}, 0, peerIDInvalidErr()
}
return out, 0, nil
case *tg.InputPeerChat:
if p.ChatID <= 0 {
return domain.Peer{}, 0, peerIDInvalidErr()
}
return domain.Peer{Type: domain.PeerTypeChannel, ID: p.ChatID}, 0, nil
case *tg.InputPeerSelf:
if userID == 0 {
return domain.Peer{}, 0, peerIDInvalidErr()
}
var accessHash int64
if r.deps.Users != nil {
if self, err := r.deps.Users.Self(ctx, userID); err == nil {
accessHash = self.AccessHash
}
}
return domain.Peer{Type: domain.PeerTypeUser, ID: userID}, accessHash, nil
default:
return domain.Peer{}, 0, peerIDInvalidErr()
}
}
func (r *Router) domainPeerFromInputPeer(userID int64, peer tg.InputPeerClass) (domain.Peer, bool) {
if inputPeerClassNil(peer) {
return domain.Peer{}, false
}
switch p := peer.(type) {
case *tg.InputPeerEmpty:
return domain.Peer{}, false
case *tg.InputPeerUser:
return domain.Peer{Type: domain.PeerTypeUser, ID: p.UserID}, true
case *tg.InputPeerChannel:
return domain.Peer{Type: domain.PeerTypeChannel, ID: p.ChannelID}, true
case *tg.InputPeerChannelFromMessage:
return domain.Peer{Type: domain.PeerTypeChannel, ID: p.ChannelID}, p.ChannelID > 0
case *tg.InputPeerChat:
return domain.Peer{Type: domain.PeerTypeChannel, ID: p.ChatID}, p.ChatID > 0
case *tg.InputPeerSelf:
if userID == 0 {
return domain.Peer{}, false
}
return domain.Peer{Type: domain.PeerTypeUser, ID: userID}, true
default:
return domain.Peer{}, false
}
}
func inputPeerClassNil(peer tg.InputPeerClass) bool {
switch typed := peer.(type) {
case nil:
return true
case *tg.InputPeerEmpty:
return typed == nil
case *tg.InputPeerSelf:
return typed == nil
case *tg.InputPeerChat:
return typed == nil
case *tg.InputPeerUser:
return typed == nil
case *tg.InputPeerChannel:
return typed == nil
case *tg.InputPeerUserFromMessage:
return typed == nil
case *tg.InputPeerChannelFromMessage:
return typed == nil
default:
return false
}
}
func isLegacyInputPeerChat(peer tg.InputPeerClass) bool {
typed, ok := peer.(*tg.InputPeerChat)
return ok && typed != nil
}
func inputPeerChannelRef(peer tg.InputPeerClass) (channelInputRef, bool) {
switch p := peer.(type) {
case *tg.InputPeerChannel:
if p == nil {
return channelInputRef{}, false
}
return channelInputRef{
ID: p.ChannelID,
AccessHash: p.AccessHash,
CheckAccessHash: p.AccessHash != 0,
}, p.ChannelID > 0
case *tg.InputPeerChannelFromMessage:
if p == nil {
return channelInputRef{}, false
}
return channelInputRef{ID: p.ChannelID}, p.ChannelID > 0
default:
return channelInputRef{}, false
}
}
func (r *Router) validateInputPeerChannelAccess(ctx context.Context, userID int64, peer tg.InputPeerClass, channelID int64) error {
ref, ok := inputPeerChannelRef(peer)
if !ok || ref.ID != channelID || channelID <= 0 {
return nil
}
if !ref.CheckAccessHash || r.deps.Channels == nil {
return nil
}
// 这里只校验 InputPeerChannel 的 access_hash不消费 dialog/read/unread/boost
// 等完整频道视图字段。走 ResolveChannel避免 messages.search/getPeerSettings
// 这类高频入口为一次纯 access check 触发完整 GetChannel 投影。
view, err := r.deps.Channels.ResolveChannel(ctx, userID, channelID)
if err != nil {
return channelInvalidErr(err)
}
if !inputChannelAccessHashMatches(ref, view.Channel) {
return channelInvalidErr(domain.ErrChannelPrivate)
}
return nil
}
func (r *Router) resolveInputPeerChannelView(ctx context.Context, userID int64, peer tg.InputPeerClass, channelID int64) (domain.ChannelView, error) {
if channelID <= 0 || r.deps.Channels == nil {
return domain.ChannelView{}, domain.ErrChannelInvalid
}
view, err := r.deps.Channels.ResolveChannel(ctx, userID, channelID)
if err != nil {
return domain.ChannelView{}, err
}
if ref, ok := inputPeerChannelRef(peer); ok {
if ref.ID != channelID || (ref.CheckAccessHash && !inputChannelAccessHashMatches(ref, view.Channel)) {
return domain.ChannelView{}, domain.ErrChannelPrivate
}
}
return view, nil
}
func (r *Router) checkedDomainPeerFromInputPeer(ctx context.Context, userID int64, peer tg.InputPeerClass) (domain.Peer, error) {
out, ok := r.domainPeerFromInputPeer(userID, peer)
if !ok || out.ID == 0 {
return domain.Peer{}, peerIDInvalidErr()
}
if out.Type == domain.PeerTypeChannel {
if err := r.validateInputPeerChannelAccess(ctx, userID, peer, out.ID); err != nil {
return domain.Peer{}, err
}
}
return out, nil
}
func (r *Router) chatsForInputPeer(ctx context.Context, userID int64, peer tg.InputPeerClass) []tg.ChatClass {
p, err := r.checkedDomainPeerFromInputPeer(ctx, userID, peer)
if err != nil || p.Type != domain.PeerTypeChannel || p.ID == 0 || r.deps.Channels == nil {
return []tg.ChatClass{}
}
view, err := r.deps.Channels.ResolveChannel(ctx, userID, p.ID)
if err != nil || view.Channel.ID == 0 {
return []tg.ChatClass{}
}
return []tg.ChatClass{tgChannelChatForView(userID, view)}
}