From 077437d33c35ffae1a9f914ee9c179c06dea9ff0 Mon Sep 17 00:00:00 2001 From: user-penguin Date: Tue, 12 Nov 2024 02:32:06 +0700 Subject: [PATCH] init app --- .env.example | 2 + .gitignore | 4 + .vscode/launch.json | 11 +++ Makefile | 53 +++++++++++++ README.md | 22 ++++++ build/ci/.drone.yml | 79 +++++++++++++++++++ build/ci/Dockerfile | 4 + build/ci/docker-compose.yml | 15 ++++ cmd/smm_tg/main.go | 40 ++++++++++ go.mod | 15 ++++ go.sum | 12 +++ internal/modules/entities/user.go | 6 ++ internal/modules/messenger/interface.go | 16 ++++ .../modules/messenger/telegram/telegram.go | 64 +++++++++++++++ internal/services/bot/bot_ping/bot.go | 35 ++++++++ internal/services/bot/interface.go | 11 +++ internal/services/listener/listener.go | 31 ++++++++ migrations/20241111180238_add_tg_user.sql | 12 +++ 18 files changed, 432 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 .vscode/launch.json create mode 100644 Makefile create mode 100644 README.md create mode 100644 build/ci/.drone.yml create mode 100644 build/ci/Dockerfile create mode 100644 build/ci/docker-compose.yml create mode 100644 cmd/smm_tg/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/modules/entities/user.go create mode 100644 internal/modules/messenger/interface.go create mode 100644 internal/modules/messenger/telegram/telegram.go create mode 100644 internal/services/bot/bot_ping/bot.go create mode 100644 internal/services/bot/interface.go create mode 100644 internal/services/listener/listener.go create mode 100644 migrations/20241111180238_add_tg_user.sql diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..069c418 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +BOT_TOKEN=PLEASE_ADD_YOU_TOKEN_TO_DOT_ENV_FILE +POSTGRES_URL_CONNECT=postgresql://user:password@hos:port/db?sslmode=disable \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a80c21f --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.env +/Dockerfile +/docker-compose.yml +__debug* \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..e697f64 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,11 @@ +{ + "configurations": [ + { + "name": "Launch smm-tg", + "type": "go", + "request": "launch", + "mode": "debug", + "program": "${workspaceFolder}/cmd/smm_tg" + } + ] +} \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2803521 --- /dev/null +++ b/Makefile @@ -0,0 +1,53 @@ +LOCAL_BIN := $(CURDIR)/bin +PATH := $(LOCAL_BIN):$(PATH) +GO_IMAGE_VER := 1.23.3 + +ifeq (local-migrations-create,$(firstword $(MAKECMDGOALS))) + migrationName := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS)) + $(eval $(migrationName):;@:) +endif + +local-migrate-up: + @goose -allow-missing -dir ./migrations postgres "user=crab dbname=smm-core host=localhost port=5432 sslmode=disable" up + +local-migrations-create: + @goose -dir ./migrations postgres "user=crab dbname=smm-core host=localhost port=5432 sslmode=disable" create "${migrationName}" sql + + +local-init-arm: local-init-common + @if [ ! -f Dockerfile ]; then \ + echo "Creating Dockerfile file..."; \ + echo "FROM golang:1.23.3-alpine3.19 as builder" >> Dockerfile; \ + echo "WORKDIR /app" >> Dockerfile; \ + echo "COPY . ." >> Dockerfile; \ + echo "ENV GOOS=linux" >> Dockerfile; \ + echo "ENV GOARCH=arm" >> Dockerfile; \ + echo "RUN go build -o smm_tg cmd/smm_tg/main.go" >> Dockerfile; \ + echo "FROM alpine:3.19" >> Dockerfile; \ + echo "COPY --from=builder /app/smm_tg /usr/local/bin/smm_tg" >> Dockerfile; \ + echo "RUN chmod +x /usr/local/bin/smm_tg" >> Dockerfile; \ + echo "CMD [\"smm_tg\"]" >> Dockerfile; \ + echo "Dockerfile file created"; \ + else \ + echo "Dockerfile file already exists"; \ + fi + +local-init-amd64: local-init-common + +local-init-common: + @if [ ! -f .env ]; then \ + echo "Creating .env file..."; \ + echo "BOT_TOKEN=" >> .env; \ + echo "POSTGRES_URL_CONNECT=" >> .env; \ + echo ".env file created"; \ + else \ + echo ".env file already exists"; \ + fi + + @if [ ! -f docker-compose.yml ]; then \ + echo "Creating docker-compose.yml file..."; \ + cp build/ci/docker-compose.yml .; \ + echo "docker-compose.yml file created"; \ + else \ + echo "docker-compose.yml file already exists"; \ + fi \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..be0fa97 --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# Save My Money Telegram Bot +телеграм бот сервиса-помощника, создающегося для контроля за расходами и накоплениями + +```yaml +- кто мы? +- работяги +- чего мы хотим? +- меньше тратить денег на хуйню! +``` + +# Для пользователей +1. Найти бота __@samymoney_bot__ +2. Начать контролировать денежные потоки и дыхание вселенной + +# Для девелоперов +1. Выполнить +```bash +make local-init +``` +2. Создать себе тестового бота и положить его токен в __.env__ под ключ __BOT_TOKEN__ +3. Писать только хороший код +4. Плохой код не писать \ No newline at end of file diff --git a/build/ci/.drone.yml b/build/ci/.drone.yml new file mode 100644 index 0000000..0b3cc87 --- /dev/null +++ b/build/ci/.drone.yml @@ -0,0 +1,79 @@ +kind: pipeline +type: docker +name: default + +steps: + - name: build + pull: if-not-exists + image: golang:1.23 + environment: + TELEGRAM_TOKEN: + from_secret: bot_token + POSTGRES_URL_CONNECT: + from_secret: postgres_url_connect + settings: + envs: [ TELEGRAM_TOKEN,POSTGRES_URL_CONNECT ] + commands: + - CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o smm_tg cmd/smm_tg/main.go + - touch .env + - echo TELEGRAM_TOKEN=$${TELEGRAM_TOKEN} >> .env + - echo POSTGRES_URL_CONNECT=$${POSTGRES_URL_CONNECT} >> .env + + - name: test + pull: if-not-exists + image: golang:1.23 + commands: + - go test ./... + + - name: copy to server + pull: if-not-exists + image: appleboy/drone-scp + settings: + host: + from_secret: ssh_ip + username: + from_secret: ssh_user + key: + from_secret: ssh_key + port: + from_secret: ssh_port + target: /home/crab/deploys/butler_bot + source: + - .env + - build/ci/Dockerfile + - deploy/docker-compose.yml + - migrations + - smm_tg + rm: true + + - name: deploy + pull: if-not-exists + image: appleboy/drone-ssh + environment: + DB_PASS: + from_secret: db_pass + settings: + host: + from_secret: ssh_ip + username: + from_secret: ssh_user + key: + from_secret: ssh_key + port: + from_secret: ssh_port + envs: [ DB_PASS ] + script: + - cd /home/crab/deploys/smm_tg + - cp /etc/ssl/certs/ca-certificates.crt . + - mv build/ci/Dockerfile . + - mv deploy/docker-compose.yml . + - rm -r build deploy + - export PATH=$PATH:/usr/local/go/bin:/$HOME/go/bin + - goose -allow-missing -dir ./migrations postgres "user=crab password=$${DB_PASS} dbname=smm_tg host=localhost port=5432 sslmode=disable" up + - docker-compose up -d --force-recreate + +trigger: + branch: + - master + event: + - push diff --git a/build/ci/Dockerfile b/build/ci/Dockerfile new file mode 100644 index 0000000..bef68bc --- /dev/null +++ b/build/ci/Dockerfile @@ -0,0 +1,4 @@ +FROM golang:1.23.3-alpine3.19 +COPY smm_tg /usr/local/bin/smm_tg +RUN chmod +x /usr/local/bin/smm_tg +CMD ["smm_tg"] \ No newline at end of file diff --git a/build/ci/docker-compose.yml b/build/ci/docker-compose.yml new file mode 100644 index 0000000..af79d95 --- /dev/null +++ b/build/ci/docker-compose.yml @@ -0,0 +1,15 @@ +version: '3.8' + +services: + smm_tg: + build: + context: . + dockerfile: Dockerfile + ports: + - "8185:8090" + networks: + - common-network + +networks: + common-network: + external: true \ No newline at end of file diff --git a/cmd/smm_tg/main.go b/cmd/smm_tg/main.go new file mode 100644 index 0000000..fbf4f10 --- /dev/null +++ b/cmd/smm_tg/main.go @@ -0,0 +1,40 @@ +package main + +import ( + "context" + "os" + + "git.3crabs.ru/save_my_money/smm_tg/internal/modules/messenger/telegram" + "git.3crabs.ru/save_my_money/smm_tg/internal/services/bot" + "git.3crabs.ru/save_my_money/smm_tg/internal/services/bot/bot_ping" + "git.3crabs.ru/save_my_money/smm_tg/internal/services/listener" + "github.com/joho/godotenv" +) + +func main() { + // try get sysenv + telegarmToken := os.Getenv("BOT_TOKEN") + if len(telegarmToken) == 0 { + if err := godotenv.Load("../../.env"); err != nil { + panic(err) + } + telegarmToken = os.Getenv("BOT_TOKEN") + } + if len(telegarmToken) == 0 { + panic("empty bot token") + } + + messengerTelegram, err := telegram.NewMessengerTelegram(telegarmToken) + if err != nil { + panic(err) + } + + listenerService := listener.ListenerService{ + Messenger: messengerTelegram, + Bots: []bot.IBot{ + bot_ping.NewBotPing(messengerTelegram), + }, + } + ctx := context.Background() + listenerService.Run(ctx) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..5696cd1 --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module git.3crabs.ru/save_my_money/smm_tg + +go 1.23.3 + +require ( + github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 + github.com/joho/godotenv v1.5.1 + github.com/lib/pq v1.10.9 + github.com/samber/lo v1.47.0 +) + +require ( + go.uber.org/mock v0.5.0 // indirect + golang.org/x/text v0.16.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..8ecf39c --- /dev/null +++ b/go.sum @@ -0,0 +1,12 @@ +github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc= +github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= +github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= diff --git a/internal/modules/entities/user.go b/internal/modules/entities/user.go new file mode 100644 index 0000000..b882330 --- /dev/null +++ b/internal/modules/entities/user.go @@ -0,0 +1,6 @@ +package entities + +type User struct { + ChatID string `sql:"chat_id"` + UserID string `sql:"user_id"` +} \ No newline at end of file diff --git a/internal/modules/messenger/interface.go b/internal/modules/messenger/interface.go new file mode 100644 index 0000000..5bb7915 --- /dev/null +++ b/internal/modules/messenger/interface.go @@ -0,0 +1,16 @@ +package messenger + +//go:generate mockgen -source=$GOFILE -destination=mocks/$GOFILE -package=mocks + +import "context" + +type Message struct { + ChatID string + UserID string + Text string +} + +type IMessenger interface { + GetMessage(ctx context.Context) (*Message, error) + SendMessage(ctx context.Context, msg *Message) error +} diff --git a/internal/modules/messenger/telegram/telegram.go b/internal/modules/messenger/telegram/telegram.go new file mode 100644 index 0000000..120d161 --- /dev/null +++ b/internal/modules/messenger/telegram/telegram.go @@ -0,0 +1,64 @@ +package telegram + +import ( + "context" + "fmt" + "log" + "strconv" + + "git.3crabs.ru/save_my_money/smm_tg/internal/modules/messenger" + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" +) + +type messengerTelegram struct { + bot *tgbotapi.BotAPI + updates tgbotapi.UpdatesChannel +} + +func NewMessengerTelegram(token string) (messenger.IMessenger, error) { + bot, err := tgbotapi.NewBotAPI(token) + if err != nil { + return nil, err + } + log.Printf("Authorized on account %s", bot.Self.UserName) + u := tgbotapi.NewUpdate(0) + u.Timeout = 60 + + updates := bot.GetUpdatesChan(u) + return &messengerTelegram{ + bot: bot, + updates: updates, + }, nil +} + +func (m *messengerTelegram) GetMessage(ctx context.Context) (*messenger.Message, error) { + tgMsg := <-m.updates + log.Printf("get grom tg: %+v", tgMsg) + chatID := int64(0) + if tgMsg.Message != nil && tgMsg.Message.Chat != nil { + chatID = tgMsg.Message.Chat.ID + } + if chatID == 0 { + return nil, fmt.Errorf("get chat id error %+v", tgMsg) + } + msg := &messenger.Message{ + ChatID: fmt.Sprintf("%d", chatID), + UserID: tgMsg.Message.From.UserName, + Text: tgMsg.Message.Text, + } + log.Println("get:", msg) + return msg, nil +} + +func (m *messengerTelegram) SendMessage(ctx context.Context, msg *messenger.Message) error { + chatID, err := strconv.ParseInt(msg.ChatID, 10, 64) + if err != nil { + return err + } + tgMsg := tgbotapi.NewMessage(chatID, msg.Text) + if _, err = m.bot.Send(tgMsg); err != nil { + return err + } + log.Println("send:", msg) + return nil +} diff --git a/internal/services/bot/bot_ping/bot.go b/internal/services/bot/bot_ping/bot.go new file mode 100644 index 0000000..aba66b6 --- /dev/null +++ b/internal/services/bot/bot_ping/bot.go @@ -0,0 +1,35 @@ +package bot_ping + +import ( + "context" + "strings" + + "git.3crabs.ru/save_my_money/smm_tg/internal/modules/messenger" + "git.3crabs.ru/save_my_money/smm_tg/internal/services/bot" +) + +type botPing struct { + messenger messenger.IMessenger +} + +func NewBotPing( + messenger messenger.IMessenger, +) bot.IBot { + return &botPing{ + messenger: messenger, + } +} + +func (bot *botPing) Process(ctx context.Context, msg *messenger.Message) error { + if !strings.Contains(msg.Text, "/ping") { + return nil + } + bot.messenger.SendMessage( + ctx, + &messenger.Message{ + ChatID: msg.ChatID, + Text: "pong", + }, + ) + return nil +} diff --git a/internal/services/bot/interface.go b/internal/services/bot/interface.go new file mode 100644 index 0000000..944dfaf --- /dev/null +++ b/internal/services/bot/interface.go @@ -0,0 +1,11 @@ +package bot + +import ( + "context" + + "git.3crabs.ru/save_my_money/smm_tg/internal/modules/messenger" +) + +type IBot interface { + Process(ctx context.Context, msg *messenger.Message) error +} diff --git a/internal/services/listener/listener.go b/internal/services/listener/listener.go new file mode 100644 index 0000000..963a666 --- /dev/null +++ b/internal/services/listener/listener.go @@ -0,0 +1,31 @@ +package listener + +import ( + "context" + "log" + + "git.3crabs.ru/save_my_money/smm_tg/internal/modules/messenger" + "git.3crabs.ru/save_my_money/smm_tg/internal/services/bot" +) + +type ListenerService struct { + Messenger messenger.IMessenger + Bots []bot.IBot +} + +func (s *ListenerService) Run(ctx context.Context) error { + for { + msg, err := s.Messenger.GetMessage(ctx) + if err != nil { + if err == context.Canceled { + return nil + } + return err + } + for _, b := range s.Bots { + if err := b.Process(ctx, msg); err != nil { + log.Println(err) + } + } + } +} diff --git a/migrations/20241111180238_add_tg_user.sql b/migrations/20241111180238_add_tg_user.sql new file mode 100644 index 0000000..2c9c2a4 --- /dev/null +++ b/migrations/20241111180238_add_tg_user.sql @@ -0,0 +1,12 @@ +-- +goose Up +-- +goose StatementBegin +CREATE TABLE IF NOT EXISTS "user" ( + id SERIAL PRIMARY KEY, + chat_id TEXT NOT NULL +); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE IF EXISTS "user"; +-- +goose StatementEnd