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@@@

<?php

require '../vendor/autoload.php';

use Neili\Client;
use Neili\Settings;

$token = 'TOKEN'; // telegram api bot access token
$chatId = 123456789; // example chat id
$message = 'Hello from Neili async!';

$settings = (new Settings)
    ->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@@@

<?php
require '../vendor/autoload.php';

use Neili\Client;
use Neili\Settings;
use Neili\Media;

$token = 'TOKEN'; // telegram api bot access token
$chatId = 123456789; // example chat id
$message = 'Hello from Neili async!';

$settings = (new Settings)
    ->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@@@

<?php

require '../vendor/autoload.php';

use Neili\Client;
use Neili\Settings;

$token = 'TOKEN'; // telegram api bot access token

$settings = (new Settings)
    ->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@@@

<?php

/**
 * @version 2.0.0
 * @author Abolfazl Majidi (Afaz)
 * @package neili
 * @license https://opensource.org/licenses/MIT
 * @link https://github.com/AfazTech/neili
 */

declare(strict_types=1);

namespace Neili;

class Media
{
    /**
     * Absolute path to the media file
     * @var string
     */
    public string $filePath;

    /**
     * Media constructor
     * Validates the file exists and resolves absolute path
     *
     * @param string $filePath Relative or absolute path to the file
     * @throws \RuntimeException if the file does not exist
     */
    public function __construct(string $filePath)
    {
        if (!file_exists($filePath)) {
            throw new \RuntimeException("File not found: $filePath");
        }
        $this->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@@@

<?php

require '../vendor/autoload.php';

use Neili\Client;
use Neili\Settings;
use Neili\Poller;

$token = 'TOKEN';

$settings = (new Settings)
    ->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@@@

<?php

/**
 * @version 2.0.0
 * @author Abolfazl Majidi (Afaz)
 * @package neili
 * @license https://opensource.org/licenses/MIT
 * @link https://github.com/AfazTech/neili
 */

declare(strict_types=1);

namespace Neili;

use Amp\File;
use Amp\Future;
use function Amp\async;
use Psr\Log\AbstractLogger;
use Psr\Log\LoggerInterface;

class Logger extends AbstractLogger implements LoggerInterface
{
    /**
     * File path to store logs
     */
    private string $filePath;

    /**
     * Flag to print log messages to console
     */
    private bool $printToConsole;

    /**
     * Async handle for file operations
     */
    private ?Future $fileHandle = null;

    /**
     * Constructor
     * @param string $filePath Path to log file
     * @param bool $printToConsole Whether to print logs to console
     */
    public function __construct(string $filePath, bool $printToConsole = true)
    {
        $this->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@@@

<?php

/**
 * @version 2.2.2
 * @author Abolfazl Majidi (Afaz)
 * @package neili
 * @license https://opensource.org/licenses/MIT
 * @link https://github.com/AfazTech/neili
 */

declare(strict_types=1);

namespace Neili;

class KeyboardBuilder
{
    /**
     * Rows for regular keyboard
     */
    private array $rows = [];

    /**
     * Rows for inline keyboard
     */
    private array $inlineRows = [];

    /**
     * Flag indicating inline keyboard
     */
    private bool $isInline = false;

    /**
     * Flag to resize keyboard (Telegram feature)
     */
    private bool $resize = true;

    /**
     * Flag for one-time keyboard
     */
    private bool $oneTime = false;

    /**
     * Add a row to a reply keyboard
     * @param string ...$buttons Text labels for buttons
     */
    public function row(string ...$buttons): self
    {
        if ($this->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@@@

<?php

/**
 * @version 2.0.0
 * @author Abolfazl Majidi (Afaz)
 * @package neili
 * @license https://opensource.org/licenses/MIT
 * @link https://github.com/AfazTech/neili
 */
declare(strict_types=1);

namespace Neili;

use Psr\Log\LoggerInterface;

class Settings
{
    /**
     * Telegram bot access token
     */
    private string $accessToken;

    /**
     * Base API URL
     */
    private string $apiUrl = 'https://api.telegram.org/bot';

    /**
     * Enable SSL verification for API requests
     */
    private bool $apiVerifySSL = true;

    /**
     * Request timeout in seconds
     */
    private int $timeout = 10;

    /**
     * Connection timeout in seconds
     */
    private int $connectionTimeout = 5;

    /**
     * Flag to enable multi-process usage
     */
    private bool $useMultiProcess = false;

    /**
     * Path to PHP binary for multi-process execution
     */
    private string $phpBinary = '/usr/bin/php';

    /**
     * Timeout for poller getUpdates requests
     */
    private int $pollerTimeout = 3;

    /**
     * Base seconds for exponential backoff in poller
     */
    private int $pollerBackoffBase = 1;

    /**
     * Maximum backoff seconds in poller
     */
    private int $pollerMaxBackoff = 32;

    /**
     * Maximum concurrent async handlers in poller
     */
    private ?int $pollerMaxConcurrency = null;

    /**
     * Logger instance
     */
    private LoggerInterface $logger;

    /**
     * Constructor
     * @param Logger|null $logger Optional custom logger
     */
    public function __construct(?Logger $logger = null)
    {
        $this->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@@@

<?php

/**
 * @version 2.2.0
 * @author Abolfazl Majidi (Afaz)
 * @package neili
 * @license https://opensource.org/licenses/MIT
 * @link https://github.com/AfazTech/neili
 */

declare(strict_types=1);

namespace Neili;

use Amp\Future;
use function Amp\async;
use function Amp\delay;
use Amp\Sync\LocalSemaphore;

class Poller
{
    private Client $client;
    private array $handlers = [];
    private $updateHandler = null; // backward compatible single update callback
    private int $offset = 0; // last processed update ID
    private bool $running = false; // poller active state
    private ?Future $mainFuture = null; // main async loop future
    private string $lockFile = 'neili.lock'; // optional lock file for single instance
    private Logger $logger;
    private ?LocalSemaphore $semaphore = null; // concurrency control semaphore

    public function __construct(Client $client)
    {
        $this->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@@@

<?php

/**
 * @version 2.2.8
 * @author Abolfazl Majidi (Afaz)
 * @package neili
 * @license https://opensource.org/licenses/MIT
 * @link https://github.com/AfazTech/neili
 */

declare(strict_types=1);

namespace Neili;

use Amp\Http\Client\HttpClientBuilder;
use Amp\Http\Client\HttpClient;
use Amp\Http\Client\Request;
use Amp\Future;
use Amp\Http\Client\Form;
use InvalidArgumentException;
use function Amp\async;

class Client
{
    /**
     * Telegram bot settings (token, API URL, etc)
     */
    private Settings $settings;

    /**
     * HTTP client for async requests
     */
    private HttpClient $httpClient;

    /**
     * Constructor
     * Initializes the HTTP client and stores settings
     */
    public function __construct(Settings $settings)
    {
        $this->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]


