init code
This commit is contained in:
21
internal/configs/binance/asset.go
Normal file
21
internal/configs/binance/asset.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package binance
|
||||||
|
|
||||||
|
const (
|
||||||
|
AssetUsdt = "USDT"
|
||||||
|
AssetUsdc = "USDC"
|
||||||
|
AssetFdusd = "FDUSD"
|
||||||
|
AssetBtc = "BTC"
|
||||||
|
AssetBnb = "BNB"
|
||||||
|
AssetEth = "ETH"
|
||||||
|
AssetXrp = "XRP"
|
||||||
|
)
|
||||||
|
|
||||||
|
var SymbolPrefixList = []string{"1000"}
|
||||||
|
var SymbolSuffixMap = map[string]string{
|
||||||
|
"USDT": "",
|
||||||
|
"USDC": "c",
|
||||||
|
}
|
||||||
|
|
||||||
|
var Future2SpotSymbolMap = map[string]string{
|
||||||
|
"LUNA2USDT": "LUNAUSDT",
|
||||||
|
}
|
||||||
5
internal/configs/config.go
Normal file
5
internal/configs/config.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package configs
|
||||||
|
|
||||||
|
var (
|
||||||
|
LogEnv = true
|
||||||
|
)
|
||||||
7
internal/configs/key/env.go
Normal file
7
internal/configs/key/env.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package key
|
||||||
|
|
||||||
|
const (
|
||||||
|
LogEnv = "LOG_ENV"
|
||||||
|
|
||||||
|
TelegramToken = "TELEGRAM_TOKEN"
|
||||||
|
)
|
||||||
20
internal/configs/strategy/market.go
Normal file
20
internal/configs/strategy/market.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package strategy
|
||||||
|
|
||||||
|
const (
|
||||||
|
symbolBTC = "BTCUSDT"
|
||||||
|
symbolETH = "ETHUSDT"
|
||||||
|
symbolBNB = "BNBUSDT"
|
||||||
|
symbolTON = "TONUSDT"
|
||||||
|
symbolXRP = "XRPUSDT"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
TopPriceSymbols = []string{symbolBTC, symbolETH, symbolBNB, symbolTON, symbolXRP}
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
FundRateBuyThreshold = -0.001
|
||||||
|
FundRateSellThreshold = 0.00077
|
||||||
|
|
||||||
|
LiquidInfPrice = 1000_000_000
|
||||||
|
)
|
||||||
17
internal/configs/tele/sticker.go
Normal file
17
internal/configs/tele/sticker.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package tele
|
||||||
|
|
||||||
|
const (
|
||||||
|
StickerSet = "verichains"
|
||||||
|
|
||||||
|
StickerTop = "AgADfQwAAinYuVQ"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Token2StickerIdxMap = map[string]int{
|
||||||
|
"BNB": 3,
|
||||||
|
"TON": 7,
|
||||||
|
}
|
||||||
|
|
||||||
|
var Sticker2TokenMap = map[string]string{
|
||||||
|
"AgAD-BQAAtQesFQ": "TON",
|
||||||
|
"AgADxhEAAoNAIFQ": "BNB",
|
||||||
|
}
|
||||||
8
internal/data/imarket.go
Normal file
8
internal/data/imarket.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package data
|
||||||
|
|
||||||
|
type IMarket interface {
|
||||||
|
GetFuturePrice(symbol string) (float64, float64, int64, bool)
|
||||||
|
GetAllFundRate() (map[string]float64, map[string]int64)
|
||||||
|
|
||||||
|
GetSpotPrice(symbol string) (float64, bool)
|
||||||
|
}
|
||||||
50
internal/data/market/future_price.go
Normal file
50
internal/data/market/future_price.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package market
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/adshao/go-binance/v2/futures"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (ms MarketData) GetFuturePrice(symbol string) (float64, float64, int64, bool) {
|
||||||
|
p, ok := ms.futureMarkPrice[symbol]
|
||||||
|
if !ok {
|
||||||
|
return 0, 0, 0, false
|
||||||
|
}
|
||||||
|
return p, ms.futureFundingRate[symbol], ms.futureNextFundingTime[symbol], true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ms MarketData) StartFutureWsMarkPrice() error {
|
||||||
|
_, _, err := futures.WsAllMarkPriceServe(ms.futureWsMarkPriceHandler, ms.futureWsErrHandler)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ms MarketData) futureWsMarkPriceHandler(event futures.WsAllMarkPriceEvent) {
|
||||||
|
for _, priceEvent := range event {
|
||||||
|
price, err := strconv.ParseFloat(priceEvent.MarkPrice, 64)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fundingRate, err := strconv.ParseFloat(priceEvent.FundingRate, 64)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ms.futureMarkPrice[priceEvent.Symbol] = price
|
||||||
|
ms.futureFundingRate[priceEvent.Symbol] = fundingRate
|
||||||
|
ms.futureNextFundingTime[priceEvent.Symbol] = priceEvent.NextFundingTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ms MarketData) futureWsErrHandler(err error) {
|
||||||
|
log.Debug().Err(err).Msg("Ws Error. Restart socket")
|
||||||
|
_ = ms.StartFutureWsMarkPrice()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ms MarketData) GetAllFundRate() (map[string]float64, map[string]int64) {
|
||||||
|
return ms.futureFundingRate, ms.futureNextFundingTime
|
||||||
|
}
|
||||||
25
internal/data/market/main.go
Normal file
25
internal/data/market/main.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package market
|
||||||
|
|
||||||
|
import "github.com/rs/zerolog/log"
|
||||||
|
|
||||||
|
type MarketData struct {
|
||||||
|
futureMarkPrice map[string]float64
|
||||||
|
futureFundingRate map[string]float64
|
||||||
|
futureNextFundingTime map[string]int64
|
||||||
|
|
||||||
|
spotPrice map[string]float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMarketData() *MarketData {
|
||||||
|
log.Info().Msg("Start market service")
|
||||||
|
ms := &MarketData{
|
||||||
|
futureMarkPrice: make(map[string]float64),
|
||||||
|
futureFundingRate: make(map[string]float64),
|
||||||
|
futureNextFundingTime: make(map[string]int64),
|
||||||
|
|
||||||
|
spotPrice: make(map[string]float64),
|
||||||
|
}
|
||||||
|
_ = ms.StartFutureWsMarkPrice()
|
||||||
|
_ = ms.StartSpotWsMarkPrice()
|
||||||
|
return ms
|
||||||
|
}
|
||||||
36
internal/data/market/spot_price.go
Normal file
36
internal/data/market/spot_price.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package market
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/adshao/go-binance/v2"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (ms MarketData) GetSpotPrice(symbol string) (float64, bool) {
|
||||||
|
p, ok := ms.spotPrice[symbol]
|
||||||
|
return p, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ms MarketData) StartSpotWsMarkPrice() error {
|
||||||
|
_, _, err := binance.WsAllMarketsStatServe(ms.spotWsAllMarketsStatHandler, ms.spotWsErrHandler) //.WsAllMarkPriceServe(ms.futureWsMarkPriceHandler, ms.futureWsErrHandler)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ms MarketData) spotWsAllMarketsStatHandler(event binance.WsAllMarketsStatEvent) {
|
||||||
|
for _, priceEvent := range event {
|
||||||
|
price, err := strconv.ParseFloat(priceEvent.LastPrice, 64)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ms.spotPrice[priceEvent.Symbol] = price
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ms MarketData) spotWsErrHandler(err error) {
|
||||||
|
log.Debug().Err(err).Msg("Spot Ws Error. Restart socket")
|
||||||
|
_ = ms.StartSpotWsMarkPrice()
|
||||||
|
}
|
||||||
15
internal/data/repo.go
Normal file
15
internal/data/repo.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package data
|
||||||
|
|
||||||
|
import "sync"
|
||||||
|
|
||||||
|
var (
|
||||||
|
Market IMarket
|
||||||
|
once sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewRepository(market IMarket) {
|
||||||
|
once.Do(
|
||||||
|
func() {
|
||||||
|
Market = market
|
||||||
|
})
|
||||||
|
}
|
||||||
12
internal/deps/data.go
Normal file
12
internal/deps/data.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package deps
|
||||||
|
|
||||||
|
import (
|
||||||
|
"me.thuanle/bbot/internal/data"
|
||||||
|
market2 "me.thuanle/bbot/internal/data/market"
|
||||||
|
)
|
||||||
|
|
||||||
|
func InitData() {
|
||||||
|
marketData := market2.NewMarketData()
|
||||||
|
|
||||||
|
data.NewRepository(marketData)
|
||||||
|
}
|
||||||
95
internal/helper/binancex/symbol.go
Normal file
95
internal/helper/binancex/symbol.go
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
package binancex
|
||||||
|
|
||||||
|
import (
|
||||||
|
"me.thuanle/bbot/internal/configs/binance"
|
||||||
|
"me.thuanle/bbot/internal/data"
|
||||||
|
"me.thuanle/bbot/internal/utils/stringx"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Symbol2Token(sym string) string {
|
||||||
|
token := strings.ToUpper(sym)
|
||||||
|
|
||||||
|
for _, prefix := range binance.SymbolPrefixList {
|
||||||
|
token, _ = strings.CutPrefix(token, prefix)
|
||||||
|
}
|
||||||
|
for suffix, abbr := range binance.SymbolSuffixMap {
|
||||||
|
var f bool
|
||||||
|
token, f = stringx.ReplaceSuffix(token, suffix, abbr)
|
||||||
|
if f {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
checkingPrefixList = append(binance.SymbolPrefixList, "")
|
||||||
|
)
|
||||||
|
|
||||||
|
func testSym(sym string) bool {
|
||||||
|
_, _, _, test := data.Market.GetFuturePrice(sym)
|
||||||
|
return test
|
||||||
|
}
|
||||||
|
|
||||||
|
func Token2Symbols(token string) []string {
|
||||||
|
var syms []string
|
||||||
|
if !stringx.IsAlphaNumeric(token) {
|
||||||
|
return syms
|
||||||
|
}
|
||||||
|
|
||||||
|
token = strings.ToUpper(token)
|
||||||
|
|
||||||
|
for _, prefix := range checkingPrefixList {
|
||||||
|
prefix = strings.ToUpper(prefix)
|
||||||
|
|
||||||
|
s := prefix + token
|
||||||
|
|
||||||
|
//no suffix
|
||||||
|
if testSym(s) {
|
||||||
|
syms = append(syms, s)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for suffix, abbr := range binance.SymbolSuffixMap {
|
||||||
|
suffix = strings.ToUpper(suffix)
|
||||||
|
abbr = strings.ToUpper(abbr)
|
||||||
|
|
||||||
|
//suffix
|
||||||
|
sym := s + suffix
|
||||||
|
if testSym(sym) {
|
||||||
|
syms = append(syms, sym)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
//suffix abbr
|
||||||
|
symAbr, found := stringx.ReplaceSuffix(s, abbr, suffix)
|
||||||
|
if found && testSym(symAbr) {
|
||||||
|
syms = append(syms, symAbr)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return syms
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsToken(s string) bool {
|
||||||
|
return len(Token2Symbols(s)) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Future2SpotSymbol convert future symbol to spot symbol
|
||||||
|
func Future2SpotSymbol(sym string) string {
|
||||||
|
for _, prefix := range binance.SymbolPrefixList {
|
||||||
|
var f bool
|
||||||
|
sym, f = strings.CutPrefix(sym, prefix)
|
||||||
|
if f {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spotSym, ok := binance.Future2SpotSymbolMap[sym]
|
||||||
|
if !ok {
|
||||||
|
return sym
|
||||||
|
}
|
||||||
|
return spotSym
|
||||||
|
}
|
||||||
27
internal/load.go
Normal file
27
internal/load.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"me.thuanle/bbot/internal/configs"
|
||||||
|
"me.thuanle/bbot/internal/configs/key"
|
||||||
|
"me.thuanle/bbot/internal/utils/envx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Startup() {
|
||||||
|
prepareEnv()
|
||||||
|
|
||||||
|
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
||||||
|
|
||||||
|
log.Info().Msg("Starting up...")
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareEnv() {
|
||||||
|
if err := godotenv.Load(); err != nil {
|
||||||
|
fmt.Println("Error loading .env file", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configs.LogEnv, _ = envx.GetEnvBool(key.LogEnv)
|
||||||
|
}
|
||||||
52
internal/services/controllers/market.go
Normal file
52
internal/services/controllers/market.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"me.thuanle/bbot/internal/configs/strategy"
|
||||||
|
"me.thuanle/bbot/internal/data"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetTopPrices returns the top symbol, price and rate
|
||||||
|
func GetTopPrices() ([]string, []float64, []float64) {
|
||||||
|
n := len(strategy.TopPriceSymbols)
|
||||||
|
topSym := make([]string, n)
|
||||||
|
topPrice := make([]float64, n)
|
||||||
|
topRate := make([]float64, n)
|
||||||
|
|
||||||
|
for i, sym := range strategy.TopPriceSymbols {
|
||||||
|
price, rate, _, ok := data.Market.GetFuturePrice(sym)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
topSym[i] = sym
|
||||||
|
topPrice[i] = price
|
||||||
|
topRate[i] = rate
|
||||||
|
|
||||||
|
}
|
||||||
|
return topSym, topPrice, topRate
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTopFundingFee returns the top funding fee: symbol, rate and cd time
|
||||||
|
func GetTopFundingFee() ([]string, []float64, []int64) {
|
||||||
|
allRates, allCd := data.Market.GetAllFundRate()
|
||||||
|
var resSym []string
|
||||||
|
for sym, rate := range allRates {
|
||||||
|
if strategy.FundRateBuyThreshold < rate && rate < strategy.FundRateSellThreshold {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
resSym = append(resSym, sym)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(resSym, func(i, j int) bool {
|
||||||
|
return allRates[resSym[i]] < allRates[resSym[j]]
|
||||||
|
})
|
||||||
|
|
||||||
|
resRate := make([]float64, len(resSym))
|
||||||
|
resCd := make([]int64, len(resSym))
|
||||||
|
for i, sym := range resSym {
|
||||||
|
resRate[i] = allRates[sym]
|
||||||
|
resCd[i] = allCd[sym]
|
||||||
|
}
|
||||||
|
return resSym, resRate, resCd
|
||||||
|
}
|
||||||
9
internal/services/tele/chat/helper/wrap.go
Normal file
9
internal/services/tele/chat/helper/wrap.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package helper
|
||||||
|
|
||||||
|
func Pre(msg string) string {
|
||||||
|
return "<pre>" + msg + "</pre>"
|
||||||
|
}
|
||||||
|
|
||||||
|
func Code(msg string) string {
|
||||||
|
return "<code>" + msg + "</code>"
|
||||||
|
}
|
||||||
39
internal/services/tele/chat/reply.go
Normal file
39
internal/services/tele/chat/reply.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package chat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"gopkg.in/telebot.v3"
|
||||||
|
"me.thuanle/bbot/internal/services/tele/chat/helper"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ReplyMessage(context telebot.Context, formattedText string, opts ...interface{}) error {
|
||||||
|
if opts == nil {
|
||||||
|
return context.Reply(formattedText, telebot.ModeHTML, telebot.NoPreview)
|
||||||
|
}
|
||||||
|
return context.Reply(formattedText, append(opts, telebot.ModeHTML, telebot.NoPreview)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReplyMessagef(context telebot.Context, menu *telebot.ReplyMarkup, format string, v ...interface{}) error {
|
||||||
|
if menu != nil {
|
||||||
|
return ReplyMessage(context, fmt.Sprintf(format, v...), menu)
|
||||||
|
} else {
|
||||||
|
return ReplyMessage(context, fmt.Sprintf(format, v...))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReplyMessageCode(context telebot.Context, message string) error {
|
||||||
|
return ReplyMessage(context, helper.Code(message))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReplyMessagePre(context telebot.Context, message string) error {
|
||||||
|
return ReplyMessage(context, helper.Pre(message))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReplyMessageJson(context telebot.Context, obj any) error {
|
||||||
|
jsonStr, err := json.MarshalIndent(obj, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return ReplyMessagePre(context, string(jsonStr))
|
||||||
|
}
|
||||||
45
internal/services/tele/command.go
Normal file
45
internal/services/tele/command.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package tele
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"gopkg.in/telebot.v3"
|
||||||
|
"me.thuanle/bbot/internal/services/tele/commands"
|
||||||
|
"me.thuanle/bbot/internal/services/tele/middlewares"
|
||||||
|
)
|
||||||
|
|
||||||
|
var commandList = []telebot.Command{
|
||||||
|
{
|
||||||
|
Text: "p",
|
||||||
|
Description: "(p) - Get mark price",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Text: "fee",
|
||||||
|
Description: "(f) - show top funding fee",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupCommands(b *telebot.Bot) error {
|
||||||
|
if err := b.SetCommands(commandList); err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("setup telebot commands")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Use(middlewares.IgnoreBot)
|
||||||
|
b.Use(middlewares.SendErrorMiddleware)
|
||||||
|
|
||||||
|
//welcome
|
||||||
|
b.Handle("/start", commands.OnStart)
|
||||||
|
|
||||||
|
//general
|
||||||
|
b.Handle("/ip", commands.OnGetIp)
|
||||||
|
|
||||||
|
//info
|
||||||
|
b.Handle("/p", commands.OnGetTopPrices)
|
||||||
|
b.Handle("/fee", commands.OnGetTopFundingFee)
|
||||||
|
|
||||||
|
//any text
|
||||||
|
b.Handle(telebot.OnText, commands.OnChatHandler)
|
||||||
|
b.Handle(telebot.OnSticker, commands.OnStickerHandler)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
43
internal/services/tele/commands/chat.go
Normal file
43
internal/services/tele/commands/chat.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"gopkg.in/telebot.v3"
|
||||||
|
"me.thuanle/bbot/internal/configs/tele"
|
||||||
|
"me.thuanle/bbot/internal/helper/binancex"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func OnChatHandler(context telebot.Context) error {
|
||||||
|
text := strings.ToLower(context.Text())
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case text == "p":
|
||||||
|
return OnGetTopPrices(context)
|
||||||
|
case text == "f" || text == "fee":
|
||||||
|
return OnGetTopFundingFee(context)
|
||||||
|
case binancex.IsToken(text):
|
||||||
|
return OnTokenInfo(context)
|
||||||
|
default:
|
||||||
|
//ignore it
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func OnStickerHandler(context telebot.Context) error {
|
||||||
|
stickerId := context.Message().Sticker.UniqueID
|
||||||
|
switch stickerId {
|
||||||
|
case tele.StickerTop:
|
||||||
|
return OnGetTopPrices(context)
|
||||||
|
default:
|
||||||
|
token, ok := tele.Sticker2TokenMap[stickerId]
|
||||||
|
if !ok {
|
||||||
|
log.Debug().
|
||||||
|
Any("sticker", context.Message().Sticker).
|
||||||
|
Msg("Sticker received")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return OnTokenInfoByToken(context, token)
|
||||||
|
}
|
||||||
|
}
|
||||||
12
internal/services/tele/commands/general.go
Normal file
12
internal/services/tele/commands/general.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gopkg.in/telebot.v3"
|
||||||
|
"me.thuanle/bbot/internal/services/tele/chat"
|
||||||
|
"me.thuanle/bbot/internal/utils/netx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func OnGetIp(context telebot.Context) error {
|
||||||
|
ip := netx.GetPublicIp()
|
||||||
|
return chat.ReplyMessageCode(context, ip)
|
||||||
|
}
|
||||||
5
internal/services/tele/commands/helper/error.go
Normal file
5
internal/services/tele/commands/helper/error.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package helper
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var ErrorParamCountMismatch = errors.New("param count mismatched")
|
||||||
41
internal/services/tele/commands/helper/handler.go
Normal file
41
internal/services/tele/commands/helper/handler.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package helper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gopkg.in/telebot.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func throwErrorParamCountMismatch(_ telebot.Context) error {
|
||||||
|
return ErrorParamCountMismatch
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base on the number of Args in context, it will execute the (n+1)-th function in the list
|
||||||
|
// - If the context have 0 argument, it will call funcs[0]
|
||||||
|
// - If the context have 1 argument, it will call funcs[1]
|
||||||
|
// Otherwise, it will call `other` function
|
||||||
|
func HandleZ0nArgs(other telebot.HandlerFunc, funcs ...telebot.HandlerFunc) telebot.HandlerFunc {
|
||||||
|
return func(context telebot.Context) error {
|
||||||
|
args := context.Args()
|
||||||
|
|
||||||
|
if len(args) > len(funcs)-1 {
|
||||||
|
return other(context)
|
||||||
|
}
|
||||||
|
return funcs[len(args)](context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base on the number of Args in context, it will execute the (n+1)-th function in the list
|
||||||
|
// - If the context have 0 argument, it will call funcs[0]
|
||||||
|
// - If the context have 1 argument, it will call funcs[1]
|
||||||
|
// Otherwise, it will throw error
|
||||||
|
func Handle0nArgs(funcs ...telebot.HandlerFunc) telebot.HandlerFunc {
|
||||||
|
return HandleZ0nArgs(throwErrorParamCountMismatch, funcs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleGetSet(getFunc telebot.HandlerFunc, setFunc telebot.HandlerFunc) telebot.HandlerFunc {
|
||||||
|
return func(context telebot.Context) error {
|
||||||
|
if len(context.Args()) == 0 {
|
||||||
|
return getFunc(context)
|
||||||
|
}
|
||||||
|
return setFunc(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
18
internal/services/tele/commands/market.go
Normal file
18
internal/services/tele/commands/market.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gopkg.in/telebot.v3"
|
||||||
|
"me.thuanle/bbot/internal/services/controllers"
|
||||||
|
"me.thuanle/bbot/internal/services/tele/chat"
|
||||||
|
"me.thuanle/bbot/internal/services/tele/view"
|
||||||
|
)
|
||||||
|
|
||||||
|
func OnGetTopPrices(context telebot.Context) error {
|
||||||
|
sym, price, rate := controllers.GetTopPrices()
|
||||||
|
return chat.ReplyMessagePre(context, view.RenderOnGetTopPricesMessage(sym, price, rate))
|
||||||
|
}
|
||||||
|
|
||||||
|
func OnGetTopFundingFee(context telebot.Context) error {
|
||||||
|
fee, float64s, cds := controllers.GetTopFundingFee()
|
||||||
|
return chat.ReplyMessagePre(context, view.RenderOnGetTopFundingFeeMessage(fee, float64s, cds))
|
||||||
|
}
|
||||||
11
internal/services/tele/commands/start.go
Normal file
11
internal/services/tele/commands/start.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
import "gopkg.in/telebot.v3"
|
||||||
|
|
||||||
|
const what = `Welcome on board! Here is some tips:
|
||||||
|
- /p to get pricing
|
||||||
|
- /l to list current positions`
|
||||||
|
|
||||||
|
func OnStart(context telebot.Context) error {
|
||||||
|
return context.Send(what)
|
||||||
|
}
|
||||||
49
internal/services/tele/commands/token.go
Normal file
49
internal/services/tele/commands/token.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gopkg.in/telebot.v3"
|
||||||
|
"me.thuanle/bbot/internal/configs/tele"
|
||||||
|
"me.thuanle/bbot/internal/data"
|
||||||
|
"me.thuanle/bbot/internal/helper/binancex"
|
||||||
|
"me.thuanle/bbot/internal/services/tele/chat"
|
||||||
|
"me.thuanle/bbot/internal/services/tele/view"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func showStickerMode(context telebot.Context, token string) {
|
||||||
|
token = strings.ToUpper(token)
|
||||||
|
stickerIdx, ok := tele.Token2StickerIdxMap[token]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stickers, err := context.Bot().StickerSet(tele.StickerSet)
|
||||||
|
if err == nil && stickers != nil {
|
||||||
|
_ = context.Reply(&stickers.Stickers[stickerIdx])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func OnTokenInfoByToken(context telebot.Context, token string) error {
|
||||||
|
symbols := binancex.Token2Symbols(token)
|
||||||
|
if len(symbols) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
showStickerMode(context, token)
|
||||||
|
|
||||||
|
fp, fundRate, fundTime, ok := data.Market.GetFuturePrice(symbols[0])
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
sSymbol := binancex.Future2SpotSymbol(symbols[0])
|
||||||
|
sp, _ := data.Market.GetSpotPrice(sSymbol)
|
||||||
|
|
||||||
|
_ = chat.ReplyMessage(context, view.RenderOnPriceMessage(symbols[0], sp, fp, fundRate, fundTime))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func OnTokenInfo(context telebot.Context) error {
|
||||||
|
text := strings.ToLower(context.Text())
|
||||||
|
|
||||||
|
return OnTokenInfoByToken(context, text)
|
||||||
|
}
|
||||||
19
internal/services/tele/middlewares/error.go
Normal file
19
internal/services/tele/middlewares/error.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package middlewares
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"gopkg.in/telebot.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SendErrorMiddleware(next telebot.HandlerFunc) telebot.HandlerFunc {
|
||||||
|
return func(c telebot.Context) error {
|
||||||
|
err := next(c) // continue execution chain
|
||||||
|
if err != nil {
|
||||||
|
log.Debug().Err(err).Msg("Chat Middleware error reply")
|
||||||
|
return c.Reply(fmt.Sprintf("Error: %v", err), telebot.RemoveKeyboard)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
14
internal/services/tele/middlewares/ignore_bot.go
Normal file
14
internal/services/tele/middlewares/ignore_bot.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package middlewares
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gopkg.in/telebot.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func IgnoreBot(next telebot.HandlerFunc) telebot.HandlerFunc {
|
||||||
|
return func(c telebot.Context) error {
|
||||||
|
if c.Sender().IsBot {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return next(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
51
internal/services/tele/tele_service.go
Normal file
51
internal/services/tele/tele_service.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package tele
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"gopkg.in/telebot.v3"
|
||||||
|
"me.thuanle/bbot/internal/configs/key"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
b *telebot.Bot
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ts *Service
|
||||||
|
once sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
func Ins() *Service {
|
||||||
|
if ts != nil {
|
||||||
|
return ts
|
||||||
|
}
|
||||||
|
once.Do(func() {
|
||||||
|
pref := telebot.Settings{
|
||||||
|
Token: os.Getenv(key.TelegramToken),
|
||||||
|
Poller: &telebot.LongPoller{Timeout: 10 * time.Second},
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := telebot.NewBot(pref)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("Failed to create bot")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = setupCommands(b)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("Failed to setup commands")
|
||||||
|
}
|
||||||
|
|
||||||
|
ts = &Service{
|
||||||
|
b: b,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return ts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Service) Start() {
|
||||||
|
log.Info().Msg("Starting telegram service")
|
||||||
|
t.b.Start()
|
||||||
|
}
|
||||||
34
internal/services/tele/view/fee.go
Normal file
34
internal/services/tele/view/fee.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package view
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/jedib0t/go-pretty/v6/table"
|
||||||
|
"me.thuanle/bbot/internal/helper/binancex"
|
||||||
|
"me.thuanle/bbot/internal/services/tele/view/helper"
|
||||||
|
"me.thuanle/bbot/internal/utils/opx"
|
||||||
|
"me.thuanle/bbot/internal/utils/timex"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func IconOfFundingFeeDirection(fund float64) string {
|
||||||
|
return opx.Ite(fund > 0, "📉", "📈")
|
||||||
|
}
|
||||||
|
|
||||||
|
func RenderOnGetTopFundingFeeMessage(sym []string, rate []float64, cd []int64) string {
|
||||||
|
t := helper.NewNoBorderTableWriter("Symbol", "Rate", "Cd")
|
||||||
|
|
||||||
|
flag := true
|
||||||
|
for i, s := range sym {
|
||||||
|
if rate[i] > 0 && flag {
|
||||||
|
t.AppendRow(table.Row{"...", "...", "..."})
|
||||||
|
flag = false
|
||||||
|
}
|
||||||
|
t.AppendRow(table.Row{
|
||||||
|
binancex.Symbol2Token(s),
|
||||||
|
fmt.Sprintf("%.3f%%", rate[i]*100),
|
||||||
|
fmt.Sprintf("%s", timex.CdMinuteStringTime(time.UnixMilli(cd[i]))),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.Render()
|
||||||
|
}
|
||||||
27
internal/services/tele/view/helper/table.go
Normal file
27
internal/services/tele/view/helper/table.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package helper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/jedib0t/go-pretty/v6/table"
|
||||||
|
"github.com/jedib0t/go-pretty/v6/text"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewNoBorderTableWriter(headers ...string) table.Writer {
|
||||||
|
t := table.NewWriter()
|
||||||
|
t.Style().Options.DrawBorder = false
|
||||||
|
t.Style().Options.SeparateColumns = false
|
||||||
|
|
||||||
|
var configs []table.ColumnConfig
|
||||||
|
configs = append(configs, table.ColumnConfig{Number: 1, AlignHeader: text.AlignCenter, Align: text.AlignLeft, AlignFooter: text.AlignLeft})
|
||||||
|
for i := 2; i <= len(headers); i++ {
|
||||||
|
configs = append(configs, table.ColumnConfig{Number: i, AlignHeader: text.AlignCenter, Align: text.AlignRight, AlignFooter: text.AlignRight})
|
||||||
|
}
|
||||||
|
t.SetColumnConfigs(configs)
|
||||||
|
|
||||||
|
var h table.Row
|
||||||
|
for _, header := range headers {
|
||||||
|
h = append(h, header)
|
||||||
|
}
|
||||||
|
t.AppendHeader(h)
|
||||||
|
|
||||||
|
return t
|
||||||
|
}
|
||||||
60
internal/services/tele/view/price.go
Normal file
60
internal/services/tele/view/price.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package view
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/jedib0t/go-pretty/v6/table"
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
"golang.org/x/text/message"
|
||||||
|
"me.thuanle/bbot/internal/helper/binancex"
|
||||||
|
"me.thuanle/bbot/internal/services/tele/view/helper"
|
||||||
|
"me.thuanle/bbot/internal/utils/timex"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RenderPrice(price float64) string {
|
||||||
|
mFmt := message.NewPrinter(language.AmericanEnglish)
|
||||||
|
switch {
|
||||||
|
case price > 1000:
|
||||||
|
return mFmt.Sprintf("%0.0f", price)
|
||||||
|
case price > 100:
|
||||||
|
return mFmt.Sprintf("%0.1f", price)
|
||||||
|
case price > 10:
|
||||||
|
return mFmt.Sprintf("%0.2f", price)
|
||||||
|
case price > 1:
|
||||||
|
return mFmt.Sprintf("%0.3f", price)
|
||||||
|
case price > 0.1:
|
||||||
|
return mFmt.Sprintf("%0.4f", price)
|
||||||
|
case price > 0.01:
|
||||||
|
return mFmt.Sprintf("%0.5f", price)
|
||||||
|
case price > 0.001:
|
||||||
|
return mFmt.Sprintf("%0.6f", price)
|
||||||
|
case price > 0.0001:
|
||||||
|
return mFmt.Sprintf("%0.7f", price)
|
||||||
|
default:
|
||||||
|
return mFmt.Sprintf("%0.8f", price)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func RenderOnPriceMessage(symbol string, spotPrice float64, futurePrice float64, fundrate float64, fundtime int64) string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"%s: %s\n"+
|
||||||
|
"Fund rate: %.4f%% %s in %s\n"+
|
||||||
|
"Spot: %s",
|
||||||
|
RenderSymbolInfo(symbol), RenderPrice(futurePrice),
|
||||||
|
fundrate*100, IconOfFundingFeeDirection(fundrate), timex.CdMinuteStringTime(time.UnixMilli(fundtime)),
|
||||||
|
RenderPrice(spotPrice),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RenderOnGetTopPricesMessage(symbols []string, price []float64, fundRate []float64) string {
|
||||||
|
t := helper.NewNoBorderTableWriter("", "Price", "Rate")
|
||||||
|
mFmt := message.NewPrinter(language.AmericanEnglish)
|
||||||
|
for i, symbol := range symbols {
|
||||||
|
t.AppendRow(table.Row{
|
||||||
|
binancex.Symbol2Token(symbol),
|
||||||
|
mFmt.Sprintf("%.2f", price[i]),
|
||||||
|
mFmt.Sprintf("%.3f%%", fundRate[i]*100),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return t.Render()
|
||||||
|
}
|
||||||
11
internal/services/tele/view/symbol.go
Normal file
11
internal/services/tele/view/symbol.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package view
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"me.thuanle/bbot/internal/helper/binancex"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RenderSymbolInfo(sym string) string {
|
||||||
|
token := binancex.Symbol2Token(sym)
|
||||||
|
return fmt.Sprintf("<a href=\"https://www.binance.com/futures/%s\">%s</a>", sym, token)
|
||||||
|
}
|
||||||
42
internal/utils/collectionx/count_map.go
Normal file
42
internal/utils/collectionx/count_map.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package collectionx
|
||||||
|
|
||||||
|
import "github.com/samber/lo"
|
||||||
|
|
||||||
|
type CountMapN[T comparable] struct {
|
||||||
|
N int
|
||||||
|
Map map[T][]int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCountMapN[T comparable](n int) *CountMapN[T] {
|
||||||
|
return &CountMapN[T]{
|
||||||
|
N: n,
|
||||||
|
Map: make(map[T][]int),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cm *CountMapN[T]) Inc(key T, ith int, value int) {
|
||||||
|
m, ok := cm.Map[key]
|
||||||
|
if !ok {
|
||||||
|
m = make([]int, cm.N)
|
||||||
|
cm.Map[key] = m
|
||||||
|
}
|
||||||
|
|
||||||
|
m[ith] += value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cm *CountMapN[T]) Get(key T, ith int) int {
|
||||||
|
m, ok := cm.Map[key]
|
||||||
|
if !ok {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return m[ith]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cm *CountMapN[T]) Sum(key T) int {
|
||||||
|
m, ok := cm.Map[key]
|
||||||
|
if !ok {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return lo.Sum(m)
|
||||||
|
}
|
||||||
31
internal/utils/convertx/number.go
Normal file
31
internal/utils/convertx/number.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package convertx
|
||||||
|
|
||||||
|
import "strconv"
|
||||||
|
|
||||||
|
func Int642String(i int64) string {
|
||||||
|
return strconv.FormatInt(i, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
func String2Float64(s string, defValue ...float64) float64 {
|
||||||
|
f64, err := strconv.ParseFloat(s, 64)
|
||||||
|
if err != nil {
|
||||||
|
return defValue[0]
|
||||||
|
}
|
||||||
|
return f64
|
||||||
|
}
|
||||||
|
|
||||||
|
func String2Int(s string, defValue ...int) int {
|
||||||
|
i64, err := strconv.Atoi(s)
|
||||||
|
if err != nil {
|
||||||
|
return defValue[0]
|
||||||
|
}
|
||||||
|
return i64
|
||||||
|
}
|
||||||
|
|
||||||
|
func String2Int64(s string, defValue ...int64) int64 {
|
||||||
|
i64, err := strconv.ParseInt(s, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return defValue[0]
|
||||||
|
}
|
||||||
|
return i64
|
||||||
|
}
|
||||||
47
internal/utils/envx/envx.go
Normal file
47
internal/utils/envx/envx.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package envx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"me.thuanle/bbot/internal/configs"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetEnvJsonArray[T any](key string) ([]T, error) {
|
||||||
|
values, found := os.LookupEnv(key)
|
||||||
|
if !found {
|
||||||
|
return nil, errors.New("env not found. Key: " + key)
|
||||||
|
}
|
||||||
|
var des []T
|
||||||
|
err := json.Unmarshal([]byte(values), &des)
|
||||||
|
if configs.LogEnv {
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).
|
||||||
|
Str("key", key).
|
||||||
|
Str("value", values).
|
||||||
|
Msg("Failed to load env")
|
||||||
|
} else {
|
||||||
|
log.Info().
|
||||||
|
Str("key", key).
|
||||||
|
Interface("value", des).
|
||||||
|
Msg("Loaded env")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return des, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetEnvBool(key string) (bool, error) {
|
||||||
|
value, found := os.LookupEnv(key)
|
||||||
|
if !found {
|
||||||
|
return false, errors.New("env not found. Key: " + key)
|
||||||
|
}
|
||||||
|
val := value == "true" || value == "1"
|
||||||
|
if configs.LogEnv {
|
||||||
|
log.Info().
|
||||||
|
Str("key", key).
|
||||||
|
Bool("value", val).
|
||||||
|
Msg("Loaded env")
|
||||||
|
}
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
9
internal/utils/errorx/error.go
Normal file
9
internal/utils/errorx/error.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package errorx
|
||||||
|
|
||||||
|
import "github.com/rs/zerolog/log"
|
||||||
|
|
||||||
|
func PanicOnError(err error, msg string) {
|
||||||
|
if err != nil {
|
||||||
|
log.Panic().Err(err).Msg(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
23
internal/utils/mathx/maxmin.go
Normal file
23
internal/utils/mathx/maxmin.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package mathx
|
||||||
|
|
||||||
|
import "cmp"
|
||||||
|
|
||||||
|
func MaxN[T cmp.Ordered](a T, b ...T) T {
|
||||||
|
m := a
|
||||||
|
for _, v := range b {
|
||||||
|
if v > m {
|
||||||
|
m = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func MinN[T cmp.Ordered](a T, b ...T) T {
|
||||||
|
m := a
|
||||||
|
for _, v := range b {
|
||||||
|
if v < m {
|
||||||
|
m = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
21
internal/utils/netx/ip.go
Normal file
21
internal/utils/netx/ip.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package netx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetPublicIp() string {
|
||||||
|
req, err := http.Get("http://ip.thuanle.me")
|
||||||
|
if err != nil {
|
||||||
|
return err.Error()
|
||||||
|
}
|
||||||
|
defer req.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(req.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(body)
|
||||||
|
}
|
||||||
10
internal/utils/opx/ternary.go
Normal file
10
internal/utils/opx/ternary.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package opx
|
||||||
|
|
||||||
|
// Ite If-then-else
|
||||||
|
func Ite[T any](condition bool, trueValue T, falseValue T) T {
|
||||||
|
if condition {
|
||||||
|
return trueValue
|
||||||
|
} else {
|
||||||
|
return falseValue
|
||||||
|
}
|
||||||
|
}
|
||||||
9
internal/utils/slicex/slice.go
Normal file
9
internal/utils/slicex/slice.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package slicex
|
||||||
|
|
||||||
|
import "sort"
|
||||||
|
|
||||||
|
func Sort[T any](slice []T, compareTo func(o1, o2 T) bool) {
|
||||||
|
sort.Slice(slice, func(i, j int) bool {
|
||||||
|
return compareTo(slice[i], slice[j])
|
||||||
|
})
|
||||||
|
}
|
||||||
11
internal/utils/stringx/replace.go
Normal file
11
internal/utils/stringx/replace.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package stringx
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
func ReplaceSuffix(s string, suffix string, newSuffix string) (string, bool) {
|
||||||
|
if strings.HasSuffix(s, suffix) {
|
||||||
|
replaced := s[:len(s)-len(suffix)] + newSuffix
|
||||||
|
return replaced, true
|
||||||
|
}
|
||||||
|
return s, false
|
||||||
|
}
|
||||||
9
internal/utils/stringx/validation.go
Normal file
9
internal/utils/stringx/validation.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package stringx
|
||||||
|
|
||||||
|
import "regexp"
|
||||||
|
|
||||||
|
func IsAlphaNumeric(s string) bool {
|
||||||
|
match, _ := regexp.MatchString("^[a-zA-Z0-9]*$", s)
|
||||||
|
return match
|
||||||
|
|
||||||
|
}
|
||||||
19
internal/utils/timex/cd.go
Normal file
19
internal/utils/timex/cd.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package timex
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CdMinuteStringDuration(dur time.Duration) string {
|
||||||
|
dur -= dur % time.Minute
|
||||||
|
rString := dur.String()
|
||||||
|
if strings.HasSuffix(rString, "0s") {
|
||||||
|
rString = rString[:len(rString)-2]
|
||||||
|
}
|
||||||
|
return rString
|
||||||
|
}
|
||||||
|
|
||||||
|
func CdMinuteStringTime(t time.Time) string {
|
||||||
|
return CdMinuteStringDuration(-time.Since(t))
|
||||||
|
}
|
||||||
6
internal/utils/typex/pair.go
Normal file
6
internal/utils/typex/pair.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package typex
|
||||||
|
|
||||||
|
type Pair[T any, V any] struct {
|
||||||
|
First T
|
||||||
|
Second V
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user