Разработка Битрикс-бота: история о том, как документация врала, а облака смеялись
- среда, 13 августа 2025 г. в 00:00:08
 
Привет, Хабр! (И тебе, случайный читатель, который зашёл сюда просто просто потому, что заскучал в корпоративном чате.)
Сегодня я расскажу вам историю о том, какая задача посетила меня на этот раз и как я сделал «корпоративного бота с возможностью оценки сотрудников» — казалось бы, простая задача, но… Нас ждёт много удивительных вещей :-)
Изначально мой план был такой:
Сделать бота в Телеграме.
Дать боту ролевую модель и базовый функционал для дальнейшего простого расширения возможностей (по сути, я пришёл к тому, что сделал свой конструктор бота для Bitrix24, но об этом не в этой статье).
Научить его собирать фидбэк о коллегах.
Добавить машинное обучение, чтобы он сам выявлял, кто у нас в офисе трудяга, а кто мастерски имитирует бурную деятельность, ну и вообще эмоциональный фон в коллективе).
Но, как это часто бывает в IT, реальность внесла коррективы.
Отдел информационной безопасности посмотрел на мой прототип и сказал:
 — «Персональные данные в Телеграме? Ну уж нет, это не по-нашему, не по-православному!»
И вот я стою на распутье: куда же пристроить бота? Или месяц коту под хвост и сказать, что всё это плохая идея и пора завязывать со всем.
Мне предложили три варианта отечественных мессенджеров:
VK – «Круто, есть API!» Но…
Половина сотрудников им не пользуется.
Безопасность? Ха! То аккаунты взламывают, то сам VK их теряет.

MAX (тот самый «патриотичный» мессенджер) – «Звучит гордо!» Но…
Из 250 сотрудников им пользуется ровно один.
API выглядит кстати очень здорово (приятно удивило).
Битрикс24 – «Он у нас уже есть, сервера свои, все сотрудники уже там!» Идеально, подумал я и начал изучать API? Как бы не так…
API Битрикса – это как квест с сюрпризами, болью, слезами и страданием, а ещё в конце появляется скример.

Написано конечно всё классно, плюс/минус понятным языком, жаль конечно, что из примера реализации каждого метода то JS, то PHP, причём реально где-то откроешь метод и там сидит ПХП, а где-то полный набор с cURL, походу зависело от познаний разработчиков. Но вот вопрос, а где Python как можно было забыть этого красавчика.

Ладно, перейдём к примерам…
Открываем метод imbot.message.add – это отправка сообщений от лица бота, здесь у нас PHP

Открываем метод по созданию структуры компании (department.add) и здесь у нас уже полный набор.

И как я понял тот, кто делал API для бота владел одним стеком (PHP), а в остальных разделах API были разносторонние ребята, ну или те, кто умели пользоваться ГПТ, но это на самом деле не очень, важно…
Важно это:
Берём к примеру метод imbot.message.add – простой и понятный метод, отвечающий за отправку сообщений от лица бота. Смотрим на него детальнее…



Красиво? Понятно? – Мне показалось, что да. А теперь хотите узнать, как оно на самом деле?

Да, в KEYBOARD у нас вложился BUTTONS. В BUTTONS вообще массив с перечислением кнопок. CLIENT_ID у нас так же появился из неоткуда, без него вообще запрос возвращал ошибку запрета доступа (скрин со скримером). А MENU где? Его нет.
Как я это понял?
5 часов гугления.
10 часов перебора вариантов (просто тупо перебора вариантов как бы это могло работать...).
1 нервный срыв.
Но самое весёлое – документация врёт нам во многом.

Например, параметр BG_COLOR якобы меняет цвет кнопки. На деле – нет. Есть только четыре предустановленных варианта и то они в BG_COLOR_TOKEN:




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

Кстати, перед тем как, к примеру создавать command в KEYBOARD её ещё надо зарегистрировать в системе Битрикса. Это делается через метод imbot.command.register. Смотрим на пример.

А делаем вот так:

Вывод: Документация Битрикса — как квантовая физика: если кажется, что вы её поняли, значит, вы точно что-то упустили.
Для начала давайте посмотрим, как вообще создаётся бот.
Из раздела "Разработчикам" мы можем попасть в меню выбора разработки.

Для создания бота мы выбираем «Добавить чат-бот», но на самом деле если вы будете делать нечто большее чем просто чат бот с быстрыми командами, то придется попотеть. Но пока не об этом.
После того как мы перешли в «Добавить чат-бот» и перешли следующий пункт (кстати который может у всех отличаться и как я понимаю вы можете брать понравившийся вам) у меня «Информировать сотрудников в чате». Переходим и видим панель создания бота.

Вебхук для вызова rest api – это механизм уведомления одного сервиса другим о произошедших событиях. Но простыми словами и в данном случае это просто API ключ. Иногда когда вы будете менять права доступа своему боту, вам может понадобиться перегенерировать данный ключ.
Создание бота:
Название бота* - Здесь у нас будет название бота таким каким пользователи будут его видеть и находить через поиск по чатам битрикса.

Кстати, если вы хотите изменить картинку боту, то это "ТИПА" можно сделать через запрос imbot.update в формате base64 - но у меня сколько я не пытался не получилось, возможно здесь таже ситуация с неправильной или не актуально докой, я сделал это через «Панель администратора Битрикс», о ней будет в следующем разделе.

URL обработчика бота* - это, пожалуй, самая важная штука. Сюда мы указываем адрес нашего сервера, на котором мы будем слушать обращения. Это нужно, чтобы, к примеру, когда пользователь пишет боту сообщение, или запускает команду, бот нам слал Сообщение.
А теперь перейдём к самой теме…
Мы столкнулись с интересной проблемой: несмотря на то, что сервер с Битрикс и мой локальный компьютер разработки находились в одной сети, сервер не мог достучаться до URL обработчика, указанного на моей машине. Это вылилось в долгий процесс поиска причины и решения.
Игра «Кто хочет стать разработчиком Bitrix-бота»:

И тут «A» понятно, что шутка. «С» – порт мы пробросили через NAT, что бы он пинговал мою машину. «D». Тут как бы не столь критично, но самогенерированный (псевдо) сертификат hpps мы сделали. А верный ответ «B».
У нас on-premise версия Битрикса (чтобы всё было на наших серверах).
Но вот загадка:

Почему запросы от бота идут через облако сторонней компании?
Разгадка:
Бот отправляет сообщение → запрос улетает на Corp Soft, Seleznevskaya street, 32 (это облачный провайдер Битрикса) и он такой не один, мы так же поймали как минимум пять ip VK (походу он так сильно хотел нашего бота).
Вопрос: Зачем гонять трафик через третьи лица, если у нас локальная версия?
Теории:
Кто-то накосячил в архитектуре.
Это фича, а не баг (чтобы мы купили облако).
Это весьма интересный вопрос, от которого вообще теряется весь смысл on-prem поставки продукта. Ведь нельзя было использовать мощности сервера для отправки запроса, зачем гонять трафик через 3-х лиц. Короче мне кажется, что здесь кто-то просто сильно накосячил.
Хочу обратить внимание на проблему: ваш бот по умолчанию не может читать файлы — он их "боится". В целом, это логично, но не до конца.
Вы создали бота под себя — вы его автор и владелец, царь и бог. Однако Битрикс сохраняет все файлы на Битрикс Диск в каталоге чата. Но бот — это не совсем вы. Он должен самостоятельно обрабатывать переданные файлы через свой вебхук, а не полагаться на ваши права.
К сожалению, стандартного решения нет. В интернете я нашёл только информацию о платных подписках на "супер-пупер бота", который решает эту проблему. Но мы же разработчики-экспериментаторы!
Решение?
Получаем права администратора (каждый делает это по-своему). И заходим в администрирование Bitrix.
Находим бота среди пользователей (извините, скриншотов не будет — это конфиденциально).
Выдаём ему права обычного сотрудника (чтобы он не выделялся).
Авторизуемся под ботом через кнопку «Авторизоваться под сотрудником» в Панеле администрования Bitrix.
Настраиваем персональный вебхук для бота (как показано на скриншотах).



Даём права в «Настройка прав». А лучше сразу дать всё чтобы больше не лазить сюда. Копируем сформированный Вебхук. Закрываем и забываем как страшный сон.
И получается у нас смешная картина. Мы сделали бота, наделили его правами создателя, а затем воскресили его (превратив в сотрудника) и выдали ему ещё токен под его правами, и теперь у нас есть 2 токена (1 супер крутой – принадлежит боту, другой простенький и принадлежит нам), и пишем код, который будет маршрутизировать использование прав в зависимости от ситуаций. Не хотите писать, я уже написал, правда не судите строго))
import json
import requests
import itertools
from typing import Dict, Any, List, Optional
class Bitrix24API:
    """Класс для работы с REST API Bitrix24 через вебхук(и)"""
    def __init__(self, webhook_url: str, array_webhooks: Optional[List[str]] = None, logger=None, fetch_all_pages: bool = True, page_size: int = 50):
        """
        Инициализация класса с URL вебхука(ов) Bitrix24
        ---
        :param webhook_url: URL вебхука Bitrix24
        :param array_webhooks: Список альтернативных вебхуков для перебора
        :param logger: Объект логирования от библиотеки logging
        :param fetch_all_pages: Автоматически загружать все страницы при пагинации
        :param page_size: Количество элементов на странице (по умолчанию 50)
        """
        self.webhook_url = webhook_url
        self.array_webhooks = array_webhooks or []
        self.logger = logger
        self.specifically = None
        self.fetch_all_pages = fetch_all_pages
        self.page_size = page_size
    # ---Внутрение методы---
    def _try_call_method(self, webhook_url: str, method: str, params: Dict[str, Any]) -> Optional[Dict[str, Any]]:
        """Внутренний метод для попытки вызова метода через конкретный вебхук"""
        url = f"{webhook_url}{method}"
        try:
            response = requests.post(url, json=params)
            response.raise_for_status()
            result = response.json()
            if 'error' in result:
                if self.logger: self.logger.error(f"Bitrix API ошибка в '{webhook_url}': {result['error']}")
                return None
            if self.logger:  self.logger.info(f"Запрос к Bitrix API выполнен успешно - method: {method} - 200")
            return result
        except requests.exceptions.RequestException as e:
            if self.logger:  self.logger.error(f"Ошибка запроса в '{webhook_url}': {e}")
            return None
        except json.JSONDecodeError as e:
            if self.logger: self.logger.error(f"Ошибка декодирования JSON в '{webhook_url}': {e}")
            return None
    def _call_with_pagination(self, webhook_url: str, method: str, params: Dict[str, Any], start: int = 0) -> Optional[Dict[str, Any]]:
        """
        Внутренний метод для обработки пагинации
        ---
        :param webhook_url: URL вебхука
        :param method: Метод API
        :param params: Параметры запроса
        :param start: Смещение для пагинации
        :return: Объединенный результат или None при ошибке
        """
        params = params.copy()
        params['start'] = start
        result = self._try_call_method(webhook_url, method, params)
        if not result: return None
        if not self.fetch_all_pages or 'next' not in result or start > 0: return result.get('result')
        # Если нужно получить все страницы и это первый запрос
        total = result.get('total', 0)
        if total <= self.page_size:
            return result.get('result')
        # Вычисляем количество дополнительных запросов
        count_pages = (total // self.page_size) - (1 if total % self.page_size == 0 else 0)
        # Собираем результаты со всех страниц
        all_results = [result.get('result', [])]
        for page in range(1, count_pages + 1):
            next_result = self._call_with_pagination( webhook_url, method, params, page * self.page_size)
            if next_result is not None: all_results.append(next_result)
        # Объединяем результаты
        if isinstance(all_results[0], list):
            return list(itertools.chain.from_iterable(all_results))
        elif isinstance(all_results[0], dict):
            merged = {}
            for res in all_results: merged.update(res)
            return merged
        return all_results[0]
    # ---Внешние методы---
    def callMethod(self, method: str, params: Optional[Dict[str, Any]] = None, 
                  specifically: Optional[str] = None) -> Any:
        """
        Отправляет запрос к Bitrix24 REST API с поддержкой пагинации
        ---
        :param method: Метод API (например, "crm.deal.list")
        :param params: Параметры запроса в виде словаря
        :param specifically: Конкретный вебхук для использования
        :return: Ответ от сервера Bitrix24 или None в случае ошибки
        """
        if params is None: params = {}
        webhooks_to_try = []
        if specifically is not None: webhooks_to_try = [specifically]
        else:
            if self.webhook_url: webhooks_to_try.append(self.webhook_url)
            webhooks_to_try.extend(self.array_webhooks)
        for webhook in webhooks_to_try:
            result = self._call_with_pagination(webhook, method, params)
            if result is not None: return result
        if self.logger:
            self.logger.error(f"Запрос к Bitrix API никак не отработал {method}, {params}")
        return None
# # ====================================
# # ----------Пример использования------
# if __name__ == "__main__":
#     # Инициализация клиента с несколькими вебхуками
#     main_webhook = "https://...Токен_Бота"
#     backup_webhooks = ["https://...Токен_Разработчика", "https://...Токен_Васи_Пупкина", "https://...Токен_ФигЗнаетКого"]
    
#     bitrix = Bitrix24API(webhook_url=main_webhook, array_webhooks=backup_webhooks, logger=C.LOGGER_BITRIX_API)
#     result = bitrix.callMethod("calendar.section.get", {"type": "user", "ownerId": "0"}, specifically="https://...Токен_Разработчика")
#     print(result)
# # ====================================Вот такую я нарисовал схему взаимодействия, на первый взгляд возможно не очевидную.

Документация Битрикса – это квест. Гугл и метод тыка спасают.
«Локальный» Битрикс иногда ведёт себя как облачный.
Боты боятся файлов, пока не дашь самостоятельность боту (а это может быть страшно).
Мораль:
Если ваш бот в Битриксе работает с первого раза – проверьте, не снится ли вам это.
P.S. В следующей статье расскажу, как сделать боту ролевую модель, научить бота отвечать на любые произвольные команды пользователя, подружить его с СУБД , заставить этого бота анализировать сотрудников (и не сойти при этом с ума).