FIRST COMMIT In the beginning there was darkness

This commit is contained in:
maximo tejeda 2025-02-08 14:01:05 -04:00
commit 50bcb37513
35 changed files with 1703 additions and 0 deletions

1
.dockerignore Normal file
View File

@ -0,0 +1 @@
.env

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
.env
k8s/deployment.yml
bin/
dolardb/
/icon.png

21
Dockerfile Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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=

View File

@ -0,0 +1 @@
package db

View File

@ -0,0 +1 @@
package grpc

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

View File

@ -0,0 +1 @@
package nats

View 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,
})
}

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

View File

@ -0,0 +1 @@
package domains

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

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

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

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

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

View 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,
})
}

View 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")
}

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

View 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
View File

@ -0,0 +1 @@
package ports

27
internal/ports/user.go Normal file
View 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
View 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
View 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>

View File

View File

View 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>

View File

View File