A webhook driven handler for Telegram Bots
Steps
git clone https://github.com/ismailian/bot-web-handler my-bot
cd my-bot
composer install
cp .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
{
echo '[+] File ID: ' . $photo->photos[0]->fileId;
// to download photo
$photo->photos[0]->save(
filename: 'photo.png', // optional
directory: '/path/to/save/photo' // optional
);
}
/**
* 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
input
value toage
in order to be able to intercept it later - remove the
input
property from the session once you've used it, otherwise any text message containing a number will be captured by the handler. new NumberValidator
is 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 product
is 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 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 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\Jobs
directory where your jobs will live. Job classes must implement theIJob
interface.
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();
}
}
You can easily configure auto-deployment using GitHub webhooks by following these 2 steps:
-
Environment Variables:
- set the path to the
git
executable - set
GIT_AUTO_DEPLOY
totrue
- set webhook secret for verification
GIT_WEBHOOK_SECRET
- set comma-separated usernames allowed in auto-deployments
GIT_COMMIT_USERS
- set comma-separated trigger keywords for auto-deployments
GIT_COMMIT_KEYWORDS
- set the path to the
-
Git Routes:
- set custom route for github events in the
config.php
file
/** * @var array $routes allowed routes */ 'routes' => [ 'git' => [ 'post' => [ '/git' => null ], ] ],
- set custom route for github events in the
Example:
# .env file
GIT_PATH=/usr/bin/git
GIT_AUTO_DEPLOY=true
GIT_WEBHOOK_SECRET=1b785ac87f73bd8701d0a92ca9284bdc
GIT_COMMIT_USERS=ismailian
GIT_COMMIT_KEYWORDS=auto,merge
whenever a verified incoming github webhook event comes in, that contains #auto
or #merge
in the commit message, and is committed by ismailian
, it will trigger a git pull
command.
use the 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);