Added PhotoAttachment

This commit is contained in:
Alex
2025-07-19 15:40:42 +03:00
parent b9c04bed33
commit 4661edffdf
11 changed files with 420 additions and 14 deletions
+73
View File
@@ -6,18 +6,23 @@ namespace BushlanovDev\MaxMessengerBot;
use BushlanovDev\MaxMessengerBot\Enums\MessageFormat;
use BushlanovDev\MaxMessengerBot\Enums\UpdateType;
use BushlanovDev\MaxMessengerBot\Enums\UploadType;
use BushlanovDev\MaxMessengerBot\Exceptions\ClientApiException;
use BushlanovDev\MaxMessengerBot\Exceptions\NetworkException;
use BushlanovDev\MaxMessengerBot\Exceptions\SerializationException;
use BushlanovDev\MaxMessengerBot\Models\AbstractModel;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Requests\AbstractAttachmentRequest;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Requests\PhotoAttachmentRequest;
use BushlanovDev\MaxMessengerBot\Models\BotInfo;
use BushlanovDev\MaxMessengerBot\Models\Message;
use BushlanovDev\MaxMessengerBot\Models\MessageLink;
use BushlanovDev\MaxMessengerBot\Models\Result;
use BushlanovDev\MaxMessengerBot\Models\Subscription;
use BushlanovDev\MaxMessengerBot\Models\UploadEndpoint;
use InvalidArgumentException;
use LogicException;
use ReflectionException;
use RuntimeException;
/**
* The main entry point for interacting with the Max Bot API.
@@ -37,6 +42,7 @@ class Api
private const string ACTION_ME = '/me';
private const string ACTION_SUBSCRIPTIONS = '/subscriptions';
private const string ACTION_MESSAGES = '/messages';
private const string ACTION_UPLOADS = '/uploads';
private readonly ClientApiInterface $client;
@@ -204,4 +210,71 @@ class Api
return $this->modelFactory->createMessage($response['message']);
}
/**
* Returns the URL for the subsequent file upload.
*
* @param UploadType $type Uploaded file type.
*
* @return UploadEndpoint Endpoint you should upload to your binaries.
* @throws ReflectionException
*/
public function getUploadUrl(UploadType $type): UploadEndpoint
{
return $this->modelFactory->createUploadEndpoint(
$this->client->request(
self::METHOD_POST,
self::ACTION_UPLOADS,
['type' => $type->value],
)
);
}
/**
* A simplified method for uploading a file and getting the resulting attachment object.
*
* @param UploadType $type Uploaded file type.
* @param string $filePath Path to the file on the local disk.
*
* @return AbstractAttachmentRequest
* @throws ReflectionException
* @throws SerializationException
* @throws InvalidArgumentException
* @throws RuntimeException
* @throws LogicException
* @throws NetworkException
* @throws ClientApiException
*/
public function uploadAttachment(UploadType $type, string $filePath): AbstractAttachmentRequest
{
if (!file_exists($filePath) || !is_readable($filePath)) {
throw new InvalidArgumentException("File not found or not readable: $filePath");
}
$fileHandle = fopen($filePath, 'r');
if ($fileHandle === false) {
throw new RuntimeException("Could not open file for reading: $filePath");
}
$uploadEndpoint = $this->getUploadUrl($type);
$uploadResult = $this->client->upload(
$uploadEndpoint->url,
$fileHandle,
basename($filePath),
);
fclose($fileHandle);
if (!isset($uploadResult['token'])) {
throw new SerializationException('Could not find "token" in upload server response.');
}
return match ($type) {
UploadType::Image => PhotoAttachmentRequest::fromToken($uploadResult['token']),
default => throw new LogicException(
"Attachment creation for type '$type->value' is not yet implemented."
),
};
}
}
+42
View File
@@ -100,6 +100,48 @@ final class Client implements ClientApiInterface
}
}
/**
* @inheritDoc
*/
public function upload(string $uri, mixed $fileContents, string $fileName): array
{
$boundary = '--------------------------' . microtime(true);
$bodyStream = $this->streamFactory->createStream();
$bodyStream->write("--$boundary\r\n");
$bodyStream->write("Content-Disposition: form-data; name=\"data\"; filename=\"{$fileName}\"\r\n");
$bodyStream->write("Content-Type: application/octet-stream\r\n\r\n");
if (is_resource($fileContents)) {
$bodyStream->write((string)stream_get_contents($fileContents));
} else {
$bodyStream->write((string)$fileContents);
}
$bodyStream->write("\r\n");
$bodyStream->write("--$boundary--\r\n");
$request = $this->requestFactory
->createRequest('POST', $uri)
->withHeader('Content-Type', 'multipart/form-data; boundary=' . $boundary)
->withBody($bodyStream);
try {
$response = $this->httpClient->sendRequest($request);
} catch (ClientExceptionInterface $e) {
throw new NetworkException($e->getMessage(), $e->getCode(), $e);
}
$this->handleErrorResponse($response);
$responseBody = (string)$response->getBody();
try {
return json_decode($responseBody, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
throw new SerializationException('Failed to decode upload server response JSON.', 0, $e);
}
}
/**
* Checks the response for an error status code and throws a corresponding typed exception.
*
+14
View File
@@ -24,4 +24,18 @@ interface ClientApiInterface
* @throws SerializationException for JSON encoding/decoding failures.
*/
public function request(string $method, string $uri, array $queryParams = [], array $body = []): array;
/**
* Performs a file download at the specified URL.
*
* @param string $uri URL received from the download API.
* @param resource|string $fileContents File content (stream resource or string).
* @param string $fileName The name of the file that will be sent to the server.
*
* @return array<string, mixed>
* @throws ClientApiException
* @throws NetworkException
* @throws SerializationException
*/
public function upload(string $uri, mixed $fileContents, string $fileName): array;
}
+13
View File
@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Enums;
enum UploadType: string
{
case Image = 'image';
case Video = 'video';
case Audio = 'audio';
case File = 'file';
}
+14
View File
@@ -8,6 +8,7 @@ use BushlanovDev\MaxMessengerBot\Models\BotInfo;
use BushlanovDev\MaxMessengerBot\Models\Message;
use BushlanovDev\MaxMessengerBot\Models\Result;
use BushlanovDev\MaxMessengerBot\Models\Subscription;
use BushlanovDev\MaxMessengerBot\Models\UploadEndpoint;
use ReflectionException;
/**
@@ -81,4 +82,17 @@ class ModelFactory
{
return Message::fromArray($data);
}
/**
* Endpoint you should upload to your binaries.
*
* @param array<string, mixed> $data
*
* @return UploadEndpoint
* @throws ReflectionException
*/
public function createUploadEndpoint(array $data): UploadEndpoint
{
return UploadEndpoint::fromArray($data);
}
}
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Models\Attachments\Payloads;
use BushlanovDev\MaxMessengerBot\Attributes\ArrayOf;
use InvalidArgumentException;
/**
* Request to attach image. All fields are mutually exclusive.
*/
final readonly class PhotoAttachmentPayload extends AbstractAttachmentPayload
{
/**
* @param string|null $url Any external image URL you want to attach.
* @param string|null $token Token of any existing attachment.
* @param PhotoToken[]|null $photos Tokens were obtained after uploading images.
*/
public function __construct(
public ?string $url = null,
public ?string $token = null,
#[ArrayOf(PhotoToken::class)]
public ?array $photos = null,
) {
if (count(array_filter([$this->url, $this->token, $this->photos])) !== 1) {
throw new InvalidArgumentException(
'Provide exactly one of "url", "token", or "photos" for PhotoAttachmentPayload.'
);
}
}
}
@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Models\Attachments\Payloads;
use BushlanovDev\MaxMessengerBot\Models\AbstractModel;
/**
* Encoded information of uploaded image
*/
final readonly class PhotoToken extends AbstractModel
{
/**
* @param string $token Encoded information of uploaded image.
*/
public function __construct(
public string $token,
) {
}
}
@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Models\Attachments\Requests;
use BushlanovDev\MaxMessengerBot\Enums\AttachmentType;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Payloads\PhotoAttachmentPayload;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Payloads\PhotoToken;
/**
* Request to attach some data to message.
*/
final readonly class PhotoAttachmentRequest extends AbstractAttachmentRequest
{
/**
* Creates a request to attach an image by URL.
*
* @param string $url
*
* @return PhotoAttachmentRequest
*/
public static function fromUrl(string $url): self
{
return new self(new PhotoAttachmentPayload(url: $url));
}
/**
* Creates a request to attach an image using the token received after uploading.
*
* @param string $token
*
* @return PhotoAttachmentRequest
*/
public static function fromToken(string $token): self
{
return new self(new PhotoAttachmentPayload(token: $token));
}
/**
* Creates a request to attach an image using the tokens received after uploading.
*
* @param PhotoToken[] $photos
*
* @return PhotoAttachmentRequest
*/
public static function fromPhotos(array $photos): self
{
return new self(new PhotoAttachmentPayload(photos: $photos));
}
/**
* @param PhotoAttachmentPayload $payload Request to attach image.
*/
private function __construct(PhotoAttachmentPayload $payload)
{
parent::__construct(AttachmentType::Image, $payload);
}
}
+21
View File
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Models;
/**
* Endpoint you should upload to your binaries
*/
final readonly class UploadEndpoint extends AbstractModel
{
/**
* @param string $url URL to upload.
* @param string|null $token Video or audio token for send message.
*/
public function __construct(
public string $url,
public ?string $token = null,
) {
}
}