feat(chat): свой приём сообщений вместо вебхука Jivo

Заменяет JivoBotController+JivoBotClient на ChatController (POST /api/chat/message)
и ProcessJivoMessageJob на ProcessChatMessageJob. Реплику клиента теперь пишет
контроллер и возвращает её номер (message_id); джоба получает этот номер и берёт
историю строго до него, не отправляя ответ во внешний Jivo — он ложится в bot_dialogs,
откуда его заберёт своё окошко (Задача 3). Мозг бота не менялся.

Спека: docs/superpowers/specs/2026-07-13-own-chat-widget-design.md §5
This commit is contained in:
Дмитрий
2026-07-13 07:04:38 +03:00
parent 197e771a4f
commit cfd26e38ef
14 changed files with 205 additions and 303 deletions
@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Jobs\Bot\ProcessChatMessageJob;
use App\Models\BotDialog;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
/**
* Свой чат (спека 2026-07-13-own-chat-widget-design §5). Заменяет вебхук Jivo.
* Реплику клиента пишем ЗДЕСЬ и возвращаем её номер: окошко опрашивает ответы
* «после» него и не получает обратно собственное сообщение.
* Публичный адрес (лендинг открыт всему интернету) защита в ChatRateLimiter (Task 4).
*/
class ChatController extends Controller
{
public function send(Request $request): JsonResponse
{
$data = $request->validate([
'chat_id' => ['required', 'string', 'regex:/^[a-f0-9]{32}$/'],
'text' => ['required', 'string', 'max:1000'],
]);
$text = trim($data['text']);
if ($text === '') {
return response()->json(['message' => 'Пустое сообщение.'], 422);
}
$userId = Auth::id();
$row = BotDialog::create([
'chat_id' => $data['chat_id'],
'direction' => 'in',
'message' => $text,
'source' => $userId !== null ? 'portal' : 'landing',
'user_id' => $userId,
'ip' => $request->ip(),
'created_at' => now(),
]);
ProcessChatMessageJob::dispatch($data['chat_id'], (int) $row->id, $text);
return response()->json(['message_id' => (int) $row->id]);
}
}