INITIAL COMMIT
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 5s

This commit is contained in:
maximo tejeda 2024-07-21 11:40:07 -04:00
parent 41b4cee5c4
commit ceb402a65d
33 changed files with 1691 additions and 1195 deletions

View File

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

View File

@ -1,14 +1,17 @@
FROM golang:latest as builder #FROM golang:latest as builder
WORKDIR /app #WORKDIR /app
COPY . . #COPY . .
RUN go mod download && go mod tidy #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 FROM debian:unstable-slim
ARG BINAME=us-dop-bot-linux-arm64-0.0.0_1
RUN apt-get update RUN apt-get update
RUN apt-get install -y ca-certificates 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 WORKDIR /app
RUN echo "bin name ${BINAME}"
# RUN mv /app/${BINAME} /app/us-dop-bot
CMD ["/app/us-dop-bot"] CMD ["/app/us-dop-bot"]

14
Dockerfile.old Normal file
View File

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

View File

@ -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 .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 # 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 # 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 run-image: build-image
@$(CONTAINERS) compose -f docker-compose.yaml up @$(CONTAINERS) compose -f docker-compose.yaml up
@ -37,7 +41,7 @@ run-image-debug: build-image-debug
run-local:clean build run-local:clean build
@bin/$(BINAME) @bin/$(BINAME)
build: build: clean
#@mkdir dolardb #@mkdir dolardb
@env GOOS=$(OS) GOARCH=$(arch) go build -o ./bin/$(BINAME) ./cmd/bot/. @env GOOS=$(OS) GOARCH=$(arch) go build -o ./bin/$(BINAME) ./cmd/bot/.
@env GOOS=$(OS) GOARCH=arm64 go build -o ./bin/$(BINAMEARM) ./cmd/bot/. @env GOOS=$(OS) GOARCH=arm64 go build -o ./bin/$(BINAMEARM) ./cmd/bot/.

View File

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

View File

@ -2,148 +2,147 @@ package main
import ( import (
"context" "context"
"database/sql"
"errors"
"fmt"
"log/slog" "log/slog"
"os" "os"
"os/signal" "os/signal"
"strings" "runtime"
"syscall" "syscall"
"time"
tb "github.com/go-telegram-bot-api/telegram-bot-api/v5" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
"github.com/maximotejeda/us_dop_bot/broadcast" "github.com/maximotejeda/us_dop_bot/config"
commands "github.com/maximotejeda/us_dop_bot/command" "github.com/maximotejeda/us_dop_bot/internal/adapters/dolar"
"github.com/maximotejeda/us_dop_bot/db" "github.com/maximotejeda/us_dop_bot/internal/adapters/user"
edb "github.com/maximotejeda/us_dop_bot/edb" "github.com/maximotejeda/us_dop_bot/internal/application/api"
"github.com/maximotejeda/us_dop_bot/query" "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" "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() { 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)) 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() 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 { if err != nil {
log.Error("token not found", "error", err)
panic(err) panic(err)
} }
bot.Debug = false
log.Info("Bot Authorized", "username", bot.Self.UserName) botName := bot.Self.UserName
u := tb.NewUpdate(0)
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 u.Timeout = 60
// bot user update channel // bot user update channel
updtChan := bot.GetUpdatesChan(u) updtChan := bot.GetUpdatesChan(u)
// subs chann // subs chann
ch := make(chan *nats.Msg, 64) changeChan := make(chan *nats.Msg, 64)
defer close(ch) broadcastChan := make(chan *nats.Msg, 64)
sub, err := nc.ChanSubscribe("dolar-crawler", ch) 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 { if err != nil {
log.Error("subscribing", "error", err.Error()) log.Error("subscribing", "error", err.Error())
} }
defer sub.Drain() defer sub.Drain()
defer info.Drain()
defer nc.Close() defer nc.Close()
// exit channel // exit channel
sign := make(chan os.Signal, 1) sign := make(chan os.Signal, 1)
signal.Notify(sign, syscall.SIGINT, syscall.SIGTERM) signal.Notify(sign, syscall.SIGINT, syscall.SIGTERM)
defer close(sign) defer close(sign)
app := api.NewApi(bot)
for { for {
select { select {
case update := <-updtChan: case update := <-updtChan:
usr := db.NewUser(userDB, log) if err = sem.Acquire(ctx, 1); err != nil {
_, err := usr.Get(update.SentFrom().ID) bot.Send(tgbotapi.NewMessage(update.FromChat().ID, "error adquiring update"))
if err != nil { continue
if errors.Is(err, sql.ErrNoRows) {
usr.Add(update.SentFrom().ID)
}
} }
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 { bcast := broadcaster.NewBroadCast(ctx, user, dol, message.Data)
msg := tb.NewMessage(update.Message.Chat.ID, "") userList := bcast.SendList()
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)
if resp, err := bot.Request(tb.NewDeleteMessage(update.Message.From.ID, update.Message.MessageID)); err != nil || !resp.Ok { for _, msg := range userList {
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 {
go bot.Send(msg) 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: case <-sign:
log.Error("killing app due to syscall ") log.Error("killing app due to syscall ")
os.Exit(1) 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
}

View File

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

37
config/config.go Normal file
View File

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

BIN
crawler.db Normal file

Binary file not shown.

View File

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

View File

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

View File

@ -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
);

302
edb/db.go
View File

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

21
go.mod
View File

@ -2,4 +2,23 @@ module github.com/maximotejeda/us_dop_bot
go 1.22.0 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
)

30
go.sum
View File

@ -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 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8= 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=

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<operacion>([cC]omprar?\s?(me)?|[vV]en(de)?(r)?(ta)?\s?(me)?))\s(?P<cantidad>[0-9.]{1,8})\s(?P<moneda>(dolar(e)?(s)?|peso(s)?|dollars?))\s?(?P<institucion>(.*))?`)
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
}

View File

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

View File

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

13
internal/ports/dolar.go Normal file
View File

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

7
internal/ports/tgb.go Normal file
View File

@ -0,0 +1,7 @@
package ports
type Tgb interface {
Send()
Empty()
Handler()
}

17
internal/ports/user.go Normal file
View File

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

View File

@ -19,18 +19,15 @@ spec:
- name: us-dop-bot - name: us-dop-bot
image: localhost:32000/us-dop-bot:latest image: localhost:32000/us-dop-bot:latest
env: env:
- name: DBURINST - name: DBURI
value: $DBURINST value: $DBURI
- name: DBURIUSER
value: $DBURIUSER
- name: NATSURI - name: NATSURI
value: "nats://nats-svc:4222" value: "nats://nats-svc:4222"
- name: TOKEN - name: TOKEN
value: "$TOKEN" value: "$PRODTOKEN"
volumeMounts: - name: DOLLAR_SERVICE_URL
- name: database value: "dolar-grpc-svc:80"
mountPath: /app/dolardb - name: TGBUSER_SERVICE_URL
volumes: value: "tgbuser-grpc-svc:80"
- name: database - name: ENV
persistentVolumeClaim: value: "production"
claimName: bank-crawler-pvc

View File

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

View File

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