feat: sync group call livestream support

This commit is contained in:
A 2026-07-06 14:31:19 +08:00
parent 56d995474c
commit f1a27996d3
37 changed files with 2219 additions and 83 deletions

View file

@ -0,0 +1,151 @@
package rpc
import (
"testing"
"github.com/gotd/td/tg"
)
// TestScheduledGroupCallLifecycle 覆盖定时通话闭环:create(schedule_date) →
// scheduled 服务消息 + groupCall.schedule_date → 订阅开播提醒(per-viewer flag)→
// startScheduledGroupCall 清 schedule_date + started 服务消息 → join 正常入会。
func TestScheduledGroupCallLifecycle(t *testing.T) {
f := newGroupCallFixture(t)
ownerCtx := f.userCtx(f.owner, 11)
memberCtx := f.userCtx(f.member, 22)
scheduleDate := int(f.clk.Now().Unix()) + 3600
// --- 过去时间拒绝 ---
pastReq := &tg.PhoneCreateGroupCallRequest{
Peer: &tg.InputPeerChannel{ChannelID: f.channel.ID, AccessHash: f.channel.AccessHash},
RandomID: 1,
}
pastReq.SetScheduleDate(int(f.clk.Now().Unix()) - 10)
if _, err := f.router.onPhoneCreateGroupCall(ownerCtx, pastReq); err == nil {
t.Fatalf("past schedule_date accepted")
} else {
assertPhoneRPCErr(t, err, "SCHEDULE_DATE_INVALID")
}
// --- create scheduled ---
createReq := &tg.PhoneCreateGroupCallRequest{
Peer: &tg.InputPeerChannel{ChannelID: f.channel.ID, AccessHash: f.channel.AccessHash},
RandomID: 2,
}
createReq.SetScheduleDate(scheduleDate)
createRes, err := f.router.onPhoneCreateGroupCall(ownerCtx, createReq)
if err != nil {
t.Fatalf("create scheduled group call: %v", err)
}
call := findUpdate[*tg.UpdateGroupCall](t, createRes).Call.(*tg.GroupCall)
if got, ok := call.GetScheduleDate(); !ok || got != scheduleDate {
t.Fatalf("groupCall.schedule_date = %d ok=%v, want %d", got, ok, scheduleDate)
}
// scheduled 服务消息。
msgUpdate := findUpdate[*tg.UpdateNewChannelMessage](t, createRes)
svc, ok := msgUpdate.Message.(*tg.MessageService)
if !ok {
t.Fatalf("create response message = %T, want MessageService", msgUpdate.Message)
}
scheduledAction, ok := svc.Action.(*tg.MessageActionGroupCallScheduled)
if !ok {
t.Fatalf("service action = %T, want MessageActionGroupCallScheduled", svc.Action)
}
if scheduledAction.ScheduleDate != scheduleDate {
t.Fatalf("service action schedule_date = %d, want %d", scheduledAction.ScheduleDate, scheduleDate)
}
input := &tg.InputGroupCall{ID: call.ID, AccessHash: call.AccessHash}
// --- member 订阅开播提醒 ---
subRes, err := f.router.onPhoneToggleGroupCallStartSubscription(memberCtx, &tg.PhoneToggleGroupCallStartSubscriptionRequest{
Call: input, Subscribed: true,
})
if err != nil {
t.Fatalf("toggle start subscription: %v", err)
}
subCall := findUpdate[*tg.UpdateGroupCall](t, subRes).Call.(*tg.GroupCall)
if !subCall.ScheduleStartSubscribed {
t.Fatalf("subscription response missing schedule_start_subscribed")
}
// getGroupCall 回填 per-viewer flag:member 已订阅、owner 未订阅。
memberView, err := f.router.onPhoneGetGroupCall(memberCtx, &tg.PhoneGetGroupCallRequest{Call: input, Limit: 10})
if err != nil {
t.Fatalf("member getGroupCall: %v", err)
}
if !memberView.Call.(*tg.GroupCall).ScheduleStartSubscribed {
t.Fatalf("member getGroupCall missing subscribed flag")
}
ownerView, err := f.router.onPhoneGetGroupCall(ownerCtx, &tg.PhoneGetGroupCallRequest{Call: input, Limit: 10})
if err != nil {
t.Fatalf("owner getGroupCall: %v", err)
}
if ownerView.Call.(*tg.GroupCall).ScheduleStartSubscribed {
t.Fatalf("owner getGroupCall unexpectedly subscribed")
}
// --- 非管理员不能开播 ---
if _, err := f.router.onPhoneStartScheduledGroupCall(memberCtx, input); err == nil {
t.Fatalf("non-admin started scheduled call")
} else {
assertPhoneRPCErr(t, err, "CHAT_ADMIN_REQUIRED")
}
// --- start ---
f.sessions.reset()
startRes, err := f.router.onPhoneStartScheduledGroupCall(ownerCtx, input)
if err != nil {
t.Fatalf("startScheduledGroupCall: %v", err)
}
started := findUpdate[*tg.UpdateGroupCall](t, startRes).Call.(*tg.GroupCall)
if _, ok := started.GetScheduleDate(); ok {
t.Fatalf("started call still has schedule_date")
}
// started 服务消息(messageActionGroupCall 无 duration)。
startMsg := findUpdate[*tg.UpdateNewChannelMessage](t, startRes)
startSvc := startMsg.Message.(*tg.MessageService)
if _, ok := startSvc.Action.(*tg.MessageActionGroupCall); !ok {
t.Fatalf("start service action = %T, want MessageActionGroupCall", startSvc.Action)
}
// 在线成员收到 schedule_date 已清的 updateGroupCall(客户端据此自动入会)。
memberGotStart := false
for _, rec := range f.sessions.records() {
if rec.userID != f.member.ID {
continue
}
if box, ok := rec.msg.(*tg.Updates); ok {
for _, u := range box.Updates {
if gc, ok := u.(*tg.UpdateGroupCall); ok {
if call, ok := gc.Call.(*tg.GroupCall); ok {
if _, has := call.GetScheduleDate(); !has {
memberGotStart = true
}
}
}
}
}
}
if !memberGotStart {
t.Fatalf("member did not receive started updateGroupCall: %+v", f.sessions.records())
}
// --- 幂等重复 start ---
if _, err := f.router.onPhoneStartScheduledGroupCall(ownerCtx, input); err != nil {
t.Fatalf("idempotent re-start: %v", err)
}
// --- 已开始后订阅提醒非法 ---
if _, err := f.router.onPhoneToggleGroupCallStartSubscription(memberCtx, &tg.PhoneToggleGroupCallStartSubscriptionRequest{
Call: input, Subscribed: true,
}); err == nil {
t.Fatalf("subscription toggle allowed after start")
}
// --- start 后正常 join ---
if _, err := f.router.onPhoneJoinGroupCall(memberCtx, &tg.PhoneJoinGroupCallRequest{
Call: input,
JoinAs: &tg.InputPeerSelf{},
Params: groupCallJoinParams(t, 7001),
}); err != nil {
t.Fatalf("join after start: %v", err)
}
}