Compare commits

...

4 Commits

14 changed files with 265 additions and 70 deletions
+4 -5
View File
@@ -1,5 +1,4 @@
/.gitattributes export-ignore /.* export-ignore
/.gitignore export-ignore /phpunit.xml export-ignore
/.github export-ignore /phpstan.neon export-ignore
/phpunit.xml export-ignore /tests export-ignore
/tests export-ignore
+1 -1
View File
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Enums; namespace BushlanovDev\MaxMessengerBot\Enums;
enum AttachmentType: string enum AttachmentType: string
{ {
case Image = 'image'; case Image = 'image';
case Video = 'video'; case Video = 'video';
+44 -26
View File
@@ -47,6 +47,7 @@ use BushlanovDev\MaxMessengerBot\Models\Markup\StrongMarkup;
use BushlanovDev\MaxMessengerBot\Models\Markup\UnderlineMarkup; use BushlanovDev\MaxMessengerBot\Models\Markup\UnderlineMarkup;
use BushlanovDev\MaxMessengerBot\Models\Markup\UserMentionMarkup; use BushlanovDev\MaxMessengerBot\Models\Markup\UserMentionMarkup;
use BushlanovDev\MaxMessengerBot\Models\Message; use BushlanovDev\MaxMessengerBot\Models\Message;
use BushlanovDev\MaxMessengerBot\Models\MessageBody;
use BushlanovDev\MaxMessengerBot\Models\Result; use BushlanovDev\MaxMessengerBot\Models\Result;
use BushlanovDev\MaxMessengerBot\Models\Subscription; use BushlanovDev\MaxMessengerBot\Models\Subscription;
use BushlanovDev\MaxMessengerBot\Models\UpdateList; use BushlanovDev\MaxMessengerBot\Models\UpdateList;
@@ -136,23 +137,54 @@ class ModelFactory
*/ */
public function createMessage(array $data): Message public function createMessage(array $data): Message
{ {
if (isset($data['body']['attachments']) && is_array($data['body']['attachments'])) { if (isset($data['body']) && is_array($data['body'])) {
$data['body']['attachments'] = array_map( $data['body'] = $this->createMessageBody($data['body']);
[$this, 'createAttachment'],
$data['body']['attachments'],
);
}
if (isset($data['body']['markup']) && is_array($data['body']['markup'])) {
$data['body']['markup'] = array_map(
[$this, 'createMarkupElement'],
$data['body']['markup'],
);
} }
return Message::fromArray($data); return Message::fromArray($data);
} }
/**
* List of messages.
*
* @param array<string, mixed> $data
*
* @return Message[]
*/
public function createMessages(array $data): array
{
return isset($data['messages']) && is_array($data['messages'])
? array_map([$this, 'createMessage'], $data['messages'])
: [];
}
/**
* Creates a MessageBody object from raw API data, handling polymorphic attachments and markup.
*
* @param array<string, mixed> $data
*
* @return MessageBody
* @throws ReflectionException
*/
private function createMessageBody(array $data): MessageBody
{
if (isset($data['attachments']) && is_array($data['attachments'])) {
$data['attachments'] = array_map(
[$this, 'createAttachment'],
$data['attachments'],
);
}
if (isset($data['markup']) && is_array($data['markup'])) {
$data['markup'] = array_map(
[$this, 'createMarkupElement'],
$data['markup'],
);
}
return MessageBody::fromArray($data);
}
/** /**
* Creates a specific Attachment model based on the 'type' field. * Creates a specific Attachment model based on the 'type' field.
* *
@@ -237,20 +269,6 @@ class ModelFactory
}; };
} }
/**
* List of messages.
*
* @param array<string, mixed> $data
*
* @return Message[]
*/
public function createMessages(array $data): array
{
return isset($data['messages']) && is_array($data['messages'])
? array_map([$this, 'createMessage'], $data['messages'])
: [];
}
/** /**
* Endpoint you should upload to your binaries. * Endpoint you should upload to your binaries.
* *
@@ -12,7 +12,8 @@ final readonly class InlineKeyboardAttachment extends AbstractAttachment
/** /**
* @param KeyboardPayload $payload Keyboard payload. * @param KeyboardPayload $payload Keyboard payload.
*/ */
public function __construct(public KeyboardPayload $payload) { public function __construct(public KeyboardPayload $payload)
{
parent::__construct(AttachmentType::InlineKeyboard); parent::__construct(AttachmentType::InlineKeyboard);
} }
} }
@@ -14,9 +14,7 @@ final readonly class PhotoToken extends AbstractModel
/** /**
* @param string $token Encoded information of uploaded image. * @param string $token Encoded information of uploaded image.
*/ */
public function __construct( public function __construct(public string $token)
public string $token, {
) {
} }
} }
@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Models\Attachments\Payloads; namespace BushlanovDev\MaxMessengerBot\Models\Attachments\Payloads;
use BushlanovDev\MaxMessengerBot\Attributes\ArrayOf;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Buttons\Reply\AbstractReplyButton; use BushlanovDev\MaxMessengerBot\Models\Attachments\Buttons\Reply\AbstractReplyButton;
final readonly class ReplyKeyboardAttachmentRequestPayload extends AbstractAttachmentRequestPayload final readonly class ReplyKeyboardAttachmentRequestPayload extends AbstractAttachmentRequestPayload
@@ -15,7 +14,6 @@ final readonly class ReplyKeyboardAttachmentRequestPayload extends AbstractAttac
* @param int|null $directUserId If set, reply keyboard will only be shown to this participant. * @param int|null $directUserId If set, reply keyboard will only be shown to this participant.
*/ */
public function __construct( public function __construct(
#[ArrayOf(AbstractReplyButton::class)]
public array $buttons, public array $buttons,
public bool $direct = false, public bool $direct = false,
public ?int $directUserId = null, public ?int $directUserId = null,
@@ -1,19 +1,18 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Models\Attachments\Payloads; namespace BushlanovDev\MaxMessengerBot\Models\Attachments\Payloads;
/** /**
* Payload for attachments that are uploaded to the server first (video, audio, file). * Payload for attachments that are uploaded to the server first (video, audio, file).
*/ */
final readonly class UploadedInfoAttachmentRequestPayload extends AbstractAttachmentRequestPayload final readonly class UploadedInfoAttachmentRequestPayload extends AbstractAttachmentRequestPayload
{ {
/** /**
* @param string $token The unique token received after a successful file upload. * @param string $token The unique token received after a successful file upload.
*/ */
public function __construct( public function __construct(public string $token)
public string $token, {
) { }
} }
}
@@ -12,7 +12,8 @@ final readonly class ReplyKeyboardAttachment extends AbstractAttachment
/** /**
* @param AbstractReplyButton[][] $buttons * @param AbstractReplyButton[][] $buttons
*/ */
public function __construct(public array $buttons) { public function __construct(public array $buttons)
{
parent::__construct(AttachmentType::ReplyKeyboard); parent::__construct(AttachmentType::ReplyKeyboard);
} }
} }
+2 -2
View File
@@ -11,8 +11,8 @@ final readonly class Message extends AbstractModel
{ {
/** /**
* @param int $timestamp Unix-time when message was created. * @param int $timestamp Unix-time when message was created.
* @param MessageBody $body Body of created message. Text + attachments.
* @param Recipient $recipient Message recipient. Could be user or chat. * @param Recipient $recipient Message recipient. Could be user or chat.
* @param MessageBody|null $body Body of created message. Text + attachments.
* @param User|null $sender User who sent this message. Can be null if message has been posted on behalf of a channel. * @param User|null $sender User who sent this message. Can be null if message has been posted on behalf of a channel.
* @param string|null $url Message public URL. Can be null for dialogs or non-public chats/channels. * @param string|null $url Message public URL. Can be null for dialogs or non-public chats/channels.
* @param LinkedMessage|null $link Forwarded or replied message. * @param LinkedMessage|null $link Forwarded or replied message.
@@ -20,8 +20,8 @@ final readonly class Message extends AbstractModel
*/ */
public function __construct( public function __construct(
public int $timestamp, public int $timestamp,
public MessageBody $body,
public Recipient $recipient, public Recipient $recipient,
public ?MessageBody $body,
public ?User $sender, public ?User $sender,
public ?string $url, public ?string $url,
public ?LinkedMessage $link, public ?LinkedMessage $link,
-3
View File
@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Models; namespace BushlanovDev\MaxMessengerBot\Models;
use BushlanovDev\MaxMessengerBot\Attributes\ArrayOf;
use BushlanovDev\MaxMessengerBot\Models\Attachments\AbstractAttachment; use BushlanovDev\MaxMessengerBot\Models\Attachments\AbstractAttachment;
use BushlanovDev\MaxMessengerBot\Models\Markup\AbstractMarkup; use BushlanovDev\MaxMessengerBot\Models\Markup\AbstractMarkup;
@@ -24,9 +23,7 @@ final readonly class MessageBody extends AbstractModel
public string $mid, public string $mid,
public int $seq, public int $seq,
public ?string $text, public ?string $text,
#[ArrayOf(AbstractAttachment::class)]
public ?array $attachments, public ?array $attachments,
#[ArrayOf(AbstractMarkup::class)]
public ?array $markup, public ?array $markup,
) { ) {
} }
@@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Tests\Models\Attachments\Payloads;
use BushlanovDev\MaxMessengerBot\Attributes\ArrayOf;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Payloads\PhotoAttachmentRequestPayload;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Payloads\PhotoToken;
use InvalidArgumentException;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;
#[CoversClass(PhotoAttachmentRequestPayload::class)]
#[UsesClass(PhotoToken::class)]
#[UsesClass(ArrayOf::class)]
final class PhotoAttachmentRequestPayloadTest extends TestCase
{
#[Test]
public function canBeCreatedWithUrlOnly(): void
{
$payload = new PhotoAttachmentRequestPayload(url: 'https://example.com/photo.jpg');
$this->assertSame('https://example.com/photo.jpg', $payload->url);
$this->assertNull($payload->token);
$this->assertNull($payload->photos);
$expectedArray = [
'url' => 'https://example.com/photo.jpg',
'token' => null,
'photos' => null,
];
$this->assertEquals($expectedArray, $payload->toArray());
}
#[Test]
public function canBeCreatedWithTokenOnly(): void
{
$payload = new PhotoAttachmentRequestPayload(token: 'uploaded_token_abc');
$this->assertSame('uploaded_token_abc', $payload->token);
$this->assertNull($payload->url);
$this->assertNull($payload->photos);
$expectedArray = [
'token' => 'uploaded_token_abc',
'url' => null,
'photos' => null,
];
$this->assertEquals($expectedArray, $payload->toArray());
}
#[Test]
public function canBeCreatedWithPhotosOnly(): void
{
$photos = [
new PhotoToken('token_1'),
new PhotoToken('token_2'),
];
$payload = new PhotoAttachmentRequestPayload(photos: $photos);
$this->assertSame($photos, $payload->photos);
$this->assertNull($payload->url);
$this->assertNull($payload->token);
$expectedArray = [
'photos' => [
['token' => 'token_1'],
['token' => 'token_2'],
],
'url' => null,
'token' => null,
];
$this->assertEquals($expectedArray, $payload->toArray());
}
/**
* Data provider for invalid constructor arguments.
*
* @return array<string, array{0: string|null, 1: string|null, 2: array|null}>
*/
public static function invalidPayloadProvider(): array
{
return [
'all null (no arguments)' => [null, null, null],
'url and token provided' => ['https://a.com', 'token123', null],
'url and photos provided' => ['https://a.com', null, [new PhotoToken('t')]],
'token and photos provided' => [null, 'token123', [new PhotoToken('t')]],
'all three arguments provided' => ['https://a.com', 'token123', [new PhotoToken('t')]],
];
}
#[Test]
#[DataProvider('invalidPayloadProvider')]
public function constructorThrowsExceptionForInvalidArguments(
?string $url,
?string $token,
?array $photos
): void {
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Provide exactly one of "url", "token", or "photos" for PhotoAttachmentRequestPayload.');
new PhotoAttachmentRequestPayload($url, $token, $photos);
}
}
+76
View File
@@ -4,12 +4,35 @@ declare(strict_types=1);
namespace BushlanovDev\MaxMessengerBot\Tests\Models; namespace BushlanovDev\MaxMessengerBot\Tests\Models;
use BushlanovDev\MaxMessengerBot\ModelFactory;
use BushlanovDev\MaxMessengerBot\Models\Attachments\AbstractAttachment;
use BushlanovDev\MaxMessengerBot\Models\Attachments\ContactAttachment;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Payloads\ContactAttachmentPayload;
use BushlanovDev\MaxMessengerBot\Models\Attachments\Payloads\PhotoAttachmentPayload;
use BushlanovDev\MaxMessengerBot\Models\Attachments\PhotoAttachment;
use BushlanovDev\MaxMessengerBot\Models\Markup\AbstractMarkup;
use BushlanovDev\MaxMessengerBot\Models\Markup\StrongMarkup;
use BushlanovDev\MaxMessengerBot\Models\Message;
use BushlanovDev\MaxMessengerBot\Models\MessageBody; use BushlanovDev\MaxMessengerBot\Models\MessageBody;
use BushlanovDev\MaxMessengerBot\Models\Recipient;
use BushlanovDev\MaxMessengerBot\Models\User;
use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
#[CoversClass(MessageBody::class)] #[CoversClass(MessageBody::class)]
#[UsesClass(AbstractAttachment::class)]
#[UsesClass(ContactAttachment::class)]
#[UsesClass(ContactAttachmentPayload::class)]
#[UsesClass(PhotoAttachmentPayload::class)]
#[UsesClass(PhotoAttachment::class)]
#[UsesClass(ModelFactory::class)]
#[UsesClass(AbstractMarkup::class)]
#[UsesClass(StrongMarkup::class)]
#[UsesClass(Message::class)]
#[UsesClass(Recipient::class)]
#[UsesClass(User::class)]
final class MessageBodyTest extends TestCase final class MessageBodyTest extends TestCase
{ {
#[Test] #[Test]
@@ -59,4 +82,57 @@ final class MessageBodyTest extends TestCase
$this->assertIsArray($array); $this->assertIsArray($array);
$this->assertSame($data, $array); $this->assertSame($data, $array);
} }
#[Test]
public function createMessageCorrectlyHydratesComplexMessageBody(): void
{
$messageData = [
'timestamp' => time(),
'body' => [
'mid' => 'mid.poly.test',
'seq' => 200,
'text' => 'Message with mixed content',
'attachments' => [
[
'type' => 'contact',
'payload' => [
'vcf_info' => 'vcf...',
'max_info' => [
'user_id' => 1111,
'first_name' => 'aaaaa',
'is_bot' => false,
'last_activity_time' => 1754385571000,
],
],
],
[
'type' => 'image',
'payload' => ['photo_id' => 1, 'token' => 't', 'url' => 'u'],
]
],
'markup' => [
['type' => 'strong', 'from' => 0, 'length' => 7],
]
],
'recipient' => ['chat_type' => 'dialog', 'user_id' => 123],
];
$factory = new ModelFactory();
$message = $factory->createMessage($messageData);
$this->assertInstanceOf(Message::class, $message);
$this->assertInstanceOf(MessageBody::class, $message->body);
$attachments = $message->body->attachments;
$this->assertIsArray($attachments);
$this->assertCount(2, $attachments);
$this->assertInstanceOf(ContactAttachment::class, $attachments[0]);
$this->assertInstanceOf(PhotoAttachment::class, $attachments[1]);
$markup = $message->body->markup;
$this->assertIsArray($markup);
$this->assertCount(1, $markup);
$this->assertInstanceOf(StrongMarkup::class, $markup[0]);
$this->assertSame(0, $markup[0]->from);
}
} }
+5 -5
View File
@@ -28,6 +28,11 @@ final class MessageTest extends TestCase
{ {
$data = [ $data = [
'timestamp' => time(), 'timestamp' => time(),
'recipient' => [
'chat_type' => 'dialog',
'user_id' => 123,
'chat_id' => null,
],
'body' => [ 'body' => [
'mid' => 'mid.456.xyz', 'mid' => 'mid.456.xyz',
'seq' => 101, 'seq' => 101,
@@ -35,11 +40,6 @@ final class MessageTest extends TestCase
'attachments' => null, 'attachments' => null,
'markup' => null, 'markup' => null,
], ],
'recipient' => [
'chat_type' => 'dialog',
'user_id' => 123,
'chat_id' => null,
],
'sender' =>[ 'sender' =>[
'user_id' => 123, 'user_id' => 123,
'first_name' => 'John', 'first_name' => 'John',
+1 -1
View File
@@ -79,8 +79,8 @@ final class WebhookHandlerTest extends TestCase
); );
$message = new Message( $message = new Message(
$data['message']['timestamp'], $data['message']['timestamp'],
$messageBody,
$recipient, $recipient,
$messageBody,
null, null,
null, null,
null, null,