Browse Source

Initial commit

TooManySugar 3 years ago
commit
6ab6a99648
14 changed files with 1194 additions and 0 deletions
  1. 8 0
      .gitignore
  2. 24 0
      README.md
  3. 59 0
      code/api_structs.go
  4. 42 0
      code/connection.go
  5. 336 0
      code/handlers.go
  6. 52 0
      code/logger.go
  7. 94 0
      code/main.go
  8. 83 0
      code/message_handler.go
  9. 41 0
      code/models.go
  10. 61 0
      code/settings.go
  11. 45 0
      code/unmasrshal_url.go
  12. 318 0
      code/update_data.go
  13. 16 0
      go.mod
  14. 15 0
      go.sum

+ 8 - 0
.gitignore

@@ -0,0 +1,8 @@
+# .log files
+*.log
+
+# .json files
+config.json
+
+# Visual Studio Code directory
+.vscode/

+ 24 - 0
README.md

@@ -0,0 +1,24 @@
+
+## Progress
+
+- [x] AppSettings.cs - settings.go
+- [x] DataUpdater.cs Updater in main.go
+- [x] EFDatabase.cs - Not needed: analog - database\models.go
+- [x] JsonDownloader.cs - ethermine_api\unmasrshal_url.go
+    - [x] JsonCurrentStats.cs - in ethermine_api\api_structs.go
+    - [x] JsonNetworkStats.cs - in ethermine_api\api_structs.go
+    - [x] JsonPayouts.cs - in ethermine_api\api_structs.go
+    - [x] JsonWorker.cs - in ethermine_api\api_structs.go
+- [x] Logger.cs
+- [ ] Program.cs 
+	- [ ] Main - main.go Fully ported needs testing
+	- [ ] BotOnMessage - Listener in main.go
+	- [x] GetActualRate - in main.go
+	- [x] GetActualData - in main.go
+	- [ ] GetActualDataFromDatabase - 
+	- [ ] AddUser
+	- [ ] SetWallet
+    - [ ] DeleteUser - see TODO
+	- [x] SendHelp - in handlers.go
+    - [ ] GetLastPayout
+- [ ] UpdateData.cs - database\update_data.go Fully ported needs testing

+ 59 - 0
code/api_structs.go

@@ -0,0 +1,59 @@
+package main
+
+type JsonCurrentStats struct {
+	Status string
+	Data   struct {
+		Time             int64
+		LastSeen         int
+		ReportedHashrate int64
+		CurrentHashrate  float64
+		ValidShares      int
+		InvalidShares    int
+		StaleShares      int
+		AverageHashrate  float64
+		ActiveWorkers    int
+		Unpaid           int64
+		Unconfirmed      interface{}
+		CoinsPerMin      float64
+		UsdPerMin        float64
+		BtcPerMin        float64
+	}
+}
+
+type JsonNetworkStats struct {
+	Status string
+	Data   struct {
+		Time       int
+		BlockTime  float64
+		Difficulty int64
+		Hashrate   int64
+		Usd        float64
+		Btc        float64
+	}
+}
+
+type JsonPayouts struct {
+	Status string
+	Data   []struct {
+		Start  int
+		End    int
+		Amount int64
+		TxHash string
+		PaidOn int64
+	}
+}
+
+type JsonWorker struct {
+	Status string
+	Data   []struct {
+		Worker           string
+		Time             int64
+		LastSeen         int64
+		ReportedHashrate int64
+		CurrentHashrate  float64
+		ValidShares      int
+		InvalidShares    int
+		StaleShares      int
+		AverageHashrate  float64
+	}
+}

+ 42 - 0
code/connection.go

@@ -0,0 +1,42 @@
+package main
+
+import (
+	"fmt"
+
+	"gorm.io/gorm"
+	"gorm.io/driver/mysql"
+)
+
+type ConnectionProperties struct {
+	User string
+	Pass string
+	Host string
+	Db   string
+}
+
+type Connection struct {
+	GormDb         *gorm.DB
+	DbOpenStr      string
+	ConnProperties ConnectionProperties
+}
+
+func (c *Connection) Init() {
+	c.ConnProperties.User = appSettings.DbUser
+	c.ConnProperties.Pass = appSettings.DbPassword
+	c.ConnProperties.Host = appSettings.DbHost
+	c.ConnProperties.Db = appSettings.DbName
+
+	c.DbOpenStr = fmt.Sprintf("%s:%s@tcp(%s)/%s", c.ConnProperties.User, c.ConnProperties.Pass, c.ConnProperties.Host, c.ConnProperties.Db)
+	LogInfo.Println("Connecting with:")
+	LogInfo.Println(c.DbOpenStr)
+
+	gormDb, err := gorm.Open(mysql.Open(c.DbOpenStr), &gorm.Config{})
+	if err != nil {
+		LogError.Println("failed to connect database")
+		LogError.Println(err)
+	} else {
+		LogInfo.Println("Connected to database using gorm")
+	}
+
+	c.GormDb = gormDb
+}

+ 336 - 0
code/handlers.go

@@ -0,0 +1,336 @@
+// Response creators 
+package main
+
+import (
+	"fmt"
+	"strings"
+	"time"
+	"math"
+
+	tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
+)
+
+func (c *Connection) AddUser(reqChat *tgbotapi.Chat) (string, error) {
+	// Check if user already registered
+	var userCount int64
+	result := c.GormDb.Model(&User{}).
+		Where("chat_id = ?", reqChat.ID).
+		Count(&userCount)
+	if result.Error != nil {
+		return "", fmt.Errorf("failed creating user: %w", result.Error)
+	}
+	if userCount != 0 {
+		return "Already registered", nil
+	}
+
+	// Adding new user to database
+	newUser := User{ChatId: reqChat.ID}
+	result = c.GormDb.Create(newUser)
+	if result.Error != nil {
+		return "", fmt.Errorf("failed creating user: %w", result.Error)
+	}
+	LogInfo.Println("User was created:", newUser)
+	return "Added new user to database. Now you can connect your miner wallet using command /setwallet <address>", nil
+}
+
+
+func (c *Connection) SetWallet(reqMsg *tgbotapi.Message) (string, error) {
+	// Check if user already registered
+	var userCount int64
+	result := c.GormDb.Model(&User{}).
+		Where("chat_id = ?", reqMsg.Chat.ID).
+		Count(&userCount)
+	if result.Error != nil {
+		return "", fmt.Errorf("failed wallet set: %w", result.Error)
+	}
+	if userCount == 0 {
+		return "You are not registered! Type /start first!", nil
+	}
+
+	// *Strange* check for length of predifined eth wallet hash
+	if len(strings.Split(reqMsg.Text, " ")[1]) > 2 {
+		// Check if wallet hash typed with 0x
+		// TODO Should be: starts with '0x' or in format 0x....
+		if strings.Contains(strings.Split(reqMsg.Text, " ")[1], "0x") {
+			// TODO replace inefficient branching
+			result = c.GormDb.Model(&User{}).
+				Where("chat_id = ?", reqMsg.Chat.ID).
+				Update("wallet", strings.Split(reqMsg.Text, " ")[1])
+			if result.Error != nil {
+				return "", fmt.Errorf("failed querying user: %w", result.Error)
+			}
+		} else {
+			result = c.GormDb.Model(&User{}).
+				Where("chat_id = ?", reqMsg.Chat.ID).
+				Update("wallet", "0x"+strings.Split(reqMsg.Text, " ")[1])
+			if result.Error != nil {
+				return "", fmt.Errorf("failed querying user: %w", result.Error)
+			}
+		}
+		LogInfo.Printf("Updated wallet of user %d to %s\n", reqMsg.Chat.ID, strings.Split(reqMsg.Text, " ")[1])
+		// TODO     this is ▼▼  lame bro
+		return fmt.Sprintf("0x%s set!", strings.Split(reqMsg.Text, " ")[1]), nil
+	} else {
+		return "Too short!", nil
+	}
+}
+
+func (c *Connection) DeleteUser(reqChat *tgbotapi.Chat) (string, error) {
+	// Check for user existance
+	var userCount int64
+	result := c.GormDb.Model(&User{}).
+		Where("chat_id = ?", reqChat.ID).
+		Count(&userCount)
+	if result.Error != nil {
+		return "", fmt.Errorf("failed creating user: %w", result.Error)
+	}
+	if userCount == 0 {
+		return "Already deleted", nil
+	}
+
+	// Taking target for deletion
+	var user User
+	result = c.GormDb.Take(&user, "chat_id = ?", reqChat.ID)
+	if result.Error != nil {
+		return "", fmt.Errorf("failed querying user: %w", result.Error)
+	}
+
+	var walletHoldersCount int64
+	result = c.GormDb.Model(&User{}).
+		Where("wallet = ?", user.Wallet).
+		Count(&walletHoldersCount)
+	if result.Error != nil {
+		return "", fmt.Errorf("failed querying user: %w", result.Error)
+	}
+
+	// TODO Remove code repition
+	if walletHoldersCount > 1 {
+		// Wallet in multiple chats
+		result = c.GormDb.Where("chat_id = ?", reqChat.ID).Delete(User{})
+		if result.Error != nil {
+			return "", fmt.Errorf("failed deleting user: %w", result.Error)
+		}
+	} else {
+		// Wallet in single chat
+		result = c.GormDb.Where("chat_id = ?", reqChat.ID).Delete(User{})
+		if result.Error != nil {
+			return "", fmt.Errorf("failed deleting user: %w", result.Error)
+		}
+		// TODO Delete from workers, miners, payouts
+	}
+
+	LogInfo.Println("User was deleted:", user)
+	return "Done!", nil
+}
+
+func (c *Connection) GetActualDataFromDatabase(chatId int64) string {
+	errMsgToUser := "An exception occurred on sending messages while executing /actual command"
+	var wallet string
+	result := c.GormDb.Model(&User{}).
+		Where("chat_id = ?", chatId).
+		Select("wallet").
+		Scan(&wallet)
+	if result.Error != nil {
+		LogError.Println(result.Error)
+		return errMsgToUser
+	}
+	if wallet == "" {
+		return "Set wallet at first! Use /setwallet"
+	}
+
+	var lastMinerRecord Miner
+	result = c.GormDb.
+		Where(&Miner{Wallet: wallet}).
+		Order("time desc").
+		Limit(1).
+		Find(&lastMinerRecord)
+	if result.Error != nil {
+		LogError.Println(result.Error)
+		return "No miner data for now!"
+	}
+
+	var lastTime int64
+	result = c.GormDb.Model(&Worker{}).
+		Where("wallet = ?", wallet).
+		Select("MAX(time)").
+		Scan(&lastTime)
+	if result.Error != nil {
+		LogError.Println(result.Error)
+		return errMsgToUser
+	}
+
+	var lastWorkerRecord []Worker
+	result = c.GormDb.
+		Where(&Worker{Wallet: wallet, Time: lastTime}).
+		Find(&lastWorkerRecord)
+	if result.Error != nil {
+		LogError.Println(result.Error)
+		return errMsgToUser
+	}
+
+	if len(lastWorkerRecord) == 0 {
+		return "No workers data for now!"
+	}
+
+	msgText := fmt.Sprintf("Miner %s stats:\n", lastMinerRecord.Wallet)
+	msgText += fmt.Sprintf("Updated: %s\n", time.Unix(lastMinerRecord.Time, 0).Format("Monday, January 2, 2006 3:04 PM"))
+	msgText += fmt.Sprintf("Reported Hashrate: %.3f MH/s\n", math.Round(float64(lastMinerRecord.ReportedHashrate)/1000)/1000)
+	msgText += fmt.Sprintf("Current Hashrate: %.3f MH/s\n", math.Round(float64(lastMinerRecord.CurrentHashrate)/1000)/1000)
+	msgText += fmt.Sprintf("Valid Shares: %d\n", lastMinerRecord.ValidShares)
+	msgText += fmt.Sprintf("Stale Shares: %d\n", lastMinerRecord.StaleShares)
+	msgText += fmt.Sprintf("Workers: %d\n", lastMinerRecord.Workers)
+	msgText += fmt.Sprintf("Unpaid Balance: %.5f ETH\n", float64(lastMinerRecord.Unpaid)/math.Pow10(18)) //TODO ETH = AppSettings.currency
+
+	msg := tgbotapi.NewMessage(chatId, msgText)
+	myBotClient.Send(msg)
+	LogInfo.Printf("Replied with: %s", strings.ReplaceAll(msg.Text, "\n", "\\n"))
+
+	for i := range lastWorkerRecord {
+		// Per worker message
+		time.Sleep(1 * time.Second)
+
+		// Anti spam filter
+		if i != 0 && i%20 == 0 { //TODO SOMETIHNG BETTER THAN USUAL TIMER
+			time.Sleep(30 * time.Second)
+		}
+
+		msgText := fmt.Sprintf("Worker %s stats:\n", lastWorkerRecord[i].Worker)
+		msgText += fmt.Sprintf("Updated: %s\n", time.Unix(lastWorkerRecord[i].Time, 0).Format("Monday, January 2, 2006 3:04 PM"))
+		msgText += fmt.Sprintf("Reported Hashrate: %.3f MH/s\n", math.Round(float64(lastWorkerRecord[i].ReportedHashrate)/1000)/1000)
+		msgText += fmt.Sprintf("Current Hashrate: %.3f MH/s\n", math.Round(float64(lastWorkerRecord[i].CurrentHashrate)/1000)/1000)
+		msgText += fmt.Sprintf("Valid Shares: %d\n", lastWorkerRecord[i].ValidShares)
+		msgText += fmt.Sprintf("Stale Shares: %d\n", lastWorkerRecord[i].StaleShares)
+		msgText += fmt.Sprintf("Unpaid Balance: %.5f ETH\n", float64(lastWorkerRecord[i].WorkerUnpaid)/math.Pow10(18)) //TODO ETH = AppSettings.currency
+		msg := tgbotapi.NewMessage(chatId, msgText)
+		myBotClient.Send(msg)
+		LogInfo.Printf("Replied with: %s", strings.ReplaceAll(msg.Text, "\n", "\\n"))
+	}
+
+	return "nice"
+}
+
+func (c *Connection) GetLastPayout(chatId int64) string {
+	errMsgToUser := "An exception occurred on sending messages while executing /actual command"
+
+	var wallet string
+	result := c.GormDb.Model(&User{}).
+		Where(User{ChatId: chatId}).
+		Select("wallet").
+		Scan(&wallet)
+	if result.Error != nil {
+		LogError.Println(result.Error)
+		return errMsgToUser
+	}
+	if wallet == "" {
+		return "Set wallet at first! Use /setwallet"
+	}
+
+	var payoutTime int64
+	result = c.GormDb.Model(&Payout{}).
+		Where(&Payout{Wallet: wallet}).
+		Order("time desc").
+		Limit(1).
+		Select("time").
+		Scan(&payoutTime)
+	if result.Error != nil {
+		LogInfo.Printf("No payouts data for %s! Time is set to 0!", wallet)
+		LogError.Println(result.Error)
+		payoutTime = 0
+	}
+
+	// NOTE Pointless if previous 'if' statement occurs
+	// Must go from the start to line 235
+	var dbPayoutRecords []Payout
+	result = c.GormDb.
+		Where(&Payout{Wallet: wallet, Time: payoutTime}).
+		Find(&dbPayoutRecords)
+	//Line 560 of Program.cs
+	if len(dbPayoutRecords) == 0 {
+		url := fmt.Sprintf("%s/miner/%s/payouts", appSettings.ApiUrl, wallet)
+		var payouts JsonPayouts
+		err := UnmasrshalFromUrl(url, &payouts)
+		if err != nil {
+			LogError.Println(err)
+			return fmt.Sprintf("No payout data for %s", wallet)
+		}
+		lastPayout := payouts.Data[0]
+
+		message := fmt.Sprintf("Payout date: %s\n", time.Unix(lastPayout.PaidOn, 0).Format("Monday, January 2, 2006 3:04 PM"))
+		message += fmt.Sprintf("Amount: %.5f %s\n", float64(lastPayout.Amount)/math.Pow10(18), appSettings.Currency)
+		message += "Data source: Ethermine API \n"
+
+		return message
+	}
+	if result.Error != nil {
+		LogError.Println(result.Error)
+		return errMsgToUser
+	}
+
+	//Line 545 of Program.cs
+	message := fmt.Sprintf("Payout date: %s\n", time.Unix(dbPayoutRecords[0].Time, 0).Format("Monday, January 2, 2006 3:04 PM"))
+	message += fmt.Sprintf("Amount: %.5f ETH\n", float64(dbPayoutRecords[0].Amount)/math.Pow10(18))
+
+	for _, payoutRecord := range dbPayoutRecords {
+		message += fmt.Sprintf("Worker %s paid: %.5f %s\n", //TODO ETH = AppSettings.currency
+			payoutRecord.Worker,
+			float64(payoutRecord.Amount)/math.Pow10(18),
+			appSettings.Currency)
+	}
+
+	message += "Data source: Bot database \n"
+
+	return message
+}
+
+
+
+func GetActualRate() string {
+	url := fmt.Sprintf("%s/networkStats", appSettings.ApiUrl)
+	var networkStats JsonNetworkStats
+	err := UnmasrshalFromUrl(url, &networkStats)
+	if err != nil {
+		return fmt.Sprintf("Error with getting data from %s", appSettings.ApiUrl)
+	}
+	data := networkStats.Data
+	actualRate := fmt.Sprintf("ETH: %.2f\nBTC: %.2f", data.Usd, float64(data.Usd)/float64(data.Btc))
+	return actualRate
+}
+
+func GetActualData(miner string) string {
+	url := fmt.Sprintf("%s/miner/%s/currentStats", appSettings.ApiUrl, miner)
+	var currentStats JsonCurrentStats
+	err := UnmasrshalFromUrl(url, &currentStats)
+	if err != nil {
+		// As in original...
+		return "An exception occurred while executing this command."
+		// return fmt.Sprintf("No data for specified wallet\n")
+	}
+
+	data := currentStats.Data
+
+	actualData := fmt.Sprintf("Stats of miner\n%s\n", miner)
+	actualData += fmt.Sprintf("Updated: %s\n", time.Unix(data.Time, 0).Format("Monday, January 2, 2006 3:04 PM"))
+	actualData += fmt.Sprintf("Reported Hashrate: %.3f MH/s\n", math.Round(float64(data.ReportedHashrate)/1000)/1000)
+	actualData += fmt.Sprintf("Current Hashrate: %.3f MH/s\n", math.Round(data.CurrentHashrate/1000)/1000)
+	actualData += fmt.Sprintf("Valid Shares: %d\n", data.ValidShares)
+	actualData += fmt.Sprintf("Stale Shares: %d\n", data.StaleShares)
+	actualData += fmt.Sprintf("Workers: %d\n", data.ActiveWorkers)
+	actualData += fmt.Sprintf("Unpaid Balance: %.5f ETH\n", float64(data.Unpaid)/math.Pow10(18))
+
+	return actualData
+}
+
+func GetHelp() (help string) {
+	// TODO separate help variable
+	help = "This is bot tracks your miner stats and calculates unpaid per worker.\n"
+	help += "How to start:\n"
+	help += "1) \"/start\" - it will add you to database\n"
+	help += "2) \"/setwallet <wallet>\" - it will set wallet for tracking, bot will start to add info about your miner and workers to database\n"
+	help += "3) \"/actual\" - it will send you up to date data about your worker\n"
+	help += "4) \"/lastpayout\" - it will send you last payout data with calculated worker unpaid for that period if avaliable\n"
+	help += "5) \"/stop\" - it will clear all data about you from database\n"
+	help += "Additional commands:\n"
+	help += "\"/rate\" - get actual ETH and BTC rate from ethermine\n"
+	help += "\"/actual <wallet>\" - get up to date data about any wallet without unpaid per worker\n"
+	return help
+}

+ 52 - 0
code/logger.go

@@ -0,0 +1,52 @@
+package main
+
+import (
+	"io"
+	"log"
+	"os"
+	"time"
+)
+
+type customLogWriter struct {
+	Writer io.Writer
+}
+
+// Initializes Loggers
+func LoggersInit() {
+	fullLogPath := appSettings.FullLogPath
+	errorLogPath := appSettings.ErrorLogPath
+
+	fullLog, err := os.OpenFile(fullLogPath, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0766)
+	if err != nil {
+		log.Panic(err)
+	}
+	// Prints log to os.Stdout, fullLog,
+	fullLogWriter := &customLogWriter{io.MultiWriter(os.Stdout, fullLog)}
+
+	errLog, err := os.OpenFile(errorLogPath, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0766)
+	if err != nil {
+		log.Panic(err)
+	}
+	// Prints log to os.Stdout, fullLog, errLog
+	errLogWriter := &customLogWriter{io.MultiWriter(os.Stdout, fullLog, errLog)}
+
+	LogInfo = log.New(fullLogWriter, "INFO\t", 0)
+	LogWarn = log.New(fullLogWriter, "WARNING\t", 0)
+	LogError = log.New(errLogWriter, "ERROR\t", log.Lshortfile)
+	LogDebug = log.New(fullLogWriter, "DEBUG\t", log.Lshortfile)
+
+}
+
+// Passes message to writer with added current time
+func (writer customLogWriter) Write(message []byte) (int, error) {
+	bytes := append([]byte(time.Now().Format("[01-02-2006 15:04:05] "))[:], message...)
+
+	n, err := writer.Writer.Write(bytes)
+	if err != nil {
+		return n, err
+	}
+	if n != len(bytes) {
+		return n, io.ErrShortWrite
+	}
+	return len(bytes), nil
+}

+ 94 - 0
code/main.go

@@ -0,0 +1,94 @@
+package main
+
+import (
+	"fmt"
+	"log"
+	"bufio"
+	"os"
+	"time"
+
+	tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
+)
+
+
+var (
+	appSettings Settings
+	dBConnector Connection
+	myBotClient tgbotapi.BotAPI
+
+	LogInfo  *log.Logger
+	LogWarn  *log.Logger
+	LogError *log.Logger
+	LogDebug *log.Logger
+)
+
+func main() {
+	appSettings.LoadFromFile("config.json")
+
+	LoggersInit()
+	LogInfo.Println("App started")
+
+	botClient, err := tgbotapi.NewBotAPI(appSettings.Token)
+	if err != nil {
+		LogError.Panic(err)
+	}
+	myBotClient = *botClient
+	var me = botClient.Self
+	LogInfo.Printf("Hello, World! I am user %d and my name is %s.\n", me.ID, me.FirstName)
+
+	if len(os.Args[1:]) >= 2 {
+		appSettings.DbUser = os.Args[1]
+		appSettings.DbPassword = os.Args[2]
+	} else {
+		fmt.Print("DataBase Username:")
+		_, err := fmt.Scanln(&appSettings.DbUser)
+		if err != nil {
+			LogError.Panic(os.Stderr, err)
+			return
+		}
+		fmt.Print("DataBase Password:")
+		_, err = fmt.Scanln(&appSettings.DbPassword)
+		if err != nil {
+			LogError.Panic(os.Stderr, err)
+			return
+		}
+	}
+
+	dBConnector.Init()
+
+	go Updater()
+	go Listener(botClient)
+
+	fmt.Println("Press Enter to exit")
+	reader := bufio.NewReader(os.Stdin)
+	reader.ReadString('\n')
+}
+
+
+
+// Listens for comming messages.
+func Listener(botClient *tgbotapi.BotAPI) {
+	u := tgbotapi.NewUpdate(0)
+	u.Timeout = 60
+
+	updates, _ := botClient.GetUpdatesChan(u)
+
+	for update := range updates {
+		// Ignore any non-Message Updates
+		if update.Message == nil {
+			continue
+		}
+
+		go HandleMessage(update.Message, botClient)
+	}
+}
+
+// Starts DataUpdater goroutine every 4 minutes
+func Updater() {
+	LogInfo.Println("Scheduler started!")
+	for {
+		go dBConnector.UpdateData()
+		time.Sleep(4 * time.Minute)
+	}
+}
+

+ 83 - 0
code/message_handler.go

@@ -0,0 +1,83 @@
+package main
+
+import (
+	"strings"
+
+	tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
+)
+
+func HandleMessage(reqMsg *tgbotapi.Message, botClient *tgbotapi.BotAPI) {
+	LogInfo.Printf("Received a text message in chat %d [%s]:%s", reqMsg.Chat.ID, reqMsg.From.UserName, reqMsg.Text)
+
+	msg := tgbotapi.NewMessage(reqMsg.Chat.ID, "")
+
+	switch strings.Split(reqMsg.Text, " ")[0] {
+
+	case "/start":
+		text, err := dBConnector.AddUser(reqMsg.Chat)
+		if err != nil {
+			LogError.Println(err)
+			text = "Internal error, please contact bot developer"
+		}
+		msg.Text = text
+		break
+
+	case "/stop":
+		text, err := dBConnector.DeleteUser(reqMsg.Chat)
+		if err != nil {
+			LogError.Println(err)
+			text = "Internal error, please contact bot developer"
+		}
+		msg.Text = text
+		break
+
+	case "/forceupdate":
+		dBConnector.UpdateData()
+		msg.Text = "Done!"
+		break
+
+	case "/actual":
+		if len(strings.Split(reqMsg.Text, " ")) > 1 {
+			msg.Text = GetActualData(strings.Split(reqMsg.Text, " ")[1])
+		} else {
+			result := dBConnector.GetActualDataFromDatabase(reqMsg.Chat.ID)
+			if (result == "nice"){
+				return //TODO better somehow different
+			}
+		}
+		break
+
+	case "/setwallet":
+		if len(strings.Split(reqMsg.Text, " ")) > 1 {
+			text, err := dBConnector.SetWallet(reqMsg)
+			if err != nil {
+				LogError.Println(err)
+				text = "An exception occurred while executing this command."
+			}
+			msg.Text = text
+		} else {
+			// LOL
+			msg.Text = "An exception occurred while executing this command."
+		}
+		break
+
+	case "/rate":
+		msg.Text = GetActualRate()
+		break
+
+	case "/help":
+		msg.Text = GetHelp()
+		break
+
+	case "/lastpayout":
+		msg.Text = dBConnector.GetLastPayout(reqMsg.Chat.ID)
+		break
+
+	default:
+		msg.Text = "Incorrect message"
+		break
+	}
+
+	LogInfo.Printf("Replied with: %s", strings.ReplaceAll(msg.Text, "\n", "\\n"))
+	botClient.Send(msg)
+}

+ 41 - 0
code/models.go

@@ -0,0 +1,41 @@
+// Models to represent db fields using gorm
+package main
+
+type User struct {
+	ChatId      int64
+	Wallet      string
+	LastMessage int64
+	Timer       int64
+}
+
+type Miner struct {
+	Wallet           string
+	Time             int64
+	ReportedHashrate int64
+	CurrentHashrate  float64
+	ValidShares      int
+	StaleShares      int
+	InvalidShares    int
+	Workers          int
+	Unpaid           int64
+}
+
+type Worker struct {
+	Wallet           string
+	Time             int64
+	Worker           string
+	ReportedHashrate int64
+	CurrentHashrate  float64
+	ValidShares      int
+	StaleShares      int
+	InvalidShares    int
+	WorkerUnpaid     float64
+}
+
+type Payout struct {
+	Wallet       string
+	Time         int64
+	Amount       int64
+	Worker       string
+	WorkerAmount float64
+}

+ 61 - 0
code/settings.go

@@ -0,0 +1,61 @@
+package main
+
+import (
+	"os"
+	"fmt"
+	"log"
+	"encoding/json"
+
+)
+
+type Settings struct{
+	Token 		string
+	ApiUrl 		string
+
+	DbHost 		string
+	DbUser		string `json:"-"`
+	DbPassword 	string `json:"-"`
+	DbName 		string
+
+	FullLogPath 	string
+	ErrorLogPath 	string
+
+	Currency 	string
+}
+
+func (s *Settings) LoadFromFile(filepath string) {
+	b, err := os.ReadFile(filepath)
+    if err != nil {
+     	if err.Error() == fmt.Sprintf("open %s: The system cannot find the file specified.", filepath) {
+	    	err = nil
+	    	path, err := os.Getwd()
+			if err != nil {
+		    	log.Panic(err)
+		   	}
+			path += "\\"+ filepath
+	     	fmt.Printf("The system cannot find the config file.\nCreating new config file at:\n%s\n",
+	      		path)
+	      	s.SaveToFile(path)
+	      	log.Fatal("Please fill the config file")	
+	    } else {
+		    log.Panic(err)
+	    } 
+      return
+    }
+    propJson := string(b)
+
+	json.Unmarshal([]byte(propJson), &s)
+	s.SaveToFile(filepath)
+}
+
+func (s *Settings) SaveToFile(filepath string) {
+	res, err := json.MarshalIndent(s, "", "    ")
+	if err != nil {
+        log.Panic(err)
+    }
+
+    err = os.WriteFile(filepath, res, 0666)
+    if err != nil {
+		log.Fatal(err)
+	}
+}

+ 45 - 0
code/unmasrshal_url.go

@@ -0,0 +1,45 @@
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"io"
+	"log"
+	"net/http"
+	"strings"
+)
+
+func UnmasrshalFromUrl(url string, v interface{}) error {
+	// Usage:
+	// <url> - string with web url to raw json data
+	// <JsonStruct> - struct type that describes json data at <url>
+	//
+	//    -- code --
+	//    var parseTo <JsonStruct>
+	//    err := UnmasrshalFromUrl(<url>, &parseTo)
+	//    -- code --
+
+	webClient := &http.Client{}
+	// Get response from url
+	webResp, err := webClient.Get(url)
+	if err != nil {
+		log.Println(err)
+		return err
+	}
+	// Close response body
+	defer webResp.Body.Close()
+	// Convert from Reader to []byte
+	jsonData, err := io.ReadAll(webResp.Body)
+	if err != nil {
+		log.Println(err)
+		return err
+	}
+	// Convert []byte to string
+	jsonDataStr := fmt.Sprintf("%s", jsonData)
+	jsonDataStr = strings.ReplaceAll(jsonDataStr, "null", "0")
+	// Covenrt string ot []byte
+	jsonData = []byte(jsonDataStr)
+
+	// LogDebug.Println(v)
+	return json.Unmarshal([]byte(jsonData), &v)
+}

+ 318 - 0
code/update_data.go

@@ -0,0 +1,318 @@
+package main
+
+import (
+	"fmt"
+	"math"
+
+	tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
+)
+
+func (c *Connection) UpdateData() {
+	LogInfo.Println("Updating data!")
+	// Getting list of unique wallets in user table
+	var walletList []string
+	result := c.GormDb.Model(&User{}).
+		Not("wallet = ?", "").
+		Select("DISTINCT(wallet)").
+		Scan(&walletList)
+
+	if result.Error != nil {
+		LogError.Println(result.Error)
+		return
+	}
+
+	for _, wallet := range walletList {
+		LogInfo.Printf("Getting data for wallet %s\n", wallet)
+
+		url := fmt.Sprintf("%s/miner/%s/currentStats", appSettings.ApiUrl, wallet)
+		LogInfo.Print("From: " + url)
+
+		var currentStats JsonCurrentStats
+		err := UnmasrshalFromUrl(url, &currentStats)
+
+		if err != nil {
+			LogInfo.Println("Error with getting currentStats data")
+			continue
+		}
+
+		if currentStats.Status == "OK" {
+			if currentStats.Data == (JsonCurrentStats{}).Data {
+				// if fmt.Sprintf("%T", currentStats.(map[string]interface{})["data"]) != "map[string]interface {}" {
+				LogInfo.Printf("data in response from %s for wallet %s not an interface!\n", appSettings.ApiUrl, wallet)
+				continue
+			}
+			// data := currentStats.(map[string]interface{})["data"].(map[string]interface{})
+			LogInfo.Printf("Creating new record for miner %s, time %d\n", wallet, currentStats.Data.Time)
+			// Line 28 of UpdateData.cs
+			// NOTE: No use for creating new record here
+			var newMinerRecord Miner
+			newMinerRecord.Wallet = wallet
+			newMinerRecord.Time = currentStats.Data.Time
+			newMinerRecord.ReportedHashrate = currentStats.Data.ReportedHashrate
+			newMinerRecord.CurrentHashrate = currentStats.Data.CurrentHashrate
+			newMinerRecord.ValidShares = currentStats.Data.ValidShares
+			newMinerRecord.StaleShares = currentStats.Data.StaleShares
+			newMinerRecord.InvalidShares = currentStats.Data.InvalidShares
+			newMinerRecord.Workers = currentStats.Data.ActiveWorkers
+			newMinerRecord.Unpaid = currentStats.Data.Unpaid
+			LogInfo.Printf("New miner record creating complete %s", newMinerRecord.Wallet)
+
+			// Line 42 of UpdateData.cs
+			// Search for latest record for miner
+			var lastMinerRecord Miner
+			result = c.GormDb.
+				Where("wallet = ?", newMinerRecord.Wallet).
+				Order("time desc").
+				Limit(1).
+				Find(&lastMinerRecord)
+			if result.Error != nil {
+				LogError.Println(result.Error)
+				continue
+			}
+
+			// No previous records for miner
+			// Or previous record from different time
+			if result.RowsAffected == 0 || lastMinerRecord.Time != newMinerRecord.Time {
+				result = c.GormDb.Create(&newMinerRecord)
+				if result.Error != nil {
+					LogInfo.Println(result.Error)
+					continue
+				}
+				LogInfo.Printf("Added new row for miner %s\n", newMinerRecord.Wallet)
+
+				// Line 54 of UpdateData.cs
+				if lastMinerRecord != (Miner{}) && newMinerRecord.Unpaid < lastMinerRecord.Unpaid {
+					var walletChats []int64
+					result := c.GormDb.Model(&User{}).
+						Where("wallet = ?", newMinerRecord.Wallet).
+						Select("chat_id").
+						Scan(&walletChats)
+					if result.Error != nil {
+						LogError.Println(result.Error)
+						continue
+					}
+
+					for _, chat := range walletChats {
+						LogInfo.Println("Sended 'Payout detected!' message to chat ", chat)
+						go myBotClient.Send(tgbotapi.NewMessage(chat, "Payout detected!"))
+					}
+				}
+
+				// Line 64 of UpdateData.cs
+				LogInfo.Printf("Getting workers data for wallet %s\n", wallet)
+				url = fmt.Sprintf("%s/miner/%s/workers", appSettings.ApiUrl, wallet)
+
+				LogInfo.Print("From: " + url)
+				var currentWorker JsonWorker
+				err := UnmasrshalFromUrl(url, &currentWorker)
+				if err != nil {
+					LogError.Println(err)
+					continue
+				}
+
+				if currentWorker.Status == "OK" {
+					if len(currentWorker.Data) == 0 {
+						LogInfo.Printf("data in response from %s for workers of wallet %s not an interface!\n", appSettings.ApiUrl, wallet)
+						continue
+					}
+
+					for i := range currentWorker.Data {
+						workerData := currentWorker.Data[i]
+						LogInfo.Printf("Creating new worker record for %s, time %d\n", workerData.Worker, workerData.Time)
+
+						var newWorkerRecord Worker
+						newWorkerRecord.Wallet = newMinerRecord.Wallet
+						newWorkerRecord.Time = workerData.Time
+						newWorkerRecord.Worker = workerData.Worker
+						newWorkerRecord.ReportedHashrate = workerData.ReportedHashrate
+						newWorkerRecord.CurrentHashrate = workerData.CurrentHashrate
+						newWorkerRecord.ValidShares = workerData.ValidShares
+						newWorkerRecord.StaleShares = workerData.StaleShares
+						newWorkerRecord.InvalidShares = workerData.InvalidShares
+
+						LogInfo.Printf("New worker record creating complete %s", newWorkerRecord.Worker)
+
+						//Search for latest record for worker
+						var lastWorkerRecord Worker
+						result = c.GormDb.
+							Where("worker = ?", workerData.Worker).
+							Order("time desc").
+							Limit(1).
+							Find(&lastWorkerRecord)
+						if result.Error != nil {
+							LogError.Println(result.Error)
+						}
+
+						// Line 85 of UpdateData.cs
+						if lastWorkerRecord != (Worker{}) {
+							// Search for latest miner record... again?
+							var lastMinerRecord1 Miner
+							result = c.GormDb.
+								Where("wallet = ?", newMinerRecord.Wallet).
+								Order("time desc").
+								Limit(1).
+								Find(&lastMinerRecord1)
+							if result.Error != nil {
+								LogError.Println(result.Error)
+							}
+
+							//Line 91 of UpdateData.cs
+							if lastMinerRecord1 != (Miner{}) {
+								//check for payout
+								if newMinerRecord.Unpaid < lastMinerRecord1.Unpaid {
+									// Adding new payout field to db
+									url := fmt.Sprintf("%s/miner/%s/payouts", appSettings.ApiUrl, wallet)
+									var payouts JsonPayouts
+									err := UnmasrshalFromUrl(url, &payouts)
+									if err != nil {
+										LogError.Println(err)
+										continue
+									}
+									lastPayout := payouts.Data[0]
+
+									LogInfo.Println("Last payout time =", lastPayout.PaidOn)
+
+									var newPayoutRecord Payout
+									newPayoutRecord.Wallet = wallet
+									newPayoutRecord.Time = lastPayout.PaidOn
+									newPayoutRecord.Amount = lastPayout.Amount
+									newPayoutRecord.Worker = workerData.Worker
+
+									workerAmount := float64(lastWorkerRecord.WorkerUnpaid+
+										(float64(lastPayout.Amount-lastMinerRecord.Unpaid))) *
+										(float64(workerData.ReportedHashrate) / float64(newMinerRecord.ReportedHashrate))
+									if math.IsNaN(workerAmount) || math.IsInf(workerAmount, 0) {
+										workerAmount = lastWorkerRecord.WorkerUnpaid
+									}
+									newPayoutRecord.WorkerAmount = workerAmount
+
+									workerUnpaid := float64(newMinerRecord.Unpaid) *
+										(float64(workerData.ReportedHashrate) / float64(newMinerRecord.ReportedHashrate))
+									LogDebug.Println("newMinerRecord.Unpaid:", newMinerRecord.Unpaid)
+									LogDebug.Println("lastMinerRecord.Unpaid:", lastMinerRecord.Unpaid)
+									LogDebug.Println("workerData.ReportedHashrate:", workerData.ReportedHashrate)
+									LogDebug.Println("newMinerRecord.ReportedHashrate:", newMinerRecord.ReportedHashrate)
+									LogDebug.Println("workerUnpaid:", workerUnpaid)
+									if math.IsNaN(workerUnpaid) || math.IsInf(workerUnpaid, 0) {
+										LogDebug.Println("workerUnpaid is NaN or Inf")
+										workerUnpaid = 0
+									}
+									newWorkerRecord.WorkerUnpaid = workerUnpaid
+
+									result = c.GormDb.Create(&newPayoutRecord)
+									if result.Error != nil {
+										LogError.Println(result.Error)
+										continue
+									}
+									LogInfo.Println("Add newPayoutRecord, time =", newPayoutRecord.Time)
+
+									//removing old records
+									result = c.GormDb.
+										Where("wallet = ? AND time < ?", newPayoutRecord.Wallet, newPayoutRecord.Time).
+										Delete(Worker{})
+									if result.Error != nil {
+										LogError.Println(result.Error)
+									}
+
+									result = c.GormDb.
+										Where("wallet = ? AND time < ?", newPayoutRecord.Wallet, newPayoutRecord.Time).
+										Delete(Miner{})
+									if result.Error != nil {
+										LogError.Println(result.Error)
+									}
+
+									result = c.GormDb.
+										Where("wallet = ? AND time < ?", newPayoutRecord.Wallet, newPayoutRecord.Time).
+										Delete(Payout{})
+									if result.Error != nil {
+										LogError.Println(result.Error)
+									}
+									//Line 135 of UpdateData.cs
+								} else {
+									//no check that last balance and prev balance are the same
+									//don't sure > or >=
+									//TODO rewrite this
+									var max int64
+									result := c.GormDb.Model(&Payout{}).
+										Where("wallet = ?", lastWorkerRecord.Wallet).
+										Order("time desc").
+										Limit(1).
+										Select("time").
+										Scan(&max)
+									if result.Error != nil {
+										LogWarn.Printf("No payouts data for %s! Time is set to 0!\n", lastWorkerRecord.Wallet)
+										LogError.Println(result.Error)
+										max = 0
+									}
+
+									if lastWorkerRecord.Time > max {
+										workerUnpaid := float64(lastWorkerRecord.WorkerUnpaid+
+											float64(newMinerRecord.Unpaid-lastMinerRecord.Unpaid)) *
+											(float64(workerData.ReportedHashrate) / float64(newMinerRecord.ReportedHashrate))
+										if math.IsNaN(workerUnpaid) || math.IsInf(workerUnpaid, 0) {
+											workerUnpaid = lastWorkerRecord.WorkerUnpaid
+										}
+										newWorkerRecord.WorkerUnpaid = workerUnpaid
+										// Line 169 of UpdateData.cs
+									} else {
+										workerUnpaid := float64((newMinerRecord.Unpaid - lastMinerRecord.Unpaid)) *
+											(float64(workerData.ReportedHashrate) / float64(newMinerRecord.ReportedHashrate))
+										if math.IsNaN(workerUnpaid) || math.IsInf(workerUnpaid, 0) {
+											workerUnpaid = lastWorkerRecord.WorkerUnpaid
+										}
+										newWorkerRecord.WorkerUnpaid = workerUnpaid
+
+										var targetedChats []int64
+										result = c.GormDb.
+											Where(&User{Wallet: newMinerRecord.Wallet}).
+											Select("DISTINCT(chat_id)").
+											Scan(&targetedChats)
+										if result.Error != nil {
+											LogError.Println(result.Error)
+										} else {
+											msg := fmt.Sprintf("Debug info: Your worker %s hasn't been zeroed on payout!", workerData.Worker)
+											for _, chatId := range targetedChats {
+												go myBotClient.Send(tgbotapi.NewMessage(int64(chatId), msg))
+											}
+											LogDebug.Printf("Worker %s on address %s hasn't been zeroed on payout!\n", workerData.Worker, wallet)
+										}
+									}
+								}
+							}
+							// Line 196 of UpdateData.cs
+						} else {
+							LogDebug.Println("Setting WorkerUnpaid to 0")
+							newWorkerRecord.WorkerUnpaid = 0
+						}
+
+						var rowsCount int64
+						result = c.GormDb.Model(&Worker{}).
+							Where("worker = ? and time = ?", newWorkerRecord.Worker, newWorkerRecord.Time).
+							Count(&rowsCount)
+						if result.Error != nil {
+							LogError.Println(result.Error)
+							continue
+						}
+
+						if rowsCount != 0 {
+							LogInfo.Printf("Error adding new row: row with worker: %s, time: %d already exists!\n", newWorkerRecord.Worker, newWorkerRecord.Time)
+							continue
+						}
+						result = c.GormDb.Create(&newWorkerRecord)
+
+						if result.Error != nil {
+							LogError.Println(result.Error)
+						} else {
+							LogInfo.Printf("Added new row for worker %s\n", newWorkerRecord.Worker)
+						}
+					}
+				}
+				// Line 222 of UpdateData.cs
+			} else {
+				LogInfo.Printf("Row with wallet: %s and time: %d already exists!", wallet, currentStats.Data.Time)
+			}
+		} else {
+			LogError.Printf("Error response from pool for %s!\n", wallet)
+		}
+	}
+}

+ 16 - 0
go.mod

@@ -0,0 +1,16 @@
+module GoEthemineTelegramBot
+
+go 1.17
+
+require (
+	github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
+	gorm.io/driver/mysql v1.1.3
+	gorm.io/gorm v1.22.2
+)
+
+require (
+	github.com/go-sql-driver/mysql v1.6.0 // indirect
+	github.com/jinzhu/inflection v1.0.0 // indirect
+	github.com/jinzhu/now v1.1.2 // indirect
+	github.com/technoweenie/multipartstreamer v1.0.1 // indirect
+)

+ 15 - 0
go.sum

@@ -0,0 +1,15 @@
+github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
+github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible h1:2cauKuaELYAEARXRkq2LrJ0yDDv1rW7+wrTEdVL3uaU=
+github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM=
+github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
+github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.1.2 h1:eVKgfIdy9b6zbWBMgFpfDPoAMifwSZagU9HmEU6zgiI=
+github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM=
+github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog=
+gorm.io/driver/mysql v1.1.3 h1:+5g1UElqN0sr2gZqmg9djlu1zT3cErHiscc6+IbLHgw=
+gorm.io/driver/mysql v1.1.3/go.mod h1:4P/X9vSc3WTrhTLZ259cpFd6xKNYiSSdSZngkSBGIMM=
+gorm.io/gorm v1.21.12/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0=
+gorm.io/gorm v1.22.2 h1:1iKcvyJnR5bHydBhDqTwasOkoo6+o4Ms5cknSt6qP7I=
+gorm.io/gorm v1.22.2/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0=