commit 3ec9d26c67ac192ca17e64b062eb81c98242a0e5 Author: maximo tejeda Date: Wed Dec 18 11:08:21 2024 -0400 FIRST commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d2ba111 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/bin/ +/.env +/k8s/deployment.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2e67f74 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +#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 +ARG BINAME=us-dop-bot-linux-arm64-0.0.0_1 +RUN apt-get update +RUN apt-get install -y ca-certificates + +COPY ./bin/${BINAME} /app/us-dop-bot +WORKDIR /app +RUN echo "bin name ${BINAME}" +# RUN mv /app/${BINAME} /app/us-dop-bot +CMD ["/app/us-dop-bot"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..cde54c9 --- /dev/null +++ b/Makefile @@ -0,0 +1,66 @@ +# must create a .env file with info +# must have compose installed +include .env +export +OS:=${shell go env GOOS} +ARCH=$(shell go env GOARCH) +OOSS="linux" +ARRCHS="arm 386" +DEBUG=1 +SERVICE=consulta-c-bot +VERSION=0.0.0_1 +BINAME=$(SERVICE)-$(OS)-$(ARCH)-$(VERSION) +BINAMEARM=$(SERVICE)-$(OS)-arm64-$(VERSION) +# can be docker or podman or whatever +CONTAINERS=docker +COMPOSE=$(CONTAINERS)-compose +# Configure local registry +REGADDR=192.168.0.151:32000 +K8SRSNAME=$(shell kubectl get rs --no-headers -o custom-columns=":metadata.name" | grep $(SERVICE)) +.phony: all clean build test clean-image build-image build-image-debug run-image run-image-debug run-local + + +build-image: build +# here we made the images and push to registry with buildx + @$(CONTAINERS) buildx build --build-arg="BINAME=$(BINAMEARM)" --platform linux/arm64 --push -t $(REGADDR)/$(SERVICE):latest . + +# Here we upload it to local + +build-test-image: + @$(CONTAINERS) buildx build --platform linux/arm64 --push -t $(REGADDR)/$(SERVICE):latest . + +run-image: build-image + @$(CONTAINERS) compose -f docker-compose.yaml up + +build-image-debug: clean + @$(CONTAINERS) compose -f docker-compose-debug.yaml build + +run-image-debug: build-image-debug + @$(CONTAINERS) compose -f docker-compose-debug.yaml up + +run-local:clean build + @bin/$(BINAME) +debug-local: + @dlv debug cmd/bot/main.go +build: clean + #@mkdir dolardb + @env GOOS=$(OS) GOARCH=$(arch) go build --tags "fts5" -o ./bin/$(BINAME) ./cmd/bot/. + @env GOOS=$(OS) GOARCH=arm64 go build --tags "fts5" -o ./bin/$(BINAMEARM) ./cmd/bot/. + +create-descriptors: + @envsubst < k8s/deployment.yml.template > k8s/deployment.yml + +deploy: build-image create-descriptors + @kubectl apply -f k8s/deployment.yml + @kubectl scale rs $(K8SRSNAME) --replicas=0 + @kubectl scale rs $(K8SRSNAME) --replicas=1 + +test: + @go -count=1 test ./... +clean: + @rm -rf ./bin + +clean-image: + @$(CONTAINERS) system prune -f + + diff --git a/cmd/bot/main.go b/cmd/bot/main.go new file mode 100644 index 0000000..15544bf --- /dev/null +++ b/cmd/bot/main.go @@ -0,0 +1,101 @@ +package main + +import ( + "context" + "log/slog" + "runtime" + + "git.maximotejeda.com/maximo/cedulados-bot/config" + "git.maximotejeda.com/maximo/cedulados-bot/internal/adapter/grpc/cedulados" + user "git.maximotejeda.com/maximo/cedulados-bot/internal/adapter/grpc/users" + "git.maximotejeda.com/maximo/cedulados-bot/internal/application/api" + "git.maximotejeda.com/maximo/cedulados-bot/internal/ports" + tgb "github.com/go-telegram-bot-api/telegram-bot-api/v5" + "golang.org/x/sync/semaphore" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +var ( + maxWorkers = runtime.GOMAXPROCS(4) + sem = semaphore.NewWeighted(2) +) + +func main() { + ctx := context.Background() + + bot, err := tgb.NewBotAPI(config.GetToken()) + log := slog.Default() + + if err != nil { + log.Error("initiating bot", "error",err.Error()) + return + } + + bot.Debug = true + + log.Info("Authorized on account %s", "username", bot.Self.UserName, "maxWorkers", 2) + + u := tgb.NewUpdate(0) + u.Timeout = 60 + + updates := bot.GetUpdatesChan(u) + sign := make(chan int) + app := api.NewApi(ctx, log, bot) + _, us, cCoon, uConn := CreateAdaptersGRPC() + _ = us.CreateBot(bot.Self.UserName) + cCoon.Close() + uConn.Close() + // break if not admin set + _ = config.GetAdminsList() + for { + select { + case update := <-updates: + if err := sem.Acquire(ctx, 1); err != nil{ + log.Error("failed to get semaphore", "err", err.Error) + bot.Send(tgb.NewMessage(update.Message.Chat.ID, "too may request, Please try again")) + continue + } + go func(){ + defer sem.Release(1) + c, u, cConn, uConn := CreateAdaptersGRPC() + app.Run(&update, c, u) + defer cConn.Close() + defer uConn.Close() + }() + + case <-sign: + } + } +} + + +func CreateAdaptersGRPC() (ports.CeduladosService, 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())) + cedConn, err := grpc.NewClient(config.GetCeduladosServiceURL(), opts...) + if err != nil { + log.Error("creating gerpc conn", "error", err) + panic(err) + } + userConn, err := grpc.NewClient(config.GetUserServiceURL(), opts...) + if err != nil { + log.Error("creating gerpc conn", "error", err) + panic(err) + } + ced, err := cedulados.NewAdapter(cedConn) + 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 ced, user, cedConn, userConn +} diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..5e7d624 --- /dev/null +++ b/config/config.go @@ -0,0 +1,38 @@ +package config + +import ( + "log" + "os" +) + +func GetToken() string { + return getEnvVariable("BOT_TOKEN") +} + +func GetNatsURI() string { + return getEnvVariable("NATS_SERVICE_URL") +} + +func GetCeduladosServiceURL() string { + return getEnvVariable("CEDULADOS_SERVICE_URL") +} + +func GetUserServiceURL() string { + return getEnvVariable("TGBUSER_SERVICE_URL") +} + +func GetEnvironment() string { + return getEnvVariable("ENV") +} + +func GetAdminsList()string { + return getEnvVariable("ADMINS") +} + +func getEnvVariable(key string) string { + if os.Getenv(key) == "" { + log.Fatal("error getting key ", key) + } + return os.Getenv(key) +} + diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1aba6c4 --- /dev/null +++ b/go.mod @@ -0,0 +1,19 @@ +module git.maximotejeda.com/maximo/cedulados-bot + +go 1.22.0 + +require ( + git.maximotejeda.com/maximo/cedulados v0.0.2 + git.maximotejeda.com/maximo/tgb-user v0.0.5 + github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 + golang.org/x/sync v0.10.0 + golang.org/x/text v0.21.0 + google.golang.org/grpc v1.69.0 +) + +require ( + golang.org/x/net v0.32.0 // indirect + golang.org/x/sys v0.28.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect + google.golang.org/protobuf v1.35.2 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..bc51599 --- /dev/null +++ b/go.sum @@ -0,0 +1,38 @@ +git.maximotejeda.com/maximo/cedulados v0.0.1 h1:00ghxI7KnTS/kIjpt0kX8co84YoMTCmUqZMRphrF/Qk= +git.maximotejeda.com/maximo/cedulados v0.0.1/go.mod h1:LngBMRNqF+CCqPaK3wvLs8gvJZgfMqPTYuEPZy9nOPM= +git.maximotejeda.com/maximo/cedulados v0.0.2 h1:xGAUfy6UXPV3nOrW+12JUWCXZrEDkb1kIEZPk+1Ov+o= +git.maximotejeda.com/maximo/cedulados v0.0.2/go.mod h1:LngBMRNqF+CCqPaK3wvLs8gvJZgfMqPTYuEPZy9nOPM= +git.maximotejeda.com/maximo/tgb-user v0.0.5 h1:OTACcjzOld9TsQHqzDqGXgdN3CqWrZ2p1ro0mUErCR0= +git.maximotejeda.com/maximo/tgb-user v0.0.5/go.mod h1:7KpTUAnwap6cp5pHRKgJygxrN3rftAdTkpCG2zJIpYI= +github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc= +github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= +golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= +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/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.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= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6 h1:DujSIu+2tC9Ht0aPNA7jgj23Iq8Ewi5sgkQ++wdvonE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= +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/grpc v1.69.0 h1:quSiOM1GJPmPH5XtU+BCoVXcDVJJAzNcoyfC2cCjGkI= +google.golang.org/grpc v1.69.0/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= +google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4= +google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= +google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= diff --git a/internal/adapter/chat/chat.go b/internal/adapter/chat/chat.go new file mode 100644 index 0000000..13d1512 --- /dev/null +++ b/internal/adapter/chat/chat.go @@ -0,0 +1,228 @@ +package chat + +import ( + "bytes" + "context" + "fmt" + "regexp" + "strconv" + "strings" + "sync" + "unicode" + + "git.maximotejeda.com/maximo/cedulados-bot/config" + "git.maximotejeda.com/maximo/cedulados-bot/internal/adapter/grpc/cedulados" + "git.maximotejeda.com/maximo/cedulados-bot/internal/adapter/helper" + "git.maximotejeda.com/maximo/cedulados-bot/internal/application/domain" + "git.maximotejeda.com/maximo/cedulados-bot/internal/ports" + 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" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +var ChatPool *sync.Pool + +type ChatObj struct { + ctx context.Context + update *tgb.Update + bot *tgb.BotAPI + u ports.UserService + c ports.CeduladosService +} + +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 (ch *ChatObj)Empty() { + ch.update = nil + ch.c = nil + ch.u = nil + ChatPool.Put(ch) +} + +func (ch *ChatObj)Send(){ + +} + +func (ch *ChatObj)Handler( cSVC ports.CeduladosService, uSVC ports.UserService) { + text := NormalizeText(ch.update.Message.Text) + textList := strings.Split(text, " ") + msg := tgb.NewMessage(ch.update.Message.Chat.ID, "") + if len(textList) >= 1 && MessageChecker(text) == "digit" { + // in case of message match a cedula + ced, err := helper.NewCedula(ch.ctx, text) + if err != nil { + msg.Text = "cedula no reconocida " + err.Error() + } else { + msg, photoMsg := ProcessByCedula(ch.ctx, cSVC, uSVC, ced) + msg.ChatID = ch.update.Message.Chat.ID + if photoMsg != nil { + photoMsg.ChatID = ch.update.Message.Chat.ID + ch.bot.Send(photoMsg) + } + ch.bot.Send(msg) + return + } + } else if len(textList) >= 2 && MessageChecker(text) == "word" { + msg := ProcessByName(ch.ctx, cSVC, uSVC, textList) + msg.ChatID = ch.update.Message.Chat.ID + ch.bot.Send(msg) + } + + msg.ReplyToMessageID = ch.update.Message.MessageID + ch.bot.Send(msg) +} + +// ProcessByCedula +// +// When a text arrives the chat if it match a cedula try to query db +func ProcessByCedula(ctx context.Context, cSVC ports.CeduladosService, uSVC ports.UserService, ced *domain.Cedula) (message *tgb.MessageConfig, fotoMsg *tgb.PhotoConfig) { + msg := tgb.NewMessage(0, "") + message = &msg + info , err := cSVC.CeduladoByCedula(context.Background(), ced) + if err != nil { + fmt.Println("error on query ", err) + } + + fmt.Println("!!!!success on query", info) + + 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) + } + var opts []grpc.DialOption + opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials())) + cedConn, err := grpc.NewClient(config.GetCeduladosServiceURL(), opts...) + if err != nil { + + panic(err) + } + c, err := cedulados.NewAdapter(cedConn) + if err != nil { + + panic(err) + } + defer cedConn.Close() + + foto, err := c.QueryFotoByID(ctx, 1) + 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.Imagen)} + fotost := tgb.NewPhoto(msg.ChatID, rq) + fotoMsg = &fotost + + } + + return +} + +func ProcessByName(ctx context.Context, cSVC ports.CeduladosService, uSVC ports.UserService, nameList []string) (message *tgb.MessageConfig) { + var err error + page := int64(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.ParseInt(lastItem, 10, 64) + if err != nil { + fmt.Println(err) + } + nameList = nameList[:len(nameList)-1] + if pageInt < 20 { + page = pageInt + } + } + rows := &domain.MultipleResults{} + message = &tgb.MessageConfig{} + text := strings.Join(nameList, " ") + + res, err := cSVC.CeduladoByFTS(ctx, "maximo tejeda", 0) + if err != nil { + fmt.Println("error oon fts", err) + } + fmt.Println("sucess", res) + + rows, err = cSVC.CeduladoByFTS(ctx, text, page) + if err != nil { + message.Text = "no hubo resultados para la busqueda" + return + } + + textToSend := fmt.Sprintf(` +Busqueda: + Texto: %s + Pagina: %d + resultados desde: %d A %d +De un total de: %d + +`, + text, + (rows.Page), + (rows.Page*10)-9, (rows.Page)*10, + rows.Total) + for _, v := range rows.Data { + textToSend = textToSend + fmt.Sprintf("\n %s %s %s \n %s-%s-%s\n", strings.ToLower(v.Nombres), strings.ToLower(v.Apellido1), strings.ToLower(v.Apellido2), v.MunCed, v.SeqCed, v.VerCed) + } + message.Text = textToSend + return +} + +func MessageChecker(text string) string { + // Check is text start with number + checkSpace := regexp.MustCompile(`^[\s]+.*`) + checkNumber := regexp.MustCompile(`^[\d]+.*`) + checkWord := regexp.MustCompile(`^[a-zA-Z% ]+.*`) + + if checkNumber.MatchString(text) { + return "digit" + } else if checkWord.MatchString(text) { + return "word" + } else if checkSpace.MatchString(text) { + t := strings.TrimSpace(text) + return MessageChecker(t) + } + return "" +} + +// NormalizeText +// remove foreign accent +func NormalizeText(text string) string { + t := transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC) + result, _, _ := transform.String(t, text) + return result +} + +func RemoveSpaces(text string) (res string) { + re := regexp.MustCompile(`[\s]+`) + + res = re.ReplaceAllString(text, " ") + return +} diff --git a/internal/adapter/command/command.go b/internal/adapter/command/command.go new file mode 100644 index 0000000..d47dcf0 --- /dev/null +++ b/internal/adapter/command/command.go @@ -0,0 +1 @@ +package command diff --git a/internal/adapter/grpc/cedulados/cedulados.go b/internal/adapter/grpc/cedulados/cedulados.go new file mode 100644 index 0000000..86af801 --- /dev/null +++ b/internal/adapter/grpc/cedulados/cedulados.go @@ -0,0 +1,190 @@ +package cedulados + +import ( + "context" +"fmt" + + "git.maximotejeda.com/maximo/cedulados-bot/internal/application/domain" + ced "git.maximotejeda.com/maximo/cedulados/proto/golang/cedulados" + "google.golang.org/grpc" +) + +// Adapter +type Adapter struct { + cedulados ced.QueryCeduladoClient + conn *grpc.ClientConn +} +// NewAdapter +func NewAdapter(conn *grpc.ClientConn) (*Adapter, error){ + client := ced.NewQueryCeduladoClient(conn) + if client == nil{ + fmt.Println("error creating client, cant be nil" ) + } + return &Adapter{cedulados: client, conn: conn}, nil +} +// CeduladoByCedula +func(a Adapter) CeduladoByCedula(ctx context.Context, c *domain.Cedula)(resp *domain.Info, err error){ + cReq := &ced.QueryByCedulaRequest{ + Cedula: &ced.Cedula{ + MunCed: c.MunCed, + SeqCed: c.SeqCed, + VerCed: c.VerCed, + }, + } + r, err := a.cedulados.CeduladosByCedula(ctx, cReq) + if err != nil { + return nil, err + } + + resp = mapLocalDomain (r.Cedulado) + return +} +// CeduladoGetByNameLastName +func(a Adapter) CeduladoGetByNameLastName(ctx context.Context, params domain.Info, page int64)(info *domain.MultipleResults, err error){ + cReq := &ced.QueryByNameLastNameRequest{ + Name: params.Nombres, + Apellido_1: params.Apellido1, + Apellido_2: params.Apellido2, + Page: page, + } + r, err := a.cedulados.CeduladosByNameLastName(ctx, cReq) + if err != nil { + return nil, err + } + info = &domain.MultipleResults{ + Page: r.Page, + Total: r.Total, + } + for _, val := range r.Cedulados{ + info.Data = append(info.Data, mapLocalDomain (val)) + } + return +} + +// CeduladoByFTS +func(a Adapter) CeduladoByFTS(ctx context.Context, params string, page int64)(info *domain.MultipleResults, err error){ + cReq := &ced.QueryByFTSRequest{ + Name: params, + Page: page, + } + r, err := a.cedulados.CeduladosByFTS(ctx, cReq) + if err != nil { + return nil, err + } + info = &domain.MultipleResults{ + Page: r.Page, + Total: r.Total, + } + for _, val := range r.Cedulados{ + info.Data = append(info.Data, mapLocalDomain ( val)) + } + return +} + +// CeduladoByNameAndLocation +func(a Adapter) CeduladoByNameAndLocation(ctx context.Context, params domain.Info, page int64, municipio string)(info *domain.MultipleResults, err error){ + cReq := &ced.QueryByNamesLocationRequest{ + Name: params.Nombres, + Apellido_1: params.Apellido1, + Apellido_2: params.Apellido2, + Municipio: municipio, + Page: page, + } + r, err := a.cedulados.CeduladosByNameAndLocation(ctx, cReq) + if err != nil { + return nil, err + } + info = &domain.MultipleResults{ + Page: r.Page, + Total: r.Total, + } + for _, val := range r.Cedulados{ + info.Data = append(info.Data, mapLocalDomain ( val)) + } + return +} +// QueryFotoByCedula +func(a Adapter) QueryFotoByCedula(ctx context.Context, c *domain.Cedula)(info *domain.Foto, err error){ + cReq := &ced.QueryFotoByCedulaRequest{ + Cedula: &ced.Cedula{ + MunCed: c.MunCed, + SeqCed: c.SeqCed, + VerCed: c.VerCed, + }, + } + + r, err := a.cedulados.QueryFotoByCedula(ctx, cReq) + if err != nil { + return nil, err + } + // here should be a photo + info = &domain.Foto{} + info.Imagen = r.Foto.Foto + info.MunCed = c.MunCed + info.SeqCed = c.SeqCed + info.VerCed = c.VerCed + info.ID = r.Foto.Id + return +} +// QueryFotoByID +func(a Adapter) QueryFotoByID(ctx context.Context, id int64)(info *domain.Foto, err error){ + r, err := a.cedulados.QueryFotoById(ctx, &ced.QueryFotoByIdRequest{Id: id}) + if err != nil { + return nil, err + } + info = &domain.Foto{} + info.Imagen = r.Foto.Foto + info.ID = r.Foto.Id + info.MunCed = r.Foto.MunCed + info.VerCed = r.Foto.VerCed + info.SeqCed = r.Foto.SeqCed + return +} +// QueryFotoAllCedulas +func(a Adapter) QueryFotoAllCedulas(ctx context.Context, ceds []*domain.Cedula)(fotos []*domain.Foto, err error){ + for _, c := range ceds { + foto := &domain.Foto{} + cReq := &ced.QueryFotoByCedulaRequest{ + Cedula: &ced.Cedula{ + MunCed: c.MunCed, + SeqCed: c.SeqCed, + VerCed: c.VerCed, + }, + } + r, err := a.cedulados.QueryFotoByCedula(ctx, cReq) + if err != nil { + fmt.Println("err on ced: ", c) + continue + } + // here should be a photo + foto.Imagen = r.Foto.Foto + foto.MunCed = c.MunCed + foto.SeqCed = c.SeqCed + foto.VerCed = c.VerCed + foto.ID = r.Foto.Id + fotos = append(fotos, foto) + } + return +} + +func mapLocalDomain(info *ced.Cedulado)(ifo *domain.Info){ + if info == nil { + fmt.Println("nil info ") + return nil + } + ifo = &domain.Info{ + Cedula: domain.Cedula{ + VerCed: info.Cedula.VerCed, + MunCed: info.Cedula.MunCed, + SeqCed: info.Cedula.SeqCed, + }, + ID: info.Id, + Nombres: info.Nombres, + Apellido1: info.Apellido_1, + Apellido2: info.Apellido_2, + Sexo: info.Sexo, + Direccion: info.Direccion, + Telefono: info.Telefono, + } + return +} diff --git a/internal/adapter/grpc/users/users.go b/internal/adapter/grpc/users/users.go new file mode 100644 index 0000000..035ff14 --- /dev/null +++ b/internal/adapter/grpc/users/users.go @@ -0,0 +1,245 @@ +package user + +import ( + "context" + "log/slog" + + "git.maximotejeda.com/maximo/cedulados-bot/internal/application/domain" + "git.maximotejeda.com/maximo/tgb-user/proto/golang/tgbuser" + tgb "github.com/go-telegram-bot-api/telegram-bot-api/v5" + "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, + Username: hr.User.Username, + FirstName: hr.User.FirstName, + LastName: hr.User.LastName, + 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 +} + +func (a Adapter) CreateBot(botname string) (error){ + _, err := a.user.CreateBot(context.Background(), &tgbuser.TGBBotNameRequest{BotName: botname}) + if err != nil { + a.log.Error("creating bot", "error", err) + return err + } + return nil +} + +func (a Adapter) CreateAccessRequest(tgbID int64, botName string)(bool, error){ + req := tgbuser.TGBUserBotNameRequest{ + TgbId: tgbID, + BotName: botName, + } + r,err := a.user.CreateAccessRequest(context.Background(), &req) + if err != nil { + a.log.Error("creating access request", "error", err) + return false, err + } + return r.Response, nil +} + +func (a Adapter) GrantAccess(tgbID int64, botName string)(bool, error){ + req := tgbuser.TGBUserBotNameRequest{ + TgbId: tgbID, + BotName: botName, + } + r,err := a.user.GrantAccess(context.Background(), &req) + if err != nil { + a.log.Error("creating access request", "error", err) + return false, err + } + return r.Response, nil +} + +func (a Adapter) GetAllAccessRequest(botName string)(*tgbuser.GetAccessResponse, error){ + req := tgbuser.TGBBotNameRequest{ + BotName: botName, + } + r,err := a.user.GetAllAccessRequest(context.Background(), &req) + if err != nil { + a.log.Error("creating access request", "error", err) + return nil, err + } + return r, nil +} + +func (a Adapter) BanUser(tgbID int64, until int64, botName string)(bool, error){ + req:= tgbuser.TGBBanUserRequest{ + TgbId: tgbID, + BotName: botName, + Until: until, + } + r, err := a.user.BanUser(context.Background(), &req) + if err != nil { + a.log.Error("banning user", "error", err) + return false, err + } + return r.Response, nil +} + +func (a Adapter) UnBanUser(tgbID int64, botName string)(bool, error){ + req:= tgbuser.TGBUserBotNameRequest{ + TgbId: tgbID, + BotName: botName, + } + r, err := a.user.UnBanUser(context.Background(), &req) + if err != nil { + a.log.Error("unbaning user", "error", err) + return false, err + } + return r.Response, nil +} + +func (a Adapter) GetAllBannedUsers(botName string)(*tgbuser.GetBanResponse, error){ + req := tgbuser.TGBBotNameRequest{ + BotName: botName, + } + r, err := a.user.GetAllBannedUsers(context.Background(), &req) + if err != nil { + a.log.Error("getting all banned users", "error", err) + return nil, err + } + + return r, nil +} + +func (a Adapter)GetAccessRequest(tgbID int64) (*tgbuser.GetAccessResponse, error){ + req := tgbuser.TGBUserRequest{ + TgbId: tgbID, + } + r, err := a.user.GetAccessRequest(context.Background(), &req) + if err != nil { + a.log.Error("geting access request", "userID", tgbID,"error", err) + return nil, err + } + return r, nil +} diff --git a/internal/adapter/helper/helpers.go b/internal/adapter/helper/helpers.go new file mode 100644 index 0000000..14c1940 --- /dev/null +++ b/internal/adapter/helper/helpers.go @@ -0,0 +1,126 @@ +package helper + +import ( + "context" + "fmt" + "regexp" + "strings" + "unicode" + + "git.maximotejeda.com/maximo/cedulados-bot/internal/application/domain" + tgbotapi "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" +) + +// AsserCedulas +// check for cedula format +// 000-+ 0000000 +- 0 +func NewCedula(ctx context.Context, ced string)(c *domain.Cedula, err error){ + c = &domain.Cedula{} + if len(ced) <= 0 { + return nil, fmt.Errorf("cedula length err lenght:%d", len(ced)) + } + // use Regexp to simplify cases + re := regexp.MustCompile(`(?P\d{3})[\s-]+(?P\d{7})[\s-]+(?P\d{1})$`) + result := re.FindStringSubmatch(ced) + if len(result) != 4 { + return nil, fmt.Errorf("Cedula format Err (%s) is not a good format", ced) + } + for i, name := range re.SubexpNames() { + if i != 0 && name != "" { + switch name { + case "munCed": + c.MunCed = result[i] + case "seqCed": + c.SeqCed = result[i] + case "verCed": + c.VerCed = result[i] + default: + continue + } + } + } + return c, nil +} + +// NewMultiCedula +// create a list of Multiple Cedulas from a string +// separated by \n +func NewMultiCedula(ctx context.Context, ceds string) (cList []*domain.Cedula, err error){ + splitedCed := strings.Split(ceds, "\n") + for _, ced := range splitedCed{ + c, err := NewCedula(ctx, ced) + if err != nil { + fmt.Printf("error querying cedula: %s", err) + continue + } + cList = append(cList, c) + } + return cList, err +} + +// 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 +} + + +// RemoveSpaces +func RemoveSpaces(text string) (res string) { + re := regexp.MustCompile(`[\s]+`) + + res = re.ReplaceAllString(text, " ") + return +} + +// MessageChecker +// Check if message start with a number or letter +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 "" +} + +// CreateKeyboard +// create keybowrds of two rows of any map[string]string input +func CreateKeyboard(data map[string]string) tgbotapi.InlineKeyboardMarkup { + // hardcoded models + keyboard := tgbotapi.NewInlineKeyboardMarkup() + // subbuttons := []tgbot.InlineKeyboardButton{} + rows := tgbotapi.NewInlineKeyboardRow() + counter := 0 + for key, val := range data { + + if counter != 0 && counter%3 == 0 { + keyboard.InlineKeyboard = append(keyboard.InlineKeyboard, rows) + rows = tgbotapi.NewInlineKeyboardRow() + } + rows = append(rows, tgbotapi.NewInlineKeyboardButtonData(key, val)) + if counter >= len(data)-1 { + keyboard.InlineKeyboard = append(keyboard.InlineKeyboard, rows) + } + counter++ + } + return keyboard +} diff --git a/internal/adapter/query/query.go b/internal/adapter/query/query.go new file mode 100644 index 0000000..f6b34a8 --- /dev/null +++ b/internal/adapter/query/query.go @@ -0,0 +1 @@ +package query diff --git a/internal/application/api/api.go b/internal/application/api/api.go new file mode 100644 index 0000000..47f78b8 --- /dev/null +++ b/internal/application/api/api.go @@ -0,0 +1,59 @@ +package api + +import ( + "context" + "log/slog" + + "git.maximotejeda.com/maximo/cedulados-bot/internal/application/auth" + "git.maximotejeda.com/maximo/cedulados-bot/internal/application/command" + "git.maximotejeda.com/maximo/cedulados-bot/internal/application/message" + "git.maximotejeda.com/maximo/cedulados-bot/internal/application/query" + "git.maximotejeda.com/maximo/cedulados-bot/internal/ports" + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" +) + +type api struct { + ctx context.Context + log *slog.Logger + bot *tgbotapi.BotAPI + update *tgbotapi.Update + command ports.Tgb + message ports.Tgb + query ports.Tgb + cedulados ports.CeduladosService + user ports.UserService + +} + +// NewApi +// Create a new instance for api +func NewApi(ctx context.Context, log *slog.Logger, bot *tgbotapi.BotAPI) *api { + log = log.With("location", "api") + return &api{ctx: ctx, log: log, bot: bot} +} + +// Run +// Start bot user interaction process +func (a *api) Run(update *tgbotapi.Update, cedSVC ports.CeduladosService, user ports.UserService) { + au := auth.NewAuth(a.ctx, a.log, a.bot, update, update.SentFrom(), user) + if !(au.Authenticate()) && !update.Message.IsCommand(){ + a.log.Error("Authentication failed") + return + } + msg := update.Message + if msg != nil { // message is not nil can be a command or a text message + if msg.IsCommand() { + com := command.Newcommand(a.bot, update, cedSVC, user) + com.Handler() + // is a command + } else if msg.Text != "" { + // is a text message + message := message.NewMessage(a.bot, update, cedSVC, user) + message.Handler() + } + } else if update.CallbackQuery != nil { + // is a cal back + qr := query.NewQuery(a.bot, update, cedSVC, user) + qr.Handler() + } +} diff --git a/internal/application/auth/auth.go b/internal/application/auth/auth.go new file mode 100644 index 0000000..1fb4657 --- /dev/null +++ b/internal/application/auth/auth.go @@ -0,0 +1,186 @@ +package auth + +import ( + "context" + "fmt" + "log/slog" + "slices" + "strconv" + "strings" + + "git.maximotejeda.com/maximo/cedulados-bot/config" + "git.maximotejeda.com/maximo/cedulados-bot/internal/ports" + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" +) + +type Auth struct { + bot *tgbotapi.BotAPI + update *tgbotapi.Update + user *tgbotapi.User + uSVC ports.UserService + log *slog.Logger + ctx context.Context +} + +func NewAuth(ctx context.Context, log *slog.Logger, bot *tgbotapi.BotAPI, update *tgbotapi.Update, user *tgbotapi.User, uSVC ports.UserService) *Auth { + a := &Auth{ + ctx: ctx, + log: log, + user: user, + bot: bot, + uSVC: uSVC, + update: update, + } + return a +} + +func (a *Auth) Authenticate() bool { + switch IsUserAdmin(a.user.ID) { + case true: + // theres an user env admin + // check if user exist on db + _, err := a.uSVC.Get(a.user.ID) + if err != nil { + a.log.Error("geting user", "error", err) + a.log.Debug("user seems to not exists") + if strings.Contains(err.Error(), "sql: no rows in result set") { + // if user does not exist create it + _, err := a.uSVC.Create(a.user) + a.log.Debug("creating user") + if err != nil { + a.log.Error("creating new user for admnin", "err", err) + } + // add bot to user list + _, err = a.uSVC.AddBot(a.user.ID, a.bot.Self.UserName) + if err != nil { + a.log.Error("Adding bot to admin user list", "err", err) + } + } + } + return true + case false: + // user is not admin and need to be authorized + // check if user is on db + _, err := a.uSVC.Get(a.update.SentFrom().ID) + if err != nil { + // user need auth + a.log.Error("user not in db, authorization from an admin is required", "error", err) + // add user to manage it + + if strings.Contains(err.Error(), "sql: no rows in result set") { + // check if theres an access request from the same user and bot + _, err := a.uSVC.Create(a.user) + if err != nil { + a.log.Error("creating new user", "user", a.user.ID, "error", err) + } + + // create access request + _, err = a.uSVC.CreateAccessRequest(a.user.ID, a.bot.Self.UserName) + if err != nil { + a.log.Error("creating access request for ", "user", a.user.ID, "error", err) + } + } + } else { + bot, err := a.uSVC.GetBots(a.user.ID) + if err != nil { + a.log.Error("checking bots on user access") + } + switch HasUserAccess(bot, a.bot.Self.UserName) { + case true: + return true + case false: + // check for banned user + buser, err := a.uSVC.GetAllBannedUsers(a.bot.Self.UserName) + if err != nil { + a.log.Error("error querying banned user") + } + for _, u := range buser.GetBans(){ + if u.TgbId == a.update.SentFrom().ID{ + msg := tgbotapi.NewMessage(u.TgbId, "user access is restricted, please ask for permission") + a.bot.Send(msg) + return false + } + } + ac, err := a.uSVC.GetAccessRequest(a.user.ID) + acl := []string{} + for _, val := range ac.Access { + acl = append(acl, val.BotName) + } + if slices.Contains(acl, a.bot.Self.UserName) { + // create access request + a.log.Info("Access Request found returning early", "user", a.user.ID, "error", err) + return false + } else { + + // create one + _, err = a.uSVC.CreateAccessRequest(a.user.ID, a.bot.Self.UserName) + if err != nil { + a.log.Error("creating access request", "err", err) + } + + } + } + } + } + // get all admins + userL, _ := GetAdminFromEnv() + // send a mesage to all admins + for _, adm := range userL { + msg := GenerateAccessRequestMessage(a.update.SentFrom(), adm, a.bot.Self.UserName) + a.bot.Send(msg) + } + return false +} + +// GetAdminFromEnv +// will get an env variable that is a list of tgbID comma separated +// if the user trying to enter is admin auth the user +func GetAdminFromEnv() (adminList []int64, errList []error) { + adminsStrList := config.GetAdminsList() + list := strings.Split(adminsStrList, ",") + adminList = []int64{} + errList = []error{} + for _, item := range list { + adm, err := strconv.ParseInt(item, 10, 64) + if err != nil { + err = fmt.Errorf("parsing tgb admin id: %s\n err: %w", item, err) + fmt.Println(err) + errList = append(errList, err) + continue + } + adminList = append(adminList, adm) + } + return +} + +// IsUserAdmin +// check if userID is admin on bot +func IsUserAdmin(userID int64) bool { + userL, errl := GetAdminFromEnv() + if len(errl) > 0 { + fmt.Printf("error no admin in var %v", errl) + } + return slices.Contains(userL, userID) +} + +func HasUserAccess(bots []string, botName string) bool { + return slices.Contains(bots, botName) +} + +func GenerateAccessRequestMessage(up *tgbotapi.User, adm int64, bn string) *tgbotapi.MessageConfig { + txt := fmt.Sprintf(`User %s is requesting access +ID: %d +FirstName: %s +LastName: %s +ChatID: %d +`, up.UserName, up.ID, up.FirstName, up.LastName, up.ID) + msg := tgbotapi.NewMessage(adm, txt) + keyboard := tgbotapi.InlineKeyboardMarkup{} + row := tgbotapi.NewInlineKeyboardRow() + row = append(row, tgbotapi.NewInlineKeyboardButtonData("Grant", fmt.Sprintf("operation=grant&userID=%d&bot=%s", up.ID, bn))) + row = append(row, tgbotapi.NewInlineKeyboardButtonData("Deny", fmt.Sprintf("operation=deny&userID=%d&bot=%s", up.ID, bn))) + row = append(row, tgbotapi.NewInlineKeyboardButtonData("Ignore", fmt.Sprintf("operation=ignore&userID=%d&bot=%s", up.ID, bn))) + keyboard.InlineKeyboard = append(keyboard.InlineKeyboard, row) + msg.ReplyMarkup = keyboard + return &msg +} diff --git a/internal/application/command/command.go b/internal/application/command/command.go new file mode 100644 index 0000000..aa87b40 --- /dev/null +++ b/internal/application/command/command.go @@ -0,0 +1,382 @@ +package command + +import ( + "fmt" + "log/slog" + "slices" + "strconv" + "strings" + "sync" + + "git.maximotejeda.com/maximo/cedulados-bot/internal/application/auth" + "git.maximotejeda.com/maximo/cedulados-bot/internal/ports" + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" +) + +var CommandPool *sync.Pool + +type command struct { + bot *tgbotapi.BotAPI + update *tgbotapi.Update + msg *tgbotapi.MessageConfig + delMSG *tgbotapi.DeleteMessageConfig + log *slog.Logger + ce ports.CeduladosService + uSVC ports.UserService +} + +// NewMessage +// Factory for query handler +func Newcommand(bot *tgbotapi.BotAPI, update *tgbotapi.Update, cSVC ports.CeduladosService, 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.From.ID, "userid", update.Message.From.ID, "username", update.Message.From.UserName) + query := CommandPool.Get().(*command) + query.update = update + query.bot = bot + query.log = log + query.ce = cSVC + query.uSVC = user + return query +} + +// Empty +// Returns pointer to pool +func (c *command) Empty() { + c.update = nil + c.msg = nil + c.log = nil + c.ce = nil + c.uSVC = nil + CommandPool.Put(c) +} + +// Send +// Process message sending to bot +func (c *command) Send() { + defer c.Empty() + c.bot.Send(c.msg) + if c.delMSG != nil { + c.bot.Send(c.delMSG) + } +} + +func (c *command) Handler(){ + msg := tgbotapi.NewMessage(c.update.Message.From.ID, "") + delMSG := tgbotapi.NewDeleteMessage(c.update.Message.From.ID, c.update.Message.MessageID) + + c.msg = &msg + c.delMSG = &delMSG + command := c.update.Message.Command() + + switch strings.ToLower(command) { + case "baned": + if !checkUserIsAdmin(c){ + return + } + bannedUserCommand(c, msg) + case "appeal": + appealUserCommand(c, msg) + case "pending": + if !checkUserIsAdmin(c){ + return + } + pendingUserCommand(c, msg) + case "users": + if !checkUserIsAdmin(c){ + return + } + usersCommand(c, msg) + case "user": + if !checkUserIsAdmin(c){ + return + } + userCommand(c) + case "userid": + if !checkUserIsAdmin(c){ + return + } + userIDCommand(c) + case "ban": + if !checkUserIsAdmin(c){ + return + } + banCommand(c, msg) + } +} + +func GenerateAppealRequestMessage(up *tgbotapi.User, adm int64, bn string) *tgbotapi.MessageConfig { + txt := fmt.Sprintf(`User %s appeal +ID: %d +FirstName: %s +LastName: %s +ChatID: %d +`, up.UserName, up.ID, up.FirstName, up.LastName, up.ID) + msg := tgbotapi.NewMessage(adm, txt) + keyboard := tgbotapi.InlineKeyboardMarkup{} + row := tgbotapi.NewInlineKeyboardRow() + row = append(row, tgbotapi.NewInlineKeyboardButtonData("Unban", fmt.Sprintf("operation=unban&userID=%d&bot=%s", up.ID, bn))) + row = append(row, tgbotapi.NewInlineKeyboardButtonData("Delete", fmt.Sprintf("operation=delete&userID=%d&bot=%s", up.ID, bn))) + row = append(row, tgbotapi.NewInlineKeyboardButtonData("Ignore", fmt.Sprintf("operation=ignore&userID=%d&bot=%s", up.ID, bn))) + keyboard.InlineKeyboard = append(keyboard.InlineKeyboard, row) + msg.ReplyMarkup = keyboard + return &msg +} + +func GenerateUserMessage(up *tgbotapi.User, adm int64, bn string) *tgbotapi.MessageConfig { + txt := fmt.Sprintf(`User %s +ID: %d +FirstName: %s +LastName: %s +ChatID: %d +`, up.UserName, up.ID, up.FirstName, up.LastName, up.ID) + msg := tgbotapi.NewMessage(adm, txt) + keyboard := tgbotapi.InlineKeyboardMarkup{} + row := tgbotapi.NewInlineKeyboardRow() + row = append(row, tgbotapi.NewInlineKeyboardButtonData("ban", fmt.Sprintf("operation=ban&userID=%d&bot=%s", up.ID, bn))) + row = append(row, tgbotapi.NewInlineKeyboardButtonData("Delete", fmt.Sprintf("operation=delete&userID=%d&bot=%s", up.ID, bn))) + row = append(row, tgbotapi.NewInlineKeyboardButtonData("Ignore", fmt.Sprintf("operation=ignore&userID=%d&bot=%s", up.ID, bn))) + keyboard.InlineKeyboard = append(keyboard.InlineKeyboard, row) + msg.ReplyMarkup = keyboard + return &msg +} + +// checkUserIsAdmin +// if user is admin return true else false +func checkUserIsAdmin(c *command)bool{ + if !auth.IsUserAdmin(c.update.Message.From.ID){ + text := "only admin can execute this command" + c.bot.Send(tgbotapi.NewMessage(c.update.FromChat().ID, text)) + return false + } + return true +} + +// bannedUserCommand +// check for user banned on the bot and send all for revision to admin requesting it +func bannedUserCommand(c *command, msg tgbotapi.MessageConfig){ + buser, err := c.uSVC.GetAllBannedUsers(c.bot.Self.UserName) + if err != nil { + c.log.Error("error querying banned user") + } + if len(buser.Bans) <=0{ + + msg.Text = "users baned not found" + c.bot.Send(msg ) + return + } + for _, u := range buser.GetBans(){ + us, _ := c.uSVC.Get(u.TgbId) + tgbUs := tgbotapi.User{ + ID: us.TguID, + FirstName: us.FirstName, + LastName: us.LastName, + UserName: us.Username, + } + // send message to issuer + msg := GenerateAppealRequestMessage(&tgbUs, c.update.SentFrom().ID, c.bot.Self.UserName) + c.bot.Send(msg ) + } +} + +// appealUserCommand +// user banned has the oportunity to appeal a command if is unjustify its banning +func appealUserCommand(c *command, msg tgbotapi.MessageConfig){ + buser, err := c.uSVC.GetAllBannedUsers(c.bot.Self.UserName) + if err != nil { + c.log.Error("error querying banned user") + } + if len(buser.GetBans())<= 0{ + msg.Text = "users baned not found" + c.bot.Send(msg ) + return + } + for _, u := range buser.GetBans(){ + adms, _:= auth.GetAdminFromEnv() + if u.TgbId == c.update.SentFrom().ID{ + for _, adm := range adms{ + // send message to all admins + msg := GenerateAppealRequestMessage(c.update.SentFrom(), adm, c.bot.Self.UserName) + c.bot.Send(msg ) + + } + + c.bot.Send(msg) + return + } + } +} + +// pendingUserCommand +// render pending access request for bot +func pendingUserCommand(c *command, msg tgbotapi.MessageConfig){ + pac, _ := c.uSVC.GetAllAccessRequest(c.bot.Self.UserName) + if len(pac.Access) <= 0 { + msg.Text = "there are no users access request" + c.bot.Send(msg ) + return + } + for _, ac := range pac.Access { + us, err := c.uSVC.Get(ac.TgbId) + if err != nil{ + c.log.Error("geting user", "tgbID", us.TguID) + return + } + tgbUS := tgbotapi.User{ + UserName: us.Username, + FirstName: us.FirstName, + LastName: us.LastName, + ID: us.TguID, + } + msg := auth.GenerateAccessRequestMessage(&tgbUS, c.update.Message.From.ID, c.bot.Self.UserName) + c.bot.Send(msg) + } + +} + +// usersCommand +// render all users on the bot to ban or delete +func usersCommand(c *command, msg tgbotapi.MessageConfig){ + usL , err :=c.uSVC.GetAllBotsUsers(c.bot.Self.UserName) + if err != nil { + c.log.Error("geting users from bot") + return + } + if len(usL) <= 0 { + msg.Text = "users not found registered" + c.bot.Send(msg ) + return + } + for _, us := range usL{ + if auth.IsUserAdmin(us.TguID){ + continue + } + tgbUS := tgbotapi.User{ + UserName: us.Username, + FirstName: us.FirstName, + LastName: us.LastName, + ID: us.TguID, + } + msg := GenerateUserMessage(&tgbUS, c.update.SentFrom().ID, c.bot.Self.UserName) + c.bot.Send(msg) + } + +} + +// userCommand +// render an user message with querys ban delete +func userCommand(c *command){ + msgTXT := strings.Split(c.update.Message.Text, " ") + if len(msgTXT) <= 1{ + text := "command require username argument" + c.bot.Send(tgbotapi.NewMessage(c.update.FromChat().ID, text)) + return + } + userName := msgTXT[1] + usL , err :=c.uSVC.GetAllBotsUsers(c.bot.Self.UserName) + if err != nil { + c.log.Error("geting users from bot") + return + } + for _, us := range usL{ + if us.Username == userName{ + tgbUS := tgbotapi.User{ + UserName: us.Username, + FirstName: us.FirstName, + LastName: us.LastName, + ID: us.TguID, + } + msg := GenerateUserMessage(&tgbUS, c.update.SentFrom().ID, c.bot.Self.UserName) + c.bot.Send(msg) + } + } + +} + +// userIDCommand +// Render a message with user info and querys ban delete +func userIDCommand(c *command){ + msgTXT := strings.Split(c.update.Message.Text, " ") + if len(msgTXT) <= 1{ + text := "command require username argument" + c.bot.Send(tgbotapi.NewMessage(c.update.FromChat().ID, text)) + return + } + userIDSTR := msgTXT[1] + userID, err := strconv.ParseInt(userIDSTR, 10, 64) + if err != nil { + c.log.Error("geting users from bot", "error", err) + text := "wrong userID argument "+ err.Error() + c.bot.Send(tgbotapi.NewMessage(c.update.FromChat().ID, text)) + return + } + us , err :=c.uSVC.Get(userID) + if err != nil { + c.log.Error("geting user from bot", "error", err) + text := "ERROR: "+ err.Error() + c.bot.Send(tgbotapi.NewMessage(c.update.FromChat().ID, text)) + return + } + bots , err := c.uSVC.GetBots(us.TguID) + if err != nil { + c.log.Error("geting bots for user ", "error", err) + text := "ERROR: "+ err.Error() + c.bot.Send(tgbotapi.NewMessage(c.update.FromChat().ID, text)) + return + } + if !slices.Contains(bots, c.bot.Self.UserName){ + c.log.Error("user is not part of bot list") + text := "user is not part of bot list" + c.bot.Send(tgbotapi.NewMessage(c.update.FromChat().ID, text)) + return + } + tgbUS := tgbotapi.User{ + UserName: us.Username, + FirstName: us.FirstName, + LastName: us.LastName, + ID: us.TguID, + } + msg := GenerateUserMessage(&tgbUS, c.update.SentFrom().ID, c.bot.Self.UserName) + c.bot.Send(msg) +} + +// banCommand +// ban an user by its user name as param +func banCommand(c *command, msg tgbotapi.MessageConfig){ + msgTXT := strings.Split(c.update.Message.Text, " ") + if len(msgTXT) <= 1{ + text := "command require username argument" + c.bot.Send(tgbotapi.NewMessage(c.update.FromChat().ID, text)) + return + } + userName := msgTXT[1] + usL , err :=c.uSVC.GetAllBotsUsers(c.bot.Self.UserName) + if err != nil { + c.log.Error("geting users from bot") + text := "ERROR: "+ err.Error() + c.bot.Send(tgbotapi.NewMessage(c.update.FromChat().ID, text)) + return + } + for _, us := range usL{ + if us.Username == userName{ + tgbUS := tgbotapi.User{ + UserName: us.Username, + FirstName: us.FirstName, + LastName: us.LastName, + ID: us.TguID, + } + msg := GenerateUserMessage(&tgbUS, c.update.SentFrom().ID, c.bot.Self.UserName) + c.bot.Send(msg) + return + } + } + msg.Text = "user not foun on bot list" + c.bot.Send(msg) +} diff --git a/internal/application/domain/cedula.go b/internal/application/domain/cedula.go new file mode 100644 index 0000000..17198a5 --- /dev/null +++ b/internal/application/domain/cedula.go @@ -0,0 +1,7 @@ +package domain + +type Cedula struct { + MunCed string `json:"mun_ced"` + SeqCed string `json:"seq_ced"` + VerCed string `json:"ver_ced"` +} diff --git a/internal/application/domain/foto.go b/internal/application/domain/foto.go new file mode 100644 index 0000000..7d8734e --- /dev/null +++ b/internal/application/domain/foto.go @@ -0,0 +1,12 @@ +package domain + +type imagen []byte + +type Foto struct { + ID int64 `json:"id"` + MunCed string `json:"mun_ced"` + SeqCed string `json:"seq_ced"` + VerCed string `json:"ver_ced"` + Sequencia int64 `json:"sequencia"` + Imagen imagen `json:"imagen"` +} diff --git a/internal/application/domain/info.go b/internal/application/domain/info.go new file mode 100644 index 0000000..51b08a4 --- /dev/null +++ b/internal/application/domain/info.go @@ -0,0 +1,19 @@ +package domain + +type Info struct { + Cedula + ID int64 `json:"id"` + Nombres string `json:"nombres"` + Apellido1 string `json:"apellido1"` + Apellido2 string `json:"apellido2"` + Sexo string `json:"sexo"` + Direccion string `json:"direccion"` + Telefono string `json:"telefono"` + FechaNac string `json:"fecha_nac"` +} + +type MultipleResults struct { + Data []*Info `json:"data"` + Total int64 `json:"total"` + Page int64 `json:"page"` +} diff --git a/internal/application/domain/user.go b/internal/application/domain/user.go new file mode 100644 index 0000000..3eee387 --- /dev/null +++ b/internal/application/domain/user.go @@ -0,0 +1,13 @@ +package domain + +type User struct { + ID int64 + TguID int64 + Username string `json:"username"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Subs []string + Created int64 + Edited int64 + Deleted int64 +} diff --git a/internal/application/message/#message.go# b/internal/application/message/#message.go# new file mode 100644 index 0000000..9d04e76 --- /dev/null +++ b/internal/application/message/#message.go# @@ -0,0 +1,192 @@ +package message + +import ( + "bytes" + "context" + "fmt" + "log/slog" + "strconv" + "strings" + "sync" + + "git.maximotejeda.com/maximo/cedulados-bot/internal/adapter/helper" + "git.maximotejeda.com/maximo/cedulados-bot/internal/application/domain" + "git.maximotejeda.com/maximo/cedulados-bot/internal/ports" + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" +) + +var ChatPool *sync.Pool + +type Message struct { + bot *tgbotapi.BotAPI + update *tgbotapi.Update + msg *tgbotapi.MessageConfig + log *slog.Logger + ce ports.CeduladosService + user ports.UserService +} + +// NewMessage +// Factory for message handler +func NewMessage(bot *tgbotapi.BotAPI, update *tgbotapi.Update, cSVC ports.CeduladosService, 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.ce = cSVC + message.user = user + return message +} + +// Empty +// Returns pointer to pool +func (m *Message) Empty() { + m.update = nil + m.msg = nil + m.log = nil + m.ce = 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) +} + +func (m *Message) Handler(){ + msg := tgbotapi.NewMessage(m.update.Message.Chat.ID, "") + m.msg = &msg + text := helper.RemoveAccent(m.update.Message.Text) + textList := strings.Split(text, " ") + + if len(textList) >= 1 && helper.MessageChecker(text) == "digit" { + // in case of message match a cedula + ced, err := helper.NewCedula(context.Background(), text) + if err != nil { + msg.Text = "cedula no reconocida " + err.Error() + } else { + msg, photoMsg := ProcessByCedula(context.Background(), m.ce, m.user, ced) + msg.ChatID = m.update.Message.Chat.ID + if photoMsg != nil { + photoMsg.ChatID = m.update.Message.Chat.ID + m.bot.Send(photoMsg) + } + m.bot.Send(msg) + return + } + } else if len(textList) >= 2 && helper.MessageChecker(text) == "word" { + msg := ProcessByName(context.Background(), m.ce, m.user, textList) + msg.ChatID = m.update.Message.Chat.ID + m.bot.Send(msg) + } + + msg.ReplyToMessageID = m.update.Message.MessageID + m.bot.Send(msg) + +} + + +// ProcessByCedula +// +// When a text arrives the chat if it match a cedula try to query db +func ProcessByCedula(ctx context.Context, cSVC ports.CeduladosService, uSVC ports.UserService, ced *domain.Cedula) (message *tgbotapi.MessageConfig, fotoMsg *tgbotapi.PhotoConfig) { + + msg := tgbotapi.NewMessage(0, "") + message = &msg + info , err := cSVC.CeduladoByCedula(context.Background(), ced) + if err != nil { + fmt.Println("error on query ", err) + } + + fmt.Println("!!!!success on query", info) + + 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, helper.RemoveSpaces(info.Direccion), info.Telefono) + } + + foto, err := cSVC.QueryFotoByCedula(ctx, ced) + if err != nil { + fmt.Println("Photo not found", err.Error()) + return + } + if foto != nil { + rq := tgbotapi.FileReader{Name: fmt.Sprintf("%s-%s-%s", ced.MunCed, ced.SeqCed, ced.VerCed), Reader: bytes.NewReader(foto.Imagen)} + fotost := tgbotapi.NewPhoto(msg.ChatID, rq) + fotoMsg = &fotost + + } + + return +} + +func ProcessByName(ctx context.Context, cSVC ports.CeduladosService, uSVC ports.UserService, nameList []string) (message *tgbotapi.MessageConfig) { + var err error + page := int64(0) + // look for if the last part of the list is a number + lastItem := nameList[len(nameList)-1] + + if helper.MessageChecker(lastItem) == "digit" && !strings.HasPrefix(lastItem, "0") { + pageInt, err := strconv.ParseInt(lastItem, 10, 64) + if err != nil { + fmt.Println(err) + } + nameList = nameList[:len(nameList)-1] + if pageInt < 20 { + page = pageInt + } + } + rows := &domain.MultipleResults{} + message = &tgbotapi.MessageConfig{} + text := strings.Join(nameList, " ") + + res, err := cSVC.CeduladoByFTS(ctx, text, 0) + if err != nil { + fmt.Println("error oon fts", err) + } + fmt.Println("sucess", res) + + rows, err = cSVC.CeduladoByFTS(ctx, text, 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 +} diff --git a/internal/application/message/message.go b/internal/application/message/message.go new file mode 100644 index 0000000..80db4a2 --- /dev/null +++ b/internal/application/message/message.go @@ -0,0 +1,191 @@ +package message + +import ( + "bytes" + "context" + "fmt" + "log/slog" + "strconv" + "strings" + "sync" + + "git.maximotejeda.com/maximo/cedulados-bot/internal/adapter/helper" + "git.maximotejeda.com/maximo/cedulados-bot/internal/application/domain" + "git.maximotejeda.com/maximo/cedulados-bot/internal/ports" + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" +) + +var ChatPool *sync.Pool + +type Message struct { + bot *tgbotapi.BotAPI + update *tgbotapi.Update + msg *tgbotapi.MessageConfig + log *slog.Logger + ce ports.CeduladosService + user ports.UserService +} + +// NewMessage +// Factory for message handler +func NewMessage(bot *tgbotapi.BotAPI, update *tgbotapi.Update, cSVC ports.CeduladosService, 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.ce = cSVC + message.user = user + return message +} + +// Empty +// Returns pointer to pool +func (m *Message) Empty() { + m.update = nil + m.msg = nil + m.log = nil + m.ce = 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) +} + +func (m *Message) Handler(){ + msg := tgbotapi.NewMessage(m.update.Message.Chat.ID, "") + m.msg = &msg + text := helper.RemoveAccent(m.update.Message.Text) + textList := strings.Split(text, " ") + + if len(textList) >= 1 && helper.MessageChecker(text) == "digit" { + // in case of message match a cedula + ced, err := helper.NewCedula(context.Background(), text) + if err != nil { + msg.Text = "cedula no reconocida " + err.Error() + } else { + msg, photoMsg := ProcessByCedula(context.Background(), m.ce, m.user, ced) + msg.ChatID = m.update.Message.Chat.ID + if photoMsg != nil { + photoMsg.ChatID = m.update.Message.Chat.ID + m.bot.Send(photoMsg) + } + m.bot.Send(msg) + return + } + } else if len(textList) >= 2 && helper.MessageChecker(text) == "word" { + msg := ProcessByName(context.Background(), m.ce, m.user, textList) + msg.ChatID = m.update.Message.Chat.ID + m.bot.Send(msg) + } + + msg.ReplyToMessageID = m.update.Message.MessageID + m.bot.Send(msg) + +} + + +// ProcessByCedula +// +// When a text arrives the chat if it match a cedula try to query db +func ProcessByCedula(ctx context.Context, cSVC ports.CeduladosService, uSVC ports.UserService, ced *domain.Cedula) (message *tgbotapi.MessageConfig, fotoMsg *tgbotapi.PhotoConfig) { + msg := tgbotapi.NewMessage(0, "") + message = &msg + info , err := cSVC.CeduladoByCedula(context.Background(), ced) + if err != nil { + fmt.Println("error on query ", err) + } + + fmt.Println("!!!!success on query", info) + + 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, helper.RemoveSpaces(info.Direccion), info.Telefono) + } + + foto, err := cSVC.QueryFotoByCedula(ctx, ced) + if err != nil { + fmt.Println("Photo not found", err.Error()) + return + } + if foto != nil { + rq := tgbotapi.FileReader{Name: fmt.Sprintf("%s-%s-%s", ced.MunCed, ced.SeqCed, ced.VerCed), Reader: bytes.NewReader(foto.Imagen)} + fotost := tgbotapi.NewPhoto(msg.ChatID, rq) + fotoMsg = &fotost + + } + + return +} + +func ProcessByName(ctx context.Context, cSVC ports.CeduladosService, uSVC ports.UserService, nameList []string) (message *tgbotapi.MessageConfig) { + var err error + page := int64(0) + // look for if the last part of the list is a number + lastItem := nameList[len(nameList)-1] + + if helper.MessageChecker(lastItem) == "digit" && !strings.HasPrefix(lastItem, "0") { + pageInt, err := strconv.ParseInt(lastItem, 10, 64) + if err != nil { + fmt.Println(err) + } + nameList = nameList[:len(nameList)-1] + if pageInt < 20 { + page = pageInt + } + } + rows := &domain.MultipleResults{} + message = &tgbotapi.MessageConfig{} + text := strings.Join(nameList, " ") + + res, err := cSVC.CeduladoByFTS(ctx, text, 0) + if err != nil { + fmt.Println("error oon fts", err) + } + fmt.Println("sucess", res) + + rows, err = cSVC.CeduladoByFTS(ctx, text, 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 +} diff --git a/internal/application/query/query.go b/internal/application/query/query.go new file mode 100644 index 0000000..3075344 --- /dev/null +++ b/internal/application/query/query.go @@ -0,0 +1,167 @@ +package query + +import ( + "fmt" + "log/slog" + "strconv" + "strings" + "sync" + "time" + + "git.maximotejeda.com/maximo/cedulados-bot/internal/application/auth" + "git.maximotejeda.com/maximo/cedulados-bot/internal/ports" + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" +) + +var ChatPool *sync.Pool + +type Query struct { + bot *tgbotapi.BotAPI + update *tgbotapi.Update + msg *tgbotapi.MessageConfig + delMSG *tgbotapi.DeleteMessageConfig + log *slog.Logger + ce ports.CeduladosService + uSVC ports.UserService +} + +// NewMessage +// Factory for query handler +func NewQuery(bot *tgbotapi.BotAPI, update *tgbotapi.Update, cSVC ports.CeduladosService, 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.ce = cSVC + query.uSVC = user + return query +} + +// Empty +// Returns pointer to pool +func (q *Query) Empty() { + q.update = nil + q.msg = nil + q.log = nil + q.ce = nil + q.uSVC = nil + ChatPool.Put(q) +} + +// Send +// Process message sending to bot +func (q *Query) Send() { + defer q.Empty() + q.bot.Send(q.msg) + if q.delMSG != nil { + q.bot.Send(q.delMSG) + } +} + +func (q *Query) Handler(){ + msg := tgbotapi.NewMessage(q.update.CallbackQuery.From.ID, "") + delMSG := tgbotapi.NewDeleteMessage(q.update.CallbackQuery.From.ID, q.update.CallbackQuery.Message.MessageID) + text := "" + q.msg = &msg + q.delMSG = &delMSG + 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] + } + userSTR := dataMap["userID"] + userID, err := strconv.ParseInt(userSTR, 10, 64) + if err != nil { + text = fmt.Sprintf("could not parse int %s", userSTR) + q.log.Error("could not parse id", "userID", userSTR) + return + } + switch dataMap["operation"]{ + case "grant": + // grant user access to a bot + if !auth.IsUserAdmin(q.update.CallbackQuery.From.ID){ + text = "only admin can execute this command" + }else{ + q.log.Info("inside grant") + _, err := q.uSVC.GrantAccess(userID, dataMap["bot"]) + if err != nil { + q.log.Error("granting access to bot", "user", userID, "bot", dataMap["bot"]) + text = fmt.Sprintf("ERROR: grant user access: %s", err) + }else { + text = fmt.Sprintf("granted access to user %s to bot %s", userSTR, dataMap["bot"]) + go func (){ + userMsg := tgbotapi.NewMessage(userID, "Granted access permissions!!") + q.bot.Send(userMsg) + }() + } + } + case "deny": + if !auth.IsUserAdmin(q.update.CallbackQuery.From.ID){ + text = "only admin can execute this command" + }else{ + // deny user access to a bot + _, err := q.uSVC.BanUser(userID, time.Now().Add(24 * 365 * time.Hour).Unix(), dataMap["bot"]) + if err != nil { + q.log.Error("baning user", "user", userID, "bot", dataMap["bot"]) + text = fmt.Sprintf("ERROR: banning user: %s", err) + }else { + text = fmt.Sprintf("user banned user: %s, bot: %s", userSTR, dataMap["bot"]) + } + + } + case "ignore": + text = fmt.Sprintf("user request ignored user: %s", userSTR) + // ignore user access request + case "ban": + if !auth.IsUserAdmin(q.update.CallbackQuery.From.ID){ + text = "only admin can execute this command" + }else{ + _, err := q.uSVC.BanUser(userID, time.Now().Add(24 * 365 * time.Hour).Unix(), dataMap["bot"]) + if err != nil { + q.log.Error("baning user", "user", userID, "bot", dataMap["bot"]) + text = fmt.Sprintf("ERROR: banning user: %s", err) + }else{ + text = fmt.Sprintf("baned user: %s, bot: %s", userSTR, dataMap["bot"]) + } + // TODO: ext = fmt.Sprintf("user banned user: %s, bot: %s", userSTR, dataMap["bot"]) + } + case "unban": + if !auth.IsUserAdmin(q.update.CallbackQuery.From.ID){ + text = "only admin can execute this command" + }else{ + _, err := q.uSVC.UnBanUser(userID, dataMap["bot"]) + if err != nil { + q.log.Error("baning user", "user", userID, "bot", dataMap["bot"]) + text = fmt.Sprintf("ERROR: unbanning user: %s", err) + }else { + text = fmt.Sprintf("user banned user: %s, bot: %s", userSTR, dataMap["bot"]) + } + } + case "delete": + if !auth.IsUserAdmin(q.update.CallbackQuery.From.ID){ + text = "only admin can execute this command" + }else{ + _, err := q.uSVC.DeleteBot(userID, q.bot.Self.UserName) + if err != nil { + q.log.Error("baning user", "user", userID, "bot", dataMap["bot"]) + text = fmt.Sprintf("ERROR: deleted user: %s from bot %s", err, q.bot.Self.UserName) + }else { + text = fmt.Sprintf("user deleted user: %s, bot: %s", userSTR, dataMap["bot"]) + } + } + } + q.msg.Text = text + q.Send() +} diff --git a/internal/ports/cedulados.go b/internal/ports/cedulados.go new file mode 100644 index 0000000..1f9f074 --- /dev/null +++ b/internal/ports/cedulados.go @@ -0,0 +1,17 @@ +package ports + +import ( + "context" + + "git.maximotejeda.com/maximo/cedulados-bot/internal/application/domain" +) + +type CeduladosService interface { + CeduladoByCedula(ctx context.Context, c *domain.Cedula)(resp *domain.Info, err error) + CeduladoGetByNameLastName(ctx context.Context, params domain.Info, page int64)(info *domain.MultipleResults, err error) + CeduladoByFTS(ctx context.Context, params string, page int64)(info *domain.MultipleResults, err error) + CeduladoByNameAndLocation(ctx context.Context, params domain.Info, page int64, municipio string)(info *domain.MultipleResults, err error) + QueryFotoByCedula(ctx context.Context, c *domain.Cedula)(info *domain.Foto, err error) + QueryFotoByID(ctx context.Context, id int64)(info *domain.Foto, err error) + QueryFotoAllCedulas(ctx context.Context, ceds []*domain.Cedula)(fotos []*domain.Foto, err error) +} diff --git a/internal/ports/tgb.go b/internal/ports/tgb.go new file mode 100644 index 0000000..929c3e7 --- /dev/null +++ b/internal/ports/tgb.go @@ -0,0 +1,7 @@ +package ports + +type Tgb interface { + Send() + Empty() + Handler() +} diff --git a/internal/ports/user.go b/internal/ports/user.go new file mode 100644 index 0000000..96b78b0 --- /dev/null +++ b/internal/ports/user.go @@ -0,0 +1,26 @@ +package ports + +import ( + "git.maximotejeda.com/maximo/cedulados-bot/internal/application/domain" + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" + "git.maximotejeda.com/maximo/tgb-user/proto/golang/tgbuser" +) + +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) + CreateBot(string)(error) + CreateAccessRequest(int64, string)(bool, error) + GrantAccess(int64, string)(bool, error) + GetAllAccessRequest(string)(*tgbuser.GetAccessResponse, error) + BanUser(int64, int64, string)(bool, error) + UnBanUser(int64, string)(bool, error) + GetAllBannedUsers(string)(*tgbuser.GetBanResponse, error) + GetAccessRequest(int64) (*tgbuser.GetAccessResponse, error) +} diff --git a/k8s/deployment.yml.template b/k8s/deployment.yml.template new file mode 100644 index 0000000..a368c3e --- /dev/null +++ b/k8s/deployment.yml.template @@ -0,0 +1,34 @@ +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" + volumeMounts: + - name: database + mountPath: /app/dolardb + volumes: + - name: database + persistentVolumeClaim: + claimName: bank-crawler-pvc \ No newline at end of file