chore: refresh gramsrv public release
This commit is contained in:
parent
75cebe8dbf
commit
70b6820474
1274 changed files with 378751 additions and 59919 deletions
543
internal/store/postgres/channel_boosts.go
Normal file
543
internal/store/postgres/channel_boosts.go
Normal file
|
|
@ -0,0 +1,543 @@
|
|||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store/postgres/sqlcgen"
|
||||
)
|
||||
|
||||
func (s *ChannelStore) GetPremiumBoostStatus(ctx context.Context, viewerUserID, channelID int64, now int) (domain.PremiumBoostStatus, error) {
|
||||
if viewerUserID == 0 || channelID == 0 {
|
||||
return domain.PremiumBoostStatus{}, domain.ErrChannelInvalid
|
||||
}
|
||||
if _, _, _, err := s.getChannelForViewer(ctx, s.db, viewerUserID, channelID); err != nil {
|
||||
return domain.PremiumBoostStatus{}, err
|
||||
}
|
||||
peer := domain.Peer{Type: domain.PeerTypeChannel, ID: channelID}
|
||||
total, err := s.countActiveBoostsForPeer(ctx, s.db, peer, now)
|
||||
if err != nil {
|
||||
return domain.PremiumBoostStatus{}, err
|
||||
}
|
||||
my, err := listActiveUserBoostsForPeer(ctx, s.db, viewerUserID, peer, now)
|
||||
if err != nil {
|
||||
return domain.PremiumBoostStatus{}, err
|
||||
}
|
||||
return domain.PremiumBoostStatusForCount(peer, total, my), nil
|
||||
}
|
||||
|
||||
func (s *ChannelStore) ListPremiumBoosts(ctx context.Context, viewerUserID, channelID int64, gifts bool, offset string, limit, now int) (domain.PremiumBoostList, error) {
|
||||
if viewerUserID == 0 || channelID == 0 {
|
||||
return domain.PremiumBoostList{}, domain.ErrChannelInvalid
|
||||
}
|
||||
if limit <= 0 || limit > domain.MaxPremiumBoostsListLimit {
|
||||
limit = domain.MaxPremiumBoostsListLimit
|
||||
}
|
||||
_, member, err := s.getChannelForMember(ctx, s.db, viewerUserID, channelID)
|
||||
if err != nil {
|
||||
return domain.PremiumBoostList{}, err
|
||||
}
|
||||
if member.Role != domain.ChannelRoleCreator && member.Role != domain.ChannelRoleAdmin {
|
||||
return domain.PremiumBoostList{}, domain.ErrChannelAdminRequired
|
||||
}
|
||||
cursor, err := parseBoostCursor(offset)
|
||||
if err != nil {
|
||||
return domain.PremiumBoostList{}, domain.ErrChannelInvalid
|
||||
}
|
||||
return listPremiumBoostsPage(ctx, s.db, domain.Peer{Type: domain.PeerTypeChannel, ID: channelID}, gifts, cursor, limit, now)
|
||||
}
|
||||
|
||||
func (s *ChannelStore) GetPremiumMyBoosts(ctx context.Context, userID int64, now, premiumUntil int) (domain.PremiumMyBoosts, error) {
|
||||
if userID == 0 {
|
||||
return domain.PremiumMyBoosts{}, domain.ErrChannelInvalid
|
||||
}
|
||||
return s.premiumMyBoosts(ctx, s.db, userID, now, premiumUntil)
|
||||
}
|
||||
|
||||
func (s *ChannelStore) ApplyPremiumBoost(ctx context.Context, userID, channelID int64, slots []int, now, premiumUntil int) (domain.PremiumMyBoosts, error) {
|
||||
if userID == 0 || channelID == 0 || len(slots) == 0 {
|
||||
return domain.PremiumMyBoosts{}, domain.ErrChannelInvalid
|
||||
}
|
||||
if premiumUntil <= now {
|
||||
return domain.PremiumMyBoosts{}, domain.ErrPremiumRequired
|
||||
}
|
||||
beginner, ok := s.db.(txBeginner)
|
||||
if !ok {
|
||||
return domain.PremiumMyBoosts{}, fmt.Errorf("apply premium boost: db does not support transactions")
|
||||
}
|
||||
tx, err := beginner.Begin(ctx)
|
||||
if err != nil {
|
||||
return domain.PremiumMyBoosts{}, fmt.Errorf("begin apply premium boost: %w", err)
|
||||
}
|
||||
committed := false
|
||||
defer func() {
|
||||
if !committed {
|
||||
_ = tx.Rollback(ctx)
|
||||
}
|
||||
}()
|
||||
if _, _, err := s.getChannelForMember(ctx, tx, userID, channelID); err != nil {
|
||||
return domain.PremiumMyBoosts{}, err
|
||||
}
|
||||
peer := domain.Peer{Type: domain.PeerTypeChannel, ID: channelID}
|
||||
invalidatedPeers := make(map[domain.Peer]struct{}, len(slots)+1)
|
||||
for _, slotID := range slots {
|
||||
if slotID != domain.DefaultPremiumBoostSlotID {
|
||||
return domain.PremiumMyBoosts{}, domain.ErrChannelInvalid
|
||||
}
|
||||
current, err := getPremiumBoostSlotForUpdate(ctx, tx, userID, slotID)
|
||||
if err != nil {
|
||||
return domain.PremiumMyBoosts{}, err
|
||||
}
|
||||
next, changed, err := domain.ApplyPremiumBoostSlot(
|
||||
current,
|
||||
userID,
|
||||
slotID,
|
||||
peer,
|
||||
now,
|
||||
premiumUntil,
|
||||
domain.DefaultPremiumBoostReassignCooldownSeconds,
|
||||
)
|
||||
if err != nil {
|
||||
return domain.PremiumMyBoosts{}, err
|
||||
}
|
||||
if !changed {
|
||||
continue
|
||||
}
|
||||
if current.Assigned(now) {
|
||||
invalidatedPeers[current.Peer] = struct{}{}
|
||||
}
|
||||
if next.Assigned(now) {
|
||||
invalidatedPeers[next.Peer] = struct{}{}
|
||||
}
|
||||
if err := upsertPremiumBoostSlot(ctx, tx, next); err != nil {
|
||||
return domain.PremiumMyBoosts{}, fmt.Errorf("upsert premium boost slot: %w", err)
|
||||
}
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return domain.PremiumMyBoosts{}, fmt.Errorf("commit apply premium boost: %w", err)
|
||||
}
|
||||
committed = true
|
||||
s.invalidateBoostCacheForUserAndPeers(userID, invalidatedPeers)
|
||||
return s.premiumMyBoosts(ctx, s.db, userID, now, premiumUntil)
|
||||
}
|
||||
|
||||
func getPremiumBoostSlotForUpdate(ctx context.Context, db sqlcgen.DBTX, userID int64, slotID int) (domain.PremiumBoostSlot, error) {
|
||||
row := db.QueryRow(ctx, `
|
||||
SELECT user_id, slot, peer_type, peer_id, assigned_at, expires_at, cooldown_until, multiplier,
|
||||
gift, giveaway, unclaimed, giveaway_msg_id, used_gift_slug, stars
|
||||
FROM channel_boost_slots
|
||||
WHERE user_id = $1 AND slot = $2
|
||||
FOR UPDATE`, userID, slotID)
|
||||
slot, err := scanPremiumBoostSlot(row)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.PremiumBoostSlot{}, nil
|
||||
}
|
||||
return domain.PremiumBoostSlot{}, fmt.Errorf("lock premium boost slot: %w", err)
|
||||
}
|
||||
return slot, nil
|
||||
}
|
||||
|
||||
func upsertPremiumBoostSlot(ctx context.Context, db sqlcgen.DBTX, slot domain.PremiumBoostSlot) error {
|
||||
peerType := string(slot.Peer.Type)
|
||||
peerID := slot.Peer.ID
|
||||
if peerID == 0 {
|
||||
peerType = ""
|
||||
}
|
||||
_, err := db.Exec(ctx, `
|
||||
INSERT INTO channel_boost_slots (
|
||||
user_id, slot, peer_type, peer_id, assigned_at, expires_at, cooldown_until, multiplier,
|
||||
gift, giveaway, unclaimed, giveaway_msg_id, used_gift_slug, stars
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
|
||||
ON CONFLICT (user_id, slot) DO UPDATE SET
|
||||
peer_type = EXCLUDED.peer_type,
|
||||
peer_id = EXCLUDED.peer_id,
|
||||
assigned_at = EXCLUDED.assigned_at,
|
||||
expires_at = EXCLUDED.expires_at,
|
||||
cooldown_until = EXCLUDED.cooldown_until,
|
||||
multiplier = EXCLUDED.multiplier,
|
||||
gift = EXCLUDED.gift,
|
||||
giveaway = EXCLUDED.giveaway,
|
||||
unclaimed = EXCLUDED.unclaimed,
|
||||
giveaway_msg_id = EXCLUDED.giveaway_msg_id,
|
||||
used_gift_slug = EXCLUDED.used_gift_slug,
|
||||
stars = EXCLUDED.stars,
|
||||
updated_at = now()`,
|
||||
slot.UserID,
|
||||
slot.Slot,
|
||||
peerType,
|
||||
peerID,
|
||||
slot.Date,
|
||||
slot.Expires,
|
||||
slot.CooldownUntil,
|
||||
slot.Multiplier,
|
||||
slot.Gift,
|
||||
slot.Giveaway,
|
||||
slot.Unclaimed,
|
||||
slot.GiveawayMsgID,
|
||||
slot.UsedGiftSlug,
|
||||
slot.Stars,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *ChannelStore) GetPremiumUserBoosts(ctx context.Context, viewerUserID, channelID, targetUserID int64, now int) (domain.PremiumBoostList, error) {
|
||||
if viewerUserID == 0 || channelID == 0 || targetUserID == 0 {
|
||||
return domain.PremiumBoostList{}, domain.ErrChannelInvalid
|
||||
}
|
||||
_, member, err := s.getChannelForMember(ctx, s.db, viewerUserID, channelID)
|
||||
if err != nil {
|
||||
return domain.PremiumBoostList{}, err
|
||||
}
|
||||
if member.Role != domain.ChannelRoleCreator && member.Role != domain.ChannelRoleAdmin {
|
||||
return domain.PremiumBoostList{}, domain.ErrChannelAdminRequired
|
||||
}
|
||||
peer := domain.Peer{Type: domain.PeerTypeChannel, ID: channelID}
|
||||
items, err := listActiveUserBoostsForPeer(ctx, s.db, targetUserID, peer, now)
|
||||
if err != nil {
|
||||
return domain.PremiumBoostList{}, err
|
||||
}
|
||||
users, err := listUsersByIDs(ctx, s.db, []int64{targetUserID})
|
||||
if err != nil {
|
||||
return domain.PremiumBoostList{}, err
|
||||
}
|
||||
return domain.PremiumBoostList{Count: len(items), Boosts: items, Users: users}, nil
|
||||
}
|
||||
|
||||
func (s *ChannelStore) SetBoostsToUnblockRestrictions(ctx context.Context, userID, channelID int64, boosts int) (domain.Channel, error) {
|
||||
if userID == 0 || channelID == 0 || boosts < 0 || boosts > domain.MaxChannelBoostsToUnblockRestrictions {
|
||||
return domain.Channel{}, domain.ErrChannelInvalid
|
||||
}
|
||||
channel, member, err := s.getChannelForMember(ctx, s.db, userID, channelID)
|
||||
if err != nil {
|
||||
return domain.Channel{}, err
|
||||
}
|
||||
if !canChangeChannelInfo(member) {
|
||||
return domain.Channel{}, domain.ErrChannelAdminRequired
|
||||
}
|
||||
if _, err := s.db.Exec(ctx, `UPDATE channels SET boosts_unrestrict = $2, updated_at = now() WHERE id = $1`, channelID, boosts); err != nil {
|
||||
return domain.Channel{}, fmt.Errorf("set boosts unrestrict: %w", err)
|
||||
}
|
||||
channel.BoostsUnrestrict = boosts
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
func (s *ChannelStore) premiumMyBoosts(ctx context.Context, db sqlcgen.DBTX, userID int64, now, premiumUntil int) (domain.PremiumMyBoosts, error) {
|
||||
out := domain.PremiumMyBoosts{}
|
||||
if premiumUntil <= now {
|
||||
return out, nil
|
||||
}
|
||||
rows, err := db.Query(ctx, `
|
||||
SELECT user_id, slot, peer_type, peer_id, assigned_at, expires_at, cooldown_until, multiplier,
|
||||
gift, giveaway, unclaimed, giveaway_msg_id, used_gift_slug, stars
|
||||
FROM channel_boost_slots
|
||||
WHERE user_id = $1
|
||||
AND (expires_at = 0 OR expires_at > $2)
|
||||
ORDER BY slot ASC`, userID, now)
|
||||
if err != nil {
|
||||
return domain.PremiumMyBoosts{}, fmt.Errorf("list my boost slots: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
foundBase := false
|
||||
channelRefs := make(map[int64]struct{})
|
||||
for rows.Next() {
|
||||
slot, err := scanPremiumBoostSlot(rows)
|
||||
if err != nil {
|
||||
return domain.PremiumMyBoosts{}, err
|
||||
}
|
||||
if slot.Slot == domain.DefaultPremiumBoostSlotID {
|
||||
foundBase = true
|
||||
if slot.Expires < premiumUntil {
|
||||
slot.Expires = premiumUntil
|
||||
}
|
||||
}
|
||||
out.Slots = append(out.Slots, slot)
|
||||
if slot.Peer.Type == domain.PeerTypeChannel && slot.Peer.ID != 0 {
|
||||
channelRefs[slot.Peer.ID] = struct{}{}
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return domain.PremiumMyBoosts{}, err
|
||||
}
|
||||
if !foundBase {
|
||||
out.Slots = append(out.Slots, domain.PremiumBoostSlot{
|
||||
UserID: userID,
|
||||
Slot: domain.DefaultPremiumBoostSlotID,
|
||||
Multiplier: 1,
|
||||
})
|
||||
}
|
||||
if len(channelRefs) > 0 {
|
||||
channels, err := listChannelsByIDsInOrder(ctx, db, mapKeysInt64(channelRefs))
|
||||
if err != nil {
|
||||
return domain.PremiumMyBoosts{}, err
|
||||
}
|
||||
out.Channels = channels
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func countActiveBoostsForPeer(ctx context.Context, db sqlcgen.DBTX, peer domain.Peer, now int) (int, error) {
|
||||
if peer.Type != domain.PeerTypeChannel || peer.ID == 0 {
|
||||
return 0, domain.ErrChannelInvalid
|
||||
}
|
||||
var total int
|
||||
if err := db.QueryRow(ctx, `
|
||||
SELECT COALESCE(SUM(multiplier), 0)::int
|
||||
FROM channel_boost_slots
|
||||
WHERE peer_type = $1
|
||||
AND peer_id = $2
|
||||
AND (expires_at = 0 OR expires_at > $3)`, string(peer.Type), peer.ID, now).Scan(&total); err != nil {
|
||||
return 0, fmt.Errorf("count peer boosts: %w", err)
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
|
||||
func (s *ChannelStore) countActiveBoostsForPeer(ctx context.Context, db sqlcgen.DBTX, peer domain.Peer, now int) (int, error) {
|
||||
if !s.boostCacheActive(db) {
|
||||
return countActiveBoostsForPeer(ctx, db, peer, now)
|
||||
}
|
||||
return s.boostCache.getPeerTotalOrLoad(ctx, peer, func() (int, error) {
|
||||
return countActiveBoostsForPeer(ctx, db, peer, now)
|
||||
})
|
||||
}
|
||||
|
||||
func countActiveUserBoostsForPeer(ctx context.Context, db sqlcgen.DBTX, userID int64, peer domain.Peer, now int) (int, error) {
|
||||
if userID == 0 || peer.Type != domain.PeerTypeChannel || peer.ID == 0 {
|
||||
return 0, domain.ErrChannelInvalid
|
||||
}
|
||||
var total int
|
||||
if err := db.QueryRow(ctx, `
|
||||
SELECT COALESCE(SUM(multiplier), 0)::int
|
||||
FROM channel_boost_slots
|
||||
WHERE user_id = $1
|
||||
AND peer_type = $2
|
||||
AND peer_id = $3
|
||||
AND (expires_at = 0 OR expires_at > $4)`, userID, string(peer.Type), peer.ID, now).Scan(&total); err != nil {
|
||||
return 0, fmt.Errorf("count user peer boosts: %w", err)
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
|
||||
func (s *ChannelStore) countActiveUserBoostsForPeer(ctx context.Context, db sqlcgen.DBTX, userID int64, peer domain.Peer, now int) (int, error) {
|
||||
if !s.boostCacheActive(db) {
|
||||
return countActiveUserBoostsForPeer(ctx, db, userID, peer, now)
|
||||
}
|
||||
return s.boostCache.getOrLoad(ctx, userID, peer, func() (int, error) {
|
||||
return countActiveUserBoostsForPeer(ctx, db, userID, peer, now)
|
||||
})
|
||||
}
|
||||
|
||||
func countActiveUserBoostsForChannels(ctx context.Context, db sqlcgen.DBTX, userID int64, channelIDs []int64, now int) (map[int64]int, error) {
|
||||
out := make(map[int64]int)
|
||||
ids := uniqueNonZeroInt64s(channelIDs...)
|
||||
if userID == 0 || len(ids) == 0 {
|
||||
return out, nil
|
||||
}
|
||||
rows, err := db.Query(ctx, `
|
||||
SELECT peer_id, COALESCE(SUM(multiplier), 0)::int
|
||||
FROM channel_boost_slots
|
||||
WHERE user_id = $1
|
||||
AND peer_type = 'channel'
|
||||
AND peer_id = ANY($2::bigint[])
|
||||
AND (expires_at = 0 OR expires_at > $3)
|
||||
GROUP BY peer_id`, userID, ids, now)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("count user channel boosts: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var channelID int64
|
||||
var boosts int
|
||||
if err := rows.Scan(&channelID, &boosts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out[channelID] = boosts
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *ChannelStore) countActiveUserBoostsForChannels(ctx context.Context, db sqlcgen.DBTX, userID int64, channelIDs []int64, now int) (map[int64]int, error) {
|
||||
if !s.boostCacheActive(db) {
|
||||
return countActiveUserBoostsForChannels(ctx, db, userID, channelIDs, now)
|
||||
}
|
||||
out := make(map[int64]int)
|
||||
misses := make([]int64, 0, len(channelIDs))
|
||||
for _, channelID := range uniqueNonZeroInt64s(channelIDs...) {
|
||||
peer := domain.Peer{Type: domain.PeerTypeChannel, ID: channelID}
|
||||
if boosts, ok := s.boostCache.get(userID, peer); ok {
|
||||
out[channelID] = boosts
|
||||
continue
|
||||
}
|
||||
misses = append(misses, channelID)
|
||||
}
|
||||
if len(misses) == 0 {
|
||||
return out, nil
|
||||
}
|
||||
loaded, err := countActiveUserBoostsForChannels(ctx, db, userID, misses, now)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, channelID := range misses {
|
||||
boosts := loaded[channelID]
|
||||
out[channelID] = boosts
|
||||
s.boostCache.put(userID, domain.Peer{Type: domain.PeerTypeChannel, ID: channelID}, boosts)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *ChannelStore) invalidateBoostCacheForUserAndPeers(userID int64, peers map[domain.Peer]struct{}) {
|
||||
if s.boostCache == nil {
|
||||
return
|
||||
}
|
||||
s.boostCache.deleteUser(userID)
|
||||
for peer := range peers {
|
||||
s.boostCache.deletePeerTotal(peer)
|
||||
}
|
||||
}
|
||||
|
||||
func listActiveUserBoostsForPeer(ctx context.Context, db sqlcgen.DBTX, userID int64, peer domain.Peer, now int) ([]domain.PremiumBoostSlot, error) {
|
||||
rows, err := db.Query(ctx, `
|
||||
SELECT user_id, slot, peer_type, peer_id, assigned_at, expires_at, cooldown_until, multiplier,
|
||||
gift, giveaway, unclaimed, giveaway_msg_id, used_gift_slug, stars
|
||||
FROM channel_boost_slots
|
||||
WHERE user_id = $1
|
||||
AND peer_type = $2
|
||||
AND peer_id = $3
|
||||
AND (expires_at = 0 OR expires_at > $4)
|
||||
ORDER BY assigned_at DESC, user_id ASC, slot ASC`, userID, string(peer.Type), peer.ID, now)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list user boosts for peer: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
return scanPremiumBoostSlots(rows)
|
||||
}
|
||||
|
||||
type boostCursor struct {
|
||||
assignedAt int
|
||||
userID int64
|
||||
slot int
|
||||
set bool
|
||||
}
|
||||
|
||||
func listPremiumBoostsPage(ctx context.Context, db sqlcgen.DBTX, peer domain.Peer, gifts bool, cursor boostCursor, limit, now int) (domain.PremiumBoostList, error) {
|
||||
var count int
|
||||
if err := db.QueryRow(ctx, `
|
||||
SELECT COUNT(*)::int
|
||||
FROM channel_boost_slots
|
||||
WHERE peer_type = $1
|
||||
AND peer_id = $2
|
||||
AND (expires_at = 0 OR expires_at > $3)
|
||||
AND (NOT $4::boolean OR gift OR giveaway)`, string(peer.Type), peer.ID, now, gifts).Scan(&count); err != nil {
|
||||
return domain.PremiumBoostList{}, fmt.Errorf("count premium boosts: %w", err)
|
||||
}
|
||||
args := []any{string(peer.Type), peer.ID, now, gifts, limit + 1}
|
||||
cursorSQL := ""
|
||||
if cursor.set {
|
||||
args = append(args, cursor.assignedAt, cursor.userID, cursor.slot)
|
||||
cursorSQL = fmt.Sprintf(` AND (
|
||||
assigned_at < $%d
|
||||
OR (assigned_at = $%d AND user_id > $%d)
|
||||
OR (assigned_at = $%d AND user_id = $%d AND slot > $%d)
|
||||
)`, len(args)-2, len(args)-2, len(args)-1, len(args)-2, len(args)-1, len(args))
|
||||
}
|
||||
rows, err := db.Query(ctx, `
|
||||
SELECT user_id, slot, peer_type, peer_id, assigned_at, expires_at, cooldown_until, multiplier,
|
||||
gift, giveaway, unclaimed, giveaway_msg_id, used_gift_slug, stars
|
||||
FROM channel_boost_slots
|
||||
WHERE peer_type = $1
|
||||
AND peer_id = $2
|
||||
AND (expires_at = 0 OR expires_at > $3)
|
||||
AND (NOT $4::boolean OR gift OR giveaway)`+cursorSQL+`
|
||||
ORDER BY assigned_at DESC, user_id ASC, slot ASC
|
||||
LIMIT $5`, args...)
|
||||
if err != nil {
|
||||
return domain.PremiumBoostList{}, fmt.Errorf("list premium boosts: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
items, err := scanPremiumBoostSlots(rows)
|
||||
if err != nil {
|
||||
return domain.PremiumBoostList{}, err
|
||||
}
|
||||
next := ""
|
||||
if len(items) > limit {
|
||||
last := items[limit-1]
|
||||
next = formatBoostCursor(last)
|
||||
items = items[:limit]
|
||||
}
|
||||
userIDs := make([]int64, 0, len(items))
|
||||
for _, item := range items {
|
||||
userIDs = append(userIDs, item.UserID)
|
||||
}
|
||||
users, err := listUsersByIDs(ctx, db, uniqueNonZeroInt64s(userIDs...))
|
||||
if err != nil {
|
||||
return domain.PremiumBoostList{}, err
|
||||
}
|
||||
return domain.PremiumBoostList{Count: count, Boosts: items, Users: users, NextOffset: next}, nil
|
||||
}
|
||||
|
||||
func scanPremiumBoostSlots(rows pgx.Rows) ([]domain.PremiumBoostSlot, error) {
|
||||
items := make([]domain.PremiumBoostSlot, 0)
|
||||
for rows.Next() {
|
||||
slot, err := scanPremiumBoostSlot(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, slot)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func scanPremiumBoostSlot(row rowScanner) (domain.PremiumBoostSlot, error) {
|
||||
var slot domain.PremiumBoostSlot
|
||||
var peerType string
|
||||
if err := row.Scan(
|
||||
&slot.UserID, &slot.Slot, &peerType, &slot.Peer.ID, &slot.Date, &slot.Expires, &slot.CooldownUntil, &slot.Multiplier,
|
||||
&slot.Gift, &slot.Giveaway, &slot.Unclaimed, &slot.GiveawayMsgID, &slot.UsedGiftSlug, &slot.Stars,
|
||||
); err != nil {
|
||||
return domain.PremiumBoostSlot{}, err
|
||||
}
|
||||
slot.Peer.Type = domain.PeerType(peerType)
|
||||
if slot.Peer.Type == "" || slot.Peer.ID == 0 {
|
||||
slot.Peer = domain.Peer{}
|
||||
}
|
||||
return slot, nil
|
||||
}
|
||||
|
||||
func parseBoostCursor(raw string) (boostCursor, error) {
|
||||
if raw == "" {
|
||||
return boostCursor{}, nil
|
||||
}
|
||||
parts := strings.Split(raw, ":")
|
||||
if len(parts) != 3 {
|
||||
return boostCursor{}, fmt.Errorf("invalid boost cursor")
|
||||
}
|
||||
assignedAt, err := strconv.Atoi(parts[0])
|
||||
if err != nil || assignedAt < 0 {
|
||||
return boostCursor{}, fmt.Errorf("invalid boost cursor")
|
||||
}
|
||||
userID, err := strconv.ParseInt(parts[1], 10, 64)
|
||||
if err != nil || userID <= 0 {
|
||||
return boostCursor{}, fmt.Errorf("invalid boost cursor")
|
||||
}
|
||||
slot, err := strconv.Atoi(parts[2])
|
||||
if err != nil || slot <= 0 {
|
||||
return boostCursor{}, fmt.Errorf("invalid boost cursor")
|
||||
}
|
||||
return boostCursor{assignedAt: assignedAt, userID: userID, slot: slot, set: true}, nil
|
||||
}
|
||||
|
||||
func formatBoostCursor(slot domain.PremiumBoostSlot) string {
|
||||
return fmt.Sprintf("%d:%d:%d", slot.Date, slot.UserID, slot.Slot)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue