package main import ( "fmt" "regexp" "strconv" "strings" "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" ) var exchangeRates = mycurrencynet.New() func initExchangeRates() { if err := exchangeRates.Update(); err != nil { logger.Errorw("Failed to update exchange rates", "err", err) } logger.Info("Exchange rates updated") go exchangeRates.UpdateEvery( time.Hour, func(err error) { logger.Errorw("Failed to update exchange rates", "err", err) }, func() { logger.Info("Exchange rates updated") }, ) } 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) }