feat: max-bot-test initial — full surface coverage
Some checks failed
Deploy Max Bot Test / deploy (push) Failing after 3s
Some checks failed
Deploy Max Bot Test / deploy (push) Failing after 3s
- Slim 4 PSR-15 webhook, PHP-DI 7, Symfony Console 8 - 13 update handlers, 30+ command scenarios, all 7 button types - Composer pulls pkirillw/max-bot-api-php from GitHub vcs - Multi-stage Dockerfile (php-fpm 8.4-alpine) + nginx - docker-compose.yml targets server (npm_default external) - docker-compose.override.yml for local dev (gitignored) - .gitea/workflows/deploy.yml mirrors ghost-blog-bot pattern Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
63
src/Bootstrap.php
Normal file
63
src/Bootstrap.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App;
|
||||
|
||||
use DI\Container;
|
||||
use Nyholm\Psr7\Factory\Psr17Factory;
|
||||
use Slim\App;
|
||||
use Slim\Factory\AppFactory;
|
||||
use Symfony\Component\Dotenv\Dotenv;
|
||||
|
||||
final class Bootstrap
|
||||
{
|
||||
public static function loadEnv(): void
|
||||
{
|
||||
$envFile = __DIR__ . '/../.env';
|
||||
if (is_readable($envFile)) {
|
||||
(new Dotenv())->usePutenv(true)->load($envFile);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function envConfig(): array
|
||||
{
|
||||
return [
|
||||
'bot.token' => (string) getenv('BOT_TOKEN'),
|
||||
'bot.secret' => (string) getenv('MAX_BOT_API_SECRET'),
|
||||
'webhook.url' => (string) getenv('WEBHOOK_URL'),
|
||||
'webapp.url' => (string) getenv('WEB_APP_URL'),
|
||||
'debug.chat_id' => (int) (getenv('DEBUG_CHAT_ID') ?: 0),
|
||||
'selftest.chat_id' => (int) (getenv('SELFTEST_CHAT_ID') ?: 0),
|
||||
'app.env' => (string) (getenv('APP_ENV') ?: 'dev'),
|
||||
'app.debug' => filter_var(getenv('APP_DEBUG') ?: true, FILTER_VALIDATE_BOOLEAN),
|
||||
];
|
||||
}
|
||||
|
||||
public static function container(): Container
|
||||
{
|
||||
self::loadEnv();
|
||||
$definitions = require __DIR__ . '/../config/container.php';
|
||||
return new Container($definitions);
|
||||
}
|
||||
|
||||
public static function app(): App
|
||||
{
|
||||
$container = self::container();
|
||||
$cfg = $container->get('config');
|
||||
AppFactory::setContainer($container);
|
||||
|
||||
$app = AppFactory::create();
|
||||
$app->addErrorMiddleware(
|
||||
displayErrorDetails: (bool) ($cfg['app.debug'] ?? false),
|
||||
logErrors: true,
|
||||
logErrorDetails: true,
|
||||
logger: $container->get(\Psr\Log\LoggerInterface::class),
|
||||
);
|
||||
(require __DIR__ . '/../config/routes.php')($app);
|
||||
return $app;
|
||||
}
|
||||
}
|
||||
37
src/Command/AbstractCommand.php
Normal file
37
src/Command/AbstractCommand.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use Pkirillw\MaxBotApi\Api;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
abstract class AbstractCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
protected Api $api,
|
||||
protected LoggerInterface $logger,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
try {
|
||||
return $this->doExecute($io, $input, $output);
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
||||
$io->error($e->getMessage());
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
abstract protected function doExecute(SymfonyStyle $io, InputInterface $input, OutputInterface $output): int;
|
||||
}
|
||||
29
src/Command/GetBotInfoCommand.php
Normal file
29
src/Command/GetBotInfoCommand.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(name: 'bot:info', description: 'GET /me — print BotInfo.')]
|
||||
final class GetBotInfoCommand extends AbstractCommand
|
||||
{
|
||||
|
||||
protected function doExecute(SymfonyStyle $io, InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$info = $this->api->bots->getBot();
|
||||
$io->text('name: ' . $info->name);
|
||||
$io->text('username: @' . ($info->username ?? '—'));
|
||||
$io->text('user_id: ' . $info->userId);
|
||||
$io->text('description: ' . ($info->description ?? '—'));
|
||||
$io->text('commands:');
|
||||
foreach ($info->commands as $c) {
|
||||
$io->text(" /{$c->name} — {$c->description}");
|
||||
}
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
30
src/Command/ListSubscriptionsCommand.php
Normal file
30
src/Command/ListSubscriptionsCommand.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(name: 'subs:list', description: 'GET /subscriptions — list registered webhooks.')]
|
||||
final class ListSubscriptionsCommand extends AbstractCommand
|
||||
{
|
||||
|
||||
protected function doExecute(SymfonyStyle $io, InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$subs = $this->api->subscriptions->getSubscriptions();
|
||||
if ($subs->subscriptions === []) {
|
||||
$io->text('(no subscriptions)');
|
||||
return self::SUCCESS;
|
||||
}
|
||||
$rows = [];
|
||||
foreach ($subs->subscriptions as $s) {
|
||||
$rows[] = [$s->url, $s->version ?? '—', implode(',', $s->updateTypes) ?: '(all)'];
|
||||
}
|
||||
$io->table(['url', 'version', 'update_types'], $rows);
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
96
src/Command/SelfTestCommand.php
Normal file
96
src/Command/SelfTestCommand.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Bootstrap;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Pkirillw\MaxBotApi\Builder\Keyboard;
|
||||
use Pkirillw\MaxBotApi\Builder\Message as MessageBuilder;
|
||||
use Pkirillw\MaxBotApi\Scheme\Enum\Intent;
|
||||
use Pkirillw\MaxBotApi\Scheme\PinMessageBody;
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(name: 'selftest', description: 'Smoke-test every endpoint group end-to-end.')]
|
||||
final class SelfTestCommand extends AbstractCommand
|
||||
{
|
||||
|
||||
/** @var list<array{0:string,1:string,2:bool}> */
|
||||
private array $results = [];
|
||||
|
||||
protected function doExecute(SymfonyStyle $io, InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$config = Bootstrap::envConfig();
|
||||
$chatId = (int) $config['selftest.chat_id'];
|
||||
|
||||
if ($chatId <= 0) {
|
||||
$io->warning('SELFTEST_CHAT_ID is not set — will skip chat-scoped checks (send/edit/delete/pin/leave).');
|
||||
}
|
||||
|
||||
$this->probe($io, 'bots.getBot', fn () => $this->api->bots->getBot()->name);
|
||||
$this->probe($io, 'chats.getChats', fn () => count($this->api->chats->getChats(count: 1)->chats) . ' chat(s)');
|
||||
$this->probe($io, 'subscriptions.getSubscriptions', fn () => count($this->api->subscriptions->getSubscriptions()->subscriptions) . ' sub(s)');
|
||||
$this->probe($io, 'uploads.uploadPhotoFromBytes', function () {
|
||||
$png = base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=', true);
|
||||
$tokens = $this->api->uploads->uploadPhotoFromBytes($png, 'pixel.png');
|
||||
return $tokens->photos === [] ? 'no tokens returned' : count($tokens->photos) . ' token(s)';
|
||||
});
|
||||
|
||||
if ($chatId > 0) {
|
||||
$this->probe($io, 'messages.send (markdown + keyboard)', function () use ($chatId) {
|
||||
$kb = (new Keyboard())->addRow()->addCallback('selftest', 'selftest:ok', Intent::Positive);
|
||||
$msg = $this->api->messages->sendWithResult(
|
||||
MessageBuilder::new()
|
||||
->setChat($chatId)
|
||||
->setText('*selftest* message')
|
||||
->addKeyboard($kb),
|
||||
);
|
||||
$mid = $msg->body->mid;
|
||||
if ($mid === '') {
|
||||
throw new \RuntimeException('no mid returned');
|
||||
}
|
||||
$this->probe($io, 'messages.editMessage', function () use ($mid, $chatId) {
|
||||
$this->api->messages->editMessage($mid, MessageBuilder::new()->setChat($chatId)->setText('*selftest* edited'));
|
||||
});
|
||||
$this->probe($io, 'chats.sendAction (typing)', fn () => $this->api->chats->sendAction($chatId, \Pkirillw\MaxBotApi\Scheme\Enum\SenderAction::TypingOn));
|
||||
$this->probe($io, 'chats.pinMessage', fn () => $this->api->chats->pinMessage($chatId, new PinMessageBody(messageId: $mid)));
|
||||
$this->probe($io, 'messages.getMessage', fn () => $this->api->messages->getMessage($mid)->body?->text ?? '?');
|
||||
$this->probe($io, 'messages.getMessages', fn () => count($this->api->messages->getMessages($chatId, count: 1)->messages) . ' msg(s)');
|
||||
$this->probe($io, 'messages.deleteMessage', fn () => $this->api->messages->deleteMessage($mid)->success ? 'ok' : 'false');
|
||||
$this->probe($io, 'chats.getChat', fn () => $this->api->chats->getChat($chatId)->title ?? '(no title)');
|
||||
$this->probe($io, 'chats.getChatMembership', fn () => $this->api->chats->getChatMembership($chatId)->userId);
|
||||
$this->probe($io, 'chats.getChatMembers', fn () => count($this->api->chats->getChatMembers($chatId, count: 1)->members) . ' member(s)');
|
||||
});
|
||||
}
|
||||
|
||||
$this->probe($io, 'debugs.sendText', function () {
|
||||
if ($this->api->debugs->getChatId() === null) {
|
||||
return 'skipped (no DEBUG_CHAT_ID)';
|
||||
}
|
||||
$this->api->debugs->sendText('selftest: ' . date('c'));
|
||||
return 'sent';
|
||||
});
|
||||
|
||||
$table = new Table($output);
|
||||
$table->setHeaders(['probe', 'result', 'ok']);
|
||||
$table->setRows($this->results);
|
||||
$table->render();
|
||||
|
||||
$failures = array_filter($this->results, fn($r) => !$r[2]);
|
||||
return $failures === [] ? self::SUCCESS : self::FAILURE;
|
||||
}
|
||||
|
||||
private function probe(SymfonyStyle $io, string $name, callable $callback): void
|
||||
{
|
||||
try {
|
||||
$result = $callback();
|
||||
$this->results[] = [$name, (string) $result, true];
|
||||
} catch (\Throwable $e) {
|
||||
$this->results[] = [$name, mb_substr($e->getMessage(), 0, 120), false];
|
||||
}
|
||||
}
|
||||
}
|
||||
59
src/Command/SubscribeWebhookCommand.php
Normal file
59
src/Command/SubscribeWebhookCommand.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Bootstrap;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(name: 'webhook:subscribe', description: 'POST /subscriptions — register this bot webhook with MAX.')]
|
||||
final class SubscribeWebhookCommand extends AbstractCommand
|
||||
{
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addOption('url', 'u', InputOption::VALUE_OPTIONAL, 'Override WEBHOOK_URL from env');
|
||||
$this->addOption('secret', 's', InputOption::VALUE_OPTIONAL, 'Override MAX_BOT_API_SECRET from env');
|
||||
$this->addOption('types', 't', InputOption::VALUE_OPTIONAL, 'Comma-separated update types (default: empty = all)');
|
||||
}
|
||||
|
||||
protected function doExecute(SymfonyStyle $io, InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$config = Bootstrap::envConfig();
|
||||
$url = (string) ($input->getOption('url') ?: $config['webhook.url']);
|
||||
$secret = (string) ($input->getOption('secret') ?: $config['bot.secret']);
|
||||
$typesArg = $input->getOption('types');
|
||||
$types = $typesArg === null ? [] : array_values(array_filter(explode(',', (string) $typesArg)));
|
||||
|
||||
if ($url === '' || str_contains($url, 'your-tunnel-host')) {
|
||||
$io->error('WEBHOOK_URL is empty or still default. Run `make logs-tunnel` to read the cloudflared URL and put it in .env.');
|
||||
return self::FAILURE;
|
||||
}
|
||||
if ($secret === '') {
|
||||
$io->error('MAX_BOT_API_SECRET is empty. Generate one (e.g. `openssl rand -hex 32`) and put in .env.');
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$io->text("Subscribing $url with " . ($types === [] ? 'all update types' : 'types: ' . implode(',', $types)));
|
||||
$result = $this->api->subscriptions->subscribe($url, $types, $secret);
|
||||
if (!$result->success) {
|
||||
$io->error('Subscribe failed: ' . ($result->message ?? 'unknown'));
|
||||
return self::FAILURE;
|
||||
}
|
||||
$io->success('Subscribed. Verifying via getSubscriptions()…');
|
||||
|
||||
$subs = $this->api->subscriptions->getSubscriptions();
|
||||
$rows = [];
|
||||
foreach ($subs->subscriptions as $s) {
|
||||
$rows[] = [$s->url, $s->version ?? '—', implode(',', $s->updateTypes) ?: '(all)'];
|
||||
}
|
||||
$io->table(['url', 'version', 'update_types'], $rows);
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
35
src/Command/UnsubscribeWebhookCommand.php
Normal file
35
src/Command/UnsubscribeWebhookCommand.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Bootstrap;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(name: 'webhook:unsubscribe', description: 'DELETE /subscriptions — remove webhook registration.')]
|
||||
final class UnsubscribeWebhookCommand extends AbstractCommand
|
||||
{
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addArgument('url', InputArgument::OPTIONAL, 'Webhook URL to remove (defaults to WEBHOOK_URL)');
|
||||
}
|
||||
|
||||
protected function doExecute(SymfonyStyle $io, InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$config = Bootstrap::envConfig();
|
||||
$url = (string) ($input->getArgument('url') ?: $config['webhook.url']);
|
||||
if ($url === '') {
|
||||
$io->error('URL argument required (or set WEBHOOK_URL).');
|
||||
return self::FAILURE;
|
||||
}
|
||||
$result = $this->api->subscriptions->unsubscribe($url);
|
||||
$io->success(sprintf('Unsubscribe %s: success=%s', $url, $result->success ? 'true' : 'false'));
|
||||
return $result->success ? self::SUCCESS : self::FAILURE;
|
||||
}
|
||||
}
|
||||
70
src/Handler/AbstractUpdateHandler.php
Normal file
70
src/Handler/AbstractUpdateHandler.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Handler;
|
||||
|
||||
use App\Keyboard\KeyboardFactory;
|
||||
use Pkirillw\MaxBotApi\Api;
|
||||
use Pkirillw\MaxBotApi\Builder\Message as MessageBuilder;
|
||||
use Pkirillw\MaxBotApi\Exception\ApiException;
|
||||
use Pkirillw\MaxBotApi\Exception\MaxBotApiException;
|
||||
use Pkirillw\MaxBotApi\Exception\NetworkException;
|
||||
use Pkirillw\MaxBotApi\Exception\TimeoutException;
|
||||
use Pkirillw\MaxBotApi\Scheme\Enum\Format;
|
||||
use Pkirillw\MaxBotApi\Scheme\Update\UpdateInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
abstract class AbstractUpdateHandler implements UpdateHandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
protected Api $api,
|
||||
protected LoggerInterface $logger,
|
||||
protected KeyboardFactory $keyboards,
|
||||
) {}
|
||||
|
||||
public function safeHandle(UpdateInterface $update): void
|
||||
{
|
||||
$this->logger->info('update: ' . $update::class, [
|
||||
'type' => $update->getUpdateType()->value,
|
||||
'chat_id' => $update->getChatId(),
|
||||
'user_id' => $update->getUserId(),
|
||||
]);
|
||||
|
||||
try {
|
||||
$this->handle($update);
|
||||
} catch (ApiException $e) {
|
||||
$this->logger->error('API error: ' . $e->getMessage(), [
|
||||
'api_code' => $e->apiCode,
|
||||
'http_code' => $e->httpCode,
|
||||
]);
|
||||
$this->notifyAdmin($update, sprintf('API error `%s`: %s', $e->apiCode, $e->getMessage()));
|
||||
} catch (NetworkException $e) {
|
||||
$this->logger->warning('network: ' . $e->getMessage());
|
||||
} catch (TimeoutException $e) {
|
||||
$this->logger->warning('timeout: ' . $e->getMessage());
|
||||
} catch (MaxBotApiException $e) {
|
||||
$this->logger->error('lib error: ' . $e->getMessage());
|
||||
$this->api->debugs->sendErr($e);
|
||||
$this->notifyAdmin($update, 'Library error: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
protected function notifyAdmin(UpdateInterface $update, string $text): void
|
||||
{
|
||||
$chatId = $update->getChatId();
|
||||
if ($chatId <= 0) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
$this->api->messages->send(
|
||||
MessageBuilder::new()
|
||||
->setChat($chatId)
|
||||
->setText('⚠ ' . $text)
|
||||
->setFormat(Format::Markdown),
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->error('notifyAdmin failed: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
39
src/Handler/Update/BotAddedToChatHandler.php
Normal file
39
src/Handler/Update/BotAddedToChatHandler.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Handler\Update;
|
||||
|
||||
use App\Handler\AbstractUpdateHandler;
|
||||
use Pkirillw\MaxBotApi\Api;
|
||||
use Pkirillw\MaxBotApi\Builder\Message as MessageBuilder;
|
||||
use Pkirillw\MaxBotApi\Scheme\Enum\Format;
|
||||
use Pkirillw\MaxBotApi\Scheme\Enum\SenderAction;
|
||||
use Pkirillw\MaxBotApi\Scheme\Update\BotAddedToChatUpdate;
|
||||
use Pkirillw\MaxBotApi\Scheme\Update\UpdateInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
final class BotAddedToChatHandler extends AbstractUpdateHandler
|
||||
{
|
||||
public function updateClass(): string { return \Pkirillw\MaxBotApi\Scheme\Update\BotAddedToChatUpdate::class; }
|
||||
|
||||
public function handle(UpdateInterface $update): void
|
||||
{
|
||||
if (!$update instanceof BotAddedToChatUpdate) {
|
||||
return;
|
||||
}
|
||||
$this->logger->info('bot added to chat', [
|
||||
'chat_id' => $update->chatId,
|
||||
'by_user_id' => $update->user->userId,
|
||||
]);
|
||||
// Use chats->sendAction to demo SenderAction::TypingOn.
|
||||
$this->api->chats->sendAction($update->chatId, SenderAction::TypingOn);
|
||||
$this->api->messages->send(
|
||||
MessageBuilder::new()
|
||||
->setChat($update->chatId)
|
||||
->setText("👋 Спасибо за приглашение! Набери `/help` для списка команд.")
|
||||
->setFormat(Format::Markdown),
|
||||
);
|
||||
$this->api->chats->sendAction($update->chatId, SenderAction::TypingOff);
|
||||
}
|
||||
}
|
||||
30
src/Handler/Update/BotRemovedFromChatHandler.php
Normal file
30
src/Handler/Update/BotRemovedFromChatHandler.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Handler\Update;
|
||||
|
||||
use App\Handler\AbstractUpdateHandler;
|
||||
use Pkirillw\MaxBotApi\Api;
|
||||
use Pkirillw\MaxBotApi\Scheme\Update\BotRemovedFromChatUpdate;
|
||||
use Pkirillw\MaxBotApi\Scheme\Update\UpdateInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
final class BotRemovedFromChatHandler extends AbstractUpdateHandler
|
||||
{
|
||||
public function updateClass(): string { return \Pkirillw\MaxBotApi\Scheme\Update\BotRemovedFromChatUpdate::class; }
|
||||
|
||||
public function handle(UpdateInterface $update): void
|
||||
{
|
||||
if (!$update instanceof BotRemovedFromChatUpdate) {
|
||||
return;
|
||||
}
|
||||
$this->logger->info('bot removed from chat', [
|
||||
'chat_id' => $update->chatId,
|
||||
'by_user_id' => $update->user->userId,
|
||||
]);
|
||||
if ($this->api->debugs->getChatId() !== null) {
|
||||
$this->api->debugs->sendText(sprintf('bot removed from chat %d by user %d', $update->chatId, $update->user->userId));
|
||||
}
|
||||
}
|
||||
}
|
||||
51
src/Handler/Update/BotStartedHandler.php
Normal file
51
src/Handler/Update/BotStartedHandler.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Handler\Update;
|
||||
|
||||
use App\Handler\AbstractUpdateHandler;
|
||||
use App\Scenario\HelpScenario;
|
||||
use App\Util\ParsedCommand;
|
||||
use Pkirillw\MaxBotApi\Api;
|
||||
use Pkirillw\MaxBotApi\Scheme\Update\BotStartedUpdate;
|
||||
use Pkirillw\MaxBotApi\Scheme\Update\UpdateInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
final class BotStartedHandler extends AbstractUpdateHandler
|
||||
{
|
||||
public function updateClass(): string { return \Pkirillw\MaxBotApi\Scheme\Update\BotStartedUpdate::class; }
|
||||
|
||||
public function __construct(
|
||||
Api $api,
|
||||
LoggerInterface $logger,
|
||||
\App\Keyboard\KeyboardFactory $keyboards,
|
||||
private HelpScenario $help,
|
||||
) {
|
||||
parent::__construct($api, $logger, $keyboards);
|
||||
}
|
||||
|
||||
public function handle(UpdateInterface $update): void
|
||||
{
|
||||
if (!$update instanceof BotStartedUpdate) {
|
||||
return;
|
||||
}
|
||||
$this->logger->info('bot started', [
|
||||
'chat_id' => $update->chatId,
|
||||
'user_id' => $update->user->userId,
|
||||
'payload' => $update->payload,
|
||||
]);
|
||||
|
||||
// Build a synthetic MessageCreatedUpdate-like ParsedCommand so HelpScenario handles it.
|
||||
$dummy = new \Pkirillw\MaxBotApi\Scheme\Update\MessageCreatedUpdate(
|
||||
message: $update->user->userId ? new \Pkirillw\MaxBotApi\Scheme\Message(
|
||||
sender: $update->user,
|
||||
recipient: new \Pkirillw\MaxBotApi\Scheme\Recipient(chatId: $update->chatId, userId: $update->user->userId),
|
||||
) : new \Pkirillw\MaxBotApi\Scheme\Message(),
|
||||
timestamp: $update->timestamp,
|
||||
debugRaw: $update->debugRaw,
|
||||
);
|
||||
$command = new ParsedCommand(name: 'help', args: [], raw: '/help', tail: '');
|
||||
$this->help->handle($dummy, $command);
|
||||
}
|
||||
}
|
||||
30
src/Handler/Update/BotStopedFromChatHandler.php
Normal file
30
src/Handler/Update/BotStopedFromChatHandler.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Handler\Update;
|
||||
|
||||
use App\Handler\AbstractUpdateHandler;
|
||||
use Pkirillw\MaxBotApi\Api;
|
||||
use Pkirillw\MaxBotApi\Scheme\Update\BotStopedFromChatUpdate;
|
||||
use Pkirillw\MaxBotApi\Scheme\Update\UpdateInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
final class BotStopedFromChatHandler extends AbstractUpdateHandler
|
||||
{
|
||||
public function updateClass(): string { return \Pkirillw\MaxBotApi\Scheme\Update\BotStopedFromChatUpdate::class; }
|
||||
|
||||
public function handle(UpdateInterface $update): void
|
||||
{
|
||||
if (!$update instanceof BotStopedFromChatUpdate) {
|
||||
return;
|
||||
}
|
||||
$this->logger->info('bot stopped (blocked) by user', [
|
||||
'chat_id' => $update->chatId,
|
||||
'user_id' => $update->user->userId,
|
||||
]);
|
||||
if ($this->api->debugs->getChatId() !== null) {
|
||||
$this->api->debugs->sendText(sprintf('user %d stopped the bot', $update->user->userId));
|
||||
}
|
||||
}
|
||||
}
|
||||
36
src/Handler/Update/ChatTitleChangedHandler.php
Normal file
36
src/Handler/Update/ChatTitleChangedHandler.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Handler\Update;
|
||||
|
||||
use App\Handler\AbstractUpdateHandler;
|
||||
use Pkirillw\MaxBotApi\Api;
|
||||
use Pkirillw\MaxBotApi\Builder\Message as MessageBuilder;
|
||||
use Pkirillw\MaxBotApi\Scheme\Enum\Format;
|
||||
use Pkirillw\MaxBotApi\Scheme\Update\ChatTitleChangedUpdate;
|
||||
use Pkirillw\MaxBotApi\Scheme\Update\UpdateInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
final class ChatTitleChangedHandler extends AbstractUpdateHandler
|
||||
{
|
||||
public function updateClass(): string { return \Pkirillw\MaxBotApi\Scheme\Update\ChatTitleChangedUpdate::class; }
|
||||
|
||||
public function handle(UpdateInterface $update): void
|
||||
{
|
||||
if (!$update instanceof ChatTitleChangedUpdate) {
|
||||
return;
|
||||
}
|
||||
$this->logger->info('chat title changed', [
|
||||
'chat_id' => $update->chatId,
|
||||
'new_title' => $update->title,
|
||||
'by_user_id' => $update->user->userId,
|
||||
]);
|
||||
$this->api->messages->send(
|
||||
MessageBuilder::new()
|
||||
->setChat($update->chatId)
|
||||
->setText(sprintf('📝 Новый заголовок чата: *%s*', $update->title))
|
||||
->setFormat(Format::Markdown),
|
||||
);
|
||||
}
|
||||
}
|
||||
31
src/Handler/Update/DialogClearedHandler.php
Normal file
31
src/Handler/Update/DialogClearedHandler.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Handler\Update;
|
||||
|
||||
use App\Handler\AbstractUpdateHandler;
|
||||
use Pkirillw\MaxBotApi\Api;
|
||||
use Pkirillw\MaxBotApi\Scheme\Update\DialogClearedFromChatUpdate;
|
||||
use Pkirillw\MaxBotApi\Scheme\Update\UpdateInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
final class DialogClearedHandler extends AbstractUpdateHandler
|
||||
{
|
||||
public function updateClass(): string { return \Pkirillw\MaxBotApi\Scheme\Update\DialogClearedFromChatUpdate::class; }
|
||||
|
||||
public function handle(UpdateInterface $update): void
|
||||
{
|
||||
if (!$update instanceof DialogClearedFromChatUpdate) {
|
||||
return;
|
||||
}
|
||||
$this->logger->info('dialog cleared', [
|
||||
'chat_id' => $update->chatId,
|
||||
'by_user_id' => $update->user->userId,
|
||||
]);
|
||||
// Demonstrate Debugs::send(update) — pushes raw payload to the debug chat.
|
||||
if ($this->api->debugs->getChatId() !== null) {
|
||||
$this->api->debugs->send($update);
|
||||
}
|
||||
}
|
||||
}
|
||||
27
src/Handler/Update/DialogRemovedHandler.php
Normal file
27
src/Handler/Update/DialogRemovedHandler.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Handler\Update;
|
||||
|
||||
use App\Handler\AbstractUpdateHandler;
|
||||
use Pkirillw\MaxBotApi\Api;
|
||||
use Pkirillw\MaxBotApi\Scheme\Update\DialogRemovedFromChatUpdate;
|
||||
use Pkirillw\MaxBotApi\Scheme\Update\UpdateInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
final class DialogRemovedHandler extends AbstractUpdateHandler
|
||||
{
|
||||
public function updateClass(): string { return \Pkirillw\MaxBotApi\Scheme\Update\DialogRemovedFromChatUpdate::class; }
|
||||
|
||||
public function handle(UpdateInterface $update): void
|
||||
{
|
||||
if (!$update instanceof DialogRemovedFromChatUpdate) {
|
||||
return;
|
||||
}
|
||||
$this->logger->info('dialog removed', [
|
||||
'chat_id' => $update->chatId,
|
||||
'by_user_id' => $update->user->userId,
|
||||
]);
|
||||
}
|
||||
}
|
||||
49
src/Handler/Update/MessageCallbackHandler.php
Normal file
49
src/Handler/Update/MessageCallbackHandler.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Handler\Update;
|
||||
|
||||
use App\Handler\AbstractUpdateHandler;
|
||||
use App\Keyboard\KeyboardFactory;
|
||||
use Pkirillw\MaxBotApi\Api;
|
||||
use Pkirillw\MaxBotApi\Builder\Message as MessageBuilder;
|
||||
use Pkirillw\MaxBotApi\Scheme\CallbackAnswer;
|
||||
use Pkirillw\MaxBotApi\Scheme\Enum\Format;
|
||||
use Pkirillw\MaxBotApi\Scheme\Update\MessageCallbackUpdate;
|
||||
use Pkirillw\MaxBotApi\Scheme\Update\UpdateInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
final class MessageCallbackHandler extends AbstractUpdateHandler
|
||||
{
|
||||
public function updateClass(): string { return \Pkirillw\MaxBotApi\Scheme\Update\MessageCallbackUpdate::class; }
|
||||
|
||||
public function handle(UpdateInterface $update): void
|
||||
{
|
||||
if (!$update instanceof MessageCallbackUpdate) {
|
||||
return;
|
||||
}
|
||||
$callback = $update->callback;
|
||||
$this->logger->info('callback: ' . $callback->payload, [
|
||||
'callback_id' => $callback->callbackId,
|
||||
'user_id' => $callback->user?->userId ?? 0,
|
||||
]);
|
||||
|
||||
// 1. answerOnCallback — show a notification toast.
|
||||
$this->api->messages->answerOnCallback(
|
||||
$callback->callbackId,
|
||||
new CallbackAnswer(notification: 'Нажата кнопка: ' . $callback->payload),
|
||||
);
|
||||
|
||||
// 2. also send a regular chat message so the interaction is visible in history.
|
||||
$chatId = $update->getChatId();
|
||||
if ($chatId > 0) {
|
||||
$this->api->messages->send(
|
||||
MessageBuilder::new()
|
||||
->setChat($chatId)
|
||||
->setText(sprintf('*Клик по кнопке*\npayload: `%s`', $callback->payload))
|
||||
->setFormat(Format::Markdown),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
59
src/Handler/Update/MessageCreatedHandler.php
Normal file
59
src/Handler/Update/MessageCreatedHandler.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Handler\Update;
|
||||
|
||||
use App\Handler\AbstractUpdateHandler;
|
||||
use App\Keyboard\KeyboardFactory;
|
||||
use App\Scenario\HelpScenario;
|
||||
use App\Util\CommandRouter;
|
||||
use App\Util\ParsedCommand;
|
||||
use Pkirillw\MaxBotApi\Api;
|
||||
use Pkirillw\MaxBotApi\Scheme\Update\MessageCreatedUpdate;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
final class MessageCreatedHandler extends AbstractUpdateHandler
|
||||
{
|
||||
public function updateClass(): string { return \Pkirillw\MaxBotApi\Scheme\Update\MessageCreatedUpdate::class; }
|
||||
|
||||
public function __construct(
|
||||
Api $api,
|
||||
LoggerInterface $logger,
|
||||
KeyboardFactory $keyboards,
|
||||
private CommandRouter $router,
|
||||
private HelpScenario $help,
|
||||
) {
|
||||
parent::__construct($api, $logger, $keyboards);
|
||||
}
|
||||
|
||||
public function handle(\Pkirillw\MaxBotApi\Scheme\Update\UpdateInterface $update): void
|
||||
{
|
||||
if (!$update instanceof MessageCreatedUpdate) {
|
||||
return;
|
||||
}
|
||||
$command = ParsedCommand::parse($update->getText());
|
||||
|
||||
if ($command === null) {
|
||||
// Free-text message — echo it back so the bot feels alive in dialog mode.
|
||||
$senderName = $update->message->sender?->name ?? 'незнакомец';
|
||||
$this->api->messages->send(
|
||||
\Pkirillw\MaxBotApi\Builder\Message::new()
|
||||
->setChat($update->getChatId())
|
||||
->setText(sprintf('Привет, %s! Я бот-демо. Набери /help для списка команд.', $senderName)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->logger->info('command: ' . $command->name, ['args' => $command->args]);
|
||||
|
||||
if ($command->name === 'start') {
|
||||
$this->help->handle($update, $command);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->router->route($update, $command)) {
|
||||
$this->help->handleUnknown($update, $command);
|
||||
}
|
||||
}
|
||||
}
|
||||
33
src/Handler/Update/MessageEditedHandler.php
Normal file
33
src/Handler/Update/MessageEditedHandler.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Handler\Update;
|
||||
|
||||
use App\Handler\AbstractUpdateHandler;
|
||||
use Pkirillw\MaxBotApi\Api;
|
||||
use Pkirillw\MaxBotApi\Builder\Message as MessageBuilder;
|
||||
use Pkirillw\MaxBotApi\Scheme\Enum\Format;
|
||||
use Pkirillw\MaxBotApi\Scheme\Update\MessageEditedUpdate;
|
||||
use Pkirillw\MaxBotApi\Scheme\Update\UpdateInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
final class MessageEditedHandler extends AbstractUpdateHandler
|
||||
{
|
||||
public function updateClass(): string { return \Pkirillw\MaxBotApi\Scheme\Update\MessageEditedUpdate::class; }
|
||||
|
||||
public function handle(UpdateInterface $update): void
|
||||
{
|
||||
if (!$update instanceof MessageEditedUpdate) {
|
||||
return;
|
||||
}
|
||||
$mid = $update->message->body?->mid ?? '?';
|
||||
$text = $update->message->body?->text ?? '';
|
||||
$this->api->messages->send(
|
||||
MessageBuilder::new()
|
||||
->setChat($update->getChatId())
|
||||
->setText(sprintf('✎ *Отредактировано* (%s): %s', $mid, mb_substr($text, 0, 200)))
|
||||
->setFormat(Format::Markdown),
|
||||
);
|
||||
}
|
||||
}
|
||||
33
src/Handler/Update/MessageRemovedHandler.php
Normal file
33
src/Handler/Update/MessageRemovedHandler.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Handler\Update;
|
||||
|
||||
use App\Handler\AbstractUpdateHandler;
|
||||
use Pkirillw\MaxBotApi\Api;
|
||||
use Pkirillw\MaxBotApi\Builder\Message as MessageBuilder;
|
||||
use Pkirillw\MaxBotApi\Scheme\Update\MessageRemovedUpdate;
|
||||
use Pkirillw\MaxBotApi\Scheme\Update\UpdateInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
final class MessageRemovedHandler extends AbstractUpdateHandler
|
||||
{
|
||||
public function updateClass(): string { return \Pkirillw\MaxBotApi\Scheme\Update\MessageRemovedUpdate::class; }
|
||||
|
||||
public function handle(UpdateInterface $update): void
|
||||
{
|
||||
if (!$update instanceof MessageRemovedUpdate) {
|
||||
return;
|
||||
}
|
||||
$this->logger->info('message removed', [
|
||||
'message_id' => $update->messageId,
|
||||
'chat_id' => $update->chatId,
|
||||
'user_id' => $update->userId,
|
||||
]);
|
||||
// No chat context to reply into reliably here — leave a debug trail.
|
||||
if ($this->api->debugs->getChatId() !== null) {
|
||||
$this->api->debugs->sendText(sprintf('removed: mid=%s user=%d', $update->messageId, $update->userId));
|
||||
}
|
||||
}
|
||||
}
|
||||
37
src/Handler/Update/UserAddedToChatHandler.php
Normal file
37
src/Handler/Update/UserAddedToChatHandler.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Handler\Update;
|
||||
|
||||
use App\Handler\AbstractUpdateHandler;
|
||||
use Pkirillw\MaxBotApi\Api;
|
||||
use Pkirillw\MaxBotApi\Builder\Message as MessageBuilder;
|
||||
use Pkirillw\MaxBotApi\Scheme\Enum\Format;
|
||||
use Pkirillw\MaxBotApi\Scheme\Update\UpdateInterface;
|
||||
use Pkirillw\MaxBotApi\Scheme\Update\UserAddedToChatUpdate;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
final class UserAddedToChatHandler extends AbstractUpdateHandler
|
||||
{
|
||||
public function updateClass(): string { return \Pkirillw\MaxBotApi\Scheme\Update\UserAddedToChatUpdate::class; }
|
||||
|
||||
public function handle(UpdateInterface $update): void
|
||||
{
|
||||
if (!$update instanceof UserAddedToChatUpdate) {
|
||||
return;
|
||||
}
|
||||
$this->logger->info('user added to chat', [
|
||||
'chat_id' => $update->chatId,
|
||||
'added_user_id' => $update->user->userId,
|
||||
'by_inviter_id' => $update->inviterId,
|
||||
]);
|
||||
$name = $update->user->name ?: ('user #' . $update->user->userId);
|
||||
$this->api->messages->send(
|
||||
MessageBuilder::new()
|
||||
->setChat($update->chatId)
|
||||
->setText(sprintf('👋 Добро пожаловать, *%s*!', $name))
|
||||
->setFormat(Format::Markdown),
|
||||
);
|
||||
}
|
||||
}
|
||||
28
src/Handler/Update/UserRemovedFromChatHandler.php
Normal file
28
src/Handler/Update/UserRemovedFromChatHandler.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Handler\Update;
|
||||
|
||||
use App\Handler\AbstractUpdateHandler;
|
||||
use Pkirillw\MaxBotApi\Api;
|
||||
use Pkirillw\MaxBotApi\Scheme\Update\UpdateInterface;
|
||||
use Pkirillw\MaxBotApi\Scheme\Update\UserRemovedFromChatUpdate;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
final class UserRemovedFromChatHandler extends AbstractUpdateHandler
|
||||
{
|
||||
public function updateClass(): string { return \Pkirillw\MaxBotApi\Scheme\Update\UserRemovedFromChatUpdate::class; }
|
||||
|
||||
public function handle(UpdateInterface $update): void
|
||||
{
|
||||
if (!$update instanceof UserRemovedFromChatUpdate) {
|
||||
return;
|
||||
}
|
||||
$this->logger->info('user removed from chat', [
|
||||
'chat_id' => $update->chatId,
|
||||
'removed_user_id' => $update->user->userId,
|
||||
'by_admin_id' => $update->adminId,
|
||||
]);
|
||||
}
|
||||
}
|
||||
17
src/Handler/UpdateHandlerInterface.php
Normal file
17
src/Handler/UpdateHandlerInterface.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Handler;
|
||||
|
||||
use Pkirillw\MaxBotApi\Scheme\Update\UpdateInterface;
|
||||
|
||||
interface UpdateHandlerInterface
|
||||
{
|
||||
/**
|
||||
* @return class-string<UpdateInterface>
|
||||
*/
|
||||
public function updateClass(): string;
|
||||
|
||||
public function handle(UpdateInterface $update): void;
|
||||
}
|
||||
39
src/Handler/UpdateRouter.php
Normal file
39
src/Handler/UpdateRouter.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Handler;
|
||||
|
||||
use Pkirillw\MaxBotApi\Scheme\Update\UpdateInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
final class UpdateRouter
|
||||
{
|
||||
/** @var list<UpdateHandlerInterface> */
|
||||
private array $handlers = [];
|
||||
|
||||
public function __construct(
|
||||
private LoggerInterface $logger,
|
||||
) {}
|
||||
|
||||
public function add(UpdateHandlerInterface $handler): void
|
||||
{
|
||||
$this->handlers[] = $handler;
|
||||
}
|
||||
|
||||
public function dispatch(object $update): void
|
||||
{
|
||||
if (!$update instanceof UpdateInterface) {
|
||||
$this->logger->warning('unsupported update payload: ' . $update::class);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->handlers as $handler) {
|
||||
if ($handler->updateClass() === $update::class) {
|
||||
$handler->safeHandle($update);
|
||||
return;
|
||||
}
|
||||
}
|
||||
$this->logger->warning('no handler registered for ' . $update::class);
|
||||
}
|
||||
}
|
||||
52
src/Keyboard/KeyboardFactory.php
Normal file
52
src/Keyboard/KeyboardFactory.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Keyboard;
|
||||
|
||||
use Pkirillw\MaxBotApi\Builder\Keyboard;
|
||||
use Pkirillw\MaxBotApi\Scheme\Enum\Intent;
|
||||
|
||||
/**
|
||||
* Pre-built keyboards that exercise every button type supported by the library.
|
||||
*/
|
||||
final class KeyboardFactory
|
||||
{
|
||||
public function __construct(private readonly ?string $webAppUrl = null) {}
|
||||
|
||||
/**
|
||||
* Full keyboard — one row per button type, plus a 3-button callback row with all Intents.
|
||||
* OpenApp button is only added when WEB_APP_URL points to a real, bot-registered web app.
|
||||
*/
|
||||
public function full(): Keyboard
|
||||
{
|
||||
$kb = new Keyboard();
|
||||
|
||||
$row = $kb->addRow();
|
||||
$row->addCallback('Callback: Positive', 'cb:positive', Intent::Positive)
|
||||
->addCallback('Callback: Negative', 'cb:negative', Intent::Negative)
|
||||
->addCallback('Callback: Default', 'cb:default', Intent::Default);
|
||||
|
||||
$kb->addRow()->addLink('Link: dev.max.ru', 'https://dev.max.ru');
|
||||
$kb->addRow()->addMessage('Message: send this text');
|
||||
$kb->addRow()->addClipboard('Clipboard: copy token', 'TOKEN-12345');
|
||||
$kb->addRow()->addContact('Request contact');
|
||||
$geo = $kb->addRow();
|
||||
$geo->addGeolocation('Request geo')
|
||||
->addGeolocation('Request geo (quick)', quick: true);
|
||||
if ($this->webAppUrl !== null && $this->webAppUrl !== '') {
|
||||
$kb->addRow()->addOpenApp('Open web app', $this->webAppUrl, 'demo-payload');
|
||||
}
|
||||
|
||||
return $kb;
|
||||
}
|
||||
|
||||
public function simpleCallback(): Keyboard
|
||||
{
|
||||
$kb = new Keyboard();
|
||||
$kb->addRow()
|
||||
->addCallback('Yes', 'vote:yes', Intent::Positive)
|
||||
->addCallback('No', 'vote:no', Intent::Negative);
|
||||
return $kb;
|
||||
}
|
||||
}
|
||||
33
src/Logger/LoggerFactory.php
Normal file
33
src/Logger/LoggerFactory.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Logger;
|
||||
|
||||
use Monolog\Handler\StreamHandler;
|
||||
use Monolog\Logger;
|
||||
use Monolog\Processor\IntrospectionProcessor;
|
||||
use Monolog\Processor\WebProcessor;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
final class LoggerFactory
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $config
|
||||
*/
|
||||
public static function create(array $config): LoggerInterface
|
||||
{
|
||||
$name = 'bot';
|
||||
$debug = (bool) ($config['app.debug'] ?? false);
|
||||
$path = __DIR__ . '/../../storage/logs/app.log';
|
||||
$dir = dirname($path);
|
||||
if (!is_dir($dir)) {
|
||||
mkdir($dir, 0777, true);
|
||||
}
|
||||
$logger = new Logger($name);
|
||||
$logger->pushHandler(new StreamHandler($path, $debug ? Logger::DEBUG : Logger::INFO));
|
||||
$logger->pushHandler(new StreamHandler('php://stderr', $debug ? Logger::DEBUG : Logger::INFO));
|
||||
$logger->pushProcessor(new WebProcessor());
|
||||
return $logger;
|
||||
}
|
||||
}
|
||||
43
src/Scenario/AbstractScenario.php
Normal file
43
src/Scenario/AbstractScenario.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scenario;
|
||||
|
||||
use App\Keyboard\KeyboardFactory;
|
||||
use App\Util\ParsedCommand;
|
||||
use Pkirillw\MaxBotApi\Api;
|
||||
use Pkirillw\MaxBotApi\Builder\Message as MessageBuilder;
|
||||
use Pkirillw\MaxBotApi\Scheme\Enum\Format;
|
||||
use Pkirillw\MaxBotApi\Scheme\Update\MessageCreatedUpdate;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
abstract class AbstractScenario implements ScenarioInterface
|
||||
{
|
||||
public function __construct(
|
||||
protected Api $api,
|
||||
protected LoggerInterface $logger,
|
||||
protected KeyboardFactory $keyboards,
|
||||
) {}
|
||||
|
||||
protected function reply(MessageCreatedUpdate $update, string $text, ?Format $format = null): void
|
||||
{
|
||||
$builder = MessageBuilder::new()
|
||||
->setChat($update->getChatId())
|
||||
->setText($text);
|
||||
if ($format !== null) {
|
||||
$builder->setFormat($format);
|
||||
}
|
||||
$this->api->messages->send($builder);
|
||||
}
|
||||
|
||||
protected function replyMarkdown(MessageCreatedUpdate $update, string $text): void
|
||||
{
|
||||
$this->reply($update, $text, Format::Markdown);
|
||||
}
|
||||
|
||||
protected function err(string $message, ParsedCommand $command): void
|
||||
{
|
||||
$this->logger->error($message, ['command' => $command->name, 'args' => $command->args]);
|
||||
}
|
||||
}
|
||||
63
src/Scenario/AttachmentScenario.php
Normal file
63
src/Scenario/AttachmentScenario.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scenario;
|
||||
|
||||
use App\Util\ParsedCommand;
|
||||
use Pkirillw\MaxBotApi\Builder\Message as MessageBuilder;
|
||||
use Pkirillw\MaxBotApi\Scheme\Enum\Format;
|
||||
use Pkirillw\MaxBotApi\Scheme\Update\MessageCreatedUpdate;
|
||||
|
||||
final class AttachmentScenario extends AbstractScenario
|
||||
{
|
||||
public function handle(MessageCreatedUpdate $update, ParsedCommand $command): void
|
||||
{
|
||||
switch ($command->name) {
|
||||
case 'attach_location':
|
||||
$this->attachLocation($update, $command);
|
||||
return;
|
||||
case 'attach_contact':
|
||||
$this->attachContact($update, $command);
|
||||
return;
|
||||
case 'attach_sticker':
|
||||
$this->attachSticker($update, $command);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private function attachLocation(MessageCreatedUpdate $update, ParsedCommand $command): void
|
||||
{
|
||||
$lat = (float) ($command->arg(0) ?: '55.7558');
|
||||
$lng = (float) ($command->arg(1) ?: '37.6173');
|
||||
$this->api->messages->send(
|
||||
MessageBuilder::new()
|
||||
->setChat($update->getChatId())
|
||||
->setText(sprintf('Location: %.4f, %.4f (Moscow by default)', $lat, $lng))
|
||||
->setFormat(Format::Markdown)
|
||||
->addLocation($lat, $lng),
|
||||
);
|
||||
}
|
||||
|
||||
private function attachContact(MessageCreatedUpdate $update, ParsedCommand $command): void
|
||||
{
|
||||
$name = $command->arg(0, 'Test Contact');
|
||||
$contactId = $command->argInt(1, $update->getUserId());
|
||||
$this->api->messages->send(
|
||||
MessageBuilder::new()
|
||||
->setChat($update->getChatId())
|
||||
->setText('Contact attachment:')
|
||||
->addContact($name, $contactId),
|
||||
);
|
||||
}
|
||||
|
||||
private function attachSticker(MessageCreatedUpdate $update, ParsedCommand $command): void
|
||||
{
|
||||
$code = $command->arg(0, '281');
|
||||
$this->api->messages->send(
|
||||
MessageBuilder::new()
|
||||
->setChat($update->getChatId())
|
||||
->addSticker($code),
|
||||
);
|
||||
}
|
||||
}
|
||||
61
src/Scenario/BotInfoScenario.php
Normal file
61
src/Scenario/BotInfoScenario.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scenario;
|
||||
|
||||
use App\Util\ParsedCommand;
|
||||
use Pkirillw\MaxBotApi\Builder\Message as MessageBuilder;
|
||||
use Pkirillw\MaxBotApi\Scheme\BotPatch;
|
||||
use Pkirillw\MaxBotApi\Scheme\Enum\Format;
|
||||
use Pkirillw\MaxBotApi\Scheme\Update\MessageCreatedUpdate;
|
||||
|
||||
final class BotInfoScenario extends AbstractScenario
|
||||
{
|
||||
public function handle(MessageCreatedUpdate $update, ParsedCommand $command): void
|
||||
{
|
||||
if ($command->name === 'bot') {
|
||||
$this->showBot($update);
|
||||
return;
|
||||
}
|
||||
if ($command->name === 'bot_patch') {
|
||||
$this->patchBot($update, $command);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private function showBot(MessageCreatedUpdate $update): void
|
||||
{
|
||||
$info = $this->api->bots->getBot();
|
||||
$commands = [];
|
||||
foreach ($info->commands as $c) {
|
||||
$commands[] = sprintf(' • /%s — %s', $c->name, $c->description);
|
||||
}
|
||||
$text = sprintf(
|
||||
"*Bot info*\nname: %s\nusername: @%s\nuserId: %d\ndescription: %s\ncommands:\n%s",
|
||||
$info->name,
|
||||
$info->username ?? '—',
|
||||
$info->userId,
|
||||
$info->description ?? '—',
|
||||
$commands === [] ? ' (none)' : implode("\n", $commands),
|
||||
);
|
||||
|
||||
$this->api->messages->send(
|
||||
MessageBuilder::new()
|
||||
->setChat($update->getChatId())
|
||||
->setText($text)
|
||||
->setFormat(Format::Markdown),
|
||||
);
|
||||
}
|
||||
|
||||
private function patchBot(MessageCreatedUpdate $update, ParsedCommand $command): void
|
||||
{
|
||||
$name = $command->arg(0);
|
||||
if ($name === null || $name === '') {
|
||||
$this->replyMarkdown($update, 'Usage: `/bot_patch <newName>`');
|
||||
return;
|
||||
}
|
||||
$patched = $this->api->bots->patchBot(new BotPatch(name: $name));
|
||||
$this->replyMarkdown($update, sprintf('Bot name updated to *%s*', $patched->name));
|
||||
}
|
||||
}
|
||||
168
src/Scenario/ChatAdminScenario.php
Normal file
168
src/Scenario/ChatAdminScenario.php
Normal file
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scenario;
|
||||
|
||||
use App\Util\ParsedCommand;
|
||||
use Pkirillw\MaxBotApi\Builder\Message as MessageBuilder;
|
||||
use Pkirillw\MaxBotApi\Scheme\ChatPatch;
|
||||
use Pkirillw\MaxBotApi\Scheme\Enum\Format;
|
||||
use Pkirillw\MaxBotApi\Scheme\Enum\SenderAction;
|
||||
use Pkirillw\MaxBotApi\Scheme\PinMessageBody;
|
||||
use Pkirillw\MaxBotApi\Scheme\Update\MessageCreatedUpdate;
|
||||
use Pkirillw\MaxBotApi\Scheme\UserIdsList;
|
||||
|
||||
final class ChatAdminScenario extends AbstractScenario
|
||||
{
|
||||
public function handle(MessageCreatedUpdate $update, ParsedCommand $command): void
|
||||
{
|
||||
switch ($command->name) {
|
||||
case 'members':
|
||||
$this->listMembers($update, $command);
|
||||
return;
|
||||
case 'admins':
|
||||
$this->listAdmins($update, $command);
|
||||
return;
|
||||
case 'action':
|
||||
$this->sendAction($update, $command);
|
||||
return;
|
||||
case 'pin':
|
||||
$this->pinMessage($update, $command);
|
||||
return;
|
||||
case 'edit_chat':
|
||||
$this->editChat($update, $command);
|
||||
return;
|
||||
case 'add_member':
|
||||
$this->addMember($update, $command);
|
||||
return;
|
||||
case 'remove_member':
|
||||
$this->removeMember($update, $command);
|
||||
return;
|
||||
case 'leave':
|
||||
$this->leaveChat($update, $command);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private function listMembers(MessageCreatedUpdate $update, ParsedCommand $command): void
|
||||
{
|
||||
$chatId = $command->argInt(0);
|
||||
if ($chatId <= 0) {
|
||||
$this->replyMarkdown($update, 'Usage: `/members <chatId>`');
|
||||
return;
|
||||
}
|
||||
$members = $this->api->chats->getChatMembers($chatId, count: 20);
|
||||
$lines = [];
|
||||
foreach ($members->members as $m) {
|
||||
$lines[] = sprintf('• %d %s @%s owner=%s admin=%s', $m->userId, $m->name, $m->username ?? '', $m->isOwner ? '✓' : '·', $m->isAdmin ? '✓' : '·');
|
||||
}
|
||||
$text = "*Members of $chatId*\n" . ($lines === [] ? '(empty)' : implode("\n", $lines));
|
||||
|
||||
// Also demo getSpecificChatMembers + getChatAdmins.
|
||||
$userIds = array_slice(array_map(fn($m) => $m->userId, $members->members), 0, 3);
|
||||
$specificNote = '';
|
||||
if ($userIds !== []) {
|
||||
$specific = $this->api->chats->getSpecificChatMembers($chatId, $userIds);
|
||||
$specificNote = sprintf("\n\ngetSpecificChatMembers(%s): %d hit(s)", implode(',', $userIds), count($specific->members));
|
||||
}
|
||||
$admins = $this->api->chats->getChatAdmins($chatId);
|
||||
$adminNote = sprintf("\ngetChatAdmins(): %d admin(s)", count($admins->members));
|
||||
|
||||
$this->api->messages->send(
|
||||
MessageBuilder::new()->setChat($update->getChatId())->setText($text . $specificNote . $adminNote)->setFormat(Format::Markdown),
|
||||
);
|
||||
}
|
||||
|
||||
private function listAdmins(MessageCreatedUpdate $update, ParsedCommand $command): void
|
||||
{
|
||||
$chatId = $command->argInt(0);
|
||||
if ($chatId <= 0) {
|
||||
$this->replyMarkdown($update, 'Usage: `/admins <chatId>`');
|
||||
return;
|
||||
}
|
||||
$admins = $this->api->chats->getChatAdmins($chatId);
|
||||
$lines = [];
|
||||
foreach ($admins->members as $m) {
|
||||
$lines[] = sprintf('• %d %s (owner=%s)', $m->userId, $m->name, $m->isOwner ? '✓' : '·');
|
||||
}
|
||||
$this->replyMarkdown($update, "*Admins of $chatId*\n" . ($lines === [] ? '(none)' : implode("\n", $lines)));
|
||||
}
|
||||
|
||||
private function sendAction(MessageCreatedUpdate $update, ParsedCommand $command): void
|
||||
{
|
||||
$chatId = $command->argInt(0);
|
||||
$actionStr = $command->arg(1);
|
||||
if ($chatId <= 0 || $actionStr === null) {
|
||||
$valid = implode(', ', array_map(fn($c) => $c->value, SenderAction::cases()));
|
||||
$this->replyMarkdown($update, "Usage: `/action <chatId> <$valid>`");
|
||||
return;
|
||||
}
|
||||
$action = SenderAction::tryFrom($actionStr);
|
||||
if ($action === null) {
|
||||
$this->replyMarkdown($update, "Unknown action `$actionStr`");
|
||||
return;
|
||||
}
|
||||
$this->api->chats->sendAction($chatId, $action);
|
||||
$this->replyMarkdown($update, "sendAction `$action->value` sent to `$chatId`");
|
||||
}
|
||||
|
||||
private function pinMessage(MessageCreatedUpdate $update, ParsedCommand $command): void
|
||||
{
|
||||
$chatId = $command->argInt(0);
|
||||
$messageId = $command->arg(1);
|
||||
if ($chatId <= 0 || $messageId === null) {
|
||||
$this->replyMarkdown($update, 'Usage: `/pin <chatId> <messageId>`');
|
||||
return;
|
||||
}
|
||||
$this->api->chats->pinMessage($chatId, new PinMessageBody(messageId: $messageId, notify: true));
|
||||
$this->replyMarkdown($update, "Pinned `$messageId` in chat `$chatId`");
|
||||
}
|
||||
|
||||
private function editChat(MessageCreatedUpdate $update, ParsedCommand $command): void
|
||||
{
|
||||
$chatId = $command->argInt(0);
|
||||
$title = $command->arg(1);
|
||||
if ($chatId <= 0 || $title === null) {
|
||||
$this->replyMarkdown($update, 'Usage: `/edit_chat <chatId> <newTitle>`');
|
||||
return;
|
||||
}
|
||||
$chat = $this->api->chats->editChat($chatId, new ChatPatch(title: $title));
|
||||
$this->replyMarkdown($update, "Chat `$chatId` title updated to *$chat->title*");
|
||||
}
|
||||
|
||||
private function addMember(MessageCreatedUpdate $update, ParsedCommand $command): void
|
||||
{
|
||||
$chatId = $command->argInt(0);
|
||||
$userId = $command->argInt(1);
|
||||
if ($chatId <= 0 || $userId <= 0) {
|
||||
$this->replyMarkdown($update, 'Usage: `/add_member <chatId> <userId>`');
|
||||
return;
|
||||
}
|
||||
$result = $this->api->chats->addMember($chatId, new UserIdsList(userIds: [$userId]));
|
||||
$this->replyMarkdown($update, sprintf('addMember result: success=%s', $result->success ? 'true' : 'false'));
|
||||
}
|
||||
|
||||
private function removeMember(MessageCreatedUpdate $update, ParsedCommand $command): void
|
||||
{
|
||||
$chatId = $command->argInt(0);
|
||||
$userId = $command->argInt(1);
|
||||
if ($chatId <= 0 || $userId <= 0) {
|
||||
$this->replyMarkdown($update, 'Usage: `/remove_member <chatId> <userId>`');
|
||||
return;
|
||||
}
|
||||
$result = $this->api->chats->removeMember($chatId, $userId);
|
||||
$this->replyMarkdown($update, sprintf('removeMember result: success=%s', $result->success ? 'true' : 'false'));
|
||||
}
|
||||
|
||||
private function leaveChat(MessageCreatedUpdate $update, ParsedCommand $command): void
|
||||
{
|
||||
$chatId = $command->argInt(0);
|
||||
if ($chatId <= 0) {
|
||||
$this->replyMarkdown($update, 'Usage: `/leave <chatId>`');
|
||||
return;
|
||||
}
|
||||
$result = $this->api->chats->leaveChat($chatId);
|
||||
$this->replyMarkdown($update, sprintf('Left chat %d: success=%s', $chatId, $result->success ? 'true' : 'false'));
|
||||
}
|
||||
}
|
||||
89
src/Scenario/ChatsScenario.php
Normal file
89
src/Scenario/ChatsScenario.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scenario;
|
||||
|
||||
use App\Util\ParsedCommand;
|
||||
use Pkirillw\MaxBotApi\Builder\Message as MessageBuilder;
|
||||
use Pkirillw\MaxBotApi\Scheme\Enum\Format;
|
||||
use Pkirillw\MaxBotApi\Scheme\Update\MessageCreatedUpdate;
|
||||
|
||||
final class ChatsScenario extends AbstractScenario
|
||||
{
|
||||
public function handle(MessageCreatedUpdate $update, ParsedCommand $command): void
|
||||
{
|
||||
switch ($command->name) {
|
||||
case 'chats':
|
||||
$this->listChats($update, $command);
|
||||
return;
|
||||
case 'chat':
|
||||
$this->showChat($update, $command);
|
||||
return;
|
||||
case 'chat_membership':
|
||||
$this->showMembership($update, $command);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private function listChats(MessageCreatedUpdate $update, ParsedCommand $command): void
|
||||
{
|
||||
$count = $command->argInt(0, 10);
|
||||
$list = $this->api->chats->getChats($count);
|
||||
$lines = [];
|
||||
foreach ($list->chats as $chat) {
|
||||
$lines[] = sprintf('• `%d` %s (type=%s, members=%d)', $chat->chatId, $chat->title ?? '—', $chat->type?->value ?? '?', $chat->participantsCount);
|
||||
}
|
||||
$text = "*Chats* (count=$count, marker=" . ($list->marker ?? 'null') . ")\n" . ($lines === [] ? '(empty)' : implode("\n", $lines));
|
||||
$this->api->messages->send(
|
||||
MessageBuilder::new()->setChat($update->getChatId())->setText($text)->setFormat(Format::Markdown),
|
||||
);
|
||||
}
|
||||
|
||||
private function showChat(MessageCreatedUpdate $update, ParsedCommand $command): void
|
||||
{
|
||||
$chatId = $command->argInt(0);
|
||||
if ($chatId <= 0) {
|
||||
$this->replyMarkdown($update, 'Usage: `/chat <chatId>`');
|
||||
return;
|
||||
}
|
||||
$chat = $this->api->chats->getChat($chatId);
|
||||
$text = sprintf(
|
||||
"*Chat %d*\ntitle: %s\ntype: %s\nstatus: %s\nowner: %d\nmembers: %d\npublic: %s\nlink: %s\ndescription: %s",
|
||||
$chat->chatId,
|
||||
$chat->title ?? '—',
|
||||
$chat->type?->value ?? '?',
|
||||
$chat->status?->value ?? '?',
|
||||
$chat->ownerId,
|
||||
$chat->participantsCount,
|
||||
$chat->isPublic ? 'yes' : 'no',
|
||||
$chat->link ?? '—',
|
||||
$chat->description ?? '—',
|
||||
);
|
||||
$this->api->messages->send(
|
||||
MessageBuilder::new()->setChat($update->getChatId())->setText($text)->setFormat(Format::Markdown),
|
||||
);
|
||||
}
|
||||
|
||||
private function showMembership(MessageCreatedUpdate $update, ParsedCommand $command): void
|
||||
{
|
||||
$chatId = $command->argInt(0);
|
||||
if ($chatId <= 0) {
|
||||
$this->replyMarkdown($update, 'Usage: `/chat_membership <chatId>`');
|
||||
return;
|
||||
}
|
||||
$m = $this->api->chats->getChatMembership($chatId);
|
||||
$perms = array_map(fn($p) => $p->value, $m->permissions);
|
||||
$text = sprintf(
|
||||
"*Membership in %d*\nisOwner: %s\nisAdmin: %s\njoinTime: %d\npermissions: %s",
|
||||
$chatId,
|
||||
$m->isOwner ? 'yes' : 'no',
|
||||
$m->isAdmin ? 'yes' : 'no',
|
||||
$m->joinTime,
|
||||
$perms === [] ? '(none)' : '`' . implode('`, `', $perms) . '`',
|
||||
);
|
||||
$this->api->messages->send(
|
||||
MessageBuilder::new()->setChat($update->getChatId())->setText($text)->setFormat(Format::Markdown),
|
||||
);
|
||||
}
|
||||
}
|
||||
55
src/Scenario/DebugScenario.php
Normal file
55
src/Scenario/DebugScenario.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scenario;
|
||||
|
||||
use App\Util\ParsedCommand;
|
||||
use Pkirillw\MaxBotApi\Builder\Message as MessageBuilder;
|
||||
use Pkirillw\MaxBotApi\Scheme\Enum\Format;
|
||||
use Pkirillw\MaxBotApi\Scheme\Update\MessageCreatedUpdate;
|
||||
|
||||
final class DebugScenario extends AbstractScenario
|
||||
{
|
||||
public function handle(MessageCreatedUpdate $update, ParsedCommand $command): void
|
||||
{
|
||||
$mode = $command->arg(0, 'status');
|
||||
$debugChatId = $this->api->debugs->getChatId();
|
||||
|
||||
switch ($mode) {
|
||||
case 'on':
|
||||
$this->api->debugs->withChatId($update->getChatId());
|
||||
$this->replyMarkdown($update, 'Debugs enabled in this chat. Send `/debug raw` to dump next update, `/debug err` to push a synthetic error.');
|
||||
return;
|
||||
case 'off':
|
||||
// We can't null out chatId from outside (no setter back to null); re-create Api via options.
|
||||
// Workaround: send a notice. The library does not currently expose unsetChatId().
|
||||
$this->replyMarkdown($update, 'To disable debugs, set DEBUG_CHAT_ID= in .env and restart the container.');
|
||||
return;
|
||||
case 'raw':
|
||||
if ($debugChatId === null) {
|
||||
$this->replyMarkdown($update, 'Debugs not enabled. Run `/debug on` first.');
|
||||
return;
|
||||
}
|
||||
$this->api->debugs->sendText('raw dump: ' . $update->getDebugRaw());
|
||||
$this->replyMarkdown($update, 'Sent raw update JSON to debug chat.');
|
||||
return;
|
||||
case 'err':
|
||||
if ($debugChatId === null) {
|
||||
$this->replyMarkdown($update, 'Debugs not enabled. Run `/debug on` first.');
|
||||
return;
|
||||
}
|
||||
$this->api->debugs->sendErr(new \RuntimeException('synthetic error from /debug err'));
|
||||
$this->replyMarkdown($update, 'Sent synthetic error to debug chat.');
|
||||
return;
|
||||
case 'status':
|
||||
default:
|
||||
$this->api->messages->send(
|
||||
MessageBuilder::new()
|
||||
->setChat($update->getChatId())
|
||||
->setText(sprintf('Debug chat: %s', $debugChatId === null ? '(disabled)' : "`$debugChatId`"))
|
||||
->setFormat(Format::Markdown),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
101
src/Scenario/HelpScenario.php
Normal file
101
src/Scenario/HelpScenario.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scenario;
|
||||
|
||||
use App\Util\ParsedCommand;
|
||||
use Pkirillw\MaxBotApi\Builder\Keyboard;
|
||||
use Pkirillw\MaxBotApi\Builder\Message as MessageBuilder;
|
||||
use Pkirillw\MaxBotApi\Scheme\Enum\Format;
|
||||
use Pkirillw\MaxBotApi\Scheme\Enum\Intent;
|
||||
use Pkirillw\MaxBotApi\Scheme\Update\MessageCreatedUpdate;
|
||||
|
||||
final class HelpScenario extends AbstractScenario
|
||||
{
|
||||
public function handle(MessageCreatedUpdate $update, ParsedCommand $command): void
|
||||
{
|
||||
$keyboard = new Keyboard();
|
||||
$keyboard->addRow()
|
||||
->addCallback('Клавиатуры', 'demo_keyboard', Intent::Positive)
|
||||
->addCallback('Загрузки', 'demo_uploads');
|
||||
$keyboard->addRow()->addLink('Документация MAX', 'https://dev.max.ru');
|
||||
|
||||
$text = <<<'MD'
|
||||
*max-bot-test* — тестовый бот для [`max-bot-api-php`]
|
||||
|
||||
Доступные команды:
|
||||
|
||||
*Bot*
|
||||
/bot — getBot()
|
||||
/bot_patch <name> — patchBot()
|
||||
|
||||
*Chats*
|
||||
/chats [count] — getChats()
|
||||
/chat <id> — getChat() + getChatMembership()
|
||||
/members <id> — getChatMembers() + getChatAdmins()
|
||||
/action <chatId> <typing_on|typing_off|mark_seen|sending_photo|sending_video|sending_audio> — sendAction()
|
||||
/pin <chatId> <messageId> — pinMessage()
|
||||
/edit_chat <chatId> <title> — editChat()
|
||||
/add_member <chatId> <userId> — addMember()
|
||||
/remove_member <chatId> <userId> — removeMember()
|
||||
/leave <chatId> — leaveChat()
|
||||
|
||||
*Messages*
|
||||
/send_text <text> — send() plain
|
||||
/send_markdown <text> — Format::Markdown
|
||||
/send_html <text> — Format::Html
|
||||
/reply <messageId> <text> — setReply()
|
||||
/forward <messageId> — setForward()
|
||||
/edit <messageId> <text> — editMessage()
|
||||
/delete <messageId> — deleteMessage()
|
||||
/messages <chatId> [count] — getMessages()
|
||||
/message <messageId> — getMessage()
|
||||
/markup <userId> — addMarkUp() (mention)
|
||||
/notify <text> — setNotify(false) (silent)
|
||||
/disable_preview <url> — setDisableLinkPreview(true)
|
||||
/keyboard — inline keyboard со всеми 8 типами кнопок
|
||||
|
||||
*Uploads*
|
||||
/upload_photo_file [path] — uploadPhotoFromFile()
|
||||
/upload_photo_url <url> — uploadPhotoFromUrl()
|
||||
/upload_photo_bytes — uploadPhotoFromBytes()
|
||||
/upload_photo_base64 — uploadPhotoFromBase64()
|
||||
/upload_audio [path] — uploadMediaFromFile(Audio)
|
||||
/upload_video [path] — uploadMediaFromFile(Video)
|
||||
/upload_file [path] — uploadMediaFromFile(File)
|
||||
/upload_url <audio|video|file> <url> — uploadMediaFromUrl()
|
||||
|
||||
*Attachments*
|
||||
/attach_location <lat> <lng> — addLocation()
|
||||
/attach_contact <name> <userId> — addContact()
|
||||
/attach_sticker <code> — addSticker()
|
||||
|
||||
*Phone (notify)*
|
||||
/phone_check <phone> — check()
|
||||
/phone_list <phone1,phone2> — listExist()
|
||||
|
||||
*Subscriptions (CLI)*
|
||||
`make subscribe`, `make unsubscribe`, `bin/console subs:list`
|
||||
|
||||
*Debug*
|
||||
/debug <on|off|raw|err> — переключение Debugs + Options::withDebug
|
||||
MD;
|
||||
|
||||
$this->api->messages->send(
|
||||
MessageBuilder::new()
|
||||
->setChat($update->getChatId())
|
||||
->setText($text)
|
||||
->setFormat(Format::Markdown)
|
||||
->addKeyboard($keyboard),
|
||||
);
|
||||
}
|
||||
|
||||
public function handleUnknown(MessageCreatedUpdate $update, ParsedCommand $command): void
|
||||
{
|
||||
$this->replyMarkdown(
|
||||
$update,
|
||||
sprintf('Не знаю команду `%s`. Набери `/help`.', $command->name),
|
||||
);
|
||||
}
|
||||
}
|
||||
24
src/Scenario/KeyboardScenario.php
Normal file
24
src/Scenario/KeyboardScenario.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scenario;
|
||||
|
||||
use App\Util\ParsedCommand;
|
||||
use Pkirillw\MaxBotApi\Builder\Message as MessageBuilder;
|
||||
use Pkirillw\MaxBotApi\Scheme\Enum\Format;
|
||||
use Pkirillw\MaxBotApi\Scheme\Update\MessageCreatedUpdate;
|
||||
|
||||
final class KeyboardScenario extends AbstractScenario
|
||||
{
|
||||
public function handle(MessageCreatedUpdate $update, ParsedCommand $command): void
|
||||
{
|
||||
$this->api->messages->send(
|
||||
MessageBuilder::new()
|
||||
->setChat($update->getChatId())
|
||||
->setText('Клавиатура со всеми типами кнопок. Жми — увидишь callback payload.')
|
||||
->setFormat(Format::Markdown)
|
||||
->addKeyboard($this->keyboards->full()),
|
||||
);
|
||||
}
|
||||
}
|
||||
192
src/Scenario/MessagesScenario.php
Normal file
192
src/Scenario/MessagesScenario.php
Normal file
@@ -0,0 +1,192 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scenario;
|
||||
|
||||
use App\Util\ParsedCommand;
|
||||
use Pkirillw\MaxBotApi\Builder\Message as MessageBuilder;
|
||||
use Pkirillw\MaxBotApi\Scheme\Enum\Format;
|
||||
use Pkirillw\MaxBotApi\Scheme\Update\MessageCreatedUpdate;
|
||||
|
||||
final class MessagesScenario extends AbstractScenario
|
||||
{
|
||||
public function handle(MessageCreatedUpdate $update, ParsedCommand $command): void
|
||||
{
|
||||
switch ($command->name) {
|
||||
case 'send_text':
|
||||
$this->sendText($update, $command);
|
||||
return;
|
||||
case 'send_markdown':
|
||||
$this->sendFormatted($update, $command, Format::Markdown);
|
||||
return;
|
||||
case 'send_html':
|
||||
$this->sendFormatted($update, $command, Format::Html);
|
||||
return;
|
||||
case 'reply':
|
||||
$this->replyToMessage($update, $command);
|
||||
return;
|
||||
case 'forward':
|
||||
$this->forward($update, $command);
|
||||
return;
|
||||
case 'edit':
|
||||
$this->edit($update, $command);
|
||||
return;
|
||||
case 'delete':
|
||||
$this->delete($update, $command);
|
||||
return;
|
||||
case 'messages':
|
||||
$this->listMessages($update, $command);
|
||||
return;
|
||||
case 'message':
|
||||
$this->getMessage($update, $command);
|
||||
return;
|
||||
case 'markup':
|
||||
$this->markup($update, $command);
|
||||
return;
|
||||
case 'notify':
|
||||
$this->silentMessage($update, $command);
|
||||
return;
|
||||
case 'disable_preview':
|
||||
$this->disablePreview($update, $command);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private function sendText(MessageCreatedUpdate $update, ParsedCommand $command): void
|
||||
{
|
||||
$text = $command->tail !== '' ? $command->tail : 'Просто текст.';
|
||||
$this->api->messages->send(MessageBuilder::new()->setChat($update->getChatId())->setText($text));
|
||||
}
|
||||
|
||||
private function sendFormatted(MessageCreatedUpdate $update, ParsedCommand $command, Format $format): void
|
||||
{
|
||||
$text = $command->tail !== '' ? $command->tail : '_empty_';
|
||||
$this->api->messages->send(
|
||||
MessageBuilder::new()->setChat($update->getChatId())->setText($text)->setFormat($format),
|
||||
);
|
||||
}
|
||||
|
||||
private function replyToMessage(MessageCreatedUpdate $update, ParsedCommand $command): void
|
||||
{
|
||||
$messageId = $command->arg(0);
|
||||
$text = $command->tail;
|
||||
if ($messageId === null || str_starts_with($text, $messageId)) {
|
||||
// tail still includes messageId — strip it.
|
||||
$text = trim(implode(' ', array_slice($command->args, 1)));
|
||||
}
|
||||
if ($messageId === null || $text === '') {
|
||||
$this->replyMarkdown($update, 'Usage: `/reply <messageId> <text>`');
|
||||
return;
|
||||
}
|
||||
$this->api->messages->send(
|
||||
MessageBuilder::new()
|
||||
->setChat($update->getChatId())
|
||||
->setReply($text, $messageId),
|
||||
);
|
||||
}
|
||||
|
||||
private function forward(MessageCreatedUpdate $update, ParsedCommand $command): void
|
||||
{
|
||||
$messageId = $command->arg(0);
|
||||
if ($messageId === null) {
|
||||
$this->replyMarkdown($update, 'Usage: `/forward <messageId>`');
|
||||
return;
|
||||
}
|
||||
$this->api->messages->send(
|
||||
MessageBuilder::new()
|
||||
->setChat($update->getChatId())
|
||||
->setForward($messageId),
|
||||
);
|
||||
}
|
||||
|
||||
private function edit(MessageCreatedUpdate $update, ParsedCommand $command): void
|
||||
{
|
||||
$messageId = $command->arg(0);
|
||||
$text = trim(implode(' ', array_slice($command->args, 1)));
|
||||
if ($messageId === null || $text === '') {
|
||||
$this->replyMarkdown($update, 'Usage: `/edit <messageId> <new text>`');
|
||||
return;
|
||||
}
|
||||
$this->api->messages->editMessage(
|
||||
$messageId,
|
||||
MessageBuilder::new()->setChat($update->getChatId())->setText($text),
|
||||
);
|
||||
$this->replyMarkdown($update, "Edited `$messageId`");
|
||||
}
|
||||
|
||||
private function delete(MessageCreatedUpdate $update, ParsedCommand $command): void
|
||||
{
|
||||
$messageId = $command->arg(0);
|
||||
if ($messageId === null) {
|
||||
$this->replyMarkdown($update, 'Usage: `/delete <messageId>`');
|
||||
return;
|
||||
}
|
||||
$result = $this->api->messages->deleteMessage($messageId);
|
||||
$this->replyMarkdown($update, sprintf('deleteMessage(%s): success=%s', $messageId, $result->success ? 'true' : 'false'));
|
||||
}
|
||||
|
||||
private function listMessages(MessageCreatedUpdate $update, ParsedCommand $command): void
|
||||
{
|
||||
$chatId = $command->argInt(0);
|
||||
$count = $command->argInt(1, 5);
|
||||
if ($chatId <= 0) {
|
||||
$this->replyMarkdown($update, 'Usage: `/messages <chatId> [count]`');
|
||||
return;
|
||||
}
|
||||
$list = $this->api->messages->getMessages($chatId, count: $count);
|
||||
$lines = [];
|
||||
foreach ($list->messages as $m) {
|
||||
$lines[] = sprintf('• `%s` %s', $m->body?->mid ?? '?', mb_substr($m->body?->text ?? '', 0, 80));
|
||||
}
|
||||
$this->replyMarkdown($update, "*Messages in $chatId*\n" . ($lines === [] ? '(empty)' : implode("\n", $lines)));
|
||||
}
|
||||
|
||||
private function getMessage(MessageCreatedUpdate $update, ParsedCommand $command): void
|
||||
{
|
||||
$messageId = $command->arg(0);
|
||||
if ($messageId === null) {
|
||||
$this->replyMarkdown($update, 'Usage: `/message <messageId>`');
|
||||
return;
|
||||
}
|
||||
$msg = $this->api->messages->getMessage($messageId);
|
||||
$sender = $msg->sender?->name ?? '—';
|
||||
$text = $msg->body?->text ?? '(no text)';
|
||||
$this->replyMarkdown($update, sprintf("*Message `%s`*\nsender: %s\ntext: %s", $messageId, $sender, $text));
|
||||
}
|
||||
|
||||
private function markup(MessageCreatedUpdate $update, ParsedCommand $command): void
|
||||
{
|
||||
$userId = $command->argInt(0, $update->getUserId());
|
||||
$text = sprintf('Hey user #%d — you are mentioned!', $userId);
|
||||
// Mention the user by adding a markup over the entire text.
|
||||
$this->api->messages->send(
|
||||
MessageBuilder::new()
|
||||
->setChat($update->getChatId())
|
||||
->setText($text)
|
||||
->addMarkUp($userId, 0, mb_strlen($text)),
|
||||
);
|
||||
}
|
||||
|
||||
private function silentMessage(MessageCreatedUpdate $update, ParsedCommand $command): void
|
||||
{
|
||||
$text = $command->tail !== '' ? $command->tail : 'Тихое сообщение (без уведомления).';
|
||||
$this->api->messages->send(
|
||||
MessageBuilder::new()
|
||||
->setChat($update->getChatId())
|
||||
->setText($text)
|
||||
->setNotify(false),
|
||||
);
|
||||
}
|
||||
|
||||
private function disablePreview(MessageCreatedUpdate $update, ParsedCommand $command): void
|
||||
{
|
||||
$url = $command->arg(0, 'https://dev.max.ru');
|
||||
$this->api->messages->send(
|
||||
MessageBuilder::new()
|
||||
->setChat($update->getChatId())
|
||||
->setText('Ссылка без превью: ' . $url)
|
||||
->setDisableLinkPreview(true),
|
||||
);
|
||||
}
|
||||
}
|
||||
56
src/Scenario/PhoneCheckScenario.php
Normal file
56
src/Scenario/PhoneCheckScenario.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scenario;
|
||||
|
||||
use App\Util\ParsedCommand;
|
||||
use Pkirillw\MaxBotApi\Builder\Message as MessageBuilder;
|
||||
use Pkirillw\MaxBotApi\Scheme\Enum\Format;
|
||||
use Pkirillw\MaxBotApi\Scheme\Update\MessageCreatedUpdate;
|
||||
|
||||
final class PhoneCheckScenario extends AbstractScenario
|
||||
{
|
||||
public function handle(MessageCreatedUpdate $update, ParsedCommand $command): void
|
||||
{
|
||||
switch ($command->name) {
|
||||
case 'phone_check':
|
||||
$this->checkPhone($update, $command);
|
||||
return;
|
||||
case 'phone_list':
|
||||
$this->listPhones($update, $command);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private function checkPhone(MessageCreatedUpdate $update, ParsedCommand $command): void
|
||||
{
|
||||
$phone = $command->arg(0, '70001234567');
|
||||
$msg = MessageBuilder::new()
|
||||
->setChat($update->getChatId())
|
||||
->setPhoneNumbers([$phone]);
|
||||
$exists = $this->api->messages->check($msg);
|
||||
$this->replyMarkdown($update, sprintf('`check(%s)` → %s', $phone, $exists ? 'exists' : 'not found'));
|
||||
}
|
||||
|
||||
private function listPhones(MessageCreatedUpdate $update, ParsedCommand $command): void
|
||||
{
|
||||
$raw = $command->arg(0, '70001234567,70009990000');
|
||||
$phones = array_values(array_filter(explode(',', $raw)));
|
||||
$msg = MessageBuilder::new()
|
||||
->setChat($update->getChatId())
|
||||
->setPhoneNumbers($phones);
|
||||
$existing = $this->api->messages->listExist($msg);
|
||||
$this->api->messages->send(
|
||||
MessageBuilder::new()
|
||||
->setChat($update->getChatId())
|
||||
->setText(sprintf(
|
||||
"*listExist*\ninput: `%s`\nfound (%d): `%s`",
|
||||
implode(', ', $phones),
|
||||
count($existing),
|
||||
implode('`, `', $existing),
|
||||
))
|
||||
->setFormat(Format::Markdown),
|
||||
);
|
||||
}
|
||||
}
|
||||
13
src/Scenario/ScenarioInterface.php
Normal file
13
src/Scenario/ScenarioInterface.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scenario;
|
||||
|
||||
use App\Util\ParsedCommand;
|
||||
use Pkirillw\MaxBotApi\Scheme\Update\MessageCreatedUpdate;
|
||||
|
||||
interface ScenarioInterface
|
||||
{
|
||||
public function handle(MessageCreatedUpdate $update, ParsedCommand $command): void;
|
||||
}
|
||||
154
src/Scenario/UploadsScenario.php
Normal file
154
src/Scenario/UploadsScenario.php
Normal file
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scenario;
|
||||
|
||||
use App\Util\ParsedCommand;
|
||||
use Pkirillw\MaxBotApi\Builder\Message as MessageBuilder;
|
||||
use Pkirillw\MaxBotApi\Scheme\Attachment\PhotoTokens;
|
||||
use Pkirillw\MaxBotApi\Scheme\Attachment\UploadedInfo;
|
||||
use Pkirillw\MaxBotApi\Scheme\Enum\Format;
|
||||
use Pkirillw\MaxBotApi\Scheme\Enum\UploadType;
|
||||
use Pkirillw\MaxBotApi\Scheme\Update\MessageCreatedUpdate;
|
||||
|
||||
final class UploadsScenario extends AbstractScenario
|
||||
{
|
||||
private const DEFAULT_PHOTO_URL = 'https://placehold.co/600x400/png';
|
||||
private const DEFAULT_FILE_URL = 'https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf';
|
||||
|
||||
public function handle(MessageCreatedUpdate $update, ParsedCommand $command): void
|
||||
{
|
||||
switch ($command->name) {
|
||||
case 'upload_photo_file':
|
||||
$this->uploadPhotoFromFile($update, $command);
|
||||
return;
|
||||
case 'upload_photo_url':
|
||||
$this->uploadPhotoFromUrl($update, $command);
|
||||
return;
|
||||
case 'upload_photo_bytes':
|
||||
$this->uploadPhotoFromBytes($update);
|
||||
return;
|
||||
case 'upload_photo_base64':
|
||||
$this->uploadPhotoFromBase64($update);
|
||||
return;
|
||||
case 'upload_audio':
|
||||
$this->uploadMediaFromFile($update, $command, UploadType::Audio);
|
||||
return;
|
||||
case 'upload_video':
|
||||
$this->uploadMediaFromFile($update, $command, UploadType::Video);
|
||||
return;
|
||||
case 'upload_file':
|
||||
$this->uploadMediaFromFile($update, $command, UploadType::File);
|
||||
return;
|
||||
case 'upload_url':
|
||||
$this->uploadMediaFromUrl($update, $command);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private function uploadPhotoFromFile(MessageCreatedUpdate $update, ParsedCommand $command): void
|
||||
{
|
||||
$path = $command->arg(0, __DIR__ . '/../../storage/uploads/photo.png');
|
||||
if (!is_file($path)) {
|
||||
$this->replyMarkdown($update, "File not found: `$path`. Положи PNG в storage/uploads/photo.png.");
|
||||
return;
|
||||
}
|
||||
$tokens = $this->api->uploads->uploadPhotoFromFile($path);
|
||||
$this->sendPhotoWithCaption($update, $tokens, 'photo from file');
|
||||
}
|
||||
|
||||
private function uploadPhotoFromUrl(MessageCreatedUpdate $update, ParsedCommand $command): void
|
||||
{
|
||||
$url = $command->arg(0, self::DEFAULT_PHOTO_URL);
|
||||
$tokens = $this->api->uploads->uploadPhotoFromUrl($url);
|
||||
$this->sendPhotoWithCaption($update, $tokens, "photo from $url");
|
||||
}
|
||||
|
||||
private function uploadPhotoFromBytes(MessageCreatedUpdate $update): void
|
||||
{
|
||||
// Synthesize a 1x1 PNG via PNG header bytes (no GD dependency).
|
||||
$png = base64_decode(
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=',
|
||||
strict: true,
|
||||
);
|
||||
$tokens = $this->api->uploads->uploadPhotoFromBytes($png, 'pixel.png');
|
||||
$this->sendPhotoWithCaption($update, $tokens, 'photo from raw bytes');
|
||||
}
|
||||
|
||||
private function uploadPhotoFromBase64(MessageCreatedUpdate $update): void
|
||||
{
|
||||
$base64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
|
||||
$tokens = $this->api->uploads->uploadPhotoFromBase64($base64, 'pixel.png');
|
||||
$this->sendPhotoWithCaption($update, $tokens, 'photo from base64');
|
||||
}
|
||||
|
||||
private function uploadMediaFromFile(MessageCreatedUpdate $update, ParsedCommand $command, UploadType $type): void
|
||||
{
|
||||
$default = __DIR__ . '/../../storage/uploads/' . match ($type) {
|
||||
UploadType::Audio => 'audio.mp3',
|
||||
UploadType::Video => 'video.mp4',
|
||||
UploadType::File => 'doc.pdf',
|
||||
UploadType::Photo => 'photo.png',
|
||||
};
|
||||
$path = $command->arg(0, $default);
|
||||
if (!is_file($path)) {
|
||||
$this->replyMarkdown($update, "File not found: `$path`. Положи файл в storage/uploads/.");
|
||||
return;
|
||||
}
|
||||
$info = $this->api->uploads->uploadMediaFromFile($type, $path);
|
||||
$this->sendMediaWithCaption($update, $info, $type);
|
||||
}
|
||||
|
||||
private function uploadMediaFromUrl(MessageCreatedUpdate $update, ParsedCommand $command): void
|
||||
{
|
||||
$typeStr = $command->arg(0);
|
||||
$url = $command->arg(1, self::DEFAULT_FILE_URL);
|
||||
if ($typeStr === null) {
|
||||
$this->replyMarkdown($update, 'Usage: `/upload_url <audio|video|file> [url]`');
|
||||
return;
|
||||
}
|
||||
$type = UploadType::tryFrom($typeStr);
|
||||
if ($type === null || $type === UploadType::Photo) {
|
||||
$this->replyMarkdown($update, 'Type must be audio, video or file. Use /upload_photo_url for photos.');
|
||||
return;
|
||||
}
|
||||
$info = $this->api->uploads->uploadMediaFromUrl($type, $url);
|
||||
$this->sendMediaWithCaption($update, $info, $type);
|
||||
}
|
||||
|
||||
private function sendPhotoWithCaption(MessageCreatedUpdate $update, PhotoTokens $tokens, string $caption): void
|
||||
{
|
||||
if ($tokens->photos === []) {
|
||||
$this->replyMarkdown($update, 'Upload returned no photo tokens (server may still be processing).');
|
||||
return;
|
||||
}
|
||||
$this->api->messages->send(
|
||||
MessageBuilder::new()
|
||||
->setChat($update->getChatId())
|
||||
->setText($caption)
|
||||
->setFormat(Format::Markdown)
|
||||
->addPhoto($tokens),
|
||||
);
|
||||
}
|
||||
|
||||
private function sendMediaWithCaption(MessageCreatedUpdate $update, UploadedInfo $info, UploadType $type): void
|
||||
{
|
||||
$caption = match ($type) {
|
||||
UploadType::Audio => 'audio upload',
|
||||
UploadType::Video => 'video upload',
|
||||
UploadType::File => 'file upload',
|
||||
UploadType::Photo => 'photo upload',
|
||||
};
|
||||
$builder = MessageBuilder::new()
|
||||
->setChat($update->getChatId())
|
||||
->setText($caption . ' (token=' . $info->token . ')');
|
||||
match ($type) {
|
||||
UploadType::Audio => $builder->addAudio($info),
|
||||
UploadType::Video => $builder->addVideo($info),
|
||||
UploadType::File => $builder->addFile($info),
|
||||
UploadType::Photo => $builder,
|
||||
};
|
||||
$this->api->messages->send($builder);
|
||||
}
|
||||
}
|
||||
37
src/Util/CommandRouter.php
Normal file
37
src/Util/CommandRouter.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Util;
|
||||
|
||||
use App\Scenario\ScenarioInterface;
|
||||
use Pkirillw\MaxBotApi\Scheme\Update\MessageCreatedUpdate;
|
||||
|
||||
final class CommandRouter
|
||||
{
|
||||
/** @var array<string, ScenarioInterface> */
|
||||
private array $scenarios = [];
|
||||
|
||||
public function add(string $commandName, ScenarioInterface $scenario): void
|
||||
{
|
||||
$this->scenarios[$commandName] = $scenario;
|
||||
}
|
||||
|
||||
public function route(MessageCreatedUpdate $update, ParsedCommand $command): bool
|
||||
{
|
||||
$scenario = $this->scenarios[$command->name] ?? null;
|
||||
if ($scenario === null) {
|
||||
return false;
|
||||
}
|
||||
$scenario->handle($update, $command);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public function commands(): array
|
||||
{
|
||||
return array_keys($this->scenarios);
|
||||
}
|
||||
}
|
||||
58
src/Util/ParsedCommand.php
Normal file
58
src/Util/ParsedCommand.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Util;
|
||||
|
||||
final readonly class ParsedCommand
|
||||
{
|
||||
/**
|
||||
* @param list<string> $args
|
||||
*/
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public array $args,
|
||||
public string $raw,
|
||||
public string $tail,
|
||||
) {}
|
||||
|
||||
public function arg(int $index, ?string $default = null): ?string
|
||||
{
|
||||
return $this->args[$index] ?? $default;
|
||||
}
|
||||
|
||||
public function argInt(int $index, int $default = 0): int
|
||||
{
|
||||
$value = $this->args[$index] ?? '';
|
||||
return $value === '' ? $default : (int) $value;
|
||||
}
|
||||
|
||||
public static function parse(string $text): ?self
|
||||
{
|
||||
$text = trim($text);
|
||||
if ($text === '' || !str_starts_with($text, '/')) {
|
||||
return null;
|
||||
}
|
||||
$parts = preg_split('/\s+/', $text) ?: [];
|
||||
$head = array_shift($parts) ?? '';
|
||||
if ($head === '') {
|
||||
return null;
|
||||
}
|
||||
if (str_contains($head, ':')) {
|
||||
[$name, $param] = explode(':', $head, 2) + [1 => ''];
|
||||
if ($param !== '') {
|
||||
array_unshift($parts, $param);
|
||||
}
|
||||
} else {
|
||||
$name = $head;
|
||||
}
|
||||
$name = ltrim($name, '/');
|
||||
$tail = implode(' ', $parts);
|
||||
return new self(
|
||||
name: $name,
|
||||
args: array_values($parts),
|
||||
raw: $text,
|
||||
tail: $tail,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user