alpha version

This commit is contained in:
2025-02-03 17:49:29 -05:00
commit dac9e95d12
144 changed files with 5694 additions and 0 deletions

18
.vscode/launch.json vendored Normal file
View File

@ -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"
]
}
]
}

493
app/app.go Normal file
View File

@ -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 := "<code>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("<b>$%.2f</b>", r.Total) + "\n</code>"
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], " ")]
}

216
app/hooks.go Normal file
View File

@ -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) {}

116
app/processor.go Normal file
View File

@ -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
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

BIN
blobs/AQAD-K0xG8NvKEd-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

BIN
blobs/AQAD-a0xG50ISUR-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

BIN
blobs/AQAD-awxG42ZWUZ-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
blobs/AQAD0K8xG5FI0Ud-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

BIN
blobs/AQAD1a0xG45smUZ-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

BIN
blobs/AQAD1a4xG_axSEZ-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

BIN
blobs/AQAD2K8xG4eUOER-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

BIN
blobs/AQAD3K0xGyt2sUZ-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

BIN
blobs/AQAD3K4xG7pBeEd-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

BIN
blobs/AQAD4KwxGz80WUd-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

BIN
blobs/AQAD4a0xGzSJ0Ed-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

BIN
blobs/AQAD4q0xGzSJ0Ed-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

BIN
blobs/AQAD5a4xG7pBeEd-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

BIN
blobs/AQAD6K0xG2cZeEd-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

BIN
blobs/AQAD6K0xG78WgUZ-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

BIN
blobs/AQAD6a0xG2cZeEd-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

BIN
blobs/AQAD6q0xGzSJ0Ed-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

BIN
blobs/AQAD6qwxGz80WUd-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

BIN
blobs/AQAD6rQxGxYVEEd-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

BIN
blobs/AQAD7K0xG78WgUZ-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

BIN
blobs/AQAD7K4xG1UCiUd-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

BIN
blobs/AQAD860xG22YQUZ-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

BIN
blobs/AQAD864xG42ZUUZ-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

BIN
blobs/AQAD864xGxh44EZ-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

BIN
blobs/AQAD96wxG42ZWUZ-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

BIN
blobs/AQAD9K4xG42ZUUZ-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

BIN
blobs/AQAD9q4xG42ZUUZ-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

BIN
blobs/AQADBK4xGz80UUd-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

BIN
blobs/AQADEK8xG50v-Ed-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

BIN
blobs/AQADGq4xG78WiUZ-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

BIN
blobs/AQADHbAxG82qAUd-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

BIN
blobs/AQADIa8xG5FI2Ud-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

BIN
blobs/AQADJ60xG42ZWUZ-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

BIN
blobs/AQADJ64xG5twKUd-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

BIN
blobs/AQADJq0xG42ZWUZ-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

BIN
blobs/AQADK7AxGzSJyEd-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

BIN
blobs/AQADK7IxGyDEMUZ-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

BIN
blobs/AQADKbIxGyDEMUZ-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

BIN
blobs/AQADKrIxGyDEMUZ-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

BIN
blobs/AQADL7IxGyDEMUZ-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

BIN
blobs/AQADLLIxGyDEMUZ-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

BIN
blobs/AQADLbIxGyDEMUZ-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

BIN
blobs/AQADLrIxGyDEMUZ-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

BIN
blobs/AQADMLIxGyDEMUZ-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

BIN
blobs/AQADMrExGyXiUUR-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

BIN
blobs/AQADNK8xGyDEOUZ-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

BIN
blobs/AQADNrAxGzSJyEd-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 KiB

BIN
blobs/AQADObIxGyDEMUZ-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

BIN
blobs/AQADOrIxGyDEMUZ-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

BIN
blobs/AQADP68xGxYVGEd-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

BIN
blobs/AQADPq8xGyDEOUZ-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

BIN
blobs/AQADPrIxGyDEMUZ-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

BIN
blobs/AQADQK8xGyDEOUZ-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

BIN
blobs/AQADQrAxG1vkeEZ-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

BIN
blobs/AQADR7IxGyDEMUZ-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

BIN
blobs/AQADRrIxGyDEMUZ-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

BIN
blobs/AQADS60xGyXiYUR-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

BIN
blobs/AQADSLIxGyDEMUZ-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

BIN
blobs/AQADT68xGyDEOUZ-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

BIN
blobs/AQADTa8xGyDEOUZ-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

BIN
blobs/AQADTq8xGyDEOUZ-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

BIN
blobs/AQADTrIxG9b46EZ-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

BIN
blobs/AQADUK8xGyDEOUZ-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

BIN
blobs/AQADULIxGyDEMUZ-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

BIN
blobs/AQADUa8xGyDEOUZ-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

BIN
blobs/AQADVMYxGxvmKER-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

BIN
blobs/AQADZa0xG1vkcEZ-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

BIN
blobs/AQAD_68xGwyDYUR-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

BIN
blobs/AQADaq8xG869KEd-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

BIN
blobs/AQADc6wxGzr6mUd-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

BIN
blobs/AQADcK4xG7g6EER-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

BIN
blobs/AQADeqwxGzr6mUd-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

BIN
blobs/AQADf64xG2cZaEd-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

BIN
blobs/AQADfKwxGxdNMEZ-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

BIN
blobs/AQADfawxGzr6mUd-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

BIN
blobs/AQADfq8xG869KEd-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

BIN
blobs/AQADgK4xG2cZaEd-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

BIN
blobs/AQADgq4xG2cZaEd-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

BIN
blobs/AQADi64xG22YOUZ-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

BIN
blobs/AQADj8QxG5CMCVR-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

BIN
blobs/AQADkK0xGxdNIEZ-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

BIN
blobs/AQADl60xG7g6CER-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

BIN
blobs/AQADlKwxG45skUZ-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

BIN
blobs/AQADlb8xGwUICVR-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

BIN
blobs/AQADlq8xGwyDcUR-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

BIN
blobs/AQADmK0xGxdNIEZ-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

BIN
blobs/AQADn60xG1WZEEV-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

BIN
blobs/AQADna0xG7pBgEd-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

BIN
blobs/AQADqbQxGxYVEEd-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

BIN
blobs/AQADs6wxG45skUZ-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
blobs/AQADv60xG601WUd-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

BIN
blobs/AQADvK0xG50IQUR-.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Some files were not shown because too many files have changed in this diff Show More