package rpc import ( "context" "github.com/iamxvbaba/td/tg" "telesrv/internal/domain" ) func (r *Router) forumSendAsPeer(ctx context.Context, peerInput, sendAsInput tg.InputPeerClass) (*domain.Peer, error) { peer, err := r.forumTopicPeer(ctx, peerInput) if err != nil { return nil, err } userID, _, err := r.currentUserID(ctx) if err != nil { return nil, internalErr() } // 复用统一的 send-as 校验:自己→nil(以自己发言),当前频道/群→匿名/广播自帖, // 自己拥有的其它广播频道→个人频道(需会员)。forum topic 与普通消息走同一套资格判定。 return r.sendAsPeerFromInput(ctx, userID, peer, sendAsInput) } func (r *Router) onMessagesSaveDefaultSendAs(ctx context.Context, req *tg.MessagesSaveDefaultSendAsRequest) (bool, error) { userID, _, err := r.currentUserID(ctx) if err != nil { return false, internalErr() } if userID == 0 { return false, peerIDInvalidErr() } peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer) if err != nil { return false, err } if peer.Type != domain.PeerTypeChannel { return false, peerIDInvalidErr() } sendAs, err := r.sendAsPeerFromInput(ctx, userID, peer, req.SendAs) if err != nil { return false, err } if r.deps.Channels == nil { return false, channelInvalidErr(domain.ErrChannelInvalid) } if _, err := r.deps.Channels.SaveDefaultSendAs(ctx, userID, domain.SaveChannelDefaultSendAsRequest{ UserID: userID, ChannelID: peer.ID, SendAs: sendAs, }); err != nil { return false, channelInvalidErr(err) } r.invalidateRPCProjectionForPeer(userID, peer) return true, nil } func (r *Router) sendAsPeerFromInput(ctx context.Context, userID int64, to domain.Peer, input tg.InputPeerClass) (*domain.Peer, error) { if input == nil { return nil, nil } if to.Type != domain.PeerTypeChannel { return nil, sendAsPeerInvalidErr() } sendAs, err := r.checkedDomainPeerFromInputPeer(ctx, userID, input) if err != nil { return nil, sendAsPeerInvalidErr() } switch sendAs.Type { case domain.PeerTypeUser: if sendAs.ID != userID { return nil, sendAsPeerInvalidErr() } return nil, nil case domain.PeerTypeChannel: if sendAs.ID == to.ID { // 以当前频道/群本身发言(广播自帖、匿名管理员)。 if err := r.validateCurrentChannelSendAs(ctx, userID, to.ID); err != nil { return nil, err } } else { // 以自己拥有的其它广播频道身份在本群发言(个人频道需会员)。 if err := r.validateForeignChannelSendAs(ctx, userID, to.ID, sendAs.ID); err != nil { return nil, err } } out := sendAs return &out, nil default: return nil, sendAsPeerInvalidErr() } } // validateForeignChannelSendAs 校验「以自己拥有的另一个(广播)频道身份在 toChannel 里发言」: // 必须是该频道的 creator 或有 PostMessages 权的 admin;个人频道(非 toChannel 的关联讨论频道) // 需有效会员。gotd 错误集无 CHAT_SEND_AS_DENIED,故一律收敛为 SEND_AS_PEER_INVALID(客户端在 // 选择阶段已按 premium_required 拦截,此为服务端兜底)。 func (r *Router) validateForeignChannelSendAs(ctx context.Context, userID, toChannelID, sendAsChannelID int64) error { if r.deps.Channels == nil { return sendAsPeerInvalidErr() } view, err := r.deps.Channels.ResolveChannel(ctx, userID, sendAsChannelID) if err != nil || !canSendAsForeignChannel(view) { return sendAsPeerInvalidErr() } // 接受 iff 会员 或 该频道是本群关联讨论频道(免会员)。先查会员(带 user:base 缓存、且注册默认赠送 // 故多数命中 true)即短路,省掉 linked 判定那次 ResolveChannel(toChannel)。 if !r.userIsPremium(ctx, userID) && !r.foreignSendAsIsLinkedChannel(ctx, userID, toChannelID, sendAsChannelID) { return sendAsPeerInvalidErr() } return nil } // foreignSendAsIsLinkedChannel 报告 sendAsChannel 是否是 toChannel 的关联讨论频道(该情形免会员, // 对齐官方:在自己频道的讨论组里以频道身份发言不需要会员)。 func (r *Router) foreignSendAsIsLinkedChannel(ctx context.Context, userID, toChannelID, sendAsChannelID int64) bool { if r.deps.Channels == nil { return false } toView, err := r.deps.Channels.ResolveChannel(ctx, userID, toChannelID) if err != nil { return false } return toView.Channel.LinkedChatID != 0 && toView.Channel.LinkedChatID == sendAsChannelID } // canSendAsForeignChannel 判定 view 描述的频道能否被当前用户「作为身份」在别的群里发言: // 仅限广播频道,且用户是 creator 或持 PostMessages 权的 admin(以自己拥有的 megagroup 身份发言 // 不是真实的 Telegram 能力,故排除)。 func canSendAsForeignChannel(view domain.ChannelView) bool { if !view.Channel.Broadcast { return false } if view.Self.Role == domain.ChannelRoleCreator { return true } if view.Self.Role == domain.ChannelRoleAdmin && view.Self.AdminRights.PostMessages { return true } return false } // userIsPremium 报告用户当前是否为有效会员(PremiumActiveAt 即时派生)。 func (r *Router) userIsPremium(ctx context.Context, userID int64) bool { if r.deps.Users == nil || userID == 0 { return false } u, err := r.deps.Users.Self(ctx, userID) if err != nil { return false } return u.PremiumActiveAt(r.clock.Now().Unix()) } func (r *Router) resolveSendAsPeer(ctx context.Context, userID int64, to domain.Peer, input tg.InputPeerClass) (*domain.Peer, error) { if input != nil { return r.sendAsPeerFromInput(ctx, userID, to, input) } if to.Type != domain.PeerTypeChannel || r.deps.Channels == nil { return nil, nil } view, err := r.deps.Channels.GetChannel(ctx, userID, to.ID) if err != nil { return nil, nil } return r.effectiveDefaultSendAs(ctx, userID, view), nil } // effectiveDefaultSendAs 解析某频道会话保存的默认 send-as(无显式 send_as 时用)。每次发送都重新 // 校验:当前频道默认按 canCurrentChannelSendAs,外部频道默认按 validateForeignChannelSendAs;失效的 // 默认(被降权 / 不再拥有 / 会员过期)静默回落为「以自己发言」(nil),对齐官方客户端的 reload-on-400。 func (r *Router) effectiveDefaultSendAs(ctx context.Context, userID int64, view domain.ChannelView) *domain.Peer { def := view.Dialog.DefaultSendAs if def == nil || def.ID == 0 || def.Type != domain.PeerTypeChannel { return nil } if def.ID == view.Channel.ID { if !canCurrentChannelSendAs(view) { return nil } out := *def return &out } if err := r.validateForeignChannelSendAs(ctx, userID, view.Channel.ID, def.ID); err != nil { return nil } out := *def return &out } // applyForeignDefaultSendAsToFull 在 getFullChannel 投影里补上「外部频道默认 send-as」:当某会话保存的 // 默认是用户自己拥有的另一个广播频道时,设置 channelFull.default_send_as 并把该频道对象放进 Chats // (否则客户端无法渲染默认 chip)。当前频道默认由 tgChannelFull 处理,此处只管外部频道;按所有权(而非 // 会员)投影——会员到期等情形由发送侧 effectiveDefaultSendAs 兜底回落为自己。 func (r *Router) applyForeignDefaultSendAsToFull(ctx context.Context, userID int64, view domain.ChannelView, full *tg.ChannelFull, chats *[]tg.ChatClass) { def := view.Dialog.DefaultSendAs if def == nil || def.Type != domain.PeerTypeChannel || def.ID == 0 || def.ID == view.Channel.ID { return } if r.deps.Channels == nil { return } sendAsView, err := r.deps.Channels.ResolveChannel(ctx, userID, def.ID) if err != nil || !canSendAsForeignChannel(sendAsView) { return } full.SetDefaultSendAs(&tg.PeerChannel{ChannelID: def.ID}) *chats = appendUniqueTGChats(*chats, tgChannels(userID, []domain.Channel{sendAsView.Channel})...) } func validDefaultSendAsPeer(view domain.ChannelView) *domain.Peer { if view.Dialog.DefaultSendAs == nil || view.Dialog.DefaultSendAs.ID == 0 { return nil } switch view.Dialog.DefaultSendAs.Type { case domain.PeerTypeUser: return nil case domain.PeerTypeChannel: if view.Dialog.DefaultSendAs.ID != view.Channel.ID || !canCurrentChannelSendAs(view) { return nil } out := *view.Dialog.DefaultSendAs return &out default: return nil } } func (r *Router) validateCurrentChannelSendAs(ctx context.Context, userID, channelID int64) error { if r.deps.Channels == nil { return sendAsPeerInvalidErr() } view, err := r.deps.Channels.ResolveChannel(ctx, userID, channelID) if err != nil { return sendAsPeerInvalidErr() } if canCurrentChannelSendAs(view) { return nil } return sendAsPeerInvalidErr() } func canCurrentChannelSendAs(view domain.ChannelView) bool { if view.Self.Role == domain.ChannelRoleCreator { return true } if view.Channel.Broadcast && view.Self.Role == domain.ChannelRoleAdmin && view.Self.AdminRights.PostMessages { return true } if !view.Channel.Broadcast && view.Self.Role == domain.ChannelRoleAdmin && view.Self.AdminRights.Anonymous { return true } return false }