fix(admin): resolve moderation, gift, and admin-panel bugs from QA
- scam/fake: enforce mutual exclusivity (scam precedence) at TL conversion, admin command, and UI toggles; both flags could render as neither - scam/fake flicker: persist scam/fake in the Redis user base cache so cache hits no longer drop the flags - getSavedStarGifts: hidden (unsaved) gifts are now force-excluded for non-owner viewers; same guard for get-by-ref - convertStarGift: map already-upgraded/owner-invalid/unavailable to a clean client error instead of 500 - channel force-settings: send only changed fields (partial patch) and re-sync toggles after reload, so one setting no longer resets another - give gifts: removed custom collectible numbers (auto sequential only); locked sender to the system account 777000 - give gifts UI: fixed picker card spacing/right gap; static (hover-play) Lottie previews on emoji + picker to stop render lag; fixed emoji ID field overflow
This commit is contained in:
parent
313624eab2
commit
90792cdfab
22 changed files with 214 additions and 135 deletions
|
|
@ -358,7 +358,6 @@ type GiveGiftRequest struct {
|
|||
ModelAttributeID int64 `json:"model_attribute_id"`
|
||||
PatternAttributeID int64 `json:"pattern_attribute_id"`
|
||||
BackdropAttributeID int64 `json:"backdrop_attribute_id"`
|
||||
Num int `json:"num"`
|
||||
}
|
||||
|
||||
type StarGiftCollectibleAnimationUpload struct {
|
||||
|
|
@ -871,6 +870,11 @@ func (s *Service) SetUserFlags(ctx context.Context, req SetUserFlagsRequest) (Co
|
|||
if s == nil || s.users == nil {
|
||||
return CommandResult{}, fmt.Errorf("admin user dependency is not configured")
|
||||
}
|
||||
// scam and fake are mutually exclusive (a peer is never both in Telegram).
|
||||
// scam takes precedence so the two never persist together.
|
||||
if req.Scam {
|
||||
req.Fake = false
|
||||
}
|
||||
return s.runCommand(ctx, req.CommandMeta, ActionSetUserFlags, req.UserID, domain.Peer{}, req, func() (CommandResult, error) {
|
||||
u, found, err := s.users.AdminUser(ctx, req.UserID)
|
||||
if err != nil {
|
||||
|
|
@ -967,8 +971,8 @@ func (s *Service) GiveGift(ctx context.Context, req GiveGiftRequest) (CommandRes
|
|||
if req.Upgrade && recipient.Type != domain.PeerTypeUser {
|
||||
return CommandResult{}, fmt.Errorf("upgraded gift delivery is supported for user recipients only")
|
||||
}
|
||||
if !req.Upgrade && (req.ModelAttributeID > 0 || req.PatternAttributeID > 0 || req.BackdropAttributeID > 0 || req.Num > 0) {
|
||||
return CommandResult{}, fmt.Errorf("collectible attributes and number require upgrade")
|
||||
if !req.Upgrade && (req.ModelAttributeID > 0 || req.PatternAttributeID > 0 || req.BackdropAttributeID > 0) {
|
||||
return CommandResult{}, fmt.Errorf("collectible attributes require upgrade")
|
||||
}
|
||||
return s.runCommand(ctx, req.CommandMeta, ActionGiveGift, req.UserID, recipient, req, func() (CommandResult, error) {
|
||||
details := map[string]any{
|
||||
|
|
@ -1012,9 +1016,6 @@ func (s *Service) GiveGift(ctx context.Context, req GiveGiftRequest) (CommandRes
|
|||
if req.BackdropAttributeID > 0 && !collectibleAttrPresent(preview.Backdrops, req.BackdropAttributeID) {
|
||||
return CommandResult{}, fmt.Errorf("backdrop attribute %d is not part of gift %d", req.BackdropAttributeID, req.GiftID)
|
||||
}
|
||||
if req.Num > 0 && req.Num > preview.SupplyTotal {
|
||||
return CommandResult{}, fmt.Errorf("number %d exceeds collectible supply %d", req.Num, preview.SupplyTotal)
|
||||
}
|
||||
details["collectible_supply_total"] = preview.SupplyTotal
|
||||
details["collectible_issued"] = preview.Issued
|
||||
if req.ModelAttributeID > 0 {
|
||||
|
|
@ -1026,9 +1027,6 @@ func (s *Service) GiveGift(ctx context.Context, req GiveGiftRequest) (CommandRes
|
|||
if req.BackdropAttributeID > 0 {
|
||||
details["backdrop_attribute_id"] = req.BackdropAttributeID
|
||||
}
|
||||
if req.Num > 0 {
|
||||
details["num"] = req.Num
|
||||
}
|
||||
}
|
||||
}
|
||||
if req.DryRun {
|
||||
|
|
@ -1044,7 +1042,6 @@ func (s *Service) GiveGift(ctx context.Context, req GiveGiftRequest) (CommandRes
|
|||
ModelAttributeID: req.ModelAttributeID,
|
||||
PatternAttributeID: req.PatternAttributeID,
|
||||
BackdropAttributeID: req.BackdropAttributeID,
|
||||
Num: req.Num,
|
||||
}); err != nil {
|
||||
return CommandResult{}, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -328,21 +328,21 @@ type StarGiftUpgradeRequest struct {
|
|||
OriginAuthKeyID [8]byte
|
||||
OriginSessionID int64
|
||||
|
||||
// Admin-controlled minting overrides. When non-zero these pin the specific
|
||||
// collectible attributes / number instead of the random pool draw and the
|
||||
// sequential issued+1 number. They are only honoured on the admin grant path;
|
||||
// the DB FK (attribute must belong to the revision) and UNIQUE(gift_id,num)
|
||||
// constraints remain the source of truth.
|
||||
// Admin-controlled attribute overrides. When non-zero these pin the specific
|
||||
// collectible model/pattern/backdrop instead of the random pool draw. They
|
||||
// are only honoured on the admin grant path; the DB FK (attribute must belong
|
||||
// to the revision) remains the source of truth. The collectible number is
|
||||
// always assigned automatically (sequential).
|
||||
ModelAttributeID int64
|
||||
PatternAttributeID int64
|
||||
BackdropAttributeID int64
|
||||
Num int
|
||||
}
|
||||
|
||||
// AdminStarGiftGrant is one admin "give gift" command: deliver GiftID to
|
||||
// Recipient from Sender (0 => official system account 777000) at no charge.
|
||||
// When Upgrade is set the gift is minted as a collectible; the optional
|
||||
// attribute IDs / Num pin specific collectible facts (0 => random/auto).
|
||||
// attribute IDs pin specific model/pattern/backdrop (0 => random). The
|
||||
// collectible number is always assigned automatically.
|
||||
type AdminStarGiftGrant struct {
|
||||
SenderID int64
|
||||
Recipient Peer
|
||||
|
|
@ -353,7 +353,6 @@ type AdminStarGiftGrant struct {
|
|||
ModelAttributeID int64
|
||||
PatternAttributeID int64
|
||||
BackdropAttributeID int64
|
||||
Num int
|
||||
}
|
||||
|
||||
type StarGiftPurchaseRequest struct {
|
||||
|
|
@ -927,7 +926,6 @@ var (
|
|||
ErrStarGiftAlreadyUpgraded = errors.New("stargift: already upgraded")
|
||||
ErrStarGiftCollectibleSoldOut = errors.New("stargift: collectible supply exhausted")
|
||||
ErrStarGiftCollectibleInvalid = errors.New("stargift: invalid collectible definition")
|
||||
ErrStarGiftCollectibleNumberTaken = errors.New("stargift: collectible number already taken")
|
||||
ErrStarGiftCollectionNotFound = errors.New("stargift: collection not found")
|
||||
ErrStarGiftCollectionsFull = errors.New("stargift: collections full")
|
||||
ErrStarGiftUnavailable = errors.New("stargift: unavailable")
|
||||
|
|
|
|||
|
|
@ -451,7 +451,9 @@ func tgChannel(viewerUserID int64, ch domain.Channel, self *domain.ChannelMember
|
|||
Creator: ch.CreatorUserID == viewerUserID && viewerUserID != 0,
|
||||
Verified: ch.Verified,
|
||||
Scam: ch.Scam,
|
||||
Fake: ch.Fake,
|
||||
// scam and fake are mutually exclusive in Telegram; a peer flagged as
|
||||
// both would render neither badge on clients. scam takes precedence.
|
||||
Fake: ch.Fake && !ch.Scam,
|
||||
Gigagroup: ch.Gigagroup,
|
||||
Broadcast: ch.Broadcast,
|
||||
Megagroup: ch.Megagroup,
|
||||
|
|
|
|||
|
|
@ -53,7 +53,9 @@ func tgUser(u domain.User) *tg.User {
|
|||
Phone: u.Phone,
|
||||
Verified: u.Verified,
|
||||
Scam: u.Scam,
|
||||
Fake: u.Fake,
|
||||
// scam and fake are mutually exclusive in Telegram; a peer flagged as
|
||||
// both would render neither badge on clients. scam takes precedence.
|
||||
Fake: u.Fake && !u.Scam,
|
||||
Support: u.Support,
|
||||
Contact: u.Contact,
|
||||
MutualContact: u.Mutual,
|
||||
|
|
|
|||
|
|
@ -523,10 +523,17 @@ func (r *Router) onPaymentsGetSavedStarGifts(ctx context.Context, req *tg.Paymen
|
|||
if r.deps.Gifts == nil {
|
||||
return emptySavedStarGifts(), nil
|
||||
}
|
||||
// Gifts hidden from the profile (unsaved) are visible only to the owner (or a
|
||||
// channel admin). Never trust the client's exclude_unsaved flag for other
|
||||
// viewers: force-exclude hidden gifts unless the requester manages the owner.
|
||||
excludeUnsaved := req.ExcludeUnsaved
|
||||
if r.ensureCanManageStarGiftOwner(ctx, userID, owner) != nil {
|
||||
excludeUnsaved = true
|
||||
}
|
||||
collectionID, _ := req.GetCollectionID()
|
||||
page, err := r.deps.Gifts.ListSavedFiltered(ctx, domain.SavedStarGiftFilter{
|
||||
Owner: owner,
|
||||
ExcludeUnsaved: req.ExcludeUnsaved,
|
||||
ExcludeUnsaved: excludeUnsaved,
|
||||
ExcludeSaved: req.ExcludeSaved,
|
||||
ExcludeUnlimited: req.ExcludeUnlimited,
|
||||
ExcludeUnique: req.ExcludeUnique,
|
||||
|
|
@ -556,6 +563,17 @@ func (r *Router) onPaymentsGetSavedStarGift(ctx context.Context, refs []tg.Input
|
|||
return emptySavedStarGifts(), nil
|
||||
}
|
||||
gifts := make([]domain.SavedStarGift, 0, len(refs))
|
||||
// A gift hidden from the profile (unsaved) is visible only to the owner or a
|
||||
// channel admin. Memoize the manage check per owner to avoid repeat lookups.
|
||||
manageCache := make(map[domain.Peer]bool)
|
||||
canManageOwner := func(owner domain.Peer) bool {
|
||||
if v, ok := manageCache[owner]; ok {
|
||||
return v
|
||||
}
|
||||
v := r.ensureCanManageStarGiftOwner(ctx, userID, owner) == nil
|
||||
manageCache[owner] = v
|
||||
return v
|
||||
}
|
||||
for _, ref := range refs {
|
||||
dref, ok, err := r.starGiftRefFromInput(ctx, userID, ref)
|
||||
if err != nil {
|
||||
|
|
@ -569,6 +587,9 @@ func (r *Router) onPaymentsGetSavedStarGift(ctx context.Context, refs []tg.Input
|
|||
return nil, internalErr()
|
||||
}
|
||||
if found && !g.Converted {
|
||||
if g.Unsaved && !canManageOwner(g.Owner) {
|
||||
continue
|
||||
}
|
||||
gifts = append(gifts, g)
|
||||
}
|
||||
}
|
||||
|
|
@ -669,9 +690,14 @@ func (r *Router) onPaymentsConvertStarGift(ctx context.Context, ref tg.InputSave
|
|||
})
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, domain.ErrStarGiftNotFound):
|
||||
return false, starGiftInvalidErr()
|
||||
case errors.Is(err, domain.ErrStarGiftAlreadyConverted):
|
||||
case errors.Is(err, domain.ErrStarGiftNotFound),
|
||||
errors.Is(err, domain.ErrStarGiftAlreadyConverted),
|
||||
errors.Is(err, domain.ErrStarGiftAlreadyUpgraded),
|
||||
errors.Is(err, domain.ErrStarGiftOwnerInvalid),
|
||||
errors.Is(err, domain.ErrStarGiftUnavailable):
|
||||
// These are known business conditions (e.g. converting an already
|
||||
// upgraded/unique gift). Surface a clean client error instead of a
|
||||
// 500 INTERNAL_SERVER_ERROR.
|
||||
return false, starGiftInvalidErr()
|
||||
default:
|
||||
return false, internalErr()
|
||||
|
|
|
|||
|
|
@ -90,7 +90,6 @@ func (r *Router) adminGrantUpgradedStarGift(ctx context.Context, senderID int64,
|
|||
ModelAttributeID: grant.ModelAttributeID,
|
||||
PatternAttributeID: grant.PatternAttributeID,
|
||||
BackdropAttributeID: grant.BackdropAttributeID,
|
||||
Num: grant.Num,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -164,19 +164,6 @@ WHERE collectible_revision_id=$1 AND crafted
|
|||
}
|
||||
|
||||
num := revision.Issued + 1
|
||||
if req.Num > 0 {
|
||||
if req.Num > revision.SupplyTotal {
|
||||
return domain.ErrStarGiftCollectibleInvalid
|
||||
}
|
||||
var numTaken bool
|
||||
if err := tx.QueryRow(ctx, `SELECT EXISTS (SELECT 1 FROM unique_star_gifts WHERE gift_id=$1 AND num=$2)`, locked.GiftID, req.Num).Scan(&numTaken); err != nil {
|
||||
return fmt.Errorf("check collectible number availability: %w", err)
|
||||
}
|
||||
if numTaken {
|
||||
return domain.ErrStarGiftCollectibleNumberTaken
|
||||
}
|
||||
num = req.Num
|
||||
}
|
||||
var uniqueID int64
|
||||
if err := tx.QueryRow(ctx, `SELECT nextval('unique_star_gift_id_seq')`).Scan(&uniqueID); err != nil {
|
||||
return fmt.Errorf("allocate unique star gift id: %w", err)
|
||||
|
|
|
|||
|
|
@ -41,6 +41,10 @@ type userBaseValue struct {
|
|||
CountryCode string `json:"country_code"`
|
||||
Verified bool `json:"verified"`
|
||||
Support bool `json:"support"`
|
||||
// scam / fake 同理必须随缓存往返:丢失会让缓存命中路径把带标记的账号输出成
|
||||
// 普通账号,导致资料页 SCAM/FAKE 标记随缓存命中/未命中间歇性消失(与 bot 列同坑)。
|
||||
Scam bool `json:"scam,omitempty"`
|
||||
Fake bool `json:"fake,omitempty"`
|
||||
// bot 字段必须随缓存往返:丢失会让缓存命中路径把 bot 输出成普通用户,
|
||||
// 污染客户端本地缓存(TDesktop 的 bot 标记不可逆)。
|
||||
Bot bool `json:"bot,omitempty"`
|
||||
|
|
@ -78,6 +82,8 @@ func baseValueFromUser(u domain.User) userBaseValue {
|
|||
CountryCode: u.CountryCode,
|
||||
Verified: u.Verified,
|
||||
Support: u.Support,
|
||||
Scam: u.Scam,
|
||||
Fake: u.Fake,
|
||||
Bot: u.Bot,
|
||||
BotInfoVersion: u.BotInfoVersion,
|
||||
PremiumUntil: u.PremiumUntil,
|
||||
|
|
@ -110,6 +116,8 @@ func (v userBaseValue) user() domain.User {
|
|||
CountryCode: v.CountryCode,
|
||||
Verified: v.Verified,
|
||||
Support: v.Support,
|
||||
Scam: v.Scam,
|
||||
Fake: v.Fake,
|
||||
Bot: v.Bot,
|
||||
BotInfoVersion: v.BotInfoVersion,
|
||||
PremiumUntil: v.PremiumUntil,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue