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