From b872b73ebf10a1d1c5e84f71b9d8bf5e6aa466bf Mon Sep 17 00:00:00 2001 From: Yiyang Kang Date: Fri, 9 Dec 2022 14:49:38 +0800 Subject: [PATCH] feat: add inline buttons to traffic command --- bot.go | 233 ++-------------------------------------- botcmd_exchangerates.go | 170 +++++++++++++++++++++++++++++ botcmd_traffic.go | 140 ++++++++++++++++++++++++ 3 files changed, 320 insertions(+), 223 deletions(-) create mode 100644 botcmd_exchangerates.go create mode 100644 botcmd_traffic.go diff --git a/bot.go b/bot.go index 5f68502..8bb8905 100644 --- a/bot.go +++ b/bot.go @@ -4,20 +4,14 @@ import ( "fmt" "html" "math" - "regexp" - "strconv" "strings" "text/tabwriter" "time" - "git.gensokyo.cafe/kkyy/mycurrencynet" - "github.com/dustin/go-humanize" "github.com/go-errors/errors" - "github.com/samber/lo" tele "gopkg.in/telebot.v3" "git.gensokyo.cafe/kkyy/tgbot_misaka_5882f7/cmds" - "git.gensokyo.cafe/kkyy/tgbot_misaka_5882f7/stats" ) const ( @@ -62,6 +56,9 @@ func initBot() (*tele.Bot, error) { adminGrp := b.Group() adminGrp.Use(adminMiddleware) adminGrp.Handle("/traffic", handleTrafficCmd) + adminGrp.Handle(&trafficBtnDays, handleTrafficBtnDays) + adminGrp.Handle(&trafficBtnMonths, handleTrafficBtnMonths) + adminGrp.Handle("/dig", handleDigCmd) // adminGrp.Handle("/test", handleTestCmd) @@ -71,7 +68,13 @@ func initBot() (*tele.Bot, error) { func adminMiddleware(next tele.HandlerFunc) tele.HandlerFunc { return func(c tele.Context) error { - if !isFromAdmin(c.Sender()) { + u := c.Sender() + if u == nil { + if cb := c.Callback(); cb != nil { + u = cb.Sender + } + } + if !isFromAdmin(u) { return nil } return next(c) @@ -97,59 +100,6 @@ func handleStartCmd(c tele.Context) error { return c.Send("Hi :)") } -func handleTrafficCmd(c tele.Context) error { - dailyTraffic, err := stats.VnstatDailyTraffic(config.WatchedInterface) - if err != nil { - _ = c.Reply(stickerFromID(stickerPanic), tele.Silent) - return err - } - monthlyTraffic, err := stats.VnstatMonthlyTraffic(config.WatchedInterface) - if err != nil { - _ = c.Reply(stickerFromID(stickerPanic), tele.Silent) - return err - } - - var responseParts = []string{"*Traffic Usage Summaries*\n"} - // Yesterday's traffic if present - if len(dailyTraffic) > 1 { - row := dailyTraffic[len(dailyTraffic)-2] - day := fmt.Sprintf("%d-%02d-%02d", row.Date.Year, row.Date.Month, row.Date.Day) - responseParts = append(responseParts, fmt.Sprintf("Yesterday (%s):` %s`", day, fmtTraffic(row))) - } - // Today's traffic if present - if len(dailyTraffic) > 0 { - row := dailyTraffic[len(dailyTraffic)-1] - responseParts = append(responseParts, fmt.Sprintf("Today so far:` %s`\n", fmtTraffic(row))) - } - - // Last month's traffic, if present - if len(monthlyTraffic) > 1 { - row := monthlyTraffic[len(monthlyTraffic)-2] - month := fmt.Sprintf("%d-%02d", row.Date.Year, row.Date.Month) - responseParts = append(responseParts, fmt.Sprintf("Last month (%s):` %s`", month, fmtTraffic(row))) - } - // This month's traffic, if present - if len(monthlyTraffic) > 0 { - row := monthlyTraffic[len(monthlyTraffic)-1] - responseParts = append(responseParts, fmt.Sprintf("This month so far:` %s`", fmtTraffic(row))) - responseParts = append(responseParts, drawBarForTrafficRecord(row)) - } - - var respText string - if len(responseParts) == 1 { - respText = "No traffic data available." - } else { - respText = strings.Join(responseParts, "\n") - } - - return c.Reply(respText, &tele.SendOptions{ParseMode: tele.ModeMarkdown}, tele.Silent) -} - -func fmtTraffic(r stats.VnstatTrafficRecord) string { - effective := lo.Max([]uint64{r.Rx, r.Tx}) - return fmt.Sprintf("%.2f GiB", float64(effective)/1024/1024/1024) -} - func handleUserInfoCmd(c tele.Context) error { u := c.Sender() if u == nil { @@ -221,13 +171,6 @@ func drawBar(progress float64, length int) string { return string(buf) } -func drawBarForTrafficRecord(r stats.VnstatTrafficRecord) string { - effective := lo.Max([]uint64{r.Rx, r.Tx}) - max := config.MonthlyTrafficLimitGiB - ratio := float64(effective) / 1024 / 1024 / 1024 / float64(max) - return fmt.Sprintf("`%s %2.0f%%`", drawBar(ratio, 16), ratio*100) -} - func handleYearProgressCmd(c tele.Context) error { yearStart := time.Date(time.Now().Year(), 1, 1, 0, 0, 0, 0, time.Local) yearEnd := yearStart.AddDate(1, 0, 0) @@ -297,159 +240,3 @@ func handleDigCmd(c tele.Context) error { } return c.Reply(strings.Join(replyText, ""), &tele.SendOptions{ParseMode: tele.ModeHTML}, tele.Silent) } - -func handleExchangeRateCmd(c tele.Context) error { - msg := c.Message() - if msg == nil { - return nil - } - - if msg.Payload == "" { - return c.Reply("Usage: `/xr [ ...] [to] `", - &tele.SendOptions{ParseMode: tele.ModeMarkdown}, - tele.Silent, - ) - } - - from, to, err := parseXrRequest(msg.Payload) - if err != nil { - return c.Reply(err.Error(), tele.Silent) - } - codes := append([]string{to.Code}, lo.Map(from, func(i xrCurrency, _ int) string { return i.Code })...) - currencies, err := exchangeRates.Get(codes...) - if err != nil { - return c.Reply(err.Error(), tele.Silent) - } - toCur := currencies[0] - - // format reply - var replyLines []string - for i, fromItem := range from { - fromCur := currencies[i+1] - replyLines = append(replyLines, fmtXrPair(fromCur, toCur, fromItem.Amount)) - } - - return c.Reply(strings.Join(replyLines, "\n"), &tele.SendOptions{ParseMode: tele.ModeMarkdown}, tele.Silent) -} - -type xrCurrency struct { - Code string - Amount float64 -} - -func parseXrRequest(input string) (from []xrCurrency, to xrCurrency, err error) { - args := strings.Fields(input) - - var tmpTo *xrCurrency = nil - fillFrom := true - - c := xrCurrency{Amount: -1} - for _, arg := range args { - arg = strings.ToUpper(arg) - - switch { - case arg == "TO": - fillFrom = false - continue - - case fillFrom: - num, sym, tErr := parseXrToken(arg) - if tErr != nil { - err = tErr - return - } - if num != -1 { - if c.Amount != -1 { - err = errors.New("invalid input") - return - } - c.Amount = num - } - if c.Amount == -1 { - c.Amount = 1 - } - - if sym != "" { - c.Code = sym - from = append(from, c) - c = xrCurrency{Amount: -1} - } - - case !fillFrom: - if tmpTo != nil { - err = errors.New("invalid input") - return - } - tmpTo = &xrCurrency{Code: arg} - - default: - err = errors.New("invalid input") - return - } - } - - if len(from) < 1 || len(from) > 10 { - err = errors.New("invalid input") - return - } - - if fillFrom { - if len(from) < 2 { - err = errors.New("invalid input") - return - } - tmpTo = &from[len(from)-1] - from = from[:len(from)-1] - } - - if tmpTo == nil { - err = errors.New("invalid input") - return - } - to = *tmpTo - - return -} - -var xrTokenRe = regexp.MustCompile(`^([0-9.]+)?([A-Z]{3,})?$`) - -func parseXrToken(token string) (num float64, sym string, err error) { - num = -1 - matches := xrTokenRe.FindStringSubmatch(token) - if len(matches) != 3 { - err = errors.New("invalid input") - return - } - - if matches[1] != "" { - num, err = strconv.ParseFloat(matches[1], 64) - if err != nil { - return - } - if num <= 0 { - err = errors.New("invalid input: amount must be positive") - return - } - } - - sym = matches[2] - if num == 0 && sym == "" { - err = errors.New("invalid input") - } - return -} - -func fmtXrPair(from, to *mycurrencynet.Currency, amount float64) string { - rate := from.To(to) * amount - fromInfo := fmt.Sprintf("%s (%s)", from.Name, from.Code) - toInfo := fmt.Sprintf("%s (%s)", to.Name, to.Code) - - return fmt.Sprintf("*%s* %s\n= *%s* %s\n", fmtXrFloat(amount), fromInfo, fmtXrFloat(rate), toInfo) -} - -func fmtXrFloat(num float64) string { - if num < 100 { - return fmt.Sprintf("%.4g", num) - } - return humanize.CommafWithDigits(num, 2) -} diff --git a/botcmd_exchangerates.go b/botcmd_exchangerates.go new file mode 100644 index 0000000..36c5d1a --- /dev/null +++ b/botcmd_exchangerates.go @@ -0,0 +1,170 @@ +package main + +import ( + "fmt" + "regexp" + "strconv" + "strings" + + "git.gensokyo.cafe/kkyy/mycurrencynet" + "github.com/dustin/go-humanize" + "github.com/go-errors/errors" + "github.com/samber/lo" + tele "gopkg.in/telebot.v3" +) + +func handleExchangeRateCmd(c tele.Context) error { + msg := c.Message() + if msg == nil { + return nil + } + + if msg.Payload == "" { + return c.Reply("Usage: `/xr [ ...] [to] `", + &tele.SendOptions{ParseMode: tele.ModeMarkdown}, + tele.Silent, + ) + } + + from, to, err := parseXrRequest(msg.Payload) + if err != nil { + return c.Reply(err.Error(), tele.Silent) + } + codes := append([]string{to.Code}, lo.Map(from, func(i xrCurrency, _ int) string { return i.Code })...) + currencies, err := exchangeRates.Get(codes...) + if err != nil { + return c.Reply(err.Error(), tele.Silent) + } + toCur := currencies[0] + + // format reply + var replyLines []string + for i, fromItem := range from { + fromCur := currencies[i+1] + replyLines = append(replyLines, fmtXrPair(fromCur, toCur, fromItem.Amount)) + } + + return c.Reply(strings.Join(replyLines, "\n"), &tele.SendOptions{ParseMode: tele.ModeMarkdown}, tele.Silent) +} + +type xrCurrency struct { + Code string + Amount float64 +} + +func parseXrRequest(input string) (from []xrCurrency, to xrCurrency, err error) { + args := strings.Fields(input) + + var tmpTo *xrCurrency = nil + fillFrom := true + + c := xrCurrency{Amount: -1} + for _, arg := range args { + arg = strings.ToUpper(arg) + + switch { + case arg == "TO": + fillFrom = false + continue + + case fillFrom: + num, sym, tErr := parseXrToken(arg) + if tErr != nil { + err = tErr + return + } + if num != -1 { + if c.Amount != -1 { + err = errors.New("invalid input") + return + } + c.Amount = num + } + if c.Amount == -1 { + c.Amount = 1 + } + + if sym != "" { + c.Code = sym + from = append(from, c) + c = xrCurrency{Amount: -1} + } + + case !fillFrom: + if tmpTo != nil { + err = errors.New("invalid input") + return + } + tmpTo = &xrCurrency{Code: arg} + + default: + err = errors.New("invalid input") + return + } + } + + if len(from) < 1 || len(from) > 10 { + err = errors.New("invalid input") + return + } + + if fillFrom { + if len(from) < 2 { + err = errors.New("invalid input") + return + } + tmpTo = &from[len(from)-1] + from = from[:len(from)-1] + } + + if tmpTo == nil { + err = errors.New("invalid input") + return + } + to = *tmpTo + + return +} + +var xrTokenRe = regexp.MustCompile(`^([0-9.]+)?([A-Z]{3,})?$`) + +func parseXrToken(token string) (num float64, sym string, err error) { + num = -1 + matches := xrTokenRe.FindStringSubmatch(token) + if len(matches) != 3 { + err = errors.New("invalid input") + return + } + + if matches[1] != "" { + num, err = strconv.ParseFloat(matches[1], 64) + if err != nil { + return + } + if num <= 0 { + err = errors.New("invalid input: amount must be positive") + return + } + } + + sym = matches[2] + if num == 0 && sym == "" { + err = errors.New("invalid input") + } + return +} + +func fmtXrPair(from, to *mycurrencynet.Currency, amount float64) string { + rate := from.To(to) * amount + fromInfo := fmt.Sprintf("%s (%s)", from.Name, from.Code) + toInfo := fmt.Sprintf("%s (%s)", to.Name, to.Code) + + return fmt.Sprintf("*%s* %s\n= *%s* %s\n", fmtXrFloat(amount), fromInfo, fmtXrFloat(rate), toInfo) +} + +func fmtXrFloat(num float64) string { + if num < 100 { + return fmt.Sprintf("%.4g", num) + } + return humanize.CommafWithDigits(num, 2) +} diff --git a/botcmd_traffic.go b/botcmd_traffic.go new file mode 100644 index 0000000..d1e1fd6 --- /dev/null +++ b/botcmd_traffic.go @@ -0,0 +1,140 @@ +package main + +import ( + "fmt" + "strings" + "time" + + "github.com/samber/lo" + tele "gopkg.in/telebot.v3" + + "git.gensokyo.cafe/kkyy/tgbot_misaka_5882f7/stats" +) + +var ( + trafficMenu = &tele.ReplyMarkup{} + trafficBtnDays, trafficBtnMonths tele.Btn +) + +func init() { + trafficBtnDays = trafficMenu.Data("🌝 Days", "btn_traffic_days") + trafficBtnMonths = trafficMenu.Data("🗓️ Months", "btn_traffic_months") + + trafficMenu.Inline(trafficMenu.Row(trafficBtnDays, trafficBtnMonths)) +} + +func handleTrafficCmd(c tele.Context) error { + dailyTraffic, err := stats.VnstatDailyTraffic(config.WatchedInterface) + if err != nil { + _ = c.Reply(stickerFromID(stickerPanic), tele.Silent) + return err + } + monthlyTraffic, err := stats.VnstatMonthlyTraffic(config.WatchedInterface) + if err != nil { + _ = c.Reply(stickerFromID(stickerPanic), tele.Silent) + return err + } + + var responseParts = []string{"*Traffic Usage Summaries*\n"} + // Yesterday's traffic if present + if len(dailyTraffic) > 1 { + row := dailyTraffic[len(dailyTraffic)-2] + day := fmt.Sprintf("%d-%02d-%02d", row.Date.Year, row.Date.Month, row.Date.Day) + responseParts = append(responseParts, fmt.Sprintf("Yesterday (%s):` %s`", day, fmtTraffic(row))) + } + // Today's traffic if present + if len(dailyTraffic) > 0 { + row := dailyTraffic[len(dailyTraffic)-1] + responseParts = append(responseParts, fmt.Sprintf("Today so far:` %s`\n", fmtTraffic(row))) + } + + // Last month's traffic, if present + if len(monthlyTraffic) > 1 { + row := monthlyTraffic[len(monthlyTraffic)-2] + month := fmt.Sprintf("%d-%02d", row.Date.Year, row.Date.Month) + responseParts = append(responseParts, fmt.Sprintf("Last month (%s):` %s`", month, fmtTraffic(row))) + } + // This month's traffic, if present + if len(monthlyTraffic) > 0 { + row := monthlyTraffic[len(monthlyTraffic)-1] + responseParts = append(responseParts, fmt.Sprintf("This month so far:` %s`", fmtTraffic(row))) + responseParts = append(responseParts, drawBarForTrafficRecord(row)) + } + + var respText string + if len(responseParts) == 1 { + respText = "No traffic data available." + } else { + respText = strings.Join(responseParts, "\n") + } + + return c.Reply(respText, &tele.SendOptions{ParseMode: tele.ModeMarkdown}, tele.Silent, trafficMenu) +} + +func fmtTraffic(r stats.VnstatTrafficRecord) string { + effective := lo.Max([]uint64{r.Rx, r.Tx}) + return fmt.Sprintf("%.2f GiB", float64(effective)/1024/1024/1024) +} + +func drawBarForTrafficRecord(r stats.VnstatTrafficRecord) string { + effective := lo.Max([]uint64{r.Rx, r.Tx}) + max := config.MonthlyTrafficLimitGiB + ratio := float64(effective) / 1024 / 1024 / 1024 / float64(max) + return fmt.Sprintf("`%s %2.0f%%`", drawBar(ratio, 16), ratio*100) +} + +func handleTrafficBtnDays(c tele.Context) error { + dailyTraffic, err := stats.VnstatDailyTraffic(config.WatchedInterface) + if err != nil { + _ = c.Reply(stickerFromID(stickerPanic), tele.Silent) + return err + } + + var responseParts = []string{ + "*Traffic usage of recent days*", + fmt.Sprintf("_updated at %s_\n", time.Now().Format("15:04:05")), + } + + offset := lo.Max([]int{0, len(dailyTraffic) - 14}) + for _, tr := range dailyTraffic[offset:] { + day := time.Date(int(tr.Date.Year), time.Month(tr.Date.Month), int(tr.Date.Day), 0, 0, 0, 0, time.Local) + dayStr := fmt.Sprintf("%s (%s)", day.Format("01-02"), day.Weekday().String()[0:3]) + + responseParts = append(responseParts, fmt.Sprintf("`%s: %10s`", dayStr, fmtTraffic(tr))) + } + + var respText string + if len(responseParts) == 1 { + respText = "No daily traffic data available." + } else { + respText = strings.Join(responseParts, "\n") + } + return c.Edit(respText, &tele.SendOptions{ParseMode: tele.ModeMarkdown}, trafficMenu) +} + +func handleTrafficBtnMonths(c tele.Context) error { + monthlyTraffic, err := stats.VnstatMonthlyTraffic(config.WatchedInterface) + if err != nil { + _ = c.Reply(stickerFromID(stickerPanic), tele.Silent) + return err + } + + var responseParts = []string{ + "*Traffic usage of recent months*", + fmt.Sprintf("_updated at %s_\n", time.Now().Format("15:04:05")), + } + for _, tr := range monthlyTraffic { + responseParts = append( + responseParts, + fmt.Sprintf("`%d-%02d: %10s`", tr.Date.Year, tr.Date.Month, fmtTraffic(tr)), + ) + } + + var respText string + if len(responseParts) == 1 { + respText = "No monthly traffic data available." + } else { + respText = strings.Join(responseParts, "\n") + } + return c.Edit(respText, &tele.SendOptions{ParseMode: tele.ModeMarkdown}, trafficMenu) +}