diff --git a/.gitea/workflows/demo.yaml b/.gitea/workflows/demo.yaml new file mode 100644 index 0000000..e942159 --- /dev/null +++ b/.gitea/workflows/demo.yaml @@ -0,0 +1,19 @@ +name: Gitea Actions Demo +run-name: ${{ gitea.actor }} is testing out Gitea Actions πŸš€ +on: [push] + +jobs: + Explore-Gitea-Actions: + runs-on: ubuntu-latest + steps: + - run: echo "πŸŽ‰ The job was automatically and automagically triggered by a ${{ gitea.event_name }} event." + - run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by Gitea!" + - run: echo "πŸ”Ž The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}." + - name: Check out repository code + uses: actions/checkout@v4 + - run: echo "πŸ’‘ The ${{ gitea.repository }} repository has been cloned to the runner." + - run: echo "πŸ–₯️ The workflow is now ready to test your code on the runner." + - name: List files in the repository + run: | + ls ${{ gitea.workspace }} + - run: echo "🍏 This job's status is ${{ job.status }}." diff --git a/Dockerfile b/Dockerfile index 3190bc3..2e67f74 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,17 @@ -FROM golang:latest as builder -WORKDIR /app -COPY . . -RUN go mod download && go mod tidy +#FROM golang:latest as builder +#WORKDIR /app +#COPY . . +#RUN go mod download && go mod tidy -RUN go build -o ./bin/us-dop-bot ./cmd/bot +#RUN go build -o ./bin/us-dop-bot ./cmd/bot FROM debian:unstable-slim +ARG BINAME=us-dop-bot-linux-arm64-0.0.0_1 RUN apt-get update RUN apt-get install -y ca-certificates -COPY --from=builder /app/bin/us-dop-bot /app/us-dop-bot +COPY ./bin/${BINAME} /app/us-dop-bot WORKDIR /app +RUN echo "bin name ${BINAME}" +# RUN mv /app/${BINAME} /app/us-dop-bot CMD ["/app/us-dop-bot"] diff --git a/Dockerfile.old b/Dockerfile.old new file mode 100644 index 0000000..3190bc3 --- /dev/null +++ b/Dockerfile.old @@ -0,0 +1,14 @@ +FROM golang:latest as builder +WORKDIR /app +COPY . . +RUN go mod download && go mod tidy + +RUN go build -o ./bin/us-dop-bot ./cmd/bot + +FROM debian:unstable-slim +RUN apt-get update +RUN apt-get install -y ca-certificates + +COPY --from=builder /app/bin/us-dop-bot /app/us-dop-bot +WORKDIR /app +CMD ["/app/us-dop-bot"] diff --git a/Makefile b/Makefile index cea2357..e684202 100644 --- a/Makefile +++ b/Makefile @@ -20,11 +20,15 @@ K8SRSNAME=$(shell kubectl get rs --no-headers -o custom-columns=":metadata.name" .phony: all clean build test clean-image build-image build-image-debug run-image run-image-debug run-local -build-image: +build-image: build # here we made the images and push to registry with buildx - @$(CONTAINERS) buildx build --platform linux/arm64 --push -t $(REGADDR)/us-dop-bot:latest . + @$(CONTAINERS) buildx build --build-arg="BINAME=$(BINAMEARM)" --platform linux/arm64 --push -t $(REGADDR)/us-dop-bot:latest . + # Here we upload it to local +build-test-image: + @$(CONTAINERS) buildx build --platform linux/arm64 --push -t $(REGADDR)/us-dop-bot:latest . + run-image: build-image @$(CONTAINERS) compose -f docker-compose.yaml up @@ -37,7 +41,7 @@ run-image-debug: build-image-debug run-local:clean build @bin/$(BINAME) -build: +build: clean #@mkdir dolardb @env GOOS=$(OS) GOARCH=$(arch) go build -o ./bin/$(BINAME) ./cmd/bot/. @env GOOS=$(OS) GOARCH=arm64 go build -o ./bin/$(BINAMEARM) ./cmd/bot/. diff --git a/broadcast/broadcast.go b/broadcast/broadcast.go deleted file mode 100644 index d82ff3a..0000000 --- a/broadcast/broadcast.go +++ /dev/null @@ -1,62 +0,0 @@ -package broadcast - -import ( - "context" - "encoding/json" - "fmt" - "log/slog" - "time" - - tb "github.com/go-telegram-bot-api/telegram-bot-api/v5" - "github.com/maximotejeda/us_dop_bot/db" - message "github.com/maximotejeda/us_dop_bot/edb" - "github.com/maximotejeda/us_dop_bot/helpers" -) - -func SendList(ctx context.Context, userDB *db.DB, log *slog.Logger, data []byte) []tb.MessageConfig { - user := db.NewUser(userDB, log) - // convert data to map - m := message.Message{} - listMsg := []tb.MessageConfig{} - err := json.Unmarshal(data, &m) - if err != nil { - log.Error("unmarshaling data", "error", err) - } - if m.Message == "change registered" { - - userList, err := user.GetAll(m.Data.After.Name) - if err != nil { - log.Error("querying DB data", "error", err) - return nil - } - cancelBTN := map[string]string{} - cancelBTN["Eliminar ❌"] = "cancelar=true" - keyboard := helpers.CreateKeyboard(cancelBTN) - log.Info("printing change", "user list", userList, "name", m.Data.After.Name) - compraCHG := comparer(m.Data.Before.Compra, m.Data.After.Compra) - ventaCHG := comparer(m.Data.Before.Venta, m.Data.After.Venta) - text := fmt.Sprintf("Cambio Registrado:\n\nInstitucion: %s\n\t Compra: %.2f %s %.2f\n\t Venta: %.2f %s %.2f\n\n\t %s", m.Data.After.Name, m.Data.Before.Compra, compraCHG, m.Data.After.Compra, m.Data.Before.Venta, ventaCHG, m.Data.After.Venta, m.Data.After.Parsed.Format(time.DateTime)) - - for _, user := range userList { - if user.TguID != 0 { - msg := tb.NewMessage(int64(user.TguID), text) - msg.ReplyMarkup = keyboard - listMsg = append(listMsg, msg) - } else { - continue - } - } - } - return listMsg -} - -func comparer(before, after float64) string { - if before > after { - return "⬇️" - } else if before < after { - return "⬆️" - } else { - return "🟰" - } - -} diff --git a/cmd/bot/main.go b/cmd/bot/main.go index a87af86..1f22f62 100644 --- a/cmd/bot/main.go +++ b/cmd/bot/main.go @@ -2,148 +2,147 @@ package main import ( "context" - "database/sql" - "errors" - "fmt" "log/slog" "os" "os/signal" - "strings" + "runtime" "syscall" - "time" - tb "github.com/go-telegram-bot-api/telegram-bot-api/v5" - "github.com/maximotejeda/us_dop_bot/broadcast" - commands "github.com/maximotejeda/us_dop_bot/command" - "github.com/maximotejeda/us_dop_bot/db" - edb "github.com/maximotejeda/us_dop_bot/edb" - "github.com/maximotejeda/us_dop_bot/query" + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" + "github.com/maximotejeda/us_dop_bot/config" + "github.com/maximotejeda/us_dop_bot/internal/adapters/dolar" + "github.com/maximotejeda/us_dop_bot/internal/adapters/user" + "github.com/maximotejeda/us_dop_bot/internal/application/api" + "github.com/maximotejeda/us_dop_bot/internal/application/broadcaster" + "github.com/maximotejeda/us_dop_bot/internal/ports" + "golang.org/x/sync/semaphore" + "github.com/nats-io/nats.go" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +var ( + maxWorkers = runtime.GOMAXPROCS(0) + sem = semaphore.NewWeighted(int64(maxWorkers) * 2) ) func main() { - dbUserUri := os.Getenv("DBURIUSER") - dbInstUri := os.Getenv("DBURINST") - token := os.Getenv("TOKEN") - natsURI := os.Getenv("NATSURI") log := slog.New(slog.NewJSONHandler(os.Stderr, nil)) - nc, _ := nats.Connect(natsURI) + log = log.With("location", "main") + nc, _ := nats.Connect(config.GetNatsURI()) ctx := context.Background() - userDB := db.Dial(ctx, db.DEFAULT_DRIVER, dbUserUri) - instDB := edb.Dial(dbInstUri, log) - bot, err := tb.NewBotAPI(token) + bot, err := tgbotapi.NewBotAPI(config.GetToken()) if err != nil { + log.Error("token not found", "error", err) panic(err) } - bot.Debug = false - log.Info("Bot Authorized", "username", bot.Self.UserName) - u := tb.NewUpdate(0) + + botName := bot.Self.UserName + + bot.Debug = config.GetEnvironment() == "development" + log.Info("Bot Authorized", "username", botName) + log.Info("Initiated with a concurrency limit", "max concurrency", maxWorkers*2) + u := tgbotapi.NewUpdate(0) u.Timeout = 60 // bot user update channel updtChan := bot.GetUpdatesChan(u) // subs chann - ch := make(chan *nats.Msg, 64) - defer close(ch) - sub, err := nc.ChanSubscribe("dolar-crawler", ch) + changeChan := make(chan *nats.Msg, 64) + broadcastChan := make(chan *nats.Msg, 64) + defer close(changeChan) + defer close(broadcastChan) + sub, err := nc.ChanSubscribe("dolar-bot-change", changeChan) + if err != nil { + log.Error("subscribing", "error", err.Error()) + } + info, err := nc.ChanSubscribe("dolar-bot", broadcastChan) if err != nil { log.Error("subscribing", "error", err.Error()) } defer sub.Drain() + defer info.Drain() defer nc.Close() + // exit channel sign := make(chan os.Signal, 1) signal.Notify(sign, syscall.SIGINT, syscall.SIGTERM) defer close(sign) + app := api.NewApi(bot) for { select { case update := <-updtChan: - usr := db.NewUser(userDB, log) - _, err := usr.Get(update.SentFrom().ID) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - usr.Add(update.SentFrom().ID) - } - + if err = sem.Acquire(ctx, 1); err != nil { + bot.Send(tgbotapi.NewMessage(update.FromChat().ID, "error adquiring update")) + continue } + go func() { + defer sem.Release(1) + dol, user, dolarConn, userConn := CreateAdaptersGRPC() + app.Run(&update, dol, user) + dolarConn.Close() + userConn.Close() + }() + case message := <-changeChan: + log.Info("broadcasting Change") + dol, user, dolarConn, userConn := CreateAdaptersGRPC() - if update.Message != nil { - msg := tb.NewMessage(update.Message.Chat.ID, "") - if update.Message.Text != "" && !update.Message.IsCommand() { - log.Info("update", "username", update.Message.From.UserName, "message", update.Message.Text) - msg.Text = update.Message.Text - msg.ReplyToMessageID = update.Message.MessageID - bot.Send(msg) - } else if update.Message.IsCommand() { - go func(update tb.Update) { - msg = commands.CommandHandler(ctx, userDB, instDB, log, update) + bcast := broadcaster.NewBroadCast(ctx, user, dol, message.Data) + userList := bcast.SendList() - if resp, err := bot.Request(tb.NewDeleteMessage(update.Message.From.ID, update.Message.MessageID)); err != nil || !resp.Ok { - log.Error(err.Error()) - } - - bot.Send(msg) - }(update) - } - } else if update.CallbackQuery != nil { - ctx, cancel := context.WithTimeout(ctx, 3*time.Second) - go func(update tb.Update) { - msg := query.QueryHandler(ctx, userDB, instDB, log, update.CallbackQuery) - - //del := tb.NewDeleteMessage(update.CallbackQuery.From.ID, update.CallbackQuery.Message.MessageID) - if resp, err := bot.Request(tb.NewDeleteMessage(update.CallbackQuery.From.ID, update.CallbackQuery.Message.MessageID)); err != nil || !resp.Ok { - log.Error(err.Error()) - } - if msg != nil { - if _, err := bot.Send(msg); err != nil { - log.Error(err.Error()) - } - } else { - data := update.CallbackQuery.Data - - dataList := strings.Split(data, "&") - - dataMap := map[string]string{} - for _, val := range dataList { - subData := strings.Split(val, "=") - dataMap[subData[0]] = subData[1] - } - queryinf := tb.CallbackConfig{} - name := dataMap["name"] - if _, ok := dataMap["subs"]; ok { - queryinf.Text = fmt.Sprintf("Te haz suscrito a %s:\nRecibiras un mensaje cuando cambie el precio del dolar.", name) - } else if _, ok := dataMap["unsubs"]; ok { - queryinf.Text = fmt.Sprintf("Haz eliminado a %s de tus suscripciones\nNo recibiras notificaciones de %s", name, name) - } else if _, ok := dataMap["reset"]; ok { - queryinf.Text = "Haz eliminado todas tus suscripciones" - } - if queryinf.Text != "" { - queryinf.ShowAlert = true - queryinf.CallbackQueryID = update.CallbackQuery.ID - if resp, err := bot.Request(queryinf); err != nil || !resp.Ok { - log.Error(err.Error()) - } - } - } - cancel() - }(update) - - } - case message := <-ch: - // we are outside update so we will be querying db to - // get users interested in specific updates ex bpd, brd, apa - // userID inst=> comma separated string - msgList := broadcast.SendList(ctx, userDB, log, message.Data) - //log.Info(string(message.Data)) - for _, msg := range msgList { + for _, msg := range userList { go bot.Send(msg) } + dolarConn.Close() + userConn.Close() + case message := <-broadcastChan: + dol, user, dolarConn, userConn := CreateAdaptersGRPC() + + bcast := broadcaster.NewBroadCast(ctx, user, dol, message.Data) + msgs := bcast.SendAllUsers(ctx, log, message.Data, bot.Self.UserName) + log.Info("broadcast", "data", string(message.Data), "msg", msgs) + for _, msg := range msgs { + go bot.Send(msg) + } + dolarConn.Close() + userConn.Close() case <-sign: log.Error("killing app due to syscall ") os.Exit(1) } } } + +func CreateAdaptersGRPC() (ports.DolarService, ports.UserService, *grpc.ClientConn, *grpc.ClientConn) { + log := slog.Default() + // we are outside update so we will be querying db to + // get users interested in specific updates ex bpd, brd, apa + // userID inst=> comma separated string + var opts []grpc.DialOption + opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials())) + dolarConn, err := grpc.Dial(config.GetDollarServiceURL(), opts...) + if err != nil { + log.Error("creating gerpc conn", "error", err) + panic(err) + } + userConn, err := grpc.Dial(config.GetUserServiceURL(), opts...) + if err != nil { + log.Error("creating gerpc conn", "error", err) + panic(err) + } + dol, err := dolar.NewAdapter(dolarConn) + if err != nil { + log.Error("creating service adapter", "error", err) + panic(err) + } + user, err := user.NewAdapter(userConn) + if err != nil { + log.Error("creating service adapter", "error", err) + panic(err) + } + return dol, user, dolarConn, userConn +} diff --git a/command/command.go b/command/command.go deleted file mode 100644 index ec49eba..0000000 --- a/command/command.go +++ /dev/null @@ -1,165 +0,0 @@ -package commands - -import ( - "context" - "fmt" - "log/slog" - "slices" - "strings" - - tgbot "github.com/go-telegram-bot-api/telegram-bot-api/v5" - "github.com/maximotejeda/us_dop_bot/db" - edb "github.com/maximotejeda/us_dop_bot/edb" - "github.com/maximotejeda/us_dop_bot/helpers" -) - -// CommandHandler -// Options for user to create queries on the bot -func CommandHandler(ctx context.Context, userDB *db.DB, instDB *edb.DB, log *slog.Logger, update tgbot.Update) tgbot.MessageConfig { - var ( - err error - msg tgbot.MessageConfig - usr = db.NewUser(userDB, log) - ) - command := update.Message.Command() - chatID := update.Message.Chat.ID - - msg.ChatID = chatID - - switch strings.ToLower(command) { - case "list", "lista": - msg, err = lista(update, userDB, instDB, log, "bancos") - if err != nil { - log.Error("command-status", "err", err) - } - case "listabancos": - msg, err = lista(update, userDB, instDB, log, "bancos") - if err != nil { - log.Error("command-status", "err", err) - } - case "listacajas": - msg, err = lista(update, userDB, instDB, log, "cajas") - if err != nil { - log.Error("command-status", "err", err) - } - - case "listagentes": - msg, err = lista(update, userDB, instDB, log, "agentes") - if err != nil { - log.Error("command-status", "err", err) - } - case "consulta": - _, err := usr.Get(update.Message.From.ID) - if err != nil { - log.Error("command-status", "err", err) - } - btnSTR := map[string]string{} - for _, inst := range usr.Subs { - switch inst { - case "asociacion popular de ahorros y prestamos": - btnSTR[inst] = "consultar=true&name=apap" - case "asociacion cibao de ahorros y prestamos": - btnSTR[inst] = "consultar=true&name=acap" - case "asociacion la nacional de ahorros y prestamos": - btnSTR[inst] = "consultar=true&name=anap" - default: - btnSTR[inst] = "consultar=true&name=" + inst - } - - } - btnSTR["cancelar ❌"] = "cancelar=true" - keyboard := helpers.CreateKeyboard(btnSTR) - msg.ReplyMarkup = keyboard - msg.Text = "Suscripciones actuales πŸ’°\nPuedes hacer click para Ver cambios en los precios de las suscripcion\n o presionar cancelar" - - case "status", "info": - _, err := usr.Get(update.Message.From.ID) - if err != nil { - log.Error("command-status", "err", err) - } - btnSTR := map[string]string{} - for _, inst := range usr.Subs { - btnSTR[inst] = "unsubs=true&name=" + inst - } - btnSTR["cancelar ❌"] = "cancelar=true" - keyboard := helpers.CreateKeyboard(btnSTR) - msg.ReplyMarkup = keyboard - msg.Text = "Suscripciones actuales πŸ’°\n Puedes hacer click en una para eliminar su subscripcion\nPresionar cancelar para omitir." - - case "reset": - reset := map[string]string{"Reset": "reset=true", "cancelar ❌": "cancelar=true"} - keyboard := helpers.CreateKeyboard(reset) - msg.ReplyMarkup = keyboard - msg.Text = "Elimina todas las suscripciones del usuario." - case "help", "start", "ayuda", "h": - - help := ` -Asistente de cambio US <-> DOP - πŸ‡ΊπŸ‡Έ ↔️ πŸ‡©πŸ‡΄ -Tracker del precio del dolar para RD. - -Funciona suscribiendo instituciones πŸ’Έ. - - Tracker precio del dolar ❇️. - - Notificacion mensaje automatico πŸ“ˆ. - - Precio actual ⌚. - - Historico de precios πŸ“…. - - Comandos Conocidos por el bot: - - /help: Muestra este mensaje ❓ - /listabancos: Muestra bancos 🏦 - /listacajas: Muestra asociaciones - /listagentes: Muestra agentes πŸ“Š - /consulta: Consulta entidad suscrita πŸ›ŽοΈ - /reset: Borra tada suscripcion 🧹 - /status: Estado del usuario πŸ“‹ -` - - msg.Text = help - default: - msg.Text = "Commando desconocido intenta con\n/help: to get bot info." - } - return msg -} - -func lista(update tgbot.Update, user *db.DB, insts *edb.DB, log *slog.Logger, instSTR string) (msg tgbot.MessageConfig, err error) { - var instList []string - usr := db.NewUser(user, log) - msg = tgbot.MessageConfig{} - msg.ChatID = update.Message.Chat.ID - _, err = usr.Get(update.Message.From.ID) - if err != nil { - log.Error("command-status", "err", err) - return msg, err - } - - // TODO list all institutions - - switch instSTR { - case "bancos": - instList, err = insts.GetBancos() - case "cajas": - instList, err = insts.GetCajas() - case "agentes": - instList, err = insts.GetAgentes() - default: - return msg, fmt.Errorf("tipio de institucion desconocida") - } - if err != nil { - log.Error("[inst-list-query]", "error", err) - return msg, err - } - instMap := map[string]string{} - for _, i := range instList { - if slices.Contains[[]string](usr.Subs, i) { - continue - } - instMap[i] = "subs=true&name=" + i - } - instMap["cancelar ❌"] = "cancelar=true" - keyboard := helpers.CreateKeyboard(instMap) - - msg.Text = "Differentes cajas disponibles para track el precio del cambio\n\n\tasociacion popular\n\n\tasociacion cibao\n\n" - msg.ReplyMarkup = keyboard - return msg, nil -} diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..815590e --- /dev/null +++ b/config/config.go @@ -0,0 +1,37 @@ +package config + +import ( + "log" + "os" +) + +func GetToken() string { + return getEnvVariable("TOKEN") +} + +func GetDBUSERURI() string { + return getEnvVariable("DBURIUSER") +} + +func GetNatsURI() string { + return getEnvVariable("NATSURI") +} + +func GetDollarServiceURL() string { + return getEnvVariable("DOLLAR_SERVICE_URL") +} + +func GetUserServiceURL() string { + return getEnvVariable("TGBUSER_SERVICE_URL") +} + +func GetEnvironment() string { + return getEnvVariable("ENV") +} + +func getEnvVariable(key string) string { + if os.Getenv(key) == "" { + log.Fatal("error getting key", key) + } + return os.Getenv(key) +} diff --git a/crawler.db b/crawler.db new file mode 100644 index 0000000..5425738 Binary files /dev/null and b/crawler.db differ diff --git a/db/db.go b/db/db.go deleted file mode 100644 index 6497395..0000000 --- a/db/db.go +++ /dev/null @@ -1,43 +0,0 @@ -package db - -import ( - "context" - "database/sql" - _ "embed" - "fmt" - - _ "modernc.org/sqlite" -) - -const ( - DEFAULT_DRIVER = "sqlite" -) - -var ( - //go:embed schema.sql - schema string -) - -type DB struct { - *sql.DB -} - -func Dial(ctx context.Context, driver, uri string) *DB { - db, err := sql.Open(driver, uri) - if err != nil { - fmt.Println("Failed to connect to database:", err) - panic(err) - } - err = db.PingContext(ctx) - if err != nil { - fmt.Printf("Pinging with context: %s", err) - panic(err) - } - - _, err = db.ExecContext(ctx, schema) - if err != nil { - panic(err) - } - - return &DB{db} -} diff --git a/db/models.go b/db/models.go deleted file mode 100644 index 60093c0..0000000 --- a/db/models.go +++ /dev/null @@ -1,326 +0,0 @@ -package db - -import ( - "fmt" - "log/slog" - "slices" - "strings" - "time" -) - -type User struct { - db *DB - log *slog.Logger - ID int - TguID int - Subs []string - Created time.Time - Edited time.Time - Deleted time.Time -} - -type Inst struct { - db *DB - log *slog.Logger - List []string -} - -func NewUser(db *DB, log *slog.Logger) User { - return User{ - log: log, - db: db, - } -} - -// GetAll -// Get all users in db -func (u *User) GetAll(name string) (users []User, err error) { - stmt, err := u.db.Prepare("SELECT users.id, users.tgu_id, users.subs FROM users WHERE users.subs LIKE ?") - if err != nil { - u.log.Error("[user-GetAll]", "error", err) - return nil, err - } - - rows, err := stmt.Query("%" + name + "%") - if err != nil { - u.log.Error("[user-GetAll-stmt]", "error", err) - return nil, err - } - - defer rows.Close() - users = []User{} - for rows.Next() { - user := User{} - subs := "" - if err = rows.Scan(&user.ID, &user.TguID, &subs); err != nil { - u.log.Error("[user-GetAll-scanning]", "error", err) - return users, err - } - if subs == "" { - u.log.Info("[user-GetAll-scanning] returning subs empty") - continue - } - user.Subs = strings.Split(subs, ",") - u.log.Info("user", "no", user) - users = append(users, user) - } - if err := rows.Err(); err != nil { - u.log.Error("[user-GetAll-returning]", "error", err) - return users, err - } - return users, nil -} - -// Get -// Get an specific user -func (u *User) Get(telegramID int64) (user User, err error) { - stmt, err := u.db.Prepare("SELECT users.id, users.tgu_id, users.subs FROM users WHERE tgu_id=?") - if err != nil { - u.log.Error("[user-Get]", "error", err) - return user, err - } - subs := "" - err = stmt.QueryRow(telegramID).Scan(&user.ID, &user.TguID, &subs) - if err != nil { - u.log.Error("[user-Get-stmt]", "error", err) - return user, err - } - if subs != "" { - user.Subs = strings.Split(subs, ",") - } else { - user.Subs = []string{} - } - u.ID, u.TguID, u.Subs = user.ID, user.TguID, user.Subs - return user, nil -} - -// Add -// Add user to database -func (u *User) Add(telegramID int64) (bool, error) { - stmt, err := u.db.Prepare("INSERT INTO users ('tgu_id', 'subs', 'created', 'edited') VALUES(?,?,datetime(),datetime())") - if err != nil { - u.log.Error("[user-Add-stmt]", "error", err) - return false, err - } - _, err = stmt.Exec(telegramID, "") - if err != nil { - u.log.Error("[user-Add-exec]", "error", err) - return false, err - } - return true, nil - -} - -// Edit -// edit user info in database -func (u *User) Subscribe(name string) (err error) { - if u.TguID == 0 || u.ID == 0 { - err = fmt.Errorf("user needs to have a telegram id or reference on db") - u.log.Error("[user-Subscribe-check]", "error", err) - return err - } - idx := slices.Index[[]string](u.Subs, name) - if idx >= 0 { - return fmt.Errorf("user alredy subscribed to inst") - } - u.Subs = append(u.Subs, name) - stmt, err := u.db.Prepare("UPDATE users SET subs=? WHERE tgu_id=?") - if err != nil { - u.log.Error("[user-Subscribe-stmt]", "error", err) - return err - } - strSubs := strings.Join(u.Subs, ",") - _, err = stmt.Exec(strSubs, u.TguID) - if err != nil { - u.log.Error("[user-Subscribe-exec]", "error", err) - return err - } - return nil -} - -// Delete -// Delete user from database -func (u *User) Unsubscribe(name string) (err error) { - if u.TguID == 0 || u.ID == 0 { - err = fmt.Errorf("user needs to have a telegram id or reference on db") - u.log.Error("[user-UnSubscribe-check]", "error", err) - return err - } - if len(u.Subs) <= 0 { - err = fmt.Errorf("user needs to have a subscription to unsubcribe") - u.log.Error("[user-UnSubscribe-check]", "error", err) - return err - } - - idx := slices.Index[[]string](u.Subs, name) - if idx >= 0 { - u.Subs = slices.Delete[[]string](u.Subs, idx, idx+1) - } else { - err = fmt.Errorf("user is not subscribed to %s", name) - u.log.Error("[user-UnSubscribe-check]", "error", err) - return err - } - - stmt, err := u.db.Prepare("UPDATE users SET subs=? WHERE tgu_id=?") - if err != nil { - u.log.Error("[user-UnSubscribe-stmt]", "error", err) - return err - } - strSubs := strings.Join(u.Subs, ",") - _, err = stmt.Exec(strSubs, u.TguID) - if err != nil { - u.log.Error("[user-UnSubscribe-exec]", "error", err) - return err - } - return nil -} - -// Reset -// Clean user subscriptions on system -func (u *User) Reset() (err error) { - if u.TguID == 0 || u.ID == 0 { - err = fmt.Errorf("user needs to have a telegram id or reference on db") - u.log.Error("[user-Subscribe-check]", "error", err) - return err - } - stmt, err := u.db.Prepare("UPDATE users SET subs=? WHERE tgu_id=?") - if err != nil { - u.log.Error("[user-Subscribe-stmt]", "error", err) - return err - } - _, err = stmt.Exec("", u.TguID) - if err != nil { - u.log.Error("[user-Subscribe-exec]", "error", err) - return err - } - return nil -} - -func NewInst(dbx *DB, log *slog.Logger) Inst { - return Inst{ - log: log, - db: dbx, - } -} - -func (i *Inst) GetAll() ([]string, error) { - stmt, err := i.db.Prepare("SELECT DISTINCT dolars.name FROM dolars WHERE name LIKE '%ban%' OR name LIKE '%scoti%' OR name LIKE '%asociacion%'") - if err != nil { - i.log.Error("[inst-GetAll]", "error", err) - return nil, err - } - rows, err := stmt.Query() - if err != nil { - i.log.Error("[inst-GetAll-stmt]", "error", err) - return nil, err - } - defer rows.Close() - insts := []string{} - for rows.Next() { - inst := "" - - if err = rows.Scan(&inst); err != nil { - return nil, err - } - if inst == "" { - continue - } - insts = append(insts, inst) - } - if err := rows.Err(); err != nil { - return insts, err - } - return insts, nil - -} -func (i *Inst) GetBancos() ([]string, error) { - stmt, err := i.db.Prepare("SELECT DISTINCT dolars.name FROM dolars WHERE name LIKE '%ban%' OR name LIKE '%scoti%'") - if err != nil { - i.log.Error("[inst-GetAll]", "error", err) - return nil, err - } - rows, err := stmt.Query() - if err != nil { - i.log.Error("[inst-GetAll-stmt]", "error", err) - return nil, err - } - defer rows.Close() - insts := []string{} - for rows.Next() { - inst := "" - - if err = rows.Scan(&inst); err != nil { - return nil, err - } - if inst == "" { - continue - } - insts = append(insts, inst) - } - if err := rows.Err(); err != nil { - return insts, err - } - return insts, nil - -} -func (i *Inst) GetCajas() ([]string, error) { - stmt, err := i.db.Prepare("SELECT DISTINCT dolars.name FROM dolars WHERE name LIKE '%asociacion%'") - if err != nil { - i.log.Error("[inst-GetAll]", "error", err) - return nil, err - } - rows, err := stmt.Query() - if err != nil { - i.log.Error("[inst-GetAll-stmt]", "error", err) - return nil, err - } - defer rows.Close() - insts := []string{} - for rows.Next() { - inst := "" - - if err = rows.Scan(&inst); err != nil { - return nil, err - } - if inst == "" { - continue - } - insts = append(insts, inst) - } - if err := rows.Err(); err != nil { - return insts, err - } - return insts, nil - -} - -func (i *Inst) GetAgentes() ([]string, error) { - stmt, err := i.db.Prepare("SELECT DISTINCT dolars.name FROM dolars WHERE name NOT LIKE '%ban%' AND name NOT LIKE '%scoti%' AND name NOT LIKE '%asociacion%'") - if err != nil { - i.log.Error("[inst-GetAll]", "error", err) - return nil, err - } - rows, err := stmt.Query() - if err != nil { - i.log.Error("[inst-GetAll-stmt]", "error", err) - return nil, err - } - defer rows.Close() - insts := []string{} - for rows.Next() { - inst := "" - - if err = rows.Scan(&inst); err != nil { - return nil, err - } - if inst == "" { - continue - } - insts = append(insts, inst) - } - if err := rows.Err(); err != nil { - return insts, err - } - return insts, nil - -} diff --git a/db/schema.sql b/db/schema.sql deleted file mode 100644 index be015db..0000000 --- a/db/schema.sql +++ /dev/null @@ -1,10 +0,0 @@ -PRAGMA foreign_keys = ON; - -CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY NOT NULL, - tgu_id INTEGER NOT NULL UNIQUE, - subs TEXT NOT NULL, - created TEXT NOT NULL, - edited TEXT NOT NULL, - deleted TEXT -); diff --git a/edb/db.go b/edb/db.go deleted file mode 100644 index 7c56137..0000000 --- a/edb/db.go +++ /dev/null @@ -1,302 +0,0 @@ -package db - -import ( - "database/sql" - _ "embed" - - "errors" - "fmt" - "log/slog" - "time" - - "github.com/maximotejeda/us_dop_bot/models" - _ "modernc.org/sqlite" -) - -type DB struct { - *sql.DB - log *slog.Logger -} - -type change struct { - Before models.Institucion `json:"before"` - After models.Institucion `json:"after"` -} - -type Message struct { - Message string `json:"message"` - Data change `json:"data"` - Error error `json:"error"` -} - -// Dial -func Dial(path string, log *slog.Logger) *DB { - db, err := sql.Open("sqlite", path) - if err != nil { - fmt.Printf("opening database: %s", err.Error()) - panic("opening database") - } - if err := db.Ping(); err != nil { - fmt.Printf("pinging database: %s", err.Error()) - panic("pinging database") - } - return &DB{db, log} -} - -// Inspect -// Handle behavior of the changes -// Will report errors to a nats consumer -func (db *DB) Inspect(enter models.Institucion) error { - if db == nil { - return fmt.Errorf("nil or empty database") - } - - // Get last row added - inst, err := db.GetLatest(enter.Parser, enter.Name) - // if no rows are found because of first enter a name - parser ? - if errors.Is(sql.ErrNoRows, err) { - db.log.Info("adding new item to table: ", "parse", enter.Parser, "name", enter.Name) - - if err != nil { - db.log.Error("marshaling struct", "error", err) - } - - return db.AddNew(enter) - } - // check prices compra venta - if inst == nil { - db.log.Error("row is nil", "name", enter.Name, "parser", enter.Parser) - return fmt.Errorf("row is nil, not entering row") - } - if enter.Compra == inst.Compra && enter.Venta == inst.Venta { - return nil - } else { - // if one of them changes create a new row - db.log.Info("change registered, adding item", "parse", enter.Parser, "name", enter.Name, "compra enter", enter.Compra, "compra db", inst.Compra, "venta enter", enter.Venta, "venta db", inst.Venta) - - if err != nil { - db.log.Error("marshaling struct", "error", err) - } - - return db.AddNew(enter) - } -} - -// GetLatest -// returns the latest row in a specific parser and name -// we are using DateTime in DB and date.Datetime in go -func (db *DB) GetLatest(parser string, name string) (inst *models.Institucion, err error) { - var parsed string - inst = &models.Institucion{} - stmt, err := db.Prepare("SELECT name, parser, compra, venta, parsed FROM dolars WHERE parser = ? AND name = ? ORDER BY parsed DESC LIMIT 1;") - if err != nil { - db.log.Error("preparing", "error", err.Error()) - return nil, err - } - defer stmt.Close() - - if err := stmt.QueryRow(parser, name).Scan(&inst.Name, &inst.Parser, &inst.Compra, &inst.Venta, &parsed); err != nil { - db.log.Error("getting latest", "error", err.Error(), "parser", parser, "name", name) - return nil, err - } - - inst.Parsed, err = time.Parse(time.DateTime, parsed) - if err != nil { - //db.log.Error("parsed", "error", err.Error()) - return nil, err - } - return inst, nil -} - -// AddNew -// Add a new row in the dolar table -// Will send to nats changes on prices -func (db *DB) AddNew(row models.Institucion) error { - stmt, err := db.Prepare("INSERT INTO dolars (name, compra, venta, parser, parsed) VALUES(?,?,?,?,?);") - if err != nil { - return err - } - defer stmt.Close() - parsed := row.Parsed.Format(time.DateTime) - _, err = stmt.Exec(&row.Name, &row.Compra, &row.Venta, &row.Parser, &parsed) - if err != nil { - return err - } - return nil -} - -func (db *DB) GetAll() ([]string, error) { - stmt, err := db.Prepare("SELECT DISTINCT dolars.name FROM dolars WHERE name LIKE '%ban%' OR name LIKE '%scoti%' OR name LIKE '%asociacion%'") - if err != nil { - db.log.Error("[db-GetAll]", "error", err) - return nil, err - } - rows, err := stmt.Query() - if err != nil { - db.log.Error("[db-GetAll-stmt]", "error", err) - return nil, err - } - defer rows.Close() - insts := []string{} - for rows.Next() { - inst := "" - - if err = rows.Scan(&inst); err != nil { - return nil, err - } - if inst == "" { - continue - } - insts = append(insts, inst) - } - if err := rows.Err(); err != nil { - return insts, err - } - return insts, nil - -} -func (db *DB) GetBancos() ([]string, error) { - stmt, err := db.Prepare("SELECT DISTINCT dolars.name FROM dolars WHERE name LIKE '%ban%' OR name LIKE '%scoti%'") - if err != nil { - db.log.Error("[inst-GetAll]", "error", err) - return nil, err - } - rows, err := stmt.Query() - if err != nil { - db.log.Error("[inst-GetAll-stmt]", "error", err) - return nil, err - } - defer rows.Close() - insts := []string{} - for rows.Next() { - inst := "" - - if err = rows.Scan(&inst); err != nil { - return nil, err - } - if inst == "" { - continue - } - insts = append(insts, inst) - } - if err := rows.Err(); err != nil { - return insts, err - } - return insts, nil - -} -func (db *DB) GetCajas() ([]string, error) { - stmt, err := db.Prepare("SELECT DISTINCT dolars.name FROM dolars WHERE name LIKE '%asociacion%'") - if err != nil { - db.log.Error("[inst-GetAll]", "error", err) - return nil, err - } - rows, err := stmt.Query() - if err != nil { - db.log.Error("[inst-GetAll-stmt]", "error", err) - return nil, err - } - defer rows.Close() - insts := []string{} - for rows.Next() { - inst := "" - - if err = rows.Scan(&inst); err != nil { - return nil, err - } - if inst == "" { - continue - } - insts = append(insts, inst) - } - if err := rows.Err(); err != nil { - return insts, err - } - return insts, nil - -} - -func (db *DB) GetAgentes() ([]string, error) { - stmt, err := db.Prepare("SELECT DISTINCT dolars.name FROM dolars WHERE name NOT LIKE '%ban%' AND name NOT LIKE '%scoti%' AND name NOT LIKE '%asociacion%'") - if err != nil { - db.log.Error("[inst-GetAll]", "error", err) - return nil, err - } - rows, err := stmt.Query() - if err != nil { - db.log.Error("[inst-GetAll-stmt]", "error", err) - return nil, err - } - defer rows.Close() - insts := []string{} - for rows.Next() { - inst := "" - - if err = rows.Scan(&inst); err != nil { - return nil, err - } - if inst == "" { - continue - } - insts = append(insts, inst) - } - if err := rows.Err(); err != nil { - return insts, err - } - return insts, nil - -} - -func (db *DB) GetLastPrice(name string) (inst *models.Institucion, err error) { - var parsed string - inst = &models.Institucion{} - stmt, err := db.Prepare("SELECT name, parser, compra, venta, parsed FROM dolars WHERE name = ? ORDER BY parsed DESC LIMIT 1;") - if err != nil { - db.log.Error("preparing", "error", err.Error()) - return nil, err - } - defer stmt.Close() - - if err := stmt.QueryRow(name).Scan(&inst.Name, &inst.Parser, &inst.Compra, &inst.Venta, &parsed); err != nil { - db.log.Error("getting last price", "error", err.Error(), "name", name) - return nil, err - } - - inst.Parsed, err = time.Parse(time.DateTime, parsed) - if err != nil { - //db.log.Error("parsed", "error", err.Error()) - return nil, err - } - return inst, nil - -} -func (db *DB) GetChangeSince(name string, duration time.Duration) (insts []*models.Institucion, err error) { - date := time.Now().Add(-duration).Format(time.DateTime) - stmt, err := db.Prepare("SELECT name, parser, compra, venta, parsed FROM dolars WHERE name = ? AND parsed > ? ORDER BY parsed DESC;") - if err != nil { - db.log.Error("[GetChangeSince] preparing", "error", err.Error()) - return nil, err - } - defer stmt.Close() - rows, err := stmt.Query(name, date) - if err != nil { - db.log.Error("[GetChangeSince] preparing", "error", err.Error()) - return nil, err - } - defer rows.Close() - for rows.Next() { - inst := models.Institucion{} - parsed := "" - if err := rows.Scan(&inst.Name, &inst.Parser, &inst.Compra, &inst.Venta, &parsed); err != nil { - db.log.Error("[GetChangeSince] scanning", "error", err) - return nil, err - } - inst.Parsed, err = time.Parse(time.DateTime, parsed) - if err != nil { - //db.log.Error("parsed", "error", err.Error()) - continue - } - insts = append(insts, &inst) - } - return insts, nil -} diff --git a/go.mod b/go.mod index 2f5a99c..85a8302 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,23 @@ module github.com/maximotejeda/us_dop_bot go 1.22.0 -require github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 +require ( + github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 + github.com/maximotejeda/msvc-proto/golang/dolar v0.0.0-8 + github.com/maximotejeda/msvc-proto/golang/tgbuser v0.0.0-11 + github.com/nats-io/nats.go v1.34.1 + golang.org/x/sync v0.6.0 + golang.org/x/text v0.14.0 + google.golang.org/grpc v1.63.2 +) + +require ( + github.com/klauspost/compress v1.17.2 // indirect + github.com/nats-io/nkeys v0.4.7 // indirect + github.com/nats-io/nuid v1.0.1 // indirect + golang.org/x/crypto v0.22.0 // indirect + golang.org/x/net v0.24.0 // indirect + golang.org/x/sys v0.19.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be // indirect + google.golang.org/protobuf v1.33.0 // indirect +) diff --git a/go.sum b/go.sum index db8e45c..36699fc 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,32 @@ github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc= github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8= +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/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4= +github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/maximotejeda/msvc-proto/golang/dolar v0.0.0-8 h1:ldphxrQiAhmctWBMCaNShDphZfHmOeKuoSWwCxV62Ho= +github.com/maximotejeda/msvc-proto/golang/dolar v0.0.0-8/go.mod h1:bAs0mlC1Vyn/BkHONL2Ik8ox9px9s9bhbJWgUQFMMWo= +github.com/maximotejeda/msvc-proto/golang/tgbuser v0.0.0-11 h1:4ePlM4kOlvqygH5o6K039DRJSRBEsJ9HbswrbaiA8+U= +github.com/maximotejeda/msvc-proto/golang/tgbuser v0.0.0-11/go.mod h1:UeWAtY6XdFuWJwdJPAK++BLE50F6KPotvX7F5DYgCls= +github.com/nats-io/nats.go v1.34.1 h1:syWey5xaNHZgicYBemv0nohUPPmaLteiBEUT6Q5+F/4= +github.com/nats-io/nats.go v1.34.1/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8= +github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI= +github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be h1:LG9vZxsWGOmUKieR8wPAUR3u3MpnYFQZROPIMaXh7/A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= +google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= diff --git a/internal/adapters/chat/chat.go b/internal/adapters/chat/chat.go new file mode 100644 index 0000000..ca1e17d --- /dev/null +++ b/internal/adapters/chat/chat.go @@ -0,0 +1,192 @@ +package chat + +import ( + "bytes" + "context" + "fmt" + "regexp" + "strconv" + "strings" + "sync" + "unicode" + + tgb "github.com/go-telegram-bot-api/telegram-bot-api/v5" + "golang.org/x/text/runes" + "golang.org/x/text/transform" + "golang.org/x/text/unicode/norm" +) + +var ChatPool *sync.Pool + +type ChatObj struct { + update *tgb.Update + bot *tgb.BotAPI +} + +func NewChatObj(bot *tgb.BotAPI, updt *tgb.Update) (chat *ChatObj) { + if ChatPool == nil { + ChatPool = &sync.Pool{ + New: func() any { return &ChatObj{} }, + } + for i := 0; i < 20; i++ { + ChatPool.Put(ChatPool.New()) + } + } else { + fmt.Println("alredy populated") + } + + chat = ChatPool.Get().(*ChatObj) + chat.update = updt + chat.bot = bot + return chat +} + +func EmptyChat(chat *ChatObj) { + chat.update = nil + ChatPool.Put(chat) +} + +func HandleChat(bot *tgb.BotAPI, updt *tgb.Update) { + chat := NewChatObj(bot, updt) + defer EmptyChat(chat) + text := NormalizeText(updt.Message.Text) + textList := strings.Split(text, " ") + msg := tgb.NewMessage(updt.Message.Chat.ID, "") + if len(textList) >= 1 && MessageChecker(text) == "digit" { + // in case of message match a cedula + ced, err := info.NewCedula(text) + if err != nil { + msg.Text = "cedula no reconocida " + err.Error() + } else { + msg, photoMsg := ProcessByCedula(*ced) + msg.ChatID = updt.Message.Chat.ID + if photoMsg != nil { + photoMsg.ChatID = updt.Message.Chat.ID + bot.Send(photoMsg) + } + bot.Send(msg) + return + } + } else if len(textList) >= 2 && MessageChecker(text) == "word" { + msg := ProcessByName(textList) + msg.ChatID = updt.Message.Chat.ID + bot.Send(msg) + } + + msg.ReplyToMessageID = updt.Message.MessageID + bot.Send(msg) + +} + +// ProcessByCedula +// +// When a text arrives the chat if it match a cedula try to query db +func ProcessByCedula(dbS *db.DB, ced info.Cedula) (message *tgb.MessageConfig, fotoMsg *tgb.PhotoConfig) { + msg := tgb.NewMessage(0, "") + message = &msg + ctx := context.Background() + info, err := info.GetByCedula(ctx, dbS, ced) + if err != nil { + msg.Text = "error buscando cedula " + err.Error() + } + if info != nil { + msg.Text = fmt.Sprintf(` +Nombres: %s +Apellidos: %s %s +Cedula: %s +Direccion: %s +Telefono: %s +`, + strings.ToLower(info.Nombres), strings.ToLower(info.Apellido1), strings.ToLower(info.Apellido2), + info.MunCed+info.SeqCed+info.VerCed, RemoveSpaces(info.Direccion), info.Telefono) + } + foto, err := fotos.GetImageByCedulas(ctx, dbS, ced) + if err != nil { + fmt.Println("Photo not found", err.Error()) + return + } + if foto != nil { + rq := tgb.FileReader{Name: fmt.Sprintf("%s-%s-%s", ced.MunCed, ced.SeqCed, ced.VerCed), Reader: bytes.NewReader(foto)} + fotost := tgb.NewPhoto(msg.ChatID, rq) + fotoMsg = &fotost + + } + + return +} + +func ProcessByName(dbS *db.DB, nameList []string) (message *tgb.MessageConfig) { + ctx := context.Background() + var err error + page := 0 + // look for if the last part of the list is a number + lastItem := nameList[len(nameList)-1] + + if MessageChecker(lastItem) == "digit" && !strings.HasPrefix(lastItem, "0") { + pageInt, err := strconv.Atoi(lastItem) + if err != nil { + fmt.Println(err) + } + nameList = nameList[:len(nameList)-1] + if pageInt < 20 { + page = pageInt + } + } + rows := &info.MultipleResults{} + message = &tgb.MessageConfig{} + text := strings.Join(nameList, " ") + rows, err = info.GetByFTS(ctx, dbS, text, uint(page)) + if err != nil { + message.Text = "no hubo resultados para la busqueda" + return + } + textToSend := fmt.Sprintf(` +Busqueda: + Texto: %s + Pagina: %d + resultados desde: %d A %d +De un total de: %d + +`, + text, + (rows.Page), + (rows.Page*10)-9, (rows.Page)*10, + rows.Total) + for _, v := range rows.Data { + textToSend = textToSend + fmt.Sprintf("\n %s %s %s \n %s-%s-%s\n", strings.ToLower(v.Nombres), strings.ToLower(v.Apellido1), strings.ToLower(v.Apellido2), v.MunCed, v.SeqCed, v.VerCed) + } + message.Text = textToSend + return +} + +func MessageChecker(text string) string { + // Check is text start with number + checkSpace := regexp.MustCompile(`^[\s]+.*`) + checkNumber := regexp.MustCompile(`^[\d]+.*`) + checkWord := regexp.MustCompile(`^[a-zA-Z% ]+.*`) + + if checkNumber.MatchString(text) { + return "digit" + } else if checkWord.MatchString(text) { + return "word" + } else if checkSpace.MatchString(text) { + t := strings.TrimSpace(text) + return MessageChecker(t) + } + return "" +} + +// NormalizeText +// remove foreign accent +func NormalizeText(text string) string { + t := transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC) + result, _, _ := transform.String(t, text) + return result +} + +func RemoveSpaces(text string) (res string) { + re := regexp.MustCompile(`[\s]+`) + + res = re.ReplaceAllString(text, " ") + return +} diff --git a/internal/adapters/dolar/dolar.go b/internal/adapters/dolar/dolar.go new file mode 100644 index 0000000..542f71d --- /dev/null +++ b/internal/adapters/dolar/dolar.go @@ -0,0 +1,118 @@ +package dolar + +import ( + "context" + + "github.com/maximotejeda/msvc-proto/golang/dolar" + "github.com/maximotejeda/us_dop_bot/internal/application/domain" + "google.golang.org/grpc" +) + +type Adapter struct { + dolar dolar.DollarClient + conn *grpc.ClientConn +} + +func NewAdapter(conn *grpc.ClientConn) (*Adapter, error) { + client := dolar.NewDollarClient(conn) + return &Adapter{dolar: client, conn: conn}, nil +} +func (a *Adapter) GetLatest(name string) (*domain.History, error) { + hr, err := a.dolar.GetLatest(context.Background(), &dolar.GetLatestRequest{Name: name}) + if err != nil { + return nil, err + } + history := &domain.History{ + ID: hr.Actual.Id, + Institution: domain.Institution{ + Name: hr.Actual.Name, + }, + Compra: float64(hr.Actual.Compra), + Venta: float64(hr.Actual.Venta), + Parser: hr.Actual.Parser, + Parsed: hr.Actual.Parsed, + } + return history, nil +} + +func (a Adapter) GetSince(name string, duration int64) (list []*domain.History, err error) { + hrl, err := a.dolar.GetSince(context.Background(), &dolar.GetSinceRequest{ + Name: name, + Duration: duration, + }) + if err != nil { + return nil, err + } + + list = []*domain.History{} + for _, hr := range hrl.Histories { + hist := &domain.History{ + ID: hr.Id, + Institution: domain.Institution{ + Name: hr.Name, + }, + Compra: float64(hr.Compra), + Venta: float64(hr.Venta), + Parser: hr.Parser, + Parsed: hr.Parsed, + } + list = append(list, hist) + } + return list, nil +} + +func (a Adapter) GetInstByType(name string) (list []string, err error) { + hrl, err := a.dolar.GetInstByType(context.Background(), &dolar.GetInstByTypeRequest{ + Name: name, + }) + if err != nil { + return nil, err + } + + list = []string{} + + list = append(list, hrl.InstList...) + return list, nil +} + +func (a Adapter) Subscribe(tgbid int64, instName string) (bool, error) { + ctx := context.Background() + _, err := a.dolar.TGBSubscribe(ctx, &dolar.TGBSubscribeRequest{TgbId: tgbid, InstName: instName}) + if err != nil { + return false, err + } + return true, nil +} + +func (a Adapter) Unsubscribe(tgbid int64, instName string) (bool, error) { + ctx := context.Background() + _, err := a.dolar.TGBUnsubscribe(ctx, &dolar.TGBUnsubscribeRequest{TgbId: tgbid, InstName: instName}) + if err != nil { + return false, err + } + return true, nil + +} +func (a Adapter) GetSubscribedUsers(instName string) ([]int64, error) { + ctx := context.Background() + users, err := a.dolar.TGBGetSubscribedUsers(ctx, &dolar.TGBGetSubscribedUsersRequest{InstName: instName}) + if err != nil { + return nil, err + } + list := []int64{} + list = append(list, users.TgbIds...) + + return list, nil + +} +func (a Adapter) GetSubscribedInsts(tgbid int64) ([]string, error) { + ctx := context.Background() + insts, err := a.dolar.TGBGetSubscribedInsts(ctx, &dolar.TGBGetSubscribedInstRequest{TgbId: tgbid}) + if err != nil { + return nil, err + } + list := []string{} + + list = append(list, insts.InstName...) + return list, nil +} diff --git a/internal/adapters/user/user.go b/internal/adapters/user/user.go new file mode 100644 index 0000000..dcc3fb7 --- /dev/null +++ b/internal/adapters/user/user.go @@ -0,0 +1,143 @@ +package user + +import ( + "context" + "log/slog" + + tgb "github.com/go-telegram-bot-api/telegram-bot-api/v5" + "github.com/maximotejeda/msvc-proto/golang/tgbuser" + "github.com/maximotejeda/us_dop_bot/internal/application/domain" + "google.golang.org/grpc" +) + +type Adapter struct { + user tgbuser.UserManagerClient + conn *grpc.ClientConn + log *slog.Logger +} + +func NewAdapter(conn *grpc.ClientConn) (*Adapter, error) { + log := slog.Default() + log = log.With("location", "user adapter") + client := tgbuser.NewUserManagerClient(conn) + return &Adapter{user: client, conn: conn, log: log}, nil +} +func (a *Adapter) Get(tgbid int64) (*domain.User, error) { + hr, err := a.user.Get(context.Background(), &tgbuser.GetTGBUserRequest{TgbId: tgbid}) + if err != nil { + return nil, err + } + + user := &domain.User{ + ID: hr.User.Id, + TguID: hr.User.TgbId, + Created: hr.User.Created, + Edited: hr.User.Edited, + } + return user, nil +} + +func (a Adapter) Create(user *tgb.User) (b bool, err error) { + _, err = a.user.Create(context.Background(), &tgbuser.CreateTGBUserRequest{ + User: &tgbuser.User{ + TgbId: user.ID, + FirstName: user.FirstName, + LastName: user.LastName, + Username: user.UserName, + }, + }) + if err != nil { + return false, err + } + return true, nil +} + +func (a Adapter) Edit(user *tgb.User) (b bool, err error) { + + _, err = a.user.Edit(context.Background(), &tgbuser.EditTGBUserRequest{ + User: &tgbuser.User{ + Username: user.UserName, + FirstName: user.FirstName, + LastName: user.LastName, + }, + }) + if err != nil { + return false, err + } + + return true, nil +} +func (a Adapter) Delete(tgbid int64) (b bool, err error) { + _, err = a.user.Delete(context.Background(), &tgbuser.DeleteTGBUserRequest{ + TgbId: tgbid, + }) + if err != nil { + return false, err + } + + return true, nil +} + +func (a Adapter) GetBots(tgbid int64) (s []string, err error) { + hr, err := a.user.GetBots(context.Background(), &tgbuser.GetBotsTGBUserRequest{ + TgbId: tgbid, + }) + if err != nil { + return nil, err + } + s = []string{} + + if len(hr.Bots) <= 0 { + return s, nil + } + + for _, it := range hr.Bots { + s = append(s, it.BotName) + } + return s, nil +} + +func (a Adapter) AddBot(tgbid int64, botname string) (b bool, err error) { + _, err = a.user.AddBot(context.Background(), &tgbuser.AddBotTGBUserRequest{ + TgbId: tgbid, + BotName: botname, + }) + if err != nil { + return false, err + } + return true, nil +} + +func (a Adapter) DeleteBot(tgbid int64, botname string) (b bool, err error) { + _, err = a.user.DeleteBot(context.Background(), &tgbuser.DeleteBotTGBUserRequest{ + TgbId: tgbid, + BotName: botname, + }) + if err != nil { + return false, err + } + return true, nil +} + +func (a Adapter) GetAllBotsUsers(botname string) ([]*domain.User, error) { + users, err := a.user.GetAllBotsUsers(context.Background(), &tgbuser.GetAllBotsUsersRequest{BotName: botname}) + if err != nil { + a.log.Error("get all bots users", "error", err) + return nil, err + } + a.log.Info("users", "result", users) + list := []*domain.User{} + for _, us := range users.Users { + user := &domain.User{ + ID: us.Id, + TguID: us.TgbId, + Username: us.Username, + FirstName: us.FirstName, + LastName: us.LastName, + Created: us.Created, + Edited: us.Edited, + } + list = append(list, user) + } + return list, nil +} diff --git a/internal/application/api/api.go b/internal/application/api/api.go new file mode 100644 index 0000000..afcc334 --- /dev/null +++ b/internal/application/api/api.go @@ -0,0 +1,74 @@ +package api + +import ( + "log/slog" + "slices" + "strings" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" + "github.com/maximotejeda/us_dop_bot/internal/application/command" + "github.com/maximotejeda/us_dop_bot/internal/application/message" + "github.com/maximotejeda/us_dop_bot/internal/application/query" + "github.com/maximotejeda/us_dop_bot/internal/ports" +) + +type api struct { + bot *tgbotapi.BotAPI + command ports.Tgb + message ports.Tgb + query ports.Tgb + dolar ports.DolarService + user ports.UserService + log *slog.Logger +} + +func NewApi(bot *tgbotapi.BotAPI) *api { + log := slog.Default() + log = log.With("location", "root") + return &api{bot: bot, log: log} +} + +func (a *api) Run(update *tgbotapi.Update, dolar ports.DolarService, user ports.UserService) { + // check for user in db + _, err := user.Get(update.SentFrom().ID) + if err != nil { + + if strings.Contains(err.Error(), "sql: no rows in result set") { + a.log.Info("user not in database ") + a.log.Info("bot not restricted adding user to DB") + user.Create(update.SentFrom()) + a.log.Info("Adding query permision to user over bot") + user.AddBot(update.SentFrom().ID, a.bot.Self.UserName) + } + a.log.Error("getting user", "error", err) + a.log.Info("bot not restricted adding user to DB") + } + bots, err := user.GetBots(update.SentFrom().ID) + if err != nil { + a.log.Error("getting bots", "error", err) + } + if !slices.Contains(bots, a.bot.Self.UserName) { + a.log.Info("bot not found in db for user", "bot", a.bot.Self.UserName) + a.log.Info("adding bots", "user", update.SentFrom().UserName, "bot", a.bot.Self.UserName) + _, err := user.AddBot(update.SentFrom().ID, a.bot.Self.UserName) + if err != nil { + a.log.Error("adding bots", "user", update.SentFrom().UserName, "bot", a.bot.Self.UserName, "error", err) + } + } + msg := update.Message + if msg != nil { // message is not nil can be a command or a text message + if msg.IsCommand() { + a.command = command.NewCommand(a.bot, update, dolar, user) + a.command.Handler() + // is a command + } else if msg.Text != "" { + // is a text message + a.message = message.NewMessage(a.bot, update, dolar, user) + a.message.Handler() + } + } else if update.CallbackQuery != nil { + // is a cal back query + a.query = query.NewQuery(a.bot, update, dolar, user) + a.query.Handler() + } +} diff --git a/internal/application/broadcaster/broadcaster.go b/internal/application/broadcaster/broadcaster.go new file mode 100644 index 0000000..f2dc845 --- /dev/null +++ b/internal/application/broadcaster/broadcaster.go @@ -0,0 +1,109 @@ +package broadcaster + +import ( + "context" + "encoding/json" + "fmt" + "log/slog" + "strings" + "time" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" + "github.com/maximotejeda/us_dop_bot/internal/application/domain" + "github.com/maximotejeda/us_dop_bot/internal/application/helpers" + "github.com/maximotejeda/us_dop_bot/internal/ports" +) + +type Broadcaster struct { + ctx context.Context + log *slog.Logger + user ports.UserService + dolar ports.DolarService + data []byte + botName string +} + +type Message struct { + Message string `json:"message"` + Data change `json:"data"` + Error error `json:"error"` +} +type change struct { + Before *domain.History `json:"before"` + After *domain.History `json:"after"` +} + +func NewBroadCast(ctx context.Context, user ports.UserService, dolar ports.DolarService, data []byte) *Broadcaster { + log := slog.Default() + log = log.With("place", "broadcast") + return &Broadcaster{ + ctx: ctx, + user: user, + dolar: dolar, + data: data, + log: log, + } +} + +func (b *Broadcaster) SendList() []tgbotapi.MessageConfig { + // convert data to map + m := Message{} + listMsg := []tgbotapi.MessageConfig{} + err := json.Unmarshal(b.data, &m) + if err != nil { + b.log.Error("unmarshaling data", "error", err) + } + if strings.Contains(m.Message, "change") && m.Error == nil { + + userList, err := b.dolar.GetSubscribedUsers(m.Data.After.Institution.Name) + if err != nil { + b.log.Error("querying DB data", "error", err) + return nil + } + cancelBTN := map[string]string{} + cancelBTN["Eliminar ❌"] = "cancelar=true" + keyboard := helpers.CreateKeyboard(cancelBTN) + b.log.Info("printing change", "user list", userList, "name", m.Data.After.Institution.Name, "data", m.Data.After) + compraCHG := comparer(m.Data.Before.Compra, m.Data.After.Compra) + ventaCHG := comparer(m.Data.Before.Venta, m.Data.After.Venta) + loc, _ := time.LoadLocation("America/Santo_Domingo") + text := fmt.Sprintf("Cambio Registrado:\n\nInstitucion: %s\n\t Compra: %.2f %s %.2f\n\t Venta: %.2f %s %.2f\n\n\t %s", m.Data.After.Institution.Name, m.Data.Before.Compra, compraCHG, m.Data.After.Compra, m.Data.Before.Venta, ventaCHG, m.Data.After.Venta, time.Unix(m.Data.After.Parsed, 0).In(loc).Format(time.DateTime)) + + for _, userID := range userList { + msg := tgbotapi.NewMessage(userID, text) + msg.ReplyMarkup = keyboard + listMsg = append(listMsg, msg) + } + } + return listMsg +} + +func (b Broadcaster) SendAllUsers(ctx context.Context, log *slog.Logger, data []byte, botname string) []tgbotapi.MessageConfig { + userList, err := b.user.GetAllBotsUsers(botname) + b.log.Info("broadcast", "user list", userList) + msgs := []tgbotapi.MessageConfig{} + if err != nil { + return msgs + } + cancelBTN := map[string]string{} + cancelBTN["Eliminar ❌"] = "cancelar=true" + keyboard := helpers.CreateKeyboard(cancelBTN) + for _, user := range userList { + msg := tgbotapi.NewMessage(user.TguID, "") + msg.Text = string(data) + msg.ReplyMarkup = keyboard + msgs = append(msgs, msg) + } + return msgs +} + +func comparer(before, after float64) string { + if before > after { + return "⬇️" + } else if before < after { + return "⬆️" + } else { + return "🟰" + } + +} diff --git a/internal/application/command/command.go b/internal/application/command/command.go new file mode 100644 index 0000000..bfb4882 --- /dev/null +++ b/internal/application/command/command.go @@ -0,0 +1,220 @@ +package command + +import ( + "log/slog" + "slices" + "strings" + "sync" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" + "github.com/maximotejeda/us_dop_bot/internal/application/helpers" + "github.com/maximotejeda/us_dop_bot/internal/ports" +) + +var commandPool *sync.Pool + +type Command struct { + bot *tgbotapi.BotAPI + update *tgbotapi.Update + msg *tgbotapi.MessageConfig + log *slog.Logger + dolar ports.DolarService + user ports.UserService +} + +// NewCommand +// Factory for Command Handler +func NewCommand(bot *tgbotapi.BotAPI, update *tgbotapi.Update, dolar ports.DolarService, user ports.UserService) *Command { + if commandPool == nil { + commandPool = &sync.Pool{ + New: func() any { return &Command{} }, + } + for i := 0; i < 20; i++ { + commandPool.Put(commandPool.New()) + } + } + log := slog.Default() + log = log.With("function", "command", "chat", update.Message.Chat.ID, "userid", update.Message.From.ID, "username", update.Message.From.UserName) + commands := commandPool.Get().(*Command) + commands.update = update + commands.bot = bot + commands.log = log + commands.dolar = dolar + commands.user = user + return commands +} + +// Empty +// Returns pointer to command pool +func (c *Command) Empty() { + c.update = nil + c.msg = nil + c.log = nil + c.dolar = nil + commandPool.Put(c) +} + +// Send +// Process command handlers +func (c *Command) Send() { + defer c.Empty() + c.bot.Send(*c.msg) + del := tgbotapi.NewDeleteMessage(c.update.Message.From.ID, c.update.Message.MessageID) + c.bot.Send(del) + +} + +// Handler +// Manage command message for chat +func (c *Command) Handler() { + msg := tgbotapi.NewMessage(c.update.Message.Chat.ID, "command") + c.msg = &msg + command := c.update.Message.Command() + + switch strings.ToLower(command) { + case "list", "lista": + msg.Text = "list todo" + case "listabancos": + + bancos, err := c.lista("bancos") + if err != nil { + c.log.Error("query bancos", "error", err) + return + } + msg = bancos + case "listacajas": + cajas, err := c.lista("cajas") + if err != nil { + c.log.Error("query cajas", "error", err) + return + } + msg = cajas + + case "listagentes": + agentes, err := c.lista("agentes") + if err != nil { + c.log.Error("query bancos", "error", err) + return + } + msg = agentes + case "consulta": + instSubs, err := c.dolar.GetSubscribedInsts(c.update.Message.From.ID) + if err != nil { + c.log.Error("command-consulta", "err", err) + } + if len(instSubs) > 0 { + btnSTR := map[string]string{} + for _, inst := range instSubs { + switch inst { + case "asociacion popular de ahorros y prestamos": + btnSTR[inst] = "consultar=true&name=apap" + case "asociacion cibao de ahorros y prestamos": + btnSTR[inst] = "consultar=true&name=acap" + case "asociacion la nacional de ahorros y prestamos": + btnSTR[inst] = "consultar=true&name=anap" + case "asociacion peravia de ahorros y prestamos": + btnSTR[inst] = "consultar=true&name=aperap" + default: + btnSTR[inst] = "consultar=true&name=" + inst + } + + } + btnSTR["cancelar ❌"] = "cancelar=true" + keyboard := helpers.CreateKeyboard(btnSTR) + msg.ReplyMarkup = keyboard + msg.Text = "Suscripciones actuales πŸ’°\nPuedes hacer click para Ver cambios en los precios de las suscripcion\n o presionar cancelar" + } else { + msg.Text = "No existen subscripciones actualmente" + } + case "status", "info": + instSubbed, err := c.dolar.GetSubscribedInsts(c.update.Message.From.ID) + if err != nil { + c.log.Error("command-status", "err", err) + } + if len(instSubbed) > 0 { + btnSTR := map[string]string{} + for _, inst := range instSubbed { + btnSTR[inst] = "unsubs=true&name=" + inst + } + btnSTR["cancelar ❌"] = "cancelar=true" + keyboard := helpers.CreateKeyboard(btnSTR) + msg.ReplyMarkup = keyboard + msg.Text = "Suscripciones actuales πŸ’°\n Puedes hacer click en una para eliminar su subscripcion\nPresionar cancelar para omitir." + } else { + msg.Text = "No existen subscripciones actualmente" + } + case "reset": + reset := map[string]string{"Reset": "reset=true", "cancelar ❌": "cancelar=true"} + keyboard := helpers.CreateKeyboard(reset) + msg.ReplyMarkup = keyboard + msg.Text = "Esta Seguro DE Eliminar todas las suscripciones del usuario." + case "help", "start", "ayuda", "h": + + help := ` +Asistente de cambio US <-> DOP + πŸ‡ΊπŸ‡Έ ↔️ πŸ‡©πŸ‡΄ +Tracker del precio del dolar para RD. + +Funciona suscribiendo instituciones πŸ’Έ. + - Tracker precio del dolar ❇️. + - Notificacion mensaje automatico πŸ“ˆ. + - Precio actual ⌚. + - Historico de precios πŸ“…. + + Comandos Conocidos por el bot: + + /help: Muestra este mensaje ❓ + /listabancos: Muestra bancos 🏦 + /listacajas: Muestra asociaciones + /listagentes: Muestra agentes πŸ“Š + /consulta: Consulta entidad suscrita πŸ›ŽοΈ + /reset: Borra tada suscripcion 🧹 + /status: Estado del usuario πŸ“‹ +` + + msg.Text = help + default: + msg.Text = "Commando desconocido intenta con\n/help: to get bot info." + } + + c.Send() + +} +func (c *Command) lista(instSTR string) (msg tgbotapi.MessageConfig, err error) { + var instList []string + + msg = tgbotapi.MessageConfig{} + msg.ChatID = c.update.Message.Chat.ID + + instList, err = c.dolar.GetInstByType(instSTR) + if err != nil { + c.log.Error("[inst-list-query]", "error", err) + return msg, err + } + instMap := map[string]string{} + subscribed, err := c.dolar.GetSubscribedInsts(c.update.Message.From.ID) + if err != nil { + c.log.Error("getting subscription", "error", err) + } + + for _, i := range instList { + if slices.Contains(subscribed, i) { + continue + } + instMap[i] = "subs=true&name=" + i + } + instMap["cancelar ❌"] = "cancelar=true" + keyboard := helpers.CreateKeyboard(instMap) + switch instSTR { + case "bancos": + msg.Text = "Differentes Bancos disponibles para track el precio del cambio ejemplos:\n\n\tBanco Popular\n\n\tBanreservas\n\n" + case "agentes": + msg.Text = "Differentes Agentes disponibles para track el precio del cambio ejemplos:\n\n\trm\n\n\tgirosol\n\n" + case "cajas": + msg.Text = "Differentes cajas disponibles para track el precio del cambio ejemplos\n\n\tasociacion popular\n\n\tasociacion cibao\n\n" + default: + msg.Text = "lista de instuiticiones no reconocidad Opciones disponibles:\n\n\tbancos\n\n\tcajas\n\n\tagentes" + } + msg.ReplyMarkup = keyboard + return msg, nil +} diff --git a/internal/application/domain/history.go b/internal/application/domain/history.go new file mode 100644 index 0000000..ae6148f --- /dev/null +++ b/internal/application/domain/history.go @@ -0,0 +1,17 @@ +package domain + +type History struct { + ID int64 `json:"id"` + Institution Institution `json:"institution"` + Compra float64 `json:"compra,omitempty"` + Venta float64 `json:"venta,omitempty"` + Parser string `json:"parser,omitempty"` + Parsed int64 `json:"parsed,omitempty"` +} + +type Institution struct { + ID int64 `json:"id"` + Name string `json:"name"` + ShortName string `json:"short_name"` + Created int64 `json:"created"` +} diff --git a/internal/application/domain/user.go b/internal/application/domain/user.go new file mode 100644 index 0000000..3eee387 --- /dev/null +++ b/internal/application/domain/user.go @@ -0,0 +1,13 @@ +package domain + +type User struct { + ID int64 + TguID int64 + Username string `json:"username"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Subs []string + Created int64 + Edited int64 + Deleted int64 +} diff --git a/helpers/helpers.go b/internal/application/helpers/helpers.go similarity index 100% rename from helpers/helpers.go rename to internal/application/helpers/helpers.go diff --git a/internal/application/message/message.go b/internal/application/message/message.go new file mode 100644 index 0000000..c74d5c6 --- /dev/null +++ b/internal/application/message/message.go @@ -0,0 +1,215 @@ +package message + +import ( + "fmt" + "log/slog" + "regexp" + "strconv" + "strings" + "sync" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" + "github.com/maximotejeda/us_dop_bot/internal/application/domain" + "github.com/maximotejeda/us_dop_bot/internal/application/static" + "github.com/maximotejeda/us_dop_bot/internal/ports" +) + +var ChatPool *sync.Pool + +type Message struct { + bot *tgbotapi.BotAPI + update *tgbotapi.Update + msg *tgbotapi.MessageConfig + log *slog.Logger + dolar ports.DolarService + user ports.UserService +} + +// NewMessage +// Factory for message handler +func NewMessage(bot *tgbotapi.BotAPI, update *tgbotapi.Update, dolar ports.DolarService, user ports.UserService) *Message { + if ChatPool == nil { + ChatPool = &sync.Pool{ + New: func() any { return &Message{} }, + } + for i := 0; i < 20; i++ { + ChatPool.Put(ChatPool.New()) + } + } + log := slog.Default() + log = log.With("function", "message", "chat", update.Message.Chat.ID, "userid", update.Message.From.ID, "username", update.Message.From.UserName) + message := ChatPool.Get().(*Message) + message.update = update + message.bot = bot + message.log = log + message.dolar = dolar + message.user = user + return message +} + +// Empty +// Returns pointer to pool +func (m *Message) Empty() { + m.update = nil + m.msg = nil + m.log = nil + m.dolar = nil + m.user = nil + ChatPool.Put(m) +} + +// Send +// Process message sending to bot +func (m *Message) Send() { + defer m.Empty() + m.bot.Send(m.msg) +} + +// Handler +// Manage features for messages +func (m *Message) Handler() { + msg := tgbotapi.NewMessage(m.update.Message.Chat.ID, "") + m.msg = &msg + msgtext := static.RemoveAccent(m.update.Message.Text) + re := regexp.MustCompile(`(?P([cC]omprar?\s?(me)?|[vV]en(de)?(r)?(ta)?\s?(me)?))\s(?P[0-9.]{1,8})\s(?P(dolar(e)?(s)?|peso(s)?|dollars?))\s?(?P(.*))?`) + match := re.FindStringSubmatch(msgtext) + if len(match) != 0 { + operacion := match[1] + operacion = strings.ToLower(operacion) + cantidadStr := match[8] + cantidad, err := strconv.ParseFloat(cantidadStr, 64) + if err != nil { + m.log.Error("converting to float cantidad", "error", err) + m.msg.Text = "cantidad no reconocidad " + cantidadStr + m.Send() + return + } + moneda := match[9] + moneda = strings.ToLower(moneda) + institucion := match[14] + institucion = strings.ToLower(institucion) + switch { + case strings.Contains(operacion, "compra"): + txt := m.Compra(cantidad, moneda, institucion) + m.msg.Text = txt + case strings.Contains(operacion, "vend"): + txt := m.Venta(cantidad, moneda, institucion) + m.msg.Text = txt + + default: + m.msg.Text = "operacion no reconocida" + } + + m.Send() + } +} + +func (m *Message) Compra(cantidad float64, moneda, institucion string) string { + txt := "" + inst := static.NewInstList() + list := []*domain.History{} + if institucion == "" { + brd, bp, bhd, bcd := m.getPrincipalBank() + list = []*domain.History{brd, bp, bhd, bcd} + } else { + + txtList := inst.GetName(institucion) + if len(txtList) <= 0 { + txt = "no institution with name " + institucion + return txt + } else { + for _, v := range txtList { + i, err := m.dolar.GetLatest(v.Name) + if err != nil { + m.log.Error("getting latest", "inst", v, "error", err) + continue + } + list = append(list, i) + } + } + } + if strings.Contains(moneda, "peso") { + txt = fmt.Sprintf("Comprando dolares \nRD.$ %.2f pesos:\n", cantidad) + for _, it := range list { + compra := cantidad / it.Venta + txt = txt + fmt.Sprintf(" %s \t->\t USD$. %.2f\n", inst.GetAbbrev(it.Institution.Name), compra) + } + } else if strings.Contains(moneda, "dol") { + txt = fmt.Sprintf("Comprando %.2f dolares:\n", cantidad) + for _, it := range list { + compra := cantidad * it.Venta + txt = txt + fmt.Sprintf(" %s \t->\t RD$. %.2f\n", inst.GetAbbrev(it.Institution.Name), compra) + } + } + + return txt +} + +func (m *Message) Venta(cantidad float64, moneda, institucion string) string { + txt := "" + list := []*domain.History{} + + inst := static.NewInstList() + if institucion == "" { + brd, bp, bhd, bcd := m.getPrincipalBank() + list = []*domain.History{brd, bp, bhd, bcd} + } else { + txtList := inst.GetName(institucion) + if len(txtList) <= 0 { + txt = "no institution with name " + institucion + return txt + } else { + for _, v := range txtList { + i, err := m.dolar.GetLatest(v.Name) + if err != nil { + m.log.Error("getting latest", "inst", v, "error", err) + continue + } + list = append(list, i) + } + } + + } + if len(list) <= 0 { + return "no institutions found with name " + institucion + } + if strings.Contains(moneda, "peso") { + txt = fmt.Sprintf("Vendiendo equivalente a RD.$ %.2f pesos:\n", cantidad) + for _, it := range list { + venta := cantidad / it.Compra + txt = txt + fmt.Sprintf(" %s \t->\t USD$. %.2f\n", inst.GetAbbrev(it.Institution.Name), venta) + } + + } else if strings.Contains(moneda, "dol") { + txt = fmt.Sprintf("Vendiendo USD.$ %.2f dolares\n", cantidad) + for _, it := range list { + venta := cantidad * it.Compra + txt = txt + fmt.Sprintf(" %s \t->\t RD$. %.2f\n", inst.GetAbbrev(it.Institution.Name), venta) + } + + } + + return txt +} + +func (m *Message) getPrincipalBank() (brd, bp, bhd, bcd *domain.History) { + // principales bancos + // bhd reservas popular central + brd, err := m.dolar.GetLatest("banreservas") + if err != nil { + m.log.Error("query latest for banreservas", "error", err) + } + bp, err = m.dolar.GetLatest("banco popular") + if err != nil { + m.log.Error("query latest for banco popular") + } + bhd, err = m.dolar.GetLatest("banco hipotecario dominicano") + if err != nil { + m.log.Error("query latest for banreservas") + } + bcd, err = m.dolar.GetLatest("banco central dominicano") + if err != nil { + m.log.Error("query latest for banreservas") + } + return brd, bp, bhd, bcd +} diff --git a/internal/application/query/query.go b/internal/application/query/query.go new file mode 100644 index 0000000..d23ac9a --- /dev/null +++ b/internal/application/query/query.go @@ -0,0 +1,195 @@ +package query + +import ( + "fmt" + "log/slog" + "strconv" + "strings" + "sync" + "time" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" + "github.com/maximotejeda/us_dop_bot/internal/application/domain" + "github.com/maximotejeda/us_dop_bot/internal/application/helpers" + "github.com/maximotejeda/us_dop_bot/internal/ports" +) + +var chatPool *sync.Pool + +type Query struct { + bot *tgbotapi.BotAPI + update *tgbotapi.Update + msg *tgbotapi.MessageConfig + log *slog.Logger + dolar ports.DolarService + user ports.UserService +} + +// NewQuery +// Factory for query handlers +func NewQuery(bot *tgbotapi.BotAPI, update *tgbotapi.Update, dolar ports.DolarService, user ports.UserService) *Query { + if chatPool == nil { + chatPool = &sync.Pool{ + New: func() any { return &Query{} }, + } + for i := 0; i < 20; i++ { + chatPool.Put(chatPool.New()) + } + } + log := slog.Default() + log = log.With("function", "query", "chat", update.CallbackQuery.From.ID, "userid", update.CallbackQuery.From.ID, "username", update.CallbackQuery.From.UserName) + query := chatPool.Get().(*Query) + query.update = update + query.bot = bot + query.log = log + query.dolar = dolar + query.user = user + return query +} + +// Empty +// Returns pointer to pool +func (q *Query) Empty() { + q.update = nil + q.msg = nil + q.log = nil + chatPool.Put(q) +} + +// Send +// Process Query message +func (q *Query) Send() { + defer q.Empty() + q.bot.Send(q.msg) + // Delete previous message + del := tgbotapi.NewDeleteMessage(q.update.CallbackQuery.From.ID, q.update.CallbackQuery.Message.MessageID) + q.bot.Send(del) + +} + +// Handler +// Manage query message +func (q *Query) Handler() { + msg := tgbotapi.NewMessage(q.update.CallbackQuery.Message.Chat.ID, "") + q.msg = &msg + tUser := q.update.CallbackQuery.From + + data := q.update.CallbackQuery.Data + + dataList := strings.Split(data, "&") + + dataMap := map[string]string{} + for _, val := range dataList { + subData := strings.Split(val, "=") + dataMap[subData[0]] = subData[1] + } + + switch { + case dataMap["subs"] != "": + _, err := q.dolar.Subscribe(q.update.CallbackQuery.From.ID, dataMap["name"]) + if err != nil { + q.log.Error("subs-query", "error", err.Error(), "user", q.update.CallbackQuery.From) + } + case dataMap["unsubs"] != "": + _, err := q.dolar.Unsubscribe(q.update.CallbackQuery.From.ID, dataMap["name"]) + if err != nil { + q.log.Error("subs-query", "error", err.Error(), "user", q.update.CallbackQuery.From) + } + + case dataMap["reset"] != "": + subscriptions, _ := q.dolar.GetSubscribedInsts(q.update.CallbackQuery.From.ID) + if len(subscriptions) > 0 { + for _, inst := range subscriptions { + q.dolar.Unsubscribe(q.update.CallbackQuery.From.ID, inst) + q.log.Info("unsubscribing", "institution", inst) + } + } + case dataMap["consultar"] != "": + name := "&name=" + dataMap["name"] + queryMap := map[string]string{ + "Actual": "query=true&time=0&unit=now" + name, + //"30 Minutos": "query=true&time=30&unit=minute" + name, + "1 Hora": "query=true&time=1&unit=hour" + name, + "6 Horas": "query=true&time=6&unit=hour" + name, + "12 Horas": "query=true&time=12&unit=hour" + name, + "1 Dia": "query=true&time=24&unit=hour" + name, + "1 Semana": "query=true&time=168&unit=hour" + name, + "2 Semanas": "query=true&time=336&unit=hour" + name, + "1 Mes": "query=true&time=672&unit=hour" + name, + } + keyboard := helpers.CreateKeyboard(queryMap) + msg = tgbotapi.MessageConfig{} + msg.ChatID = tUser.ID + msg.ReplyMarkup = keyboard + msg.Text = fmt.Sprintf("Intervalos de tiempo disponibles para consulta en %s el cambio del dolar desde hace:", dataMap["name"]) + case dataMap["query"] != "": + var timeUnit time.Duration + timeAmntSTR := dataMap["time"] + timeUnitSTR := dataMap["unit"] + name := dataMap["name"] + timeAmnt, err := strconv.Atoi(timeAmntSTR) + switch name { + case "apap": + name = "asociacion popular de ahorros y prestamos" + case "acap": + name = "asociacion cibao de ahorros y prestamos" + case "anap": + name = "asociacion la nacional de ahorros y prestamos" + case "aperap": + name = "asociacion peravia de ahorros y prestamos" + } + if err != nil { + q.log.Error("[query-query] converting amount of time to int", "error", err) + q.Send() + return + + } + switch timeUnitSTR { + case "hour": + timeUnit = time.Hour * time.Duration(timeAmnt) + instList, err := q.dolar.GetSince(name, int64(timeUnit.Minutes())) + //q.log.Info("hour provided", "hour", timeUnit, "int hours", int64(timeUnit.Minutes())) + if err != nil { + q.log.Error("[GETLIST] querying the inst database hours", "error", err) + return + } + queryMap := map[string]string{"clear": "cancel=true"} + keyboard := helpers.CreateKeyboard(queryMap) + msg = tgbotapi.MessageConfig{} + msg.ChatID = tUser.ID + msg.ReplyMarkup = keyboard + + msg.Text = instMessage(instList) + case "now": + instRes, err := q.dolar.GetLatest(name) + if err != nil { + q.log.Error("queriing the inst database now", "error", err) + return + } + queryMap := map[string]string{"clear": "cancel=true"} + keyboard := helpers.CreateKeyboard(queryMap) + msg = tgbotapi.MessageConfig{} + msg.ChatID = tUser.ID + + msg.ReplyMarkup = keyboard + msg.Text = fmt.Sprintf("%s\nCompra: %.2f\nVenta: %.2f", instRes.Institution.Name, instRes.Compra, instRes.Venta) + } + } + + q.Send() +} + +func instMessage(insts []*domain.History) string { + if len(insts) <= 0 { + return "Sin cambios registrados en este intervalo\nPrueba a ampliar el rango del tiempo deseado." + } + name := insts[0].Institution.Name + resultText := fmt.Sprintf("%s\n\n", name) + + for _, i := range insts { + date := time.Unix(i.Parsed, 0) + loc, _ := time.LoadLocation("America/Santo_Domingo") + resultText = resultText + fmt.Sprintf(" %s\n Compra: %.2f Venta: %.2f\n", date.In(loc).Format(time.DateTime), i.Compra, i.Venta) + } + return resultText +} diff --git a/internal/application/static/static.go b/internal/application/static/static.go new file mode 100644 index 0000000..e50d470 --- /dev/null +++ b/internal/application/static/static.go @@ -0,0 +1,114 @@ +package static + +import ( + "strings" + "unicode" + + "golang.org/x/text/runes" + "golang.org/x/text/transform" + "golang.org/x/text/unicode/norm" +) + +var institutionList *instList + +type Institution struct { + ID int64 + Name string + ShortName string +} +type instList struct { + inst []*Institution +} + +func NewInstList() *instList { + return &instList{} +} + +func (i instList) GetName(name string) []*Institution { + inititate() + // strip banco dominicano asociacion de ahorros y prestamos + replacer := strings.NewReplacer( + "banco", "", + "dominicano", "", + "asociacion ", "", + "ahorros", "", + "prestamos", "", + " de ", "", + " y ", "", + " ", " ", + ) + name = replacer.Replace(name) + list := []*Institution{} + if len(name) <= 1 { + return []*Institution{} + } + for _, inst := range institutionList.inst { + if strings.Contains(inst.Name, name) || strings.Contains(inst.ShortName, name) { + list = append(list, inst) + } + } + return list +} +func (i instList) GetAbbrev(name string) string { + inititate() + for _, i := range institutionList.inst { + if i.Name == name { + return i.ShortName + } + } + return "" +} +func inititate() { + if institutionList == nil || len(institutionList.inst) <= 0 { + institutionList = &instList{ + inst: []*Institution{ + {ID: 1, Name: "banco central dominicano", ShortName: "bcd"}, + {ID: 2, Name: "banco popular", ShortName: "bpd"}, + {ID: 3, Name: "banco hipotecario dominicano", ShortName: "bhd"}, + {ID: 4, Name: "banreservas", ShortName: "brd"}, + {ID: 5, Name: "asociacion peravia de ahorros y prestamos", ShortName: "apeap"}, + {ID: 6, Name: "asociacion popular de ahorros y prestamos", ShortName: "apap"}, + {ID: 7, Name: "asociacion cibao de ahorros y prestamos", ShortName: "acap"}, + {ID: 8, Name: "asociacion la nacional de ahorros y prestamos", ShortName: "alnap"}, + {ID: 9, Name: "banco promerica", ShortName: "bpr"}, + {ID: 10, Name: "banco bdi", ShortName: "bbd"}, + {ID: 11, Name: "banco caribe", ShortName: "bca"}, + {ID: 12, Name: "banco santa cruz", ShortName: "bsc"}, + {ID: 13, Name: "banco vimenca", ShortName: "bvi"}, + {ID: 14, Name: "scotiabank cambio online", ShortName: "scline"}, + {ID: 15, Name: "scotiabank", ShortName: "scotiabank"}, + {ID: 16, Name: "bonanza banco", ShortName: "bba"}, + {ID: 17, Name: "banco atlantico", ShortName: "bat"}, + {ID: 18, Name: "banco lopez de haro", ShortName: "blh"}, + {ID: 19, Name: "banco ademi", ShortName: "bad"}, + {ID: 20, Name: "banco lafise", ShortName: "bla"}, + {ID: 21, Name: "banesco", ShortName: "banesco"}, + {ID: 22, Name: "banco activo dominicana", ShortName: "bacd"}, + {ID: 23, Name: "girosol", ShortName: "girosol"}, + {ID: 24, Name: "moneycorps", ShortName: "moneycorps"}, + {ID: 25, Name: "imbert y balbuena", ShortName: "imb"}, + {ID: 26, Name: "rm", ShortName: "rm"}, + {ID: 27, Name: "motor credito", ShortName: "mcr"}, + {ID: 28, Name: "cambio extranjero", ShortName: "cex"}, + {ID: 29, Name: "capla", ShortName: "capla"}, + {ID: 30, Name: "taveras", ShortName: "taveras"}, + {ID: 31, Name: "gamelin", ShortName: "gamelin"}, + {ID: 32, Name: "sct", ShortName: "sct"}, + {ID: 33, Name: "panora exchange", ShortName: "inf"}, + }, + } + } +} + +// RemoveAccent +// helps normalize names in db +// https://stackoverflow.com/questions/24588295/go-removing-accents-from-strings +func RemoveAccent(str string) string { + if str == "" { + return "" + } + t := transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC) + s, _, _ := transform.String(t, str) + + return s +} diff --git a/internal/ports/dolar.go b/internal/ports/dolar.go new file mode 100644 index 0000000..3da99b7 --- /dev/null +++ b/internal/ports/dolar.go @@ -0,0 +1,13 @@ +package ports + +import "github.com/maximotejeda/us_dop_bot/internal/application/domain" + +type DolarService interface { + GetLatest(name string) (*domain.History, error) + GetSince(name string, duration int64) ([]*domain.History, error) + GetInstByType(name string) ([]string, error) + Subscribe(int64, string) (bool, error) + Unsubscribe(int64, string) (bool, error) + GetSubscribedUsers(string) ([]int64, error) + GetSubscribedInsts(int64) ([]string, error) +} diff --git a/internal/ports/tgb.go b/internal/ports/tgb.go new file mode 100644 index 0000000..929c3e7 --- /dev/null +++ b/internal/ports/tgb.go @@ -0,0 +1,7 @@ +package ports + +type Tgb interface { + Send() + Empty() + Handler() +} diff --git a/internal/ports/user.go b/internal/ports/user.go new file mode 100644 index 0000000..20e33db --- /dev/null +++ b/internal/ports/user.go @@ -0,0 +1,17 @@ +package ports + +import ( + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" + "github.com/maximotejeda/us_dop_bot/internal/application/domain" +) + +type UserService interface { + Get(int64) (*domain.User, error) + Edit(*tgbotapi.User) (bool, error) + Delete(int64) (bool, error) + Create(*tgbotapi.User) (bool, error) + AddBot(int64, string) (bool, error) + GetBots(int64) ([]string, error) + DeleteBot(int64, string) (bool, error) + GetAllBotsUsers(string) ([]*domain.User, error) +} diff --git a/k8s/deployment.yml.template b/k8s/deployment.yml.template index 6a36ca2..b6e4ea2 100644 --- a/k8s/deployment.yml.template +++ b/k8s/deployment.yml.template @@ -19,18 +19,15 @@ spec: - name: us-dop-bot image: localhost:32000/us-dop-bot:latest env: - - name: DBURINST - value: $DBURINST - - name: DBURIUSER - value: $DBURIUSER + - name: DBURI + value: $DBURI - name: NATSURI value: "nats://nats-svc:4222" - name: TOKEN - value: "$TOKEN" - volumeMounts: - - name: database - mountPath: /app/dolardb - volumes: - - name: database - persistentVolumeClaim: - claimName: bank-crawler-pvc \ No newline at end of file + value: "$PRODTOKEN" + - name: DOLLAR_SERVICE_URL + value: "dolar-grpc-svc:80" + - name: TGBUSER_SERVICE_URL + value: "tgbuser-grpc-svc:80" + - name: ENV + value: "production" diff --git a/models/models.go b/models/models.go deleted file mode 100644 index f6008d1..0000000 --- a/models/models.go +++ /dev/null @@ -1,13 +0,0 @@ -package models - -import ( - "time" -) - -type Institucion struct { - Name string `json:"name"` - Compra float64 `json:"compra"` - Venta float64 `json:"venta"` - Parser string `json:"parser"` - Parsed time.Time `json:"parsed"` -} diff --git a/query/query.go b/query/query.go deleted file mode 100644 index 2b7ef96..0000000 --- a/query/query.go +++ /dev/null @@ -1,152 +0,0 @@ -package query - -import ( - "context" - "fmt" - "log/slog" - "strconv" - "strings" - "time" - - tg "github.com/go-telegram-bot-api/telegram-bot-api/v5" - "github.com/maximotejeda/us_dop_bot/db" - edb "github.com/maximotejeda/us_dop_bot/edb" - "github.com/maximotejeda/us_dop_bot/helpers" - "github.com/maximotejeda/us_dop_bot/models" -) - -// QueryHandler -// Manage queries to execute user commands -func QueryHandler(ctx context.Context, dbx *db.DB, inst *edb.DB, log *slog.Logger, query *tg.CallbackQuery) (msg *tg.MessageConfig) { - tUser := query.From - user := db.NewUser(dbx, log) - _, err := user.Get(tUser.ID) - if err != nil { - log.Error("callback", "error", err) - } - data := query.Data - - dataList := strings.Split(data, "&") - - dataMap := map[string]string{} - for _, val := range dataList { - subData := strings.Split(val, "=") - dataMap[subData[0]] = subData[1] - } - - switch { - case dataMap["subs"] != "": - err := user.Subscribe(dataMap["name"]) - if err != nil { - log.Error("subs-query", "error", err.Error(), "user", user) - } - case dataMap["unsubs"] != "": - user.Get(tUser.ID) - err := user.Unsubscribe(dataMap["name"]) - if err != nil { - log.Error("unsubs-query", "error", err.Error()) - } - case dataMap["reset"] != "": - err := user.Reset() - if err != nil { - log.Error("[query reset] ", "error", err) - } - case dataMap["consultar"] != "": - name := "&name=" + dataMap["name"] - queryMap := map[string]string{ - "Actual": "query=true&time=0&unit=now" + name, - "30 Minutos": "query=true&time=30&unit=minute" + name, - "1 Hora": "query=true&time=1&unit=hour" + name, - "6 Horas": "query=true&time=6&unit=hour" + name, - "12 Horas": "query=true&time=12&unit=hour" + name, - "1 Dia": "query=true&time=24&unit=hour" + name, - "1 Semana": "query=true&time=168&unit=hour" + name, - "2 Semanas": "query=true&time=336&unit=hour" + name, - "1 Mes": "query=true&time=672&unit=hour" + name, - } - keyboard := helpers.CreateKeyboard(queryMap) - msg = &tg.MessageConfig{} - msg.ChatID = tUser.ID - msg.ReplyMarkup = keyboard - msg.Text = fmt.Sprintf("Intervalos de tiempo disponibles para consulta en %s el cambio del dolar desde hace:", dataMap["name"]) - case dataMap["query"] != "": - var timeUnit time.Duration - timeAmntSTR := dataMap["time"] - timeUnitSTR := dataMap["unit"] - name := dataMap["name"] - timeAmnt, err := strconv.Atoi(timeAmntSTR) - switch name { - case "apap": - name = "asociacion popular de ahorros y prestamos" - case "acap": - name = "asociacion cibao de ahorros y prestamos" - case "anap": - name = "asociacion la nacional de ahorros y prestamos" - } - if err != nil { - log.Error("[query-query] converting amount of time to int", "error", err) - return msg - } - switch timeUnitSTR { - case "minute": - timeUnit = time.Minute * time.Duration(timeAmnt) - instList, err := inst.GetChangeSince(name, timeUnit) - if err != nil { - log.Error("[GETLIST] querying the inst database minutes", "error", err) - return - } - queryMap := map[string]string{"clear": "cancel=true"} - keyboard := helpers.CreateKeyboard(queryMap) - msg = &tg.MessageConfig{} - msg.ChatID = tUser.ID - msg.ReplyMarkup = keyboard - - msg.Text = instMessage(instList) - - case "hour": - timeUnit = time.Hour * time.Duration(timeAmnt) - instList, err := inst.GetChangeSince(name, timeUnit) - if err != nil { - log.Error("[GETLIST] querying the inst database hours", "error", err) - return - } - queryMap := map[string]string{"clear": "cancel=true"} - keyboard := helpers.CreateKeyboard(queryMap) - msg = &tg.MessageConfig{} - msg.ChatID = tUser.ID - msg.ReplyMarkup = keyboard - - msg.Text = instMessage(instList) - case "now": - instRes, err := inst.GetLastPrice(name) - if err != nil { - log.Error("queriing the inst database now", "error", err) - return - } - queryMap := map[string]string{"clear": "cancel=true"} - keyboard := helpers.CreateKeyboard(queryMap) - msg = &tg.MessageConfig{} - msg.ChatID = tUser.ID - - msg.ReplyMarkup = keyboard - msg.Text = fmt.Sprintf("%s\nCompra: %.2f\nVenta: %.2f", instRes.Name, instRes.Compra, instRes.Venta) - - } - - //log.Info("", "time unit", timeUnit, "timeAmount", timeAmnt) - - } - return msg -} - -func instMessage(insts []*models.Institucion) string { - if len(insts) <= 0 { - return "Sin cambios registrados en este intervalo\nPrueba a ampliar el rango del tiempo deseado." - } - name := insts[0].Name - resultText := fmt.Sprintf("%s\n\n", name) - for _, i := range insts { - resultText = resultText + fmt.Sprintf(" %s\n Compra: %.2f Venta: %.2f\n", i.Parsed.Format(time.DateTime), i.Compra, i.Venta) - } - return resultText -}