l3070c95901s193k Generated by CAI (Codebase AI Interface) https://github.com/AfazTech/cai LLM INSTRUCTIONS: This file contains the complete project structure and the content of all source files. - The "PROJECT STRUCTURE" section shows the project tree. - The "FILES" section lists each file's content, separated by "@@@FILE: filename@@@". - Use this information to understand the codebase, answer questions, or generate new code. - Pay attention to file paths, dependencies, and the overall architecture. The following section contains the complete project structure: # PROJECT STRUCTURE . ├── .cai.json (553B) ├── .github │ └── workflows │ └── release.yml (341B) ├── .gitignore (74B) ├── LICENSE (1KB) ├── README.md (12KB) ├── composer.json (1KB) ├── examples │ ├── Polling.php (1KB) │ ├── SendMedia.php (502B) │ ├── SendMessage.php (462B) │ ├── Webhook.php (597B) │ └── afaz.jpg (99KB) └── src ├── Client.php (55KB) ├── KeyboardBuilder.php (3KB) ├── Logger.php (2KB) ├── Media.php (806B) ├── Poller.php (7KB) └── Settings.php (5KB) The following section contains the content of project files. @@@FILE: .gitignore@@@ vendor composer.lock tests/ examples/neili.lock examples/neili.log cai.txt @@@FILE: .github/workflows/release.yml@@@ name: Release on: push: tags: - 'v*' permissions: contents: write jobs: release: name: Create Release runs-on: ubuntu-latest steps: - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: tag_name: ${{ github.ref_name }} generate_release_notes: true @@@FILE: examples/SendMessage.php@@@ setAccessToken($token); $client = new Client($settings); $future = $client->sendMessage($chatId, $message); echo "Message sent, waiting for response..." . PHP_EOL; $result = $future->await(); print_r($result); @@@FILE: examples/SendMedia.php@@@ setAccessToken($token); $client = new Client($settings); $file = new Media('afaz.jpg'); $future = $client->sendPhoto($chatId, $file); echo "Media sent, waiting for response..." . PHP_EOL; $result = $future->await(); print_r($result); @@@FILE: .cai.json@@@ { "name": "neili", "description": "", "maxSizeMB": 50, "ignore": [ ".git", "vendor", "node_modules", "dist", "build", "storage", ".env", "cai.json", "go.mod", "go.sum", ".cai" ], "include": [ "*", ".gitignore", "README.md", "LICENSE", "Makefile", "Dockerfile" ], "useGitignore": false, "tree": true, "files": true, "chunkSize": 0, "tokenBudget": 0, "outputMode": "file", "compressEmpty": true, "orderBy": "size", "diffMode": false, "showStats": true } @@@FILE: examples/Webhook.php@@@ setAccessToken($token) ->setMultiProcess(true); // enabled multi process $client = new Client($settings); $update = $client->handleUpdate(); if (isset($update['message']['chat']['id']) && isset($update['message']['text'])) { $chatId = $update['message']['chat']['id']; $text = $update['message']['text']; $reply = 'Echo: ' . $text; $future = $client->sendMessage($chatId, $reply); $future->await(); } @@@FILE: src/Media.php@@@ filePath = realpath($filePath); } } @@@FILE: LICENSE@@@ MIT License Copyright (c) 2023 Abolfazl Majidi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. @@@FILE: composer.json@@@ { "name": "afaztech/neili", "description": "Neili is an async-first PHP library built on Amp that streamlines creating robust Telegram bots. It provides a non-blocking HTTP client, wrappers for all Telegram Bot API methods, a long-polling Poller with concurrency control, and a flexible webhook handler. Neili is optimized for both constrained hosting environments and long-running worker processes.", "type": "library", "license": "MIT", "keywords": [ "afaztech", "neili", "telegram", "telegram bot", "telegram api", "telegram automation", "async", "amp" ], "require": { "php": ">=8.1", "psr/log": "^3.0", "amphp/file": "^3.2", "amphp/http-client": "^5.3" }, "autoload": { "psr-4": { "Neili\\": "src/" } }, "homepage": "https://github.com/imafaz/neili", "authors": [ { "name": "Abolfazl Majidi (Afaz)", "email": "contact@afaz.me", "homepage": "https://afaz.me" } ], "minimum-stability": "stable", "prefer-stable": true } @@@FILE: examples/Polling.php@@@ setAccessToken($token) ->setPollerTimeout(1) ->setPollerBackoffBase(1) ->setPollerMaxBackoff(16) ->setPollerMaxConcurrency(100); $client = new Client($settings); $poller = new Poller($client); // Global update handler (optional) $poller->onUpdate(function ($update) { // Can be used for logging or handling all updates globally }); // Separate specific handlers $poller->onMessage(function ($update) use ($client) { $chatId = $update['message']['chat']['id'] ?? null; $text = $update['message']['text'] ?? null; if ($chatId && $text) { $client->sendMessage($chatId, "Echo: " . $text); } }); $poller->onEditedMessage(function ($update) use ($client) { $chatId = $update['edited_message']['chat']['id'] ?? null; $text = $update['edited_message']['text'] ?? null; if ($chatId && $text) { $client->sendMessage($chatId, "Edited: " . $text); } }); // Add more onX handlers as needed //$poller->onCallbackQuery(...); //$poller->onInlineQuery(...); $poller->start(); @@@FILE: src/Logger.php@@@ filePath = $filePath; $this->printToConsole = $printToConsole; } /** * Open file asynchronously if not already opened * @return Future Async handle for the file */ private function openFile(): Future { if ($this->fileHandle === null) { $this->fileHandle = async(function () { return yield File\openFile($this->filePath, "a"); }); } return $this->fileHandle; } /** * Log a message with specified level and context * @param string $level Log level (PSR-3) * @param string $message Log message with placeholders * @param array $context Context array for message interpolation */ public function log($level, $message, array $context = []): void { $interpolated = $this->interpolate($message, $context); $timestamp = date('Y-m-d H:i:s'); $formatted = "[{$timestamp}] {$level}: {$interpolated}\n"; if ($this->printToConsole) { echo $formatted; } // Write log asynchronously to file async(function () use ($formatted) { $handle = yield $this->openFile(); yield $handle->write($formatted); }); } /** * Interpolate context values into message placeholders * @param string $message Log message * @param array $context Context key-value pairs * @return string Interpolated message */ private function interpolate(string $message, array $context): string { foreach ($context as $key => $value) { $message = str_replace("{" . $key . "}", (string) $value, $message); } return $message; } /** * Close the log file asynchronously * @return Future */ public function close(): Future { if ($this->fileHandle === null) { return async(fn() => null); } return async(function () { $handle = yield $this->fileHandle; yield $handle->close(); }); } } @@@FILE: src/KeyboardBuilder.php@@@ isInline) throw new \RuntimeException("Use inlineRow() for inline keyboard"); $this->rows[] = array_map(fn($text) => ['text' => $text], $buttons); return $this; } /** * Add a row to an inline keyboard * @param array $buttons Array of 'ButtonText' => 'CallbackData' */ public function inlineRow(array $buttons): self { $row = []; foreach ($buttons as $text => $callbackData) { $row[] = ['text' => $text, 'callback_data' => $callbackData]; } $this->inlineRows[] = $row; $this->isInline = true; return $this; } /** * Add a row to an inline keyboard with URL buttons * @param array $buttons Array of 'ButtonText' => 'URL' */ public function inlineUrlRow(array $buttons): self { $row = []; foreach ($buttons as $text => $url) { $row[] = ['text' => $text, 'url' => $url]; } $this->inlineRows[] = $row; $this->isInline = true; return $this; } /** * Convert keyboard to inline mode */ public function inline(): self { $this->isInline = true; return $this; } /** * Set resize option for reply keyboard */ public function resize(bool $resize = true): self { $this->resize = $resize; return $this; } /** * Set one-time keyboard option */ public function oneTime(bool $oneTime = true): self { $this->oneTime = $oneTime; return $this; } /** * Clear all keyboard rows and reset options */ public function clear(): self { $this->rows = []; $this->inlineRows = []; $this->isInline = false; $this->resize = true; $this->oneTime = false; return $this; } /** * Build final keyboard array for Telegram API * @return array */ public function build(): array { if ($this->isInline) { return ['inline_keyboard' => $this->inlineRows]; } return [ 'keyboard' => $this->rows, 'resize_keyboard' => $this->resize, 'one_time_keyboard' => $this->oneTime ]; } } @@@FILE: src/Settings.php@@@ logger = $logger ?? new Logger('/neili.log'); } /** * Set bot access token */ public function setAccessToken(string $token): self { $this->accessToken = $token; return $this; } /** * Get bot access token */ public function getAccessToken(): string { return $this->accessToken; } /** * Set API URL */ public function setApiUrl(string $url): self { $this->apiUrl = $url; return $this; } /** * Get API URL */ public function getApiUrl(): string { return $this->apiUrl; } /** * Enable or disable SSL verification */ public function setApiVerifySSL(bool $state): self { $this->apiVerifySSL = $state; return $this; } /** * Check if SSL verification is enabled */ public function isApiVerifySSL(): bool { return $this->apiVerifySSL; } /** * Set request timeout and optionally connection timeout */ public function setTimeout(int $timeout, ?int $connectionTimeout = null): self { $this->timeout = $timeout; if ($connectionTimeout !== null) $this->connectionTimeout = $connectionTimeout; return $this; } /** * Get request timeout */ public function getTimeout(): int { return $this->timeout; } /** * Get connection timeout */ public function getConnectionTimeout(): int { return $this->connectionTimeout; } /** * Enable or disable multi-process * @throws \RuntimeException if exec is disabled */ public function setMultiProcess(bool $state): self { if ($state && !function_exists('exec')) throw new \RuntimeException("Cannot enable multi-process: exec disabled"); $this->useMultiProcess = $state; return $this; } /** * Check if multi-process is enabled */ public function isMultiProcess(): bool { return $this->useMultiProcess; } /** * Set PHP binary path for multi-process execution */ public function setPhpBinary(string $path): self { $this->phpBinary = $path; return $this; } /** * Get PHP binary path */ public function getPhpBinary(): string { return $this->phpBinary; } /** * Set base seconds for poller backoff */ public function setPollerBackoffBase(int $seconds): self { $this->pollerBackoffBase = $seconds; return $this; } /** * Get poller backoff base seconds */ public function getPollerBackoffBase(): int { return $this->pollerBackoffBase; } /** * Set maximum backoff seconds for poller */ public function setPollerMaxBackoff(int $seconds): self { $this->pollerMaxBackoff = $seconds; return $this; } /** * Get poller maximum backoff seconds */ public function getPollerMaxBackoff(): int { return $this->pollerMaxBackoff; } /** * Set maximum concurrent handlers for poller */ public function setPollerMaxConcurrency(?int $n): self { $this->pollerMaxConcurrency = $n; return $this; } /** * Get maximum concurrent handlers */ public function getPollerMaxConcurrency(): ?int { return $this->pollerMaxConcurrency; } /** * Get logger instance */ public function getLogger(): LoggerInterface { return $this->logger; } /** * Set poller request timeout */ public function setPollerTimeout(int $seconds): self { $this->pollerTimeout = $seconds; return $this; } /** * Get poller request timeout */ public function getPollerTimeout(): int { return $this->pollerTimeout; } } @@@FILE: src/Poller.php@@@ client = $client; $this->logger = $this->client->getSettings()->getLogger(); $maxConcurrency = $this->client->getSettings()->getPollerMaxConcurrency(); $this->semaphore = $maxConcurrency ? new LocalSemaphore($maxConcurrency) : null; } // Set a global update callback public function onUpdate(callable $callback): void { $this->updateHandler = $callback; } // Register callback handlers for specific Telegram update types public function onMessage(callable $callback): void { $this->handlers['message'][] = $callback; } public function onEditedMessage(callable $callback): void { $this->handlers['edited_message'][] = $callback; } public function onMessageReaction(callable $callback): void { $this->handlers['message_reaction'][] = $callback; } public function onMessageReactionCount(callable $callback): void { $this->handlers['message_reaction_count'][] = $callback; } public function onChatBoost(callable $callback): void { $this->handlers['chat_boost'][] = $callback; } public function onRemovedChatBoost(callable $callback): void { $this->handlers['removed_chat_boost'][] = $callback; } public function onChannelPost(callable $callback): void { $this->handlers['channel_post'][] = $callback; } public function onEditedChannelPost(callable $callback): void { $this->handlers['edited_channel_post'][] = $callback; } public function onInlineQuery(callable $callback): void { $this->handlers['inline_query'][] = $callback; } public function onChosenInlineResult(callable $callback): void { $this->handlers['chosen_inline_result'][] = $callback; } public function onCallbackQuery(callable $callback): void { $this->handlers['callback_query'][] = $callback; } public function onShippingQuery(callable $callback): void { $this->handlers['shipping_query'][] = $callback; } public function onPreCheckoutQuery(callable $callback): void { $this->handlers['pre_checkout_query'][] = $callback; } public function onPoll(callable $callback): void { $this->handlers['poll'][] = $callback; } public function onPollAnswer(callable $callback): void { $this->handlers['poll_answer'][] = $callback; } public function onMyChatMember(callable $callback): void { $this->handlers['my_chat_member'][] = $callback; } public function onChatMember(callable $callback): void { $this->handlers['chat_member'][] = $callback; } public function onChatJoinRequest(callable $callback): void { $this->handlers['chat_join_request'][] = $callback; } public function onBusinessMessage(callable $callback): void { $this->handlers['business_message'][] = $callback; } public function onEditedBusinessMessage(callable $callback): void { $this->handlers['edited_business_message'][] = $callback; } public function onDeletedBusinessMessage(callable $callback): void { $this->handlers['deleted_business_message'][] = $callback; } public function onBusinessConnection(callable $callback): void { $this->handlers['business_connection'][] = $callback; } // Determine the type of incoming update based on registered handlers private function detectType(array $update): string { foreach (array_keys($this->handlers) as $type) { if (isset($update[$type])) return $type; } return 'unknown'; } // Start polling loop with optional discarding of old updates public function start(bool $discardOldUpdates = true): void { if ($this->running) throw new \RuntimeException('Poller already running'); // Ensure background execution and unlimited script runtime if (function_exists('ignore_user_abort')) ignore_user_abort(true); if (function_exists('set_time_limit')) set_time_limit(0); if (function_exists('ini_set')) @ini_set('max_execution_time', '0'); // Send headers and flush if not running in CLI if (PHP_SAPI !== 'cli' && !headers_sent()) { header('Connection: close'); header('Content-Type: text/html'); echo "Poller started in background"; flush(); if (function_exists('fastcgi_finish_request')) fastcgi_finish_request(); if (function_exists('litespeed_finish_request')) litespeed_finish_request(); } $this->running = true; $settings = $this->client->getSettings(); $timeout = $settings->getPollerTimeout(); $backoffBase = $settings->getPollerBackoffBase(); $maxBackoff = $settings->getPollerMaxBackoff(); // Optionally discard old updates to start fresh if ($discardOldUpdates) { try { $latest = $this->client->getUpdates(null,null,$timeout)->await(); $result = $latest['result'] ?? []; if ($result) $this->offset = (int) end($result)['update_id'] + 1; } catch (\Throwable $e) { $this->logger->warning("Discard old updates failed: ".$e->getMessage()); } } // Main asynchronous polling loop $this->mainFuture = async(function () use ($timeout, $backoffBase, $maxBackoff) { $failCount = 0; while ($this->running) { try { $response = $this->client->getUpdates($this->offset,null,$timeout,array_keys($this->handlers))->await(); $updates = $response['result'] ?? []; foreach ($updates as $update) { if (!is_array($update)) continue; $this->offset = (int) ($update['update_id'] ?? $this->offset) + 1; async(function () use ($update) { $lock = $this->semaphore?->acquire(); try { $type = $this->detectType($update); foreach ($this->handlers[$type] ?? [] as $handler) { try { $handler($update); } catch (\Throwable $e) { $this->logger->error("Handler error for {$type}: ".$e->getMessage()); } } if ($this->updateHandler !== null) { try { ($this->updateHandler)($update); } catch (\Throwable $e) { $this->logger->error("onUpdate handler error: ".$e->getMessage()); } } } finally { $lock?->release(); } }); } $failCount = 0; } catch (\Throwable $e) { $failCount++; $backoff = min($backoffBase << min($failCount,6), $maxBackoff); $this->logger->error("Poller error: ".$e->getMessage()); delay($backoff * 1000); } } $this->logger->info("Poller stopped"); }); $this->mainFuture->await(); } // Stop the poller loop public function stop(): void { $this->running = false; } // Check if poller is currently running public function isRunning(): bool { return $this->running; } } @@@FILE: README.md@@@ # Neili — Asynchronous Telegram Bot Library for PHP Neili is an async-first PHP library built on Amp that streamlines creating robust Telegram bots. It provides a non-blocking HTTP client, wrappers for all Telegram Bot API methods, a long-polling `Poller` with concurrency control, and a flexible webhook handler. Neili is optimized for both constrained hosting environments and long-running worker processes. **If this project is helpful to you, you may wish to give it a**:star2: **to support future updates and feature additions!** ### AI-Assisted Development For everything you need to know about the project, including AI-assisted ("vibe") coding, give the neili.txt file to your AI assistant. This file provides comprehensive context and documentation to help LLMs understand the project, its architecture, conventions, and codebase. --- ## Table of contents * [Introduction](#introduction) * [Features](#features) * [Requirements](#requirements) * [Installation](#installation) * [Configuration](#configuration) * [Usage](#usage) * [Multi-process](#multi-process) * [Polling vs Webhook](#polling-vs-webhook) * [Settings](#settings) * [Keyboard Builder](#keyboard-builder) * [Client Methods Reference](#client-methods-reference) * [TODO](#TODO) * [License](#license) --- ## Introduction Neili wraps the Telegram Bot API with Amp-based asynchronous primitives. It offers: * Safe, non-blocking HTTP requests * Async message sending, media uploads, and chat management * Receiving updates via long polling or webhooks * Lightweight, framework-agnostic architecture suitable for both short-lived webhook endpoints and long-running pollers --- ## Features * Async-first Telegram Bot API wrapper built on Amp HTTP client * `Poller` implementation with configurable timeout, exponential backoff, and concurrency * Webhook helper compatible with standard PHP and multi-process setups * Minimal external dependencies for easy integration * Multiprocess support for concurrent update handling * Examples included for polling, webhook, sending messages/media, and keyboards --- ## Requirements Neili requires the following environment and extensions: * **PHP >= 8.1** * **amphp/file ^3.2** — Async file handling * **amphp/http-client ^5.3** — Non-blocking HTTP requests * **PHP extensions:** `fileinfo`, `posix` * **PHP function `exec()`** — Required only if using multi-process mode in webhook --- ## Installation Install via Composer: ```bash composer require afaztech/neili ``` Or clone repository: ```bash git clone https://github.com/afaztech/neili.git cd neili composer install ``` Autoloading is PSR-4 (`Neili\` → `src/`). --- ## Configuration Neili uses `Neili\Settings` for configuration. Example: ```php use Neili\Settings; $settings = (new Settings()) ->setAccessToken('TELEGRAM_BOT_TOKEN') ->setApiUrl('https://api.telegram.org/bot') ->setPollerTimeout(5) ->setPollerMaxConcurrency(null); ``` --- ## Usage ### Long Polling Example ```php use Neili\Client; use Neili\Poller; use Neili\Settings; $settings = (new Settings())->setAccessToken('TELEGRAM_BOT_TOKEN'); $client = new Client($settings); $poller = new Poller($client); $poller->onUpdate(function(array $update) use ($client) { $chatId = $update['message']['chat']['id'] ?? null; $text = $update['message']['text'] ?? null; if ($chatId && $text) { $client->sendMessage((int)$chatId, 'Echo: '.$text); } }); $poller->start(); ``` ### Webhook Example ```php use Neili\Client; use Neili\Settings; $settings = (new Settings())->setAccessToken('TELEGRAM_BOT_TOKEN'); $client = new Client($settings); $update = $client->handleUpdate('WEBHOOK_SECRET_TOKEN'); if ($update) { // Async dispatch example } ``` --- ## Multi-process Neili supports multi-process update handling in webhook mode using PHP `exec()`. ```php $settings->setUseMultiProcess(true) ->setPhpBinary('/usr/bin/php'); ``` Incoming webhook updates are automatically forked into separate PHP processes for non-blocking execution. --- ## Polling vs Webhook | Method | Pros | Cons | | -------------------------- | -------------------------------------------------- | ----------------------------- | | Standard Webhook | Easy to integrate with HTTP servers | Single-threaded by default | | Webhook with Multi-process | Non-blocking, concurrent handling of updates | Requires PHP CLI and `exec()` | | Long Polling | Simple, reliable, no external server config needed | Continuous running process | --- ## Settings | Attribute | Description | Type | Required | Default | | -------------------- | ------------------------------------------- | ------ | -------- | ------------------------------ | | apiUrl | Base Telegram API URL | string | no | `https://api.telegram.org/bot` | | apiVerifySSL | Enable TLS verification | bool | no | `true` | | timeout | HTTP request timeout (seconds) | int | no | `30` | | connectionTimeout | HTTP connection timeout (seconds) | int | no | `5` | | useMultiProcess | Enable multi-process mode | bool | no | `false` | | phpBinary | Path to PHP CLI binary for worker processes | string | no | `/usr/bin/php` | | pollerTimeout | Long polling request timeout (seconds) | int | no | `5` | | pollerBackoffBase | Base seconds for exponential backoff | int | no | `1` | | pollerMaxBackoff | Maximum backoff seconds | int | no | `32` | | pollerMaxConcurrency | Maximum concurrent async handlers | int | no | `null` | | accessToken | Telegram bot token | string | yes | `null` | | logger | PSR-3 compatible logger instance | object | no | `null` | --- ### Logger Neili’s `Settings` constructor **directly accepts a PSR-3 logger**: * You can pass any PSR-3 compatible logger (e.g., Monolog). * If you do **not** provide a logger, Neili will use its **default lightweight async logger**. Example: ```php use Neili\Settings; use Monolog\Logger as MonologLogger; use Monolog\Handler\StreamHandler; $logger = new MonologLogger('bot', [new StreamHandler('/path/to/logfile.log')]); $settings = new Settings($logger) ->setAccessToken('TELEGRAM_BOT_TOKEN'); ``` This ensures full async logging support in both long-polling and multi-process webhook modes. --- ## Keyboard Builder `Neili\KeyboardBuilder` provides a fluent interface for building **either** a reply keyboard **or** an inline keyboard. **Important:** You cannot mix inline and regular rows in the same keyboard. ### Methods Table | Method | Description | Parameters | Returns | | --------- | ------------------------------------------------ | ----------------------------------------- | ------- | | row | Add a row of buttons for a reply keyboard | `string ...$buttons` | `self` | | inlineRow | Add a row of inline buttons with callback data | `array $buttons` (`text => callbackData`) | `self` | | inline | Convert the keyboard to inline mode | none | `self` | | resize | Set `resize_keyboard` option for reply keyboards | `bool $resize = true` | `self` | | oneTime | Set `one_time_keyboard` option | `bool $oneTime = true` | `self` | | clear | Clear all rows and reset options | none | `self` | | build | Compile final array for Telegram API | none | `array` | ### Example: Reply Keyboard ```php use Neili\KeyboardBuilder; $keyboard = (new KeyboardBuilder()) ->row('Yes', 'No') ->row('Maybe') ->resize(true) ->oneTime(true) ->build(); ``` ### Example: Inline Keyboard ```php use Neili\KeyboardBuilder; $keyboard = (new KeyboardBuilder()) ->inlineRow(['Button1' => 'callback_1', 'Button2' => 'callback_2']) ->inline() ->build(); ``` --- ## Client Methods Reference All `Client` methods in Neili are **asynchronous** and return `Amp\Future` objects. A `Future` represents a pending result; you can use `onResolve()` to handle the result or error when it completes. | Method | Description | Parameters | Returns | Usage / Notes | | ------------------- | ------------------------------------------ | ------------------------------------------------------------------------ | -------- | ----------------------------------------------------------------------------------- | | sendMessage | Send a text message | `$chatId: int, $text: string, $options: array = []` | `Future` | `.onResolve(fn($err, $res) => ...)` gets the result asynchronously | | sendPhoto | Send a photo to chat | `$chatId: int, $media: Media, $options: array = []` | `Future` | `Media` object wraps local file path; resolves to API response | | sendDocument | Send a document/file | `$chatId: int, $media: Media, $options: array = []` | `Future` | Supports local file upload asynchronously | | editMessageText | Edit the text of a previously sent message | `$chatId: int, $messageId: int, $text: string, $options: array = []` | `Future` | Can edit inline or regular messages; async response from Telegram API | | deleteMessage | Delete a message | `$chatId: int, $messageId: int` | `Future` | Resolves to boolean success/failure | | forwardMessage | Forward a message from one chat to another | `$fromChatId: int, $toChatId: int, $messageId: int` | `Future` | Returns message object of forwarded message | | getUpdates | Fetch updates (long polling) | `$params: array = []` | `Future` | Returns array of updates; use in Poller or manual async handling | | answerCallbackQuery | Respond to inline button callback | `$callbackQueryId: string, $text: string = '', $showAlert: bool = false` | `Future` | Needed to acknowledge inline button presses | | sendChatAction | Send typing / upload action to chat | `$chatId: int, $action: string` | `Future` | e.g., `'typing'`, `'upload_photo'`; resolves when action is sent | | handleUpdate | Process incoming webhook update | `$secretToken: string` | `Future` | Resolves to update array if a valid request; integrates with multi-process workflow | ### How `Future` Works `Amp\Future` lets you work asynchronously: ```php $future = $client->sendMessage($chatId, 'Hello async'); $future->onResolve(function($error, $result) { if ($error) { echo "Error: ".$error->getMessage(); } else { print_r($result); // Telegram API response } }); ``` Or `await()` to block until the result is ready (inside an async context): ```php $result = $client->sendMessage($chatId, 'Hello')->await(); print_r($result); ``` **Note:** Every `Client` method returns a `Future`, which means all network requests are **non-blocking** by default, letting you run multiple requests concurrently without waiting. --- ## TODO - [ ] Add MTProto support - [ ] Implement login with User Bot --- ## License MIT License — See [LICENSE](LICENSE) file. @@@FILE: src/Client.php@@@ settings = $settings; $this->httpClient = HttpClientBuilder::buildDefault(); } /** * Magic method for dynamically calling Telegram API methods. * Converts calls like $client->sendMessage(...) to request('sendMessage', [...]) */ public function __call($method, $arguments): Future { return $this->request($method, $arguments[0] ?? []); } /** * Get current settings */ public function getSettings(): Settings { return $this->settings; } /** * Checks if a string is a valid URL */ private static function isUrl(string $string): bool { return filter_var($string, FILTER_VALIDATE_URL) !== false; } /** * Handle incoming update * Supports both CLI (for multi-process) and webhook mode */ public function handleUpdate(?string $secretToken = null): array { $isCli = (php_sapi_name() === 'cli'); global $argv; if (!$isCli) { // Webhook mode $headers = getallheaders(); if ($secretToken !== null) { $headerToken = $headers['X-Telegram-Bot-Api-Secret-Token'] ?? null; if ($headerToken !== $secretToken) { throw new \RuntimeException('Invalid secret token'); } } $rawInput = file_get_contents('php://input'); $update = json_decode($rawInput, true); if (json_last_error() !== JSON_ERROR_NONE) { throw new \RuntimeException('Invalid JSON: ' . json_last_error_msg()); } // Multi-process support: fork a new PHP process for the update if ($this->settings->isMultiProcess()) { $payload = base64_encode(json_encode($update)); $executedFile = $_SERVER['SCRIPT_FILENAME']; $phpBinary = $this->settings->getPhpBinary(); exec("{$phpBinary} {$executedFile} '$payload' > /dev/null 2>&1 &"); http_response_code(200); exit; } return $update; } else { // CLI mode if (!isset($argv[1])) { throw new \RuntimeException('No payload provided in CLI'); } $update = json_decode(base64_decode($argv[1]), true); if (json_last_error() !== JSON_ERROR_NONE) { throw new \RuntimeException('Invalid JSON in CLI payload: ' . json_last_error_msg()); } return $update; } } /** * Perform async HTTP request to Telegram API */ private function request(string $method, array $params = []): Future { $url = $this->settings->getApiUrl() . $this->settings->getAccessToken() . '/' . $method; return async(function () use ($url, $params) { try { $request = new Request($url, 'POST'); $request->setHeader('Content-Type', 'application/json'); $request->setBody(json_encode($params)); $response = $this->httpClient->request($request); $body = $response->getBody()->buffer(); return json_decode($body, true); } catch (\Throwable $e) { $this->settings->getLogger()->error( "HTTP request failed | " . "Message: " . $e->getMessage() . " | File: " . $e->getFile() . " | Line: " . $e->getLine() . " | Trace: " . $e->getTraceAsString() ); throw $e; } }); } /** * Send request with file upload support * Useful for photos, documents, audio, stickers, etc. */ private function requestWithFile(string $method, array $fields, array $files = []): Future { $url = $this->settings->getApiUrl() . $this->settings->getAccessToken() . '/' . $method; return async(function () use ($url, $fields, $files) { try { $form = new Form(); foreach ($fields as $key => $value) { $form->addField($key, (string) $value); } foreach ($files as $key => $filePath) { $realPath = realpath($filePath); if (!$realPath) throw new \RuntimeException("File not found: $filePath"); $form->addFile($key, $realPath); } $request = new Request($url, 'POST'); $request->setBody($form); $response = $this->httpClient->request($request); $body = $response->getBody()->buffer(); return json_decode($body, true); } catch (\Throwable $e) { $this->settings->getLogger()->error( "HTTP request failed | " . "Message: " . $e->getMessage() . " | File: " . $e->getFile() . " | Line: " . $e->getLine() . " | Trace: " . $e->getTraceAsString() ); throw $e; } }); } /** * Get bot info (getMe) */ public function getMe(): Future { return $this->request('getMe', []); } /** * Send text message */ public function sendMessage(int $chatId, string $text, ?array $keyboard = null, ?array $extraParams = null): Future { $payload = ['chat_id' => $chatId, 'text' => $text]; if ($keyboard !== null) $payload['reply_markup'] = json_encode($keyboard); return $this->request('sendMessage', $extraParams ? array_merge($payload, $extraParams) : $payload); } public function reply(int $chatId, int $replyToMessageId, string $text, ?array $keyboard = null, ?array $extraParams = null): Future { $payload = ['chat_id' => $chatId, 'text' => $text, 'reply_to_message_id' => $replyToMessageId]; if ($keyboard !== null) $payload['reply_markup'] = json_encode($keyboard); return $this->request('sendMessage', $extraParams ? array_merge($payload, $extraParams) : $payload); } /** * Send photo * Supports both Media object or URL/file_id string */ public function sendPhoto(int $chatId, string|Media $photo, ?string $caption = null, ?array $keyboard = null, ?array $extraParams = null): Future { $fields = ['chat_id' => $chatId]; if ($caption !== null) $fields['caption'] = $caption; if ($keyboard !== null) $fields['reply_markup'] = json_encode($keyboard); if ($extraParams !== null) $fields = array_merge($fields, $extraParams); if ($photo instanceof Media) return $this->requestWithFile('sendPhoto', $fields, ['photo' => $photo->filePath]); $fields['photo'] = $photo; return $this->request('sendPhoto', $fields); } /** * Send video * Supports Media object for file upload or string for URL/file_id */ public function sendVideo(int $chatId, string|Media $video, ?string $caption = null, ?array $keyboard = null, ?array $extraParams = null): Future { $fields = ['chat_id' => $chatId]; if ($caption !== null) $fields['caption'] = $caption; if ($keyboard !== null) $fields['reply_markup'] = json_encode($keyboard); if ($extraParams !== null) $fields = array_merge($fields, $extraParams); if ($video instanceof Media) return $this->requestWithFile('sendVideo', $fields, ['video' => $video->filePath]); $fields['video'] = $video; return $this->request('sendVideo', $fields); } /** * Send audio (music or voice) */ public function sendAudio(int $chatId, string|Media $audio, ?string $caption = null, ?array $keyboard = null, ?array $extraParams = null): Future { $fields = ['chat_id' => $chatId]; if ($caption !== null) $fields['caption'] = $caption; if ($keyboard !== null) $fields['reply_markup'] = json_encode($keyboard); if ($extraParams !== null) $fields = array_merge($fields, $extraParams); if ($audio instanceof Media) return $this->requestWithFile('sendAudio', $fields, ['audio' => $audio->filePath]); $fields['audio'] = $audio; return $this->request('sendAudio', $fields); } /** * Send document (pdf, zip, etc) */ public function sendDocument(int $chatId, string|Media $document, ?string $caption = null, ?array $keyboard = null, ?array $extraParams = null): Future { $fields = ['chat_id' => $chatId]; if ($caption !== null) $fields['caption'] = $caption; if ($keyboard !== null) $fields['reply_markup'] = json_encode($keyboard); if ($extraParams !== null) $fields = array_merge($fields, $extraParams); if ($document instanceof Media) return $this->requestWithFile('sendDocument', $fields, ['document' => $document->filePath]); $fields['document'] = $document; return $this->request('sendDocument', $fields); } /** * Send animation (GIF) */ public function sendAnimation(int $chatId, string|Media $animation, ?string $caption = null, ?array $keyboard = null, ?array $extraParams = null): Future { $fields = ['chat_id' => $chatId]; if ($caption !== null) $fields['caption'] = $caption; if ($keyboard !== null) $fields['reply_markup'] = json_encode($keyboard); if ($extraParams !== null) $fields = array_merge($fields, $extraParams); if ($animation instanceof Media) return $this->requestWithFile('sendAnimation', $fields, ['animation' => $animation->filePath]); $fields['animation'] = $animation; return $this->request('sendAnimation', $fields); } /** * Send sticker by ID */ public function sendSticker(int $chatId, string $stickerId, ?array $extraParams = null): Future { return $this->request('sendSticker', array_merge(['chat_id' => $chatId, 'sticker' => $stickerId], $extraParams ?? [])); } /** * Edit existing message text */ public function editMessageText(int $chatId, int $messageId, string $text, ?array $keyboard = null, ?array $extraParams = null): Future { $payload = ['chat_id' => $chatId, 'message_id' => $messageId, 'text' => $text]; if ($keyboard !== null) $payload['reply_markup'] = json_encode($keyboard); return $this->request('editMessageText', $extraParams ? array_merge($payload, $extraParams) : $payload); } /** * Delete message */ public function deleteMessage(int $chatId, int $messageId): Future { return $this->request('deleteMessage', ['chat_id' => $chatId, 'message_id' => $messageId]); } /** * Forward message from one chat to another */ public function forwardMessage(int $chatId, int $fromChatId, int $messageId, ?array $extraParams = null): Future { $payload = ['chat_id' => $chatId, 'from_chat_id' => $fromChatId, 'message_id' => $messageId]; return $this->request('forwardMessage', $extraParams ? array_merge($payload, $extraParams) : $payload); } /** * Answer callback query (from inline keyboards) */ public function answerCallbackQuery(string $callbackQueryId, ?string $text = null, ?bool $showAlert = false, ?array $extraParams = null): Future { $payload = ['callback_query_id' => $callbackQueryId]; if ($text !== null) $payload['text'] = $text; if ($showAlert !== null) $payload['show_alert'] = $showAlert; return $this->request('answerCallbackQuery', $extraParams ? array_merge($payload, $extraParams) : $payload); } /** * Get chat info */ public function getChat(int|string $chatId): Future { return $this->request('getChat', ['chat_id' => $chatId]); } /** * Get specific chat member info */ public function getChatMember(int $chatId, int $userId): Future { return $this->request('getChatMember', ['chat_id' => $chatId, 'user_id' => $userId]); } /** * Get chat administrators */ public function getChatAdministrators(int $chatId): Future { return $this->request('getChatAdministrators', ['chat_id' => $chatId]); } /** * Get chat members count */ public function getChatMembersCount(int $chatId): Future { return $this->request('getChatMembersCount', ['chat_id' => $chatId]); } /** * Pin message in chat */ public function pinChatMessage(int $chatId, int $messageId, ?bool $disableNotification = false): Future { return $this->request('pinChatMessage', ['chat_id' => $chatId, 'message_id' => $messageId, 'disable_notification' => $disableNotification]); } /** * Unpin pinned message */ public function unpinChatMessage(int $chatId): Future { return $this->request('unpinChatMessage', ['chat_id' => $chatId]); } /** * Set chat title */ public function setChatTitle(int $chatId, string $title): Future { return $this->request('setChatTitle', ['chat_id' => $chatId, 'title' => $title]); } /** * Set chat description */ public function setChatDescription(int $chatId, string $description): Future { return $this->request('setChatDescription', ['chat_id' => $chatId, 'description' => $description]); } /** * Set chat photo */ public function setChatPhoto(int $chatId, string $photoUrl): Future { return $this->request('setChatPhoto', ['chat_id' => $chatId, 'photo' => $photoUrl]); } /** * Get file info from Telegram server */ public function getFile(string $fileId): Future { return $this->request('getFile', ['file_id' => $fileId]); } /** * Send dice animation */ public function sendDice(int $chatId, ?string $emoji = null, ?array $extraParams = null): Future { $payload = ['chat_id' => $chatId]; if ($emoji !== null) $payload['emoji'] = $emoji; return $this->request('sendDice', $payload + ($extraParams ?? [])); } /** * Send poll (quiz or survey) */ public function sendPoll(int $chatId, string $question, array $options, ?bool $isAnonymous = true, ?string $type = 'regular', ?array $extraParams = null): Future { $payload = ['chat_id' => $chatId, 'question' => $question, 'options' => json_encode($options), 'is_anonymous' => $isAnonymous, 'type' => $type]; return $this->request('sendPoll', $payload + ($extraParams ?? [])); } /** * Stop a running poll */ public function stopPoll(int $chatId, int $messageId, ?array $extraParams = null): Future { $payload = ['chat_id' => $chatId, 'message_id' => $messageId]; return $this->request('stopPoll', $payload + ($extraParams ?? [])); } /** * Send venue location */ public function sendVenue(int $chatId, float $latitude, float $longitude, string $title, string $address, ?array $extraParams = null): Future { $payload = ['chat_id' => $chatId, 'latitude' => $latitude, 'longitude' => $longitude, 'title' => $title, 'address' => $address]; return $this->request('sendVenue', $payload + ($extraParams ?? [])); } /** * Send live location */ public function sendLocation(int $chatId, float $latitude, float $longitude, ?array $extraParams = null): Future { $payload = ['chat_id' => $chatId, 'latitude' => $latitude, 'longitude' => $longitude]; return $this->request('sendLocation', $payload + ($extraParams ?? [])); } /** * Send contact info */ public function sendContact(int $chatId, string $phoneNumber, string $firstName, ?string $lastName = null, ?array $extraParams = null): Future { $payload = ['chat_id' => $chatId, 'phone_number' => $phoneNumber, 'first_name' => $firstName]; if ($lastName !== null) $payload['last_name'] = $lastName; return $this->request('sendContact', $payload + ($extraParams ?? [])); } /** * Get sticker set info */ public function getStickerSet(string $name): Future { return $this->request('getStickerSet', ['name' => $name]); } /** * Upload PNG sticker file */ public function uploadStickerFile(int $userId, Media $pngSticker): Future { return $this->requestWithFile('uploadStickerFile', ['user_id' => $userId], ['png_sticker' => $pngSticker->filePath]); } /** * Create new sticker set */ public function createNewStickerSet(int $userId, string $name, string $title, string $emojis, Media $pngSticker, ?array $extraParams = null): Future { $fields = ['user_id' => $userId, 'name' => $name, 'title' => $title, 'emojis' => $emojis]; if ($extraParams !== null) $fields = array_merge($fields, $extraParams); return $this->requestWithFile('createNewStickerSet', $fields, ['png_sticker' => $pngSticker->filePath]); } /** * Add sticker to existing set */ public function addStickerToSet(int $userId, string $name, string $emojis, Media $pngSticker, ?array $extraParams = null): Future { $fields = ['user_id' => $userId, 'name' => $name, 'emojis' => $emojis]; if ($extraParams !== null) $fields = array_merge($fields, $extraParams); return $this->requestWithFile('addStickerToSet', $fields, ['png_sticker' => $pngSticker->filePath]); } /** * Delete sticker from set */ public function deleteStickerFromSet(string $stickerId): Future { return $this->request('deleteStickerFromSet', ['sticker' => $stickerId]); } /** * Set sticker position inside set */ public function setStickerPositionInSet(string $stickerId, int $position): Future { return $this->request('setStickerPositionInSet', ['sticker' => $stickerId, 'position' => $position]); } /** * Set thumbnail of a sticker set */ public function setStickerSetThumb(string $name, Media $thumb): Future { return $this->requestWithFile('setStickerSetThumb', ['name' => $name], ['thumb' => $thumb->filePath]); } /** * Send "typing", "upload_photo", etc. action indicator */ public function sendChatAction(int $chatId, string $action): Future { return $this->request('sendChatAction', ['chat_id' => $chatId, 'action' => $action]); } /** * Get user profile photos */ public function getUserProfilePhotos(int $userId, ?int $offset = null, ?int $limit = null): Future { $payload = ['user_id' => $userId]; if ($offset !== null) $payload['offset'] = $offset; if ($limit !== null) $payload['limit'] = $limit; return $this->request('getUserProfilePhotos', $payload); } /** * Kick user from chat */ public function kickChatMember(int $chatId, int $userId, ?int $untilDate = null, ?array $extraParams = null): Future { $payload = ['chat_id' => $chatId, 'user_id' => $userId]; if ($untilDate !== null) $payload['until_date'] = $untilDate; return $this->request('kickChatMember', $payload + ($extraParams ?? [])); } /** * Unban user */ public function unbanChatMember(int $chatId, int $userId, ?array $extraParams = null): Future { $payload = ['chat_id' => $chatId, 'user_id' => $userId]; return $this->request('unbanChatMember', $payload + ($extraParams ?? [])); } /** * Restrict user permissions in chat */ public function restrictChatMember(int $chatId, int $userId, array $permissions, ?int $untilDate = null, ?array $extraParams = null): Future { $payload = ['chat_id' => $chatId, 'user_id' => $userId, 'permissions' => json_encode($permissions)]; if ($untilDate !== null) $payload['until_date'] = $untilDate; return $this->request('restrictChatMember', $payload + ($extraParams ?? [])); } /** * Promote user with admin privileges */ public function promoteChatMember(int $chatId, int $userId, array $privileges, ?array $extraParams = null): Future { $payload = ['chat_id' => $chatId, 'user_id' => $userId] + $privileges; return $this->request('promoteChatMember', $payload + ($extraParams ?? [])); } /** * Set chat-wide permissions */ public function setChatPermissions(int $chatId, array $permissions, ?array $extraParams = null): Future { $payload = ['chat_id' => $chatId, 'permissions' => json_encode($permissions)]; return $this->request('setChatPermissions', $payload + ($extraParams ?? [])); } /** * Export chat invite link */ public function exportChatInviteLink(int $chatId): Future { return $this->request('exportChatInviteLink', ['chat_id' => $chatId]); } /** * Create a new invite link */ public function createChatInviteLink(int $chatId, ?array $extraParams = null): Future { $payload = ['chat_id' => $chatId]; return $this->request('createChatInviteLink', $payload + ($extraParams ?? [])); } /** * Edit an existing invite link */ public function editChatInviteLink(int $chatId, string $inviteLink, ?array $extraParams = null): Future { $payload = ['chat_id' => $chatId, 'invite_link' => $inviteLink]; return $this->request('editChatInviteLink', $payload + ($extraParams ?? [])); } /** * Revoke an invite link */ public function revokeChatInviteLink(int $chatId, string $inviteLink): Future { return $this->request('revokeChatInviteLink', ['chat_id' => $chatId, 'invite_link' => $inviteLink]); } /** * Answer inline query (used in inline bots) */ public function answerInlineQuery(string $inlineQueryId, array $results, ?bool $cacheTime = null, ?bool $isPersonal = null, ?array $extraParams = null): Future { $payload = ['inline_query_id' => $inlineQueryId, 'results' => json_encode($results)]; if ($cacheTime !== null) $payload['cache_time'] = $cacheTime; if ($isPersonal !== null) $payload['is_personal'] = $isPersonal; return $this->request('answerInlineQuery', $payload + ($extraParams ?? [])); } /** * Send invoice for payments */ public function sendInvoice(int $chatId, string $title, string $description, string $payloadStr, string $providerToken, string $currency, array $prices, ?array $extraParams = null): Future { $payload = ['chat_id' => $chatId, 'title' => $title, 'description' => $description, 'payload' => $payloadStr, 'provider_token' => $providerToken, 'currency' => $currency, 'prices' => json_encode($prices)]; return $this->request('sendInvoice', $payload + ($extraParams ?? [])); } /** * Answer shipping query */ public function answerShippingQuery(string $shippingQueryId, bool $ok, ?array $shippingOptions = null, ?string $errorMessage = null): Future { $payload = ['shipping_query_id' => $shippingQueryId, 'ok' => $ok]; if ($shippingOptions !== null) $payload['shipping_options'] = json_encode($shippingOptions); if ($errorMessage !== null) $payload['error_message'] = $errorMessage; return $this->request('answerShippingQuery', $payload); } /** * Answer pre-checkout query */ public function answerPreCheckoutQuery(string $preCheckoutQueryId, bool $ok, ?string $errorMessage = null): Future { $payload = ['pre_checkout_query_id' => $preCheckoutQueryId, 'ok' => $ok]; if ($errorMessage !== null) $payload['error_message'] = $errorMessage; return $this->request('answerPreCheckoutQuery', $payload); } /** * Send game message */ public function sendGame(int $chatId, string $gameShortName, ?array $extraParams = null): Future { $payload = ['chat_id' => $chatId, 'game_short_name' => $gameShortName]; return $this->request('sendGame', $payload + ($extraParams ?? [])); } /** * Set game score */ public function setGameScore(int $userId, int $score, int $chatId, int $messageId, ?bool $force = false, ?bool $disableEditMessage = false): Future { $payload = ['user_id' => $userId, 'score' => $score, 'chat_id' => $chatId, 'message_id' => $messageId]; if ($force !== null) $payload['force'] = $force; if ($disableEditMessage !== null) $payload['disable_edit_message'] = $disableEditMessage; return $this->request('setGameScore', $payload); } /** * Get game high scores */ public function getGameHighScores(int $userId, int $chatId, int $messageId): Future { return $this->request('getGameHighScores', ['user_id' => $userId, 'chat_id' => $chatId, 'message_id' => $messageId]); } /** * Get file URL for download */ public function getFileUrl(string $fileId): string { return "https://api.telegram.org/file/bot" . $this->settings->getAccessToken() . "/" . $fileId; } /** * Download file from Telegram servers */ public function downloadFile(string $fileId, string $destinationPath): Future { return async(function () use ($fileId, $destinationPath) { $fileInfo = yield $this->getFile($fileId); if (!isset($fileInfo['result']['file_path'])) throw new \RuntimeException("Invalid file_id or file not found"); $filePath = $fileInfo['result']['file_path']; $url = "https://api.telegram.org/file/bot" . $this->settings->getAccessToken() . "/" . $filePath; $request = new Request($url); $response = yield $this->httpClient->request($request); $body = yield $response->getBody()->buffer(); file_put_contents($destinationPath, $body); return $destinationPath; }); } /** * Set webhook for receiving updates * @param string $url Webhook URL * @param string|null $certificate Path to public key certificate * @param int|null $maxConnections Maximum allowed connections * @param array|null $allowedUpdates List of update types to receive * @param bool|null $dropPendingUpdates Drop pending updates * @param string|null $secretToken Secret token for webhook verification */ public function setWebhook(string $url, ?string $certificate = null, ?int $maxConnections = null, ?array $allowedUpdates = null, ?bool $dropPendingUpdates = null, ?string $secretToken = null): Future { $payload = ['url' => $url]; if ($certificate !== null) $payload['certificate'] = $certificate; if ($maxConnections !== null) $payload['max_connections'] = $maxConnections; if ($allowedUpdates !== null) $payload['allowed_updates'] = $allowedUpdates; if ($dropPendingUpdates !== null) $payload['drop_pending_updates'] = $dropPendingUpdates; if ($secretToken !== null) $payload['secret_token'] = $secretToken; return $this->request('setWebhook', $payload); } /** * Delete webhook */ public function deleteWebhook(?bool $dropPendingUpdates = null): Future { $payload = []; if ($dropPendingUpdates !== null) $payload['drop_pending_updates'] = $dropPendingUpdates; return $this->request('deleteWebhook', $payload); } /** * Get webhook info */ public function getWebhookInfo(): Future { return $this->request('getWebhookInfo'); } /** * Copy messages of any kind. * Service messages and invoice messages can't be copied. */ public function copyMessage( int $chatId, int $fromChatId, int $messageId, ?string $caption = null, ?array $keyboard = null, ?array $extraParams = null ): Future { $payload = [ 'chat_id' => $chatId, 'from_chat_id' => $fromChatId, 'message_id' => $messageId ]; if ($caption !== null) { $payload['caption'] = $caption; } if ($keyboard !== null) { $payload['reply_markup'] = json_encode($keyboard); } return $this->request('copyMessage', $extraParams ? array_merge($payload, $extraParams) : $payload); } /** * Send voice messages. */ public function sendVoice( int $chatId, string|Media $voice, ?string $caption = null, ?array $keyboard = null, ?array $extraParams = null ): Future { $fields = ['chat_id' => $chatId]; if ($caption !== null) { $fields['caption'] = $caption; } if ($keyboard !== null) { $fields['reply_markup'] = json_encode($keyboard); } if ($extraParams !== null) { $fields = array_merge($fields, $extraParams); } if ($voice instanceof Media) { return $this->requestWithFile('sendVoice', $fields, ['voice' => $voice->filePath]); } $fields['voice'] = $voice; return $this->request('sendVoice', $fields); } /** * Send video notes (round videos). */ public function sendVideoNote( int $chatId, string|Media $videoNote, ?array $keyboard = null, ?array $extraParams = null ): Future { $fields = ['chat_id' => $chatId]; if ($keyboard !== null) { $fields['reply_markup'] = json_encode($keyboard); } if ($extraParams !== null) { $fields = array_merge($fields, $extraParams); } if ($videoNote instanceof Media) { return $this->requestWithFile('sendVideoNote', $fields, ['video_note' => $videoNote->filePath]); } $fields['video_note'] = $videoNote; return $this->request('sendVideoNote', $fields); } /** * Send a group of photos, videos, documents or audios as an album. */ public function sendMediaGroup( int $chatId, array $mediaItems, ?string $caption = null, ?bool $disableNotification = null, ?int $replyToMessageId = null, ?array $extraParams = null ): Future { $inputMedia = []; $attachments = []; $hasLocalFile = false; foreach ($mediaItems as $index => $item) { if ($item instanceof Media) { $attachKey = "file_{$index}_" . bin2hex(random_bytes(4)); $type = $this->detectMediaType($item->filePath); $mediaEntry = [ 'type' => $type, 'media' => "attach://{$attachKey}", ]; if ($index === 0 && $caption !== null) { $mediaEntry['caption'] = $caption; } $inputMedia[] = $mediaEntry; $attachments[$attachKey] = $item->filePath; $hasLocalFile = true; } elseif (is_string($item)) { $type = $this->guessTypeFromString($item); $mediaEntry = [ 'type' => $type, 'media' => $item, ]; if ($index === 0 && $caption !== null) { $mediaEntry['caption'] = $caption; } $inputMedia[] = $mediaEntry; } elseif (is_array($item) && isset($item['type'], $item['media'])) { $inputMedia[] = $item; } else { throw new InvalidArgumentException( "Unsupported media item at position {$index}. " . "Expected Media object, string (file_id/url), or InputMedia array." ); } } if (empty($inputMedia)) { throw new InvalidArgumentException("Media items array cannot be empty"); } if (count($inputMedia) > 10) { throw new InvalidArgumentException("Telegram allows maximum 10 media items in one group"); } $payload = [ 'chat_id' => $chatId, 'media' => json_encode($inputMedia), ]; if ($disableNotification !== null) { $payload['disable_notification'] = $disableNotification; } if ($replyToMessageId !== null) { $payload['reply_to_message_id'] = $replyToMessageId; } if ($extraParams) { $payload += $extraParams; } return $hasLocalFile ? $this->requestWithFile('sendMediaGroup', $payload, $attachments) : $this->request('sendMediaGroup', $payload); } private function detectMediaType(string $path): string { $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION)); return match ($ext) { 'jpg', 'jpeg', 'png', 'webp', 'heic', 'bmp' => 'photo', 'gif' => 'animation', 'mp4', 'mov', 'mkv', 'webm', 'avi' => 'video', 'mp3', 'm4a', 'ogg', 'wav', 'flac' => 'audio', default => 'document', }; } private function guessTypeFromString(string $value): string { if (str_starts_with($value, 'http') || str_starts_with($value, 'https')) { $ext = strtolower(pathinfo(parse_url($value, PHP_URL_PATH), PATHINFO_EXTENSION)); return match ($ext) { 'jpg', 'jpeg', 'png', 'webp' => 'photo', 'gif' => 'animation', 'mp4', 'mov', 'webm' => 'video', default => 'document', }; } if (str_starts_with($value, 'attach://')) { return 'document'; } return 'document'; } /** * Edit the caption of a message. */ public function editMessageCaption( int $chatId, int $messageId, ?string $caption = null, ?array $keyboard = null, ?array $extraParams = null ): Future { $payload = [ 'chat_id' => $chatId, 'message_id' => $messageId ]; if ($caption !== null) { $payload['caption'] = $caption; } if ($keyboard !== null) { $payload['reply_markup'] = json_encode($keyboard); } return $this->request('editMessageCaption', $extraParams ? array_merge($payload, $extraParams) : $payload); } /** * Edit only the reply markup of a message. */ public function editMessageReplyMarkup( int $chatId, int $messageId, ?array $keyboard = null, ?array $extraParams = null ): Future { $payload = [ 'chat_id' => $chatId, 'message_id' => $messageId ]; if ($keyboard !== null) { $payload['reply_markup'] = json_encode($keyboard); } return $this->request('editMessageReplyMarkup', $extraParams ? array_merge($payload, $extraParams) : $payload); } /** * Stop updating a live location message before live_period expires. */ public function stopMessageLiveLocation( int $chatId, int $messageId, ?array $keyboard = null, ?array $extraParams = null ): Future { $payload = [ 'chat_id' => $chatId, 'message_id' => $messageId ]; if ($keyboard !== null) { $payload['reply_markup'] = json_encode($keyboard); } return $this->request('stopMessageLiveLocation', $extraParams ? array_merge($payload, $extraParams) : $payload); } /** * Unpin all pinned messages in a chat. */ public function unpinAllChatMessages(int $chatId): Future { return $this->request('unpinAllChatMessages', ['chat_id' => $chatId]); } /** * Leave a chat. */ public function leaveChat(int $chatId): Future { return $this->request('leaveChat', ['chat_id' => $chatId]); } /** * Get the number of members in a chat. */ public function getChatMemberCount(int $chatId): Future { return $this->request('getChatMemberCount', ['chat_id' => $chatId]); } /** * Approve a chat join request. */ public function approveChatJoinRequest(int $chatId, int $userId): Future { return $this->request('approveChatJoinRequest', [ 'chat_id' => $chatId, 'user_id' => $userId ]); } /** * Decline a chat join request. */ public function declineChatJoinRequest(int $chatId, int $userId): Future { return $this->request('declineChatJoinRequest', [ 'chat_id' => $chatId, 'user_id' => $userId ]); } /** * Set custom emoji sticker set thumbnail for a chat. */ public function setChatStickerSet(int $chatId, string $stickerSetName): Future { return $this->request('setChatStickerSet', [ 'chat_id' => $chatId, 'sticker_set_name' => $stickerSetName ]); } /** * Delete custom emoji sticker set from a chat. */ public function deleteChatStickerSet(int $chatId): Future { return $this->request('deleteChatStickerSet', ['chat_id' => $chatId]); } /** * Create a topic in a forum supergroup chat. */ public function createForumTopic( int $chatId, string $name, ?int $iconColor = null, ?string $iconCustomEmojiId = null, ?array $extraParams = null ): Future { $payload = [ 'chat_id' => $chatId, 'name' => $name ]; if ($iconColor !== null) { $payload['icon_color'] = $iconColor; } if ($iconCustomEmojiId !== null) { $payload['icon_custom_emoji_id'] = $iconCustomEmojiId; } return $this->request('createForumTopic', $extraParams ? array_merge($payload, $extraParams) : $payload); } /** * Edit name and icon of a topic in a forum supergroup chat. */ public function editForumTopic( int $chatId, int $messageThreadId, ?string $name = null, ?string $iconCustomEmojiId = null, ?array $extraParams = null ): Future { $payload = [ 'chat_id' => $chatId, 'message_thread_id' => $messageThreadId ]; if ($name !== null) { $payload['name'] = $name; } if ($iconCustomEmojiId !== null) { $payload['icon_custom_emoji_id'] = $iconCustomEmojiId; } return $this->request('editForumTopic', $extraParams ? array_merge($payload, $extraParams) : $payload); } /** * Close an open topic in a forum supergroup chat. */ public function closeForumTopic(int $chatId, int $messageThreadId): Future { return $this->request('closeForumTopic', [ 'chat_id' => $chatId, 'message_thread_id' => $messageThreadId ]); } /** * Reopen a closed topic in a forum supergroup chat. */ public function reopenForumTopic(int $chatId, int $messageThreadId): Future { return $this->request('reopenForumTopic', [ 'chat_id' => $chatId, 'message_thread_id' => $messageThreadId ]); } /** * Delete a forum topic along with all its messages in a forum supergroup chat. */ public function deleteForumTopic(int $chatId, int $messageThreadId): Future { return $this->request('deleteForumTopic', [ 'chat_id' => $chatId, 'message_thread_id' => $messageThreadId ]); } /** * Clear the list of pinned messages in a forum topic. */ public function unpinAllForumTopicMessages(int $chatId, int $messageThreadId): Future { return $this->request('unpinAllForumTopicMessages', [ 'chat_id' => $chatId, 'message_thread_id' => $messageThreadId ]); } /** * Get custom emoji stickers, which can be used as a forum topic icon by any user. */ public function getForumTopicIconStickers(): Future { return $this->request('getForumTopicIconStickers', []); } /** * Edit the name of the 'General' topic in a forum supergroup chat. */ public function editGeneralForumTopic(int $chatId, string $name): Future { return $this->request('editGeneralForumTopic', [ 'chat_id' => $chatId, 'name' => $name ]); } /** * Close an open 'General' topic in a forum supergroup chat. */ public function closeGeneralForumTopic(int $chatId): Future { return $this->request('closeGeneralForumTopic', ['chat_id' => $chatId]); } /** * Reopen a closed 'General' topic in a forum supergroup chat. */ public function reopenGeneralForumTopic(int $chatId): Future { return $this->request('reopenGeneralForumTopic', ['chat_id' => $chatId]); } /** * Hide the 'General' topic in a forum supergroup chat. */ public function hideGeneralForumTopic(int $chatId): Future { return $this->request('hideGeneralForumTopic', ['chat_id' => $chatId]); } /** * Unhide the 'General' topic in a forum supergroup chat. */ public function unhideGeneralForumTopic(int $chatId): Future { return $this->request('unhideGeneralForumTopic', ['chat_id' => $chatId]); } /** * Change the bot's description. */ public function setMyDescription( ?string $description = null, ?string $languageCode = null, ?array $extraParams = null ): Future { $payload = []; if ($description !== null) { $payload['description'] = $description; } if ($languageCode !== null) { $payload['language_code'] = $languageCode; } return $this->request('setMyDescription', $extraParams ? array_merge($payload, $extraParams) : $payload); } /** * Change the bot's name. */ public function setMyName( ?string $name = null, ?string $languageCode = null, ?array $extraParams = null ): Future { $payload = []; if ($name !== null) { $payload['name'] = $name; } if ($languageCode !== null) { $payload['language_code'] = $languageCode; } return $this->request('setMyName', $extraParams ? array_merge($payload, $extraParams) : $payload); } /** * Change the bot's short description. */ public function setMyShortDescription( ?string $shortDescription = null, ?string $languageCode = null, ?array $extraParams = null ): Future { $payload = []; if ($shortDescription !== null) { $payload['short_description'] = $shortDescription; } if ($languageCode !== null) { $payload['language_code'] = $languageCode; } return $this->request('setMyShortDescription', $extraParams ? array_merge($payload, $extraParams) : $payload); } /** * Get the current bot description. */ public function getMyDescription( ?string $languageCode = null, ?array $extraParams = null ): Future { $payload = []; if ($languageCode !== null) { $payload['language_code'] = $languageCode; } return $this->request('getMyDescription', $extraParams ? array_merge($payload, $extraParams) : $payload); } /** * Get the current bot name. */ public function getMyName( ?string $languageCode = null, ?array $extraParams = null ): Future { $payload = []; if ($languageCode !== null) { $payload['language_code'] = $languageCode; } return $this->request('getMyName', $extraParams ? array_merge($payload, $extraParams) : $payload); } /** * Get the current bot short description. */ public function getMyShortDescription( ?string $languageCode = null, ?array $extraParams = null ): Future { $payload = []; if ($languageCode !== null) { $payload['language_code'] = $languageCode; } return $this->request('getMyShortDescription', $extraParams ? array_merge($payload, $extraParams) : $payload); } /** * Change the list of the bot's commands. */ public function setMyCommands( array $commands, ?array $scope = null, ?string $languageCode = null, ?array $extraParams = null ): Future { $payload = [ 'commands' => json_encode($commands) ]; if ($scope !== null) { $payload['scope'] = json_encode($scope); } if ($languageCode !== null) { $payload['language_code'] = $languageCode; } return $this->request('setMyCommands', $extraParams ? array_merge($payload, $extraParams) : $payload); } /** * Delete the list of the bot's commands. */ public function deleteMyCommands( ?array $scope = null, ?string $languageCode = null, ?array $extraParams = null ): Future { $payload = []; if ($scope !== null) { $payload['scope'] = json_encode($scope); } if ($languageCode !== null) { $payload['language_code'] = $languageCode; } return $this->request('deleteMyCommands', $extraParams ? array_merge($payload, $extraParams) : $payload); } /** * Get the current list of the bot's commands. */ public function getMyCommands( ?array $scope = null, ?string $languageCode = null, ?array $extraParams = null ): Future { $payload = []; if ($scope !== null) { $payload['scope'] = json_encode($scope); } if ($languageCode !== null) { $payload['language_code'] = $languageCode; } return $this->request('getMyCommands', $extraParams ? array_merge($payload, $extraParams) : $payload); } /** * Change the bot's menu button. */ public function setChatMenuButton( ?int $chatId = null, ?array $menuButton = null, ?array $extraParams = null ): Future { $payload = []; if ($chatId !== null) { $payload['chat_id'] = $chatId; } if ($menuButton !== null) { $payload['menu_button'] = json_encode($menuButton); } return $this->request('setChatMenuButton', $extraParams ? array_merge($payload, $extraParams) : $payload); } /** * Get the current value of the bot's menu button. */ public function getChatMenuButton( ?int $chatId = null, ?array $extraParams = null ): Future { $payload = []; if ($chatId !== null) { $payload['chat_id'] = $chatId; } return $this->request('getChatMenuButton', $extraParams ? array_merge($payload, $extraParams) : $payload); } /** * Change the default administrator rights requested by the bot. */ public function setMyDefaultAdministratorRights( ?array $rights = null, ?bool $forChannels = null, ?array $extraParams = null ): Future { $payload = []; if ($rights !== null) { $payload['rights'] = json_encode($rights); } if ($forChannels !== null) { $payload['for_channels'] = $forChannels; } return $this->request('setMyDefaultAdministratorRights', $extraParams ? array_merge($payload, $extraParams) : $payload); } /** * Get the current default administrator rights of the bot. */ public function getMyDefaultAdministratorRights( ?bool $forChannels = null, ?array $extraParams = null ): Future { $payload = []; if ($forChannels !== null) { $payload['for_channels'] = $forChannels; } return $this->request('getMyDefaultAdministratorRights', $extraParams ? array_merge($payload, $extraParams) : $payload); } /** * Change the bot's profile photo. */ public function setMyProfilePhoto(Media $photo): Future { return $this->requestWithFile('setMyProfilePhoto', [], ['photo' => $photo->filePath]); } /** * Delete the bot's profile photo. */ public function deleteMyProfilePhoto(?string $photoId = null): Future { $payload = []; if ($photoId !== null) { $payload['photo_id'] = $photoId; } return $this->request('deleteMyProfilePhoto', $payload); } /** * Get the current list of the bot's profile photos. */ public function getMyProfilePhotos(): Future { return $this->request('getMyProfilePhotos', []); } /** * Set a custom title for an administrator in a supergroup. */ public function setChatAdministratorCustomTitle( int $chatId, int $userId, string $customTitle ): Future { return $this->request('setChatAdministratorCustomTitle', [ 'chat_id' => $chatId, 'user_id' => $userId, 'custom_title' => $customTitle ]); } /** * Ban a channel chat in a supergroup or a channel. */ public function banChatSenderChat( int $chatId, int $senderChatId, ?array $extraParams = null ): Future { $payload = [ 'chat_id' => $chatId, 'sender_chat_id' => $senderChatId ]; return $this->request('banChatSenderChat', $extraParams ? array_merge($payload, $extraParams) : $payload); } /** * Unban a previously banned channel chat in a supergroup or a channel. */ public function unbanChatSenderChat( int $chatId, int $senderChatId, ?array $extraParams = null ): Future { $payload = [ 'chat_id' => $chatId, 'sender_chat_id' => $senderChatId ]; return $this->request('unbanChatSenderChat', $extraParams ? array_merge($payload, $extraParams) : $payload); } /** * Get the list of banned users in a supergroup or channel. */ public function getChatBannedUsers(int $chatId): Future { return $this->request('getChatBannedUsers', ['chat_id' => $chatId]); } /** * Delete a chat photo. */ public function deleteChatPhoto(int $chatId): Future { return $this->request('deleteChatPhoto', ['chat_id' => $chatId]); } /** * Get custom emoji stickers. */ public function getCustomEmojiStickers(array $customEmojiIds): Future { return $this->request('getCustomEmojiStickers', [ 'custom_emoji_ids' => json_encode($customEmojiIds) ]); } /** * Set the thumbnail of a regular or mask sticker set. */ public function setStickerSetThumbnail( string $name, int $userId, ?Media $thumbnail = null, ?string $format = null, ?array $extraParams = null ): Future { $fields = [ 'name' => $name, 'user_id' => $userId ]; if ($format !== null) { $fields['format'] = $format; } if ($extraParams !== null) { $fields = array_merge($fields, $extraParams); } if ($thumbnail instanceof Media) { return $this->requestWithFile('setStickerSetThumbnail', $fields, ['thumbnail' => $thumbnail->filePath]); } return $this->request('setStickerSetThumbnail', $fields); } /** * Set the title of a created sticker set. */ public function setStickerSetTitle(string $name, string $title): Future { return $this->request('setStickerSetTitle', [ 'name' => $name, 'title' => $title ]); } /** * Delete a sticker set. */ public function deleteStickerSet(string $name): Future { return $this->request('deleteStickerSet', ['name' => $name]); } /** * Set the thumbnail of a custom emoji sticker set. */ public function setCustomEmojiStickerSetThumbnail( string $name, ?string $customEmojiId = null, ?array $extraParams = null ): Future { $payload = ['name' => $name]; if ($customEmojiId !== null) { $payload['custom_emoji_id'] = $customEmojiId; } return $this->request('setCustomEmojiStickerSetThumbnail', $extraParams ? array_merge($payload, $extraParams) : $payload); } /** * Set the emoji list of a sticker. */ public function setStickerEmojiList(string $stickerId, array $emojiList): Future { return $this->request('setStickerEmojiList', [ 'sticker' => $stickerId, 'emoji_list' => json_encode($emojiList) ]); } /** * Set the keywords of a sticker. */ public function setStickerKeywords(string $stickerId, array $keywords): Future { return $this->request('setStickerKeywords', [ 'sticker' => $stickerId, 'keywords' => json_encode($keywords) ]); } /** * Set the mask position of a mask sticker. */ public function setStickerMaskPosition(string $stickerId, array $maskPosition): Future { return $this->request('setStickerMaskPosition', [ 'sticker' => $stickerId, 'mask_position' => json_encode($maskPosition) ]); } /** * Set the result of an interaction with a Web App. */ public function answerWebAppQuery( string $webAppQueryId, array $result, ?array $extraParams = null ): Future { $payload = [ 'web_app_query_id' => $webAppQueryId, 'result' => json_encode($result) ]; return $this->request('answerWebAppQuery', $extraParams ? array_merge($payload, $extraParams) : $payload); } /** * Get updates. */ public function getUpdates( ?int $offset = null, ?int $limit = null, ?int $timeout = null, ?array $allowedUpdates = null, ?array $extraParams = null ): Future { $payload = []; if ($offset !== null) { $payload['offset'] = $offset; } if ($limit !== null) { $payload['limit'] = $limit; } if ($timeout !== null) { $payload['timeout'] = $timeout; } if ($allowedUpdates !== null) { $payload['allowed_updates'] = json_encode($allowedUpdates); } return $this->request('getUpdates', $extraParams ? array_merge($payload, $extraParams) : $payload); } /** * Log out from the cloud Bot API server. */ public function logOut(): Future { return $this->request('logOut', []); } /** * Close the bot instance. */ public function close(): Future { return $this->request('close', []); } } @@@FILE: examples/afaz.jpg@@@ [binary file omitted]