From 12f856bd229c58774d6eab8f29870ce0f6c6e456 Mon Sep 17 00:00:00 2001 From: Ilia Agafonov Date: Sat, 19 Sep 2026 02:46:38 +0700 Subject: [PATCH 1/3] Update for Telegram Bot API 10.3 --- README.md | 2 +- src/BotApi.php | 54 ++-- src/Event/BotSubscriptionUpdated.php | 21 ++ src/Event/MessageGenerationStopped.php | 21 ++ src/Method/DeleteEphemeralMessage.php | 36 +++ src/Method/EditEphemeralMessageCaption.php | 67 +++++ src/Method/EditEphemeralMessageMedia.php | 48 ++++ .../EditEphemeralMessageReplyMarkup.php | 42 +++ src/Method/EditEphemeralMessageText.php | 74 +++++ src/Method/PromoteChatMember.php | 5 + src/Method/SendAnimation.php | 6 + src/Method/SendAudio.php | 6 + src/Method/SendContact.php | 6 + src/Method/SendDocument.php | 6 + src/Method/SendLivePhoto.php | 6 + src/Method/SendLocation.php | 10 +- src/Method/SendMessage.php | 6 + src/Method/SendMessageDraft.php | 13 + src/Method/SendPhoto.php | 6 + src/Method/SendRichMessage.php | 6 + src/Method/SendRichMessageDraft.php | 15 +- src/Method/SendSticker.php | 6 + src/Method/SendVenue.php | 6 + src/Method/SendVideo.php | 6 + src/Method/SendVideoNote.php | 6 + src/Method/SendVoice.php | 6 + src/Type/BotCommand.php | 5 + src/Type/BotSubscriptionUpdated.php | 32 +++ src/Type/ChatAdministratorRights.php | 5 + src/Type/ChatFullInfo.php | 5 + src/Type/ChatMemberAdministrator.php | 5 + src/Type/Community.php | 28 ++ src/Type/CommunityChatAdded.php | 21 ++ src/Type/CommunityChatJoined.php | 21 ++ src/Type/CommunityChatRemoved.php | 17 ++ src/Type/DisabledButton.php | 17 ++ src/Type/EphemeralMessageParameters.php | 33 +++ src/Type/InlineKeyboardButton.php | 6 + src/Type/InlineKeyboardMarkup.php | 6 + src/Type/InputMediaVoiceNote.php | 49 ++++ src/Type/InputRichBlock/InputRichBlock.php | 46 ++++ .../InputRichBlock/InputRichBlockAnchor.php | 22 ++ .../InputRichBlockAnimation.php | 30 ++ .../InputRichBlock/InputRichBlockAudio.php | 30 ++ .../InputRichBlockBlockQuotation.php | 37 +++ .../InputRichBlock/InputRichBlockButtons.php | 33 +++ .../InputRichBlock/InputRichBlockCollage.php | 33 +++ .../InputRichBlock/InputRichBlockDetails.php | 42 +++ .../InputRichBlock/InputRichBlockDivider.php | 18 ++ .../InputRichBlock/InputRichBlockDocument.php | 30 ++ ...InputRichBlockExpandableBlockQuotation.php | 36 +++ .../InputRichBlock/InputRichBlockFooter.php | 28 ++ .../InputRichBlock/InputRichBlockList.php | 27 ++ .../InputRichBlock/InputRichBlockListItem.php | 46 ++++ src/Type/InputRichBlock/InputRichBlockMap.php | 45 +++ .../InputRichBlockMathematicalExpression.php | 22 ++ .../InputRichBlockParagraph.php | 28 ++ .../InputRichBlock/InputRichBlockPhoto.php | 30 ++ .../InputRichBlockPreformatted.php | 33 +++ .../InputRichBlockPullQuotation.php | 36 +++ .../InputRichBlockSectionHeading.php | 33 +++ .../InputRichBlockSlideshow.php | 33 +++ .../InputRichBlock/InputRichBlockTable.php | 53 ++++ .../InputRichBlock/InputRichBlockThinking.php | 30 ++ .../InputRichBlock/InputRichBlockVideo.php | 30 ++ .../InputRichBlockVoiceNote.php | 30 ++ src/Type/InputRichMessage.php | 23 +- src/Type/InputRichMessageMedia.php | 27 ++ src/Type/Location.php | 2 +- src/Type/Message.php | 29 +- src/Type/MessageGenerationStopped.php | 31 +++ src/Type/ReplyKeyboardMarkup.php | 6 + src/Type/ReplyParameters.php | 19 +- src/Type/RichBlock/RichBlock.php | 6 + src/Type/RichBlock/RichBlockButtons.php | 33 +++ src/Type/RichBlock/RichBlockCaption.php | 2 +- src/Type/RichBlock/RichBlockDocument.php | 29 ++ .../RichBlockExpandableBlockQuotation.php | 36 +++ src/Type/RichBlock/RichBlockTable.php | 5 + src/Type/RichBlock/RichBlockTableCell.php | 2 +- src/Type/RichMessageButton.php | 89 ++++++ src/Type/RichText/RichText.php | 2 + src/Type/RichText/RichTextButton.php | 24 ++ src/Type/UniqueGiftInfo.php | 20 ++ src/Type/Update.php | 12 + tests/BotApiFileUploadTest.php | 257 ++++++++++++++++++ tests/BotSubscriptionTest.php | 77 ++++++ tests/CommunityTest.php | 76 ++++++ tests/EphemeralMessageTest.php | 185 +++++++++++++ tests/Helper/TestHttpClient.php | 30 ++ tests/Helper/TestPsrFactory.php | 46 ++++ tests/Helper/TestRequest.php | 123 +++++++++ tests/Helper/TestResponse.php | 99 +++++++ tests/Helper/TestStream.php | 115 ++++++++ tests/InputRichBlockTest.php | 180 ++++++++++++ tests/MessageGenerationStoppedTest.php | 113 ++++++++ tests/ReplyMarkupTest.php | 97 +++++++ tests/RichMessageButtonTest.php | 87 ++++++ tests/data/events/botSubscriptionUpdated.json | 8 + tests/data/events/communityChatAdded.json | 11 + tests/data/events/ephemeralMessage.json | 12 + .../data/events/messageGenerationStopped.json | 7 + 102 files changed, 3472 insertions(+), 45 deletions(-) create mode 100644 src/Event/BotSubscriptionUpdated.php create mode 100644 src/Event/MessageGenerationStopped.php create mode 100644 src/Method/DeleteEphemeralMessage.php create mode 100644 src/Method/EditEphemeralMessageCaption.php create mode 100644 src/Method/EditEphemeralMessageMedia.php create mode 100644 src/Method/EditEphemeralMessageReplyMarkup.php create mode 100644 src/Method/EditEphemeralMessageText.php create mode 100644 src/Type/BotSubscriptionUpdated.php create mode 100644 src/Type/Community.php create mode 100644 src/Type/CommunityChatAdded.php create mode 100644 src/Type/CommunityChatJoined.php create mode 100644 src/Type/CommunityChatRemoved.php create mode 100644 src/Type/DisabledButton.php create mode 100644 src/Type/EphemeralMessageParameters.php create mode 100644 src/Type/InputMediaVoiceNote.php create mode 100644 src/Type/InputRichBlock/InputRichBlock.php create mode 100644 src/Type/InputRichBlock/InputRichBlockAnchor.php create mode 100644 src/Type/InputRichBlock/InputRichBlockAnimation.php create mode 100644 src/Type/InputRichBlock/InputRichBlockAudio.php create mode 100644 src/Type/InputRichBlock/InputRichBlockBlockQuotation.php create mode 100644 src/Type/InputRichBlock/InputRichBlockButtons.php create mode 100644 src/Type/InputRichBlock/InputRichBlockCollage.php create mode 100644 src/Type/InputRichBlock/InputRichBlockDetails.php create mode 100644 src/Type/InputRichBlock/InputRichBlockDivider.php create mode 100644 src/Type/InputRichBlock/InputRichBlockDocument.php create mode 100644 src/Type/InputRichBlock/InputRichBlockExpandableBlockQuotation.php create mode 100644 src/Type/InputRichBlock/InputRichBlockFooter.php create mode 100644 src/Type/InputRichBlock/InputRichBlockList.php create mode 100644 src/Type/InputRichBlock/InputRichBlockListItem.php create mode 100644 src/Type/InputRichBlock/InputRichBlockMap.php create mode 100644 src/Type/InputRichBlock/InputRichBlockMathematicalExpression.php create mode 100644 src/Type/InputRichBlock/InputRichBlockParagraph.php create mode 100644 src/Type/InputRichBlock/InputRichBlockPhoto.php create mode 100644 src/Type/InputRichBlock/InputRichBlockPreformatted.php create mode 100644 src/Type/InputRichBlock/InputRichBlockPullQuotation.php create mode 100644 src/Type/InputRichBlock/InputRichBlockSectionHeading.php create mode 100644 src/Type/InputRichBlock/InputRichBlockSlideshow.php create mode 100644 src/Type/InputRichBlock/InputRichBlockTable.php create mode 100644 src/Type/InputRichBlock/InputRichBlockThinking.php create mode 100644 src/Type/InputRichBlock/InputRichBlockVideo.php create mode 100644 src/Type/InputRichBlock/InputRichBlockVoiceNote.php create mode 100644 src/Type/InputRichMessageMedia.php create mode 100644 src/Type/MessageGenerationStopped.php create mode 100644 src/Type/RichBlock/RichBlockButtons.php create mode 100644 src/Type/RichBlock/RichBlockDocument.php create mode 100644 src/Type/RichBlock/RichBlockExpandableBlockQuotation.php create mode 100644 src/Type/RichMessageButton.php create mode 100644 src/Type/RichText/RichTextButton.php create mode 100644 tests/BotApiFileUploadTest.php create mode 100644 tests/BotSubscriptionTest.php create mode 100644 tests/CommunityTest.php create mode 100644 tests/EphemeralMessageTest.php create mode 100644 tests/Helper/TestHttpClient.php create mode 100644 tests/Helper/TestPsrFactory.php create mode 100644 tests/Helper/TestRequest.php create mode 100644 tests/Helper/TestResponse.php create mode 100644 tests/Helper/TestStream.php create mode 100644 tests/InputRichBlockTest.php create mode 100644 tests/MessageGenerationStoppedTest.php create mode 100644 tests/ReplyMarkupTest.php create mode 100644 tests/RichMessageButtonTest.php create mode 100644 tests/data/events/botSubscriptionUpdated.json create mode 100644 tests/data/events/communityChatAdded.json create mode 100644 tests/data/events/ephemeralMessage.json create mode 100644 tests/data/events/messageGenerationStopped.json diff --git a/README.md b/README.md index a221db13..fffb84d8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Telegram Bot API Client for PHP -[![Bot Api 10.1](https://img.shields.io/badge/Bot%20API-10.1-0088cc.svg?style=flat)](https://core.telegram.org/bots/api-changelog#june-11-2026) +[![Bot Api 10.3](https://img.shields.io/badge/Bot%20API-10.3-0088cc.svg?style=flat)](https://core.telegram.org/bots/api-changelog#august-24-2026) ![PHP >=8.2](https://img.shields.io/badge/PHP->=8.2-777bb3.svg?style=flat) ![Tests Status](https://img.shields.io/github/actions/workflow/status/luzrain/telegram-bot-api/tests.yaml?branch=master&label=Tests) ![Downloads](https://img.shields.io/packagist/dt/luzrain/telegram-bot-api?label=Downloads&color=f28d1a) diff --git a/src/BotApi.php b/src/BotApi.php index 67e34e43..ce9989ee 100644 --- a/src/BotApi.php +++ b/src/BotApi.php @@ -8,12 +8,8 @@ use Luzrain\TelegramBotApi\Exception\TelegramApiServerException; use Luzrain\TelegramBotApi\Internal\HttpClient\RequestBuilder; use Luzrain\TelegramBotApi\Method\GetFile; -use Luzrain\TelegramBotApi\Method\SendMediaGroup; -use Luzrain\TelegramBotApi\Method\SendPaidMedia; use Luzrain\TelegramBotApi\Type\File; use Luzrain\TelegramBotApi\Type\InputFile; -use Luzrain\TelegramBotApi\Type\InputMedia; -use Luzrain\TelegramBotApi\Type\InputPaidMedia; use Luzrain\TelegramBotApi\Type\ResponseParameters; use Psr\Http\Client\ClientExceptionInterface; use Psr\Http\Client\ClientInterface; @@ -58,42 +54,36 @@ public function call(Method $method): mixed $multiparts = []; $files = []; - /** @psalm-suppress UndefinedPropertyFetch */ - $extractFiles = static function (InputMedia|InputPaidMedia $inputMedia): array { - $files = []; - if (\property_exists($inputMedia, 'media') && $inputMedia->media instanceof InputFile) { - $files[] = $inputMedia->media; - } - if (\property_exists($inputMedia, 'thumbnail') && $inputMedia->thumbnail instanceof InputFile) { - $files[] = $inputMedia->thumbnail; - } - if (\property_exists($inputMedia, 'photo') && $inputMedia->photo instanceof InputFile) { - $files[] = $inputMedia->photo; - } - if (\property_exists($inputMedia, 'cover') && $inputMedia->cover instanceof InputFile) { - $files[] = $inputMedia->cover; + /** + * Collects every InputFile reachable from a method parameter graph. + * + * Keyed by unique name so that one InputFile instance referenced from two places + * is streamed once. Note that get_object_vars() runs in BotApi scope and therefore + * sees public properties only — every Type property is public by convention. + * + * @param array $files + */ + $collectFiles = static function (mixed $value, array &$files) use (&$collectFiles): void { + if ($value instanceof InputFile) { + $files[$value->getUniqueName()] = $value; + } elseif (\is_array($value)) { + foreach ($value as $item) { + $collectFiles($item, $files); + } + } elseif ($value instanceof Type) { + foreach (\get_object_vars($value) as $property) { + $collectFiles($property, $files); + } } - return $files; }; foreach ($method->getIterator() as $name => $value) { if ($value instanceof InputFile) { $multiparts[$name] = $value->getAttachPath(); - $files[] = $value; + $files[$value->getUniqueName()] = $value; } else { $multiparts[$name] = \is_scalar($value) ? $value : \json_encode($value, JSON_UNESCAPED_UNICODE); - } - - if ($value instanceof InputMedia || $value instanceof InputPaidMedia) { - $files = [...$files, ...$extractFiles($value)]; - } - - /** @psalm-suppress TypeDoesNotContainType */ - if ($name === 'media' && \is_array($value) && ($method instanceof SendMediaGroup || $method instanceof SendPaidMedia)) { - /** @var array $value */ - foreach ($value as $media) { - $files = [...$files, ...$extractFiles($media)]; - } + $collectFiles($value, $files); } } diff --git a/src/Event/BotSubscriptionUpdated.php b/src/Event/BotSubscriptionUpdated.php new file mode 100644 index 00000000..2463f9f0 --- /dev/null +++ b/src/Event/BotSubscriptionUpdated.php @@ -0,0 +1,21 @@ +subscription !== null; + } + + public function executeCallback(Type\Update $update): mixed + { + return $this->callback($update->subscription); + } +} diff --git a/src/Event/MessageGenerationStopped.php b/src/Event/MessageGenerationStopped.php new file mode 100644 index 00000000..ce2aca91 --- /dev/null +++ b/src/Event/MessageGenerationStopped.php @@ -0,0 +1,21 @@ +stoppedMessageGeneration !== null; + } + + public function executeCallback(Type\Update $update): mixed + { + return $this->callback($update->stoppedMessageGeneration); + } +} diff --git a/src/Method/DeleteEphemeralMessage.php b/src/Method/DeleteEphemeralMessage.php new file mode 100644 index 00000000..78b7a9b1 --- /dev/null +++ b/src/Method/DeleteEphemeralMessage.php @@ -0,0 +1,36 @@ + + */ +final class DeleteEphemeralMessage extends Method +{ + protected static string $methodName = 'deleteEphemeralMessage'; + + public function __construct( + /** + * Unique identifier for the target chat or username of the target supergroup in the format @username + */ + protected int|string $chatId, + + /** + * Identifier of the user who received the message + */ + protected int $receiverUserId, + + /** + * Identifier of the ephemeral message to delete + */ + protected int $ephemeralMessageId, + ) { + } +} diff --git a/src/Method/EditEphemeralMessageCaption.php b/src/Method/EditEphemeralMessageCaption.php new file mode 100644 index 00000000..d86b94b8 --- /dev/null +++ b/src/Method/EditEphemeralMessageCaption.php @@ -0,0 +1,67 @@ + + */ +final class EditEphemeralMessageCaption extends Method +{ + protected static string $methodName = 'editEphemeralMessageCaption'; + + public function __construct( + /** + * Unique identifier for the target chat or username of the target supergroup in the format @username + */ + protected int|string $chatId, + + /** + * Identifier of the user who received the message + */ + protected int $receiverUserId, + + /** + * Identifier of the ephemeral message to edit + */ + protected int $ephemeralMessageId, + + /** + * New caption of the message, 0-1024 characters after entities parsing + */ + protected string|null $caption = null, + + /** + * Mode for parsing entities in the message caption. See formatting options for more details. + * + * @see https://core.telegram.org/bots/api#formatting-options + */ + protected string|null $parseMode = null, + + /** + * A JSON-serialized list of special entities that appear in the caption, which can be specified instead of parse_mode + * + * @var list|null + */ + protected array|null $captionEntities = null, + + /** + * Pass True if the caption must be shown above the message media. Supported only for animation, photo and video messages. + */ + protected bool|null $showCaptionAboveMedia = null, + + /** + * A JSON-serialized object for an inline keyboard + */ + protected InlineKeyboardMarkup|null $replyMarkup = null, + ) { + } +} diff --git a/src/Method/EditEphemeralMessageMedia.php b/src/Method/EditEphemeralMessageMedia.php new file mode 100644 index 00000000..2eadc3e7 --- /dev/null +++ b/src/Method/EditEphemeralMessageMedia.php @@ -0,0 +1,48 @@ + + */ +final class EditEphemeralMessageMedia extends Method +{ + protected static string $methodName = 'editEphemeralMessageMedia'; + + public function __construct( + /** + * Unique identifier for the target chat or username of the target supergroup in the format @username + */ + protected int|string $chatId, + + /** + * Identifier of the user who received the message + */ + protected int $receiverUserId, + + /** + * Identifier of the ephemeral message to edit + */ + protected int $ephemeralMessageId, + + /** + * A JSON-serialized object for the new media content of the message + */ + protected InputMedia $media, + + /** + * A JSON-serialized object for an inline keyboard + */ + protected InlineKeyboardMarkup|null $replyMarkup = null, + ) { + } +} diff --git a/src/Method/EditEphemeralMessageReplyMarkup.php b/src/Method/EditEphemeralMessageReplyMarkup.php new file mode 100644 index 00000000..216a4599 --- /dev/null +++ b/src/Method/EditEphemeralMessageReplyMarkup.php @@ -0,0 +1,42 @@ + + */ +final class EditEphemeralMessageReplyMarkup extends Method +{ + protected static string $methodName = 'editEphemeralMessageReplyMarkup'; + + public function __construct( + /** + * Unique identifier for the target chat or username of the target supergroup in the format @username + */ + protected int|string $chatId, + + /** + * Identifier of the user who received the message + */ + protected int $receiverUserId, + + /** + * Identifier of the ephemeral message to edit + */ + protected int $ephemeralMessageId, + + /** + * A JSON-serialized object for an inline keyboard + */ + protected InlineKeyboardMarkup|null $replyMarkup = null, + ) { + } +} diff --git a/src/Method/EditEphemeralMessageText.php b/src/Method/EditEphemeralMessageText.php new file mode 100644 index 00000000..6e88f3ed --- /dev/null +++ b/src/Method/EditEphemeralMessageText.php @@ -0,0 +1,74 @@ + + */ +final class EditEphemeralMessageText extends Method +{ + protected static string $methodName = 'editEphemeralMessageText'; + + public function __construct( + /** + * Unique identifier for the target chat or username of the target supergroup in the format @username + */ + protected int|string $chatId, + + /** + * Identifier of the user who received the message + */ + protected int $receiverUserId, + + /** + * Identifier of the ephemeral message to edit + */ + protected int $ephemeralMessageId, + + /** + * New text of the message, 1-4096 characters after entity parsing; required if rich_message isn't specified + */ + protected string|null $text = null, + + /** + * Mode for parsing entities in the message text. See formatting options for more details. + * + * @see https://core.telegram.org/bots/api#formatting-options + */ + protected string|null $parseMode = null, + + /** + * A JSON-serialized list of special entities that appear in message text, which can be specified instead of parse_mode + * + * @var list|null + */ + protected array|null $entities = null, + + /** + * New rich content of the message; required if text isn't specified + */ + protected InputRichMessage|null $richMessage = null, + + /** + * Link preview generation options for the message + */ + protected LinkPreviewOptions|null $linkPreviewOptions = null, + + /** + * A JSON-serialized object for an inline keyboard + */ + protected InlineKeyboardMarkup|null $replyMarkup = null, + ) { + } +} diff --git a/src/Method/PromoteChatMember.php b/src/Method/PromoteChatMember.php index 5fce0630..c9329434 100644 --- a/src/Method/PromoteChatMember.php +++ b/src/Method/PromoteChatMember.php @@ -115,6 +115,11 @@ public function __construct( * Pass True if the administrator can edit the tags of regular members; for groups and supergroups only */ protected bool|null $canManageTags = null, + + /** + * Pass True if the administrator can manage chat welcome messages or directly send them in the case of bots + */ + protected bool|null $canSendWelcomeMessages = null, ) { } } diff --git a/src/Method/SendAnimation.php b/src/Method/SendAnimation.php index 573b383d..37e5e6fa 100644 --- a/src/Method/SendAnimation.php +++ b/src/Method/SendAnimation.php @@ -5,6 +5,7 @@ namespace Luzrain\TelegramBotApi\Method; use Luzrain\TelegramBotApi\Method; +use Luzrain\TelegramBotApi\Type\EphemeralMessageParameters; use Luzrain\TelegramBotApi\Type\ForceReply; use Luzrain\TelegramBotApi\Type\InlineKeyboardMarkup; use Luzrain\TelegramBotApi\Type\InputFile; @@ -142,6 +143,11 @@ public function __construct( * instructions to remove a reply keyboard or to force a reply from the user. */ protected InlineKeyboardMarkup|ReplyKeyboardMarkup|ReplyKeyboardRemove|ForceReply|null $replyMarkup = null, + + /** + * A JSON-serialized object containing the parameters of the ephemeral message to send + */ + protected EphemeralMessageParameters|null $ephemeralMessageParameters = null, ) { } } diff --git a/src/Method/SendAudio.php b/src/Method/SendAudio.php index 137d76c5..9bbab40d 100644 --- a/src/Method/SendAudio.php +++ b/src/Method/SendAudio.php @@ -5,6 +5,7 @@ namespace Luzrain\TelegramBotApi\Method; use Luzrain\TelegramBotApi\Method; +use Luzrain\TelegramBotApi\Type\EphemeralMessageParameters; use Luzrain\TelegramBotApi\Type\ForceReply; use Luzrain\TelegramBotApi\Type\InlineKeyboardMarkup; use Luzrain\TelegramBotApi\Type\InputFile; @@ -133,6 +134,11 @@ public function __construct( * instructions to remove a reply keyboard or to force a reply from the user. */ protected InlineKeyboardMarkup|ReplyKeyboardMarkup|ReplyKeyboardRemove|ForceReply|null $replyMarkup = null, + + /** + * A JSON-serialized object containing the parameters of the ephemeral message to send + */ + protected EphemeralMessageParameters|null $ephemeralMessageParameters = null, ) { } } diff --git a/src/Method/SendContact.php b/src/Method/SendContact.php index 8c871e0f..c0a0c828 100644 --- a/src/Method/SendContact.php +++ b/src/Method/SendContact.php @@ -5,6 +5,7 @@ namespace Luzrain\TelegramBotApi\Method; use Luzrain\TelegramBotApi\Method; +use Luzrain\TelegramBotApi\Type\EphemeralMessageParameters; use Luzrain\TelegramBotApi\Type\ForceReply; use Luzrain\TelegramBotApi\Type\InlineKeyboardMarkup; use Luzrain\TelegramBotApi\Type\Message; @@ -101,6 +102,11 @@ public function __construct( * instructions to remove a reply keyboard or to force a reply from the user. */ protected InlineKeyboardMarkup|ReplyKeyboardMarkup|ReplyKeyboardRemove|ForceReply|null $replyMarkup = null, + + /** + * A JSON-serialized object containing the parameters of the ephemeral message to send + */ + protected EphemeralMessageParameters|null $ephemeralMessageParameters = null, ) { } } diff --git a/src/Method/SendDocument.php b/src/Method/SendDocument.php index e3e07f72..e05d8c91 100644 --- a/src/Method/SendDocument.php +++ b/src/Method/SendDocument.php @@ -5,6 +5,7 @@ namespace Luzrain\TelegramBotApi\Method; use Luzrain\TelegramBotApi\Method; +use Luzrain\TelegramBotApi\Type\EphemeralMessageParameters; use Luzrain\TelegramBotApi\Type\ForceReply; use Luzrain\TelegramBotApi\Type\InlineKeyboardMarkup; use Luzrain\TelegramBotApi\Type\InputFile; @@ -122,6 +123,11 @@ public function __construct( * instructions to remove a reply keyboard or to force a reply from the user. */ protected InlineKeyboardMarkup|ReplyKeyboardMarkup|ReplyKeyboardRemove|ForceReply|null $replyMarkup = null, + + /** + * A JSON-serialized object containing the parameters of the ephemeral message to send + */ + protected EphemeralMessageParameters|null $ephemeralMessageParameters = null, ) { } } diff --git a/src/Method/SendLivePhoto.php b/src/Method/SendLivePhoto.php index 85919b54..d78a837c 100644 --- a/src/Method/SendLivePhoto.php +++ b/src/Method/SendLivePhoto.php @@ -5,6 +5,7 @@ namespace Luzrain\TelegramBotApi\Method; use Luzrain\TelegramBotApi\Method; +use Luzrain\TelegramBotApi\Type\EphemeralMessageParameters; use Luzrain\TelegramBotApi\Type\ForceReply; use Luzrain\TelegramBotApi\Type\InlineKeyboardMarkup; use Luzrain\TelegramBotApi\Type\InputFile; @@ -125,6 +126,11 @@ public function __construct( * instructions to remove a reply keyboard or to force a reply from the user. */ protected InlineKeyboardMarkup|ReplyKeyboardMarkup|ReplyKeyboardRemove|ForceReply|null $replyMarkup = null, + + /** + * A JSON-serialized object containing the parameters of the ephemeral message to send + */ + protected EphemeralMessageParameters|null $ephemeralMessageParameters = null, ) { } } diff --git a/src/Method/SendLocation.php b/src/Method/SendLocation.php index 83076b7b..aec2cb16 100644 --- a/src/Method/SendLocation.php +++ b/src/Method/SendLocation.php @@ -5,6 +5,7 @@ namespace Luzrain\TelegramBotApi\Method; use Luzrain\TelegramBotApi\Method; +use Luzrain\TelegramBotApi\Type\EphemeralMessageParameters; use Luzrain\TelegramBotApi\Type\ForceReply; use Luzrain\TelegramBotApi\Type\InlineKeyboardMarkup; use Luzrain\TelegramBotApi\Type\Message; @@ -60,8 +61,8 @@ public function __construct( protected float|null $horizontalAccuracy = null, /** - * Period in seconds during which the location will be updated (see Live Locations, should be between 60 and 86400, - * or 0x7FFFFFFF for live locations that can be edited indefinitely + * Period in seconds during which the location will be updated (see Live Locations), must be between 60 and 86400, + * or 0x7FFFFFFF for live locations that can be edited indefinitely. Must be 0 for ephemeral messages. */ protected int|null $livePeriod = null, @@ -113,6 +114,11 @@ public function __construct( * instructions to remove a reply keyboard or to force a reply from the user. */ protected InlineKeyboardMarkup|ReplyKeyboardMarkup|ReplyKeyboardRemove|ForceReply|null $replyMarkup = null, + + /** + * A JSON-serialized object containing the parameters of the ephemeral message to send + */ + protected EphemeralMessageParameters|null $ephemeralMessageParameters = null, ) { } } diff --git a/src/Method/SendMessage.php b/src/Method/SendMessage.php index 3d4e7ce5..3cd26cad 100644 --- a/src/Method/SendMessage.php +++ b/src/Method/SendMessage.php @@ -5,6 +5,7 @@ namespace Luzrain\TelegramBotApi\Method; use Luzrain\TelegramBotApi\Method; +use Luzrain\TelegramBotApi\Type\EphemeralMessageParameters; use Luzrain\TelegramBotApi\Type\ForceReply; use Luzrain\TelegramBotApi\Type\InlineKeyboardMarkup; use Luzrain\TelegramBotApi\Type\LinkPreviewOptions; @@ -107,6 +108,11 @@ public function __construct( * instructions to remove a reply keyboard or to force a reply from the user. */ protected InlineKeyboardMarkup|ReplyKeyboardMarkup|ReplyKeyboardRemove|ForceReply|null $replyMarkup = null, + + /** + * A JSON-serialized object containing the parameters of the ephemeral message to send + */ + protected EphemeralMessageParameters|null $ephemeralMessageParameters = null, ) { } } diff --git a/src/Method/SendMessageDraft.php b/src/Method/SendMessageDraft.php index 63848361..230e74a7 100644 --- a/src/Method/SendMessageDraft.php +++ b/src/Method/SendMessageDraft.php @@ -26,6 +26,7 @@ public function __construct( /** * Unique identifier of the message draft; must be non-zero. Changes to drafts with the same identifier are animated. + * Otherwise, the draft is replaced without animation. */ protected int $draftId, @@ -52,6 +53,18 @@ public function __construct( * @var list|null */ protected array|null $entities = null, + + /** + * Pass True to show the user a button to stop further drafts. + * The bot will receive an Update "stopped_message_generation" if the user presses the button. + */ + protected bool|null $canStop = null, + + /** + * Pass True to keep the draft in the chat when the button is pressed. The draft will still disappear after a short time + * or if the bot sends a message. To fully preserve the partial draft, the bot should send it as a new message. + */ + protected bool|null $keepOnStop = null, ) { } } diff --git a/src/Method/SendPhoto.php b/src/Method/SendPhoto.php index ddf0fd92..313683c6 100644 --- a/src/Method/SendPhoto.php +++ b/src/Method/SendPhoto.php @@ -5,6 +5,7 @@ namespace Luzrain\TelegramBotApi\Method; use Luzrain\TelegramBotApi\Method; +use Luzrain\TelegramBotApi\Type\EphemeralMessageParameters; use Luzrain\TelegramBotApi\Type\ForceReply; use Luzrain\TelegramBotApi\Type\InlineKeyboardMarkup; use Luzrain\TelegramBotApi\Type\InputFile; @@ -119,6 +120,11 @@ public function __construct( * instructions to remove a reply keyboard or to force a reply from the user. */ protected InlineKeyboardMarkup|ReplyKeyboardMarkup|ReplyKeyboardRemove|ForceReply|null $replyMarkup = null, + + /** + * A JSON-serialized object containing the parameters of the ephemeral message to send + */ + protected EphemeralMessageParameters|null $ephemeralMessageParameters = null, ) { } } diff --git a/src/Method/SendRichMessage.php b/src/Method/SendRichMessage.php index 4dd2cc0f..cdbee05e 100644 --- a/src/Method/SendRichMessage.php +++ b/src/Method/SendRichMessage.php @@ -5,6 +5,7 @@ namespace Luzrain\TelegramBotApi\Method; use Luzrain\TelegramBotApi\Method; +use Luzrain\TelegramBotApi\Type\EphemeralMessageParameters; use Luzrain\TelegramBotApi\Type\ForceReply; use Luzrain\TelegramBotApi\Type\InlineKeyboardMarkup; use Luzrain\TelegramBotApi\Type\InputRichMessage; @@ -86,6 +87,11 @@ public function __construct( * Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions to remove a reply keyboard or to force a reply from the user. */ protected InlineKeyboardMarkup|ReplyKeyboardMarkup|ReplyKeyboardRemove|ForceReply|null $replyMarkup = null, + + /** + * A JSON-serialized object containing the parameters of the ephemeral message to send + */ + protected EphemeralMessageParameters|null $ephemeralMessageParameters = null, ) { } } diff --git a/src/Method/SendRichMessageDraft.php b/src/Method/SendRichMessageDraft.php index ca942c22..7836527c 100644 --- a/src/Method/SendRichMessageDraft.php +++ b/src/Method/SendRichMessageDraft.php @@ -26,11 +26,12 @@ public function __construct( /** * Unique identifier of the message draft; must be non-zero. Changes to drafts with the same identifier are animated. + * Otherwise, the draft is replaced without animation. */ protected int $draftId, /** - * The partial message to be streamed + * The partial message to be streamed. Direct upload of new files and explicit upload of files by a URL isn't supported. */ protected InputRichMessage $richMessage, @@ -38,6 +39,18 @@ public function __construct( * Unique identifier for the target message thread */ protected int|null $messageThreadId = null, + + /** + * Pass True to show the user a button to stop further drafts. + * The bot will receive an Update "stopped_message_generation" if the user presses the button. + */ + protected bool|null $canStop = null, + + /** + * Pass True to keep the draft in the chat when the button is pressed. The draft will still disappear after a short time + * or if the bot sends a message. To fully preserve the partial draft, the bot should send it as a new message. + */ + protected bool|null $keepOnStop = null, ) { } } diff --git a/src/Method/SendSticker.php b/src/Method/SendSticker.php index ce6f986a..6a4d3ff9 100644 --- a/src/Method/SendSticker.php +++ b/src/Method/SendSticker.php @@ -5,6 +5,7 @@ namespace Luzrain\TelegramBotApi\Method; use Luzrain\TelegramBotApi\Method; +use Luzrain\TelegramBotApi\Type\EphemeralMessageParameters; use Luzrain\TelegramBotApi\Type\ForceReply; use Luzrain\TelegramBotApi\Type\InlineKeyboardMarkup; use Luzrain\TelegramBotApi\Type\InputFile; @@ -94,6 +95,11 @@ public function __construct( * instructions to remove a reply keyboard or to force a reply from the user. */ protected InlineKeyboardMarkup|ReplyKeyboardMarkup|ReplyKeyboardRemove|ForceReply|null $replyMarkup = null, + + /** + * A JSON-serialized object containing the parameters of the ephemeral message to send + */ + protected EphemeralMessageParameters|null $ephemeralMessageParameters = null, ) { } } diff --git a/src/Method/SendVenue.php b/src/Method/SendVenue.php index 03dc6788..025940ef 100644 --- a/src/Method/SendVenue.php +++ b/src/Method/SendVenue.php @@ -5,6 +5,7 @@ namespace Luzrain\TelegramBotApi\Method; use Luzrain\TelegramBotApi\Method; +use Luzrain\TelegramBotApi\Type\EphemeralMessageParameters; use Luzrain\TelegramBotApi\Type\ForceReply; use Luzrain\TelegramBotApi\Type\InlineKeyboardMarkup; use Luzrain\TelegramBotApi\Type\Message; @@ -121,6 +122,11 @@ public function __construct( * instructions to remove a reply keyboard or to force a reply from the user. */ protected InlineKeyboardMarkup|ReplyKeyboardMarkup|ReplyKeyboardRemove|ForceReply|null $replyMarkup = null, + + /** + * A JSON-serialized object containing the parameters of the ephemeral message to send + */ + protected EphemeralMessageParameters|null $ephemeralMessageParameters = null, ) { } } diff --git a/src/Method/SendVideo.php b/src/Method/SendVideo.php index 9659a2e6..c85c495b 100644 --- a/src/Method/SendVideo.php +++ b/src/Method/SendVideo.php @@ -5,6 +5,7 @@ namespace Luzrain\TelegramBotApi\Method; use Luzrain\TelegramBotApi\Method; +use Luzrain\TelegramBotApi\Type\EphemeralMessageParameters; use Luzrain\TelegramBotApi\Type\ForceReply; use Luzrain\TelegramBotApi\Type\InlineKeyboardMarkup; use Luzrain\TelegramBotApi\Type\InputFile; @@ -159,6 +160,11 @@ public function __construct( * instructions to remove a reply keyboard or to force a reply from the user. */ protected InlineKeyboardMarkup|ReplyKeyboardMarkup|ReplyKeyboardRemove|ForceReply|null $replyMarkup = null, + + /** + * A JSON-serialized object containing the parameters of the ephemeral message to send + */ + protected EphemeralMessageParameters|null $ephemeralMessageParameters = null, ) { } } diff --git a/src/Method/SendVideoNote.php b/src/Method/SendVideoNote.php index a3712ffc..d92575ea 100644 --- a/src/Method/SendVideoNote.php +++ b/src/Method/SendVideoNote.php @@ -5,6 +5,7 @@ namespace Luzrain\TelegramBotApi\Method; use Luzrain\TelegramBotApi\Method; +use Luzrain\TelegramBotApi\Type\EphemeralMessageParameters; use Luzrain\TelegramBotApi\Type\ForceReply; use Luzrain\TelegramBotApi\Type\InlineKeyboardMarkup; use Luzrain\TelegramBotApi\Type\InputFile; @@ -107,6 +108,11 @@ public function __construct( * instructions to remove a reply keyboard or to force a reply from the user. */ protected InlineKeyboardMarkup|ReplyKeyboardMarkup|ReplyKeyboardRemove|ForceReply|null $replyMarkup = null, + + /** + * A JSON-serialized object containing the parameters of the ephemeral message to send + */ + protected EphemeralMessageParameters|null $ephemeralMessageParameters = null, ) { } } diff --git a/src/Method/SendVoice.php b/src/Method/SendVoice.php index 4269b07d..aa28e4fd 100644 --- a/src/Method/SendVoice.php +++ b/src/Method/SendVoice.php @@ -5,6 +5,7 @@ namespace Luzrain\TelegramBotApi\Method; use Luzrain\TelegramBotApi\Method; +use Luzrain\TelegramBotApi\Type\EphemeralMessageParameters; use Luzrain\TelegramBotApi\Type\ForceReply; use Luzrain\TelegramBotApi\Type\InlineKeyboardMarkup; use Luzrain\TelegramBotApi\Type\InputFile; @@ -116,6 +117,11 @@ public function __construct( * instructions to remove a reply keyboard or to force a reply from the user. */ protected InlineKeyboardMarkup|ReplyKeyboardMarkup|ReplyKeyboardRemove|ForceReply|null $replyMarkup = null, + + /** + * A JSON-serialized object containing the parameters of the ephemeral message to send + */ + protected EphemeralMessageParameters|null $ephemeralMessageParameters = null, ) { } } diff --git a/src/Type/BotCommand.php b/src/Type/BotCommand.php index 5f3f54cd..b39ce322 100644 --- a/src/Type/BotCommand.php +++ b/src/Type/BotCommand.php @@ -21,6 +21,11 @@ public function __construct( * Description of the command; 1-256 characters */ public string $description, + + /** + * Optional. True, if the command sends an ephemeral message, which can be seen only by the sender of the message and the bot + */ + public bool|null $isEphemeral = null, ) { } } diff --git a/src/Type/BotSubscriptionUpdated.php b/src/Type/BotSubscriptionUpdated.php new file mode 100644 index 00000000..80b333e4 --- /dev/null +++ b/src/Type/BotSubscriptionUpdated.php @@ -0,0 +1,32 @@ +inlineKeyboard = \is_array($inlineKeyboard) ? $inlineKeyboard : $inlineKeyboard->toArray(); } diff --git a/src/Type/InputMediaVoiceNote.php b/src/Type/InputMediaVoiceNote.php new file mode 100644 index 00000000..ed1d7ba9 --- /dev/null +++ b/src/Type/InputMediaVoiceNote.php @@ -0,0 +1,49 @@ +" + * to upload a new one using multipart/form-data under name. More information on Sending Files » + */ + public InputFile|string $media, + + /** + * Optional. Caption of the voice message to be sent, 0-1024 characters after entities parsing + */ + public string|null $caption = null, + + /** + * Optional. Mode for parsing entities in the voice message caption. See formatting options for more details. + */ + public string|null $parseMode = null, + + /** + * Optional. List of special entities that appear in the caption, which can be specified instead of parse_mode + * + * @var list|null + */ + #[ArrayType(MessageEntity::class)] + public array|null $captionEntities = null, + + /** + * Optional. Duration of the voice message in seconds + */ + public int|null $duration = null, + ) { + parent::__construct(self::TYPE); + } +} diff --git a/src/Type/InputRichBlock/InputRichBlock.php b/src/Type/InputRichBlock/InputRichBlock.php new file mode 100644 index 00000000..dfa23a86 --- /dev/null +++ b/src/Type/InputRichBlock/InputRichBlock.php @@ -0,0 +1,46 @@ + with the attribute name. + */ +final readonly class InputRichBlockAnchor extends InputRichBlock +{ + public const TYPE = 'anchor'; + + public function __construct( + /** + * The name of the anchor + */ + public string $name, + ) { + parent::__construct(self::TYPE); + } +} diff --git a/src/Type/InputRichBlock/InputRichBlockAnimation.php b/src/Type/InputRichBlock/InputRichBlockAnimation.php new file mode 100644 index 00000000..8b138b0c --- /dev/null +++ b/src/Type/InputRichBlock/InputRichBlockAnimation.php @@ -0,0 +1,30 @@ +. + */ +final readonly class InputRichBlockAnimation extends InputRichBlock +{ + public const TYPE = 'animation'; + + public function __construct( + /** + * The animation. Caption is ignored. + */ + public InputMediaAnimation $animation, + + /** + * Optional. Caption of the block + */ + public RichBlockCaption|null $caption = null, + ) { + parent::__construct(self::TYPE); + } +} diff --git a/src/Type/InputRichBlock/InputRichBlockAudio.php b/src/Type/InputRichBlock/InputRichBlockAudio.php new file mode 100644 index 00000000..dfb04f20 --- /dev/null +++ b/src/Type/InputRichBlock/InputRichBlockAudio.php @@ -0,0 +1,30 @@ +. + */ +final readonly class InputRichBlockAudio extends InputRichBlock +{ + public const TYPE = 'audio'; + + public function __construct( + /** + * The audio. Caption is ignored. + */ + public InputMediaAudio $audio, + + /** + * Optional. Caption of the block + */ + public RichBlockCaption|null $caption = null, + ) { + parent::__construct(self::TYPE); + } +} diff --git a/src/Type/InputRichBlock/InputRichBlockBlockQuotation.php b/src/Type/InputRichBlock/InputRichBlockBlockQuotation.php new file mode 100644 index 00000000..74ced471 --- /dev/null +++ b/src/Type/InputRichBlock/InputRichBlockBlockQuotation.php @@ -0,0 +1,37 @@ +. + */ +final readonly class InputRichBlockBlockQuotation extends InputRichBlock +{ + public const TYPE = 'blockquote'; + + public function __construct( + /** + * Content of the block + * + * @var list + */ + #[ArrayType(InputRichBlock::class)] + public array $blocks, + + /** + * Optional. Credit of the block + * + * @var RichText|string|list|null + */ + #[RichTextType] + public RichText|string|array|null $credit = null, + ) { + parent::__construct(self::TYPE); + } +} diff --git a/src/Type/InputRichBlock/InputRichBlockButtons.php b/src/Type/InputRichBlock/InputRichBlockButtons.php new file mode 100644 index 00000000..dda718b3 --- /dev/null +++ b/src/Type/InputRichBlock/InputRichBlockButtons.php @@ -0,0 +1,33 @@ +. + */ +final readonly class InputRichBlockButtons extends InputRichBlock +{ + public const TYPE = 'buttons'; + + public function __construct( + /** + * List of 1-8 buttons to send + * + * @var list + */ + #[ArrayType(RichMessageButton::class)] + public array $buttons, + + /** + * Optional. Horizontal alignment of the buttons. Currently, must be one of "left", "center", or "right". + */ + public string|null $align = null, + ) { + parent::__construct(self::TYPE); + } +} diff --git a/src/Type/InputRichBlock/InputRichBlockCollage.php b/src/Type/InputRichBlock/InputRichBlockCollage.php new file mode 100644 index 00000000..5991e56c --- /dev/null +++ b/src/Type/InputRichBlock/InputRichBlockCollage.php @@ -0,0 +1,33 @@ +. + */ +final readonly class InputRichBlockCollage extends InputRichBlock +{ + public const TYPE = 'collage'; + + public function __construct( + /** + * Elements of the collage + * + * @var list + */ + #[ArrayType(InputRichBlock::class)] + public array $blocks, + + /** + * Optional. Caption of the block + */ + public RichBlockCaption|null $caption = null, + ) { + parent::__construct(self::TYPE); + } +} diff --git a/src/Type/InputRichBlock/InputRichBlockDetails.php b/src/Type/InputRichBlock/InputRichBlockDetails.php new file mode 100644 index 00000000..03e05b37 --- /dev/null +++ b/src/Type/InputRichBlock/InputRichBlockDetails.php @@ -0,0 +1,42 @@ +. + */ +final readonly class InputRichBlockDetails extends InputRichBlock +{ + public const TYPE = 'details'; + + public function __construct( + /** + * Always shown summary of the block + * + * @var RichText|string|list + */ + #[RichTextType] + public RichText|string|array $summary, + + /** + * Content of the block + * + * @var list + */ + #[ArrayType(InputRichBlock::class)] + public array $blocks, + + /** + * Optional. Pass True if the content of the block is visible by default + */ + public bool|null $isOpen = null, + ) { + parent::__construct(self::TYPE); + } +} diff --git a/src/Type/InputRichBlock/InputRichBlockDivider.php b/src/Type/InputRichBlock/InputRichBlockDivider.php new file mode 100644 index 00000000..68e65c90 --- /dev/null +++ b/src/Type/InputRichBlock/InputRichBlockDivider.php @@ -0,0 +1,18 @@ +. + */ +final readonly class InputRichBlockDivider extends InputRichBlock +{ + public const TYPE = 'divider'; + + public function __construct() + { + parent::__construct(self::TYPE); + } +} diff --git a/src/Type/InputRichBlock/InputRichBlockDocument.php b/src/Type/InputRichBlock/InputRichBlockDocument.php new file mode 100644 index 00000000..9ae3ecee --- /dev/null +++ b/src/Type/InputRichBlock/InputRichBlockDocument.php @@ -0,0 +1,30 @@ +. + */ +final readonly class InputRichBlockDocument extends InputRichBlock +{ + public const TYPE = 'document'; + + public function __construct( + /** + * The document. Caption is ignored. + */ + public InputMediaDocument $document, + + /** + * Optional. Caption of the block + */ + public RichBlockCaption|null $caption = null, + ) { + parent::__construct(self::TYPE); + } +} diff --git a/src/Type/InputRichBlock/InputRichBlockExpandableBlockQuotation.php b/src/Type/InputRichBlock/InputRichBlockExpandableBlockQuotation.php new file mode 100644 index 00000000..6dcaafa0 --- /dev/null +++ b/src/Type/InputRichBlock/InputRichBlockExpandableBlockQuotation.php @@ -0,0 +1,36 @@ + with custom attribute "expandable". + */ +final readonly class InputRichBlockExpandableBlockQuotation extends InputRichBlock +{ + public const TYPE = 'expandable_blockquote'; + + public function __construct( + /** + * Content of the block + * + * @var RichText|string|list + */ + #[RichTextType] + public RichText|string|array $text, + + /** + * Optional. Credit of the block + * + * @var RichText|string|list|null + */ + #[RichTextType] + public RichText|string|array|null $credit = null, + ) { + parent::__construct(self::TYPE); + } +} diff --git a/src/Type/InputRichBlock/InputRichBlockFooter.php b/src/Type/InputRichBlock/InputRichBlockFooter.php new file mode 100644 index 00000000..d5028208 --- /dev/null +++ b/src/Type/InputRichBlock/InputRichBlockFooter.php @@ -0,0 +1,28 @@ +. + */ +final readonly class InputRichBlockFooter extends InputRichBlock +{ + public const TYPE = 'footer'; + + public function __construct( + /** + * Text of the block + * + * @var RichText|string|list + */ + #[RichTextType] + public RichText|string|array $text, + ) { + parent::__construct(self::TYPE); + } +} diff --git a/src/Type/InputRichBlock/InputRichBlockList.php b/src/Type/InputRichBlock/InputRichBlockList.php new file mode 100644 index 00000000..6627e3cf --- /dev/null +++ b/src/Type/InputRichBlock/InputRichBlockList.php @@ -0,0 +1,27 @@ + or
    with multiple nested tags
  1. . + */ +final readonly class InputRichBlockList extends InputRichBlock +{ + public const TYPE = 'list'; + + public function __construct( + /** + * Items of the list + * + * @var list + */ + #[ArrayType(InputRichBlockListItem::class)] + public array $items, + ) { + parent::__construct(self::TYPE); + } +} diff --git a/src/Type/InputRichBlock/InputRichBlockListItem.php b/src/Type/InputRichBlock/InputRichBlockListItem.php new file mode 100644 index 00000000..afdcf1c8 --- /dev/null +++ b/src/Type/InputRichBlock/InputRichBlockListItem.php @@ -0,0 +1,46 @@ + + */ + #[ArrayType(InputRichBlock::class)] + public array $blocks, + + /** + * Optional. Pass True if the item has a checkbox + */ + public bool|null $hasCheckbox = null, + + /** + * Optional. Pass True if the item has a checked checkbox + */ + public bool|null $isChecked = null, + + /** + * Optional. For ordered lists, the numeric value of the item label + */ + public int|null $value = null, + + /** + * Optional. For ordered lists, the type of the item label; must be one of "a" for lowercase letters, + * "A" for uppercase letters, "i" for lowercase Roman numerals, "I" for uppercase Roman numerals, or "1" for decimal numbers + */ + public string|null $type = null, + ) { + } +} diff --git a/src/Type/InputRichBlock/InputRichBlockMap.php b/src/Type/InputRichBlock/InputRichBlockMap.php new file mode 100644 index 00000000..ba342339 --- /dev/null +++ b/src/Type/InputRichBlock/InputRichBlockMap.php @@ -0,0 +1,45 @@ +. The map's width and height must not exceed 10000 in total. The width and height ratio must be at most 20. + */ +final readonly class InputRichBlockMap extends InputRichBlock +{ + public const TYPE = 'map'; + + public function __construct( + /** + * Location of the center of the map + */ + public Location $location, + + /** + * Optional. Map zoom level; 0-24 + */ + public int|null $zoom = null, + + /** + * Optional. Map width; 0-10000 + */ + public int|null $width = null, + + /** + * Optional. Map height; 0-10000 + */ + public int|null $height = null, + + /** + * Optional. Caption of the block + */ + public RichBlockCaption|null $caption = null, + ) { + parent::__construct(self::TYPE); + } +} diff --git a/src/Type/InputRichBlock/InputRichBlockMathematicalExpression.php b/src/Type/InputRichBlock/InputRichBlockMathematicalExpression.php new file mode 100644 index 00000000..4edf8a09 --- /dev/null +++ b/src/Type/InputRichBlock/InputRichBlockMathematicalExpression.php @@ -0,0 +1,22 @@ +. + */ +final readonly class InputRichBlockMathematicalExpression extends InputRichBlock +{ + public const TYPE = 'mathematical_expression'; + + public function __construct( + /** + * The mathematical expression in LaTeX format + */ + public string $expression, + ) { + parent::__construct(self::TYPE); + } +} diff --git a/src/Type/InputRichBlock/InputRichBlockParagraph.php b/src/Type/InputRichBlock/InputRichBlockParagraph.php new file mode 100644 index 00000000..538b97a9 --- /dev/null +++ b/src/Type/InputRichBlock/InputRichBlockParagraph.php @@ -0,0 +1,28 @@ +. + */ +final readonly class InputRichBlockParagraph extends InputRichBlock +{ + public const TYPE = 'paragraph'; + + public function __construct( + /** + * Text of the block + * + * @var RichText|string|list + */ + #[RichTextType] + public RichText|string|array $text, + ) { + parent::__construct(self::TYPE); + } +} diff --git a/src/Type/InputRichBlock/InputRichBlockPhoto.php b/src/Type/InputRichBlock/InputRichBlockPhoto.php new file mode 100644 index 00000000..9f3d2e7f --- /dev/null +++ b/src/Type/InputRichBlock/InputRichBlockPhoto.php @@ -0,0 +1,30 @@ +. + */ +final readonly class InputRichBlockPhoto extends InputRichBlock +{ + public const TYPE = 'photo'; + + public function __construct( + /** + * The photo. Caption is ignored. + */ + public InputMediaPhoto $photo, + + /** + * Optional. Caption of the block + */ + public RichBlockCaption|null $caption = null, + ) { + parent::__construct(self::TYPE); + } +} diff --git a/src/Type/InputRichBlock/InputRichBlockPreformatted.php b/src/Type/InputRichBlock/InputRichBlockPreformatted.php new file mode 100644 index 00000000..574d5cb3 --- /dev/null +++ b/src/Type/InputRichBlock/InputRichBlockPreformatted.php @@ -0,0 +1,33 @@ + and . + */ +final readonly class InputRichBlockPreformatted extends InputRichBlock +{ + public const TYPE = 'pre'; + + public function __construct( + /** + * Text of the block + * + * @var RichText|string|list + */ + #[RichTextType] + public RichText|string|array $text, + + /** + * Optional. The programming language of the text + */ + public string|null $language = null, + ) { + parent::__construct(self::TYPE); + } +} diff --git a/src/Type/InputRichBlock/InputRichBlockPullQuotation.php b/src/Type/InputRichBlock/InputRichBlockPullQuotation.php new file mode 100644 index 00000000..ab7a73d0 --- /dev/null +++ b/src/Type/InputRichBlock/InputRichBlockPullQuotation.php @@ -0,0 +1,36 @@ +. + */ +final readonly class InputRichBlockPullQuotation extends InputRichBlock +{ + public const TYPE = 'pullquote'; + + public function __construct( + /** + * Text of the block + * + * @var RichText|string|list + */ + #[RichTextType] + public RichText|string|array $text, + + /** + * Optional. Credit of the block + * + * @var RichText|string|list|null + */ + #[RichTextType] + public RichText|string|array|null $credit = null, + ) { + parent::__construct(self::TYPE); + } +} diff --git a/src/Type/InputRichBlock/InputRichBlockSectionHeading.php b/src/Type/InputRichBlock/InputRichBlockSectionHeading.php new file mode 100644 index 00000000..1b0f12a2 --- /dev/null +++ b/src/Type/InputRichBlock/InputRichBlockSectionHeading.php @@ -0,0 +1,33 @@ +,

    ,

    ,

    ,

    , or
    . + */ +final readonly class InputRichBlockSectionHeading extends InputRichBlock +{ + public const TYPE = 'heading'; + + public function __construct( + /** + * Text of the block + * + * @var RichText|string|list + */ + #[RichTextType] + public RichText|string|array $text, + + /** + * Relative size of the text font; 1-6, 1 is the largest, 6 is the smallest + */ + public int $size, + ) { + parent::__construct(self::TYPE); + } +} diff --git a/src/Type/InputRichBlock/InputRichBlockSlideshow.php b/src/Type/InputRichBlock/InputRichBlockSlideshow.php new file mode 100644 index 00000000..38098043 --- /dev/null +++ b/src/Type/InputRichBlock/InputRichBlockSlideshow.php @@ -0,0 +1,33 @@ +. + */ +final readonly class InputRichBlockSlideshow extends InputRichBlock +{ + public const TYPE = 'slideshow'; + + public function __construct( + /** + * Elements of the slideshow + * + * @var list + */ + #[ArrayType(InputRichBlock::class)] + public array $blocks, + + /** + * Optional. Caption of the block + */ + public RichBlockCaption|null $caption = null, + ) { + parent::__construct(self::TYPE); + } +} diff --git a/src/Type/InputRichBlock/InputRichBlockTable.php b/src/Type/InputRichBlock/InputRichBlockTable.php new file mode 100644 index 00000000..99a3093b --- /dev/null +++ b/src/Type/InputRichBlock/InputRichBlockTable.php @@ -0,0 +1,53 @@ +. + */ +final readonly class InputRichBlockTable extends InputRichBlock +{ + public const TYPE = 'table'; + + public function __construct( + /** + * Cells of the table + * + * @var list> + */ + #[ArrayOfArayType(RichBlockTableCell::class)] + public array $cells, + + /** + * Optional. Pass True if the table has borders + */ + public bool|null $isBordered = null, + + /** + * Optional. Pass True if the table is striped + */ + public bool|null $isStriped = null, + + /** + * Optional. Caption of the table + * + * @var RichText|string|list|null + */ + #[RichTextType] + public RichText|string|array|null $caption = null, + + /** + * Optional. Pass True if table cells must have smaller indents + */ + public bool|null $isCompact = null, + ) { + parent::__construct(self::TYPE); + } +} diff --git a/src/Type/InputRichBlock/InputRichBlockThinking.php b/src/Type/InputRichBlock/InputRichBlockThinking.php new file mode 100644 index 00000000..8e75288b --- /dev/null +++ b/src/Type/InputRichBlock/InputRichBlockThinking.php @@ -0,0 +1,30 @@ +. + * The block may be used only in sendRichMessageDraft, therefore it can't be received in messages. + * See https://t.me/addemoji/AIActions for examples of custom emoji that are recommended for usage in the block. + */ +final readonly class InputRichBlockThinking extends InputRichBlock +{ + public const TYPE = 'thinking'; + + public function __construct( + /** + * Text of the block. See https://t.me/addemoji/AIActions for examples of custom emoji that are recommended for usage in the block. + * + * @var RichText|string|list + */ + #[RichTextType] + public RichText|string|array $text, + ) { + parent::__construct(self::TYPE); + } +} diff --git a/src/Type/InputRichBlock/InputRichBlockVideo.php b/src/Type/InputRichBlock/InputRichBlockVideo.php new file mode 100644 index 00000000..55ff5590 --- /dev/null +++ b/src/Type/InputRichBlock/InputRichBlockVideo.php @@ -0,0 +1,30 @@ +. + */ +final readonly class InputRichBlockVideo extends InputRichBlock +{ + public const TYPE = 'video'; + + public function __construct( + /** + * The video. Caption is ignored. + */ + public InputMediaVideo $video, + + /** + * Optional. Caption of the block + */ + public RichBlockCaption|null $caption = null, + ) { + parent::__construct(self::TYPE); + } +} diff --git a/src/Type/InputRichBlock/InputRichBlockVoiceNote.php b/src/Type/InputRichBlock/InputRichBlockVoiceNote.php new file mode 100644 index 00000000..23b6a059 --- /dev/null +++ b/src/Type/InputRichBlock/InputRichBlockVoiceNote.php @@ -0,0 +1,30 @@ +. + */ +final readonly class InputRichBlockVoiceNote extends InputRichBlock +{ + public const TYPE = 'voice_note'; + + public function __construct( + /** + * The voice note. Caption is ignored. + */ + public InputMediaVoiceNote $voiceNote, + + /** + * Optional. Caption of the block + */ + public RichBlockCaption|null $caption = null, + ) { + parent::__construct(self::TYPE); + } +} diff --git a/src/Type/InputRichMessage.php b/src/Type/InputRichMessage.php index 0c7805be..7b99a060 100644 --- a/src/Type/InputRichMessage.php +++ b/src/Type/InputRichMessage.php @@ -4,21 +4,25 @@ namespace Luzrain\TelegramBotApi\Type; +use Luzrain\TelegramBotApi\Internal\ArrayType; use Luzrain\TelegramBotApi\Type; +use Luzrain\TelegramBotApi\Type\InputRichBlock\InputRichBlock; /** - * Describes a rich message to be sent. Exactly one of the fields html or markdown must be used. + * Describes a rich message to be sent. Exactly one of the fields html, markdown, or blocks must be used. */ final readonly class InputRichMessage extends Type { public function __construct( /** * Optional. Content of the rich message to send described using HTML formatting. See rich message formatting options for more details. + * Use media field to specify the media used in the message. */ public string|null $html = null, /** * Optional. Content of the rich message to send described using Markdown formatting. See rich message formatting options for more details. + * Use media field to specify the media used in the message. */ public string|null $markdown = null, @@ -31,6 +35,23 @@ public function __construct( * Optional. Pass True to skip automatic detection of entities (e.g., URLs, email addresses, username mentions, hashtags, cashtags, bot commands, or phone numbers) in the text */ public bool|null $skipEntityDetection = null, + + /** + * Optional. Content of the rich message to send described as a list of blocks + * + * @var list|null + */ + #[ArrayType(InputRichBlock::class)] + public array|null $blocks = null, + + /** + * Optional. List of media that are specified in the markdown or html fields using tg://photo?id=, tg://video?id=, + * tg://document?id=, and tg://audio?id= links + * + * @var list|null + */ + #[ArrayType(InputRichMessageMedia::class)] + public array|null $media = null, ) { } } diff --git a/src/Type/InputRichMessageMedia.php b/src/Type/InputRichMessageMedia.php new file mode 100644 index 00000000..43e6bd23 --- /dev/null +++ b/src/Type/InputRichMessageMedia.php @@ -0,0 +1,27 @@ +date); } diff --git a/src/Type/MessageGenerationStopped.php b/src/Type/MessageGenerationStopped.php new file mode 100644 index 00000000..83b65e3e --- /dev/null +++ b/src/Type/MessageGenerationStopped.php @@ -0,0 +1,31 @@ +keyboard = \is_array($keyboard) ? $keyboard : $keyboard->toArray(); } diff --git a/src/Type/ReplyParameters.php b/src/Type/ReplyParameters.php index 6135b4bb..3e1432c2 100644 --- a/src/Type/ReplyParameters.php +++ b/src/Type/ReplyParameters.php @@ -14,19 +14,22 @@ { public function __construct( /** - * Identifier of the message that will be replied to in the current chat, or in the chat chat_id if it is specified + * Optional. Identifier of the message that will be replied to in the current chat, or in the chat chat_id if it is specified. + * Required if ephemeral_message_id isn't specified. */ - public int $messageId, + public int|null $messageId = null, /** * Optional. If the message to be replied to is from a different chat, unique identifier for the chat or username of the bot, supergroup - * or channel in the format @username. Not supported for messages sent on behalf of a business account and messages from channel direct messages chats. + * or channel in the format @username. Not supported for messages sent on behalf of a business account, messages from channel direct messages chats + * and ephemeral messages. */ public int|string|null $chatId = null, /** * Optional. Pass True if the message should be sent even if the specified message to be replied to is not found. - * Always False for replies in another chat or forum topic. Always True for messages sent on behalf of a business account. + * Always False for replies in another chat or forum topic, and sent ephemeral messages. + * Always True for messages sent on behalf of a business account. */ public bool|null $allowSendingWithoutReply = null, @@ -34,6 +37,7 @@ public function __construct( * Optional. Quoted part of the message to be replied to; 0-1024 characters after entities parsing. * The quote must be an exact substring of the message to be replied to, including bold, italic, underline, strikethrough, * spoiler, custom_emoji, and date_time entities. The message will fail to send if the quote isn't found in the original message. + * Ignored for ephemeral messages. */ public string|null $quote = null, @@ -66,6 +70,13 @@ public function __construct( * Optional. Persistent identifier of the specific poll option to be replied to */ public string|null $pollOptionId = null, + + /** + * Optional. Identifier of the incoming ephemeral message that will be replied to in the current chat. + * A reply to an ephemeral message must itself be an ephemeral message. An ephemeral message may only be replied to + * within 15 seconds of being sent. Required if message_id isn't specified. + */ + public int|null $ephemeralMessageId = null, ) { } } diff --git a/src/Type/RichBlock/RichBlock.php b/src/Type/RichBlock/RichBlock.php index a72acd69..ca209de9 100644 --- a/src/Type/RichBlock/RichBlock.php +++ b/src/Type/RichBlock/RichBlock.php @@ -18,14 +18,17 @@ * @see RichBlockAnchor * @see RichBlockList * @see RichBlockBlockQuotation + * @see RichBlockExpandableBlockQuotation * @see RichBlockPullQuotation * @see RichBlockCollage * @see RichBlockSlideshow * @see RichBlockTable * @see RichBlockDetails * @see RichBlockMap + * @see RichBlockButtons * @see RichBlockAnimation * @see RichBlockAudio + * @see RichBlockDocument * @see RichBlockPhoto * @see RichBlockVideo * @see RichBlockVoiceNote @@ -59,14 +62,17 @@ public static function fromArray(array $data): static RichBlockAnchor::TYPE => RichBlockAnchor::fromArray($data), RichBlockList::TYPE => RichBlockList::fromArray($data), RichBlockBlockQuotation::TYPE => RichBlockBlockQuotation::fromArray($data), + RichBlockExpandableBlockQuotation::TYPE => RichBlockExpandableBlockQuotation::fromArray($data), RichBlockPullQuotation::TYPE => RichBlockPullQuotation::fromArray($data), RichBlockCollage::TYPE => RichBlockCollage::fromArray($data), RichBlockSlideshow::TYPE => RichBlockSlideshow::fromArray($data), RichBlockTable::TYPE => RichBlockTable::fromArray($data), RichBlockDetails::TYPE => RichBlockDetails::fromArray($data), RichBlockMap::TYPE => RichBlockMap::fromArray($data), + RichBlockButtons::TYPE => RichBlockButtons::fromArray($data), RichBlockAnimation::TYPE => RichBlockAnimation::fromArray($data), RichBlockAudio::TYPE => RichBlockAudio::fromArray($data), + RichBlockDocument::TYPE => RichBlockDocument::fromArray($data), RichBlockPhoto::TYPE => RichBlockPhoto::fromArray($data), RichBlockVideo::TYPE => RichBlockVideo::fromArray($data), RichBlockVoiceNote::TYPE => RichBlockVoiceNote::fromArray($data), diff --git a/src/Type/RichBlock/RichBlockButtons.php b/src/Type/RichBlock/RichBlockButtons.php new file mode 100644 index 00000000..80159c87 --- /dev/null +++ b/src/Type/RichBlock/RichBlockButtons.php @@ -0,0 +1,33 @@ +. + */ +final readonly class RichBlockButtons extends RichBlock +{ + public const TYPE = 'buttons'; + + public function __construct( + /** + * The buttons + * + * @var list + */ + #[ArrayType(RichMessageButton::class)] + public array $buttons, + + /** + * Optional. Horizontal alignment of the buttons. Currently, must be one of "left", "center", or "right". + */ + public string|null $align = null, + ) { + parent::__construct(self::TYPE); + } +} diff --git a/src/Type/RichBlock/RichBlockCaption.php b/src/Type/RichBlock/RichBlockCaption.php index 345b11ae..4a14b473 100644 --- a/src/Type/RichBlock/RichBlockCaption.php +++ b/src/Type/RichBlock/RichBlockCaption.php @@ -13,7 +13,7 @@ */ final readonly class RichBlockCaption extends Type { - protected function __construct( + public function __construct( /** * Block caption * diff --git a/src/Type/RichBlock/RichBlockDocument.php b/src/Type/RichBlock/RichBlockDocument.php new file mode 100644 index 00000000..5ccbbcdf --- /dev/null +++ b/src/Type/RichBlock/RichBlockDocument.php @@ -0,0 +1,29 @@ +. + */ +final readonly class RichBlockDocument extends RichBlock +{ + public const TYPE = 'document'; + + public function __construct( + /** + * The document + */ + public Document $document, + + /** + * Optional. Caption of the block + */ + public RichBlockCaption|null $caption = null, + ) { + parent::__construct(self::TYPE); + } +} diff --git a/src/Type/RichBlock/RichBlockExpandableBlockQuotation.php b/src/Type/RichBlock/RichBlockExpandableBlockQuotation.php new file mode 100644 index 00000000..5fb18f67 --- /dev/null +++ b/src/Type/RichBlock/RichBlockExpandableBlockQuotation.php @@ -0,0 +1,36 @@ + with custom attribute "expandable". + */ +final readonly class RichBlockExpandableBlockQuotation extends RichBlock +{ + public const TYPE = 'expandable_blockquote'; + + public function __construct( + /** + * Content of the block + * + * @var RichText|string|list + */ + #[RichTextType] + public RichText|string|array $text, + + /** + * Optional. Credit of the block + * + * @var RichText|string|list|null + */ + #[RichTextType] + public RichText|string|array|null $credit = null, + ) { + parent::__construct(self::TYPE); + } +} diff --git a/src/Type/RichBlock/RichBlockTable.php b/src/Type/RichBlock/RichBlockTable.php index a623ef2c..0ca57cb5 100644 --- a/src/Type/RichBlock/RichBlockTable.php +++ b/src/Type/RichBlock/RichBlockTable.php @@ -41,6 +41,11 @@ public function __construct( */ #[RichTextType] public RichText|string|array|null $caption = null, + + /** + * Optional. True, if table cells have smaller indents + */ + public true|null $isCompact = null, ) { parent::__construct(self::TYPE); } diff --git a/src/Type/RichBlock/RichBlockTableCell.php b/src/Type/RichBlock/RichBlockTableCell.php index 26ddb88e..c84a30f1 100644 --- a/src/Type/RichBlock/RichBlockTableCell.php +++ b/src/Type/RichBlock/RichBlockTableCell.php @@ -13,7 +13,7 @@ */ final readonly class RichBlockTableCell extends Type { - protected function __construct( + public function __construct( /** * Horizontal cell content alignment. Currently, must be one of "left", "center", or "right". */ diff --git a/src/Type/RichMessageButton.php b/src/Type/RichMessageButton.php new file mode 100644 index 00000000..d5146df2 --- /dev/null +++ b/src/Type/RichMessageButton.php @@ -0,0 +1,89 @@ + + */ + #[RichTextType] + public RichText|string|array $text, + + /** + * Optional. Style of the button. Must be one of "danger", "success", "primary", or "link" + * (the button is shown as a regular link without borders). Apps may use theme-specific colors for the button background + * and text based on the style. The style "link" is allowed only for callback buttons. + */ + public string|null $style = null, + + /** + * Optional. HTTP or tg:// URL to be opened when the button is pressed. Links tg://user?id= can be used to mention + * a user by their identifier without using a username, if this is allowed by their privacy settings. + */ + public string|null $url = null, + + /** + * Optional. Data to be sent in a callback query to the bot when the button is pressed, 1-64 bytes + */ + public string|null $callbackData = null, + + /** + * Optional. Description of the Web App that will be launched when the user presses the button. The Web App will be able + * to send an arbitrary message on behalf of the user using the method answerWebAppQuery. Available only in private chats + * between a user and the bot. Not supported for messages sent on behalf of a business account. + */ + public WebAppInfo|null $webApp = null, + + /** + * Optional. An HTTPS URL used to automatically authorize the user. Can be used as a replacement for the Telegram Login Widget. + * Not supported for ephemeral messages. + */ + public LoginUrl|null $loginUrl = null, + + /** + * Optional. If set, pressing the button will prompt the user to select one of their chats, open that chat and insert + * the bot's username and the specified inline query in the input field. May be empty, in which case just the bot's username + * will be inserted. Not supported for messages sent in channel direct messages chats and on behalf of a business account. + */ + public string|null $switchInlineQuery = null, + + /** + * Optional. If set, pressing the button will insert the bot's username and the specified inline query in the current chat's + * input field. May be empty, in which case only the bot's username will be inserted. Not supported in channels and for messages + * sent in channel direct messages chats and on behalf of a business account. + */ + public string|null $switchInlineQueryCurrentChat = null, + + /** + * Optional. If set, pressing the button will prompt the user to select one of their chats of the specified type, open that chat + * and insert the bot's username and the specified inline query in the input field. Not supported for messages sent in channel + * direct messages chats and on behalf of a business account. + */ + public SwitchInlineQueryChosenChat|null $switchInlineQueryChosenChat = null, + + /** + * Optional. A button that copies the specified text to the clipboard + */ + public CopyTextButton|null $copyText = null, + + /** + * Optional. If set, then the button is disabled and does nothing + */ + public DisabledButton|null $disabled = null, + ) { + } +} diff --git a/src/Type/RichText/RichText.php b/src/Type/RichText/RichText.php index c625a75a..a8f9e2ed 100644 --- a/src/Type/RichText/RichText.php +++ b/src/Type/RichText/RichText.php @@ -30,6 +30,7 @@ * @see RichTextHashtag * @see RichTextCashtag * @see RichTextBotCommand + * @see RichTextButton * @see RichTextAnchor * @see RichTextAnchorLink * @see RichTextReference @@ -75,6 +76,7 @@ public static function fromArray(array $data): static RichTextHashtag::TYPE => RichTextHashtag::fromArray($data), RichTextCashtag::TYPE => RichTextCashtag::fromArray($data), RichTextBotCommand::TYPE => RichTextBotCommand::fromArray($data), + RichTextButton::TYPE => RichTextButton::fromArray($data), RichTextAnchor::TYPE => RichTextAnchor::fromArray($data), RichTextAnchorLink::TYPE => RichTextAnchorLink::fromArray($data), RichTextReference::TYPE => RichTextReference::fromArray($data), diff --git a/src/Type/RichText/RichTextButton.php b/src/Type/RichText/RichTextButton.php new file mode 100644 index 00000000..1bd1d5be --- /dev/null +++ b/src/Type/RichText/RichTextButton.php @@ -0,0 +1,24 @@ +|null + */ + #[ArrayType(MessageEntity::class)] + public array|null $entities = null, + + /** + * Optional. True, if the sender and gift text are shown only to the gift receiver; + * otherwise, everyone will be able to see them + */ + public true|null $isPrivate = null, ) { } } diff --git a/src/Type/Update.php b/src/Type/Update.php index e2834da6..f39cf1f0 100644 --- a/src/Type/Update.php +++ b/src/Type/Update.php @@ -43,6 +43,8 @@ 'chat_boost', 'removed_chat_boost', 'managed_bot', + 'subscription', + 'stopped_message_generation', ]; protected function __construct( @@ -189,6 +191,16 @@ protected function __construct( * Optional. A new bot was created to be managed by the bot, or token or owner of a managed bot was changed */ public ManagedBotUpdated|null $managedBot = null, + + /** + * Optional. User payment subscription has changed + */ + public BotSubscriptionUpdated|null $subscription = null, + + /** + * Optional. A user asked the bot to stop the generation of a message + */ + public MessageGenerationStopped|null $stoppedMessageGeneration = null, ) { } diff --git a/tests/BotApiFileUploadTest.php b/tests/BotApiFileUploadTest.php new file mode 100644 index 00000000..0f6de3e3 --- /dev/null +++ b/tests/BotApiFileUploadTest.php @@ -0,0 +1,257 @@ +.". + * Matching that exact shape avoids both the JSON-escaped "attach:\/\/" form and any + * accidental match inside an uploaded file's own contents. + * + * @return array{parts: list, attachRefs: list, body: string} + */ + private function inspectRequest(TestHttpClient $client): array + { + $body = (string) $client->getLastRequest()->getBody(); + + \preg_match_all('/name="([^"]+)"/', $body, $partMatches); + \preg_match_all('#attach:(?:\\\\?/){2}(attach\.[0-9a-f]+\.[0-9]+)#', $body, $refMatches); + + return [ + 'parts' => $partMatches[1], + 'attachRefs' => \array_values(\array_unique($refMatches[1])), + 'body' => $body, + ]; + } + + private function createApi(TestPsrFactory $factory, TestHttpClient $client): BotApi + { + return new BotApi( + requestFactory: $factory, + streamFactory: $factory, + client: $client, + token: 'TEST_TOKEN', + ); + } + + public function testTopLevelInputFileIsUploadedExactlyOnce(): void + { + $factory = new TestPsrFactory(); + + $this->createApi($factory, new TestHttpClient(self::MESSAGE_RESPONSE))->call(new Method\SendPhoto( + chatId: 1, + photo: new Type\InputFile(__FILE__), + )); + + $this->assertSame([__FILE__], $factory->getStreamFromFileCalls()); + } + + public function testSeveralTopLevelInputFilesAreUploaded(): void + { + $factory = new TestPsrFactory(); + + $this->createApi($factory, new TestHttpClient(self::MESSAGE_RESPONSE))->call(new Method\SendVideo( + chatId: 1, + video: new Type\InputFile(__FILE__), + thumbnail: new Type\InputFile(__DIR__ . '/BaseTypeTest.php'), + )); + + $calls = $factory->getStreamFromFileCalls(); + \sort($calls); + $expected = [__FILE__, __DIR__ . '/BaseTypeTest.php']; + \sort($expected); + + $this->assertSame($expected, $calls); + } + + public function testInputFilesNestedInsideMediaArrayAreUploaded(): void + { + $factory = new TestPsrFactory(); + + $this->createApi($factory, new TestHttpClient('{"ok":true,"result":[]}'))->call(new Method\SendMediaGroup( + chatId: 1, + media: [ + new Type\InputMediaPhoto(media: new Type\InputFile(__FILE__)), + new Type\InputMediaVideo( + media: new Type\InputFile(__DIR__ . '/BaseTypeTest.php'), + thumbnail: new Type\InputFile(__DIR__ . '/UpdateTypeTest.php'), + ), + ], + )); + + $this->assertCount(3, $factory->getStreamFromFileCalls()); + } + + public function testInputFileDeepInsideRichBlockTreeIsUploaded(): void + { + $factory = new TestPsrFactory(); + + $this->createApi($factory, new TestHttpClient(self::MESSAGE_RESPONSE))->call(new Method\SendRichMessage( + chatId: 1, + richMessage: new Type\InputRichMessage(blocks: [ + new Type\InputRichBlock\InputRichBlockDetails( + summary: 'S', + blocks: [ + new Type\InputRichBlock\InputRichBlockCollage(blocks: [ + new Type\InputRichBlock\InputRichBlockPhoto( + photo: new Type\InputMediaPhoto(media: new Type\InputFile(__FILE__)), + ), + ]), + ], + ), + ]), + )); + + $this->assertSame([__FILE__], $factory->getStreamFromFileCalls()); + } + + public function testEveryAttachReferenceHasMatchingMultipartPart(): void + { + $factory = new TestPsrFactory(); + $client = new TestHttpClient(self::MESSAGE_RESPONSE); + + $this->createApi($factory, $client)->call(new Method\SendRichMessage( + chatId: 1, + richMessage: new Type\InputRichMessage(blocks: [ + new Type\InputRichBlock\InputRichBlockCollage(blocks: [ + new Type\InputRichBlock\InputRichBlockPhoto( + photo: new Type\InputMediaPhoto(media: new Type\InputFile(self::FILE_A)), + ), + new Type\InputRichBlock\InputRichBlockPhoto( + photo: new Type\InputMediaPhoto(media: new Type\InputFile(self::FILE_B)), + ), + ]), + ]), + )); + + ['parts' => $parts, 'attachRefs' => $refs] = $this->inspectRequest($client); + + $this->assertCount(2, $refs, 'both nested files must be referenced as attach://'); + foreach ($refs as $ref) { + $this->assertContains($ref, $parts, \sprintf('attach://%s has no multipart part named %s', $ref, $ref)); + } + } + + public function testTopLevelFileIsReferencedByAttachPathNotByFilePath(): void + { + $factory = new TestPsrFactory(); + $client = new TestHttpClient(self::MESSAGE_RESPONSE); + + $this->createApi($factory, $client)->call(new Method\SendPhoto( + chatId: 1, + photo: new Type\InputFile(self::FILE_A), + )); + + ['parts' => $parts, 'attachRefs' => $refs, 'body' => $body] = $this->inspectRequest($client); + + $this->assertCount(1, $refs); + $this->assertContains('photo', $parts); + $this->assertContains($refs[0], $parts); + $this->assertStringContainsString('attach://' . $refs[0], $body); + } + + public function testCallWithoutParametersIsSentAsGet(): void + { + $factory = new TestPsrFactory(); + $client = new TestHttpClient('{"ok":true,"result":{"id":1,"is_bot":true,"first_name":"B"}}'); + + $this->createApi($factory, $client)->call(new Method\GetMe()); + + $this->assertSame('GET', $client->getLastRequest()->getMethod()); + } + + public function testCallWithParametersIsSentAsMultipartPost(): void + { + $factory = new TestPsrFactory(); + $client = new TestHttpClient(self::MESSAGE_RESPONSE); + + $this->createApi($factory, $client)->call(new Method\SendMessage(chatId: 1, text: 'hi')); + + $request = $client->getLastRequest(); + + $this->assertSame('POST', $request->getMethod()); + $this->assertStringContainsString('multipart/form-data; boundary=', $request->getHeaderLine('Content-Type')); + } + + public function testRequestUrlContainsTokenAndMethodName(): void + { + $factory = new TestPsrFactory(); + $client = new TestHttpClient(self::MESSAGE_RESPONSE); + + $this->createApi($factory, $client)->call(new Method\SendMessage(chatId: 1, text: 'hi')); + + $this->assertSame( + 'https://api.telegram.org/botTEST_TOKEN/sendMessage', + $client->getLastRequest()->getRequestTarget(), + ); + } + + public function testInputFilesInsidePaidMediaAreUploaded(): void + { + $factory = new TestPsrFactory(); + + $this->createApi($factory, new TestHttpClient(self::MESSAGE_RESPONSE))->call(new Method\SendPaidMedia( + chatId: 1, + starCount: 5, + media: [ + new Type\InputPaidMediaPhoto(media: new Type\InputFile(__FILE__)), + new Type\InputPaidMediaVideo( + media: new Type\InputFile(__DIR__ . '/BaseTypeTest.php'), + thumbnail: new Type\InputFile(__DIR__ . '/UpdateTypeTest.php'), + ), + ], + )); + + $this->assertCount(3, $factory->getStreamFromFileCalls()); + } + + /** + * Regression guard for a behaviour the recursive walk fixed as a side effect: the previous + * flat scan only descended into InputMedia/InputPaidMedia, so InputSticker::$sticker was + * serialized as attach:// with no matching multipart part and Telegram rejected the call. + */ + public function testInputFileInsideInputStickerIsUploaded(): void + { + $factory = new TestPsrFactory(); + $client = new TestHttpClient(); + + $this->createApi($factory, $client)->call(new Method\CreateNewStickerSet( + userId: 1, + name: 'pack_by_bot', + title: 'Pack', + stickers: [ + new Type\InputSticker( + sticker: new Type\InputFile(self::FILE_A), + format: 'static', + emojiList: ['*'], + ), + ], + )); + + ['parts' => $parts, 'attachRefs' => $refs] = $this->inspectRequest($client); + + $this->assertCount(1, $factory->getStreamFromFileCalls()); + $this->assertCount(1, $refs); + $this->assertContains($refs[0], $parts, 'attach:// reference must have a matching multipart part'); + } +} diff --git a/tests/BotSubscriptionTest.php b/tests/BotSubscriptionTest.php new file mode 100644 index 00000000..eb9e4efb --- /dev/null +++ b/tests/BotSubscriptionTest.php @@ -0,0 +1,77 @@ +json()); + + $this->assertSame(900000003, $update->updateId); + $this->assertInstanceOf(Type\BotSubscriptionUpdated::class, $update->subscription); + $this->assertInstanceOf(Type\User::class, $update->subscription->user); + $this->assertSame(123456789, $update->subscription->user->id); + $this->assertSame('sub-payload-1', $update->subscription->invoicePayload); + $this->assertSame('canceled', $update->subscription->state); + $this->assertNull($update->message); + } + + public function testEventIsDispatched(): void + { + $helper = new ClosureTestHelper(); + $client = new ClientApi(); + $client->on(new Event\BotSubscriptionUpdated($helper->getClosure())); + $client->handle(Type\Update::fromJson($this->json())); + + $this->assertTrue($helper->isCalled()); + $this->assertInstanceOf(Type\BotSubscriptionUpdated::class, $helper->getParameter()); + $this->assertSame('canceled', $helper->getParameter()->state); + } + + public function testEventIsNotDispatchedForUnrelatedUpdate(): void + { + $helper = new ClosureTestHelper(); + $client = new ClientApi(); + $client->on(new Event\BotSubscriptionUpdated($helper->getClosure())); + $client->handle(Type\Update::fromJson(\file_get_contents(__DIR__ . '/data/events/command.json'))); + + $this->assertFalse($helper->isCalled(), 'checker must not match an unrelated update'); + } + + /** + * UPDATE_TYPES drives setWebhook(allowedUpdates:). An entry missing from it silently drops + * that update type for every consumer, and nothing else in the suite would notice. + */ + public function testUpdateTypesMatchesUpdateProperties(): void + { + $reflection = new \ReflectionClass(Type\Update::class); + + $constant = $reflection->getConstant('UPDATE_TYPES'); + $properties = []; + foreach ($reflection->getConstructor()->getParameters() as $parameter) { + if ($parameter->getName() === 'updateId') { + continue; + } + $properties[] = \strtolower(\preg_replace('/(?getName())); + } + + \sort($constant); + \sort($properties); + + $this->assertSame($properties, $constant); + } +} diff --git a/tests/CommunityTest.php b/tests/CommunityTest.php new file mode 100644 index 00000000..1106ea86 --- /dev/null +++ b/tests/CommunityTest.php @@ -0,0 +1,76 @@ +assertInstanceOf(CommunityChatAdded::class, $update->message->communityChatAdded); + $this->assertInstanceOf(Community::class, $update->message->communityChatAdded->community); + $this->assertSame(5555555555555, $update->message->communityChatAdded->community->id); + $this->assertSame('Test Community', $update->message->communityChatAdded->community->name); + $this->assertNull($update->message->communityChatRemoved); + $this->assertNull($update->message->communityChatJoined); + } + + public function testCommunityChatJoinedHydration(): void + { + $joined = CommunityChatJoined::fromArray(['community' => ['id' => 1, 'name' => 'C']]); + + $this->assertSame(1, $joined->community->id); + } + + public function testCommunityChatRemovedSerializesAsJsonObjectNotArray(): void + { + $this->assertSame('{}', \json_encode(CommunityChatRemoved::fromArray([]))); + } + + public function testMessageMapsAllThreeCommunityServiceFields(): void + { + $message = \Luzrain\TelegramBotApi\Type\Message::fromArray([ + 'message_id' => 1, + 'date' => 1, + 'chat' => ['id' => 1, 'type' => 'supergroup'], + 'community_chat_added' => ['community' => ['id' => 1, 'name' => 'A']], + 'community_chat_removed' => [], + 'community_chat_joined' => ['community' => ['id' => 2, 'name' => 'J']], + ]); + + $this->assertInstanceOf(CommunityChatAdded::class, $message->communityChatAdded); + $this->assertInstanceOf(CommunityChatRemoved::class, $message->communityChatRemoved); + $this->assertInstanceOf(CommunityChatJoined::class, $message->communityChatJoined); + $this->assertSame('J', $message->communityChatJoined->community->name); + } + + public function testChatFullInfoCarriesCommunity(): void + { + $chat = \Luzrain\TelegramBotApi\Type\ChatFullInfo::fromArray([ + 'id' => 1, + 'type' => 'supergroup', + 'accent_color_id' => 0, + 'max_reaction_count' => 11, + 'accepted_gift_types' => [ + 'unlimited_gifts' => true, + 'limited_gifts' => true, + 'unique_gifts' => true, + 'premium_subscription' => false, + ], + 'community' => ['id' => 9, 'name' => 'Nine'], + ]); + + $this->assertInstanceOf(Community::class, $chat->community); + $this->assertSame(9, $chat->community->id); + } +} diff --git a/tests/EphemeralMessageTest.php b/tests/EphemeralMessageTest.php new file mode 100644 index 00000000..9b1ff56f --- /dev/null +++ b/tests/EphemeralMessageTest.php @@ -0,0 +1,185 @@ +assertSame('editEphemeralMessageText', (new Method\EditEphemeralMessageText(chatId: 1, receiverUserId: 2, ephemeralMessageId: 3))->getName()); + $this->assertSame('editEphemeralMessageMedia', (new Method\EditEphemeralMessageMedia(chatId: 1, receiverUserId: 2, ephemeralMessageId: 3, media: new Type\InputMediaPhoto(media: 'f')))->getName()); + $this->assertSame('editEphemeralMessageCaption', (new Method\EditEphemeralMessageCaption(chatId: 1, receiverUserId: 2, ephemeralMessageId: 3))->getName()); + $this->assertSame('editEphemeralMessageReplyMarkup', (new Method\EditEphemeralMessageReplyMarkup(chatId: 1, receiverUserId: 2, ephemeralMessageId: 3))->getName()); + $this->assertSame('deleteEphemeralMessage', (new Method\DeleteEphemeralMessage(chatId: 1, receiverUserId: 2, ephemeralMessageId: 3))->getName()); + } + + public function testMethodsSerializeSnakeCaseParameters(): void + { + $params = \iterator_to_array((new Method\DeleteEphemeralMessage(chatId: 42, receiverUserId: 7, ephemeralMessageId: 9))->getIterator()); + + $this->assertSame(['chat_id' => 42, 'receiver_user_id' => 7, 'ephemeral_message_id' => 9], $params); + } + + public function testEditMethodsSerializeTheirRequiredParameters(): void + { + $methods = [ + new Method\EditEphemeralMessageText(chatId: 4, receiverUserId: 5, ephemeralMessageId: 6), + new Method\EditEphemeralMessageCaption(chatId: 4, receiverUserId: 5, ephemeralMessageId: 6), + new Method\EditEphemeralMessageReplyMarkup(chatId: 4, receiverUserId: 5, ephemeralMessageId: 6), + ]; + + foreach ($methods as $method) { + $params = \iterator_to_array($method->getIterator()); + + $this->assertSame( + ['chat_id' => 4, 'receiver_user_id' => 5, 'ephemeral_message_id' => 6], + $params, + $method->getName() . ' must emit exactly its three required parameters', + ); + } + } + + public function testEditEphemeralMessageMediaUploadsLocalFile(): void + { + $factory = new \Luzrain\TelegramBotApi\Test\Helper\TestPsrFactory(); + $client = new \Luzrain\TelegramBotApi\Test\Helper\TestHttpClient(); + + (new \Luzrain\TelegramBotApi\BotApi( + requestFactory: $factory, + streamFactory: $factory, + client: $client, + token: 'T', + ))->call(new Method\EditEphemeralMessageMedia( + chatId: 1, + receiverUserId: 2, + ephemeralMessageId: 3, + media: new Type\InputMediaPhoto(media: new Type\InputFile(__DIR__ . '/data/events/command.json')), + )); + + $this->assertCount(1, $factory->getStreamFromFileCalls()); + } + + public function testEphemeralMessageParametersSerialization(): void + { + $decoded = \json_decode(\json_encode(new Type\EphemeralMessageParameters( + receiverUserId: 42, + replaceCallbackQueryMessage: true, + )), true); + + $this->assertSame(['receiver_user_id' => 42, 'replace_callback_query_message' => true], $decoded); + $this->assertArrayNotHasKey('callback_query_id', $decoded); + } + + public function testEphemeralParametersAreSerialized(): void + { + $params = \iterator_to_array((new Method\SendMessage( + chatId: 1, + text: 't', + ephemeralMessageParameters: new Type\EphemeralMessageParameters(receiverUserId: 5), + ))->getIterator()); + + $this->assertArrayHasKey('ephemeral_message_parameters', $params); + $this->assertSame(5, $params['ephemeral_message_parameters']->receiverUserId); + } + + public function testAllFourteenMethodsAcceptEphemeralParameters(): void + { + $expected = [ + 'SendMessage', 'SendAnimation', 'SendAudio', 'SendDocument', 'SendLivePhoto', + 'SendPhoto', 'SendSticker', 'SendVideo', 'SendVideoNote', 'SendVoice', + 'SendContact', 'SendLocation', 'SendVenue', 'SendRichMessage', + ]; + + foreach ($expected as $short) { + $names = \array_map( + static fn(\ReflectionParameter $p) => $p->getName(), + (new \ReflectionClass('Luzrain\\TelegramBotApi\\Method\\' . $short))->getConstructor()->getParameters(), + ); + + $this->assertContains('ephemeralMessageParameters', $names, $short . ' must accept ephemeralMessageParameters'); + } + } + + public function testMethodsWithoutEphemeralSupportAreUntouched(): void + { + $excluded = ['SendMediaGroup', 'SendPoll', 'SendDice', 'SendChecklist', 'SendGame', 'SendInvoice', 'SendPaidMedia']; + + foreach ($excluded as $short) { + $names = \array_map( + static fn(\ReflectionParameter $p) => $p->getName(), + (new \ReflectionClass('Luzrain\\TelegramBotApi\\Method\\' . $short))->getConstructor()->getParameters(), + ); + + $this->assertNotContains('ephemeralMessageParameters', $names, $short . ' must NOT accept ephemeralMessageParameters'); + } + } + + public function testEphemeralMessageHydration(): void + { + $update = Type\Update::fromJson(\file_get_contents(__DIR__ . '/data/events/ephemeralMessage.json')); + + $this->assertSame(0, $update->message->messageId); + $this->assertSame(55, $update->message->ephemeralMessageId); + $this->assertInstanceOf(Type\User::class, $update->message->receiverUser); + $this->assertSame(987654321, $update->message->receiverUser->id); + $this->assertSame('Receiver', $update->message->receiverUser->firstName); + } + + public function testBotCommandIsEphemeral(): void + { + $command = Type\BotCommand::fromArray(['command' => 'x', 'description' => 'd', 'is_ephemeral' => true]); + + $this->assertTrue($command->isEphemeral); + } + + public function testReplyParametersAcceptsEphemeralMessageIdWithoutMessageId(): void + { + $decoded = \json_decode(\json_encode(new Type\ReplyParameters(ephemeralMessageId: 7)), true); + + $this->assertSame(['ephemeral_message_id' => 7], $decoded); + $this->assertArrayNotHasKey('message_id', $decoded); + } + + public function testReplyParametersStillAcceptsMessageIdPositionally(): void + { + $decoded = \json_decode(\json_encode(new Type\ReplyParameters(5)), true); + + $this->assertSame(5, $decoded['message_id']); + } + + /** + * Appending rather than inserting is what keeps positional construction working for existing + * callers. assertContains would not notice a mid-signature insertion; asserting the position does. + */ + public function testEphemeralParametersIsAlwaysTheLastConstructorParameter(): void + { + $expected = [ + 'SendMessage', 'SendAnimation', 'SendAudio', 'SendDocument', 'SendLivePhoto', + 'SendPhoto', 'SendSticker', 'SendVideo', 'SendVideoNote', 'SendVoice', + 'SendContact', 'SendLocation', 'SendVenue', 'SendRichMessage', + ]; + + foreach ($expected as $short) { + $names = \array_map( + static fn(\ReflectionParameter $p) => $p->getName(), + (new \ReflectionClass('Luzrain\\TelegramBotApi\\Method\\' . $short))->getConstructor()->getParameters(), + ); + + $this->assertSame('ephemeralMessageParameters', \end($names), $short . ': parameter must be appended last'); + } + } + + public function testReplyParametersKeepsThreeArgumentPositionalCallsIntact(): void + { + $decoded = \json_decode(\json_encode(new Type\ReplyParameters(123, null, true)), true); + + $this->assertSame(['message_id' => 123, 'allow_sending_without_reply' => true], $decoded); + $this->assertArrayNotHasKey('ephemeral_message_id', $decoded); + } +} diff --git a/tests/Helper/TestHttpClient.php b/tests/Helper/TestHttpClient.php new file mode 100644 index 00000000..082e3ae2 --- /dev/null +++ b/tests/Helper/TestHttpClient.php @@ -0,0 +1,30 @@ +lastRequest = $request; + + return new TestResponse(TestStream::fromString($this->responseJson)); + } + + public function getLastRequest(): RequestInterface|null + { + return $this->lastRequest; + } +} diff --git a/tests/Helper/TestPsrFactory.php b/tests/Helper/TestPsrFactory.php new file mode 100644 index 00000000..406f0874 --- /dev/null +++ b/tests/Helper/TestPsrFactory.php @@ -0,0 +1,46 @@ + */ + private array $streamFromFileCalls = []; + + public function createRequest(string $method, $uri): RequestInterface + { + return new TestRequest($method, (string) $uri); + } + + public function createStream(string $content = ''): StreamInterface + { + return TestStream::fromString($content); + } + + public function createStreamFromFile(string $filename, string $mode = 'r'): StreamInterface + { + $this->streamFromFileCalls[] = $filename; + + return new TestStream(\fopen($filename, $mode)); + } + + public function createStreamFromResource($resource): StreamInterface + { + return new TestStream($resource); + } + + /** + * @return list + */ + public function getStreamFromFileCalls(): array + { + return $this->streamFromFileCalls; + } +} diff --git a/tests/Helper/TestRequest.php b/tests/Helper/TestRequest.php new file mode 100644 index 00000000..aeb9ad19 --- /dev/null +++ b/tests/Helper/TestRequest.php @@ -0,0 +1,123 @@ +uri; + } + + public function getMethod(): string + { + return $this->method; + } + + public function getUri(): UriInterface + { + throw new \LogicException('Not needed in tests; use getUriString()'); + } + + public function getUriString(): string + { + return $this->uri; + } + + public function getProtocolVersion(): string + { + return '1.1'; + } + + public function getHeaders(): array + { + return $this->headers; + } + + public function hasHeader(string $name): bool + { + return isset($this->headers[$name]); + } + + public function getHeader(string $name): array + { + return $this->headers[$name] ?? []; + } + + public function getHeaderLine(string $name): string + { + return \implode(', ', $this->getHeader($name)); + } + + public function getBody(): StreamInterface + { + return $this->body ?? TestStream::fromString(''); + } + + public function withAddedHeader(string $name, $value): static + { + $clone = clone $this; + $clone->headers[$name][] = $value; + + return $clone; + } + + public function withBody(StreamInterface $body): static + { + $clone = clone $this; + $clone->body = $body; + + return $clone; + } + + public function withRequestTarget(string $requestTarget): static + { + return $this; + } + + public function withMethod(string $method): static + { + return $this; + } + + public function withUri(UriInterface $uri, bool $preserveHost = false): static + { + return $this; + } + + public function withProtocolVersion(string $version): static + { + return $this; + } + + public function withHeader(string $name, $value): static + { + $clone = clone $this; + $clone->headers[$name] = [$value]; + + return $clone; + } + + public function withoutHeader(string $name): static + { + $clone = clone $this; + unset($clone->headers[$name]); + + return $clone; + } +} diff --git a/tests/Helper/TestResponse.php b/tests/Helper/TestResponse.php new file mode 100644 index 00000000..ebdd881d --- /dev/null +++ b/tests/Helper/TestResponse.php @@ -0,0 +1,99 @@ +statusCode; + } + + public function getReasonPhrase(): string + { + return $this->reasonPhrase; + } + + public function getBody(): StreamInterface + { + return $this->body; + } + + public function getProtocolVersion(): string + { + return '1.1'; + } + + public function getHeaders(): array + { + return $this->headers; + } + + public function hasHeader(string $name): bool + { + return isset($this->headers[$name]); + } + + public function getHeader(string $name): array + { + return $this->headers[$name] ?? []; + } + + public function getHeaderLine(string $name): string + { + return \implode(', ', $this->getHeader($name)); + } + + public function withStatus(int $code, string $reasonPhrase = ''): static + { + return $this; + } + + public function withProtocolVersion(string $version): static + { + return $this; + } + + public function withHeader(string $name, $value): static + { + $clone = clone $this; + $clone->headers[$name] = [$value]; + + return $clone; + } + + public function withAddedHeader(string $name, $value): static + { + $clone = clone $this; + $clone->headers[$name][] = $value; + + return $clone; + } + + public function withoutHeader(string $name): static + { + $clone = clone $this; + unset($clone->headers[$name]); + + return $clone; + } + + public function withBody(StreamInterface $body): static + { + return new self($body, $this->statusCode, $this->reasonPhrase); + } +} diff --git a/tests/Helper/TestStream.php b/tests/Helper/TestStream.php new file mode 100644 index 00000000..7c466165 --- /dev/null +++ b/tests/Helper/TestStream.php @@ -0,0 +1,115 @@ +resource = $resource; + } + + public static function fromString(string $content): self + { + $resource = \fopen('php://temp', 'r+'); + \fwrite($resource, $content); + \rewind($resource); + + return new self($resource); + } + + public function __toString(): string + { + $this->rewind(); + + return $this->getContents(); + } + + public function close(): void + { + if ($this->resource !== null) { + \fclose($this->resource); + $this->resource = null; + } + } + + public function detach() + { + $resource = $this->resource; + $this->resource = null; + + return $resource; + } + + public function getSize(): int|null + { + return $this->resource === null ? null : (\fstat($this->resource)['size'] ?? null); + } + + public function tell(): int + { + return \ftell($this->resource); + } + + public function eof(): bool + { + return $this->resource === null || \feof($this->resource); + } + + public function isSeekable(): bool + { + return true; + } + + public function seek(int $offset, int $whence = SEEK_SET): void + { + \fseek($this->resource, $offset, $whence); + } + + public function rewind(): void + { + \rewind($this->resource); + } + + public function isWritable(): bool + { + return true; + } + + public function write(string $string): int + { + return \fwrite($this->resource, $string); + } + + public function isReadable(): bool + { + return true; + } + + public function read(int $length): string + { + return \fread($this->resource, $length); + } + + public function getContents(): string + { + return \stream_get_contents($this->resource); + } + + public function getMetadata(string|null $key = null) + { + $meta = \stream_get_meta_data($this->resource); + + return $key === null ? $meta : ($meta[$key] ?? null); + } +} diff --git a/tests/InputRichBlockTest.php b/tests/InputRichBlockTest.php new file mode 100644 index 00000000..acac5d90 --- /dev/null +++ b/tests/InputRichBlockTest.php @@ -0,0 +1,180 @@ +assertSame('paragraph', \json_decode(\json_encode(new InputRichBlockParagraph(text: 'x')), true)['type']); + $this->assertSame('divider', \json_decode(\json_encode(new InputRichBlockDivider()), true)['type']); + $this->assertSame('heading', \json_decode(\json_encode(new InputRichBlockSectionHeading(text: 'x', size: 2)), true)['type']); + } + + public function testNullFieldsAreOmitted(): void + { + $decoded = \json_decode(\json_encode(new InputRichBlockCollage(blocks: [])), true); + + $this->assertSame('collage', $decoded['type']); + $this->assertSame([], $decoded['blocks']); + $this->assertArrayNotHasKey('caption', $decoded); + } + + public function testNestedBlocksArePreserved(): void + { + $tree = new InputRichBlockDetails( + summary: 'Summary', + blocks: [ + new InputRichBlockCollage(blocks: [ + new InputRichBlockPhoto(photo: new InputMediaPhoto(media: new InputFile(__FILE__))), + ]), + new InputRichBlockParagraph(text: 'text'), + ], + isOpen: true, + ); + + $decoded = \json_decode(\json_encode($tree), true); + + $this->assertSame('details', $decoded['type']); + $this->assertTrue($decoded['is_open']); + $this->assertSame('collage', $decoded['blocks'][0]['type']); + $this->assertSame('photo', $decoded['blocks'][0]['blocks'][0]['type']); + $this->assertStringStartsWith('attach://', $decoded['blocks'][0]['blocks'][0]['photo']['media']); + $this->assertSame('paragraph', $decoded['blocks'][1]['type']); + } + + public function testListItemSerializesNumberingFields(): void + { + $decoded = \json_decode(\json_encode(new InputRichBlockList(items: [ + new InputRichBlockListItem(blocks: [new InputRichBlockParagraph(text: 'a')], value: 3, type: 'i'), + ])), true); + + $this->assertSame('list', $decoded['type']); + $this->assertSame(3, $decoded['items'][0]['value']); + $this->assertSame('i', $decoded['items'][0]['type']); + } + + public function testTableCellsSurviveArrayOfArrayNesting(): void + { + $cell = new \Luzrain\TelegramBotApi\Type\RichBlock\RichBlockTableCell(align: 'left', valign: 'top', text: 'c1'); + + $decoded = \json_decode(\json_encode( + new \Luzrain\TelegramBotApi\Type\InputRichBlock\InputRichBlockTable(cells: [[$cell, $cell], [$cell]]), + ), true); + + $this->assertCount(2, $decoded['cells']); + $this->assertCount(2, $decoded['cells'][0]); + $this->assertSame('left', $decoded['cells'][0][0]['align']); + $this->assertSame('c1', $decoded['cells'][0][0]['text']); + } + + public function testInputRichMessageCarriesBlocksAndMedia(): void + { + $message = new \Luzrain\TelegramBotApi\Type\InputRichMessage( + blocks: [new InputRichBlockParagraph(text: 'p')], + media: [new \Luzrain\TelegramBotApi\Type\InputRichMessageMedia( + id: 'pic-1', + media: new InputMediaPhoto(media: 'file_id_here'), + )], + ); + + $decoded = \json_decode(\json_encode($message), true); + + $this->assertSame('paragraph', $decoded['blocks'][0]['type']); + $this->assertSame('pic-1', $decoded['media'][0]['id']); + $this->assertArrayNotHasKey('html', $decoded); + } + + public function testNewInputBlocksSerializeCorrectDiscriminators(): void + { + $buttons = \json_decode(\json_encode(new \Luzrain\TelegramBotApi\Type\InputRichBlock\InputRichBlockButtons( + buttons: [new \Luzrain\TelegramBotApi\Type\RichMessageButton(text: 'A', callbackData: 'x')], + align: 'center', + )), true); + $quote = \json_decode(\json_encode(new \Luzrain\TelegramBotApi\Type\InputRichBlock\InputRichBlockExpandableBlockQuotation( + text: 'q', + credit: 'c', + )), true); + $doc = \json_decode(\json_encode(new \Luzrain\TelegramBotApi\Type\InputRichBlock\InputRichBlockDocument( + document: new \Luzrain\TelegramBotApi\Type\InputMediaDocument(media: 'file_id'), + )), true); + + $this->assertSame('buttons', $buttons['type']); + $this->assertSame('center', $buttons['align']); + $this->assertSame('x', $buttons['buttons'][0]['callback_data']); + $this->assertSame('expandable_blockquote', $quote['type']); + $this->assertSame('c', $quote['credit']); + $this->assertSame('document', $doc['type']); + } + + public function testInputRichBlockTableIsCompact(): void + { + $decoded = \json_decode(\json_encode( + new \Luzrain\TelegramBotApi\Type\InputRichBlock\InputRichBlockTable(cells: [], isCompact: true), + ), true); + + $this->assertTrue($decoded['is_compact']); + $this->assertArrayNotHasKey('is_bordered', $decoded); + } + + /** + * The realistic defect in a Bot API catch-up is a mistyped TYPE string, and the five-check + * gate cannot see it. The incoming RichBlock family predates this upgrade and is known good, + * so pinning every outgoing TYPE against its incoming twin catches a typo on either side. + * + * @return iterable + */ + public static function blockClassPairs(): iterable + { + foreach (\glob(__DIR__ . '/../src/Type/InputRichBlock/InputRichBlock*.php') as $file) { + $short = \basename($file, '.php'); + if ($short === 'InputRichBlock' || $short === 'InputRichBlockListItem') { + continue; + } + + yield $short => [ + 'Luzrain\\TelegramBotApi\\Type\\InputRichBlock\\' . $short, + 'Luzrain\\TelegramBotApi\\Type\\RichBlock\\' . \substr($short, 5), + ]; + } + } + + /** + * @dataProvider blockClassPairs + * @param class-string $inputClass + * @param class-string $outputClass + */ + public function testEveryInputBlockTypeMatchesItsOutgoingTwin(string $inputClass, string $outputClass): void + { + $this->assertTrue(\class_exists($outputClass), $outputClass . ' must exist'); + $this->assertSame( + \constant($outputClass . '::TYPE'), + \constant($inputClass . '::TYPE'), + $inputClass . '::TYPE must equal ' . $outputClass . '::TYPE', + ); + } + + public function testAllTwentyFourBlockPairsAreCovered(): void + { + $this->assertCount(24, \iterator_to_array(self::blockClassPairs())); + } + + public function testBlockWithoutFieldsSerializesAsJsonObjectNotArray(): void + { + $this->assertSame('{"type":"divider"}', \json_encode(new InputRichBlockDivider())); + } +} diff --git a/tests/MessageGenerationStoppedTest.php b/tests/MessageGenerationStoppedTest.php new file mode 100644 index 00000000..8f0e3a0c --- /dev/null +++ b/tests/MessageGenerationStoppedTest.php @@ -0,0 +1,113 @@ + 'f', + 'file_unique_id' => 'u', + 'type' => 'regular', + 'width' => 1, + 'height' => 1, + 'is_animated' => false, + 'is_video' => false, + ]; + + private function json(): string + { + return \file_get_contents(__DIR__ . '/data/events/messageGenerationStopped.json'); + } + + public function testHydrationIgnoresConstructorParameterOrder(): void + { + $update = Type\Update::fromJson($this->json()); + + $this->assertInstanceOf(Type\MessageGenerationStopped::class, $update->stoppedMessageGeneration); + $this->assertSame(99, $update->stoppedMessageGeneration->draftId); + $this->assertNull($update->stoppedMessageGeneration->messageThreadId); + $this->assertSame(14612111, $update->stoppedMessageGeneration->chat->id); + } + + public function testEventIsDispatched(): void + { + $helper = new ClosureTestHelper(); + $client = new ClientApi(); + $client->on(new Event\MessageGenerationStopped($helper->getClosure())); + $client->handle(Type\Update::fromJson($this->json())); + + $this->assertTrue($helper->isCalled()); + $this->assertSame(99, $helper->getParameter()->draftId); + } + + public function testUniqueGiftInfoKeepsExistingPositionalArguments(): void + { + $names = \array_map( + static fn(\ReflectionParameter $p) => $p->getName(), + (new \ReflectionClass(Type\UniqueGiftInfo::class))->getConstructor()->getParameters(), + ); + + $this->assertSame('gift', $names[0]); + $this->assertSame('origin', $names[1]); + $this->assertSame('lastResaleStarCount', $names[2]); + $this->assertSame(['text', 'entities', 'isPrivate'], \array_slice($names, -3)); + } + + public function testUniqueGiftInfoNewFieldTypes(): void + { + $byName = []; + foreach ((new \ReflectionClass(Type\UniqueGiftInfo::class))->getConstructor()->getParameters() as $p) { + $byName[$p->getName()] = (string) $p->getType(); + } + + $this->assertSame('?string', $byName['text']); + $this->assertSame('?array', $byName['entities']); + $this->assertSame('?true', $byName['isPrivate']); + } + + public function testEventIsNotDispatchedForUnrelatedUpdate(): void + { + $helper = new ClosureTestHelper(); + $client = new ClientApi(); + $client->on(new Event\MessageGenerationStopped($helper->getClosure())); + $client->handle(Type\Update::fromJson(\file_get_contents(__DIR__ . '/data/events/command.json'))); + + $this->assertFalse($helper->isCalled(), 'checker must not match an unrelated update'); + } + + public function testUniqueGiftInfoHydratesNewFields(): void + { + $info = Type\UniqueGiftInfo::fromArray([ + 'gift' => [ + 'gift_id' => 'gift-1', + 'base_name' => 'Gift', + 'name' => 'Gift-1', + 'number' => 1, + 'model' => ['name' => 'm', 'sticker' => self::STICKER, 'rarity_per_mille' => 1], + 'symbol' => ['name' => 's', 'sticker' => self::STICKER, 'rarity_per_mille' => 1], + 'backdrop' => [ + 'name' => 'b', + 'colors' => ['center_color' => 1, 'edge_color' => 2, 'symbol_color' => 3, 'text_color' => 4], + 'rarity_per_mille' => 1, + ], + ], + 'origin' => 'upgrade', + 'text' => 'Happy birthday', + 'entities' => [['type' => 'bold', 'offset' => 0, 'length' => 5]], + 'is_private' => true, + ]); + + $this->assertSame('Happy birthday', $info->text); + $this->assertInstanceOf(Type\MessageEntity::class, $info->entities[0]); + $this->assertSame('bold', $info->entities[0]->type); + $this->assertTrue($info->isPrivate); + } +} diff --git a/tests/ReplyMarkupTest.php b/tests/ReplyMarkupTest.php new file mode 100644 index 00000000..07207b95 --- /dev/null +++ b/tests/ReplyMarkupTest.php @@ -0,0 +1,97 @@ +assertTrue($decoded['force_reply']); + $this->assertSame('x', $decoded['inline_keyboard'][0][0]['text']); + } + + public function testInlineKeyboardMarkupStillAcceptsSinglePositionalArgument(): void + { + $markup = new Type\InlineKeyboardMarkup([[new Type\InlineKeyboardButton(text: 'x')]]); + + $this->assertCount(1, $markup->inlineKeyboard); + $this->assertNull($markup->forceReply); + } + + public function testReplyKeyboardMarkupForceReply(): void + { + $decoded = \json_decode(\json_encode(new Type\ReplyKeyboardMarkup( + keyboard: [[new Type\KeyboardButton(text: 'k')]], + forceReply: true, + )), true); + + $this->assertTrue($decoded['force_reply']); + } + + public function testInlineKeyboardButtonDisabled(): void + { + $decoded = \json_decode(\json_encode(new Type\InlineKeyboardButton( + text: 'x', + disabled: new Type\DisabledButton(), + )), true); + + $this->assertArrayHasKey('disabled', $decoded); + } + + public function testPromoteChatMemberAcceptsCanSendWelcomeMessages(): void + { + $params = \iterator_to_array((new Method\PromoteChatMember( + chatId: 1, + userId: 2, + canSendWelcomeMessages: true, + ))->getIterator()); + + $this->assertTrue($params['can_send_welcome_messages']); + } + + public function testAdministratorTypesAcceptCanSendWelcomeMessages(): void + { + foreach ([Type\ChatAdministratorRights::class, Type\ChatMemberAdministrator::class] as $class) { + $names = \array_map( + static fn(\ReflectionParameter $p) => $p->getName(), + (new \ReflectionClass($class))->getConstructor()->getParameters(), + ); + + $this->assertContains('canSendWelcomeMessages', $names, $class); + } + } + + public function testChatMemberAdministratorKeepsCustomTitleLast(): void + { + $names = \array_map( + static fn(\ReflectionParameter $p) => $p->getName(), + (new \ReflectionClass(Type\ChatMemberAdministrator::class))->getConstructor()->getParameters(), + ); + + $this->assertSame(['canSendWelcomeMessages', 'customTitle'], \array_slice($names, -2)); + } + + public function testDraftMethodsAcceptCanStopAndKeepOnStop(): void + { + foreach ([Method\SendMessageDraft::class, Method\SendRichMessageDraft::class] as $class) { + $names = \array_map( + static fn(\ReflectionParameter $p) => $p->getName(), + (new \ReflectionClass($class))->getConstructor()->getParameters(), + ); + + $this->assertContains('canStop', $names, $class); + $this->assertContains('keepOnStop', $names, $class); + } + } +} diff --git a/tests/RichMessageButtonTest.php b/tests/RichMessageButtonTest.php new file mode 100644 index 00000000..48483685 --- /dev/null +++ b/tests/RichMessageButtonTest.php @@ -0,0 +1,87 @@ + 'button', + 'button' => ['text' => 'Press', 'style' => 'primary', 'callback_data' => 'x'], + ]); + + $this->assertInstanceOf(RichTextButton::class, $rt); + $this->assertSame('button', $rt->type); + $this->assertInstanceOf(RichMessageButton::class, $rt->button); + $this->assertSame('primary', $rt->button->style); + $this->assertSame('x', $rt->button->callbackData); + } + + public function testRichMessageButtonSerializesSnakeCase(): void + { + $decoded = \json_decode(\json_encode(new RichMessageButton( + text: 'Go', + switchInlineQueryCurrentChat: 'q', + disabled: new DisabledButton(), + )), true); + + $this->assertSame('Go', $decoded['text']); + $this->assertSame('q', $decoded['switch_inline_query_current_chat']); + $this->assertArrayHasKey('disabled', $decoded); + $this->assertArrayNotHasKey('url', $decoded); + } + + public function testDisabledButtonSerializesAsJsonObjectNotArray(): void + { + $this->assertSame('{}', \json_encode(new DisabledButton())); + $this->assertStringContainsString('"disabled":{}', \json_encode(new RichMessageButton(text: 'x', disabled: new DisabledButton()))); + } + + public function testRichBlockDiscriminatorResolvesNewTypes(): void + { + $cases = [ + [['type' => 'buttons', 'buttons' => [['text' => 'A']], 'align' => 'center'], \Luzrain\TelegramBotApi\Type\RichBlock\RichBlockButtons::class], + [['type' => 'expandable_blockquote', 'text' => 'q', 'credit' => 'c'], \Luzrain\TelegramBotApi\Type\RichBlock\RichBlockExpandableBlockQuotation::class], + [['type' => 'document', 'document' => ['file_id' => 'f', 'file_unique_id' => 'u']], \Luzrain\TelegramBotApi\Type\RichBlock\RichBlockDocument::class], + ]; + + foreach ($cases as [$data, $expected]) { + $this->assertInstanceOf($expected, \Luzrain\TelegramBotApi\Type\RichBlock\RichBlock::fromArray($data)); + } + } + + public function testRichBlockButtonsCarriesButtons(): void + { + /** @var \Luzrain\TelegramBotApi\Type\RichBlock\RichBlockButtons $block */ + $block = \Luzrain\TelegramBotApi\Type\RichBlock\RichBlock::fromArray([ + 'type' => 'buttons', + 'buttons' => [['text' => 'A', 'callback_data' => 'd']], + 'align' => 'right', + ]); + + $this->assertSame('right', $block->align); + $this->assertInstanceOf(RichMessageButton::class, $block->buttons[0]); + $this->assertSame('d', $block->buttons[0]->callbackData); + } + + public function testRichBlockTableIsCompact(): void + { + /** @var \Luzrain\TelegramBotApi\Type\RichBlock\RichBlockTable $table */ + $table = \Luzrain\TelegramBotApi\Type\RichBlock\RichBlock::fromArray([ + 'type' => 'table', + 'cells' => [], + 'is_compact' => true, + ]); + + $this->assertTrue($table->isCompact); + } +} diff --git a/tests/data/events/botSubscriptionUpdated.json b/tests/data/events/botSubscriptionUpdated.json new file mode 100644 index 00000000..b930cf61 --- /dev/null +++ b/tests/data/events/botSubscriptionUpdated.json @@ -0,0 +1,8 @@ +{ + "update_id": 900000003, + "subscription": { + "user": {"id": 123456789, "is_bot": false, "first_name": "Anton", "username": "User2384885921"}, + "invoice_payload": "sub-payload-1", + "state": "canceled" + } +} diff --git a/tests/data/events/communityChatAdded.json b/tests/data/events/communityChatAdded.json new file mode 100644 index 00000000..aa1cc615 --- /dev/null +++ b/tests/data/events/communityChatAdded.json @@ -0,0 +1,11 @@ +{ + "update_id": 900000002, + "message": { + "message_id": 4242, + "date": 1756000100, + "chat": {"id": -1001234567890, "type": "supergroup", "title": "Community chat"}, + "community_chat_added": { + "community": {"id": 5555555555555, "name": "Test Community"} + } + } +} diff --git a/tests/data/events/ephemeralMessage.json b/tests/data/events/ephemeralMessage.json new file mode 100644 index 00000000..ed259fc8 --- /dev/null +++ b/tests/data/events/ephemeralMessage.json @@ -0,0 +1,12 @@ +{ + "update_id": 900000001, + "message": { + "message_id": 0, + "ephemeral_message_id": 55, + "date": 1756000000, + "chat": {"id": 14612111, "type": "supergroup", "title": "Test group"}, + "from": {"id": 123456789, "is_bot": false, "first_name": "Anton"}, + "receiver_user": {"id": 987654321, "is_bot": false, "first_name": "Receiver"}, + "text": "ephemeral hello" + } +} diff --git a/tests/data/events/messageGenerationStopped.json b/tests/data/events/messageGenerationStopped.json new file mode 100644 index 00000000..915e57ca --- /dev/null +++ b/tests/data/events/messageGenerationStopped.json @@ -0,0 +1,7 @@ +{ + "update_id": 900000004, + "stopped_message_generation": { + "chat": {"id": 14612111, "type": "private", "first_name": "Anton"}, + "draft_id": 99 + } +} From 23a8c02a9f0c35da5edd9a700297ece3d29a24f9 Mon Sep 17 00:00:00 2001 From: Ilia Agafonov Date: Sat, 19 Sep 2026 02:53:41 +0700 Subject: [PATCH 2/3] Update README.md --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index fffb84d8..c67f989c 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,13 @@ $response = $bot->call(new Method\SendDocument( */ ``` +You can pass `InputFile` where it sits inside another object: + +* `InputSticker` for `CreateNewStickerSet`, `AddStickerToSet` and `ReplaceStickerInSet` +* `InputStoryContentPhoto` and `InputStoryContentVideo` for `PostStory` and `EditStory` +* `InputProfilePhotoStatic` and `InputProfilePhotoAnimated` for `SetMyProfilePhoto` and `SetBusinessAccountProfilePhoto` +* the media blocks of `InputRichMessage` for `SendRichMessage` and `SendRichMessageDraft` + #### Send Media Group ```php From 5438f5c8204d737b22b3cd0dc3d5078276477133 Mon Sep 17 00:00:00 2001 From: Ilia Agafonov Date: Thu, 24 Sep 2026 15:05:19 +0700 Subject: [PATCH 3/3] Drop signature tests and README InputFile note - Remove reflection-based parameter position/name/type tests - Remove positional-argument construction tests - Remove nested InputFile list from README --- README.md | 7 --- tests/EphemeralMessageTest.php | 69 -------------------------- tests/MessageGenerationStoppedTest.php | 25 ---------- tests/ReplyMarkupTest.php | 43 ---------------- 4 files changed, 144 deletions(-) diff --git a/README.md b/README.md index c67f989c..fffb84d8 100644 --- a/README.md +++ b/README.md @@ -146,13 +146,6 @@ $response = $bot->call(new Method\SendDocument( */ ``` -You can pass `InputFile` where it sits inside another object: - -* `InputSticker` for `CreateNewStickerSet`, `AddStickerToSet` and `ReplaceStickerInSet` -* `InputStoryContentPhoto` and `InputStoryContentVideo` for `PostStory` and `EditStory` -* `InputProfilePhotoStatic` and `InputProfilePhotoAnimated` for `SetMyProfilePhoto` and `SetBusinessAccountProfilePhoto` -* the media blocks of `InputRichMessage` for `SendRichMessage` and `SendRichMessageDraft` - #### Send Media Group ```php diff --git a/tests/EphemeralMessageTest.php b/tests/EphemeralMessageTest.php index 9b1ff56f..a5feb47f 100644 --- a/tests/EphemeralMessageTest.php +++ b/tests/EphemeralMessageTest.php @@ -88,38 +88,6 @@ public function testEphemeralParametersAreSerialized(): void $this->assertSame(5, $params['ephemeral_message_parameters']->receiverUserId); } - public function testAllFourteenMethodsAcceptEphemeralParameters(): void - { - $expected = [ - 'SendMessage', 'SendAnimation', 'SendAudio', 'SendDocument', 'SendLivePhoto', - 'SendPhoto', 'SendSticker', 'SendVideo', 'SendVideoNote', 'SendVoice', - 'SendContact', 'SendLocation', 'SendVenue', 'SendRichMessage', - ]; - - foreach ($expected as $short) { - $names = \array_map( - static fn(\ReflectionParameter $p) => $p->getName(), - (new \ReflectionClass('Luzrain\\TelegramBotApi\\Method\\' . $short))->getConstructor()->getParameters(), - ); - - $this->assertContains('ephemeralMessageParameters', $names, $short . ' must accept ephemeralMessageParameters'); - } - } - - public function testMethodsWithoutEphemeralSupportAreUntouched(): void - { - $excluded = ['SendMediaGroup', 'SendPoll', 'SendDice', 'SendChecklist', 'SendGame', 'SendInvoice', 'SendPaidMedia']; - - foreach ($excluded as $short) { - $names = \array_map( - static fn(\ReflectionParameter $p) => $p->getName(), - (new \ReflectionClass('Luzrain\\TelegramBotApi\\Method\\' . $short))->getConstructor()->getParameters(), - ); - - $this->assertNotContains('ephemeralMessageParameters', $names, $short . ' must NOT accept ephemeralMessageParameters'); - } - } - public function testEphemeralMessageHydration(): void { $update = Type\Update::fromJson(\file_get_contents(__DIR__ . '/data/events/ephemeralMessage.json')); @@ -145,41 +113,4 @@ public function testReplyParametersAcceptsEphemeralMessageIdWithoutMessageId(): $this->assertSame(['ephemeral_message_id' => 7], $decoded); $this->assertArrayNotHasKey('message_id', $decoded); } - - public function testReplyParametersStillAcceptsMessageIdPositionally(): void - { - $decoded = \json_decode(\json_encode(new Type\ReplyParameters(5)), true); - - $this->assertSame(5, $decoded['message_id']); - } - - /** - * Appending rather than inserting is what keeps positional construction working for existing - * callers. assertContains would not notice a mid-signature insertion; asserting the position does. - */ - public function testEphemeralParametersIsAlwaysTheLastConstructorParameter(): void - { - $expected = [ - 'SendMessage', 'SendAnimation', 'SendAudio', 'SendDocument', 'SendLivePhoto', - 'SendPhoto', 'SendSticker', 'SendVideo', 'SendVideoNote', 'SendVoice', - 'SendContact', 'SendLocation', 'SendVenue', 'SendRichMessage', - ]; - - foreach ($expected as $short) { - $names = \array_map( - static fn(\ReflectionParameter $p) => $p->getName(), - (new \ReflectionClass('Luzrain\\TelegramBotApi\\Method\\' . $short))->getConstructor()->getParameters(), - ); - - $this->assertSame('ephemeralMessageParameters', \end($names), $short . ': parameter must be appended last'); - } - } - - public function testReplyParametersKeepsThreeArgumentPositionalCallsIntact(): void - { - $decoded = \json_decode(\json_encode(new Type\ReplyParameters(123, null, true)), true); - - $this->assertSame(['message_id' => 123, 'allow_sending_without_reply' => true], $decoded); - $this->assertArrayNotHasKey('ephemeral_message_id', $decoded); - } } diff --git a/tests/MessageGenerationStoppedTest.php b/tests/MessageGenerationStoppedTest.php index 8f0e3a0c..e443297c 100644 --- a/tests/MessageGenerationStoppedTest.php +++ b/tests/MessageGenerationStoppedTest.php @@ -48,31 +48,6 @@ public function testEventIsDispatched(): void $this->assertSame(99, $helper->getParameter()->draftId); } - public function testUniqueGiftInfoKeepsExistingPositionalArguments(): void - { - $names = \array_map( - static fn(\ReflectionParameter $p) => $p->getName(), - (new \ReflectionClass(Type\UniqueGiftInfo::class))->getConstructor()->getParameters(), - ); - - $this->assertSame('gift', $names[0]); - $this->assertSame('origin', $names[1]); - $this->assertSame('lastResaleStarCount', $names[2]); - $this->assertSame(['text', 'entities', 'isPrivate'], \array_slice($names, -3)); - } - - public function testUniqueGiftInfoNewFieldTypes(): void - { - $byName = []; - foreach ((new \ReflectionClass(Type\UniqueGiftInfo::class))->getConstructor()->getParameters() as $p) { - $byName[$p->getName()] = (string) $p->getType(); - } - - $this->assertSame('?string', $byName['text']); - $this->assertSame('?array', $byName['entities']); - $this->assertSame('?true', $byName['isPrivate']); - } - public function testEventIsNotDispatchedForUnrelatedUpdate(): void { $helper = new ClosureTestHelper(); diff --git a/tests/ReplyMarkupTest.php b/tests/ReplyMarkupTest.php index 07207b95..f62e81b5 100644 --- a/tests/ReplyMarkupTest.php +++ b/tests/ReplyMarkupTest.php @@ -21,14 +21,6 @@ public function testInlineKeyboardMarkupForceReply(): void $this->assertSame('x', $decoded['inline_keyboard'][0][0]['text']); } - public function testInlineKeyboardMarkupStillAcceptsSinglePositionalArgument(): void - { - $markup = new Type\InlineKeyboardMarkup([[new Type\InlineKeyboardButton(text: 'x')]]); - - $this->assertCount(1, $markup->inlineKeyboard); - $this->assertNull($markup->forceReply); - } - public function testReplyKeyboardMarkupForceReply(): void { $decoded = \json_decode(\json_encode(new Type\ReplyKeyboardMarkup( @@ -59,39 +51,4 @@ public function testPromoteChatMemberAcceptsCanSendWelcomeMessages(): void $this->assertTrue($params['can_send_welcome_messages']); } - - public function testAdministratorTypesAcceptCanSendWelcomeMessages(): void - { - foreach ([Type\ChatAdministratorRights::class, Type\ChatMemberAdministrator::class] as $class) { - $names = \array_map( - static fn(\ReflectionParameter $p) => $p->getName(), - (new \ReflectionClass($class))->getConstructor()->getParameters(), - ); - - $this->assertContains('canSendWelcomeMessages', $names, $class); - } - } - - public function testChatMemberAdministratorKeepsCustomTitleLast(): void - { - $names = \array_map( - static fn(\ReflectionParameter $p) => $p->getName(), - (new \ReflectionClass(Type\ChatMemberAdministrator::class))->getConstructor()->getParameters(), - ); - - $this->assertSame(['canSendWelcomeMessages', 'customTitle'], \array_slice($names, -2)); - } - - public function testDraftMethodsAcceptCanStopAndKeepOnStop(): void - { - foreach ([Method\SendMessageDraft::class, Method\SendRichMessageDraft::class] as $class) { - $names = \array_map( - static fn(\ReflectionParameter $p) => $p->getName(), - (new \ReflectionClass($class))->getConstructor()->getParameters(), - ); - - $this->assertContains('canStop', $names, $class); - $this->assertContains('keepOnStop', $names, $class); - } - } }