How to build a Telegram Bot with the GOLANG (PostgreSQL)

Nikolay Benlioglu
7 min readMay 1, 2023

--

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.

  1. The handlers layer is the layer that processes incoming commands.
  2. The services layer is the layer that implements business logic.
  3. The repositories layer is the layer that we use to perform operations with the database.

The picture below shows the structure of the project

  1. 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 ;)

--

--