Про телеграм написано немеренно статей и заметок, а про ботов еще больше. Вся главная документация разработчика есть на официальном сайте. Я постараюсь написать более или менее полезного бота от начала и до конца, чтобы его можно было использовать на практике. Мой пример будет представлять отправку поздравления с «Новым Годом». Приступим.
С чего начать
Для начала нам нужен действующий Телеграм-клиент. Через него нужно зарегистрировать бота. Для этого нужно воспользоваться ботом. Звучит странно, но так оно и есть. Для этого идем по адресу 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 есть ограничения на количество отправок сообщений за определенное время. О лимитах можно почитать здесь. Достаточно наглядная таблица.
Вот в таком варианте можно уже запустить приложение, подключиться к боту и написать ему что-то. На все сообщения он будет отвечать одинаково:
И все?
Пока что да. Такое приложение можно разместить на домашнем сервере или на каком-нибудь внешнем. Далее я буду показывать как его немного оживить. А пока что можно с ним немного поиграть.