Телеграм-бот с нуля

Про телеграм написано немеренно статей и заметок, а про ботов еще больше. Вся главная документация разработчика есть на официальном сайте. Я постараюсь написать более или менее полезного бота от начала и до конца, чтобы его можно было использовать на практике. Мой пример будет представлять отправку поздравления с «Новым Годом». Приступим.

С чего начать

Для начала нам нужен действующий Телеграм-клиент. Через него нужно зарегистрировать бота. Для этого нужно воспользоваться ботом. Звучит странно, но так оно и есть. Для этого идем по адресу https://core.telegram.org/bots и читаем инструкцию. Нам нужен BotFather. Как зарегистрировать бота я рассказывать не буду, так как только ленивый не писал как это сделать. А на официальном сайте прекрасно все написано. После регистрации мы должны получить токен. Он и будет нашей авторизацией для бота.

Теперь нам нужно немного немного настроить наш проект. Я буду передавать параметры через переменные окружения, по этому, чтобы каждый раз не писать вручную я настраиваю немного проект разработки. В моем случае это Visual Studio Code. Вы же можете использовать свой любимы инструмент. Как настраивать саму IDE рассказывать не буду, а только то, что касается проекта. Создаем файл launch.json (в разделе «Запуск и отладка») и немного наполняем его:


{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Launch Bot",
            "type": "go",
            "request": "launch",
            "mode": "debug",
            "program": "${workspaceFolder}/main.go",
            "env": {
                "TBOT_API_TOKEN": "Ваш токен"
            }
        }
    ]
}

Данная конфигурация говорит о том, что запускать отладку файла main.go и передавать некоторые переменные окружения.

Теперь нужно создать настройки для самого компилятора/сборщика и т.д. В консоле с проектом вводим:


go mod init tgbot-newyear

После нам понадобится пакет для работы с Telegram:


go get github.com/go-telegram-bot-api/telegram-bot-api

Проект готово. Можно приступать к написанию кода.

Код

Для начала создадим файл 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"
	}

	prepareArguments()
}

func prepareArguments() {
	// TODO
}

Этот код получает переменные окружения и параметры командной строки и складывает их в определенную структуру. В дальнейщем будем ее дописывать.

И structs/config.go:


package structs

type Config struct {
	TBotApiToken string
	Env          string
}

Это структура для хранения настроек, чтобы можно было дергать параметры там где они нам нужны.

Теперь создадим файл srv/tgbot.go:


package srv

import (
	"log"
	"tgbot-newyear/libs"
	"tgbot-newyear/srv/handlers"
	"tgbot-newyear/tglibs"
	"tgbot-newyear/tgstructs"

	tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
)

var Bot *tgbotapi.BotAPI

func TGBot() {

	var err error
	Bot, err = tgbotapi.NewBotAPI(libs.Config.TBotApiToken)
	if err != nil {
		log.Panic(err)
	}

	if libs.Config.Env == "production" {
		Bot.Debug = false
	} else {
		Bot.Debug = true
	}

	log.Printf("Authorized on account %s", Bot.Self.UserName)

	u := tgbotapi.NewUpdate(0)
	u.Timeout = 60

	updates, err := Bot.GetUpdatesChan(u)

	if err != nil {
		panic(err)
	}

	tglibs.SetBotContext(Bot)
	tglibs.Msgs = make(chan *tgstructs.Message)
	go tglibs.TGSendChannel(tglibs.Msgs)

	for update := range updates {

		processingMessage(&update)

	}
}

func processingMessage(update *tgbotapi.Update) {

	var err error
	var message *tgbotapi.MessageConfig

	chatID := tglibs.GetChatID(update)

	message, err = handlers.Handlers(update)

	if err != nil {
		tglibs.TGSendMessage(*message)
	} else {
		tmp := tgbotapi.NewMessage(chatID, "Выберите действие")
		tglibs.TGSendMessage(tmp)
	}

}

Здесь мы создаем экземпляр бота и запускаем его. В функции TGBot я устанавливаю все настройки и запускаю цикл обработки. В данном случае поступающие сообщения приходят из канала. Если канал пуст, то цикл будет заморожен, пока не поступит новое сообщение.

После создаем файлы srv/handlers/index.go:


package handlers

import (
	tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
)

func Handlers(event *tgbotapi.Update) (msg *tgbotapi.MessageConfig, err error) {

	return

}

Это файл-заглушка, в которой пока ничего не происходит. Здесь я буду наполнять бота реакцией чтобы более логически разделить основную часть бота от обработчиков.

tgstructs/message.go:


package tgstructs

import (
	tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
)

type Message struct {
	Message *tgbotapi.MessageConfig
}

Это просто структура сообщения. В данном случае в структуру сообщения будет помещаться сообщение. Таким образом можно добавить другие типы и отправлять их пользователю. Мне понадобятся только текст, но можно отправлять файлы, картинки, видео, заметки, фото и т.д. Таким образом структуру можно расширить.

tglibs/get_chat_id.go:


package tglibs

import tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"

func GetChatID(event *tgbotapi.Update) int64 {
	if event.Message != nil {
		return event.Message.Chat.ID
	} else if event.CallbackQuery != nil {
		return event.CallbackQuery.Message.Chat.ID
	}
	return 0
}

Это просто вспомогательная функция для получения ID чата. Он нам понадобится, чтобы мы знали кому отправить сообщение.

tglibs/tgsend.go:


package tglibs

import (
	"reflect"
	"tgbot-newyear/tgstructs"
	"time"

	tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
)

var bot *tgbotapi.BotAPI

var Msgs chan *tgstructs.Message

var deferredMessages = make(map[int64]chan *tgstructs.Message)
var lastMessageTimes = make(map[int64]int64)

func SetBotContext(context *tgbotapi.BotAPI) {
	bot = context
}

func TGSendMessage(msg tgbotapi.MessageConfig) {
	var message tgstructs.Message
	message.Message = &msg
	if _, ok := deferredMessages[msg.ChatID]; !ok {
		deferredMessages[msg.ChatID] = make(chan *tgstructs.Message, 1000)
	}
	deferredMessages[msg.ChatID] <- &message
}

func TGSendChannel(msgs chan *tgstructs.Message) {
	timer := time.NewTicker(time.Second / 30)
	for range timer.C {
		cases := []reflect.SelectCase{}
		for userId, ch := range deferredMessages {
			if userCanReceiveMessage(userId) && len(ch) > 0 {
				// Формирование case
				cs := reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(ch)}
				cases = append(cases, cs)
			}
		}

		if len(cases) > 0 {
			// Достаем одно сообщение из всех каналов
			_, value, ok := reflect.Select(cases)

			if ok {
				msg := value.Interface().(*tgstructs.Message)
				// Выполняем запрос к API
				if msg == nil {
					continue
				}
				if msg.Message != nil {
					if msg.Message.ChatID == 0 {
						continue
					}
					if msg.Message != nil {
						if _, err := bot.Send(msg.Message); err != nil {
							continue
						}
						lastMessageTimes[msg.Message.ChatID] = time.Now().UnixNano()
					}
				}
			}
		}
	}
}

func userCanReceiveMessage(userId int64) bool {
	t, ok := lastMessageTimes[userId]

	return !ok || t+int64(time.Second) <= time.Now().UnixNano()
}

Достаточно сложная функция. Здесь мы получаем сообщение и кладем его в очередь. После в отдельном потоке мы эти сообщения читаем из очереди и проверяем время отправки. дело в том что в Telegram есть ограничения на количество отправок сообщений за определенное время. О лимитах можно почитать здесь. Достаточно наглядная таблица.

Вот в таком варианте можно уже запустить приложение, подключиться к боту и написать ему что-то. На все сообщения он будет отвечать одинаково:

Работа бота

И все?

Пока что да. Такое приложение можно разместить на домашнем сервере или на каком-нибудь внешнем. Далее я буду показывать как его немного оживить. А пока что можно с ним немного поиграть.

Поделиться
Вы можете оставить комментарий, или ссылку на Ваш сайт.

Оставить комментарий

Вы должны быть авторизованы, чтобы разместить комментарий.