Программки 💻
December 26, 2021

✈️ Telegram бот на golang (основная часть)

После реализации бота на Ruby, мне захотелось сделать ещё одного, но уже на моём любимом языке - Golang.

Когда я только начинал работу, информации о библиотеке go-telegram-bot-api у меня не было. Так что пришлось писать бота практически с нуля. В проекте на Ruby все самое сложное делал гем telegram-bot-ruby. Но в этот раз я захотел сам реализовать бота.

После потраченного вечера на поиск информации об архитектуре телеграм ботов, я занялся написанием самого проекта.

Main():

err := initConfig()    
if err != nil {    
   fmt.Println("Config error: ", err)    
   return    
}

Сначала бот инициализирует конфиг (ключи и токены) вызывая функцию initConfig(), которая выглядит следующим образом:

func initConfig() error {    
   viper.AddConfigPath("configs")    
   viper.SetConfigName("config")   
        
   return viper.ReadInConfig()   
}

Тут я использую библиотеку viper для считывания ключей и токенов из файла config.json

После инициализации конфига бот выполняет проверку переменной err, в которую в случае ошибки будет записан код ошибки (точнее не код, а сама ошибка). Если переменная err равна nil, бот продолжит работу, в противном случае в консоль выведется ошибка и main() закроется.

в языке Golang значение nil означает буквально "ничего"
 botUrl := "https://api.telegram.org/bot" + viper.GetString("token")    
 offSet := 0        
 
 for {    
    updates, err := getUpdates(botUrl, offSet)    
    if err != nil {    
       fmt.Println("Something went wrong: ", err)    
       return    
    }    
    for _, update := range updates {    
       respond(botUrl, update)    
       offSet = update.UpdateId + 1    
    }    
    fmt.Println(updates)    
}

После работы с инициализацией конфига, бот записывает в переменную botUrl ссылку, по которой он будет получать сообщения, или же слайс объектов Update. botUrl состоит из токена, полученного ранее из файла config.json и ссылки на api.telegram.org. Идентификатор offset нужен для обработки нескольких сообщений.

Далее идёт главный цикл бота, в котором происходит вообще всё. Для работы с пользователем боту необходимо получить от него сообщение, это реализовано с помощью функции getUpdates():

getUpdates()

func getUpdates(botUrl string, offset int) ([]mods.Update, error) {
     resp, err := http.Get(botUrl + "/getUpdates?offset=" + 
     strconv.Itoa(offset))    
     if err != nil {    
        return nil, err    
     }    
     defer resp.Body.Close()    
     body, err := ioutil.ReadAll(resp.Body)    
     if err != nil {    
        return nil, err    
     }    
     var restResponse mods.TelegramResponse    
     err = json.Unmarshal(body, &restResponse)    
     if err != nil {    
        return nil, err    
     }    
     return restResponse.Result, nil   
}

Построчно расписывать не буду, весь процесс в этой функции можно расписать так:

Бот отправляет запрос GET телеграму, на что получает json файл, который переводит в слайс byte'ов. Слайс байтов записывается в структуру restResponse, которая имеет только одно поле - "Result". Result является слайсом объектов Update. В конце функция вернёт поле "Result", т.е слайс обновлений.

type TelegramResponse struct {    
     Result []Update `json:"result"`   
}
type Update struct {
     UpdateId int `json:"update_id"`    
     Message Message `json:"message"`   
}

Структура Update имеет всего два поля: идентификатор обновления и объект Message:

type Message struct {
    Chat Chat `json:"chat"`
    Text string `json:"text"`
    Sticker Sticker `json:"sticker"`
}

У структуры Message есть поле Chat, которое бот использует только для работы с идентификатором чата. Поле Text отвечает за текст сообщения, а поле Sticker нужно для обработки сообщений, состоящих из стикера.

После получения слайса обновлений, бот проходится по каждому объекту Update функцией respond().

respond()

func respond(botUrl string, update mods.Update) error {
    msg := strings.ToLower(update.Message.Text)
    if msg != "" {        
       switch msg {
       ...
       }
    }

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

Если на вход пришло сообщение, функция пройдется по нему с помощью оператора switch:

switch msg {
    case "/check":
        mods.Check(botUrl, update)
        return nil    
    case "/git":
         mods.CheckGit(botUrl, update)
         return nil    
    case "/weather7":    
         mods.SendDailyWeather(botUrl, update, 7)    
         return nil
    ...
}

При каждой команде, вызывается определённая функция, после чего respond() возвращает nil, что означает, что всё прошло без ошибок.

} else {
   if update.Message.Sticker.File_id != "" {
      mods.SendRandomSticker(botUrl, update)
      return nil    
   }
   mods.SendMsg(botUrl, update, "Пока я воспринимаю только текст и стикеры 🤷🏻‍♂️")    
   return nil    
}

Если же пользователь отправил не текст, а что-то иное, бот сделает проверку на стикер. Если пользователь отправил стикер, бот тоже отправляет в ответ случайный стикер. В остальных случаях бот уведомит пользователя, что воспринимает только текст и стикеры.

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