init: mvp
This commit is contained in:
commit
e307989b9f
20 changed files with 835 additions and 0 deletions
86
internal/clicker/clicker.go
Normal file
86
internal/clicker/clicker.go
Normal 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()
|
||||
}
|
7
internal/clicker/const.go
Normal file
7
internal/clicker/const.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
package clicker
|
||||
|
||||
const (
|
||||
apiEndpoint string = "https://api.hamsterkombatgame.io/clicker"
|
||||
)
|
||||
|
||||
type EmptyChannel chan struct{}
|
16
internal/clicker/helpers.go
Normal file
16
internal/clicker/helpers.go
Normal 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
|
||||
}
|
80
internal/clicker/methods.go
Normal file
80
internal/clicker/methods.go
Normal 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)
|
||||
}
|
50
internal/clicker/request.go
Normal file
50
internal/clicker/request.go
Normal 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
|
||||
}
|
25
internal/clicker/request_types.go
Normal file
25
internal/clicker/request_types.go
Normal 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"`
|
||||
}
|
56
internal/clicker/update.go
Normal file
56
internal/clicker/update.go
Normal 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()
|
||||
}
|
93
internal/clicker/workers.go
Normal file
93
internal/clicker/workers.go
Normal 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()
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue