feat: max-bot-test initial — full surface coverage
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:
pkirillw
2026-06-18 00:01:06 +03:00
commit 9385fbdd6e
60 changed files with 7191 additions and 0 deletions

View 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);
}
}