feat(premium): sync promo video catalog seed
This commit is contained in:
parent
c3c079edf3
commit
3369d0bd5c
14 changed files with 1137 additions and 10 deletions
|
|
@ -55,7 +55,6 @@ func rpcAllowedWithoutAuthorization(id uint32) bool {
|
|||
tg.HelpGetPeerProfileColorsRequestTypeID,
|
||||
tg.HelpGetPromoDataRequestTypeID,
|
||||
tg.HelpGetTermsOfServiceUpdateRequestTypeID,
|
||||
tg.HelpGetPremiumPromoRequestTypeID,
|
||||
tg.LangpackGetLanguagesRequestTypeID,
|
||||
tg.LangpackGetLanguageRequestTypeID,
|
||||
tg.LangpackGetLangPackRequestTypeID,
|
||||
|
|
|
|||
|
|
@ -924,6 +924,13 @@ type ModerationService interface {
|
|||
ReportAntiSpamFalsePositive(ctx context.Context, reporterUserID, channelID int64, messageID int, now time.Time) (domain.ModerationReport, bool, error)
|
||||
}
|
||||
|
||||
// PremiumPromoService exposes the immutable promo media catalog through a
|
||||
// domain-only boundary. File bytes remain served by upload.getFile through the
|
||||
// ordinary Files service.
|
||||
type PremiumPromoService interface {
|
||||
PremiumPromo(ctx context.Context) (domain.PremiumPromoCatalog, bool, error)
|
||||
}
|
||||
|
||||
// Deps 按业务域注入服务接口。各域的 handler 注册见对应文件(auth.go / users.go / updates.go)。
|
||||
type Deps struct {
|
||||
Auth AuthService
|
||||
|
|
@ -956,6 +963,7 @@ type Deps struct {
|
|||
Channels ChannelsService
|
||||
Communities CommunitiesService
|
||||
Files FilesService
|
||||
PremiumPromo PremiumPromoService
|
||||
Bots BotsService
|
||||
Polls PollsService
|
||||
Phone PhoneService
|
||||
|
|
|
|||
|
|
@ -176,11 +176,10 @@ func (r *Router) onHelpDismissSuggestion(ctx context.Context, req *tg.HelpDismis
|
|||
return androidcompat.DismissSuggestion(req.Suggestion), nil
|
||||
}
|
||||
|
||||
// onHelpGetPremiumPromo 返回最小真实的 Premium 状态页数据:状态文案按 viewer
|
||||
// 的会员有效期生成;videos/period_options 留空——购买入口已被 appConfig
|
||||
// premium_purchase_blocked=true 关闭,订阅价格 UI 不会消费这些字段(TDesktop
|
||||
// 空 period_options 仅隐藏价格按钮,DrKLO 回退到无价文案,均不报错)。
|
||||
// 六个字段全是 TL 必填项,空值也必须给出空集合而非缺失。
|
||||
// onHelpGetPremiumPromo returns the viewer-specific Premium status plus the
|
||||
// immutable, startup-seeded video catalog. Period options intentionally remain
|
||||
// empty: telesrv has no subscription purchase backend and must not advertise
|
||||
// dead payment URLs. All six TL fields are mandatory.
|
||||
func (r *Router) onHelpGetPremiumPromo(ctx context.Context) (*tg.HelpPremiumPromo, error) {
|
||||
promo := &tg.HelpPremiumPromo{
|
||||
StatusText: branding.PremiumName + " is not active on this account.",
|
||||
|
|
@ -190,17 +189,42 @@ func (r *Router) onHelpGetPremiumPromo(ctx context.Context) (*tg.HelpPremiumProm
|
|||
PeriodOptions: []tg.PremiumSubscriptionOption{},
|
||||
Users: []tg.UserClass{},
|
||||
}
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil || r.deps.Users == nil {
|
||||
return promo, nil
|
||||
userID, authorized, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
if !authorized || userID == 0 {
|
||||
return nil, authKeyUnregisteredErr()
|
||||
}
|
||||
if r.deps.Users == nil {
|
||||
return nil, notImplementedErr()
|
||||
}
|
||||
u, err := r.deps.Users.Self(ctx, userID)
|
||||
if err != nil {
|
||||
return promo, nil
|
||||
return nil, internalErr()
|
||||
}
|
||||
if u.ID != userID {
|
||||
return nil, internalErr()
|
||||
}
|
||||
if u.Bot {
|
||||
return nil, botMethodInvalidErr()
|
||||
}
|
||||
if u.PremiumActiveAt(r.clock.Now().Unix()) {
|
||||
until := time.Unix(int64(u.PremiumUntil), 0)
|
||||
promo.StatusText = branding.PremiumName + " is active until " + until.Format("2006-01-02") + "."
|
||||
}
|
||||
if r.deps.PremiumPromo != nil {
|
||||
catalog, found, err := r.deps.PremiumPromo.PremiumPromo(ctx)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
if found {
|
||||
if len(catalog.VideoSections) != len(catalog.Videos) {
|
||||
return nil, internalErr()
|
||||
}
|
||||
promo.VideoSections = append([]string(nil), catalog.VideoSections...)
|
||||
promo.Videos = tgDocuments(catalog.Videos)
|
||||
}
|
||||
}
|
||||
return promo, nil
|
||||
}
|
||||
|
|
|
|||
176
internal/rpc/help_premium_promo_test.go
Normal file
176
internal/rpc/help_premium_promo_test.go
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/iamxvbaba/td/tg"
|
||||
"github.com/iamxvbaba/td/tgerr"
|
||||
"github.com/iamxvbaba/td/tlprofile"
|
||||
"go.uber.org/zap/zaptest"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
type staticPremiumPromoService struct {
|
||||
catalog domain.PremiumPromoCatalog
|
||||
found bool
|
||||
err error
|
||||
}
|
||||
|
||||
func (s staticPremiumPromoService) PremiumPromo(context.Context) (domain.PremiumPromoCatalog, bool, error) {
|
||||
out := domain.PremiumPromoCatalog{
|
||||
VideoSections: append([]string(nil), s.catalog.VideoSections...),
|
||||
Videos: append([]domain.Document(nil), s.catalog.Videos...),
|
||||
}
|
||||
for i := range out.Videos {
|
||||
out.Videos[i].FileReference = append([]byte(nil), out.Videos[i].FileReference...)
|
||||
out.Videos[i].Attributes = append([]domain.DocumentAttribute(nil), out.Videos[i].Attributes...)
|
||||
out.Videos[i].Thumbs = append([]domain.PhotoSize(nil), out.Videos[i].Thumbs...)
|
||||
}
|
||||
return out, s.found, s.err
|
||||
}
|
||||
|
||||
func TestHelpGetPremiumPromoReturnsSeededCatalogAcrossExactProfiles(t *testing.T) {
|
||||
const userID int64 = 1000000001
|
||||
now := time.Date(2026, 7, 26, 8, 0, 0, 0, time.UTC)
|
||||
user := domain.User{
|
||||
ID: userID,
|
||||
AccessHash: 17,
|
||||
FirstName: "Alice",
|
||||
PremiumUntil: int(now.Add(48 * time.Hour).Unix()),
|
||||
}
|
||||
catalog := premiumPromoRPCTestCatalog()
|
||||
r := New(Config{}, Deps{
|
||||
Users: staticUsersService{user: user},
|
||||
PremiumPromo: staticPremiumPromoService{catalog: catalog, found: true},
|
||||
}, zaptest.NewLogger(t), fixedClock{now: now})
|
||||
ctx := WithUserID(context.Background(), userID)
|
||||
|
||||
for profile := tlprofile.Profile225; profile <= tlprofile.Profile228; profile++ {
|
||||
t.Run(fmt.Sprintf("layer_%d", profile), func(t *testing.T) {
|
||||
result, method := dispatchExactLayerRPCTest(t, r, ctx, profile, &tg.HelpGetPremiumPromoRequest{})
|
||||
if method != "help.getPremiumPromo" {
|
||||
t.Fatalf("method = %q", method)
|
||||
}
|
||||
promo, ok := dispatchCanonicalValue(result).(*tg.HelpPremiumPromo)
|
||||
if !ok {
|
||||
t.Fatalf("response = %T, want *tg.HelpPremiumPromo", dispatchCanonicalValue(result))
|
||||
}
|
||||
if len(promo.VideoSections) != 1 || promo.VideoSections[0] != "no_ads" || len(promo.Videos) != 1 {
|
||||
t.Fatalf("promo vectors = sections:%v videos:%d", promo.VideoSections, len(promo.Videos))
|
||||
}
|
||||
doc, ok := promo.Videos[0].(*tg.Document)
|
||||
if !ok {
|
||||
t.Fatalf("video = %T, want *tg.Document", promo.Videos[0])
|
||||
}
|
||||
if doc.ID != catalog.Videos[0].ID || doc.DCID != 2 || len(doc.Thumbs) != 1 {
|
||||
t.Fatalf("document = %+v", doc)
|
||||
}
|
||||
thumb, ok := doc.Thumbs[0].(*tg.PhotoSize)
|
||||
if !ok || thumb.Type != "m" || thumb.Size != 1234 {
|
||||
t.Fatalf("thumb = %#v", doc.Thumbs[0])
|
||||
}
|
||||
if len(promo.PeriodOptions) != 0 {
|
||||
t.Fatalf("period options = %+v, want no dead purchase entry", promo.PeriodOptions)
|
||||
}
|
||||
if !strings.Contains(promo.StatusText, "2026-07-28") {
|
||||
t.Fatalf("status text = %q, want viewer expiry", promo.StatusText)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelpGetPremiumPromoAuthorizationBotAndFallback(t *testing.T) {
|
||||
const userID int64 = 1000000001
|
||||
user := domain.User{ID: userID, AccessHash: 17, FirstName: "Alice"}
|
||||
r := New(Config{}, Deps{
|
||||
Users: staticUsersService{user: user},
|
||||
PremiumPromo: staticPremiumPromoService{},
|
||||
}, zaptest.NewLogger(t), fixedClock{now: time.Unix(1_700_000_000, 0)})
|
||||
|
||||
if rpcAllowedWithoutAuthorization(tg.HelpGetPremiumPromoRequestTypeID) {
|
||||
t.Fatal("help.getPremiumPromo must require a fully authorized user")
|
||||
}
|
||||
if _, err := r.onHelpGetPremiumPromo(context.Background()); !tgerr.Is(err, "AUTH_KEY_UNREGISTERED") {
|
||||
t.Fatalf("unauthorized error = %v, want AUTH_KEY_UNREGISTERED", err)
|
||||
}
|
||||
|
||||
promo, err := r.onHelpGetPremiumPromo(WithUserID(context.Background(), userID))
|
||||
if err != nil {
|
||||
t.Fatalf("fallback response: %v", err)
|
||||
}
|
||||
if len(promo.VideoSections) != 0 || len(promo.Videos) != 0 || len(promo.PeriodOptions) != 0 {
|
||||
t.Fatalf("fallback vectors = %+v", promo)
|
||||
}
|
||||
|
||||
botRouter := New(Config{}, Deps{
|
||||
Users: staticUsersService{user: domain.User{
|
||||
ID: userID,
|
||||
AccessHash: 19,
|
||||
FirstName: "PromoBot",
|
||||
Bot: true,
|
||||
}},
|
||||
}, zaptest.NewLogger(t), fixedClock{now: time.Unix(1_700_000_000, 0)})
|
||||
if _, err := botRouter.onHelpGetPremiumPromo(WithUserID(context.Background(), userID)); !tgerr.Is(err, "BOT_METHOD_INVALID") {
|
||||
t.Fatalf("bot error = %v, want BOT_METHOD_INVALID", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelpGetPremiumPromoResponsesDoNotShareMutableDocuments(t *testing.T) {
|
||||
const userID int64 = 1000000001
|
||||
catalog := premiumPromoRPCTestCatalog()
|
||||
r := New(Config{}, Deps{
|
||||
Users: staticUsersService{user: domain.User{ID: userID}},
|
||||
PremiumPromo: staticPremiumPromoService{catalog: catalog, found: true},
|
||||
}, zaptest.NewLogger(t), fixedClock{now: time.Unix(1_700_000_000, 0)})
|
||||
ctx := WithUserID(context.Background(), userID)
|
||||
|
||||
first, err := r.onHelpGetPremiumPromo(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
first.VideoSections[0] = "mutated"
|
||||
firstDoc := first.Videos[0].(*tg.Document)
|
||||
firstDoc.DCID = 99
|
||||
firstDoc.FileReference[0] ^= 0xff
|
||||
|
||||
second, err := r.onHelpGetPremiumPromo(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
secondDoc := second.Videos[0].(*tg.Document)
|
||||
if second.VideoSections[0] != "no_ads" || secondDoc.DCID != 2 || secondDoc.FileReference[0] != 0 {
|
||||
t.Fatalf("second response inherited mutation: sections=%v doc=%+v", second.VideoSections, secondDoc)
|
||||
}
|
||||
}
|
||||
|
||||
func premiumPromoRPCTestCatalog() domain.PremiumPromoCatalog {
|
||||
return domain.PremiumPromoCatalog{
|
||||
VideoSections: []string{"no_ads"},
|
||||
Videos: []domain.Document{{
|
||||
ID: 5814500255441357739,
|
||||
AccessHash: 5876417653416908580,
|
||||
FileReference: []byte{0, 1, 2, 3},
|
||||
Date: 1_654_006_663,
|
||||
MimeType: "video/mp4",
|
||||
Size: 2_650_178,
|
||||
DCID: 2,
|
||||
Attributes: []domain.DocumentAttribute{
|
||||
{Kind: domain.DocAttrFilename, FileName: "promo.mp4"},
|
||||
{Kind: domain.DocAttrVideo, W: 720, H: 1070, Duration: 5, SupportsStreaming: true},
|
||||
{Kind: domain.DocAttrAnimated},
|
||||
},
|
||||
Thumbs: []domain.PhotoSize{{
|
||||
Kind: domain.PhotoSizeKindDefault,
|
||||
Type: "m",
|
||||
W: 160,
|
||||
H: 240,
|
||||
Size: 1234,
|
||||
}},
|
||||
}},
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue