Skip to content

Commit f595b03

Browse files
committed
Feature. Add bot app structure and implement simple echo bot
1 parent 0c21a36 commit f595b03

File tree

132 files changed

+33070
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

132 files changed

+33070
-0
lines changed

.env.example

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Telegram settings
2+
TELEGRAM_API_TOKEN=
3+
WEBHOOK_URL=https://europe-west3-samplebot.cloudfunctions.net/BotUpdate
4+
5+
# Google Cloud settings
6+
GCP_PROJECT=
7+
GOOGLE_APPLICATION_CREDENTIALS=/path/to/credentials/gcloud_cred.json

.gcloudignore

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Use Go modules
2+
vendor
3+
4+
# Also ignore Git directories. Delete the following two lines if you want to
5+
# upload them.
6+
.git
7+
.gitignore
8+
9+
# Other directories and files
10+
bin
11+
scripts
12+
tools
13+
LICENSE
14+
Makefile
15+
README.md
16+
.env
17+
gcloud_cred.json

.gitignore

+8
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,11 @@
1313

1414
# Dependency directories (remove the comment below to include it)
1515
# vendor/
16+
17+
# IDE settings
18+
.idea
19+
.vscode
20+
21+
# Local settings
22+
.env
23+
gcloud_cred.json

.golangci.yml

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
run:
2+
skip-dirs:
3+
- scripts
4+
5+
linters:
6+
disable-all: true
7+
enable:
8+
- deadcode
9+
- errcheck
10+
- gosimple
11+
- govet
12+
- ineffassign
13+
- staticcheck
14+
- structcheck
15+
- typecheck
16+
- unused
17+
- varcheck
18+
- bodyclose
19+
- dogsled
20+
- errorlint
21+
- exportloopref
22+
- funlen
23+
- gocognit
24+
- goconst
25+
- gocritic
26+
- gocyclo
27+
- gofmt
28+
- goimports
29+
- gosec
30+
- interfacer
31+
- lll
32+
- nakedret
33+
- nestif
34+
- nlreturn
35+
- noctx
36+
- nolintlint
37+
- rowserrcheck
38+
- unconvert
39+
- unparam
40+
- whitespace
41+
- wrapcheck
42+
- wsl
43+
44+
linters-settings:
45+
funlen:
46+
lines: 120
47+
statements: 40
48+
gocognit:
49+
min-complexity: 10
50+
nestif:
51+
min-complexity: 4
52+
gocyclo:
53+
min-complexity: 10

Makefile

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
BINPATH = $(PWD)/bin
2+
LOCAL = $(MODULE)
3+
MODULE = $(shell $(GO) list -m)
4+
PATHS = $(shell $(GO) list ./... 2> /dev/null | sed -e "s|$(MODULE)/\{0,1\}||g")
5+
SHELL = /bin/bash -euo pipefail
6+
7+
export PATH := $(BINPATH):$(PATH)
8+
9+
.PHONY: tools
10+
tools:
11+
cd tools && go mod tidy && go mod verify && go generate tools.go
12+
13+
.PHONY: go-deps
14+
go-deps:
15+
go mod tidy && go mod vendor && go mod verify
16+
17+
.PHONY: deps
18+
deps: tools go-deps
19+
20+
.PHONY: generate
21+
generate: go-generate
22+
23+
.PHONY: go-gen
24+
go-gen:
25+
go generate ./...
26+
27+
.PHONY: lint
28+
lint:
29+
golangci-lint run ./...
30+
31+
.PHONY: test
32+
test:
33+
go test -race ./... -coverprofile=coverage.out && go tool cover -func=coverage.out && echo "Ok"
34+
35+
.PHONY: deploy
36+
deploy:
37+
gcloud functions deploy BotUpdate --runtime go113 --trigger-http --timeout 300s --allow-unauthenticated --region europe-west3
38+
39+
.PHONY: run
40+
run:
41+
go run cmd/bot/main.go

README.md

+6
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,8 @@
11
# tg-bot-template
22
Template repo with boilerplate code to write Telegram bots in Go
3+
4+
## TODO
5+
- [x] Sample handlers (echo bot)
6+
- [x] Bootstrapping code in app/boot
7+
- [x] Entrypoints: local and Google Cloud Function
8+
- [ ] Project description

bin/.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
*
2+
!.gitignore

cmd/bot/main.go

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"errors"
6+
"os"
7+
"os/signal"
8+
"syscall"
9+
10+
"github.com/joho/godotenv"
11+
"golang.org/x/sync/errgroup"
12+
13+
"github.com/nskondratev/tg-bot-template/internal/boot"
14+
"github.com/nskondratev/tg-bot-template/internal/env"
15+
"github.com/nskondratev/tg-bot-template/internal/logger"
16+
)
17+
18+
func main() {
19+
_ = godotenv.Load()
20+
21+
log := logger.Must(env.String("LOG_LEVEL", "debug"), os.Stdout)
22+
23+
ctx, cancel := context.WithCancel(context.Background())
24+
25+
b, err := boot.InitBot(ctx, log)
26+
if err != nil {
27+
log.Fatal().Err(err).Msg("failed to init bot")
28+
}
29+
30+
g, ctx := errgroup.WithContext(ctx)
31+
32+
// Wait for interruption
33+
g.Go(func() error {
34+
ic := make(chan os.Signal, 1)
35+
signal.Notify(ic, os.Interrupt, syscall.SIGTERM)
36+
<-ic
37+
log.Info().Msg("application is interrupted. Stopping appCtx...")
38+
cancel()
39+
40+
return ctx.Err()
41+
})
42+
43+
// Poll bot updates
44+
g.Go(func() error {
45+
log.Info().Msg("Starting polling updates...")
46+
47+
return b.PollUpdates(ctx)
48+
})
49+
50+
err = g.Wait()
51+
if err != nil && !errors.Is(err, context.Canceled) {
52+
log.Fatal().Err(err).Msg("errgroup finished with error")
53+
}
54+
55+
log.Info().Msg("exit from app")
56+
}

function.go

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package gcp
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"os"
7+
8+
"github.com/rs/zerolog"
9+
10+
"github.com/nskondratev/tg-bot-template/internal/app/bot"
11+
"github.com/nskondratev/tg-bot-template/internal/boot"
12+
"github.com/nskondratev/tg-bot-template/internal/env"
13+
"github.com/nskondratev/tg-bot-template/internal/logger"
14+
)
15+
16+
var (
17+
b *bot.Bot
18+
log zerolog.Logger
19+
)
20+
21+
func init() {
22+
var err error
23+
24+
log = logger.Must(env.String("LOG_LEVEL", "debug"), os.Stdout)
25+
26+
b, err = boot.InitBot(context.Background(), log)
27+
if err != nil {
28+
panic("failed to init bot: " + err.Error())
29+
}
30+
}
31+
32+
func BotUpdate(w http.ResponseWriter, r *http.Request) {
33+
log.Info().Msg("New update")
34+
35+
up, err := b.UpdateFromRequest(r)
36+
if err != nil {
37+
log.Err(err).Msg("failed to get update from request")
38+
http.Error(w, "bad input", http.StatusBadRequest)
39+
40+
return
41+
}
42+
43+
b.HandleUpdate(r.Context(), up)
44+
w.WriteHeader(http.StatusOK)
45+
}

go.mod

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module github.com/nskondratev/tg-bot-template
2+
3+
go 1.13
4+
5+
require (
6+
github.com/go-telegram-bot-api/telegram-bot-api v1.0.1-0.20201107014523-54104a08f947
7+
github.com/joho/godotenv v1.3.0
8+
github.com/rs/zerolog v1.20.0
9+
github.com/stretchr/testify v1.6.1
10+
golang.org/x/sync v0.0.0-20190423024810-112230192c58
11+
)

internal/app/bot/bot.go

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package bot
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"net/http"
8+
9+
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
10+
)
11+
12+
type Bot struct {
13+
tg *tgbotapi.BotAPI
14+
handler Handler
15+
}
16+
17+
func New(apiToken string, handler Handler) (*Bot, error) {
18+
if apiToken == "" {
19+
return nil, errors.New("[app_bot] api token must be provided")
20+
}
21+
22+
if handler == nil {
23+
return nil, errors.New("[app_bot] handler must be provided")
24+
}
25+
26+
tg, err := tgbotapi.NewBotAPI(apiToken)
27+
if err != nil {
28+
return nil, errors.New("[app_bot] failed to create telegram bot instance")
29+
}
30+
31+
return &Bot{tg: tg, handler: handler}, nil
32+
}
33+
34+
func Must(apiToken string, handler Handler) *Bot {
35+
b, err := New(apiToken, handler)
36+
if err != nil {
37+
panic(err)
38+
}
39+
40+
return b
41+
}
42+
43+
func (b *Bot) PollUpdates(ctx context.Context) error {
44+
if b.handler == nil {
45+
panic("handler must be set before running updater")
46+
}
47+
48+
updateConfig := tgbotapi.NewUpdate(0)
49+
updateConfig.Timeout = 60
50+
51+
updates, err := b.tg.GetUpdatesChan(updateConfig)
52+
if err != nil {
53+
return fmt.Errorf("failed to get updates channel: %w", err)
54+
}
55+
56+
for {
57+
select {
58+
case <-ctx.Done():
59+
return ctx.Err()
60+
case update, ok := <-updates:
61+
if !ok {
62+
return nil
63+
}
64+
65+
b.handler.Handle(ctx, b, &update)
66+
}
67+
}
68+
}
69+
70+
func (b *Bot) UpdateFromRequest(r *http.Request) (*tgbotapi.Update, error) {
71+
return b.tg.HandleUpdate(r)
72+
}
73+
74+
func (b *Bot) HandleUpdate(ctx context.Context, update *tgbotapi.Update) {
75+
if update == nil {
76+
return
77+
}
78+
79+
b.handler.Handle(ctx, b, update)
80+
}
81+
82+
func (b *Bot) Send(c tgbotapi.Chattable) (tgbotapi.Message, error) {
83+
return b.tg.Send(c)
84+
}
85+
86+
func (b *Bot) GetFileDirectURL(fileID string) (string, error) {
87+
return b.tg.GetFileDirectURL(fileID)
88+
}

0 commit comments

Comments
 (0)