nicoosh/telegram-bot
nicoosh/telegram-bot is a Laravel package for building Telegram bots with a fluent API, webhook and update handling, commands, keyboards, and Laravel-friendly configuration. It simplifies sending messages and creating interactive bot workflows without boilerplate.
Install and get a webhook receiving updates in under fifteen minutes.
composer require nicoosh/telegram-bot
@).vendor/nicoosh/telegram-bot/example-bot.php and config.php. They show the exact bootstrap the library expects — everything else in the docs is a variation on those two files.manager.php/set.php scripts initially. In Laravel you rarely use them; wire the library up yourself so you control the route, logging, and container binding.Minimal webhook controller:
// routes/api.php (no CSRF, no auth, POST)
Route::post('/telegram/webhook', [TelegramController::class, 'handle']);
// app/Http/Controllers/TelegramController.php
public function handle(Request $request)
{
$telegram = new Longman\TelegramBot\Telegram(
config('services.telegram.token'),
config('services.telegram.username')
);
// Critical: php://input is single-read; hand Laravel's raw body over explicitly.
Longman\TelegramBot\Request::setCustomInput($request->getContent());
return response()->json($telegram->handle()->getRawData());
}
Register the webhook once (Artisan command, tinker, or deploy hook):
Longman\TelegramBot\Request::setWebhook(['url' => 'https://your.app/api/telegram/webhook']);
First use case to build: a /start command. Create app/Telegram/Commands/StartCommand.php, extend Longman\TelegramBot\Commands\UserCommand, implement execute() returning $this->replyToChat('Hello'), and point the config's commands.paths at app/Telegram/Commands. You now have the whole pattern — everything later is more command classes and richer replies.
Commands are the unit of work. One class per command, discovered by filename (EchoCommand.php → /echo). Use UserCommand for normal commands, SystemCommand for reserved hooks (start, help, generic, genericmessage, inlinequery, callbackquery), and AdminCommand for operator-only tools. execute() returns a ServerResponse; return it, don't echo.
Replying. Prefer the helpers over raw Request:: calls inside commands:
return $this->replyToChat($text, ['parse_mode' => 'HTML']);
Keyboards. Build ReplyKeyboardMarkup / InlineKeyboard entities, then send as reply_markup. Handle button presses in CallbackqueryCommand (a SystemCommand) and route on $this->getCallbackQuery()->getData().
Multi-step flows. The Conversation trait gives you per-chat/user state without writing your own tables:
$this->conversation = new Conversation($user_id, $chat_id, $this->getName());
// ... ask, store, advance notes, $this->conversation->update()
This requires the library's MySQL schema (structure.sql); it is the only reason you need a database at all.
Sending outside a command. For notifications, alerts, or scheduled jobs, instantiate Telegram once (the constructor registers it statically) and then call Request:: directly — no Update needed:
new Longman\TelegramBot\Telegram($token, $username);
Longman\TelegramBot\Request::sendMessage(['chat_id' => $id, 'text' => $msg]);
Bind that instance as a container singleton in a service provider so queue workers reuse one configured Telegram rather than re-scanning command folders per job.
Custom command folder. Keep bot commands in app/Telegram/Commands (config commands.paths) so they live alongside your app and are autoloaded by Composer; leave the vendor Commands folders for system commands.
Logging. TelegramLog::initError(), initDebug(), initUpdate() write to Monolog channels. Point them at storage/logs/telegram-*.log; log the raw update too when debugging — it is the only way to reproduce a bug report.
Testing. You can exercise command logic without Telegram: build an Update entity from an array fixture (new Update($array)), set up Request::setCustomInput(json_encode($fixture)), and assert on the returned ServerResponse (or on Request::getLastResponse() with a fake transport). Test the command's decision logic, not the network call.
getUpdates are mutually exclusive. Local polling fails with 409 Conflict until you deleteWebhook. Use unsetWebhook/setWebhook in a deploy script and never run both modes against one token.php://input exactly once. In Laravel, Request::setCustomInput($request->getContent()) must happen before handle(); forgetting it produces an empty update and silent no-ops.auth, and any global middleware that might reject POSTs — Telegram sends no cookies, headers, or tokens unless you set a secret path/secret token.setWebhook. ngrok works for local dev; a private tunnel with a self-signed cert does not.update_id.ServerResponse::isOk(). The library does not throw on API errors — getErrorCode()/getDescription() are where the truth is. Handle 429 with retry_after rather than retrying blindly, and watch the per-chat ~30 msg/s limit for broadcast jobs.null. $update->getMessage() is null for callback_query and inline queries; getFrom() can be null on channel posts. Guard every accessor chain.callback_data 64 bytes, and file uploads need absolute paths wrapped in Request::encodeFile(). Chunk long text yourself.Telegram more than once per request/job.start, help, generic, genericmessage, inlinequery, callbackquery belong to SystemCommand; a UserCommand with that name is shadowed. Use GenericmessageCommand as a deliberate catch-all for unknown input.admin ID list in config. /debug dumps internals — never leave it enabled in production.TelegramLog is uninitialised; set the log paths before deploying or you will debug blind.structure.sql, and prune the conversation table — stale state silently hijacks a user's next command.config/services.php + .env, and give each bot instance its own webhook route if you run multiple bots — a shared Telegram singleton leaks state across them.How can I help you explore Laravel packages today?