FIRST COMMIT In the beginning there was darkness
This commit is contained in:
commit
50bcb37513
1
.dockerignore
Normal file
1
.dockerignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
.env
|
||||||
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
.env
|
||||||
|
k8s/deployment.yml
|
||||||
|
bin/
|
||||||
|
dolardb/
|
||||||
|
|
||||||
|
/icon.png
|
||||||
21
Dockerfile
Normal file
21
Dockerfile
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
FROM golang:alpine AS builder
|
||||||
|
ARG TARGETARCH
|
||||||
|
ARG version=not-set
|
||||||
|
ARG SHORTSHA=not-set
|
||||||
|
WORKDIR /app
|
||||||
|
COPY . .
|
||||||
|
RUN apk --no-cache add git
|
||||||
|
# https://stackoverflow.com/questions/70369368/check-architecture-in-dockerfile-to-get-amd-arm
|
||||||
|
RUN go build -o bin/bot \
|
||||||
|
-ldflags "-X main.Shortsha=${SHORTSHA} \
|
||||||
|
-X main.Version=${version} \
|
||||||
|
-X main.Aarch=${TARGETARCH}" ./cmd/bot/main.go
|
||||||
|
|
||||||
|
FROM alpine AS runner
|
||||||
|
COPY --from=builder /app/bin/bot /usr/bin/
|
||||||
|
WORKDIR /app
|
||||||
|
RUN apk --no-cache add --no-check-certificate ca-certificates \
|
||||||
|
&& update-ca-certificates
|
||||||
|
RUN apk add --no-cache tzdata
|
||||||
|
|
||||||
|
ENTRYPOINT /usr/bin/bot
|
||||||
66
Makefile
Normal file
66
Makefile
Normal file
@ -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=bot
|
||||||
|
BINAME=$(SERVICE)-$(OS)-$(ARCH)
|
||||||
|
BINAMEARM=$(SERVICE)-$(OS)-arm64
|
||||||
|
# can be docker or podman or whatever
|
||||||
|
CONTAINERS=docker
|
||||||
|
COMPOSE=$(CONTAINERS)-compose
|
||||||
|
# Configure local registry
|
||||||
|
REGADDR=10.0.0.150
|
||||||
|
K8SRSNAME=$(shell kubectl get rs --no-headers -o custom-columns=":metadata.name" | grep us-dop-bot)
|
||||||
|
.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)/us-dop-bot:latest .
|
||||||
|
|
||||||
|
# Here we upload it to local
|
||||||
|
|
||||||
|
build-test-image:
|
||||||
|
@$(CONTAINERS) buildx build --platform linux/arm64 --push -t $(REGADDR)/us-dop-bot:latest .
|
||||||
|
|
||||||
|
run-image: build-image
|
||||||
|
@$(CONTAINERS) compose -f docker-compose.yaml up
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
run-webapp: clean build
|
||||||
|
@bin/$(BINAME)-webapp
|
||||||
|
|
||||||
|
build: clean
|
||||||
|
@go build -o ./bin/$(BINAME) ./cmd/bot/.
|
||||||
|
@go build -o ./bin/$(BINAME)-webapp ./cmd/webapp/.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
117
Readme.org
Normal file
117
Readme.org
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
#+Author: Maximo Tejeda
|
||||||
|
#+Email: root@maximotejeda.com
|
||||||
|
#+Date: 10/10/2024
|
||||||
|
|
||||||
|
* Telegram Bot Repository Template
|
||||||
|
|
||||||
|
This README serves as a guide and template for setting up new Telegram bot projects. Follow the
|
||||||
|
instructions below to get your bot up and running.
|
||||||
|
|
||||||
|
** Table of Contents
|
||||||
|
|
||||||
|
- [[#introduction][Introduction]]
|
||||||
|
- [[#prerequisites][Prerequisites]]
|
||||||
|
- [[#setup-instructions][Setup Instructions]]
|
||||||
|
- [[#clone-the-repository][Clone the Repository]]
|
||||||
|
- [[#install-dependencies][Install Dependencies]]
|
||||||
|
- [[#configure-your-bot][Configure Your Bot]]
|
||||||
|
- [[#running-the-bot][Running the Bot]]
|
||||||
|
- [[#Description][Description]]
|
||||||
|
** Introduction
|
||||||
|
<<Introduction>>
|
||||||
|
This repository contains a basic template for developing Telegram bots. It includes essential
|
||||||
|
scripts, configuration files, and documentation to help you get started quickly.
|
||||||
|
|
||||||
|
** Prerequisites
|
||||||
|
<<Prerequisites>>
|
||||||
|
|
||||||
|
Before setting up your bot, ensure you have the following installed:
|
||||||
|
|
||||||
|
- *go*: The go programming for running go code.
|
||||||
|
- *Telegram Bot Token*: Obtain it by creating a new bot via [[https://t.me/botfather][BotFather]] on
|
||||||
|
Telegram.
|
||||||
|
|
||||||
|
** Setup Instructions
|
||||||
|
<<Setup Instructions>>
|
||||||
|
|
||||||
|
*** Clone the Repository
|
||||||
|
|
||||||
|
To get started, clone this repository to your local machine:
|
||||||
|
#+BEGIN_SRC bash
|
||||||
|
git clone https://github.com/yourusername/telegram-bot-template.git
|
||||||
|
cd telegram-bot-template
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
*** Install Dependencies
|
||||||
|
|
||||||
|
Install all required dependencies using go tools:
|
||||||
|
#+BEGIN_SRC bash
|
||||||
|
go mod tidy
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
*** Configure Your Bot
|
||||||
|
|
||||||
|
Create a ~.env~ file in the root directory and add your Telegram bot token:
|
||||||
|
#+BEGIN_SRC plaintext
|
||||||
|
#.env file example
|
||||||
|
|
||||||
|
BOT_TOKEN=yout_bot_token_goes_here
|
||||||
|
NATS_SERVICE_URL=natsuri:4222
|
||||||
|
TGBUSER_SERVICE_URL=localhost:3001 // needed to start svc
|
||||||
|
ENV=development // define log level on start
|
||||||
|
ADMINS=admin_ids_comma_separated // id for admins, those will not need auth
|
||||||
|
RATE_LIMIT_SEC=12 // amount of time to limit hits in sec
|
||||||
|
RATE_LIMIT_AMOUNT=2 // amount of hit limits in x times
|
||||||
|
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
** Running the Bot
|
||||||
|
<<Running the Bot>>
|
||||||
|
|
||||||
|
To start the bot, execute one of the following commands
|
||||||
|
|
||||||
|
If the .env Vars ar e in the environment is dafe to run:
|
||||||
|
#+BEGIN_SRC bash
|
||||||
|
go run cmd/bot/.
|
||||||
|
#+END_SRC
|
||||||
|
|
||||||
|
Else you can run though Makefile with target run-local
|
||||||
|
|
||||||
|
The Makefile populate .env, build and run the source code, on each
|
||||||
|
run the clean target will be called.
|
||||||
|
#+begin_src bash
|
||||||
|
make run-local
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
Your bot should now be running locally.
|
||||||
|
|
||||||
|
** Description
|
||||||
|
<<Description>>
|
||||||
|
|
||||||
|
This is a template Telegram bot designed to be shared across various
|
||||||
|
projects. It includes ready-to-use handlers for commands or queries,
|
||||||
|
serving as examples to simplify development. The bot is built using
|
||||||
|
the current version of the [[https://github.com/go-telegram/bot][go-telegram/bot]] library, ensuring
|
||||||
|
up-to-date functionality and integration.
|
||||||
|
|
||||||
|
In the functions added for the template you can find things like:
|
||||||
|
- Message Handlers
|
||||||
|
- Command Handlers
|
||||||
|
- Callback Query Handlers
|
||||||
|
- Interactions Handlers
|
||||||
|
- Middlewares
|
||||||
|
- Singleflight
|
||||||
|
- Loging
|
||||||
|
- Rate Limiting
|
||||||
|
- Auth Func
|
||||||
|
- Helpers
|
||||||
|
- Auth
|
||||||
|
- File
|
||||||
|
- Photo
|
||||||
|
- Keyboard
|
||||||
|
|
||||||
|
This template heavily relies on the **[[tgbuser]]** microservice for
|
||||||
|
handling user authentication and permissions.
|
||||||
|
|
||||||
|
As The current date theres still a TODO on the WebApp part of the bot
|
||||||
|
for serving and templating an example for miniApps
|
||||||
104
cmd/bot/main.go
Normal file
104
cmd/bot/main.go
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.maximotejeda.com/maximo/telegram-base-bot/config"
|
||||||
|
"git.maximotejeda.com/maximo/telegram-base-bot/internal/adapters/grpc/tgbuser"
|
||||||
|
"git.maximotejeda.com/maximo/telegram-base-bot/internal/application/commands"
|
||||||
|
"git.maximotejeda.com/maximo/telegram-base-bot/internal/application/messages"
|
||||||
|
"git.maximotejeda.com/maximo/telegram-base-bot/internal/application/middlewares"
|
||||||
|
"git.maximotejeda.com/maximo/telegram-base-bot/internal/application/queries"
|
||||||
|
"git.maximotejeda.com/maximo/telegram-base-bot/internal/ports"
|
||||||
|
"github.com/go-telegram/bot"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
var log *slog.Logger
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
lvelEnv:= config.GetEnvironment()
|
||||||
|
var lvel slog.Level
|
||||||
|
if lvelEnv == "dev" || lvelEnv == "development"{
|
||||||
|
lvel = slog.LevelDebug
|
||||||
|
}else {
|
||||||
|
lvel = slog.LevelInfo
|
||||||
|
}
|
||||||
|
log = slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{
|
||||||
|
AddSource: true,
|
||||||
|
Level: lvel,
|
||||||
|
}))
|
||||||
|
|
||||||
|
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||||
|
defer cancel()
|
||||||
|
userSVC, conn := CreateAdaptersGRPC()
|
||||||
|
defer conn.Close()
|
||||||
|
authRequired := middlewares.SetAuthRequired(userSVC, log)
|
||||||
|
logMD := middlewares.CreateLogMiddleWare(ctx, log)
|
||||||
|
opts := []bot.Option{
|
||||||
|
bot.WithMiddlewares(logMD, authRequired),
|
||||||
|
bot.WithAllowedUpdates(bot.AllowedUpdates{
|
||||||
|
"message",
|
||||||
|
"edited_message",
|
||||||
|
"message_reaction",
|
||||||
|
"message_reaction_count",
|
||||||
|
"callback_query",
|
||||||
|
"id",
|
||||||
|
}),
|
||||||
|
// bot.WithDefaultHandler(api.Handler),
|
||||||
|
}
|
||||||
|
b, err := bot.New(config.GetToken(), opts...)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
bInfo , err := b.GetMe(ctx)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// attempt to add bot to db
|
||||||
|
err = userSVC.CreateBot(bInfo.Username)
|
||||||
|
if err != nil {
|
||||||
|
// want to fail fast in case of creating and svc not available
|
||||||
|
// if i cant auth i dont want to run
|
||||||
|
if strings.Contains(err.Error(), "rpc error: code = Unavailable desc = connection error: desc") {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
commands.RegisterCommands(ctx, log, b)
|
||||||
|
messages.RegisterMessageHandler(ctx, log, b)
|
||||||
|
queries.RegisterQueries(ctx, log, b)
|
||||||
|
messages.RegisterMessageReactionHandler(ctx, log, b)
|
||||||
|
b.Start(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateAdaptersGRPC
|
||||||
|
// Create connections for service
|
||||||
|
func CreateAdaptersGRPC() (ports.UserService, *grpc.ClientConn) {
|
||||||
|
// 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()))
|
||||||
|
userConn, err := grpc.NewClient(config.GetUserServiceURL(), opts...)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("creating grpc conn", "error", err)
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("success creating conn", "error", err)
|
||||||
|
user, err := tgbuser.NewAdapter(userConn)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("creating service adapter", "error", err)
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
log.Info("success creating svc ", "error", err)
|
||||||
|
return user, userConn
|
||||||
|
}
|
||||||
54
cmd/webapp/main.go
Normal file
54
cmd/webapp/main.go
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
// Simple server to serve static template files for webapp
|
||||||
|
// telegram miniapp let the webapp work with bot comunicating info
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.maximotejeda.com/maximo/telegram-base-bot/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
log *slog.Logger
|
||||||
|
lv slog.Level
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
e := config.GetEnvironment()
|
||||||
|
switch e {
|
||||||
|
case "dev", "development":
|
||||||
|
lv = slog.LevelDebug
|
||||||
|
case "prod", "production":
|
||||||
|
lv = slog.LevelInfo
|
||||||
|
default:
|
||||||
|
panic(fmt.Errorf("env variable not recognized"))
|
||||||
|
}
|
||||||
|
|
||||||
|
log = slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{
|
||||||
|
Level: lv,
|
||||||
|
}))
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
|
mux.Handle("GET /public/", http.StripPrefix("/public/", http.HandlerFunc(serveStaticFiles)))
|
||||||
|
mux.HandleFunc("GET /{$}", serveStaticFiles)
|
||||||
|
if err := http.ListenAndServe(":8081", mux); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
fmt.Println("webapp", ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// serveStaticFiles
|
||||||
|
func serveStaticFiles(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fs := http.Dir("webapp")
|
||||||
|
h := http.FileServer(fs)
|
||||||
|
h.ServeHTTP(w, r)
|
||||||
|
log.Debug("serving staitc file", "route", r.URL.Path)
|
||||||
|
}
|
||||||
68
config/config.go
Normal file
68
config/config.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetRateLimitSec
|
||||||
|
// Get the rate limit time amount to limit user request
|
||||||
|
// for example a user can make 1 request each sec "1 req x sec"
|
||||||
|
func GetRateLimitSec()float64{
|
||||||
|
amntStr := getEnvVariable("RATE_LIMIT_SEC")
|
||||||
|
amnt, err := strconv.ParseFloat(amntStr, 64)
|
||||||
|
if err != nil{
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return amnt
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRateLimitAmount
|
||||||
|
// Get the rate limit amount of request to limit
|
||||||
|
// for example an user can make 10 request each 10 secs
|
||||||
|
func GetRateLimitAmount()int64{
|
||||||
|
amntStr := getEnvVariable("RATE_LIMIT_AMOUNT")
|
||||||
|
amnt, err := strconv.ParseInt(amntStr, 10, 64)
|
||||||
|
if err != nil{
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return amnt
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAdminsList
|
||||||
|
// Get admin list who dont need auth
|
||||||
|
func GetAdminsList()string {
|
||||||
|
return getEnvVariable("ADMINS")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetToken
|
||||||
|
// Get telegram auth token
|
||||||
|
func GetToken() string {
|
||||||
|
return getEnvVariable("BOT_TOKEN")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNatsURI
|
||||||
|
// Get nats uri for server connection
|
||||||
|
func GetNatsURI() string {
|
||||||
|
return getEnvVariable("NATS_SERVICE_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserServiceURL
|
||||||
|
// Get uri for user service control GRPC
|
||||||
|
func GetUserServiceURL() string {
|
||||||
|
return getEnvVariable("TGBUSER_SERVICE_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEnvironment
|
||||||
|
// Get the environment to debug or prod
|
||||||
|
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)
|
||||||
|
}
|
||||||
17
go.mod
Normal file
17
go.mod
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
module git.maximotejeda.com/maximo/telegram-base-bot
|
||||||
|
|
||||||
|
go 1.23.2
|
||||||
|
|
||||||
|
require (
|
||||||
|
git.maximotejeda.com/maximo/tgb-user v0.0.5
|
||||||
|
github.com/go-telegram/bot v1.12.1
|
||||||
|
google.golang.org/grpc v1.69.2
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
golang.org/x/net v0.30.0 // indirect
|
||||||
|
golang.org/x/sys v0.26.0 // indirect
|
||||||
|
golang.org/x/text v0.19.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect
|
||||||
|
google.golang.org/protobuf v1.35.1 // indirect
|
||||||
|
)
|
||||||
36
go.sum
Normal file
36
go.sum
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
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-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||||
|
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/go-telegram/bot v1.12.1 h1:2CSwMd+g71/XrmuSpvEjLtsmkfL/s63PdnLboGJQxtw=
|
||||||
|
github.com/go-telegram/bot v1.12.1/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM=
|
||||||
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
|
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/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY=
|
||||||
|
go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE=
|
||||||
|
go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE=
|
||||||
|
go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8=
|
||||||
|
go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys=
|
||||||
|
go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A=
|
||||||
|
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
||||||
|
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
||||||
|
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||||
|
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||||
|
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 h1:X58yt85/IXCx0Y3ZwN6sEIKZzQtDEYaBWrDvErdXrRE=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
|
||||||
|
google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU=
|
||||||
|
google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
|
||||||
|
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
|
||||||
|
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||||
1
internal/adapters/db/db.go
Normal file
1
internal/adapters/db/db.go
Normal file
@ -0,0 +1 @@
|
|||||||
|
package db
|
||||||
1
internal/adapters/grpc/grpc.go
Normal file
1
internal/adapters/grpc/grpc.go
Normal file
@ -0,0 +1 @@
|
|||||||
|
package grpc
|
||||||
246
internal/adapters/grpc/tgbuser/tgb.go
Normal file
246
internal/adapters/grpc/tgbuser/tgb.go
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
package tgbuser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"git.maximotejeda.com/maximo/telegram-base-bot/internal/application/domains"
|
||||||
|
"git.maximotejeda.com/maximo/tgb-user/proto/golang/tgbuser"
|
||||||
|
"github.com/go-telegram/bot/models"
|
||||||
|
"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) (*domains.User, error) {
|
||||||
|
hr, err := a.user.Get(context.Background(), &tgbuser.GetTGBUserRequest{TgbId: tgbid})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
user := &domains.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 *models.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 *models.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) ([]*domains.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 := []*domains.User{}
|
||||||
|
for _, us := range users.Users {
|
||||||
|
user := &domains.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
|
||||||
|
}
|
||||||
1
internal/adapters/nats/nats.go
Normal file
1
internal/adapters/nats/nats.go
Normal file
@ -0,0 +1 @@
|
|||||||
|
package nats
|
||||||
16
internal/application/api/api.go
Normal file
16
internal/application/api/api.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/go-telegram/bot"
|
||||||
|
"github.com/go-telegram/bot/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Handler(ctx context.Context, b *bot.Bot, update *models.Update){
|
||||||
|
|
||||||
|
b.SendMessage(ctx, &bot.SendMessageParams{
|
||||||
|
ChatID: update.Message.Chat.ID,
|
||||||
|
Text: update.Message.Text,
|
||||||
|
})
|
||||||
|
}
|
||||||
66
internal/application/commands/commands.go
Normal file
66
internal/application/commands/commands.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.maximotejeda.com/maximo/telegram-base-bot/internal/application/helpers"
|
||||||
|
"github.com/go-telegram/bot"
|
||||||
|
"github.com/go-telegram/bot/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
HELPSTRING =`
|
||||||
|
# Telegram Base Bot
|
||||||
|
|
||||||
|
This is an Simple example template to
|
||||||
|
initiate bots projects with the same
|
||||||
|
basic functionalities needed common
|
||||||
|
on all prj like:
|
||||||
|
|
||||||
|
-\ Keyboard Making
|
||||||
|
-\ Command Handler
|
||||||
|
-\ Interaction Handler
|
||||||
|
-\ Message Handler
|
||||||
|
-\ Callback Query
|
||||||
|
-\ MiddleWares:
|
||||||
|
-\ Singleflight
|
||||||
|
-\ rate limiting
|
||||||
|
-\ Login
|
||||||
|
|
||||||
|
The service is highly dependant on
|
||||||
|
the tgb user microservice which is
|
||||||
|
in charge of hanlding the bot and
|
||||||
|
authing users and banning
|
||||||
|
`
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func RegisterCommands(ctx context.Context, log *slog.Logger, b *bot.Bot){
|
||||||
|
b.RegisterHandler(bot.HandlerTypeMessageText, "/help",bot.MatchTypeExact, HelpCommand)
|
||||||
|
}
|
||||||
|
|
||||||
|
func HelpCommand(ctx context.Context, b *bot.Bot, update *models.Update){
|
||||||
|
_, err := b.SendMessage(ctx, &bot.SendMessageParams{
|
||||||
|
ChatID: update.Message.Chat.ID,
|
||||||
|
Text: fmt.Sprintf("%s", HELPSTRING),
|
||||||
|
ParseMode: models.ParseModeHTML,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
icon , err := os.ReadFile("icon.png")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
fmt.Println(os.Getwd())
|
||||||
|
}
|
||||||
|
//helpers.SendDocument(ctx, b, update, []byte("hello try"), "hello.txt")
|
||||||
|
err = helpers.SendPhotos(ctx, b, update, icon, "fb logo")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
1
internal/application/domains/models.go
Normal file
1
internal/application/domains/models.go
Normal file
@ -0,0 +1 @@
|
|||||||
|
package domains
|
||||||
13
internal/application/domains/users.go
Normal file
13
internal/application/domains/users.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package domains
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
202
internal/application/helpers/auth.go
Normal file
202
internal/application/helpers/auth.go
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
package helpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.maximotejeda.com/maximo/telegram-base-bot/config"
|
||||||
|
"git.maximotejeda.com/maximo/telegram-base-bot/internal/ports"
|
||||||
|
"github.com/go-telegram/bot"
|
||||||
|
"github.com/go-telegram/bot/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func IsUserRegistered() {}
|
||||||
|
func IsUserAuthorized() {}
|
||||||
|
func GetPendingAuthRequest() {}
|
||||||
|
func CreatePendingAuthRequest() {}
|
||||||
|
func DeletePendingAuthRequest() {}
|
||||||
|
func BanUSer() {}
|
||||||
|
func UnBanUser() {}
|
||||||
|
|
||||||
|
func Authenticate(ctx context.Context, log *slog.Logger, b *bot.Bot, update *models.Update, uSVC ports.UserService) bool {
|
||||||
|
var (
|
||||||
|
user models.User
|
||||||
|
|
||||||
|
)
|
||||||
|
// select user
|
||||||
|
if update.CallbackQuery != nil {
|
||||||
|
user = update.CallbackQuery.From
|
||||||
|
} else {
|
||||||
|
if update.MessageReaction != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
user = *update.Message.From
|
||||||
|
}
|
||||||
|
// bot name
|
||||||
|
bn, _ := b.GetMe(ctx)
|
||||||
|
switch IsUserAdmin(user.ID) {
|
||||||
|
case true:
|
||||||
|
// theres an user env admin
|
||||||
|
// check if user exist on db
|
||||||
|
_, err := uSVC.Get(user.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("geting user", "error", err)
|
||||||
|
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 := uSVC.Create(&user)
|
||||||
|
log.Debug("creating user")
|
||||||
|
if err != nil {
|
||||||
|
log.Error("creating new user for admnin", "err", err)
|
||||||
|
}
|
||||||
|
// add bot to user list
|
||||||
|
_, err = uSVC.AddBot(user.ID, bn.Username)
|
||||||
|
if err != nil {
|
||||||
|
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 := uSVC.Get(user.ID)
|
||||||
|
if err != nil {
|
||||||
|
// user need auth
|
||||||
|
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 := uSVC.Create(&user)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("creating new user", "user", user.ID, "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create access request
|
||||||
|
_, err = uSVC.CreateAccessRequest(user.ID, bn.Username)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("creating access request for ", "user", user.ID, "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bots, err := uSVC.GetBots(user.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("checking bots on user access")
|
||||||
|
}
|
||||||
|
switch HasUserAccess(bots, bn.Username) {
|
||||||
|
case true:
|
||||||
|
return true
|
||||||
|
case false:
|
||||||
|
// check for banned user
|
||||||
|
buser, err := uSVC.GetAllBannedUsers(bn.Username)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("error querying banned user")
|
||||||
|
}
|
||||||
|
for _, u := range buser.GetBans() {
|
||||||
|
if u.TgbId == user.ID {
|
||||||
|
b.SendMessage(ctx, &bot.SendMessageParams{
|
||||||
|
ChatID: u.TgbId,
|
||||||
|
Text: "user access is restricted, please ask for permission",
|
||||||
|
},)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ac, err := uSVC.GetAccessRequest(user.ID)
|
||||||
|
acl := []string{}
|
||||||
|
for _, val := range ac.Access {
|
||||||
|
acl = append(acl, val.BotName)
|
||||||
|
}
|
||||||
|
if slices.Contains(acl, bn.Username) {
|
||||||
|
// create access request
|
||||||
|
log.Info("Access Request found returning early", "user", user.ID, "error", err)
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// create one
|
||||||
|
_, err = uSVC.CreateAccessRequest(user.ID, bn.Username)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("creating access request", "err", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// get all admins
|
||||||
|
userL, _ := GetAdminFromEnv()
|
||||||
|
// send a mesage to all admins
|
||||||
|
for _, adm := range userL {
|
||||||
|
|
||||||
|
param := GenerateAccessRequestMessage(user, adm, b, update)
|
||||||
|
b.SendMessage(ctx, param)
|
||||||
|
}
|
||||||
|
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 models.User, adm int64, b *bot.Bot, update *models.Update) *bot.SendMessageParams {
|
||||||
|
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 := &bot.SendMessageParams{
|
||||||
|
ChatID: adm,
|
||||||
|
Text: txt,
|
||||||
|
}
|
||||||
|
|
||||||
|
bn, _ := b.GetMe(context.Background())
|
||||||
|
kbd := &models.InlineKeyboardMarkup{
|
||||||
|
InlineKeyboard: [][]models.InlineKeyboardButton{
|
||||||
|
{
|
||||||
|
{Text: "Grant", CallbackData: fmt.Sprintf("operation=grant&userID=%d&bot=%s", up.ID, bn.Username)},
|
||||||
|
{Text: "Deny", CallbackData: fmt.Sprintf("operation=deny&userID=%d&bot=%s", up.ID, bn.Username)},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
{Text: "Ignore", CallbackData: fmt.Sprintf("operation=ignore&userID=%d&bot=%s", up.ID, bn.Username)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
msg.ReplyMarkup = kbd
|
||||||
|
return msg
|
||||||
|
}
|
||||||
24
internal/application/helpers/file.go
Normal file
24
internal/application/helpers/file.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package helpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/go-telegram/bot"
|
||||||
|
"github.com/go-telegram/bot/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func SendDocument(ctx context.Context, b *bot.Bot, update *models.Update, document []byte, title string)error{
|
||||||
|
if document == nil {
|
||||||
|
return errors.New("file provided cant be nil")
|
||||||
|
}
|
||||||
|
params := &bot.SendDocumentParams{
|
||||||
|
ChatID: update.Message.Chat.ID,
|
||||||
|
Document: &models.InputFileUpload{Filename: title, Data: bytes.NewReader(document)},
|
||||||
|
Caption: title,
|
||||||
|
}
|
||||||
|
b.SendDocument(ctx, params)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
139
internal/application/helpers/keyboard.go
Normal file
139
internal/application/helpers/keyboard.go
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
package helpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/go-telegram/bot/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrRowLen = errors.New("row len alredy full")
|
||||||
|
)
|
||||||
|
|
||||||
|
type InlineKeyboard struct {
|
||||||
|
Rows []Row
|
||||||
|
}
|
||||||
|
|
||||||
|
type Row struct {
|
||||||
|
Len int
|
||||||
|
Buttons []Button
|
||||||
|
}
|
||||||
|
|
||||||
|
type Button struct {
|
||||||
|
Data string
|
||||||
|
Text string
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddButton
|
||||||
|
// Add a button to the last row of the keyboard
|
||||||
|
// if the row if full and there are still pending buttons
|
||||||
|
// to add, a new row will be created and buttons added to it
|
||||||
|
func AddButton(ik *InlineKeyboard, text, data string) {
|
||||||
|
r := &ik.Rows[len(ik.Rows)-1]
|
||||||
|
if len(r.Buttons) < r.Len {
|
||||||
|
button := Button{Text: text, Data: data}
|
||||||
|
r.Buttons = append(r.Buttons, button)
|
||||||
|
fmt.Println("ADD BUTTON ", r, ik)
|
||||||
|
}else {
|
||||||
|
r = &Row{
|
||||||
|
Len: r.Len,
|
||||||
|
Buttons: []Button{},
|
||||||
|
}
|
||||||
|
button := Button{Text: text, Data: data}
|
||||||
|
r.Buttons = append(r.Buttons, button)
|
||||||
|
fmt.Println("ADD BUTTON ", r, ik)
|
||||||
|
|
||||||
|
ik.Rows = append(ik.Rows, *r)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddRow
|
||||||
|
// Create a new row with a len property to limit wide of kbd
|
||||||
|
func AddRow(ik *InlineKeyboard, len int){
|
||||||
|
row := &Row{Len: len, Buttons: []Button{}}
|
||||||
|
ik.Rows = append(ik.Rows, *row)
|
||||||
|
fmt.Println("ADD Row ", row.Len)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateKeyBoard
|
||||||
|
// render the structure into a models.InlineKeyboardMarkup
|
||||||
|
func (ik *InlineKeyboard) CreateKeyBoard()models.InlineKeyboardMarkup{
|
||||||
|
kbd := models.InlineKeyboardMarkup{}
|
||||||
|
fmt.Println("creating keyboard ---- ", fmt.Sprintf("%#v", ik))
|
||||||
|
fmt.Println("row 0 ", ik.Rows[0])
|
||||||
|
for _, row := range ik.Rows {
|
||||||
|
r := []models.InlineKeyboardButton{}
|
||||||
|
for _, button := range row.Buttons{
|
||||||
|
r = append(r, models.InlineKeyboardButton{Text: button.Text, CallbackData: button.Data})
|
||||||
|
}
|
||||||
|
kbd.InlineKeyboard = append(kbd.InlineKeyboard, r)
|
||||||
|
}
|
||||||
|
return kbd
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyboardWithAcceptCancel
|
||||||
|
func KeyboardWithAcceptCancel(textData [][]string, buttonSize int, up bool)models.InlineKeyboardMarkup{
|
||||||
|
kbd := &InlineKeyboard{}
|
||||||
|
if up {
|
||||||
|
AddRow(kbd, 2)
|
||||||
|
AddButton(kbd, "Accept ✅", "operation=accept")
|
||||||
|
AddButton(kbd, "Cancel ❌", "operation=cancel")
|
||||||
|
}
|
||||||
|
AddRow(kbd, buttonSize)
|
||||||
|
for _, it := range textData{
|
||||||
|
if len(it) ==2{
|
||||||
|
AddButton(kbd, it[0], it[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !up {
|
||||||
|
AddRow(kbd, 2)
|
||||||
|
AddButton(kbd, "Accept ✅", "operation=accept")
|
||||||
|
AddButton(kbd, "Cancel ❌", "operation=cancel")
|
||||||
|
}
|
||||||
|
return kbd.CreateKeyBoard()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// KeyboardWithCancel
|
||||||
|
func KeyboardWithCancel(textData [][]string, buttonSize int, up bool)models.InlineKeyboardMarkup{
|
||||||
|
kbd := &InlineKeyboard{}
|
||||||
|
if up {
|
||||||
|
AddRow(kbd, 2)
|
||||||
|
AddButton(kbd, "Cancel ❌", "operation=cancel")
|
||||||
|
}
|
||||||
|
AddRow(kbd, buttonSize)
|
||||||
|
for _, it := range textData{
|
||||||
|
if len(it) ==2{
|
||||||
|
AddButton(kbd, it[0], it[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !up {
|
||||||
|
AddRow(kbd, 2)
|
||||||
|
AddButton(kbd, "Cancel ❌", "operation=cancel")
|
||||||
|
}
|
||||||
|
return kbd.CreateKeyBoard()
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyboardWithBackNext
|
||||||
|
func KeyboardWithBackNext(textData [][]string, buttonSize int, up bool)models.InlineKeyboardMarkup{
|
||||||
|
kbd := &InlineKeyboard{}
|
||||||
|
if up {
|
||||||
|
AddRow(kbd, 2)
|
||||||
|
AddButton(kbd, "⬅️ Back", "operation=accept")
|
||||||
|
AddButton(kbd, "Next ➡️", "operation=cancel")
|
||||||
|
}
|
||||||
|
AddRow(kbd, buttonSize)
|
||||||
|
for _, it := range textData{
|
||||||
|
if len(it) ==2{
|
||||||
|
AddButton(kbd, it[0], it[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !up {
|
||||||
|
AddRow(kbd, 2)
|
||||||
|
AddButton(kbd, "⬅️ Back", "operation=accept")
|
||||||
|
AddButton(kbd, "Next ➡️", "operation=cancel")
|
||||||
|
}
|
||||||
|
return kbd.CreateKeyBoard()
|
||||||
|
}
|
||||||
27
internal/application/helpers/photo.go
Normal file
27
internal/application/helpers/photo.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package helpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/go-telegram/bot"
|
||||||
|
"github.com/go-telegram/bot/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SendPhotos(ctx context.Context, b *bot.Bot, update *models.Update, data []byte, title string)error{
|
||||||
|
|
||||||
|
if data == nil {
|
||||||
|
return errors.New("data cant be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
params := &bot.SendPhotoParams{
|
||||||
|
ChatID: update.Message.Chat.ID,
|
||||||
|
Photo: &models.InputFileUpload{
|
||||||
|
Data: bytes.NewReader(data),
|
||||||
|
},
|
||||||
|
Caption: title,
|
||||||
|
}
|
||||||
|
_, err := b.SendPhoto(ctx, params)
|
||||||
|
return err
|
||||||
|
}
|
||||||
32
internal/application/messages/messages.go
Normal file
32
internal/application/messages/messages.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package messages
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"git.maximotejeda.com/maximo/telegram-base-bot/internal/application/helpers"
|
||||||
|
"git.maximotejeda.com/maximo/telegram-base-bot/internal/application/middlewares"
|
||||||
|
"github.com/go-telegram/bot"
|
||||||
|
"github.com/go-telegram/bot/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RegisterMessageHandler(ctx context.Context, log *slog.Logger, b *bot.Bot){
|
||||||
|
messageRL := middlewares.CreateRateLimitUser(ctx, log, 15, 1)
|
||||||
|
// b.RegisterHandler(bot.HandlerTypeMessageText, "hello", bot.MatchTypeExact, HandleHelloMessage, messageRL)
|
||||||
|
b.RegisterHandler(bot.HandlerTypeMessageText, "h", bot.MatchTypeContains, HandleHelloMessage, messageRL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleHelloMessage(ctx context.Context, b *bot.Bot, update *models.Update){
|
||||||
|
// kbd := &helpers.InlineKeyboard{}
|
||||||
|
it := [][]string{}
|
||||||
|
for x := range 9{
|
||||||
|
it = append(it, []string{fmt.Sprintf("%d", x), fmt.Sprintf("button_%d", x)})
|
||||||
|
}
|
||||||
|
kb:= helpers.KeyboardWithCancel(it, 3, false)
|
||||||
|
b.SendMessage(ctx, &bot.SendMessageParams{
|
||||||
|
ChatID: update.Message.Chat.ID,
|
||||||
|
Text: "managing text",
|
||||||
|
ReplyMarkup: kb,
|
||||||
|
})
|
||||||
|
}
|
||||||
61
internal/application/messages/reactions.go
Normal file
61
internal/application/messages/reactions.go
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
package messages
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"github.com/go-telegram/bot"
|
||||||
|
"github.com/go-telegram/bot/models"
|
||||||
|
)
|
||||||
|
var (
|
||||||
|
log *slog.Logger
|
||||||
|
)
|
||||||
|
|
||||||
|
// RegisterMessageReactionHandler
|
||||||
|
// Register a match function for an specific emoji to control reactions from users on messages
|
||||||
|
// quick reaction emojis:
|
||||||
|
// "❤", "🔥", "👍", "👎", "🥰", "👏", "😁"
|
||||||
|
// those are default emojis for telegram client
|
||||||
|
func RegisterMessageReactionHandler(ctx context.Context, llog *slog.Logger, b *bot.Bot){
|
||||||
|
log = llog
|
||||||
|
heartHandler := matchReaction("❤")
|
||||||
|
fireHandler := matchReaction("🔥")
|
||||||
|
thumbsUpHandler := matchReaction("👍")
|
||||||
|
thumbsDownHandler := matchReaction("👎")
|
||||||
|
loveHandler := matchReaction("🥰")
|
||||||
|
clapHandler := matchReaction("👏")
|
||||||
|
laughtHandler := matchReaction("😁")
|
||||||
|
b.RegisterHandlerMatchFunc( fireHandler, handler)
|
||||||
|
b.RegisterHandlerMatchFunc( heartHandler, handler)
|
||||||
|
b.RegisterHandlerMatchFunc( thumbsUpHandler, handler)
|
||||||
|
b.RegisterHandlerMatchFunc(thumbsDownHandler, handler)
|
||||||
|
b.RegisterHandlerMatchFunc(loveHandler, handler)
|
||||||
|
b.RegisterHandlerMatchFunc(laughtHandler, handler)
|
||||||
|
b.RegisterHandlerMatchFunc(clapHandler, handler)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchReaction(emoji string)func(*models.Update)bool{
|
||||||
|
return func (update *models.Update)bool{
|
||||||
|
if update.MessageReaction != nil && len(update.MessageReaction.NewReaction) > 0{
|
||||||
|
switch reaction := update.MessageReaction.NewReaction[0].Type; reaction {
|
||||||
|
case models.ReactionTypeTypeEmoji:
|
||||||
|
log.Info("emoji message reaction encounter", "reactions", update.MessageReaction.NewReaction)
|
||||||
|
if update.MessageReaction.NewReaction[0].ReactionTypeEmoji.Emoji == emoji{
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
log.Info("not the same character")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
log.Info("not a reaction")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func handler(ctx context.Context, b *bot.Bot, update *models.Update){
|
||||||
|
log.Info("message from reaction")
|
||||||
|
|
||||||
|
}
|
||||||
252
internal/application/middlewares/middlewares.go
Normal file
252
internal/application/middlewares/middlewares.go
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
package middlewares
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.maximotejeda.com/maximo/telegram-base-bot/config"
|
||||||
|
"git.maximotejeda.com/maximo/telegram-base-bot/internal/application/helpers"
|
||||||
|
"git.maximotejeda.com/maximo/telegram-base-bot/internal/ports"
|
||||||
|
"github.com/go-telegram/bot"
|
||||||
|
"github.com/go-telegram/bot/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
var log = slog.New(slog.NewJSONHandler(os.Stderr, nil))
|
||||||
|
|
||||||
|
func ShowMessageWithUserID(next bot.HandlerFunc) bot.HandlerFunc {
|
||||||
|
return func(ctx context.Context, bot *bot.Bot, update *models.Update) {
|
||||||
|
if update.Message != nil {
|
||||||
|
log.Info("User new message", fmt.Sprintf("%d", update.Message.From.ID), update.Message.Text)
|
||||||
|
}
|
||||||
|
next(ctx, bot, update)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// singleFlight is a middleware that ensures that only one callback query is processed at a time.
|
||||||
|
// example from https://github.com/go-telegram/bot/blob/main/examples/middleware/main.go
|
||||||
|
func SingleFlight(next bot.HandlerFunc) bot.HandlerFunc {
|
||||||
|
sf := sync.Map{}
|
||||||
|
return func(ctx context.Context, b *bot.Bot, update *models.Update) {
|
||||||
|
if update.CallbackQuery != nil {
|
||||||
|
key := update.CallbackQuery.Message.Message.ID
|
||||||
|
if _, loaded := sf.LoadOrStore(key, struct{}{}); loaded {
|
||||||
|
b.SendMessage(ctx, &bot.SendMessageParams{ChatID: update.CallbackQuery.From.ID, Text: "Query on flight, please wait"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer sf.Delete(key)
|
||||||
|
next(ctx, b, update)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func LogMessage(next bot.HandlerFunc) bot.HandlerFunc {
|
||||||
|
return func(ctx context.Context, b *bot.Bot, update *models.Update) {
|
||||||
|
txt := ""
|
||||||
|
user := &models.User{}
|
||||||
|
if update.CallbackQuery == nil {
|
||||||
|
txt = update.Message.Text
|
||||||
|
user = update.Message.From
|
||||||
|
} else {
|
||||||
|
txt = fmt.Sprintf("%#v", update.CallbackQuery)
|
||||||
|
user = &update.CallbackQuery.From
|
||||||
|
}
|
||||||
|
log.Info(txt, "user", user.Username)
|
||||||
|
next(ctx, b, update)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NotifyAdmin(next bot.HandlerFunc) bot.HandlerFunc {
|
||||||
|
return func(ctx context.Context, b *bot.Bot, update *models.Update) {
|
||||||
|
next(ctx, b, update)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAuthRequired
|
||||||
|
// set authorization middleware
|
||||||
|
// authorization will be set to an amount of time in minutes
|
||||||
|
func SetAuthRequired(svc ports.UserService, log *slog.Logger) func(bot.HandlerFunc) bot.HandlerFunc {
|
||||||
|
userLastAuth := sync.Map{}
|
||||||
|
return func(next bot.HandlerFunc) bot.HandlerFunc {
|
||||||
|
return func(ctx context.Context, b *bot.Bot, update *models.Update) {
|
||||||
|
var key int64
|
||||||
|
if update.CallbackQuery == nil {
|
||||||
|
if update.MessageReaction == nil {
|
||||||
|
key = update.Message.From.ID
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
key = update.CallbackQuery.From.ID
|
||||||
|
}
|
||||||
|
slog.Debug("executing auth func","user", key)
|
||||||
|
authMe := func() {
|
||||||
|
k := helpers.Authenticate(ctx, log, b, update, svc)
|
||||||
|
if !k {
|
||||||
|
log.Info("user not Authenticated", "user", key)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Debug("storing user last auth to map")
|
||||||
|
userLastAuth.Store(key, time.Now())
|
||||||
|
log.Info("user Authenticated", "user", key, "time", time.Now())
|
||||||
|
next(ctx, b, update)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, loaded := userLastAuth.LoadOrStore(key, time.Now()); loaded {
|
||||||
|
when, _ := userLastAuth.Load(key)
|
||||||
|
switch {
|
||||||
|
case time.Since(when.(time.Time)).Minutes() < 3: // the time user will remain auth on the bot
|
||||||
|
log.Info("user on cache available", "user", key, "time", when)
|
||||||
|
next(ctx, b, update)
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
log.Debug("user last auth is more than x min, auth again")
|
||||||
|
authMe()
|
||||||
|
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Debug("user not auth racently, authenticating")
|
||||||
|
authMe()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func RateLimitUser(next bot.HandlerFunc) bot.HandlerFunc {
|
||||||
|
rl := sync.Map{}
|
||||||
|
type data struct {
|
||||||
|
when time.Time
|
||||||
|
amount int64
|
||||||
|
}
|
||||||
|
return func(ctx context.Context, b *bot.Bot, update *models.Update) {
|
||||||
|
var (
|
||||||
|
key int64
|
||||||
|
rLimitTime = config.GetRateLimitSec()
|
||||||
|
rLimitAmnt = config.GetRateLimitAmount()
|
||||||
|
)
|
||||||
|
|
||||||
|
if update.CallbackQuery == nil {
|
||||||
|
key = update.Message.From.ID
|
||||||
|
} else {
|
||||||
|
key = update.CallbackQuery.From.ID
|
||||||
|
}
|
||||||
|
log.Info("got key ", "key ", key)
|
||||||
|
if _, loaded := rl.LoadOrStore(key, data{when: time.Now(), amount: 0}); loaded {
|
||||||
|
log.Info("user loaded on map")
|
||||||
|
dt, _ := rl.Load(key)
|
||||||
|
dtl := dt.(data)
|
||||||
|
|
||||||
|
dtl.amount++
|
||||||
|
amnt := dtl.amount
|
||||||
|
rl.Store(key, data{when: dtl.when, amount: amnt})
|
||||||
|
switch time.Since(dtl.when).Seconds() < rLimitTime {
|
||||||
|
case true:
|
||||||
|
switch dtl.amount > rLimitAmnt {
|
||||||
|
case true:
|
||||||
|
log.Info("user rl execeed", "since", time.Since(dtl.when).Seconds(), "amount", dtl.amount)
|
||||||
|
rl.Store(key, data{when: time.Now(), amount: amnt})
|
||||||
|
b.SendMessage(ctx, &bot.SendMessageParams{ChatID: key, Text: "Rate Limit Exeded"})
|
||||||
|
return
|
||||||
|
case false:
|
||||||
|
|
||||||
|
log.Info("user rl not execeed", "since", time.Since(dtl.when).Seconds(), "amount", dtl.amount)
|
||||||
|
}
|
||||||
|
case false:
|
||||||
|
rl.Store(key, data{when: time.Now(), amount: 1})
|
||||||
|
log.Info("user time", "since", time.Since(dtl.when).Seconds(), "amount", dtl.amount)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
rl.Store(key, data{when: time.Now(), amount: 1})
|
||||||
|
log.Info("user not loaded", "user", key)
|
||||||
|
}
|
||||||
|
next(ctx, b, update)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateRateLimitUser
|
||||||
|
// Create an specific rate limiting with distincts values of time and hits
|
||||||
|
func CreateRateLimitUser(ctx context.Context, log *slog.Logger, delay float64, hits int64) func(bot.HandlerFunc) bot.HandlerFunc {
|
||||||
|
return func(next bot.HandlerFunc) bot.HandlerFunc {
|
||||||
|
rl := sync.Map{}
|
||||||
|
type data struct {
|
||||||
|
when time.Time
|
||||||
|
amount int64
|
||||||
|
}
|
||||||
|
return func(ctx context.Context, b *bot.Bot, update *models.Update) {
|
||||||
|
var (
|
||||||
|
key int64
|
||||||
|
rLimitTime = delay // time in secs to check
|
||||||
|
rLimitAmnt = hits // number of hits
|
||||||
|
)
|
||||||
|
|
||||||
|
if update.CallbackQuery == nil {
|
||||||
|
key = update.Message.From.ID
|
||||||
|
} else {
|
||||||
|
key = update.CallbackQuery.From.ID
|
||||||
|
}
|
||||||
|
log.Info("got key ", "key ", key)
|
||||||
|
if _, loaded := rl.LoadOrStore(key, data{when: time.Now(), amount: 0}); loaded {
|
||||||
|
log.Info("user loaded on map")
|
||||||
|
dt, _ := rl.Load(key)
|
||||||
|
dtl := dt.(data)
|
||||||
|
|
||||||
|
dtl.amount++
|
||||||
|
amnt := dtl.amount
|
||||||
|
rl.Store(key, data{when: dtl.when, amount: amnt})
|
||||||
|
switch time.Since(dtl.when).Seconds() < rLimitTime {
|
||||||
|
case true:
|
||||||
|
switch dtl.amount >= rLimitAmnt {
|
||||||
|
case true:
|
||||||
|
log.Info("user rl execeed", "since", time.Since(dtl.when).Seconds(), "amount", dtl.amount)
|
||||||
|
rl.Store(key, data{when: time.Now(), amount: amnt})
|
||||||
|
b.SendMessage(ctx, &bot.SendMessageParams{ChatID: key, Text: "Rate Limit Exeded"})
|
||||||
|
return
|
||||||
|
case false:
|
||||||
|
|
||||||
|
log.Info("user rl not execeed", "since", time.Since(dtl.when).Seconds(), "amount", dtl.amount)
|
||||||
|
}
|
||||||
|
case false:
|
||||||
|
rl.Store(key, data{when: time.Now(), amount: 1})
|
||||||
|
log.Info("user time", "since", time.Since(dtl.when).Seconds(), "amount", dtl.amount)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
log.Info("user not loaded", "user", key)
|
||||||
|
}
|
||||||
|
next(ctx, b, update)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateLogMiddleWare(ctx context.Context, log *slog.Logger) func(bot.HandlerFunc) bot.HandlerFunc{
|
||||||
|
return func(next bot.HandlerFunc)bot.HandlerFunc{
|
||||||
|
return func(ctx context.Context, b *bot.Bot, update *models.Update) {
|
||||||
|
start := time.Now()
|
||||||
|
txt := ""
|
||||||
|
user := &models.User{}
|
||||||
|
if update.CallbackQuery == nil {
|
||||||
|
if update.MessageReaction == nil {
|
||||||
|
txt = update.Message.Text
|
||||||
|
user = update.Message.From
|
||||||
|
}else {
|
||||||
|
user = update.MessageReaction.User
|
||||||
|
txt = "reaction"
|
||||||
|
log.Info("reaction", "react", update.MessageReaction)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
} else {
|
||||||
|
txt = fmt.Sprintf("%#v", update.CallbackQuery)
|
||||||
|
user = &update.CallbackQuery.From
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info(txt, "user", user.Username)
|
||||||
|
log.Debug("reponse", "user", user.Username, "id", user.ID, "elapsed ms", time.Since(start).Milliseconds())
|
||||||
|
next(ctx, b, update)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
internal/application/queries/queries.go
Normal file
43
internal/application/queries/queries.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package queries
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.maximotejeda.com/maximo/telegram-base-bot/internal/application/middlewares"
|
||||||
|
"github.com/go-telegram/bot"
|
||||||
|
"github.com/go-telegram/bot/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RegisterQueries
|
||||||
|
func RegisterQueries(ctx context.Context, log *slog.Logger, b *bot.Bot){
|
||||||
|
b.RegisterHandler(bot.HandlerTypeCallbackQueryData, "button",bot.MatchTypeContains, HandleQuery, middlewares.LogMessage, middlewares.SingleFlight)
|
||||||
|
b.RegisterHandler(bot.HandlerTypeCallbackQueryData, "operation=cancel",bot.MatchTypeContains, deleteQuery, middlewares.LogMessage, middlewares.SingleFlight)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleQuery
|
||||||
|
// Example of handle query
|
||||||
|
func HandleQuery(ctx context.Context, b *bot.Bot, update *models.Update){
|
||||||
|
// Simulate work
|
||||||
|
time.Sleep(2*time.Second)
|
||||||
|
// send a response
|
||||||
|
b.SendMessage(ctx, &bot.SendMessageParams{
|
||||||
|
ChatID: update.CallbackQuery.From.ID, // seems that query can come only fron one source USER
|
||||||
|
Text: "handling query " + update.CallbackQuery.Data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// deleteQuery
|
||||||
|
// Delete message sending the query
|
||||||
|
func deleteQuery(ctx context.Context, b *bot.Bot, update *models.Update){
|
||||||
|
//Message to delete
|
||||||
|
mtd := update.CallbackQuery.Message.Message.ID
|
||||||
|
// Chat where message come from
|
||||||
|
cwmcf := update.CallbackQuery.From.ID
|
||||||
|
|
||||||
|
b.DeleteMessage(ctx, &bot.DeleteMessageParams{
|
||||||
|
ChatID: cwmcf,
|
||||||
|
MessageID: mtd,
|
||||||
|
})
|
||||||
|
}
|
||||||
1
internal/ports/ports.go
Normal file
1
internal/ports/ports.go
Normal file
@ -0,0 +1 @@
|
|||||||
|
package ports
|
||||||
27
internal/ports/user.go
Normal file
27
internal/ports/user.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package ports
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.maximotejeda.com/maximo/telegram-base-bot/internal/application/domains"
|
||||||
|
"github.com/go-telegram/bot/models"
|
||||||
|
|
||||||
|
"git.maximotejeda.com/maximo/tgb-user/proto/golang/tgbuser"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserService interface {
|
||||||
|
Get(int64) (*domains.User, error)
|
||||||
|
Edit(*models.User) (bool, error)
|
||||||
|
Delete(int64) (bool, error)
|
||||||
|
Create(*models.User) (bool, error)
|
||||||
|
AddBot(int64, string) (bool, error)
|
||||||
|
GetBots(int64) ([]string, error)
|
||||||
|
DeleteBot(int64, string) (bool, error)
|
||||||
|
GetAllBotsUsers(string) ([]*domains.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)
|
||||||
|
}
|
||||||
20
webapp/#index.html#
Normal file
20
webapp/#index.html#
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>test-mini-app</title>
|
||||||
|
<script src="https://telegram.org/js/telegram-web-app.js?56" defer></script>
|
||||||
|
<script src="//unpkg.com/alpinejs" defer></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div x-data="{ open: false }">
|
||||||
|
<button @click="open = !open">Expand</button>
|
||||||
|
|
||||||
|
<span x-show="open">
|
||||||
|
Contenidob0...
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
20
webapp/index.html
Normal file
20
webapp/index.html
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>test-mini-app</title>
|
||||||
|
<script src="https://telegram.org/js/telegram-web-app.js?56" defer></script>
|
||||||
|
<script src="//unpkg.com/alpinejs" defer></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div x-data="{ open: false }">
|
||||||
|
<button @click="open = !open">Expand</button>
|
||||||
|
|
||||||
|
<span x-show="open">
|
||||||
|
Contenido...
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
0
webapp/static/css/#index.css#
Normal file
0
webapp/static/css/#index.css#
Normal file
0
webapp/static/css/index.css
Normal file
0
webapp/static/css/index.css
Normal file
20
webapp/static/html/index.html
Normal file
20
webapp/static/html/index.html
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>test-mini-app</title>
|
||||||
|
<script src="https://telegram.org/js/telegram-web-app.js?56" defer></script>
|
||||||
|
<script src="//unpkg.com/alpinejs" defer></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div x-data="{ open: false }">
|
||||||
|
<button @click="open = !open">Expand</button>
|
||||||
|
|
||||||
|
<span x-show="open">
|
||||||
|
Content...
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
0
webapp/static/js/index.js
Normal file
0
webapp/static/js/index.js
Normal file
0
webapp/template/index.html.template
Normal file
0
webapp/template/index.html.template
Normal file
Loading…
x
Reference in New Issue
Block a user