Compare commits

..

No commits in common. "ceb402a65dfc0132245e677526afc3fea05e5a4c" and "e6258f9de2d486d4c3e1465cbb16ec6bc7c0d8d3" have entirely different histories.

26 changed files with 10 additions and 1793 deletions

View File

@ -1,19 +0,0 @@
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 }}."

3
.gitignore vendored
View File

@ -1,5 +1,2 @@
.env .env
k8s/deployment.yml k8s/deployment.yml
bin/
dolardb/

View File

@ -1,17 +1,14 @@
#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 ./bin/${BINAME} /app/us-dop-bot COPY --from=builder /app/bin/us-dop-bot /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"]

View File

@ -1,14 +0,0 @@
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,14 +20,10 @@ 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 build-image:
# 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 --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 . @$(CONTAINERS) buildx build --platform linux/arm64 --push -t $(REGADDR)/us-dop-bot:latest .
# Here we upload it to local
run-image: build-image run-image: build-image
@$(CONTAINERS) compose -f docker-compose.yaml up @$(CONTAINERS) compose -f docker-compose.yaml up
@ -41,7 +37,7 @@ run-image-debug: build-image-debug
run-local:clean build run-local:clean build
@bin/$(BINAME) @bin/$(BINAME)
build: clean build:
#@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,148 +0,0 @@
package main
import (
"context"
"log/slog"
"os"
"os/signal"
"runtime"
"syscall"
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() {
log := slog.New(slog.NewJSONHandler(os.Stderr, nil))
log = log.With("location", "main")
nc, _ := nats.Connect(config.GetNatsURI())
ctx := context.Background()
bot, err := tgbotapi.NewBotAPI(config.GetToken())
if err != nil {
log.Error("token not found", "error", err)
panic(err)
}
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
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:
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()
bcast := broadcaster.NewBroadCast(ctx, user, dol, message.Data)
userList := bcast.SendList()
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
}

View File

@ -1,37 +0,0 @@
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)
}

Binary file not shown.

21
go.mod
View File

@ -2,23 +2,4 @@ module github.com/maximotejeda/us_dop_bot
go 1.22.0 go 1.22.0
require ( require github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
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,32 +1,2 @@
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

@ -1,192 +0,0 @@
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

@ -1,118 +0,0 @@
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

@ -1,143 +0,0 @@
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

@ -1,74 +0,0 @@
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

@ -1,109 +0,0 @@
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

@ -1,220 +0,0 @@
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

@ -1,17 +0,0 @@
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

@ -1,13 +0,0 @@
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

@ -1,26 +0,0 @@
package helpers
import tgb "github.com/go-telegram-bot-api/telegram-bot-api/v5"
// CreateKeyboard
// create keybowrds of two rows of any map[string]string input
func CreateKeyboard(data map[string]string) tgb.InlineKeyboardMarkup {
// hardcoded models
keyboard := tgb.NewInlineKeyboardMarkup()
// subbuttons := []tgbot.InlineKeyboardButton{}
rows := tgb.NewInlineKeyboardRow()
counter := 0
for key, val := range data {
if counter != 0 && counter%3 == 0 {
keyboard.InlineKeyboard = append(keyboard.InlineKeyboard, rows)
rows = tgb.NewInlineKeyboardRow()
}
rows = append(rows, tgb.NewInlineKeyboardButtonData(key, val))
if counter >= len(data)-1 {
keyboard.InlineKeyboard = append(keyboard.InlineKeyboard, rows)
}
counter++
}
return keyboard
}

View File

@ -1,215 +0,0 @@
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

@ -1,195 +0,0 @@
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

@ -1,114 +0,0 @@
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
}

View File

@ -1,13 +0,0 @@
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)
}

View File

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

View File

@ -1,17 +0,0 @@
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

@ -1,33 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: us-dop-bot
labels:
app: us-dop-bot
spec:
replicas: 1
selector:
matchLabels:
app: us-dop-bot
template:
metadata:
labels:
app: us-dop-bot
name: us-dop-bot
spec:
containers:
- name: us-dop-bot
image: localhost:32000/us-dop-bot:latest
env:
- name: DBURI
value: $DBURI
- name: NATSURI
value: "nats://nats-svc:4222"
- name: TOKEN
value: "$PRODTOKEN"
- name: DOLLAR_SERVICE_URL
value: "dolar-grpc-svc:80"
- name: TGBUSER_SERVICE_URL
value: "tgbuser-grpc-svc:80"
- name: ENV
value: "production"