704 lines
18 KiB
Go
704 lines
18 KiB
Go
package sniper
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"huya-go/internal/huya"
|
|
)
|
|
|
|
const (
|
|
CategoryReady = "ready"
|
|
CategorySuccess = "success"
|
|
CategoryNotFound = "not_found"
|
|
CategoryNotOpen = "not_open"
|
|
CategoryTooFrequent = "too_frequent"
|
|
CategoryNotEnough = "not_enough"
|
|
CategoryAlreadyDone = "already_done"
|
|
CategoryOffShelf = "off_shelf"
|
|
CategorySoldOut = "sold_out"
|
|
CategoryFailed = "failed"
|
|
CategoryError = "error"
|
|
)
|
|
|
|
type Account struct {
|
|
UID int64 `json:"uid"`
|
|
Cookie string `json:"-"`
|
|
}
|
|
|
|
type Target struct {
|
|
PID int32 `json:"pid"`
|
|
Name string `json:"name"`
|
|
Auto bool `json:"auto,omitempty"`
|
|
MaxPrice int64 `json:"max_price,omitempty"`
|
|
MinLeft int64 `json:"min_left,omitempty"`
|
|
}
|
|
|
|
type Config struct {
|
|
SID int32
|
|
Loops int
|
|
Delay time.Duration
|
|
Concurrency int
|
|
StopAfterSuccess bool
|
|
StopAccountAfterSuccess bool
|
|
Prewarm bool
|
|
DirectExchange bool
|
|
ListBeforeExchange bool
|
|
SharePrizeList bool
|
|
RefreshAfterSuccess bool
|
|
StopOnNotEnough bool
|
|
ContinueOnNotEnough bool
|
|
StopOnSoldOut bool
|
|
ContinueOnSoldOut bool
|
|
TooFrequentBackoff time.Duration
|
|
ErrorBackoff time.Duration
|
|
}
|
|
|
|
type Result struct {
|
|
Loop int `json:"loop"`
|
|
UID int64 `json:"uid"`
|
|
TargetIndex int `json:"target_index,omitempty"`
|
|
Target Target `json:"target"`
|
|
Action string `json:"action"`
|
|
Category string `json:"category,omitempty"`
|
|
Message string `json:"message,omitempty"`
|
|
Prize *huya.PrizeInfo `json:"prize,omitempty"`
|
|
Exchange *huya.ScoreExchangePrizeResp `json:"exchange,omitempty"`
|
|
Score *huya.GetUserScoreResp `json:"score,omitempty"`
|
|
Records *huya.GetUserPrizeRecordsResp `json:"records,omitempty"`
|
|
Done bool `json:"done,omitempty"`
|
|
RetryAfter int64 `json:"retry_after,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
type Runner struct {
|
|
Client huya.ActivityClient
|
|
}
|
|
|
|
type ActivityPrewarmer interface {
|
|
PrewarmActivity(ctx context.Context, uid int64, cookie string, sid int32) error
|
|
}
|
|
|
|
type runState struct {
|
|
mu sync.Mutex
|
|
accounts map[int64][]targetState
|
|
targetDone []bool
|
|
}
|
|
|
|
type targetState struct {
|
|
Done bool
|
|
RetryAfter time.Time
|
|
}
|
|
|
|
type listSnapshot struct {
|
|
resp *huya.GetActPrizeListResp
|
|
err error
|
|
}
|
|
|
|
func (r *Runner) Run(ctx context.Context, accounts []Account, target Target, cfg Config, emit func(Result)) error {
|
|
return r.RunTargets(ctx, accounts, []Target{target}, cfg, emit)
|
|
}
|
|
|
|
func (r *Runner) RunTargets(ctx context.Context, accounts []Account, targets []Target, cfg Config, emit func(Result)) error {
|
|
cfg = normalizeConfig(cfg)
|
|
targets = normalizeTargets(targets)
|
|
if len(accounts) == 0 {
|
|
return errors.New("账号不能为空")
|
|
}
|
|
if len(targets) == 0 {
|
|
return errors.New("目标奖品不能为空")
|
|
}
|
|
client := r.client()
|
|
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
|
|
if cfg.Prewarm {
|
|
if err := r.Prewarm(ctx, accounts, cfg, emit); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
state := newRunState(accounts, len(targets))
|
|
for loop := 1; loop <= cfg.Loops; loop++ {
|
|
if ctx.Err() != nil {
|
|
return ctx.Err()
|
|
}
|
|
if state.allDone() {
|
|
return nil
|
|
}
|
|
|
|
sharedList := r.sharedList(ctx, client, accounts, targets, cfg, state)
|
|
sem := make(chan struct{}, cfg.Concurrency)
|
|
var wg sync.WaitGroup
|
|
for _, account := range accounts {
|
|
account := account
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
select {
|
|
case sem <- struct{}{}:
|
|
defer func() { <-sem }()
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
r.runAccountLoop(ctx, client, state, account, targets, cfg, loop, sharedList, emit, cancel)
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
if ctx.Err() != nil {
|
|
return ctx.Err()
|
|
}
|
|
if state.allDone() {
|
|
return nil
|
|
}
|
|
if loop < cfg.Loops {
|
|
if err := sleepContext(ctx, cfg.Delay); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *Runner) Prewarm(ctx context.Context, accounts []Account, cfg Config, emit func(Result)) error {
|
|
cfg = normalizeConfig(cfg)
|
|
if len(accounts) == 0 {
|
|
return errors.New("账号不能为空")
|
|
}
|
|
client := r.client()
|
|
prewarmer, ok := client.(ActivityPrewarmer)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
sem := make(chan struct{}, cfg.Concurrency)
|
|
var wg sync.WaitGroup
|
|
for _, account := range accounts {
|
|
account := account
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
select {
|
|
case sem <- struct{}{}:
|
|
defer func() { <-sem }()
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
err := prewarmer.PrewarmActivity(ctx, account.UID, account.Cookie, cfg.SID)
|
|
result := Result{
|
|
UID: account.UID,
|
|
Action: "prewarm",
|
|
Category: CategoryReady,
|
|
Message: "连接已预热",
|
|
}
|
|
if err != nil {
|
|
result.Category = CategoryError
|
|
result.Error = err.Error()
|
|
result.Message = "连接预热失败"
|
|
}
|
|
if emit != nil {
|
|
emit(result)
|
|
}
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
return ctx.Err()
|
|
}
|
|
|
|
func (r *Runner) runAccountLoop(ctx context.Context, client huya.ActivityClient, state *runState, account Account, targets []Target, cfg Config, loop int, sharedList *listSnapshot, emit func(Result), cancel context.CancelFunc) {
|
|
active := state.activeTargets(account.UID, len(targets), time.Now())
|
|
if len(active) == 0 {
|
|
return
|
|
}
|
|
|
|
listTargets := make([]int, 0, len(active))
|
|
for _, index := range active {
|
|
target := targets[index]
|
|
if shouldDirectExchange(target, cfg) {
|
|
result := r.exchangeTarget(ctx, client, account, target, index, loop, cfg.SID, nil)
|
|
result = r.applyExchangeDecision(state, account, index, cfg, result, cancel)
|
|
if emit != nil {
|
|
emit(result)
|
|
}
|
|
r.refreshAfterSuccess(client, account, target, index, loop, cfg, result, emit)
|
|
if ctx.Err() != nil {
|
|
return
|
|
}
|
|
continue
|
|
}
|
|
listTargets = append(listTargets, index)
|
|
}
|
|
if len(listTargets) == 0 || ctx.Err() != nil {
|
|
return
|
|
}
|
|
|
|
list := sharedList
|
|
if list == nil {
|
|
resp, err := client.GetActPrizeList(ctx, account.UID, account.Cookie, cfg.SID)
|
|
list = &listSnapshot{resp: resp, err: err}
|
|
}
|
|
if list.err != nil || list.resp == nil {
|
|
errText := "奖品列表响应为空"
|
|
if list.err != nil {
|
|
errText = list.err.Error()
|
|
}
|
|
for _, index := range listTargets {
|
|
retryAt := time.Now().Add(cfg.ErrorBackoff)
|
|
state.setRetryAfter(account.UID, index, retryAt)
|
|
result := Result{
|
|
Loop: loop,
|
|
UID: account.UID,
|
|
TargetIndex: index,
|
|
Target: targets[index],
|
|
Action: "list_error",
|
|
Category: CategoryError,
|
|
Message: "查询奖品列表失败",
|
|
RetryAfter: retryAt.UnixMilli(),
|
|
Error: errText,
|
|
}
|
|
if emit != nil {
|
|
emit(result)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
for _, index := range listTargets {
|
|
target := targets[index]
|
|
prize := matchPrize(list.resp.Prizes, target)
|
|
if prize == nil {
|
|
result := Result{
|
|
Loop: loop,
|
|
UID: account.UID,
|
|
TargetIndex: index,
|
|
Target: target,
|
|
Action: "not_found",
|
|
Category: CategoryNotFound,
|
|
Message: "未查询到所需物品,继续监控",
|
|
}
|
|
if emit != nil {
|
|
emit(result)
|
|
}
|
|
continue
|
|
}
|
|
result := r.exchangeTarget(ctx, client, account, target, index, loop, cfg.SID, prize)
|
|
result = r.applyExchangeDecision(state, account, index, cfg, result, cancel)
|
|
if emit != nil {
|
|
emit(result)
|
|
}
|
|
r.refreshAfterSuccess(client, account, target, index, loop, cfg, result, emit)
|
|
if ctx.Err() != nil {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (r *Runner) exchangeTarget(ctx context.Context, client huya.ActivityClient, account Account, target Target, index int, loop int, sid int32, prize *huya.PrizeInfo) Result {
|
|
pid := target.PID
|
|
if prize != nil {
|
|
pid = int32(prize.ID)
|
|
}
|
|
result := Result{
|
|
Loop: loop,
|
|
UID: account.UID,
|
|
TargetIndex: index,
|
|
Target: target,
|
|
Action: "exchange",
|
|
Prize: prize,
|
|
}
|
|
exchange, err := client.ScoreExchangePrize(ctx, account.UID, account.Cookie, sid, pid)
|
|
result.Exchange = exchange
|
|
result.Category = ClassifyExchange(exchange, err)
|
|
result.Message = exchangeMessage(exchange)
|
|
if err != nil {
|
|
result.Action = "exchange_error"
|
|
result.Error = err.Error()
|
|
if result.Message == "" {
|
|
result.Message = "兑换请求失败"
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (r *Runner) applyExchangeDecision(state *runState, account Account, index int, cfg Config, result Result, cancel context.CancelFunc) Result {
|
|
now := time.Now()
|
|
switch result.Category {
|
|
case CategorySuccess:
|
|
if cfg.StopAccountAfterSuccess {
|
|
state.markDone(account.UID, index)
|
|
result.Done = true
|
|
}
|
|
if cfg.StopAfterSuccess {
|
|
cancel()
|
|
}
|
|
case CategoryNotEnough, CategoryAlreadyDone:
|
|
if cfg.StopOnNotEnough {
|
|
state.markDone(account.UID, index)
|
|
result.Done = true
|
|
}
|
|
case CategoryOffShelf, CategorySoldOut:
|
|
if cfg.StopOnSoldOut {
|
|
state.markTargetDone(index)
|
|
result.Done = true
|
|
} else {
|
|
retryAt := now.Add(cfg.ErrorBackoff)
|
|
state.setRetryAfter(account.UID, index, retryAt)
|
|
result.RetryAfter = retryAt.UnixMilli()
|
|
}
|
|
case CategoryTooFrequent:
|
|
retryAt := now.Add(cfg.TooFrequentBackoff)
|
|
state.setRetryAfter(account.UID, index, retryAt)
|
|
result.RetryAfter = retryAt.UnixMilli()
|
|
case CategoryError:
|
|
retryAt := now.Add(cfg.ErrorBackoff)
|
|
state.setRetryAfter(account.UID, index, retryAt)
|
|
result.RetryAfter = retryAt.UnixMilli()
|
|
case CategoryFailed:
|
|
retryAt := now.Add(cfg.ErrorBackoff)
|
|
state.setRetryAfter(account.UID, index, retryAt)
|
|
result.RetryAfter = retryAt.UnixMilli()
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (r *Runner) refreshAfterSuccess(client huya.ActivityClient, account Account, target Target, index int, loop int, cfg Config, result Result, emit func(Result)) {
|
|
if !cfg.RefreshAfterSuccess || emit == nil || result.Category != CategorySuccess {
|
|
return
|
|
}
|
|
go func() {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
score, err := client.QueryUserScore(ctx, account.UID, account.Cookie, cfg.SID)
|
|
scoreResult := Result{
|
|
Loop: loop,
|
|
UID: account.UID,
|
|
TargetIndex: index,
|
|
Target: target,
|
|
Action: "refresh_score",
|
|
Category: CategoryReady,
|
|
Message: "刷新积分完成",
|
|
Score: score,
|
|
}
|
|
if err != nil {
|
|
scoreResult.Category = CategoryError
|
|
scoreResult.Message = "刷新积分失败"
|
|
scoreResult.Error = err.Error()
|
|
} else if score != nil {
|
|
scoreResult.Message = fmt.Sprintf("剩余积分: %d", score.AvailableScore())
|
|
}
|
|
emit(scoreResult)
|
|
|
|
records, err := client.GetUserPrizeRecords(ctx, account.UID, account.Cookie, cfg.SID)
|
|
recordResult := Result{
|
|
Loop: loop,
|
|
UID: account.UID,
|
|
TargetIndex: index,
|
|
Target: target,
|
|
Action: "refresh_records",
|
|
Category: CategoryReady,
|
|
Message: "刷新兑换记录完成",
|
|
Records: records,
|
|
}
|
|
if err != nil {
|
|
recordResult.Category = CategoryError
|
|
recordResult.Message = "刷新兑换记录失败"
|
|
recordResult.Error = err.Error()
|
|
}
|
|
emit(recordResult)
|
|
}()
|
|
}
|
|
|
|
func (r *Runner) sharedList(ctx context.Context, client huya.ActivityClient, accounts []Account, targets []Target, cfg Config, state *runState) *listSnapshot {
|
|
if !cfg.SharePrizeList || len(accounts) == 0 {
|
|
return nil
|
|
}
|
|
needsList := false
|
|
for _, account := range accounts {
|
|
for _, index := range state.activeTargets(account.UID, len(targets), time.Now()) {
|
|
if !shouldDirectExchange(targets[index], cfg) {
|
|
needsList = true
|
|
break
|
|
}
|
|
}
|
|
if needsList {
|
|
break
|
|
}
|
|
}
|
|
if !needsList {
|
|
return nil
|
|
}
|
|
resp, err := client.GetActPrizeList(ctx, accounts[0].UID, accounts[0].Cookie, cfg.SID)
|
|
return &listSnapshot{resp: resp, err: err}
|
|
}
|
|
|
|
func (r *Runner) client() huya.ActivityClient {
|
|
if r.Client != nil {
|
|
return r.Client
|
|
}
|
|
return huya.NewHTTPClient(nil)
|
|
}
|
|
|
|
func newRunState(accounts []Account, targetCount int) *runState {
|
|
state := &runState{
|
|
accounts: make(map[int64][]targetState, len(accounts)),
|
|
targetDone: make([]bool, targetCount),
|
|
}
|
|
for _, account := range accounts {
|
|
state.accounts[account.UID] = make([]targetState, targetCount)
|
|
}
|
|
return state
|
|
}
|
|
|
|
func (s *runState) activeTargets(uid int64, targetCount int, now time.Time) []int {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
items := s.accounts[uid]
|
|
active := make([]int, 0, targetCount)
|
|
for i := 0; i < targetCount; i++ {
|
|
if i < len(s.targetDone) && s.targetDone[i] {
|
|
continue
|
|
}
|
|
if i >= len(items) || items[i].Done {
|
|
continue
|
|
}
|
|
if !items[i].RetryAfter.IsZero() && items[i].RetryAfter.After(now) {
|
|
continue
|
|
}
|
|
active = append(active, i)
|
|
}
|
|
return active
|
|
}
|
|
|
|
func (s *runState) markDone(uid int64, index int) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if items := s.accounts[uid]; index >= 0 && index < len(items) {
|
|
items[index].Done = true
|
|
items[index].RetryAfter = time.Time{}
|
|
s.accounts[uid] = items
|
|
}
|
|
}
|
|
|
|
func (s *runState) markTargetDone(index int) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if index >= 0 && index < len(s.targetDone) {
|
|
s.targetDone[index] = true
|
|
}
|
|
}
|
|
|
|
func (s *runState) setRetryAfter(uid int64, index int, when time.Time) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if items := s.accounts[uid]; index >= 0 && index < len(items) {
|
|
items[index].RetryAfter = when
|
|
s.accounts[uid] = items
|
|
}
|
|
}
|
|
|
|
func (s *runState) allDone() bool {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
for index, globallyDone := range s.targetDone {
|
|
if globallyDone {
|
|
continue
|
|
}
|
|
for _, items := range s.accounts {
|
|
if index < len(items) && !items[index].Done {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func normalizeConfig(cfg Config) Config {
|
|
if cfg.Loops <= 0 {
|
|
cfg.Loops = 1
|
|
}
|
|
if cfg.Delay <= 0 {
|
|
cfg.Delay = 150 * time.Millisecond
|
|
}
|
|
if cfg.Concurrency <= 0 {
|
|
cfg.Concurrency = 1
|
|
}
|
|
if cfg.TooFrequentBackoff <= 0 {
|
|
cfg.TooFrequentBackoff = maxDuration(cfg.Delay*2, 300*time.Millisecond)
|
|
}
|
|
if cfg.ErrorBackoff <= 0 {
|
|
cfg.ErrorBackoff = maxDuration(cfg.Delay, 300*time.Millisecond)
|
|
}
|
|
if !cfg.ContinueOnNotEnough {
|
|
cfg.StopOnNotEnough = true
|
|
}
|
|
if !cfg.ContinueOnSoldOut {
|
|
cfg.StopOnSoldOut = true
|
|
}
|
|
if !cfg.ListBeforeExchange {
|
|
cfg.DirectExchange = true
|
|
}
|
|
return cfg
|
|
}
|
|
|
|
func normalizeTargets(targets []Target) []Target {
|
|
out := make([]Target, 0, len(targets))
|
|
seen := map[string]bool{}
|
|
for _, target := range targets {
|
|
target.Name = strings.TrimSpace(target.Name)
|
|
if target.PID <= 0 && target.Name == "" && !target.Auto {
|
|
continue
|
|
}
|
|
key := targetKey(target)
|
|
if seen[key] {
|
|
continue
|
|
}
|
|
seen[key] = true
|
|
out = append(out, target)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func shouldDirectExchange(target Target, cfg Config) bool {
|
|
return cfg.DirectExchange && !cfg.ListBeforeExchange && target.PID > 0 && !target.Auto
|
|
}
|
|
|
|
func matchPrize(prizes []huya.PrizeInfo, target Target) *huya.PrizeInfo {
|
|
if target.Auto {
|
|
return autoPrize(prizes, target)
|
|
}
|
|
for i := range prizes {
|
|
if target.PID > 0 && int32(prizes[i].ID) != target.PID {
|
|
continue
|
|
}
|
|
if target.PID == 0 && target.Name != "" && !strings.Contains(prizes[i].Name, target.Name) {
|
|
continue
|
|
}
|
|
if target.MaxPrice > 0 && prizes[i].Price() > target.MaxPrice {
|
|
continue
|
|
}
|
|
if target.MinLeft > 0 && prizes[i].EstimatedLeft() < target.MinLeft {
|
|
continue
|
|
}
|
|
return &prizes[i]
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func autoPrize(prizes []huya.PrizeInfo, target Target) *huya.PrizeInfo {
|
|
var fallback *huya.PrizeInfo
|
|
for i := range prizes {
|
|
if target.MaxPrice > 0 && prizes[i].Price() > target.MaxPrice {
|
|
continue
|
|
}
|
|
if target.MinLeft > 0 && prizes[i].EstimatedLeft() < target.MinLeft {
|
|
continue
|
|
}
|
|
if fallback == nil {
|
|
fallback = &prizes[i]
|
|
}
|
|
if prizes[i].EstimatedLeft() > 0 {
|
|
return &prizes[i]
|
|
}
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func ClassifyExchange(resp *huya.ScoreExchangePrizeResp, err error) string {
|
|
if err != nil {
|
|
return CategoryError
|
|
}
|
|
if resp == nil {
|
|
return CategoryFailed
|
|
}
|
|
if resp.Success() {
|
|
return CategorySuccess
|
|
}
|
|
msg := exchangeMessage(resp)
|
|
switch {
|
|
case containsAny(msg, "操作频繁", "太频繁", "稍后再试"):
|
|
return CategoryTooFrequent
|
|
case containsAny(msg, "暂未开放", "未开放", "未开始", "活动未开始"):
|
|
return CategoryNotOpen
|
|
case containsAny(msg, "已抢完", "已兑完", "库存不足", "没有库存", "领完了"):
|
|
return CategorySoldOut
|
|
case containsAny(msg, "货币不足", "积分不足", "余额不足", "点券不足", "不足"):
|
|
return CategoryNotEnough
|
|
case containsAny(msg, "已兑换", "重复兑换", "已领取", "超过兑换次数", "达到上限", "限购"):
|
|
return CategoryAlreadyDone
|
|
case containsAny(msg, "奖品已下架", "已下架", "奖品不存在", "活动不存在"):
|
|
return CategoryOffShelf
|
|
case resp.Status != 0:
|
|
return CategoryFailed
|
|
default:
|
|
return CategoryFailed
|
|
}
|
|
}
|
|
|
|
func exchangeMessage(resp *huya.ScoreExchangePrizeResp) string {
|
|
if resp == nil {
|
|
return ""
|
|
}
|
|
for _, text := range []string{
|
|
resp.Msg,
|
|
resp.ActPreCondition.Tip,
|
|
resp.ActPreCondition.ButtonDesc,
|
|
resp.ExchangeInfo.Desc,
|
|
} {
|
|
if strings.TrimSpace(text) != "" {
|
|
return strings.TrimSpace(text)
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func containsAny(text string, needles ...string) bool {
|
|
for _, needle := range needles {
|
|
if strings.Contains(text, needle) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func targetKey(target Target) string {
|
|
return strings.Join([]string{
|
|
strings.TrimSpace(target.Name),
|
|
int32String(target.PID),
|
|
strconv.FormatBool(target.Auto),
|
|
int64String(target.MaxPrice),
|
|
int64String(target.MinLeft),
|
|
}, "\x00")
|
|
}
|
|
|
|
func int32String(value int32) string {
|
|
return strconv.FormatInt(int64(value), 10)
|
|
}
|
|
|
|
func int64String(value int64) string {
|
|
return strconv.FormatInt(value, 10)
|
|
}
|
|
|
|
func sleepContext(ctx context.Context, d time.Duration) error {
|
|
timer := time.NewTimer(d)
|
|
defer timer.Stop()
|
|
select {
|
|
case <-timer.C:
|
|
return nil
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
|
|
func maxDuration(a, b time.Duration) time.Duration {
|
|
if a > b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|