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

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
json/
accounts.json

5
go.mod Normal file
View 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
View 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=

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()
}

View 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
}

View 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()
}

View file

@ -0,0 +1,8 @@
package config
type Cycle string
const (
Repeatedly Cycle = "repeatedly"
Once Cycle = "once"
)

View 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
}

View 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
}

View 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()
}
}

View 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

View 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
View 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)
}