commit dac9e95d12528dff2ec7772feedd252e31bb83f1 Author: Alex Savin Date: Mon Feb 3 17:49:29 2025 -0500 alpha version diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..5a51d6f --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,18 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Package", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}", + "args": [ + "-o", "output.log" + ] + } + ] +} \ No newline at end of file diff --git a/app/app.go b/app/app.go new file mode 100644 index 0000000..e37efd5 --- /dev/null +++ b/app/app.go @@ -0,0 +1,493 @@ +package app + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "log/slog" + "runtime" + "strings" + "sync/atomic" + "time" + + "github.com/uptrace/bun" + tele "gopkg.in/telebot.v4" + + "github.com/alex-savin/go-receipt-tracker/bus" + "github.com/alex-savin/go-receipt-tracker/config" + "github.com/alex-savin/go-receipt-tracker/listeners" + "github.com/alex-savin/go-receipt-tracker/models" + "github.com/alex-savin/go-receipt-tracker/system" + "github.com/alex-savin/go-receipt-tracker/workers" +) + +const ( + AppVersion = "1.0.0" // the current application version. + AppName = "go-receipt-tracker" // the short app name +) + +var ( + ErrWorkerIDExists = errors.New("worker id already exists") + ErrListenerIDExists = errors.New("listener id already exists") +) + +type App struct { + Options *config.Config // configurable server options + Workers *workers.Workers // worker are network interfaces which listen for new connections + Listeners *listeners.Listeners // listeners are network interfaces which listen for new connections + Info *system.Info // values about the server commonly known as $SYS topics + Log *slog.Logger // minimal no-alloc logger + db *bun.DB // + subscriptions map[string]chan bus.Event // + bus *bus.Bus // + loop *loop // loop contains tickers for the system event loop + cancel context.CancelFunc // + hooks *Hooks // hooks contains hooks for extra functionality such as auth and persistent storage + // done chan bool // indicate that the server is ending +} + +// loop contains interval tickers for the system events loop. +type loop struct { + sysInfo *time.Ticker // interval ticker for sending updating $SYS topics +} +type Info struct { + AppVersion string `json:"version"` // the current version of the server + Started int64 `json:"started"` // the time the server started in unix seconds + MemoryAlloc int64 `json:"memory_alloc"` // memory currently allocated + Threads int64 `json:"threads"` // number of active goroutines, named as threads for platform ambiguity + Time int64 `json:"time"` // current time on the server + Uptime int64 `json:"uptime"` // the number of seconds the server has been online +} + +// EstablishConnection establishes a new client when a listener accepts a new connection. +// func (a *App) EstablishConnection(listener string, c net.Conn) error { +// cl := a.NewClient(c, listener, "", false) +// return a.attachClient(cl, listener) +// } + +// New returns a new instance of mochi mqtt broker. Optional parameters +// can be specified to override some default settings (see Options). +func New(opts *config.Config, b *bus.Bus, db *bun.DB, logger *slog.Logger) *App { + if opts == nil { + slog.Error("empty config") + } + + var m runtime.MemStats + runtime.ReadMemStats(&m) + a := &App{ + Options: opts, + Workers: workers.New(), + Listeners: listeners.New(), + Info: &system.Info{ + Version: AppVersion, + Started: time.Now().Unix(), + MemoryAlloc: int64(m.HeapInuse), + Threads: int64(runtime.NumGoroutine()), + // Time: time.Now().Unix(), + // Uptime: time.Now().Unix() - atomic.LoadInt64(&a.Started), + }, + Log: logger, + db: db, + subscriptions: make(map[string]chan bus.Event), + bus: b, + hooks: &Hooks{ + Log: logger, + }, + loop: &loop{ + sysInfo: time.NewTicker(time.Second * time.Duration(1)), + }, + // done: make(chan bool), + } + + return a +} + +// AddHook attaches a new Hook to the server. Ideally, this should be called +// before the server is started with s.Serve(). +func (a *App) AddHook(hook Hook, config any) error { + nl := a.Log.With("hook", hook.ID()) + hook.SetOpts(nl) + + a.Log.Info("added hook", "hook", hook.ID()) + return a.hooks.Add(hook, config) +} + +// AddListener adds a new network listener to the server, for receiving incoming client connections. +func (a *App) AddWorker(w workers.Worker) error { + if _, ok := a.Workers.Get(w.ID()); ok { + return ErrWorkerIDExists + } + + nl := a.Log.With(slog.String("worker", w.ID())) + err := w.Init(nl) + if err != nil { + return err + } + + a.Workers.Add(w) + + a.Log.Info("attached worker", "id", w.ID(), "type", w.Type()) + return nil +} + +// AddListener adds a new network listener to the server, for receiving incoming client connections. +func (a *App) AddListener(l listeners.Listener) error { + if _, ok := a.Listeners.Get(l.ID()); ok { + return ErrListenerIDExists + } + + nl := a.Log.With(slog.String("listener", l.ID())) + err := l.Init(nl) + if err != nil { + return err + } + + a.Listeners.Add(l) + + a.Log.Info("attached listener", "id", l.ID(), "protocol", l.Protocol(), "address", l.Address()) + return nil +} + +// Serve starts the event loops responsible for establishing client connections +// on all attached listeners, publishing the system topics, and starting all hooks. +func (a *App) Serve() error { + a.Log.Info(AppName+" starting", "version", AppVersion) + defer a.Log.Info(AppName + " is started") + + a.subscribe("processor:user_add") + a.subscribe("processor:user_edit") + a.subscribe("processor:user_delete") + a.subscribe("processor:image") + a.subscribe("processor:receipt_add") + a.subscribe("processor:items_list") + + ctx, cancel := context.WithCancel(context.Background()) + a.cancel = cancel + + go a.eventLoop(ctx) + + a.Workers.ServeAll() // start listening on all workers. + a.Listeners.ServeAll() // start listening on all listeners. + a.publishSysTopics() + a.hooks.OnStarted() + + return nil +} + +// Close attempts to gracefully shut down the server, all listeners, clients, and stores. +func (a *App) Close() error { + // close(a.done) + a.cancel() + a.Log.Info("gracefully stopping " + AppName) + a.Workers.CloseAll() + a.Listeners.CloseAll() + a.hooks.OnStopped() + a.hooks.Stop() + + a.Log.Info(AppName + " is stopped") + return nil +} + +func (a *App) subscribe(chn string) error { + s, err := a.bus.Subscribe(chn, AppName) + if err != nil { + a.Log.Error("couldn't subscribe to a channel", "channel", chn, "error", err.Error()) + return err + } + a.subscriptions[chn] = s + + return nil +} + +func (a *App) eventLoop(ctx context.Context) { + a.Log.Debug(AppName + " main communication event loop started") + defer a.Log.Debug(AppName + " main communication event loop halted") + + for { + select { + // case time.Time: + // a.publishSysTopics() + + case event := <-a.subscriptions["processor:user_add"]: + a.Log.Debug("service got a new message from a channel", "channel", event.ChannelName, "app", AppName) + msg := event.Payload.(*bus.Message) + + if models.UserExists(a.db, ctx, event.Payload.(*bus.Message).TbContext.Sender().ID) { + msg.Text = "Welcome back, " + event.Payload.(*bus.Message).TbContext.Sender().FirstName + " " + event.Payload.(*bus.Message).TbContext.Sender().LastName + msg.InlineKeyboard = &tele.ReplyMarkup{ + InlineKeyboard: [][]tele.InlineButton{ + { + {Text: "Edit Profile", Data: models.String(&models.TelegramBotCommand{Cmd: models.BtnCmd["user_edit"], ID: msg.TbContext.Sender().ID})}, + }, + { + {Text: "Create Group", Data: models.String(&models.TelegramBotCommand{Cmd: models.BtnCmd["group_add"], ID: msg.TbContext.Sender().ID})}, + }, + { + {Text: "List my Receipts", Data: models.String(&models.TelegramBotCommand{Cmd: models.BtnCmd["receipts_list"], ID: msg.TbContext.Sender().ID})}, + }, + }, + } + } else { + user := &models.User{ + Name: fmt.Sprintf("%s %s", event.Payload.(*bus.Message).TbContext.Sender().FirstName, event.Payload.(*bus.Message).TbContext.Sender().LastName), + TelegramID: event.Payload.(*bus.Message).TbContext.Sender().ID, + TelegramUsername: event.Payload.(*bus.Message).TbContext.Sender().Username, + } + + ctx := context.Background() + user.Add(a.db, ctx) + + msg.Text = config.Message_welcome + } + a.Log.Debug("publishing message to a channel", "channel", "telegram:send", "app", AppName) + err := a.bus.Publish("telegram:send", msg) + if err != nil { + a.Log.Error("couldn't publish to the channel", "channel", "telegram:send", "error", err.Error()) + } + + case event := <-a.subscriptions["processor:user_delete"]: + a.Log.Debug("service got a new message from a channel", "channel", event.ChannelName, "app", AppName) + msg := event.Payload.(*bus.Message) + + if models.UserExists(a.db, ctx, event.Payload.(*bus.Message).TbContext.Sender().ID) { + msg.Text = "Hey " + event.Payload.(*bus.Message).TbContext.Sender().FirstName + " it looks like that you would like to DELETE your account, please confirm it:\n" + msg.InlineKeyboard = &tele.ReplyMarkup{ + InlineKeyboard: [][]tele.InlineButton{ + { + {Text: "Yes, DELETE IT", Data: models.String(&models.TelegramBotCommand{Cmd: models.BtnCmd["user_delete"], ID: msg.TbContext.Sender().ID, RefCmd: models.BtnCmd["user_delete"], Extra: "1"})}, + {Text: "No, KEEP IT", Data: models.String(&models.TelegramBotCommand{Cmd: models.BtnCmd["user_edit"], ID: msg.TbContext.Sender().ID})}, + }, + }, + } + } else { + msg.Text = "The user does not exist" + } + + a.Log.Debug("publishing message to a channel", "channel", "telegram:user_delete", "app", AppName) + err := a.bus.Publish("telegram:user_delete", msg) + if err != nil { + a.Log.Error("couldn't publish to the channel", "channel", "telegram:user_delete", "error", err.Error()) + } + + case event := <-a.subscriptions["processor:image"]: + a.Log.Debug("service got a new message from a channel", "channel", event.ChannelName, "app", AppName) + msg := event.Payload.(*bus.Message) + + ctx := context.Background() + if !models.UserExists(a.db, ctx, event.Payload.(*bus.Message).TbContext.Sender().ID) { + a.Log.Error("user is not registered", "user", event.Payload.(*bus.Message).TbContext.Sender().ID) + } + + msg.Image.Type = imgType(msg.Image.Filename) + msg.Image.Base64 = readImage(msg.Image.Filename) + msg.Image.Parsed = make(map[string]string) + + err := a.bus.Publish("parser:openai", msg) + if err != nil { + a.Log.Error("couldn't publish to the channel", "channel", "parser:openai", "error", err.Error()) + } + + // err = a.bus.Publish("parser:gemini", msg) + // if err != nil { + // a.Log.Error("couldn't publish to the channel", "channel", "parser:gemini", "error", err.Error()) + // } + + // err = a.bus.Publish("parser:claude", msg) + // if err != nil { + // a.Log.Error("couldn't publish to the channel", "channel", "parser:claude", "error", err.Error()) + // } + // } + + // ADDING A NEWLY PARSED RECEIPT + case event := <-a.subscriptions["processor:receipt_add"]: + a.Log.Debug("service got a new message from a channel", "channel", event.ChannelName, "app", AppName) + msg := event.Payload.(*bus.Message) + + if models.UserExists(a.db, ctx, event.Payload.(*bus.Message).TbContext.Sender().ID) { + + user := new(models.User) + a.db.NewSelect().Model(user).Where("telegram_id = ?", event.Payload.(*bus.Message).TbContext.Sender().ID).Scan(ctx) + + group := new(models.Group) + if event.Payload.(*bus.Message).TbContext.Sender().ID != event.Payload.(*bus.Message).TbContext.Chat().ID { + if models.GroupExists(a.db, ctx, event.Payload.(*bus.Message).TbContext.Chat().ID) { + a.db.NewSelect().Model(group).Where("telegram_chat_id = ?", event.Payload.(*bus.Message).TbContext.Chat().ID).Scan(ctx) + } else { + group.TelegramChatID = event.Payload.(*bus.Message).TbContext.Chat().ID + group.Add(a.db, ctx) + group.UserAdd(a.db, ctx, user.UserID) + } + } + + var receipt models.Receipt + if err := json.Unmarshal([]byte(msg.Image.Parsed["openai"]), &receipt); err != nil { + a.Log.Error("cannot parse receipt json", "error", err) + } + + ctx := context.Background() + receipt.UserID = user.UserID + receipt.Merchant.Add(a.db, ctx) + receipt.MerchantID = receipt.Merchant.MerchantID + receipt.CreditCard.UserID = user.UserID + receipt.CreditCard.Add(a.db, ctx) + receipt.CreditCardID = receipt.CreditCard.CreditCardID + receipt.Add(a.db, ctx) + + msg.Text = "We got your receipt" + + msg.InlineKeyboard = &tele.ReplyMarkup{ + InlineKeyboard: [][]tele.InlineButton{ + { + {Text: fmt.Sprintf("Merchant: %s", strings.Title(strings.ToLower(receipt.Merchant.Title))), Data: models.String(&models.TelegramBotCommand{Cmd: models.BtnCmd["merchant"]})}, + }, + { + {Text: fmt.Sprintf("Category: %s", strings.Title(strings.ToLower(receipt.Category))), Data: models.String(&models.TelegramBotCommand{Cmd: models.BtnCmd["category"]})}, + }, + { + {Text: fmt.Sprintf("Tax: %.2f", receipt.Tax), Data: models.String(&models.TelegramBotCommand{Cmd: models.BtnCmd["tax"], ID: receipt.ReceiptID})}, + {Text: fmt.Sprintf("Total: %.2f", receipt.Total), Data: models.String(&models.TelegramBotCommand{Cmd: models.BtnCmd["total"], ID: receipt.ReceiptID})}, + }, + { + {Text: fmt.Sprintf("List of the receipt items (%d)", len(receipt.Items)), Data: models.String(&models.TelegramBotCommand{Cmd: models.BtnCmd["items_list"], ID: receipt.ReceiptID})}, + }, + }, + } + + a.Log.Debug("publishing message to a channel", "channel", "telegram:send", "app", AppName) + err := a.bus.Publish("telegram:send", msg) + if err != nil { + a.Log.Error("couldn't publish to the channel", "channel", "telegram:send", "error", err.Error()) + } + } + + case event := <-a.subscriptions["processor:items_list"]: + a.Log.Debug("service got a new message from a channel", "channel", event.ChannelName, "app", AppName) + msg := event.Payload.(*bus.Message) + + if models.UserExists(a.db, ctx, event.Payload.(*bus.Message).TbContext.Sender().ID) { + + user := new(models.User) + a.db.NewSelect().Model(user).Where("telegram_id = ?", event.Payload.(*bus.Message).TbContext.Sender().ID).Scan(ctx) + + r := new(models.Receipt) + err := a.db.NewSelect().Model(r).Where("receipt_id = ?", event.Payload.(*bus.Message).TBCmd.ID).Scan(ctx) + if err != nil { + panic(err) + } + + r.CreditCard = new(models.CreditCard) + err = a.db.NewSelect().Model(r.CreditCard).Where("credit_card_id = ?", r.CreditCardID).Scan(ctx) + if err != nil { + panic(err) + } + + r.Merchant = new(models.Merchant) + err = a.db.NewSelect().Model(r.Merchant).Where("merchant_id = ?", r.MerchantID).Scan(ctx) + if err != nil { + panic(err) + } + + err = a.db.NewSelect().Model(&r.Items).Where("receipt_id = ?", r.ReceiptID).Scan(ctx) + if err != nil { + panic(err) + } + + message := "List of the Receipt Items:\n" + strings.Repeat("-", 32) + "\n" + + merchant := truncateText(r.Merchant.Title, 16) + spaces := 32 - (len("Merchant:") + len(merchant)) + message += "Merchant:" + strings.Repeat(" ", spaces) + merchant + "\n" + spaces = 32 - (len("Date/Time:") + len(r.PaidAt)) + message += "Date/Time:" + strings.Repeat(" ", spaces) + r.PaidAt + "\n" + strings.Repeat("-", 32) + "\n" + + keyboard := &tele.ReplyMarkup{InlineKeyboard: [][]tele.InlineButton{}} + + for _, item := range r.Items { + // b := tele.InlineButton{Text: fmt.Sprintf("%s (%.2f) $%.2f", strings.Title(strings.ToLower(item.Title)), item.Quantity, item.Price), Data: String(&TelegramBotCommand{Cmd: btnCmd["item_edit"], ID: item.ItemID})} + // keyboard.InlineKeyboard = append(keyboard.InlineKeyboard, []tele.InlineButton{b}) + // if item.Quantity == math.Trunc(item.Quantity) { + // fmt.Sprintf("%d", math.Trunc(item.Quantity)) + // } else { + // fmt.Sprintf("%.2f", item.Quantity) + // } + title := truncateText(item.Title, 16) + spaces := 32 - (len(title) + len("x"+fmt.Sprintf("%+v", item.Quantity)) + len(fmt.Sprintf("$%.2f", item.Price)) + 3) + message += title + strings.Repeat(".", spaces) + "x" + fmt.Sprintf("%+v", item.Quantity) + "..." + fmt.Sprintf("$%.2f", item.Price) + "\n" + a.Log.Debug("list of items", "title", strings.Title(strings.ToLower(item.Title)), "quantity", item.Quantity, "price", item.Price) + } + // keyboard.InlineKeyboard = append(keyboard.InlineKeyboard, []tele.InlineButton{{Text: "«", Data: String(&TelegramBotCommand{Cmd: btnPage["prev"], ID: tbc.ID})}, {Text: "»", Data: String(&TelegramBotCommand{Cmd: btnPage["next"], ID: tbc.ID})}}) + keyboard.InlineKeyboard = append(keyboard.InlineKeyboard, []tele.InlineButton{{Text: "« Back to Receipt", Data: models.String(&models.TelegramBotCommand{Cmd: models.BtnCmd["receipt_edit"], ID: r.ReceiptID})}}) + + message += strings.Repeat("-", 32) + "\n" + spaces = 32 - (len("Tax:") + len(fmt.Sprintf("$%.2f", r.Tax))) + message += "Tax:" + strings.Repeat(" ", spaces) + fmt.Sprintf("$%.2f", r.Tax) + "\n" + if r.CCFee > 0 { + spaces = 32 - (len("CC Fee:") + len(fmt.Sprintf("$%.2f", r.CCFee))) + message += "CC Fee:" + strings.Repeat(" ", spaces) + fmt.Sprintf("$%.2f", r.CCFee) + "\n" + } + spaces = 32 - (len("Total:") + len(fmt.Sprintf("$%.2f", r.Total))) + message += "Total:" + strings.Repeat(" ", spaces) + fmt.Sprintf("$%.2f", r.Total) + "\n" + + msg.TBParseMode = "HTML" + msg.InlineKeyboard = keyboard + + a.Log.Debug("publishing message to a channel", "channel", "telegram:send", "app", AppName) + err = a.bus.Publish("telegram:send", msg) + if err != nil { + a.Log.Error("couldn't publish to the channel", "channel", "telegram:send", "error", err.Error()) + } + } + + case <-ctx.Done(): + a.Log.Info("stopping " + AppName + " main communication event loop") + return + } + } +} + +// eventLoop loops forever +// func (a *App) eventLoop(ctx context.Context, chAppWorker chan bus.Event) { +// a.Log.Info("app communication event loop started") +// defer a.Log.Info("app communication event loop halted") + +// for { +// select { +// case <-a.loop.sysInfo.C: +// a.publishSysTopics() +// case event := <-chAppWorker: +// message := event.Data.(*bus.ConnectionStatus) +// a.Log.Info("got a message from worker", "worker", message.WorkerID, "type", message.WorkerType, "isconnected", message.IsConnected) +// if !message.IsConnected { +// a.hooks.OnWorkerDisconnected() +// a.Workers.Close(message.WorkerID) +// } +// a.hooks.OnWorkerConnected() +// case <-ctx.Done(): +// a.Log.Info("stopping app communication event loop") +// return +// } +// } +// } + +// publishSysTopics publishes the current values to the server $SYS topics. +// Due to the int to string conversions this method is not as cheap as +// some of the others so the publishing interval should be set appropriately. +func (a *App) publishSysTopics() { + var m runtime.MemStats + runtime.ReadMemStats(&m) + atomic.StoreInt64(&a.Info.MemoryAlloc, int64(m.HeapInuse)) + atomic.StoreInt64(&a.Info.Threads, int64(runtime.NumGoroutine())) + atomic.StoreInt64(&a.Info.Time, time.Now().Unix()) + atomic.StoreInt64(&a.Info.Uptime, time.Now().Unix()-atomic.LoadInt64(&a.Info.Started)) + + a.hooks.OnSysInfoTick(a.Info) +} + +func truncateText(s string, max int) string { + if max > len(s) { + return s + } + + return s[:strings.LastIndex(s[:max], " ")] +} diff --git a/app/hooks.go b/app/hooks.go new file mode 100644 index 0000000..826de75 --- /dev/null +++ b/app/hooks.go @@ -0,0 +1,216 @@ +package app + +import ( + "errors" + "fmt" + "log/slog" + "sync" + "sync/atomic" + + "github.com/alex-savin/go-receipt-tracker/system" +) + +const ( + SetOptions byte = iota + OnSysInfoTick + OnStarted + OnStopped + OnWorkerConnected + OnWorkerDisconnected + // OnSysInfoTick +) + +var ( + // ErrInvalidConfigType indicates a different Type of config value was expected to what was received. + ErrInvalidConfigType = errors.New("invalid config type provided") +) + +// Hook provides an interface of handlers for different events which occur +// during the lifecycle of the broker. +type Hook interface { + ID() string + Provides(b byte) bool + Init(config any) error + Stop() error + SetOpts(l *slog.Logger) + OnSysInfoTick(*system.Info) + OnStarted() + OnStopped() + OnWorkerConnected() + OnWorkerDisconnected() + // OnSysInfoTick(*system.Info) +} + +// Hooks is a slice of Hook interfaces to be called in sequence. +type Hooks struct { + Log *slog.Logger // a logger for the hook (from the server) + internal atomic.Value // a slice of []Hook + wg sync.WaitGroup // a waitgroup for syncing hook shutdown + qty int64 // the number of hooks in use + sync.Mutex // a mutex for locking when adding hooks +} + +// Len returns the number of hooks added. +func (h *Hooks) Len() int64 { + return atomic.LoadInt64(&h.qty) +} + +// Provides returns true if any one hook provides any of the requested hook methods. +func (h *Hooks) Provides(b ...byte) bool { + for _, hook := range h.GetAll() { + for _, hb := range b { + if hook.Provides(hb) { + return true + } + } + } + + return false +} + +// Add adds and initializes a new hook. +func (h *Hooks) Add(hook Hook, config any) error { + h.Lock() + defer h.Unlock() + + err := hook.Init(config) + if err != nil { + return fmt.Errorf("failed initialising %s hook: %w", hook.ID(), err) + } + + i, ok := h.internal.Load().([]Hook) + if !ok { + i = []Hook{} + } + + i = append(i, hook) + h.internal.Store(i) + atomic.AddInt64(&h.qty, 1) + h.wg.Add(1) + + return nil +} + +// GetAll returns a slice of all the hooks. +func (h *Hooks) GetAll() []Hook { + i, ok := h.internal.Load().([]Hook) + if !ok { + return []Hook{} + } + + return i +} + +// Stop indicates all attached hooks to gracefully end. +func (h *Hooks) Stop() { + go func() { + for _, hook := range h.GetAll() { + h.Log.Info("stopping hook", "hook", hook.ID()) + if err := hook.Stop(); err != nil { + h.Log.Debug("problem stopping hook", "error", err, "hook", hook.ID()) + } + + h.wg.Done() + } + }() + + h.wg.Wait() +} + +// OnSysInfoTick is called when the $SYS topic values are published out. +func (h *Hooks) OnSysInfoTick(sys *system.Info) { + for _, hook := range h.GetAll() { + if hook.Provides(OnSysInfoTick) { + hook.OnSysInfoTick(sys) + } + } +} + +// OnStarted is called when the server has successfully started. +func (h *Hooks) OnStarted() { + for _, hook := range h.GetAll() { + if hook.Provides(OnStarted) { + hook.OnStarted() + } + } +} + +// OnStopped is called when the server has successfully stopped. +func (h *Hooks) OnStopped() { + for _, hook := range h.GetAll() { + if hook.Provides(OnStopped) { + hook.OnStopped() + } + } +} + +// OnWorkerConnected is called when the worker has successfully connected. +func (h *Hooks) OnWorkerConnected() { + for _, hook := range h.GetAll() { + if hook.Provides(OnWorkerConnected) { + hook.OnWorkerConnected() + } + } +} + +// OnWorkerDisconnected is called when the worker has disconnected. +func (h *Hooks) OnWorkerDisconnected() { + for _, hook := range h.GetAll() { + if hook.Provides(OnWorkerDisconnected) { + hook.OnWorkerDisconnected() + } + } +} + +// HookBase provides a set of default methods for each hook. It should be embedded in +// all hooks. +type HookBase struct { + Hook + Log *slog.Logger +} + +// ID returns the ID of the hook. +func (h *HookBase) ID() string { + return "base" +} + +// Provides indicates which methods a hook provides. The default is none - this method +// should be overridden by the embedding hook. +func (h *HookBase) Provides(b byte) bool { + return false +} + +// Init performs any pre-start initializations for the hook, such as connecting to databases +// or opening files. +func (h *HookBase) Init(config any) error { + return nil +} + +// SetOpts is called by the server to propagate internal values and generally should +// not be called manually. +func (h *HookBase) SetOpts(l *slog.Logger) { + h.Log = l +} + +// Stop is called to gracefully shut down the hook. +func (h *HookBase) Stop() error { + return nil +} + +// OnSysInfoTick is called when the server publishes system info. +func (h *HookBase) OnSysInfoTick(*system.Info) {} + +// OnStarted is called when the server starts. +func (h *HookBase) OnStarted() {} + +// OnStopped is called when the server stops. +func (h *HookBase) OnStopped() {} + +// OnWorkerConnected is called when the worker connected. +func (h *HookBase) OnWorkerConnected() {} + +// OnWorkerDisconnected is called when the worker disconnected. +func (h *HookBase) OnWorkerDisconnected() {} + +// // OnSysInfoTick is called when the server publishes system info. +// func (h *HookBase) OnSysInfoTick(*system.Info) {} diff --git a/app/processor.go b/app/processor.go new file mode 100644 index 0000000..a797e0c --- /dev/null +++ b/app/processor.go @@ -0,0 +1,116 @@ +package app + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "log" + "net/http" + "os" + + "github.com/alex-savin/go-receipt-tracker/models" + tele "gopkg.in/telebot.v4" +) + +func Parse(c *tele.Context) error { + return nil +} + +// parseReceipt . +func parseReceipt(j string) (*models.Receipt, error) { + var receipt *models.Receipt + if err := json.Unmarshal([]byte(j), &receipt); err != nil { + return nil, err + } + return receipt, nil +} + +func imgType(i string) string { + if doesFileExist(i) { + bytes, err := os.ReadFile(i) + if err != nil { + log.Fatal(err) + } + + // Determine the content type of the image file + mimeType := http.DetectContentType(bytes) + + switch mimeType { + case "image/jpeg": + return "image/jpeg" + case "image/png": + return "image/png" + case "image/webp": + return "image/webp" + case "image/svg+xml": + return "image/svg+xml" + case "image/gif": + return "image/gif" + case "image/avif": + return "image/avif" + case "image/apng": + return "image/apng" + } + + } else { + return "" + } + return "" +} + +// readImage . +func readImage(i string) string { + if doesFileExist(i) { + bytes, err := os.ReadFile(i) + if err != nil { + log.Fatal(err) + } + + // Prepend the appropriate URI scheme header depending + // on the MIME type + var base64Encoding string + + // Append the base64 encoded output + base64Encoding += toBase64(bytes) + + return base64Encoding + } else { + return "" + } +} + +// toBase64 . +func toBase64(b []byte) string { + return base64.StdEncoding.EncodeToString(b) +} + +// doesFileExist function to check if file exists +func doesFileExist(fileName string) bool { + _, error := os.Stat(fileName) + + // check if error is "file not exists" + if os.IsNotExist(error) { + log.Printf("%v file does not exist\n", fileName) + return false + } else { + log.Printf("%v file exist\n", fileName) + return true + } +} + +// imgType . +func (a App) imgType(fileName string) (string, error) { + f, err := os.Open(fileName) + if err != nil { + a.Log.Error("") + } + defer f.Close() + + buff := make([]byte, 512) // docs tell that it take only first 512 bytes into consideration + if _, err = f.Read(buff); err != nil { + fmt.Println(err) // do something with that error + return "", err + } + + return http.DetectContentType(buff), nil +} diff --git a/blobs/AQAD-64xGwABW-FHfg.jpg b/blobs/AQAD-64xGwABW-FHfg.jpg new file mode 100644 index 0000000..141e2a9 Binary files /dev/null and b/blobs/AQAD-64xGwABW-FHfg.jpg differ diff --git a/blobs/AQAD-K0xG8NvKEd-.jpg b/blobs/AQAD-K0xG8NvKEd-.jpg new file mode 100644 index 0000000..2d03229 Binary files /dev/null and b/blobs/AQAD-K0xG8NvKEd-.jpg differ diff --git a/blobs/AQAD-a0xG50ISUR-.jpg b/blobs/AQAD-a0xG50ISUR-.jpg new file mode 100644 index 0000000..5f870d7 Binary files /dev/null and b/blobs/AQAD-a0xG50ISUR-.jpg differ diff --git a/blobs/AQAD-awxG42ZWUZ-.jpg b/blobs/AQAD-awxG42ZWUZ-.jpg new file mode 100644 index 0000000..d6da34a Binary files /dev/null and b/blobs/AQAD-awxG42ZWUZ-.jpg differ diff --git a/blobs/AQAD0K8xG5FI0Ud-.jpg b/blobs/AQAD0K8xG5FI0Ud-.jpg new file mode 100644 index 0000000..9faaf1d Binary files /dev/null and b/blobs/AQAD0K8xG5FI0Ud-.jpg differ diff --git a/blobs/AQAD1a0xG45smUZ-.jpg b/blobs/AQAD1a0xG45smUZ-.jpg new file mode 100644 index 0000000..2173eda Binary files /dev/null and b/blobs/AQAD1a0xG45smUZ-.jpg differ diff --git a/blobs/AQAD1a4xG_axSEZ-.jpg b/blobs/AQAD1a4xG_axSEZ-.jpg new file mode 100644 index 0000000..85688ee Binary files /dev/null and b/blobs/AQAD1a4xG_axSEZ-.jpg differ diff --git a/blobs/AQAD2K8xG4eUOER-.jpg b/blobs/AQAD2K8xG4eUOER-.jpg new file mode 100644 index 0000000..ed4a601 Binary files /dev/null and b/blobs/AQAD2K8xG4eUOER-.jpg differ diff --git a/blobs/AQAD3K0xGyt2sUZ-.jpg b/blobs/AQAD3K0xGyt2sUZ-.jpg new file mode 100644 index 0000000..ae8b3b0 Binary files /dev/null and b/blobs/AQAD3K0xGyt2sUZ-.jpg differ diff --git a/blobs/AQAD3K4xG7pBeEd-.jpg b/blobs/AQAD3K4xG7pBeEd-.jpg new file mode 100644 index 0000000..c555299 Binary files /dev/null and b/blobs/AQAD3K4xG7pBeEd-.jpg differ diff --git a/blobs/AQAD4KwxGz80WUd-.jpg b/blobs/AQAD4KwxGz80WUd-.jpg new file mode 100644 index 0000000..40c5143 Binary files /dev/null and b/blobs/AQAD4KwxGz80WUd-.jpg differ diff --git a/blobs/AQAD4a0xGzSJ0Ed-.jpg b/blobs/AQAD4a0xGzSJ0Ed-.jpg new file mode 100644 index 0000000..55d324f Binary files /dev/null and b/blobs/AQAD4a0xGzSJ0Ed-.jpg differ diff --git a/blobs/AQAD4q0xGzSJ0Ed-.jpg b/blobs/AQAD4q0xGzSJ0Ed-.jpg new file mode 100644 index 0000000..7d1c6b8 Binary files /dev/null and b/blobs/AQAD4q0xGzSJ0Ed-.jpg differ diff --git a/blobs/AQAD5a4xG7pBeEd-.jpg b/blobs/AQAD5a4xG7pBeEd-.jpg new file mode 100644 index 0000000..130ba01 Binary files /dev/null and b/blobs/AQAD5a4xG7pBeEd-.jpg differ diff --git a/blobs/AQAD6K0xG2cZeEd-.jpg b/blobs/AQAD6K0xG2cZeEd-.jpg new file mode 100644 index 0000000..52e43bc Binary files /dev/null and b/blobs/AQAD6K0xG2cZeEd-.jpg differ diff --git a/blobs/AQAD6K0xG78WgUZ-.jpg b/blobs/AQAD6K0xG78WgUZ-.jpg new file mode 100644 index 0000000..9114606 Binary files /dev/null and b/blobs/AQAD6K0xG78WgUZ-.jpg differ diff --git a/blobs/AQAD6a0xG2cZeEd-.jpg b/blobs/AQAD6a0xG2cZeEd-.jpg new file mode 100644 index 0000000..45bc847 Binary files /dev/null and b/blobs/AQAD6a0xG2cZeEd-.jpg differ diff --git a/blobs/AQAD6q0xGzSJ0Ed-.jpg b/blobs/AQAD6q0xGzSJ0Ed-.jpg new file mode 100644 index 0000000..d212b9c Binary files /dev/null and b/blobs/AQAD6q0xGzSJ0Ed-.jpg differ diff --git a/blobs/AQAD6qwxGz80WUd-.jpg b/blobs/AQAD6qwxGz80WUd-.jpg new file mode 100644 index 0000000..19bf9c7 Binary files /dev/null and b/blobs/AQAD6qwxGz80WUd-.jpg differ diff --git a/blobs/AQAD6rQxGxYVEEd-.jpg b/blobs/AQAD6rQxGxYVEEd-.jpg new file mode 100644 index 0000000..36ac182 Binary files /dev/null and b/blobs/AQAD6rQxGxYVEEd-.jpg differ diff --git a/blobs/AQAD7K0xG78WgUZ-.jpg b/blobs/AQAD7K0xG78WgUZ-.jpg new file mode 100644 index 0000000..bdd4a40 Binary files /dev/null and b/blobs/AQAD7K0xG78WgUZ-.jpg differ diff --git a/blobs/AQAD7K4xG1UCiUd-.jpg b/blobs/AQAD7K4xG1UCiUd-.jpg new file mode 100644 index 0000000..b367b51 Binary files /dev/null and b/blobs/AQAD7K4xG1UCiUd-.jpg differ diff --git a/blobs/AQAD860xG22YQUZ-.jpg b/blobs/AQAD860xG22YQUZ-.jpg new file mode 100644 index 0000000..cbc60d8 Binary files /dev/null and b/blobs/AQAD860xG22YQUZ-.jpg differ diff --git a/blobs/AQAD864xG42ZUUZ-.jpg b/blobs/AQAD864xG42ZUUZ-.jpg new file mode 100644 index 0000000..425ce0e Binary files /dev/null and b/blobs/AQAD864xG42ZUUZ-.jpg differ diff --git a/blobs/AQAD864xGxh44EZ-.jpg b/blobs/AQAD864xGxh44EZ-.jpg new file mode 100644 index 0000000..9965737 Binary files /dev/null and b/blobs/AQAD864xGxh44EZ-.jpg differ diff --git a/blobs/AQAD96wxG42ZWUZ-.jpg b/blobs/AQAD96wxG42ZWUZ-.jpg new file mode 100644 index 0000000..f2bdfdc Binary files /dev/null and b/blobs/AQAD96wxG42ZWUZ-.jpg differ diff --git a/blobs/AQAD9K4xG42ZUUZ-.jpg b/blobs/AQAD9K4xG42ZUUZ-.jpg new file mode 100644 index 0000000..c41eedd Binary files /dev/null and b/blobs/AQAD9K4xG42ZUUZ-.jpg differ diff --git a/blobs/AQAD9q4xG42ZUUZ-.jpg b/blobs/AQAD9q4xG42ZUUZ-.jpg new file mode 100644 index 0000000..48bfefe Binary files /dev/null and b/blobs/AQAD9q4xG42ZUUZ-.jpg differ diff --git a/blobs/AQADBK4xGz80UUd-.jpg b/blobs/AQADBK4xGz80UUd-.jpg new file mode 100644 index 0000000..5badc60 Binary files /dev/null and b/blobs/AQADBK4xGz80UUd-.jpg differ diff --git a/blobs/AQADEK8xG50v-Ed-.jpg b/blobs/AQADEK8xG50v-Ed-.jpg new file mode 100644 index 0000000..1cc637b Binary files /dev/null and b/blobs/AQADEK8xG50v-Ed-.jpg differ diff --git a/blobs/AQADG68xGwABW-FHfg.jpg b/blobs/AQADG68xGwABW-FHfg.jpg new file mode 100644 index 0000000..e1e1b2d Binary files /dev/null and b/blobs/AQADG68xGwABW-FHfg.jpg differ diff --git a/blobs/AQADGq4xG78WiUZ-.jpg b/blobs/AQADGq4xG78WiUZ-.jpg new file mode 100644 index 0000000..741437a Binary files /dev/null and b/blobs/AQADGq4xG78WiUZ-.jpg differ diff --git a/blobs/AQADHbAxG82qAUd-.jpg b/blobs/AQADHbAxG82qAUd-.jpg new file mode 100644 index 0000000..1d62437 Binary files /dev/null and b/blobs/AQADHbAxG82qAUd-.jpg differ diff --git a/blobs/AQADIa8xG5FI2Ud-.jpg b/blobs/AQADIa8xG5FI2Ud-.jpg new file mode 100644 index 0000000..d5c282e Binary files /dev/null and b/blobs/AQADIa8xG5FI2Ud-.jpg differ diff --git a/blobs/AQADJ60xG42ZWUZ-.jpg b/blobs/AQADJ60xG42ZWUZ-.jpg new file mode 100644 index 0000000..8d77560 Binary files /dev/null and b/blobs/AQADJ60xG42ZWUZ-.jpg differ diff --git a/blobs/AQADJ64xG5twKUd-.jpg b/blobs/AQADJ64xG5twKUd-.jpg new file mode 100644 index 0000000..3ba34d2 Binary files /dev/null and b/blobs/AQADJ64xG5twKUd-.jpg differ diff --git a/blobs/AQADJq0xG42ZWUZ-.jpg b/blobs/AQADJq0xG42ZWUZ-.jpg new file mode 100644 index 0000000..d08dea2 Binary files /dev/null and b/blobs/AQADJq0xG42ZWUZ-.jpg differ diff --git a/blobs/AQADK7AxGzSJyEd-.jpg b/blobs/AQADK7AxGzSJyEd-.jpg new file mode 100644 index 0000000..446f8b6 Binary files /dev/null and b/blobs/AQADK7AxGzSJyEd-.jpg differ diff --git a/blobs/AQADK7IxGyDEMUZ-.jpg b/blobs/AQADK7IxGyDEMUZ-.jpg new file mode 100644 index 0000000..8bcede0 Binary files /dev/null and b/blobs/AQADK7IxGyDEMUZ-.jpg differ diff --git a/blobs/AQADKbIxGyDEMUZ-.jpg b/blobs/AQADKbIxGyDEMUZ-.jpg new file mode 100644 index 0000000..f3e08fd Binary files /dev/null and b/blobs/AQADKbIxGyDEMUZ-.jpg differ diff --git a/blobs/AQADKrIxGyDEMUZ-.jpg b/blobs/AQADKrIxGyDEMUZ-.jpg new file mode 100644 index 0000000..565faf8 Binary files /dev/null and b/blobs/AQADKrIxGyDEMUZ-.jpg differ diff --git a/blobs/AQADL7IxGyDEMUZ-.jpg b/blobs/AQADL7IxGyDEMUZ-.jpg new file mode 100644 index 0000000..d5a1f92 Binary files /dev/null and b/blobs/AQADL7IxGyDEMUZ-.jpg differ diff --git a/blobs/AQADLLIxGyDEMUZ-.jpg b/blobs/AQADLLIxGyDEMUZ-.jpg new file mode 100644 index 0000000..1f21613 Binary files /dev/null and b/blobs/AQADLLIxGyDEMUZ-.jpg differ diff --git a/blobs/AQADLbIxGyDEMUZ-.jpg b/blobs/AQADLbIxGyDEMUZ-.jpg new file mode 100644 index 0000000..3d8058e Binary files /dev/null and b/blobs/AQADLbIxGyDEMUZ-.jpg differ diff --git a/blobs/AQADLrIxGyDEMUZ-.jpg b/blobs/AQADLrIxGyDEMUZ-.jpg new file mode 100644 index 0000000..dc0817b Binary files /dev/null and b/blobs/AQADLrIxGyDEMUZ-.jpg differ diff --git a/blobs/AQADMLIxGyDEMUZ-.jpg b/blobs/AQADMLIxGyDEMUZ-.jpg new file mode 100644 index 0000000..cc42fba Binary files /dev/null and b/blobs/AQADMLIxGyDEMUZ-.jpg differ diff --git a/blobs/AQADMrExGyXiUUR-.jpg b/blobs/AQADMrExGyXiUUR-.jpg new file mode 100644 index 0000000..f10f0b0 Binary files /dev/null and b/blobs/AQADMrExGyXiUUR-.jpg differ diff --git a/blobs/AQADNK8xGyDEOUZ-.jpg b/blobs/AQADNK8xGyDEOUZ-.jpg new file mode 100644 index 0000000..dc37170 Binary files /dev/null and b/blobs/AQADNK8xGyDEOUZ-.jpg differ diff --git a/blobs/AQADNrAxGzSJyEd-.jpg b/blobs/AQADNrAxGzSJyEd-.jpg new file mode 100644 index 0000000..b22b767 Binary files /dev/null and b/blobs/AQADNrAxGzSJyEd-.jpg differ diff --git a/blobs/AQADObIxGyDEMUZ-.jpg b/blobs/AQADObIxGyDEMUZ-.jpg new file mode 100644 index 0000000..ffe1ea6 Binary files /dev/null and b/blobs/AQADObIxGyDEMUZ-.jpg differ diff --git a/blobs/AQADOrIxGyDEMUZ-.jpg b/blobs/AQADOrIxGyDEMUZ-.jpg new file mode 100644 index 0000000..cb55a54 Binary files /dev/null and b/blobs/AQADOrIxGyDEMUZ-.jpg differ diff --git a/blobs/AQADP68xGxYVGEd-.jpg b/blobs/AQADP68xGxYVGEd-.jpg new file mode 100644 index 0000000..cff0357 Binary files /dev/null and b/blobs/AQADP68xGxYVGEd-.jpg differ diff --git a/blobs/AQADPq8xGyDEOUZ-.jpg b/blobs/AQADPq8xGyDEOUZ-.jpg new file mode 100644 index 0000000..b7a4132 Binary files /dev/null and b/blobs/AQADPq8xGyDEOUZ-.jpg differ diff --git a/blobs/AQADPrIxGyDEMUZ-.jpg b/blobs/AQADPrIxGyDEMUZ-.jpg new file mode 100644 index 0000000..f60df00 Binary files /dev/null and b/blobs/AQADPrIxGyDEMUZ-.jpg differ diff --git a/blobs/AQADQK8xGyDEOUZ-.jpg b/blobs/AQADQK8xGyDEOUZ-.jpg new file mode 100644 index 0000000..0b92525 Binary files /dev/null and b/blobs/AQADQK8xGyDEOUZ-.jpg differ diff --git a/blobs/AQADQrAxG1vkeEZ-.jpg b/blobs/AQADQrAxG1vkeEZ-.jpg new file mode 100644 index 0000000..92d19f6 Binary files /dev/null and b/blobs/AQADQrAxG1vkeEZ-.jpg differ diff --git a/blobs/AQADR7IxGyDEMUZ-.jpg b/blobs/AQADR7IxGyDEMUZ-.jpg new file mode 100644 index 0000000..43cc9f7 Binary files /dev/null and b/blobs/AQADR7IxGyDEMUZ-.jpg differ diff --git a/blobs/AQADRrIxGyDEMUZ-.jpg b/blobs/AQADRrIxGyDEMUZ-.jpg new file mode 100644 index 0000000..c8dc276 Binary files /dev/null and b/blobs/AQADRrIxGyDEMUZ-.jpg differ diff --git a/blobs/AQADS60xGyXiYUR-.jpg b/blobs/AQADS60xGyXiYUR-.jpg new file mode 100644 index 0000000..b0cd11d Binary files /dev/null and b/blobs/AQADS60xGyXiYUR-.jpg differ diff --git a/blobs/AQADSLIxGyDEMUZ-.jpg b/blobs/AQADSLIxGyDEMUZ-.jpg new file mode 100644 index 0000000..2bf1233 Binary files /dev/null and b/blobs/AQADSLIxGyDEMUZ-.jpg differ diff --git a/blobs/AQADT68xGyDEOUZ-.jpg b/blobs/AQADT68xGyDEOUZ-.jpg new file mode 100644 index 0000000..85688ee Binary files /dev/null and b/blobs/AQADT68xGyDEOUZ-.jpg differ diff --git a/blobs/AQADTa8xGyDEOUZ-.jpg b/blobs/AQADTa8xGyDEOUZ-.jpg new file mode 100644 index 0000000..cd78c1e Binary files /dev/null and b/blobs/AQADTa8xGyDEOUZ-.jpg differ diff --git a/blobs/AQADTq8xGyDEOUZ-.jpg b/blobs/AQADTq8xGyDEOUZ-.jpg new file mode 100644 index 0000000..63527c9 Binary files /dev/null and b/blobs/AQADTq8xGyDEOUZ-.jpg differ diff --git a/blobs/AQADTrIxG9b46EZ-.jpg b/blobs/AQADTrIxG9b46EZ-.jpg new file mode 100644 index 0000000..3d7e9fb Binary files /dev/null and b/blobs/AQADTrIxG9b46EZ-.jpg differ diff --git a/blobs/AQADUK8xGyDEOUZ-.jpg b/blobs/AQADUK8xGyDEOUZ-.jpg new file mode 100644 index 0000000..0b11e37 Binary files /dev/null and b/blobs/AQADUK8xGyDEOUZ-.jpg differ diff --git a/blobs/AQADULIxGyDEMUZ-.jpg b/blobs/AQADULIxGyDEMUZ-.jpg new file mode 100644 index 0000000..c87e390 Binary files /dev/null and b/blobs/AQADULIxGyDEMUZ-.jpg differ diff --git a/blobs/AQADUa8xGyDEOUZ-.jpg b/blobs/AQADUa8xGyDEOUZ-.jpg new file mode 100644 index 0000000..f60df00 Binary files /dev/null and b/blobs/AQADUa8xGyDEOUZ-.jpg differ diff --git a/blobs/AQADVMYxGxvmKER-.jpg b/blobs/AQADVMYxGxvmKER-.jpg new file mode 100644 index 0000000..7aa5d06 Binary files /dev/null and b/blobs/AQADVMYxGxvmKER-.jpg differ diff --git a/blobs/AQADY6wxG50vAAFEfg.jpg b/blobs/AQADY6wxG50vAAFEfg.jpg new file mode 100644 index 0000000..0ee0817 Binary files /dev/null and b/blobs/AQADY6wxG50vAAFEfg.jpg differ diff --git a/blobs/AQADZa0xG1vkcEZ-.jpg b/blobs/AQADZa0xG1vkcEZ-.jpg new file mode 100644 index 0000000..c90cfd2 Binary files /dev/null and b/blobs/AQADZa0xG1vkcEZ-.jpg differ diff --git a/blobs/AQAD_68xGwyDYUR-.jpg b/blobs/AQAD_68xGwyDYUR-.jpg new file mode 100644 index 0000000..feb15f3 Binary files /dev/null and b/blobs/AQAD_68xGwyDYUR-.jpg differ diff --git a/blobs/AQAD_K4xGwABW-FHfg.jpg b/blobs/AQAD_K4xGwABW-FHfg.jpg new file mode 100644 index 0000000..a7ad816 Binary files /dev/null and b/blobs/AQAD_K4xGwABW-FHfg.jpg differ diff --git a/blobs/AQADaq8xG869KEd-.jpg b/blobs/AQADaq8xG869KEd-.jpg new file mode 100644 index 0000000..0acefe1 Binary files /dev/null and b/blobs/AQADaq8xG869KEd-.jpg differ diff --git a/blobs/AQADc6wxGzr6mUd-.jpg b/blobs/AQADc6wxGzr6mUd-.jpg new file mode 100644 index 0000000..f22a32e Binary files /dev/null and b/blobs/AQADc6wxGzr6mUd-.jpg differ diff --git a/blobs/AQADcK4xG7g6EER-.jpg b/blobs/AQADcK4xG7g6EER-.jpg new file mode 100644 index 0000000..cfccf24 Binary files /dev/null and b/blobs/AQADcK4xG7g6EER-.jpg differ diff --git a/blobs/AQADeqwxGzr6mUd-.jpg b/blobs/AQADeqwxGzr6mUd-.jpg new file mode 100644 index 0000000..f16db5b Binary files /dev/null and b/blobs/AQADeqwxGzr6mUd-.jpg differ diff --git a/blobs/AQADf64xG2cZaEd-.jpg b/blobs/AQADf64xG2cZaEd-.jpg new file mode 100644 index 0000000..156938d Binary files /dev/null and b/blobs/AQADf64xG2cZaEd-.jpg differ diff --git a/blobs/AQADfKwxGxdNMEZ-.jpg b/blobs/AQADfKwxGxdNMEZ-.jpg new file mode 100644 index 0000000..af6ac1d Binary files /dev/null and b/blobs/AQADfKwxGxdNMEZ-.jpg differ diff --git a/blobs/AQADfawxGzr6mUd-.jpg b/blobs/AQADfawxGzr6mUd-.jpg new file mode 100644 index 0000000..3229ee1 Binary files /dev/null and b/blobs/AQADfawxGzr6mUd-.jpg differ diff --git a/blobs/AQADfq8xG869KEd-.jpg b/blobs/AQADfq8xG869KEd-.jpg new file mode 100644 index 0000000..c105360 Binary files /dev/null and b/blobs/AQADfq8xG869KEd-.jpg differ diff --git a/blobs/AQADgK4xG2cZaEd-.jpg b/blobs/AQADgK4xG2cZaEd-.jpg new file mode 100644 index 0000000..06f291c Binary files /dev/null and b/blobs/AQADgK4xG2cZaEd-.jpg differ diff --git a/blobs/AQADgq4xG2cZaEd-.jpg b/blobs/AQADgq4xG2cZaEd-.jpg new file mode 100644 index 0000000..1e32c74 Binary files /dev/null and b/blobs/AQADgq4xG2cZaEd-.jpg differ diff --git a/blobs/AQADi64xG22YOUZ-.jpg b/blobs/AQADi64xG22YOUZ-.jpg new file mode 100644 index 0000000..1765fbd Binary files /dev/null and b/blobs/AQADi64xG22YOUZ-.jpg differ diff --git a/blobs/AQADj8QxG5CMCVR-.jpg b/blobs/AQADj8QxG5CMCVR-.jpg new file mode 100644 index 0000000..ac5b373 Binary files /dev/null and b/blobs/AQADj8QxG5CMCVR-.jpg differ diff --git a/blobs/AQADkK0xGxdNIEZ-.jpg b/blobs/AQADkK0xGxdNIEZ-.jpg new file mode 100644 index 0000000..f3e08fd Binary files /dev/null and b/blobs/AQADkK0xGxdNIEZ-.jpg differ diff --git a/blobs/AQADl60xG7g6CER-.jpg b/blobs/AQADl60xG7g6CER-.jpg new file mode 100644 index 0000000..e09a35f Binary files /dev/null and b/blobs/AQADl60xG7g6CER-.jpg differ diff --git a/blobs/AQADlKwxG45skUZ-.jpg b/blobs/AQADlKwxG45skUZ-.jpg new file mode 100644 index 0000000..4fe61ff Binary files /dev/null and b/blobs/AQADlKwxG45skUZ-.jpg differ diff --git a/blobs/AQADlb8xGwUICVR-.jpg b/blobs/AQADlb8xGwUICVR-.jpg new file mode 100644 index 0000000..d0a68db Binary files /dev/null and b/blobs/AQADlb8xGwUICVR-.jpg differ diff --git a/blobs/AQADlq8xGwyDcUR-.jpg b/blobs/AQADlq8xGwyDcUR-.jpg new file mode 100644 index 0000000..84745ff Binary files /dev/null and b/blobs/AQADlq8xGwyDcUR-.jpg differ diff --git a/blobs/AQADmK0xGxdNIEZ-.jpg b/blobs/AQADmK0xGxdNIEZ-.jpg new file mode 100644 index 0000000..af6ac1d Binary files /dev/null and b/blobs/AQADmK0xGxdNIEZ-.jpg differ diff --git a/blobs/AQADn60xG1WZEEV-.jpg b/blobs/AQADn60xG1WZEEV-.jpg new file mode 100644 index 0000000..26dbc4c Binary files /dev/null and b/blobs/AQADn60xG1WZEEV-.jpg differ diff --git a/blobs/AQADna0xG7pBgEd-.jpg b/blobs/AQADna0xG7pBgEd-.jpg new file mode 100644 index 0000000..453ddd1 Binary files /dev/null and b/blobs/AQADna0xG7pBgEd-.jpg differ diff --git a/blobs/AQADqbQxGxYVEEd-.jpg b/blobs/AQADqbQxGxYVEEd-.jpg new file mode 100644 index 0000000..388645d Binary files /dev/null and b/blobs/AQADqbQxGxYVEEd-.jpg differ diff --git a/blobs/AQADs6wxG45skUZ-.jpg b/blobs/AQADs6wxG45skUZ-.jpg new file mode 100644 index 0000000..df06932 Binary files /dev/null and b/blobs/AQADs6wxG45skUZ-.jpg differ diff --git a/blobs/AQADv60xG601WUd-.jpg b/blobs/AQADv60xG601WUd-.jpg new file mode 100644 index 0000000..0f0093b Binary files /dev/null and b/blobs/AQADv60xG601WUd-.jpg differ diff --git a/blobs/AQADvK0xG50IQUR-.jpg b/blobs/AQADvK0xG50IQUR-.jpg new file mode 100644 index 0000000..dc585cd Binary files /dev/null and b/blobs/AQADvK0xG50IQUR-.jpg differ diff --git a/blobs/AQADva0xG50IQUR-.jpg b/blobs/AQADva0xG50IQUR-.jpg new file mode 100644 index 0000000..9b38bfd Binary files /dev/null and b/blobs/AQADva0xG50IQUR-.jpg differ diff --git a/blobs/AQADwa0xG04Q4UZ-.jpg b/blobs/AQADwa0xG04Q4UZ-.jpg new file mode 100644 index 0000000..1eb4c66 Binary files /dev/null and b/blobs/AQADwa0xG04Q4UZ-.jpg differ diff --git a/blobs/AQADx60xGyt2sUZ-.jpg b/blobs/AQADx60xGyt2sUZ-.jpg new file mode 100644 index 0000000..9787adc Binary files /dev/null and b/blobs/AQADx60xGyt2sUZ-.jpg differ diff --git a/blobs/AQADxK0xG5twMUd-.jpg b/blobs/AQADxK0xG5twMUd-.jpg new file mode 100644 index 0000000..b91788d Binary files /dev/null and b/blobs/AQADxK0xG5twMUd-.jpg differ diff --git a/blobs/AQADxa4xG4i3CEd-.jpg b/blobs/AQADxa4xG4i3CEd-.jpg new file mode 100644 index 0000000..03896e8 Binary files /dev/null and b/blobs/AQADxa4xG4i3CEd-.jpg differ diff --git a/blobs/AQADxq0xGyt2sUZ-.jpg b/blobs/AQADxq0xGyt2sUZ-.jpg new file mode 100644 index 0000000..32741dc Binary files /dev/null and b/blobs/AQADxq0xGyt2sUZ-.jpg differ diff --git a/blobs/AQADy60xG5twMUd-.jpg b/blobs/AQADy60xG5twMUd-.jpg new file mode 100644 index 0000000..457dcb0 Binary files /dev/null and b/blobs/AQADy60xG5twMUd-.jpg differ diff --git a/blobs/AQADy64xGyXiWUR-.jpg b/blobs/AQADy64xGyXiWUR-.jpg new file mode 100644 index 0000000..c83f69f Binary files /dev/null and b/blobs/AQADy64xGyXiWUR-.jpg differ diff --git a/blobs/AQADyq0xG78WiUZ-.jpg b/blobs/AQADyq0xG78WiUZ-.jpg new file mode 100644 index 0000000..2ba273a Binary files /dev/null and b/blobs/AQADyq0xG78WiUZ-.jpg differ diff --git a/bus/bus.go b/bus/bus.go new file mode 100644 index 0000000..8900bf5 --- /dev/null +++ b/bus/bus.go @@ -0,0 +1,90 @@ +package bus + +import ( + "fmt" + "sync" + + "log/slog" + + "github.com/alex-savin/go-receipt-tracker/config" +) + +// Bus should be created by New(). +type Bus struct { + opts *config.Bus + log *slog.Logger + channels map[string]*eventChannel + mu sync.Mutex +} + +// New creates new Bus instance. +func New(opts *config.Bus, log *slog.Logger) *Bus { + b := &Bus{ + opts: opts, + log: log, + channels: make(map[string]*eventChannel), + } + + return b +} + +// Subscribe adds new subscriber to channel. +func (b *Bus) Subscribe(channelName, subName string) (chan Event, error) { + b.mu.Lock() + defer b.mu.Unlock() + + if _, ok := b.channels[channelName]; !ok { + b.channels[channelName] = newEventChannel() + b.log.Debug("created a new channel", "channel", channelName) + } + + ch := b.channels[channelName] + + if err := ch.addSubscriber(subName, b.opts.SubscriptionSize[channelName]); err != nil { + return nil, err + } + b.log.Debug("added a new subscriber to a channel", "channel", channelName, "subscriber", subName) + + return ch.subscribers[subName], nil +} + +// Unsubscribe removes subscriber from channel. +// If this is last subscriber channel will be removed. +func (b *Bus) Unsubscribe(channelName, subName string) { + b.mu.Lock() + defer b.mu.Unlock() + + channel, ok := b.channels[channelName] + if !ok { + return + } + channel.delSubscriber(subName) + + if len(channel.subscribers) == 0 { + delete(b.channels, channelName) + } +} + +// Publish data to channel. +func (b *Bus) Publish(channelName string, payload interface{}) error { + channel, ok := b.channels[channelName] + if !ok { + return fmt.Errorf("channel %s don't exists", channelName) + } + e := Event{ + ChannelName: channelName, + Payload: payload, + } + + for subName, subscriber := range channel.subscribers { + b.log.Debug("some channel info", "channel", channelName, "cap", cap(subscriber), "len", len(subscriber)) + if cap(subscriber) > 0 && len(subscriber) >= cap(subscriber) { + b.log.Info("channel %s for subscriber %s is full, not publishing new messages", channelName, subName) + continue + } + subscriber <- e + b.log.Debug("published a new message to a channel", "channel", channelName, "subscriber", subName) + // b.log.Debug("published a new message to a channel", "channel", channelName, "subscriber", subName, "payload", payload) + } + return nil +} diff --git a/bus/bus_test.go_ b/bus/bus_test.go_ new file mode 100644 index 0000000..950563a --- /dev/null +++ b/bus/bus_test.go_ @@ -0,0 +1,495 @@ +package bus + +import ( + "errors" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestNewEventChannel(t *testing.T) { + ch := newEventChannel() + require.Equal(t, 0, len(ch.subscribers)) +} + +func TestAddSubscriber(t *testing.T) { + tests := []struct { + inputEventChannel *eventChannel + inputSubscriptionName string + inputSubscriptionSize int + expectedError error + expectedSubscribers int + expectedSubscriptionSize int + }{ + { + inputEventChannel: &eventChannel{ + subscribers: make(map[string]chan Event), + }, + inputSubscriptionName: "test", + inputSubscriptionSize: 1024, + expectedSubscribers: 1, + expectedSubscriptionSize: 1024, + }, + { + inputEventChannel: &eventChannel{ + subscribers: map[string]chan Event{ + "existing": make(chan Event), + }, + }, + inputSubscriptionName: "test", + inputSubscriptionSize: 1024, + expectedSubscribers: 2, + expectedSubscriptionSize: 1024, + }, + { + inputEventChannel: &eventChannel{ + subscribers: map[string]chan Event{ + "existing": make(chan Event), + }, + }, + inputSubscriptionName: "test", + inputSubscriptionSize: 0, + expectedSubscribers: 2, + expectedSubscriptionSize: 0, + }, + { + inputEventChannel: &eventChannel{ + subscribers: map[string]chan Event{ + "test": make(chan Event), + }, + }, + inputSubscriptionName: "test", + inputSubscriptionSize: 1024, + expectedSubscribers: 1, + expectedSubscriptionSize: 0, + expectedError: errors.New("subscriber test already exists"), + }, + } + + for _, test := range tests { + err := test.inputEventChannel.addSubscriber(test.inputSubscriptionName, test.inputSubscriptionSize) + require.Equal(t, test.expectedError, err) + require.Equal(t, test.expectedSubscribers, len(test.inputEventChannel.subscribers)) + require.Equal(t, test.expectedSubscriptionSize, cap(test.inputEventChannel.subscribers[test.inputSubscriptionName])) + } +} + +func TestDelSubscriber(t *testing.T) { + tests := []struct { + inputEventChannel *eventChannel + inputSubscriptionName string + expectedSubscribers int + expectedSubscriptionSize int + }{ + { + inputEventChannel: &eventChannel{ + subscribers: make(map[string]chan Event), + }, + inputSubscriptionName: "test", + expectedSubscribers: 0, + }, + { + inputEventChannel: &eventChannel{ + subscribers: map[string]chan Event{ + "existing": make(chan Event), + }, + }, + inputSubscriptionName: "test", + expectedSubscribers: 1, + }, + { + inputEventChannel: &eventChannel{ + subscribers: map[string]chan Event{ + "test": make(chan Event), + }, + }, + inputSubscriptionName: "test", + expectedSubscribers: 0, + }, + } + for _, test := range tests { + test.inputEventChannel.delSubscriber(test.inputSubscriptionName) + require.Equal(t, test.expectedSubscribers, len(test.inputEventChannel.subscribers)) + } + +} + +func TestNew(t *testing.T) { + b := New() + require.Equal(t, 0, len(b.channels)) +} + +func TestSubscribe(t *testing.T) { + tests := []struct { + inputBus *Bus + inputChannelName string + inputSubscriptionName string + inputSubscriptionSize int + expectedError error + expectedChannels int + expectedSubscribers int + expectedSubscriptionSize int + }{ + { + inputBus: New(), + inputChannelName: "test", + inputSubscriptionName: "test", + inputSubscriptionSize: 1024, + expectedChannels: 1, + expectedSubscribers: 1, + expectedSubscriptionSize: 1024, + }, + { + inputBus: &Bus{ + channels: map[string]*eventChannel{ + "existing": newEventChannel(), + }, + }, + inputChannelName: "test", + inputSubscriptionName: "test", + inputSubscriptionSize: 1024, + expectedChannels: 2, + expectedSubscribers: 1, + expectedSubscriptionSize: 1024, + }, + { + inputBus: &Bus{ + channels: map[string]*eventChannel{ + "test": { + subscribers: map[string]chan Event{ + "existing": make(chan Event), + }, + }, + }, + }, + inputChannelName: "test", + inputSubscriptionName: "test", + inputSubscriptionSize: 1024, + expectedChannels: 1, + expectedSubscribers: 2, + expectedSubscriptionSize: 1024, + }, + { + inputBus: &Bus{ + channels: map[string]*eventChannel{ + "test": { + subscribers: map[string]chan Event{ + "test": make(chan Event), + }, + }, + }, + }, + inputChannelName: "test", + inputSubscriptionName: "test", + inputSubscriptionSize: 1024, + expectedChannels: 1, + expectedSubscribers: 1, + expectedSubscriptionSize: 0, + expectedError: errors.New("subscriber test already exists"), + }, + } + for _, test := range tests { + ch, err := test.inputBus.Subscribe(test.inputChannelName, test.inputSubscriptionName, test.inputSubscriptionSize) + require.Equal(t, test.expectedError, err) + require.Equal(t, test.expectedChannels, len(test.inputBus.channels)) + require.Equal(t, test.expectedSubscribers, len(test.inputBus.channels[test.inputChannelName].subscribers)) + require.Equal(t, test.expectedSubscriptionSize, cap(ch)) + } +} + +func TestUnsubscribe(t *testing.T) { + tests := []struct { + inputBus *Bus + inputChannelName string + inputSubscriptionName string + expectedChannels int + expectedSubscribers int + }{ + { + inputBus: New(), + inputChannelName: "test", + inputSubscriptionName: "test", + expectedChannels: 0, + expectedSubscribers: 0, + }, + { + inputBus: &Bus{ + channels: map[string]*eventChannel{ + "existing": newEventChannel(), + }, + }, + inputChannelName: "test", + inputSubscriptionName: "test", + expectedChannels: 1, + expectedSubscribers: 0, + }, + { + inputBus: &Bus{ + channels: map[string]*eventChannel{ + "test": { + subscribers: map[string]chan Event{ + "existing": make(chan Event), + }, + }, + }, + }, + inputChannelName: "test", + inputSubscriptionName: "test", + expectedChannels: 1, + expectedSubscribers: 1, + }, + { + inputBus: &Bus{ + channels: map[string]*eventChannel{ + "test": { + subscribers: map[string]chan Event{ + "test": make(chan Event), + }, + }, + }, + }, + inputChannelName: "test", + inputSubscriptionName: "test", + expectedChannels: 0, + expectedSubscribers: 0, + }, + { + inputBus: &Bus{ + channels: map[string]*eventChannel{ + "test": { + subscribers: map[string]chan Event{ + "existing": make(chan Event), + "test": make(chan Event), + }, + }, + }, + }, + inputChannelName: "test", + inputSubscriptionName: "test", + expectedChannels: 1, + expectedSubscribers: 1, + }, + } + for _, test := range tests { + test.inputBus.Unsubscribe(test.inputChannelName, test.inputSubscriptionName) + require.Equal(t, test.expectedChannels, len(test.inputBus.channels)) + if test.expectedSubscribers > 0 { + require.Equal(t, test.expectedSubscribers, len(test.inputBus.channels[test.inputChannelName].subscribers)) + } + } +} + +func TestPublish(t *testing.T) { + tests := []struct { + inputSubscribers func(*Bus, chan map[string]int) + inputPublish func(*Bus) + expectedOutput map[string]int + }{ + { + inputSubscribers: func(b *Bus, output chan map[string]int) { + s1, err := b.Subscribe("test", "s1", 1024) + require.Nil(t, err) + go func() { + r := make(map[string]int) + for { + select { + case e := <-s1: + require.Equal(t, "test", e.ChannelName) + r["s1"]++ + case <-time.After(5 * time.Millisecond): + output <- r + return + } + } + }() + }, + inputPublish: func(b *Bus) { + err := b.Publish("test", "test") + require.Nil(t, err) + }, + expectedOutput: map[string]int{"s1": 1}, + }, + { + inputSubscribers: func(b *Bus, output chan map[string]int) { + s1, err := b.Subscribe("test", "s1", 1024) + require.Nil(t, err) + go func() { + r := make(map[string]int) + for { + select { + case e := <-s1: + require.Equal(t, "test", e.ChannelName) + r["s1"]++ + case <-time.After(5 * time.Millisecond): + output <- r + return + } + } + }() + }, + inputPublish: func(b *Bus) { + err := b.Publish("test", "test") + require.Nil(t, err) + err = b.Publish("test", "test") + require.Nil(t, err) + }, + expectedOutput: map[string]int{"s1": 2}, + }, + { + inputSubscribers: func(b *Bus, output chan map[string]int) { + s1, err := b.Subscribe("test", "s1", 1024) + require.Nil(t, err) + s2, err := b.Subscribe("test", "s2", 1024) + require.Nil(t, err) + go func() { + r := make(map[string]int) + for { + select { + case e := <-s1: + require.Equal(t, "test", e.ChannelName) + r["s1"]++ + case e := <-s2: + require.Equal(t, "test", e.ChannelName) + r["s2"]++ + case <-time.After(5 * time.Millisecond): + output <- r + return + } + } + }() + }, + inputPublish: func(b *Bus) { + err := b.Publish("test", "test") + require.Nil(t, err) + err = b.Publish("test", "test") + require.Nil(t, err) + }, + expectedOutput: map[string]int{"s1": 2, "s2": 2}, + }, + { + inputSubscribers: func(b *Bus, output chan map[string]int) { + s1, err := b.Subscribe("test", "s1", 1024) + require.Nil(t, err) + s2, err := b.Subscribe("test2", "s2", 1024) + require.Nil(t, err) + go func() { + r := make(map[string]int) + for { + select { + case e := <-s1: + require.Equal(t, "test", e.ChannelName) + r["s1"]++ + case e := <-s2: + require.Equal(t, "test2", e.ChannelName) + r["s2"]++ + case <-time.After(5 * time.Millisecond): + output <- r + return + } + } + }() + }, + inputPublish: func(b *Bus) { + err := b.Publish("test", "test") + require.Nil(t, err) + err = b.Publish("test", "test") + require.Nil(t, err) + }, + expectedOutput: map[string]int{"s1": 2}, + }, + { + inputSubscribers: func(b *Bus, output chan map[string]int) { + s1, err := b.Subscribe("test", "s1", 1024) + require.Nil(t, err) + s2, err := b.Subscribe("test", "s2", 1) + require.Nil(t, err) + go func() { + r := make(map[string]int) + e := <-s2 + require.Equal(t, "test", e.ChannelName) + r["s2"]++ + for { + select { + case e := <-s1: + require.Equal(t, "test", e.ChannelName) + r["s1"]++ + case <-time.After(5 * time.Millisecond): + output <- r + return + } + } + }() + }, + inputPublish: func(b *Bus) { + err := b.Publish("test", "test") + require.Nil(t, err) + err = b.Publish("test", "test") + require.Nil(t, err) + }, + expectedOutput: map[string]int{"s1": 2, "s2": 1}, + }, + { + inputSubscribers: func(b *Bus, output chan map[string]int) { + ch, err := b.Subscribe("test", "s1", 1024) + require.Nil(t, err) + go func() { + var mu sync.Mutex + r := make(map[string]int) + var wg sync.WaitGroup + wg.Add(3) + w := func(r map[string]int, n string) { + e := <-ch + require.Equal(t, "test", e.ChannelName) + mu.Lock() + defer mu.Unlock() + r[n]++ + wg.Done() + } + go w(r, "s1") + go w(r, "s2") + go w(r, "s3") + wg.Wait() + output <- r + }() + }, + inputPublish: func(b *Bus) { + go func(b *Bus) { + err := b.Publish("test", "test") + require.Nil(t, err) + err = b.Publish("test", "test") + require.Nil(t, err) + err = b.Publish("test", "test") + require.Nil(t, err) + }(b) + }, + expectedOutput: map[string]int{"s1": 1, "s2": 1, "s3": 1}, + }, + { + inputSubscribers: func(b *Bus, output chan map[string]int) { + go func() { + r := make(map[string]int) + output <- r + }() + }, + inputPublish: func(b *Bus) { + err := b.Publish("test", "test") + require.Equal(t, errors.New("channel test don't exists"), err) + }, + expectedOutput: map[string]int{}, + }, + } + + for _, test := range tests { + b := New() + ch := make(chan map[string]int) + test.inputSubscribers(b, ch) + test.inputPublish(b) + + received := <-ch + + require.Equal(t, test.expectedOutput, received) + + } +} diff --git a/bus/events.go b/bus/events.go new file mode 100644 index 0000000..c4ac007 --- /dev/null +++ b/bus/events.go @@ -0,0 +1,38 @@ +package bus + +import ( + "fmt" +) + +// Event is used to transport user data over bus. +type Event struct { + ChannelName string + Payload interface{} +} + +// eventChannel stores subscriptions for channels. +type eventChannel struct { + subscribers map[string]chan Event +} + +// newEventChannel creates new eventChannel. +func newEventChannel() *eventChannel { + return &eventChannel{ + subscribers: make(map[string]chan Event), + } +} + +// addSubscriber adds new subscriber. +func (ch *eventChannel) addSubscriber(subName string, size int) error { + if _, ok := ch.subscribers[subName]; ok { + return fmt.Errorf("subscriber %s already exists", subName) + } + ch.subscribers[subName] = make(chan Event, size) + + return nil +} + +// delSubscriber removes subscriber. +func (ch *eventChannel) delSubscriber(subName string) { + delete(ch.subscribers, subName) +} diff --git a/bus/messages.go b/bus/messages.go new file mode 100644 index 0000000..257923f --- /dev/null +++ b/bus/messages.go @@ -0,0 +1,31 @@ +package bus + +import ( + "github.com/alex-savin/go-receipt-tracker/models" + tele "gopkg.in/telebot.v4" +) + +type Message struct { + TBCmd *models.TelegramBotCommand + TBParseMode string + TbContext tele.Context // + Text string // + Image *Image // + InlineKeyboard *tele.ReplyMarkup // + ReplyType string // +} + +type Image struct { + ID string // + Filename string // + Type string // + Base64 string // + Caption byte // + Parsed map[string]string // +} + +type ConnectionStatus struct { + WorkerID string + WorkerType string + IsConnected bool +} diff --git a/config.yml b/config.yml new file mode 100644 index 0000000..7711678 --- /dev/null +++ b/config.yml @@ -0,0 +1,47 @@ +--- +database: + dsn: "postgres://bun:bun@localhost:5432/bun?sslmode=disable" +parsers: + openai: + type: "openai" + apikey: "sk-svcacct-o2Ued-dZUXs6gKJ2Lk0YkEAUeZiBPp-PVOieoWYuMBhVoTDDl8jAMAvRjXMT5BT-8ZR9XTP8YEoLT3BlbkFJri_8f6i9iJMJmzSH3owy4nSQ4DUbq0drCTvdrqb7MnVsc_vJY7xuR5i-JWJt1Owm5pqQ0p4YR0wA" + model: "gpt-4o-mini" + gemini: + type: "gemini" + apikey: "AIzaSyBdyf4684EPy__xxLH5NDf8GImS1hGEQxE" + model: "gemini-1.5-flash" + claude: + type: "claude" + apikey: "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" + model: "claude-3-5-sonnet-latest" # Fastest claude-3-5-haiku-latest +# Default workspace +# apikey: "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" +telegram: + # NPFD bot API key2 + # apikey: "5523988787:AAGcElqNtIDCcZacgQrUddFjoQcm3PZ1_Vs" + # Receipt Tracker Bot + apikey: "7307273503:AAGpj5eEd8eC9Ophi8rCDvtmfG46GRf2XEs" + storagepath: "./blobs" +listeners: + stats: + port: 8889 +bus: + subscriptionsize: + "processor:image": 1024 + "processor:user_add": 1024 + "processor:user_edit": 1024 + "processor:receipt_add": 1024 + "processor:receipts_list": 1024 + "processor:items_list": 1024 + "parser:openai": 1024 + "parser:gemini": 1024 + "parser:claude": 1024 + "telegram:send": 1024 + "telegram:reply": 1024 + "telegram:edit_caption": 1024 +storagepath: "./blobs" +timezone: America/New_York +logging: + level: DEBUG + output: JSON + source: false diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..7345e0c --- /dev/null +++ b/config/config.go @@ -0,0 +1,171 @@ +package config + +import ( + "log/slog" + "os" + + "github.com/spf13/viper" +) + +var ( + Message_welcome = `Welcome to Receipts Tracker Bot – Your Ultimate Receipt Tracking Solution! + +We are thrilled to have you join our community. At Receipts Tracker, we are committed to making your financial management effortless and efficient. Our advanced receipt tracking system is designed to help you organize, store, and manage all your receipts in one secure place. + +Why Choose Receipts Tracker? + +Easy Organization: Seamlessly categorize and store your receipts for quick access. +Secure Storage: Enjoy peace of mind with our top-notch security measures. +Effortless Management: Track your expenses and stay on top of your finances with ease. +User-Friendly Interface: Navigate our intuitive platform with ease, even if you’re not tech-savvy. +Getting Started + +Sign Up: Create your free account in just a few clicks. +Upload Receipts: Start uploading your receipts using our easy-to-use tools. +Track and Manage: Monitor your expenses, generate reports, and stay organized effortlessly. +We are here to support you every step of the way. If you have any questions or need assistance, our dedicated customer support team is just a click away. + +Welcome aboard! We look forward to helping you take control of your financial management. + +Best regards, + +The Receipts Tracker Team` + + Request = `That is a receipt. +Please parse it and send back only these items: +- items (any item has a type, like (meat, eggs, sandwich, burger, alcohol, cigars, ammo, gasoline, cat food, dog food, candies and etc)). If quantity of the item is not present consider it as 1, but check it right nex to the item name/title, it could be on the nest line or present like (2@1.99). Check if the item is taxable (true/false) +- quantity (if it is gas put gallons number) +- prices (if it is a gas use prices per gallon) +- datetime (convert it to the US format) +- merchant (name, address and phone number, in the format 123-456-7890, if present) +- payment type (cash, credit card, wire transfer, paypal, venmo and etc) +- credit card type ["AMEX", "VISA", "MASTER", "DISCOVER" and etc], number (only last 4 digits) and nethod of credit card payment (example: swipe, chip, contactless and etc), if type unknown keep variables empty +- category of the purchase, like (grocery, gas, restaurant, dinner, cafe, coffee, alcohol, parking and etc) +- credit card fee, if it present, otherwise consider it 0.0 (float) +- tax (could be found as Sales Tax), otherwise consider it 0.0 (float) +- tips, if that field filled and present, otherwise consider it 0.0 (float) +- total (if you cannot find total calculate it) +- receipt type, could be one of the three options (itemized, transactional or combined). Itemized receipt contains only list of the purchased items and does not include transaction (payment) data. Transactional include only information about payment/transaction and missing itemized information. Combined if it has items description and transaction data +in json format, please use this as an example +json +{ + \"paid_at\": "10/14/2024 14:56:17", + \"type\": \"combined\", + \"merchant\": { + \"title\": \"Costco Wholesale\", + \"address\": \"123 John str, Manhattan, NY 10010\", + \"phone\": \"123-456-7890\" + } + \"payment_type\": \"credit card\", + \"credit_card\": { + \"type\": \"VISA\", + \"digits\": \"1234\", + \"method\": \"swipe\", + }, + \"category\": \"gas\", + \"tax\": 0.0, + \"total\": 38.32, + \"cc_fee\": 0.0, + \"tips\": 0.0, + \"items\": [ + { + \"item\": \"Regular Gas\", + \"quantity\": 13.740, + \"price\": 2.789, + \"category\": \"gasoline\" + \"taxable\": \"false\", + } + ] +} + +If you see column \"You pay\", please use that price. +Do not add any other text to the response. +Do not wrap output in markdown, only pure json` + // ApiGenimi = "AIzaSyBdyf4684EPy__xxLH5NDf8GImS1hGEQxE" + // ApiOpenAi = "sk-svcacct-o2Ued-dZUXs6gKJ2Lk0YkEAUeZiBPp-PVOieoWYuMBhVoTDDl8jAMAvRjXMT5BT-8ZR9XTP8YEoLT3BlbkFJri_8f6i9iJMJmzSH3owy4nSQ4DUbq0drCTvdrqb7MnVsc_vJY7xuR5i-JWJt1Owm5pqQ0p4YR0wA" + // ApiTelegram = "7307273503:AAGpj5eEd8eC9Ophi8rCDvtmfG46GRf2XEs" +) + +// Config . +type Config struct { + Telegram *Telegram `json:"telegram" yaml:"telegram"` + Parsers map[string]*Parser `json:"parsers" yaml:"parsers"` + DataBase *DataBase `json:"database" yaml:"database"` + Listeners map[string]*Listener `json:"listeners" yaml:"listeners"` + Bus *Bus `json:"bus" yaml:"bus"` + StoragePath string `json:"storage_path" yaml:"storage_path"` + Timezone string `json:"timezone" yaml:"timezone"` + Logging *Logging `json:"logging" yaml:"logging"` +} + +// Parser . +type Parser struct { + Type string `json:"type" yaml:"type"` + ApiKey string `json:"apikey" yaml:"apikey"` + Model string `json:"model,omitempty" yaml:"model,omitempty"` +} + +// Telegram . +type Telegram struct { + ApiKey string `json:"apikey" yaml:"apikey"` + StoragePath string `json:"storagepath" yaml:"storagepath"` +} + +// DB . +type DataBase struct { + DSN string `json:"dsn" yaml:"dsn"` +} + +// Bus . +type Bus struct { + SubscriptionSize map[string]int `json:"subscriptionsize" yaml:"subscriptionsize"` +} + +// Listeners . +type Listener struct { + ID string + Port int `json:"port" yaml:"port"` + Cert string `json:"cert,omitempty" yaml:"cert,omitempty"` + PKey string `json:"pkey,omitempty" yaml:"pkey,omitempty"` +} + +// Logging . +type Logging struct { + Level string `json:"level" yaml:"level"` + Output string `json:"output" yaml:"output"` + Source bool `json:"source,omitempty" yaml:"source,omitempty"` +} + +// New . +func New() (*Config, error) { + viper.SetConfigName("config") // name of config file (without extension) + viper.SetConfigType("yml") // REQUIRED if the config file does not have the extension in the name + viper.AddConfigPath("./") // optionally look for config in the working directory + viper.AddConfigPath("/etc/tracker") // optionally look for config in the working directory + + viper.AutomaticEnv() + + viper.SetDefault("Timezone", "America/New_York") + viper.SetDefault("Logging.Level", "DEBUG") + viper.SetDefault("Logging.Output", "TEXT") + viper.SetDefault("Logging.Source", "false") + + err := viper.ReadInConfig() // Find and read the config file + if err != nil { + slog.Error("cannot open config file", "error", err.Error()) + os.Exit(1) + } + + // viper.WatchConfig() + // viper.OnConfigChange(func(e fsnotify.Event) { + // log.Println("Config file changed:", e.Name) + // }) + + var config Config + if err := viper.Unmarshal(&config); err != nil { + slog.Error("cannot unmarshal config file", "error", err.Error()) + os.Exit(1) + } + + return &config, err +} diff --git a/database/database.go b/database/database.go new file mode 100644 index 0000000..344cf21 --- /dev/null +++ b/database/database.go @@ -0,0 +1,48 @@ +package database + +import ( + "context" + "database/sql" + "log/slog" + + "github.com/alex-savin/go-receipt-tracker/config" + "github.com/alex-savin/go-receipt-tracker/models" + "github.com/uptrace/bun" + "github.com/uptrace/bun/dialect/pgdialect" + "github.com/uptrace/bun/driver/pgdriver" + "github.com/uptrace/bun/extra/bundebug" +) + +func NewDB(cfg *config.DataBase, log *slog.Logger) (*bun.DB, error) { + ctx := context.Background() + + // dsn := "postgres://bun:bun@localhost:5432/bun?sslmode=disable" + sqldb := sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(cfg.DSN))) + + db := bun.NewDB(sqldb, pgdialect.New()) + db.AddQueryHook(bundebug.NewQueryHook(bundebug.WithVerbose(true))) + + // Register models before loading fixtures. + db.RegisterModel((*models.GroupToUser)(nil), (*models.User)(nil), (*models.Group)(nil), (*models.Receipt)(nil), (*models.Merchant)(nil), (*models.CreditCard)(nil), (*models.Item)(nil), (*models.WhiteList)(nil), (*models.BlackList)(nil)) + + models := []interface{}{ + (*models.User)(nil), + (*models.Group)(nil), + (*models.GroupToUser)(nil), + (*models.Receipt)(nil), + (*models.Merchant)(nil), + (*models.CreditCard)(nil), + (*models.Item)(nil), + (*models.WhiteList)(nil), + (*models.BlackList)(nil), + // (*models.Settings)(nil), + } + + for _, model := range models { + if _, err := db.NewCreateTable().Model(model).Exec(ctx); err != nil { + continue + } + } + + return db, nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1aedeff --- /dev/null +++ b/go.mod @@ -0,0 +1,80 @@ +module github.com/alex-savin/go-receipt-tracker + +go 1.23 + +require ( + github.com/google/generative-ai-go v0.19.0 + github.com/liushuangls/go-anthropic/v2 v2.13.1 + github.com/sashabaranov/go-openai v1.36.1 + github.com/spf13/viper v1.19.0 + github.com/uptrace/bun v1.2.9 + github.com/uptrace/bun/dialect/pgdialect v1.2.9 + github.com/uptrace/bun/driver/pgdriver v1.2.9 + github.com/uptrace/bun/extra/bundebug v1.2.9 + google.golang.org/api v0.219.0 + gopkg.in/telebot.v4 v4.0.0-beta.4 +) + +require ( + cloud.google.com/go v0.118.1 // indirect + cloud.google.com/go/ai v0.10.0 // indirect + cloud.google.com/go/auth v0.14.1 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect + cloud.google.com/go/compute/metadata v0.6.0 // indirect + cloud.google.com/go/longrunning v0.6.4 // indirect + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/buger/jsonparser v1.1.1 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect + github.com/google/s2a-go v0.1.9 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect + github.com/googleapis/gax-go/v2 v2.14.1 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/magiconair/properties v1.8.9 // indirect + github.com/mailru/easyjson v0.9.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/puzpuzpuz/xsync/v3 v3.5.0 // indirect + github.com/sagikazarmark/locafero v0.7.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.12.0 // indirect + github.com/spf13/cast v1.7.1 // indirect + github.com/spf13/pflag v1.0.6 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect + github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + github.com/wk8/go-ordered-map/v2 v2.1.9-0.20240816141633-0a40785b4f41 // indirect + go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect + go.opentelemetry.io/otel v1.34.0 // indirect + go.opentelemetry.io/otel/metric v1.34.0 // indirect + go.opentelemetry.io/otel/trace v1.34.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/crypto v0.32.0 // indirect + golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c // indirect + golang.org/x/net v0.34.0 // indirect + golang.org/x/oauth2 v0.25.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.29.0 // indirect + golang.org/x/text v0.21.0 // indirect + golang.org/x/time v0.9.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250127172529-29210b9bc287 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287 // indirect + google.golang.org/grpc v1.70.0 // indirect + google.golang.org/protobuf v1.36.4 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + mellium.im/sasl v0.3.2 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..65106b5 --- /dev/null +++ b/go.sum @@ -0,0 +1,1122 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= +cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= +cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= +cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= +cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= +cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= +cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE= +cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U= +cloud.google.com/go v0.118.1 h1:b8RATMcrK9A4BH0rj8yQupPXp+aP+cJ0l6H7V9osV1E= +cloud.google.com/go v0.118.1/go.mod h1:CFO4UPEPi8oV21xoezZCrd3d81K4fFkDTEJu4R8K+9M= +cloud.google.com/go/ai v0.9.0 h1:r1Ig8O8+Qr3Ia3WfoO+gokD0fxB2Rk4quppuKjmGMsY= +cloud.google.com/go/ai v0.9.0/go.mod h1:28bKM/oxmRgxmRgI1GLumFv+NSkt+DscAg/gF+54zzY= +cloud.google.com/go/ai v0.10.0 h1:hwj6CI6sMKubXodoJJGTy/c2T1RbbLGM6TL3QoAvzU8= +cloud.google.com/go/ai v0.10.0/go.mod h1:kvnt2KeHqX8+41PVeMRBETDyQAp/RFvBWGdx/aGjNMo= +cloud.google.com/go/auth v0.10.2 h1:oKF7rgBfSHdp/kuhXtqU/tNDr0mZqhYbEh+6SiqzkKo= +cloud.google.com/go/auth v0.10.2/go.mod h1:xxA5AqpDrvS+Gkmo9RqrGGRh6WSNKKOXhY3zNOr38tI= +cloud.google.com/go/auth v0.11.0 h1:Ic5SZz2lsvbYcWT5dfjNWgw6tTlGi2Wc8hyQSC9BstA= +cloud.google.com/go/auth v0.11.0/go.mod h1:xxA5AqpDrvS+Gkmo9RqrGGRh6WSNKKOXhY3zNOr38tI= +cloud.google.com/go/auth v0.14.1 h1:AwoJbzUdxA/whv1qj3TLKwh3XX5sikny2fc40wUl+h0= +cloud.google.com/go/auth v0.14.1/go.mod h1:4JHUxlGXisL0AW8kXPtUF6ztuOksyfUQNFjfsOCXkPM= +cloud.google.com/go/auth/oauth2adapt v0.2.5 h1:2p29+dePqsCHPP1bqDJcKj4qxRyYCcbzKpFyKGt3MTk= +cloud.google.com/go/auth/oauth2adapt v0.2.5/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8= +cloud.google.com/go/auth/oauth2adapt v0.2.6 h1:V6a6XDu2lTwPZWOawrAa9HUK+DB2zfJyTuciBG5hFkU= +cloud.google.com/go/auth/oauth2adapt v0.2.6/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8= +cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M= +cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= +cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= +cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= +cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= +cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= +cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo= +cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= +cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= +cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= +cloud.google.com/go/longrunning v0.6.2 h1:xjDfh1pQcWPEvnfjZmwjKQEcHnpz6lHjfy7Fo0MK+hc= +cloud.google.com/go/longrunning v0.6.2/go.mod h1:k/vIs83RN4bE3YCswdXC5PFfWVILjm3hpEUlSko4PiI= +cloud.google.com/go/longrunning v0.6.3 h1:A2q2vuyXysRcwzqDpMMLSI6mb6o39miS52UEG/Rd2ng= +cloud.google.com/go/longrunning v0.6.3/go.mod h1:k/vIs83RN4bE3YCswdXC5PFfWVILjm3hpEUlSko4PiI= +cloud.google.com/go/longrunning v0.6.4 h1:3tyw9rO3E2XVXzSApn1gyEEnH2K9SynNQjMlBi3uHLg= +cloud.google.com/go/longrunning v0.6.4/go.mod h1:ttZpLCe6e7EXvn9OxpBRx7kZEB0efv8yBO6YnVMfhJs= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/goccy/go-yaml v1.9.5/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/generative-ai-go v0.18.0 h1:6ybg9vOCLcI/UpBBYXOTVgvKmcUKFRNj+2Cj3GnebSo= +github.com/google/generative-ai-go v0.18.0/go.mod h1:JYolL13VG7j79kM5BtHz4qwONHkeJQzOCkKXnpqtS/E= +github.com/google/generative-ai-go v0.19.0 h1:R71szggh8wHMCUlEMsW2A/3T+5LdEIkiaHSYgSpUgdg= +github.com/google/generative-ai-go v0.19.0/go.mod h1:JYolL13VG7j79kM5BtHz4qwONHkeJQzOCkKXnpqtS/E= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= +github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= +github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= +github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= +github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= +github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= +github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= +github.com/googleapis/gax-go/v2 v2.14.0 h1:f+jMrjBPl+DL9nI4IQzLUxMq7XrAqFYB7hBPqMNIe8o= +github.com/googleapis/gax-go/v2 v2.14.0/go.mod h1:lhBCnjdLrWRaPvLWhmc8IS24m9mr07qSYnHncrgo+zk= +github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= +github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0= +github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= +github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= +github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/liushuangls/go-anthropic/v2 v2.11.0 h1:YKyxDWQNaKPPgtLCgBH+JqzuznNWw8ZqQVeSdQNDMds= +github.com/liushuangls/go-anthropic/v2 v2.11.0/go.mod h1:8BKv/fkeTaL5R9R9bGkaknYBueyw2WxY20o7bImbOek= +github.com/liushuangls/go-anthropic/v2 v2.13.1 h1:RiLr5Ec7gDFu3BFiT/I65mdUI3+OxSjSHooqY/VLJ+Y= +github.com/liushuangls/go-anthropic/v2 v2.13.1/go.mod h1:BG+8VNOl7eoEjwW1yhOzFArWA9rzaG2aouth+PQyZgQ= +github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM= +github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= +github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4= +github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= +github.com/puzpuzpuz/xsync/v3 v3.5.0 h1:i+cMcpEDY1BkNm7lPDkCtE4oElsYLn+EKF8kAu2vXT4= +github.com/puzpuzpuz/xsync/v3 v3.5.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sagikazarmark/crypt v0.6.0/go.mod h1:U8+INwJo3nBv1m6A/8OBXAq7Jnpspk5AxSgDyEQcea8= +github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk= +github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0= +github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= +github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sashabaranov/go-openai v1.35.6 h1:oi0rwCvyxMxgFALDGnyqFTyCJm6n72OnEG3sybIFR0g= +github.com/sashabaranov/go-openai v1.35.6/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= +github.com/sashabaranov/go-openai v1.35.7 h1:icyrRbkYoKPa4rbO1WSInpJu3qDQrPEnsoJVZ6QymdI= +github.com/sashabaranov/go-openai v1.35.7/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= +github.com/sashabaranov/go-openai v1.36.1 h1:EVfRXwIlW2rUzpx6vR+aeIKCK/xylSrVYAx1TMTSX3g= +github.com/sashabaranov/go-openai v1.36.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= +github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= +github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= +github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo= +github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/uptrace/bun v1.2.5 h1:gSprL5xiBCp+tzcZHgENzJpXnmQwRM/A6s4HnBF85mc= +github.com/uptrace/bun v1.2.5/go.mod h1:vkQMS4NNs4VNZv92y53uBSHXRqYyJp4bGhMHgaNCQpY= +github.com/uptrace/bun v1.2.6 h1:lyGBQAhNiClchb97HA2cBnDeRxwTRLhSIgiFPXVisV8= +github.com/uptrace/bun v1.2.6/go.mod h1:xMgnVFf+/5xsrFBU34HjDJmzZnXbVuNEt/Ih56I8qBU= +github.com/uptrace/bun v1.2.9 h1:OOt2DlIcRUMSZPr6iXDFg/LaQd59kOxbAjpIVHddKRs= +github.com/uptrace/bun v1.2.9/go.mod h1:r2ZaaGs9Ru5bpGTr8GQfp8jp+TlCav9grYCPOu2CJSg= +github.com/uptrace/bun/dialect/pgdialect v1.2.5 h1:dWLUxpjTdglzfBks2x+U2WIi+nRVjuh7Z3DLYVFswJk= +github.com/uptrace/bun/dialect/pgdialect v1.2.5/go.mod h1:stwnlE8/6x8cuQ2aXcZqwDK/d+6jxgO3iQewflJT6C4= +github.com/uptrace/bun/dialect/pgdialect v1.2.6 h1:iNd1YLx619K+sZK+dRcWPzluurXYK1QwIkp9FEfNB/8= +github.com/uptrace/bun/dialect/pgdialect v1.2.6/go.mod h1:OL7d3qZLxKYP8kxNhMg3IheN1pDR3UScGjoUP+ivxJQ= +github.com/uptrace/bun/dialect/pgdialect v1.2.9 h1:caf5uFbOGiXvadV6pA5gn87k0awFFxL1kuuY3SpxnWk= +github.com/uptrace/bun/dialect/pgdialect v1.2.9/go.mod h1:m7L9JtOp/Lt8HccET70ULxplMweE/u0S9lNUSxz2duo= +github.com/uptrace/bun/driver/pgdriver v1.2.5 h1:+0Ofdg/tW7DsIXdTizYWapSex6Csh9VdBg6/bbAZWJw= +github.com/uptrace/bun/driver/pgdriver v1.2.5/go.mod h1:RsYV08Z72glum3swBhag7IBl1D+eztjWmodfcOZFHJ0= +github.com/uptrace/bun/driver/pgdriver v1.2.6 h1:dD7ckqIhVDayfYTwMKZ0dJM9AZfNJNBu5Cg/8g0EMOk= +github.com/uptrace/bun/driver/pgdriver v1.2.6/go.mod h1:ChrVrMZlRzPQHTP4QCm/p1FfQqgnYXWlES0GS9qjWEY= +github.com/uptrace/bun/driver/pgdriver v1.2.9 h1:wPXQwD78mYeR7o5tQTM/tgBaVd5QWMN/Nq02h+zHlsI= +github.com/uptrace/bun/driver/pgdriver v1.2.9/go.mod h1:YnlfL8hiQ++jSCPySK3k8BotpwbLL9SRDzssvts1Bm4= +github.com/uptrace/bun/extra/bundebug v1.2.5 h1:DsI/gl4jvq5tQ84yPqnlRYIQ4U6AouTqxJ8Y5Oijfz4= +github.com/uptrace/bun/extra/bundebug v1.2.5/go.mod h1:JFeYvklf5p92ZILXx4siMe2tEVn5JLelww3IGcJb4yA= +github.com/uptrace/bun/extra/bundebug v1.2.6 h1:5l61LXIR2YWk/gqGSq5esha+x/qPPyhtQKORAuTfV34= +github.com/uptrace/bun/extra/bundebug v1.2.6/go.mod h1:11C5ajtPrFcmIRo31TfQrmK5D2LgNIxxTQEZMz6lD2k= +github.com/uptrace/bun/extra/bundebug v1.2.9 h1:3SU66p+q76XhfeUUzl9XooVu7hVNueZ/2Q3J8S1uzCU= +github.com/uptrace/bun/extra/bundebug v1.2.9/go.mod h1:/rp83jYAtwZUQIz+L3KwvREXaSd5GQGPJUusqq+Qtis= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/wk8/go-ordered-map/v2 v2.1.9-0.20240816141633-0a40785b4f41 h1:rnB8ZLMeAr3VcqjfRkAm27qb8y6zFKNfuHvy1Gfe7KI= +github.com/wk8/go-ordered-map/v2 v2.1.9-0.20240816141633-0a40785b4f41/go.mod h1:DbzwytT4g/odXquuOCqroKvtxxldI4nb3nuesHF/Exo= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= +go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v2 v2.305.4/go.mod h1:Ud+VUwIi9/uQHOMA+4ekToJ12lTxlv0zB/+DHwTGEbU= +go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.57.0 h1:qtFISDHKolvIxzSs0gIaiPUPR0Cucb0F2coHC7ZLdps= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.57.0/go.mod h1:Y+Pop1Q6hCOnETWTW4NROK/q1hv50hM7yDaUTjG8lp8= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 h1:DheMAlT6POBP+gh8RUH19EOTnQIor5QE0uSRPtzCpSw= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0/go.mod h1:wZcGmeVO9nzP67aYSLDqXNWK87EZWhi7JWj1v7ZXf94= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I= +go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= +go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= +go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= +go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= +golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo= +golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak= +golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c h1:KL/ZBHXgKGVmuZBZ01Lt57yE5ws8ZPSkkihmEyq7FXc= +golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= +golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= +golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= +golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= +golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= +google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= +google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= +google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= +google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU= +google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= +google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= +google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= +google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= +google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= +google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= +google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= +google.golang.org/api v0.81.0/go.mod h1:FA6Mb/bZxj706H2j+j2d6mHEEaHBmbbWnkfvmorOCko= +google.golang.org/api v0.206.0 h1:A27GClesCSheW5P2BymVHjpEeQ2XHH8DI8Srs2HI2L8= +google.golang.org/api v0.206.0/go.mod h1:BtB8bfjTYIrai3d8UyvPmV9REGgox7coh+ZRwm0b+W8= +google.golang.org/api v0.207.0 h1:Fvt6IGCYjf7YLcQ+GCegeAI2QSQCfIWhRkmrMPj3JRM= +google.golang.org/api v0.207.0/go.mod h1:I53S168Yr/PNDNMi5yPnDc0/LGRZO6o7PoEbl/HY3CM= +google.golang.org/api v0.209.0 h1:Ja2OXNlyRlWCWu8o+GgI4yUn/wz9h/5ZfFbKz+dQX+w= +google.golang.org/api v0.209.0/go.mod h1:I53S168Yr/PNDNMi5yPnDc0/LGRZO6o7PoEbl/HY3CM= +google.golang.org/api v0.219.0 h1:nnKIvxKs/06jWawp2liznTBnMRQBEPpGo7I+oEypTX0= +google.golang.org/api v0.219.0/go.mod h1:K6OmjGm+NtLrIkHxv1U3a0qIf/0JOvAHd5O/6AoyKYE= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= +google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= +google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto/googleapis/api v0.0.0-20241113202542-65e8d215514f h1:M65LEviCfuZTfrfzwwEoxVtgvfkFkBUbFnRbxCXuXhU= +google.golang.org/genproto/googleapis/api v0.0.0-20241113202542-65e8d215514f/go.mod h1:Yo94eF2nj7igQt+TiJ49KxjIH8ndLYPZMIRSiRcEbg0= +google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 h1:pgr/4QbFyktUv9CtQ/Fq4gzEE6/Xs7iCXbktaGzLHbQ= +google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697/go.mod h1:+D9ySVjN8nY8YCVjc5O7PZDIdZporIDY3KaGfJunh88= +google.golang.org/genproto/googleapis/api v0.0.0-20250127172529-29210b9bc287 h1:A2ni10G3UlplFrWdCDJTl7D7mJ7GSRm37S+PDimaKRw= +google.golang.org/genproto/googleapis/api v0.0.0-20250127172529-29210b9bc287/go.mod h1:iYONQfRdizDB8JJBybql13nArx91jcUk7zCXEsOofM4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f h1:C1QccEa9kUwvMgEUORqQD9S17QesQijxjZ84sO82mfo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 h1:LWZqQOEjDyONlF1H6afSWpAL/znlREo2tHfLoe+8LMA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287 h1:J1H9f+LEdWAfHcez/4cvaVBox7cOYT+IU6rgqj5x++8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0= +google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA= +google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= +google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= +google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= +google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/telebot.v4 v4.0.0-beta.4 h1:9O3elrJ1GYJhNBpi7WDlBOaM/KQPvr5xpFPUEbA+dpk= +gopkg.in/telebot.v4 v4.0.0-beta.4/go.mod h1:jhcQjM/176jZm/s9Up/MzV5VFGPjyI8oiJhWvCMxayI= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +mellium.im/sasl v0.3.2 h1:PT6Xp7ccn9XaXAnJ03FcEjmAn7kK1x7aoXV6F+Vmrl0= +mellium.im/sasl v0.3.2/go.mod h1:NKXDi1zkr+BlMHLQjY3ofYuU4KSPFxknb8mfEu6SveY= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/hooks/debug/debug.go b/hooks/debug/debug.go new file mode 100644 index 0000000..e5a78d5 --- /dev/null +++ b/hooks/debug/debug.go @@ -0,0 +1,67 @@ +package debug + +import ( + "log/slog" + + app "github.com/alex-savin/go-receipt-tracker/app" +) + +// Options contains configuration settings for the debug output. +type Options struct { + // ShowPacketData bool // include decoded packet data (default false) + // ShowPings bool // show ping requests and responses (default false) + // ShowPasswords bool // show connecting user passwords (default false) +} + +// Hook is a debugging hook which logs additional low-level information from the server. +type Hook struct { + app.HookBase + Log *slog.Logger +} + +// ID returns the ID of the hook. +func (h *Hook) ID() string { + return "debug" +} + +// Provides indicates that this hook provides all methods. +func (h *Hook) Provides(b byte) bool { + return true +} + +// Init is called when the hook is initialized. +func (h *Hook) Init(config any) error { + if _, ok := config.(*Options); !ok && config != nil { + return app.ErrInvalidConfigType + } + + if config == nil { + config = new(Options) + } + + // h.config = config.(*Options) + + return nil +} + +// SetOpts is called when the hook receives inheritable server parameters. +func (h *Hook) SetOpts(l *slog.Logger) { + h.Log = l + h.Log.Debug("", "method", "SetOpts") +} + +// Stop is called when the hook is stopped. +func (h *Hook) Stop() error { + h.Log.Debug("", "method", "Stop") + return nil +} + +// OnStarted is called when the server starts. +func (h *Hook) OnStarted() { + h.Log.Debug("", "method", "OnStarted") +} + +// OnStopped is called when the server stops. +func (h *Hook) OnStopped() { + h.Log.Debug("", "method", "OnStopped") +} diff --git a/listeners/listeners.go b/listeners/listeners.go new file mode 100644 index 0000000..47c7c8c --- /dev/null +++ b/listeners/listeners.go @@ -0,0 +1,122 @@ +package listeners + +import ( + "crypto/tls" + "sync" + + "log/slog" +) + +// Config contains configuration values for a listener. +type Config struct { + // TLSConfig is a tls.Config configuration to be used with the listener. + // See examples folder for basic and mutual-tls use. + TLSConfig *tls.Config +} + +// Listener is an interface for network listeners. A network listener listens +// for incoming client connections and adds them to the server. +type Listener interface { + Init(*slog.Logger) error // open the network address + Serve() // starting actively listening for new connections + ID() string // return the id of the listener + Address() string // the address of the listener + Protocol() string // the protocol in use by the listener + Close() // stop and close the listener +} + +// Listeners contains the network listeners for the broker. +type Listeners struct { + ClientsWg sync.WaitGroup // a waitgroup that waits for all clients in all listeners to finish. + internal map[string]Listener // a map of active listeners. + sync.RWMutex +} + +// New returns a new instance of Listeners. +func New() *Listeners { + return &Listeners{ + internal: map[string]Listener{}, + } +} + +// Add adds a new listener to the listeners map, keyed on id. +func (l *Listeners) Add(val Listener) { + l.Lock() + defer l.Unlock() + l.internal[val.ID()] = val +} + +// Get returns the value of a listener if it exists. +func (l *Listeners) Get(id string) (Listener, bool) { + l.RLock() + defer l.RUnlock() + val, ok := l.internal[id] + return val, ok +} + +// Len returns the length of the listeners map. +func (l *Listeners) Len() int { + l.RLock() + defer l.RUnlock() + return len(l.internal) +} + +// Delete removes a listener from the internal map. +func (l *Listeners) Delete(id string) { + l.Lock() + defer l.Unlock() + delete(l.internal, id) +} + +// Serve starts a listener serving from the internal map. +func (l *Listeners) Serve(id string) { + l.RLock() + defer l.RUnlock() + listener := l.internal[id] + + go func() { + listener.Serve() + }() +} + +// ServeAll starts all listeners serving from the internal map. +func (l *Listeners) ServeAll() { + l.RLock() + i := 0 + ids := make([]string, len(l.internal)) + for id := range l.internal { + ids[i] = id + i++ + } + l.RUnlock() + + for _, id := range ids { + l.Serve(id) + } +} + +// Close stops a listener from the internal map. +func (l *Listeners) Close(id string) { + l.RLock() + defer l.RUnlock() + if listener, ok := l.internal[id]; ok { + listener.Close() + } +} + +// CloseAll iterates and closes all registered listeners. +func (l *Listeners) CloseAll() { + l.RLock() + i := 0 + ids := make([]string, len(l.internal)) + for id := range l.internal { + ids[i] = id + i++ + } + l.RUnlock() + + for _, id := range ids { + l.Close(id) + } + l.ClientsWg.Wait() +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..9aa1ed8 --- /dev/null +++ b/main.go @@ -0,0 +1,154 @@ +package main + +import ( + "fmt" + "log/slog" + "os" + "os/signal" + "syscall" + + "github.com/alex-savin/go-receipt-tracker/app" + "github.com/alex-savin/go-receipt-tracker/bus" + "github.com/alex-savin/go-receipt-tracker/config" + "github.com/alex-savin/go-receipt-tracker/database" + "github.com/alex-savin/go-receipt-tracker/hooks/debug" + "github.com/alex-savin/go-receipt-tracker/workers" +) + +// var log = *slog.Logger +var cfg = &config.Config{} + +const ( + LoggingOutputJson = "JSON" + LoggingOutputText = "TEXT" +) + +func configureLogging(config *config.Logging) *slog.Logger { //nolint:unparam + if config == nil { + return nil + } + + var level slog.Level + if err := level.UnmarshalText([]byte(config.Level)); err != nil { + slog.Warn(err.Error()) + slog.Warn(fmt.Sprintf("logging level not recognized, defaulting to level %s", slog.LevelInfo.String())) + level = slog.LevelInfo + } + + var handler slog.Handler + switch config.Output { + case LoggingOutputJson: + handler = slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{AddSource: cfg.Logging.Source, Level: level}) + case LoggingOutputText: + handler = slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{AddSource: cfg.Logging.Source, Level: level}) + default: + handler = slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{AddSource: cfg.Logging.Source, Level: level}) + } + + return slog.New(handler) +} + +func main() { + sigs := make(chan os.Signal, 1) + done := make(chan bool, 1) + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + go func() { + <-sigs + done <- true + }() + + var err error + cfg, err = config.New() + if err != nil { // Handle errors reading the config file + slog.Error("Fatal error config file", "error", err.Error()) + os.Exit(1) + } + + // LOGGER + logger := configureLogging(cfg.Logging) + logger.Debug("CONFIG", "config", cfg) + + // BUS + evBus := bus.New(cfg.Bus, logger) + + // DATABASE + db, err := database.NewDB(cfg.DataBase, logger) + if err != nil { + logger.Error(err.Error()) + os.Exit(1) + } + + app := app.New(cfg, evBus, db, logger) + + for _, opts := range cfg.Parsers { + if opts.Type == "openai" { + openai := workers.NewOpenAi(opts, evBus) + err = app.AddWorker(openai) + if err != nil { + app.Log.Error(err.Error()) + } + } + // if opts.Type == "claude" { + // claude := workers.NewClaude(opts, evBus) + // err = app.AddWorker(claude) + // if err != nil { + // app.Log.Error(err.Error()) + // } + // } + // if opts.Type == "gemini" { + // gemini := workers.NewGemini(opts, evBus) + // err = app.AddWorker(gemini) + // if err != nil { + // app.Log.Error(err.Error()) + // } + // } + } + + tb := workers.NewTelegramBot(cfg.Telegram, evBus) + err = app.AddWorker(tb) + if err != nil { + app.Log.Error(err.Error()) + os.Exit(1) + } + + // if len(cfg.Listeners) > 0 { + // for name, opts := range cfg.Listeners { + // listener := listeners.NewHTTPStats(name, ":"+strconv.Itoa(opts.Port), nil, app.Info) + // err = app.AddListener(listener) + // if err != nil { + // app.Log.Error(err.Error()) + // } + // } + // } + + err = app.AddHook(new(debug.Hook), nil) + if err != nil { + app.Log.Error(err.Error()) + } + + // err = app.AddHook(new(consul.Hook), &consul.Options{ + // Hostname: cfg.Consul.Host, + // Port: cfg.Consul.Port, + // DC: cfg.Consul.DataCenter, + // ServiceID: "srv-mysubaru-01", + // NodeName: "mysubaru", + // Tags: []string{"mysubaru", "hassio", "mqtt"}, + // LocalHost: cfg.Consul.Interfaces.WAN, + // LocalPort: cfg.Listeners["stats"].Port, + // }) + // if err != nil { + // app.Log.Error(err.Error()) + // } + + go func() { + err := app.Serve() + if err != nil { + app.Log.Error(err.Error()) + } + }() + + <-done + app.Log.Warn("caught signal, stopping...") + _ = app.Close() + app.Log.Info("main.go finished") +} diff --git a/models/blacklist.go b/models/blacklist.go new file mode 100644 index 0000000..8236547 --- /dev/null +++ b/models/blacklist.go @@ -0,0 +1,29 @@ +package models + +import ( + "context" + + "github.com/uptrace/bun" +) + +type BlackList struct { + bun.BaseModel `bun:"table:blacklist"` + UserID int64 `bun:",pk,autoincrement"` + Name string `bun:"name,type:text,unique" json:"name" yaml:"name"` + TelegramID int64 `bun:"telegram_id,type:int,unique" json:"telegram_id" yaml:"telegram_id"` + TelegramUsername string `bun:"telegram_username,type:text,unique" json:"telegram_username" yaml:"telegram_username"` +} + +var _ bun.AfterCreateTableHook = (*BlackList)(nil) + +func (*BlackList) AfterCreateTable(ctx context.Context, query *bun.CreateTableQuery) error { + _, err := query.DB().NewCreateIndex().Model((*BlackList)(nil)).Index("idx_bl_telegram_id").Column("telegram_id").Exec(ctx) + if err != nil { + return err + } + _, err = query.DB().NewCreateIndex().Model((*BlackList)(nil)).Index("idx_bl_telegram_username").Column("telegram_username").Exec(ctx) + if err != nil { + return err + } + return nil +} diff --git a/models/commands.go b/models/commands.go new file mode 100644 index 0000000..8c34501 --- /dev/null +++ b/models/commands.go @@ -0,0 +1,92 @@ +package models + +import ( + "fmt" + "log" + "reflect" + "regexp" + "strconv" +) + +var BtnCmd = map[string]int64{ + "user_add": 1, + "user_edit": 2, + "user_detete": 3, + "users_list": 4, + "group_add": 11, + "group_edit": 12, + "group_delete": 13, + "groups_list": 14, + "receipt_add": 21, + "receipt_edit": 22, + "receipt_delete": 23, + "receipts_list": 24, + "item_add": 31, + "item_edit": 32, + "item_delete": 33, + "items_list": 34, + "merchant": 96, + "category": 97, + "tax": 98, + "total": 99, +} + +// var btnPage = map[string]int64{ +// "prev": 1, +// "next": 2, +// } + +// TelegramBotCommand . +type TelegramBotCommand struct { + Cmd int64 // Internal Command ( user_add | user_edit | user_delete ) + ID int64 // Internal UserID + RefCmd int64 // Internal Reference Command + RefID int64 // Internal Reference ID + TelegramID int64 // Telegram UserID + Pagination int64 // Pagination + Extra string // Internal String filed to pass Telegram FileID +} + +// Unmarshal . +func (t *TelegramBotCommand) Unmarshal(c string) error { + r := regexp.MustCompile(`^(?P.*)\|(?P.*)\|(?P.*)\|(?P.*)\|(?P.*)\|(?P.*)\|(?P.*)$`) + m := r.FindStringSubmatch(c) + + res := make(map[string]string) + for i, name := range r.SubexpNames() { + if i != 0 && name != "" { + res[name] = m[i] + } + } + + for k, val := range res { + v := reflect.ValueOf(&t).Elem() + f := reflect.Indirect(v).FieldByName(k) + + if !f.IsValid() { + continue + } + if f.Type().Kind() == reflect.Int64 { + age, err := strconv.Atoi(val) + if err != nil { + continue + } + f.SetInt(int64(age)) + } else { + f.SetString(val) + } + } + log.Printf("CALLBACK (UNMARSHAL) %d|%d|%d|%d|%d|%d|%s", t.Cmd, t.ID, t.RefCmd, t.RefID, t.TelegramID, t.Pagination, t.Extra) + + return nil +} + +// Marshal . +func (t *TelegramBotCommand) Marshal() string { + return fmt.Sprintf("%d|%d|%d|%d|%d|%d|%s", t.Cmd, t.ID, t.RefCmd, t.RefID, t.TelegramID, t.Pagination, t.Extra) +} + +// String . +func String(t *TelegramBotCommand) string { + return t.Marshal() +} diff --git a/models/credit_cards.go b/models/credit_cards.go new file mode 100644 index 0000000..ed19135 --- /dev/null +++ b/models/credit_cards.go @@ -0,0 +1,50 @@ +package models + +import ( + "context" + "time" + + "github.com/uptrace/bun" +) + +type CreditCard struct { + bun.BaseModel `bun:"table:credit_cards"` + CreditCardID int64 `bun:",pk,autoincrement" json:"credit_card_id" yaml:"credit_card_id"` + UserID int64 `json:"user_id" yaml:"user_id"` + Title string `bun:"title,type:text" json:"title,omitempty" yaml:"title,omitempty"` + Bank string `bun:"bank,type:text" json:"bank,omitempty" yaml:"bank,omitempty"` + Type string `bun:"type,type:text" json:"type,omitempty" yaml:"type,omitempty"` + Digits string `bun:"digits,type:text,unique" json:"digits,omitempty" yaml:"digits,omitempty"` + Created time.Time `bun:",nullzero,notnull,default:current_timestamp" json:"created" yaml:"created"` + Updated time.Time `json:"updated" yaml:"updated"` +} + +type CreditCards []*CreditCard + +var _ bun.AfterCreateTableHook = (*CreditCard)(nil) + +func (*CreditCard) AfterCreateTable(ctx context.Context, query *bun.CreateTableQuery) error { + _, err := query.DB().NewCreateIndex().Model((*CreditCard)(nil)).Index("idx_credit_card_title").Column("title").Exec(ctx) + if err != nil { + return err + } + _, err = query.DB().NewCreateIndex().Model((*CreditCard)(nil)).Index("idx_credit_card_type").Column("type").Exec(ctx) + if err != nil { + return err + } + _, err = query.DB().NewCreateIndex().Model((*CreditCard)(nil)).Index("idx_credit_card_digits").Column("digits").Exec(ctx) + if err != nil { + return err + } + return nil +} + +// Add . +func (cc *CreditCard) Add(db *bun.DB, ctx context.Context) error { + _, err := db.NewInsert().Model(cc).On("CONFLICT (digits) DO UPDATE").Set("updated = EXCLUDED.updated").Exec(ctx) + if err != nil { + return err + } + + return nil +} diff --git a/models/groups.go b/models/groups.go new file mode 100644 index 0000000..3decf61 --- /dev/null +++ b/models/groups.go @@ -0,0 +1,92 @@ +package models + +import ( + "context" + + "github.com/uptrace/bun" +) + +type Group struct { + bun.BaseModel `bun:"table:groups"` + UserID int64 + GroupID int64 `bun:",pk,autoincrement"` + Name string `bun:"name,type:text" json:"name" yaml:"name"` + TelegramChatID int64 `bun:"telegram_chat_id,type:bigint" json:"telegram_chat_id" yaml:"telegram_chat_id"` + Users Users `bun:"m2m:group_to_users,join:Group=User"` +} + +type GroupToUser struct { + GroupID int64 `bun:",pk"` + Group *Group `bun:"rel:belongs-to,join:group_id=user_id"` + UserID int64 `bun:",pk"` + User *User `bun:"rel:belongs-to,join:user_id=group_id"` +} + +func NewGroup(j []byte) (*Group, error) { + g := &Group{} + return g, nil +} + +// Add . +func (g *Group) Add(db *bun.DB, ctx context.Context) error { + _, err := db.NewInsert().Model(g).Exec(ctx) + if err != nil { + return err + } + return nil +} + +// Update . +func (g *Group) Update(db *bun.DB, ctx context.Context) error { + _, err := db.NewUpdate().Model(g).WherePK().Exec(ctx) + if err != nil { + return err + } + return nil +} + +// Remove . +func (g *Group) Remove(db *bun.DB, ctx context.Context) error { + _, err := db.NewDelete().Model(g).WherePK().Exec(ctx) + if err != nil { + return err + } + return nil +} + +// UserAdd . +func (g *Group) UserAdd(db *bun.DB, ctx context.Context, u int64) error { + _, err := db.NewInsert().Model(g).Exec(ctx) + if err != nil { + return err + } + return nil +} + +// UserList . +func (g *Group) UserList(db *bun.DB, ctx context.Context) (Users, error) { + var users Users + _, err := db.NewSelect().Model(&users).ScanAndCount(ctx) + if err != nil { + return nil, err + } + return users, nil +} + +// UserRemove . +func (g *Group) UserRemove(db *bun.DB, ctx context.Context, u int) error { + _, err := db.NewInsert().Model(g).Exec(ctx) + if err != nil { + return err + } + return nil +} + +func GroupExists(db *bun.DB, ctx context.Context, tid int64) bool { + exists, err := db.NewSelect().Model((*Group)(nil)).Where("telegram_chat_id = ?", tid).Exists(ctx) + if err != nil { + panic(err) + } + + return exists +} diff --git a/models/items.go b/models/items.go new file mode 100644 index 0000000..5e6e773 --- /dev/null +++ b/models/items.go @@ -0,0 +1,79 @@ +package models + +import ( + "context" + + "github.com/uptrace/bun" +) + +type Item struct { + bun.BaseModel `bun:"table:items"` + ItemID int64 `bun:",pk,autoincrement" json:"item_id" yaml:"item_id"` + ReceiptID int64 `json:"receipt_id" yaml:"receipt_id"` + Title string `bun:"item,type:text" json:"item" yaml:"item"` + Quantity float64 `bun:"quantity" json:"quantity" yaml:"quantity"` + Price float64 `bun:"price" json:"price" yaml:"price"` + Category string `bun:"category" json:"category" yaml:"category"` +} + +type Items []*Item + +var _ bun.AfterCreateTableHook = (*Item)(nil) + +func (*Item) AfterCreateTable(ctx context.Context, query *bun.CreateTableQuery) error { + _, err := query.DB().NewCreateIndex().Model((*Item)(nil)).Index("idx_items_item").Column("item").Exec(ctx) + if err != nil { + return err + } + _, err = query.DB().NewCreateIndex().Model((*Item)(nil)).Index("idx_items_category").Column("category").Exec(ctx) + if err != nil { + return err + } + return nil +} + +// Add . +func (i *Item) Add(db *bun.DB, ctx context.Context) error { + _, err := db.NewInsert().Model(i).Exec(ctx) + if err != nil { + return err + } + return nil +} + +// Update . +func (i *Item) Update(db *bun.DB, ctx context.Context) error { + _, err := db.NewUpdate().Model(i).WherePK().Exec(ctx) + if err != nil { + return err + } + return nil +} + +// Add . +func (i *Items) Add(db *bun.DB, ctx context.Context) error { + _, err := db.NewInsert().Model(i).Exec(ctx) + if err != nil { + return err + } + return nil +} + +// List . +func (i *Items) List(db *bun.DB, ctx context.Context, r int) (Items, error) { + var items Items + _, err := db.NewSelect().Model(&items).Where("receipt_id = ?", r).ScanAndCount(ctx) + if err != nil { + return nil, err + } + return items, nil +} + +// Remove . +func (i *Items) Remove(db *bun.DB, ctx context.Context) error { + _, err := db.NewDelete().Model(i).WherePK().Exec(ctx) + if err != nil { + return err + } + return nil +} diff --git a/models/merchants.go b/models/merchants.go new file mode 100644 index 0000000..4bae205 --- /dev/null +++ b/models/merchants.go @@ -0,0 +1,47 @@ +package models + +import ( + "context" + "time" + + "github.com/uptrace/bun" +) + +type Merchant struct { + bun.BaseModel `bun:"table:merchants"` + MerchantID int64 `bun:",pk,autoincrement" json:"merchant_id" yaml:"merchant_id"` + Title string `bun:"title" json:"title" yaml:"title"` + Address string `bun:"address,type:text,unique" json:"address,omitempty" yaml:"address,omitempty"` + Phone string `bun:"phone,type:text,unique" json:"phone,omitempty" yaml:"phone,omitempty"` + Created time.Time `bun:",nullzero,notnull,default:current_timestamp" json:"created" yaml:"created"` + Updated time.Time `json:"updated" yaml:"updated"` +} + +var _ bun.AfterCreateTableHook = (*Merchant)(nil) + +func (*Merchant) AfterCreateTable(ctx context.Context, query *bun.CreateTableQuery) error { + _, err := query.DB().NewCreateIndex().Model((*Merchant)(nil)).Index("idx_merchant_title").Column("title").Exec(ctx) + if err != nil { + return err + } + _, err = query.DB().NewCreateIndex().Model((*Merchant)(nil)).Index("idx_merchant_address").Column("address").Exec(ctx) + if err != nil { + return err + } + _, err = query.DB().NewCreateIndex().Model((*Merchant)(nil)).Index("idx_merchant_phone").Column("phone").Exec(ctx) + if err != nil { + return err + } + return nil +} + +// Add . +func (m *Merchant) Add(db *bun.DB, ctx context.Context) error { + _, err := db.NewInsert().Model(m).On("CONFLICT (phone) DO UPDATE").Set("updated = EXCLUDED.updated").Exec(ctx) + // _, err := db.NewInsert().Model(m).Exec(ctx) + if err != nil { + return err + } + + return nil +} diff --git a/models/receipts.go b/models/receipts.go new file mode 100644 index 0000000..f145c15 --- /dev/null +++ b/models/receipts.go @@ -0,0 +1,119 @@ +package models + +import ( + "context" + "encoding/json" + "strings" + "time" + + "github.com/uptrace/bun" +) + +type Receipt struct { + bun.BaseModel `bun:"table:receipts"` + ReceiptID int64 `bun:",pk,autoincrement" json:"receipt_id" yaml:"receipt_id"` + UserID int64 `json:"user_id" yaml:"user_id"` + GroupID int64 `json:"group_id" yaml:"group_id"` + MerchantID int64 `json:"merchant_id" yaml:"merchant_id"` + CreditCardID int64 `json:"credit_card_id" yaml:"credit_card_id"` + CreditCard *CreditCard `bun:"credit_card,rel:belongs-to,join:credit_card_id=credit_card_id" json:"credit_card,omitempty" yaml:"credit_card,omitempty"` + Merchant *Merchant `bun:"merchant,rel:belongs-to,join:merchant_id=merchant_id" json:"merchant" yaml:"merchant"` + URL string `bun:"url" json:"url" yaml:"url"` + Type string `bun:"type" json:"type" yaml:"type"` + Category string `bun:"category" json:"category" yaml:"category"` + Items Items `bun:"rel:has-many,join:receipt_id=item_id" json:"items" yaml:"items"` + Tax float64 `bun:"tax" json:"tax,omitempty" yaml:"tax,omitempty"` + CCFee float64 `bun:"cc_fee" json:"cc_fee,omitempty" yaml:"cc_fee,omitempty"` + Tips float64 `bun:"tips" json:"tips,omitempty" yaml:"tips,omitempty"` + Total float64 `bun:"total" json:"total" yaml:"total"` + PaidWith string `bun:"paid_with" json:"paid_with" yaml:"paid_with"` + PaidAt string `bun:"paid_at" json:"paid_at" yaml:"paid_at"` + Created time.Time `bun:",nullzero,notnull,default:current_timestamp" json:"created" yaml:"created"` + Updated time.Time `json:"updated" yaml:"updated"` +} + +type Receipts []*Receipt + +var _ bun.AfterCreateTableHook = (*Receipt)(nil) + +// AfterCreateTable . +func (*Receipt) AfterCreateTable(ctx context.Context, query *bun.CreateTableQuery) error { + _, err := query.DB().NewCreateIndex().Model((*Receipt)(nil)).Index("idx_receipts_category").Column("category").Exec(ctx) + if err != nil { + return err + } + return nil +} + +// NewReceipt . +func NewReceipt(j []byte) (*Receipt, error) { + var r *Receipt + if err := json.Unmarshal([]byte(j), &r); err != nil { + panic(err) + } + return r, nil +} + +// Add . +func (r *Receipt) Add(db *bun.DB, ctx context.Context) error { + + err := r.Merchant.Add(db, ctx) + if err != nil { + return err + } + + err = r.CreditCard.Add(db, ctx) + if err != nil { + return err + } + + _, err = db.NewInsert().Model(r).Exec(ctx) + if err != nil { + return err + } + + if len(r.Items) > 0 { + for _, item := range r.Items { + item.ReceiptID = r.ReceiptID + item.Title = strings.Title(strings.ToLower(item.Title)) + } + err = r.Items.Add(db, ctx) + if err != nil { + return err + } + } + + return nil +} + +// Update . +func (r *Receipt) Update(db *bun.DB, ctx context.Context) error { + _, err := db.NewUpdate().Model(r).WherePK().Exec(ctx) + if err != nil { + return err + } + return nil +} + +// Remove . +func (r *Receipt) Remove(db *bun.DB, ctx context.Context) error { + err := r.Items.Remove(db, ctx) + if err != nil { + return err + } + _, err = db.NewDelete().Where("receipt_id = ?", r.ReceiptID).Exec(ctx) + if err != nil { + return err + } + return nil +} + +// Select . +func (r *Receipt) Select(db *bun.DB, ctx context.Context, id int64) error { + err := db.NewSelect().Model(r).Where("receipt_id = ?", id).Scan(ctx) + if err != nil { + return err + } + + return nil +} diff --git a/models/settings.go b/models/settings.go new file mode 100644 index 0000000..589a7c9 --- /dev/null +++ b/models/settings.go @@ -0,0 +1,5 @@ +package models + +type Settings struct { + OpenRegistration bool `bun:"open_registration,type:boolean" json:"open_registration" yaml:"open_registration"` +} diff --git a/models/users.go b/models/users.go new file mode 100644 index 0000000..7172f94 --- /dev/null +++ b/models/users.go @@ -0,0 +1,83 @@ +package models + +import ( + "context" + "time" + + "github.com/uptrace/bun" +) + +type User struct { + bun.BaseModel `bun:"table:users"` + UserID int64 `bun:",pk,autoincrement" json:"user_id" yaml:"user_id"` + GroupID int64 `json:"-" yaml:"-"` + Name string `bun:"name,type:text,unique" json:"name" yaml:"name"` + Email string `bun:"email,type:text" json:"email,omitempty" yaml:"email,omitempty"` + TelegramID int64 `bun:"telegram_id,type:int,unique" json:"telegram_id" yaml:"telegram_id"` + TelegramUsername string `bun:"telegram_username,type:text,unique" json:"telegram_username" yaml:"telegram_username"` + Receipts Receipts `bun:"rel:has-many,join:user_id=receipt_id"` + CreditCards CreditCards `bun:"rel:has-many,join:user_id=credit_card_id"` + Created time.Time `bun:",nullzero,notnull,default:current_timestamp" json:"created" yaml:"created"` + Updated time.Time `json:"updated" yaml:"updated"` +} + +type Users []*User + +var _ bun.AfterCreateTableHook = (*User)(nil) + +func (*User) AfterCreateTable(ctx context.Context, query *bun.CreateTableQuery) error { + _, err := query.DB().NewCreateIndex().Model((*User)(nil)).Index("idx_users_name").Column("name").Exec(ctx) + if err != nil { + return err + } + _, err = query.DB().NewCreateIndex().Model((*User)(nil)).Index("idx_users_telegram_id").Column("telegram_id").Exec(ctx) + if err != nil { + return err + } + _, err = query.DB().NewCreateIndex().Model((*User)(nil)).Index("idx_users_telegram_username").Column("telegram_username").Exec(ctx) + if err != nil { + return err + } + return nil +} + +func NewUser(j []byte) (*User, error) { + u := &User{} + // u := &User{Name: fmt.Sprintf("%s %s", c.Sender().FirstName, c.Sender().LastName), TelegramID: c.Sender().ID, TelegramUsername: c.Sender().Username} + return u, nil +} + +func (u *User) Add(db *bun.DB, ctx context.Context) error { + _, err := db.NewInsert().Model(u).Exec(ctx) + if err != nil { + return err + } + return nil +} + +func (u *User) Update(db *bun.DB, ctx context.Context) error { + _, err := db.NewUpdate().Model(u).WherePK().Exec(ctx) + if err != nil { + return err + } + + return nil +} + +func (u *User) Remove(db *bun.DB, ctx context.Context) error { + _, err := db.NewDelete().Model(u).WherePK().Exec(ctx) + if err != nil { + return err + } + + return nil +} + +func UserExists(db *bun.DB, ctx context.Context, tid int64) bool { + exists, err := db.NewSelect().Model((*User)(nil)).Where("telegram_id = ?", tid).Exists(ctx) + if err != nil { + panic(err) + } + + return exists +} diff --git a/models/whitelist.go b/models/whitelist.go new file mode 100644 index 0000000..4b15df9 --- /dev/null +++ b/models/whitelist.go @@ -0,0 +1,29 @@ +package models + +import ( + "context" + + "github.com/uptrace/bun" +) + +type WhiteList struct { + bun.BaseModel `bun:"table:whitelist"` + UserID int64 `bun:",pk,autoincrement"` + Name string `bun:"name,type:text,unique" json:"name" yaml:"name"` + TelegramID int64 `bun:"telegram_id,type:int,unique" json:"telegram_id" yaml:"telegram_id"` + TelegramUsername string `bun:"telegram_username,type:text,unique" json:"telegram_username" yaml:"telegram_username"` +} + +var _ bun.AfterCreateTableHook = (*WhiteList)(nil) + +func (*WhiteList) AfterCreateTable(ctx context.Context, query *bun.CreateTableQuery) error { + _, err := query.DB().NewCreateIndex().Model((*WhiteList)(nil)).Index("idx_wl_telegram_id").Column("telegram_id").Exec(ctx) + if err != nil { + return err + } + _, err = query.DB().NewCreateIndex().Model((*WhiteList)(nil)).Index("idx_wl_telegram_username").Column("telegram_username").Exec(ctx) + if err != nil { + return err + } + return nil +} diff --git a/system/system.go b/system/system.go new file mode 100644 index 0000000..5370efd --- /dev/null +++ b/system/system.go @@ -0,0 +1,27 @@ +package system + +import "sync/atomic" + +// Info contains atomic counters and values for various server statistics +type Info struct { + App string `json:"app"` + Version string `json:"version"` // the current version of the server + Started int64 `json:"started"` // the time the server started in unix seconds + Time int64 `json:"time"` // current time on the server + Uptime int64 `json:"uptime"` // the number of seconds the server has been online + MemoryAlloc int64 `json:"memory_alloc"` // memory currently allocated + Threads int64 `json:"threads"` // number of active goroutines, named as threads for platform ambiguity +} + +// Clone makes a copy of Info using atomic operation +func (i *Info) Clone() *Info { + return &Info{ + App: "mysubarumq", + Version: i.Version, + Started: atomic.LoadInt64(&i.Started), + Time: atomic.LoadInt64(&i.Time), + Uptime: atomic.LoadInt64(&i.Uptime), + MemoryAlloc: atomic.LoadInt64(&i.MemoryAlloc), + Threads: atomic.LoadInt64(&i.Threads), + } +} diff --git a/utils/utils.go b/utils/utils.go new file mode 100644 index 0000000..843481c --- /dev/null +++ b/utils/utils.go @@ -0,0 +1,10 @@ +package utils + +import ( + "encoding/json" +) + +func IsJSON(str string) bool { + var js json.RawMessage + return json.Unmarshal([]byte(str), &js) == nil +} diff --git a/workers/claude.go b/workers/claude.go new file mode 100644 index 0000000..2f97031 --- /dev/null +++ b/workers/claude.go @@ -0,0 +1,158 @@ +package workers + +import ( + "context" + "errors" + "log/slog" + "sync/atomic" + + "github.com/alex-savin/go-receipt-tracker/bus" + "github.com/alex-savin/go-receipt-tracker/config" + "github.com/alex-savin/go-receipt-tracker/utils" + "github.com/liushuangls/go-anthropic/v2" +) + +type Claude struct { + id string + model string + opts *config.Parser + client *anthropic.Client + subscriptions map[string]chan bus.Event // + bus *bus.Bus + log *slog.Logger + cancel context.CancelFunc // + end uint32 // ensure the close methods are only called once +} + +func NewClaude(cfg *config.Parser, b *bus.Bus) *Claude { + + ai := &Claude{ + id: cfg.Type, // "claude", + model: cfg.Model, // "gpt-4o-mini" + opts: cfg, + subscriptions: make(map[string]chan bus.Event), + bus: b, + } + + return ai +} + +func (ai *Claude) ID() string { + return ai.id +} + +func (ai *Claude) Type() string { + return "parser" +} + +func (ai *Claude) Init(log *slog.Logger) error { + ai.client = anthropic.NewClient(ai.opts.ApiKey) + ai.log = log + + return nil +} + +func (ai *Claude) Serve() { + if atomic.LoadUint32(&ai.end) == 1 { + return + } + + ai.subscribe("parser:" + ai.ID()) + ai.subscribe("parser:*") + + ctx, cancel := context.WithCancel(context.Background()) + ai.cancel = cancel + + go ai.eventLoop(ctx) +} + +func (ai *Claude) OneTime() error { + return nil +} + +func (ai *Claude) Stop() { +} + +func (ai *Claude) Close() { +} + +func (ai *Claude) subscribe(chn string) error { + s, err := ai.bus.Subscribe(chn, ai.Type()+":"+ai.ID()) + if err != nil { + ai.log.Error("couldn't subscribe to a channel", "channel", chn, "error", err.Error()) + return err + } + ai.subscriptions[chn] = s + + return nil +} + +// eventLoop loops forever +func (ai *Claude) eventLoop(ctx context.Context) { + ai.log.Debug(ai.ID() + " communication event loop started") + defer ai.log.Debug(ai.ID() + " communication event loop halted") + + for { + for chn, ch := range ai.subscriptions { + select { + case event := <-ch: + switch event.Payload.(type) { + case *bus.Message: + ai.log.Debug("got a new message to a channel", "channel", chn, "worker type", ai.Type(), "worker id", ai.ID()) + + msg := event.Payload.(*bus.Message) + res, err := ai.recognize(event.Payload.(*bus.Message).Image.Base64, event.Payload.(*bus.Message).Image.Type) + if err != nil { + ai.log.Error("got an error from parser ("+ai.ID()+")", "error", err) + } + + msg.Image.Parsed[ai.ID()] = res + err = ai.bus.Publish("processor:receipt_add", msg) + if err != nil { + ai.log.Error("couldn't publish to a channel", "channel", "processor:receipt_add", "error", err.Error()) + } + } + case <-ctx.Done(): + ai.log.Info("stopping " + ai.ID() + " communication event loop") + return + } + } + } +} + +func (ai *Claude) recognize(img, imgType string) (res string, err error) { + resp, err := ai.client.CreateMessages(context.Background(), anthropic.MessagesRequest{ + Model: anthropic.ModelClaude3Opus20240229, + Messages: []anthropic.Message{ + { + Role: anthropic.RoleUser, + Content: []anthropic.MessageContent{ + anthropic.NewImageMessageContent(anthropic.MessageContentSource{ + Type: "base64", + MediaType: imgType, + Data: img, + }), + anthropic.NewTextMessageContent(config.Request), + }, + }, + }, + MaxTokens: 1000, + }) + if err != nil { + var e *anthropic.APIError + if errors.As(err, &e) { + ai.log.Error("cannot recognize a receipt", "type", e.Type, "message", e.Message) + } else { + ai.log.Error("cannot recognize a receipt", "Messages error: %v\n", err) + } + return "", err + } + if !utils.IsJSON(*resp.Content[0].Text) { + ai.log.Error("Claude returned not valid JSON", "%+v", resp.Content[0].Text) + return "", err + } + + ai.log.Debug("recognition result", "worker", ai.ID(), "output", resp.Content[0].Text) + + return *resp.Content[0].Text, nil +} diff --git a/workers/gemini.go b/workers/gemini.go new file mode 100644 index 0000000..06e395e --- /dev/null +++ b/workers/gemini.go @@ -0,0 +1,178 @@ +package workers + +import ( + "context" + "fmt" + "log/slog" + "regexp" + "sync/atomic" + + "github.com/alex-savin/go-receipt-tracker/bus" + "github.com/alex-savin/go-receipt-tracker/config" + "github.com/alex-savin/go-receipt-tracker/utils" + "github.com/google/generative-ai-go/genai" + "google.golang.org/api/option" +) + +type Gemini struct { + id string + model string + opts *config.Parser + client *genai.Client + subscriptions map[string]chan bus.Event // + bus *bus.Bus + log *slog.Logger + cancel context.CancelFunc // + end uint32 // ensure the close methods are only called once +} + +func NewGemini(cfg *config.Parser, b *bus.Bus) *Gemini { + ai := &Gemini{ + id: cfg.Type, // "gemini", + model: cfg.Model, // "gemini-1.5-flash" + opts: cfg, + subscriptions: make(map[string]chan bus.Event), + bus: b, + } + // model := client.GenerativeModel("gemini-1.5-flash") + + return ai +} + +func (ai *Gemini) ID() string { + return ai.id +} + +func (ai *Gemini) Type() string { + return "parser" +} + +func (ai *Gemini) Init(log *slog.Logger) error { + ai.log = log + + ctx := context.Background() + client, err := genai.NewClient(ctx, option.WithAPIKey(ai.opts.ApiKey)) + if err != nil { + log.Error("cannot", "error", err) + } + ai.client = client + + return nil +} + +func (ai *Gemini) Serve() { + if atomic.LoadUint32(&ai.end) == 1 { + return + } + + ai.subscribe("parser:" + ai.ID()) + // ai.subscribe("parser:*") + + ctx, cancel := context.WithCancel(context.Background()) + ai.cancel = cancel + + go ai.eventLoop(ctx) +} + +func (ai *Gemini) OneTime() error { + return nil +} + +func (ai *Gemini) Stop() { + ai.client.Close() +} + +func (ai *Gemini) Close() { +} + +func (ai *Gemini) subscribe(chn string) error { + s, err := ai.bus.Subscribe(chn, ai.Type()+":"+ai.ID()) + if err != nil { + ai.log.Error("couldn't subscribe to a channel", "channel", chn, "error", err.Error()) + return err + } + ai.subscriptions[chn] = s + + return nil +} + +// eventLoop loops forever +func (ai *Gemini) eventLoop(ctx context.Context) { + ai.log.Debug(ai.ID() + " communication event loop started") + defer ai.log.Debug(ai.ID() + " communication event loop halted") + + for { + for chn, ch := range ai.subscriptions { + select { + case event := <-ch: + switch event.Payload.(type) { + case *bus.Message: + ai.log.Debug("got a new message to a channel", "channel", chn, "worker type", ai.Type(), "worker id", ai.ID()) + + msg := event.Payload.(*bus.Message) + res, err := ai.recognize(event.Payload.(*bus.Message).Image.Filename) + if err != nil { + ai.log.Error("got an error from parser ("+ai.ID()+")", "error", err) + } + + msg.Image.Parsed[ai.ID()] = res + err = ai.bus.Publish("processor:receipt_add", msg) + if err != nil { + ai.log.Error("couldn't publish to a channel", "channel", "processor:receipt_add", "error", err.Error()) + } + } + case <-ctx.Done(): + ai.log.Info("stopping " + ai.ID() + " communication event loop") + return + } + } + } +} + +func (ai *Gemini) recognize(img string) (res string, err error) { + ctx := context.Background() + + r := regexp.MustCompile(`^\x60{3}(?:json)\n([\s\S]*?)\n*\x60{3}\n*$`) + + file, err := ai.client.UploadFileFromPath(ctx, img, nil) + if err != nil { + ai.log.Error("cannot upload a receipt file", "Messages error: %v\n", err) + return "", err + } + defer ai.client.DeleteFile(ctx, file.Name) + + gotFile, err := ai.client.GetFile(ctx, file.Name) + if err != nil { + ai.log.Error("cannot get uploaded file name", "error", err) + } + ai.log.Debug("successfully uploaded file", "filename", gotFile.Name) + + model := ai.client.GenerativeModel("gemini-1.5-flash") + resp, err := model.GenerateContent(ctx, + genai.FileData{URI: file.URI}, + genai.Text(config.Request)) + if err != nil { + ai.log.Error("cannot recognize a receipt", "Messages error: %v\n", err) + } + + for _, cand := range resp.Candidates { + if cand.Content != nil { + for _, part := range cand.Content.Parts { + res += fmt.Sprint(part) + } + } + } + + if r.MatchString(res) { + res = r.FindStringSubmatch(res)[1] + } + + if !utils.IsJSON(res) { + ai.log.Error("Gemini returned not valid JSON", "%+v", res) + return "", err + } + + ai.log.Debug("recognition result", "worker", ai.ID(), "output", res) + + return res, nil +} diff --git a/workers/openai.go b/workers/openai.go new file mode 100644 index 0000000..47c91b0 --- /dev/null +++ b/workers/openai.go @@ -0,0 +1,160 @@ +package workers + +import ( + "context" + "log/slog" + "sync/atomic" + + "github.com/alex-savin/go-receipt-tracker/bus" + "github.com/alex-savin/go-receipt-tracker/config" + "github.com/alex-savin/go-receipt-tracker/utils" + "github.com/sashabaranov/go-openai" +) + +type OpenAi struct { + id string + model string + opts *config.Parser + client *openai.Client + subscriptions map[string]chan bus.Event // + bus *bus.Bus + log *slog.Logger + cancel context.CancelFunc // + end uint32 // ensure the close methods are only called once +} + +func NewOpenAi(cfg *config.Parser, b *bus.Bus) *OpenAi { + + ai := &OpenAi{ + id: cfg.Type, // "openai", + model: cfg.Model, // "gpt-4o-mini" + opts: cfg, + subscriptions: make(map[string]chan bus.Event), + bus: b, + } + return ai +} + +func (ai *OpenAi) ID() string { + return ai.id +} + +func (ai *OpenAi) Type() string { + return "parser" +} + +func (ai *OpenAi) Init(log *slog.Logger) error { + ai.client = openai.NewClient(ai.opts.ApiKey) + ai.log = log + + return nil +} + +func (ai *OpenAi) Serve() { + if atomic.LoadUint32(&ai.end) == 1 { + return + } + + ai.subscribe("parser:" + ai.ID()) + + ctx, cancel := context.WithCancel(context.Background()) + ai.cancel = cancel + + go ai.eventLoop(ctx) +} + +func (ai *OpenAi) OneTime() error { + return nil +} + +func (ai *OpenAi) Stop() { +} + +func (ai *OpenAi) Close() { + ai.cancel() +} + +func (ai *OpenAi) subscribe(chn string) error { + s, err := ai.bus.Subscribe(chn, ai.Type()+":"+ai.ID()) + if err != nil { + ai.log.Error("couldn't subscribe to a channel", "channel", chn, "error", err.Error()) + return err + } + ai.subscriptions[chn] = s + + return nil +} + +// eventLoop loops forever +func (ai *OpenAi) eventLoop(ctx context.Context) { + ai.log.Debug(ai.ID() + " communication event loop is started") + defer ai.log.Debug(ai.ID() + " communication event loop halted") + + for { + ai.log.Debug("looping over the subscriptions", "channel", "parser:"+ai.ID(), "worker", ai.ID()) + + select { + case event := <-ai.subscriptions["parser:"+ai.ID()]: + ai.log.Debug("worker got a new message from a channel", "channel", "parser:"+ai.ID(), "type", ai.Type(), "id", ai.ID()) + + switch event.Payload.(type) { + case *bus.Message: + msg := event.Payload.(*bus.Message) + res, err := ai.recognize("data:" + event.Payload.(*bus.Message).Image.Type + ";base64," + event.Payload.(*bus.Message).Image.Base64) + if err != nil { + ai.log.Error("got an error from parser ("+ai.ID()+")", "error", err) + } + + msg.Image.Parsed[ai.ID()] = res + err = ai.bus.Publish("processor:receipt_add", msg) + if err != nil { + ai.log.Error("couldn't publish to a channel", "channel", "processor:receipt_add", "error", err.Error()) + } + } + case <-ctx.Done(): + ai.log.Info("stopping " + ai.ID() + " communication event loop") + return + } + } +} + +func (ai *OpenAi) recognize(img string) (res string, err error) { + resp, err := ai.client.CreateChatCompletion( + context.Background(), + openai.ChatCompletionRequest{ + Model: openai.GPT4oMini, + MaxTokens: 1000, + Messages: []openai.ChatCompletionMessage{ + { + Role: openai.ChatMessageRoleUser, + MultiContent: []openai.ChatMessagePart{ + { + Type: openai.ChatMessagePartTypeText, + Text: config.Request, + }, + { + Type: openai.ChatMessagePartTypeImageURL, + ImageURL: &openai.ChatMessageImageURL{ + URL: img, + Detail: openai.ImageURLDetailHigh, + }, + }, + }, + }, + }, + }, + ) + + if err != nil { + ai.log.Error("error during recognition process", "%+v", resp.Choices[0].Message.Content) + return "", err + } + if !utils.IsJSON(resp.Choices[0].Message.Content) { + ai.log.Error("returned not valid JSON", "%+v", resp.Choices[0].Message.Content) + return "", err + } + + ai.log.Debug("recognition result", "worker", ai.ID(), "output", resp.Choices[0].Message.Content) + + return resp.Choices[0].Message.Content, nil +} diff --git a/workers/parser/claude.go b/workers/parser/claude.go new file mode 100644 index 0000000..e5810f6 --- /dev/null +++ b/workers/parser/claude.go @@ -0,0 +1,153 @@ +package parser + +import ( + "context" + "errors" + "io" + "log/slog" + "os" + "sync/atomic" + + "github.com/alex-savin/go-receipt-tracker/bus" + "github.com/alex-savin/go-receipt-tracker/config" + "github.com/alex-savin/go-receipt-tracker/utils" + "github.com/liushuangls/go-anthropic/v2" +) + +type Claude struct { + id string + model string + opts *config.Parser + client *anthropic.Client + subscriptions map[string]chan bus.Event // + bus *bus.Bus + log *slog.Logger + cancel context.CancelFunc // + end uint32 // ensure the close methods are only called once +} + +func NewClaude(cfg *config.Parser, bus *bus.Bus) *Claude { + + ai := &Claude{ + id: cfg.Type, // "claude", + model: cfg.Model, // "gpt-4o-mini" + opts: cfg, + bus: bus, + } + + return ai +} + +func (ai *Claude) ID() string { + return ai.id +} + +func (ai *Claude) Type() string { + return "parser" +} + +func (ai *Claude) Init(log *slog.Logger) error { + ai.client = anthropic.NewClient(ai.opts.ApiKey) + ai.log = log + + return nil +} + +func (ai *Claude) Serve() { + if atomic.LoadUint32(&ai.end) == 1 { + return + } + + ai.subscribe("parser:" + ai.ID()) + ai.subscribe("parser:*") + + ctx, cancel := context.WithCancel(context.Background()) + ai.cancel = cancel + + go ai.eventLoop(ctx) +} + +func (ai *Claude) Stop() { +} + +func (ai *Claude) subscribe(chn string) error { + s, err := ai.bus.Subscribe(chn, ai.Type()+":"+ai.ID()) + if err != nil { + ai.log.Error("couldn't subscribe to a channel", "channel", chn, "error", err.Error()) + return err + } + ai.subscriptions[chn] = s + + return nil +} + +// eventLoop loops forever +func (ai *Claude) eventLoop(ctx context.Context) { + ai.log.Debug(ai.ID() + " communication event loop started") + defer ai.log.Debug(ai.ID() + " communication event loop halted") + + for { + for chn, ch := range ai.subscriptions { + select { + case event := <-ch: + ai.log.Debug("got a new message to a channel", "channel", chn) + + for _, msg := range event.Payload.([]*bus.Message) { + ai.log.Debug("publishing mqtt message", "message", msg) + } + case <-ctx.Done(): + ai.log.Info("stopping " + ai.ID() + " communication event loop") + return + } + } + } +} + +func (ai *Claude) Recognize(img string) (res string, err error) { + image := "./" + img + ".jpg" + + imageMediaType := "image/jpeg" + imageFile, err := os.Open(image) + if err != nil { + return "", err + } + imageData, err := io.ReadAll(imageFile) + if err != nil { + return "", err + } + + resp, err := ai.client.CreateMessages(context.Background(), anthropic.MessagesRequest{ + Model: anthropic.ModelClaude3Opus20240229, + Messages: []anthropic.Message{ + { + Role: anthropic.RoleUser, + Content: []anthropic.MessageContent{ + anthropic.NewImageMessageContent(anthropic.MessageContentSource{ + Type: "base64", + MediaType: imageMediaType, + Data: imageData, + }), + anthropic.NewTextMessageContent(config.Request), + }, + }, + }, + MaxTokens: 1000, + }) + if err != nil { + var e *anthropic.APIError + if errors.As(err, &e) { + ai.log.Error("cannot recognize a receipt", "type", e.Type, "message", e.Message) + } else { + ai.log.Error("cannot recognize a receipt", "Messages error: %v\n", err) + } + return "", err + } + if !utils.IsJSON(*resp.Content[0].Text) { + ai.log.Error("Claude returned not valid JSON", "%+v", resp.Content[0].Text) + return "", err + } + + ai.log.Debug("recognition output", "%+v", resp.Content[0].Text) + + return *resp.Content[0].Text, nil +} diff --git a/workers/parser/gemini.go b/workers/parser/gemini.go new file mode 100644 index 0000000..75e040e --- /dev/null +++ b/workers/parser/gemini.go @@ -0,0 +1,153 @@ +package parser + +import ( + "context" + "fmt" + "log/slog" + "sync/atomic" + + "github.com/alex-savin/go-receipt-tracker/bus" + "github.com/alex-savin/go-receipt-tracker/config" + "github.com/google/generative-ai-go/genai" + "google.golang.org/api/option" +) + +type Gemini struct { + id string + model string + opts *config.Parser + client *genai.Client + subscriptions map[string]chan bus.Event // + bus *bus.Bus + log *slog.Logger + cancel context.CancelFunc // + end uint32 // ensure the close methods are only called once +} + +func NewGemini(cfg *config.Parser, bus *bus.Bus) *Gemini { + ai := &Gemini{ + id: cfg.Type, // "gemini", + model: cfg.Model, // "gemini-1.5-flash" + opts: cfg, + bus: bus, + } + // model := client.GenerativeModel("gemini-1.5-flash") + + return ai +} + +func (ai *Gemini) ID() string { + return ai.id +} + +func (ai *Gemini) Type() string { + return "parser" +} + +func (ai *Gemini) Init(log *slog.Logger) error { + ai.log = log + + ctx := context.Background() + client, err := genai.NewClient(ctx, option.WithAPIKey(ai.opts.ApiKey)) + if err != nil { + log.Error("cannot", "error", err) + } + ai.client = client + + return nil +} + +func (ai *Gemini) Serve() { + if atomic.LoadUint32(&ai.end) == 1 { + return + } + + ai.subscribe("parser:" + ai.ID()) + ai.subscribe("parser:*") + + ctx, cancel := context.WithCancel(context.Background()) + ai.cancel = cancel + + go ai.eventLoop(ctx) +} + +func (ai *Gemini) Stop() { + ai.client.Close() +} + +func (ai *Gemini) subscribe(chn string) error { + s, err := ai.bus.Subscribe(chn, ai.Type()+":"+ai.ID()) + if err != nil { + ai.log.Error("couldn't subscribe to a channel", "channel", chn, "error", err.Error()) + return err + } + ai.subscriptions[chn] = s + + return nil +} + +// eventLoop loops forever +func (ai *Gemini) eventLoop(ctx context.Context) { + ai.log.Debug(ai.ID() + " communication event loop started") + defer ai.log.Debug(ai.ID() + " communication event loop halted") + + for { + for chn, ch := range ai.subscriptions { + select { + case event := <-ch: + ai.log.Debug("got a new message to a channel", "channel", chn) + + for _, msg := range event.Payload.([]*bus.Message) { + ai.log.Debug("publishing mqtt message", "message", msg) + } + case <-ctx.Done(): + ai.log.Info("stopping " + ai.ID() + " communication event loop") + return + } + } + } +} + +func (ai *Gemini) Recognize(img string) (res string, err error) { + ctx := context.Background() + + image := "./" + img + ".jpg" + file, err := ai.client.UploadFileFromPath(ctx, image, nil) + if err != nil { + ai.log.Error("cannot upload a receipt file", "Messages error: %v\n", err) + } + defer ai.client.DeleteFile(ctx, file.Name) + + gotFile, err := ai.client.GetFile(ctx, file.Name) + if err != nil { + ai.log.Error("cannot get uploaded file name", err) + } + fmt.Println("Got file:", gotFile.Name) + + model := ai.client.GenerativeModel("gemini-1.5-flash") + resp, err := model.GenerateContent(ctx, + genai.FileData{URI: file.URI}, + genai.Text(config.Request)) + if err != nil { + ai.log.Error("cannot recognize a receipt", "Messages error: %v\n", err) + } + + printResponse(resp) + // if !utils.IsJSON(resp.Choices[0].Message.Content) { + // ai.log.Error("OpenAI returned not valid JSON", "%+v", resp.Choices[0].Message.Content) + // return "", err + // } + + return "", nil +} + +func printResponse(resp *genai.GenerateContentResponse) { + for _, cand := range resp.Candidates { + if cand.Content != nil { + for _, part := range cand.Content.Parts { + fmt.Println(part) + } + } + } + fmt.Println("---") +} diff --git a/workers/parser/openai.go b/workers/parser/openai.go new file mode 100644 index 0000000..5a6b146 --- /dev/null +++ b/workers/parser/openai.go @@ -0,0 +1,146 @@ +package parser + +import ( + "context" + "log/slog" + "sync/atomic" + + "github.com/alex-savin/go-receipt-tracker/bus" + "github.com/alex-savin/go-receipt-tracker/config" + "github.com/alex-savin/go-receipt-tracker/utils" + "github.com/sashabaranov/go-openai" +) + +type OpenAi struct { + id string + model string + opts *config.Parser + client *openai.Client + subscriptions map[string]chan bus.Event // + bus *bus.Bus + log *slog.Logger + cancel context.CancelFunc // + end uint32 // ensure the close methods are only called once +} + +func NewOpenAi(cfg *config.Parser, bus *bus.Bus) *OpenAi { + + ai := &OpenAi{ + id: cfg.Type, // "openai", + model: cfg.Model, // "gpt-4o-mini" + opts: cfg, + bus: bus, + } + return ai +} + +func (ai *OpenAi) ID() string { + return ai.id +} + +func (ai *OpenAi) Type() string { + return "parser" +} + +func (ai *OpenAi) Init(log *slog.Logger) error { + ai.client = openai.NewClient(ai.opts.ApiKey) + ai.log = log + + return nil +} + +func (ai *OpenAi) Serve() { + if atomic.LoadUint32(&ai.end) == 1 { + return + } + + ai.subscribe("parser:" + ai.ID()) + ai.subscribe("parser:*") + + ctx, cancel := context.WithCancel(context.Background()) + ai.cancel = cancel + + go ai.eventLoop(ctx) +} + +func (ai *OpenAi) subscribe(chn string) error { + s, err := ai.bus.Subscribe(chn, ai.Type()+":"+ai.ID()) + if err != nil { + ai.log.Error("couldn't subscribe to a channel", "channel", chn, "error", err.Error()) + return err + } + ai.subscriptions[chn] = s + + return nil +} + +// eventLoop loops forever +func (ai *OpenAi) eventLoop(ctx context.Context) { + ai.log.Debug(ai.ID() + " communication event loop started") + defer ai.log.Debug(ai.ID() + " communication event loop halted") + + for { + for chn, ch := range ai.subscriptions { + select { + case event := <-ch: + switch event.Payload.(type) { + case bus.Image: + ai.log.Debug("got a new message to a channel", "channel", chn) + + res, err := ai.recognize(event.Payload.(bus.Image).Base64) + if err != nil { + ai.log.Error("got an error from parser ("+ai.ID()+")", "error", err) + } + err = ai.bus.Publish("telegram:publish", res) + if err != nil { + ai.log.Error("couldn't publish to a channel", "channel", "telegram:publish", "error", err.Error()) + } + } + case <-ctx.Done(): + ai.log.Info("stopping " + ai.ID() + " communication event loop") + return + } + } + } +} + +func (ai *OpenAi) recognize(img string) (res string, err error) { + resp, err := ai.client.CreateChatCompletion( + context.Background(), + openai.ChatCompletionRequest{ + Model: openai.GPT4oMini, + MaxTokens: 1000, + Messages: []openai.ChatCompletionMessage{ + { + Role: openai.ChatMessageRoleUser, + MultiContent: []openai.ChatMessagePart{ + { + Type: openai.ChatMessagePartTypeText, + Text: config.Request, + }, + { + Type: openai.ChatMessagePartTypeImageURL, + ImageURL: &openai.ChatMessageImageURL{ + URL: img, + Detail: openai.ImageURLDetailHigh, + }, + }, + }, + }, + }, + }, + ) + + if err != nil { + ai.log.Error("error during recognition process", "%+v", resp.Choices[0].Message.Content) + return "", err + } + if !utils.IsJSON(resp.Choices[0].Message.Content) { + ai.log.Error("OpenAI returned not valid JSON", "%+v", resp.Choices[0].Message.Content) + return "", err + } + + ai.log.Debug("recognition output", "%+v", resp.Choices[0].Message.Content) + + return resp.Choices[0].Message.Content, nil +} diff --git a/workers/parser/parser.go b/workers/parser/parser.go new file mode 100644 index 0000000..93cdf5c --- /dev/null +++ b/workers/parser/parser.go @@ -0,0 +1,6 @@ +package parser + +type Parser interface { + Recognize() + Transcribe() +} diff --git a/workers/telegram.go b/workers/telegram.go new file mode 100644 index 0000000..13e9c0d --- /dev/null +++ b/workers/telegram.go @@ -0,0 +1,341 @@ +package workers + +import ( + "context" + "log/slog" + "sync/atomic" + "time" + + "github.com/alex-savin/go-receipt-tracker/bus" + "github.com/alex-savin/go-receipt-tracker/config" + "github.com/alex-savin/go-receipt-tracker/models" + tele "gopkg.in/telebot.v4" +) + +type Bot struct { + id string // + opts *config.Telegram // + client *tele.Bot // + subscriptions map[string]chan bus.Event // + bus *bus.Bus // + log *slog.Logger // + cancel context.CancelFunc // + end uint32 // ensure the close methods are only called once +} + +func NewTelegramBot(cfg *config.Telegram, b *bus.Bus) *Bot { + + bot := &Bot{ + id: "telegram", + opts: cfg, + subscriptions: make(map[string]chan bus.Event), + bus: b, + } + + return bot +} + +func (b *Bot) ID() string { + return b.id +} + +func (b *Bot) Type() string { + return "worker" +} + +func (b *Bot) Init(log *slog.Logger) error { + var err error + b.log = log + + opts := tele.Settings{ + Token: b.opts.ApiKey, + Poller: &tele.LongPoller{ + Timeout: 10 * time.Second, + }, + } + + b.client, err = tele.NewBot(opts) + if err != nil { + b.log.Debug("cannot create telegram bot", "error", err) + return err + } + + b.client.Handle(tele.OnText, func(c tele.Context) error { + b.log.Debug("received a new text message", "chat", c.Chat().ID, "message", c.Message().Text) + + b.client.Notify(c.Chat(), tele.ChatAction("typing")) + + err = b.bus.Publish("processor:text", c.Message().Text) + if err != nil { + b.log.Error("couldn't publish to a channel", "channel", "processor:text", "error", err.Error()) + } + + return nil + }) + + b.client.Handle(tele.OnPhoto, func(c tele.Context) error { + b.client.Notify(c.Chat(), tele.ChatAction("typing")) + + photo := c.Message().Photo + b.client.Download(&photo.File, b.opts.StoragePath+"/"+photo.UniqueID+".jpg") + + msg := &bus.Message{ + Image: &bus.Image{ + ID: photo.UniqueID, + Filename: b.opts.StoragePath + "/" + photo.UniqueID + ".jpg", + }, + ReplyType: "send", + TbContext: c, + } + err := b.bus.Publish("processor:image", msg) + if err != nil { + b.log.Error("couldn't publish to the channel", "channel", "processor:image", "error", err.Error()) + } + + return nil + }) + + b.client.Handle(tele.OnCallback, func(c tele.Context) error { + tbc := new(models.TelegramBotCommand) + err := tbc.Unmarshal(c.Callback().Data) + if err != nil { + panic(err) + } + + switch tbc.Cmd { + case models.BtnCmd["user_add"]: + return nil + case models.BtnCmd["user_edit"]: + msg := &bus.Message{ + TBCmd: tbc, + TbContext: c, + ReplyType: "send", + } + err := b.bus.Publish("processor:user_edit", msg) + if err != nil { + b.log.Error("couldn't publish to the channel", "channel", "processor:user_edit", "error", err.Error()) + } + + return nil + case models.BtnCmd["user_delete"]: + // b.client.Notify(c.Chat(), tele.ChatAction("typing")) + // msg.Text = "Welcome back, " + event.Payload.(*bus.Message).TbContext.Sender().FirstName + " " + event.Payload.(*bus.Message).TbContext.Sender().LastName + // msg.InlineKeyboard = &tele.ReplyMarkup{ + // InlineKeyboard: [][]tele.InlineButton{ + // { + // {Text: "Yes, Delete", Data: models.String(&models.TelegramBotCommand{Cmd: models.BtnCmd["user_delete"], ID: msg.TbContext.Sender().ID})}, + // {Text: "No, Cancel", Data: models.String(&models.TelegramBotCommand{Cmd: models.BtnCmd["user_delete"], ID: msg.TbContext.Sender().ID})}, + // }, + // { + // {Text: "« Back to Receipt", Data: models.String(&models.TelegramBotCommand{Cmd: models.BtnCmd["user_edit"], ID: msg.TbContext.Sender().ID})}, + // }, + // }, + // } + + return nil + case models.BtnCmd["receipt_add"]: + return nil + case models.BtnCmd["receipt_edit"]: + return nil + case models.BtnCmd["receipts_list"]: + return nil + case models.BtnCmd["items_list"]: + cbr := &tele.CallbackResponse{ + CallbackID: c.Callback().ID, + Text: "You clicked the button!", + ShowAlert: false, + } + c.Respond(cbr) + msg := &bus.Message{ + TBCmd: tbc, + TbContext: c, + ReplyType: "send", + } + err := b.bus.Publish("processor:items_list", msg) + if err != nil { + b.log.Error("couldn't publish to the channel", "channel", "processor:items_list", "error", err.Error()) + } + // r := new(Receipt) + // err = db.NewSelect().Model(r).Where("receipt_id = ?", tbc.ID).Scan(ctx) + // if err != nil { + // panic(err) + // } + + // var items []Item + // _, err := db.NewSelect().Model(&items).Where("receipt_id = ?", tbc.ID).Limit(10).ScanAndCount(ctx) + // if err != nil { + // panic(err) + // } + + // message := "List of the Receipt Items:\n" + strings.Repeat("-", 32) + "\n" + + // merchant := truncateText(r.Merchant, 16) + // spaces := 32 - (len("Merchant:") + len(merchant)) + // message += "Merchant:" + strings.Repeat(" ", spaces) + merchant + "\n" + // spaces = 32 - (len("Date/Time:") + len(r.PaidAt)) + // message += "Date/Time:" + strings.Repeat(" ", spaces) + r.PaidAt + "\n" + strings.Repeat("-", 32) + "\n" + + // keyboard := &tele.ReplyMarkup{InlineKeyboard: [][]tele.InlineButton{}} + + // for _, item := range items { + // // b := tele.InlineButton{Text: fmt.Sprintf("%s (%.2f) $%.2f", strings.Title(strings.ToLower(item.Title)), item.Quantity, item.Price), Data: String(&TelegramBotCommand{Cmd: btnCmd["item_edit"], ID: item.ItemID})} + // // keyboard.InlineKeyboard = append(keyboard.InlineKeyboard, []tele.InlineButton{b}) + // // if item.Quantity == math.Trunc(item.Quantity) { + // // fmt.Sprintf("%d", math.Trunc(item.Quantity)) + // // } else { + // // fmt.Sprintf("%.2f", item.Quantity) + // // } + // title := truncateText(item.Title, 16) + // spaces := 32 - (len(title) + len("x"+fmt.Sprintf("%+v", item.Quantity)) + len(fmt.Sprintf("$%.2f", item.Price)) + 3) + // message += title + strings.Repeat(".", spaces) + "x" + fmt.Sprintf("%+v", item.Quantity) + "..." + fmt.Sprintf("$%.2f", item.Price) + "\n" + // log.Printf("ITEM: %s // %.2f // %.2f", strings.Title(strings.ToLower(item.Title)), item.Quantity, item.Price) + // } + // // keyboard.InlineKeyboard = append(keyboard.InlineKeyboard, []tele.InlineButton{{Text: "«", Data: String(&TelegramBotCommand{Cmd: btnPage["prev"], ID: tbc.ID})}, {Text: "»", Data: String(&TelegramBotCommand{Cmd: btnPage["next"], ID: tbc.ID})}}) + // keyboard.InlineKeyboard = append(keyboard.InlineKeyboard, []tele.InlineButton{{Text: "« Back to Receipt", Data: String(&TelegramBotCommand{Cmd: btnCmd["receipt_edit"], ID: tbc.ID})}}) + + // message += strings.Repeat("-", 32) + "\n" + // spaces = 32 - (len("Tax:") + len(fmt.Sprintf("$%.2f", r.Tax))) + // message += "Tax:" + strings.Repeat(" ", spaces) + fmt.Sprintf("$%.2f", r.Tax) + "\n" + // if r.CCFee > 0 { + // spaces = 32 - (len("CC Fee:") + len(fmt.Sprintf("$%.2f", r.CCFee))) + // message += "CC Fee:" + strings.Repeat(" ", spaces) + fmt.Sprintf("$%.2f", r.CCFee) + "\n" + // } + // spaces = 32 - (len("Total:") + len(fmt.Sprintf("$%.2f", r.Total))) + // message += "Total:" + strings.Repeat(" ", spaces) + fmt.Sprintf("$%.2f", r.Total) + "\n" + + // opts := &tele.SendOptions{ParseMode: "HTML", ReplyMarkup: keyboard} + + // return c.EditCaption(message, opts) + return nil + } + // cbr := &tele.CallbackResponse{ + // CallbackID: c.Callback().ID, + // Text: "No unique", + // ShowAlert: false, + // } + // return c.Respond(cbr) + return nil + }) + + b.client.Handle("/start", func(c tele.Context) error { + b.client.Notify(c.Chat(), tele.ChatAction("typing")) + msg := &bus.Message{ + ReplyType: "send", + TbContext: c, + } + + err = b.bus.Publish("processor:user_add", msg) + if err != nil { + b.log.Error("couldn't publish to a channel", "channel", "processor:user_add", "error", err.Error()) + } + + return nil + }) + + b.client.Handle("/receipts", func(c tele.Context) error { + b.log.Debug("publish message to a channel", "channel", "processor:receipts_list", "message", "Test Message") + b.client.Notify(c.Chat(), tele.ChatAction("typing")) + + err = b.bus.Publish("processor:receipts_list", "Test Message") + if err != nil { + b.log.Error("couldn't publish to a channel", "channel", "processor:receipts_list", "error", err.Error()) + } + + return nil + }) + + b.client.Handle("/reports", func(c tele.Context) error { + b.log.Debug("publish message to a channel", "channel", "processor:reports_list", "message", "Test Message") + b.client.Notify(c.Chat(), tele.ChatAction("typing")) + + err = b.bus.Publish("processor:reports_list", "Test Message") + if err != nil { + b.log.Error("couldn't publish to a channel", "channel", "processor:reports_list", "error", err.Error()) + } + + return nil + }) + + return nil +} + +func (b *Bot) OneTime() error { + return nil +} + +func (b *Bot) Serve() { + if atomic.LoadUint32(&b.end) == 1 { + return + } + + b.subscribe("telegram:send") + b.subscribe("telegram:reply") + b.subscribe("telegram:edit_caption") + + ctx, cancel := context.WithCancel(context.Background()) + b.cancel = cancel + + go b.eventLoop(ctx) + + time.Sleep(3 * time.Second) + + go b.client.Start() + + // if atomic.LoadUint32(&b.end) == 0 { + // go func() { + // if !w.mqtt.IsConnected() { + // b.bus.Publish("app:connected", &bus.ConnectionStatus{WorkerID: b.ID(), WorkerType: b.Type(), IsConnected: false}) + // b.log.Warn(b.ID() + " client is disconnected") + // } + // }() + // } +} + +func (b *Bot) Stop() { + b.client.Stop() +} + +func (b *Bot) Close() { + b.cancel() +} + +func (b *Bot) subscribe(chn string) error { + s, err := b.bus.Subscribe(chn, b.Type()+":"+b.ID()) + if err != nil { + b.log.Error("couldn't subscribe to a channel", "channel", chn, "error", err.Error()) + return err + } + b.subscriptions[chn] = s + + return nil +} + +// eventLoop loops forever +func (b *Bot) eventLoop(ctx context.Context) { + b.log.Debug(b.ID() + " communication event loop started") + defer b.log.Debug(b.ID() + " communication event loop halted") + + for { + select { + + case event := <-b.subscriptions["telegram:send"]: + b.log.Debug("worker got a new message from a channel", "channel", event.ChannelName, "type", b.Type(), "id", b.ID()) + msg := event.Payload.(*bus.Message) + b.client.Send(msg.TbContext.Sender(), msg.Text, msg.InlineKeyboard) + + case event := <-b.subscriptions["telegram:reply"]: + b.log.Debug("worker got a new message from a channel", "channel", event.ChannelName, "type", b.Type(), "id", b.ID()) + msg := event.Payload.(*bus.Message) + b.client.Reply(msg.TbContext.Message(), msg.Text, msg.InlineKeyboard) + + case event := <-b.subscriptions["telegram:edit_caption"]: + b.log.Debug("worker got a new message from a channel", "channel", event.ChannelName, "type", b.Type(), "id", b.ID()) + msg := event.Payload.(*bus.Message) + b.client.EditCaption(msg.TbContext.Message(), msg.Text, msg.InlineKeyboard) + + case <-ctx.Done(): + b.log.Info("stopping " + b.ID() + " communication event loop") + return + } + } +} diff --git a/workers/telegram/commands.go b/workers/telegram/commands.go new file mode 100644 index 0000000..0e0af66 --- /dev/null +++ b/workers/telegram/commands.go @@ -0,0 +1,92 @@ +package telegram + +import ( + "fmt" + "log" + "reflect" + "regexp" + "strconv" +) + +var btnCmd = map[string]int64{ + "user_add": 1, + "user_edit": 2, + "user_detete": 3, + "users_list": 4, + "group_add": 11, + "group_edit": 12, + "group_delete": 13, + "groups_list": 14, + "receipt_add": 21, + "receipt_edit": 22, + "receipt_delete": 23, + "receipts_list": 24, + "item_add": 31, + "item_edit": 32, + "item_delete": 33, + "items_list": 34, + "merchant": 96, + "category": 97, + "tax": 98, + "total": 99, +} + +var btnPage = map[string]int64{ + "prev": 1, + "next": 2, +} + +// TelegramBotCommand . +type TelegramBotCommand struct { + Cmd int64 // Internal Command ( user_add | user_edit | user_delete ) + ID int64 // Internal UserID + RefCmd int64 // Internal Reference Command + RefID int64 // Internal Reference ID + TelegramID int64 // Telegram UserID + Pagination int64 // Pagination + Extra string // Internal String filed to pass Telegram FileID +} + +// Unmarshal . +func (t *TelegramBotCommand) Unmarshal(c string) error { + r := regexp.MustCompile(`^(?P.*)\|(?P.*)\|(?P.*)\|(?P.*)\|(?P.*)\|(?P.*)\|(?P.*)$`) + m := r.FindStringSubmatch(c) + + res := make(map[string]string) + for i, name := range r.SubexpNames() { + if i != 0 && name != "" { + res[name] = m[i] + } + } + + for k, val := range res { + v := reflect.ValueOf(&t).Elem() + f := reflect.Indirect(v).FieldByName(k) + + if !f.IsValid() { + continue + } + if f.Type().Kind() == reflect.Int64 { + age, err := strconv.Atoi(val) + if err != nil { + continue + } + f.SetInt(int64(age)) + } else { + f.SetString(val) + } + } + log.Printf("CALLBACK (UNMARSHAL) %d|%d|%d|%d|%d|%d|%s", t.Cmd, t.ID, t.RefCmd, t.RefID, t.TelegramID, t.Pagination, t.Extra) + + return nil +} + +// Marshal . +func (t TelegramBotCommand) Marshal() string { + return fmt.Sprintf("%d|%d|%d|%d|%d|%d|%s", t.Cmd, t.ID, t.RefCmd, t.RefID, t.TelegramID, t.Pagination, t.Extra) +} + +// String . +func String(t *TelegramBotCommand) string { + return t.Marshal() +} diff --git a/workers/telegram/telegram.go b/workers/telegram/telegram.go new file mode 100644 index 0000000..6efbaac --- /dev/null +++ b/workers/telegram/telegram.go @@ -0,0 +1,213 @@ +package telegram + +import ( + "context" + "log/slog" + "sync/atomic" + "time" + + "github.com/alex-savin/go-receipt-tracker/bus" + "github.com/alex-savin/go-receipt-tracker/config" + tele "gopkg.in/telebot.v4" +) + +type Bot struct { + id string // + opts *config.Telegram // + client *tele.Bot // + subscriptions map[string]chan bus.Event // + bus *bus.Bus // + log *slog.Logger // + cancel context.CancelFunc // + end uint32 // ensure the close methods are only called once +} + +func NewTelegramBot(cfg *config.Telegram, bus *bus.Bus) (*Bot, error) { + + bot := &Bot{ + id: "telegram", + opts: cfg, + bus: bus, + } + + return bot, nil +} + +func (b *Bot) ID() string { + return b.id +} + +func (b *Bot) Type() string { + return "telegram" +} + +func (b *Bot) Init(log *slog.Logger) error { + var err error + b.log = log + + pref := tele.Settings{ + Token: b.opts.ApiKey, + Poller: &tele.LongPoller{ + Timeout: 10 * time.Second, + }, + } + + b.client, err = tele.NewBot(pref) + if err != nil { + b.log.Debug("cannot create telegram bot", "error", err) + return err + } + + b.client.Handle(tele.OnText, func(c tele.Context) error { + return c.Send("") + }) + + b.client.Handle(tele.OnPhoto, func(c tele.Context) error { + + photo := c.Message().Photo + b.client.Download(&photo.File, "./"+photo.UniqueID+".jpg") + + msg := &bus.Message{ + Image: &bus.Image{ + ID: photo.UniqueID, + Filename: b.opts.StoragePath + "/" + photo.UniqueID + ".jpg", + }, + ReplyType: "send", + TbContext: c, + } + err := b.bus.Publish("processor:image", msg) + if err != nil { + b.log.Error("couldn't publish to the channel", "channel", "processor:image", "error", err.Error()) + } + + return c.Send("") + }) + + b.client.Handle(tele.OnCallback, func(c tele.Context) error { + b.log.Debug("CALLBACK (BEFORE) %s // %s", c.Callback().ID, c.Callback().Data) + + tbc := new(TelegramBotCommand) + err := tbc.Unmarshal(c.Callback().Data) + if err != nil { + panic(err) + } + b.log.Debug("CALLBACK (AFTER) %d|%d|%d|%d|%d|%d|%s", tbc.Cmd, tbc.ID, tbc.RefCmd, tbc.RefID, tbc.TelegramID, tbc.Pagination, tbc.Extra) + + switch tbc.Cmd { + case btnCmd["user_add"]: + return c.Send("") + case btnCmd["receipt_add"]: + + return c.Send("") + case btnCmd["receipt_edit"]: + return c.Send("") + case btnCmd["receipts_list"]: + case btnCmd["items_list"]: + } + cbr := &tele.CallbackResponse{ + CallbackID: c.Callback().ID, + Text: "No unique", + ShowAlert: false, + } + return c.Respond(cbr) + }) + + b.client.Handle("/start", func(c tele.Context) error { + err = b.bus.Publish("processor:start", nil) + if err != nil { + b.log.Error("couldn't publish to a channel", "channel", "telegram:publish", "error", err.Error()) + } + + return c.Send("") + }) + + b.client.Handle("/receipts", func(c tele.Context) error { + return c.Send("") + }) + + b.client.Handle("/reports", func(c tele.Context) error { + return c.Send("") + }) + + return nil +} + +func (b *Bot) Serve() { + if atomic.LoadUint32(&b.end) == 1 { + return + } + + b.subscribe("messenger:" + b.ID()) + + b.client.Start() + + ctx, cancel := context.WithCancel(context.Background()) + b.cancel = cancel + + go b.eventLoop(ctx) + + // if atomic.LoadUint32(&b.end) == 0 { + // go func() { + // if !w.mqtt.IsConnected() { + // b.bus.Publish("app:connected", &bus.ConnectionStatus{WorkerID: b.ID(), WorkerType: b.Type(), IsConnected: false}) + // b.log.Warn(b.ID() + " client is disconnected") + // } + // }() + // } +} + +func (b *Bot) Stop() { + b.client.Stop() +} + +func (b *Bot) subscribe(chn string) error { + s, err := b.bus.Subscribe(chn, b.Type()+":"+b.ID()) + if err != nil { + b.log.Error("couldn't subscribe to a channel", "channel", chn, "error", err.Error()) + return err + } + b.subscriptions[chn] = s + + return nil +} + +// eventLoop loops forever +func (b *Bot) eventLoop(ctx context.Context) { + b.log.Debug(b.ID() + " communication event loop started") + defer b.log.Debug(b.ID() + " communication event loop halted") + + for { + for chn, ch := range b.subscriptions { + select { + case event := <-ch: + b.log.Debug("got a new message to a channel", "channel", chn) + for _, msg := range event.Payload.([]*bus.Message) { + b.log.Debug("publishing telegram message", "topic", msg) + // w.mqtt.Publish(message.Topic, message.QOS, message.Retained, message.Payload) + } + case <-ctx.Done(): + b.log.Info("stopping " + b.ID() + " communication event loop") + return + } + } + } + + // for { + // select { + // case event := <-chMQTTPublishStatus: + // for _, message := range event.Data.([]*bus.Message) { + // b.log.Debug("publishing mqtt message", "topic", message.Topic, "qos", message.QOS, "retained", message.Retained, "payload", message.Payload) + // // w.mqtt.Publish(message.Topic, message.QOS, message.Retained, message.Payload) + // } + // case event := <-chMQTTSubscribeCommand: + // for _, message := range event.Data.([]*bus.Message) { + // b.log.Debug("subscribing to a topic", "topic", message.Topic, "qos", message.QOS) + // // w.mqtt.SubscribeMultiple() + // // b.mqtt.Subscribe(message.Topic, message.QOS, w.mqttHandlers.publish) + // } + // case <-ctx.Done(): + // b.log.Info("stopping mqtt communication event loop") + // return + // } + // } +} diff --git a/workers/workers.go b/workers/workers.go new file mode 100644 index 0000000..1b4fa8d --- /dev/null +++ b/workers/workers.go @@ -0,0 +1,124 @@ +package workers + +import ( + "log/slog" + "sync" +) + +type Config struct { + // TLSConfig is a tls.Config configuration to be used with the listener. + // See examples folder for basic and mutual-tls use. + Test string +} + +// EstablishFn is a callback function for establishing new clients. +type EstablishFn func(id string) error + +// CloseFn is a callback function for closing all listener clients. +type CloseFn func(id string) + +// Worker . +type Worker interface { + ID() string // returns ID in string format + Type() string // returns the type of the worker + Init(*slog.Logger) error // + OneTime() error // + Serve() // starting actively listening for new connections + Close() // stop and close the worker +} + +// Workers contains the network workers for the app. +type Workers struct { + ClientsWg sync.WaitGroup // a waitgroup that waits for all clients in all workers to finish. + internal map[string]Worker // a map of active workers. + sync.RWMutex +} + +// New returns a new instance of Workers. +func New() *Workers { + return &Workers{ + internal: map[string]Worker{}, + } +} + +// Add adds a new worker to the workers map, keyed on id. +func (w *Workers) Add(val Worker) { + w.Lock() + defer w.Unlock() + w.internal[val.ID()] = val +} + +// Get returns the value of a worker if it exists. +func (w *Workers) Get(id string) (Worker, bool) { + w.RLock() + defer w.RUnlock() + val, ok := w.internal[id] + return val, ok +} + +// Len returns the length of the workers map. +func (w *Workers) Len() int { + w.RLock() + defer w.RUnlock() + return len(w.internal) +} + +// Delete removes a worker from the internal map. +func (w *Workers) Delete(id string) { + w.Lock() + defer w.Unlock() + delete(w.internal, id) +} + +// Serve starts a worker serving from the internal map. +func (w *Workers) Serve(id string) { + w.RLock() + defer w.RUnlock() + worker := w.internal[id] + + go func() { + worker.Serve() + worker.OneTime() + }() +} + +// ServeAll starts all workers serving from the internal map. +func (w *Workers) ServeAll() { + w.RLock() + i := 0 + ids := make([]string, len(w.internal)) + for id := range w.internal { + ids[i] = id + i++ + } + w.RUnlock() + + for _, id := range ids { + w.Serve(id) + } +} + +// Close stops a worker from the internal map. +func (w *Workers) Close(id string) { + w.RLock() + defer w.RUnlock() + if worker, ok := w.internal[id]; ok { + worker.Close() + } +} + +// CloseAll iterates and closes all registered workere. +func (w *Workers) CloseAll() { + w.RLock() + i := 0 + ids := make([]string, len(w.internal)) + for id := range w.internal { + ids[i] = id + i++ + } + w.RUnlock() + + for _, id := range ids { + w.Close(id) + } +}