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