Подписывайтесь на наш Telegram канал и будьте в курсе всех событий.

Telegram-бот на Laravel для клиентской поддержки через темы в группах

Разработка поддержки через Telegram часто заканчивается тем, что каждый менеджер ведёт переписку с клиентом в личке, а история общения теряется где-то между GIF-ками и ссылками. Это не масштабируется и выглядит как временное решение.

Я столкнулся с этой проблемой сам и решил сделать своего Telegram-бота на Laravel, который решает сразу несколько задач:

  • структурирует коммуникацию,
  • прячет личные аккаунты менеджеров,
  • даёт возможность работать с обращениями в одной Telegram-группе,
  • использует темы (топики) для диалогов с каждым клиентом.

Где посмотреть и задать вопросы

Весь код открыт, поддержка звёздочками приветствуется 😉:

🔗 https://github.com/prog-time/tg-support-bot

Если при установке возникнут вопросы — пишите в Telegram:

📬 https://t.me/prog_time_bot

Как это работает

Пользователь пишет боту. Если это новое обращение — в Telegram-группе создаётся отдельная тема, куда бот отправляет первое сообщение клиента и базовую информацию о нём. Менеджеры отвечают внутри темы, и бот пересылает ответы пользователю от своего имени.

Благодаря этому:

  • у команды появляется общая история переписки;
  • менеджеры не светят свои личные аккаунты;
  • можно подключать нескольких операторов без потери контекста;
  • клиент чувствует, что с ним работают “как в саппорте”, а не в личке.

Особенности реализации

Бот написан на Laravel 12 и использует стандартный стек.

Файлы, фото и сообщения не сохраняются — всё пересылается в Telegram напрямую. Это упрощает соблюдение конфиденциальности и избавляет от лишних забот с хранением персональных данных.

Для кого это решение

Проект подойдёт для небольших команд, фрилансеров, техподдержки онлайн-сервисов, Telegram-first продуктов, онлайн-школ, консультационных проектов и всех, кто получает обращения через Telegram и хочет организовать поддержку быстро и просто.

Установка

  1. Клонируем репозиторий:
git clone https://github.com/prog-time/tg-support-bot.git
  1. Создаём бота через BotFather.

  2. Создаём приватную Telegram-группу с включёнными темами, добавляем туда бота как администратора.

  3. Получаем ID группы (например, с помощью getmyid_bot).

  4. Настраиваем .env:

APP_URL=https://your-domain.com
TELEGRAM_TOKEN=your_bot_token
TELEGRAM_GROUP_ID=your_group_id
TELEGRAM_SECRET_KEY=ваш_уникальный_ключ
  1. Устанавливаем webhook: Переходим в браузере по адресу: https://your-domain.com/api/telegram/set_webhook

Готово. Теперь можно писать боту, и он будет маршрутизировать обращения в соответствующие темы в группе.

Если вам пригодился проект — буду рад фидбеку и идеям по улучшению.

Laravel + тестирование: как сэкономить время на валидации запросов

При разработке API на Laravel часто возникает необходимость тестировать валидацию входящих данных. Один из способов — вручную писать тесты с различными вариантами входных параметров. Однако этот процесс может быть трудоемким и рутинным.

Чтобы упростить задачу, я разработал пакет laravel-request-testdata, который автоматически создает тестовые данные на основе правил валидации Laravel Request.

Ссылка на репозиторий – https://github.com/prog-time/laravel-request-testdata

Кому будет полезен этот пакет?

  • Разработчикам, которые хотят автоматизировать тестирование валидации запросов.
  • Тем, кто пишет много API-тестов и хочет минимизировать рутинные задачи.
  • Всем, кто хочет быстро получать корректные тестовые данные без необходимости вручную прописывать каждый вариант входных данных.

Как работает модуль?

Рассмотрим стандартный Laravel Request с простыми правилами:

class SimpleRequest extends FormRequest
{
  public function rules(): array
  {
    'name' => 'required|string',
    'email' => 'required|email',
  }
}

Теперь используем laravel-request-testdata для получения тестовых данных:

use App\Http\Requests\SimpleRequest;
use ProgTime\RequestTestData\RequestDataGenerator;

$request = new SimpleRequest();
$testData = RequestDataGenerator::generate($request);

Выходные данные могут выглядеть так:

[
  'name' => 'May Walker',
  'email' => 'ola.lagua@example.com',
]

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

При таком подходе вам нужно меньше следить за актуальностью тестов при редактирование правил валидации + это избавляет вас от необходимости вручную прописывать тестовые данные.

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

Разные форматы правил валидации

Данный модуль может обрабатывать правила валидации в разном формате.

Правила в формате массива

Вы можете описать правила в виде строки, как это было сделано в предыдущих примерах, а можете передать массив со списком параметром:

[
  'name'      => ['required', 'string', 'min:3', 'max:50'],
  'age'       => ['nullable', 'integer', 'min:18'],
  'password'  => ['required', 'string', 'min:8'],
]

Использование сложных условий

По мимо типичных правил валидации, модуль также понимает такие правила, как: in, exists, unique и так далее.

[
  'status' => 'required|string|in:pending,approved,rejected',
  'category_id' => 'required|exists:categories,id',
  'email' => 'required|email|unique:users,email',
]

Использование класса Rule для описания правил

В некоторых Request классах правила валидации описываются в формате Rule конструкций. Это может быть Rule::unique для проверки на уникальность или Rule::in для проверки на соответствие конкретным значениям.

[
    'email' => ['required', 'email', Rule::unique('users')],
    'role' => ['required', Rule::in(['admin', 'user', 'moderator'])],
]

Генерация тестовых файлов для проверки валидации

Что касается валидации загружаемых файлов, то тут всё немного сложнее. На данный момент мой модуль может сгенерировать файлы таких типов, как: yml, xml, svg, sql, png, log, json, jpg, html, gif и csv.

Количество доступных форматов будет постепенно увеличиваться, по мере развития данного модуля.

Для данной проблемы есть обходной вариант, который мы рассмотрим ниже.

Указание своих тестовых данных

Бываю моменты когда вам нужно для тестирования передавать свои данные, которые более корректно смогут настроить проверку работы приложения.

Для этого необходимо в Request классе создать метод requestTestData(). Данный метод должен возвращать параметры запроса с заполненными тестовыми данными.

public function requestTestData(): array
{
    $faker = \Faker\Factory::create();
    return [
        'email' => $faker->email(),
        'age' => 25,
    ];
}

Через метод requestTestData() вы также можете передавать тестовые файлы в форматах, которые на данный момент не поддерживаются моим модулем.

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

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

Я надеюсь вам понравилось моё решение. Я буду очень благодарен если вы поддержите данный модуль звёздочкой на GitHub и напишите свой комментарий под данным постом.

Спасибо за то что дочитали данный пост до конца!

9

Несколько Laravel проектов на одном домене

В нашей продуктовой компании используется много Laravel проектов, и сервисов которые решают свои задачи. Под каждый проект мы создавали поддомены, например:

  • api.domain.example
  • call-center.domain.example
  • office.domain.example
  • metric.domain.example
  • kassir.domain.example
  • и так далее…

При этом каждые проекты разделены под разные региональные зоны: .ru, .by, .kz, .tr. А также у нас существует несколько брендов, в каждой региональной зоне, это привело к тому, что проекты начали занимать множество доменов и поддоменов и каждый раз, когда нам нужно развернуть новый проект или провести эксперимент – приходилось мучаться с созданием новых доменов, выпуском сертификатов и настройкой логирования.

Мы устали плодить поддомены под каждый проект и решили перевести всё на единый домен domain.tech, где региональность и бренд задаётся поддоменом третьего уровня, а проекты разделены внутри этого домена.

Было: {project}.{brand}.domain.{region}

Стало: {brand}-{region}.domain.tech/{project}

Как могло быть еще (для нас это показалось не так удобно): {brand}.domain.tech/{region}/{project}

Плюсы

  • Меньше поддоменов и сертификатов: Нет необходимости выпускать отдельные SSL-сертификаты для каждого поддомена, достаточно одного wildcard-сертификата на domain.tech.
  • Единообразие URL-структуры: Чёткая и понятная структура URL, которая упрощает понимание архитектуры и облегчает работу разработчикам и тестировщикам.
  • Проще поддерживать CI/CD: Поскольку используется один домен, можно стандартизировать пайплайны в TeamCity для всех проектов.
  • Меньше нагрузки на SSL/TLS: Один wildcard-сертификат снижает нагрузку на TLS-рукопожатие по сравнению с использованием множества отдельных сертификатов.
  • Проще управление CORS: Использование единого домена снижает количество потенциальных проблем с CORS при межсайтовых запросах.
  • Гибкость настройки кэша: Можно гибко управлять кэшированием на уровне Nginx в зависимости от части URL, не привязываясь к поддоменам.
  • Проще интеграция с CDN: Легче настраивать CDN для статических ресурсов, так как все они идут с одного домена.
  • Логическая изоляция приложений: Разделение по location в Nginx позволяет чётко разграничить доступ к разным проектам, сохраняя изоляцию.
  • Упрощение маршрутизации и настройки nginx: Все маршруты управляются в одном месте, нет необходимости каждый раз добавлять новые server_name.
  • Легче отслеживать статистику и аналитику: Все метрики собираются на одном домене, что упрощает аналитику и мониторинг.
  • Проще управление безопасностью: Легче управлять CSP (Content Security Policy) и другими настройками безопасности, когда все приложения работают под одним доменом.
  • Упрощение управления DNS-записями: Меньше записей в DNS, упрощение управления зонами и меньшая вероятность ошибок.
  • Меньше зависимостей от инфраструктуры: Не нужно каждый раз обновлять DNS-записи и ждать их распространения.
  • Проще добавлять новые проекты и эксперименты: Быстрое развертывание новых приложений без необходимости регистрировать домены или поддомены.
  • Проще реализовывать A/B тесты: Легче управлять эксперементами, так как можно просто добавить новый путь в Nginx.
  • Упрощение поддержки региональности и брендов: Регион и бренд задаются поддоменом третьего уровня, что упрощает архитектуру URL.
  • Возможность передавать куки между проектами: с этим нужно быть аккуратным, но может быть полезно для передачи настроек локализации, темы и других параметров.
  • Возможность передавать localStorage и события данные между проектами:также как и куки, у нас появляется возможность передавать данные через внутренние системы браузера и слать события через storageEventHandler между разными вкладами разных проектах.

Минусы

  • Повышенные требования к настройке nginx: Сложные location-директивы требуют тщательной настройки и тестирования.
  • Зависимость от X-Forwarded-Path: Laravel и Frontend становятся зависимыми от заголовка.
  • Зависимость от корректности заголовков: Если проксирование настроено неправильно, Laravel может неправильно генерировать ссылки или редиректы.
  • Потенциальные проблемы с cookies: Если куки задаются на уровне домена, они могут конфликтовать между проектами.
  • Разделение прав доступа: Усложняется настройка прав доступа, если на одном домене работают проекты с разными уровнями безопасности.
  • Зависимость от nginx: Архитектура сильно завязана на настройках nginx, что может усложнить перенос на другой веб-сервер (например, Apache).
  • Потенциальные проблемы с кэшем: Возможны конфликты при кэшировании, так как кэш-сервисы (например, Cloudflare) могут не учитывать часть URL после домена.
  • Сложности с SEO: URL-структура становится менее “плоской”, что может повлиять на индексацию в поисковых системах. (не актуально, если вы используете этот подход для админок и бэк офиса)
  • Обновление ссылок и статических ресурсов: Требуется рефакторинг всех ссылок, чтобы они стали относительными, а это может быть трудоемким процессом (не актуально, если вы делаете новый проект).
  • Риск единой точки отказа: Проблемы с основным доменом .tech могут затронуть все проекты одновременно.
  • Высокая нагрузка на первый балансировщик: все запросы начнут проходить через единый nginx сервер, что может вызвать отказ обслуживания

Настройка Nginx

Чтобы перейти на единую доменную систему – необходимо настроить nginx правильным образом: роутинг по приложениям осуществляется при помощи location, в рамках одного server_name

Пример конфигурации nginx:

server {
    listen 80;
    listen 443 ssl;
    server_name *.domain.tech;
    
    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;
    
    location /api/ {
        proxy_pass http://api-container;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Prefix /api;
    }

    location /call-center/ {
        proxy_pass http://call-center-container;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Prefix /call-center;

        rewrite ^/call-center/(.*)$ /$1 break;
    }

    location /office/ {
        proxy_pass http://office-container;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Prefix /api;
    }
}

Здесь мы проксируем запрос на основе названия проекта и передаём запрос в контейнер (или fpm) нужного нам, при этом убираем из запроса название проекта, чтобы Laravel в проекте штатно обрабатывал роуты, якобы запрос пришёл на корень проекта. Это позволит мигрировать текущие проекты и разворачивать локально как обычно, без каких либо сложностей и имитации распределенной URL структуры.

Важно прокинуть заголовок X-Forwarded-Path, по этому заголовку Laravel сможет понять, как правильно генерировать ссылки для Frontend части и редиректов. Чтобы ваш Laravel проект начал доверять этому заголовку – нужно настроить TrustProxies.

Настройка Laravel

Проверьте, что у вас включен middleware \Illuminate\Http\Middleware\TrustProxies::class

Добавьте проверьте настройки config/trustedproxy.php

<?php

use Symfony\Component\HttpFoundation\Request;

return [

    'proxies' => [
        '10.0.0.0/8',
        '172.16.0.0/12',
        '192.168.0.0/16',
        '127.0.0.0/8',
    ],

    'headers' => Request::HEADER_X_FORWARDED_FOR |
        Request::HEADER_X_FORWARDED_HOST |
        Request::HEADER_X_FORWARDED_PORT |
        Request::HEADER_X_FORWARDED_PROTO |
        Request::HEADER_X_FORWARDED_PREFIX |
        Request::HEADER_X_FORWARDED_AWS_ELB

];

В headers должен быть разрешён заголовок HEADER_X_FORWARDED_PREFIX.

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

Если ваш nginx находится не в локальной сети, то добавьте IP адрес сервера nginx в ваш список proxies

Нужно правильно задать APP_URL, вписав туда фактическую ссылку на проект, это позволит генерировать правильные ссылки через CLI окружение (консоль, команды, очереди).

Теперь нужно проверить, чтобы во всех шаблонах для путей статических файлов использовался хелпер asset(), если поизучать исходники этой функции, то вы увидите, что генерация ссылок происходит с использованием заголовка X-Forwarded-Path, X-Forwarded-Proto, X-Forwarded-Host именно для этого мы и прокидывали все эти заголовки.

Ссылки должны строиться через хелпер url(), это позволит точно также генерировать правильные ссылки внутри проекта.

После того, как мы убедились, что для статических файлов мы используем asset(), а для внутренних путей url(), нужно заменить все глобальные пути в JS файлах на относительные. Например если мы отправляли запросы таким образом: axios.get('/api/entity'), это нужно переделать вот так: axios.get('api/entity')

Тоже самое нужно проделать во всех href="/example" и src="/example" атрибутах, например для <a>, <script> и <link> если по каким-то причинам мы не смогли использовать asset() и url() хелперы. Такое может случиться, если ссылки на статические файлы нам приходят из другого проекта, а сами статические файлы хранятся в текущем проекте.

После того, как мы перевели ссылки с глобальных на статические – необходимо в начало тега <head> прописать атрибут <base href="url('/')/"> этот лайфках позволит работать с относительными ссылками из корня проекта.

У вас могут начаться проблемы с css файлами, в которых ссылки на статические изображения забиты внутри public css файла, например background-image: url(assets/img/example.png). Чтобы решить эту проблему – перенесите генерацию стилей в vite или vue файлы, тогда css и assets файлы будут находится в одной build директории и у вас получится решить эту проблему. Если изображений несколько и они небольшие (например несколько иконок), то вы можете не использовать подгрузку отдельного файла с изображением, а представить в виде base64 ссылки или svg прямо внутри css url() хелпера.

Для того, чтобы собранные файлы из public/build стали относительными – нужно задать относительный путь в base параметре vite:

export default { 
    base: './',
    // ...
}

Если вы начинаете новый проект, то я советую вам изначально для ваших frontend путей использовать BASE_URL, пусть оно будет всегда по умолчанию '/', но в таком случае вы не столкнётесь с множеством проблем, если ваш проект будет хоститься не из корня домена. BASE_URL мы можете передавать из проекта, самым примитивным образом из <head> тега:

<script>window.BASE_URL = "{{ url('/') }}"; </script>

Также полезным будет иметь такую хелпер функцию, похожую на Laravel:

function url(path) {
    const origin = window.location.origin;

    // Очищаем лишний первый и последний слеши
    const baseUrl = (window.BASE_URL ?? '').replace(/^\/|\/$/g, '');

    // Очищаем лишний первый слеш
    path = (path ?? '').replace(/^\//g, '');

    // Выводим 3 компонента, разделяя слешами
    return [origin, baseUrl, path].join('/');
}

Этой функцией вы сможете также как и на Laravel генерировать ссылки на Frontend части вашего приложения, относительно базового URL.