fix: sync support private chat content protection
This commit is contained in:
parent
74c9249091
commit
037ce017d4
25 changed files with 1395 additions and 36 deletions
|
|
@ -625,25 +625,6 @@ func (r *Router) enqueueChannelWallpaperFanout(ctx context.Context, originUserID
|
|||
})
|
||||
}
|
||||
|
||||
func (r *Router) onMessagesToggleNoForwards(ctx context.Context, req *tg.MessagesToggleNoForwardsRequest) (tg.UpdatesClass, error) {
|
||||
if r.deps.Channels == nil {
|
||||
return nil, notImplementedErr()
|
||||
}
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
channelID, err := r.channelIDFromLegacyInputPeerChecked(ctx, userID, req.Peer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
channel, err := r.deps.Channels.SetNoForwards(ctx, userID, channelID, req.Enabled)
|
||||
if err != nil {
|
||||
return nil, channelAdminErr(err)
|
||||
}
|
||||
return r.channelStateMutationUpdates(ctx, userID, channel), nil
|
||||
}
|
||||
|
||||
func (r *Router) onMessagesSetChatAvailableReactions(ctx context.Context, req *tg.MessagesSetChatAvailableReactionsRequest) (tg.UpdatesClass, error) {
|
||||
if r.deps.Channels == nil {
|
||||
return nil, notImplementedErr()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/iamxvbaba/td/tg"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
|
|
@ -145,6 +148,25 @@ func tgMessageServiceAction(msg domain.Message) tg.MessageActionClass {
|
|||
return &tg.MessageActionSetChatTheme{
|
||||
Theme: &tg.ChatTheme{Emoticon: m.ServiceAction.ChatThemeEmoticon},
|
||||
}
|
||||
case domain.MessageServiceActionNoForwardsToggle:
|
||||
action := m.ServiceAction.NoForwards
|
||||
if action == nil {
|
||||
return &tg.MessageActionEmpty{}
|
||||
}
|
||||
return &tg.MessageActionNoForwardsToggle{
|
||||
PrevValue: action.PrevValue,
|
||||
NewValue: action.NewValue,
|
||||
}
|
||||
case domain.MessageServiceActionNoForwardsRequest:
|
||||
action := m.ServiceAction.NoForwards
|
||||
if action == nil {
|
||||
return &tg.MessageActionEmpty{}
|
||||
}
|
||||
return &tg.MessageActionNoForwardsRequest{
|
||||
Expired: action.Expired || (action.ExpiresAt > 0 && int(time.Now().Unix()) >= action.ExpiresAt),
|
||||
PrevValue: action.PrevValue,
|
||||
NewValue: action.NewValue,
|
||||
}
|
||||
case domain.MessageServiceActionPhoneCall:
|
||||
if m.ServiceAction.Call == nil {
|
||||
return &tg.MessageActionEmpty{}
|
||||
|
|
|
|||
|
|
@ -604,6 +604,13 @@ type MessagesService interface {
|
|||
DeleteSavedHistory(ctx context.Context, userID int64, req domain.DeleteSavedHistoryRequest) (domain.DeleteSavedHistoryResult, error)
|
||||
}
|
||||
|
||||
// PrivateNoForwardsService is an optional messages capability used by the
|
||||
// private-user branch of messages.toggleNoForwards and userFull projection.
|
||||
type PrivateNoForwardsService interface {
|
||||
GetPrivateNoForwards(ctx context.Context, userID, peerUserID int64) (domain.PrivateNoForwardsState, error)
|
||||
TogglePrivateNoForwards(ctx context.Context, userID int64, req domain.TogglePrivateNoForwardsRequest) (domain.TogglePrivateNoForwardsResult, error)
|
||||
}
|
||||
|
||||
// TranslationService owns read-only translation and the durable per-account
|
||||
// peer preference. It only exposes domain values to the RPC edge.
|
||||
type TranslationService interface {
|
||||
|
|
|
|||
|
|
@ -297,6 +297,8 @@ func replyMessageIDInvalidErr() error { return tgerr.New(400, "REPLY_MESSAGE_ID_
|
|||
|
||||
func chatForwardsRestrictedErr() error { return tgerr.New(400, "CHAT_FORWARDS_RESTRICTED") }
|
||||
|
||||
func requestMsgExpiredErr() error { return tgerr.New(400, "REQUEST_MSG_EXPIRED") }
|
||||
|
||||
func inputRequestInvalidErr() error { return tgerr.New(400, "INPUT_REQUEST_INVALID") }
|
||||
|
||||
func inputRequestTooLongErr() error { return tgerr.New(400, "INPUT_REQUEST_TOO_LONG") }
|
||||
|
|
|
|||
|
|
@ -515,6 +515,15 @@ func (r *Router) forwardSourcesFromPrivateMessages(ctx context.Context, userID i
|
|||
if fromPeer.Type != domain.PeerTypeUser || fromPeer.ID == 0 {
|
||||
return nil, domain.ErrMessageIDInvalid
|
||||
}
|
||||
if svc, ok := r.deps.Messages.(PrivateNoForwardsService); ok {
|
||||
state, err := svc.GetPrivateNoForwards(ctx, userID, fromPeer.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if state.Enabled() {
|
||||
return nil, domain.ErrChatForwardsRestricted
|
||||
}
|
||||
}
|
||||
byID := make(map[int]domain.Message, len(messages))
|
||||
for _, msg := range messages {
|
||||
byID[msg.ID] = msg
|
||||
|
|
|
|||
144
internal/rpc/messages_no_forwards.go
Normal file
144
internal/rpc/messages_no_forwards.go
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
cryptorand "crypto/rand"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/iamxvbaba/td/tg"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
var privateNoForwardsRandomFallback atomic.Uint64
|
||||
|
||||
func (r *Router) onMessagesToggleNoForwards(ctx context.Context, req *tg.MessagesToggleNoForwardsRequest) (tg.UpdatesClass, error) {
|
||||
if req == nil {
|
||||
return nil, inputRequestInvalidErr()
|
||||
}
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if peer.Type == domain.PeerTypeChannel {
|
||||
if r.deps.Channels == nil {
|
||||
return nil, notImplementedErr()
|
||||
}
|
||||
channel, err := r.deps.Channels.SetNoForwards(ctx, userID, peer.ID, req.Enabled)
|
||||
if err != nil {
|
||||
return nil, channelAdminErr(err)
|
||||
}
|
||||
return r.channelStateMutationUpdates(ctx, userID, channel), nil
|
||||
}
|
||||
input, ok := req.Peer.(*tg.InputPeerUser)
|
||||
if !ok || input == nil || peer.Type != domain.PeerTypeUser || peer.ID == 0 || peer.ID == userID ||
|
||||
r.deps.Users == nil {
|
||||
return nil, peerIDInvalidErr()
|
||||
}
|
||||
if err := r.validateInputUser(ctx, &tg.InputUser{UserID: input.UserID, AccessHash: input.AccessHash}); err != nil {
|
||||
return nil, peerIDInvalidErr()
|
||||
}
|
||||
target, found, err := r.deps.Users.ByID(ctx, userID, peer.ID)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
if !found || target.Bot || target.Support || target.Deleted {
|
||||
return nil, peerIDInvalidErr()
|
||||
}
|
||||
self, err := r.deps.Users.Self(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
if self.Bot || self.Support || self.Deleted {
|
||||
return nil, peerIDInvalidErr()
|
||||
}
|
||||
svc, ok := r.deps.Messages.(PrivateNoForwardsService)
|
||||
if !ok {
|
||||
return nil, notImplementedErr()
|
||||
}
|
||||
requestMsgID, hasRequestMsgID := req.GetRequestMsgID()
|
||||
if hasRequestMsgID && (requestMsgID <= 0 || requestMsgID > domain.MaxMessageBoxID) {
|
||||
return nil, requestMsgExpiredErr()
|
||||
}
|
||||
if !hasRequestMsgID {
|
||||
requestMsgID = 0
|
||||
}
|
||||
current, err := svc.GetPrivateNoForwards(ctx, userID, peer.ID)
|
||||
if err != nil {
|
||||
return nil, privateNoForwardsErr(err)
|
||||
}
|
||||
// Premium is required only to create a new protected state. Disabling,
|
||||
// answering a request and no-op retries remain possible after expiry.
|
||||
if req.Enabled && requestMsgID == 0 && !current.Enabled() && !self.PremiumActiveAt(r.clock.Now().Unix()) {
|
||||
return nil, premiumAccountRequiredErr()
|
||||
}
|
||||
if requestMsgID != 0 || current.Enabled() != req.Enabled {
|
||||
if err := r.checkSendRateLimit(ctx, userID, 1); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
sessionID, _ := SessionIDFrom(ctx)
|
||||
result, err := svc.TogglePrivateNoForwards(ctx, userID, domain.TogglePrivateNoForwardsRequest{
|
||||
ActorUserID: userID,
|
||||
PeerUserID: peer.ID,
|
||||
Enabled: req.Enabled,
|
||||
RequestMsgID: requestMsgID,
|
||||
RandomID: newPrivateNoForwardsRandomID(),
|
||||
Date: int(r.clock.Now().Unix()),
|
||||
OriginAuthKeyID: rawAuthKeyIDForOrigin(ctx),
|
||||
OriginSessionID: sessionID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, privateNoForwardsErr(err)
|
||||
}
|
||||
if result.Changed {
|
||||
r.invalidateRPCProjectionForPeer(userID, peer)
|
||||
r.invalidateRPCProjectionForPeer(peer.ID, domain.Peer{Type: domain.PeerTypeUser, ID: userID})
|
||||
}
|
||||
if !result.Changed || result.Send.SenderMessage.ID == 0 {
|
||||
return tgEmptyUpdates(int(r.clock.Now().Unix())), nil
|
||||
}
|
||||
return tgPrivateMessageUpdates(
|
||||
result.Send.SenderEvent,
|
||||
result.Send.SenderMessage,
|
||||
0,
|
||||
false,
|
||||
r.usersForMessageUpdate(ctx, userID, result.Send.SenderMessage),
|
||||
[]tg.ChatClass{},
|
||||
), nil
|
||||
}
|
||||
|
||||
func privateNoForwardsErr(err error) error {
|
||||
switch {
|
||||
case errors.Is(err, domain.ErrNoForwardsRequestExpired),
|
||||
errors.Is(err, domain.ErrReplyMessageIDInvalid):
|
||||
return requestMsgExpiredErr()
|
||||
case errors.Is(err, domain.ErrMessageIDInvalid):
|
||||
return peerIDInvalidErr()
|
||||
case errors.Is(err, domain.ErrChatForwardsRestricted):
|
||||
return chatForwardsRestrictedErr()
|
||||
case errors.Is(err, domain.ErrUserFrozen):
|
||||
return frozenMethodInvalidErr()
|
||||
case errors.Is(err, domain.ErrMessageRandomIDDuplicate):
|
||||
return randomIDDuplicateErr()
|
||||
default:
|
||||
return internalErr()
|
||||
}
|
||||
}
|
||||
|
||||
func newPrivateNoForwardsRandomID() int64 {
|
||||
var raw [8]byte
|
||||
if _, err := cryptorand.Read(raw[:]); err == nil {
|
||||
if value := int64(binary.LittleEndian.Uint64(raw[:])); value != 0 {
|
||||
return value
|
||||
}
|
||||
}
|
||||
value := privateNoForwardsRandomFallback.Add(1)
|
||||
return int64(value | 1<<62)
|
||||
}
|
||||
167
internal/rpc/messages_no_forwards_rpc_test.go
Normal file
167
internal/rpc/messages_no_forwards_rpc_test.go
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/iamxvbaba/td/clock"
|
||||
"github.com/iamxvbaba/td/tg"
|
||||
"github.com/iamxvbaba/td/tgerr"
|
||||
"go.uber.org/zap/zaptest"
|
||||
|
||||
appdialogs "telesrv/internal/app/dialogs"
|
||||
appmessages "telesrv/internal/app/messages"
|
||||
appusers "telesrv/internal/app/users"
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store/memory"
|
||||
)
|
||||
|
||||
func TestMessagesToggleNoForwardsPrivateFullFlow(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
usersStore := memory.NewUserStore()
|
||||
alice, _ := usersStore.Create(ctx, domain.User{AccessHash: 5101, Phone: "15550005101", FirstName: "Alice"})
|
||||
bob, _ := usersStore.Create(ctx, domain.User{AccessHash: 5102, Phone: "15550005102", FirstName: "Bob"})
|
||||
if _, err := usersStore.SetPremiumUntil(ctx, alice.ID, int(time.Now().Add(time.Hour).Unix())); err != nil {
|
||||
t.Fatalf("grant alice premium: %v", err)
|
||||
}
|
||||
dialogsStore := memory.NewDialogStore()
|
||||
messagesStore := memory.NewMessageStore(dialogsStore)
|
||||
router := New(Config{}, Deps{
|
||||
Users: appusers.NewService(usersStore),
|
||||
Dialogs: appdialogs.NewService(dialogsStore),
|
||||
Messages: appmessages.NewService(messagesStore, dialogsStore),
|
||||
}, zaptest.NewLogger(t), clock.System)
|
||||
|
||||
enable, err := router.onMessagesToggleNoForwards(WithUserID(ctx, alice.ID), &tg.MessagesToggleNoForwardsRequest{
|
||||
Peer: &tg.InputPeerUser{UserID: bob.ID, AccessHash: bob.AccessHash}, Enabled: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("enable private noforwards: %v", err)
|
||||
}
|
||||
enableMessage := noForwardsServiceMessage(t, enable)
|
||||
if _, ok := enableMessage.Action.(*tg.MessageActionNoForwardsToggle); !ok {
|
||||
t.Fatalf("enable action = %T", enableMessage.Action)
|
||||
}
|
||||
assertNoForwardsFullFlags(t, router, ctx, alice, bob, true, false)
|
||||
assertNoForwardsFullFlags(t, router, ctx, bob, alice, false, true)
|
||||
source, err := messagesStore.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||||
SenderUserID: alice.ID, RecipientUserID: bob.ID, RandomID: 5199, Message: "protected source", Date: int(time.Now().Unix()),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("send protected source: %v", err)
|
||||
}
|
||||
if _, err := router.onMessagesForwardMessages(WithUserID(ctx, alice.ID), &tg.MessagesForwardMessagesRequest{
|
||||
FromPeer: &tg.InputPeerUser{UserID: bob.ID, AccessHash: bob.AccessHash},
|
||||
ID: []int{source.SenderMessage.ID},
|
||||
RandomID: []int64{5200},
|
||||
ToPeer: &tg.InputPeerSelf{},
|
||||
}); !tgerr.Is(err, "CHAT_FORWARDS_RESTRICTED") {
|
||||
t.Fatalf("forward protected private chat err=%v, want CHAT_FORWARDS_RESTRICTED", err)
|
||||
}
|
||||
|
||||
// The other party cannot steal ownership by setting enabled=true. This is a
|
||||
// no-op and does not require that party to be premium.
|
||||
noOp, err := router.onMessagesToggleNoForwards(WithUserID(ctx, bob.ID), &tg.MessagesToggleNoForwardsRequest{
|
||||
Peer: &tg.InputPeerUser{UserID: alice.ID, AccessHash: alice.AccessHash}, Enabled: true,
|
||||
})
|
||||
if err != nil || len(noOp.(*tg.Updates).Updates) != 0 {
|
||||
t.Fatalf("peer repeat enable = %#v err=%v, want empty no-op", noOp, err)
|
||||
}
|
||||
|
||||
requestUpdates, err := router.onMessagesToggleNoForwards(WithUserID(ctx, bob.ID), &tg.MessagesToggleNoForwardsRequest{
|
||||
Peer: &tg.InputPeerUser{UserID: alice.ID, AccessHash: alice.AccessHash}, Enabled: false,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("request sharing: %v", err)
|
||||
}
|
||||
requestMessage := noForwardsServiceMessage(t, requestUpdates)
|
||||
requestAction, ok := requestMessage.Action.(*tg.MessageActionNoForwardsRequest)
|
||||
if !ok || requestAction.Expired || !requestAction.PrevValue || requestAction.NewValue {
|
||||
t.Fatalf("request action = %#v", requestMessage.Action)
|
||||
}
|
||||
aliceHistory, err := messagesStore.ListByUser(ctx, alice.ID, domain.MessageFilter{
|
||||
HasPeer: true, Peer: domain.Peer{Type: domain.PeerTypeUser, ID: bob.ID}, Limit: 20,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var aliceRequestID int
|
||||
for _, msg := range aliceHistory.Messages {
|
||||
if msg.Media != nil && msg.Media.ServiceAction != nil &&
|
||||
msg.Media.ServiceAction.Kind == domain.MessageServiceActionNoForwardsRequest {
|
||||
aliceRequestID = msg.ID
|
||||
}
|
||||
}
|
||||
if aliceRequestID == 0 {
|
||||
t.Fatal("alice request box not found")
|
||||
}
|
||||
|
||||
answerReq := &tg.MessagesToggleNoForwardsRequest{
|
||||
Peer: &tg.InputPeerUser{UserID: bob.ID, AccessHash: bob.AccessHash},
|
||||
Enabled: false,
|
||||
}
|
||||
answerReq.SetRequestMsgID(aliceRequestID)
|
||||
answerUpdates, err := router.onMessagesToggleNoForwards(WithUserID(ctx, alice.ID), answerReq)
|
||||
if err != nil {
|
||||
t.Fatalf("accept sharing request: %v", err)
|
||||
}
|
||||
answerMessage := noForwardsServiceMessage(t, answerUpdates)
|
||||
answerAction, ok := answerMessage.Action.(*tg.MessageActionNoForwardsToggle)
|
||||
if !ok || !answerAction.PrevValue || answerAction.NewValue {
|
||||
t.Fatalf("answer action = %#v", answerMessage.Action)
|
||||
}
|
||||
if answerMessage.ReplyTo == nil {
|
||||
t.Fatal("answer service message has no reply_to")
|
||||
}
|
||||
assertNoForwardsFullFlags(t, router, ctx, alice, bob, false, false)
|
||||
assertNoForwardsFullFlags(t, router, ctx, bob, alice, false, false)
|
||||
|
||||
if _, err := router.onMessagesToggleNoForwards(WithUserID(ctx, alice.ID), answerReq); !tgerr.Is(err, "REQUEST_MSG_EXPIRED") {
|
||||
t.Fatalf("repeat request answer err=%v, want REQUEST_MSG_EXPIRED", err)
|
||||
}
|
||||
if _, err := router.onMessagesToggleNoForwards(WithUserID(ctx, bob.ID), &tg.MessagesToggleNoForwardsRequest{
|
||||
Peer: &tg.InputPeerUser{UserID: alice.ID, AccessHash: alice.AccessHash}, Enabled: true,
|
||||
}); !tgerr.Is(err, "PREMIUM_ACCOUNT_REQUIRED") {
|
||||
t.Fatalf("non-premium fresh enable err=%v, want PREMIUM_ACCOUNT_REQUIRED", err)
|
||||
}
|
||||
if _, err := router.onMessagesToggleNoForwards(WithUserID(ctx, alice.ID), &tg.MessagesToggleNoForwardsRequest{
|
||||
Peer: &tg.InputPeerUser{UserID: bob.ID, AccessHash: bob.AccessHash + 1}, Enabled: true,
|
||||
}); !tgerr.Is(err, "PEER_ID_INVALID") {
|
||||
t.Fatalf("wrong access hash err=%v, want PEER_ID_INVALID", err)
|
||||
}
|
||||
}
|
||||
|
||||
func noForwardsServiceMessage(t *testing.T, updates tg.UpdatesClass) *tg.MessageService {
|
||||
t.Helper()
|
||||
full, ok := updates.(*tg.Updates)
|
||||
if !ok || len(full.Updates) != 1 {
|
||||
t.Fatalf("updates = %#v, want one updateNewMessage", updates)
|
||||
}
|
||||
newMessage, ok := full.Updates[0].(*tg.UpdateNewMessage)
|
||||
if !ok || newMessage.Pts <= 0 || newMessage.PtsCount != 1 {
|
||||
t.Fatalf("update = %#v, want updateNewMessage pts_count=1", full.Updates[0])
|
||||
}
|
||||
service, ok := newMessage.Message.(*tg.MessageService)
|
||||
if !ok {
|
||||
t.Fatalf("message = %#v, want messageService (which has no message.noforwards field)", newMessage.Message)
|
||||
}
|
||||
return service
|
||||
}
|
||||
|
||||
func assertNoForwardsFullFlags(t *testing.T, router *Router, ctx context.Context, viewer, target domain.User, wantMy, wantPeer bool) {
|
||||
t.Helper()
|
||||
full, err := router.onUsersGetFullUser(WithUserID(ctx, viewer.ID), &tg.InputUser{
|
||||
UserID: target.ID, AccessHash: target.AccessHash,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("get full user %d->%d: %v", viewer.ID, target.ID, err)
|
||||
}
|
||||
if full.FullUser.GetNoforwardsMyEnabled() != wantMy ||
|
||||
full.FullUser.GetNoforwardsPeerEnabled() != wantPeer {
|
||||
t.Fatalf("full flags %d->%d my=%v peer=%v, want %v/%v",
|
||||
viewer.ID, target.ID,
|
||||
full.FullUser.GetNoforwardsMyEnabled(), full.FullUser.GetNoforwardsPeerEnabled(),
|
||||
wantMy, wantPeer)
|
||||
}
|
||||
}
|
||||
|
|
@ -265,6 +265,17 @@ func (r *Router) buildUserFullProjection(ctx context.Context, currentUserID int6
|
|||
Settings: tg.PeerSettings{},
|
||||
NotifySettings: *tdesktop.NotifySettings(),
|
||||
}
|
||||
if u.ID != currentUserID {
|
||||
if svc, ok := r.deps.Messages.(PrivateNoForwardsService); ok {
|
||||
state, err := svc.GetPrivateNoForwards(ctx, currentUserID, u.ID)
|
||||
if err != nil {
|
||||
return tg.UserFull{}, internalErr()
|
||||
}
|
||||
myEnabled, peerEnabled := state.ForViewer(currentUserID)
|
||||
full.SetNoforwardsMyEnabled(myEnabled)
|
||||
full.SetNoforwardsPeerEnabled(peerEnabled)
|
||||
}
|
||||
}
|
||||
// 通话入口:客户端不见 phone_calls_available=true 不显示通话按钮(P1 前置项)。
|
||||
// phone_calls_private 标记对端禁 P2P(p2p_allowed 真值在通话确认时另行计算)。
|
||||
if !u.Bot && u.ID != currentUserID {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue