chat-sdk-laravel maintained by bootdesk
bootdesk/chat-sdk-laravel
Laravel integration for laravel-bootdesk.
Install
composer require bootdesk/chat-sdk-laravel
Setup
php artisan chat:install
This publishes config/chat.php to your application.
Configuration
The published config/chat.php file contains the following sections:
return [
// The display name your bot uses when posting messages.
'user_name' => env('BOT_USERNAME', 'Bot'),
// Platform adapters to enable. Only adapters whose Composer package
// is installed (class_exists) will be loaded. For multi-tenant
// setups, omit the platform here and use an AdapterResolver instead.
'adapters' => [
// 'slack' => [
// 'bot_token' => env('SLACK_BOT_TOKEN'),
// 'signing_secret' => env('SLACK_SIGNING_SECRET'),
// ],
// 'telegram' => [
// 'bot_token' => env('TELEGRAM_BOT_TOKEN'),
// ],
// 'whatsapp' => [
// 'access_token' => env('WHATSAPP_ACCESS_TOKEN'),
// 'app_secret' => env('WHATSAPP_APP_SECRET'),
// 'phone_number_id' => env('WHATSAPP_PHONE_NUMBER_ID'),
// 'verify_token' => env('WHATSAPP_VERIFY_TOKEN'),
// ],
// 'discord' => [
// 'bot_token' => env('DISCORD_BOT_TOKEN'),
// 'application_id' => env('DISCORD_APPLICATION_ID'),
// 'public_key' => env('DISCORD_PUBLIC_KEY'),
// ],
// 'messenger' => [
// 'page_access_token' => env('MESSENGER_PAGE_ACCESS_TOKEN'),
// 'app_secret' => env('MESSENGER_APP_SECRET'),
// 'verify_token' => env('MESSENGER_VERIFY_TOKEN'),
// ],
// 'web' => [
// 'user_name' => env('BOT_USERNAME', 'Bot'),
// ],
// 'github' => [
// 'auth_token' => env('GITHUB_TOKEN'),
// 'webhook_secret' => env('GITHUB_WEBHOOK_SECRET'),
// ],
// 'linear' => [
// 'api_key' => env('LINEAR_API_KEY'),
// 'webhook_secret' => env('LINEAR_WEBHOOK_SECRET'),
// ],
],
// Cache store used for state persistence. Any Laravel cache store
// works: file, redis, database, memcached, array. Configure the
// store in config/cache.php as usual.
'state' => [
'store' => env('CHAT_STATE_STORE', 'file'),
'prefix' => env('CHAT_STATE_PREFIX', 'chat:'),
],
// Classes that register message handlers on the Chat instance.
// Each class must implement a register($chat) method.
'handlers' => [
// \App\Chat\ChatHandlers::class,
],
// How to handle concurrent messages for the same thread:
// - drop: Discard new messages while one is being processed
// - queue: Queue messages and process sequentially
// - debounce: Reset timer, process only the latest
// - concurrent: Process all messages simultaneously
'concurrency' => env('CHAT_CONCURRENCY', 'drop'),
// Scope for distributed locks: 'thread' (default) or 'channel'.
// Use 'channel' for platforms like WhatsApp/Telegram where
// conversations are per-channel (one conversation per phone number).
'lock_scope' => env('CHAT_LOCK_SCOPE', 'thread'),
// Cross-platform per-user message persistence. Requires an
// identity resolver bound to 'chat.identity' in a service provider.
'transcripts' => null,
];
Webhook Routes
Register a webhook route for incoming platform events:
// routes/web.php or routes/api.php
use BootDesk\ChatSDK\Laravel\Http\Controllers\WebhookController;
Route::match(['get', 'post'], '/api/webhooks/{adapter}', WebhookController::class);
The {adapter} segment matches the keys in your config/chat.php adapters array (e.g. slack, telegram, discord).
Handlers
Create a handler class to respond to messages:
// app/Chat/ChatHandlers.php
namespace App\Chat;
use BootDesk\ChatSDK\Core\Chat;
use BootDesk\ChatSDK\Core\MessageContext;
use BootDesk\ChatSDK\Laravel\Contracts\ChatHandler as ChatHandlerContract;
class ChatHandlers implements ChatHandlerContract
{
public function register(Chat $chat): void
{
$chat->onNewMessage('/^hello$/i', function (MessageContext $ctx) {
$ctx->thread->post('Hey!');
});
$chat->fallback(function (MessageContext $ctx) {
$ctx->thread->post("I don't understand that.");
});
}
}
Register it in config/chat.php:
'handlers' => [\App\Chat\ChatHandlers::class],
Middleware
Middleware intercept messages at different stages. Register in your handler class:
use BootDesk\ChatSDK\Core\Contracts\WebhookMiddleware;
use BootDesk\ChatSDK\Core\Contracts\ReceivingMiddleware;
use BootDesk\ChatSDK\Core\Contracts\SendingMiddleware;
class ChatHandlers
{
public function register(Chat $chat): void
{
// Intercept raw webhook before parsing
$chat->addWebhookMiddleware(new class implements WebhookMiddleware {
public function handle(ServerRequestInterface $request, callable $next): ResponseInterface {
logger()->info('Webhook received', ['path' => $request->getUri()->getPath()]);
return $next($request);
}
});
// Transform inbound messages before handlers
$chat->addReceivingMiddleware(new class implements ReceivingMiddleware {
public function handle(Message $message, Adapter $adapter, callable $next): ?Message {
// Return null to drop the message
if (str_contains($message->text, 'blocked')) {
return null;
}
return $next($message);
}
});
// Transform outbound messages before delivery
$chat->addSendingMiddleware(new class implements SendingMiddleware {
public function handle(string $threadId, PostableMessage $message, Adapter $adapter, string $operation, callable $next): ?SentMessage {
logger()->info('Sending message', ['thread' => $threadId, 'operation' => $operation]);
return $next($message);
}
});
}
}
Operations: post, edit, postEphemeral
Multi-Tenant Adapter Resolution
For multi-tenant applications where each tenant has their own bot credentials, use an AdapterResolver:
// app/Chat/MultiTenantAdapterResolver.php
namespace App\Chat;
use BootDesk\ChatSDK\Core\Contracts\Adapter;
use BootDesk\ChatSDK\Core\Contracts\AdapterResolver;
use BootDesk\ChatSDK\Slack\SlackAdapter;
use BootDesk\ChatSDK\Telegram\TelegramAdapter;
use Illuminate\Support\Facades\DB;
use Psr\Http\Message\ServerRequestInterface;
class MultiTenantAdapterResolver implements AdapterResolver
{
public function resolve(string $name, ?ServerRequestInterface $request): ?Adapter
{
// Extract tenant from request (header, subdomain, route param, etc.)
// When called from a job, $request is null - use other context (job payload, auth, etc.)
$tenantId = $request?->getHeaderLine('X-Tenant-ID')
?? $this->getTenantFromContext();
if ($tenantId === null || $tenantId === '') {
return null;
}
// Load tenant-specific credentials from database
$config = DB::table('tenant_chat_configs')
->where('tenant_id', $tenantId)
->where('adapter', $name)
->first();
if (! $config) {
return null;
}
// Instantiate adapter with tenant credentials
return match ($name) {
'slack' => new SlackAdapter(
botToken: $config->credentials['bot_token'],
httpClient: app(\Psr\Http\Client\ClientInterface::class),
signingSecret: $config->credentials['signing_secret'] ?? null,
),
'telegram' => new TelegramAdapter(
botToken: $config->credentials['bot_token'],
httpClient: app(\Psr\Http\Client\ClientInterface::class),
secretToken: $config->credentials['secret_token'] ?? null,
),
default => null,
};
}
}
Register the resolver in a service provider:
// app/Providers/AppServiceProvider.php
use BootDesk\ChatSDK\Core\Contracts\AdapterResolver;
public function register(): void
{
$this->app->bind(
AdapterResolver::class,
\App\Chat\MultiTenantAdapterResolver::class
);
}
Resolution order: Tenant-specific (resolver) → Global (config). Tenants can override specific adapters while falling back to global defaults for others.
Facade
use BootDesk\ChatSDK\Laravel\ChatFacade as Chat;
Chat::thread('slack:C123')->post('Hello!');
Artisan Commands
| Command | Description |
|---|---|
php artisan chat:list |
List configured adapters |
php artisan chat:install |
Publish config file |
Queue Processing
Incoming messages are processed asynchronously via ProcessMessageJob. Make sure your Laravel queue worker is running:
php artisan queue:work
The job dispatches automatically when webhook requests arrive. No manual setup is needed beyond configuring your queue driver in config/queue.php.
State
State persistence uses Laravel's cache system. Set CHAT_STATE_STORE to any Laravel cache driver (file, redis, database, memcached, array). The cache store is configured in config/cache.php as usual.
Error Handling
Adapter exceptions bubble up to Laravel's exception handler. Register custom handlers in app/Exceptions/Handler.php:
use BootDesk\ChatSDK\Core\Exceptions\AdapterException;
use BootDesk\ChatSDK\Core\Exceptions\AuthenticationException;
use BootDesk\ChatSDK\Core\Exceptions\RateLimitException;
use Illuminate\Http\Request;
public function register(): void
{
$this->renderable(function (AuthenticationException $e, Request $request) {
return response()->json(['error' => 'Unauthorized'], 401);
});
$this->renderable(function (RateLimitException $e, Request $request) {
return response()->json(['error' => 'Rate limited'], 429);
});
$this->renderable(function (AdapterException $e, Request $request) {
Log::error('Chat adapter error', [
'message' => $e->getMessage(),
'adapter' => $request->route('adapter'),
]);
return response()->json(['error' => 'Adapter failed'], 500);
});
}
Exception types:
AuthenticationException— Invalid credentials/tokensRateLimitException— Platform rate limit exceededAdapterException— Generic adapter errorsResourceNotFoundException— Adapter/thread not foundValidationException— Invalid input data
License
MIT