owpengram-server/internal/rpc/messages_send_as.go

252 lines
8.9 KiB
Go
Raw 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"
"github.com/gotd/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
}