package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"strconv"
"sync"
"time"
"github.com/Knetic/govaluate"
"github.com/go-redis/redis/v8"
"github.com/go-telegram-bot-api/telegram-bot-api"
)
var flagPort = flag.String("port", "8008", "determined the port the sercice runs on")
var flagTgTokenFile = flag.String("tgtoken", "/run/secrets/tg_bot_token", "determines the location of the telegram bot token file")
var changelllyAPIKeyFile = flag.String("chapikey", "/run/secrets/ch_api_key", "determines the file that holds the changelly api key")
var alertFile = flag.String("alertfile", "/run/secrets/alerts", "determines the locaiton of the alert files")
var alertsCheckInterval = flag.Int64("alertinterval", 600., "in seconds, the amount of time between alert checks")
var redisAddress = flag.String("redisaddress", "redis:6379", "determines the address of the redis instance")
var redisPassword = flag.String("redispassword", "", "determines the password of the redis db")
var redisDB = flag.Int64("redisdb", 0, "determines the db number")
const cryptocomparePriceURL = "https://min-api.cryptocompare.com/data/price?"
const changellyURL = "https://api.changelly.com"
const botChannelID = 146328407
var getRedisClientOnce sync.Once
var getTGBotOnce sync.Once
type TgToken struct {
Token string `json:"token"`
}
func getTGToken() string {
tgTokenJsonBytes, err := ioutil.ReadFile(*flagTgTokenFile)
if err != nil {
log.Fatal(err)
}
var tgToken TgToken
err = json.Unmarshal(tgTokenJsonBytes, &tgToken)
if err != nil {
log.Fatal(err)
}
return tgToken.Token
}
func getTgBot() *tgbotapi.BotAPI {
var tgbot *tgbotapi.BotAPI
getTGBotOnce.Do(func() {
tgTokenJsonBytes, err := ioutil.ReadFile(*flagTgTokenFile)
if err != nil {
log.Fatal(err)
}
var tgToken TgToken
err = json.Unmarshal(tgTokenJsonBytes, &tgToken)
if err != nil {
log.Fatal(err)
}
bot, err := tgbotapi.NewBotAPI(tgToken.Token)
if err != nil {
log.Panic(err)
}
bot.Debug = true
tgbot = bot
})
return tgbot
}
func runTgBot() {
bot := getTgBot()
log.Printf("Authorized on account %s", bot.Self.UserName)
update := tgbotapi.NewUpdate(0)
update.Timeout = 60
updates, err := bot.GetUpdatesChan(update)
if err != nil {
log.Panic(err)
}
for update := range updates {
if update.Message == nil {
continue
}
log.Printf("[%s] %s", update.Message.From.UserName, update.Message.Text)
msg := tgbotapi.NewMessage(update.Message.Chat.ID, update.Message.Text)
msg.ReplyToMessageID = update.Message.MessageID
bot.Send(msg)
}
}
type priceChanStruct struct {
name string
price float64
}
type errorChanStruct struct {
hasError bool
err error
}
func sendGetToCryptoCompare(
name, unit string,
wg *sync.WaitGroup,
priceChan chan<- priceChanStruct,
errChan chan<- errorChanStruct) {
defer wg.Done()
params := "fsym=" + url.QueryEscape(name) + "&" +
"tsyms=" + url.QueryEscape(unit)
path := cryptocomparePriceURL + params
fmt.Println(path)
resp, err := http.Get(path)
if err != nil {
priceChan <- priceChanStruct{name: name, price: 0.}
errChan <- errorChanStruct{hasError: true, err: err}
fmt.Println(err.Error())
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
priceChan <- priceChanStruct{name: name, price: 0.}
errChan <- errorChanStruct{hasError: true, err: err}
fmt.Println(err.Error())
}
jsonBody := make(map[string]float64)
err = json.Unmarshal(body, &jsonBody)
if err != nil {
priceChan <- priceChanStruct{name: name, price: 0.}
errChan <- errorChanStruct{hasError: true, err: err}
fmt.Println(err.Error())
}
fmt.Println(string(body))
priceChan <- priceChanStruct{name: name, price: jsonBody[unit]}
errChan <- errorChanStruct{hasError: false, err: nil}
}
//TODO
func healthHandler(w http.ResponseWriter, r *http.Request) {
}
func cryptoHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
http.Error(w, "Method is not supported.", http.StatusNotFound)
}
var name string
var unit string
params := r.URL.Query()
for key, value := range params {
switch key {
case "name":
name = value[0]
case "unit":
unit = value[0]
default:
log.Fatal("bad parameters for the crypto endpoint.")
}
}
var wg sync.WaitGroup
priceChan := make(chan priceChanStruct, 1)
errChan := make(chan errorChanStruct, 1)
defer close(errChan)
defer close(priceChan)
wg.Add(1)
go sendGetToCryptoCompare(name, unit, &wg, priceChan, errChan)
wg.Wait()
select {
case err := <-errChan:
if err.hasError != false {
log.Printf(err.err.Error())
}
default:
log.Fatal("this shouldnt have happened")
}
var price priceChanStruct
select {
case priceCh := <-priceChan:
price = priceCh
default:
log.Fatal("this shouldnt have happened")
}
json.NewEncoder(w).Encode(map[string]interface{}{"name": price.name, "price": price.price, "unit": unit})
}
func pairHandler(w http.ResponseWriter, r *http.Request) {
var err error
if r.Method != "GET" {
http.Error(w, "Method is not supported.", http.StatusNotFound)
}
var one string
var two string
var multiplier float64
params := r.URL.Query()
for key, value := range params {
switch key {
case "one":
one = value[0]
case "two":
two = value[0]
case "multiplier":
multiplier, err = strconv.ParseFloat(value[0], 64)
if err != nil {
log.Fatal(err)
}
default:
log.Fatal("bad parameters for the pair endpoint.")
}
}
fmt.Println(one, two, multiplier)
var wg sync.WaitGroup
priceChan := make(chan priceChanStruct, 2)
errChan := make(chan errorChanStruct, 2)
defer close(priceChan)
defer close(errChan)
wg.Add(2)
go sendGetToCryptoCompare(one, "USD", &wg, priceChan, errChan)
go sendGetToCryptoCompare(two, "USD", &wg, priceChan, errChan)
wg.Wait()
for i := 0; i < 2; i++ {
select {
case err := <-errChan:
if err.hasError != false {
log.Printf(err.err.Error())
}
default:
log.Fatal("this shouldnt have happened")
}
}
var priceOne float64
var priceTwo float64
for i := 0; i < 2; i++ {
select {
case price := <-priceChan:
if price.name == one {
priceOne = price.price
}
if price.name == two {
priceTwo = price.price
}
default:
log.Fatal("this shouldnt have happened")
}
}
ratio := priceOne * multiplier / priceTwo
fmt.Println(ratio)
json.NewEncoder(w).Encode(map[string]interface{}{"ratio": ratio})
}
type alertType struct {
Name string `json:"name"`
Expr string `json:"expr"`
}
type alertsType struct {
Alerts []alertType `json:"alerts"`
}
func getRedisClient() *redis.Client {
var client *redis.Client
getRedisClientOnce.Do(func() {
rdb := redis.NewClient(&redis.Options{
Addr: *redisAddress,
Password: *redisPassword,
DB: int(*redisDB),
})
client = rdb
})
return client
}
func getAlerts() (alertsType, error) {
var alerts alertsType
// rdb := getRedisClient()
rdb := redis.NewClient(&redis.Options{
Addr: *redisAddress,
Password: *redisPassword,
DB: int(*redisDB),
})
ctx := context.Background()
keys := rdb.SMembersMap(ctx, "alertkeys")
alerts.Alerts = make([]alertType, len(keys.Val()))
vals := keys.Val()
i := 0
for key := range vals {
alert := rdb.Get(ctx, key[6:])
expr, _ := alert.Result()
alerts.Alerts[i].Name = key
alerts.Alerts[i].Expr = expr
i++
}
return alerts, nil
}
func getAlertsFromRedis() (alertsType, error) {
// rdb := getRedisClient()
rdb := redis.NewClient(&redis.Options{
Addr: *redisAddress,
Password: *redisPassword,
DB: int(*redisDB),
})
ctx := context.Background()
val, err := rdb.Get(ctx, "alert").Result()
if err != nil {
log.Printf(err.Error())
return alertsType{}, err
}
fmt.Println(val)
return alertsType{}, nil
}
func alertManager() {
for {
alerts, err := getAlerts()
if err != nil {
log.Printf(err.Error())
return
}
fmt.Println(alerts)
for i := range alerts.Alerts {
expression, err := govaluate.NewEvaluableExpression(alerts.Alerts[i].Expr)
if err != nil {
log.Printf(err.Error())
continue
}
vars := expression.Vars()
parameters := make(map[string]interface{}, len(vars))
var wg sync.WaitGroup
priceChan := make(chan priceChanStruct, len(vars))
errChan := make(chan errorChanStruct, len(vars))
defer close(priceChan)
defer close(errChan)
wg.Add(len(vars))
for i := range vars {
go sendGetToCryptoCompare(vars[i], "USD", &wg, priceChan, errChan)
}
wg.Wait()
for i := 0; i < len(vars); i++ {
select {
case err := <-errChan:
if err.hasError != false {
log.Printf(err.err.Error())
}
default:
log.Fatal("this shouldnt have happened")
}
}
for i := 0; i < len(vars); i++ {
select {
case price := <-priceChan:
parameters[price.name] = price.price
default:
log.Fatal("this shouldnt have happened")
}
}
fmt.Println("parameters:", parameters)
result, err := expression.Evaluate(parameters)
if err != nil {
log.Println(err.Error())
}
var resultBool bool
fmt.Println("result:", result)
resultBool = result.(bool)
if resultBool == true {
bot, err := tgbotapi.NewBotAPI(getTGToken())
if err != nil {
log.Panic(err)
}
// bot := getTgBot()
msgText := "notification " + alerts.Alerts[i].Expr + " has been triggered"
msg := tgbotapi.NewMessage(botChannelID, msgText)
bot.Send(msg)
}
}
time.Sleep(time.Second * time.Duration(*alertsCheckInterval))
}
}
type addAlertJSONType struct {
Name string `json:"name"`
Expr string `json:"expr"`
}
func addAlertHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Method is not supported.", http.StatusNotFound)
}
bodyBytes, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Printf(err.Error())
}
var bodyJSON addAlertJSONType
json.Unmarshal(bodyBytes, &bodyJSON)
fmt.Println(bodyJSON)
// rdb := getRedisClient()
rdb := redis.NewClient(&redis.Options{
Addr: *redisAddress,
Password: *redisPassword,
DB: int(*redisDB),
MinIdleConns: 1,
})
ctx := context.Background()
key := "alert:" + bodyJSON.Name
rdb.Set(ctx, bodyJSON.Name, bodyJSON.Expr, 0)
rdb.SAdd(ctx, "alertkeys", key)
json.NewEncoder(w).Encode(map[string]interface{}{"isSuccessful": true, "error": ""})
}
func startServer() {
http.HandleFunc("/health", healthHandler)
http.HandleFunc("/crypto", cryptoHandler)
http.HandleFunc("/pair", pairHandler)
http.HandleFunc("/addalert", addAlertHandler)
if err := http.ListenAndServe(":"+*flagPort, nil); err != nil {
log.Fatal(err)
}
}
func main() {
go runTgBot()
go alertManager()
startServer()
}