├── .github └── workflows │ ├── docker.yml │ └── test.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README-en.md ├── README.md ├── api-entrypoint.sh ├── api.Dockerfile ├── api └── api.go ├── bot-entrypoint.sh ├── bot.Dockerfile ├── cmd ├── api │ └── api.go ├── telebot │ ├── telebot.go │ └── telebot_test.go └── telebotctl │ ├── db │ └── db.go │ └── telebotctl.go ├── conf ├── config.go └── config_test.go ├── dal ├── bill │ ├── mock.go │ ├── mysql.go │ └── repository.go ├── telegram │ ├── mock.go │ ├── mysql.go │ └── repository.go └── user │ ├── mock.go │ ├── mysql.go │ └── repository.go ├── docker-compose.yaml ├── env ├── api.env.example ├── bot.env.example └── db.env.example ├── go.mod ├── go.sum ├── mock └── telebotmock │ └── mock.go ├── models ├── bill.go ├── telegram.go └── user.go ├── service ├── bill │ ├── bill.go │ └── bill_test.go ├── telegram │ ├── telegram.go │ └── telegram_test.go └── user │ ├── user.go │ └── user_test.go ├── telebot ├── handlers.go ├── handlers_test.go ├── inlines.go ├── senders.go ├── senders_test.go ├── telebot.go ├── telebot_test.go ├── templates.go ├── templates_test.go ├── user_state.go ├── user_state_mock.go └── user_state_test.go └── utils └── strings ├── strings.go └── token.go /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Build and Push Docker Image 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | env: 9 | DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} 10 | DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} 11 | 12 | jobs: 13 | build-and-push: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v2 18 | 19 | - name: Login to Docker Hub 20 | uses: docker/login-action@v1 21 | with: 22 | username: ${{ env.DOCKER_USERNAME }} 23 | password: ${{ env.DOCKER_PASSWORD }} 24 | 25 | - name: Push Bot Docker Image 26 | uses: docker/build-push-action@v6 27 | with: 28 | context: . 29 | file: ./bot.Dockerfile 30 | push: true 31 | tags: ${{ env.DOCKER_USERNAME }}/telegram-account-bot:latest 32 | 33 | - name: Push Api Docker Image 34 | uses: docker/build-push-action@v6 35 | with: 36 | context: . 37 | file: ./api.Dockerfile 38 | push: true 39 | tags: ${{ env.DOCKER_USERNAME }}/telegram-account-bot-api:latest -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Unit Tests 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | types: [opened, synchronize, reopened] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Set up Go 16 | uses: actions/setup-go@v5 17 | with: 18 | go-version: '1.20' 19 | 20 | - name: Test 21 | run: make test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | *.env -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Orenoid 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2016 The Kubernetes Authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # This version-strategy uses git tags to set the version string 16 | VERSION ?= $(shell git describe --tags --always --dirty) 17 | # 18 | # This version-strategy uses a manual value to set the version string 19 | #VERSION ?= 0.0.1 20 | 21 | help: # @HELP 打印帮助信息 22 | help: 23 | @echo "VARIABLES:" 24 | @echo 25 | @echo "TARGETS:" 26 | @grep -E '^.*: *# *@HELP' $(MAKEFILE_LIST) \ 27 | | awk ' \ 28 | BEGIN {FS = ": *# *@HELP"}; \ 29 | { printf " %-30s %s\n", $$1, $$2 }; \ 30 | ' 31 | 32 | version: # @HELP 版本信息 33 | version: 34 | @echo $(VERSION) 35 | 36 | mock: # @HELP 为 Repository 接口生成 mock 代码 37 | mock: 38 | mockgen -source=dal/user/repository.go -destination=dal/user/mock.go -package=user 39 | mockgen -source=dal/bill/repository.go -destination=dal/bill/mock.go -package=bill 40 | mockgen -source=dal/telegram/repository.go -destination=dal/telegram/mock.go -package=telegram 41 | mockgen -source=telebot/user_state.go -destination=telebot/user_state_mock.go -package=telebot 42 | mockgen -destination=mock/telebotmock/mock.go -package=telebotmock gopkg.in/telebot.v3 Context 43 | @echo Done. 44 | 45 | test: # @HELP 运行单元测试 46 | test: 47 | @GOARCH=amd64 go test -gcflags=all=-l -cover \ 48 | ./service/...\ 49 | ./conf/...\ 50 | ./telebot/...\ 51 | ./cmd/... 52 | 53 | .PHONY: help version mock test -------------------------------------------------------------------------------- /README-en.md: -------------------------------------------------------------------------------- 1 | English | [简体中文](./README.md) 2 | 3 | # Telegram Accounting Bot 4 | 5 | This is a Telegram accounting bot that can help you record expenses and income. 6 | 7 | ## Deployment 8 | 9 | 1. Install Docker and Docker Compose. 10 | 2. Clone this repository to your local machine. 11 | 3. Rename all `.example` files in the `env` directory to `.env` files and modify configurations such as the database name and password as needed. 12 | 4. In the renamed `bot.env` file, change the `TELEBOT_TOKEN` to your own Telegram Bot Token. 13 | 5. Navigate to the project directory in the terminal and run `docker-compose up -d` to start the containers. If you don't need the OpenAPI feature, you can run `docker-compose up -d bot` instead. 14 | 6. Open Telegram, search for your bot, and start using it. 15 | 16 | ## Usage 17 | 18 | Here are the currently available commands: 19 | 20 | - `/start` - Start using the bot 21 | - `/day` - View today's bill 22 | - `/month` - View this month's bill 23 | - `/set_keyboard` - Set quick access keyboard 24 | - `/cancel` - Cancel the current operation 25 | - `/set_balance` - Set the balance 26 | - `/balance` - Check the balance 27 | - `/create_token` - Create a token for OpenAPI 28 | - `/disable_all_tokens` - Disable all tokens 29 | 30 | ## TODO 31 | - [x] Automatically update Bot Commands 32 | - [x] Open API 33 | - [ ] Multilingual 34 | - [ ] Natural language interface 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [English](./README-en.md) | 简体中文 2 | 3 | # Telegram 记账机器人 4 | 5 | 这是一个 Telegram 记账机器人,可以帮助你记录支出和收入。 6 | 7 | ## 部署 8 | 9 | 1. 安装 Docker 和 Docker Compose 10 | 2. 克隆本仓库到本地 11 | 3. 将 env 目录中的 .example 文件都重命名为 .env 文件,并按需修改数据库名称、密码等配置 12 | 4. 在重命名后的 `bot.env` 文件中修改 `TELEBOT_TOKEN` 为你自己的 Telegram Bot Token 13 | 5. 在终端中进入项目目录,运行 `docker-compose up -d` 启动容器,如果你不需要 OpenAPI 功能,则执行 `docker-compose up -d bot` 即可 14 | 6. 打开 Telegram,搜索你的 Bot,开始使用 15 | 16 | ## 使用 17 | 18 | 以下是目前可用的命令: 19 | 20 | /start - 开始使用 21 | 22 | /day - 查看当日账单 23 | 24 | /month - 查看当月账单 25 | 26 | /set_keyboard - 设置快捷键盘 27 | 28 | /cancel - 取消当前操作 29 | 30 | /set_balance - 设置余额 31 | 32 | /balance - 查询余额 33 | 34 | /create_token 创建用于 OpenAPI 的 token 35 | 36 | /disable_all_tokens 废弃所有 token 37 | 38 | ## TODO 39 | - [x] Automatically update Bot Commands 40 | - [x] Open API 41 | - [ ] Multilingual 42 | - [ ] Natural language interface 43 | -------------------------------------------------------------------------------- /api-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ./api 3 | -------------------------------------------------------------------------------- /api.Dockerfile: -------------------------------------------------------------------------------- 1 | # Build Stage 2 | FROM golang:1.20-alpine AS build 3 | WORKDIR /app 4 | COPY . . 5 | RUN go build -o api ./cmd/api/api.go 6 | 7 | # Final Stage 8 | FROM alpine:3.14 9 | RUN apk update && apk add tzdata 10 | WORKDIR /root/ 11 | COPY --from=build /app/api . 12 | COPY api-entrypoint.sh . 13 | RUN chmod +x api-entrypoint.sh 14 | ENTRYPOINT ["./api-entrypoint.sh"] 15 | -------------------------------------------------------------------------------- /api/api.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | "time" 7 | 8 | "github.com/labstack/echo/v4" 9 | "github.com/orenoid/telegram-account-bot/service/bill" 10 | "github.com/orenoid/telegram-account-bot/service/user" 11 | ) 12 | 13 | func GetEcho(hub *ControllersHub) *echo.Echo { 14 | e := echo.New() 15 | 16 | // register controllers 17 | e.GET("/ping", func(c echo.Context) error { 18 | return c.String(http.StatusOK, "pong") 19 | }) 20 | e.POST("/openapi/bills", hub.importBills) 21 | 22 | return e 23 | } 24 | 25 | type ControllersHub struct { 26 | userService *user.Service 27 | billService *bill.Service 28 | } 29 | 30 | func NewControllersHub(userService *user.Service, billService *bill.Service) *ControllersHub { 31 | return &ControllersHub{userService: userService, billService: billService} 32 | } 33 | 34 | type ImportBillsRequest struct { 35 | Bills []struct { 36 | Amount float64 `json:"amount"` 37 | Category string `json:"category"` 38 | Name *string `json:"name,omitempty"` // optional 39 | CreatedAt *time.Time `json:"createdAt,omitempty"` // if not provided, then use current time as default 40 | } 41 | } 42 | 43 | func (hub *ControllersHub) importBills(c echo.Context) error { 44 | req := &ImportBillsRequest{} 45 | if err := c.Bind(req); err != nil { 46 | return echo.NewHTTPError(http.StatusBadRequest, err.Error()) 47 | } 48 | // parse token 49 | if len(c.Request().Header["Authorization"]) != 1 { 50 | return echo.NewHTTPError(http.StatusBadRequest, "invalid header: Authorization") 51 | } 52 | token, startWithBearer := strings.CutPrefix(c.Request().Header["Authorization"][0], "Bearer ") 53 | if !startWithBearer { 54 | return echo.NewHTTPError(http.StatusBadRequest, "invalid header: Authorization") 55 | } 56 | // query base user id 57 | userID, err := hub.userService.MustGetUserIDByToken(token) 58 | if err != nil { 59 | return err 60 | } 61 | createBillDTOs := make([]bill.CreateBillDTO, 0, len(req.Bills)) 62 | for _, reqBill := range req.Bills { 63 | billDTO := bill.CreateBillDTO{ 64 | Name: reqBill.Name, 65 | Category: reqBill.Category, 66 | Amount: reqBill.Amount, 67 | CreatedAt: reqBill.CreatedAt, 68 | } 69 | createBillDTOs = append(createBillDTOs, billDTO) 70 | } 71 | // create bills 72 | err = hub.billService.CreateNewBills(userID, createBillDTOs) 73 | if err != nil { 74 | return err 75 | } 76 | return c.JSON(http.StatusOK, nil) 77 | } 78 | -------------------------------------------------------------------------------- /bot-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ./telebotctl db migrate 3 | ./telebot 4 | -------------------------------------------------------------------------------- /bot.Dockerfile: -------------------------------------------------------------------------------- 1 | # Build Stage 2 | FROM golang:1.20-alpine AS build 3 | WORKDIR /app 4 | COPY . . 5 | RUN go build -o telebot ./cmd/telebot/telebot.go 6 | RUN go build -o telebotctl ./cmd/telebotctl/telebotctl.go 7 | 8 | # Final Stage 9 | FROM alpine:3.14 10 | RUN apk update && apk add tzdata 11 | WORKDIR /root/ 12 | COPY --from=build /app/telebot . 13 | COPY --from=build /app/telebotctl . 14 | COPY bot-entrypoint.sh . 15 | RUN chmod +x bot-entrypoint.sh 16 | ENTRYPOINT ["./bot-entrypoint.sh"] 17 | -------------------------------------------------------------------------------- /cmd/api/api.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/orenoid/telegram-account-bot/api" 5 | "github.com/orenoid/telegram-account-bot/conf" 6 | billdal "github.com/orenoid/telegram-account-bot/dal/bill" 7 | userdal "github.com/orenoid/telegram-account-bot/dal/user" 8 | billservice "github.com/orenoid/telegram-account-bot/service/bill" 9 | "github.com/orenoid/telegram-account-bot/service/user" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | var apiCmd = &cobra.Command{ 14 | Use: "api", 15 | Short: "api - start the api server", 16 | Run: func(cmd *cobra.Command, args []string) { 17 | config, err := conf.GetConfigFromEnv() 18 | if err != nil { 19 | panic(err) 20 | } 21 | billRepo, err := billdal.NewMysqlRepo(config.MysqlDSN) 22 | if err != nil { 23 | panic(err) 24 | } 25 | userRepo, err := userdal.NewMysqlRepo(config.MysqlDSN) 26 | if err != nil { 27 | panic(err) 28 | } 29 | 30 | billService := billservice.NewService(billRepo, userRepo) 31 | userService := user.NewUserService(userRepo) 32 | 33 | controllersHub := api.NewControllersHub(userService, billService) 34 | e := api.GetEcho(controllersHub) 35 | e.Logger.Fatal(e.Start(":1323")) 36 | }, 37 | } 38 | 39 | func main() { 40 | if err := apiCmd.Execute(); err != nil { 41 | panic(err) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /cmd/telebot/telebot.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/orenoid/telegram-account-bot/conf" 6 | billdal "github.com/orenoid/telegram-account-bot/dal/bill" 7 | teledal "github.com/orenoid/telegram-account-bot/dal/telegram" 8 | userdal "github.com/orenoid/telegram-account-bot/dal/user" 9 | billservice "github.com/orenoid/telegram-account-bot/service/bill" 10 | teleservice "github.com/orenoid/telegram-account-bot/service/telegram" 11 | "github.com/orenoid/telegram-account-bot/service/user" 12 | "github.com/orenoid/telegram-account-bot/telebot" 13 | "github.com/sirupsen/logrus" 14 | "github.com/spf13/cobra" 15 | tele "gopkg.in/telebot.v3" 16 | "time" 17 | ) 18 | 19 | var telebotCmd = &cobra.Command{ 20 | Use: "telebotctl", 21 | Short: "telebotctl - start the telegram bot", 22 | Run: func(cmd *cobra.Command, args []string) { 23 | config, err := conf.GetConfigFromEnv() 24 | if err != nil { 25 | panic(err) 26 | } 27 | 28 | settings := tele.Settings{ 29 | Token: config.TelebotToken, 30 | Poller: &tele.LongPoller{Timeout: 10 * time.Second}, 31 | } 32 | 33 | teleRepo, err := teledal.NewMysqlRepo(config.MysqlDSN) 34 | if err != nil { 35 | panic(err) 36 | } 37 | billRepo, err := billdal.NewMysqlRepo(config.MysqlDSN) 38 | if err != nil { 39 | panic(err) 40 | } 41 | userRepo, err := userdal.NewMysqlRepo(config.MysqlDSN) 42 | if err != nil { 43 | panic(err) 44 | } 45 | 46 | teleService := teleservice.NewService(teleRepo) 47 | billService := billservice.NewService(billRepo, userRepo) 48 | userService := user.NewUserService(userRepo) 49 | 50 | telegramUserStateManager := telebot.NewInMemoryUserStateManager() 51 | 52 | hub := telebot.NewHandlerHub(billService, teleService, userService, telegramUserStateManager) 53 | bot, err := telebot.NewBot(settings, hub) 54 | if err != nil { 55 | panic(err) 56 | } 57 | err = bot.SetCommands([]tele.Command{ 58 | {Text: "/help", Description: "查看使用帮助"}, 59 | {Text: "/start", Description: "初始化"}, 60 | {Text: "/day", Description: "今日收支"}, 61 | {Text: "/month", Description: "本月收支"}, 62 | {Text: "/cancel", Description: "取消当前操作"}, 63 | {Text: "/set_keyboard", Description: "设置快捷键盘"}, 64 | {Text: "/set_balance", Description: "设置余额"}, 65 | {Text: "/balance", Description: "查询余额"}, 66 | {Text: "/create_token", Description: "创建用于 OpenAPI 的 token"}, 67 | {Text: "/disable_all_tokens", Description: "废弃所有 token"}, 68 | }) 69 | if err != nil { 70 | logrus.Warnf("failed to set commands, err: %+v", err) 71 | } 72 | 73 | fmt.Println("Running telebot with a LongPoller...") 74 | bot.Start() 75 | 76 | }, 77 | } 78 | 79 | func main() { 80 | if err := telebotCmd.Execute(); err != nil { 81 | panic(err) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /cmd/telebot/telebot_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func TestCmd(t *testing.T) { 6 | t.Skip() 7 | } 8 | -------------------------------------------------------------------------------- /cmd/telebotctl/db/db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "fmt" 5 | "github.com/orenoid/telegram-account-bot/conf" 6 | "github.com/orenoid/telegram-account-bot/models" 7 | 8 | "github.com/spf13/cobra" 9 | "gorm.io/driver/mysql" 10 | "gorm.io/gorm" 11 | ) 12 | 13 | var Cmd = &cobra.Command{ 14 | Use: "db", 15 | Short: "操作数据库", 16 | } 17 | 18 | func init() { 19 | Cmd.AddCommand(migrateCmd) 20 | } 21 | 22 | var migrateCmd = &cobra.Command{ 23 | Use: "migrate", 24 | Short: "Automatically migrate database schema", 25 | Run: func(cmd *cobra.Command, args []string) { 26 | config, err := conf.GetConfigFromEnv() 27 | if err != nil { 28 | panic(err) 29 | } 30 | db, err := gorm.Open(mysql.Open(config.MysqlDSN), &gorm.Config{DisableAutomaticPing: false}) 31 | 32 | err = db.AutoMigrate(&models.User{}, &models.Bill{}, &models.TelegramUser{}, &models.Token{}) 33 | if err != nil { 34 | panic(err) 35 | } 36 | 37 | fmt.Println("Database schema migrated successfully") 38 | }, 39 | } 40 | -------------------------------------------------------------------------------- /cmd/telebotctl/telebotctl.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/orenoid/telegram-account-bot/cmd/telebotctl/db" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | var Cmd = &cobra.Command{ 9 | Use: "", 10 | Short: "", 11 | } 12 | 13 | func init() { 14 | Cmd.AddCommand(db.Cmd) 15 | } 16 | 17 | func main() { 18 | err := Cmd.Execute() 19 | if err != nil { 20 | panic(err) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /conf/config.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | "github.com/sirupsen/logrus" 6 | "github.com/spf13/viper" 7 | "reflect" 8 | ) 9 | 10 | type config struct { 11 | MysqlDSN string `mapstructure:"MYSQL_DSN"` 12 | TelebotToken string `mapstructure:"TELEBOT_TOKEN"` 13 | } 14 | 15 | func (c config) tags() []string { 16 | var tags []string 17 | t := reflect.TypeOf(c) 18 | for i := 0; i < t.NumField(); i++ { 19 | field := t.Field(i) 20 | tags = append(tags, field.Tag.Get("mapstructure")) 21 | } 22 | return tags 23 | } 24 | 25 | func GetConfigFromEnv() (config, error) { 26 | var c config 27 | viper.SetConfigName(".env") 28 | viper.SetConfigType("dotenv") 29 | viper.AddConfigPath(".") 30 | viper.AutomaticEnv() 31 | 32 | if err := viper.ReadInConfig(); err != nil { 33 | if _, ok := err.(viper.ConfigFileNotFoundError); ok { 34 | logrus.Info("The .env file has not been found in the current directory") 35 | for _, tag := range c.tags() { 36 | err = viper.BindEnv(tag) 37 | if err != nil { 38 | return config{}, errors.Wrapf(err, "failed to bind env: %s", tag) 39 | } 40 | } 41 | } else { 42 | return config{}, err 43 | } 44 | } 45 | 46 | if err := viper.Unmarshal(&c); err != nil { 47 | return config{}, errors.Wrapf(err, "failed to unmarshal config to struct") 48 | } 49 | 50 | return c, nil 51 | } 52 | -------------------------------------------------------------------------------- /conf/config_test.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "math/rand" 6 | "os" 7 | "strconv" 8 | "testing" 9 | ) 10 | 11 | func TestGetConfigFromEnv(t *testing.T) { 12 | envs := map[string]string{ 13 | "MYSQL_DSN": strconv.Itoa(rand.Int()), 14 | "TELEBOT_TOKEN": strconv.Itoa(rand.Int()), 15 | } 16 | for key, value := range envs { 17 | _ = os.Setenv(key, value) 18 | } 19 | conf, err := GetConfigFromEnv() 20 | assert.NoError(t, err) 21 | assert.Equal(t, envs["MYSQL_DSN"], conf.MysqlDSN) 22 | assert.Equal(t, envs["TELEBOT_TOKEN"], conf.TelebotToken) 23 | } 24 | -------------------------------------------------------------------------------- /dal/bill/mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: dal/bill/repository.go 3 | 4 | // Package bill is a generated GoMock package. 5 | package bill 6 | 7 | import ( 8 | reflect "reflect" 9 | 10 | gomock "github.com/golang/mock/gomock" 11 | models "github.com/orenoid/telegram-account-bot/models" 12 | ) 13 | 14 | // MockRepository is a mock of Repository interface. 15 | type MockRepository struct { 16 | ctrl *gomock.Controller 17 | recorder *MockRepositoryMockRecorder 18 | } 19 | 20 | // MockRepositoryMockRecorder is the mock recorder for MockRepository. 21 | type MockRepositoryMockRecorder struct { 22 | mock *MockRepository 23 | } 24 | 25 | // NewMockRepository creates a new mock instance. 26 | func NewMockRepository(ctrl *gomock.Controller) *MockRepository { 27 | mock := &MockRepository{ctrl: ctrl} 28 | mock.recorder = &MockRepositoryMockRecorder{mock} 29 | return mock 30 | } 31 | 32 | // EXPECT returns an object that allows the caller to indicate expected use. 33 | func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder { 34 | return m.recorder 35 | } 36 | 37 | // CreateBillAndUpdateUserBalance mocks base method. 38 | func (m *MockRepository) CreateBillAndUpdateUserBalance(userID uint, amount float64, category string, opts ...CreateBillOptions) (*models.Bill, error) { 39 | m.ctrl.T.Helper() 40 | varargs := []interface{}{userID, amount, category} 41 | for _, a := range opts { 42 | varargs = append(varargs, a) 43 | } 44 | ret := m.ctrl.Call(m, "CreateBillAndUpdateUserBalance", varargs...) 45 | ret0, _ := ret[0].(*models.Bill) 46 | ret1, _ := ret[1].(error) 47 | return ret0, ret1 48 | } 49 | 50 | // CreateBillAndUpdateUserBalance indicates an expected call of CreateBillAndUpdateUserBalance. 51 | func (mr *MockRepositoryMockRecorder) CreateBillAndUpdateUserBalance(userID, amount, category interface{}, opts ...interface{}) *gomock.Call { 52 | mr.mock.ctrl.T.Helper() 53 | varargs := append([]interface{}{userID, amount, category}, opts...) 54 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateBillAndUpdateUserBalance", reflect.TypeOf((*MockRepository)(nil).CreateBillAndUpdateUserBalance), varargs...) 55 | } 56 | 57 | // CreateBillsAndUpdateUserBalance mocks base method. 58 | func (m *MockRepository) CreateBillsAndUpdateUserBalance(userID uint, bills []CreateBillParams) error { 59 | m.ctrl.T.Helper() 60 | ret := m.ctrl.Call(m, "CreateBillsAndUpdateUserBalance", userID, bills) 61 | ret0, _ := ret[0].(error) 62 | return ret0 63 | } 64 | 65 | // CreateBillsAndUpdateUserBalance indicates an expected call of CreateBillsAndUpdateUserBalance. 66 | func (mr *MockRepositoryMockRecorder) CreateBillsAndUpdateUserBalance(userID, bills interface{}) *gomock.Call { 67 | mr.mock.ctrl.T.Helper() 68 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateBillsAndUpdateUserBalance", reflect.TypeOf((*MockRepository)(nil).CreateBillsAndUpdateUserBalance), userID, bills) 69 | } 70 | 71 | // DeleteBillAndUpdateUserBalance mocks base method. 72 | func (m *MockRepository) DeleteBillAndUpdateUserBalance(billID uint) error { 73 | m.ctrl.T.Helper() 74 | ret := m.ctrl.Call(m, "DeleteBillAndUpdateUserBalance", billID) 75 | ret0, _ := ret[0].(error) 76 | return ret0 77 | } 78 | 79 | // DeleteBillAndUpdateUserBalance indicates an expected call of DeleteBillAndUpdateUserBalance. 80 | func (mr *MockRepositoryMockRecorder) DeleteBillAndUpdateUserBalance(billID interface{}) *gomock.Call { 81 | mr.mock.ctrl.T.Helper() 82 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBillAndUpdateUserBalance", reflect.TypeOf((*MockRepository)(nil).DeleteBillAndUpdateUserBalance), billID) 83 | } 84 | 85 | // GetUserBillsByCreateTime mocks base method. 86 | func (m *MockRepository) GetUserBillsByCreateTime(userID uint, opts ...GetUserBillsByCreateTimeOptions) ([]*models.Bill, error) { 87 | m.ctrl.T.Helper() 88 | varargs := []interface{}{userID} 89 | for _, a := range opts { 90 | varargs = append(varargs, a) 91 | } 92 | ret := m.ctrl.Call(m, "GetUserBillsByCreateTime", varargs...) 93 | ret0, _ := ret[0].([]*models.Bill) 94 | ret1, _ := ret[1].(error) 95 | return ret0, ret1 96 | } 97 | 98 | // GetUserBillsByCreateTime indicates an expected call of GetUserBillsByCreateTime. 99 | func (mr *MockRepositoryMockRecorder) GetUserBillsByCreateTime(userID interface{}, opts ...interface{}) *gomock.Call { 100 | mr.mock.ctrl.T.Helper() 101 | varargs := append([]interface{}{userID}, opts...) 102 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserBillsByCreateTime", reflect.TypeOf((*MockRepository)(nil).GetUserBillsByCreateTime), varargs...) 103 | } 104 | -------------------------------------------------------------------------------- /dal/bill/mysql.go: -------------------------------------------------------------------------------- 1 | package bill 2 | 3 | import ( 4 | "database/sql" 5 | "github.com/orenoid/telegram-account-bot/models" 6 | "github.com/pkg/errors" 7 | "github.com/shopspring/decimal" 8 | "gorm.io/driver/mysql" 9 | "gorm.io/gorm" 10 | ) 11 | 12 | type mysqlRepo struct { 13 | db *gorm.DB 14 | } 15 | 16 | func (receiver *mysqlRepo) CreateBillsAndUpdateUserBalance(userID uint, billParams []CreateBillParams) error { 17 | err := receiver.db.Transaction(func(tx *gorm.DB) error { 18 | userModel := &models.User{} 19 | result := tx.Where("id = ?", userID).First(userModel) 20 | if result.Error != nil { 21 | return errors.WithStack(result.Error) 22 | } 23 | 24 | billRecords := make([]models.Bill, 0, len(billParams)) 25 | for _, billParam := range billParams { 26 | billRecord := &models.Bill{ 27 | UserID: userID, 28 | Amount: decimal.NewFromFloat(billParam.Amount), 29 | Category: billParam.Category, 30 | } 31 | if billParam.Name != nil { 32 | billRecord.Name = sql.NullString{String: *billParam.Name, Valid: true} 33 | } 34 | if billParam.CreatedAt != nil { 35 | billRecord.CreatedAt = *billParam.CreatedAt 36 | billRecord.UpdatedAt = *billParam.CreatedAt 37 | } 38 | billRecords = append(billRecords, *billRecord) 39 | } 40 | 41 | result = tx.Create(billRecords) 42 | if result.Error != nil { 43 | return errors.WithStack(result.Error) 44 | } 45 | if result.RowsAffected != int64(len(billRecords)) { 46 | return errors.New("failed to create bill") 47 | } 48 | 49 | if userModel.Balance.Valid { 50 | for _, billRecord := range billRecords { 51 | userModel.Balance.Decimal = userModel.Balance.Decimal.Add(billRecord.Amount) 52 | } 53 | result := tx.Save(userModel) 54 | if result.Error != nil { 55 | return errors.WithStack(result.Error) 56 | } 57 | if result.RowsAffected != 1 { 58 | return errors.New("failed to update user balance") 59 | } 60 | } 61 | return nil 62 | }) 63 | if err != nil { 64 | return errors.WithStack(err) 65 | } 66 | return nil 67 | } 68 | 69 | func (receiver *mysqlRepo) CreateBillAndUpdateUserBalance(userID uint, amount float64, category string, opts ...CreateBillOptions) (*models.Bill, error) { 70 | // TODO 解决并发更新余额问题,以及查询用户与更新余额的非原子操作场景 71 | var newBill *models.Bill 72 | err := receiver.db.Transaction(func(tx *gorm.DB) error { 73 | userModel := &models.User{} 74 | result := tx.Where("id = ?", userID).First(userModel) 75 | if result.Error != nil { 76 | return errors.WithStack(result.Error) 77 | } 78 | 79 | newBill = &models.Bill{UserID: userID, Amount: decimal.NewFromFloat(amount), Category: category} 80 | for _, opt := range opts { 81 | if opt.Name != nil { 82 | newBill.Name = sql.NullString{String: *opt.Name, Valid: true} 83 | } 84 | } 85 | 86 | result = tx.Create(newBill) 87 | if result.Error != nil { 88 | return errors.WithStack(result.Error) 89 | } 90 | if result.RowsAffected == 0 { 91 | return errors.New("failed to create bill") 92 | } 93 | 94 | if userModel.Balance.Valid { 95 | userModel.Balance.Decimal = userModel.Balance.Decimal.Add(newBill.Amount) 96 | result := tx.Save(userModel) 97 | if result.Error != nil { 98 | return errors.WithStack(result.Error) 99 | } 100 | if result.RowsAffected != 1 { 101 | return errors.New("failed to update user balance") 102 | } 103 | } 104 | return nil 105 | }) 106 | if err != nil { 107 | return nil, errors.WithStack(err) 108 | } 109 | return newBill, nil 110 | } 111 | 112 | func (receiver *mysqlRepo) GetUserBillsByCreateTime(userID uint, opts ...GetUserBillsByCreateTimeOptions) ([]*models.Bill, error) { 113 | var bills []*models.Bill 114 | query := receiver.db.Where("user_id = ?", userID) 115 | if len(opts) > 0 { 116 | opt := opts[0] 117 | if opt.GreaterOrEqual { 118 | query = query.Where("created_at >= ?", opt.GreaterThan) 119 | } else { 120 | query = query.Where("created_at = ?", opt.GreaterThan) 121 | } 122 | if opt.LessOrEqual { 123 | query = query.Where("created_at <= ?", opt.LessThan) 124 | } else { 125 | query = query.Where("created_at < ?", opt.LessThan) 126 | } 127 | } 128 | result := query.Find(&bills) 129 | if result.Error != nil { 130 | return nil, errors.WithStack(result.Error) 131 | } 132 | return bills, nil 133 | } 134 | 135 | func (receiver *mysqlRepo) DeleteBillAndUpdateUserBalance(billID uint) error { 136 | err := receiver.db.Transaction(func(tx *gorm.DB) error { 137 | // 删除订单 138 | billModel := &models.Bill{} 139 | result := tx.Where("id = ?", billID).First(billModel) 140 | if result.Error != nil { 141 | return errors.WithStack(result.Error) 142 | } 143 | result = tx.Delete(billModel) 144 | if result.Error != nil { 145 | return errors.WithStack(result.Error) 146 | } 147 | // 更新用户余额 148 | userModel := &models.User{} 149 | result = tx.Where("id = ?", billModel.UserID).First(userModel) 150 | if result.Error != nil { 151 | return errors.WithStack(result.Error) 152 | } 153 | if userModel.Balance.Valid { 154 | userModel.Balance.Decimal = userModel.Balance.Decimal.Sub(billModel.Amount) 155 | result := tx.Save(userModel) 156 | if result.Error != nil { 157 | return errors.WithStack(result.Error) 158 | } 159 | if result.RowsAffected != 1 { 160 | return errors.New("failed to update user balance") 161 | } 162 | } 163 | return nil 164 | }) 165 | return err 166 | } 167 | 168 | func NewMysqlRepo(dsn string) (*mysqlRepo, error) { 169 | db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{DisableAutomaticPing: true}) 170 | if err != nil { 171 | return nil, errors.WithStack(err) 172 | } 173 | return &mysqlRepo{db}, nil 174 | } 175 | -------------------------------------------------------------------------------- /dal/bill/repository.go: -------------------------------------------------------------------------------- 1 | package bill 2 | 3 | import ( 4 | "github.com/orenoid/telegram-account-bot/models" 5 | "time" 6 | ) 7 | 8 | type Repository interface { 9 | // CreateBillAndUpdateUserBalance 为用户创建一个账单,并更新用户余额(若用户余额不为空) 10 | CreateBillAndUpdateUserBalance(userID uint, amount float64, category string, opts ...CreateBillOptions) (*models.Bill, error) 11 | // CreateBillsAndUpdateUserBalance 为用户创建多个账单,并更新用户余额(若用户余额不为空) 12 | CreateBillsAndUpdateUserBalance(userID uint, bills []CreateBillParams) error 13 | // GetUserBillsByCreateTime 获取用户在指定时间范围内的账单列表,若 opts 为空,则返回账单(opts 只取列表第一个作为查询参数) 14 | GetUserBillsByCreateTime(userID uint, opts ...GetUserBillsByCreateTimeOptions) ([]*models.Bill, error) 15 | // DeleteBillAndUpdateUserBalance 删除订单并更新用户余额 16 | DeleteBillAndUpdateUserBalance(billID uint) error 17 | } 18 | 19 | type CreateBillParams struct { 20 | UserID uint 21 | Amount float64 22 | Category string 23 | CreateBillOptions 24 | } 25 | 26 | type CreateBillOptions struct { 27 | Name *string 28 | CreatedAt *time.Time 29 | } 30 | 31 | type GetUserBillsByCreateTimeOptions struct { 32 | GreaterThan time.Time // 时间范围区间左侧 33 | GreaterOrEqual bool // 是否为闭区间 34 | LessThan time.Time // 时间范围区间右侧 35 | LessOrEqual bool // 是否为闭区间 36 | } 37 | -------------------------------------------------------------------------------- /dal/telegram/mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: dal/telegram/repository.go 3 | 4 | // Package telegram is a generated GoMock package. 5 | package telegram 6 | 7 | import ( 8 | reflect "reflect" 9 | 10 | gomock "github.com/golang/mock/gomock" 11 | models "github.com/orenoid/telegram-account-bot/models" 12 | ) 13 | 14 | // MockRepository is a mock of Repository interface. 15 | type MockRepository struct { 16 | ctrl *gomock.Controller 17 | recorder *MockRepositoryMockRecorder 18 | } 19 | 20 | // MockRepositoryMockRecorder is the mock recorder for MockRepository. 21 | type MockRepositoryMockRecorder struct { 22 | mock *MockRepository 23 | } 24 | 25 | // NewMockRepository creates a new mock instance. 26 | func NewMockRepository(ctrl *gomock.Controller) *MockRepository { 27 | mock := &MockRepository{ctrl: ctrl} 28 | mock.recorder = &MockRepositoryMockRecorder{mock} 29 | return mock 30 | } 31 | 32 | // EXPECT returns an object that allows the caller to indicate expected use. 33 | func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder { 34 | return m.recorder 35 | } 36 | 37 | // CreateOrUpdateTelegramUser mocks base method. 38 | func (m *MockRepository) CreateOrUpdateTelegramUser(userID int64, userName string, chatID int64) (*models.TelegramUser, error) { 39 | m.ctrl.T.Helper() 40 | ret := m.ctrl.Call(m, "CreateOrUpdateTelegramUser", userID, userName, chatID) 41 | ret0, _ := ret[0].(*models.TelegramUser) 42 | ret1, _ := ret[1].(error) 43 | return ret0, ret1 44 | } 45 | 46 | // CreateOrUpdateTelegramUser indicates an expected call of CreateOrUpdateTelegramUser. 47 | func (mr *MockRepositoryMockRecorder) CreateOrUpdateTelegramUser(userID, userName, chatID interface{}) *gomock.Call { 48 | mr.mock.ctrl.T.Helper() 49 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrUpdateTelegramUser", reflect.TypeOf((*MockRepository)(nil).CreateOrUpdateTelegramUser), userID, userName, chatID) 50 | } 51 | 52 | // GetUser mocks base method. 53 | func (m *MockRepository) GetUser(teleUserID int64) (*models.User, error) { 54 | m.ctrl.T.Helper() 55 | ret := m.ctrl.Call(m, "GetUser", teleUserID) 56 | ret0, _ := ret[0].(*models.User) 57 | ret1, _ := ret[1].(error) 58 | return ret0, ret1 59 | } 60 | 61 | // GetUser indicates an expected call of GetUser. 62 | func (mr *MockRepositoryMockRecorder) GetUser(teleUserID interface{}) *gomock.Call { 63 | mr.mock.ctrl.T.Helper() 64 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUser", reflect.TypeOf((*MockRepository)(nil).GetUser), teleUserID) 65 | } 66 | -------------------------------------------------------------------------------- /dal/telegram/mysql.go: -------------------------------------------------------------------------------- 1 | package telegram 2 | 3 | import ( 4 | "github.com/orenoid/telegram-account-bot/models" 5 | "github.com/pkg/errors" 6 | "gorm.io/driver/mysql" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | type mysqlRepo struct { 11 | db *gorm.DB 12 | } 13 | 14 | func (repo *mysqlRepo) CreateOrUpdateTelegramUser(userID int64, userName string, chatID int64) (*models.TelegramUser, error) { 15 | telegramUser := &models.TelegramUser{} 16 | err := repo.db.Transaction(func(tx *gorm.DB) error { 17 | var count int64 18 | result := tx.Model(&models.TelegramUser{}).Where("id = ? and chat_id = ?", userID, chatID).Count(&count) 19 | if result.Error != nil { 20 | return errors.WithStack(result.Error) 21 | } 22 | if count > 0 { 23 | result := tx.Model(&models.TelegramUser{}).Where("id = ? and chat_id = ?", userID, chatID). 24 | Updates(map[string]interface{}{"user_name": userName}) 25 | if result.Error != nil { 26 | return errors.WithStack(result.Error) 27 | } 28 | if result.RowsAffected == 0 { 29 | return errors.New("failed to update user") 30 | } 31 | } else { 32 | newBaseUser := &models.User{} 33 | result := tx.Create(newBaseUser) 34 | if result.Error != nil { 35 | return errors.WithStack(result.Error) 36 | } 37 | newTelegramUser := &models.TelegramUser{BaseUserID: newBaseUser.ID, UserName: userName, ChatID: chatID} 38 | newTelegramUser.ID = uint(userID) 39 | result = tx.Create(newTelegramUser) 40 | if result.Error != nil { 41 | return errors.WithStack(result.Error) 42 | } 43 | if result.RowsAffected == 0 { 44 | return errors.New("failed to create user") 45 | } 46 | } 47 | result = tx.First(telegramUser, userID) 48 | return result.Error 49 | }) 50 | if err != nil { 51 | return nil, err 52 | } 53 | return telegramUser, nil 54 | } 55 | 56 | func (repo *mysqlRepo) GetUser(teleUserID int64) (*models.User, error) { 57 | var user models.User 58 | result := repo.db.Model(&user).Select("users.id, users.created_at, users.updated_at, users.balance"). 59 | Joins("join telegram_users tu on tu.base_user_id = users.id"). 60 | Where("tu.id = ?", teleUserID).Scan(&user) 61 | if result.Error != nil { 62 | return nil, errors.WithStack(result.Error) 63 | } 64 | return &user, nil 65 | } 66 | 67 | func NewMysqlRepo(dsn string) (*mysqlRepo, error) { 68 | db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{DisableAutomaticPing: true}) 69 | if err != nil { 70 | return nil, errors.WithStack(err) 71 | } 72 | return &mysqlRepo{db}, nil 73 | } 74 | -------------------------------------------------------------------------------- /dal/telegram/repository.go: -------------------------------------------------------------------------------- 1 | package telegram 2 | 3 | import "github.com/orenoid/telegram-account-bot/models" 4 | 5 | type Repository interface { 6 | CreateOrUpdateTelegramUser(userID int64, userName string, chatID int64) (*models.TelegramUser, error) 7 | GetUser(teleUserID int64) (*models.User, error) 8 | } 9 | -------------------------------------------------------------------------------- /dal/user/mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: dal/user/repository.go 3 | 4 | // Package user is a generated GoMock package. 5 | package user 6 | 7 | import ( 8 | reflect "reflect" 9 | 10 | gomock "github.com/golang/mock/gomock" 11 | models "github.com/orenoid/telegram-account-bot/models" 12 | ) 13 | 14 | // MockRepository is a mock of Repository interface. 15 | type MockRepository struct { 16 | ctrl *gomock.Controller 17 | recorder *MockRepositoryMockRecorder 18 | } 19 | 20 | // MockRepositoryMockRecorder is the mock recorder for MockRepository. 21 | type MockRepositoryMockRecorder struct { 22 | mock *MockRepository 23 | } 24 | 25 | // NewMockRepository creates a new mock instance. 26 | func NewMockRepository(ctrl *gomock.Controller) *MockRepository { 27 | mock := &MockRepository{ctrl: ctrl} 28 | mock.recorder = &MockRepositoryMockRecorder{mock} 29 | return mock 30 | } 31 | 32 | // EXPECT returns an object that allows the caller to indicate expected use. 33 | func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder { 34 | return m.recorder 35 | } 36 | 37 | // CheckUserExists mocks base method. 38 | func (m *MockRepository) CheckUserExists(userID uint) (bool, error) { 39 | m.ctrl.T.Helper() 40 | ret := m.ctrl.Call(m, "CheckUserExists", userID) 41 | ret0, _ := ret[0].(bool) 42 | ret1, _ := ret[1].(error) 43 | return ret0, ret1 44 | } 45 | 46 | // CheckUserExists indicates an expected call of CheckUserExists. 47 | func (mr *MockRepositoryMockRecorder) CheckUserExists(userID interface{}) *gomock.Call { 48 | mr.mock.ctrl.T.Helper() 49 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckUserExists", reflect.TypeOf((*MockRepository)(nil).CheckUserExists), userID) 50 | } 51 | 52 | // CreateToken mocks base method. 53 | func (m *MockRepository) CreateToken(userID uint, token string) error { 54 | m.ctrl.T.Helper() 55 | ret := m.ctrl.Call(m, "CreateToken", userID, token) 56 | ret0, _ := ret[0].(error) 57 | return ret0 58 | } 59 | 60 | // CreateToken indicates an expected call of CreateToken. 61 | func (mr *MockRepositoryMockRecorder) CreateToken(userID, token interface{}) *gomock.Call { 62 | mr.mock.ctrl.T.Helper() 63 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateToken", reflect.TypeOf((*MockRepository)(nil).CreateToken), userID, token) 64 | } 65 | 66 | // CreateUser mocks base method. 67 | func (m *MockRepository) CreateUser() (*models.User, error) { 68 | m.ctrl.T.Helper() 69 | ret := m.ctrl.Call(m, "CreateUser") 70 | ret0, _ := ret[0].(*models.User) 71 | ret1, _ := ret[1].(error) 72 | return ret0, ret1 73 | } 74 | 75 | // CreateUser indicates an expected call of CreateUser. 76 | func (mr *MockRepositoryMockRecorder) CreateUser() *gomock.Call { 77 | mr.mock.ctrl.T.Helper() 78 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUser", reflect.TypeOf((*MockRepository)(nil).CreateUser)) 79 | } 80 | 81 | // DisableAllTokens mocks base method. 82 | func (m *MockRepository) DisableAllTokens(userID uint) error { 83 | m.ctrl.T.Helper() 84 | ret := m.ctrl.Call(m, "DisableAllTokens", userID) 85 | ret0, _ := ret[0].(error) 86 | return ret0 87 | } 88 | 89 | // DisableAllTokens indicates an expected call of DisableAllTokens. 90 | func (mr *MockRepositoryMockRecorder) DisableAllTokens(userID interface{}) *gomock.Call { 91 | mr.mock.ctrl.T.Helper() 92 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DisableAllTokens", reflect.TypeOf((*MockRepository)(nil).DisableAllTokens), userID) 93 | } 94 | 95 | // DisableToken mocks base method. 96 | func (m *MockRepository) DisableToken(userID uint, token string) error { 97 | m.ctrl.T.Helper() 98 | ret := m.ctrl.Call(m, "DisableToken", userID, token) 99 | ret0, _ := ret[0].(error) 100 | return ret0 101 | } 102 | 103 | // DisableToken indicates an expected call of DisableToken. 104 | func (mr *MockRepositoryMockRecorder) DisableToken(userID, token interface{}) *gomock.Call { 105 | mr.mock.ctrl.T.Helper() 106 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DisableToken", reflect.TypeOf((*MockRepository)(nil).DisableToken), userID, token) 107 | } 108 | 109 | // GetUserBalance mocks base method. 110 | func (m *MockRepository) GetUserBalance(userID uint) (float64, error) { 111 | m.ctrl.T.Helper() 112 | ret := m.ctrl.Call(m, "GetUserBalance", userID) 113 | ret0, _ := ret[0].(float64) 114 | ret1, _ := ret[1].(error) 115 | return ret0, ret1 116 | } 117 | 118 | // GetUserBalance indicates an expected call of GetUserBalance. 119 | func (mr *MockRepositoryMockRecorder) GetUserBalance(userID interface{}) *gomock.Call { 120 | mr.mock.ctrl.T.Helper() 121 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserBalance", reflect.TypeOf((*MockRepository)(nil).GetUserBalance), userID) 122 | } 123 | 124 | // MustGetToken mocks base method. 125 | func (m *MockRepository) MustGetToken(token string) (*models.Token, error) { 126 | m.ctrl.T.Helper() 127 | ret := m.ctrl.Call(m, "MustGetToken", token) 128 | ret0, _ := ret[0].(*models.Token) 129 | ret1, _ := ret[1].(error) 130 | return ret0, ret1 131 | } 132 | 133 | // MustGetToken indicates an expected call of MustGetToken. 134 | func (mr *MockRepositoryMockRecorder) MustGetToken(token interface{}) *gomock.Call { 135 | mr.mock.ctrl.T.Helper() 136 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MustGetToken", reflect.TypeOf((*MockRepository)(nil).MustGetToken), token) 137 | } 138 | 139 | // SetUserBalance mocks base method. 140 | func (m *MockRepository) SetUserBalance(userID uint, balance float64) (float64, error) { 141 | m.ctrl.T.Helper() 142 | ret := m.ctrl.Call(m, "SetUserBalance", userID, balance) 143 | ret0, _ := ret[0].(float64) 144 | ret1, _ := ret[1].(error) 145 | return ret0, ret1 146 | } 147 | 148 | // SetUserBalance indicates an expected call of SetUserBalance. 149 | func (mr *MockRepositoryMockRecorder) SetUserBalance(userID, balance interface{}) *gomock.Call { 150 | mr.mock.ctrl.T.Helper() 151 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetUserBalance", reflect.TypeOf((*MockRepository)(nil).SetUserBalance), userID, balance) 152 | } 153 | -------------------------------------------------------------------------------- /dal/user/mysql.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "github.com/orenoid/telegram-account-bot/models" 5 | "github.com/pkg/errors" 6 | "github.com/shopspring/decimal" 7 | "gorm.io/driver/mysql" 8 | "gorm.io/gorm" 9 | ) 10 | 11 | type mysqlRepo struct { 12 | db *gorm.DB 13 | } 14 | 15 | func (receiver *mysqlRepo) MustGetToken(token string) (*models.Token, error) { 16 | var foundToken models.Token 17 | 18 | // 查找指定 token 的记录 19 | err := receiver.db.Where("token = ?", token).First(&foundToken).Error 20 | if err != nil { 21 | // 如果未找到记录,或者发生其他错误,返回 nil 和错误信息 22 | return nil, errors.WithStack(err) 23 | } 24 | 25 | // 返回找到的 token 记录 26 | return &foundToken, nil 27 | } 28 | 29 | func (receiver *mysqlRepo) CreateToken(userID uint, token string) error { 30 | newTokenRecord := &models.Token{ 31 | UserID: userID, 32 | Token: token, 33 | } 34 | result := receiver.db.Create(newTokenRecord) 35 | if result.Error != nil { 36 | return errors.WithStack(result.Error) 37 | } 38 | return nil 39 | } 40 | 41 | func (receiver *mysqlRepo) ValidateToken(token string) (bool, error) { 42 | var existingToken models.Token 43 | 44 | // 查找用户的 token 45 | err := receiver.db.Where("token = ?", token).First(&existingToken).Error 46 | if err != nil { 47 | // 如果没有找到 token,返回 false 和 nil 错误 48 | if err == gorm.ErrRecordNotFound { 49 | return false, nil 50 | } 51 | // 其他数据库错误 52 | return false, errors.WithStack(err) 53 | } 54 | 55 | // 如果找到了 token,返回 true 56 | return true, nil 57 | } 58 | 59 | func (receiver *mysqlRepo) DisableToken(userID uint, token string) error { 60 | if err := receiver.db.Where( 61 | "user_id = ? AND token = ?", userID, token).Delete(&models.Token{}).Error; err != nil { 62 | return errors.WithStack(err) 63 | } 64 | return nil 65 | } 66 | 67 | func (receiver *mysqlRepo) DisableAllTokens(userID uint) error { 68 | if err := receiver.db.Where("user_id = ?", userID).Delete(&models.Token{}).Error; err != nil { 69 | return errors.WithStack(err) 70 | } 71 | return nil 72 | } 73 | 74 | func (receiver *mysqlRepo) GetUserBalance(userID uint) (float64, error) { 75 | userModel := &models.User{} 76 | result := receiver.db.First(userModel, userID) 77 | if result.Error != nil { 78 | return 0, errors.WithStack(result.Error) 79 | } 80 | return userModel.Balance.Decimal.InexactFloat64(), nil 81 | } 82 | 83 | func (receiver *mysqlRepo) CreateUser() (*models.User, error) { 84 | userModel := &models.User{} 85 | result := receiver.db.Create(userModel) 86 | if result.Error != nil { 87 | return nil, errors.WithStack(result.Error) 88 | } 89 | return userModel, nil 90 | } 91 | 92 | // CheckUserExists check if user exists 93 | func (receiver *mysqlRepo) CheckUserExists(userID uint) (bool, error) { 94 | var count int64 95 | result := receiver.db.Model(&models.User{}).Where("id = ?", userID).Count(&count) 96 | if result.Error != nil { 97 | return false, errors.WithStack(result.Error) 98 | } 99 | return count > 0, nil 100 | } 101 | 102 | func (receiver *mysqlRepo) SetUserBalance(userID uint, balanceFloat float64) (float64, error) { 103 | balanceDecimal := decimal.NewNullDecimal(decimal.NewFromFloat(balanceFloat)) 104 | 105 | result := receiver.db.Model(&models.User{}). 106 | Where("id = ?", userID).Update("balance", balanceDecimal) 107 | if result.Error != nil { 108 | return 0, errors.WithStack(result.Error) 109 | } 110 | if result.RowsAffected == 0 { 111 | return 0, errors.New("failed to set user balance") 112 | } 113 | 114 | userModel := &models.User{} 115 | result = receiver.db.First(userModel, userID) 116 | if result.Error != nil { 117 | return 0, errors.WithStack(result.Error) 118 | } 119 | return userModel.Balance.Decimal.InexactFloat64(), nil 120 | } 121 | 122 | func NewMysqlRepo(dsn string) (*mysqlRepo, error) { 123 | db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{DisableAutomaticPing: true}) 124 | if err != nil { 125 | return nil, errors.WithStack(err) 126 | } 127 | return &mysqlRepo{db}, nil 128 | } 129 | -------------------------------------------------------------------------------- /dal/user/repository.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import "github.com/orenoid/telegram-account-bot/models" 4 | 5 | type Repository interface { 6 | // user 7 | 8 | CreateUser() (*models.User, error) 9 | CheckUserExists(userID uint) (bool, error) 10 | SetUserBalance(userID uint, balance float64) (float64, error) 11 | GetUserBalance(userID uint) (float64, error) 12 | 13 | // auth 14 | 15 | CreateToken(userID uint, token string) error 16 | MustGetToken(token string) (*models.Token, error) 17 | DisableToken(userID uint, token string) error 18 | DisableAllTokens(userID uint) error 19 | } 20 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | db: 5 | image: mysql:latest 6 | restart: unless-stopped 7 | env_file: "env/db.env" 8 | ports: 9 | - "3306:3306" 10 | volumes: 11 | - db_data:/var/lib/mysql 12 | 13 | bot: 14 | image: orenoid/telegram-account-bot 15 | build: 16 | context: . 17 | dockerfile: bot.Dockerfile 18 | restart: unless-stopped 19 | env_file: "env/bot.env" 20 | ports: 21 | - "8080:8080" 22 | depends_on: 23 | - db 24 | 25 | api: 26 | image: orenoid/telegram-account-bot-api 27 | build: 28 | context: . 29 | dockerfile: api.Dockerfile 30 | restart: unless-stopped 31 | env_file: "env/api.env" 32 | ports: 33 | - "1323:1323" 34 | depends_on: 35 | - db 36 | - bot 37 | 38 | 39 | volumes: 40 | db_data: -------------------------------------------------------------------------------- /env/api.env.example: -------------------------------------------------------------------------------- 1 | MYSQL_DSN=root:password@tcp(db:3306)/account?charset=utf8mb4&parseTime=True&loc=Asia%2fShanghai 2 | TZ=Asia/Shanghai -------------------------------------------------------------------------------- /env/bot.env.example: -------------------------------------------------------------------------------- 1 | MYSQL_DSN=root:password@tcp(db:3306)/account?charset=utf8mb4&parseTime=True&loc=Asia%2fShanghai 2 | TELEBOT_TOKEN=YOUR_TELEGRAM_BOT_TOKEN_HERE 3 | TZ=Asia/Shanghai -------------------------------------------------------------------------------- /env/db.env.example: -------------------------------------------------------------------------------- 1 | MYSQL_DATABASE: account 2 | MYSQL_ROOT_PASSWORD: password -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/orenoid/telegram-account-bot 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/golang/mock v1.6.0 7 | gorm.io/driver/mysql v1.3.4 8 | gorm.io/gorm v1.23.6 9 | ) 10 | 11 | require ( 12 | github.com/agiledragon/gomonkey/v2 v2.2.0 13 | github.com/jinzhu/now v1.1.5 // indirect 14 | github.com/pkg/errors v0.9.1 15 | github.com/shopspring/decimal v1.3.1 16 | github.com/sirupsen/logrus v1.8.1 17 | github.com/spf13/cobra v1.5.0 18 | github.com/spf13/viper v1.12.0 19 | github.com/stretchr/testify v1.8.4 20 | gopkg.in/telebot.v3 v3.0.0 21 | ) 22 | 23 | require ( 24 | github.com/davecgh/go-spew v1.1.1 // indirect 25 | github.com/fsnotify/fsnotify v1.5.4 // indirect 26 | github.com/go-sql-driver/mysql v1.6.0 // indirect 27 | github.com/hashicorp/hcl v1.0.0 // indirect 28 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 29 | github.com/jinzhu/inflection v1.0.0 // indirect 30 | github.com/labstack/echo/v4 v4.12.0 // indirect 31 | github.com/labstack/gommon v0.4.2 // indirect 32 | github.com/magiconair/properties v1.8.6 // indirect 33 | github.com/mattn/go-colorable v0.1.13 // indirect 34 | github.com/mattn/go-isatty v0.0.20 // indirect 35 | github.com/mitchellh/mapstructure v1.5.0 // indirect 36 | github.com/pelletier/go-toml v1.9.5 // indirect 37 | github.com/pelletier/go-toml/v2 v2.0.1 // indirect 38 | github.com/pmezard/go-difflib v1.0.0 // indirect 39 | github.com/spf13/afero v1.8.2 // indirect 40 | github.com/spf13/cast v1.5.0 // indirect 41 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 42 | github.com/spf13/pflag v1.0.5 // indirect 43 | github.com/subosito/gotenv v1.3.0 // indirect 44 | github.com/valyala/bytebufferpool v1.0.0 // indirect 45 | github.com/valyala/fasttemplate v1.2.2 // indirect 46 | golang.org/x/crypto v0.22.0 // indirect 47 | golang.org/x/net v0.24.0 // indirect 48 | golang.org/x/sys v0.19.0 // indirect 49 | golang.org/x/text v0.14.0 // indirect 50 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect 51 | gopkg.in/ini.v1 v1.66.4 // indirect 52 | gopkg.in/yaml.v2 v2.4.0 // indirect 53 | gopkg.in/yaml.v3 v3.0.1 // indirect 54 | ) 55 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 7 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 8 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 9 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= 10 | cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= 11 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= 12 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= 13 | cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= 14 | cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= 15 | cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= 16 | cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= 17 | cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= 18 | cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= 19 | cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= 20 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 21 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= 22 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= 23 | cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= 24 | cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= 25 | cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= 26 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 27 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= 28 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 29 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= 30 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= 31 | cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= 32 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 33 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= 34 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= 35 | cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= 36 | cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= 37 | cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= 38 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 39 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 40 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 41 | github.com/agiledragon/gomonkey/v2 v2.2.0 h1:QJWqpdEhGV/JJy70sZ/LDnhbSlMrqHAWHcNOjz1kyuI= 42 | github.com/agiledragon/gomonkey/v2 v2.2.0/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY= 43 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 44 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 45 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 46 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 47 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 48 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 49 | github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 50 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 51 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 52 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 53 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 54 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 55 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 56 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 57 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 58 | github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= 59 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 60 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 61 | github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= 62 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 63 | github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= 64 | github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= 65 | github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= 66 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 67 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 68 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 69 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 70 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 71 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 72 | github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= 73 | github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= 74 | github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 75 | github.com/goccy/go-yaml v1.9.5/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA= 76 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 77 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 78 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 79 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 80 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 81 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 82 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 83 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 84 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 85 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 86 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 87 | github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= 88 | github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= 89 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 90 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 91 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 92 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 93 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 94 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 95 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 96 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 97 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 98 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 99 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 100 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 101 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 102 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 103 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 104 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 105 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 106 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 107 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 108 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 109 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 110 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 111 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 112 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 113 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 114 | github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= 115 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 116 | github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 117 | github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 118 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 119 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 120 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 121 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 122 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 123 | github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 124 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 125 | github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 126 | github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 127 | github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 128 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 129 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 130 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 131 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 132 | github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= 133 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 134 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 135 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 136 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 137 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 138 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 139 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 140 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 141 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 142 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 143 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 144 | github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 145 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 146 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 147 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 148 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 149 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 150 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 151 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 152 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 153 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 154 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 155 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 156 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 157 | github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0= 158 | github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM= 159 | github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= 160 | github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= 161 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 162 | github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= 163 | github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= 164 | github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 165 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 166 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 167 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 168 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 169 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 170 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 171 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 172 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 173 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 174 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 175 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 176 | github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= 177 | github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= 178 | github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU= 179 | github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= 180 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 181 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 182 | github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= 183 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 184 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 185 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 186 | github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= 187 | github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= 188 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 189 | github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= 190 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 191 | github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= 192 | github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 193 | github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= 194 | github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 195 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 196 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 197 | github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= 198 | github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= 199 | github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 200 | github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= 201 | github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= 202 | github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= 203 | github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= 204 | github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= 205 | github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= 206 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 207 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 208 | github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ= 209 | github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= 210 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 211 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 212 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 213 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 214 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 215 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 216 | github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= 217 | github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= 218 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 219 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 220 | github.com/subosito/gotenv v1.3.0 h1:mjC+YW8QpAdXibNi+vNWgzmgBH4+5l5dCXv8cNysBLI= 221 | github.com/subosito/gotenv v1.3.0/go.mod h1:YzJjq/33h7nrwdY+iHMhEOEEbW0ovIz0tB6t6PwAXzs= 222 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 223 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 224 | github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= 225 | github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 226 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 227 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 228 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 229 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 230 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 231 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 232 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 233 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 234 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 235 | go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 236 | go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= 237 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 238 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 239 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 240 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 241 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 242 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 243 | golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 244 | golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= 245 | golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= 246 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 247 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 248 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 249 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 250 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 251 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 252 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 253 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 254 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 255 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 256 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 257 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 258 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 259 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 260 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 261 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 262 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 263 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 264 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 265 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 266 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 267 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 268 | golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 269 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 270 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 271 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 272 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 273 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 274 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 275 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 276 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 277 | golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 278 | golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 279 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 280 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 281 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 282 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 283 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 284 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 285 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 286 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 287 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 288 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 289 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 290 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 291 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 292 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 293 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 294 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 295 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 296 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 297 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 298 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 299 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 300 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 301 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 302 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 303 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 304 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 305 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 306 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 307 | golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 308 | golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 309 | golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 310 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 311 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 312 | golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= 313 | golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= 314 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 315 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 316 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 317 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 318 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 319 | golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 320 | golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 321 | golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 322 | golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 323 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 324 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 325 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 326 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 327 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 328 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 329 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 330 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 331 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 332 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 333 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 334 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 335 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 336 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 337 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 338 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 339 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 340 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 341 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 342 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 343 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 344 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 345 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 346 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 347 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 348 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 349 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 350 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 351 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 352 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 353 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 354 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 355 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 356 | golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 357 | golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 358 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 359 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 360 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 361 | golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 362 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 363 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 364 | golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 365 | golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 366 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 367 | golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 368 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 369 | golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 370 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 371 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 372 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 373 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 374 | golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 375 | golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 376 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 377 | golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= 378 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 379 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 380 | golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= 381 | golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 382 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 383 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 384 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 385 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 386 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 387 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 388 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 389 | golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= 390 | golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 391 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 392 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 393 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 394 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 395 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 396 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 397 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 398 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 399 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 400 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 401 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 402 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 403 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 404 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 405 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 406 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 407 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 408 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 409 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 410 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 411 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 412 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 413 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 414 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 415 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 416 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 417 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 418 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 419 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 420 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 421 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 422 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 423 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 424 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 425 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 426 | golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 427 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 428 | golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 429 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 430 | golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 431 | golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 432 | golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 433 | golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 434 | golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 435 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 436 | golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 437 | golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= 438 | golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 439 | golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 440 | golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 441 | golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 442 | golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 443 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= 444 | golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 445 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 446 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 447 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 448 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 449 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 450 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 451 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 452 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 453 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 454 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 455 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 456 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 457 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 458 | google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 459 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 460 | google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 461 | google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 462 | google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 463 | google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= 464 | google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= 465 | google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= 466 | google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= 467 | google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= 468 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 469 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 470 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 471 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 472 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 473 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 474 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 475 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 476 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 477 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 478 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 479 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 480 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 481 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 482 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 483 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 484 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 485 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 486 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 487 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 488 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 489 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= 490 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 491 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 492 | google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 493 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 494 | google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 495 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 496 | google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 497 | google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 498 | google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= 499 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 500 | google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= 501 | google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 502 | google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 503 | google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 504 | google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 505 | google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 506 | google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 507 | google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 508 | google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 509 | google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 510 | google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 511 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 512 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 513 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 514 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 515 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 516 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 517 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 518 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 519 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= 520 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 521 | google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 522 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 523 | google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 524 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 525 | google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= 526 | google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 527 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 528 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 529 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 530 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 531 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 532 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 533 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 534 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 535 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 536 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 537 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 538 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 539 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 540 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 541 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 542 | gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4= 543 | gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 544 | gopkg.in/telebot.v3 v3.0.0 h1:UgHIiE/RdjoDi6nf4xACM7PU3TqiPVV9vvTydCEnrTo= 545 | gopkg.in/telebot.v3 v3.0.0/go.mod h1:7rExV8/0mDDNu9epSrDm/8j22KLaActH1Tbee6YjzWg= 546 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 547 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 548 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 549 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 550 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 551 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 552 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 553 | gorm.io/driver/mysql v1.3.4 h1:/KoBMgsUHC3bExsekDcmNYaBnfH2WNeFuXqqrqMc98Q= 554 | gorm.io/driver/mysql v1.3.4/go.mod h1:s4Tq0KmD0yhPGHbZEwg1VPlH0vT/GBHJZorPzhcxBUE= 555 | gorm.io/gorm v1.23.4/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= 556 | gorm.io/gorm v1.23.6 h1:KFLdNgri4ExFFGTRGGFWON2P1ZN28+9SJRN8voOoYe0= 557 | gorm.io/gorm v1.23.6/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= 558 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 559 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 560 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 561 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 562 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 563 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 564 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 565 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 566 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 567 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 568 | -------------------------------------------------------------------------------- /mock/telebotmock/mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: gopkg.in/telebot.v3 (interfaces: Context) 3 | 4 | // Package telebotmock is a generated GoMock package. 5 | package telebotmock 6 | 7 | import ( 8 | reflect "reflect" 9 | time "time" 10 | 11 | gomock "github.com/golang/mock/gomock" 12 | telebot "gopkg.in/telebot.v3" 13 | ) 14 | 15 | // MockContext is a mock of Context interface. 16 | type MockContext struct { 17 | ctrl *gomock.Controller 18 | recorder *MockContextMockRecorder 19 | } 20 | 21 | // MockContextMockRecorder is the mock recorder for MockContext. 22 | type MockContextMockRecorder struct { 23 | mock *MockContext 24 | } 25 | 26 | // NewMockContext creates a new mock instance. 27 | func NewMockContext(ctrl *gomock.Controller) *MockContext { 28 | mock := &MockContext{ctrl: ctrl} 29 | mock.recorder = &MockContextMockRecorder{mock} 30 | return mock 31 | } 32 | 33 | // EXPECT returns an object that allows the caller to indicate expected use. 34 | func (m *MockContext) EXPECT() *MockContextMockRecorder { 35 | return m.recorder 36 | } 37 | 38 | // Accept mocks base method. 39 | func (m *MockContext) Accept(arg0 ...string) error { 40 | m.ctrl.T.Helper() 41 | varargs := []interface{}{} 42 | for _, a := range arg0 { 43 | varargs = append(varargs, a) 44 | } 45 | ret := m.ctrl.Call(m, "Accept", varargs...) 46 | ret0, _ := ret[0].(error) 47 | return ret0 48 | } 49 | 50 | // Accept indicates an expected call of Accept. 51 | func (mr *MockContextMockRecorder) Accept(arg0 ...interface{}) *gomock.Call { 52 | mr.mock.ctrl.T.Helper() 53 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Accept", reflect.TypeOf((*MockContext)(nil).Accept), arg0...) 54 | } 55 | 56 | // Answer mocks base method. 57 | func (m *MockContext) Answer(arg0 *telebot.QueryResponse) error { 58 | m.ctrl.T.Helper() 59 | ret := m.ctrl.Call(m, "Answer", arg0) 60 | ret0, _ := ret[0].(error) 61 | return ret0 62 | } 63 | 64 | // Answer indicates an expected call of Answer. 65 | func (mr *MockContextMockRecorder) Answer(arg0 interface{}) *gomock.Call { 66 | mr.mock.ctrl.T.Helper() 67 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Answer", reflect.TypeOf((*MockContext)(nil).Answer), arg0) 68 | } 69 | 70 | // Args mocks base method. 71 | func (m *MockContext) Args() []string { 72 | m.ctrl.T.Helper() 73 | ret := m.ctrl.Call(m, "Args") 74 | ret0, _ := ret[0].([]string) 75 | return ret0 76 | } 77 | 78 | // Args indicates an expected call of Args. 79 | func (mr *MockContextMockRecorder) Args() *gomock.Call { 80 | mr.mock.ctrl.T.Helper() 81 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Args", reflect.TypeOf((*MockContext)(nil).Args)) 82 | } 83 | 84 | // Bot mocks base method. 85 | func (m *MockContext) Bot() *telebot.Bot { 86 | m.ctrl.T.Helper() 87 | ret := m.ctrl.Call(m, "Bot") 88 | ret0, _ := ret[0].(*telebot.Bot) 89 | return ret0 90 | } 91 | 92 | // Bot indicates an expected call of Bot. 93 | func (mr *MockContextMockRecorder) Bot() *gomock.Call { 94 | mr.mock.ctrl.T.Helper() 95 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Bot", reflect.TypeOf((*MockContext)(nil).Bot)) 96 | } 97 | 98 | // Callback mocks base method. 99 | func (m *MockContext) Callback() *telebot.Callback { 100 | m.ctrl.T.Helper() 101 | ret := m.ctrl.Call(m, "Callback") 102 | ret0, _ := ret[0].(*telebot.Callback) 103 | return ret0 104 | } 105 | 106 | // Callback indicates an expected call of Callback. 107 | func (mr *MockContextMockRecorder) Callback() *gomock.Call { 108 | mr.mock.ctrl.T.Helper() 109 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Callback", reflect.TypeOf((*MockContext)(nil).Callback)) 110 | } 111 | 112 | // Chat mocks base method. 113 | func (m *MockContext) Chat() *telebot.Chat { 114 | m.ctrl.T.Helper() 115 | ret := m.ctrl.Call(m, "Chat") 116 | ret0, _ := ret[0].(*telebot.Chat) 117 | return ret0 118 | } 119 | 120 | // Chat indicates an expected call of Chat. 121 | func (mr *MockContextMockRecorder) Chat() *gomock.Call { 122 | mr.mock.ctrl.T.Helper() 123 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Chat", reflect.TypeOf((*MockContext)(nil).Chat)) 124 | } 125 | 126 | // ChatJoinRequest mocks base method. 127 | func (m *MockContext) ChatJoinRequest() *telebot.ChatJoinRequest { 128 | m.ctrl.T.Helper() 129 | ret := m.ctrl.Call(m, "ChatJoinRequest") 130 | ret0, _ := ret[0].(*telebot.ChatJoinRequest) 131 | return ret0 132 | } 133 | 134 | // ChatJoinRequest indicates an expected call of ChatJoinRequest. 135 | func (mr *MockContextMockRecorder) ChatJoinRequest() *gomock.Call { 136 | mr.mock.ctrl.T.Helper() 137 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ChatJoinRequest", reflect.TypeOf((*MockContext)(nil).ChatJoinRequest)) 138 | } 139 | 140 | // ChatMember mocks base method. 141 | func (m *MockContext) ChatMember() *telebot.ChatMemberUpdate { 142 | m.ctrl.T.Helper() 143 | ret := m.ctrl.Call(m, "ChatMember") 144 | ret0, _ := ret[0].(*telebot.ChatMemberUpdate) 145 | return ret0 146 | } 147 | 148 | // ChatMember indicates an expected call of ChatMember. 149 | func (mr *MockContextMockRecorder) ChatMember() *gomock.Call { 150 | mr.mock.ctrl.T.Helper() 151 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ChatMember", reflect.TypeOf((*MockContext)(nil).ChatMember)) 152 | } 153 | 154 | // Data mocks base method. 155 | func (m *MockContext) Data() string { 156 | m.ctrl.T.Helper() 157 | ret := m.ctrl.Call(m, "Data") 158 | ret0, _ := ret[0].(string) 159 | return ret0 160 | } 161 | 162 | // Data indicates an expected call of Data. 163 | func (mr *MockContextMockRecorder) Data() *gomock.Call { 164 | mr.mock.ctrl.T.Helper() 165 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Data", reflect.TypeOf((*MockContext)(nil).Data)) 166 | } 167 | 168 | // Delete mocks base method. 169 | func (m *MockContext) Delete() error { 170 | m.ctrl.T.Helper() 171 | ret := m.ctrl.Call(m, "Delete") 172 | ret0, _ := ret[0].(error) 173 | return ret0 174 | } 175 | 176 | // Delete indicates an expected call of Delete. 177 | func (mr *MockContextMockRecorder) Delete() *gomock.Call { 178 | mr.mock.ctrl.T.Helper() 179 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockContext)(nil).Delete)) 180 | } 181 | 182 | // DeleteAfter mocks base method. 183 | func (m *MockContext) DeleteAfter(arg0 time.Duration) *time.Timer { 184 | m.ctrl.T.Helper() 185 | ret := m.ctrl.Call(m, "DeleteAfter", arg0) 186 | ret0, _ := ret[0].(*time.Timer) 187 | return ret0 188 | } 189 | 190 | // DeleteAfter indicates an expected call of DeleteAfter. 191 | func (mr *MockContextMockRecorder) DeleteAfter(arg0 interface{}) *gomock.Call { 192 | mr.mock.ctrl.T.Helper() 193 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAfter", reflect.TypeOf((*MockContext)(nil).DeleteAfter), arg0) 194 | } 195 | 196 | // Edit mocks base method. 197 | func (m *MockContext) Edit(arg0 interface{}, arg1 ...interface{}) error { 198 | m.ctrl.T.Helper() 199 | varargs := []interface{}{arg0} 200 | for _, a := range arg1 { 201 | varargs = append(varargs, a) 202 | } 203 | ret := m.ctrl.Call(m, "Edit", varargs...) 204 | ret0, _ := ret[0].(error) 205 | return ret0 206 | } 207 | 208 | // Edit indicates an expected call of Edit. 209 | func (mr *MockContextMockRecorder) Edit(arg0 interface{}, arg1 ...interface{}) *gomock.Call { 210 | mr.mock.ctrl.T.Helper() 211 | varargs := append([]interface{}{arg0}, arg1...) 212 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Edit", reflect.TypeOf((*MockContext)(nil).Edit), varargs...) 213 | } 214 | 215 | // EditCaption mocks base method. 216 | func (m *MockContext) EditCaption(arg0 string, arg1 ...interface{}) error { 217 | m.ctrl.T.Helper() 218 | varargs := []interface{}{arg0} 219 | for _, a := range arg1 { 220 | varargs = append(varargs, a) 221 | } 222 | ret := m.ctrl.Call(m, "EditCaption", varargs...) 223 | ret0, _ := ret[0].(error) 224 | return ret0 225 | } 226 | 227 | // EditCaption indicates an expected call of EditCaption. 228 | func (mr *MockContextMockRecorder) EditCaption(arg0 interface{}, arg1 ...interface{}) *gomock.Call { 229 | mr.mock.ctrl.T.Helper() 230 | varargs := append([]interface{}{arg0}, arg1...) 231 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EditCaption", reflect.TypeOf((*MockContext)(nil).EditCaption), varargs...) 232 | } 233 | 234 | // EditOrReply mocks base method. 235 | func (m *MockContext) EditOrReply(arg0 interface{}, arg1 ...interface{}) error { 236 | m.ctrl.T.Helper() 237 | varargs := []interface{}{arg0} 238 | for _, a := range arg1 { 239 | varargs = append(varargs, a) 240 | } 241 | ret := m.ctrl.Call(m, "EditOrReply", varargs...) 242 | ret0, _ := ret[0].(error) 243 | return ret0 244 | } 245 | 246 | // EditOrReply indicates an expected call of EditOrReply. 247 | func (mr *MockContextMockRecorder) EditOrReply(arg0 interface{}, arg1 ...interface{}) *gomock.Call { 248 | mr.mock.ctrl.T.Helper() 249 | varargs := append([]interface{}{arg0}, arg1...) 250 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EditOrReply", reflect.TypeOf((*MockContext)(nil).EditOrReply), varargs...) 251 | } 252 | 253 | // EditOrSend mocks base method. 254 | func (m *MockContext) EditOrSend(arg0 interface{}, arg1 ...interface{}) error { 255 | m.ctrl.T.Helper() 256 | varargs := []interface{}{arg0} 257 | for _, a := range arg1 { 258 | varargs = append(varargs, a) 259 | } 260 | ret := m.ctrl.Call(m, "EditOrSend", varargs...) 261 | ret0, _ := ret[0].(error) 262 | return ret0 263 | } 264 | 265 | // EditOrSend indicates an expected call of EditOrSend. 266 | func (mr *MockContextMockRecorder) EditOrSend(arg0 interface{}, arg1 ...interface{}) *gomock.Call { 267 | mr.mock.ctrl.T.Helper() 268 | varargs := append([]interface{}{arg0}, arg1...) 269 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EditOrSend", reflect.TypeOf((*MockContext)(nil).EditOrSend), varargs...) 270 | } 271 | 272 | // Forward mocks base method. 273 | func (m *MockContext) Forward(arg0 telebot.Editable, arg1 ...interface{}) error { 274 | m.ctrl.T.Helper() 275 | varargs := []interface{}{arg0} 276 | for _, a := range arg1 { 277 | varargs = append(varargs, a) 278 | } 279 | ret := m.ctrl.Call(m, "Forward", varargs...) 280 | ret0, _ := ret[0].(error) 281 | return ret0 282 | } 283 | 284 | // Forward indicates an expected call of Forward. 285 | func (mr *MockContextMockRecorder) Forward(arg0 interface{}, arg1 ...interface{}) *gomock.Call { 286 | mr.mock.ctrl.T.Helper() 287 | varargs := append([]interface{}{arg0}, arg1...) 288 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Forward", reflect.TypeOf((*MockContext)(nil).Forward), varargs...) 289 | } 290 | 291 | // ForwardTo mocks base method. 292 | func (m *MockContext) ForwardTo(arg0 telebot.Recipient, arg1 ...interface{}) error { 293 | m.ctrl.T.Helper() 294 | varargs := []interface{}{arg0} 295 | for _, a := range arg1 { 296 | varargs = append(varargs, a) 297 | } 298 | ret := m.ctrl.Call(m, "ForwardTo", varargs...) 299 | ret0, _ := ret[0].(error) 300 | return ret0 301 | } 302 | 303 | // ForwardTo indicates an expected call of ForwardTo. 304 | func (mr *MockContextMockRecorder) ForwardTo(arg0 interface{}, arg1 ...interface{}) *gomock.Call { 305 | mr.mock.ctrl.T.Helper() 306 | varargs := append([]interface{}{arg0}, arg1...) 307 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ForwardTo", reflect.TypeOf((*MockContext)(nil).ForwardTo), varargs...) 308 | } 309 | 310 | // Get mocks base method. 311 | func (m *MockContext) Get(arg0 string) interface{} { 312 | m.ctrl.T.Helper() 313 | ret := m.ctrl.Call(m, "Get", arg0) 314 | ret0, _ := ret[0].(interface{}) 315 | return ret0 316 | } 317 | 318 | // Get indicates an expected call of Get. 319 | func (mr *MockContextMockRecorder) Get(arg0 interface{}) *gomock.Call { 320 | mr.mock.ctrl.T.Helper() 321 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockContext)(nil).Get), arg0) 322 | } 323 | 324 | // InlineResult mocks base method. 325 | func (m *MockContext) InlineResult() *telebot.InlineResult { 326 | m.ctrl.T.Helper() 327 | ret := m.ctrl.Call(m, "InlineResult") 328 | ret0, _ := ret[0].(*telebot.InlineResult) 329 | return ret0 330 | } 331 | 332 | // InlineResult indicates an expected call of InlineResult. 333 | func (mr *MockContextMockRecorder) InlineResult() *gomock.Call { 334 | mr.mock.ctrl.T.Helper() 335 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InlineResult", reflect.TypeOf((*MockContext)(nil).InlineResult)) 336 | } 337 | 338 | // Message mocks base method. 339 | func (m *MockContext) Message() *telebot.Message { 340 | m.ctrl.T.Helper() 341 | ret := m.ctrl.Call(m, "Message") 342 | ret0, _ := ret[0].(*telebot.Message) 343 | return ret0 344 | } 345 | 346 | // Message indicates an expected call of Message. 347 | func (mr *MockContextMockRecorder) Message() *gomock.Call { 348 | mr.mock.ctrl.T.Helper() 349 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Message", reflect.TypeOf((*MockContext)(nil).Message)) 350 | } 351 | 352 | // Migration mocks base method. 353 | func (m *MockContext) Migration() (int64, int64) { 354 | m.ctrl.T.Helper() 355 | ret := m.ctrl.Call(m, "Migration") 356 | ret0, _ := ret[0].(int64) 357 | ret1, _ := ret[1].(int64) 358 | return ret0, ret1 359 | } 360 | 361 | // Migration indicates an expected call of Migration. 362 | func (mr *MockContextMockRecorder) Migration() *gomock.Call { 363 | mr.mock.ctrl.T.Helper() 364 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Migration", reflect.TypeOf((*MockContext)(nil).Migration)) 365 | } 366 | 367 | // Notify mocks base method. 368 | func (m *MockContext) Notify(arg0 telebot.ChatAction) error { 369 | m.ctrl.T.Helper() 370 | ret := m.ctrl.Call(m, "Notify", arg0) 371 | ret0, _ := ret[0].(error) 372 | return ret0 373 | } 374 | 375 | // Notify indicates an expected call of Notify. 376 | func (mr *MockContextMockRecorder) Notify(arg0 interface{}) *gomock.Call { 377 | mr.mock.ctrl.T.Helper() 378 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Notify", reflect.TypeOf((*MockContext)(nil).Notify), arg0) 379 | } 380 | 381 | // Poll mocks base method. 382 | func (m *MockContext) Poll() *telebot.Poll { 383 | m.ctrl.T.Helper() 384 | ret := m.ctrl.Call(m, "Poll") 385 | ret0, _ := ret[0].(*telebot.Poll) 386 | return ret0 387 | } 388 | 389 | // Poll indicates an expected call of Poll. 390 | func (mr *MockContextMockRecorder) Poll() *gomock.Call { 391 | mr.mock.ctrl.T.Helper() 392 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Poll", reflect.TypeOf((*MockContext)(nil).Poll)) 393 | } 394 | 395 | // PollAnswer mocks base method. 396 | func (m *MockContext) PollAnswer() *telebot.PollAnswer { 397 | m.ctrl.T.Helper() 398 | ret := m.ctrl.Call(m, "PollAnswer") 399 | ret0, _ := ret[0].(*telebot.PollAnswer) 400 | return ret0 401 | } 402 | 403 | // PollAnswer indicates an expected call of PollAnswer. 404 | func (mr *MockContextMockRecorder) PollAnswer() *gomock.Call { 405 | mr.mock.ctrl.T.Helper() 406 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PollAnswer", reflect.TypeOf((*MockContext)(nil).PollAnswer)) 407 | } 408 | 409 | // PreCheckoutQuery mocks base method. 410 | func (m *MockContext) PreCheckoutQuery() *telebot.PreCheckoutQuery { 411 | m.ctrl.T.Helper() 412 | ret := m.ctrl.Call(m, "PreCheckoutQuery") 413 | ret0, _ := ret[0].(*telebot.PreCheckoutQuery) 414 | return ret0 415 | } 416 | 417 | // PreCheckoutQuery indicates an expected call of PreCheckoutQuery. 418 | func (mr *MockContextMockRecorder) PreCheckoutQuery() *gomock.Call { 419 | mr.mock.ctrl.T.Helper() 420 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PreCheckoutQuery", reflect.TypeOf((*MockContext)(nil).PreCheckoutQuery)) 421 | } 422 | 423 | // Query mocks base method. 424 | func (m *MockContext) Query() *telebot.Query { 425 | m.ctrl.T.Helper() 426 | ret := m.ctrl.Call(m, "Query") 427 | ret0, _ := ret[0].(*telebot.Query) 428 | return ret0 429 | } 430 | 431 | // Query indicates an expected call of Query. 432 | func (mr *MockContextMockRecorder) Query() *gomock.Call { 433 | mr.mock.ctrl.T.Helper() 434 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockContext)(nil).Query)) 435 | } 436 | 437 | // Recipient mocks base method. 438 | func (m *MockContext) Recipient() telebot.Recipient { 439 | m.ctrl.T.Helper() 440 | ret := m.ctrl.Call(m, "Recipient") 441 | ret0, _ := ret[0].(telebot.Recipient) 442 | return ret0 443 | } 444 | 445 | // Recipient indicates an expected call of Recipient. 446 | func (mr *MockContextMockRecorder) Recipient() *gomock.Call { 447 | mr.mock.ctrl.T.Helper() 448 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Recipient", reflect.TypeOf((*MockContext)(nil).Recipient)) 449 | } 450 | 451 | // Reply mocks base method. 452 | func (m *MockContext) Reply(arg0 interface{}, arg1 ...interface{}) error { 453 | m.ctrl.T.Helper() 454 | varargs := []interface{}{arg0} 455 | for _, a := range arg1 { 456 | varargs = append(varargs, a) 457 | } 458 | ret := m.ctrl.Call(m, "Reply", varargs...) 459 | ret0, _ := ret[0].(error) 460 | return ret0 461 | } 462 | 463 | // Reply indicates an expected call of Reply. 464 | func (mr *MockContextMockRecorder) Reply(arg0 interface{}, arg1 ...interface{}) *gomock.Call { 465 | mr.mock.ctrl.T.Helper() 466 | varargs := append([]interface{}{arg0}, arg1...) 467 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Reply", reflect.TypeOf((*MockContext)(nil).Reply), varargs...) 468 | } 469 | 470 | // Respond mocks base method. 471 | func (m *MockContext) Respond(arg0 ...*telebot.CallbackResponse) error { 472 | m.ctrl.T.Helper() 473 | varargs := []interface{}{} 474 | for _, a := range arg0 { 475 | varargs = append(varargs, a) 476 | } 477 | ret := m.ctrl.Call(m, "Respond", varargs...) 478 | ret0, _ := ret[0].(error) 479 | return ret0 480 | } 481 | 482 | // Respond indicates an expected call of Respond. 483 | func (mr *MockContextMockRecorder) Respond(arg0 ...interface{}) *gomock.Call { 484 | mr.mock.ctrl.T.Helper() 485 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Respond", reflect.TypeOf((*MockContext)(nil).Respond), arg0...) 486 | } 487 | 488 | // Send mocks base method. 489 | func (m *MockContext) Send(arg0 interface{}, arg1 ...interface{}) error { 490 | m.ctrl.T.Helper() 491 | varargs := []interface{}{arg0} 492 | for _, a := range arg1 { 493 | varargs = append(varargs, a) 494 | } 495 | ret := m.ctrl.Call(m, "Send", varargs...) 496 | ret0, _ := ret[0].(error) 497 | return ret0 498 | } 499 | 500 | // Send indicates an expected call of Send. 501 | func (mr *MockContextMockRecorder) Send(arg0 interface{}, arg1 ...interface{}) *gomock.Call { 502 | mr.mock.ctrl.T.Helper() 503 | varargs := append([]interface{}{arg0}, arg1...) 504 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Send", reflect.TypeOf((*MockContext)(nil).Send), varargs...) 505 | } 506 | 507 | // SendAlbum mocks base method. 508 | func (m *MockContext) SendAlbum(arg0 telebot.Album, arg1 ...interface{}) error { 509 | m.ctrl.T.Helper() 510 | varargs := []interface{}{arg0} 511 | for _, a := range arg1 { 512 | varargs = append(varargs, a) 513 | } 514 | ret := m.ctrl.Call(m, "SendAlbum", varargs...) 515 | ret0, _ := ret[0].(error) 516 | return ret0 517 | } 518 | 519 | // SendAlbum indicates an expected call of SendAlbum. 520 | func (mr *MockContextMockRecorder) SendAlbum(arg0 interface{}, arg1 ...interface{}) *gomock.Call { 521 | mr.mock.ctrl.T.Helper() 522 | varargs := append([]interface{}{arg0}, arg1...) 523 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendAlbum", reflect.TypeOf((*MockContext)(nil).SendAlbum), varargs...) 524 | } 525 | 526 | // Sender mocks base method. 527 | func (m *MockContext) Sender() *telebot.User { 528 | m.ctrl.T.Helper() 529 | ret := m.ctrl.Call(m, "Sender") 530 | ret0, _ := ret[0].(*telebot.User) 531 | return ret0 532 | } 533 | 534 | // Sender indicates an expected call of Sender. 535 | func (mr *MockContextMockRecorder) Sender() *gomock.Call { 536 | mr.mock.ctrl.T.Helper() 537 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Sender", reflect.TypeOf((*MockContext)(nil).Sender)) 538 | } 539 | 540 | // Set mocks base method. 541 | func (m *MockContext) Set(arg0 string, arg1 interface{}) { 542 | m.ctrl.T.Helper() 543 | m.ctrl.Call(m, "Set", arg0, arg1) 544 | } 545 | 546 | // Set indicates an expected call of Set. 547 | func (mr *MockContextMockRecorder) Set(arg0, arg1 interface{}) *gomock.Call { 548 | mr.mock.ctrl.T.Helper() 549 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Set", reflect.TypeOf((*MockContext)(nil).Set), arg0, arg1) 550 | } 551 | 552 | // Ship mocks base method. 553 | func (m *MockContext) Ship(arg0 ...interface{}) error { 554 | m.ctrl.T.Helper() 555 | varargs := []interface{}{} 556 | for _, a := range arg0 { 557 | varargs = append(varargs, a) 558 | } 559 | ret := m.ctrl.Call(m, "Ship", varargs...) 560 | ret0, _ := ret[0].(error) 561 | return ret0 562 | } 563 | 564 | // Ship indicates an expected call of Ship. 565 | func (mr *MockContextMockRecorder) Ship(arg0 ...interface{}) *gomock.Call { 566 | mr.mock.ctrl.T.Helper() 567 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Ship", reflect.TypeOf((*MockContext)(nil).Ship), arg0...) 568 | } 569 | 570 | // ShippingQuery mocks base method. 571 | func (m *MockContext) ShippingQuery() *telebot.ShippingQuery { 572 | m.ctrl.T.Helper() 573 | ret := m.ctrl.Call(m, "ShippingQuery") 574 | ret0, _ := ret[0].(*telebot.ShippingQuery) 575 | return ret0 576 | } 577 | 578 | // ShippingQuery indicates an expected call of ShippingQuery. 579 | func (mr *MockContextMockRecorder) ShippingQuery() *gomock.Call { 580 | mr.mock.ctrl.T.Helper() 581 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ShippingQuery", reflect.TypeOf((*MockContext)(nil).ShippingQuery)) 582 | } 583 | 584 | // Text mocks base method. 585 | func (m *MockContext) Text() string { 586 | m.ctrl.T.Helper() 587 | ret := m.ctrl.Call(m, "Text") 588 | ret0, _ := ret[0].(string) 589 | return ret0 590 | } 591 | 592 | // Text indicates an expected call of Text. 593 | func (mr *MockContextMockRecorder) Text() *gomock.Call { 594 | mr.mock.ctrl.T.Helper() 595 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Text", reflect.TypeOf((*MockContext)(nil).Text)) 596 | } 597 | 598 | // Update mocks base method. 599 | func (m *MockContext) Update() telebot.Update { 600 | m.ctrl.T.Helper() 601 | ret := m.ctrl.Call(m, "Update") 602 | ret0, _ := ret[0].(telebot.Update) 603 | return ret0 604 | } 605 | 606 | // Update indicates an expected call of Update. 607 | func (mr *MockContextMockRecorder) Update() *gomock.Call { 608 | mr.mock.ctrl.T.Helper() 609 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockContext)(nil).Update)) 610 | } 611 | -------------------------------------------------------------------------------- /models/bill.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "database/sql" 5 | "github.com/shopspring/decimal" 6 | "gorm.io/gorm" 7 | ) 8 | 9 | type Bill struct { 10 | gorm.Model 11 | UserID uint `gorm:"not null"` 12 | Amount decimal.Decimal `gorm:"type:decimal(10,2);not null"` 13 | Category string `gorm:"not null"` 14 | Name sql.NullString 15 | } 16 | -------------------------------------------------------------------------------- /models/telegram.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "gorm.io/gorm" 4 | 5 | type TelegramUser struct { 6 | gorm.Model 7 | BaseUserID uint `gorm:"not null;unique"` 8 | UserName string `gorm:"not null;unique"` 9 | ChatID int64 `gorm:"not null"` 10 | } 11 | -------------------------------------------------------------------------------- /models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/shopspring/decimal" 5 | "gorm.io/gorm" 6 | ) 7 | 8 | type User struct { 9 | gorm.Model 10 | Balance decimal.NullDecimal `gorm:"type:decimal(10,2)"` 11 | } 12 | 13 | type Token struct { 14 | gorm.Model 15 | UserID uint `gorm:"not null"` 16 | Token string `gorm:"not null;unique"` 17 | } 18 | -------------------------------------------------------------------------------- /service/bill/bill.go: -------------------------------------------------------------------------------- 1 | package bill 2 | 3 | import ( 4 | "github.com/orenoid/telegram-account-bot/dal/bill" 5 | "github.com/orenoid/telegram-account-bot/dal/user" 6 | "github.com/orenoid/telegram-account-bot/models" 7 | "github.com/pkg/errors" 8 | "time" 9 | ) 10 | 11 | type Service struct { 12 | billRepo bill.Repository 13 | userRepo user.Repository 14 | } 15 | 16 | func (receiver *Service) CreateNewBill(userID uint, amount float64, category string, opts ...bill.CreateBillOptions) (*models.Bill, error) { 17 | userExists, err := receiver.userRepo.CheckUserExists(userID) 18 | if err != nil { 19 | return nil, errors.WithStack(err) 20 | } 21 | if !userExists { 22 | return nil, errors.New("user not exists") 23 | } 24 | return receiver.billRepo.CreateBillAndUpdateUserBalance(userID, amount, category, opts...) 25 | } 26 | 27 | // GetUserBillsByCreateTime 获取用户在指定时间范围内的账单列表,若 opts 为空,则返回账单(opts 只取列表第一个作为查询参数) 28 | func (receiver *Service) GetUserBillsByCreateTime(userID uint, opts ...bill.GetUserBillsByCreateTimeOptions) ([]*models.Bill, error) { 29 | return receiver.billRepo.GetUserBillsByCreateTime(userID, opts...) 30 | } 31 | 32 | // CancelBillAndUpdateUserBalance 取消订单并更新用户余额 33 | func (receiver *Service) CancelBillAndUpdateUserBalance(billID uint) error { 34 | return receiver.billRepo.DeleteBillAndUpdateUserBalance(billID) 35 | } 36 | 37 | type CreateBillDTO struct { 38 | Amount float64 39 | Category string 40 | Name *string // optional 41 | CreatedAt *time.Time // if not provided, then use current time as default 42 | } 43 | 44 | func (receiver *Service) CreateNewBills(userID uint, billDTOs []CreateBillDTO) error { 45 | userExists, err := receiver.userRepo.CheckUserExists(userID) 46 | if err != nil { 47 | return errors.WithStack(err) 48 | } 49 | if !userExists { 50 | return errors.New("user not exists") 51 | } 52 | createBillParams := make([]bill.CreateBillParams, 0, len(billDTOs)) 53 | for _, billDTO := range billDTOs { 54 | createBillParams = append(createBillParams, bill.CreateBillParams{ 55 | Amount: billDTO.Amount, 56 | Category: billDTO.Category, 57 | CreateBillOptions: bill.CreateBillOptions{ 58 | Name: billDTO.Name, 59 | CreatedAt: billDTO.CreatedAt, 60 | }, 61 | }) 62 | } 63 | return receiver.billRepo.CreateBillsAndUpdateUserBalance(userID, createBillParams) 64 | } 65 | 66 | func NewService(billRepo bill.Repository, userRepo user.Repository) *Service { 67 | return &Service{billRepo: billRepo, userRepo: userRepo} 68 | } 69 | -------------------------------------------------------------------------------- /service/bill/bill_test.go: -------------------------------------------------------------------------------- 1 | package bill 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "github.com/golang/mock/gomock" 7 | "github.com/orenoid/telegram-account-bot/dal/bill" 8 | "github.com/orenoid/telegram-account-bot/dal/user" 9 | "github.com/orenoid/telegram-account-bot/models" 10 | "github.com/shopspring/decimal" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/suite" 13 | "gorm.io/gorm" 14 | "math/rand" 15 | "testing" 16 | "time" 17 | ) 18 | 19 | func TestNewService(t *testing.T) { 20 | ctrl := gomock.NewController(t) 21 | billRepo := bill.NewMockRepository(ctrl) 22 | userRepo := user.NewMockRepository(ctrl) 23 | service := NewService(billRepo, userRepo) 24 | assert.True(t, billRepo == service.billRepo) 25 | assert.True(t, userRepo == service.userRepo) 26 | } 27 | 28 | type BillServiceTestSuite struct { 29 | suite.Suite 30 | 31 | userMockCtrl *gomock.Controller 32 | userRepo *user.MockRepository 33 | 34 | billMockCtrl *gomock.Controller 35 | billRepo *bill.MockRepository 36 | billService *Service 37 | } 38 | 39 | func (suite *BillServiceTestSuite) SetupTest(t *testing.T) func() { 40 | suite.billMockCtrl = gomock.NewController(t) 41 | suite.billRepo = bill.NewMockRepository(suite.billMockCtrl) 42 | suite.userMockCtrl = gomock.NewController(t) 43 | suite.userRepo = user.NewMockRepository(suite.userMockCtrl) 44 | 45 | suite.billService = NewService(suite.billRepo, suite.userRepo) 46 | 47 | return func() { 48 | suite.billMockCtrl.Finish() 49 | suite.billMockCtrl = nil 50 | suite.billRepo = nil 51 | suite.billMockCtrl = nil 52 | suite.userRepo = nil 53 | suite.billService = nil 54 | } 55 | } 56 | 57 | func (suite *BillServiceTestSuite) TestCreateBill() { 58 | tearDown := suite.SetupTest(suite.T()) 59 | defer tearDown() 60 | 61 | params := []struct { 62 | userID uint 63 | billAmount float64 64 | category string 65 | billName string 66 | }{ 67 | {userID: 42, billAmount: 22.33, category: "饮食", billName: ""}, 68 | {userID: 43, billAmount: 300, category: "娱乐", billName: "十三机兵防卫圈"}, 69 | } 70 | for i, param := range params { 71 | suite.Run(fmt.Sprintf("param%d", i), func() { 72 | var optsI []interface{} 73 | var opts []bill.CreateBillOptions 74 | newBill := &models.Bill{ 75 | UserID: param.userID, Amount: decimal.NewFromFloat(param.billAmount), Category: param.category, Model: gorm.Model{ID: uint(rand.Int())}, 76 | } 77 | if param.billName != "" { 78 | newBill.Name = sql.NullString{String: param.billName, Valid: true} 79 | optsI = append(optsI, bill.CreateBillOptions{Name: ¶m.billName}) 80 | opts = append(opts, bill.CreateBillOptions{Name: ¶m.billName}) 81 | } 82 | suite.userRepo.EXPECT().CheckUserExists(param.userID).Return(true, nil) 83 | suite.billRepo.EXPECT().CreateBillAndUpdateUserBalance(param.userID, param.billAmount, param.category, optsI...).Return(newBill, nil) 84 | returnedBill, err := suite.billService.CreateNewBill(param.userID, param.billAmount, param.category, opts...) 85 | suite.NoError(err) 86 | suite.Equal(newBill, returnedBill) 87 | }) 88 | 89 | } 90 | } 91 | 92 | func (suite *BillServiceTestSuite) TestCreateBillIfUserNotFound() { 93 | tearDown := suite.SetupTest(suite.T()) 94 | defer tearDown() 95 | 96 | userID := uint(42) 97 | suite.userRepo.EXPECT().CheckUserExists(userID).Return(false, nil) 98 | suite.billRepo.EXPECT().CreateBillAndUpdateUserBalance(userID, 0, "").Times(0) 99 | returnedBill, err := suite.billService.CreateNewBill(userID, 0, "") 100 | suite.ErrorContains(err, "user not exists") 101 | suite.Nil(returnedBill) 102 | } 103 | 104 | func (suite *BillServiceTestSuite) TestGetBillsByCreateTime() { 105 | tearDown := suite.SetupTest(suite.T()) 106 | defer tearDown() 107 | 108 | var userID uint = 99 109 | optsSlice := []bill.GetUserBillsByCreateTimeOptions{{GreaterThan: time.Now(), LessThan: time.Now().Add(10 * time.Second)}} 110 | 111 | repoReturnBills := []*models.Bill{{UserID: 99, Amount: decimal.NewFromFloat(9)}} 112 | suite.billRepo.EXPECT().GetUserBillsByCreateTime(userID, optsSlice[0]).Return(repoReturnBills, nil) 113 | 114 | bills, err := suite.billService.GetUserBillsByCreateTime(userID, optsSlice...) 115 | suite.NoError(err) 116 | suite.Equal(repoReturnBills, bills) 117 | } 118 | 119 | func TestBillServiceSuite(t *testing.T) { 120 | suite.Run(t, new(BillServiceTestSuite)) 121 | } 122 | -------------------------------------------------------------------------------- /service/telegram/telegram.go: -------------------------------------------------------------------------------- 1 | package telegram 2 | 3 | import ( 4 | "github.com/orenoid/telegram-account-bot/dal/telegram" 5 | "github.com/orenoid/telegram-account-bot/models" 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | type Service struct { 10 | teleRepo telegram.Repository 11 | } 12 | 13 | func (s *Service) CreateOrUpdateTelegramUser(userID int64, userName string, chatID int64) (*models.TelegramUser, error) { 14 | return s.teleRepo.CreateOrUpdateTelegramUser(userID, userName, chatID) 15 | } 16 | 17 | func (s *Service) GetBaseUserID(teleUserID int64) (uint, error) { 18 | baseUser, err := s.teleRepo.GetUser(teleUserID) 19 | if err != nil { 20 | return 0, errors.WithStack(err) 21 | } 22 | return baseUser.ID, nil 23 | } 24 | 25 | func NewService(teleRepo telegram.Repository) *Service { 26 | return &Service{ 27 | teleRepo: teleRepo, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /service/telegram/telegram_test.go: -------------------------------------------------------------------------------- 1 | package telegram 2 | 3 | import ( 4 | "github.com/golang/mock/gomock" 5 | "github.com/orenoid/telegram-account-bot/dal/bill" 6 | "github.com/orenoid/telegram-account-bot/dal/telegram" 7 | "github.com/orenoid/telegram-account-bot/models" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/suite" 10 | "gorm.io/gorm" 11 | "math/rand" 12 | "strconv" 13 | "testing" 14 | ) 15 | 16 | func TestNewService(t *testing.T) { 17 | ctrl := gomock.NewController(t) 18 | teleRepo := telegram.NewMockRepository(ctrl) 19 | 20 | service := NewService(teleRepo) 21 | assert.NotNil(t, service) 22 | assert.True(t, service.teleRepo == teleRepo) 23 | } 24 | 25 | type ServiceTestSuite struct { 26 | suite.Suite 27 | 28 | teleMockCtrl *gomock.Controller 29 | teleRepo *telegram.MockRepository 30 | billMockCtrl *gomock.Controller 31 | billRepo *bill.MockRepository 32 | teleService *Service 33 | } 34 | 35 | func (suite *ServiceTestSuite) SetupTest(t *testing.T) func() { 36 | suite.teleMockCtrl = gomock.NewController(t) 37 | suite.teleRepo = telegram.NewMockRepository(suite.teleMockCtrl) 38 | suite.billMockCtrl = gomock.NewController(t) 39 | suite.billRepo = bill.NewMockRepository(suite.billMockCtrl) 40 | suite.teleService = NewService(suite.teleRepo) 41 | 42 | return func() { 43 | suite.teleMockCtrl = nil 44 | suite.teleRepo = nil 45 | suite.billMockCtrl = nil 46 | suite.billRepo = nil 47 | suite.teleService = nil 48 | } 49 | } 50 | 51 | func (suite *ServiceTestSuite) TestCreateOrUpdateUser() { 52 | tearDown := suite.SetupTest(suite.T()) 53 | defer tearDown() 54 | 55 | userID := int64(rand.Int()) 56 | userName := strconv.Itoa(rand.Int()) 57 | chatID := int64(rand.Int()) 58 | 59 | // userFromRepo 重新使用新的随机值,是为了校验 service 返回的 user 是由 repo 提供的 60 | userFromRepo := &models.TelegramUser{BaseUserID: uint(rand.Int()), UserName: "whatever", ChatID: int64(rand.Int())} 61 | suite.teleRepo.EXPECT().CreateOrUpdateTelegramUser(userID, userName, chatID).Return(userFromRepo, nil) 62 | 63 | userFromService, err := suite.teleService.CreateOrUpdateTelegramUser(userID, userName, chatID) 64 | suite.NoError(err) 65 | suite.Equal(userFromRepo, userFromService) 66 | } 67 | 68 | func (suite *ServiceTestSuite) TestGetBaseUserID() { 69 | tearDown := suite.SetupTest(suite.T()) 70 | defer tearDown() 71 | 72 | var teleUserID int64 = 426 73 | userFromRepo := &models.User{Model: gorm.Model{ID: 624}} 74 | suite.teleRepo.EXPECT().GetUser(teleUserID).Return(userFromRepo, nil) 75 | 76 | idFromService, err := suite.teleService.GetBaseUserID(teleUserID) 77 | suite.NoError(err) 78 | suite.Equal(userFromRepo.ID, idFromService) 79 | } 80 | 81 | func TestService(t *testing.T) { 82 | suite.Run(t, new(ServiceTestSuite)) 83 | } 84 | -------------------------------------------------------------------------------- /service/user/user.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "github.com/orenoid/telegram-account-bot/dal/user" 5 | "github.com/orenoid/telegram-account-bot/models" 6 | "github.com/orenoid/telegram-account-bot/utils/strings" 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | type Service struct { 11 | userRepo user.Repository 12 | } 13 | 14 | func (receiver *Service) CreateUser() (*models.User, error) { 15 | return receiver.userRepo.CreateUser() 16 | } 17 | 18 | func (receiver *Service) SetUserBalance(userID uint, balance float64) (float64, error) { 19 | userExists, err := receiver.userRepo.CheckUserExists(userID) 20 | if err != nil { 21 | return 0, err 22 | } 23 | if !userExists { 24 | return 0, errors.New("user not found") 25 | } 26 | newBalance, err := receiver.userRepo.SetUserBalance(userID, balance) 27 | if err != nil { 28 | return 0, err 29 | } 30 | return newBalance, nil 31 | } 32 | 33 | func (receiver *Service) GetUserBalance(userID uint) (float64, error) { 34 | userExists, err := receiver.userRepo.CheckUserExists(userID) 35 | if err != nil { 36 | return 0, err 37 | } 38 | if !userExists { 39 | return 0, errors.New("user not found") 40 | } 41 | balance, err := receiver.userRepo.GetUserBalance(userID) 42 | if err != nil { 43 | return 0, err 44 | } 45 | return balance, nil 46 | } 47 | 48 | func (receiver *Service) CreateToken(userID uint) (string, error) { 49 | token, err := strings.GenerateToken() 50 | if err != nil { 51 | return "", err 52 | } 53 | err = receiver.userRepo.CreateToken(userID, token) 54 | if err != nil { 55 | return "", err 56 | } 57 | return token, nil 58 | } 59 | 60 | func (receiver *Service) DisableAllTokens(userID uint) error { 61 | return receiver.userRepo.DisableAllTokens(userID) 62 | } 63 | 64 | func (receiver *Service) MustGetUserIDByToken(token string) (uint, error) { 65 | tokenRecord, err := receiver.userRepo.MustGetToken(token) 66 | if err != nil { 67 | return 0, err 68 | } 69 | return tokenRecord.UserID, nil 70 | } 71 | 72 | func NewUserService(userRepo user.Repository) *Service { 73 | return &Service{userRepo: userRepo} 74 | } 75 | -------------------------------------------------------------------------------- /service/user/user_test.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "github.com/golang/mock/gomock" 5 | "github.com/orenoid/telegram-account-bot/dal/user" 6 | "github.com/orenoid/telegram-account-bot/models" 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/suite" 9 | "gorm.io/gorm" 10 | "math/rand" 11 | "testing" 12 | "time" 13 | ) 14 | 15 | func TestNewUserService(t *testing.T) { 16 | 17 | ctrl := gomock.NewController(t) 18 | defer ctrl.Finish() 19 | ur := user.NewMockRepository(ctrl) 20 | us := NewUserService(ur) 21 | 22 | assert.True(t, ur == us.userRepo) 23 | } 24 | 25 | type UserServiceTestSuite struct { 26 | suite.Suite 27 | userMockCtrl *gomock.Controller 28 | userRepo *user.MockRepository 29 | userService *Service 30 | } 31 | 32 | func (suite *UserServiceTestSuite) SetupTest(t *testing.T) func() { 33 | suite.userMockCtrl = gomock.NewController(t) 34 | suite.userRepo = user.NewMockRepository(suite.userMockCtrl) 35 | var err error 36 | suite.userService = NewUserService(suite.userRepo) 37 | if err != nil { 38 | panic(err) 39 | } 40 | return func() { 41 | suite.userMockCtrl.Finish() 42 | suite.userMockCtrl = nil 43 | suite.userRepo = nil 44 | suite.userService = nil 45 | } 46 | } 47 | 48 | func (suite *UserServiceTestSuite) TestCreateUser() { 49 | tearDown := suite.SetupTest(suite.T()) 50 | defer tearDown() 51 | expectUser := &models.User{ 52 | Model: gorm.Model{ID: uint(rand.Int()), CreatedAt: getRandTime(), UpdatedAt: getRandTime()}, 53 | } 54 | suite.userRepo.EXPECT().CreateUser().Return(expectUser, nil) 55 | 56 | newUser, err := suite.userService.CreateUser() 57 | suite.NoError(err) 58 | suite.Equal(expectUser, newUser) 59 | } 60 | 61 | func (suite *UserServiceTestSuite) TestSetUserBalanceSuccessFully() { 62 | tearDown := suite.SetupTest(suite.T()) 63 | defer tearDown() 64 | 65 | userID := uint(rand.Uint64()) 66 | balance := rand.Float64() 67 | suite.userRepo.EXPECT().CheckUserExists(userID).Return(true, nil) 68 | suite.userRepo.EXPECT().SetUserBalance(userID, balance).Return(balance, nil) 69 | 70 | returnedBalance, err := suite.userService.SetUserBalance(userID, balance) 71 | suite.NoError(err) 72 | suite.Equal(balance, returnedBalance) 73 | } 74 | 75 | func (suite *UserServiceTestSuite) TestSetUserBalanceIfUserNotFound() { 76 | tearDown := suite.SetupTest(suite.T()) 77 | defer tearDown() 78 | 79 | userID := uint(rand.Uint64()) 80 | suite.userRepo.EXPECT().CheckUserExists(userID).Return(false, nil) 81 | suite.userRepo.EXPECT().SetUserBalance(userID, 0).Times(0) 82 | 83 | _, err := suite.userService.SetUserBalance(userID, 0) 84 | suite.ErrorContains(err, "user not found") 85 | } 86 | 87 | func getRandTime() time.Time { 88 | return time.Unix(rand.Int63(), 0) 89 | } 90 | 91 | func TestUserServiceTestSuite(t *testing.T) { 92 | suite.Run(t, new(UserServiceTestSuite)) 93 | } 94 | -------------------------------------------------------------------------------- /telebot/handlers.go: -------------------------------------------------------------------------------- 1 | package telebot 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | "time" 10 | 11 | billDAL "github.com/orenoid/telegram-account-bot/dal/bill" 12 | "github.com/orenoid/telegram-account-bot/service/bill" 13 | "github.com/orenoid/telegram-account-bot/service/telegram" 14 | "github.com/orenoid/telegram-account-bot/service/user" 15 | "github.com/pkg/errors" 16 | "gopkg.in/telebot.v3" 17 | ) 18 | 19 | type HandlersHub struct { 20 | teleService *telegram.Service 21 | billService *bill.Service 22 | userService *user.Service 23 | userStateManager UserStateManager 24 | } 25 | 26 | func NewHandlerHub(billService *bill.Service, teleService *telegram.Service, userService *user.Service, userStateManager UserStateManager) *HandlersHub { 27 | return &HandlersHub{billService: billService, teleService: teleService, userService: userService, userStateManager: userStateManager} 28 | } 29 | 30 | const helpMessage = `欢迎使用记账机器人! 31 | 32 | 以下是可用的命令: 33 | /start - 开始使用 34 | /day - 查看当日账单 35 | /month - 查看当月账单 36 | /set_keyboard - 设置快捷键盘 37 | /cancel - 取消当前操作 38 | /set_balance - 设置余额 39 | /balance - 查询余额 40 | /create_token - 创建用于 OpenAPI 的 token 41 | /disable_all_tokens - 废弃所有 token 42 | 43 | 如何记账: 44 | 直接向机器人发送要记录的账单类别 45 | 待机器人回复后,再发送金额,即可完成本次记账 46 | 47 | 金额前面带上\"+\"号表示收入,不带表示支出 48 | 账单类别可以使用快捷键盘,也可以手动输入 49 | 50 | 如何自定义快捷键盘: 51 | 请按照以下格式输入你想要设置的快捷键盘,例如: 52 | 53 | 饮食,出行,杂项|娱乐,购物,房租|工资,基金 54 | 55 | 其中\"|\"表示换行 56 | 在上面的例子中,则表示设置一个三行的快捷键盘,第一行设置了「饮食」、「出行」、「杂项」三个账单类别,以此类推 57 | 58 | 如何设置余额: 59 | 在聊天框中输入 /set_balance 后跟上您想要设置的余额,例如: 60 | 61 | /set_balance 1000 62 | ` 63 | 64 | func (hub *HandlersHub) HandleStartCommand(ctx telebot.Context) error { 65 | chat := ctx.Chat() 66 | if chat == nil { 67 | return errors.New("nil chat of context") 68 | } 69 | sender := ctx.Sender() 70 | _, err := hub.teleService.CreateOrUpdateTelegramUser(sender.ID, sender.Username, chat.ID) 71 | if err != nil { 72 | return err 73 | } 74 | defaultKeyboard := textToKeyboard("饮食,出行,杂项|娱乐,购物,房租|工资") 75 | err = ctx.Send(helpMessage, &telebot.ReplyMarkup{ReplyKeyboard: defaultKeyboard}) 76 | if err != nil { 77 | return errors.WithStack(err) 78 | } 79 | return nil 80 | } 81 | 82 | func (hub *HandlersHub) HandleHelpCommand(ctx telebot.Context) error { 83 | err := ctx.Send(helpMessage) 84 | return errors.WithStack(err) 85 | } 86 | 87 | func (hub *HandlersHub) HandleDayCommand(ctx telebot.Context) error { 88 | sender := ctx.Sender() 89 | now := time.Now() 90 | // TODO get location from user, same for other commands 91 | begin, end := getDayRange(now) 92 | 93 | baseUserID, err := hub.teleService.GetBaseUserID(sender.ID) 94 | if err != nil { 95 | return err 96 | } 97 | bills, err := hub.billService.GetUserBillsByCreateTime(baseUserID, 98 | billDAL.GetUserBillsByCreateTimeOptions{GreaterThan: begin, GreaterOrEqual: true, LessThan: end}) 99 | if err != nil { 100 | return err 101 | } 102 | return ctx.Send(&DateBillsSender{bills, now.Year(), int(now.Month()), now.Day(), false}) 103 | } 104 | 105 | func (hub *HandlersHub) HandleSetKeyboardCommand(ctx telebot.Context) error { 106 | sender := ctx.Sender() 107 | if sender == nil { 108 | return nil 109 | } 110 | // 记录用户状态 111 | err := hub.userStateManager.SetUserState(sender.ID, &UserState{Type: SettingKeyboard}) 112 | if err != nil { 113 | return err 114 | } 115 | // 发送提示信息 116 | err = ctx.Send("快捷键盘用于设置一些日常生活中的支出/收入类别,用于快速记录\n\n请按照以下格式输入你想要设置的快捷键盘,例如:\n\n饮食,出行,杂项|娱乐,购物|工资,基金\n\n其中\"|\"表示换行,在上面的例子中,则表示设置一个三行的快捷键盘,第一行设置了「饮食」、「出行」、「杂项」三个账单类别,以此类推") 117 | return errors.WithStack(err) 118 | } 119 | 120 | // 获取某个时刻当天的0点-24点范围 121 | func getDayRange(t time.Time) (time.Time, time.Time) { 122 | begin := time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location()) 123 | tomorrow := t.Add(24 * time.Hour) 124 | end := time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), 0, 0, 0, 0, tomorrow.Location()) 125 | return begin, end 126 | } 127 | 128 | func (hub *HandlersHub) HandleMonthCommand(ctx telebot.Context) error { 129 | sender := ctx.Sender() 130 | now := time.Now() 131 | begin, end := getMonthRange(now) 132 | 133 | baseUserID, err := hub.teleService.GetBaseUserID(sender.ID) 134 | if err != nil { 135 | return err 136 | } 137 | bills, err := hub.billService.GetUserBillsByCreateTime(baseUserID, 138 | billDAL.GetUserBillsByCreateTimeOptions{GreaterThan: begin, GreaterOrEqual: true, LessThan: end}) 139 | if err != nil { 140 | return err 141 | } 142 | var sendable telebot.Sendable = &MonthBillsSender{Bills: bills, Year: now.Year(), Month: int(now.Month())} 143 | return ctx.Send(sendable) 144 | } 145 | 146 | func (hub *HandlersHub) HandleCancelCommand(ctx telebot.Context) error { 147 | sender := ctx.Sender() 148 | if sender == nil { 149 | return nil 150 | } 151 | err := hub.userStateManager.ClearUserState(sender.ID) 152 | if err != nil { 153 | return err 154 | } 155 | err = ctx.Send("已取消") 156 | return err 157 | } 158 | 159 | func getMonthRange(t time.Time) (time.Time, time.Time) { 160 | currentYear, currentMonth, _ := t.Date() 161 | currentLocation := t.Location() 162 | 163 | firstOfMonth := time.Date(currentYear, currentMonth, 1, 0, 0, 0, 0, currentLocation) 164 | firstOfNextMonth := firstOfMonth.AddDate(0, 1, 0) 165 | return firstOfMonth, firstOfNextMonth 166 | } 167 | 168 | func (hub *HandlersHub) HandleText(ctx telebot.Context) error { 169 | userState, err := hub.userStateManager.GetUserState(ctx.Sender().ID) 170 | if err != nil { 171 | return err 172 | } 173 | switch userState.Type { 174 | case Empty: 175 | return hub.OnEmpty(ctx) 176 | case CreatingBill: 177 | return hub.OnCreatingBill(ctx, userState) 178 | case SettingKeyboard: 179 | return hub.OnSettingKeyboard(ctx) 180 | } 181 | return nil 182 | } 183 | 184 | func (hub *HandlersHub) OnEmpty(ctx telebot.Context) error { 185 | text := ctx.Text() 186 | if len(text) == 0 { 187 | return nil 188 | } 189 | category, name := ParseBill(text) 190 | err := hub.userStateManager.SetUserState(ctx.Sender().ID, 191 | &UserState{Type: CreatingBill, BillCategory: &category, BillName: name}, 192 | ) 193 | if err != nil { 194 | return err 195 | } 196 | err = ctx.Send(fmt.Sprintf("账单类别:%s,请输入账单金额\n默认记做支出,若想记为收入,可在金额前带上\"+\"号\n若想取消本次操作,请输入 /cancel", category)) 197 | return errors.WithStack(err) 198 | } 199 | 200 | func (hub *HandlersHub) OnCreatingBill(ctx telebot.Context, userState *UserState) error { 201 | amount, err := parseAmount(ctx.Text()) 202 | if err != nil { 203 | return err 204 | } 205 | sender := ctx.Sender() 206 | baseUserID, err := hub.teleService.GetBaseUserID(sender.ID) 207 | if err != nil { 208 | return err 209 | } 210 | newBill, err := hub.billService.CreateNewBill( 211 | baseUserID, amount, *userState.BillCategory, billDAL.CreateBillOptions{Name: userState.BillName}, 212 | ) 213 | if err != nil { 214 | return err 215 | } 216 | err = hub.userStateManager.ClearUserState(sender.ID) 217 | if err != nil { 218 | return err 219 | } 220 | err = ctx.Send(&NewBillSender{newBill}) 221 | return errors.WithStack(err) 222 | } 223 | 224 | var validAmount = regexp.MustCompile("^([+-]?)([0-9]*\\.?[0-9]+)$") 225 | 226 | // parseAmount 解析数额,若前面不带 "+",则默认会解析为负数(平时大多数时候为支出) 227 | func parseAmount(text string) (float64, error) { 228 | matchResult := validAmount.FindStringSubmatch(text) 229 | if len(matchResult) == 0 { 230 | return 0, errors.Errorf("invalid amount text: %s", text) 231 | } else if len(matchResult) == 3 { 232 | amount, err := strconv.ParseFloat(matchResult[2], 64) 233 | if err != nil { 234 | return 0, errors.WithStack(err) 235 | } 236 | if matchResult[1] != "+" { 237 | amount = -amount 238 | } 239 | return amount, nil 240 | } 241 | return 0, errors.Errorf("invalid amount text: %s", text) 242 | } 243 | 244 | func ParseBill(text string) (string, *string) { 245 | ss := strings.SplitN(text, " ", 2) 246 | var category, name string 247 | if len(ss) == 1 { 248 | category = ss[0] 249 | return category, nil 250 | } else { 251 | category, name = ss[0], ss[1] 252 | return category, &name 253 | } 254 | } 255 | 256 | func (hub *HandlersHub) OnSettingKeyboard(ctx telebot.Context) error { 257 | keyboardStr := ctx.Text() 258 | keyboard := textToKeyboard(keyboardStr) 259 | err := hub.userStateManager.ClearUserState(ctx.Sender().ID) 260 | if err != nil { 261 | return err 262 | } 263 | err = ctx.Send("已设置", &telebot.ReplyMarkup{ReplyKeyboard: keyboard}) 264 | return errors.WithStack(err) 265 | } 266 | 267 | func textToKeyboard(text string) [][]telebot.ReplyButton { 268 | var result [][]telebot.ReplyButton 269 | rows := strings.Split(text, "|") 270 | for _, rowStr := range rows { 271 | categories := strings.Split(rowStr, ",") 272 | btnsInRow := make([]telebot.ReplyButton, 0, len(categories)) 273 | for _, category := range categories { 274 | btn := telebot.ReplyButton{Text: category} 275 | btnsInRow = append(btnsInRow, btn) 276 | } 277 | result = append(result, btnsInRow) 278 | } 279 | return result 280 | } 281 | 282 | // HandleDayBillSelectionCallback 处理切换日期账单的回调事件 283 | func (hub *HandlersHub) HandleDayBillSelectionCallback(ctx telebot.Context) error { 284 | // 解析回调按钮的日期 285 | callback := ctx.Callback() 286 | if callback == nil { 287 | return nil 288 | } 289 | data := DayBillBtnData{} 290 | err := json.Unmarshal([]byte(callback.Data), &data) 291 | if err != nil { 292 | return errors.WithStack(err) 293 | } 294 | // 查询当日账单 295 | baseUserID, err := hub.teleService.GetBaseUserID(callback.Sender.ID) 296 | if err != nil { 297 | return err 298 | } 299 | date := time.Date(data.Year, time.Month(data.Month), data.Day, 0, 0, 0, 0, time.Local) 300 | begin, end := getDayRange(date) 301 | bills, err := hub.billService.GetUserBillsByCreateTime(baseUserID, 302 | billDAL.GetUserBillsByCreateTimeOptions{GreaterThan: begin, GreaterOrEqual: true, LessThan: end}) 303 | if err != nil { 304 | return err 305 | } 306 | // 更新消息,切换账单 307 | sender := &DateBillsSender{bills, data.Year, data.Month, data.Day, false} 308 | err = ctx.Edit(sender.Text(), sender.ReplyMarkup()) 309 | if err != nil { 310 | return errors.WithStack(err) 311 | } 312 | return nil 313 | } 314 | 315 | // HandleMonthBillSelectionCallback 处理切换月度账单的回调事件 316 | func (hub *HandlersHub) HandleMonthBillSelectionCallback(ctx telebot.Context) error { 317 | callback := ctx.Callback() 318 | if callback == nil { 319 | return nil 320 | } 321 | data := MonthBillBtnData{} 322 | err := json.Unmarshal([]byte(callback.Data), &data) 323 | if err != nil { 324 | return errors.WithStack(err) 325 | } 326 | // 查询月度账单 327 | baseUserID, err := hub.teleService.GetBaseUserID(callback.Sender.ID) 328 | if err != nil { 329 | return err 330 | } 331 | date := time.Date(data.Year, time.Month(data.Month), 1, 0, 0, 0, 0, time.Local) 332 | begin, end := getMonthRange(date) 333 | bills, err := hub.billService.GetUserBillsByCreateTime(baseUserID, 334 | billDAL.GetUserBillsByCreateTimeOptions{GreaterThan: begin, GreaterOrEqual: true, LessThan: end}) 335 | if err != nil { 336 | return err 337 | } 338 | // 更新消息,切换账单 339 | sender := &MonthBillsSender{bills, data.Year, data.Month} 340 | err = ctx.Edit(sender.Text(), sender.ReplyMarkup()) 341 | if err != nil { 342 | return errors.WithStack(err) 343 | } 344 | return nil 345 | } 346 | 347 | func (hub *HandlersHub) HandleCancelBillCallback(ctx telebot.Context) error { 348 | callback := ctx.Callback() 349 | if callback == nil { 350 | return nil 351 | } 352 | data := CancelBillData{} 353 | err := json.Unmarshal([]byte(callback.Data), &data) 354 | if err != nil { 355 | return errors.WithStack(err) 356 | } 357 | err = hub.billService.CancelBillAndUpdateUserBalance(data.BillID) 358 | if err != nil { 359 | return err 360 | } 361 | err = ctx.Edit("已撤销账单") 362 | return errors.WithStack(err) 363 | } 364 | 365 | func (hub *HandlersHub) HandleSetBalanceCommand(ctx telebot.Context) error { 366 | sender := ctx.Sender() 367 | if sender == nil { 368 | return nil 369 | } 370 | amount, err := strconv.ParseFloat(strings.TrimSpace(strings.TrimPrefix(ctx.Text(), "/set_balance")), 64) 371 | if err != nil { 372 | return err 373 | } 374 | baseUserID, err := hub.teleService.GetBaseUserID(sender.ID) 375 | if err != nil { 376 | return err 377 | } 378 | amount, err = hub.userService.SetUserBalance(baseUserID, amount) 379 | if err != nil { 380 | return err 381 | } 382 | err = ctx.Send(fmt.Sprintf("已将您的余额设置为 %.2f", amount)) 383 | return errors.WithStack(err) 384 | } 385 | 386 | func (hub *HandlersHub) HandleBalanceCommand(ctx telebot.Context) error { 387 | sender := ctx.Sender() 388 | if sender == nil { 389 | return nil 390 | } 391 | baseUserID, err := hub.teleService.GetBaseUserID(sender.ID) 392 | if err != nil { 393 | return err 394 | } 395 | balance, err := hub.userService.GetUserBalance(baseUserID) 396 | if err != nil { 397 | return err 398 | } 399 | err = ctx.Send(fmt.Sprintf("您的余额为 %.2f", balance)) 400 | return errors.WithStack(err) 401 | } 402 | 403 | func (hub *HandlersHub) HandleCreateTokenCommand(ctx telebot.Context) error { 404 | // get user id 405 | sender := ctx.Sender() 406 | if sender == nil { 407 | return nil 408 | } 409 | baseUserID, err := hub.teleService.GetBaseUserID(sender.ID) 410 | if err != nil { 411 | return err 412 | } 413 | // create token 414 | token, err := hub.userService.CreateToken(baseUserID) 415 | if err != nil { 416 | return err 417 | } 418 | err = ctx.Send(fmt.Sprintf("New token created:\n%s", token)) 419 | return errors.WithStack(err) 420 | } 421 | 422 | func (hub *HandlersHub) HandleDisableAllTokensCommand(ctx telebot.Context) error { 423 | sender := ctx.Sender() 424 | if sender == nil { 425 | return nil 426 | } 427 | baseUserID, err := hub.teleService.GetBaseUserID(sender.ID) 428 | if err != nil { 429 | return err 430 | } 431 | err = hub.userService.DisableAllTokens(baseUserID) 432 | if err != nil { 433 | return err 434 | } 435 | err = ctx.Send("All tokens disabled") 436 | return errors.WithStack(err) 437 | } 438 | -------------------------------------------------------------------------------- /telebot/handlers_test.go: -------------------------------------------------------------------------------- 1 | package telebot 2 | 3 | import ( 4 | "fmt" 5 | "github.com/agiledragon/gomonkey/v2" 6 | "github.com/golang/mock/gomock" 7 | billDAL "github.com/orenoid/telegram-account-bot/dal/bill" 8 | teleDAL "github.com/orenoid/telegram-account-bot/dal/telegram" 9 | userDAL "github.com/orenoid/telegram-account-bot/dal/user" 10 | "github.com/orenoid/telegram-account-bot/mock/telebotmock" 11 | "github.com/orenoid/telegram-account-bot/models" 12 | "github.com/orenoid/telegram-account-bot/service/bill" 13 | "github.com/orenoid/telegram-account-bot/service/telegram" 14 | "github.com/orenoid/telegram-account-bot/service/user" 15 | "github.com/orenoid/telegram-account-bot/utils/strings" 16 | "github.com/pkg/errors" 17 | "github.com/stretchr/testify/assert" 18 | "github.com/stretchr/testify/suite" 19 | "gopkg.in/telebot.v3" 20 | "reflect" 21 | "testing" 22 | "time" 23 | ) 24 | 25 | func TestNewHandlerHub(t *testing.T) { 26 | billService := &bill.Service{} 27 | teleService := &telegram.Service{} 28 | userService := &user.Service{} 29 | manager := NewInMemoryUserStateManager() 30 | hub := NewHandlerHub(billService, teleService, userService, manager) 31 | assert.IsType(t, &HandlersHub{}, hub) 32 | assert.NotNil(t, hub) 33 | assert.True(t, hub.teleService == teleService) 34 | assert.True(t, hub.userStateManager == manager) 35 | assert.True(t, hub.billService == billService) 36 | assert.True(t, hub.userService == userService) 37 | } 38 | 39 | type HandlersHubTestSuite struct { 40 | suite.Suite 41 | 42 | teleRepo *teleDAL.MockRepository 43 | billRepo *billDAL.MockRepository 44 | userRepo *userDAL.MockRepository 45 | 46 | teleService *telegram.Service 47 | billService *bill.Service 48 | userService *user.Service 49 | 50 | userStateManager *MockUserStateManager 51 | 52 | hub *HandlersHub 53 | } 54 | 55 | func (suite *HandlersHubTestSuite) SetupTest(t *testing.T) func() { 56 | suite.teleRepo = teleDAL.NewMockRepository(gomock.NewController(t)) 57 | suite.billRepo = billDAL.NewMockRepository(gomock.NewController(t)) 58 | suite.userRepo = userDAL.NewMockRepository(gomock.NewController(t)) 59 | 60 | suite.teleService = telegram.NewService(suite.teleRepo) 61 | suite.billService = bill.NewService(suite.billRepo, suite.userRepo) 62 | suite.userService = user.NewUserService(suite.userRepo) 63 | suite.userStateManager = NewMockUserStateManager(gomock.NewController(t)) 64 | 65 | suite.hub = NewHandlerHub(suite.billService, suite.teleService, suite.userService, suite.userStateManager) 66 | 67 | return func() { 68 | suite.teleRepo = nil 69 | suite.billRepo = nil 70 | suite.userRepo = nil 71 | suite.teleService = nil 72 | suite.billService = nil 73 | suite.userStateManager = nil 74 | suite.hub = nil 75 | } 76 | } 77 | 78 | func (suite *HandlersHubTestSuite) TestHandleStartCommand() { 79 | tearDown := suite.SetupTest(suite.T()) 80 | defer tearDown() 81 | 82 | ctx := telebotmock.NewMockContext(gomock.NewController(suite.T())) 83 | 84 | ctx.EXPECT().Chat().Return(&telebot.Chat{ID: 592371906012}).Times(1) 85 | ctx.EXPECT().Sender().Return(&telebot.User{ID: 417714530102, Username: "JustARandomName"}).Times(1) 86 | ctx.EXPECT().Send(helpMessage, &telebot.ReplyMarkup{ 87 | ReplyKeyboard: [][]telebot.ReplyButton{ 88 | { 89 | {Text: "饮食"}, {Text: "出行"}, {Text: "杂项"}, 90 | }, 91 | { 92 | {Text: "娱乐"}, {Text: "购物"}, {Text: "房租"}, 93 | }, 94 | { 95 | {Text: "工资"}, 96 | }, 97 | }, 98 | }) 99 | 100 | type MethodParams struct { 101 | userID int64 102 | userName string 103 | chatID int64 104 | } 105 | var methodParams MethodParams 106 | gomonkey.ApplyMethod(reflect.TypeOf(suite.teleService), "CreateOrUpdateTelegramUser", 107 | func(service *telegram.Service, userID int64, userName string, chatID int64) (*models.TelegramUser, error) { 108 | methodParams = MethodParams{userID, userName, chatID} 109 | return nil, nil 110 | }, 111 | ) 112 | err := suite.hub.HandleStartCommand(ctx) 113 | suite.NoError(err) 114 | suite.Equal(MethodParams{417714530102, "JustARandomName", 592371906012}, methodParams) 115 | } 116 | 117 | func (suite *HandlersHubTestSuite) TestHandleText() { 118 | tearDown := suite.SetupTest(suite.T()) 119 | defer tearDown() 120 | 121 | userStates := []*UserState{ 122 | {Type: Empty}, {Type: CreatingBill}, 123 | } 124 | 125 | type OnEmptyPatchesHelper struct { 126 | called bool 127 | paramCtx telebot.Context 128 | returnErr error 129 | } 130 | onEmpty := &OnEmptyPatchesHelper{returnErr: errors.New("OnEmptyError")} 131 | patches := gomonkey.ApplyMethod(reflect.TypeOf(suite.hub), "OnEmpty", func(_ *HandlersHub, ctx telebot.Context) error { 132 | onEmpty.called = true 133 | onEmpty.paramCtx = ctx 134 | return onEmpty.returnErr 135 | }) 136 | defer patches.Reset() 137 | 138 | type OnCreatingBill struct { 139 | called bool 140 | paramCtx telebot.Context 141 | paramState *UserState 142 | returnErr error 143 | } 144 | 145 | onCreatingBill := &OnCreatingBill{returnErr: errors.New("OnCreatingBillError")} 146 | patches = gomonkey.ApplyMethod(reflect.TypeOf(suite.hub), "OnCreatingBill", 147 | func(_ *HandlersHub, ctx telebot.Context, state *UserState) error { 148 | onCreatingBill.called = true 149 | onCreatingBill.paramState = state 150 | onCreatingBill.paramCtx = ctx 151 | return onCreatingBill.returnErr 152 | }) 153 | defer patches.Reset() 154 | 155 | for _, state := range userStates { 156 | ctx := telebotmock.NewMockContext(gomock.NewController(suite.T())) 157 | ctx.EXPECT().Sender().Return(&telebot.User{ID: 42}).Times(1) 158 | suite.userStateManager.EXPECT().GetUserState(int64(42)).Return(state, nil).Times(1) 159 | 160 | err := suite.hub.HandleText(ctx) 161 | 162 | switch state.Type { 163 | case Empty: 164 | suite.True(onEmpty.called) 165 | suite.False(onCreatingBill.called) 166 | suite.True(onEmpty.paramCtx == ctx) 167 | suite.Equal(onEmpty.returnErr, err) 168 | case CreatingBill: 169 | suite.True(onCreatingBill.called) 170 | suite.False(onEmpty.called) 171 | suite.True(state == onCreatingBill.paramState) 172 | suite.True(onCreatingBill.paramCtx == ctx) 173 | suite.Equal(onCreatingBill.returnErr, err) 174 | default: 175 | suite.NoError(err) 176 | } 177 | onEmpty.called = false 178 | onEmpty.paramCtx = nil 179 | onCreatingBill.called = false 180 | onCreatingBill.paramCtx = nil 181 | onCreatingBill.paramState = nil 182 | } 183 | 184 | } 185 | 186 | func (suite *HandlersHubTestSuite) TestOnEmpty() { 187 | tearDown := suite.SetupTest(suite.T()) 188 | defer tearDown() 189 | 190 | type ParseBillPatchesHelper struct { 191 | called bool 192 | // params 193 | text string 194 | // return 195 | category string 196 | name *string 197 | } 198 | parseBillPatchesHelper := &ParseBillPatchesHelper{category: "饮食"} 199 | patches := gomonkey.ApplyFunc(ParseBill, func(text string) (string, *string) { 200 | parseBillPatchesHelper.called = true 201 | parseBillPatchesHelper.text = text 202 | return parseBillPatchesHelper.category, parseBillPatchesHelper.name 203 | }) 204 | defer patches.Reset() 205 | 206 | ctx := telebotmock.NewMockContext(gomock.NewController(suite.T())) 207 | ctx.EXPECT().Text().Return("饮食").Times(1) 208 | ctx.EXPECT().Sender().Return(&telebot.User{ID: 6379}) 209 | suite.userStateManager.EXPECT().SetUserState(int64(6379), 210 | &UserState{CreatingBill, &parseBillPatchesHelper.category, parseBillPatchesHelper.name}).Times(1) 211 | ctx.EXPECT().Send(fmt.Sprintf("账单类别:%s,请输入账单金额\n默认记做支出,若想记为收入,可在金额前带上\"+\"号\n若想取消本次操作,请输入 /cancel", parseBillPatchesHelper.category)).Times(1) 212 | 213 | err := suite.hub.OnEmpty(ctx) 214 | 215 | suite.Equal("饮食", parseBillPatchesHelper.text) 216 | suite.True(parseBillPatchesHelper.called) 217 | suite.NoError(err) 218 | } 219 | 220 | func (suite *HandlersHubTestSuite) TestOnEmptyIfEmptyText() { 221 | tearDown := suite.SetupTest(suite.T()) 222 | defer tearDown() 223 | 224 | ctx := telebotmock.NewMockContext(gomock.NewController(suite.T())) 225 | ctx.EXPECT().Text().Return("").Times(1) 226 | suite.userStateManager.EXPECT().SetUserState(0, nil).Times(0) 227 | err := suite.hub.OnEmpty(ctx) 228 | suite.Nil(err) 229 | } 230 | 231 | func (suite *HandlersHubTestSuite) TestOnCreatingBill() { 232 | tearDown := suite.SetupTest(suite.T()) 233 | defer tearDown() 234 | 235 | // mock teleService.GetBaseUserID 236 | type GetBaseUserID struct { 237 | called bool 238 | paramTeleUserID int64 239 | returnBaseUserID uint 240 | returnErr error 241 | } 242 | getBaseUserID := &GetBaseUserID{returnBaseUserID: 255, returnErr: nil} 243 | patches := gomonkey.ApplyMethod(reflect.TypeOf(suite.hub.teleService), "GetBaseUserID", 244 | func(_ *telegram.Service, teleUserID int64) (uint, error) { 245 | getBaseUserID.called = true 246 | getBaseUserID.paramTeleUserID = teleUserID 247 | return getBaseUserID.returnBaseUserID, getBaseUserID.returnErr 248 | }) 249 | defer patches.Reset() 250 | 251 | // mock billService.CreateNewBill 252 | type CreateNewBill struct { 253 | called bool 254 | paramUserID uint 255 | paramAmount float64 256 | paramCategory string 257 | paramOpts []billDAL.CreateBillOptions 258 | returnBill *models.Bill 259 | returnErr error 260 | } 261 | createNewBill := &CreateNewBill{returnBill: &models.Bill{}, returnErr: nil} 262 | patches = gomonkey.ApplyMethod(reflect.TypeOf(suite.billService), "CreateNewBill", 263 | func(_ *bill.Service, userID uint, amount float64, category string, opts ...billDAL.CreateBillOptions) (*models.Bill, error) { 264 | createNewBill.called = true 265 | createNewBill.paramUserID = userID 266 | createNewBill.paramAmount = amount 267 | createNewBill.paramCategory = category 268 | createNewBill.paramOpts = opts 269 | return createNewBill.returnBill, createNewBill.returnErr 270 | }) 271 | defer patches.Reset() 272 | 273 | ctx := telebotmock.NewMockContext(gomock.NewController(suite.T())) 274 | userState := &UserState{BillCategory: strings.Pointer("娱乐"), BillName: strings.Pointer("龙珠漫画")} 275 | ctx.EXPECT().Text().Return("23.14") 276 | 277 | ctx.EXPECT().Sender().Return(&telebot.User{ID: 7}) 278 | defer func() { suite.True(getBaseUserID.called) }() 279 | defer func() { suite.Equal(int64(7), getBaseUserID.paramTeleUserID) }() 280 | 281 | defer func() { suite.True(createNewBill.called) }() 282 | defer func() { suite.Equal(getBaseUserID.returnBaseUserID, createNewBill.paramUserID) }() 283 | defer func() { suite.Equal(-23.14, createNewBill.paramAmount) }() 284 | defer func() { suite.Equal("娱乐", createNewBill.paramCategory) }() 285 | defer func() { suite.Len(createNewBill.paramOpts, 1) }() 286 | defer func() { suite.Equal("龙珠漫画", *createNewBill.paramOpts[0].Name) }() 287 | 288 | suite.userStateManager.EXPECT().ClearUserState(int64(7)).Return(nil).Times(1) 289 | ctx.EXPECT().Send(&NewBillSender{createNewBill.returnBill}) 290 | 291 | err := suite.hub.OnCreatingBill(ctx, userState) 292 | suite.NoError(err) 293 | } 294 | 295 | func (suite *HandlersHubTestSuite) TestHandleStartCommandIfNilChat() { 296 | tearDown := suite.SetupTest(suite.T()) 297 | defer tearDown() 298 | 299 | ctx := telebotmock.NewMockContext(gomock.NewController(suite.T())) 300 | ctx.EXPECT().Chat().Return(nil) 301 | ctx.EXPECT().Sender().Times(0) 302 | 303 | err := suite.hub.HandleStartCommand(ctx) 304 | suite.ErrorContains(err, "nil chat of context") 305 | } 306 | 307 | func TestHandlersHub(t *testing.T) { 308 | suite.Run(t, new(HandlersHubTestSuite)) 309 | } 310 | 311 | func TestParseBill(t *testing.T) { 312 | category, name := ParseBill("饮食") 313 | assert.Equal(t, "饮食", category) 314 | assert.Nil(t, name) 315 | 316 | category, name = ParseBill("娱乐 欧卡2") 317 | assert.Equal(t, "娱乐", category) 318 | assert.NotNil(t, name) 319 | assert.Equal(t, "欧卡2", *name) 320 | 321 | category, name = ParseBill("娱乐 欧卡2 后面的字符串也当作 name 的一部分") 322 | assert.Equal(t, "娱乐", category) 323 | assert.NotNil(t, name) 324 | assert.Equal(t, "欧卡2 后面的字符串也当作 name 的一部分", *name) 325 | } 326 | 327 | func TestParseAmount(t *testing.T) { 328 | testCases := map[string]float64{ 329 | "+1.23": 1.23, 330 | "1.23": -1.23, 331 | ".2": -0.2, 332 | } 333 | for text, expectedAmount := range testCases { 334 | amount, err := parseAmount(text) 335 | assert.NoError(t, err) 336 | assert.Equal(t, expectedAmount, amount) 337 | } 338 | 339 | badCases := []string{"", "+-1", "abc", "++2"} 340 | for _, text := range badCases { 341 | _, err := parseAmount(text) 342 | assert.Error(t, err) 343 | } 344 | } 345 | 346 | func TestGetDayRange(t *testing.T) { 347 | testTime := time.Date(2011, 1, 2, 9, 5, 7, 1234, time.UTC) 348 | begin, end := getDayRange(testTime) 349 | assert.Equal(t, time.Date(2011, 1, 2, 0, 0, 0, 0, time.UTC), begin) 350 | assert.Equal(t, time.Date(2011, 1, 3, 0, 0, 0, 0, time.UTC), end) 351 | 352 | testTime = time.Date(2011, 2, 28, 0, 0, 0, 0, time.UTC) 353 | begin, end = getDayRange(testTime) 354 | assert.Equal(t, time.Date(2011, 2, 28, 0, 0, 0, 0, time.UTC), begin) 355 | assert.Equal(t, time.Date(2011, 3, 1, 0, 0, 0, 0, time.UTC), end) 356 | } 357 | -------------------------------------------------------------------------------- /telebot/inlines.go: -------------------------------------------------------------------------------- 1 | package telebot 2 | 3 | import ( 4 | "encoding/json" 5 | "gopkg.in/telebot.v3" 6 | "time" 7 | ) 8 | 9 | var ( 10 | prevDayBillBtnTmpl = telebot.Btn{Text: "<️", Unique: "dayBillSelectorBtn"} 11 | nextDayBillBtnTmpl = telebot.Btn{Text: ">️", Unique: "dayBillSelectorBtn"} 12 | cancelBillBtnTmpl = telebot.Btn{Text: "撤销", Unique: "cancelBillBtn"} 13 | prevMonthBtnTmpl = telebot.Btn{Text: "<", Unique: "monthBillSelectorBtn"} 14 | nextMonthBtnTmpl = telebot.Btn{Text: ">", Unique: "monthBillSelectorBtn"} 15 | ) 16 | 17 | type DayBillBtnData struct { 18 | Year, Month, Day int 19 | } 20 | 21 | // PrevDayBillBtn 根据传入的年月日,生成用于将账单切换到前一天的按钮 22 | func PrevDayBillBtn(year, month, day int) telebot.Btn { 23 | date := time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.Local) 24 | prevDate := date.Add(-24 * time.Hour) 25 | data := &DayBillBtnData{prevDate.Year(), int(prevDate.Month()), prevDate.Day()} 26 | dataRaw, _ := json.Marshal(data) 27 | return telebot.Btn{ 28 | Text: prevDayBillBtnTmpl.Text, 29 | Unique: prevDayBillBtnTmpl.Unique, 30 | Data: string(dataRaw), 31 | } 32 | } 33 | 34 | // NextDayBillBtn 根据传入的年月日,生成用于将账单切换到后一天的按钮 35 | func NextDayBillBtn(year, month, day int) telebot.Btn { 36 | date := time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.Local) 37 | nextDate := date.Add(24 * time.Hour) 38 | data := &DayBillBtnData{nextDate.Year(), int(nextDate.Month()), nextDate.Day()} 39 | dataRaw, _ := json.Marshal(data) 40 | return telebot.Btn{ 41 | Text: nextDayBillBtnTmpl.Text, 42 | Unique: nextDayBillBtnTmpl.Unique, 43 | Data: string(dataRaw), 44 | } 45 | } 46 | 47 | type MonthBillBtnData struct { 48 | Year, Month int 49 | } 50 | 51 | func PrevMonthBillBtn(year, month int) telebot.Btn { 52 | date := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.Local) 53 | prevMonthDate := date.AddDate(0, -1, 0) 54 | data := &MonthBillBtnData{Year: prevMonthDate.Year(), Month: int(prevMonthDate.Month())} 55 | dataRaw, _ := json.Marshal(data) 56 | return telebot.Btn{ 57 | Text: prevMonthBtnTmpl.Text, 58 | Unique: prevMonthBtnTmpl.Unique, 59 | Data: string(dataRaw), 60 | } 61 | } 62 | 63 | func NextMonthBillBtn(year, month int) telebot.Btn { 64 | date := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.Local) 65 | nextMonthDate := date.AddDate(0, 1, 0) 66 | data := &MonthBillBtnData{Year: nextMonthDate.Year(), Month: int(nextMonthDate.Month())} 67 | dataRaw, _ := json.Marshal(data) 68 | return telebot.Btn{ 69 | Text: nextMonthBtnTmpl.Text, 70 | Unique: nextMonthBtnTmpl.Unique, 71 | Data: string(dataRaw), 72 | } 73 | } 74 | 75 | type CancelBillData struct { 76 | BillID uint 77 | } 78 | 79 | // CancelBillBtn 构造用于取消某个订单的按钮 80 | func CancelBillBtn(billID uint) telebot.Btn { 81 | data := &CancelBillData{billID} 82 | dataRaw, _ := json.Marshal(data) 83 | return telebot.Btn{ 84 | Text: cancelBillBtnTmpl.Text, 85 | Unique: cancelBillBtnTmpl.Unique, 86 | Data: string(dataRaw), 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /telebot/senders.go: -------------------------------------------------------------------------------- 1 | package telebot 2 | 3 | import ( 4 | "github.com/orenoid/telegram-account-bot/models" 5 | "github.com/pkg/errors" 6 | "gopkg.in/telebot.v3" 7 | ) 8 | 9 | var _ telebot.Sendable = (*NewBillSender)(nil) 10 | var _ telebot.Sendable = (*DateBillsSender)(nil) 11 | 12 | type NewBillSender struct { 13 | bill *models.Bill 14 | } 15 | 16 | func (sender *NewBillSender) Send(bot *telebot.Bot, recipient telebot.Recipient, _ *telebot.SendOptions) (*telebot.Message, error) { 17 | if sender.bill == nil { 18 | return nil, errors.New("bill should be nil") 19 | } 20 | template := &BillCreatedTemplate{sender.bill} 21 | menu := &telebot.ReplyMarkup{} 22 | menu.Inline(menu.Row(CancelBillBtn(sender.bill.ID))) 23 | return bot.Send(recipient, template.Render(), menu) 24 | } 25 | 26 | type DateBillsSender struct { 27 | Bills []*models.Bill 28 | Year, Month, Day int 29 | ShowYear bool 30 | } 31 | 32 | func (sender *DateBillsSender) Send(bot *telebot.Bot, recipient telebot.Recipient, _ *telebot.SendOptions) (*telebot.Message, error) { 33 | return bot.Send(recipient, sender.Text(), sender.ReplyMarkup()) 34 | } 35 | 36 | func (sender *DateBillsSender) Text() string { 37 | titleTemplate := DateTitleTemplate{sender.Year, sender.Month, sender.Day, sender.ShowYear} 38 | billsTemplate := BillListTemplate{Bills: sender.Bills, MergeCategory: false} 39 | return titleTemplate.Render() + "\n\n" + billsTemplate.Render() 40 | } 41 | 42 | func (sender *DateBillsSender) ReplyMarkup() *telebot.ReplyMarkup { 43 | selector := &telebot.ReplyMarkup{ResizeKeyboard: true} 44 | selector.Inline(selector.Row( 45 | PrevDayBillBtn(sender.Year, sender.Month, sender.Day), 46 | NextDayBillBtn(sender.Year, sender.Month, sender.Day), 47 | )) 48 | return selector 49 | } 50 | 51 | type MonthBillsSender struct { 52 | Bills []*models.Bill 53 | Year, Month int 54 | } 55 | 56 | func (sender *MonthBillsSender) Send(bot *telebot.Bot, recipient telebot.Recipient, _ *telebot.SendOptions) (*telebot.Message, error) { 57 | return bot.Send(recipient, sender.Text(), sender.ReplyMarkup()) 58 | } 59 | 60 | func (sender *MonthBillsSender) Text() string { 61 | titleTemplate := MonthTitleTemplate{Year: sender.Year, Month: sender.Month} 62 | billsTemplate := BillListTemplate{Bills: sender.Bills, MergeCategory: true} 63 | return titleTemplate.Render() + "\n\n" + billsTemplate.Render() 64 | } 65 | 66 | func (sender *MonthBillsSender) ReplyMarkup() *telebot.ReplyMarkup { 67 | selector := &telebot.ReplyMarkup{ResizeKeyboard: true} 68 | selector.Inline(selector.Row( 69 | PrevMonthBillBtn(sender.Year, sender.Month), 70 | NextMonthBillBtn(sender.Year, sender.Month), 71 | )) 72 | return selector 73 | } 74 | -------------------------------------------------------------------------------- /telebot/senders_test.go: -------------------------------------------------------------------------------- 1 | package telebot 2 | 3 | import ( 4 | "github.com/agiledragon/gomonkey/v2" 5 | "github.com/orenoid/telegram-account-bot/models" 6 | "github.com/pkg/errors" 7 | "github.com/shopspring/decimal" 8 | "github.com/stretchr/testify/assert" 9 | "gopkg.in/telebot.v3" 10 | "reflect" 11 | "testing" 12 | ) 13 | 14 | func TestDateBillsSender_Send(t *testing.T) { 15 | bills := []*models.Bill{{Amount: decimal.NewFromFloat(-1), UserID: 1, Category: "杂项"}} 16 | sender := &DateBillsSender{bills, 2022, 12, 1, false} 17 | 18 | type Send struct { 19 | called bool 20 | paramTo telebot.Recipient 21 | paramWhat interface{} 22 | paramOpts []interface{} 23 | returnMessage *telebot.Message 24 | returnErr error 25 | } 26 | send := &Send{returnMessage: &telebot.Message{Text: "some special text"}, returnErr: errors.New("send error")} 27 | patches := gomonkey.ApplyMethod(reflect.TypeOf(&telebot.Bot{}), "Send", 28 | func(bot *telebot.Bot, to telebot.Recipient, what interface{}, opts ...interface{}) (*telebot.Message, error) { 29 | send.called = true 30 | send.paramTo = to 31 | send.paramWhat = what 32 | send.paramOpts = opts 33 | return send.returnMessage, send.returnErr 34 | }) 35 | defer patches.Reset() 36 | 37 | bot := &telebot.Bot{Me: &telebot.User{ID: 314}} 38 | recipient := &telebot.User{ID: 618} 39 | msg, err := sender.Send(bot, recipient, nil) 40 | titleTemplate := DateTitleTemplate{sender.Year, sender.Month, sender.Day, sender.ShowYear} 41 | billsTemplate := BillListTemplate{Bills: sender.Bills, MergeCategory: false} 42 | assert.Equal(t, titleTemplate.Render()+"\n\n"+billsTemplate.Render(), send.paramWhat) 43 | assert.Equal(t, recipient, send.paramTo) 44 | assert.Equal(t, send.returnErr, err) 45 | assert.Equal(t, send.returnMessage, msg) 46 | } 47 | -------------------------------------------------------------------------------- /telebot/telebot.go: -------------------------------------------------------------------------------- 1 | package telebot 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | "gopkg.in/telebot.v3" 6 | ) 7 | 8 | func NewBot(settings telebot.Settings, hub *HandlersHub) (*telebot.Bot, error) { 9 | bot, err := telebot.NewBot(settings) 10 | if err != nil { 11 | return &telebot.Bot{}, errors.WithStack(err) 12 | } 13 | 14 | RegisterHandlers(bot, hub) 15 | 16 | return bot, nil 17 | } 18 | 19 | func RegisterHandlers(bot *telebot.Bot, hub *HandlersHub) { 20 | // 基础命令 21 | bot.Handle("/help", hub.HandleHelpCommand) 22 | bot.Handle("/start", hub.HandleStartCommand) 23 | bot.Handle("/day", hub.HandleDayCommand) 24 | bot.Handle("/month", hub.HandleMonthCommand) 25 | bot.Handle("/cancel", hub.HandleCancelCommand) 26 | bot.Handle("/set_keyboard", hub.HandleSetKeyboardCommand) 27 | bot.Handle("/set_balance", hub.HandleSetBalanceCommand) 28 | bot.Handle("/balance", hub.HandleBalanceCommand) 29 | bot.Handle("/create_token", hub.HandleCreateTokenCommand) 30 | bot.Handle("/disable_all_tokens", hub.HandleDisableAllTokensCommand) 31 | bot.Handle(telebot.OnText, hub.HandleText) 32 | // 回调 33 | bot.Handle(&prevDayBillBtnTmpl, hub.HandleDayBillSelectionCallback) 34 | bot.Handle(&nextDayBillBtnTmpl, hub.HandleDayBillSelectionCallback) 35 | bot.Handle(&prevMonthBtnTmpl, hub.HandleMonthBillSelectionCallback) 36 | bot.Handle(&nextMonthBtnTmpl, hub.HandleMonthBillSelectionCallback) 37 | bot.Handle(&cancelBillBtnTmpl, hub.HandleCancelBillCallback) 38 | } 39 | -------------------------------------------------------------------------------- /telebot/telebot_test.go: -------------------------------------------------------------------------------- 1 | package telebot 2 | 3 | import ( 4 | "github.com/agiledragon/gomonkey/v2" 5 | "github.com/stretchr/testify/assert" 6 | "gopkg.in/telebot.v3" 7 | "math/rand" 8 | "reflect" 9 | "strconv" 10 | "testing" 11 | ) 12 | 13 | func TestNewBot(t *testing.T) { 14 | inputSettings := telebot.Settings{Token: strconv.Itoa(rand.Int()), Offline: true} 15 | inputHub := &HandlersHub{} 16 | 17 | var telebotNewBotPref telebot.Settings 18 | telebotNewBotReturnedBot := &telebot.Bot{Token: strconv.Itoa(rand.Int())} 19 | patches := gomonkey.ApplyFunc(telebot.NewBot, func(pref telebot.Settings) (*telebot.Bot, error) { 20 | telebotNewBotPref = pref 21 | return telebotNewBotReturnedBot, nil 22 | }) 23 | defer patches.Reset() 24 | 25 | var registerHandlersBot *telebot.Bot 26 | var registerHandlersHub *HandlersHub 27 | patches = gomonkey.ApplyFunc(RegisterHandlers, func(bot *telebot.Bot, hub *HandlersHub) { 28 | registerHandlersBot = bot 29 | registerHandlersHub = hub 30 | }) 31 | defer patches.Reset() 32 | 33 | newBot, err := NewBot(inputSettings, inputHub) 34 | assert.Equal(t, inputSettings, telebotNewBotPref) 35 | assert.Equal(t, telebotNewBotReturnedBot, registerHandlersBot) 36 | assert.True(t, inputHub == registerHandlersHub) 37 | assert.Equal(t, telebotNewBotReturnedBot, newBot) 38 | assert.NoError(t, err) 39 | } 40 | 41 | func TestRegisterHandlers(t *testing.T) { 42 | var bot *telebot.Bot 43 | hub := &HandlersHub{} 44 | realRegistered := map[interface{}]telebot.HandlerFunc{} 45 | patches := gomonkey.ApplyMethod(reflect.TypeOf(bot), "Handle", func(_ *telebot.Bot, endpoint interface{}, h telebot.HandlerFunc, m ...telebot.MiddlewareFunc) { 46 | realRegistered[endpoint] = h 47 | }) 48 | expectedRegistered := map[interface{}]telebot.HandlerFunc{ 49 | "/help": hub.HandleHelpCommand, 50 | "/start": hub.HandleStartCommand, 51 | "/day": hub.HandleDayCommand, 52 | "/month": hub.HandleMonthCommand, 53 | "/cancel": hub.HandleCancelCommand, 54 | "/set_keyboard": hub.HandleSetKeyboardCommand, 55 | "/set_balance": hub.HandleSetBalanceCommand, 56 | "/balance": hub.HandleBalanceCommand, 57 | "/create_token": hub.HandleCreateTokenCommand, 58 | telebot.OnText: hub.HandleText, 59 | &prevDayBillBtnTmpl: hub.HandleDayBillSelectionCallback, 60 | &nextDayBillBtnTmpl: hub.HandleDayBillSelectionCallback, 61 | &prevMonthBtnTmpl: hub.HandleMonthBillSelectionCallback, 62 | &nextMonthBtnTmpl: hub.HandleMonthBillSelectionCallback, 63 | &cancelBillBtnTmpl: hub.HandleCancelBillCallback, 64 | } 65 | defer patches.Reset() 66 | RegisterHandlers(bot, hub) 67 | 68 | for endpoint := range expectedRegistered { 69 | _, found := realRegistered[endpoint] 70 | assert.Truef(t, found, "expected endpoint: %v not registered", endpoint) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /telebot/templates.go: -------------------------------------------------------------------------------- 1 | package telebot 2 | 3 | import ( 4 | "fmt" 5 | "github.com/orenoid/telegram-account-bot/models" 6 | "github.com/shopspring/decimal" 7 | "sort" 8 | "strings" 9 | ) 10 | 11 | type Template interface { 12 | Render() string 13 | } 14 | 15 | type BillCreatedTemplate struct { 16 | bill *models.Bill 17 | } 18 | 19 | func (template *BillCreatedTemplate) Render() string { 20 | format := 21 | `%s:%s 元,类别:%s 22 | 点击 "撤销" 可撤回该账单并回滚余额` 23 | var inOrOut, amountStr string 24 | if template.bill.Amount.LessThan(decimal.NewFromFloat(0)) { 25 | inOrOut, amountStr = "支出", template.bill.Amount.Abs().String() 26 | } else { 27 | inOrOut, amountStr = "收入", template.bill.Amount.String() 28 | } 29 | return fmt.Sprintf(format, inOrOut, amountStr, template.bill.Category) 30 | } 31 | 32 | type MonthTitleTemplate struct { 33 | Year int 34 | Month int 35 | } 36 | 37 | func (template *MonthTitleTemplate) Render() string { 38 | return fmt.Sprintf("%d年%d月收支统计", template.Year, template.Month) 39 | } 40 | 41 | type DateTitleTemplate struct { 42 | Year int 43 | Month int 44 | Day int 45 | ShowYear bool 46 | } 47 | 48 | func (template *DateTitleTemplate) Render() string { 49 | if template.ShowYear { 50 | return fmt.Sprintf("%d年%d月%d日收支统计", template.Year, template.Month, template.Day) 51 | } else { 52 | return fmt.Sprintf("%d月%d日收支统计", template.Month, template.Day) 53 | 54 | } 55 | } 56 | 57 | type BillListTemplate struct { 58 | Bills []*models.Bill 59 | MergeCategory bool // 对相同类别的账单求总和后展示 60 | ShowSub bool // 显示净增值 61 | } 62 | 63 | func (template *BillListTemplate) Render() string { 64 | if len(template.Bills) == 0 { 65 | return "暂无收支记录" 66 | } 67 | 68 | result := &strings.Builder{} 69 | expendSection, expendSum := template.expendSection() 70 | if len(expendSection) > 0 { 71 | result.Write([]byte(expendSection)) 72 | } 73 | incomeSection, incomeSum := template.incomeSection() 74 | if len(expendSection) > 0 && len(incomeSection) > 0 { 75 | // 支出与收入记录之间多间隔一行 76 | result.Write([]byte("\n\n")) 77 | } 78 | if len(incomeSection) > 0 { 79 | result.Write([]byte(incomeSection)) 80 | } 81 | if template.ShowSub { 82 | sub := incomeSum.Add(expendSum) 83 | result.Write([]byte(fmt.Sprintf("\n\n净增:%s 元", sub.String()))) 84 | } 85 | 86 | return result.String() 87 | } 88 | 89 | // expendSection 支出记录 90 | func (template *BillListTemplate) expendSection() (sectionText string, sum decimal.Decimal) { 91 | expendSum := decimal.NewFromFloat(0) 92 | categoryMapping := map[string]decimal.Decimal{} 93 | var expendBills []*models.Bill 94 | 95 | // 筛选支出账单 96 | for _, bill := range template.Bills { 97 | if bill.Amount.LessThan(decimal.NewFromFloat(0)) { 98 | expendBills = append(expendBills, bill) 99 | expendSum = expendSum.Add(bill.Amount) 100 | categoryMapping[bill.Category] = categoryMapping[bill.Category].Add(bill.Amount) 101 | } 102 | } 103 | if len(expendBills) == 0 { 104 | return "", decimal.Decimal{} 105 | } 106 | 107 | result := &strings.Builder{} 108 | result.Write([]byte(fmt.Sprintf("合计支出:%s 元\n\n", expendSum.Abs().String()))) 109 | if !template.MergeCategory { 110 | // 展示所有账单时,按创建时间排序 111 | sort.Sort(billsSortByCreateTime(expendBills)) 112 | count := 0 113 | for _, bill := range expendBills { 114 | count++ 115 | result.Write([]byte(fmt.Sprintf(" - %s:%s 元", bill.Category, bill.Amount.Abs().String()))) 116 | if count < len(expendBills) { 117 | result.Write([]byte("\n")) 118 | } 119 | } 120 | } else { 121 | count := 0 122 | // 按类别支出总和由大到小排序 123 | for _, category := range template.getSortedCategories(categoryMapping) { 124 | amount := categoryMapping[category] 125 | count++ 126 | result.Write([]byte(fmt.Sprintf(" - %s:%s 元", category, amount.Abs().String()))) 127 | if count < len(categoryMapping) { 128 | result.Write([]byte("\n")) 129 | } 130 | } 131 | } 132 | return result.String(), expendSum 133 | } 134 | 135 | // incomeSection 收入记录 136 | func (template *BillListTemplate) incomeSection() (sectionText string, sum decimal.Decimal) { 137 | var incomeBills []*models.Bill 138 | incomeSum := decimal.NewFromFloat(0) 139 | categoryMapping := map[string]decimal.Decimal{} 140 | 141 | // 筛选收入账单 142 | for _, bill := range template.Bills { 143 | if !bill.Amount.LessThanOrEqual(decimal.NewFromFloat(0)) { 144 | incomeBills = append(incomeBills, bill) 145 | incomeSum = incomeSum.Add(bill.Amount) 146 | categoryMapping[bill.Category] = categoryMapping[bill.Category].Add(bill.Amount) 147 | } 148 | } 149 | if len(incomeBills) == 0 { 150 | return "", decimal.Decimal{} 151 | } 152 | 153 | result := &strings.Builder{} 154 | result.Write([]byte(fmt.Sprintf("合计收入:%s 元\n\n", incomeSum.String()))) 155 | if !template.MergeCategory { 156 | // 展示所有账单时,按创建时间排序 157 | sort.Sort(billsSortByCreateTime(incomeBills)) 158 | count := 0 159 | for _, bill := range incomeBills { 160 | count++ 161 | result.Write([]byte(fmt.Sprintf(" - %s:%s 元", bill.Category, bill.Amount.String()))) 162 | if count < len(incomeBills) { 163 | result.Write([]byte("\n")) 164 | } 165 | } 166 | } else { 167 | count := 0 168 | categories := template.getSortedCategories(categoryMapping) 169 | // 按类别收入总和由大到小排序 170 | for i := len(categories) - 1; i >= 0; i-- { 171 | category := categories[i] 172 | amount := categoryMapping[category] 173 | count++ 174 | result.Write([]byte(fmt.Sprintf(" - %s:%s 元", category, amount.Abs().String()))) 175 | if count < len(categoryMapping) { 176 | result.Write([]byte("\n")) 177 | } 178 | } 179 | } 180 | return result.String(), incomeSum 181 | } 182 | 183 | func (template *BillListTemplate) getSortedCategories(categoriesMapping map[string]decimal.Decimal) []string { 184 | var sortHelper [][2]interface{} 185 | for cate, amount := range categoriesMapping { 186 | sortHelper = append(sortHelper, [2]interface{}{cate, amount}) 187 | } 188 | sort.Sort(sortByAmount(sortHelper)) 189 | var categories []string 190 | for _, item := range sortHelper { 191 | categories = append(categories, item[0].(string)) 192 | } 193 | return categories 194 | } 195 | 196 | type sortByAmount [][2]interface{} 197 | 198 | func (items sortByAmount) Len() int { 199 | return len(items) 200 | } 201 | 202 | func (items sortByAmount) Less(i, j int) bool { 203 | return items[i][1].(decimal.Decimal).LessThan(items[j][1].(decimal.Decimal)) 204 | } 205 | 206 | func (items sortByAmount) Swap(i, j int) { 207 | items[i], items[j] = items[j], items[i] 208 | } 209 | 210 | type billsSortByCreateTime []*models.Bill 211 | 212 | func (bills billsSortByCreateTime) Len() int { 213 | return len(bills) 214 | } 215 | 216 | func (bills billsSortByCreateTime) Less(i, j int) bool { 217 | return bills[i].CreatedAt.Before(bills[j].CreatedAt) 218 | } 219 | 220 | func (bills billsSortByCreateTime) Swap(i, j int) { 221 | bills[i], bills[j] = bills[j], bills[i] 222 | } 223 | -------------------------------------------------------------------------------- /telebot/templates_test.go: -------------------------------------------------------------------------------- 1 | package telebot 2 | 3 | import ( 4 | "github.com/orenoid/telegram-account-bot/models" 5 | "github.com/shopspring/decimal" 6 | "github.com/stretchr/testify/assert" 7 | "gorm.io/gorm" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func TestBillCreatedTemplate_Render(t *testing.T) { 13 | template := &BillCreatedTemplate{&models.Bill{Category: "购物", Amount: decimal.NewFromFloat(-23)}} 14 | assert.Equal(t, "支出:23 元,类别:购物\n点击 \"撤销\" 可撤回该账单并回滚余额", template.Render()) 15 | 16 | template = &BillCreatedTemplate{&models.Bill{Category: "咸鱼", Amount: decimal.NewFromFloat(23.33)}} 17 | assert.Equal(t, "收入:23.33 元,类别:咸鱼\n点击 \"撤销\" 可撤回该账单并回滚余额", template.Render()) 18 | } 19 | 20 | func TestBillListTemplate_Render(t *testing.T) { 21 | // 测试合并统计支出 22 | bills := []*models.Bill{ 23 | {Category: "饮食", Amount: decimal.NewFromFloat(-10)}, 24 | {Category: "饮食", Amount: decimal.NewFromFloat(-20.22)}, 25 | {Category: "出行", Amount: decimal.NewFromFloat(-10)}, 26 | } 27 | 28 | template := BillListTemplate{Bills: bills, MergeCategory: true} 29 | assert.Equal(t, `合计支出:40.22 元 30 | 31 | - 饮食:30.22 元 32 | - 出行:10 元`, template.Render()) 33 | 34 | // 测试合并统计收入 35 | bills = []*models.Bill{ 36 | {Category: "工资", Amount: decimal.NewFromFloat(10000)}, 37 | {Category: "咸鱼", Amount: decimal.NewFromFloat(100.01)}, 38 | {Category: "咸鱼", Amount: decimal.NewFromFloat(50.01)}, 39 | } 40 | template = BillListTemplate{Bills: bills, MergeCategory: true} 41 | assert.Equal(t, `合计收入:10150.02 元 42 | 43 | - 工资:10000 元 44 | - 咸鱼:150.02 元`, template.Render()) 45 | 46 | // 测试同时有支出和收入的字符串拼接行为 47 | bills = []*models.Bill{ 48 | {Category: "饮食", Amount: decimal.NewFromFloat(-10)}, 49 | {Category: "饮食", Amount: decimal.NewFromFloat(-20.22)}, 50 | {Category: "出行", Amount: decimal.NewFromFloat(-10)}, 51 | {Category: "工资", Amount: decimal.NewFromFloat(10000)}, 52 | {Category: "咸鱼", Amount: decimal.NewFromFloat(100.01)}, 53 | {Category: "咸鱼", Amount: decimal.NewFromFloat(50.01)}, 54 | } 55 | template = BillListTemplate{Bills: bills, MergeCategory: true} 56 | assert.Equal(t, 57 | `合计支出:40.22 元 58 | 59 | - 饮食:30.22 元 60 | - 出行:10 元 61 | 62 | 合计收入:10150.02 元 63 | 64 | - 工资:10000 元 65 | - 咸鱼:150.02 元`, template.Render()) 66 | 67 | template = BillListTemplate{Bills: nil, ShowSub: true} 68 | assert.Equal(t, "暂无收支记录", template.Render()) 69 | 70 | // 测试订单单独展示行为(按创建时间排序) 71 | now := time.Now() 72 | bills = []*models.Bill{ 73 | {Category: "饮食", Amount: decimal.NewFromFloat(-10), Model: gorm.Model{CreatedAt: now}}, 74 | {Category: "饮食", Amount: decimal.NewFromFloat(-20.22), Model: gorm.Model{CreatedAt: now.Add(2 * time.Second)}}, 75 | {Category: "出行", Amount: decimal.NewFromFloat(-10), Model: gorm.Model{CreatedAt: now.Add(-2 * time.Second)}}, 76 | 77 | {Category: "工资", Amount: decimal.NewFromFloat(10000), Model: gorm.Model{CreatedAt: now}}, 78 | {Category: "咸鱼", Amount: decimal.NewFromFloat(100.01), Model: gorm.Model{CreatedAt: now.Add(-2 * time.Second)}}, 79 | } 80 | template = BillListTemplate{Bills: bills, MergeCategory: false} 81 | assert.Equal(t, 82 | `合计支出:40.22 元 83 | 84 | - 出行:10 元 85 | - 饮食:10 元 86 | - 饮食:20.22 元 87 | 88 | 合计收入:10100.01 元 89 | 90 | - 咸鱼:100.01 元 91 | - 工资:10000 元`, template.Render()) 92 | 93 | bills = []*models.Bill{ 94 | {Category: "饮食", Amount: decimal.NewFromFloat(-0.01)}, 95 | {Category: "工资", Amount: decimal.NewFromFloat(10000)}, 96 | } 97 | template = BillListTemplate{Bills: bills, MergeCategory: false, ShowSub: true} 98 | assert.Equal(t, 99 | `合计支出:0.01 元 100 | 101 | - 饮食:0.01 元 102 | 103 | 合计收入:10000 元 104 | 105 | - 工资:10000 元 106 | 107 | 净增:9999.99 元`, template.Render()) 108 | 109 | bills = []*models.Bill{ 110 | {Category: "饮食", Amount: decimal.NewFromFloat(-0.01)}, 111 | } 112 | template = BillListTemplate{Bills: bills, MergeCategory: false, ShowSub: true} 113 | assert.Equal(t, 114 | `合计支出:0.01 元 115 | 116 | - 饮食:0.01 元 117 | 118 | 净增:-0.01 元`, template.Render()) 119 | 120 | bills = []*models.Bill{ 121 | {Category: "工资", Amount: decimal.NewFromFloat(3000)}, 122 | } 123 | template = BillListTemplate{Bills: bills, MergeCategory: false, ShowSub: true} 124 | assert.Equal(t, 125 | `合计收入:3000 元 126 | 127 | - 工资:3000 元 128 | 129 | 净增:3000 元`, template.Render()) 130 | } 131 | 132 | func TestMonthTitleTemplate_Render(t *testing.T) { 133 | template := &MonthTitleTemplate{Year: 2022, Month: 1} 134 | assert.Equal(t, "2022年1月收支统计", template.Render()) 135 | } 136 | 137 | func TestDateTitleTemplate_Render(t *testing.T) { 138 | template := &DateTitleTemplate{2022, 12, 25, true} 139 | assert.Equal(t, "2022年12月25日收支统计", template.Render()) 140 | 141 | template = &DateTitleTemplate{2022, 12, 25, false} 142 | assert.Equal(t, "12月25日收支统计", template.Render()) 143 | } 144 | -------------------------------------------------------------------------------- /telebot/user_state.go: -------------------------------------------------------------------------------- 1 | package telebot 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | "sync" 6 | ) 7 | 8 | type UserStateType string 9 | 10 | const ( 11 | Empty UserStateType = "empty" 12 | CreatingBill UserStateType = "creatingBill" 13 | SettingKeyboard UserStateType = "settingKeyboard" 14 | ) 15 | 16 | type UserState struct { 17 | Type UserStateType `json:"type"` 18 | 19 | // Type 为 CreatingBill 时不为空 20 | BillCategory *string `json:"bill_category"` 21 | // Type 为 CreatingBill 时可能有值,若为空则代表未设置名称 22 | BillName *string `json:"bill_name"` 23 | } 24 | 25 | type UserStateManager interface { 26 | GetUserState(userID int64) (state *UserState, err error) 27 | SetUserState(userID int64, state *UserState) error 28 | ClearUserState(userID int64) error 29 | } 30 | 31 | type InMemoryUserStateManager struct { 32 | cache *sync.Map 33 | } 34 | 35 | func (manager *InMemoryUserStateManager) GetUserState(userID int64) (state *UserState, err error) { 36 | value, found := manager.cache.Load(userID) 37 | if !found { 38 | return &UserState{Type: Empty}, nil 39 | } 40 | state, ok := value.(*UserState) 41 | if !ok { 42 | return nil, errors.Errorf("invalid type of state value: %T", value) 43 | } 44 | return state, nil 45 | } 46 | 47 | func (manager *InMemoryUserStateManager) SetUserState(userID int64, state *UserState) error { 48 | manager.cache.Store(userID, state) 49 | return nil 50 | } 51 | 52 | func (manager *InMemoryUserStateManager) ClearUserState(userID int64) error { 53 | manager.cache.Delete(userID) 54 | return nil 55 | } 56 | 57 | func NewInMemoryUserStateManager() *InMemoryUserStateManager { 58 | manager := &InMemoryUserStateManager{cache: &sync.Map{}} 59 | return manager 60 | } 61 | -------------------------------------------------------------------------------- /telebot/user_state_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: telebot/user_state.go 3 | 4 | // Package telebot is a generated GoMock package. 5 | package telebot 6 | 7 | import ( 8 | reflect "reflect" 9 | 10 | gomock "github.com/golang/mock/gomock" 11 | ) 12 | 13 | // MockUserStateManager is a mock of UserStateManager interface. 14 | type MockUserStateManager struct { 15 | ctrl *gomock.Controller 16 | recorder *MockUserStateManagerMockRecorder 17 | } 18 | 19 | // MockUserStateManagerMockRecorder is the mock recorder for MockUserStateManager. 20 | type MockUserStateManagerMockRecorder struct { 21 | mock *MockUserStateManager 22 | } 23 | 24 | // NewMockUserStateManager creates a new mock instance. 25 | func NewMockUserStateManager(ctrl *gomock.Controller) *MockUserStateManager { 26 | mock := &MockUserStateManager{ctrl: ctrl} 27 | mock.recorder = &MockUserStateManagerMockRecorder{mock} 28 | return mock 29 | } 30 | 31 | // EXPECT returns an object that allows the caller to indicate expected use. 32 | func (m *MockUserStateManager) EXPECT() *MockUserStateManagerMockRecorder { 33 | return m.recorder 34 | } 35 | 36 | // ClearUserState mocks base method. 37 | func (m *MockUserStateManager) ClearUserState(userID int64) error { 38 | m.ctrl.T.Helper() 39 | ret := m.ctrl.Call(m, "ClearUserState", userID) 40 | ret0, _ := ret[0].(error) 41 | return ret0 42 | } 43 | 44 | // ClearUserState indicates an expected call of ClearUserState. 45 | func (mr *MockUserStateManagerMockRecorder) ClearUserState(userID interface{}) *gomock.Call { 46 | mr.mock.ctrl.T.Helper() 47 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClearUserState", reflect.TypeOf((*MockUserStateManager)(nil).ClearUserState), userID) 48 | } 49 | 50 | // GetUserState mocks base method. 51 | func (m *MockUserStateManager) GetUserState(userID int64) (*UserState, error) { 52 | m.ctrl.T.Helper() 53 | ret := m.ctrl.Call(m, "GetUserState", userID) 54 | ret0, _ := ret[0].(*UserState) 55 | ret1, _ := ret[1].(error) 56 | return ret0, ret1 57 | } 58 | 59 | // GetUserState indicates an expected call of GetUserState. 60 | func (mr *MockUserStateManagerMockRecorder) GetUserState(userID interface{}) *gomock.Call { 61 | mr.mock.ctrl.T.Helper() 62 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserState", reflect.TypeOf((*MockUserStateManager)(nil).GetUserState), userID) 63 | } 64 | 65 | // SetUserState mocks base method. 66 | func (m *MockUserStateManager) SetUserState(userID int64, state *UserState) error { 67 | m.ctrl.T.Helper() 68 | ret := m.ctrl.Call(m, "SetUserState", userID, state) 69 | ret0, _ := ret[0].(error) 70 | return ret0 71 | } 72 | 73 | // SetUserState indicates an expected call of SetUserState. 74 | func (mr *MockUserStateManagerMockRecorder) SetUserState(userID, state interface{}) *gomock.Call { 75 | mr.mock.ctrl.T.Helper() 76 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetUserState", reflect.TypeOf((*MockUserStateManager)(nil).SetUserState), userID, state) 77 | } 78 | -------------------------------------------------------------------------------- /telebot/user_state_test.go: -------------------------------------------------------------------------------- 1 | package telebot 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestNewInMemoryUserStateManager(t *testing.T) { 9 | m := NewInMemoryUserStateManager() 10 | var _ UserStateManager = m 11 | assert.IsType(t, &InMemoryUserStateManager{}, m) 12 | } 13 | 14 | func TestInMemoryUserStateManager_ClearUserState(t *testing.T) { 15 | m := NewInMemoryUserStateManager() 16 | 17 | m.cache.Store(int64(7), nil) 18 | m.cache.Store(int64(8), nil) 19 | 20 | err := m.ClearUserState(7) 21 | assert.NoError(t, err) 22 | _, found := m.cache.Load(int64(7)) 23 | assert.False(t, found) 24 | _, found = m.cache.Load(int64(8)) 25 | assert.True(t, found) 26 | } 27 | 28 | func TestInMemoryUserStateManager_GetUserState(t *testing.T) { 29 | m := NewInMemoryUserStateManager() 30 | currState := &UserState{Type: UserStateType("currState")} 31 | m.cache.Store(int64(42), currState) 32 | m.cache.Store(int64(44), "not *UserState") 33 | 34 | state, err := m.GetUserState(42) 35 | assert.Equal(t, currState, state) 36 | assert.NoError(t, err) 37 | assert.True(t, currState == state) 38 | 39 | state, err = m.GetUserState(43) 40 | assert.Equal(t, Empty, state.Type) 41 | 42 | state, err = m.GetUserState(44) 43 | assert.ErrorContains(t, err, "invalid type of state value: string") 44 | } 45 | 46 | func TestInMemoryUserStateManager_SetUserState(t *testing.T) { 47 | testCases := map[int64]*UserState{ 48 | 83: {Type: CreatingBill}, 49 | 84: {Type: ""}, 50 | } 51 | m := NewInMemoryUserStateManager() 52 | for userID, state := range testCases { 53 | err := m.SetUserState(userID, state) 54 | assert.NoError(t, err) 55 | } 56 | cacheLen := 0 57 | m.cache.Range(func(key, value interface{}) bool { 58 | userID, ok := key.(int64) 59 | assert.True(t, ok) 60 | expectedState, found := testCases[userID] 61 | assert.True(t, found) 62 | state, ok := value.(*UserState) 63 | assert.True(t, expectedState == state) 64 | cacheLen++ 65 | return true 66 | }) 67 | assert.Equal(t, len(testCases), cacheLen) 68 | } 69 | -------------------------------------------------------------------------------- /utils/strings/strings.go: -------------------------------------------------------------------------------- 1 | package strings 2 | 3 | func Pointer(s string) *string { 4 | return &s 5 | } 6 | -------------------------------------------------------------------------------- /utils/strings/token.go: -------------------------------------------------------------------------------- 1 | package strings 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/base64" 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | func GenerateToken() (string, error) { 10 | // 定义生成的 token 长度 11 | const tokenLength = 32 12 | 13 | // 创建一个字节数组来存储随机字节 14 | b := make([]byte, tokenLength) 15 | 16 | // 使用 crypto/rand 生成加密安全的随机字节 17 | _, err := rand.Read(b) 18 | if err != nil { 19 | return "", errors.Wrap(err, "failed to generate token") 20 | } 21 | 22 | // 将字节数组编码为 base64 字符串 23 | token := base64.URLEncoding.EncodeToString(b) 24 | 25 | return token, nil 26 | } 27 | --------------------------------------------------------------------------------