golang

Шаблон телеграмм бота на go

  • суббота, 17 февраля 2024 г. в 00:00:17
https://habr.com/ru/articles/794098/

Добрый день, сегодня я поделюсь с вами, на мой взгляд, довольно удачным шаблоном для телеграмм ботов на go

В нем заложена большая часть популярных сценариев работы и его расширение не должно вызывать проблем.

Функционал шаблона

  • Выполнение запросов в Горутинах

  • Выполнение команд из консоли

  • Хранение контекста во время выполнения

  • Вывод клавиатур и сообщений с локализацией

  • Возвращение к предыдущему меню

Для тех кому не терпеться взглянуть добро пожаловать в GitHub.

Заранее прошу меня простить если покорёжит от стиля кода, шаблон я составил на 2й день изучения языка и ещё не успел впитать в себя его дух.


Шаблон основан на идеи единой архитектуры системы обработки сообщений, где функционал можно добавлять виде модулей.

Все возможные действия которые может выполнять бот заключены в одну абстрактную функцию Run (Run go ! Run !).

Для хранения контекста запуска этих функций создана структура CallStack

type Run func(CallStack) CallStack
type CallStack struct {
	ChatID  int64
	Bot     *tgBotAPI.BotAPI
	Update  *tgBotAPI.Update
	Action  Run
	IsPrint bool
	Parent  *CallStack
	Data    string
}

var userRuns = map[int64]CallStack{}

stack := userRuns[ID]
  if stack.Action != nil {
      stack.Update = &update
      userRuns[ID] = userRuns[ID].Action(stack)
  } else {
      if update.Message != nil {
          userRuns[ID] = RunTemplate(CallStack{
              ChatID:  ID,
              Bot:     bot,
              Update:  &update,
              IsPrint: true,
          })
      }
  }

В BotLoop реализован готовый цикл обработки сообщений.

Для разделения стеков контекстов выполнения используется map, где ключ это ID чата, но Вы можете выбрать свой, например логин пользователя.

Если в хранилище есть готовый стек контекста, передаем ему обновление и запускаем Run, который вернёт контекст для следующей обработки.

Если действие не задано возвращаем пользователя в начальную точку.

RunTemplate это шаблон реализации Run, он заключает в себе сразу 3 вещи:

  • Вывод интереса для пользователя.

  • Обработка сообщений.

  • Инициализация нового контекста.

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

Далее повествование уходит в код:

Это общий шаблон для всех Run
func RunTemplate(stack CallStack) CallStack {
    Когда Run получает свой контекст он выставляет в нём себя в качетве
    выполняемого действия.
    Далее практически всё происходит на его основе контекста
	stack.Action = RunTemplate
    
    Информация о пользователе
	data := userDatas[stack.ChatID]

	if stack.IsPrint {
        Если нужно что-то передать пользователю пишем это тут
		
        Если передачу нужно повторять при повсторном заходе в Run 
        Уберите это снятие флага, но обработку сообщений тогда нужно будет вынести из else
        stack.IsPrint = false
        
		Тут пример вывода локализованного форматированого сообщения с прикреплёнными кнопками 
		Не пишите текст прямо в Run, заносите шаблоны для вывода в MessageTemplates
        msg := tgBotAPI.NewMessage(stack.ChatID, fmt.Sprintf(SelectTemplate("RunTemplate", data.languageСode),
			data.firstName,
		))
		mainMenuInlineKeyboard := tgBotAPI.NewInlineKeyboardMarkup(
			tgBotAPI.NewInlineKeyboardRow(
				tgBotAPI.NewInlineKeyboardButtonData(SelectTemplate("back", data.languageСode), "back"),
			),
		)
		msg.ReplyMarkup = mainMenuInlineKeyboard
		_, _ = stack.Bot.Send(msg)

        Тут отчищается прошлый набор кнопок
        Также можно задать свой набор для отображания
		mainMenuKeyboard := tgBotAPI.NewRemoveKeyboard(true)
		msg = tgBotAPI.NewMessage(stack.ChatID, "")
		msg.ReplyMarkup = mainMenuKeyboard
		_, _ = stack.Bot.Send(msg)

        И так Run подготовил интерфейс и возвратил контекст выполнения для его сохранения
        Исходя из этого, следующеt сообщение пользователя будет обработанно в этом же Run
		return stack
	} else {
        И так, пользователь ввел сообщение, вызвал комманду или другим образом попытался сломать бота )
		
        Сообщения
        if stack.Update.Message != nil {
			switch stack.Update.Message.Text {
			case "back":
				{
					return ReturnOnParent(stack)
				}
			}
		}
      
        Кнопки с inline
		if stack.Update != nil {
			// Processing a message
			if stack.Update.CallbackQuery != nil {
				switch stack.Update.CallbackQuery.Data {
				case "back":
					{
						stack.Data = stack.Update.CallbackQuery.Data
						return ReturnOnParent(stack)
					}
				}
			}
		}
      
        Комманды
        if update.Message.IsCommand() {
          switch update.Message.Command() {
          case "back":
              {
                    return ReturnOnParent(stack)
              }
          }
        }
        и т.д.
	}
    
	Если произошло что-то не предвиденное и код пришел сюда
    можно просто отправить этот же контекст для следующей обработки 
    return stack

    Или применить Затычку
    return Chop(stack)
}


func Chop(stack CallStack) CallStack {
    Затычка выведет сообшение ввиде картинки, которая сообщает что
    пользователь достиг не проработанной конечной точки
	photo := tgBotAPI.NewPhoto(stack.ChatID, chopFile)
	photo.ReplyMarkup = tgBotAPI.NewRemoveKeyboard(true)
	_, _ = stack.Bot.Send(photo)
    И попытается вернуть пользователя назад
	return ReturnOnParent(stack)
}

func ReturnOnParent(stack CallStack) CallStack {
	if stack.Parent != nil {
		stack.Parent.IsPrint = true
		return stack.Parent.Action(*stack.Parent)
	}
	return RunTemplate(CallStack{
		IsPrint: true,
		ChatID:  stack.ChatID,
		Bot:     stack.Bot,
	})
}

Это достаточно гибкая архитектура, изменяя Run можно добиться множество интересных эффектов.

  • Меню ввода с возвращением информации предыдущему Run.

  • Сквозной запуск последовательностей Run.

  • Сохранение стека контекста при остановке и восстановление при включении.

  • Если добавить рефлексии можно сделать бота полностью конфигурируемого внешними файлами.

  • Изменение последовательности выполнения Run-ов во время работы.