init code

This commit is contained in:
thuanle
2024-10-24 09:53:23 +07:00
parent a7559b3f9d
commit 92a63c7885
43 changed files with 1115 additions and 0 deletions

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

View File

@@ -0,0 +1,5 @@
package configs
var (
LogEnv = true
)

View File

@@ -0,0 +1,7 @@
package key
const (
LogEnv = "LOG_ENV"
TelegramToken = "TELEGRAM_TOKEN"
)

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

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

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

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

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

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

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

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

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

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

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

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

View File

@@ -0,0 +1,5 @@
package helper
import "errors"
var ErrorParamCountMismatch = errors.New("param count mismatched")

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View File

@@ -0,0 +1,9 @@
package stringx
import "regexp"
func IsAlphaNumeric(s string) bool {
match, _ := regexp.MatchString("^[a-zA-Z0-9]*$", s)
return match
}

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

View File

@@ -0,0 +1,6 @@
package typex
type Pair[T any, V any] struct {
First T
Second V
}