feat: sync monoforum and collectible emoji status
Sync telesrv bd15657 (feat(account): implement collectible emoji status). Skipped telesrv docs changes per public sync rules.
This commit is contained in:
parent
edb7057757
commit
0c99ae0a9d
91 changed files with 4061 additions and 693 deletions
|
|
@ -2,6 +2,7 @@ package rpc
|
|||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/iamxvbaba/td/bin"
|
||||
|
|
@ -10,6 +11,7 @@ import (
|
|||
"go.uber.org/zap/zaptest"
|
||||
|
||||
appchannels "telesrv/internal/app/channels"
|
||||
appdialogs "telesrv/internal/app/dialogs"
|
||||
appusers "telesrv/internal/app/users"
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store/memory"
|
||||
|
|
@ -17,7 +19,7 @@ import (
|
|||
|
||||
// TestMonoforumSavedDialogsAndHistory 验证频道私信(monoforum)读侧 RPC:管理员经
|
||||
// getSavedDialogs(parent_peer=monoforum) 看订阅者子会话列表、经 getSavedHistory 看某订阅者历史
|
||||
// (消息带 saved_peer_id);非管理员被拒。
|
||||
// (消息带 saved_peer_id);订阅者经普通 getHistory 只看自己的子会话。
|
||||
func TestMonoforumSavedDialogsAndHistory(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
userStore := memory.NewUserStore()
|
||||
|
|
@ -98,12 +100,31 @@ func TestMonoforumSavedDialogsAndHistory(t *testing.T) {
|
|||
if !seenChats[monoID] || !seenChats[created.Channel.ID] {
|
||||
t.Fatalf("main monoforum chats = %+v, want monoforum %d and parent %d", seenChats, monoID, created.Channel.ID)
|
||||
}
|
||||
var deniedRaw bin.Buffer
|
||||
if err := (&tg.MessagesGetHistoryRequest{Peer: monoInput, Limit: 20}).Encode(&deniedRaw); err != nil {
|
||||
var subscriberRaw bin.Buffer
|
||||
if err := (&tg.MessagesGetHistoryRequest{Peer: monoInput, Limit: 20}).Encode(&subscriberRaw); err != nil {
|
||||
t.Fatalf("encode non-admin getHistory(monoforum): %v", err)
|
||||
}
|
||||
if _, err := r.Dispatch(WithUserID(ctx, sub.ID), [8]byte{}, 0, &deniedRaw); err == nil {
|
||||
t.Fatalf("non-admin getHistory(monoforum) = nil err, want denied")
|
||||
subscriberEnc, err := r.Dispatch(WithUserID(ctx, sub.ID), [8]byte{}, 0, &subscriberRaw)
|
||||
if err != nil {
|
||||
t.Fatalf("non-admin getHistory(monoforum): %v", err)
|
||||
}
|
||||
subscriberHistory, ok := subscriberEnc.(*tg.MessagesChannelMessages)
|
||||
if !ok {
|
||||
t.Fatalf("non-admin getHistory(monoforum) = %T, want *tg.MessagesChannelMessages", subscriberEnc)
|
||||
}
|
||||
if len(subscriberHistory.Messages) != 1 {
|
||||
t.Fatalf("non-admin getHistory(monoforum) = %d msgs, want own sublist message", len(subscriberHistory.Messages))
|
||||
}
|
||||
subscriberMessage, ok := subscriberHistory.Messages[0].(*tg.Message)
|
||||
if !ok || subscriberMessage.Message != "hello channel" {
|
||||
t.Fatalf("non-admin history[0] = %#v, want own 'hello channel'", subscriberHistory.Messages[0])
|
||||
}
|
||||
subscriberSavedPeer, ok := subscriberMessage.GetSavedPeerID()
|
||||
if !ok {
|
||||
t.Fatalf("non-admin history message missing saved_peer_id")
|
||||
}
|
||||
if peer, ok := subscriberSavedPeer.(*tg.PeerUser); !ok || peer.UserID != sub.ID {
|
||||
t.Fatalf("non-admin history saved_peer_id = %#v, want self %d", subscriberSavedPeer, sub.ID)
|
||||
}
|
||||
|
||||
// 管理员看私信列表。
|
||||
|
|
@ -179,9 +200,9 @@ func TestMonoforumSavedDialogsAndHistory(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestMonoforumSendMessageWritePath 验证写侧:订阅者经 sendMessage(peer=monoforum,
|
||||
// reply_to=InputReplyToMonoForum{自己}) 发私信;管理员回复到目标订阅者;订阅者不能写他人子会话;
|
||||
// 普通发送(无 monoforum_peer_id)不受影响。
|
||||
// TestMonoforumSendMessageWritePath 验证写侧:订阅者按 TDesktop 实际请求仅以
|
||||
// peer=monoforum 发到自己的子会话;管理员必须显式指定目标订阅者;suggested_post 被持久化返回;
|
||||
// 订阅者不能写他人子会话。
|
||||
func TestMonoforumSendMessageWritePath(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
userStore := memory.NewUserStore()
|
||||
|
|
@ -196,39 +217,160 @@ func TestMonoforumSendMessageWritePath(t *testing.T) {
|
|||
|
||||
channelStore := memory.NewChannelStore()
|
||||
channelSvc := appchannels.NewService(channelStore)
|
||||
dialogSvc := appdialogs.NewService(memory.NewDialogStore(), channelStore)
|
||||
r := New(Config{}, Deps{
|
||||
Users: appusers.NewService(userStore),
|
||||
Channels: channelSvc,
|
||||
Dialogs: dialogSvc,
|
||||
}, zaptest.NewLogger(t), clock.System)
|
||||
|
||||
created, err := channelSvc.CreateChannel(ctx, owner.ID, domain.CreateChannelRequest{Title: "DM Broadcast", Broadcast: true, Date: 1000})
|
||||
if err != nil {
|
||||
t.Fatalf("create channel: %v", err)
|
||||
}
|
||||
enabled, err := channelStore.SetPaidMessagesPrice(ctx, owner.ID, created.Channel.ID, 0, true)
|
||||
enabled, err := channelStore.SetPaidMessagesPrice(ctx, owner.ID, created.Channel.ID, 10, true)
|
||||
if err != nil {
|
||||
t.Fatalf("enable DM: %v", err)
|
||||
}
|
||||
monoID := enabled.Channel.LinkedMonoforumID
|
||||
monoInput := &tg.InputPeerChannel{ChannelID: monoID}
|
||||
mono, err := channelStore.GetChannelByID(ctx, monoID)
|
||||
if err != nil {
|
||||
t.Fatalf("get monoforum: %v", err)
|
||||
}
|
||||
monoInput := &tg.InputPeerChannel{ChannelID: monoID, AccessHash: mono.AccessHash}
|
||||
monoChannelInput := &tg.InputChannel{ChannelID: monoID, AccessHash: mono.AccessHash}
|
||||
|
||||
// 订阅者发私信到自己的子会话。
|
||||
// 订阅者不是 monoforum 成员,但 TDesktop 打开会话时必须能读取 full channel shell。
|
||||
full, err := r.onChannelsGetFullChannel(WithUserID(ctx, sub.ID), monoChannelInput)
|
||||
if err != nil {
|
||||
t.Fatalf("subscriber getFullChannel(monoforum): %v", err)
|
||||
}
|
||||
if full == nil || full.FullChat == nil {
|
||||
t.Fatalf("subscriber getFullChannel(monoforum) = %#v, want full chat", full)
|
||||
}
|
||||
|
||||
// monoforum 永远不能通过 join 变成普通频道成员,否则会生成错误的 joined service message。
|
||||
if _, err := r.onChannelsJoinChannel(WithUserID(ctx, sub.ID), monoChannelInput); err == nil || !strings.Contains(err.Error(), "CHANNEL_MONOFORUM_UNSUPPORTED") {
|
||||
t.Fatalf("subscriber joinChannel(monoforum) err = %v, want CHANNEL_MONOFORUM_UNSUPPORTED", err)
|
||||
}
|
||||
|
||||
// TDesktop 在发送前保存相同 suggested_post 草稿;它必须可写、可恢复,不能变成 CHANNEL_PRIVATE。
|
||||
draftSuggested := tg.SuggestedPost{}
|
||||
draftSuggested.SetPrice(&tg.StarsAmount{Amount: 10})
|
||||
draftSuggested.SetScheduleDate(1_700_100_000)
|
||||
draftReq := &tg.MessagesSaveDraftRequest{Peer: monoInput, Message: "pending suggested post"}
|
||||
draftReq.SetSuggestedPost(draftSuggested)
|
||||
if ok, err := r.onMessagesSaveDraft(WithUserID(ctx, sub.ID), draftReq); err != nil || !ok {
|
||||
t.Fatalf("subscriber saveDraft(monoforum) = %v, %v; want true, nil", ok, err)
|
||||
}
|
||||
storedDraft, found, err := dialogSvc.GetDraft(ctx, sub.ID, domain.Peer{Type: domain.PeerTypeChannel, ID: monoID}, 0)
|
||||
if err != nil || !found {
|
||||
t.Fatalf("get persisted monoforum draft = %+v, %v, %v; want found", storedDraft, found, err)
|
||||
}
|
||||
if storedDraft.Message != "pending suggested post" || storedDraft.SuggestedPost == nil || storedDraft.SuggestedPost.Price == nil || storedDraft.SuggestedPost.Price.Amount != 10 || storedDraft.SuggestedPost.ScheduleDate != 1_700_100_000 {
|
||||
t.Fatalf("persisted monoforum draft = %+v, want suggested post content", storedDraft)
|
||||
}
|
||||
tooLow := &tg.MessagesSendMessageRequest{Peer: monoInput, Message: "under-authorized", RandomID: 554}
|
||||
tooLow.SetAllowPaidStars(9)
|
||||
if _, err := r.onMessagesSendMessage(WithUserID(ctx, sub.ID), tooLow); err == nil || !strings.Contains(err.Error(), "ALLOW_PAYMENT_REQUIRED") || !strings.Contains(err.Error(), "(10)") {
|
||||
t.Fatalf("under-authorized paid message err = %v, want ALLOW_PAYMENT_REQUIRED_10", err)
|
||||
}
|
||||
|
||||
// TDesktop 的订阅者请求不携带 InputReplyToMonoForum;服务端必须从调用者推导 saved_peer=self。
|
||||
subReq := &tg.MessagesSendMessageRequest{Peer: monoInput, Message: "hi from sub", RandomID: 555}
|
||||
subReq.SetReplyTo(&tg.InputReplyToMonoForum{MonoforumPeerID: &tg.InputPeerUser{UserID: sub.ID}})
|
||||
subReq.ClearDraft = true
|
||||
subReq.SetAllowPaidStars(20)
|
||||
suggestedInput := tg.SuggestedPost{}
|
||||
suggestedInput.SetPrice(&tg.StarsAmount{Amount: 10})
|
||||
suggestedInput.SetScheduleDate(1_700_100_000)
|
||||
subReq.SetSuggestedPost(suggestedInput)
|
||||
subUpd, err := r.onMessagesSendMessage(WithUserID(ctx, sub.ID), subReq)
|
||||
if err != nil {
|
||||
t.Fatalf("subscriber sendMessage(monoforum): %v", err)
|
||||
}
|
||||
if _, ok := subUpd.(*tg.Updates); !ok {
|
||||
subUpdates, ok := subUpd.(*tg.Updates)
|
||||
if !ok {
|
||||
t.Fatalf("subscriber send updates = %T, want *tg.Updates", subUpd)
|
||||
}
|
||||
var subMessageID int
|
||||
var subPaidStars, subBalance int64
|
||||
for _, update := range subUpdates.Updates {
|
||||
if newMessage, ok := update.(*tg.UpdateNewChannelMessage); ok {
|
||||
if message, ok := newMessage.Message.(*tg.Message); ok {
|
||||
subMessageID = message.ID
|
||||
subPaidStars, _ = message.GetPaidMessageStars()
|
||||
}
|
||||
}
|
||||
if balance, ok := update.(*tg.UpdateStarsBalance); ok {
|
||||
if amount, ok := balance.Balance.(*tg.StarsAmount); ok {
|
||||
subBalance = amount.Amount
|
||||
}
|
||||
}
|
||||
}
|
||||
if subMessageID == 0 || subPaidStars != 10 || subBalance != 990 {
|
||||
t.Fatalf("subscriber send updates id/paid/balance = %d/%d/%d, want id>0/10/990: %#v", subMessageID, subPaidStars, subBalance, subUpdates.Updates)
|
||||
}
|
||||
if _, found, err := dialogSvc.GetDraft(ctx, sub.ID, domain.Peer{Type: domain.PeerTypeChannel, ID: monoID}, 0); err != nil || found {
|
||||
t.Fatalf("clear_draft after paid send found/err = %v/%v, want false/nil", found, err)
|
||||
}
|
||||
duplicateUpd, err := r.onMessagesSendMessage(WithUserID(ctx, sub.ID), subReq)
|
||||
if err != nil {
|
||||
t.Fatalf("subscriber paid replay: %v", err)
|
||||
}
|
||||
duplicateUpdates, ok := duplicateUpd.(*tg.Updates)
|
||||
if !ok {
|
||||
t.Fatalf("subscriber paid replay = %T, want *tg.Updates", duplicateUpd)
|
||||
}
|
||||
var duplicateBalance int64
|
||||
for _, update := range duplicateUpdates.Updates {
|
||||
if balance, ok := update.(*tg.UpdateStarsBalance); ok {
|
||||
if amount, ok := balance.Balance.(*tg.StarsAmount); ok {
|
||||
duplicateBalance = amount.Amount
|
||||
}
|
||||
}
|
||||
}
|
||||
if duplicateBalance != 990 {
|
||||
t.Fatalf("subscriber paid replay balance = %d, want 990 without a second debit", duplicateBalance)
|
||||
}
|
||||
|
||||
// 管理员回复到该订阅者的子会话。
|
||||
// 管理员回复到该订阅者的子会话:同一个 inputReplyToMessage 同时携带真实 reply id
|
||||
// 和 monoforum target,两部分都必须保留。
|
||||
adminReq := &tg.MessagesSendMessageRequest{Peer: monoInput, Message: "admin reply", RandomID: 556}
|
||||
adminReq.SetReplyTo(&tg.InputReplyToMonoForum{MonoforumPeerID: &tg.InputPeerUser{UserID: sub.ID}})
|
||||
if _, err := r.onMessagesSendMessage(WithUserID(ctx, owner.ID), adminReq); err != nil {
|
||||
adminReply := &tg.InputReplyToMessage{ReplyToMsgID: subMessageID}
|
||||
adminReply.SetMonoforumPeerID(&tg.InputPeerUser{UserID: sub.ID})
|
||||
adminReq.SetReplyTo(adminReply)
|
||||
adminReq.SetAllowPaidStars(100)
|
||||
adminUpd, err := r.onMessagesSendMessage(WithUserID(ctx, owner.ID), adminReq)
|
||||
if err != nil {
|
||||
t.Fatalf("admin reply sendMessage(monoforum): %v", err)
|
||||
}
|
||||
if updates, ok := adminUpd.(*tg.Updates); ok {
|
||||
for _, update := range updates.Updates {
|
||||
if _, balance := update.(*tg.UpdateStarsBalance); balance {
|
||||
t.Fatalf("admin free reply emitted a balance debit: %#v", updates.Updates)
|
||||
}
|
||||
if newMessage, ok := update.(*tg.UpdateNewChannelMessage); ok {
|
||||
if message, ok := newMessage.Message.(*tg.Message); ok {
|
||||
if stars, paid := message.GetPaidMessageStars(); paid || stars != 0 {
|
||||
t.Fatalf("admin reply paid_message_stars = %d/%v, want 0/false", stars, paid)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 带媒体的 suggested post 必须走同一 monoforum 子会话,不能退化为普通频道消息或丢 flags。
|
||||
mediaSuggested := tg.SuggestedPost{}
|
||||
mediaSuggested.SetPrice(&tg.StarsAmount{Amount: 15})
|
||||
mediaReq := &tg.MessagesSendMediaRequest{
|
||||
Peer: monoInput, RandomID: 558, Message: "media suggestion",
|
||||
Media: &tg.InputMediaContact{PhoneNumber: "+15550003003", FirstName: "Media", LastName: "Contact", Vcard: ""},
|
||||
}
|
||||
mediaReq.SetSuggestedPost(mediaSuggested)
|
||||
mediaReq.SetAllowPaidStars(15)
|
||||
if _, err := r.onMessagesSendMedia(WithUserID(ctx, sub.ID), mediaReq); err != nil {
|
||||
t.Fatalf("subscriber sendMedia(monoforum): %v", err)
|
||||
}
|
||||
|
||||
// 订阅者不能写他人(owner)的子会话。
|
||||
sneaky := &tg.MessagesSendMessageRequest{Peer: monoInput, Message: "sneaky", RandomID: 557}
|
||||
|
|
@ -237,7 +379,7 @@ func TestMonoforumSendMessageWritePath(t *testing.T) {
|
|||
t.Fatalf("subscriber writing another's sublist = nil err, want REPLY_TO_MONOFORUM_PEER_INVALID")
|
||||
}
|
||||
|
||||
// 经管理员读历史:子会话含两条(订阅者发 + 管理员回复),倒序。
|
||||
// 经管理员读历史:子会话含三条(订阅者文本 + 管理员回复 + 订阅者媒体),倒序。
|
||||
hreq := &tg.MessagesGetSavedHistoryRequest{Peer: &tg.InputPeerUser{UserID: sub.ID}}
|
||||
hreq.SetParentPeer(monoInput)
|
||||
hres, err := r.onMessagesGetSavedHistory(WithUserID(ctx, owner.ID), hreq)
|
||||
|
|
@ -248,11 +390,54 @@ func TestMonoforumSendMessageWritePath(t *testing.T) {
|
|||
if !ok {
|
||||
t.Fatalf("getSavedHistory = %T, want *tg.MessagesMessagesSlice", hres)
|
||||
}
|
||||
if len(slice.Messages) != 2 {
|
||||
t.Fatalf("history = %d msgs, want 2 (sub + admin)", len(slice.Messages))
|
||||
if len(slice.Messages) != 3 {
|
||||
t.Fatalf("history = %d msgs, want 3 (sub text + admin + sub media)", len(slice.Messages))
|
||||
}
|
||||
top, ok := slice.Messages[0].(*tg.Message)
|
||||
if !ok || top.Message != "admin reply" {
|
||||
t.Fatalf("history[0] = %#v, want newest 'admin reply'", slice.Messages[0])
|
||||
if !ok || top.Message != "media suggestion" {
|
||||
t.Fatalf("history[0] = %#v, want newest media suggestion", slice.Messages[0])
|
||||
}
|
||||
if _, ok := top.Media.(*tg.MessageMediaContact); !ok {
|
||||
t.Fatalf("history[0] media = %T, want MessageMediaContact", top.Media)
|
||||
}
|
||||
if paid, ok := top.GetPaidMessageStars(); !ok || paid != 10 {
|
||||
t.Fatalf("media paid_message_stars = %d/%v, want actual configured price 10", paid, ok)
|
||||
}
|
||||
topSuggested, ok := top.GetSuggestedPost()
|
||||
if !ok {
|
||||
t.Fatalf("media message missing suggested_post")
|
||||
}
|
||||
topPrice, ok := topSuggested.GetPrice()
|
||||
if !ok {
|
||||
t.Fatalf("media suggested_post missing price")
|
||||
}
|
||||
if stars, ok := topPrice.(*tg.StarsAmount); !ok || stars.Amount != 15 {
|
||||
t.Fatalf("media suggested_post price = %#v, want 15 Stars", topPrice)
|
||||
}
|
||||
adminMessage, ok := slice.Messages[1].(*tg.Message)
|
||||
if !ok || adminMessage.Message != "admin reply" {
|
||||
t.Fatalf("history[1] = %#v, want admin reply", slice.Messages[1])
|
||||
}
|
||||
if header, ok := adminMessage.ReplyTo.(*tg.MessageReplyHeader); !ok || header.ReplyToMsgID != subMessageID {
|
||||
t.Fatalf("admin reply header = %#v, want reply_to_msg_id %d", adminMessage.ReplyTo, subMessageID)
|
||||
}
|
||||
suggestedMessage, ok := slice.Messages[2].(*tg.Message)
|
||||
if !ok {
|
||||
t.Fatalf("history[2] = %T, want *tg.Message", slice.Messages[2])
|
||||
}
|
||||
suggested, ok := suggestedMessage.GetSuggestedPost()
|
||||
if !ok {
|
||||
t.Fatalf("subscriber message missing suggested_post")
|
||||
}
|
||||
price, ok := suggested.GetPrice()
|
||||
if !ok {
|
||||
t.Fatalf("suggested_post missing price")
|
||||
}
|
||||
stars, ok := price.(*tg.StarsAmount)
|
||||
if !ok || stars.Amount != 10 {
|
||||
t.Fatalf("suggested_post price = %#v, want 10 Stars", price)
|
||||
}
|
||||
if scheduleDate, ok := suggested.GetScheduleDate(); !ok || scheduleDate != 1_700_100_000 {
|
||||
t.Fatalf("suggested_post schedule = %d/%v, want 1700100000/true", scheduleDate, ok)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue