Я уже писал про телеграм-бот с нуля. В этот раз я добавлю базу данных, в которой буду хранить сообщения и показывать случайным образом. На самом деле ничего сложного в этом нету, а базы данных не кусаются. Продолжим.
Материал
Прежде чем что-то делать нужно что-то иметь. Нам нужен текст. Не буду писать о нем много. Скажу только что сообщения предварительно нужно подготовить. Я сделал 2 файла CSV. Первый файл у меня содержит в первой колонке раздел, а во второй само сообщение. Второй файл будет содержать раздел и его отображаемое сообщение.
База данных
Для начала нужен пакет для работы с БД. Я возьму GORM. Библиотека может много чего взять на себя, а мы этим воспользуемся. Я буду использовать MariaDB так как она у меня уже есть и новый сервер я разворачивать не собираюсь (а зачем?). Устанавливаем:
go get gorm.io/gorm
go get gorm.io/driver/mysql
go get github.com/satori/go.uuid
Для работы с MariaDB мне понадобится пакет mysql. Так же я буду немного маньячить. Для этого и нужен пакет для работы с UUID. В принципе все готово.
Структура БД
Для начала создадим базовую структуру, которая будет будет использоваться во всех моделях (db/models/base.go):
package models
import (
"time"
"gorm.io/gorm"
)
type Base struct {
ID uint `gorm:"type:uuid;primary_key;"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `sql:"index"`
}
Эта структура описывает все самое необходимое для моделей. Теперь нам нужны 2е таблицы. Первая db/models/congrat_cats.go:
package models
type CongratCats struct {
Base
Name string `gorm:"column:name;type:varchar(50);"`
Display string `gorm:"column:name;type:text"`
}
Тут я перечисляю категории поздравлений. Вторая db/models/congrat_text.go:
package models
type CongratText struct {
Base
CongratCatsID uint `gorm:"column:id_congrat_cat"`
CongratCats CongratCats
txt string `gorm:"column:txt;type:text"`
}
Подготовка БД
Прежде чем работать с БД необходимо немного можифицировать код из предыдущей статьи. В файле structs/config.go добавим некоторые свойства структуры:
type Config struct {
TBotApiToken string
Env string
MigrateDB bool
MigrateDBReferences bool
DbName string
DbAddress string
DbPort string
DbLogin string
DbPassword string
}
И теперь нужно сделать получить все необходимые параметры. Для этого подправим libs/config.go:
package libs
import (
"os"
"tgbot-newyear/structs"
)
var Config structs.Config
func LoadConfig() {
var exists bool
Config.TBotApiToken = os.Getenv("TBOT_API_TOKEN")
if Config.Env, exists = os.LookupEnv("ENV"); !exists {
Config.Env = "development"
}
if Config.DbName, exists = os.LookupEnv("DB_NAME"); !exists {
Config.DbName = "bot"
}
if Config.DbAddress, exists = os.LookupEnv("DB_ADDRESS"); !exists {
Config.DbAddress = "bot"
}
if Config.DbLogin, exists = os.LookupEnv("DB_LOGIN"); !exists {
Config.DbLogin = "bot"
}
if Config.DbPassword, exists = os.LookupEnv("DB_PASSWORD"); !exists {
Config.DbPassword = "bot"
}
if Config.DbPort, exists = os.LookupEnv("DB_PORT"); !exists {
Config.DbPort = "3306"
}
prepareArguments()
}
func prepareArguments() {
args := os.Args[1:]
for i := range args {
switch args[i] {
case "migrate":
Config.MigrateDB = true
}
}
}
Так же добавим вспомогательную функцию в файл libs/end_transaction.go:
package libs
import "gorm.io/gorm"
func EndTransaction(tx *gorm.DB, err error) (errout error) {
if err != nil {
// Откат транзакции, так как получилось неудачно
errout = tx.Rollback().Error
} else {
// Ошабок нет, значит все хорошо. Подтверждаем транзакцию
tx.Commit()
errout = nil
}
return
}
И добавим в launch.json переменные окружения:
{
"name": "Launch Bot",
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${workspaceFolder}/main.go",
"env": {
"TBOT_API_TOKEN": "Ваш токен",
"DB_ADDRESS": "localhost",
"DB_NAME": "bot",
"DB_LOGIN": "bot",
"DB_PASSWORD": "bot"
}
}
Далее нам нужно написать немного кода для подключения в базе. Наполняем файл db/db.go:
package db
import (
"embed"
"log"
"os"
"time"
"tgbot-newyear/db/models"
"tgbot-newyear/libs"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
//go:embed cats.csv.gz
//go:embed congrats.csv.gz
var f embed.FS
var DB *gorm.DB
func Connect() {
dsnMySQL := libs.Config.DbLogin + ":" + libs.Config.DbPassword + "@tcp(" + libs.Config.DbAddress + ":" + libs.Config.DbPort + ")/" + libs.Config.DbName + "?charset=utf8mb4&parseTime=True&loc=Local"
var logLevel logger.LogLevel
if libs.Config.Env == "production" {
logLevel = logger.Silent
} else {
logLevel = logger.Info
}
var err error
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
logger.Config{
SlowThreshold: time.Second, // Slow SQL threshold
LogLevel: logLevel, // Log level
Colorful: false, // Disable color
},
)
newLogger.LogMode(logger.Info)
DB, err = gorm.Open(mysql.Open(dsnMySQL), &gorm.Config{
Logger: newLogger,
DisableForeignKeyConstraintWhenMigrating: true,
})
if err != nil {
panic(err)
}
// Установим пул соединений
dbSettings, err := DB.DB()
if err != nil {
panic(err)
}
dbSettings.SetMaxIdleConns(10)
dbSettings.SetMaxOpenConns(50)
dbSettings.SetConnMaxLifetime(time.Minute * 10)
if libs.Config.MigrateDB {
// Создание БД
DB.AutoMigrate(
&models.CongratCats{},
&models.CongratText{},
)
if libs.Config.MigrateDBReferences {
migrateCats()
migrateCongrats()
}
}
}
func migrateCats() {
// TODO
}
func migrateCongrats() {
// TODO
}
Здесь я настраиваю подключение к базе. Если будет передан параметр migrate, то будет создана структура базы либо ее обновление (если она уже есть). В промышленной среде это называется процесс «миграции БД». Так как у нас всего 2 таблицы, то не смысла заморачиваться с чем-то сложным. По этому в данном случае я использую простой вариант.
Теперь, чтобы все работало правильно нужно немного изменить main.go:
func main() {
libs.LoadConfig()
db.Connect()
if !libs.Config.MigrateDB {
libs.LoadConfig()
srv.TGBot()
}
}
Обновление справочников
Помимо создания структуры БД еще необходимо наполнить таблицы данными, из которых будем брать данные. В файле db/db.go я подготовил 2е функции: migrateCats и migrateCongrats. Вот их и будем использовать. И так, для начала migrateCats:
func migrateCats() {
var catsFile fs.File
var err error
catsFile, err = f.Open("cats.csv.gz")
if err != nil {
panic(err)
}
defer catsFile.Close()
var gz *gzip.Reader
gz, err = gzip.NewReader(catsFile)
if err != nil {
panic(err)
}
defer gz.Close()
reader := csv.NewReader(gz)
reader.Comma = ';'
var record []string
z := 0
fmt.Println("Update categories")
tx := DB.Begin()
defer func() {
libs.EndTransaction(tx, err)
}()
for {
record, err = reader.Read()
if err != nil {
break
}
var cat models.CongratCats
if res := tx.First(&cat, "name = ?", record[0]); res.RowsAffected == 0 {
// Создаем
cat.Name = record[0]
cat.Display = record[1]
if res := tx.Create(&cat); res.RowsAffected == 0 {
panic(res.Error)
}
} else {
// Обновляем
cat.Display = record[1]
if res := tx.Save(&cat); res.RowsAffected == 0 {
panic(res.Error.Error())
}
}
}
fmt.Printf("Finished %d records\n", z)
err = nil
}
И migrateCongrats:
func migrateCongrats() {
var catsFile fs.File
var err error
catsFile, err = f.Open("congrats.csv.gz")
if err != nil {
panic(err)
}
defer catsFile.Close()
var gz *gzip.Reader
gz, err = gzip.NewReader(catsFile)
if err != nil {
panic(err)
}
defer gz.Close()
reader := csv.NewReader(gz)
reader.Comma = ';'
var record []string
fmt.Println("Update texts")
tx := DB.Begin()
defer func() {
libs.EndTransaction(tx, err)
}()
// Очистим все поздравления. Нам они не нужны
tx.Delete(&models.CongratText{}, "1 = 1")
for {
record, err = reader.Read()
if err != nil {
break
}
var cat models.CongratCats
if res := tx.First(&cat, "name = ?", record[0]); res.RowsAffected == 0 {
continue
}
// Добавим поздравление
var text models.CongratText
text.CongratCatsID = cat.ID
text.Txt = record[1]
if res := tx.Create(&text); res.RowsAffected == 0 {
panic(res.Error)
}
}
err = nil
}
Я не стал вводить проверку существующего сообщения в таблице, так как записей не много и выполняется быстро. В академических целях этого достаточно. В реальном же приложении нужно предусмотреть именно добавление и обновление записей, так как это может быть справочная информация и при удалении и новой вставке связи могут быть смещены и потеряны. Так что подбирайте методы в соответствии с требованиями.
Поздравления
Вот теперь мы подошли к реализации результата. В файле srv/handlers/index.go пишем следующее:
package handlers
import (
"fmt"
"math/rand"
"tgbot-newyear/db"
"tgbot-newyear/db/models"
"tgbot-newyear/libs"
"tgbot-newyear/tglibs"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
)
func Handlers(event *tgbotapi.Update) (msg *tgbotapi.MessageConfig, err error) {
if event.CallbackQuery != nil {
tx := db.DB.Begin()
defer func() {
libs.EndTransaction(tx, err)
}()
var cats models.CongratCats
if res := tx.Preload("CongratText").First(&cats, "name = ?", event.CallbackQuery.Data); res.RowsAffected == 0 {
tmp := tgbotapi.NewMessage(tglibs.GetChatID(event), fmt.Sprintf("Что вы имели ввиду?"))
msg = &tmp
err = res.Error
return
}
if len(cats.CongratText) == 0 {
tmp := tgbotapi.NewMessage(tglibs.GetChatID(event), fmt.Sprintf("Извините, но я не знаю что Вам сказать..."))
msg = &tmp
return
}
num := rand.Int31n(int32(len(cats.CongratText)))
tmp := tgbotapi.NewMessage(tglibs.GetChatID(event), cats.CongratText[num].Txt)
tglibs.TGSendMessage(tmp)
}
return
}
И немного изменим srv/tgbot.go функцию processingMessage:
func processingMessage(update *tgbotapi.Update) {
var err error
var message *tgbotapi.MessageConfig
chatID := tglibs.GetChatID(update)
message, err = handlers.Handlers(update)
if err != nil {
tmp := tgbotapi.NewMessage(chatID, err.Error())
tglibs.TGSendMessage(tmp)
} else if message != nil {
tglibs.TGSendMessage(*message)
} else {
tx := db.DB.Begin()
defer func() {
libs.EndTransaction(tx, err)
}()
var cats []models.CongratCats
if res := tx.Find(&cats); res.RowsAffected == 0 {
tmp := tgbotapi.NewMessage(chatID, "Ой! Что-то случилось...")
tglibs.TGSendMessage(tmp)
} else {
tmp := tgbotapi.NewMessage(chatID, "Выберите действие")
var buttons [][]tgbotapi.InlineKeyboardButton
for i := range cats {
b := tgbotapi.NewInlineKeyboardButtonData(cats[i].Display, cats[i].Name)
row := tgbotapi.NewInlineKeyboardRow(b)
buttons = append(buttons, row)
if len(buttons) == 100 {
var kb tgbotapi.InlineKeyboardMarkup
kb.InlineKeyboard = buttons
message.ReplyMarkup = kb
tglibs.TGSendMessage(tmp)
tmp = tgbotapi.NewMessage(chatID, "Продолжение")
buttons = [][]tgbotapi.InlineKeyboardButton{}
}
}
var kb tgbotapi.InlineKeyboardMarkup
kb.InlineKeyboard = buttons
tmp.ReplyMarkup = kb
tglibs.TGSendMessage(tmp)
}
}
}
Теперь можно запускать.
В заключении
Бот готов и им можно пользоваться. Мой результат получился вот таким.