init: mvp
This commit is contained in:
commit
e307989b9f
20 changed files with 835 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
json/
|
||||||
|
accounts.json
|
5
go.mod
Normal file
5
go.mod
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
module github.com/wzrayyy/tappin
|
||||||
|
|
||||||
|
go 1.19
|
||||||
|
|
||||||
|
require golang.org/x/sync v0.8.0 // indirect
|
2
go.sum
Normal file
2
go.sum
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||||
|
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
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()
|
||||||
|
}
|
41
internal/entity/boosts/boosts.go
Normal file
41
internal/entity/boosts/boosts.go
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
package boosts
|
||||||
|
|
||||||
|
type Item struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Price int `json:"price"`
|
||||||
|
EarnPerTap int `json:"earnPerTap"`
|
||||||
|
MaxTaps int `json:"maxTaps"`
|
||||||
|
CooldownSeconds *int `json:"cooldownSeconds"`
|
||||||
|
TotalCooldownSeconds *int `json:"totalCooldownSeconds"`
|
||||||
|
Level int `json:"level"`
|
||||||
|
MaxTapsDelta int `json:"maxTapsDelta"`
|
||||||
|
EarnPerTapDelta int `json:"earnPerTapDelta"`
|
||||||
|
MaxLevel *int `json:"maxLevel,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Item) Tick() {
|
||||||
|
if i.TotalCooldownSeconds != nil && *i.TotalCooldownSeconds > 0 {
|
||||||
|
(*i.TotalCooldownSeconds)--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Boosts []*Item
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
Boosts `json:"boostsForBuy"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Boosts) Tick() {
|
||||||
|
for _, el := range *b {
|
||||||
|
el.Tick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Boosts) SelectById(id string) *Item {
|
||||||
|
for _, i := range *b {
|
||||||
|
if i.ID == id {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
54
internal/entity/config/config.go
Normal file
54
internal/entity/config/config.go
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
type Task struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Link *string `json:"link"`
|
||||||
|
Reward int `json:"rewardCoins"`
|
||||||
|
Cycle Cycle `json:"periodicity"`
|
||||||
|
ChannelId *int `json:"channelId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClickerConfig struct {
|
||||||
|
MaxPassive int `json:"maxPassiveDtSeconds"`
|
||||||
|
Tasks []Task `json:"tasks"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DailyCipher struct {
|
||||||
|
Cipher string `json:"cipher"`
|
||||||
|
BonusCoins int `json:"bonusCoins"`
|
||||||
|
IsClaimed bool `json:"isClaimed"`
|
||||||
|
RemainSeconds int `json:"remainSeconds"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DailyKeys struct {
|
||||||
|
StartDate string `json:"startDate"`
|
||||||
|
LevelConfig string `json:"levelConfig"`
|
||||||
|
BonusKeys int `json:"bonusKeys"`
|
||||||
|
IsClaimed bool `json:"isClaimed"`
|
||||||
|
SecondsToNext int `json:"totalSecondsToNextAttempt"`
|
||||||
|
RemainToGuess float32 `json:"remainSecondsToGuess"`
|
||||||
|
RemainSeconds float32 `json:"remainSeconds"`
|
||||||
|
RemainToNext float32 `json:"remainSecondsToNextAttempt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
ClickerConfig ClickerConfig `json:"clickerConfig"`
|
||||||
|
DailyCipher DailyCipher `json:"dailyCipher"`
|
||||||
|
DailyKeys DailyKeys `json:"DailyKeysMiniGame"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *DailyCipher) Tick() {
|
||||||
|
c.RemainSeconds--
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *DailyKeys) Tick() {
|
||||||
|
k.RemainToGuess--
|
||||||
|
k.RemainToNext--
|
||||||
|
k.RemainSeconds--
|
||||||
|
k.SecondsToNext--
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Response) Tick() {
|
||||||
|
r.DailyCipher.Tick()
|
||||||
|
r.DailyKeys.Tick()
|
||||||
|
}
|
8
internal/entity/config/const.go
Normal file
8
internal/entity/config/const.go
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
type Cycle string
|
||||||
|
|
||||||
|
const (
|
||||||
|
Repeatedly Cycle = "repeatedly"
|
||||||
|
Once Cycle = "once"
|
||||||
|
)
|
12
internal/entity/config/decodecipher.go
Normal file
12
internal/entity/config/decodecipher.go
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import "encoding/base64"
|
||||||
|
|
||||||
|
func (c *DailyCipher) Decode() (string, error) {
|
||||||
|
enc, err := base64.StdEncoding.DecodeString(c.Cipher[:3] + c.Cipher[4:])
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(enc), nil
|
||||||
|
}
|
68
internal/entity/upgrades/condition.go
Normal file
68
internal/entity/upgrades/condition.go
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
package upgrades
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Type string
|
||||||
|
|
||||||
|
const (
|
||||||
|
byUpgrade Type = "ByUpgrade"
|
||||||
|
referralCount Type = "ReferralCount"
|
||||||
|
moreReferralCount Type = "MoreReferralsCount"
|
||||||
|
subscribeTelegramChannel Type = "SubscribeTelegramChannel"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Condition struct {
|
||||||
|
Type Type
|
||||||
|
ByUpgrade *ByUpgrade
|
||||||
|
ReferralCount *ReferralCount
|
||||||
|
MoreReferralCount *MoreReferralCount
|
||||||
|
SubscribeTelegramChannel *SubscribeTelegramChannel
|
||||||
|
}
|
||||||
|
|
||||||
|
type ByUpgrade struct {
|
||||||
|
Level int
|
||||||
|
UpgradeID string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReferralCount struct {
|
||||||
|
ReferralCount int
|
||||||
|
}
|
||||||
|
|
||||||
|
type MoreReferralCount struct {
|
||||||
|
MoreReferralCount int
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubscribeTelegramChannel struct {
|
||||||
|
ChannelID int
|
||||||
|
Link string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Condition) UnmarshalJSON(d []byte) error {
|
||||||
|
type tmpStruct struct {
|
||||||
|
Type Type `json:"_type"`
|
||||||
|
ByUpgrade
|
||||||
|
ReferralCount
|
||||||
|
MoreReferralCount
|
||||||
|
SubscribeTelegramChannel
|
||||||
|
}
|
||||||
|
var tmp = new(tmpStruct)
|
||||||
|
if err := json.Unmarshal(d, tmp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Type = tmp.Type
|
||||||
|
switch tmp.Type {
|
||||||
|
case byUpgrade:
|
||||||
|
c.ByUpgrade = &tmp.ByUpgrade
|
||||||
|
case referralCount:
|
||||||
|
c.ReferralCount = &tmp.ReferralCount
|
||||||
|
case moreReferralCount:
|
||||||
|
c.MoreReferralCount = &tmp.MoreReferralCount
|
||||||
|
case subscribeTelegramChannel:
|
||||||
|
c.SubscribeTelegramChannel = &tmp.SubscribeTelegramChannel
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
37
internal/entity/upgrades/price.go
Normal file
37
internal/entity/upgrades/price.go
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
package upgrades
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (u *Upgrades) RecurseUnavailable(item *Item) []*Item {
|
||||||
|
if item.IsAvailable || (item.Condition == nil || item.Condition.ByUpgrade == nil) {
|
||||||
|
return []*Item{item}
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(u.RecurseUnavailable((*u)[item.Condition.ByUpgrade.UpgradeID]), item)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Response) UnmarshalJSON(data []byte) error {
|
||||||
|
type tempType Response
|
||||||
|
var temp *tempType = (*tempType)(r)
|
||||||
|
|
||||||
|
err := json.Unmarshal(data, temp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Upgrades = make(Upgrades)
|
||||||
|
|
||||||
|
for _, i := range r.UpgradesArray {
|
||||||
|
r.Upgrades[i.ID] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Response) Tick() {
|
||||||
|
for _, e := range r.UpgradesArray {
|
||||||
|
e.Tick()
|
||||||
|
}
|
||||||
|
}
|
76
internal/entity/upgrades/upgrades.go
Normal file
76
internal/entity/upgrades/upgrades.go
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
package upgrades
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Item struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Price int `json:"price"`
|
||||||
|
ProfitPerHour int `json:"profitPerHour"`
|
||||||
|
Condition *Condition `json:"condition,omitempty"`
|
||||||
|
Section string `json:"section"`
|
||||||
|
Level int `json:"level"`
|
||||||
|
CurrentProfit int `json:"currentProfitPerHour"`
|
||||||
|
ProfitDelta int `json:"profitPerHourDelta"`
|
||||||
|
IsAvailable bool `json:"isAvailable"`
|
||||||
|
IsExpired bool `json:"isExpired"`
|
||||||
|
Cooldown *int `json:"cooldownSeconds"`
|
||||||
|
TotalCooldown *int `json:"totalCooldownSeconds"`
|
||||||
|
ReleaseAt *string `json:"releaseAt"`
|
||||||
|
ExpiresAt *string `json:"expiresAt"`
|
||||||
|
MaxLevel *int `json:"maxLevel"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Item) Tick() {
|
||||||
|
if i.ExpiresAt != nil && !i.IsExpired && *i.ExpiresAt != "" {
|
||||||
|
expire_time, _ := time.Parse(time.RFC3339, *i.ExpiresAt)
|
||||||
|
if expire_time.Sub(time.Now()) < 0 {
|
||||||
|
i.IsExpired = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if i.Cooldown != nil && *i.Cooldown > 0 {
|
||||||
|
(*i.Cooldown)--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Section struct {
|
||||||
|
Name string `json:"section"`
|
||||||
|
IsAvailable bool `json:"isAvailable"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DailyCombo struct {
|
||||||
|
UpgradeIDs []string `json:"upgradeIds"`
|
||||||
|
BonusCoins int `json:"bonusCoins"`
|
||||||
|
IsClaimed bool `json:"isClaimed"`
|
||||||
|
RemainingSeconds int `json:"remainSeconds"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Upgrades map[string]*Item
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
UpgradesArray []*Item `json:"upgradesForBuy"`
|
||||||
|
Upgrades Upgrades `json:"-"`
|
||||||
|
Sections []Section `json:"sections"`
|
||||||
|
DailyCombo DailyCombo `json:"dailyCombo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Item) PriceByLevel(level int) int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Item) ProfitDeltaByLevel(level int) int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Item) CooldownByLevel(level int) int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// def profit_delta_by_level(self, level: int) -> int:
|
||||||
|
// return round(self.profit_per_hour_delta * 1.07 ** level)
|
||||||
|
//
|
||||||
|
// def price_by_level(self, level: int) -> int:
|
||||||
|
// return round(self.price * 1.05 ** ((level + 3) * level / 2))
|
||||||
|
//
|
||||||
|
// def cooldown_by_level(self, level: int) -> int:
|
||||||
|
// return self.cooldown_seconds * 2 ** level if self.cooldown_seconds else 0
|
64
internal/entity/user/user.go
Normal file
64
internal/entity/user/user.go
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
package user
|
||||||
|
|
||||||
|
type Boost struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Level int `json:"level"`
|
||||||
|
LastUpgradeTime int `json:"lastUpgradeAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Boosts struct {
|
||||||
|
BoostMaxTaps Boost `json:"boostMaxTaps"`
|
||||||
|
BoostEarnPerTap Boost `json:"boostEarnPerTap"`
|
||||||
|
BoostFullAvailableTaps Boost `json:"boostFullAvailableTaps"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Upgrade struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Level int `json:"level"`
|
||||||
|
LastUpgradeAt int `json:"lastUpgradeAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type StreakDays struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
CompletedAt string `json:"completedAt"`
|
||||||
|
Days int `json:"days"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Tasks struct {
|
||||||
|
StreakDays StreakDays `json:"streak_days"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClickerUser struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
TotalCoins float32 `json:"totalCoins"`
|
||||||
|
Balance float32 `json:"balanceCoins"`
|
||||||
|
Level int `json:"level"`
|
||||||
|
AvailableTaps int `json:"availableTaps"`
|
||||||
|
Boosts Boosts `json:"boosts"`
|
||||||
|
Tasks Tasks `json:"tasks"`
|
||||||
|
ReferralsCount int `json:"referralsCount"`
|
||||||
|
MaxTaps int `json:"maxTaps"`
|
||||||
|
EarnPerTap int `json:"earnPerTap"`
|
||||||
|
PassivePerSecond float32 `json:"earnPassivePerSec"`
|
||||||
|
PassivePerHour float32 `json:"earnPassivePerHour"`
|
||||||
|
RecoverPerSecond int `json:"tapsRecoverPerSec"`
|
||||||
|
CreatedAt string `json:"createdAt"`
|
||||||
|
BalanceTickets int `json:"balanceTickets"`
|
||||||
|
TotalKeys int `json:"totalKeys"`
|
||||||
|
BalanceKeys int `json:"balanceKeys"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ClickerUser) Tick() {
|
||||||
|
if u.AvailableTaps+u.RecoverPerSecond < u.MaxTaps {
|
||||||
|
u.AvailableTaps += u.RecoverPerSecond
|
||||||
|
} else {
|
||||||
|
u.AvailableTaps = u.MaxTaps
|
||||||
|
}
|
||||||
|
|
||||||
|
u.Balance += u.PassivePerSecond
|
||||||
|
u.TotalCoins += u.PassivePerSecond
|
||||||
|
}
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
ClickerUser `json:"clickerUser"`
|
||||||
|
}
|
53
main.go
Normal file
53
main.go
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
_ "sync"
|
||||||
|
"time"
|
||||||
|
_ "time"
|
||||||
|
|
||||||
|
"github.com/wzrayyy/tappin/internal/clicker"
|
||||||
|
)
|
||||||
|
|
||||||
|
func die_if(err error) {
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type account struct {
|
||||||
|
Name string
|
||||||
|
Phone string
|
||||||
|
AuthKey string
|
||||||
|
UserID int
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
data, err := os.ReadFile("./accounts.json")
|
||||||
|
die_if(err)
|
||||||
|
|
||||||
|
var accounts []account
|
||||||
|
json.Unmarshal(data, &accounts)
|
||||||
|
account := accounts[0]
|
||||||
|
|
||||||
|
c, err := clicker.NewClicker(account.AuthKey, account.UserID, clicker.Config{
|
||||||
|
TapsPerSecond: 0,
|
||||||
|
TapInterval: 2,
|
||||||
|
UpdateFrequency: 3,
|
||||||
|
})
|
||||||
|
die_if(err)
|
||||||
|
fmt.Println("a")
|
||||||
|
|
||||||
|
go c.Start()
|
||||||
|
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
|
||||||
|
c.Config.TapInterval = 1
|
||||||
|
time.Sleep(10 * time.Second)
|
||||||
|
|
||||||
|
err = c.Stop()
|
||||||
|
|
||||||
|
die_if(err)
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue