diff options
| author | terminaldweller <thabogre@gmail.com> | 2021-02-25 08:49:18 +0000 | 
|---|---|---|
| committer | terminaldweller <thabogre@gmail.com> | 2021-02-25 08:49:18 +0000 | 
| commit | ff12756915d452e7b92959062ed6315adf0e424e (patch) | |
| tree | bb86d9405a8a63d4865594f20a13327920485cf8 | |
| parent | removed the expression parser. using a lib now. the addalert endpoint is work... (diff) | |
| download | hived-ff12756915d452e7b92959062ed6315adf0e424e.tar.gz hived-ff12756915d452e7b92959062ed6315adf0e424e.zip | |
added a new endpoint for changelly. added somewhat decent logging. cleaned up the code a bit. the secrets are all environment variables now since we want publicly availale CI. the api and postman docs are added. added travis integration.
| -rw-r--r-- | .travis.yml | 14 | ||||
| -rw-r--r-- | Dockerfile | 5 | ||||
| -rw-r--r-- | README.md | 47 | ||||
| -rw-r--r-- | api/hived.postman_collection.json | 64 | ||||
| -rw-r--r-- | api/swagger.yaml | 101 | ||||
| -rw-r--r-- | docker-compose.yaml | 11 | ||||
| -rwxr-xr-x | docker-entrypoint.sh | 8 | ||||
| -rw-r--r-- | go.mod | 1 | ||||
| -rw-r--r-- | go.sum | 6 | ||||
| -rw-r--r-- | hived.go | 235 | ||||
| -rwxr-xr-x | test/endpoints.sh | 7 | 
11 files changed, 414 insertions, 85 deletions
| diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..e51bcae --- /dev/null +++ b/.travis.yml @@ -0,0 +1,14 @@ +language: go + +env: +  - DOCKER_COMPOSE_VERSION=1.25.0 + +services: +  - docker + +before_install: +  - docker pull redis:6.2-alpine + +install: +  - docker build -t hived ./ && docker-compose up -d +  - ./test/endpoints.sh @@ -2,11 +2,12 @@ FROM alpine:3.13 as builder  RUN apk update && apk upgrade  RUN apk add go git -COPY ./go.* /hived/ +COPY go.* /hived/  RUN cd /hived && go mod download  COPY *.go /hived/  RUN cd /hived && go build  FROM alpine:3.13  COPY --from=builder /hived/hived /hived/ -ENTRYPOINT ["/hived/hived"] +COPY ./docker-entrypoint.sh /hived/ +ENTRYPOINT ["/hived/docker-entrypoint.sh"] @@ -1,2 +1,47 @@  # hived -hived in go +`hived` is the second version of my personal cryptocurrency server.<br/> +hived is currently using redis as its DB because its tiny and fast.<br/> +It sends notifications through telegram.<br/> +Currently it has 4 endpoint:<br/> + +### /price +Lets you ask for the price of the currency. You can determine the currency the value is returned in.<br/> + +### /pair +Takes in a pair of currencies and a multiplier. Determines and returns the ratio.<br/> + +### /addalert +Takes in a name and a math expression containing the names of the currencies. Checks the expression periodically. Sends a message over telegram when the expression holds true.<br/> +The expression's result must be boolean. As an example:<br/> +```Go +ETH*50>50000. +ETH*60/(DOGE*300000) < 4. +``` +You can have as many parameters as you like. The requests for the crypto prices are all turned into individual goroutines so it's fast.<br/> +The expression evaluation is powered by [govaluate](https://github.com/Knetic/govaluate). So for a set of rules and what you can and cannot do please check the documentation over there.<br/> + +### /ex +Gets the list of currencies that are available to be traded.<br/> + +You can check under `./test` for some examples of curl commands.<br/> + +## How to Run +Before you can run this, you need a [telegram bot token](https://core.telegram.org/bots#6-botfather) and a [changelly](https://changelly.com/) API key.<br/> +The keys are put in files and then given to Docker as secrets.The docker entrypoint script then exports these as environment variables.<br/> + +```sh +TELEGRAM_BOT_TOKEN="my-telegram-bot-api-key" +``` +And +```sh +CHANGELLY_API_KEY:"my-changelly-api-key" +``` +And +```sh +CHANGELLY_API_SECRET:"my-changelly-api-secret" +``` +If you want to use docker-compose, it's as  simple as running `docker-compose up`. You just need to provide the files. You can check the file names in the docker-compose file.<br/> +Both the server itself and the redis image are alpine-based so they're pretty small.<br/> + +## Docs +You can find the swagger and postman docs under `/api`.<br/> diff --git a/api/hived.postman_collection.json b/api/hived.postman_collection.json new file mode 100644 index 0000000..3cab1e7 --- /dev/null +++ b/api/hived.postman_collection.json @@ -0,0 +1,64 @@ +{ +	"info": { +		"_postman_id": "ca2c71c1-27a5-466c-a3c7-16b71f62af34", +		"name": "hived", +		"schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json" +	}, +	"item": [ +		{ +			"name": "price", +			"request": { +				"method": "GET", +				"header": [], +				"url": "" +			}, +			"response": [] +		}, +		{ +			"name": "pair", +			"request": { +				"method": "GET", +				"header": [], +				"url": { +					"raw": "http://127.0.0.1:8008/pair?one=ETH&two=CAKE&multiplier=4.0", +					"protocol": "http", +					"host": [ +						"127", +						"0", +						"0", +						"1" +					], +					"port": "8008", +					"path": [ +						"pair" +					], +					"query": [ +						{ +							"key": "one", +							"value": "ETH" +						}, +						{ +							"key": "two", +							"value": "CAKE" +						}, +						{ +							"key": "multiplier", +							"value": "4.0" +						} +					] +				} +			}, +			"response": [] +		}, +		{ +			"name": "addalert", +			"request": { +				"method": "GET", +				"header": [], +				"url": "" +			}, +			"response": [] +		} +	], +	"protocolProfileBehavior": {} +}
\ No newline at end of file diff --git a/api/swagger.yaml b/api/swagger.yaml new file mode 100644 index 0000000..9100e82 --- /dev/null +++ b/api/swagger.yaml @@ -0,0 +1,101 @@ +openapi: 3.0.0 +info: +  version: 1.0.0-oas3 +  title: hived +  description: hived's API +servers: +  - description: SwaggerHub API Auto Mocking +    url: 'https://virtserver.swaggerhub.com/xashmith/hived/0.1' +paths: +  /price: +    get: +      description: Returns the price of the crypto +      parameters: +        - name: name +          in: query +          description: the symbol of the cryptocurrency +          schema: +            type: string +        - name: unit +          in: query +          description: the unit the return the price in +          schema: +            type: string +      responses: +        '200': +          description: Successful response +          content: +            application/json: +              schema: +                type: object +                required: +                  - name +                  - unit +                  - price +                properties: +                  name: +                    type: string +                  unit: +                    type: string +                  price: +                    type: number +  /pair: +    get: +      description: Returns the ratio of one to two multiplied by a value +      parameters: +        - name: one +          in: query +          description: the name of the currency that's going to be multiplied +          schema: +            type: string +        - name: two +          in: query +          description: the name of the second currency +          schema: +            type: string +        - name: multiplier +          in: query +          description: the amount hte first currency is going to be multiplied +          schema: +            type: number +      responses: +        '200': +          description: Successful response +          content: +            application/json: +              schema: +                type: object +                required: +                  - ratio +                properties: +                  ratio: +                    type: number +  /addalerts: +    post: +      description: Add alerts to the alertmanager's list +      requestBody: +        required: true +        content: +          application/json: +            schema: +              type: object +              required: +                - name +                - expr +              properties: +                name: +                  type: string +                expr: +                  type: string +      responses: +        '200': +          description: successful update +          content: +            application/json: +              schema: +                type: object +                properties: +                  err: +                    type: string +                  isSuccessful: +                    type: boolean diff --git a/docker-compose.yaml b/docker-compose.yaml index 4285550..0b33516 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -7,6 +7,7 @@ services:      secrets:        - tg_bot_token        - ch_api_key +      - ch_api_secret      networks:        - hivednet      restart: unless-stopped @@ -14,6 +15,7 @@ services:        - "8008:8008"      depends_on:        - redis +    entrypoint: /hived/docker-entrypoint.sh    redis:      image: redis:6.2-alpine      networks: @@ -27,10 +29,15 @@ services:        - redis-data:/data/  networks:    hivednet: +    driver: bridge +  routenet: +    driver: bridge  secrets:    tg_bot_token: -    file: ./tgtoken.json +    file: ./tgtoken    ch_api_key: -    file: ./changelly_api_key.json +    file: ./changelly_api_key +  ch_api_secret: +    file: ./changelly_api_secret  volumes:    redis-data: diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100755 index 0000000..6d88ff1 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env sh +set -e + +export $(cat /run/secrets/tg_bot_token) +export $(cat /run/secrets/ch_api_key) +export $(cat /run/secrets/ch_api_secret) + +"/hived/hived" @@ -6,5 +6,6 @@ require (  	github.com/Knetic/govaluate v3.0.0+incompatible  	github.com/go-redis/redis/v8 v8.6.0  	github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible +	github.com/rs/zerolog v1.20.0  	github.com/technoweenie/multipartstreamer v1.0.1 // indirect  ) @@ -3,6 +3,7 @@ github.com/Knetic/govaluate v3.0.0+incompatible h1:7o6+MAPhYTCF0+fdvoz1xDedhRb4f  github.com/Knetic/govaluate v3.0.0+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=  github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=  github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=  github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=  github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=  github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= @@ -33,7 +34,11 @@ github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISq  github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=  github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=  github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=  github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/zerolog v1.20.0 h1:38k9hgtUBdxFwE34yS8rTHmHBa4eN16E4DJlv177LNs= +github.com/rs/zerolog v1.20.0/go.mod h1:IzD0RJ65iWH0w97OQQebJEvTZYvsCUm9WVLWBQrJRjo=  github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=  github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=  github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM= @@ -71,6 +76,7 @@ golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7w  golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=  golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=  golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=  golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=  golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=  golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1,14 +1,19 @@  package main  import ( +	"bytes"  	"context" +	"crypto/hmac" +	"crypto/sha512" +	"encoding/hex"  	"encoding/json" +	"errors"  	"flag"  	"fmt"  	"io/ioutil" -	"log"  	"net/http"  	"net/url" +	"os"  	"strconv"  	"sync"  	"time" @@ -16,79 +21,43 @@ import (  	"github.com/Knetic/govaluate"  	"github.com/go-redis/redis/v8"  	"github.com/go-telegram-bot-api/telegram-bot-api" +	"github.com/rs/zerolog" +	"github.com/rs/zerolog/log"  )  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") +var botChannelID = flag.Int64("botchannelid", 146328407, "determines the channel id the telgram bot should send messages to")  const cryptocomparePriceURL = "https://min-api.cryptocompare.com/data/price?"  const changellyURL = "https://api.changelly.com" -const botChannelID = 146328407 +const TELEGRAM_BOT_TOKEN_ENV_VAR = "TELEGRAM_BOT_TOKEN" +const CHANGELLY_API_KEY_ENV_VAR = "CHANGELLY_API_KEY" +const CHANGELLY_API_SECRET_ENV_VAR = "CHANGELLY_API_SECRET"  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) +func runTgBot() { +	// bot := getTgBot() +	token := os.Getenv(TELEGRAM_BOT_TOKEN_ENV_VAR) +	bot, err := tgbotapi.NewBotAPI(token[1 : len(token)-1])  	if err != nil { -		log.Fatal(err) +		log.Error().Err(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) +	log.Debug().Msg("authorized on account bot_bloodstalker")  	update := tgbotapi.NewUpdate(0)  	update.Timeout = 60  	updates, err := bot.GetUpdatesChan(update)  	if err != nil { -		log.Panic(err) +		log.Error().Err(err)  	}  	for update := range updates { @@ -159,7 +128,7 @@ func sendGetToCryptoCompare(  func healthHandler(w http.ResponseWriter, r *http.Request) {  } -func cryptoHandler(w http.ResponseWriter, r *http.Request) { +func priceHandler(w http.ResponseWriter, r *http.Request) {  	if r.Method != "GET" {  		http.Error(w, "Method is not supported.", http.StatusNotFound)  	} @@ -174,10 +143,18 @@ func cryptoHandler(w http.ResponseWriter, r *http.Request) {  		case "unit":  			unit = value[0]  		default: -			log.Fatal("bad parameters for the crypto endpoint.") +			log.Error().Err(errors.New("bad parameters for the crypto endpoint."))  		}  	} +	if name == "" || unit == "" { +		json.NewEncoder(w).Encode(map[string]interface{}{ +			"err":          "query parameters must include name and unit", +			"isSuccessful": false}) +		log.Error().Err(errors.New("query parameters must include name and unit.")) +		return +	} +  	var wg sync.WaitGroup  	priceChan := make(chan priceChanStruct, 1)  	errChan := make(chan errorChanStruct, 1) @@ -190,10 +167,10 @@ func cryptoHandler(w http.ResponseWriter, r *http.Request) {  	select {  	case err := <-errChan:  		if err.hasError != false { -			log.Printf(err.err.Error()) +			log.Error().Err(err.err)  		}  	default: -		log.Fatal("this shouldnt have happened") +		log.Error().Err(errors.New("this shouldn't have happened'"))  	}  	var price priceChanStruct @@ -201,10 +178,15 @@ func cryptoHandler(w http.ResponseWriter, r *http.Request) {  	case priceCh := <-priceChan:  		price = priceCh  	default: -		log.Fatal("this shouldnt have happened") +		log.Fatal().Err(errors.New("this shouldnt have happened"))  	} -	json.NewEncoder(w).Encode(map[string]interface{}{"name": price.name, "price": price.price, "unit": unit}) +	json.NewEncoder(w).Encode(map[string]interface{}{ +		"name":         price.name, +		"price":        price.price, +		"unit":         unit, +		"err":          "", +		"isSuccessful": true})  }  func pairHandler(w http.ResponseWriter, r *http.Request) { @@ -226,13 +208,16 @@ func pairHandler(w http.ResponseWriter, r *http.Request) {  		case "multiplier":  			multiplier, err = strconv.ParseFloat(value[0], 64)  			if err != nil { -				log.Fatal(err) +				log.Fatal().Err(err)  			}  		default: -			log.Fatal("bad parameters for the pair endpoint.") +			log.Fatal().Err(errors.New("unknown parameters for the pair endpoint."))  		}  	} -	fmt.Println(one, two, multiplier) + +	if one == "" || two == "" || multiplier == 0. { +		log.Error().Err(errors.New("the query must include one()),two and multiplier")) +	}  	var wg sync.WaitGroup  	priceChan := make(chan priceChanStruct, 2) @@ -249,10 +234,10 @@ func pairHandler(w http.ResponseWriter, r *http.Request) {  		select {  		case err := <-errChan:  			if err.hasError != false { -				log.Printf(err.err.Error()) +				log.Error().Err(err.err)  			}  		default: -			log.Fatal("this shouldnt have happened") +			log.Fatal().Err(errors.New("this shouldnt have happened"))  		}  	} @@ -268,7 +253,7 @@ func pairHandler(w http.ResponseWriter, r *http.Request) {  				priceTwo = price.price  			}  		default: -			log.Fatal("this shouldnt have happened") +			log.Fatal().Err(errors.New("this shouldnt have happened"))  		}  	} @@ -286,6 +271,7 @@ type alertsType struct {  	Alerts []alertType `json:"alerts"`  } +//FIXME  func getRedisClient() *redis.Client {  	var client *redis.Client  	getRedisClientOnce.Do(func() { @@ -325,6 +311,7 @@ func getAlerts() (alertsType, error) {  	return alerts, nil  } +//not being used  func getAlertsFromRedis() (alertsType, error) {  	// rdb := getRedisClient()  	rdb := redis.NewClient(&redis.Options{ @@ -335,11 +322,16 @@ func getAlertsFromRedis() (alertsType, error) {  	ctx := context.Background()  	val, err := rdb.Get(ctx, "alert").Result()  	if err != nil { -		log.Printf(err.Error()) +		log.Error().Err(err)  		return alertsType{}, err  	}  	fmt.Println(val) +	err = rdb.Close() +	if err != nil { +		log.Error().Err(err) +	} +  	return alertsType{}, nil  } @@ -347,7 +339,7 @@ func alertManager() {  	for {  		alerts, err := getAlerts()  		if err != nil { -			log.Printf(err.Error()) +			log.Error().Err(err)  			return  		}  		fmt.Println(alerts) @@ -355,7 +347,7 @@ func alertManager() {  		for i := range alerts.Alerts {  			expression, err := govaluate.NewEvaluableExpression(alerts.Alerts[i].Expr)  			if err != nil { -				log.Printf(err.Error()) +				log.Error().Err(err)  				continue  			} @@ -381,7 +373,7 @@ func alertManager() {  						log.Printf(err.err.Error())  					}  				default: -					log.Fatal("this shouldnt have happened") +					log.Error().Err(errors.New("this shouldnt have happened"))  				}  			} @@ -390,27 +382,28 @@ func alertManager() {  				case price := <-priceChan:  					parameters[price.name] = price.price  				default: -					log.Fatal("this shouldnt have happened") +					log.Error().Err(errors.New("this shouldnt have happened"))  				}  			}  			fmt.Println("parameters:", parameters)  			result, err := expression.Evaluate(parameters)  			if err != nil { -				log.Println(err.Error()) +				log.Error().Err(err)  			}  			var resultBool bool  			fmt.Println("result:", result)  			resultBool = result.(bool)  			if resultBool == true { -				bot, err := tgbotapi.NewBotAPI(getTGToken()) +				// bot := getTgBot() +				token := os.Getenv(TELEGRAM_BOT_TOKEN_ENV_VAR) +				bot, err := tgbotapi.NewBotAPI(token[1 : len(token)-1])  				if err != nil { -					log.Panic(err) +					log.Error().Err(err)  				} -				// bot := getTgBot()  				msgText := "notification " + alerts.Alerts[i].Expr + " has been triggered" -				msg := tgbotapi.NewMessage(botChannelID, msgText) +				msg := tgbotapi.NewMessage(*botChannelID, msgText)  				bot.Send(msg)  			}  		} @@ -438,32 +431,114 @@ func addAlertHandler(w http.ResponseWriter, r *http.Request) {  	json.Unmarshal(bodyBytes, &bodyJSON)  	fmt.Println(bodyJSON) +	if bodyJSON.Name == "" || bodyJSON.Expr == "" { +		json.NewEncoder(w).Encode(map[string]interface{}{ +			"isSuccessful": false, +			"error":        "not all parameters are valid."}) +		log.Fatal().Err(errors.New("not all parameters are valid.")) +		return +	} +  	// rdb := getRedisClient()  	rdb := redis.NewClient(&redis.Options{ -		Addr:         *redisAddress, -		Password:     *redisPassword, -		DB:           int(*redisDB), -		MinIdleConns: 1, +		Addr:     *redisAddress, +		Password: *redisPassword, +		DB:       int(*redisDB),  	})  	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": ""}) +	json.NewEncoder(w).Encode(map[string]interface{}{ +		"isSuccessful": true, +		"error":        ""}) + +	err = rdb.Close() +	if err != nil { +		log.Error().Err(err) +	} +} + +func exHandler(w http.ResponseWriter, r *http.Request) { +	if r.Method != "GET" { +		http.Error(w, "Method is not supported.", http.StatusNotFound) +	} + +	apiKey := os.Getenv(CHANGELLY_API_KEY_ENV_VAR) +	apiSecret := os.Getenv(CHANGELLY_API_SECRET_ENV_VAR) + +	body := struct { +		Jsonrpc string   `json:"jsonrpc"` +		Id      string   `json:"id"` +		Method  string   `json:"method"` +		Params  []string `json:"params"` +	}{ +		Jsonrpc: "2.0", +		Id:      "test", +		Method:  "getCurrencies", +		Params:  nil} + +	bodyJSON, err := json.Marshal(body) +	if err != nil { +		log.Error().Err(err) +	} + +	secretBytes := []byte(apiSecret[1 : len(apiSecret)-1]) +	mac := hmac.New(sha512.New, secretBytes) +	mac.Write(bodyJSON) + +	client := &http.Client{} +	req, err := http.NewRequest("POST", changellyURL, bytes.NewReader(bodyJSON)) +	if err != nil { +		log.Error().Err(err) +	} + +	macDigest := hex.EncodeToString(mac.Sum(nil)) +	req.Header.Add("Content-Type", "application/json") +	req.Header.Add("api-key", apiKey[1:len(apiKey)-1]) +	req.Header.Add("sign", macDigest) + +	resp, err := client.Do(req) +	if err != nil { +		log.Error().Err(err) +	} +	defer resp.Body.Close() + +	responseBody, err := ioutil.ReadAll(resp.Body) +	log.Printf(string(responseBody)) + +	responseUnmarshalled := struct { +		Jsonrpc string   `json:"jsonrpc"` +		Id      string   `json:"id"` +		Result  []string `json:"result"` +	}{} + +	err = json.Unmarshal(responseBody, &responseUnmarshalled) +	if err != nil { +		log.Error().Err(err) +	} + +	json.NewEncoder(w).Encode(responseUnmarshalled)  }  func startServer() {  	http.HandleFunc("/health", healthHandler) -	http.HandleFunc("/crypto", cryptoHandler) +	http.HandleFunc("/price", priceHandler)  	http.HandleFunc("/pair", pairHandler)  	http.HandleFunc("/addalert", addAlertHandler) +	http.HandleFunc("/ex", exHandler)  	if err := http.ListenAndServe(":"+*flagPort, nil); err != nil { -		log.Fatal(err) +		log.Fatal().Err(err)  	}  } +func setupLogging() { +	zerolog.TimeFieldFormat = zerolog.TimeFormatUnix +} +  func main() { +	setupLogging()  	go runTgBot()  	go alertManager()  	startServer() diff --git a/test/endpoints.sh b/test/endpoints.sh new file mode 100755 index 0000000..0c47cd6 --- /dev/null +++ b/test/endpoints.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env sh +set -e +set -x + +curl -X GET http://127.0.0.1:8008/price?name=CAKE&unit=USD +curl -X GET http://127.0.0.1:8008/pair?one=ETH&two=CAKE&multiplier=4.0 +curl -X POST -H "Content-Type: application/json" -d '{"name":"alert1", "expr":"ETH>CAKE"}' http://127.0.0.1:8008/addalert | 
