How to build a Telegram Bot with the GOLANG (PostgreSQL)
Github: https://github.com/nikben08/todolist-telegram_bot-medium
Register your bot on Telegram
Let’s create a project
There are 3 layers in my project.
- The handlers layer is the layer that processes incoming commands.
- The services layer is the layer that implements business logic.
- The repositories layer is the layer that we use to perform operations with the database.
The picture below shows the structure of the project
- Create .env file.
First of all, we need to create .env file where we will store the environment variables like the Telegram API token, Project’s port, DBHost, etc.
TELEGRAM_APITOKEN= #your telegram api toke
PORT=83
POSTGRES_HOST=127.0.0.1
POSTGRES_USER=postgres
POSTGRES_PASSWORD=0811
POSTGRES_DATABASE_NAME=todolist
POSTGRES_PORT=5432
SSL_MODE="disable"
2. Create config file (config > config.go).
Then we need to create config.go file. In that file we need to write a function that gets the environment variable from .env file.
package config
import (
"fmt"
"os"
"github.com/joho/godotenv"
)
// Config func to get env value from key ---
func Config(key string) string {
// load .env file
err := godotenv.Load(".env")
if err != nil {
fmt.Print("Error loading .env file")
}
return os.Getenv(key)
}
3. Create a telegram client file (clients > telegram.go).
Then we need to create a Telegram client file. In that file, I wrote a function that creates a connection with the Telegram bot and returns the connection as a pointer. As a telegram library, I use go-telegram-bot-api.
package clients
import (
"log"
"telegram-todolist/config"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
)
func Init() *tgbotapi.BotAPI {
bot, err := tgbotapi.NewBotAPI(config.Config("TELEGRAM_APITOKEN"))
if err != nil {
log.Panic(err)
}
bot.Debug = true
return bot
}
4. Create a database connection file (database > database.go)
Next, we need to create database.go file. In that file, I wrote a function that creates a connection with PostgreSQL. Then that function recreates the database by dropping todolist database if it exists and creating the new one. Then the function creates a connection to the database it previously created. Then the Init function makes migration and returns the database connection as a pointer.
package database
import (
"fmt"
"log"
"telegram-todolist/config"
"telegram-todolist/models"
_ "github.com/lib/pq"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
func Init() *gorm.DB {
//Connect to database
dsn := fmt.Sprintf("host=%s user=%s password=%s port=%s, dbname=%s", config.Config("POSTGRES_HOST"), config.Config("POSTGRES_USER"), config.Config("POSTGRES_PASSWORD"), config.Config("POSTGRES_PORT"), "postgres")
DB, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatal(err)
}
if err := DB.Exec("DROP DATABASE IF EXISTS todolist;").Error; err != nil {
panic(err)
}
if err := DB.Exec("CREATE DATABASE todolist").Error; err != nil {
panic(err)
}
dsn = fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s", config.Config("POSTGRES_HOST"), config.Config("POSTGRES_USER"), config.Config("POSTGRES_PASSWORD"), config.Config("POSTGRES_DATABASE_NAME"), config.Config("POSTGRES_PORT"))
DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatal(err)
}
// Migrate tables
DB.AutoMigrate(&models.Task{})
return DB
}
5. Create model Task (models > task.go)
Next, we need to create a model that we will use to create todos. I created the model by creating a Task struct. Task struct has an ID that automatically generates. As an ID I use uuid from Google. The task also has ChatId that we will use as a user identifier and Task containing text with the todo.
package models
import (
"github.com/google/uuid"
)
type Task struct {
ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();"`
ChatId int64 `gorm:"chat_id"`
Task string `gorm:"task"`
}
6. Create handler functions (handlers > init.go, commands.go, callbacks.go, messages.go)
init.go
The Init function checks updates and determines the type of update.
package handlers
import (
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
)
func Init(bot *tgbotapi.BotAPI) {
u := tgbotapi.NewUpdate(0)
u.Timeout = 60
updates := bot.GetUpdatesChan(u)
// Loop through each update.
for update := range updates {
if update.CallbackQuery != nil {
Callbacks(bot, update)
} else if update.Message.IsCommand() {
Commands(bot, update)
} else {
Messages(bot, update)
}
}
}
commands.go
The Commands function handles commands that come from Init function.
package handlers
import (
"telegram-todolist/services"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
)
func Commands(bot *tgbotapi.BotAPI, update tgbotapi.Update) {
switch update.Message.Command() {
case "start":
services.Start(bot, update)
case "set_todo":
services.SetTask(bot, update)
case "delete_todo":
services.DeleteTask(bot, update)
case "show_all_todos":
services.ShowAllTasks(bot, update)
}
}
callbacks.go
The Сallbacks function handles callbacks. At the beginning of the function, we split update.CallbackQuery.Data to get command and taskId (todo ID).
package handlers
import (
"telegram-todolist/services"
"telegram-todolist/utils"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
)
func Callbacks(bot *tgbotapi.BotAPI, update tgbotapi.Update) {
cmd, taskId := utils.GetKeyValue(update.CallbackQuery.Data)
switch {
case cmd == "delete_task":
services.DeleteTaskCallback(bot, update, taskId)
}
}
messages.go
The messages function handles received messages. As a message we send only Todo’s text, that's why we don't need to use a case switch here.
package handlers
import (
"telegram-todolist/services"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
)
func Messages(bot *tgbotapi.BotAPI, update tgbotapi.Update) {
services.SetTaskCallback(bot, update)
}
7. Create the services layer (services > tasks.go)
In the services layer, I implement the business logic of the project. DeleteTask function creates a markup keyboard of todos so that the user can select the todo to be deleted.
package services
import (
"fmt"
"telegram-todolist/keyboards"
"telegram-todolist/repositories"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
)
func Start(bot *tgbotapi.BotAPI, update tgbotapi.Update) {
text := "Hi, here you can create todos for your todolist."
msg := tgbotapi.NewMessage(update.Message.Chat.ID, text)
msg.ReplyMarkup = keyboards.CmdKeyboard()
if _, err := bot.Send(msg); err != nil {
panic(err)
}
}
func SetTask(bot *tgbotapi.BotAPI, update tgbotapi.Update) {
text := "Please, write todo."
msg := tgbotapi.NewMessage(update.Message.Chat.ID, text)
if _, err := bot.Send(msg); err != nil {
panic(err)
}
}
func SetTaskCallback(bot *tgbotapi.BotAPI, update tgbotapi.Update) {
text := "Todo successfully created"
err := repositories.SetTask(update)
if err != nil {
text = "Couldnt set task"
}
msg := tgbotapi.NewMessage(update.Message.Chat.ID, text)
if _, err := bot.Send(msg); err != nil {
panic(err)
}
}
func DeleteTask(bot *tgbotapi.BotAPI, update tgbotapi.Update) {
data, _ := repositories.GetAllTasks(update.Message.Chat.ID)
var btns []tgbotapi.InlineKeyboardButton
for i := 0; i < len(data); i++ {
btn := tgbotapi.NewInlineKeyboardButtonData(data[i].Task, "delete_task="+data[i].ID.String())
btns = append(btns, btn)
}
var rows [][]tgbotapi.InlineKeyboardButton
for i := 0; i < len(btns); i += 2 {
if i < len(btns) && i+1 < len(btns) {
row := tgbotapi.NewInlineKeyboardRow(btns[i], btns[i+1])
rows = append(rows, row)
} else if i < len(btns) {
row := tgbotapi.NewInlineKeyboardRow(btns[i])
rows = append(rows, row)
}
}
fmt.Println(len(rows))
var keyboard = tgbotapi.InlineKeyboardMarkup{InlineKeyboard: rows}
//keyboard.InlineKeyboard = rows
text := "Please, select todo you want to delete"
msg := tgbotapi.NewMessage(update.Message.Chat.ID, text)
msg.ReplyMarkup = keyboard
if _, err := bot.Send(msg); err != nil {
panic(err)
}
}
func DeleteTaskCallback(bot *tgbotapi.BotAPI, update tgbotapi.Update, taskId string) {
text := "Task successfully deleted"
err := repositories.DeleteTask(taskId)
if err != nil {
text = "Couldnt delete task"
}
msg := tgbotapi.NewMessage(update.CallbackQuery.Message.Chat.ID, text)
if _, err := bot.Send(msg); err != nil {
panic(err)
}
}
func ShowAllTasks(bot *tgbotapi.BotAPI, update tgbotapi.Update) {
text := "Tasks: \n"
tasks, err := repositories.GetAllTasks(update.Message.Chat.ID)
if err != nil {
text = "Couldnt get tasks"
}
for i := 0; i < len(tasks); i++ {
text += tasks[i].Task + " \n"
}
msg := tgbotapi.NewMessage(update.Message.Chat.ID, text)
if _, err := bot.Send(msg); err != nil {
panic(err)
}
}
8. Create repositories layer (repositories > repository.go, tasks.go)
repository.go
In the repository file, we create a connection to the database.
package repositories
import (
"telegram-todolist/database"
"gorm.io/gorm"
)
var DB *gorm.DB = database.Init()
tasks.go
In task.go file we create functions that we use in the services layer. These functions we use to perform operations with the database.
package repositories
import (
"telegram-todolist/models"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
)
func SetTask(update tgbotapi.Update) error {
task := models.Task{
ChatId: update.Message.Chat.ID,
Task: update.Message.Text,
}
if result := DB.Create(&task); result.Error != nil {
return result.Error
}
return nil
}
func DeleteTask(taskId string) error {
if result := DB.Where("id = ?", taskId).Delete(&models.Task{}); result.Error != nil {
return result.Error
}
return nil
}
func GetAllTasks(chatId int64) ([]models.Task, error) {
var tasks []models.Task
if result := DB.Where("chat_id = ?", chatId).Find(&tasks); result.Error != nil {
return tasks, result.Error
}
return tasks, nil
}
9. Create commands keyboard file (keyboards > cmd_keyboard.go)
CmdKeyaboard creates a keyboard of commands.
package keyboards
import (
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
)
func CmdKeyboard() tgbotapi.ReplyKeyboardMarkup {
var cmdKeyboard = tgbotapi.NewReplyKeyboard(
tgbotapi.NewKeyboardButtonRow(
tgbotapi.NewKeyboardButton("/set_todo"),
),
tgbotapi.NewKeyboardButtonRow(
tgbotapi.NewKeyboardButton("/delete_todo"),
),
tgbotapi.NewKeyboardButtonRow(
tgbotapi.NewKeyboardButton("/show_all_todos"),
),
)
return cmdKeyboard
}
10. Create main.go file
Main function sets the app variable, telegram client, and calls handlers.Init() function.
package main
import (
"log"
"telegram-todolist/clients"
"telegram-todolist/config"
"telegram-todolist/handlers"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
)
func main() {
app := fiber.New()
app.Use(cors.New(cors.Config{
AllowOrigins: "*",
AllowHeaders: "*",
}))
bot := clients.Init()
handlers.Init(bot)
log.Fatal(app.Listen(":" + config.Config("PORT")))
}
11. Run “go mod init project_name” and “go mod tidy” commands in terminal. Then run “go run main.go” command.
Congrats, your bot is now live! 🔥
At Last:
If you like this article, please follow or subscribe to receive high-quality content in time. Thank you for your support ;)