1
0
Fork 0

init: mvp

This commit is contained in:
Arthur Khachaturov 2024-08-17 01:20:24 +03:00
commit e307989b9f
No known key found for this signature in database
GPG key ID: CAC2B7EB6DF45D55
20 changed files with 835 additions and 0 deletions

View file

@ -0,0 +1,86 @@
package clicker
import (
"net/http"
"net/http/cookiejar"
"net/url"
"sync"
"github.com/wzrayyy/tappin/internal/entity/boosts"
"github.com/wzrayyy/tappin/internal/entity/config"
"github.com/wzrayyy/tappin/internal/entity/upgrades"
"github.com/wzrayyy/tappin/internal/entity/user"
"golang.org/x/sync/errgroup"
)
type Clicker struct {
client *http.Client
authKey string
baseUrl *url.URL
clickerConfig *config.Response
user *user.Response
boosts *boosts.Response
upgrades *upgrades.Response
telegramUserID int
errorGroup errgroup.Group
locks struct {
User sync.RWMutex
Boosts sync.RWMutex
Config sync.RWMutex
Upgrades sync.RWMutex
}
channels struct {
Update EmptyChannel
Tap EmptyChannel
Upgrade EmptyChannel
Global EmptyChannel
}
Config Config
}
type Config struct {
UpdateFrequency int
TapsPerSecond int
TapInterval int
}
func NewClicker(auth_key string, user_id int, config Config) (*Clicker, error) {
c := new(Clicker)
var err error
c.client = new(http.Client)
c.client.Jar = new(cookiejar.Jar)
c.authKey = auth_key
c.telegramUserID = user_id
c.baseUrl, err = url.Parse(apiEndpoint)
if err != nil {
return c, err
}
c.Config = config
return c, c.Update()
}
func (c *Clicker) Tick() {
c.locks.Config.Lock()
c.clickerConfig.Tick()
c.locks.Config.Unlock()
c.locks.User.Lock()
c.user.Tick()
c.locks.User.Unlock()
c.locks.Boosts.Lock()
c.boosts.Tick()
c.locks.Boosts.Unlock()
c.locks.Upgrades.Lock()
c.upgrades.Tick()
c.locks.Upgrades.Unlock()
}

View file

@ -0,0 +1,7 @@
package clicker
const (
apiEndpoint string = "https://api.hamsterkombatgame.io/clicker"
)
type EmptyChannel chan struct{}

View file

@ -0,0 +1,16 @@
package clicker
// this was added in go 1.21, which is not available on Debian 12
func max[T int | int64 | float32 | float64](a T, b T) T {
if a > b {
return a
}
return b
}
func min[T int | int64 | float32 | float64](a T, b T) T {
if a < b {
return a
}
return b
}

View file

@ -0,0 +1,80 @@
package clicker
import (
"encoding/base64"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/wzrayyy/tappin/internal/entity/user"
)
func (c *Clicker) ClaimDailyKeys() error {
m, err := json.Marshal(&claimDailyCipherRequest{
Cipher: base64.StdEncoding.EncodeToString([]byte(strings.Repeat("0", 10) + fmt.Sprintf("|%d", c.telegramUserID))),
})
if err != nil {
return err
}
err = c.requestAndDecode("start-keys-minigame", nil, nil)
if err != nil {
return err
}
return c.requestAndDecode("claim-daily-keys-minigame", m, nil)
}
func (c *Clicker) ClaimDailyCipher() error {
cipher, err := c.clickerConfig.DailyCipher.Decode()
if err != nil {
return err
}
m, err := json.Marshal(claimDailyCipherRequest{
Cipher: cipher,
})
return c.requestAndDecode("claim-daily-cipher", m, nil)
}
func (c *Clicker) Tap(taps int) error {
c.locks.User.RLock()
m, err := json.Marshal(tapRequest{
Count: taps,
AvailableTaps: c.user.AvailableTaps - taps,
Timestamp: time.Now().UnixMilli(),
})
c.locks.User.RUnlock()
if err != nil {
return err
}
var u user.Response
err = c.requestAndDecode("tap", m, &u)
if err != nil {
return err
}
c.locks.User.Lock()
c.user = &u
c.locks.User.Unlock()
return nil
}
func (c *Clicker) BuyBoost(boost_id string) error {
r, err := json.Marshal(buyBoostRequest{
BoostID: boost_id,
Timestamp: time.Now().UnixMilli(),
})
if err != nil {
return err
}
return c.requestAndDecode("buy-boost", r, nil)
}

View file

@ -0,0 +1,50 @@
package clicker
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
func (c *Clicker) requestAndDecode(path string, data []byte, output any) error {
var r io.Reader
r = nil
if data != nil {
r = bytes.NewReader(data)
}
req, err := http.NewRequest("POST", c.baseUrl.JoinPath(path).String(), r)
if err != nil {
return err
}
if data != nil {
req.Header.Set("Content-Type", "application/json")
}
resp, err := c.doRequest(req)
defer resp.Body.Close()
if err != nil {
return err
}
if output != nil {
return json.NewDecoder(resp.Body).Decode(output)
}
return nil
}
func (c *Clicker) doRequest(req *http.Request) (*http.Response, error) {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.authKey))
resp, err := c.client.Do(req)
if resp.StatusCode != 200 {
raw_body, _ := io.ReadAll(resp.Body)
body := string(raw_body)
err = fmt.Errorf("request: Request to %s failed with status code %d\n%s", req.URL.String(), resp.StatusCode, body)
}
return resp, err
}

View file

@ -0,0 +1,25 @@
package clicker
type tapRequest struct {
Count int `json:"count"`
AvailableTaps int `json:"availableTaps"`
Timestamp int64 `json:"timestamp"`
}
type buyUpgradeRequest struct {
UpgradeID string `json:"upgradeId"`
Timestamp int64 `json:"timestamp"`
}
type buyBoostRequest struct {
BoostID string `json:"boostId"`
Timestamp int64 `json:"timestamp"`
}
type checkTaskRequest struct {
TaskID string `json:"taskId"`
}
type claimDailyCipherRequest struct {
Cipher string `json:"cipher"`
}

View file

@ -0,0 +1,56 @@
package clicker
import (
"sync"
"github.com/wzrayyy/tappin/internal/entity/boosts"
"github.com/wzrayyy/tappin/internal/entity/config"
"github.com/wzrayyy/tappin/internal/entity/upgrades"
"github.com/wzrayyy/tappin/internal/entity/user"
"golang.org/x/sync/errgroup"
)
func fetchAndUpdate[T any](c *Clicker, endpoint string, lock *sync.RWMutex, setter func(*T)) error {
var resp T
err := c.requestAndDecode(endpoint, nil, &resp)
if err != nil {
return err
}
lock.Lock()
setter(&resp)
lock.Unlock()
return nil
}
func (c *Clicker) Update() error {
errs := errgroup.Group{}
errs.Go(func() error {
return fetchAndUpdate(c, "sync", &c.locks.User, func(r *user.Response) {
c.user = r
})
})
errs.Go(func() error {
return fetchAndUpdate(c, "config", &c.locks.Config, func(r *config.Response) {
c.clickerConfig = r
})
})
errs.Go(func() error {
return fetchAndUpdate(c, "boosts-for-buy", &c.locks.Boosts, func(r *boosts.Response) {
c.boosts = r
})
})
errs.Go(func() error {
return fetchAndUpdate(c, "upgrades-for-buy", &c.locks.Upgrades, func(r *upgrades.Response) {
c.upgrades = r
})
})
return errs.Wait()
}

View file

@ -0,0 +1,93 @@
package clicker
import (
"fmt"
"time"
)
func (c *Clicker) genericWorker(fn func() error, period *int, done EmptyChannel) error {
return c.genericWorkerWithChannel(func(EmptyChannel) error { return fn() }, period, done)
}
func (c *Clicker) genericWorkerWithChannel(fn func(EmptyChannel) error, period *int, done EmptyChannel) error {
for {
select {
case <-done:
return nil
case <-c.channels.Global:
fmt.Println("got global stop")
if done != nil {
close(done)
}
return nil
case <-time.After(time.Second * time.Duration(*period)):
err := fn(done)
if err != nil {
close(c.channels.Global)
return err
}
}
}
}
func (c *Clicker) tapWorker() error {
return c.genericWorkerWithChannel(func(done EmptyChannel) error {
c.locks.User.RLock()
taps_consumed := int(c.Config.TapInterval) * c.user.EarnPerTap
taps_left := c.user.AvailableTaps - taps_consumed
c.locks.User.RUnlock()
fmt.Println("tap")
if taps_left < 0 {
c.locks.Boosts.RLock()
b := c.boosts.SelectById("BoostFullAvailableTaps").CooldownSeconds
c.locks.Boosts.RUnlock()
if b != nil && *b <= 0 {
fmt.Println("buy taps")
c.BuyBoost("BoostFullAvailableTaps")
} else {
c.locks.User.RLock()
time_sleep := (c.user.MaxTaps - c.user.AvailableTaps) / c.user.RecoverPerSecond
c.locks.User.RUnlock()
select {
case <-c.channels.Global:
return nil
case <-done:
return nil
case <-time.After(time.Duration(time_sleep) * time.Second):
break
}
}
}
return c.Tap(c.Config.TapInterval * c.Config.TapsPerSecond)
}, &c.Config.TapInterval, c.channels.Tap)
}
func (c *Clicker) updateWorker() error {
return c.genericWorker(func() error {
return c.Update()
}, &c.Config.UpdateFrequency, c.channels.Update)
}
func (c *Clicker) tickWorker() error {
interval := 1
return c.genericWorker(func() error {
c.Tick()
return nil
}, &interval, nil)
}
func (c *Clicker) Start() error {
c.errorGroup.Go(c.tapWorker)
c.errorGroup.Go(c.updateWorker)
c.errorGroup.Go(c.tickWorker)
return c.errorGroup.Wait()
}
func (c *Clicker) Stop() error {
close(c.channels.Global)
return c.errorGroup.Wait()
}