A webhook driven handler for Telegram Bots
Requirements
Steps
git clone https://github.com/ismailian/bot-web-handler my-botcd my-botcomposer installcp .env.sample .env
Configurations
- domain url
APP_DOMAIN - bot token
BOT_TOKEN - webhook secret
TG_WEBHOOK_SIGNATURE(Optional) - Telegram source IP
TG_WEBHOOK_SOURCE_IP(Optional)- I don't recommend setting this, because the Telegram IP will definitely change.
- routes - routes to accept requests from (Optional)
- whitelist - list of allowed user ids (Optional)
- blacklist - list of disallowed user ids (Optional)
| Command | Description |
|---|---|
php cli update:check |
check for available updates |
php cli update:apply |
apply available updates |
php cli handler:make <name> |
create new handler |
php cli handler:delete <name> |
delete a handler |
php cli webhook:set [uri] |
set bot webhook (URI is optional) |
php cli webhook:unset |
unset bot webhook |
php cli migrate <tables> |
migrate tables (users, events, sessions) |
php cli queue:init |
create queue table + jobs directory |
php cli queue:work |
run queue |
/**
* handle all incoming photos
*
* @param IncomingPhoto $photo
* @return void
*/
#[Photo]
public function photos(IncomingPhoto $photo): void
{
// access 3 sizes of the photo (small, medium, large)
$smallPhotoFileId = $photo->small->fileId;
// or iterate through each one:
$photo->each(function (PhotoSize $photoSize) {
$photoSize->save(directory: 'tmp/images');
});
}/**
* handle all incoming videos
*
* @param IncomingVideo $video
* @return void
*/
#[Video]
public function videos(IncomingVideo $video): void
{
echo '[+] File ID: ' . $video->fileId;
// to download video
$video->save(
filename: 'video.mp4', // optional
directory: '/path/to/save/video' // optional
);
}/**
* handle start command
*
* @return void
*/
#[Command('start')]
public function onStart(IncomingCommand $command): void
{
$this->telegram->sendMessage('welcome!');
}/**
* handle incoming callback query
*
* @param IncomingCallbackQuery $query
* @return void
*/
#[CallbackQuery('game:type')]
public function callbacks(IncomingCallbackQuery $query): void
{
echo '[+] response: ' . $query->data['game:type'];
}/**
* handle incoming text from private chats
*
* @return void
*/
#[Text]
#[Chat(Chat::PRIVATE)]
public function text(IncomingMessage $message): void
{
echo '[+] user sent: ' . $message->text;
}/**
* handle incoming text from specific user/users
*
* @return void
*/
#[Text]
#[Only(userId: '<id>', userIds: [...'<id>'])]
public function text(IncomingMessage $message): void
{
echo '[+] user sent: ' . $message->text;
}- we would set the
inputvalue toagein order to be able to intercept it later - remove the
inputproperty from the session once you've used it, otherwise any text message containing a number will be captured by the handler. new NumberValidatoris used to ensure that the handler only intercepts numeric text messages
/**
* handle incoming age command
*
* @return void
*/
#[Command('age')]
public function age(): void
{
session()->set('input', 'age');
$this->telegram->sendMessage('Please type in your age:');
}
/**
* handle incoming user input
*
* @return void
*/
#[Awaits('input', 'age')]
#[Text(Validator: new NumberValidator())]
public function setAge(IncomingMessage $message): void
{
$age = $message->text;
session()->unset('input');
}- Create invoice and send it to users
- Answer pre-checkout query by confirming
the productis available - Process successful payments
/**
* handle incoming purchase command
*
* @return void
*/
#[Command('purchase')]
public function purchase(): void
{
$this->telegram->sendInvoice(
title: 'Product title',
description: 'Product description',
payload: 'data for your internal processing',
prices: [
['label' => 'Product Name', 'amount' => 100]
],
currency: 'USD',
providerToken: 'Token assigned to you after linking your stripe account with telegram'
);
}
/**
* handle an incoming pre-checkout query
*
* @param IncomingPreCheckoutQuery $preCheckoutQuery
* @return void
*/
#[PreCheckoutQuery]
public function preCheckout(IncomingPreCheckoutQuery $preCheckoutQuery): void
{
$this->telegram->answerPreCheckoutQuery(
queryId: $preCheckoutQuery->id,
ok: true, // true if ok, otherwise false
errorMessage: 'if you have any errors'
);
}
/**
* handle incoming successful payment
*
* @return void
*/
#[SuccessfulPayment]
public function paid(IncomingSuccessfulPayment $successfulPayment): void
{
// save payment info and send a thank you message
}Currently, the queue only uses database to manage jobs, in the future, other methods will be integrated.
- run migration:
php cli queue:init - run queue worker:
php cli queue:work - create job:
typically, you would create the job in the
App\Jobsdirectory where your jobs will live. Job classes must implement theIJobinterface.
use TeleBot\System\Core\Queuable;
use TeleBot\System\Interfaces\IJob;
readonly class UrlParserJob implements IJob
{
use Queuable;
/**
* @inheritDoc
*/
public function __construct(protected int $id, protected array $data) {}
/**
* @inheritDoc
*/
public function process(): void
{
// process your data
}
}/**
* handle incoming urls
*
* @return void
*/
#[Url]
public function urls(IncomingUrl $url): void
{
// dispatch job using:
UrlParserJob::dispatch(['url' => $url]);
// or:
queue()->dispatch(UrlParserJob::class, [
'url' => $url
]);
$this->telegram->sendMessage('Your url is being processed!');
}In config.php, you can configure your routes to handle other requests.
/**
* @var array $routes allowed routes
*/
'routes' => [
'web' => [
'get' => [
'/api/health-check' => 'HealthCheck::index'
],
'post' => [
'/api/whitelist' => 'Whitelist::update'
]
]
],delegates are meant to intercept incoming requests before hitting the final handler.
To create a delegate, simply add new class to the App\Delegates directory, and implement the IDelegate interface.
This example demonstrates how to verify that the http request is coming from the admin.
Example Web Handler:
use TeleBot\App\Delegates\IsAdmin;
/**
* list all users
*
* @return void
*/
#[Delegate(IsAdmin::class)]
public function users(): void
{
// some logic to fetch users
$users = [];
response()->send(['users' => $users], true);
}IsAdmin delegate:
/**
* check if request is coming from the admin
*
* @return void
*/
public function __invoke(): void
{
$apiKey = response()->headers('X-Admin-Api-Key');
if (empty($apiKey)) {
response()->setStatusCode(401)->end();
}
$secret = getenv('ADMIN_API_KEY');
if (!hash_equals($secret, $apiKey)) {
response()->setStatusCode(401)->end();
}
}Use Cache or cache() to access the cache interface. Data can be stored globally or per user.
Globally:
if (($weatherData = cache()->get('weather_data'))) {
response()->json($weatherData);
}
$weatherData = []; // get data from API
cache()->remember('weather_data', $weatherData);
response()->json($weatherData);Per User:
$cacheKey = cache()->fingerprint();
if (($weatherData = cache()->get($cacheKey))) {
response()->json($weatherData);
}
$weatherData = []; // get data from API
cache()->remember($cacheKey, $weatherData);
response()->json($weatherData);You can configure maintenance mode by setting MAINTENANCE_MODE in the env file to DOWN
and set a handler in config.php like this example:
/**
* @var string|callable $maintenance handler to trigger when maintenance mode is enabled
*/
'maintenance' => 'Maintenance::handle',You can configure Cors in config.php for acceptable domains like this example:
/**
* @var array $cors CORS configurations
*/
'cors' => [
'example1.com' => [
'methods' => ['GET', 'POST', 'OPTIONS'],
'headers' => ['Accept', 'Authorization'],
'allow_credentials' => true,
],
'example2.net' => [
'methods' => '*',
'headers' => '*',
],
],