From 65aabecd22e583bee9ae6d8b326540fd49fa9e6c Mon Sep 17 00:00:00 2001 From: Tii Date: Sat, 8 Jun 2024 16:50:25 +0200 Subject: [PATCH 01/20] Complete rewrite from scratch Initial version --- .editorconfig | 12 - .gitattributes | 5 - .github/ISSUE_TEMPLATE/BC_Break.md | 56 - .github/ISSUE_TEMPLATE/Bug.md | 46 - .github/ISSUE_TEMPLATE/Feature_Request.md | 11 - .github/ISSUE_TEMPLATE/Support_Question.md | 38 - .github/PULL_REQUEST_TEMPLATE.md | 19 - .github/workflows/tests.yaml | 84 - .gitignore | 2 +- .scrutinizer.yml | 17 - CHANGELOG.md | 740 ------- CONTRIBUTING.md | 59 - CREDITS | 27 - LICENSE => LICENSE.txt | 5 +- README.md | 771 +------- SECURITY.md | 3 - composer.json | 142 +- doc/01-utils.md | 58 - phpcs.xml.dist | 19 - phpunit.xml.dist | 26 - src/ChatAction.php | 70 - src/Commands/AdminCommand.php | 20 - src/Commands/AdminCommands/ChatsCommand.php | 143 -- src/Commands/AdminCommands/CleanupCommand.php | 435 ----- src/Commands/AdminCommands/DebugCommand.php | 127 -- .../AdminCommands/SendtoallCommand.php | 113 -- .../AdminCommands/SendtochannelCommand.php | 379 ---- src/Commands/AdminCommands/WhoisCommand.php | 190 -- src/Commands/Command.php | 456 ----- src/Commands/SystemCommand.php | 124 -- .../SystemCommands/CallbackqueryCommand.php | 74 - .../SystemCommands/GenericCommand.php | 35 - .../SystemCommands/GenericmessageCommand.php | 81 - .../SystemCommands/InlinequeryCommand.php | 50 - src/Commands/UserCommand.php | 17 - src/Commands/UserCommands/StartCommand.php | 56 - src/Conversation.php | 264 --- src/ConversationDB.php | 132 -- src/DB.php | 1725 ----------------- src/Entities/Animation.php | 42 - src/Entities/Audio.php | 40 - src/Entities/BotCommand.php | 27 - .../BotCommandScope/BotCommandScope.php | 8 - .../BotCommandScopeAllChatAdministrators.php | 19 - .../BotCommandScopeAllGroupChats.php | 19 - .../BotCommandScopeAllPrivateChats.php | 19 - .../BotCommandScope/BotCommandScopeChat.php | 30 - .../BotCommandScopeChatAdministrators.php | 30 - .../BotCommandScopeChatMember.php | 33 - .../BotCommandScopeDefault.php | 19 - src/Entities/BotDescription.php | 17 - src/Entities/BotName.php | 17 - src/Entities/BotShortDescription.php | 17 - src/Entities/CallbackQuery.php | 57 - src/Entities/ChannelPost.php | 17 - src/Entities/Chat.php | 143 -- src/Entities/ChatAdministratorRights.php | 43 - src/Entities/ChatBoost.php | 37 - src/Entities/ChatBoostRemoved.php | 38 - .../ChatBoostSource/ChatBoostSource.php | 16 - .../ChatBoostSourceGiftCode.php | 27 - .../ChatBoostSourceGiveaway.php | 29 - .../ChatBoostSourceNotImplemented.php | 15 - .../ChatBoostSourcePremium.php | 27 - src/Entities/ChatBoostSource/Factory.php | 24 - src/Entities/ChatBoostUpdated.php | 34 - src/Entities/ChatInviteLink.php | 42 - src/Entities/ChatJoinRequest.php | 29 - src/Entities/ChatLocation.php | 35 - src/Entities/ChatMember/ChatMember.php | 16 - .../ChatMember/ChatMemberAdministrator.php | 44 - src/Entities/ChatMember/ChatMemberBanned.php | 28 - src/Entities/ChatMember/ChatMemberLeft.php | 27 - src/Entities/ChatMember/ChatMemberMember.php | 27 - .../ChatMember/ChatMemberNotImplemented.php | 17 - src/Entities/ChatMember/ChatMemberOwner.php | 29 - .../ChatMember/ChatMemberRestricted.php | 60 - src/Entities/ChatMember/Factory.php | 27 - src/Entities/ChatMemberUpdated.php | 47 - src/Entities/ChatPermissions.php | 52 - src/Entities/ChatPhoto.php | 27 - src/Entities/ChatShared.php | 18 - src/Entities/ChosenInlineResult.php | 37 - src/Entities/Contact.php | 28 - src/Entities/Dice.php | 27 - src/Entities/Document.php | 37 - src/Entities/EditedChannelPost.php | 17 - src/Entities/EditedMessage.php | 17 - src/Entities/Entity.php | 313 --- src/Entities/ExternalReplyInfo.php | 74 - src/Entities/Factory.php | 21 - src/Entities/File.php | 27 - src/Entities/Games/CallbackGame.php | 26 - src/Entities/Games/Game.php | 46 - src/Entities/Games/GameHighScore.php | 39 - src/Entities/Giveaway/Giveaway.php | 30 - src/Entities/Giveaway/GiveawayCompleted.php | 25 - src/Entities/Giveaway/GiveawayCreated.php | 10 - src/Entities/Giveaway/GiveawayWinners.php | 35 - src/Entities/InlineKeyboard.php | 21 - src/Entities/InlineKeyboardButton.php | 80 - src/Entities/InlineQuery.php | 57 - src/Entities/InlineQuery/InlineEntity.php | 24 - .../InlineQuery/InlineQueryResult.php | 8 - .../InlineQuery/InlineQueryResultArticle.php | 72 - .../InlineQuery/InlineQueryResultAudio.php | 70 - .../InlineQueryResultCachedAudio.php | 61 - .../InlineQueryResultCachedDocument.php | 67 - .../InlineQueryResultCachedGif.php | 64 - .../InlineQueryResultCachedMpeg4Gif.php | 64 - .../InlineQueryResultCachedPhoto.php | 67 - .../InlineQueryResultCachedSticker.php | 54 - .../InlineQueryResultCachedVideo.php | 67 - .../InlineQueryResultCachedVoice.php | 64 - .../InlineQuery/InlineQueryResultContact.php | 71 - .../InlineQuery/InlineQueryResultDocument.php | 79 - .../InlineQuery/InlineQueryResultGame.php | 50 - .../InlineQuery/InlineQueryResultGif.php | 77 - .../InlineQuery/InlineQueryResultLocation.php | 81 - .../InlineQuery/InlineQueryResultMpeg4Gif.php | 77 - .../InlineQuery/InlineQueryResultPhoto.php | 76 - .../InlineQuery/InlineQueryResultVenue.php | 81 - .../InlineQuery/InlineQueryResultVideo.php | 82 - .../InlineQuery/InlineQueryResultVoice.php | 67 - src/Entities/InlineQueryResultsButton.php | 27 - src/Entities/InputMedia/InputMedia.php | 8 - .../InputMedia/InputMediaAnimation.php | 66 - src/Entities/InputMedia/InputMediaAudio.php | 64 - .../InputMedia/InputMediaDocument.php | 57 - src/Entities/InputMedia/InputMediaPhoto.php | 54 - src/Entities/InputMedia/InputMediaVideo.php | 69 - .../InputContactMessageContent.php | 43 - .../InputInvoiceMessageContent.php | 66 - .../InputLocationMessageContent.php | 48 - .../InputMessageContent.php | 8 - .../InputTextMessageContent.php | 34 - .../InputVenueMessageContent.php | 53 - src/Entities/InputSticker.php | 20 - src/Entities/Keyboard.php | 231 --- src/Entities/KeyboardButton.php | 91 - src/Entities/KeyboardButtonPollType.php | 28 - src/Entities/KeyboardButtonRequestChat.php | 33 - src/Entities/KeyboardButtonRequestUsers.php | 23 - src/Entities/LinkPreviewOptions.php | 28 - src/Entities/Location.php | 29 - src/Entities/LoginUrl.php | 29 - src/Entities/MaskPosition.php | 27 - src/Entities/MenuButton/Factory.php | 24 - src/Entities/MenuButton/MenuButton.php | 15 - .../MenuButton/MenuButtonCommands.php | 15 - src/Entities/MenuButton/MenuButtonDefault.php | 15 - .../MenuButton/MenuButtonNotImplemented.php | 8 - src/Entities/MenuButton/MenuButtonWebApp.php | 30 - src/Entities/Message.php | 337 ---- src/Entities/Message/Factory.php | 21 - src/Entities/Message/InaccessibleMessage.php | 21 - .../Message/MaybeInaccessibleMessage.php | 15 - .../MessageAutoDeleteTimerChanged.php | 26 - src/Entities/MessageEntity.php | 38 - src/Entities/MessageOrigin/Factory.php | 25 - src/Entities/MessageOrigin/MessageOrigin.php | 14 - .../MessageOrigin/MessageOriginChannel.php | 27 - .../MessageOrigin/MessageOriginChat.php | 26 - .../MessageOrigin/MessageOriginHiddenUser.php | 19 - .../MessageOriginNotImplemented.php | 14 - .../MessageOrigin/MessageOriginUser.php | 25 - src/Entities/MessageReactionCountUpdated.php | 24 - src/Entities/MessageReactionUpdated.php | 33 - src/Entities/Payments/Invoice.php | 32 - src/Entities/Payments/LabeledPrice.php | 29 - src/Entities/Payments/OrderInfo.php | 39 - src/Entities/Payments/PreCheckoutQuery.php | 62 - src/Entities/Payments/ShippingAddress.php | 33 - src/Entities/Payments/ShippingOption.php | 38 - src/Entities/Payments/ShippingQuery.php | 59 - src/Entities/Payments/SuccessfulPayment.php | 42 - src/Entities/PhotoSize.php | 28 - src/Entities/Poll.php | 47 - src/Entities/PollAnswer.php | 38 - src/Entities/PollOption.php | 27 - src/Entities/ProximityAlertTriggered.php | 37 - src/Entities/ReactionCount.php | 24 - src/Entities/ReactionType/Factory.php | 23 - src/Entities/ReactionType/ReactionType.php | 15 - .../ReactionType/ReactionTypeCustomEmoji.php | 22 - .../ReactionType/ReactionTypeEmoji.php | 22 - .../ReactionTypeNotImplemented.php | 10 - src/Entities/ReplyParameters.php | 38 - src/Entities/ReplyToMessage.php | 35 - src/Entities/SentWebAppMessage.php | 20 - src/Entities/ServerResponse.php | 164 -- src/Entities/Sticker.php | 48 - src/Entities/StickerSet.php | 39 - src/Entities/Story.php | 22 - src/Entities/SwitchInlineQueryChosenChat.php | 21 - .../TelegramPassport/EncryptedCredentials.php | 30 - .../EncryptedPassportElement.php | 49 - .../TelegramPassport/PassportData.php | 38 - .../PassportElementError.php | 17 - .../PassportElementErrorDataField.php | 41 - .../PassportElementErrorFile.php | 40 - .../PassportElementErrorFiles.php | 40 - .../PassportElementErrorFrontSide.php | 40 - .../PassportElementErrorReverseSide.php | 40 - .../PassportElementErrorSelfie.php | 40 - .../PassportElementErrorTranslationFile.php | 40 - .../PassportElementErrorTranslationFiles.php | 40 - .../PassportElementErrorUnspecified.php | 40 - .../TelegramPassport/PassportFile.php | 31 - src/Entities/TextQuote.php | 35 - src/Entities/Topics/ForumTopicClosed.php | 15 - src/Entities/Topics/ForumTopicCreated.php | 21 - src/Entities/Topics/ForumTopicEdited.php | 20 - src/Entities/Topics/ForumTopicReopened.php | 15 - .../Topics/GeneralForumTopicHidden.php | 15 - .../Topics/GeneralForumTopicUnhidden.php | 15 - src/Entities/Update.php | 132 -- src/Entities/User.php | 34 - src/Entities/UserChatBoosts.php | 32 - src/Entities/UserProfilePhotos.php | 54 - src/Entities/UsersShared.php | 16 - src/Entities/Venue.php | 38 - src/Entities/Video.php | 40 - src/Entities/VideoChatEnded.php | 24 - src/Entities/VideoChatParticipantsInvited.php | 32 - src/Entities/VideoChatScheduled.php | 24 - src/Entities/VideoChatStarted.php | 22 - src/Entities/VideoNote.php | 37 - src/Entities/Voice.php | 28 - src/Entities/WebAppData.php | 21 - src/Entities/WebAppInfo.php | 19 - src/Entities/WebhookInfo.php | 32 - src/Entities/WriteAccessAllowed.php | 19 - src/Exception/InvalidBotTokenException.php | 26 - src/Exception/TelegramException.php | 22 - src/Exception/TelegramLogException.php | 20 - src/Exceptions/NotYetImplementedException.php | 11 + src/Exceptions/TelegramException.php | 7 + src/HttpClient.php | 43 + src/Request.php | 1038 ---------- src/Telegram.php | 1337 +------------ src/TelegramLog.php | 168 -- src/Types/Poll.php | 26 + src/Types/Type.php | 18 + structure.sql | 438 ----- tests/Unit/Commands/CommandTest.php | 171 -- tests/Unit/Commands/CommandTestCase.php | 56 - .../CustomTestCommands/DummyAdminCommand.php | 47 - .../CustomTestCommands/DummySystemCommand.php | 47 - .../CustomTestCommands/DummyUserCommand.php | 47 - .../CustomTestCommands/HiddenCommand.php | 57 - .../CustomTestCommands/VisibleCommand.php | 57 - tests/Unit/ConversationTest.php | 126 -- tests/Unit/Entities/AudioTest.php | 53 - tests/Unit/Entities/ChatTest.php | 74 - tests/Unit/Entities/EntityTest.php | 54 - tests/Unit/Entities/FileTest.php | 84 - .../Entities/InlineKeyboardButtonTest.php | 95 - tests/Unit/Entities/InlineKeyboardTest.php | 137 -- tests/Unit/Entities/KeyboardButtonTest.php | 67 - tests/Unit/Entities/KeyboardTest.php | 208 -- tests/Unit/Entities/LocationTest.php | 57 - tests/Unit/Entities/MessageTest.php | 96 - tests/Unit/Entities/ReplyToMessageTest.php | 51 - tests/Unit/Entities/ServerResponseTest.php | 386 ---- tests/Unit/Entities/UpdateTest.php | 52 - tests/Unit/Entities/UserTest.php | 85 - tests/Unit/Entities/WebhookInfoTest.php | 143 -- tests/Unit/TelegramLogTest.php | 116 -- tests/Unit/TelegramTest.php | 261 --- tests/Unit/TestCase.php | 31 - tests/Unit/TestHelpers.php | 251 --- utils/db-schema-update/0.44.1-0.45.0.sql | 4 - utils/db-schema-update/0.47.1-0.48.0.sql | 1 - utils/db-schema-update/0.50.0-0.51.0.sql | 1 - utils/db-schema-update/0.52.0-0.53.0.sql | 1 - utils/db-schema-update/0.53.0-0.54.0.sql | 1 - utils/db-schema-update/0.54.1-0.55.0.sql | 2 - utils/db-schema-update/0.56.0-0.57.0.sql | 69 - utils/db-schema-update/0.57.0-0.58.0.sql | 1 - utils/db-schema-update/0.60.0-0.61.0.sql | 10 - utils/db-schema-update/0.61.1-0.62.0.sql | 20 - utils/db-schema-update/0.62.0-0.63.0.sql | 15 - utils/db-schema-update/0.64.0-0.70.0.sql | 3 - utils/db-schema-update/0.71.0-0.72.0.sql | 27 - utils/db-schema-update/0.72.0-0.73.0.sql | 2 - utils/db-schema-update/0.74.0-0.75.0.sql | 20 - utils/db-schema-update/0.76.1-0.77.0.sql | 7 - utils/db-schema-update/0.77.1-0.78.0.sql | 2 - utils/db-schema-update/0.79.0-0.80.0.sql | 9 - utils/db-schema-update/0.80.0-0.81.0.sql | 8 - utils/db-schema-update/0.81.0-0.82.0.sql | 2 - utils/db-schema-update/0.82.0-unreleased.sql | 67 - utils/importFromLog.php | 59 - 294 files changed, 311 insertions(+), 20661 deletions(-) delete mode 100644 .editorconfig delete mode 100644 .gitattributes delete mode 100644 .github/ISSUE_TEMPLATE/BC_Break.md delete mode 100644 .github/ISSUE_TEMPLATE/Bug.md delete mode 100644 .github/ISSUE_TEMPLATE/Feature_Request.md delete mode 100644 .github/ISSUE_TEMPLATE/Support_Question.md delete mode 100644 .github/PULL_REQUEST_TEMPLATE.md delete mode 100644 .github/workflows/tests.yaml delete mode 100644 .scrutinizer.yml delete mode 100644 CHANGELOG.md delete mode 100644 CONTRIBUTING.md delete mode 100644 CREDITS rename LICENSE => LICENSE.txt (87%) delete mode 100644 SECURITY.md delete mode 100644 doc/01-utils.md delete mode 100644 phpcs.xml.dist delete mode 100644 phpunit.xml.dist delete mode 100644 src/ChatAction.php delete mode 100644 src/Commands/AdminCommand.php delete mode 100644 src/Commands/AdminCommands/ChatsCommand.php delete mode 100644 src/Commands/AdminCommands/CleanupCommand.php delete mode 100644 src/Commands/AdminCommands/DebugCommand.php delete mode 100644 src/Commands/AdminCommands/SendtoallCommand.php delete mode 100644 src/Commands/AdminCommands/SendtochannelCommand.php delete mode 100644 src/Commands/AdminCommands/WhoisCommand.php delete mode 100644 src/Commands/Command.php delete mode 100644 src/Commands/SystemCommand.php delete mode 100644 src/Commands/SystemCommands/CallbackqueryCommand.php delete mode 100644 src/Commands/SystemCommands/GenericCommand.php delete mode 100644 src/Commands/SystemCommands/GenericmessageCommand.php delete mode 100644 src/Commands/SystemCommands/InlinequeryCommand.php delete mode 100644 src/Commands/UserCommand.php delete mode 100644 src/Commands/UserCommands/StartCommand.php delete mode 100644 src/Conversation.php delete mode 100644 src/ConversationDB.php delete mode 100644 src/DB.php delete mode 100644 src/Entities/Animation.php delete mode 100644 src/Entities/Audio.php delete mode 100644 src/Entities/BotCommand.php delete mode 100644 src/Entities/BotCommandScope/BotCommandScope.php delete mode 100644 src/Entities/BotCommandScope/BotCommandScopeAllChatAdministrators.php delete mode 100644 src/Entities/BotCommandScope/BotCommandScopeAllGroupChats.php delete mode 100644 src/Entities/BotCommandScope/BotCommandScopeAllPrivateChats.php delete mode 100644 src/Entities/BotCommandScope/BotCommandScopeChat.php delete mode 100644 src/Entities/BotCommandScope/BotCommandScopeChatAdministrators.php delete mode 100644 src/Entities/BotCommandScope/BotCommandScopeChatMember.php delete mode 100644 src/Entities/BotCommandScope/BotCommandScopeDefault.php delete mode 100644 src/Entities/BotDescription.php delete mode 100644 src/Entities/BotName.php delete mode 100644 src/Entities/BotShortDescription.php delete mode 100644 src/Entities/CallbackQuery.php delete mode 100644 src/Entities/ChannelPost.php delete mode 100644 src/Entities/Chat.php delete mode 100644 src/Entities/ChatAdministratorRights.php delete mode 100644 src/Entities/ChatBoost.php delete mode 100644 src/Entities/ChatBoostRemoved.php delete mode 100644 src/Entities/ChatBoostSource/ChatBoostSource.php delete mode 100644 src/Entities/ChatBoostSource/ChatBoostSourceGiftCode.php delete mode 100644 src/Entities/ChatBoostSource/ChatBoostSourceGiveaway.php delete mode 100644 src/Entities/ChatBoostSource/ChatBoostSourceNotImplemented.php delete mode 100644 src/Entities/ChatBoostSource/ChatBoostSourcePremium.php delete mode 100644 src/Entities/ChatBoostSource/Factory.php delete mode 100644 src/Entities/ChatBoostUpdated.php delete mode 100644 src/Entities/ChatInviteLink.php delete mode 100644 src/Entities/ChatJoinRequest.php delete mode 100644 src/Entities/ChatLocation.php delete mode 100644 src/Entities/ChatMember/ChatMember.php delete mode 100644 src/Entities/ChatMember/ChatMemberAdministrator.php delete mode 100644 src/Entities/ChatMember/ChatMemberBanned.php delete mode 100644 src/Entities/ChatMember/ChatMemberLeft.php delete mode 100644 src/Entities/ChatMember/ChatMemberMember.php delete mode 100644 src/Entities/ChatMember/ChatMemberNotImplemented.php delete mode 100644 src/Entities/ChatMember/ChatMemberOwner.php delete mode 100644 src/Entities/ChatMember/ChatMemberRestricted.php delete mode 100644 src/Entities/ChatMember/Factory.php delete mode 100644 src/Entities/ChatMemberUpdated.php delete mode 100644 src/Entities/ChatPermissions.php delete mode 100644 src/Entities/ChatPhoto.php delete mode 100644 src/Entities/ChatShared.php delete mode 100644 src/Entities/ChosenInlineResult.php delete mode 100644 src/Entities/Contact.php delete mode 100644 src/Entities/Dice.php delete mode 100644 src/Entities/Document.php delete mode 100644 src/Entities/EditedChannelPost.php delete mode 100644 src/Entities/EditedMessage.php delete mode 100644 src/Entities/Entity.php delete mode 100644 src/Entities/ExternalReplyInfo.php delete mode 100644 src/Entities/Factory.php delete mode 100644 src/Entities/File.php delete mode 100644 src/Entities/Games/CallbackGame.php delete mode 100644 src/Entities/Games/Game.php delete mode 100644 src/Entities/Games/GameHighScore.php delete mode 100644 src/Entities/Giveaway/Giveaway.php delete mode 100644 src/Entities/Giveaway/GiveawayCompleted.php delete mode 100644 src/Entities/Giveaway/GiveawayCreated.php delete mode 100644 src/Entities/Giveaway/GiveawayWinners.php delete mode 100644 src/Entities/InlineKeyboard.php delete mode 100644 src/Entities/InlineKeyboardButton.php delete mode 100644 src/Entities/InlineQuery.php delete mode 100644 src/Entities/InlineQuery/InlineEntity.php delete mode 100644 src/Entities/InlineQuery/InlineQueryResult.php delete mode 100644 src/Entities/InlineQuery/InlineQueryResultArticle.php delete mode 100644 src/Entities/InlineQuery/InlineQueryResultAudio.php delete mode 100644 src/Entities/InlineQuery/InlineQueryResultCachedAudio.php delete mode 100644 src/Entities/InlineQuery/InlineQueryResultCachedDocument.php delete mode 100644 src/Entities/InlineQuery/InlineQueryResultCachedGif.php delete mode 100644 src/Entities/InlineQuery/InlineQueryResultCachedMpeg4Gif.php delete mode 100644 src/Entities/InlineQuery/InlineQueryResultCachedPhoto.php delete mode 100644 src/Entities/InlineQuery/InlineQueryResultCachedSticker.php delete mode 100644 src/Entities/InlineQuery/InlineQueryResultCachedVideo.php delete mode 100644 src/Entities/InlineQuery/InlineQueryResultCachedVoice.php delete mode 100644 src/Entities/InlineQuery/InlineQueryResultContact.php delete mode 100644 src/Entities/InlineQuery/InlineQueryResultDocument.php delete mode 100644 src/Entities/InlineQuery/InlineQueryResultGame.php delete mode 100644 src/Entities/InlineQuery/InlineQueryResultGif.php delete mode 100644 src/Entities/InlineQuery/InlineQueryResultLocation.php delete mode 100644 src/Entities/InlineQuery/InlineQueryResultMpeg4Gif.php delete mode 100644 src/Entities/InlineQuery/InlineQueryResultPhoto.php delete mode 100644 src/Entities/InlineQuery/InlineQueryResultVenue.php delete mode 100644 src/Entities/InlineQuery/InlineQueryResultVideo.php delete mode 100644 src/Entities/InlineQuery/InlineQueryResultVoice.php delete mode 100644 src/Entities/InlineQueryResultsButton.php delete mode 100644 src/Entities/InputMedia/InputMedia.php delete mode 100644 src/Entities/InputMedia/InputMediaAnimation.php delete mode 100644 src/Entities/InputMedia/InputMediaAudio.php delete mode 100644 src/Entities/InputMedia/InputMediaDocument.php delete mode 100644 src/Entities/InputMedia/InputMediaPhoto.php delete mode 100644 src/Entities/InputMedia/InputMediaVideo.php delete mode 100644 src/Entities/InputMessageContent/InputContactMessageContent.php delete mode 100644 src/Entities/InputMessageContent/InputInvoiceMessageContent.php delete mode 100644 src/Entities/InputMessageContent/InputLocationMessageContent.php delete mode 100644 src/Entities/InputMessageContent/InputMessageContent.php delete mode 100644 src/Entities/InputMessageContent/InputTextMessageContent.php delete mode 100644 src/Entities/InputMessageContent/InputVenueMessageContent.php delete mode 100644 src/Entities/InputSticker.php delete mode 100644 src/Entities/Keyboard.php delete mode 100644 src/Entities/KeyboardButton.php delete mode 100644 src/Entities/KeyboardButtonPollType.php delete mode 100644 src/Entities/KeyboardButtonRequestChat.php delete mode 100644 src/Entities/KeyboardButtonRequestUsers.php delete mode 100644 src/Entities/LinkPreviewOptions.php delete mode 100644 src/Entities/Location.php delete mode 100644 src/Entities/LoginUrl.php delete mode 100644 src/Entities/MaskPosition.php delete mode 100644 src/Entities/MenuButton/Factory.php delete mode 100644 src/Entities/MenuButton/MenuButton.php delete mode 100644 src/Entities/MenuButton/MenuButtonCommands.php delete mode 100644 src/Entities/MenuButton/MenuButtonDefault.php delete mode 100644 src/Entities/MenuButton/MenuButtonNotImplemented.php delete mode 100644 src/Entities/MenuButton/MenuButtonWebApp.php delete mode 100644 src/Entities/Message.php delete mode 100644 src/Entities/Message/Factory.php delete mode 100644 src/Entities/Message/InaccessibleMessage.php delete mode 100644 src/Entities/Message/MaybeInaccessibleMessage.php delete mode 100644 src/Entities/MessageAutoDeleteTimerChanged.php delete mode 100644 src/Entities/MessageEntity.php delete mode 100644 src/Entities/MessageOrigin/Factory.php delete mode 100644 src/Entities/MessageOrigin/MessageOrigin.php delete mode 100644 src/Entities/MessageOrigin/MessageOriginChannel.php delete mode 100644 src/Entities/MessageOrigin/MessageOriginChat.php delete mode 100644 src/Entities/MessageOrigin/MessageOriginHiddenUser.php delete mode 100644 src/Entities/MessageOrigin/MessageOriginNotImplemented.php delete mode 100644 src/Entities/MessageOrigin/MessageOriginUser.php delete mode 100644 src/Entities/MessageReactionCountUpdated.php delete mode 100644 src/Entities/MessageReactionUpdated.php delete mode 100644 src/Entities/Payments/Invoice.php delete mode 100644 src/Entities/Payments/LabeledPrice.php delete mode 100644 src/Entities/Payments/OrderInfo.php delete mode 100644 src/Entities/Payments/PreCheckoutQuery.php delete mode 100644 src/Entities/Payments/ShippingAddress.php delete mode 100644 src/Entities/Payments/ShippingOption.php delete mode 100644 src/Entities/Payments/ShippingQuery.php delete mode 100644 src/Entities/Payments/SuccessfulPayment.php delete mode 100644 src/Entities/PhotoSize.php delete mode 100644 src/Entities/Poll.php delete mode 100644 src/Entities/PollAnswer.php delete mode 100644 src/Entities/PollOption.php delete mode 100644 src/Entities/ProximityAlertTriggered.php delete mode 100644 src/Entities/ReactionCount.php delete mode 100644 src/Entities/ReactionType/Factory.php delete mode 100644 src/Entities/ReactionType/ReactionType.php delete mode 100644 src/Entities/ReactionType/ReactionTypeCustomEmoji.php delete mode 100644 src/Entities/ReactionType/ReactionTypeEmoji.php delete mode 100644 src/Entities/ReactionType/ReactionTypeNotImplemented.php delete mode 100644 src/Entities/ReplyParameters.php delete mode 100644 src/Entities/ReplyToMessage.php delete mode 100644 src/Entities/SentWebAppMessage.php delete mode 100644 src/Entities/ServerResponse.php delete mode 100644 src/Entities/Sticker.php delete mode 100644 src/Entities/StickerSet.php delete mode 100644 src/Entities/Story.php delete mode 100644 src/Entities/SwitchInlineQueryChosenChat.php delete mode 100644 src/Entities/TelegramPassport/EncryptedCredentials.php delete mode 100644 src/Entities/TelegramPassport/EncryptedPassportElement.php delete mode 100644 src/Entities/TelegramPassport/PassportData.php delete mode 100644 src/Entities/TelegramPassport/PassportElementError/PassportElementError.php delete mode 100644 src/Entities/TelegramPassport/PassportElementError/PassportElementErrorDataField.php delete mode 100644 src/Entities/TelegramPassport/PassportElementError/PassportElementErrorFile.php delete mode 100644 src/Entities/TelegramPassport/PassportElementError/PassportElementErrorFiles.php delete mode 100644 src/Entities/TelegramPassport/PassportElementError/PassportElementErrorFrontSide.php delete mode 100644 src/Entities/TelegramPassport/PassportElementError/PassportElementErrorReverseSide.php delete mode 100644 src/Entities/TelegramPassport/PassportElementError/PassportElementErrorSelfie.php delete mode 100644 src/Entities/TelegramPassport/PassportElementError/PassportElementErrorTranslationFile.php delete mode 100644 src/Entities/TelegramPassport/PassportElementError/PassportElementErrorTranslationFiles.php delete mode 100644 src/Entities/TelegramPassport/PassportElementError/PassportElementErrorUnspecified.php delete mode 100644 src/Entities/TelegramPassport/PassportFile.php delete mode 100644 src/Entities/TextQuote.php delete mode 100644 src/Entities/Topics/ForumTopicClosed.php delete mode 100644 src/Entities/Topics/ForumTopicCreated.php delete mode 100644 src/Entities/Topics/ForumTopicEdited.php delete mode 100644 src/Entities/Topics/ForumTopicReopened.php delete mode 100644 src/Entities/Topics/GeneralForumTopicHidden.php delete mode 100644 src/Entities/Topics/GeneralForumTopicUnhidden.php delete mode 100644 src/Entities/Update.php delete mode 100644 src/Entities/User.php delete mode 100644 src/Entities/UserChatBoosts.php delete mode 100644 src/Entities/UserProfilePhotos.php delete mode 100644 src/Entities/UsersShared.php delete mode 100644 src/Entities/Venue.php delete mode 100644 src/Entities/Video.php delete mode 100644 src/Entities/VideoChatEnded.php delete mode 100644 src/Entities/VideoChatParticipantsInvited.php delete mode 100644 src/Entities/VideoChatScheduled.php delete mode 100644 src/Entities/VideoChatStarted.php delete mode 100644 src/Entities/VideoNote.php delete mode 100644 src/Entities/Voice.php delete mode 100644 src/Entities/WebAppData.php delete mode 100644 src/Entities/WebAppInfo.php delete mode 100644 src/Entities/WebhookInfo.php delete mode 100644 src/Entities/WriteAccessAllowed.php delete mode 100644 src/Exception/InvalidBotTokenException.php delete mode 100644 src/Exception/TelegramException.php delete mode 100644 src/Exception/TelegramLogException.php create mode 100644 src/Exceptions/NotYetImplementedException.php create mode 100644 src/Exceptions/TelegramException.php create mode 100644 src/HttpClient.php delete mode 100644 src/Request.php delete mode 100644 src/TelegramLog.php create mode 100644 src/Types/Poll.php create mode 100644 src/Types/Type.php delete mode 100644 structure.sql delete mode 100644 tests/Unit/Commands/CommandTest.php delete mode 100644 tests/Unit/Commands/CommandTestCase.php delete mode 100644 tests/Unit/Commands/CustomTestCommands/DummyAdminCommand.php delete mode 100644 tests/Unit/Commands/CustomTestCommands/DummySystemCommand.php delete mode 100644 tests/Unit/Commands/CustomTestCommands/DummyUserCommand.php delete mode 100644 tests/Unit/Commands/CustomTestCommands/HiddenCommand.php delete mode 100644 tests/Unit/Commands/CustomTestCommands/VisibleCommand.php delete mode 100644 tests/Unit/ConversationTest.php delete mode 100644 tests/Unit/Entities/AudioTest.php delete mode 100644 tests/Unit/Entities/ChatTest.php delete mode 100644 tests/Unit/Entities/EntityTest.php delete mode 100644 tests/Unit/Entities/FileTest.php delete mode 100644 tests/Unit/Entities/InlineKeyboardButtonTest.php delete mode 100644 tests/Unit/Entities/InlineKeyboardTest.php delete mode 100644 tests/Unit/Entities/KeyboardButtonTest.php delete mode 100644 tests/Unit/Entities/KeyboardTest.php delete mode 100644 tests/Unit/Entities/LocationTest.php delete mode 100644 tests/Unit/Entities/MessageTest.php delete mode 100644 tests/Unit/Entities/ReplyToMessageTest.php delete mode 100644 tests/Unit/Entities/ServerResponseTest.php delete mode 100644 tests/Unit/Entities/UpdateTest.php delete mode 100644 tests/Unit/Entities/UserTest.php delete mode 100644 tests/Unit/Entities/WebhookInfoTest.php delete mode 100644 tests/Unit/TelegramLogTest.php delete mode 100644 tests/Unit/TelegramTest.php delete mode 100644 tests/Unit/TestCase.php delete mode 100644 tests/Unit/TestHelpers.php delete mode 100644 utils/db-schema-update/0.44.1-0.45.0.sql delete mode 100644 utils/db-schema-update/0.47.1-0.48.0.sql delete mode 100644 utils/db-schema-update/0.50.0-0.51.0.sql delete mode 100644 utils/db-schema-update/0.52.0-0.53.0.sql delete mode 100644 utils/db-schema-update/0.53.0-0.54.0.sql delete mode 100644 utils/db-schema-update/0.54.1-0.55.0.sql delete mode 100644 utils/db-schema-update/0.56.0-0.57.0.sql delete mode 100644 utils/db-schema-update/0.57.0-0.58.0.sql delete mode 100644 utils/db-schema-update/0.60.0-0.61.0.sql delete mode 100644 utils/db-schema-update/0.61.1-0.62.0.sql delete mode 100644 utils/db-schema-update/0.62.0-0.63.0.sql delete mode 100644 utils/db-schema-update/0.64.0-0.70.0.sql delete mode 100644 utils/db-schema-update/0.71.0-0.72.0.sql delete mode 100644 utils/db-schema-update/0.72.0-0.73.0.sql delete mode 100644 utils/db-schema-update/0.74.0-0.75.0.sql delete mode 100644 utils/db-schema-update/0.76.1-0.77.0.sql delete mode 100644 utils/db-schema-update/0.77.1-0.78.0.sql delete mode 100644 utils/db-schema-update/0.79.0-0.80.0.sql delete mode 100644 utils/db-schema-update/0.80.0-0.81.0.sql delete mode 100644 utils/db-schema-update/0.81.0-0.82.0.sql delete mode 100644 utils/db-schema-update/0.82.0-unreleased.sql delete mode 100644 utils/importFromLog.php diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index d84f8aa61..000000000 --- a/.editorconfig +++ /dev/null @@ -1,12 +0,0 @@ -root = true - -[*] -end_of_line = lf -insert_final_newline = true -charset = utf-8 -indent_style = space -indent_size = 4 -trim_trailing_whitespace = true - -[*.{yml, yaml}] -indent_size = 2 diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 74350b724..000000000 --- a/.gitattributes +++ /dev/null @@ -1,5 +0,0 @@ -/.* export-ignore -/composer.lock export-ignore -/phpcs.xml.dist export-ignore -/phpunit.xml.dist export-ignore -/tests export-ignore diff --git a/.github/ISSUE_TEMPLATE/BC_Break.md b/.github/ISSUE_TEMPLATE/BC_Break.md deleted file mode 100644 index 01457d033..000000000 --- a/.github/ISSUE_TEMPLATE/BC_Break.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -name: 💥 BC Break -about: Have you encountered an issue during an upgrade? 💣 -labels: bc break ---- - - - -### 💥 BC Break Report - -<-- -❗ NEVER put your Telegram API key or any other private details here. (like passwords, user IDs, etc.) -Substitute them like or etc. ---> - -#### Required Information - - - - -| ? | ! -| --- | --- -| Operating system | Name and version -| PHP Telegram Bot version | x.y.z -| PHP version | x.y.z -| MySQL version | x.y.z / none -| Update Method | Webhook / getUpdates -| Self-signed certificate | yes / no -| RAW update (if available) | `{...}` - -#### Summary - - - -#### Previous behaviour - - - -#### Current behaviour - - - -#### How to reproduce - - - -#### Expected behaviour - - diff --git a/.github/ISSUE_TEMPLATE/Bug.md b/.github/ISSUE_TEMPLATE/Bug.md deleted file mode 100644 index d52a98193..000000000 --- a/.github/ISSUE_TEMPLATE/Bug.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -name: 🐞 Bug Report -about: Something is broken? 🔨 -labels: bug ---- - -### 🐞 Bug Report - -<-- -❗ NEVER put your Telegram API key or any other private details here. (like passwords, user IDs, etc.) -Substitute them like or etc. ---> - -#### Required Information - - - - -| ? | ! -| --- | --- -| Operating system | Name and version -| PHP Telegram Bot version | x.y.z -| PHP version | x.y.z -| MySQL version | x.y.z / none -| Update Method | Webhook / getUpdates -| Self-signed certificate | yes / no -| RAW update (if available) | `{...}` - -#### Summary - - - -#### Current behaviour - - - -#### How to reproduce - - - -#### Expected behaviour - - diff --git a/.github/ISSUE_TEMPLATE/Feature_Request.md b/.github/ISSUE_TEMPLATE/Feature_Request.md deleted file mode 100644 index ac4c98293..000000000 --- a/.github/ISSUE_TEMPLATE/Feature_Request.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: 🎉 Feature Request -about: You have a neat idea that should be implemented? 🎩 -labels: feature request ---- - -### 🎉 Feature Request - -#### Summary - - diff --git a/.github/ISSUE_TEMPLATE/Support_Question.md b/.github/ISSUE_TEMPLATE/Support_Question.md deleted file mode 100644 index fe6f2b562..000000000 --- a/.github/ISSUE_TEMPLATE/Support_Question.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -name: ❓ Support Question -about: Have a problem that you can't figure out? 🤔 -labels: question ---- - -### ❓ Support Question - - - -<-- -❗ NEVER put your Telegram API key or any other private details here. (like passwords, user IDs, etc.) -Substitute them like or etc. ---> - -#### Required Information - - - - -| ? | ! -| --- | --- -| Operating system | Name and version -| PHP Telegram Bot version | x.y.z -| PHP version | x.y.z -| MySQL version | x.y.z / none -| Update Method | Webhook / getUpdates -| Self-signed certificate | yes / no -| RAW update (if available) | `{...}` - -#### Summary - - diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index d75c05408..000000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,19 +0,0 @@ - - - - -| ? | ! -|--- | --- -| Type | bug / feature / improvement -| BC Break | yes / no -| Fixed issues | - -#### Summary - - diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml deleted file mode 100644 index 87764e110..000000000 --- a/.github/workflows/tests.yaml +++ /dev/null @@ -1,84 +0,0 @@ -name: Tests - -on: - push: - branches: [ master, develop ] - pull_request: - branches: [ master, develop ] - -permissions: - contents: read - -jobs: - tests: - name: PHP ${{ matrix.php }} Test - runs-on: ubuntu-latest - - strategy: - matrix: - php: ['8.1', '8.2', '8.3'] - - services: - mariadb: - image: mariadb:10.3 - ports: - - 3306:3306 - env: - MYSQL_ROOT_PASSWORD: root - options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 - - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Verify MariaDB connection - run: | - while ! mysqladmin ping -h127.0.0.1 -P3306 --silent; do - sleep 1 - done - - - name: Test migrations - run: | - git fetch origin 0.44.1 && git checkout FETCH_HEAD -- structure.sql - mysql -h127.0.0.1 -P3306 -uroot -proot -e "create database telegrambot_migrations; use telegrambot_migrations; source structure.sql;" - git checkout HEAD -- structure.sql - mysql -h127.0.0.1 -P3306 -uroot -proot -e "create database telegrambot; use telegrambot; source structure.sql;" - for SCHEMA_UPDATE_FILE in $(ls utils/db-schema-update); do mysql -h127.0.0.1 -P3306 -uroot -proot -e "use telegrambot_migrations; source utils/db-schema-update/${SCHEMA_UPDATE_FILE};"; done; - - - name: Install PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - extensions: pdo_mysql - - - name: Cache Composer packages - id: composer-cache - uses: actions/cache@v3 - with: - path: vendor - key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} - restore-keys: | - ${{ runner.os }}-php- - - - name: Validate composer.json and composer.lock - run: composer validate --strict - - - name: Install dependencies - run: composer install --prefer-dist --no-progress - - - name: Check PHP code - run: composer check-code - - - name: Run test suite - run: composer test - -# - name: Run test suite -# if: ${{ matrix.php != '8.1'}} -# run: composer test-cov - -# - name: Run test suite (with coverage) -# if: ${{ matrix.php == '8.1'}} -# run: | -# wget https://scrutinizer-ci.com/ocular.phar -# composer test-cov -# composer test-cov-upload diff --git a/.gitignore b/.gitignore index 426dc5a82..95d0d25fa 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,4 @@ phpdoc-* # OSX ._* .Spotlight-V100 -.Trashes +.Trashes \ No newline at end of file diff --git a/.scrutinizer.yml b/.scrutinizer.yml deleted file mode 100644 index d788c4678..000000000 --- a/.scrutinizer.yml +++ /dev/null @@ -1,17 +0,0 @@ -build: - environment: - php: 8.1.0 - nodes: - analysis: - tests: - override: - - php-scrutinizer-run - -filter: - paths: - - src/ - - utils/ - -tools: - external_code_coverage: - timeout: 300 diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index c7bc92638..000000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,740 +0,0 @@ -# Changelog -The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). - -Exclamation symbols (:exclamation:) note something of importance e.g. breaking changes. Click them to learn more. - -## [Unreleased] -### Notes -- [:ledger: View file changes][Unreleased] ∙ [:page_with_curl: DB migration script][unreleased-sql-migration] -- [:exclamation:][unreleased-bc-minimum-php-81] PHP 8.1+ required! -### Added -- Bot API 7.0 (@noplanman, @TiiFuchs) (#1459) -### Changed -- [:exclamation:][unreleased-bc-user-to-users] Various fields have been pluralised from "user" to "users". -### Deprecated -### Removed -### Fixed -### Security - -## [0.82.0] - 2023-10-26 -### Notes -- [:ledger: View file changes][0.82.0] ∙ [:page_with_curl: DB migration script][0.82.0-sql-migration] -- Really the last version that supports PHP 7! Only PHP 8 after this one. -### Added -- Bot API 6.8, 6.9 (@noplanman) (#1418, #1427) -- Test against PHP 8.3 (@sergiy-petrov) (#1421) -### Fixed -- Missing `thumbnail_url` renaming. -- Fix broken exception handling (@mlocati) (#1425) - -## [0.81.0] - 2023-05-27 -### Notes -- [:ledger: View file changes][0.81.0] ∙ [:page_with_curl: DB migration script][0.81.0-sql-migration] -- Last version that supports PHP 7. -### Added -- Support dynamic Entity properties for newer PHP versions (@alesinicio, @TiiFuchs) (#1390) -- Bot API 6.4, 6.5, 6.6, 6.7 (@noplanman) (#1389) -### Removed -- Keyboard validations (@noplanman) (#1395) -### Fixed -- Fixed a bug where new incoming updates are not correctly passed to the Command object after the first time when getUpdates is used. (@uspilot) (#1384) - -## [0.80.0] - 2022-11-09 -### Notes -- [:ledger: View file changes][0.80.0] ∙ [:page_with_curl: DB migration script][0.80.0-sql-migration] -### Added -- Bot API 6.3 (@TiiFuchs) (#1371) -### Changed -- [:exclamation:][0.80.0-bc-commands-with-underscores] Commands can now contain underscores (`FooBarCommand` is handled by `/foo_bar`). (@mlocati) (#1365) - -## [0.79.0] - 2022-09-04 -### Notes -- [:ledger: View file changes][0.79.0] -### Added -- Bot API 6.2 (@OxMohsen) (#1350) -### Removed -- `VoiceChatX` entities, use `VideoChatX` entities instead. -- `all_members_are_administrators` property from `Message` entity. Database column still exists, but may be removed in the future. -- `Request::getChatMembersCount()`, use `Request::getChatMemberCount()` instead. -- `Request::kickChatMember()`, use `Request::banChatMember()` instead. - -## [0.78.0] - 2022-07-03 -### Notes -- [:ledger: View file changes][0.78.0] ∙ [:page_with_curl: DB migration script][0.78.0-sql-migration] -- 64 bit PHP install required to handle files larger than 2GB! -### Added -- Bot API 6.1 (@jyxjjj, @OxMohsen, @noplanman) (#1333, #1338, #1339) -- New Update field helpers for Command class. - -## [0.77.1] - 2022-04-24 -### Notes -- [:ledger: View file changes][0.77.1] -### Fixed -- PHP8+ `mixed` type hint removed - -## [0.77.0] - 2022-04-24 -### Notes -- [:ledger: View file changes][0.77.0] ∙ [:page_with_curl: DB migration script][0.77.0-sql-migration] -### Added -- Bot API 6.0 (@TiiFuchs) (#1318) -### Deprecated -- Telegram changed `voice_chat_X` to `video_chat_X`. `VoiceChatX` event classes are deprecated, use new `VideoChatX` event classes instead. -### Fixed -- Return correct data in `Entity::jsonSerialize` (@TiiFuchs) - -## [0.76.1] - 2022-03-30 -### Notes -- [:ledger: View file changes][0.76.1] -### Fixed -- Fix Entity serialising for Keyboards (@TiiFuchs) (#1304) - -## [0.76.0] - 2022-03-20 -### Notes -- [:ledger: View file changes][0.76.0] -### Added -- Bot API 5.6 (@TiiFuchs) (#1275) -- Bot API 5.7 (@TiiFuchs) (#1284) -- PSR3 (psr/log) version 2 and 3 compatible (@noplanman) (#1287) -- Entity implements and uses JsonSerializable now (@TiiFuchs) -- Test with PHP 8.1 on Travis CI (@osavchenko) (#1291) -### Changed -- Bugfix: Fixed condition in \Longman\TelegramBot\Entities\Chat::isGroupChat() that previously also counted super groups and channels. (@TiiFuchs) - -## [0.75.0] - 2021-12-29 -### Notes -- [:ledger: View file changes][0.75.0] ∙ [:page_with_curl: DB migration script][0.75.0-sql-migration] -### Added -- Ability to directly set commands paths. (@wright-tw, @noplanman) (#1252) -- Bot API 5.4. (@TiiFuchs, @noplanman) (#1266) -- Bot API 5.5. (@TiiFuchs, @noplanman) (#1267) -- The field `message_auto_delete_time` was added to the Chat Entity (@TiiFuchs) (#1265) -### Removed -- [:exclamation:][0.75.0-bc-removed-chatactions] Removed deprecated `ChatAction::` `RECORD_AUDIO` and `UPLOAD_AUDIO`. Use `RECORD_VOICE` and `UPLOAD_VOICE` instead. (@TiiFuchs) (#1267) -### Fixed -- PHP 8.1 deprecations. (@maxgorovenko) (#1260) - -## [0.74.0] - 2021-06-26 -### Notes -- [:ledger: View file changes][0.74.0] -### Added -- [:exclamation:][0.74.0-bc-chatmember-subentities] Bot API 5.3 (Personalized Commands, Keyboard Placeholders). (@TiiFuchs, @noplanman) (#1229, #1231) - -## [0.73.1] - 2021-06-20 -### Notes -- [:ledger: View file changes][0.73.1] -### Fixed -- Allow new optional parameters when setting and deleting webhook. (@TiiFuchs) (#1226) - -## [0.73.0] - 2021-06-14 -### Notes -- [:ledger: View file changes][0.73.0] ∙ [:page_with_curl: DB migration script][0.73.0-sql-migration] -### Added -- Bot API 5.2 (Payments 2.0). (#1216) -- Possibility to connect to MySQL DB with unix socket. (@Tynik) (#1220) -### Changed -- `Telegram::runCommands` returns array of `ServerResponse` objects of executed commands. (#1223) -### Fixed -- Regex for namespace extraction from custom command classes. -- Nested and user-triggered `Telegram::runCommands`. (#1223) - -## [0.72.0] - 2021-04-16 -### Notes -- [:ledger: View file changes][0.72.0] ∙ [:page_with_curl: DB migration script][0.72.0-sql-migration] -### Added -- Bot API 5.1 (ChatMember Update types, Improved Invite Links, Voice Chat). (@massadm, @noplanman) (#1199) -- Method to allow adding command classes directly. (@alligator77, @noplanman) (#1207, #1209) -### Deprecated -- `Telegram::handleGetUpdates` method should be passed a `$data` array for parameters. (#1202) -### Fixed -- `message.edit_date` is now of type `timestamp`. (#1191) -- Allow all update types by default when using `getUpdates` method. (#1202) - -## [0.71.0] - 2021-03-05 -### Notes -- [:ledger: View file changes][0.71.0] -### Added -- Define a custom Bot API server and file download URI. (#1168) -### Changed -- Improved error messages for empty input. (#1164) -- Log update when processing it, not when fetching input. (#1164) -### Fixed -- `getUpdates` method wrongly sends only 1 Update when a limit of 0 is passed. (#1169) -- `Telegram::runCommands()` now passes the correct message text to the commands. (#1181) -- Request limiter accepts chat ID as integer and string. (#1182) -- Calling Keyboard constructor without any parameters. (@hutattedonmyarm) (#1184) - -## [0.70.1] - 2020-12-25 -### Notes -- [:ledger: View file changes][0.70.1] -### Added -- Extra parameter for `Request::sendMessage()` to pass options and return all response objects for split messages. (#1163) -### Fixed -- Ensure download and upload path variables are defined. (#1162) - -## [0.70.0] - 2020-12-21 -### Notes -- [:ledger: View file changes][0.70.0] ∙ [:page_with_curl: DB migration script][0.70.0-sql-migration] -- [:exclamation:][0.70.0-bc-minimum-php-73] PHP 7.3+ required, so make sure your code is up to date with correct types! -### Added -- Bot API 5.0 (Big update!). (#1147) -### Changed -- Upgrade code to PHP 7.3. (#1136, #1158) -- Speed up `/clean` query. (@dva-re) (#1139) -- Various code prettifications. (@akalongman) (#1140, #1141, #1142, #1143) -### Security -- Minimum PHP 7.3, allow PHP 8.0. (#1136, #1158) - -## [0.64.0] - 2020-10-04 -### Notes -- [:ledger: View file changes][0.64.0] -### Added -- Support for Guzzle 7. (@KristobalJunta) (#1133) -### Fixed -- Correct SQL migration script for older versions of MySQL. (#1135) - -## [0.63.1] - 2020-06-24 -### Notes -- [:ledger: View file changes][0.63.1] -- This fixed version is necessary for Windows users. -### Fixed -- Regex in `getFileNamespace()` that introduced a breaking bug in #1114. (@jacklul) (#1115) -- Fixed `runCommands()` not working due to custom namespace feature. (@jacklul) (#1115, #1118) - -## [0.63.0] - 2020-06-17 -### Notes -- [:ledger: View file changes][0.63.0] ∙ [:page_with_curl: DB migration script][0.63.0-sql-migration] -### Added -- New method `setUpdateFilter($callback)` used to filter `processUpdate(Update $update)` calls. If `$callback` returns `false` the update isn't processed and an empty falsey `ServerResponse` is returned. (@VRciF) (#1045) -- Replaced 'generic' and 'genericmessage' strings with Telegram::GENERIC_COMMAND and Telegram::GENERIC_MESSAGE_COMMAND constants. (@1int) (#1074) -- Bot API 4.8 (Extra Poll and Dice features). (#1082) -- Allow custom MySQL port to be defined for tests. (#1090) -- New static method `Entity::escapeMarkdownV2` for MarkdownV2. (#1094) -- Remove bot token from debug http logs, this can be disabled by setting `TelegramLog::$remove_bot_token` parameter to `false`. (@jacklul) (#1095) -- `TelegramLog::$always_log_request_and_response` parameter to force output of the request and response data to the debug log, also for successful requests. (#1089) -- Bot API 4.9 (New `via_bot` field). (#1112) -### Changed -- [:exclamation:][0.63.0-bc-static-method-entityescapemarkdown] Made `Entity::escapeMarkdown` static, to not require an `Entity` object. (#1094) -- Allow custom namespacing for commands. (@Jonybang) (#689) -### Fixed -- Primary key for `poll_answer` also requires the `user_id`. (#1087) -- Small SQL foreign key fixes. (#1105) - -## [0.62.0] - 2020-04-08 -### Notes -- [:ledger: View file changes][0.62.0] ∙ [:page_with_curl: DB migration script][0.62.0-sql-migration] -### Added -- Bot API 4.5 (Unique file IDs, MarkdownV2). (#1046) -- Chain the exception thrown when getting commands from path. (#1030) -- Support `language_code` in `DB::selectChats()` for filtering the chats selection. (#1058) -- Bot API 4.6 (Polls 2.0). (#1066) -- Bot API 4.7 (Dice, Sticker Sets, Bot Commands). (#1067) -### Changed -- Save notes an unescaped JSON, to allow easy DB reading and editing of values. (#1005) -- `Request::setClient()` now accepts more flexible `ClientInterface`. (#1068) -### Removed -- Unnecessary `instanceof` checks for entities. (#1068) -- Unused `Request::$input` variable. (#1068) -### Fixed -- Execution of `/start` command without any custom implementation. -- Return `animation` type for GIF Message (which returns both `animation` and `document`). (#1044) -- Change lowercase function to `mb_strtolower` from `strtolower` because of `latin-extented` characters. (#1051) -- Extend `Request::mediaInputHelper()` to include `thumb` parameter. (#1059) -- Various docblock annotations. (#1068) - -## [0.61.1] - 2019-11-23 -### Notes -- [:ledger: View file changes][0.61.1] -### Added -- Tests for schema update scripts, to ensure that updates work as expected. (#1025) -### Fixed -- Parameter `inline_query_id` in `InlineQuery::answerInlineQuery`. (#1021) -- Corrected DB schema update 0.60.0-0.61.0. (#1025) - -## [0.61.0] - 2019-11-02 -### Notes -- [:ledger: View file changes][0.61.0] ∙ [:page_with_curl: DB migration script][0.61.0-sql-migration] -- :exclamation: Built-in logging (Monolog) has been removed, a custom PSR-3 logger must be used now! (see #964 for more info) -### Added -- Code snippet in `GenericmessageCommand` to keep obsolete service message system commands working. (#999) -- Static boolean property `SystemCommand::$execute_deprecated` (must be assigned before handling the request) to try and execute any deprecated system command. (#999) -- Improved MySQL DB index for `message` table, making the cleanup much faster on bigger databases. (Thanks to @damianperez) (#1015) -- `/cleanup` command now supports dry run which simply outputs all queries that would be run. (#1015) -### Changed -- Small readme and code fixes / simplifications. (#1001) -- Upgrade PHPUnit to 8.x and PHPCS to 3.5. For tests now minimum PHP version is 7.2. (#1008) -- Updated updates log importer (requires PHP7+). (#1009) -### Removed -- Service message system commands, which are now handled by `GenericmessageCommand`. (#999) -- [:exclamation:][0.61.0-bc-remove-monolog-from-core] Monolog has been removed as built-in logging engine. (#1009) -- Assets have been moved to a dedicated repository. (#1012) -### Fixed -- Boolean value for Polls gets saved correctly in MySQL DB. (#996) -- Correctly use `Request::answerInlineQuery` in `InlineQuery::answer`. (#1001) -- PSR-12 incompatibilities in the codebase. (#1008) -- Improved and corrected `/cleanup` command. (#1015) - -## [0.60.0] - 2019-08-16 -### Notes -- [:ledger: View file changes][0.60.0] -### Added -- Bot API 4.4 (Animated stickers, `ChatPermissions`). (#990) -### Changed -- Allow passing native entity objects to requests. (#991) -### Fixed -- Non-existing commands now correctly execute `GenericCommand` again. (#993) - -## [0.59.1] - 2019-07-18 -### Notes -- [:ledger: View file changes][0.59.1] -- :exclamation: Deprecated logging method will be removed in the next version, update to PSR-3 now! (see #964 for more info) -### Added -- Issue labels set automatically via templates. -### Changed -- Logging only updates or only debug/errors is now possible. (#983) -### Fixed -- Don't output deprecation notices if no logging is enabled. (#983) -- Respect custom HTTP Client initialisation, no matter when it is set. (#986) - -## [0.59.0] - 2019-07-07 -### Notes -- [:ledger: View file changes][0.59.0] -### Changed -- Touched up readme including header and badges. (#981) -- Updated changelog to be more useful for external integrations like [Tidelift] and GitHub releases. (#981) -### Removed -- Abstract methods from `InputMedia` interface, as method annotations didn't work for propagation. (#978) - -## [0.58.0] - 2019-06-26 -### Notes -- [:ledger: View file changes][0.58.0] ∙ [:page_with_curl: DB migration script][0.58.0-sql-migration] -- Logging now uses [PSR-3] `LoggerInterface`. Learn more about how to use it here: #964 -### Added -- New funding and support details. (#971) -- Custom issue templates. (#972) -- Bot API 4.3 (Seamless Telegram Login, `LoginUrl`) (#957) -- `reply_markup` field to `Message` entity. (#957) -### Changed -- Use PSR-12 for code style. (#966) -- Some general housekeeping. (#972) -- [:exclamation:][0.58.0-bc-return-value-of-empty-entity-properties] Return an empty array for Entity properties with no items, instead of `null`. (#969) -- `TelegramLog` now adheres to [PSR-3] `LoggerInterface` and allows custom logger implementations. (#964) -### Deprecated -- Old logging that uses Monolog still works but will be removed in the near future. Use `TelegramLog::initialize($logger, $update_logger);` from now on. (#964) -- [:exclamation:][0.58.0-bc-startcommand-is-now-a-usercommand] `StartCommand` is now a `UserCommand` (not `SystemCommand` any more). (#970) -### Removed -- Botan.io integration completely removed. (#968) -### Fixed -- `forward_date` is now correctly saved to the DB. (#967) -- Broken `StickerSet::getStickers()` method. (#969) -- Smaller code and docblock fixes. (#973) -### Security -- Security disclosure managed by [Tidelift]. (#971) -- Don't allow a user to call system commands directly. (#970) - -## [0.57.0] - 2019-06-01 -### Notes -- [:ledger: View file changes][0.57.0] ∙ [:page_with_curl: DB migration script][0.57.0-sql-migration] -- :grey_exclamation: This is a big update and involves a bunch of MySQL database updates, so please *review the changelog carefully*. - -### Added -- New logo! :tada: (#954) -- Bot API 4.2 (Polls). (#948) -- `getIsMember()` method to `ChatMember` entity. (#948) -- `getForwardSenderName()` method to `Message` entity. (#948) -- `forward_sender_name` (and forgotten `forward_signature`) DB fields. (#948) -- Added missing API fields to Entities and DB. (#885) -- Created database tables for `shipping_query` and `pre_checkout_query`. (#885) -### Fixed -- Missing DB table name specifier in `/cleanup` command. (#947) - -## [0.56.0] - 2019-04-15 -### Notes -- [:ledger: View file changes][0.56.0] ∙ [:page_with_curl: DB migration script][0.56.0-sql-migration] -- :grey_exclamation: This is a big update, so please *review the changelog carefully*! -### Added -- Helper for sending `InputMedia` objects using `Request::sendMediaGroup()` and `Request::editMediaMessage()` methods. -- Allow passing absolute file path for InputFile fields, instead of `Request::encodeFile($path)`. (#934) -### Changed -- All Message field types dynamically search for an existing Command class that can handle them. (#940) -- Upgrade dependencies. (#945) -### Deprecated -- Botan.io service has been discontinued. (#925) -- Most built-in System Commands will be handled by GenericmessageCommand by default in a future release and will require a custom implementation. (#940) -### Fixed -- Constraint errors in `/cleanup` command. (#930) -- Return correct objects for requests. (#934) -- PHPCS: static before visibility declaration. (#945) - -## [0.55.1] - 2019-01-06 -### Notes -- [:ledger: View file changes][0.55.1] -### Added -- Add missing `Request::editMessageMedia()` and `CallbackQuery::getChatInstance()` methods. (#916) -### Fixed -- Return correct message type. (#913) - -## [0.55.0] - 2018-12-20 -### Notes -- [:ledger: View file changes][0.55.0] ∙ [:page_with_curl: DB migration script][0.55.0-sql-migration] -### Added -- Bot API 4.0 and 4.1 (Telegram Passport) (#870, #871) -- Test PHP 7.3 with Travis. (#903) -### Changed -- [:exclamation:][0.55.0-bc-move-animation-out-of-games-namespace] Move Animation entity out of Games namespace. -### Fixed -- Added missing `video_note` to `Message` types. - -## [0.54.1] - 2018-10-23 -### Notes -- [:ledger: View file changes][0.54.1] -### Fixed -- `sendToActiveChats` now works correctly for any valid Request action. (#898) - -## [0.54.0] - 2018-07-21 -### Notes -- [:ledger: View file changes][0.54.0] ∙ [:page_with_curl: DB migration script][0.54.0-sql-migration] -### Added -- `ChatAction` class to simplify chat action selection. (#859) -- Telegram Games platform! (#732) -- Ability to set custom MySQL port. (#860) -- New `InvalidBotTokenException` exception. (#855) -### Changed -- [:exclamation:][0.54.0-bc-rename-constants] Rename and ensure no redefinition of constants: `BASE_PATH` -> `TB_BASE_PATH`, `BASE_COMMANDS_PATH` -> `TB_BASE_COMMANDS_PATH`. (#813) -- Improve readability of readme code snippets. (#861) -### Fixed -- Response from `getStickerSet`. (#838) - -## [0.53.0] - 2018-04-01 -### Notes -- [:ledger: View file changes][0.53.0] ∙ [:page_with_curl: DB migration script][0.53.0-sql-migration] -### Added -- Implemented new changes for Bot API 3.6 (streamable InputMediaVideo, connected website). (#799) -- `Telegram::getLastUpdateId()` method, returns ID of the last update that was processed. (#767) -- `Telegram::useGetUpdatesWithoutDatabase()` method, enables `Telegram::handleGetUpdates()` to run without a database. (#767) -### Changed -- Updated Travis to use Trusty containers (for HHVM) and add PHP 7.2 to the tests. (#739) -- Add debug log entry instead of throwing an exception for duplicate updates. (#765) -- `Telegram::handleGetUpdates()` can now work without a database connection (not enabled by default). (#767) -- Improved `/sendtochannel` and `/sendtoall` commands, using new message helpers. (#810) -### Fixed -- PHPCS fixes for updated CodeSniffer dependency. (#739) -- Send messages correctly via `/sendtochannel`. (#803) - -## [0.52.0] - 2018-01-07 -### Notes -- [:ledger: View file changes][0.52.0] -### Fixed -- Entity relations and wrong types for payments. (#731) -- Allow empty string for `switch_inline_query` and `switch_inline_query_current_chat` (InlineKeyboardButton). (#736) -- Fix empty date entry for User and Chat entities, using the current timestamp instead. (#738) - -## [0.51.0] - 2017-12-05 -### Notes -- [:ledger: View file changes][0.51.0] ∙ [:page_with_curl: DB migration script][0.51.0-sql-migration] -### Added -- Implemented new changes for Bot API 3.5 (InputMedia, MediaGroup). (#718) - -## [0.50.0] - 2017-10-17 -### Notes -- [:ledger: View file changes][0.50.0] -### Added -- Finish implementing payments, adding all missing type checks and docblock methods. (#647) -- Implemented new changes for Bot API 3.4 (Live Locations). (#675) -### Changed -- [:exclamation:][0.50.0-bc-messagegetcommand-return-value] `Message::getCommand()` returns `null` if not a command, instead of `false`. (#654) -### Fixed -- SQL update script for version 0.44.1-0.45.0. -- Issues found by Scrutinizer (Type hints and return values). (#654) -- Check inline keyboard button parameter value correctly. (#672) - -## [0.49.0] - 2017-09-17 -### Notes -- [:ledger: View file changes][0.49.0] -### Added -- Donation section and links in readme. -- Missing payment methods in `Request` class. -- Some helper methods for replying to commands and answering queries. -### Changed -- Updated and optimised all DB classes, removing a lot of bulky code. -### Fixed -- Ensure named SQL statement parameters are unique. -- Channel selection when using `DB::selectChats()`. - -## [0.48.0] - 2017-08-26 -### Notes -- [:ledger: View file changes][0.48.0] ∙ [:page_with_curl: DB migration script][0.48.0-sql-migration] -### Added -- New entities, methods, update types and inline keyboard button for Payments (Bot API 3.0). -- Add new methods, fields and objects for working with stickers (Bot API 3.2). -- New fields for Chat, User and Message objects (Bot API 3.3). `is_bot` added to `user` DB table. -### Changed -- [:exclamation:][0.48.0-bc-correct-printerror] Corrected `ServerResponse->printError` method to print by default and return by setting `$return` parameter. -- Ensure command names are handled as lower case. -### Fixed -- Correctly save `reply_to_message` to DB. - -## [0.47.1] - 2017-08-06 -### Notes -- [:ledger: View file changes][0.47.1] -### Added -- Linked version numbers in changelog for easy verification of code changes. -### Fixed -- Private-only commands work with edited messages now too. - -## [0.47.0] - 2017-08-06 [YANKED] -### Notes -- [:ledger: View file changes][0.47.0] -### Changed -- Updated readme to latest state of 0.47.0. -### Fixed -- `Telegram::enableAdmin()` now handles duplicate additions properly. -- `Request::getMe()` failure doesn't break cron execution any more. -### Security -- [:exclamation:][0.47.0-bc-private-only-admin-commands] New command parameter `$private_only` to enforce usage in private chats only (set by default for Admin commands). - -## [0.46.0] - 2017-07-15 -### Notes -- [:ledger: View file changes][0.46.0] -### Added -- Callbacks can be added to be executed when callback queries are called. -- New Bot API 3.1 changes (#550). -- `/cleanup` command for admins, that cleans out old entries from the DB. -### Changed -- [:exclamation:][0.46.0-bc-request-class-refactor] Big refactor of the `Request` class, removing most custom method implementations. - -## [0.45.0] - 2017-06-25 -### Notes -- [:ledger: View file changes][0.45.0] ∙ [:page_with_curl: DB migration script][0.45.0-sql-migration] -### Added -- Documents can be sent by providing its contents via Psr7 stream (as opposed to passing a file path). -- Allow setting a custom Guzzle HTTP Client for requests (#511, #536). -- First implementations towards Bots API 3.0. -### Changed -- [:exclamation:][0.45.0-bc-chats-params-array] `Request::sendToActiveChats` and `DB::selectChats` now accept parameters as an options array and allow selecting of channels. -### Deprecated -- Deprecated `Message::getNewChatMember()` (Use `Message::getNewChatMembers()` instead to get an array of all newly added members). -### Removed -- [:exclamation:][0.45.0-bc-up-download-directory] Upload and download directories are not set any more by default and must be set manually. -- [:exclamation:][0.45.0-bc-remove-deprecated-methods] Completely removed `Telegram::getBotName()` and `Entity::getBotName()` (Use `::getBotUsername()` instead). -- [:exclamation:][0.45.0-bc-remove-deprecated-methods] Completely removed deprecated `Telegram::unsetWebhook()` (Use `Telegram::deleteWebhook()` instead). -### Fixed -- ID fields are now typed with `PARAM_STR` PDO data type, to allow huge numbers. -- Message type data type for PDO corrected. -- Indexed table columns now have a fitting length. -- Take `custom_input` into account when using getUpdates method (mainly for testing). -- Request limiter has been fixed to correctly support channels. - -## [0.44.1] - 2017-04-25 -### Notes -- [:ledger: View file changes][0.44.1] -### Fixed -- Erroneous exception when using webhook without a database connection. - -## [0.44.0] - 2017-04-25 -### Notes -- [:ledger: View file changes][0.44.0] -### Added -- Proper standalone `scrutinizer.yml` config. -- Human-readable `last_error_date_string` for debug command. -### Changed -- Bot username no longer required for object instantiation. -### Removed -- All examples have been moved to a [dedicated repository][example-bot]. -### Fixed -- [:exclamation:][0.44.0-bc-update-content-type] Format of Update content type using `$update->getUpdateContent()`. - -## [0.43.0] - 2017-04-17 -### Notes -- [:ledger: View file changes][0.43.0] -### Added -- Travis CI webhook for Support Bot. -- Interval for request limiter. -- `isRunCommands()` method to check if called via `runCommands()`. -- Ensure coding standards for `tests` folder with `phpcs`. -### Changed -- Move default commands to `examples` folder. -- All links point to new organisation repo. -- Add PHP 7.1 support and update dependencies. -### Fixed -- Prevent handling the same Telegram updates multiple times, throw exception instead. - -## [0.42.0] - 2017-04-09 -### Notes -- [:ledger: View file changes][0.42.0] -### Added -- Added `getBotId()` to directly access bot ID. -### Changed -- Rename `bot_name` to `bot_username` everywhere. -### Deprecated -- Deprecated `Telegram::getBotName()` (Use `Telegram::getBotUsername()` instead). -### Fixed -- Tests are more reliable now, using a properly formatted API key. - -## [0.41.0] - 2017-03-25 -### Notes -- [:ledger: View file changes][0.41.0] -### Added -- `$show_in_help` attribute for commands, to set if it should be displayed in the `/help` command. -- Link to new Telegram group: `https://telegram.me/PHP_Telegram_Bot_Support` -- Introduce change log. - -## [0.40.1] - 2017-03-07 -### Notes -- [:ledger: View file changes][0.40.1] -### Fixed -- Infinite message loop, caused by incorrect Entity variable. - -## [0.40.0] - 2017-02-20 -### Notes -- [:ledger: View file changes][0.40.0] -### Added -- Request limiter for incoming requests. -### Fixed -- Faulty formatting in logger. - -## [0.39.0] - 2017-01-20 -### Notes -- [:ledger: View file changes][0.39.0] -### Added -- Newest bot API changes. -- Allow direct access to PDO object (`DB::getPdo()`). -- Simple `/debug` command that displays various system information to help debugging. -- Crontab-friendly script. -### Changed -- Botan integration improvements. -- Make logger more flexible. -### Fixed -- Various bugs and recommendations by Scrutinizer. - -## [0.38.1] - 2016-12-25 -### Notes -- [:ledger: View file changes][0.38.1] -### Fixed -- Usage of self-signed certificates in conjunction with the new `allowed_updates` webhook parameter. - -## [0.38.0] - 2016-12-25 -### Notes -- [:ledger: View file changes][0.38.0] -### Added -- New `switch_inline_query_current_chat` option for inline keyboard. -- Support for `channel_post` and `edited_channel_post`. -- New alias `deleteWebhook` (for `unsetWebhook`). -### Changed -- Update WebhookInfo entity and `setWebhook` to allow passing of new arguments. - -## [0.37.1] - 2016-12-24 -### Notes -- [:ledger: View file changes][0.37.1] -### Fixed -- Keyboards that are built without using the KeyboardButton objects. -- Commands that are called via `/command@botname` by correctly passing them the bot name. - -## [0.37.0] - 2016-12-13 -### Notes -- [:ledger: View file changes][0.37.0] -### Changed -- Logging improvements to Botan integration. -### Deprecated -- Move `hideKeyboard` to `removeKeyboard`. - -[unreleased-sql-migration]: https://github.com/php-telegram-bot/core/tree/develop/utils/db-schema-update/0.82.0-unreleased.sql -[unreleased-bc-unreleased-bc-minimum-php-81]: https://github.com/php-telegram-bot/core/wiki/Breaking-backwards-compatibility#minimum-php-81 -[unreleased-bc-unreleased-bc-user-to-users]: https://github.com/php-telegram-bot/core/wiki/Breaking-backwards-compatibility#user-to-users -[0.82.0-sql-migration]: https://github.com/php-telegram-bot/core/tree/master/utils/db-schema-update/0.81.0-0.82.0.sql -[0.81.0-sql-migration]: https://github.com/php-telegram-bot/core/tree/master/utils/db-schema-update/0.80.0-0.81.0.sql -[0.80.0-sql-migration]: https://github.com/php-telegram-bot/core/tree/master/utils/db-schema-update/0.79.0-0.80.0.sql -[0.80.0-bc-commands-with-underscores]: https://github.com/php-telegram-bot/core/wiki/Breaking-backwards-compatibility#commands-with-underscores -[0.78.0-sql-migration]: https://github.com/php-telegram-bot/core/tree/master/utils/db-schema-update/0.77.1-0.78.0.sql -[0.77.0-sql-migration]: https://github.com/php-telegram-bot/core/tree/master/utils/db-schema-update/0.76.1-0.77.0.sql -[0.75.0-sql-migration]: https://github.com/php-telegram-bot/core/tree/master/utils/db-schema-update/0.74.0-0.75.0.sql -[0.75.0-bc-removed-chatactions]: https://github.com/php-telegram-bot/core/wiki/Breaking-backwards-compatibility#removed-deprecated-chatactions -[0.74.0-bc-chatmember-subentities]: https://github.com/php-telegram-bot/core/wiki/Breaking-backwards-compatibility#chatmember-subentities -[0.73.0-sql-migration]: https://github.com/php-telegram-bot/core/tree/master/utils/db-schema-update/0.72.0-0.73.0.sql -[0.72.0-sql-migration]: https://github.com/php-telegram-bot/core/tree/master/utils/db-schema-update/0.71.0-0.72.0.sql -[0.70.0-sql-migration]: https://github.com/php-telegram-bot/core/tree/master/utils/db-schema-update/0.64.0-0.70.0.sql -[0.70.0-bc-minimum-php-73]: https://github.com/php-telegram-bot/core/wiki/Breaking-backwards-compatibility#minimum-php-73 -[0.63.0-sql-migration]: https://github.com/php-telegram-bot/core/tree/master/utils/db-schema-update/0.62.0-0.63.0.sql -[0.63.0-bc-static-method-entityescapemarkdown]: https://github.com/php-telegram-bot/core/wiki/Breaking-backwards-compatibility#static-method-entityescapemarkdown -[0.62.0-sql-migration]: https://github.com/php-telegram-bot/core/tree/master/utils/db-schema-update/0.61.1-0.62.0.sql -[0.61.0-sql-migration]: https://github.com/php-telegram-bot/core/tree/master/utils/db-schema-update/0.60.0-0.61.0.sql -[0.61.0-bc-remove-monolog-from-core]: https://github.com/php-telegram-bot/core/wiki/Breaking-backwards-compatibility#remove-monolog-from-core -[0.58.0-sql-migration]: https://github.com/php-telegram-bot/core/tree/master/utils/db-schema-update/0.57.0-0.58.0.sql -[0.58.0-bc-return-value-of-empty-entity-properties]: https://github.com/php-telegram-bot/core/wiki/Breaking-backwards-compatibility#return-value-of-empty-entity-properties -[0.58.0-bc-startcommand-is-now-a-usercommand]: https://github.com/php-telegram-bot/core/wiki/Breaking-backwards-compatibility#startcommand-is-now-a-usercommand -[0.57.0-sql-migration]: https://github.com/php-telegram-bot/core/tree/master/utils/db-schema-update/0.56.0-0.57.0.sql -[0.55.0-sql-migration]: https://github.com/php-telegram-bot/core/tree/master/utils/db-schema-update/0.54.1-0.55.0.sql -[0.55.0-bc-move-animation-out-of-games-namespace]: https://github.com/php-telegram-bot/core/wiki/Breaking-backwards-compatibility#move-animation-out-of-games-namespace -[0.54.0-sql-migration]: https://github.com/php-telegram-bot/core/tree/master/utils/db-schema-update/0.53.0-0.54.0.sql -[0.54.0-bc-rename-constants]: https://github.com/php-telegram-bot/core/wiki/Breaking-backwards-compatibility#rename-constants -[0.53.0-sql-migration]: https://github.com/php-telegram-bot/core/tree/master/utils/db-schema-update/0.52.0-0.53.0.sql -[0.51.0-sql-migration]: https://github.com/php-telegram-bot/core/tree/master/utils/db-schema-update/0.50.0-0.51.0.sql -[0.50.0-bc-messagegetcommand-return-value]: https://github.com/php-telegram-bot/core/wiki/Breaking-backwards-compatibility#messagegetcommand-return-value -[0.48.0-sql-migration]: https://github.com/php-telegram-bot/core/tree/master/utils/db-schema-update/0.47.1-0.48.0.sql -[0.48.0-bc-correct-printerror]: https://github.com/php-telegram-bot/core/wiki/Breaking-backwards-compatibility#correct-printerror -[0.47.0-bc-private-only-admin-commands]: https://github.com/php-telegram-bot/core/wiki/Breaking-backwards-compatibility#private-only-admin-commands -[0.46.0-bc-request-class-refactor]: https://github.com/php-telegram-bot/core/wiki/Breaking-backwards-compatibility#request-class-refactor -[0.46.0-sql-migration]: https://github.com/php-telegram-bot/core/tree/0.45.0/utils/db-schema-update/0.44.1-0.45.0.sql -[0.45.0-bc-remove-deprecated-methods]: https://github.com/php-telegram-bot/core/wiki/Breaking-backwards-compatibility#remove-deprecated-methods -[0.45.0-bc-chats-params-array]: https://github.com/php-telegram-bot/core/wiki/Breaking-backwards-compatibility#chats-params-array -[0.45.0-bc-up-download-directory]: https://github.com/php-telegram-bot/core/wiki/Breaking-backwards-compatibility#up-download-directory -[0.44.0-bc-update-content-type]: https://github.com/php-telegram-bot/core/wiki/Breaking-backwards-compatibility#update-getupdatecontent -[example-bot]: https://github.com/php-telegram-bot/example-bot -[PSR-3]: https://www.php-fig.org/psr/psr-3 -[Tidelift]: https://tidelift.com/subscription/pkg/packagist-longman-telegram-bot?utm_source=packagist-longman-telegram-bot&utm_medium=referral&utm_campaign=changelog - -[Unreleased]: https://github.com/php-telegram-bot/core/compare/master...develop -[0.82.0]: https://github.com/php-telegram-bot/core/compare/0.81.0...0.82.0 -[0.81.0]: https://github.com/php-telegram-bot/core/compare/0.80.0...0.81.0 -[0.80.0]: https://github.com/php-telegram-bot/core/compare/0.79.0...0.80.0 -[0.79.0]: https://github.com/php-telegram-bot/core/compare/0.78.0...0.79.0 -[0.78.0]: https://github.com/php-telegram-bot/core/compare/0.77.1...0.78.0 -[0.77.1]: https://github.com/php-telegram-bot/core/compare/0.77.0...0.77.1 -[0.77.0]: https://github.com/php-telegram-bot/core/compare/0.76.1...0.77.0 -[0.76.1]: https://github.com/php-telegram-bot/core/compare/0.76.0...0.76.1 -[0.76.0]: https://github.com/php-telegram-bot/core/compare/0.75.0...0.76.0 -[0.75.0]: https://github.com/php-telegram-bot/core/compare/0.74.0...0.75.0 -[0.74.0]: https://github.com/php-telegram-bot/core/compare/0.73.1...0.74.0 -[0.73.1]: https://github.com/php-telegram-bot/core/compare/0.73.0...0.73.1 -[0.73.0]: https://github.com/php-telegram-bot/core/compare/0.72.0...0.73.0 -[0.72.0]: https://github.com/php-telegram-bot/core/compare/0.71.0...0.72.0 -[0.71.0]: https://github.com/php-telegram-bot/core/compare/0.70.1...0.71.0 -[0.70.1]: https://github.com/php-telegram-bot/core/compare/0.70.0...0.70.1 -[0.70.0]: https://github.com/php-telegram-bot/core/compare/0.64.0...0.70.0 -[0.64.0]: https://github.com/php-telegram-bot/core/compare/0.63.1...0.64.0 -[0.63.1]: https://github.com/php-telegram-bot/core/compare/0.63.0...0.63.1 -[0.63.0]: https://github.com/php-telegram-bot/core/compare/0.62.0...0.63.0 -[0.62.0]: https://github.com/php-telegram-bot/core/compare/0.61.1...0.62.0 -[0.61.1]: https://github.com/php-telegram-bot/core/compare/0.61.0...0.61.1 -[0.61.0]: https://github.com/php-telegram-bot/core/compare/0.60.0...0.61.0 -[0.60.0]: https://github.com/php-telegram-bot/core/compare/0.59.1...0.60.0 -[0.59.1]: https://github.com/php-telegram-bot/core/compare/0.59.0...0.59.1 -[0.59.0]: https://github.com/php-telegram-bot/core/compare/0.58.0...0.59.0 -[0.58.0]: https://github.com/php-telegram-bot/core/compare/0.57.0...0.58.0 -[0.57.0]: https://github.com/php-telegram-bot/core/compare/0.56.0...0.57.0 -[0.56.0]: https://github.com/php-telegram-bot/core/compare/0.55.1...0.56.0 -[0.55.1]: https://github.com/php-telegram-bot/core/compare/0.55.0...0.55.1 -[0.55.0]: https://github.com/php-telegram-bot/core/compare/0.54.1...0.55.0 -[0.54.1]: https://github.com/php-telegram-bot/core/compare/0.54.0...0.54.1 -[0.54.0]: https://github.com/php-telegram-bot/core/compare/0.53.0...0.54.0 -[0.53.0]: https://github.com/php-telegram-bot/core/compare/0.52.0...0.53.0 -[0.52.0]: https://github.com/php-telegram-bot/core/compare/0.51.0...0.52.0 -[0.51.0]: https://github.com/php-telegram-bot/core/compare/0.50.0...0.51.0 -[0.50.0]: https://github.com/php-telegram-bot/core/compare/0.49.0...0.50.0 -[0.49.0]: https://github.com/php-telegram-bot/core/compare/0.48.0...0.49.0 -[0.48.0]: https://github.com/php-telegram-bot/core/compare/0.47.1...0.48.0 -[0.47.1]: https://github.com/php-telegram-bot/core/compare/0.47.0...0.47.1 -[0.47.0]: https://github.com/php-telegram-bot/core/compare/0.46.0...0.47.0 -[0.46.0]: https://github.com/php-telegram-bot/core/compare/0.45.0...0.46.0 -[0.45.0]: https://github.com/php-telegram-bot/core/compare/0.44.1...0.45.0 -[0.44.1]: https://github.com/php-telegram-bot/core/compare/0.44.0...0.44.1 -[0.44.0]: https://github.com/php-telegram-bot/core/compare/0.43.0...0.44.0 -[0.43.0]: https://github.com/php-telegram-bot/core/compare/0.42.0...0.43.0 -[0.42.0]: https://github.com/php-telegram-bot/core/compare/0.41.0...0.42.0 -[0.41.0]: https://github.com/php-telegram-bot/core/compare/0.40.1...0.41.0 -[0.40.1]: https://github.com/php-telegram-bot/core/compare/0.40.0...0.40.1 -[0.40.0]: https://github.com/php-telegram-bot/core/compare/0.39.0...0.40.0 -[0.39.0]: https://github.com/php-telegram-bot/core/compare/0.38.1...0.39.0 -[0.38.1]: https://github.com/php-telegram-bot/core/compare/0.38.0...0.38.1 -[0.38.0]: https://github.com/php-telegram-bot/core/compare/0.37.1...0.38.0 -[0.37.1]: https://github.com/php-telegram-bot/core/compare/0.37.0...0.37.1 -[0.37.0]: https://github.com/php-telegram-bot/core/compare/0.36...0.37.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 5e18c7b73..000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,59 +0,0 @@ -# Contributing - -The easiest way to contribute is to work on a checkout of your own fork. -When working on the `core` repository, it makes sense to rename your fork to `php-telegram-bot`. - -Before you contribute code, please make sure it conforms to the PSR-12 coding standard and that the unit tests still pass. -You can run the following commands to check if everything is ready to submit: - -```bash -cd php-telegram-bot -composer install -composer check-code -``` - -Which should give you no output, indicating that there are no coding standard errors. -And then (remember to set up your test database!): - -```bash -composer test -``` - -Which should give you no failures or errors. You can ignore any skipped tests as these are for external tools. - -## Pushing - -Development is based on the git flow branching model (see http://nvie.com/posts/a-successful-git-branching-model/) -If you fix a bug please push in hotfix branch. -If you develop a new feature please create a new branch. - -## Version - -Version number: 0.#version.#hotfix - -## Further code convention adopted - -- Each method and class is documented with a docblock - -Example for a function or method: -```php -/** - * Get formatted date - * - * @param string $location - * - * @return string - */ -``` - -- Each file is provided with the following header: -```php -/** - * This file is part of the TelegramBot package. - * - * (c) Avtandil Kikabidze aka LONGMAN - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -``` diff --git a/CREDITS b/CREDITS deleted file mode 100644 index 2214a4c68..000000000 --- a/CREDITS +++ /dev/null @@ -1,27 +0,0 @@ - This is at least a partial credits-file of people that have - contributed to the current project. It is sorted by name and - formatted to allow easy grepping and beautification by - scripts. The fields are: name (N), email (E), web-address - (W) and description (D). - Thanks, - - Avtandil Kikabidze ----------- - -N: Avtandil Kikabidze aka LONGMAN -E: akalongman@gmail.com -W: http://longman.me -D: Project owner, Maintainer - -N: Marco Boretto -E: marco.bore@gmail.com -D: Maintainer and Collaborator - -N: Armando Lüscher -E: armando@noplanman.ch -W: http://noplanman.ch -D: Maintainer and Collaborator - -N: Jack'lul (alias) -E: jacklulcat@gmail.com -D: Maintainer and Collaborator diff --git a/LICENSE b/LICENSE.txt similarity index 87% rename from LICENSE rename to LICENSE.txt index 5fa6ddc85..c13f99117 100644 --- a/LICENSE +++ b/LICENSE.txt @@ -1,6 +1,6 @@ -The [MIT License](http://opensource.org/licenses/mit-license.php) +MIT License -Copyright (c) 2015 [Avtandil Kikabidze aka LONGMAN](https://github.com/akalongman) +Copyright (c) 2024 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -19,4 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - diff --git a/README.md b/README.md index 8be069764..d9a1188f7 100644 --- a/README.md +++ b/README.md @@ -1,725 +1,168 @@ -

- PHP Telegram Bot
-
- PHP Telegram Bot logo -
-

- -A Telegram Bot based on the official [Telegram Bot API] - -[![API Version](https://img.shields.io/badge/Bot%20API-6.9%20%28September%202023%29-32a2da.svg)](https://core.telegram.org/bots/api-changelog#september-22-2023) -[![Join the bot support group on Telegram](https://img.shields.io/badge/telegram-@PHP__Telegram__Bot__Support-64659d.svg)](https://telegram.me/PHP_Telegram_Bot_Support) -[![Donate](https://img.shields.io/badge/%F0%9F%92%99-Donate%20%2F%20Support%20Us-blue.svg)](#donate) - -[![Tests](https://github.com/php-telegram-bot/core/actions/workflows/tests.yaml/badge.svg)](https://github.com/php-telegram-bot/core/actions/workflows/tests.yaml) -[![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/php-telegram-bot/core/master.svg?style=flat)](https://scrutinizer-ci.com/g/php-telegram-bot/core/?b=master) -[![Code Quality](https://img.shields.io/scrutinizer/g/php-telegram-bot/core/master.svg?style=flat)](https://scrutinizer-ci.com/g/php-telegram-bot/core/?b=master) -[![Latest Stable Version](https://img.shields.io/packagist/v/longman/telegram-bot.svg)](https://packagist.org/packages/longman/telegram-bot) -[![Dependencies](https://tidelift.com/badges/github/php-telegram-bot/core?style=flat)][Tidelift] -[![Total Downloads](https://img.shields.io/packagist/dt/longman/telegram-bot.svg)](https://packagist.org/packages/longman/telegram-bot) -[![Downloads Month](https://img.shields.io/packagist/dm/longman/telegram-bot.svg)](https://packagist.org/packages/longman/telegram-bot) -[![Minimum PHP Version](http://img.shields.io/badge/php-%3E%3D8.1-8892BF.svg)](https://php.net/) -[![License](https://img.shields.io/packagist/l/longman/telegram-bot.svg)](https://github.com/php-telegram-bot/core/LICENSE) - -## Table of Contents -- [Introduction](#introduction) -- [Instructions](#instructions) - - [Create your first bot](#create-your-first-bot) - - [Require this package with Composer](#require-this-package-with-composer) - - [Choose how to retrieve Telegram updates](#choose-how-to-retrieve-telegram-updates) -- [Using a custom Bot API server](#using-a-custom-bot-api-server) -- [Webhook installation](#webhook-installation) - - [Self Signed Certificate](#self-signed-certificate) - - [Unset Webhook](#unset-webhook) -- [getUpdates installation](#getupdates-installation) - - [getUpdates without database](#getupdates-without-database) -- [Filter Update](#filter-update) -- [Support](#support) - - [Types](#types) - - [Inline Query](#inline-query) - - [Methods](#methods) - - [Send Message](#send-message) - - [Send Photo](#send-photo) - - [Send Chat Action](#send-chat-action) - - [getUserProfilePhoto](#getuserprofilephoto) - - [getFile and downloadFile](#getfile-and-downloadfile) - - [Send message to all active chats](#send-message-to-all-active-chats) -- [Utils](#utils) - - [MySQL storage (Recommended)](#mysql-storage-recommended) - - [External Database connection](#external-database-connection) - - [Channels Support](#channels-support) -- [Commands](#commands) - - [Predefined Commands](#predefined-commands) - - [Custom Commands](#custom-commands) - - [Commands Configuration](#commands-configuration) - - [Admin Commands](#admin-commands) - - [Set Admins](#set-admins) - - [Channel Administration](#channel-administration) -- [Upload and Download directory path](#upload-and-download-directory-path) -- [Logging](doc/01-utils.md) -- [Documentation](#documentation) -- [Assets](#assets) -- [Example bot](#example-bot) -- [Projects with this library](#projects-with-this-library) -- [Troubleshooting](#troubleshooting) -- [Contributing](#contributing) -- [Security](#security) -- [Donate](#donate) -- [For enterprise](#for-enterprise) -- [License](#license) -- [Credits](#credits) - -## Introduction - -This is a pure PHP Telegram Bot, fully extensible via plugins. - -Telegram announced official support for a [Bot API](https://telegram.org/blog/bot-revolution), allowing integrators of all sorts to bring automated interactions to the mobile platform. -This Bot aims to provide a platform where one can simply write a bot and have interactions in a matter of minutes. - -The Bot can: -- Retrieve updates with [webhook](#webhook-installation) and [getUpdates](#getupdates-installation) methods. -- Supports all types and methods according to Telegram Bot API 6.9 (September 2023). -- Supports supergroups. -- Handle commands in chat with other bots. -- Manage Channel from the bot admin interface. -- Full support for **inline bots**. -- Inline keyboard. -- Messages, InlineQuery and ChosenInlineQuery are stored in the Database. -- Conversation feature. - ---- - -This code is available on [GitHub](https://github.com/php-telegram-bot/core). Pull requests are welcome. - -## Instructions - -### Create your first bot - -1. Message [`@BotFather`](https://telegram.me/BotFather) with the following text: `/newbot` - - If you don't know how to message by username, click the search field on your Telegram app and type `@BotFather`, where you should be able to initiate a conversation. Be careful not to send it to the wrong contact, because some users have similar usernames to `BotFather`. - - ![BotFather initial conversation](https://user-images.githubusercontent.com/9423417/60736229-bc2aeb80-9f45-11e9-8d35-5b53145347bc.png) - -2. `@BotFather` replies with: - - ``` - Alright, a new bot. How are we going to call it? Please choose a name for your bot. - ``` - -3. Type whatever name you want for your bot. - -4. `@BotFather` replies with: - - ``` - Good. Now let's choose a username for your bot. It must end in `bot`. Like this, for example: TetrisBot or tetris_bot. - ``` - -5. Type whatever username you want for your bot, minimum 5 characters, and must end with `bot`. For example: `telesample_bot` - -6. `@BotFather` replies with: - - ``` - Done! Congratulations on your new bot. You will find it at - telegram.me/telesample_bot. You can now add a description, about - section and profile picture for your bot, see /help for a list of - commands. + + - Use this token to access the HTTP API: - 123456789:AAG90e14-0f8-40183D-18491dDE - For a description of the Bot API, see this page: - https://core.telegram.org/bots/api - ``` -7. Note down the 'token' mentioned above. + +[![Contributors][contributors-shield]][contributors-url] +[![Forks][forks-shield]][forks-url] +[![Stargazers][stars-shield]][stars-url] +[![Issues][issues-shield]][issues-url] +[![MIT License][license-shield]][license-url] -*Optionally set the bot privacy:* -1. Send `/setprivacy` to `@BotFather`. - ![BotFather later conversation](https://user-images.githubusercontent.com/9423417/60736340-26439080-9f46-11e9-970f-8f33bbe39c5f.png) + +
+
+ + Php Telegram Bot Logo + -2. `@BotFather` replies with: +

project_title

- ``` - Choose a bot to change group messages settings. - ``` +

+ A Telegram Bot based on the official Telegram Bot API +
+
+ View Demo + · + Report Bug + · + Request Feature +

+
-3. Type (or select) `@telesample_bot` (change to the username you set at step 5 -above, but start it with `@`) -4. `@BotFather` replies with: - ``` - 'Enable' - your bot will only receive messages that either start with the '/' symbol or mention the bot by username. - 'Disable' - your bot will receive all messages that people send to groups. - Current status is: ENABLED - ``` + +
+ Table of Contents +
    +
  1. + About The Project +
  2. +
  3. + Getting Started + +
  4. +
  5. Usage
  6. +
  7. Roadmap
  8. +
  9. Contributing
  10. +
  11. License
  12. +
  13. Contact
  14. +
  15. Acknowledgments
  16. +
+
-5. Type (or select) `Disable` to let your bot receive all messages sent to a group. -6. `@BotFather` replies with: - ``` - Success! The new status is: DISABLED. /help - ``` + +## About The Project -### Require this package with Composer +_TODO_ -Install this package through [Composer]. -Edit your project's `composer.json` file to require `longman/telegram-bot`. +

(back to top)

-Create *composer.json* file -```json -{ - "name": "yourproject/yourproject", - "type": "project", - "require": { - "php": "^8.1", - "longman/telegram-bot": "*" - } -} -``` -and run `composer update` -**or** -run this command in your command line: + +## Getting Started -```bash -composer require longman/telegram-bot -``` +_TODO_ -### Choose how to retrieve Telegram updates +### Prerequisites -The bot can handle updates with [**Webhook**](#webhook-installation) or [**getUpdates**](#getupdates-installation) method: +- Supported PHP Versions: **8.1 - 8.3** -| | Webhook | getUpdates | -| ---- | :----: | :----: | -| Description | Telegram sends the updates directly to your host | You have to fetch Telegram updates manually | -| Host with https | Required | Not required | -| MySQL | Not required | ([Not](#getupdates-without-database)) Required | +### Installation -## Using a custom Bot API server +_TODO_ -**For advanced users only!** +

(back to top)

-As from Telegram Bot API 5.0, users can [run their own Bot API server] to handle updates. -This means, that the PHP Telegram Bot needs to be configured to serve that custom URI. -Additionally, you can define the URI where uploaded files to the bot can be downloaded (note the `{API_KEY}` placeholder). -```php -Longman\TelegramBot\Request::setCustomBotApiUri( - $api_base_uri = 'https://your-bot-api-server', // Default: https://api.telegram.org - $api_base_download_uri = '/path/to/files/{API_KEY}' // Default: /file/bot{API_KEY} -); -``` -**Note:** If you are running your bot in `--local` mode, you won't need the `Request::downloadFile()` method, since you can then access your files directly from the absolute path returned by `Request::getFile()`. + +## Usage -## Webhook installation +_TODO_ -Note: For a more detailed explanation, head over to the [example-bot repository] and follow the instructions there. +

(back to top)

-In order to set a [Webhook][api-setwebhook] you need a server with HTTPS and composer support. -(For a [self signed certificate](#self-signed-certificate) you need to add some extra code) -Create *[set.php]* with the following contents: -```php - +## Roadmap -try { - // Create Telegram API object - $telegram = new Longman\TelegramBot\Telegram($bot_api_key, $bot_username); +- [ ] Adding Telegram Types +- [ ] Adding base functionality to call API Methods +- [ ] Adding base functionality to receive Updates +- [ ] ... - // Set webhook - $result = $telegram->setWebhook($hook_url); - if ($result->isOk()) { - echo $result->getDescription(); - } -} catch (Longman\TelegramBot\Exception\TelegramException $e) { - // log telegram errors - // echo $e->getMessage(); -} -``` +

(back to top)

-Open your *set.php* via the browser to register the webhook with Telegram. -You should see `Webhook was set`. -Now, create *[hook.php]* with the following contents: -```php -handle(); -} catch (Longman\TelegramBot\Exception\TelegramException $e) { - // Silence is golden! - // log telegram errors - // echo $e->getMessage(); -} -``` - -### Self Signed Certificate - -Upload the certificate and add the path as a parameter in *set.php*: -```php -$result = $telegram->setWebhook($hook_url, ['certificate' => '/path/to/certificate']); -``` - -### Unset Webhook - -Edit *[unset.php]* with your bot credentials and execute it. - -## getUpdates installation - -For best performance, the MySQL database should be enabled for the `getUpdates` method! - -Create *[getUpdatesCLI.php]* with the following contents: -```php -#!/usr/bin/env php - 'localhost', - 'port' => 3306, // optional - 'user' => 'dbuser', - 'password' => 'dbpass', - 'database' => 'dbname', -]; - -try { - // Create Telegram API object - $telegram = new Longman\TelegramBot\Telegram($bot_api_key, $bot_username); - - // Enable MySQL - $telegram->enableMySql($mysql_credentials); - - // Handle telegram getUpdates request - $telegram->handleGetUpdates(); -} catch (Longman\TelegramBot\Exception\TelegramException $e) { - // log telegram errors - // echo $e->getMessage(); -} -``` - -Next, give the file permission to execute: -```bash -$ chmod +x getUpdatesCLI.php -``` - -Lastly, run it! -```bash -$ ./getUpdatesCLI.php -``` - -### getUpdates without database - -If you choose to / or are obliged to use the `getUpdates` method without a database, you can replace the `$telegram->enableMySql(...);` line above with: -```php -$telegram->useGetUpdatesWithoutDatabase(); -``` - -## Filter Update - -:exclamation: Note that by default, Telegram will send any new update types that may be added in the future. This may cause commands that don't take this into account to break! - -It is suggested that you specifically define which update types your bot can receive and handle correctly. - -You can define which update types are sent to your bot by defining them when setting the [webhook](#webhook-installation) or passing an array of allowed types when using [getUpdates](#getupdates-installation). - -```php -use Longman\TelegramBot\Entities\Update; - -// For all update types currently implemented in this library: -// $allowed_updates = Update::getUpdateTypes(); - -// Define the list of allowed Update types manually: -$allowed_updates = [ - Update::TYPE_MESSAGE, - Update::TYPE_CHANNEL_POST, - // etc. -]; - -// When setting the webhook. -$telegram->setWebhook($hook_url, ['allowed_updates' => $allowed_updates]); - -// When handling the getUpdates method. -$telegram->handleGetUpdates(['allowed_updates' => $allowed_updates]); -``` - -Alternatively, Update processing can be allowed or denied by defining a custom update filter. - -Let's say we only want to allow messages from a user with ID `428`, we can do the following before handling the request: - -```php -$telegram->setUpdateFilter(function (Update $update, Telegram $telegram, &$reason = 'Update denied by update_filter') { - $user_id = $update->getMessage()->getFrom()->getId(); - if ($user_id === 428) { - return true; - } - - $reason = "Invalid user with ID {$user_id}"; - return false; -}); -``` - -The reason for denying an update can be defined with the `$reason` parameter. This text gets written to the debug log. - -## Support - -### Types - -All types are implemented according to Telegram API 6.9 (September 2023). - -### Inline Query - -Full support for inline query according to Telegram API 6.9 (September 2023). - -### Methods - -All methods are implemented according to Telegram API 6.9 (September 2023). - -#### Send Message - -Messages longer than 4096 characters are split up into multiple messages. - -```php -$result = Request::sendMessage([ - 'chat_id' => $chat_id, - 'text' => 'Your utf8 text 😜 ...', -]); -``` - -#### Send Photo - -To send a local photo, add it properly to the `$data` parameter using the file path: - -```php -$result = Request::sendPhoto([ - 'chat_id' => $chat_id, - 'photo' => Request::encodeFile('/path/to/pic.jpg'), -]); -``` - -If you know the `file_id` of a previously uploaded file, just use it directly in the data array: - -```php -$result = Request::sendPhoto([ - 'chat_id' => $chat_id, - 'photo' => 'AAQCCBNtIhAoAAss4tLEZ3x6HzqVAAqC', -]); -``` - -To send a remote photo, use the direct URL instead: - -```php -$result = Request::sendPhoto([ - 'chat_id' => $chat_id, - 'photo' => 'https://example.com/path/to/pic.jpg', -]); -``` - -*sendAudio*, *sendDocument*, *sendAnimation*, *sendSticker*, *sendVideo*, *sendVoice* and *sendVideoNote* all work in the same way, just check the [API documentation](https://core.telegram.org/bots/api#sendphoto) for the exact usage. -See the *[ImageCommand.php]* for a full example. - -#### Send Chat Action - -```php -Request::sendChatAction([ - 'chat_id' => $chat_id, - 'action' => Longman\TelegramBot\ChatAction::TYPING, -]); -``` - -#### getUserProfilePhoto - -Retrieve the user photo. (see *[WhoamiCommand.php]* for a full example) - -#### getFile and downloadFile - -Get the file path and download it. (see *[WhoamiCommand.php]* for a full example) - -#### Send message to all active chats - -To do this you have to enable the MySQL connection. -Here's an example of use (check [`DB::selectChats()`][DB::selectChats] for parameter usage): - -```php -$results = Request::sendToActiveChats( - 'sendMessage', // Callback function to execute (see Request.php methods) - ['text' => 'Hey! Check out the new features!!'], // Param to evaluate the request - [ - 'groups' => true, - 'supergroups' => true, - 'channels' => false, - 'users' => true, - ] -); -``` - -You can also broadcast a message to users, from the private chat with your bot. Take a look at the [admin commands](#admin-commands) below. - -## Utils - -### MySQL storage (Recommended) - -If you want to save messages/users/chats for further usage in commands, create a new database (`utf8mb4_unicode_520_ci`), import *[structure.sql]* and enable MySQL support BEFORE `handle()` method: - -```php -$mysql_credentials = [ - 'host' => 'localhost', - 'port' => 3306, // optional - 'user' => 'dbuser', - 'password' => 'dbpass', - 'database' => 'dbname', -]; - -$telegram->enableMySql($mysql_credentials); -``` - -You can set a custom prefix to all the tables while you are enabling MySQL: - -```php -$telegram->enableMySql($mysql_credentials, $bot_username . '_'); -``` - -You can also store inline query and chosen inline query data in the database. - -#### External Database connection - -It is possible to provide the library with an external MySQL PDO connection. -Here's how to configure it: - -```php -$telegram->enableExternalMySql($external_pdo_connection); -//$telegram->enableExternalMySql($external_pdo_connection, $table_prefix) -``` -### Channels Support - -All methods implemented can be used to manage channels. -With [admin commands](#admin-commands) you can manage your channels directly with your bot private chat. - -## Commands - -### Predefined Commands - -The bot is able to recognise commands in a chat with multiple bots (/command@mybot). - -It can also execute commands that get triggered by events, so-called Service Messages. - -### Custom Commands - -Maybe you would like to develop your own commands. -There is a guide to help you [create your own commands][wiki-create-your-own-commands]. - -Also, be sure to have a look at the [example commands][ExampleCommands-folder] to learn more about custom commands and how they work. - -You can add your custom commands in different ways: - -```php -// Add a folder that contains command files -$telegram->addCommandsPath('/path/to/command/files'); -//$telegram->addCommandsPaths(['/path/to/command/files', '/another/path']); - -// Add a command directly using the class name -$telegram->addCommandClass(MyCommand::class); -//$telegram->addCommandClasses([MyCommand::class, MyOtherCommand::class]); -``` - -### Commands Configuration - -With this method you can set some command specific parameters, for example: - -```php -// Google geocode/timezone API key for /date command -$telegram->setCommandConfig('date', [ - 'google_api_key' => 'your_google_api_key_here', -]); - -// OpenWeatherMap API key for /weather command -$telegram->setCommandConfig('weather', [ - 'owm_api_key' => 'your_owm_api_key_here', -]); -``` - -### Admin Commands - -Enabling this feature, the bot admin can perform some super user commands like: -- List all the chats started with the bot */chats* -- Clean up old database entries */cleanup* -- Show debug information about the bot */debug* -- Send message to all chats */sendtoall* -- Post any content to your channels */sendtochannel* -- Inspect a user or a chat with */whois* - -Take a look at all default admin commands stored in the *[src/Commands/AdminCommands/][AdminCommands-folder]* folder. - -#### Set Admins - -You can specify one or more admins with this option: - -```php -// Single admin -$telegram->enableAdmin(your_telegram_user_id); - -// Multiple admins -$telegram->enableAdmins([ - your_telegram_user_id, - other_telegram_user_id, -]); -``` -Telegram user id can be retrieved with the *[/whoami][WhoamiCommand.php]* command. - -#### Channel Administration + +## Contributing -To enable this feature follow these steps: -- Add your bot as channel administrator, this can be done with any Telegram client. -- Enable admin interface for your user as explained in the admin section above. -- Enter your channel name as a parameter for the *[/sendtochannel][SendtochannelCommand.php]* command: -```php -$telegram->setCommandConfig('sendtochannel', [ - 'your_channel' => [ - '@type_here_your_channel', - ] -]); -``` -- If you want to manage more channels: -```php -$telegram->setCommandConfig('sendtochannel', [ - 'your_channel' => [ - '@type_here_your_channel', - '@type_here_another_channel', - '@and_so_on', - ] -]); -``` -- Enjoy! +Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. -## Upload and Download directory path +If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". +Don't forget to give the project a star! Thanks again! -To use the Upload and Download functionality, you need to set the paths with: -```php -$telegram->setDownloadPath('/your/path/Download'); -$telegram->setUploadPath('/your/path/Upload'); -``` +1. Fork the Project +2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) +3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) +4. Push to the Branch (`git push origin feature/AmazingFeature`) +5. Open a Pull Request -## Documentation +

(back to top)

-Take a look at the repo [Wiki] for further information and tutorials! -Feel free to improve! -## Assets -All project assets can be found in the [assets](https://github.com/php-telegram-bot/assets) repository. + +## License -## Example bot +Distributed under the MIT License. See `LICENSE.txt` for more information. -We're busy working on a full A-Z example bot, to help get you started with this library and to show you how to use all its features. -You can check the progress of the [example-bot repository]). - -## Projects with this library - -Here's a list of projects that feats this library, feel free to add yours! -- [Inline Games](https://github.com/jacklul/inlinegamesbot) ([@inlinegamesbot](https://telegram.me/inlinegamesbot)) -- [Super-Dice-Roll](https://github.com/RafaelDelboni/Super-Dice-Roll) ([@superdiceroll_bot](https://telegram.me/superdiceroll_bot)) -- [tg-mentioned-bot](https://github.com/gruessung/tg-mentioned-bot) -- [OSMdeWikiBot](https://github.com/OSM-de/TelegramWikiBot) ([@OSM_de](https://t.me/OSM_de)) -- [pass-generator-webbot](https://github.com/OxMohsen/pass-generator-webbot) -- [Chess Quiz Bot](https://github.com/1int/chess-quiz-bot) -- [PHP Telegram Bot - Symfony Bundle](https://github.com/m4n50n/telegram_bot_bundle) - -## Troubleshooting - -If you like living on the edge, please report any bugs you find on the [PHP Telegram Bot issues][issues] page. +

(back to top)

-## Contributing -See [CONTRIBUTING](CONTRIBUTING.md) for more information. -## Security + +## Contact -See [SECURITY](SECURITY.md) for more information. +Tii - [@Tii@chaos.social](https://chaos.social/@Tii) -## Donate +Project Link: [https://github.com/php-telegram-bot/core](https://github.com/php-telegram-bot/core) -All work on this bot consists of many hours of coding during our free time, to provide you with a Telegram Bot library that is easy to use and extend. -If you enjoy using this library and would like to say thank you, donations are a great way to show your support. +

(back to top)

-Donations are invested back into the project :+1: -Thank you for keeping this project alive :pray: -- [![Patreon](https://user-images.githubusercontent.com/9423417/59235980-a5fa6b80-8be3-11e9-8ae7-020bc4ae9baa.png) Patreon.com/phptelegrambot][Patreon] -- [![OpenCollective](https://user-images.githubusercontent.com/9423417/59235978-a561d500-8be3-11e9-89be-82ec54be1546.png) OpenCollective.com/php-telegram-bot][OpenCollective] -- [![Ko-fi](https://user-images.githubusercontent.com/9423417/59235976-a561d500-8be3-11e9-911d-b1908c3e6a33.png) Ko-fi.com/phptelegrambot][Ko-fi] -- [![Tidelift](https://user-images.githubusercontent.com/9423417/59235982-a6930200-8be3-11e9-8ac2-bfb6991d80c5.png) Tidelift.com/longman/telegram-bot][Tidelift] -- [![Liberapay](https://user-images.githubusercontent.com/9423417/59235977-a561d500-8be3-11e9-9d16-bc3b13d3ceba.png) Liberapay.com/PHP-Telegram-Bot][Liberapay] -- [![PayPal](https://user-images.githubusercontent.com/9423417/59235981-a5fa6b80-8be3-11e9-9761-15eb7a524cb0.png) PayPal.me/noplanman][PayPal-noplanman] (account of @noplanman) -- [![Bitcoin](https://user-images.githubusercontent.com/9423417/59235974-a4c93e80-8be3-11e9-9fde-260c821b6eae.png) 166NcyE7nDxkRPWidWtG1rqrNJoD5oYNiV][Bitcoin] -- [![Ethereum](https://user-images.githubusercontent.com/9423417/59235975-a4c93e80-8be3-11e9-8762-7a47c62c968d.png) 0x485855634fa212b0745375e593fAaf8321A81055][Ethereum] + +## Acknowledgments -## For enterprise +* Big Thanks to [Avtandil Kikabidze aka LONGMAN](https://github.com/akalongman), who created this library -Available as part of the Tidelift Subscription. +

(back to top)

-The maintainers of `PHP Telegram Bot` and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. [Learn more.][Tidelift] -## License -Please see the [LICENSE](LICENSE) included in this repository for a full copy of the MIT license, which this project is licensed under. - -## Credits - -Credit list in [CREDITS](CREDITS) - ---- - -[Telegram Bot API]: https://core.telegram.org/bots/api "Telegram Bot API" -[Composer]: https://getcomposer.org/ "Composer" -[run their own Bot API server]: https://core.telegram.org/bots/api#using-a-local-bot-api-server "Using a Local Bot API Server" -[example-bot repository]: https://github.com/php-telegram-bot/example-bot "Example Bot repository" -[api-setwebhook]: https://core.telegram.org/bots/api#setwebhook "Webhook on Telegram Bot API" -[set.php]: https://github.com/php-telegram-bot/example-bot/blob/master/set.php "example set.php" -[unset.php]: https://github.com/php-telegram-bot/example-bot/blob/master/unset.php "example unset.php" -[hook.php]: https://github.com/php-telegram-bot/example-bot/blob/master/hook.php "example hook.php" -[getUpdatesCLI.php]: https://github.com/php-telegram-bot/example-bot/blob/master/getUpdatesCLI.php "example getUpdatesCLI.php" -[AdminCommands-folder]: https://github.com/php-telegram-bot/core/tree/master/src/Commands/AdminCommands "Admin commands folder" -[ExampleCommands-folder]: https://github.com/php-telegram-bot/example-bot/blob/master/Commands "Example commands folder" -[ImageCommand.php]: https://github.com/php-telegram-bot/example-bot/blob/master/Commands/Other/ImageCommand.php "example /image command" -[WhoamiCommand.php]: https://github.com/php-telegram-bot/example-bot/blob/master/Commands/WhoamiCommand.php "example /whoami command" -[HelpCommand.php]: https://github.com/php-telegram-bot/example-bot/blob/master/Commands/HelpCommand.php "example /help command" -[SendtochannelCommand.php]: https://github.com/php-telegram-bot/core/blob/master/src/Commands/AdminCommands/SendtochannelCommand.php "/sendtochannel admin command" -[DB::selectChats]: https://github.com/php-telegram-bot/core/blob/0.70.0/src/DB.php#L1148 "DB::selectChats() parameters" -[structure.sql]: https://github.com/php-telegram-bot/core/blob/master/structure.sql "DB structure for importing" -[Wiki]: https://github.com/php-telegram-bot/core/wiki "PHP Telegram Bot Wiki" -[wiki-create-your-own-commands]: https://github.com/php-telegram-bot/core/wiki/Create-your-own-commands "Create your own commands" -[issues]: https://github.com/php-telegram-bot/core/issues "PHP Telegram Bot Issues" - -[Patreon]: https://www.patreon.com/phptelegrambot "Support us on Patreon" -[OpenCollective]: https://opencollective.com/php-telegram-bot "Support us on Open Collective" -[Ko-fi]: https://ko-fi.com/phptelegrambot "Support us on Ko-fi" -[Tidelift]: https://tidelift.com/subscription/pkg/packagist-longman-telegram-bot?utm_source=packagist-longman-telegram-bot&utm_medium=referral&utm_campaign=enterprise&utm_term=repo "Learn more about the Tidelift Subscription" -[Liberapay]: https://liberapay.com/PHP-Telegram-Bot "Donate with Liberapay" -[PayPal-noplanman]: https://paypal.me/noplanman "Donate with PayPal" -[Bitcoin]: https://www.blockchain.com/btc/address/166NcyE7nDxkRPWidWtG1rqrNJoD5oYNiV "Donate with Bitcoin" -[Ethereum]: https://etherscan.io/address/0x485855634fa212b0745375e593fAaf8321A81055 "Donate with Ethereum" + + +[contributors-shield]: https://img.shields.io/github/contributors/php-telegram-bot/core.svg?style=for-the-badge +[contributors-url]: https://github.com/php-telegram-bot/core/graphs/contributors +[forks-shield]: https://img.shields.io/github/forks/php-telegram-bot/core.svg?style=for-the-badge +[forks-url]: https://github.com/php-telegram-bot/core/network/members +[stars-shield]: https://img.shields.io/github/stars/php-telegram-bot/core.svg?style=for-the-badge +[stars-url]: https://github.com/php-telegram-bot/core/stargazers +[issues-shield]: https://img.shields.io/github/issues/php-telegram-bot/core.svg?style=for-the-badge +[issues-url]: https://github.com/php-telegram-bot/core/issues +[license-shield]: https://img.shields.io/github/license/php-telegram-bot/core.svg?style=for-the-badge +[license-url]: https://github.com/php-telegram-bot/core/blob/master/LICENSE.txt \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index 5358dc50b..000000000 --- a/SECURITY.md +++ /dev/null @@ -1,3 +0,0 @@ -# Security Policy - -To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. diff --git a/composer.json b/composer.json index 1c421ec93..f23593347 100644 --- a/composer.json +++ b/composer.json @@ -1,81 +1,73 @@ { - "name": "longman/telegram-bot", - "type": "library", - "description": "PHP Telegram bot", - "keywords": ["telegram", "bot", "api"], - "license": "MIT", - "homepage": "https://github.com/php-telegram-bot/core", - "support": { - "issues": "https://github.com/php-telegram-bot/core/issues", - "source": "https://github.com/php-telegram-bot/core" + "name": "php-telegram-bot/core", + "type": "library", + "description": "PHP Telegram Bot", + "keywords": [ + "telegram", + "bot", + "api" + ], + "license": "MIT", + "homepage": "https://github.com/php-telegram-bot/core", + "support": { + "issues": "https://github.com/php-telegram-bot/core/issues", + "source": "https://github.com/php-telegram-bot/core" + }, + "authors": [ + { + "name": "Avtandil Kikabidze aka LONGMAN", + "email": "akalongman@gmail.com", + "homepage": "http://longman.me", + "role": "Developer" }, - "authors": [ - { - "name": "Avtandil Kikabidze aka LONGMAN", - "email": "akalongman@gmail.com", - "homepage": "http://longman.me", - "role": "Developer" - }, - { - "name": "Armando Lüscher", - "email": "armando@noplanman.ch", - "homepage": "https://noplanman.ch", - "role": "Developer" - }, - { - "name": "PHP Telegram Bot Team", - "homepage": "https://github.com/php-telegram-bot/core/graphs/contributors", - "role": "Developer" - } - ], - "require": { - "php": "^8.1", - "ext-pdo": "*", - "ext-curl": "*", - "ext-json": "*", - "ext-mbstring": "*", - "psr/log": "^1.1|^2.0|^3.0", - "guzzlehttp/guzzle": "^6.0|^7.0" + { + "name": "Armando Lüscher", + "email": "armando@noplanman.ch", + "homepage": "https://noplanman.ch", + "role": "Developer" }, - "require-dev": { - "phpunit/phpunit": "^9.5", - "squizlabs/php_codesniffer": "^3.6", - "dms/phpunit-arraysubset-asserts": "^0.2", - "monolog/monolog": "^2.1" + { + "name": "Tii", + "email": "mail@tii.one", + "role": "Developer" }, - "autoload": { - "psr-4": { - "Longman\\TelegramBot\\": "src" - } - }, - "autoload-dev": { - "psr-4": { - "Longman\\TelegramBot\\Tests\\Unit\\": "tests/Unit" - } - }, - "scripts": { - "check-code": [ - "\"vendor/bin/phpcs\"" - ], - "test": [ - "\"vendor/bin/phpunit\"" - ], - "test-cov": [ - "XDEBUG_MODE=coverage \"vendor/bin/phpunit\" --coverage-clover clover.xml" - ], - "test-cov-upload": [ - "@php ocular.phar code-coverage:upload --format=php-clover clover.xml" - ] - }, - "config": { - "optimize-autoloader": true, - "preferred-install": "dist", - "sort-packages": true, - "process-timeout": 3600 - }, - "extra": { - "branch-alias": { - "dev-develop": "0.83.x-dev" - } + { + "name": "PHP Telegram Bot Team", + "homepage": "https://github.com/php-telegram-bot/core/graphs/contributors", + "role": "Developer" + } + ], + "require": { + "php": "^8.1", + "php-http/discovery": "^1.17", + "php-http/multipart-stream-builder": "^1.3", + "psr/http-client": "*", + "psr/http-client-implementation": "*", + "psr/http-message": "*" + }, + "require-dev": { + "guzzlehttp/psr7": "^2.6", + "laravel/pint": "^1.16", + "php-http/guzzle7-adapter": "^1.0", + "spatie/ray": "^1.41" + }, + "autoload": { + "psr-4": { + "PhpTelegramBot\\Core\\": "src" + } + }, + "config": { + "optimize-autoloader": true, + "preferred-install": "dist", + "sort-packages": true, + "process-timeout": 3600, + "allow-plugins": { + "php-http/discovery": false + } + }, + "extra": { + "branch-alias": { + "dev-develop": "1.x-dev" } + } } diff --git a/doc/01-utils.md b/doc/01-utils.md deleted file mode 100644 index e741d3cfc..000000000 --- a/doc/01-utils.md +++ /dev/null @@ -1,58 +0,0 @@ -## Logging -PHP Telegram Bot library features [PSR-3] compatible logging to store logs. - -You can find a list of compatible packages that can be used on [Packagist][PSR-3-providers]. - -Logs are divided into the following streams: -- `error`: Collects all the exceptions thrown by the library. -- `debug`: Stores requests made to the Telegram API, useful for debugging. -- `update`: Incoming raw updates (JSON string from Webhook and getUpdates). - -### Initialisation -To initialise the logger, you can pass any `LoggerInterface` objects to the `TelegramLog::initialize` method. - -The first parameter is the main logger, the second one is used for the raw updates. - -(in this example we're using [Monolog]) -```php -use Longman\TelegramBot\TelegramLog; -use Monolog\Formatter\LineFormatter; -use Monolog\Handler\StreamHandler; -use Monolog\Logger; - -TelegramLog::initialize( - // Main logger that handles all 'debug' and 'error' logs. - new Logger('telegram_bot', [ - (new StreamHandler('/path/to/debug_log_file', Logger::DEBUG))->setFormatter(new LineFormatter(null, null, true)), - (new StreamHandler('/path/to/error_log_file', Logger::ERROR))->setFormatter(new LineFormatter(null, null, true)), - ]), - // Updates logger for raw updates. - new Logger('telegram_bot_updates', [ - (new StreamHandler('/path/to/updates_log_file', Logger::INFO))->setFormatter(new LineFormatter('%message%' . PHP_EOL)), - ]) -); -``` - -### Raw data -Why do I need to log the raw updates? -Telegram API changes continuously and it often happens that the database schema is not up to date with new entities/features. So it can happen that your table schema doesn't allow storing new valuable information coming from Telegram. - -If you store the raw data you can import all updates on the newest table schema by simply using [this script](../utils/importFromLog.php). -Remember to always backup first!! - -### Always log request and response data -If you'd like to always log the request and response data to the debug log, even for successful requests, you can set the appropriate variable: -```php -\Longman\TelegramBot\TelegramLog::$always_log_request_and_response = true; -``` - -### Hiding API token from the log -By default, the API token is removed from the log, to prevent any mistaken leakage when posting logs online. -This behaviour can be changed by setting the appropriate variable: -```php -\Longman\TelegramBot\TelegramLog::$remove_bot_token = false; -``` - -[PSR-3]: https://www.php-fig.org/psr/psr-3 -[PSR-3-providers]: https://packagist.org/providers/psr/log-implementation -[Monolog]: https://github.com/Seldaek/monolog diff --git a/phpcs.xml.dist b/phpcs.xml.dist deleted file mode 100644 index 7ba753144..000000000 --- a/phpcs.xml.dist +++ /dev/null @@ -1,19 +0,0 @@ - - - PHP Code Sniffer - - - - - - - - - src/ - tests/ - utils/ - - - - - diff --git a/phpunit.xml.dist b/phpunit.xml.dist deleted file mode 100644 index 036786b84..000000000 --- a/phpunit.xml.dist +++ /dev/null @@ -1,26 +0,0 @@ - - - - - ./src - - - ./src/Exception - - - - - - - - - - - - - - - ./tests/ - - - diff --git a/src/ChatAction.php b/src/ChatAction.php deleted file mode 100644 index c98f424e1..000000000 --- a/src/ChatAction.php +++ /dev/null @@ -1,70 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot; - -class ChatAction -{ - /** - * Typing chat action - */ - public const TYPING = 'typing'; - - /** - * Upload Photo chat action - */ - public const UPLOAD_PHOTO = 'upload_photo'; - - /** - * Record Video chat action - */ - public const RECORD_VIDEO = 'record_video'; - - /** - * Upload Video chat action - */ - public const UPLOAD_VIDEO = 'upload_video'; - - /** - * Record Voice chat action - */ - public const RECORD_VOICE = 'record_voice'; - - /** - * Upload Voice chat action - */ - public const UPLOAD_VOICE = 'upload_voice'; - - /** - * Upload Document chat action - */ - public const UPLOAD_DOCUMENT = 'upload_document'; - - /** - * Choose Sticker chat action - */ - public const CHOOSE_STICKER = 'choose_sticker'; - - /** - * Find Location chat action - */ - public const FIND_LOCATION = 'find_location'; - - /** - * Record Video Note chat action - */ - public const RECORD_VIDEO_NOTE = 'record_video_note'; - - /** - * Upload Video note chat action - */ - public const UPLOAD_VIDEO_NOTE = 'upload_video_note'; -} diff --git a/src/Commands/AdminCommand.php b/src/Commands/AdminCommand.php deleted file mode 100644 index d63f6b60c..000000000 --- a/src/Commands/AdminCommand.php +++ /dev/null @@ -1,20 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Commands; - -abstract class AdminCommand extends Command -{ - /** - * @var bool - */ - protected $private_only = true; -} diff --git a/src/Commands/AdminCommands/ChatsCommand.php b/src/Commands/AdminCommands/ChatsCommand.php deleted file mode 100644 index d3d818a54..000000000 --- a/src/Commands/AdminCommands/ChatsCommand.php +++ /dev/null @@ -1,143 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Commands\AdminCommands; - -use Longman\TelegramBot\Commands\AdminCommand; -use Longman\TelegramBot\DB; -use Longman\TelegramBot\Entities\Chat; -use Longman\TelegramBot\Entities\ServerResponse; -use Longman\TelegramBot\Exception\TelegramException; -use Longman\TelegramBot\Request; - -class ChatsCommand extends AdminCommand -{ - /** - * @var string - */ - protected $name = 'chats'; - - /** - * @var string - */ - protected $description = 'List or search all chats stored by the bot'; - - /** - * @var string - */ - protected $usage = '/chats, /chats * or /chats '; - - /** - * @var string - */ - protected $version = '1.2.0'; - - /** - * @var bool - */ - protected $need_mysql = true; - - /** - * Command execute method - * - * @return ServerResponse - * @throws TelegramException - */ - public function execute(): ServerResponse - { - $message = $this->getMessage() ?: $this->getEditedMessage() ?: $this->getChannelPost() ?: $this->getEditedChannelPost(); - - $chat_id = $message->getChat()->getId(); - $text = trim($message->getText(true)); - - $results = DB::selectChats([ - 'groups' => true, - 'supergroups' => true, - 'channels' => true, - 'users' => true, - 'text' => ($text === '' || $text === '*') ? null : $text //Text to search in user/group name - ]); - - $user_chats = 0; - $group_chats = 0; - $supergroup_chats = 0; - $channel_chats = 0; - - if ($text === '') { - $text_back = ''; - } elseif ($text === '*') { - $text_back = 'List of all bot chats:' . PHP_EOL; - } else { - $text_back = 'Chat search results:' . PHP_EOL; - } - - if (is_array($results)) { - foreach ($results as $result) { - //Initialize a chat object - $result['id'] = $result['chat_id']; - $chat = new Chat($result); - - $whois = $chat->getId(); - if ($this->telegram->getCommandObject('whois')) { - // We can't use '-' in command because part of it will become unclickable - $whois = '/whois' . str_replace('-', 'g', $chat->getId()); - } - - if ($chat->isPrivateChat()) { - if ($text !== '') { - $text_back .= '- P ' . $chat->tryMention() . ' [' . $whois . ']' . PHP_EOL; - } - - ++$user_chats; - } elseif ($chat->isSuperGroup()) { - if ($text !== '') { - $text_back .= '- S ' . $chat->getTitle() . ' [' . $whois . ']' . PHP_EOL; - } - - ++$supergroup_chats; - } elseif ($chat->isGroupChat()) { - if ($text !== '') { - $text_back .= '- G ' . $chat->getTitle() . ' [' . $whois . ']' . PHP_EOL; - } - - ++$group_chats; - } elseif ($chat->isChannel()) { - if ($text !== '') { - $text_back .= '- C ' . $chat->getTitle() . ' [' . $whois . ']' . PHP_EOL; - } - - ++$channel_chats; - } - } - } - - if (($user_chats + $group_chats + $supergroup_chats) === 0) { - $text_back = 'No chats found..'; - } else { - $text_back .= PHP_EOL . 'Private Chats: ' . $user_chats; - $text_back .= PHP_EOL . 'Groups: ' . $group_chats; - $text_back .= PHP_EOL . 'Super Groups: ' . $supergroup_chats; - $text_back .= PHP_EOL . 'Channels: ' . $channel_chats; - $text_back .= PHP_EOL . 'Total: ' . ($user_chats + $group_chats + $supergroup_chats); - - if ($text === '') { - $text_back .= PHP_EOL . PHP_EOL . 'List all chats: /' . $this->name . ' *' . PHP_EOL . 'Search for chats: /' . $this->name . ' '; - } - } - - $data = [ - 'chat_id' => $chat_id, - 'text' => $text_back, - ]; - - return Request::sendMessage($data); - } -} diff --git a/src/Commands/AdminCommands/CleanupCommand.php b/src/Commands/AdminCommands/CleanupCommand.php deleted file mode 100644 index 221ed932d..000000000 --- a/src/Commands/AdminCommands/CleanupCommand.php +++ /dev/null @@ -1,435 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Commands\AdminCommands; - -use Longman\TelegramBot\Commands\AdminCommand; -use Longman\TelegramBot\DB; -use Longman\TelegramBot\Entities\ServerResponse; -use Longman\TelegramBot\Exception\TelegramException; -use Longman\TelegramBot\Request; -use Longman\TelegramBot\TelegramLog; -use PDOException; - -/** - * User "/cleanup" command - * - * Configuration options: - * - * $telegram->setCommandConfig('cleanup', [ - * // Define which tables should be cleaned. - * 'tables_to_clean' => [ - * 'message', - * 'edited_message', - * ], - * // Define how old cleaned entries should be. - * 'clean_older_than' => [ - * 'message' => '7 days', - * 'edited_message' => '30 days', - * ] - * ); - */ -class CleanupCommand extends AdminCommand -{ - /** - * @var string - */ - protected $name = 'cleanup'; - - /** - * @var string - */ - protected $description = 'Clean up the database from old records'; - - /** - * @var string - */ - protected $usage = '/cleanup [dry] or /cleanup [dry] (e.g. 3 weeks)'; - - /** - * @var string - */ - protected $version = '1.1.0'; - - /** - * @var bool - */ - protected $need_mysql = true; - - /** - * Default tables to clean, cleaning 'chat', 'user' and 'user_chat' by default is bad practice! - * - * @var array - */ - protected static $default_tables_to_clean = [ - 'callback_query', - 'chosen_inline_result', - 'conversation', - 'edited_message', - 'inline_query', - 'message', - 'request_limiter', - 'telegram_update', - ]; - - /** - * By default, remove records older than X days/hours/anything from these tables. - * - * @var array - */ - protected static $default_clean_older_than = [ - 'callback_query' => '30 days', - 'chat' => '365 days', - 'chosen_inline_result' => '30 days', - 'conversation' => '90 days', - 'edited_message' => '30 days', - 'inline_query' => '30 days', - 'message' => '30 days', - 'poll' => '90 days', - 'request_limiter' => '1 minute', - 'shipping_query' => '90 days', - 'telegram_update' => '30 days', - 'user' => '365 days', - 'user_chat' => '365 days', - ]; - - /** - * Set command config - * - * @param string $custom_time - * - * @return array - */ - private function getSettings($custom_time = ''): array - { - $tables_to_clean = self::$default_tables_to_clean; - $user_tables_to_clean = $this->getConfig('tables_to_clean'); - if (is_array($user_tables_to_clean)) { - $tables_to_clean = $user_tables_to_clean; - } - - $clean_older_than = self::$default_clean_older_than; - $user_clean_older_than = $this->getConfig('clean_older_than'); - if (is_array($user_clean_older_than)) { - $clean_older_than = array_merge($clean_older_than, $user_clean_older_than); - } - - // Convert numeric-only values to days. - array_walk($clean_older_than, function (&$time) use ($custom_time) { - if (!empty($custom_time)) { - $time = $custom_time; - } - if (is_numeric($time)) { - $time .= ' days'; - } - }); - - return compact('tables_to_clean', 'clean_older_than'); - } - - /** - * Get SQL queries array based on settings provided - * - * @param $settings - * - * @return array - * @throws TelegramException - */ - private function getQueries($settings): array - { - if (empty($settings) || !is_array($settings)) { - throw new TelegramException('Settings variable is not an array or is empty!'); - } - - // Convert all clean_older_than times to correct format. - $clean_older_than = $settings['clean_older_than']; - foreach ($clean_older_than as $table => $time) { - $clean_older_than[$table] = date('Y-m-d H:i:s', strtotime('-' . $time)); - } - $tables_to_clean = $settings['tables_to_clean']; - - $queries = []; - - if (in_array('telegram_update', $tables_to_clean, true)) { - $queries[] = sprintf( - 'DELETE FROM `%3$s` - WHERE id IN ( - SELECT id FROM ( - SELECT id FROM `%3$s` - WHERE `id` != \'%1$s\' - AND `chat_id` NOT IN ( - SELECT `id` - FROM `%4$s` - WHERE `%3$s`.`chat_id` = `id` - AND `updated_at` < \'%2$s\' - ) - AND ( - `message_id` IS NOT NULL - AND `message_id` IN ( - SELECT `id` - FROM `%5$s` - WHERE `date` < \'%2$s\' - ) - ) - OR ( - `edited_message_id` IS NOT NULL - AND `edited_message_id` IN ( - SELECT `id` - FROM `%6$s` - WHERE `edit_date` < \'%2$s\' - ) - ) - OR ( - `inline_query_id` IS NOT NULL - AND `inline_query_id` IN ( - SELECT `id` - FROM `%7$s` - WHERE `created_at` < \'%2$s\' - ) - ) - OR ( - `chosen_inline_result_id` IS NOT NULL - AND `chosen_inline_result_id` IN ( - SELECT `id` - FROM `%8$s` - WHERE `created_at` < \'%2$s\' - ) - ) - OR ( - `callback_query_id` IS NOT NULL - AND `callback_query_id` IN ( - SELECT `id` - FROM `%9$s` - WHERE `created_at` < \'%2$s\' - ) - ) - ) a - ) - ', - $this->getUpdate()->getUpdateId(), - $clean_older_than['telegram_update'], - TB_TELEGRAM_UPDATE, - TB_CHAT, - TB_MESSAGE, - TB_EDITED_MESSAGE, - TB_INLINE_QUERY, - TB_CHOSEN_INLINE_RESULT, - TB_CALLBACK_QUERY - ); - } - - if (in_array('user_chat', $tables_to_clean, true)) { - $queries[] = sprintf( - 'DELETE FROM `%1$s` - WHERE `user_id` IN ( - SELECT `id` - FROM `%2$s` - WHERE `updated_at` < \'%3$s\' - ) - ', - TB_USER_CHAT, - TB_USER, - $clean_older_than['chat'] - ); - } - - // Simple. - $simple_tables = [ - 'user' => ['table' => TB_USER, 'field' => 'updated_at'], - 'chat' => ['table' => TB_CHAT, 'field' => 'updated_at'], - 'conversation' => ['table' => TB_CONVERSATION, 'field' => 'updated_at'], - 'poll' => ['table' => TB_POLL, 'field' => 'created_at'], - 'request_limiter' => ['table' => TB_REQUEST_LIMITER, 'field' => 'created_at'], - 'shipping_query' => ['table' => TB_SHIPPING_QUERY, 'field' => 'created_at'], - ]; - - foreach (array_intersect(array_keys($simple_tables), $tables_to_clean) as $table_to_clean) { - $queries[] = sprintf( - 'DELETE FROM `%1$s` - WHERE `%2$s` < \'%3$s\' - ', - $simple_tables[$table_to_clean]['table'], - $simple_tables[$table_to_clean]['field'], - $clean_older_than[$table_to_clean] - ); - } - - // Queries. - $query_tables = [ - 'inline_query' => ['table' => TB_INLINE_QUERY, 'field' => 'created_at'], - 'chosen_inline_result' => ['table' => TB_CHOSEN_INLINE_RESULT, 'field' => 'created_at'], - 'callback_query' => ['table' => TB_CALLBACK_QUERY, 'field' => 'created_at'], - ]; - foreach (array_intersect(array_keys($query_tables), $tables_to_clean) as $table_to_clean) { - $queries[] = sprintf( - 'DELETE FROM `%1$s` - WHERE `%2$s` < \'%3$s\' - AND `id` NOT IN ( - SELECT `%4$s` - FROM `%5$s` - WHERE `%4$s` = `%1$s`.`id` - ) - ', - $query_tables[$table_to_clean]['table'], - $query_tables[$table_to_clean]['field'], - $clean_older_than[$table_to_clean], - $table_to_clean . '_id', - TB_TELEGRAM_UPDATE - ); - } - - // Messages - if (in_array('edited_message', $tables_to_clean, true)) { - $queries[] = sprintf( - 'DELETE FROM `%1$s` - WHERE `edit_date` < \'%2$s\' - AND `id` NOT IN ( - SELECT `message_id` - FROM `%3$s` - WHERE `edited_message_id` = `%1$s`.`id` - ) - ', - TB_EDITED_MESSAGE, - $clean_older_than['edited_message'], - TB_TELEGRAM_UPDATE - ); - } - - if (in_array('message', $tables_to_clean, true)) { - $queries[] = sprintf( - 'DELETE FROM `%1$s` - WHERE id IN ( - SELECT id - FROM ( - SELECT id - FROM `%1$s` - WHERE `date` < \'%2$s\' - AND `id` NOT IN ( - SELECT `message_id` - FROM `%3$s` - WHERE `message_id` = `%1$s`.`id` - ) - AND `id` NOT IN ( - SELECT `message_id` - FROM `%4$s` - WHERE `message_id` = `%1$s`.`id` - ) - AND `id` NOT IN ( - SELECT `message_id` - FROM `%5$s` - WHERE `message_id` = `%1$s`.`id` - ) - AND `id` NOT IN ( - SELECT a.`reply_to_message` FROM `%1$s` a - INNER JOIN `%1$s` b ON b.`id` = a.`reply_to_message` AND b.`chat_id` = a.`reply_to_chat` - ) - ORDER BY `id` DESC - ) a - ) - ', - TB_MESSAGE, - $clean_older_than['message'], - TB_EDITED_MESSAGE, - TB_TELEGRAM_UPDATE, - TB_CALLBACK_QUERY - ); - } - - return $queries; - } - - /** - * Execution if MySQL is required but not available - * - * @return ServerResponse - * @throws TelegramException - */ - public function executeNoDb(): ServerResponse - { - return $this->replyToChat('*No database connection!*', ['parse_mode' => 'Markdown']); - } - - /** - * Command execute method - * - * @return ServerResponse - * @throws TelegramException - */ - public function execute(): ServerResponse - { - $message = $this->getMessage() ?: $this->getEditedMessage() ?: $this->getChannelPost() ?: $this->getEditedChannelPost(); - $text = $message->getText(true); - - // Dry run? - $dry_run = strpos($text, 'dry') !== false; - $text = trim(str_replace('dry', '', $text)); - - $settings = $this->getSettings($text); - $queries = $this->getQueries($settings); - - if ($dry_run) { - return $this->replyToUser('Queries:' . PHP_EOL . implode(PHP_EOL, $queries)); - } - - $infos = []; - foreach ($settings['tables_to_clean'] as $table) { - $info = "*{$table}*"; - - if (isset($settings['clean_older_than'][$table])) { - $info .= " ({$settings['clean_older_than'][$table]})"; - } - - $infos[] = $info; - } - - $data = [ - 'chat_id' => $message->getFrom()->getId(), - 'parse_mode' => 'Markdown', - ]; - - $data['text'] = 'Cleaning up tables:' . PHP_EOL . implode(PHP_EOL, $infos); - Request::sendMessage($data); - - $rows = 0; - $pdo = DB::getPdo(); - try { - $pdo->beginTransaction(); - - foreach ($queries as $query) { - // Delete in chunks to not block / improve speed on big tables. - $query .= ' LIMIT 10000'; - while ($dbq = $pdo->query($query)) { - if ($dbq->rowCount() === 0) { - continue 2; - } - $rows += $dbq->rowCount(); - } - - TelegramLog::error('Error while executing query: ' . $query); - } - - // commit changes to the database and end transaction - $pdo->commit(); - - $data['text'] = "*Database cleanup done!* _(removed {$rows} rows)_"; - } catch (PDOException $e) { - $data['text'] = '*Database cleanup failed!* _(check your error logs)_'; - - // rollback changes on exception - // useful if you want to track down error you can't replicate it when some of the data is already deleted - $pdo->rollBack(); - - TelegramLog::error($e->getMessage()); - } - - return Request::sendMessage($data); - } -} diff --git a/src/Commands/AdminCommands/DebugCommand.php b/src/Commands/AdminCommands/DebugCommand.php deleted file mode 100644 index 8a2fe80e4..000000000 --- a/src/Commands/AdminCommands/DebugCommand.php +++ /dev/null @@ -1,127 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Commands\AdminCommands; - -use Exception; -use Longman\TelegramBot\Commands\AdminCommand; -use Longman\TelegramBot\DB; -use Longman\TelegramBot\Entities\ServerResponse; -use Longman\TelegramBot\Exception\TelegramException; -use Longman\TelegramBot\Request; - -/** - * Admin "/debug" command - */ -class DebugCommand extends AdminCommand -{ - /** - * @var string - */ - protected $name = 'debug'; - - /** - * @var string - */ - protected $description = 'Debug command to help find issues'; - - /** - * @var string - */ - protected $usage = '/debug'; - - /** - * @var string - */ - protected $version = '1.1.0'; - - /** - * Command execute method - * - * @return mixed - * @throws TelegramException - */ - public function execute(): ServerResponse - { - $pdo = DB::getPdo(); - $message = $this->getMessage() ?: $this->getEditedMessage() ?: $this->getChannelPost() ?: $this->getEditedChannelPost(); - $chat = $message->getChat(); - $text = strtolower($message->getText(true)); - - $data = ['chat_id' => $chat->getId()]; - - if ($text !== 'glasnost' && !$chat->isPrivateChat()) { - $data['text'] = 'Only available in a private chat.'; - - return Request::sendMessage($data); - } - - $debug_info = []; - - $debug_info[] = sprintf('*TelegramBot version:* `%s`', $this->telegram->getVersion()); - $debug_info[] = sprintf('*Download path:* `%s`', $this->telegram->getDownloadPath() ?: '`_Not set_`'); - $debug_info[] = sprintf('*Upload path:* `%s`', $this->telegram->getUploadPath() ?: '`_Not set_`'); - - // Commands paths. - $debug_info[] = '*Commands paths:*'; - $debug_info[] = sprintf( - '```' . PHP_EOL . '%s```', - json_encode($this->telegram->getCommandsPaths(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) - ); - - $php_bit = ''; - PHP_INT_SIZE === 4 && $php_bit = ' (32bit)'; - PHP_INT_SIZE === 8 && $php_bit = ' (64bit)'; - $debug_info[] = sprintf('*PHP version:* `%1$s%2$s; %3$s; %4$s`', PHP_VERSION, $php_bit, PHP_SAPI, PHP_OS); - $debug_info[] = sprintf('*Maximum PHP script execution time:* `%d seconds`', ini_get('max_execution_time')); - - $mysql_version = $pdo ? $pdo->query('SELECT VERSION() AS version')->fetchColumn() : null; - $debug_info[] = sprintf('*MySQL version:* `%s`', $mysql_version ?: 'disabled'); - - $debug_info[] = sprintf('*Operating System:* `%s`', php_uname()); - - if (isset($_SERVER['SERVER_SOFTWARE'])) { - $debug_info[] = sprintf('*Web Server:* `%s`', $_SERVER['SERVER_SOFTWARE']); - } - if (function_exists('curl_init')) { - $curlversion = curl_version(); - $debug_info[] = sprintf('*curl version:* `%1$s; %2$s`', $curlversion['version'], $curlversion['ssl_version']); - } - - $webhook_info_title = '*Webhook Info:*'; - try { - // Check if we're actually using the Webhook method. - if (Request::getInput() === '') { - $debug_info[] = $webhook_info_title . ' `Using getUpdates method, not Webhook.`'; - } else { - $webhook_info_result = json_decode(Request::getWebhookInfo(), true)['result']; - // Add a human-readable error date string if necessary. - if (isset($webhook_info_result['last_error_date'])) { - $webhook_info_result['last_error_date_string'] = date('Y-m-d H:i:s', $webhook_info_result['last_error_date']); - } - - $webhook_info_result_str = json_encode($webhook_info_result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); - $debug_info[] = $webhook_info_title; - $debug_info[] = sprintf( - '```' . PHP_EOL . '%s```', - $webhook_info_result_str - ); - } - } catch (Exception $e) { - $debug_info[] = $webhook_info_title . sprintf(' `Failed to get webhook info! (%s)`', $e->getMessage()); - } - - $data['parse_mode'] = 'Markdown'; - $data['text'] = implode(PHP_EOL, $debug_info); - - return Request::sendMessage($data); - } -} diff --git a/src/Commands/AdminCommands/SendtoallCommand.php b/src/Commands/AdminCommands/SendtoallCommand.php deleted file mode 100644 index f92e4fb0a..000000000 --- a/src/Commands/AdminCommands/SendtoallCommand.php +++ /dev/null @@ -1,113 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Commands\AdminCommands; - -use Longman\TelegramBot\Commands\AdminCommand; -use Longman\TelegramBot\Entities\Message; -use Longman\TelegramBot\Entities\ServerResponse; -use Longman\TelegramBot\Exception\TelegramException; -use Longman\TelegramBot\Request; - -/** - * Admin "/sendtoall" command - */ -class SendtoallCommand extends AdminCommand -{ - /** - * @var string - */ - protected $name = 'sendtoall'; - - /** - * @var string - */ - protected $description = 'Send the message to all of the bot\'s users'; - - /** - * @var string - */ - protected $usage = '/sendtoall '; - - /** - * @var string - */ - protected $version = '1.5.0'; - - /** - * @var bool - */ - protected $need_mysql = true; - - /** - * Execute command - * - * @return ServerResponse - * @throws TelegramException - */ - public function execute(): ServerResponse - { - $text = $this->getMessage()->getText(true); - - if ($text === '') { - return $this->replyToChat('Usage: ' . $this->getUsage()); - } - - /** @var ServerResponse[] $results */ - $results = Request::sendToActiveChats( - 'sendMessage', //callback function to execute (see Request.php methods) - ['text' => $text], //Param to evaluate the request - [ - 'groups' => true, - 'supergroups' => true, - 'channels' => false, - 'users' => true, - ] - ); - - if (empty($results)) { - return $this->replyToChat('No users or chats found.'); - } - - $total = 0; - $failed = 0; - - $text = 'Message sent to:' . PHP_EOL; - - foreach ($results as $result) { - $name = ''; - $type = ''; - if ($result->isOk()) { - $status = '✔️'; - - /** @var Message $message */ - $message = $result->getResult(); - $chat = $message->getChat(); - if ($chat->isPrivateChat()) { - $name = $chat->getFirstName(); - $type = 'user'; - } else { - $name = $chat->getTitle(); - $type = 'chat'; - } - } else { - $status = '✖️'; - ++$failed; - } - ++$total; - - $text .= $total . ') ' . $status . ' ' . $type . ' ' . $name . PHP_EOL; - } - $text .= 'Delivered: ' . ($total - $failed) . '/' . $total . PHP_EOL; - - return $this->replyToChat($text); - } -} diff --git a/src/Commands/AdminCommands/SendtochannelCommand.php b/src/Commands/AdminCommands/SendtochannelCommand.php deleted file mode 100644 index 12fb82842..000000000 --- a/src/Commands/AdminCommands/SendtochannelCommand.php +++ /dev/null @@ -1,379 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Commands\AdminCommands; - -use Longman\TelegramBot\Commands\AdminCommand; -use Longman\TelegramBot\Conversation; -use Longman\TelegramBot\Entities\Chat; -use Longman\TelegramBot\Entities\Entity; -use Longman\TelegramBot\Entities\Keyboard; -use Longman\TelegramBot\Entities\Message; -use Longman\TelegramBot\Entities\ServerResponse; -use Longman\TelegramBot\Exception\TelegramException; -use Longman\TelegramBot\Request; - -class SendtochannelCommand extends AdminCommand -{ - /** - * @var string - */ - protected $name = 'sendtochannel'; - - /** - * @var string - */ - protected $description = 'Send message to a channel'; - - /** - * @var string - */ - protected $usage = '/sendtochannel '; - - /** - * @var string - */ - protected $version = '0.3.0'; - - /** - * @var bool - */ - protected $need_mysql = true; - - /** - * Conversation Object - * - * @var Conversation - */ - protected $conversation; - - /** - * Command execute method - * - * @return ServerResponse|mixed - * @throws TelegramException - */ - public function execute(): ServerResponse - { - $message = $this->getMessage() ?: $this->getEditedMessage() ?: $this->getChannelPost() ?: $this->getEditedChannelPost(); - $chat_id = $message->getChat()->getId(); - $user_id = $message->getFrom()->getId(); - - $type = $message->getType(); - // 'Cast' the command type to message to protect the machine state - // if the command is recalled when the conversation is already started - in_array($type, ['command', 'text'], true) && $type = 'message'; - - $text = trim($message->getText(true)); - $text_yes_or_no = ($text === 'Yes' || $text === 'No'); - - $data = [ - 'chat_id' => $chat_id, - ]; - - // Conversation - $this->conversation = new Conversation($user_id, $chat_id, $this->getName()); - - $notes = &$this->conversation->notes; - !is_array($notes) && $notes = []; - - $channels = (array) $this->getConfig('your_channel'); - if (isset($notes['state'])) { - $state = $notes['state']; - } else { - $state = (count($channels) === 0) ? -1 : 0; - $notes['last_message_id'] = $message->getMessageId(); - } - - $yes_no_keyboard = new Keyboard( - [ - 'keyboard' => [['Yes', 'No']], - 'resize_keyboard' => true, - 'one_time_keyboard' => true, - 'selective' => true, - ] - ); - - switch ($state) { - case -1: - // getConfig has not been configured asking for channel to administer - if ($type !== 'message' || $text === '') { - $notes['state'] = -1; - $this->conversation->update(); - - $result = $this->replyToChat( - 'Insert the channel name or ID (_@yourchannel_ or _-12345_)', - [ - 'parse_mode' => 'markdown', - 'reply_markup' => Keyboard::remove(['selective' => true]), - ] - ); - - break; - } - $notes['channel'] = $text; - $notes['last_message_id'] = $message->getMessageId(); - // Jump to state 1 - goto insert; - - // no break - default: - case 0: - // getConfig has been configured choose channel - if ($type !== 'message' || $text === '') { - $notes['state'] = 0; - $this->conversation->update(); - - $keyboard = array_map(function ($channel) { - return [$channel]; - }, $channels); - - $result = $this->replyToChat( - 'Choose a channel from the keyboard' . PHP_EOL . - '_or_ insert the channel name or ID (_@yourchannel_ or _-12345_)', - [ - 'parse_mode' => 'markdown', - 'reply_markup' => new Keyboard( - [ - 'keyboard' => $keyboard, - 'resize_keyboard' => true, - 'one_time_keyboard' => true, - 'selective' => true, - ] - ), - ] - ); - break; - } - $notes['channel'] = $text; - $notes['last_message_id'] = $message->getMessageId(); - - // no break - case 1: - insert: - if (($type === 'message' && $text === '') || $notes['last_message_id'] === $message->getMessageId()) { - $notes['state'] = 1; - $this->conversation->update(); - - $result = $this->replyToChat( - 'Insert the content you want to share: text, photo, audio...', - ['reply_markup' => Keyboard::remove(['selective' => true])] - ); - break; - } - $notes['last_message_id'] = $message->getMessageId(); - $notes['message'] = $message->getRawData(); - $notes['message_type'] = $type; - // no break - case 2: - if (!$text_yes_or_no || $notes['last_message_id'] === $message->getMessageId()) { - $notes['state'] = 2; - $this->conversation->update(); - - // Grab any existing caption. - if ($caption = $message->getCaption()) { - $notes['caption'] = $caption; - $text = 'No'; - } elseif (in_array($notes['message_type'], ['video', 'photo'], true)) { - $text = 'Would you like to insert a caption?'; - if (!$text_yes_or_no && $notes['last_message_id'] !== $message->getMessageId()) { - $text .= PHP_EOL . 'Type Yes or No'; - } - - $result = $this->replyToChat( - $text, - ['reply_markup' => $yes_no_keyboard] - ); - break; - } - } - $notes['set_caption'] = ($text === 'Yes'); - $notes['last_message_id'] = $message->getMessageId(); - // no break - case 3: - if ($notes['set_caption'] && ($notes['last_message_id'] === $message->getMessageId() || $type !== 'message')) { - $notes['state'] = 3; - $this->conversation->update(); - - $result = $this->replyToChat( - 'Insert caption:', - ['reply_markup' => Keyboard::remove(['selective' => true])] - ); - break; - } - $notes['last_message_id'] = $message->getMessageId(); - if (isset($notes['caption'])) { - // If caption has already been send with the file, no need to ask for it. - $notes['set_caption'] = true; - } else { - $notes['caption'] = $text; - } - // no break - case 4: - if (!$text_yes_or_no || $notes['last_message_id'] === $message->getMessageId()) { - $notes['state'] = 4; - $this->conversation->update(); - - $result = $this->replyToChat('Message will look like this:'); - - if ($notes['message_type'] !== 'command') { - if ($notes['set_caption']) { - $data['caption'] = $notes['caption']; - } - $this->sendBack(new Message($notes['message'], $this->telegram->getBotUsername()), $data); - - $data['reply_markup'] = $yes_no_keyboard; - - $data['text'] = 'Would you like to post it?'; - if (!$text_yes_or_no && $notes['last_message_id'] !== $message->getMessageId()) { - $data['text'] .= PHP_EOL . 'Type Yes or No'; - } - $result = Request::sendMessage($data); - } - break; - } - - $notes['post_message'] = ($text === 'Yes'); - $notes['last_message_id'] = $message->getMessageId(); - // no break - case 5: - $data['reply_markup'] = Keyboard::remove(['selective' => true]); - - if ($notes['post_message']) { - $data['parse_mode'] = 'markdown'; - $data['text'] = $this->publish( - new Message($notes['message'], $this->telegram->getBotUsername()), - $notes['channel'], - $notes['caption'] - ); - } else { - $data['text'] = 'Aborted by user, message not sent..'; - } - - $this->conversation->stop(); - $result = Request::sendMessage($data); - } - - return $result; - } - - /** - * SendBack - * - * Received a message, the bot can send a copy of it to another chat/channel. - * You don't have to care about the type of the message, the function detect it and use the proper - * REQUEST:: function to send it. - * $data include all the var that you need to send the message to the proper chat - * - * @todo This method will be moved to a higher level maybe in AdminCommand or Command - * @todo Looking for a more significant name - * - * @param Message $message - * @param array $data - * - * @return ServerResponse - * @throws TelegramException - */ - protected function sendBack(Message $message, array $data): ServerResponse - { - $type = $message->getType(); - in_array($type, ['command', 'text'], true) && $type = 'message'; - - if ($type === 'message') { - $data['text'] = $message->getText(true); - } elseif ($type === 'audio') { - $data['audio'] = $message->getAudio()->getFileId(); - $data['duration'] = $message->getAudio()->getDuration(); - $data['performer'] = $message->getAudio()->getPerformer(); - $data['title'] = $message->getAudio()->getTitle(); - } elseif ($type === 'document') { - $data['document'] = $message->getDocument()->getFileId(); - } elseif ($type === 'photo') { - $data['photo'] = $message->getPhoto()[0]->getFileId(); - } elseif ($type === 'sticker') { - $data['sticker'] = $message->getSticker()->getFileId(); - } elseif ($type === 'video') { - $data['video'] = $message->getVideo()->getFileId(); - } elseif ($type === 'voice') { - $data['voice'] = $message->getVoice()->getFileId(); - } elseif ($type === 'location') { - $data['latitude'] = $message->getLocation()->getLatitude(); - $data['longitude'] = $message->getLocation()->getLongitude(); - } - - return Request::send('send' . ucfirst($type), $data); - } - - /** - * Publish a message to a channel and return success or failure message in markdown format - * - * @param Message $message - * @param string|int $channel_id - * @param string|null $caption - * - * @return string - * @throws TelegramException - */ - protected function publish(Message $message, $channel_id, $caption = null): string - { - $res = $this->sendBack($message, [ - 'chat_id' => $channel_id, - 'caption' => $caption, - ]); - - if ($res->isOk()) { - /** @var Chat $channel */ - $channel = $res->getResult()->getChat(); - $escaped_username = $channel->getUsername() ? Entity::escapeMarkdown($channel->getUsername()) : ''; - - $response = sprintf( - 'Message successfully sent to *%s*%s', - filter_var($channel->getTitle(), FILTER_SANITIZE_SPECIAL_CHARS), - $escaped_username ? " (@{$escaped_username})" : '' - ); - } else { - $escaped_username = Entity::escapeMarkdown($channel_id); - $response = "Message not sent to *{$escaped_username}*" . PHP_EOL . - '- Does the channel exist?' . PHP_EOL . - '- Is the bot an admin of the channel?'; - } - - return $response; - } - - /** - * Execute without db - * - * @todo Why send just to the first found channel? - * - * @return mixed - * @throws TelegramException - */ - public function executeNoDb(): ServerResponse - { - $message = $this->getMessage() ?: $this->getEditedMessage() ?: $this->getChannelPost() ?: $this->getEditedChannelPost(); - $text = trim($message->getText(true)); - - if ($text === '') { - return $this->replyToChat('Usage: ' . $this->getUsage()); - } - - $channels = array_filter((array) $this->getConfig('your_channel')); - if (empty($channels)) { - return $this->replyToChat('No channels defined in the command config!'); - } - - return $this->replyToChat($this->publish( - new Message($message->getRawData(), $this->telegram->getBotUsername()), - reset($channels) - ), ['parse_mode' => 'markdown']); - } -} diff --git a/src/Commands/AdminCommands/WhoisCommand.php b/src/Commands/AdminCommands/WhoisCommand.php deleted file mode 100644 index 033dbc87a..000000000 --- a/src/Commands/AdminCommands/WhoisCommand.php +++ /dev/null @@ -1,190 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - * - * Written by Jack'lul - */ - -namespace Longman\TelegramBot\Commands\AdminCommands; - -use Longman\TelegramBot\Commands\AdminCommand; -use Longman\TelegramBot\DB; -use Longman\TelegramBot\Entities\Chat; -use Longman\TelegramBot\Entities\PhotoSize; -use Longman\TelegramBot\Entities\ServerResponse; -use Longman\TelegramBot\Entities\UserProfilePhotos; -use Longman\TelegramBot\Exception\TelegramException; -use Longman\TelegramBot\Request; - -/** - * Admin "/whois" command - */ -class WhoisCommand extends AdminCommand -{ - /** - * @var string - */ - protected $name = 'whois'; - - /** - * @var string - */ - protected $description = 'Lookup user or group info'; - - /** - * @var string - */ - protected $usage = '/whois or /whois '; - - /** - * @var string - */ - protected $version = '1.3.0'; - - /** - * @var bool - */ - protected $need_mysql = true; - - /** - * Command execute method - * - * @return ServerResponse - * @throws TelegramException - */ - public function execute(): ServerResponse - { - $message = $this->getMessage() ?: $this->getEditedMessage() ?: $this->getChannelPost() ?: $this->getEditedChannelPost(); - - $chat_id = $message->getChat()->getId(); - $command = $message->getCommand(); - $text = trim($message->getText(true)); - - $data = ['chat_id' => $chat_id]; - - //No point in replying to messages in private chats - if (!$message->getChat()->isPrivateChat()) { - $data['reply_to_message_id'] = $message->getMessageId(); - } - - if ($command !== 'whois') { - $text = substr($command, 5); - - //We need that '-' now, bring it back - if (strpos($text, 'g') === 0) { - $text = str_replace('g', '-', $text); - } - } - - if ($text === '') { - $text = 'Provide the id to lookup: /whois '; - } else { - $user_id = $text; - $chat = null; - $created_at = null; - $updated_at = null; - $result = null; - - if (is_numeric($text)) { - $results = DB::selectChats([ - 'groups' => true, - 'supergroups' => true, - 'channels' => true, - 'users' => true, - 'chat_id' => $user_id, //Specific chat_id to select - ]); - - if (!empty($results)) { - $result = reset($results); - } - } else { - $results = DB::selectChats([ - 'groups' => true, - 'supergroups' => true, - 'channels' => true, - 'users' => true, - 'text' => $text //Text to search in user/group name - ]); - - if (is_array($results) && count($results) === 1) { - $result = reset($results); - } - } - - if (is_array($result)) { - $result['id'] = $result['chat_id']; - $result['username'] = $result['chat_username']; - $chat = new Chat($result); - - $user_id = $result['id']; - $created_at = $result['chat_created_at']; - $updated_at = $result['chat_updated_at']; - $old_id = $result['old_id']; - } - - if ($chat !== null) { - if ($chat->isPrivateChat()) { - $text = 'User ID: ' . $user_id . PHP_EOL; - $text .= 'Name: ' . $chat->getFirstName() . ' ' . $chat->getLastName() . PHP_EOL; - - $username = $chat->getUsername(); - if ($username !== null && $username !== '') { - $text .= 'Username: @' . $username . PHP_EOL; - } - - $text .= 'First time seen: ' . $created_at . PHP_EOL; - $text .= 'Last activity: ' . $updated_at . PHP_EOL; - - //Code from Whoami command - $limit = 10; - $offset = null; - $response = Request::getUserProfilePhotos( - [ - 'user_id' => $user_id, - 'limit' => $limit, - 'offset' => $offset, - ] - ); - - if ($response->isOk()) { - /** @var UserProfilePhotos $user_profile_photos */ - $user_profile_photos = $response->getResult(); - - if ($user_profile_photos->getTotalCount() > 0) { - $photos = $user_profile_photos->getPhotos(); - - /** @var PhotoSize $photo */ - $photo = $photos[0][2]; - $file_id = $photo->getFileId(); - - $data['photo'] = $file_id; - $data['caption'] = $text; - - return Request::sendPhoto($data); - } - } - } elseif ($chat->isGroupChat()) { - $text = 'Chat ID: ' . $user_id . (!empty($old_id) ? ' (previously: ' . $old_id . ')' : '') . PHP_EOL; - $text .= 'Type: ' . ucfirst($chat->getType()) . PHP_EOL; - $text .= 'Title: ' . $chat->getTitle() . PHP_EOL; - $text .= 'First time added to group: ' . $created_at . PHP_EOL; - $text .= 'Last activity: ' . $updated_at . PHP_EOL; - } - } elseif (is_array($results) && count($results) > 1) { - $text = 'Multiple chats matched!'; - } else { - $text = 'Chat not found!'; - } - } - - $data['text'] = $text; - - return Request::sendMessage($data); - } -} diff --git a/src/Commands/Command.php b/src/Commands/Command.php deleted file mode 100644 index 0d5325086..000000000 --- a/src/Commands/Command.php +++ /dev/null @@ -1,456 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Commands; - -use Longman\TelegramBot\DB; -use Longman\TelegramBot\Entities\CallbackQuery; -use Longman\TelegramBot\Entities\ChatJoinRequest; -use Longman\TelegramBot\Entities\ChatMemberUpdated; -use Longman\TelegramBot\Entities\ChosenInlineResult; -use Longman\TelegramBot\Entities\InlineQuery; -use Longman\TelegramBot\Entities\Message; -use Longman\TelegramBot\Entities\Payments\PreCheckoutQuery; -use Longman\TelegramBot\Entities\Payments\ShippingQuery; -use Longman\TelegramBot\Entities\Poll; -use Longman\TelegramBot\Entities\PollAnswer; -use Longman\TelegramBot\Entities\ServerResponse; -use Longman\TelegramBot\Entities\Update; -use Longman\TelegramBot\Exception\TelegramException; -use Longman\TelegramBot\Request; -use Longman\TelegramBot\Telegram; - -/** - * Class Command - * - * Base class for commands. It includes some helper methods that can fetch data directly from the Update object. - * - * @method Message getMessage() Optional. New incoming message of any kind — text, photo, sticker, etc. - * @method Message getEditedMessage() Optional. New version of a message that is known to the bot and was edited - * @method Message getChannelPost() Optional. New post in the channel, can be any kind — text, photo, sticker, etc. - * @method Message getEditedChannelPost() Optional. New version of a post in the channel that is known to the bot and was edited - * @method InlineQuery getInlineQuery() Optional. New incoming inline query - * @method ChosenInlineResult getChosenInlineResult() Optional. The result of an inline query that was chosen by a user and sent to their chat partner. - * @method CallbackQuery getCallbackQuery() Optional. New incoming callback query - * @method ShippingQuery getShippingQuery() Optional. New incoming shipping query. Only for invoices with flexible price - * @method PreCheckoutQuery getPreCheckoutQuery() Optional. New incoming pre-checkout query. Contains full information about checkout - * @method Poll getPoll() Optional. New poll state. Bots receive only updates about polls, which are sent or stopped by the bot - * @method PollAnswer getPollAnswer() Optional. A user changed their answer in a non-anonymous poll. Bots receive new votes only in polls that were sent by the bot itself. - * @method ChatMemberUpdated getMyChatMember() Optional. The bot's chat member status was updated in a chat. For private chats, this update is received only when the bot is blocked or unblocked by the user. - * @method ChatMemberUpdated getChatMember() Optional. A chat member's status was updated in a chat. The bot must be an administrator in the chat and must explicitly specify “chat_member” in the list of allowed_updates to receive these updates. - * @method ChatJoinRequest getChatJoinRequest() Optional. A request to join the chat has been sent. The bot must have the can_invite_users administrator right in the chat to receive these updates. - */ -abstract class Command -{ - /** - * Auth level for user commands - */ - public const AUTH_USER = 'User'; - - /** - * Auth level for system commands - */ - public const AUTH_SYSTEM = 'System'; - - /** - * Auth level for admin commands - */ - public const AUTH_ADMIN = 'Admin'; - - /** - * Telegram object - * - * @var Telegram - */ - protected $telegram; - - /** - * Update object - * - * @var Update - */ - protected $update; - - /** - * Name - * - * @var string - */ - protected $name = ''; - - /** - * Description - * - * @var string - */ - protected $description = 'Command description'; - - /** - * Usage - * - * @var string - */ - protected $usage = 'Command usage'; - - /** - * Show in Help - * - * @var bool - */ - protected $show_in_help = true; - - /** - * Version - * - * @var string - */ - protected $version = '1.0.0'; - - /** - * If this command is enabled - * - * @var bool - */ - protected $enabled = true; - - /** - * If this command needs mysql - * - * @var bool - */ - protected $need_mysql = false; - - /** - * Make sure this command only executes on a private chat. - * - * @var bool - */ - protected $private_only = false; - - /** - * Command config - * - * @var array - */ - protected $config = []; - - /** - * Constructor - * - * @param Telegram $telegram - * @param Update|null $update - */ - public function __construct(Telegram $telegram, ?Update $update = null) - { - $this->telegram = $telegram; - if ($update !== null) { - $this->setUpdate($update); - } - $this->config = $telegram->getCommandConfig($this->name); - } - - /** - * Set update object - * - * @param Update $update - * - * @return Command - */ - public function setUpdate(Update $update): Command - { - $this->update = $update; - - return $this; - } - - /** - * Pre-execute command - * - * @return ServerResponse - * @throws TelegramException - */ - public function preExecute(): ServerResponse - { - if ($this->need_mysql && !($this->telegram->isDbEnabled() && DB::isDbConnected())) { - return $this->executeNoDb(); - } - - if ($this->isPrivateOnly() && $this->removeNonPrivateMessage()) { - $message = $this->getMessage() ?: $this->getEditedMessage() ?: $this->getChannelPost() ?: $this->getEditedChannelPost(); - - if ($user = $message->getFrom()) { - return Request::sendMessage([ - 'chat_id' => $user->getId(), - 'parse_mode' => 'Markdown', - 'text' => sprintf( - "/%s command is only available in a private chat.\n(`%s`)", - $this->getName(), - $message->getText() - ), - ]); - } - - return Request::emptyResponse(); - } - - return $this->execute(); - } - - /** - * Execute command - * - * @return ServerResponse - * @throws TelegramException - */ - abstract public function execute(): ServerResponse; - - /** - * Execution if MySQL is required but not available - * - * @return ServerResponse - * @throws TelegramException - */ - public function executeNoDb(): ServerResponse - { - return $this->replyToChat('Sorry no database connection, unable to execute "' . $this->name . '" command.'); - } - - /** - * Get update object - * - * @return Update|null - */ - public function getUpdate(): ?Update - { - return $this->update; - } - - /** - * Relay any non-existing function calls to Update object. - * - * This is purely a helper method to make requests from within execute() method easier. - * - * @param string $name - * @param array $arguments - * - * @return Command - */ - public function __call(string $name, array $arguments) - { - if ($this->update === null) { - return null; - } - return call_user_func_array([$this->update, $name], $arguments); - } - - /** - * Get command config - * - * Look for config $name if found return it, if not return $default. - * If $name is not set return all set config. - * - * @param string|null $name - * @param mixed $default - * - * @return mixed - */ - public function getConfig(?string $name = null, $default = null) - { - if ($name === null) { - return $this->config; - } - return $this->config[$name] ?? $default; - } - - /** - * Get telegram object - * - * @return Telegram - */ - public function getTelegram(): Telegram - { - return $this->telegram; - } - - /** - * Get usage - * - * @return string - */ - public function getUsage(): string - { - return $this->usage; - } - - /** - * Get version - * - * @return string - */ - public function getVersion(): string - { - return $this->version; - } - - /** - * Get description - * - * @return string - */ - public function getDescription(): string - { - return $this->description; - } - - /** - * Get name - * - * @return string - */ - public function getName(): string - { - return $this->name; - } - - /** - * Get Show in Help - * - * @return bool - */ - public function showInHelp(): bool - { - return $this->show_in_help; - } - - /** - * Check if command is enabled - * - * @return bool - */ - public function isEnabled(): bool - { - return $this->enabled; - } - - /** - * If this command is intended for private chats only. - * - * @return bool - */ - public function isPrivateOnly(): bool - { - return $this->private_only; - } - - /** - * If this is a SystemCommand - * - * @return bool - */ - public function isSystemCommand(): bool - { - return ($this instanceof SystemCommand); - } - - /** - * If this is an AdminCommand - * - * @return bool - */ - public function isAdminCommand(): bool - { - return ($this instanceof AdminCommand); - } - - /** - * If this is a UserCommand - * - * @return bool - */ - public function isUserCommand(): bool - { - return ($this instanceof UserCommand); - } - - /** - * Delete the current message if it has been called in a non-private chat. - * - * @return bool - */ - protected function removeNonPrivateMessage(): bool - { - $message = $this->getMessage() ?: $this->getEditedMessage(); - - if ($message) { - $chat = $message->getChat(); - - if (!$chat->isPrivateChat()) { - // Delete the falsely called command message. - Request::deleteMessage([ - 'chat_id' => $chat->getId(), - 'message_id' => $message->getMessageId(), - ]); - - return true; - } - } - - return false; - } - - /** - * Helper to reply to a chat directly. - * - * @param string $text - * @param array $data - * - * @return ServerResponse - * @throws TelegramException - */ - public function replyToChat(string $text, array $data = []): ServerResponse - { - if ($message = $this->getMessage() ?: $this->getEditedMessage() ?: $this->getChannelPost() ?: $this->getEditedChannelPost()) { - $reply = [ - 'chat_id' => $message->getChat()->getId(), - 'text' => $text, - ]; - - if ($message->getIsTopicMessage()) { - $reply['message_thread_id'] = $message->getMessageThreadId(); - } - - return Request::sendMessage(array_merge($reply, $data)); - } - - return Request::emptyResponse(); - } - - /** - * Helper to reply to a user directly. - * - * @param string $text - * @param array $data - * - * @return ServerResponse - * @throws TelegramException - */ - public function replyToUser(string $text, array $data = []): ServerResponse - { - if ($message = $this->getMessage() ?: $this->getEditedMessage()) { - return Request::sendMessage(array_merge([ - 'chat_id' => $message->getFrom()->getId(), - 'text' => $text, - ], $data)); - } - - return Request::emptyResponse(); - } -} diff --git a/src/Commands/SystemCommand.php b/src/Commands/SystemCommand.php deleted file mode 100644 index 8bdebf9c5..000000000 --- a/src/Commands/SystemCommand.php +++ /dev/null @@ -1,124 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Commands; - -use Longman\TelegramBot\Conversation; -use Longman\TelegramBot\Entities\ServerResponse; -use Longman\TelegramBot\Exception\TelegramException; -use Longman\TelegramBot\Request; - -abstract class SystemCommand extends Command -{ - /** - * @var bool Try to execute any deprecated system command. - */ - public static $execute_deprecated = false; - - /** - * @{inheritdoc} - * - * Set to empty string to disallow users calling system commands. - */ - protected $usage = ''; - - /** - * A system command just executes - * - * Although system commands should just work and return a successful ServerResponse, - * each system command can override this method to add custom functionality. - * - * @return ServerResponse - */ - public function execute(): ServerResponse - { - // System command, return empty ServerResponse by default - return Request::emptyResponse(); - } - - /** - * Method to execute any active conversation. - * - * @return ServerResponse|null - * @throws TelegramException - * @internal - */ - protected function executeActiveConversation(): ?ServerResponse - { - $message = $this->getMessage() ?: $this->getEditedMessage() ?: $this->getChannelPost() ?: $this->getEditedChannelPost(); - if ($message === null) { - return null; - } - - $user = $message->getFrom(); - $chat = $message->getChat(); - if ($user === null || $chat === null) { - return null; - } - - // If a conversation is busy, execute the conversation command after handling the message. - $conversation = new Conversation($user->getId(), $chat->getId()); - - // Fetch conversation command if it exists and execute it. - if ($conversation->exists() && ($command = $conversation->getCommand())) { - return $this->getTelegram()->executeCommand($command); - } - - return null; - } - - /** - * BC helper method to execute deprecated system commands. - * - * @return ServerResponse|null - * @throws TelegramException - * @internal - */ - protected function executeDeprecatedSystemCommand(): ?ServerResponse - { - $message = $this->getMessage() ?: $this->getEditedMessage() ?: $this->getChannelPost() ?: $this->getEditedChannelPost(); - if ($message === null) { - return null; - } - - // List of service messages previously handled internally. - $service_message_getters = [ - 'newchatmembers' => 'getNewChatMembers', - 'leftchatmember' => 'getLeftChatMember', - 'newchattitle' => 'getNewChatTitle', - 'newchatphoto' => 'getNewChatPhoto', - 'deletechatphoto' => 'getDeleteChatPhoto', - 'groupchatcreated' => 'getGroupChatCreated', - 'supergroupchatcreated' => 'getSupergroupChatCreated', - 'channelchatcreated' => 'getChannelChatCreated', - 'migratefromchatid' => 'getMigrateFromChatId', - 'migratetochatid' => 'getMigrateToChatId', - 'pinnedmessage' => 'getPinnedMessage', - 'successfulpayment' => 'getSuccessfulPayment', - ]; - - foreach ($service_message_getters as $command => $service_message_getter) { - // Let's check if this message is a service message. - if ($message->$service_message_getter() === null) { - continue; - } - - // Make sure the command exists otherwise GenericCommand would be executed. - if ($this->getTelegram()->getCommandObject($command) === null) { - break; - } - - return $this->getTelegram()->executeCommand($command); - } - - return null; - } -} diff --git a/src/Commands/SystemCommands/CallbackqueryCommand.php b/src/Commands/SystemCommands/CallbackqueryCommand.php deleted file mode 100644 index b580ac1ae..000000000 --- a/src/Commands/SystemCommands/CallbackqueryCommand.php +++ /dev/null @@ -1,74 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Commands\SystemCommands; - -use Longman\TelegramBot\Commands\SystemCommand; -use Longman\TelegramBot\Entities\ServerResponse; - -/** - * Callback query command - */ -class CallbackqueryCommand extends SystemCommand -{ - /** - * @var callable[] - */ - protected static $callbacks = []; - - /** - * @var string - */ - protected $name = 'callbackquery'; - - /** - * @var string - */ - protected $description = 'Reply to callback query'; - - /** - * @var string - */ - protected $version = '1.0.0'; - - /** - * Command execute method - * - * @return ServerResponse - */ - public function execute(): ServerResponse - { - //$callback_query = $this->getCallbackQuery(); - //$user_id = $callback_query->getFrom()->getId(); - //$query_id = $callback_query->getId(); - //$query_data = $callback_query->getData(); - - $answer = null; - $callback_query = $this->getCallbackQuery(); - - // Call all registered callbacks. - foreach (self::$callbacks as $callback) { - $answer = $callback($callback_query); - } - - return ($answer instanceof ServerResponse) ? $answer : $callback_query->answer(); - } - - /** - * Add a new callback handler for callback queries. - * - * @param $callback - */ - public static function addCallbackHandler($callback): void - { - self::$callbacks[] = $callback; - } -} diff --git a/src/Commands/SystemCommands/GenericCommand.php b/src/Commands/SystemCommands/GenericCommand.php deleted file mode 100644 index d7cae29eb..000000000 --- a/src/Commands/SystemCommands/GenericCommand.php +++ /dev/null @@ -1,35 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Commands\SystemCommands; - -use Longman\TelegramBot\Commands\SystemCommand; - -/** - * Generic command - */ -class GenericCommand extends SystemCommand -{ - /** - * @var string - */ - protected $name = 'generic'; - - /** - * @var string - */ - protected $description = 'Handles generic commands or is executed by default when a command is not found'; - - /** - * @var string - */ - protected $version = '1.1.0'; -} diff --git a/src/Commands/SystemCommands/GenericmessageCommand.php b/src/Commands/SystemCommands/GenericmessageCommand.php deleted file mode 100644 index 9ac31957c..000000000 --- a/src/Commands/SystemCommands/GenericmessageCommand.php +++ /dev/null @@ -1,81 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Commands\SystemCommands; - -use Longman\TelegramBot\Commands\SystemCommand; -use Longman\TelegramBot\Entities\ServerResponse; -use Longman\TelegramBot\Exception\TelegramException; -use Longman\TelegramBot\Request; -use Longman\TelegramBot\Telegram; - -/** - * Generic message command - */ -class GenericmessageCommand extends SystemCommand -{ - /** - * @var string - */ - protected $name = Telegram::GENERIC_MESSAGE_COMMAND; - - /** - * @var string - */ - protected $description = 'Handle generic message'; - - /** - * @var string - */ - protected $version = '1.2.0'; - - /** - * @var bool - */ - protected $need_mysql = true; - - /** - * Execution if MySQL is required but not available - * - * @return ServerResponse - * @throws TelegramException - */ - public function executeNoDb(): ServerResponse - { - // Try to execute any deprecated system commands. - if (self::$execute_deprecated && $deprecated_system_command_response = $this->executeDeprecatedSystemCommand()) { - return $deprecated_system_command_response; - } - - return Request::emptyResponse(); - } - - /** - * Execute command - * - * @return ServerResponse - * @throws TelegramException - */ - public function execute(): ServerResponse - { - // Try to continue any active conversation. - if ($active_conversation_response = $this->executeActiveConversation()) { - return $active_conversation_response; - } - - // Try to execute any deprecated system commands. - if (self::$execute_deprecated && $deprecated_system_command_response = $this->executeDeprecatedSystemCommand()) { - return $deprecated_system_command_response; - } - - return Request::emptyResponse(); - } -} diff --git a/src/Commands/SystemCommands/InlinequeryCommand.php b/src/Commands/SystemCommands/InlinequeryCommand.php deleted file mode 100644 index 68c1757c7..000000000 --- a/src/Commands/SystemCommands/InlinequeryCommand.php +++ /dev/null @@ -1,50 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Commands\SystemCommands; - -use Longman\TelegramBot\Commands\SystemCommand; -use Longman\TelegramBot\Entities\ServerResponse; - -/** - * Inline query command - */ -class InlinequeryCommand extends SystemCommand -{ - /** - * @var string - */ - protected $name = 'inlinequery'; - - /** - * @var string - */ - protected $description = 'Reply to inline query'; - - /** - * @var string - */ - protected $version = '1.0.1'; - - /** - * Command execute method - * - * @return mixed - */ - public function execute(): ServerResponse - { - //$inline_query = $this->getInlineQuery(); - //$user_id = $inline_query->getFrom()->getId(); - //$query = $inline_query->getQuery(); - - return $this->getInlineQuery()->answer([]); - } -} diff --git a/src/Commands/UserCommand.php b/src/Commands/UserCommand.php deleted file mode 100644 index 2fba11236..000000000 --- a/src/Commands/UserCommand.php +++ /dev/null @@ -1,17 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Commands; - -abstract class UserCommand extends Command -{ - -} diff --git a/src/Commands/UserCommands/StartCommand.php b/src/Commands/UserCommands/StartCommand.php deleted file mode 100644 index 3f3d4b88f..000000000 --- a/src/Commands/UserCommands/StartCommand.php +++ /dev/null @@ -1,56 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Commands\UserCommands; - -use Longman\TelegramBot\Commands\UserCommand; -use Longman\TelegramBot\Entities\ServerResponse; -use Longman\TelegramBot\Request; - -/** - * Start command - */ -class StartCommand extends UserCommand -{ - /** - * @var string - */ - protected $name = 'start'; - - /** - * @var string - */ - protected $description = 'Start command'; - - /** - * @var string - */ - protected $usage = '/start'; - - /** - * @var string - */ - protected $version = '1.2.0'; - - /** - * Command execute method - * - * @return ServerResponse - */ - public function execute(): ServerResponse - { - //$message = $this->getMessage() ?: $this->getEditedMessage() ?: $this->getChannelPost() ?: $this->getEditedChannelPost(); - //$chat_id = $message->getChat()->getId(); - //$user_id = $message->getFrom()->getId(); - - return Request::emptyResponse(); - } -} diff --git a/src/Conversation.php b/src/Conversation.php deleted file mode 100644 index c9bdf76f7..000000000 --- a/src/Conversation.php +++ /dev/null @@ -1,264 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot; - -use Longman\TelegramBot\Exception\TelegramException; - -/** - * Class Conversation - * - * Only one conversation can be active at any one time. - * A conversation is directly linked to a user, chat and the command that is managing the conversation. - */ -class Conversation -{ - /** - * All information fetched from the database - * - * @var array|null - */ - protected $conversation; - - /** - * Notes stored inside the conversation - * - * @var mixed - */ - protected $protected_notes; - - /** - * Notes to be stored - * - * @var mixed - */ - public $notes; - - /** - * Telegram user id - * - * @var int - */ - protected $user_id; - - /** - * Telegram chat id - * - * @var int - */ - protected $chat_id; - - /** - * Command to be executed if the conversation is active - * - * @var string - */ - protected $command; - - /** - * Conversation constructor to initialize a new conversation - * - * @param int $user_id - * @param int $chat_id - * @param string $command - * - * @throws TelegramException - */ - public function __construct(int $user_id, int $chat_id, string $command = '') - { - $this->user_id = $user_id; - $this->chat_id = $chat_id; - $this->command = $command; - - //Try to load an existing conversation if possible - if (!$this->load() && $command !== '') { - //A new conversation start - $this->start(); - } - } - - /** - * Clear all conversation variables. - * - * @return bool Always return true, to allow this method in an if statement. - */ - protected function clear(): bool - { - $this->conversation = null; - $this->protected_notes = null; - $this->notes = null; - - return true; - } - - /** - * Load the conversation from the database - * - * @return bool - * @throws TelegramException - */ - protected function load(): bool - { - //Select an active conversation - $conversation = ConversationDB::selectConversation($this->user_id, $this->chat_id, 1); - if (isset($conversation[0])) { - //Pick only the first element - $this->conversation = $conversation[0]; - - //Load the command from the conversation if it hasn't been passed - $this->command = $this->command ?: $this->conversation['command']; - - if ($this->command !== $this->conversation['command']) { - $this->cancel(); - return false; - } - - //Load the conversation notes - $this->protected_notes = json_decode($this->conversation['notes'], true); - $this->notes = $this->protected_notes; - } - - return $this->exists(); - } - - /** - * Check if the conversation already exists - * - * @return bool - */ - public function exists(): bool - { - return $this->conversation !== null; - } - - /** - * Start a new conversation if the current command doesn't have one yet - * - * @return bool - * @throws TelegramException - */ - protected function start(): bool - { - if ( - $this->command - && !$this->exists() - && ConversationDB::insertConversation( - $this->user_id, - $this->chat_id, - $this->command - ) - ) { - return $this->load(); - } - - return false; - } - - /** - * Delete the current conversation - * - * Currently the Conversation is not deleted but just set to 'stopped' - * - * @return bool - * @throws TelegramException - */ - public function stop(): bool - { - return $this->updateStatus('stopped') && $this->clear(); - } - - /** - * Cancel the current conversation - * - * @return bool - * @throws TelegramException - */ - public function cancel(): bool - { - return $this->updateStatus('cancelled') && $this->clear(); - } - - /** - * Update the status of the current conversation - * - * @param string $status - * - * @return bool - * @throws TelegramException - */ - protected function updateStatus(string $status): bool - { - if ($this->exists()) { - $fields = ['status' => $status]; - $where = [ - 'id' => $this->conversation['id'], - 'status' => 'active', - 'user_id' => $this->user_id, - 'chat_id' => $this->chat_id, - ]; - if (ConversationDB::updateConversation($fields, $where)) { - return true; - } - } - - return false; - } - - /** - * Store the array/variable in the database with json_encode() function - * - * @return bool - * @throws TelegramException - */ - public function update(): bool - { - if ($this->exists()) { - $fields = ['notes' => json_encode($this->notes, JSON_UNESCAPED_UNICODE)]; - //I can update a conversation whatever the state is - $where = ['id' => $this->conversation['id']]; - if (ConversationDB::updateConversation($fields, $where)) { - return true; - } - } - - return false; - } - - /** - * Retrieve the command to execute from the conversation - * - * @return string - */ - public function getCommand(): string - { - return $this->command; - } - - /** - * Retrieve the user id - * - * @return int - */ - public function getUserId(): int - { - return $this->user_id; - } - - /** - * Retrieve the chat id - * - * @return int - */ - public function getChatId(): int - { - return $this->chat_id; - } -} diff --git a/src/ConversationDB.php b/src/ConversationDB.php deleted file mode 100644 index c0a568bf7..000000000 --- a/src/ConversationDB.php +++ /dev/null @@ -1,132 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot; - -use Exception; -use Longman\TelegramBot\Exception\TelegramException; -use PDO; - -class ConversationDB extends DB -{ - /** - * Initialize conversation table - */ - public static function initializeConversation(): void - { - if (!defined('TB_CONVERSATION')) { - define('TB_CONVERSATION', self::$table_prefix . 'conversation'); - } - } - - /** - * Select a conversation from the DB - * - * @param int $user_id - * @param int $chat_id - * @param int $limit - * - * @return array|bool - * @throws TelegramException - */ - public static function selectConversation(int $user_id, int $chat_id, $limit = 0) - { - if (!self::isDbConnected()) { - return false; - } - - try { - $sql = ' - SELECT * - FROM `' . TB_CONVERSATION . '` - WHERE `status` = :status - AND `chat_id` = :chat_id - AND `user_id` = :user_id - '; - - if ($limit > 0) { - $sql .= ' LIMIT :limit'; - } - - $sth = self::$pdo->prepare($sql); - - $sth->bindValue(':status', 'active'); - $sth->bindValue(':user_id', $user_id); - $sth->bindValue(':chat_id', $chat_id); - - if ($limit > 0) { - $sth->bindValue(':limit', $limit, PDO::PARAM_INT); - } - - $sth->execute(); - - return $sth->fetchAll(PDO::FETCH_ASSOC); - } catch (Exception $e) { - throw new TelegramException($e->getMessage()); - } - } - - /** - * Insert the conversation in the database - * - * @param int $user_id - * @param int $chat_id - * @param string $command - * - * @return bool - * @throws TelegramException - */ - public static function insertConversation(int $user_id, int $chat_id, string $command): bool - { - if (!self::isDbConnected()) { - return false; - } - - try { - $sth = self::$pdo->prepare('INSERT INTO `' . TB_CONVERSATION . '` - (`status`, `user_id`, `chat_id`, `command`, `notes`, `created_at`, `updated_at`) - VALUES - (:status, :user_id, :chat_id, :command, :notes, :created_at, :updated_at) - '); - - $date = self::getTimestamp(); - - $sth->bindValue(':status', 'active'); - $sth->bindValue(':command', $command); - $sth->bindValue(':user_id', $user_id); - $sth->bindValue(':chat_id', $chat_id); - $sth->bindValue(':notes', '[]'); - $sth->bindValue(':created_at', $date); - $sth->bindValue(':updated_at', $date); - - return $sth->execute(); - } catch (Exception $e) { - throw new TelegramException($e->getMessage()); - } - } - - /** - * Update a specific conversation - * - * @param array $fields_values - * @param array $where_fields_values - * - * @return bool - * @throws TelegramException - */ - public static function updateConversation(array $fields_values, array $where_fields_values): bool - { - // Auto update the update_at field. - $fields_values['updated_at'] = self::getTimestamp(); - - return self::update(TB_CONVERSATION, $fields_values, $where_fields_values); - } -} diff --git a/src/DB.php b/src/DB.php deleted file mode 100644 index cae89be1f..000000000 --- a/src/DB.php +++ /dev/null @@ -1,1725 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - * Written by Marco Boretto - */ - -namespace Longman\TelegramBot; - -use Longman\TelegramBot\Entities\CallbackQuery; -use Longman\TelegramBot\Entities\Chat; -use Longman\TelegramBot\Entities\ChatBoostRemoved; -use Longman\TelegramBot\Entities\ChatBoostUpdated; -use Longman\TelegramBot\Entities\ChatJoinRequest; -use Longman\TelegramBot\Entities\ChatMemberUpdated; -use Longman\TelegramBot\Entities\ChosenInlineResult; -use Longman\TelegramBot\Entities\InlineQuery; -use Longman\TelegramBot\Entities\Message; -use Longman\TelegramBot\Entities\MessageReactionCountUpdated; -use Longman\TelegramBot\Entities\MessageReactionUpdated; -use Longman\TelegramBot\Entities\Payments\PreCheckoutQuery; -use Longman\TelegramBot\Entities\Payments\ShippingQuery; -use Longman\TelegramBot\Entities\Poll; -use Longman\TelegramBot\Entities\PollAnswer; -use Longman\TelegramBot\Entities\Update; -use Longman\TelegramBot\Entities\User; -use Longman\TelegramBot\Exception\TelegramException; -use PDO; -use PDOException; - -class DB -{ - /** - * MySQL credentials - * - * @var array - */ - protected static $mysql_credentials = []; - - /** - * PDO object - * - * @var PDO - */ - protected static $pdo; - - /** - * Table prefix - * - * @var string - */ - protected static $table_prefix; - - /** - * Telegram class object - * - * @var Telegram - */ - protected static $telegram; - - /** - * Initialize - * - * @param array $credentials Database connection details - * @param Telegram $telegram Telegram object to connect with this object - * @param string $table_prefix Table prefix - * @param string $encoding Database character encoding - * - * @return PDO PDO database object - * @throws TelegramException - */ - public static function initialize( - array $credentials, - Telegram $telegram, - $table_prefix = '', - $encoding = 'utf8mb4', - ): PDO { - if (empty($credentials)) { - throw new TelegramException('MySQL credentials not provided!'); - } - if (isset($credentials['unix_socket'])) { - $dsn = 'mysql:unix_socket=' . $credentials['unix_socket']; - } else { - $dsn = 'mysql:host=' . $credentials['host']; - } - $dsn .= ';dbname=' . $credentials['database']; - - if (!empty($credentials['port'])) { - $dsn .= ';port=' . $credentials['port']; - } - - $options = [PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES ' . $encoding]; - try { - $pdo = new PDO($dsn, $credentials['user'], $credentials['password'], $options); - $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING); - } catch (PDOException $e) { - throw new TelegramException($e->getMessage()); - } - - self::$pdo = $pdo; - self::$telegram = $telegram; - self::$mysql_credentials = $credentials; - self::$table_prefix = $table_prefix; - - self::defineTables(); - - return self::$pdo; - } - - /** - * External Initialize - * - * Let you use the class with an external already existing Pdo Mysql connection. - * - * @param PDO $external_pdo_connection PDO database object - * @param Telegram $telegram Telegram object to connect with this object - * @param string $table_prefix Table prefix - * - * @return PDO PDO database object - * @throws TelegramException - */ - public static function externalInitialize( - PDO $external_pdo_connection, - Telegram $telegram, - string $table_prefix = '', - ): PDO { - if ($external_pdo_connection === null) { - throw new TelegramException('MySQL external connection not provided!'); - } - - self::$pdo = $external_pdo_connection; - self::$telegram = $telegram; - self::$mysql_credentials = []; - self::$table_prefix = $table_prefix; - - self::defineTables(); - - return self::$pdo; - } - - /** - * Define all the tables with the proper prefix - */ - protected static function defineTables(): void - { - $tables = [ - 'callback_query', - 'chat', - 'chat_boost_updated', - 'chat_boost_removed', - 'chat_join_request', - 'chat_member_updated', - 'chosen_inline_result', - 'edited_message', - 'inline_query', - 'message', - 'message_reaction', - 'message_reaction_count', - 'poll', - 'poll_answer', - 'pre_checkout_query', - 'request_limiter', - 'shipping_query', - 'telegram_update', - 'user', - 'user_chat', - ]; - foreach ($tables as $table) { - $table_name = 'TB_' . strtoupper($table); - if (!defined($table_name)) { - define($table_name, self::$table_prefix . $table); - } - } - } - - /** - * Check if database connection has been created - * - * @return bool - */ - public static function isDbConnected(): bool - { - return self::$pdo !== null; - } - - /** - * Get the PDO object of the connected database - * - * @return PDO|null - */ - public static function getPdo(): ?PDO - { - return self::$pdo; - } - - /** - * Fetch update(s) from DB - * - * @param int $limit Limit the number of updates to fetch - * @param string $id Check for unique update id - * - * @return array|bool Fetched data or false if not connected - * @throws TelegramException - */ - public static function selectTelegramUpdate(int $limit = 0, string $id = '') - { - if (!self::isDbConnected()) { - return false; - } - - try { - $sql = ' - SELECT `id` - FROM `' . TB_TELEGRAM_UPDATE . '` - '; - - if ($id !== '') { - $sql .= ' WHERE `id` = :id'; - } else { - $sql .= ' ORDER BY `id` DESC'; - } - - if ($limit > 0) { - $sql .= ' LIMIT :limit'; - } - - $sth = self::$pdo->prepare($sql); - - if ($limit > 0) { - $sth->bindValue(':limit', $limit, PDO::PARAM_INT); - } - if ($id !== '') { - $sth->bindValue(':id', $id); - } - - $sth->execute(); - - return $sth->fetchAll(PDO::FETCH_ASSOC); - } catch (PDOException $e) { - throw new TelegramException($e->getMessage()); - } - } - - /** - * Fetch message(s) from DB - * - * @param int $limit Limit the number of messages to fetch - * - * @return array|bool Fetched data or false if not connected - * @throws TelegramException - */ - public static function selectMessages(int $limit = 0) - { - if (!self::isDbConnected()) { - return false; - } - - try { - $sql = ' - SELECT * - FROM `' . TB_MESSAGE . '` - ORDER BY `id` DESC - '; - - if ($limit > 0) { - $sql .= ' LIMIT :limit'; - } - - $sth = self::$pdo->prepare($sql); - - if ($limit > 0) { - $sth->bindValue(':limit', $limit, PDO::PARAM_INT); - } - - $sth->execute(); - - return $sth->fetchAll(PDO::FETCH_ASSOC); - } catch (PDOException $e) { - throw new TelegramException($e->getMessage()); - } - } - - /** - * Convert from unix timestamp to timestamp - * - * @param ?int $unixtime Unix timestamp (if empty, current timestamp is used) - * - * @return string - */ - protected static function getTimestamp(?int $unixtime = null): string - { - return date('Y-m-d H:i:s', $unixtime ?? time()); - } - - /** - * Convert array of Entity items to a JSON array - * - * @param array $entities - * @param mixed $default - * - * @return mixed - * @todo Find a better way, as json_* functions are very heavy - * - */ - public static function entitiesArrayToJson(array $entities, $default = null) - { - if (empty($entities)) { - return $default; - } - - // Convert each Entity item into an object based on its JSON reflection - $json_entities = array_map(function ($entity) { - return json_decode($entity, true); - }, $entities); - - return json_encode($json_entities); - } - - /** - * Insert entry to telegram_update table - * - * @throws TelegramException - */ - protected static function insertTelegramUpdate( - int $update_id, - ?int $chat_id = null, - ?int $message_id = null, - ?int $edited_message_id = null, - ?int $channel_post_id = null, - ?int $edited_channel_post_id = null, - ?string $message_reaction_id = null, - ?string $message_reaction_count_id = null, - ?string $inline_query_id = null, - ?string $chosen_inline_result_id = null, - ?string $callback_query_id = null, - ?string $shipping_query_id = null, - ?string $pre_checkout_query_id = null, - ?string $poll_id = null, - ?string $poll_answer_poll_id = null, - ?string $my_chat_member_updated_id = null, - ?string $chat_member_updated_id = null, - ?string $chat_join_request_id = null, - ?string $chat_boost_updated_id = null, - ?string $chat_boost_removed_id = null, - ): ?bool { - if ($message_id === null && $edited_message_id === null && $channel_post_id === null && $edited_channel_post_id === null && $message_reaction_id === null && $message_reaction_count_id === null && $inline_query_id === null && $chosen_inline_result_id === null && $callback_query_id === null && $shipping_query_id === null && $pre_checkout_query_id === null && $poll_id === null && $poll_answer_poll_id === null && $my_chat_member_updated_id === null && $chat_member_updated_id === null && $chat_join_request_id === null && $chat_boost_updated_id === null && $chat_boost_removed_id === null) { - throw new TelegramException('message_id, edited_message_id, channel_post_id, edited_channel_post_id, message_reaction_id, message_reaction_count_id, inline_query_id, chosen_inline_result_id, callback_query_id, shipping_query_id, pre_checkout_query_id, poll_id, poll_answer_poll_id, my_chat_member_updated_id, chat_member_updated_id, chat_join_request_id, chat_boost_updated_id, chat_boost_removed_id are all null'); - } - - if (!self::isDbConnected()) { - return false; - } - - try { - $sth = self::$pdo->prepare(' - INSERT IGNORE INTO `' . TB_TELEGRAM_UPDATE . '` - ( - `id`, `chat_id`, `message_id`, `edited_message_id`, - `channel_post_id`, `edited_channel_post_id`, `message_reaction_id`, `message_reaction_count_id`, - `inline_query_id`, `chosen_inline_result_id`, - `callback_query_id`, `shipping_query_id`, `pre_checkout_query_id`, - `poll_id`, `poll_answer_poll_id`, `my_chat_member_updated_id`, `chat_member_updated_id`, - `chat_join_request_id`, `chat_boost_updated_id`, `chat_boost_removed_id` - ) VALUES ( - :id, :chat_id, :message_id, :edited_message_id, - :channel_post_id, :edited_channel_post_id, :message_reaction_id, :message_reaction_count_id, - :inline_query_id, :chosen_inline_result_id, - :callback_query_id, :shipping_query_id, :pre_checkout_query_id, - :poll_id, :poll_answer_poll_id, :my_chat_member_updated_id, :chat_member_updated_id, - :chat_join_request_id, :chat_boost_updated_id, :chat_boost_removed_id - ) - '); - - $sth->bindValue(':id', $update_id); - $sth->bindValue(':chat_id', $chat_id); - $sth->bindValue(':message_id', $message_id); - $sth->bindValue(':edited_message_id', $edited_message_id); - $sth->bindValue(':channel_post_id', $channel_post_id); - $sth->bindValue(':edited_channel_post_id', $edited_channel_post_id); - $sth->bindValue(':message_reaction_id', $message_reaction_id); - $sth->bindValue(':message_reaction_count_id', $message_reaction_count_id); - $sth->bindValue(':inline_query_id', $inline_query_id); - $sth->bindValue(':chosen_inline_result_id', $chosen_inline_result_id); - $sth->bindValue(':callback_query_id', $callback_query_id); - $sth->bindValue(':shipping_query_id', $shipping_query_id); - $sth->bindValue(':pre_checkout_query_id', $pre_checkout_query_id); - $sth->bindValue(':poll_id', $poll_id); - $sth->bindValue(':poll_answer_poll_id', $poll_answer_poll_id); - $sth->bindValue(':my_chat_member_updated_id', $my_chat_member_updated_id); - $sth->bindValue(':chat_member_updated_id', $chat_member_updated_id); - $sth->bindValue(':chat_join_request_id', $chat_join_request_id); - $sth->bindValue(':chat_boost_updated_id', $chat_boost_updated_id); - $sth->bindValue(':chat_boost_removed_id', $chat_boost_removed_id); - - return $sth->execute(); - } catch (PDOException $e) { - throw new TelegramException($e->getMessage()); - } - } - - /** - * Insert users and save their connection to chats - * - * @param User $user - * @param string|null $date - * @param Chat|null $chat - * - * @return bool If the insert was successful - * @throws TelegramException - */ - public static function insertUser(User $user, ?string $date = null, ?Chat $chat = null): bool - { - if (!self::isDbConnected()) { - return false; - } - - try { - $sth = self::$pdo->prepare(' - INSERT INTO `' . TB_USER . '` - (`id`, `is_bot`, `username`, `first_name`, `last_name`, `language_code`, `is_premium`, `added_to_attachment_menu`, `created_at`, `updated_at`) - VALUES - (:id, :is_bot, :username, :first_name, :last_name, :language_code, :is_premium, :added_to_attachment_menu, :created_at, :updated_at) - ON DUPLICATE KEY UPDATE - `is_bot` = VALUES(`is_bot`), - `username` = VALUES(`username`), - `first_name` = VALUES(`first_name`), - `last_name` = VALUES(`last_name`), - `language_code` = VALUES(`language_code`), - `is_premium` = VALUES(`is_premium`), - `added_to_attachment_menu` = VALUES(`added_to_attachment_menu`), - `updated_at` = VALUES(`updated_at`) - '); - - $sth->bindValue(':id', $user->getId()); - $sth->bindValue(':is_bot', $user->getIsBot(), PDO::PARAM_INT); - $sth->bindValue(':username', $user->getUsername()); - $sth->bindValue(':first_name', $user->getFirstName()); - $sth->bindValue(':last_name', $user->getLastName()); - $sth->bindValue(':language_code', $user->getLanguageCode()); - $sth->bindValue(':is_premium', $user->getIsPremium(), PDO::PARAM_INT); - $sth->bindValue(':added_to_attachment_menu', $user->getAddedToAttachmentMenu(), PDO::PARAM_INT); - $date = $date ?: self::getTimestamp(); - $sth->bindValue(':created_at', $date); - $sth->bindValue(':updated_at', $date); - - $status = $sth->execute(); - } catch (PDOException $e) { - throw new TelegramException($e->getMessage()); - } - - // Also insert the relationship to the chat into the user_chat table - if ($chat) { - try { - $sth = self::$pdo->prepare(' - INSERT IGNORE INTO `' . TB_USER_CHAT . '` - (`user_id`, `chat_id`) - VALUES - (:user_id, :chat_id) - '); - - $sth->bindValue(':user_id', $user->getId()); - $sth->bindValue(':chat_id', $chat->getId()); - - $status = $sth->execute(); - } catch (PDOException $e) { - throw new TelegramException($e->getMessage()); - } - } - - return $status; - } - - /** - * Insert chat - * - * @param Chat $chat - * @param string|null $date - * @param int|null $migrate_to_chat_id - * - * @return bool If the insert was successful - * @throws TelegramException - */ - public static function insertChat(Chat $chat, ?string $date = null, ?int $migrate_to_chat_id = null): ?bool - { - if (!self::isDbConnected()) { - return false; - } - - try { - $sth = self::$pdo->prepare(' - INSERT IGNORE INTO `' . TB_CHAT . '` - (`id`, `type`, `title`, `username`, `first_name`, `last_name`, `is_forum`, `created_at` ,`updated_at`, `old_id`) - VALUES - (:id, :type, :title, :username, :first_name, :last_name, :is_forum, :created_at, :updated_at, :old_id) - ON DUPLICATE KEY UPDATE - `type` = VALUES(`type`), - `title` = VALUES(`title`), - `username` = VALUES(`username`), - `first_name` = VALUES(`first_name`), - `last_name` = VALUES(`last_name`), - `is_forum` = VALUES(`is_forum`), - `updated_at` = VALUES(`updated_at`) - '); - - $chat_id = $chat->getId(); - $chat_type = $chat->getType(); - - if ($migrate_to_chat_id !== null) { - $chat_type = 'supergroup'; - - $sth->bindValue(':id', $migrate_to_chat_id); - $sth->bindValue(':old_id', $chat_id); - } else { - $sth->bindValue(':id', $chat_id); - $sth->bindValue(':old_id', $migrate_to_chat_id); - } - - $sth->bindValue(':type', $chat_type); - $sth->bindValue(':title', $chat->getTitle()); - $sth->bindValue(':username', $chat->getUsername()); - $sth->bindValue(':first_name', $chat->getFirstName()); - $sth->bindValue(':last_name', $chat->getLastName()); - $sth->bindValue(':is_forum', $chat->getIsForum()); - $date = $date ?: self::getTimestamp(); - $sth->bindValue(':created_at', $date); - $sth->bindValue(':updated_at', $date); - - return $sth->execute(); - } catch (PDOException $e) { - throw new TelegramException($e->getMessage()); - } - } - - /** - * Insert request into database - * - * @param Update $update - * - * @return bool - * @throws TelegramException - * @todo self::$pdo->lastInsertId() - unsafe usage if expected previous insert fails? - * - */ - public static function insertRequest(Update $update): bool - { - if (!self::isDbConnected()) { - return false; - } - - $chat_id = null; - $message_id = null; - $edited_message_id = null; - $channel_post_id = null; - $edited_channel_post_id = null; - $message_reaction_id = null; - $message_reaction_count_id = null; - $inline_query_id = null; - $chosen_inline_result_id = null; - $callback_query_id = null; - $shipping_query_id = null; - $pre_checkout_query_id = null; - $poll_id = null; - $poll_answer_poll_id = null; - $my_chat_member_updated_id = null; - $chat_member_updated_id = null; - $chat_join_request_id = null; - $chat_boost_updated_id = null; - $chat_boost_removed_id = null; - - if (($message = $update->getMessage()) && self::insertMessageRequest($message)) { - $chat_id = $message->getChat()->getId(); - $message_id = $message->getMessageId(); - } elseif (($edited_message = $update->getEditedMessage()) && self::insertEditedMessageRequest($edited_message)) { - $chat_id = $edited_message->getChat()->getId(); - $edited_message_id = (int) self::$pdo->lastInsertId(); - } elseif (($channel_post = $update->getChannelPost()) && self::insertMessageRequest($channel_post)) { - $chat_id = $channel_post->getChat()->getId(); - $channel_post_id = $channel_post->getMessageId(); - } elseif (($edited_channel_post = $update->getEditedChannelPost()) && self::insertEditedMessageRequest($edited_channel_post)) { - $chat_id = $edited_channel_post->getChat()->getId(); - $edited_channel_post_id = (int) self::$pdo->lastInsertId(); - } elseif (($message_reaction = $update->getMessageReaction()) && self::insertMessageReaction($message_reaction)) { - $chat_id = $message_reaction->getChat()->getId(); - $message_id = $message_reaction->getMessageId(); - $message_reaction_id = self::$pdo->lastInsertId(); - } elseif (($message_reaction_count = $update->getMessageReactionCount()) && self::insertMessageReactionCount($message_reaction_count)) { - $chat_id = $message_reaction_count->getChat()->getId(); - $message_id = $message_reaction_count->getMessageId(); - $message_reaction_count_id = self::$pdo->lastInsertId(); - } elseif (($inline_query = $update->getInlineQuery()) && self::insertInlineQueryRequest($inline_query)) { - $inline_query_id = $inline_query->getId(); - } elseif (($chosen_inline_result = $update->getChosenInlineResult()) && self::insertChosenInlineResultRequest($chosen_inline_result)) { - $chosen_inline_result_id = self::$pdo->lastInsertId(); - } elseif (($callback_query = $update->getCallbackQuery()) && self::insertCallbackQueryRequest($callback_query)) { - $callback_query_id = $callback_query->getId(); - } elseif (($shipping_query = $update->getShippingQuery()) && self::insertShippingQueryRequest($shipping_query)) { - $shipping_query_id = $shipping_query->getId(); - } elseif (($pre_checkout_query = $update->getPreCheckoutQuery()) && self::insertPreCheckoutQueryRequest($pre_checkout_query)) { - $pre_checkout_query_id = $pre_checkout_query->getId(); - } elseif (($poll = $update->getPoll()) && self::insertPollRequest($poll)) { - $poll_id = $poll->getId(); - } elseif (($poll_answer = $update->getPollAnswer()) && self::insertPollAnswerRequest($poll_answer)) { - $poll_answer_poll_id = $poll_answer->getPollId(); - } elseif (($my_chat_member = $update->getMyChatMember()) && self::insertChatMemberUpdatedRequest($my_chat_member)) { - $my_chat_member_updated_id = self::$pdo->lastInsertId(); - } elseif (($chat_member = $update->getChatMember()) && self::insertChatMemberUpdatedRequest($chat_member)) { - $chat_member_updated_id = self::$pdo->lastInsertId(); - } elseif (($chat_join_request = $update->getChatJoinRequest()) && self::insertChatJoinRequestRequest($chat_join_request)) { - $chat_join_request_id = self::$pdo->lastInsertId(); - } elseif (($chat_boost_updated = $update->getChatBoost()) && self::insertChatBoostUpdatedRequest($chat_boost_updated)) { - $chat_boost_updated_id = self::$pdo->lastInsertId(); - } elseif (($chat_boost_removed = $update->getRemovedChatBoost()) && self::insertChatBoostRemovedRequest($chat_boost_removed)) { - $chat_boost_removed_id = self::$pdo->lastInsertId(); - } else { - return false; - } - - return self::insertTelegramUpdate( - $update->getUpdateId(), - $chat_id, - $message_id, - $edited_message_id, - $channel_post_id, - $edited_channel_post_id, - $message_reaction_id, - $message_reaction_count_id, - $inline_query_id, - $chosen_inline_result_id, - $callback_query_id, - $shipping_query_id, - $pre_checkout_query_id, - $poll_id, - $poll_answer_poll_id, - $my_chat_member_updated_id, - $chat_member_updated_id, - $chat_join_request_id, - $chat_boost_updated_id, - $chat_boost_removed_id, - ); - } - - public static function insertMessageReaction(MessageReactionUpdated $message_reaction): bool - { - if (!self::isDbConnected()) { - return false; - } - - try { - $sth = self::$pdo->prepare(' - INSERT IGNORE INTO `' . TB_MESSAGE_REACTION . '` - (`chat_id`, `message_id`, `user_id`, `actor_chat_id`, `old_reaction`, `new_reaction`, `created_at`) - VALUES - (:chat_id, :message_id, :user_id, :actor_chat_id, :old_reaction, :new_reaction, :created_at) - '); - - $date = self::getTimestamp($message_reaction->getDate()); - $chat_id = null; - $user_id = null; - $actor_chat_id = null; - - if ($chat = $message_reaction->getChat()) { - $chat_id = $chat->getId(); - self::insertChat($chat, $date); - } - if ($user = $message_reaction->getUser()) { - $user_id = $user->getId(); - self::insertUser($user, $date); - } - if ($actor_chat = $message_reaction->getActorChat()) { - $actor_chat_id = $actor_chat->getId(); - self::insertChat($actor_chat, $date); - } - - $sth->bindValue(':chat_id', $chat_id); - $sth->bindValue(':message_id', $message_reaction->getMessageId()); - $sth->bindValue(':user_id', $user_id); - $sth->bindValue(':actor_chat_id', $actor_chat_id); - $sth->bindValue(':old_reaction', self::entitiesArrayToJson($message_reaction->getOldReaction() ?: [])); - $sth->bindValue(':new_reaction', self::entitiesArrayToJson($message_reaction->getNewReaction() ?: [])); - $sth->bindValue(':created_at', $date); - - return $sth->execute(); - } catch (PDOException $e) { - throw new TelegramException($e->getMessage()); - } - } - - public static function insertMessageReactionCount(MessageReactionCountUpdated $message_reaction_count): bool - { - if (!self::isDbConnected()) { - return false; - } - - try { - $sth = self::$pdo->prepare(' - INSERT IGNORE INTO `' . TB_MESSAGE_REACTION_COUNT . '` - (`chat_id`, `message_id`, `reactions`, `created_at`) - VALUES - (:chat_id, :message_id, :reactions, :created_at) - '); - - $date = self::getTimestamp($message_reaction_count->getDate()); - $chat_id = null; - - if ($chat = $message_reaction->getChat()) { - $chat_id = $chat->getId(); - self::insertChat($chat, $date); - } - - $sth->bindValue(':chat_id', $chat_id); - $sth->bindValue(':message_id', $message_reaction_count->getMessageId()); - $sth->bindValue(':reactions', $message_reaction_count->getReactions()); - $sth->bindValue(':created_at', $date); - - return $sth->execute(); - } catch (PDOException $e) { - throw new TelegramException($e->getMessage()); - } - } - - /** - * Insert inline query request into database - * - * @param InlineQuery $inline_query - * - * @return bool If the insert was successful - * @throws TelegramException - */ - public static function insertInlineQueryRequest(InlineQuery $inline_query): bool - { - if (!self::isDbConnected()) { - return false; - } - - try { - $sth = self::$pdo->prepare(' - INSERT IGNORE INTO `' . TB_INLINE_QUERY . '` - (`id`, `user_id`, `location`, `query`, `offset`, `chat_type`, `created_at`) - VALUES - (:id, :user_id, :location, :query, :offset, :chat_type, :created_at) - '); - - $date = self::getTimestamp(); - $user_id = null; - - if ($user = $inline_query->getFrom()) { - $user_id = $user->getId(); - self::insertUser($user, $date); - } - - $sth->bindValue(':id', $inline_query->getId()); - $sth->bindValue(':user_id', $user_id); - $sth->bindValue(':location', $inline_query->getLocation()); - $sth->bindValue(':query', $inline_query->getQuery()); - $sth->bindValue(':offset', $inline_query->getOffset()); - $sth->bindValue(':chat_type', $inline_query->getChatType()); - $sth->bindValue(':created_at', $date); - - return $sth->execute(); - } catch (PDOException $e) { - throw new TelegramException($e->getMessage()); - } - } - - /** - * Insert chosen inline result request into database - * - * @param ChosenInlineResult $chosen_inline_result - * - * @return bool If the insert was successful - * @throws TelegramException - */ - public static function insertChosenInlineResultRequest(ChosenInlineResult $chosen_inline_result): bool - { - if (!self::isDbConnected()) { - return false; - } - - try { - $sth = self::$pdo->prepare(' - INSERT INTO `' . TB_CHOSEN_INLINE_RESULT . '` - (`result_id`, `user_id`, `location`, `inline_message_id`, `query`, `created_at`) - VALUES - (:result_id, :user_id, :location, :inline_message_id, :query, :created_at) - '); - - $date = self::getTimestamp(); - $user_id = null; - - if ($user = $chosen_inline_result->getFrom()) { - $user_id = $user->getId(); - self::insertUser($user, $date); - } - - $sth->bindValue(':result_id', $chosen_inline_result->getResultId()); - $sth->bindValue(':user_id', $user_id); - $sth->bindValue(':location', $chosen_inline_result->getLocation()); - $sth->bindValue(':inline_message_id', $chosen_inline_result->getInlineMessageId()); - $sth->bindValue(':query', $chosen_inline_result->getQuery()); - $sth->bindValue(':created_at', $date); - - return $sth->execute(); - } catch (PDOException $e) { - throw new TelegramException($e->getMessage()); - } - } - - /** - * Insert callback query request into database - * - * @param CallbackQuery $callback_query - * - * @return bool If the insert was successful - * @throws TelegramException - */ - public static function insertCallbackQueryRequest(CallbackQuery $callback_query): bool - { - if (!self::isDbConnected()) { - return false; - } - - try { - $sth = self::$pdo->prepare(' - INSERT IGNORE INTO `' . TB_CALLBACK_QUERY . '` - (`id`, `user_id`, `chat_id`, `message_id`, `inline_message_id`, `chat_instance`, `data`, `game_short_name`, `created_at`) - VALUES - (:id, :user_id, :chat_id, :message_id, :inline_message_id, :chat_instance, :data, :game_short_name, :created_at) - '); - - $date = self::getTimestamp(); - $user_id = null; - - if ($user = $callback_query->getFrom()) { - $user_id = $user->getId(); - self::insertUser($user, $date); - } - - $chat_id = null; - $message_id = null; - if ($message = $callback_query->getMessage()) { - $chat_id = $message->getChat()->getId(); - $message_id = $message->getMessageId(); - - $is_message = self::$pdo->query(' - SELECT * - FROM `' . TB_MESSAGE . '` - WHERE `id` = ' . $message_id . ' - AND `chat_id` = ' . $chat_id . ' - LIMIT 1 - ')->rowCount(); - - if ($is_message) { - self::insertEditedMessageRequest($message); - } else { - self::insertMessageRequest($message); - } - } - - $sth->bindValue(':id', $callback_query->getId()); - $sth->bindValue(':user_id', $user_id); - $sth->bindValue(':chat_id', $chat_id); - $sth->bindValue(':message_id', $message_id); - $sth->bindValue(':inline_message_id', $callback_query->getInlineMessageId()); - $sth->bindValue(':chat_instance', $callback_query->getChatInstance()); - $sth->bindValue(':data', $callback_query->getData()); - $sth->bindValue(':game_short_name', $callback_query->getGameShortName()); - $sth->bindValue(':created_at', $date); - - return $sth->execute(); - } catch (PDOException $e) { - throw new TelegramException($e->getMessage()); - } - } - - /** - * Insert shipping query request into database - * - * @param ShippingQuery $shipping_query - * - * @return bool If the insert was successful - * @throws TelegramException - */ - public static function insertShippingQueryRequest(ShippingQuery $shipping_query): bool - { - if (!self::isDbConnected()) { - return false; - } - - try { - $sth = self::$pdo->prepare(' - INSERT IGNORE INTO `' . TB_SHIPPING_QUERY . '` - (`id`, `user_id`, `invoice_payload`, `shipping_address`, `created_at`) - VALUES - (:id, :user_id, :invoice_payload, :shipping_address, :created_at) - '); - - $date = self::getTimestamp(); - $user_id = null; - - if ($user = $shipping_query->getFrom()) { - $user_id = $user->getId(); - self::insertUser($user, $date); - } - - $sth->bindValue(':id', $shipping_query->getId()); - $sth->bindValue(':user_id', $user_id); - $sth->bindValue(':invoice_payload', $shipping_query->getInvoicePayload()); - $sth->bindValue(':shipping_address', $shipping_query->getShippingAddress()); - $sth->bindValue(':created_at', $date); - - return $sth->execute(); - } catch (PDOException $e) { - throw new TelegramException($e->getMessage()); - } - } - - /** - * Insert pre checkout query request into database - * - * @param PreCheckoutQuery $pre_checkout_query - * - * @return bool If the insert was successful - * @throws TelegramException - */ - public static function insertPreCheckoutQueryRequest(PreCheckoutQuery $pre_checkout_query): bool - { - if (!self::isDbConnected()) { - return false; - } - - try { - $sth = self::$pdo->prepare(' - INSERT IGNORE INTO `' . TB_PRE_CHECKOUT_QUERY . '` - (`id`, `user_id`, `currency`, `total_amount`, `invoice_payload`, `shipping_option_id`, `order_info`, `created_at`) - VALUES - (:id, :user_id, :currency, :total_amount, :invoice_payload, :shipping_option_id, :order_info, :created_at) - '); - - $date = self::getTimestamp(); - $user_id = null; - - if ($user = $pre_checkout_query->getFrom()) { - $user_id = $user->getId(); - self::insertUser($user, $date); - } - - $sth->bindValue(':id', $pre_checkout_query->getId()); - $sth->bindValue(':user_id', $user_id); - $sth->bindValue(':currency', $pre_checkout_query->getCurrency()); - $sth->bindValue(':total_amount', $pre_checkout_query->getTotalAmount()); - $sth->bindValue(':invoice_payload', $pre_checkout_query->getInvoicePayload()); - $sth->bindValue(':shipping_option_id', $pre_checkout_query->getShippingOptionId()); - $sth->bindValue(':order_info', $pre_checkout_query->getOrderInfo()); - $sth->bindValue(':created_at', $date); - - return $sth->execute(); - } catch (PDOException $e) { - throw new TelegramException($e->getMessage()); - } - } - - /** - * Insert poll request into database - * - * @param Poll $poll - * - * @return bool If the insert was successful - * @throws TelegramException - */ - public static function insertPollRequest(Poll $poll): bool - { - if (!self::isDbConnected()) { - return false; - } - - try { - $sth = self::$pdo->prepare(' - INSERT INTO `' . TB_POLL . '` - (`id`, `question`, `options`, `total_voter_count`, `is_closed`, `is_anonymous`, `type`, `allows_multiple_answers`, `correct_option_id`, `explanation`, `explanation_entities`, `open_period`, `close_date`, `created_at`) - VALUES - (:id, :question, :options, :total_voter_count, :is_closed, :is_anonymous, :type, :allows_multiple_answers, :correct_option_id, :explanation, :explanation_entities, :open_period, :close_date, :created_at) - ON DUPLICATE KEY UPDATE - `options` = VALUES(`options`), - `total_voter_count` = VALUES(`total_voter_count`), - `is_closed` = VALUES(`is_closed`), - `is_anonymous` = VALUES(`is_anonymous`), - `type` = VALUES(`type`), - `allows_multiple_answers` = VALUES(`allows_multiple_answers`), - `correct_option_id` = VALUES(`correct_option_id`), - `explanation` = VALUES(`explanation`), - `explanation_entities` = VALUES(`explanation_entities`), - `open_period` = VALUES(`open_period`), - `close_date` = VALUES(`close_date`) - '); - - $sth->bindValue(':id', $poll->getId()); - $sth->bindValue(':question', $poll->getQuestion()); - $sth->bindValue(':options', self::entitiesArrayToJson($poll->getOptions() ?: [])); - $sth->bindValue(':total_voter_count', $poll->getTotalVoterCount()); - $sth->bindValue(':is_closed', $poll->getIsClosed(), PDO::PARAM_INT); - $sth->bindValue(':is_anonymous', $poll->getIsAnonymous(), PDO::PARAM_INT); - $sth->bindValue(':type', $poll->getType()); - $sth->bindValue(':allows_multiple_answers', $poll->getAllowsMultipleAnswers(), PDO::PARAM_INT); - $sth->bindValue(':correct_option_id', $poll->getCorrectOptionId()); - $sth->bindValue(':explanation', $poll->getExplanation()); - $sth->bindValue(':explanation_entities', self::entitiesArrayToJson($poll->getExplanationEntities() ?: [])); - $sth->bindValue(':open_period', $poll->getOpenPeriod()); - $sth->bindValue(':close_date', self::getTimestamp($poll->getCloseDate())); - $sth->bindValue(':created_at', self::getTimestamp()); - - return $sth->execute(); - } catch (PDOException $e) { - throw new TelegramException($e->getMessage()); - } - } - - /** - * Insert poll answer request into database - * - * @param PollAnswer $poll_answer - * - * @return bool If the insert was successful - * @throws TelegramException - */ - public static function insertPollAnswerRequest(PollAnswer $poll_answer): bool - { - if (!self::isDbConnected()) { - return false; - } - - try { - $sth = self::$pdo->prepare(' - INSERT INTO `' . TB_POLL_ANSWER . '` - (`poll_id`, `user_id`, `option_ids`, `created_at`) - VALUES - (:poll_id, :user_id, :option_ids, :created_at) - ON DUPLICATE KEY UPDATE - `option_ids` = VALUES(`option_ids`) - '); - - $date = self::getTimestamp(); - $user_id = null; - - if ($user = $poll_answer->getUser()) { - $user_id = $user->getId(); - self::insertUser($user, $date); - } - - $sth->bindValue(':poll_id', $poll_answer->getPollId()); - $sth->bindValue(':user_id', $user_id); - $sth->bindValue(':option_ids', json_encode($poll_answer->getOptionIds())); - $sth->bindValue(':created_at', $date); - - return $sth->execute(); - } catch (PDOException $e) { - throw new TelegramException($e->getMessage()); - } - } - - /** - * Insert chat member updated request into database - * - * @param ChatMemberUpdated $chat_member_updated - * - * @return bool If the insert was successful - * @throws TelegramException - */ - public static function insertChatMemberUpdatedRequest(ChatMemberUpdated $chat_member_updated): bool - { - if (!self::isDbConnected()) { - return false; - } - - try { - $sth = self::$pdo->prepare(' - INSERT INTO `' . TB_CHAT_MEMBER_UPDATED . '` - (`chat_id`, `user_id`, `date`, `old_chat_member`, `new_chat_member`, `invite_link`, `created_at`) - VALUES - (:chat_id, :user_id, :date, :old_chat_member, :new_chat_member, :invite_link, :created_at) - '); - - $date = self::getTimestamp(); - $chat_id = null; - $user_id = null; - - if ($chat = $chat_member_updated->getChat()) { - $chat_id = $chat->getId(); - self::insertChat($chat, $date); - } - if ($user = $chat_member_updated->getFrom()) { - $user_id = $user->getId(); - self::insertUser($user, $date); - } - - $sth->bindValue(':chat_id', $chat_id); - $sth->bindValue(':user_id', $user_id); - $sth->bindValue(':date', self::getTimestamp($chat_member_updated->getDate())); - $sth->bindValue(':old_chat_member', $chat_member_updated->getOldChatMember()); - $sth->bindValue(':new_chat_member', $chat_member_updated->getNewChatMember()); - $sth->bindValue(':invite_link', $chat_member_updated->getInviteLink()); - $sth->bindValue(':created_at', $date); - - return $sth->execute(); - } catch (PDOException $e) { - throw new TelegramException($e->getMessage()); - } - } - - /** - * Insert chat join request into database - * - * @param ChatJoinRequest $chat_join_request - * - * @return bool If the insert was successful - * @throws TelegramException - */ - public static function insertChatJoinRequestRequest(ChatJoinRequest $chat_join_request): bool - { - if (!self::isDbConnected()) { - return false; - } - - try { - $sth = self::$pdo->prepare(' - INSERT INTO `' . TB_CHAT_JOIN_REQUEST . '` - (`chat_id`, `user_id`, `date`, `bio`, `invite_link`, `created_at`) - VALUES - (:chat_id, :user_id, :date, :bio, :invite_link, :created_at) - '); - - $date = self::getTimestamp(); - $chat_id = null; - $user_id = null; - - if ($chat = $chat_join_request->getChat()) { - $chat_id = $chat->getId(); - self::insertChat($chat, $date); - } - if ($user = $chat_join_request->getFrom()) { - $user_id = $user->getId(); - self::insertUser($user, $date); - } - - $sth->bindValue(':chat_id', $chat_id); - $sth->bindValue(':user_id', $user_id); - $sth->bindValue(':date', self::getTimestamp($chat_join_request->getDate())); - $sth->bindValue(':bio', $chat_join_request->getBio()); - $sth->bindValue(':invite_link', $chat_join_request->getInviteLink()); - $sth->bindValue(':created_at', $date); - - return $sth->execute(); - } catch (PDOException $e) { - throw new TelegramException($e->getMessage()); - } - } - - /** - * Insert chat boost updated into database - * - * @param ChatBoostUpdated $chat_boost_updated - * - * @return bool If the insert was successful - * @throws TelegramException - */ - public static function insertChatBoostUpdatedRequest(ChatBoostUpdated $chat_boost_updated): bool - { - if (!self::isDbConnected()) { - return false; - } - - try { - $sth = self::$pdo->prepare(' - INSERT INTO `' . TB_CHAT_BOOST_UPDATED . '` - (`chat_id`, `boost`, `created_at`) - VALUES - (:chat_id, :boost, :created_at) - '); - - $date = self::getTimestamp(); - $chat_id = null; - - if ($chat = $chat_boost_updated->getChat()) { - $chat_id = $chat->getId(); - self::insertChat($chat, $date); - } - - $sth->bindValue(':chat_id', $chat_id); - $sth->bindValue(':boost', $chat_boost_updated->getBoost()); - $sth->bindValue(':created_at', $date); - - return $sth->execute(); - } catch (PDOException $e) { - throw new TelegramException($e->getMessage()); - } - } - - /** - * Insert chat boost removed into database - * - * @param ChatBoostRemoved $chat_boost_removed - * - * @return bool If the insert was successful - * @throws TelegramException - */ - public static function insertChatBoostRemovedRequest(ChatBoostRemoved $chat_boost_removed): bool - { - if (!self::isDbConnected()) { - return false; - } - - try { - $sth = self::$pdo->prepare(' - INSERT INTO `' . TB_CHAT_BOOST_REMOVED . '` - (`chat_id`, `boost_id`, `remove_date`, `source`, `created_at`) - VALUES - (:chat_id, :boost_id, :remove_date, :source, :created_at) - '); - - $date = self::getTimestamp(); - $chat_id = null; - - if ($chat = $chat_boost_removed->getChat()) { - $chat_id = $chat->getId(); - self::insertChat($chat, $date); - } - - $sth->bindValue(':chat_id', $chat_id); - $sth->bindValue(':boost_id', $chat_boost_removed->getBoostId()); - $sth->bindValue(':remove_date', self::getTimestamp($chat_boost_removed->getRemoveDate())); - $sth->bindValue(':source', $chat_boost_removed->getSource()); - $sth->bindValue(':created_at', $date); - - return $sth->execute(); - } catch (PDOException $e) { - throw new TelegramException($e->getMessage()); - } - } - - /** - * Insert Message request in db - * - * @param Message $message - * - * @return bool If the insert was successful - * @throws TelegramException - */ - public static function insertMessageRequest(Message $message): bool - { - if (!self::isDbConnected()) { - return false; - } - - $date = self::getTimestamp($message->getDate()); - - // Insert chat, update chat id in case it migrated - $chat = $message->getChat(); - self::insertChat($chat, $date, $message->getMigrateToChatId()); - - $sender_chat_id = null; - if ($sender_chat = $message->getSenderChat()) { - self::insertChat($sender_chat); - $sender_chat_id = $sender_chat->getId(); - } - - // Insert user and the relation with the chat - if ($user = $message->getFrom()) { - self::insertUser($user, $date, $chat); - } - - // Insert the forwarded message user in users table - $forward_date = $message->getForwardDate() ? self::getTimestamp($message->getForwardDate()) : null; - - if ($forward_from = $message->getForwardFrom()) { - self::insertUser($forward_from); - $forward_from = $forward_from->getId(); - } - if ($forward_from_chat = $message->getForwardFromChat()) { - self::insertChat($forward_from_chat); - $forward_from_chat = $forward_from_chat->getId(); - } - - $via_bot_id = null; - if ($via_bot = $message->getViaBot()) { - self::insertUser($via_bot); - $via_bot_id = $via_bot->getId(); - } - - // New and left chat member - $new_chat_members_ids = null; - $left_chat_member_id = null; - - $new_chat_members = $message->getNewChatMembers(); - $left_chat_member = $message->getLeftChatMember(); - if (!empty($new_chat_members)) { - foreach ($new_chat_members as $new_chat_member) { - if ($new_chat_member instanceof User) { - // Insert the new chat user - self::insertUser($new_chat_member, $date, $chat); - $new_chat_members_ids[] = $new_chat_member->getId(); - } - } - $new_chat_members_ids = implode(',', $new_chat_members_ids); - } elseif ($left_chat_member) { - // Insert the left chat user - self::insertUser($left_chat_member, $date, $chat); - $left_chat_member_id = $left_chat_member->getId(); - } - - try { - $sth = self::$pdo->prepare(' - INSERT IGNORE INTO `' . TB_MESSAGE . '` - ( - `id`, `user_id`, `chat_id`, `message_thread_id`, `sender_chat_id`, `date`, `forward_from`, `forward_from_chat`, `forward_from_message_id`, - `forward_signature`, `forward_sender_name`, `forward_date`, `is_topic_message`, - `reply_to_chat`, `reply_to_message`, `external_reply`, `via_bot`, `link_preview_options`, `edit_date`, `media_group_id`, `author_signature`, `text`, `entities`, `caption_entities`, - `audio`, `document`, `animation`, `game`, `photo`, `sticker`, `story`, `video`, `voice`, `video_note`, `caption`, `has_media_spoiler`, `contact`, - `location`, `venue`, `poll`, `dice`, `new_chat_members`, `left_chat_member`, - `new_chat_title`, `new_chat_photo`, `delete_chat_photo`, `group_chat_created`, - `supergroup_chat_created`, `channel_chat_created`, `message_auto_delete_timer_changed`, `migrate_to_chat_id`, `migrate_from_chat_id`, - `pinned_message`, `invoice`, `successful_payment`, `users_shared`, `chat_shared`, `connected_website`, `write_access_allowed`, `passport_data`, `proximity_alert_triggered`, - `forum_topic_created`, `forum_topic_edited`, `forum_topic_closed`, `forum_topic_reopened`, `general_forum_topic_hidden`, `general_forum_topic_unhidden`, - `video_chat_scheduled`, `video_chat_started`, `video_chat_ended`, `video_chat_participants_invited`, `web_app_data`, `reply_markup` - ) VALUES ( - :message_id, :user_id, :chat_id, :message_thread_id, :sender_chat_id, :date, :forward_from, :forward_from_chat, :forward_from_message_id, - :forward_signature, :forward_sender_name, :forward_date, :is_topic_message, - :reply_to_chat, :reply_to_message, :external_reply, :via_bot, :link_preview_options, :edit_date, :media_group_id, :author_signature, :text, :entities, :caption_entities, - :audio, :document, :animation, :game, :photo, :sticker, :story, :video, :voice, :video_note, :caption, :has_media_spoiler, :contact, - :location, :venue, :poll, :dice, :new_chat_members, :left_chat_member, - :new_chat_title, :new_chat_photo, :delete_chat_photo, :group_chat_created, - :supergroup_chat_created, :channel_chat_created, :message_auto_delete_timer_changed, :migrate_to_chat_id, :migrate_from_chat_id, - :pinned_message, :invoice, :successful_payment, :users_shared, :chat_shared, :connected_website, :write_access_allowed, :passport_data, :proximity_alert_triggered, - :forum_topic_created, :forum_topic_edited, :forum_topic_closed, :forum_topic_reopened, :general_forum_topic_hidden, :general_forum_topic_unhidden, - :video_chat_scheduled, :video_chat_started, :video_chat_ended, :video_chat_participants_invited, :web_app_data, :reply_markup - ) - '); - - $user_id = $user ? $user->getId() : null; - $chat_id = $chat->getId(); - - $reply_to_message_id = null; - if ($reply_to_message = $message->getReplyToMessage()) { - $reply_to_message_id = $reply_to_message->getMessageId(); - // please notice that, as explained in the documentation, reply_to_message don't contain other - // reply_to_message field so recursion deep is 1 - self::insertMessageRequest($reply_to_message); - } - - $sth->bindValue(':message_id', $message->getMessageId()); - $sth->bindValue(':chat_id', $chat_id); - $sth->bindValue(':sender_chat_id', $sender_chat_id); - $sth->bindValue(':message_thread_id', $message->getMessageThreadId()); - $sth->bindValue(':user_id', $user_id); - $sth->bindValue(':date', $date); - $sth->bindValue(':forward_from', $forward_from); - $sth->bindValue(':forward_from_chat', $forward_from_chat); - $sth->bindValue(':forward_from_message_id', $message->getForwardFromMessageId()); - $sth->bindValue(':forward_signature', $message->getForwardSignature()); - $sth->bindValue(':forward_sender_name', $message->getForwardSenderName()); - $sth->bindValue(':forward_date', $forward_date); - $sth->bindValue(':is_topic_message', $message->getIsTopicMessage()); - - $reply_to_chat_id = null; - if ($reply_to_message_id !== null) { - $reply_to_chat_id = $chat_id; - } - $sth->bindValue(':reply_to_chat', $reply_to_chat_id); - $sth->bindValue(':reply_to_message', $reply_to_message_id); - $sth->bindValue(':external_reply', $message->getExternalReply()); - - $sth->bindValue(':via_bot', $via_bot_id); - $sth->bindValue(':link_preview_options', $message->getLinkPreviewOptions()); - $sth->bindValue(':edit_date', self::getTimestamp($message->getEditDate())); - $sth->bindValue(':media_group_id', $message->getMediaGroupId()); - $sth->bindValue(':author_signature', $message->getAuthorSignature()); - $sth->bindValue(':text', $message->getText()); - $sth->bindValue(':entities', self::entitiesArrayToJson($message->getEntities() ?: [])); - $sth->bindValue(':caption_entities', self::entitiesArrayToJson($message->getCaptionEntities() ?: [])); - $sth->bindValue(':audio', $message->getAudio()); - $sth->bindValue(':document', $message->getDocument()); - $sth->bindValue(':animation', $message->getAnimation()); - $sth->bindValue(':game', $message->getGame()); - $sth->bindValue(':photo', self::entitiesArrayToJson($message->getPhoto() ?: [])); - $sth->bindValue(':sticker', $message->getSticker()); - $sth->bindValue(':story', $message->getStory()); - $sth->bindValue(':video', $message->getVideo()); - $sth->bindValue(':voice', $message->getVoice()); - $sth->bindValue(':video_note', $message->getVideoNote()); - $sth->bindValue(':caption', $message->getCaption()); - $sth->bindValue(':has_media_spoiler', $message->getHasMediaSpoiler()); - $sth->bindValue(':contact', $message->getContact()); - $sth->bindValue(':location', $message->getLocation()); - $sth->bindValue(':venue', $message->getVenue()); - $sth->bindValue(':poll', $message->getPoll()); - $sth->bindValue(':dice', $message->getDice()); - $sth->bindValue(':new_chat_members', $new_chat_members_ids); - $sth->bindValue(':left_chat_member', $left_chat_member_id); - $sth->bindValue(':new_chat_title', $message->getNewChatTitle()); - $sth->bindValue(':new_chat_photo', self::entitiesArrayToJson($message->getNewChatPhoto() ?: [])); - $sth->bindValue(':delete_chat_photo', $message->getDeleteChatPhoto()); - $sth->bindValue(':group_chat_created', $message->getGroupChatCreated()); - $sth->bindValue(':supergroup_chat_created', $message->getSupergroupChatCreated()); - $sth->bindValue(':channel_chat_created', $message->getChannelChatCreated()); - $sth->bindValue(':message_auto_delete_timer_changed', $message->getMessageAutoDeleteTimerChanged()); - $sth->bindValue(':migrate_to_chat_id', $message->getMigrateToChatId()); - $sth->bindValue(':migrate_from_chat_id', $message->getMigrateFromChatId()); - $sth->bindValue(':pinned_message', $message->getPinnedMessage()); - $sth->bindValue(':invoice', $message->getInvoice()); - $sth->bindValue(':successful_payment', $message->getSuccessfulPayment()); - $sth->bindValue(':users_shared', $message->getUsersShared()); - $sth->bindValue(':chat_shared', $message->getChatShared()); - $sth->bindValue(':connected_website', $message->getConnectedWebsite()); - $sth->bindValue(':write_access_allowed', $message->getWriteAccessAllowed()); - $sth->bindValue(':passport_data', $message->getPassportData()); - $sth->bindValue(':proximity_alert_triggered', $message->getProximityAlertTriggered()); - $sth->bindValue(':forum_topic_created', $message->getForumTopicCreated()); - $sth->bindValue(':forum_topic_edited', $message->getForumTopicEdited()); - $sth->bindValue(':forum_topic_closed', $message->getForumTopicClosed()); - $sth->bindValue(':forum_topic_reopened', $message->getForumTopicReopened()); - $sth->bindValue(':general_forum_topic_hidden', $message->getGeneralForumTopicHidden()); - $sth->bindValue(':general_forum_topic_unhidden', $message->getGeneralForumTopicUnhidden()); - $sth->bindValue(':video_chat_scheduled', $message->getVideoChatScheduled()); - $sth->bindValue(':video_chat_started', $message->getVideoChatStarted()); - $sth->bindValue(':video_chat_ended', $message->getVideoChatEnded()); - $sth->bindValue(':video_chat_participants_invited', $message->getVideoChatParticipantsInvited()); - $sth->bindValue(':web_app_data', $message->getWebAppData()); - $sth->bindValue(':reply_markup', $message->getReplyMarkup()); - - return $sth->execute(); - } catch (PDOException $e) { - throw new TelegramException($e->getMessage()); - } - } - - /** - * Insert Edited Message request in db - * - * @param Message $edited_message - * - * @return bool If the insert was successful - * @throws TelegramException - */ - public static function insertEditedMessageRequest(Message $edited_message): bool - { - if (!self::isDbConnected()) { - return false; - } - - try { - $edit_date = self::getTimestamp($edited_message->getEditDate()); - - // Insert chat - $chat = $edited_message->getChat(); - self::insertChat($chat, $edit_date); - - // Insert user and the relation with the chat - if ($user = $edited_message->getFrom()) { - self::insertUser($user, $edit_date, $chat); - } - - $sth = self::$pdo->prepare(' - INSERT IGNORE INTO `' . TB_EDITED_MESSAGE . '` - (`chat_id`, `message_id`, `user_id`, `edit_date`, `text`, `entities`, `caption`) - VALUES - (:chat_id, :message_id, :user_id, :edit_date, :text, :entities, :caption) - '); - - $user_id = $user ? $user->getId() : null; - - $sth->bindValue(':chat_id', $chat->getId()); - $sth->bindValue(':message_id', $edited_message->getMessageId()); - $sth->bindValue(':user_id', $user_id); - $sth->bindValue(':edit_date', $edit_date); - $sth->bindValue(':text', $edited_message->getText()); - $sth->bindValue(':entities', self::entitiesArrayToJson($edited_message->getEntities() ?: [])); - $sth->bindValue(':caption', $edited_message->getCaption()); - - return $sth->execute(); - } catch (PDOException $e) { - throw new TelegramException($e->getMessage()); - } - } - - /** - * Select Groups, Supergroups, Channels and/or single user Chats (also by ID or text) - * - * @param $select_chats_params - * - * @return array|bool - * @throws TelegramException - */ - public static function selectChats($select_chats_params) - { - if (!self::isDbConnected()) { - return false; - } - - // Set defaults for omitted values. - $select = array_merge([ - 'groups' => true, - 'supergroups' => true, - 'channels' => true, - 'users' => true, - 'date_from' => null, - 'date_to' => null, - 'chat_id' => null, - 'text' => null, - 'language' => null, - ], $select_chats_params); - - if (!$select['groups'] && !$select['users'] && !$select['supergroups'] && !$select['channels']) { - return false; - } - - try { - $query = ' - SELECT * , - ' . TB_CHAT . '.`id` AS `chat_id`, - ' . TB_CHAT . '.`username` AS `chat_username`, - ' . TB_CHAT . '.`created_at` AS `chat_created_at`, - ' . TB_CHAT . '.`updated_at` AS `chat_updated_at` - '; - if ($select['users']) { - $query .= ' - , ' . TB_USER . '.`id` AS `user_id` - FROM `' . TB_CHAT . '` - LEFT JOIN `' . TB_USER . '` - ON ' . TB_CHAT . '.`id`=' . TB_USER . '.`id` - '; - } else { - $query .= 'FROM `' . TB_CHAT . '`'; - } - - // Building parts of query - $where = []; - $tokens = []; - - if (!$select['groups'] || !$select['users'] || !$select['supergroups'] || !$select['channels']) { - $chat_or_user = []; - - $select['groups'] && $chat_or_user[] = TB_CHAT . '.`type` = "group"'; - $select['supergroups'] && $chat_or_user[] = TB_CHAT . '.`type` = "supergroup"'; - $select['channels'] && $chat_or_user[] = TB_CHAT . '.`type` = "channel"'; - $select['users'] && $chat_or_user[] = TB_CHAT . '.`type` = "private"'; - - $where[] = '(' . implode(' OR ', $chat_or_user) . ')'; - } - - if (null !== $select['date_from']) { - $where[] = TB_CHAT . '.`updated_at` >= :date_from'; - $tokens[':date_from'] = $select['date_from']; - } - - if (null !== $select['date_to']) { - $where[] = TB_CHAT . '.`updated_at` <= :date_to'; - $tokens[':date_to'] = $select['date_to']; - } - - if (null !== $select['chat_id']) { - $where[] = TB_CHAT . '.`id` = :chat_id'; - $tokens[':chat_id'] = $select['chat_id']; - } - - if ($select['users'] && null !== $select['language']) { - $where[] = TB_USER . '.`language_code` = :language'; - $tokens[':language'] = $select['language']; - } - - if (null !== $select['text']) { - $text_like = '%' . strtolower($select['text']) . '%'; - if ($select['users']) { - $where[] = '( - LOWER(' . TB_CHAT . '.`title`) LIKE :text1 - OR LOWER(' . TB_USER . '.`first_name`) LIKE :text2 - OR LOWER(' . TB_USER . '.`last_name`) LIKE :text3 - OR LOWER(' . TB_USER . '.`username`) LIKE :text4 - )'; - $tokens[':text1'] = $text_like; - $tokens[':text2'] = $text_like; - $tokens[':text3'] = $text_like; - $tokens[':text4'] = $text_like; - } else { - $where[] = 'LOWER(' . TB_CHAT . '.`title`) LIKE :text'; - $tokens[':text'] = $text_like; - } - } - - if (!empty($where)) { - $query .= ' WHERE ' . implode(' AND ', $where); - } - - $query .= ' ORDER BY ' . TB_CHAT . '.`updated_at` ASC'; - - $sth = self::$pdo->prepare($query); - $sth->execute($tokens); - - return $sth->fetchAll(PDO::FETCH_ASSOC); - } catch (PDOException $e) { - throw new TelegramException($e->getMessage()); - } - } - - /** - * Get Telegram API request count for current chat / message - * - * @param int|string|null $chat_id - * @param string|null $inline_message_id - * - * @return array|bool Array containing TOTAL and CURRENT fields or false on invalid arguments - * @throws TelegramException - */ - public static function getTelegramRequestCount($chat_id = null, string $inline_message_id = null) - { - if (!self::isDbConnected()) { - return false; - } - - try { - $sth = self::$pdo->prepare('SELECT - (SELECT COUNT(DISTINCT `chat_id`) FROM `' . TB_REQUEST_LIMITER . '` WHERE `created_at` >= :created_at_1) AS LIMIT_PER_SEC_ALL, - (SELECT COUNT(*) FROM `' . TB_REQUEST_LIMITER . '` WHERE `created_at` >= :created_at_2 AND ((`chat_id` = :chat_id_1 AND `inline_message_id` IS NULL) OR (`inline_message_id` = :inline_message_id AND `chat_id` IS NULL))) AS LIMIT_PER_SEC, - (SELECT COUNT(*) FROM `' . TB_REQUEST_LIMITER . '` WHERE `created_at` >= :created_at_minute AND `chat_id` = :chat_id_2) AS LIMIT_PER_MINUTE - '); - - $date = self::getTimestamp(); - $date_minute = self::getTimestamp(strtotime('-1 minute')); - - $sth->bindValue(':chat_id_1', $chat_id); - $sth->bindValue(':chat_id_2', $chat_id); - $sth->bindValue(':inline_message_id', $inline_message_id); - $sth->bindValue(':created_at_1', $date); - $sth->bindValue(':created_at_2', $date); - $sth->bindValue(':created_at_minute', $date_minute); - - $sth->execute(); - - return $sth->fetch(); - } catch (PDOException $e) { - throw new TelegramException($e->getMessage()); - } - } - - /** - * Insert Telegram API request in db - * - * @param string $method - * @param array $data - * - * @return bool If the insert was successful - * @throws TelegramException - */ - public static function insertTelegramRequest(string $method, array $data): bool - { - if (!self::isDbConnected()) { - return false; - } - - try { - $sth = self::$pdo->prepare('INSERT INTO `' . TB_REQUEST_LIMITER . '` - (`method`, `chat_id`, `inline_message_id`, `created_at`) - VALUES - (:method, :chat_id, :inline_message_id, :created_at); - '); - - $chat_id = $data['chat_id'] ?? null; - $inline_message_id = $data['inline_message_id'] ?? null; - - $sth->bindValue(':chat_id', $chat_id); - $sth->bindValue(':inline_message_id', $inline_message_id); - $sth->bindValue(':method', $method); - $sth->bindValue(':created_at', self::getTimestamp()); - - return $sth->execute(); - } catch (PDOException $e) { - throw new TelegramException($e->getMessage()); - } - } - - /** - * Bulk update the entries of any table - * - * @param string $table - * @param array $fields_values - * @param array $where_fields_values - * - * @return bool - * @throws TelegramException - */ - public static function update(string $table, array $fields_values, array $where_fields_values): bool - { - if (empty($fields_values) || !self::isDbConnected()) { - return false; - } - - try { - // Building parts of query - $tokens = $fields = $where = []; - - // Fields with values to update - foreach ($fields_values as $field => $value) { - $token = ':' . count($tokens); - $fields[] = "`{$field}` = {$token}"; - $tokens[$token] = $value; - } - - // Where conditions - foreach ($where_fields_values as $field => $value) { - $token = ':' . count($tokens); - $where[] = "`{$field}` = {$token}"; - $tokens[$token] = $value; - } - - $sql = 'UPDATE `' . $table . '` SET ' . implode(', ', $fields); - $sql .= count($where) > 0 ? ' WHERE ' . implode(' AND ', $where) : ''; - - return self::$pdo->prepare($sql)->execute($tokens); - } catch (PDOException $e) { - throw new TelegramException($e->getMessage()); - } - } -} diff --git a/src/Entities/Animation.php b/src/Entities/Animation.php deleted file mode 100644 index 83b012a29..000000000 --- a/src/Entities/Animation.php +++ /dev/null @@ -1,42 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -/** - * Class Animation - * - * You can provide an animation for your game so that it looks stylish in chats (check out Lumberjack for an example). This object represents an animation file to be displayed in the message containing a game. - * - * @link https://core.telegram.org/bots/api#animation - * - * @method string getFileId() Identifier for this file, which can be used to download or reuse the file - * @method string getFileUniqueId() Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. - * @method int getWidth() Video width as defined by sender - * @method int getHeight() Video height as defined by sender - * @method int getDuration() Duration of the video in seconds as defined by sender - * @method PhotoSize getThumbnail() Optional. Animation thumbnail as defined by sender - * @method string getFileName() Optional. Original animation filename as defined by sender - * @method string getMimeType() Optional. MIME type of the file as defined by sender - * @method int getFileSize() Optional. File size - **/ -class Animation extends Entity -{ - /** - * {@inheritdoc} - */ - protected function subEntities(): array - { - return [ - 'thumbnail' => PhotoSize::class, - ]; - } -} diff --git a/src/Entities/Audio.php b/src/Entities/Audio.php deleted file mode 100644 index 91d1359ba..000000000 --- a/src/Entities/Audio.php +++ /dev/null @@ -1,40 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -/** - * Class Audio - * - * @link https://core.telegram.org/bots/api#audio - * - * @method string getFileId() Identifier for this file, which can be used to download or reuse the file - * @method string getFileUniqueId() Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. - * @method int getDuration() Duration of the audio in seconds as defined by sender - * @method string getPerformer() Optional. Performer of the audio as defined by sender or by audio tags - * @method string getTitle() Optional. Title of the audio as defined by sender or by audio tags - * @method string getFileName() Optional. Original filename as defined by sender - * @method string getMimeType() Optional. MIME type of the file as defined by sender - * @method int getFileSize() Optional. File size - * @method PhotoSize getThumbnail() Optional. Thumbnail of the album cover to which the music file belongs - */ -class Audio extends Entity -{ - /** - * {@inheritdoc} - */ - protected function subEntities(): array - { - return [ - 'thumbnail' => PhotoSize::class, - ]; - } -} diff --git a/src/Entities/BotCommand.php b/src/Entities/BotCommand.php deleted file mode 100644 index dc3302e45..000000000 --- a/src/Entities/BotCommand.php +++ /dev/null @@ -1,27 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -/** - * Class BotCommand - * - * This entity represents a bot command. - * - * @link https://core.telegram.org/bots/api#botcommand - * - * @method string getCommand() Text of the command, 1-32 characters. Can contain only lowercase English letters, digits and underscores. - * @method string getDescription() Description of the command, 3-256 characters. - */ -class BotCommand extends Entity -{ - -} diff --git a/src/Entities/BotCommandScope/BotCommandScope.php b/src/Entities/BotCommandScope/BotCommandScope.php deleted file mode 100644 index b0127c710..000000000 --- a/src/Entities/BotCommandScope/BotCommandScope.php +++ /dev/null @@ -1,8 +0,0 @@ - - * $data = [ - * 'chat_id' => '123456' - * ]; - * - * - * @method string getType() Scope type, must be chat - * @method string getChatId() Unique identifier for the target chat or username of the target supergroup (in the format @supergroupusername) - * - * @method $this setChatId(string $chat_id) Unique identifier for the target chat or username of the target supergroup (in the format @supergroupusername) - */ -class BotCommandScopeChat extends Entity implements BotCommandScope -{ - public function __construct(array $data = []) - { - $data['type'] = 'chat'; - parent::__construct($data); - } -} diff --git a/src/Entities/BotCommandScope/BotCommandScopeChatAdministrators.php b/src/Entities/BotCommandScope/BotCommandScopeChatAdministrators.php deleted file mode 100644 index d16588bc4..000000000 --- a/src/Entities/BotCommandScope/BotCommandScopeChatAdministrators.php +++ /dev/null @@ -1,30 +0,0 @@ - - * $data = [ - * 'chat_id' => '123456' - * ]; - * - * - * @method string getType() Scope type, must be chat_administrators - * @method string getChatId() Unique identifier for the target chat or username of the target supergroup (in the format @supergroupusername) - * - * @method $this setChatId(string $chat_id) Unique identifier for the target chat or username of the target supergroup (in the format @supergroupusername) - */ -class BotCommandScopeChatAdministrators extends Entity implements BotCommandScope -{ - public function __construct(array $data = []) - { - $data['type'] = 'chat_administrators'; - parent::__construct($data); - } -} diff --git a/src/Entities/BotCommandScope/BotCommandScopeChatMember.php b/src/Entities/BotCommandScope/BotCommandScopeChatMember.php deleted file mode 100644 index 2c57acff7..000000000 --- a/src/Entities/BotCommandScope/BotCommandScopeChatMember.php +++ /dev/null @@ -1,33 +0,0 @@ - - * $data = [ - * 'chat_id' => '123456', - * 'user_id' => 987654, - * ]; - * - * - * @method string getType() Scope type, must be chat_member - * @method string getChatId() Unique identifier for the target chat or username of the target supergroup (in the format @supergroupusername) - * @method int getUserId() Unique identifier of the target user - * - * @method $this setChatId(string $chat_id) Unique identifier for the target chat or username of the target supergroup (in the format @supergroupusername) - * @method $this setUserId(int $user_id) Unique identifier of the target user - */ -class BotCommandScopeChatMember extends Entity implements BotCommandScope -{ - public function __construct(array $data = []) - { - $data['type'] = 'chat_member'; - parent::__construct($data); - } -} diff --git a/src/Entities/BotCommandScope/BotCommandScopeDefault.php b/src/Entities/BotCommandScope/BotCommandScopeDefault.php deleted file mode 100644 index f25ef582f..000000000 --- a/src/Entities/BotCommandScope/BotCommandScopeDefault.php +++ /dev/null @@ -1,19 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -use Longman\TelegramBot\Entities\Message\Factory as MaybeInaccessibleMessageFactory; -use Longman\TelegramBot\Entities\Message\MaybeInaccessibleMessage; -use Longman\TelegramBot\Request; - -/** - * Class CallbackQuery. - * - * @link https://core.telegram.org/bots/api#callbackquery - * - * @method string getId() Unique identifier for this query - * @method User getFrom() Sender - * @method MaybeInaccessibleMessage getMessage() Optional. Message with the callback button that originated the query. Note that message content and message date will not be available if the message is too old - * @method string getInlineMessageId() Optional. Identifier of the message sent via the bot in inline mode, that originated the query - * @method string getChatInstance() Global identifier, uniquely corresponding to the chat to which the message with the callback button was sent. Useful for high scores in games. - * @method string getData() Data associated with the callback button. Be aware that a bad client can send arbitrary data in this field - * @method string getGameShortName() Optional. Short name of a Game to be returned, serves as the unique identifier for the game - */ -class CallbackQuery extends Entity -{ - /** - * {@inheritdoc} - */ - protected function subEntities(): array - { - return [ - 'from' => User::class, - 'message' => MaybeInaccessibleMessageFactory::class, - ]; - } - - /** - * Answer this callback query. - * - * @param array $data - * - * @return ServerResponse - */ - public function answer(array $data = []): ServerResponse - { - return Request::answerCallbackQuery(array_merge([ - 'callback_query_id' => $this->getId(), - ], $data)); - } -} diff --git a/src/Entities/ChannelPost.php b/src/Entities/ChannelPost.php deleted file mode 100644 index fcc07afec..000000000 --- a/src/Entities/ChannelPost.php +++ /dev/null @@ -1,17 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -class ChannelPost extends Message -{ - -} diff --git a/src/Entities/Chat.php b/src/Entities/Chat.php deleted file mode 100644 index 8a46c519f..000000000 --- a/src/Entities/Chat.php +++ /dev/null @@ -1,143 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -use Longman\TelegramBot\Entities\ReactionType\Factory as ReactionTypeFactory; -use Longman\TelegramBot\Entities\ReactionType\ReactionType; - -/** - * This object represents a chat. - * - * @link https://core.telegram.org/bots/api#chat - * - * @property string type Type of chat, can be either "private ", "group", "supergroup" or "channel" - * - * @method int getId() Unique identifier for this chat. This number may be greater than 32 bits and some programming languages may have difficulty/silent defects in interpreting it. But it smaller than 52 bits, so a signed 64 bit integer or double-precision float type are safe for storing this identifier. - * @method string getType() Type of chat, can be either "private ", "group", "supergroup" or "channel" - * @method string getTitle() Optional. Title, for channels and group chats - * @method string getUsername() Optional. Username, for private chats, supergroups and channels if available - * @method string getFirstName() Optional. First name of the other party in a private chat - * @method string getLastName() Optional. Last name of the other party in a private chat - * @method bool getIsForum() Optional. True, if the supergroup chat is a forum (has topics enabled) - * @method int getAccentColorId() Identifier of the accent color for the chat name and backgrounds of the chat photo, reply header, and link preview. See accent colors for more details. - * @method ChatPhoto getPhoto() Optional. Chat photo. Returned only in getChat. - * @method string[] getActiveUsernames() Optional. If non-empty, the list of all active chat usernames; for private chats, supergroups and channels. Returned only in getChat. - * @method ReactionType[] getAvailableReactions() Optional. List of available reactions allowed in the chat. If omitted, then all emoji reactions are allowed. Returned only in getChat. - * @method string getBackgroundCustomEmojiId() Optional. Custom emoji identifier of the emoji chosen by the chat for the reply header and link preview background - * @method int getProfileAccentColorId() Optional. Identifier of the accent color for the chat's profile background. See profile accent colors for more details. - * @method string getProfileBackgroundCustomEmojiId() Optional. Custom emoji identifier of the emoji chosen by the chat for its profile background - * @method string getEmojiStatusCustomEmojiId() Optional. Custom emoji identifier of emoji status of the other party in a private chat. Returned only in getChat. - * @method int getEmojiStatusExpirationDate() Optional. Expiration date of the emoji status of the other party in a private chat in Unix time, if any. Returned only in getChat. - * @method string getBio() Optional. Bio of the other party in a private chat. Returned only in getChat. - * @method bool getHasPrivateForwards() Optional. True, if privacy settings of the other party in the private chat allows to use tg://user?id= links only in chats with the user. Returned only in getChat. - * @method bool getHasRestrictedVoiceAndVideoMessages() Optional. True, if the privacy settings of the other party restrict sending voice and video note messages in the private chat. Returned only in getChat. - * @method bool getJoinToSendMessages() Optional. True, if users need to join the supergroup before they can send messages. Returned only in getChat. - * @method bool getJoinByRequest() Optional. True, if all users directly joining the supergroup need to be approved by supergroup administrators. Returned only in getChat. - * @method string getDescription() Optional. Description, for groups, supergroups and channel chats. Returned only in getChat. - * @method string getInviteLink() Optional. Chat invite link, for groups, supergroups and channel chats. Each administrator in a chat generates their own invite links, so the bot must first generate the link using exportChatInviteLink. Returned only in getChat. - * @method Message getPinnedMessage() Optional. Pinned message, for groups, supergroups and channels. Returned only in getChat. - * @method ChatPermissions getPermissions() Optional. Default chat member permissions, for groups and supergroups. Returned only in getChat. - * @method int getSlowModeDelay() Optional. For supergroups, the minimum allowed delay between consecutive messages sent by each unpriviledged user. Returned only in getChat. - * @method int getMessageAutoDeleteTime() Optional. The time after which all messages sent to the chat will be automatically deleted; in seconds. Returned only in getChat. - * @method bool getHasAggressiveAntiSpamEnabled() Optional. True, if aggressive anti-spam checks are enabled in the supergroup. The field is only available to chat administrators. Returned only in getChat. - * @method bool getHasHiddenMembers() Optional. True, if non-administrators can only get the list of bots and administrators in the chat. Returned only in getChat. - * @method bool getHasProtectedContent() Optional. True, if messages from the chat can't be forwarded to other chats. Returned only in getChat. - * @method bool getHasVisibleHistory() Optional. True, if new chat members will have access to old messages; available only to chat administrators - * @method string getStickerSetName() Optional. For supergroups, name of group sticker set. Returned only in getChat. - * @method bool getCanSetStickerSet() Optional. True, if the bot can change the group sticker set. Returned only in getChat. - * @method int getLinkedChatId() Optional. Unique identifier for the linked chat. Returned only in getChat. - * @method ChatLocation getLocation() Optional. For supergroups, the location to which the supergroup is connected. Returned only in getChat. - */ -class Chat extends Entity -{ - /** - * {@inheritdoc} - */ - protected function subEntities(): array - { - return [ - 'photo' => ChatPhoto::class, - 'available_reactions' => [ReactionTypeFactory::class], - 'pinned_message' => Message::class, - 'permissions' => ChatPermissions::class, - 'location' => ChatLocation::class, - ]; - } - - public function __construct(array $data) - { - parent::__construct($data); - - $id = $this->getId(); - $type = $this->getType(); - if (!$type) { - $id > 0 && $this->type = 'private'; - $id < 0 && $this->type = 'group'; - } - } - - /** - * Try to mention the user of this chat, else return the title - * - * @param bool $escape_markdown - * - * @return string - */ - public function tryMention($escape_markdown = false): string - { - if ($this->isPrivateChat()) { - return parent::tryMention($escape_markdown); - } - - return $this->getTitle(); - } - - /** - * Check if this is a group chat - * - * @return bool - */ - public function isGroupChat(): bool - { - return $this->getType() === 'group'; - } - - /** - * Check if this is a private chat - * - * @return bool - */ - public function isPrivateChat(): bool - { - return $this->getType() === 'private'; - } - - /** - * Check if this is a super group - * - * @return bool - */ - public function isSuperGroup(): bool - { - return $this->getType() === 'supergroup'; - } - - /** - * Check if this is a channel - * - * @return bool - */ - public function isChannel(): bool - { - return $this->getType() === 'channel'; - } -} diff --git a/src/Entities/ChatAdministratorRights.php b/src/Entities/ChatAdministratorRights.php deleted file mode 100644 index c6c50ff52..000000000 --- a/src/Entities/ChatAdministratorRights.php +++ /dev/null @@ -1,43 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -use Longman\TelegramBot\Entities\ChatBoostSource\Factory as ChatBoostSourceFactory; - -/** - * This object contains information about a chat boost. - * - * @link https://core.telegram.org/bots/api#chatboost - * - * @method string getBoostId() Unique identifier of the boost - * @method int getAddDate() Point in time (Unix timestamp) when the chat was boosted - * @method int getExpirationDate() Point in time (Unix timestamp) when the boost will automatically expire, unless the booster's Telegram Premium subscription is prolonged - * @method ChatBoostSource getSource() Source of the added boost - */ -class ChatBoost extends Entity -{ - /** - * {@inheritdoc} - */ - protected function subEntities(): array - { - return [ - 'source' => ChatBoostSourceFactory::class, - ]; - } -} diff --git a/src/Entities/ChatBoostRemoved.php b/src/Entities/ChatBoostRemoved.php deleted file mode 100644 index 2b893cca5..000000000 --- a/src/Entities/ChatBoostRemoved.php +++ /dev/null @@ -1,38 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -use Longman\TelegramBot\Entities\ChatBoostSource\Factory as ChatBoostSourceFactory; - -/** - * This object represents a boost removed from a chat. - * - * @link https://core.telegram.org/bots/api#chatboostremoved - * - * @method Chat getChat() Chat which was boosted - * @method string getBoostId() Unique identifier of the boost - * @method int getRemoveDate() Point in time (Unix timestamp) when the boost was removed - * @method ChatBoostSource getSource() Source of the removed boost - */ -class ChatBoostRemoved extends Entity -{ - /** - * {@inheritdoc} - */ - protected function subEntities(): array - { - return [ - 'chat' => Chat::class, - 'source' => ChatBoostSourceFactory::class, - ]; - } -} diff --git a/src/Entities/ChatBoostSource/ChatBoostSource.php b/src/Entities/ChatBoostSource/ChatBoostSource.php deleted file mode 100644 index dae048a90..000000000 --- a/src/Entities/ChatBoostSource/ChatBoostSource.php +++ /dev/null @@ -1,16 +0,0 @@ - User::class, - ]; - } -} diff --git a/src/Entities/ChatBoostSource/ChatBoostSourceGiveaway.php b/src/Entities/ChatBoostSource/ChatBoostSourceGiveaway.php deleted file mode 100644 index 7e4054801..000000000 --- a/src/Entities/ChatBoostSource/ChatBoostSourceGiveaway.php +++ /dev/null @@ -1,29 +0,0 @@ - User::class, - ]; - } -} diff --git a/src/Entities/ChatBoostSource/ChatBoostSourceNotImplemented.php b/src/Entities/ChatBoostSource/ChatBoostSourceNotImplemented.php deleted file mode 100644 index e045fcc08..000000000 --- a/src/Entities/ChatBoostSource/ChatBoostSourceNotImplemented.php +++ /dev/null @@ -1,15 +0,0 @@ - User::class, - ]; - } -} diff --git a/src/Entities/ChatBoostSource/Factory.php b/src/Entities/ChatBoostSource/Factory.php deleted file mode 100644 index 11e5af788..000000000 --- a/src/Entities/ChatBoostSource/Factory.php +++ /dev/null @@ -1,24 +0,0 @@ - ChatBoostSourcePremium::class, - 'gift_code' => ChatBoostSourceGiftCode::class, - 'giveaway' => ChatBoostSourceGiveaway::class, - ]; - - if (!isset($type[$data['source'] ?? ''])) { - return new ChatBoostSourceNotImplemented($data, $bot_username); - } - - $class = $type[$data['source']]; - return new $class($data, $bot_username); - } -} diff --git a/src/Entities/ChatBoostUpdated.php b/src/Entities/ChatBoostUpdated.php deleted file mode 100644 index 5228b9577..000000000 --- a/src/Entities/ChatBoostUpdated.php +++ /dev/null @@ -1,34 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -/** - * This object represents a boost added to a chat or changed. - * - * @link https://core.telegram.org/bots/api#chatboostupdated - * - * @method Chat getChat() Chat which was boosted - * @method ChatBoost getBoost() Information about the chat boost - */ -class ChatBoostUpdated extends Entity -{ - /** - * {@inheritdoc} - */ - protected function subEntities(): array - { - return [ - 'chat' => Chat::class, - 'chat_boost' => ChatBoost::class, - ]; - } -} diff --git a/src/Entities/ChatInviteLink.php b/src/Entities/ChatInviteLink.php deleted file mode 100644 index 9f76567e5..000000000 --- a/src/Entities/ChatInviteLink.php +++ /dev/null @@ -1,42 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -/** - * Class ChatInviteLink - * - * Represents an invite link for a chat - * - * @link https://core.telegram.org/bots/api#chatinvitelink - * - * @method string getInviteLink() The invite link. If the link was created by another chat administrator, then the second part of the link will be replaced with “…” - * @method User getCreator() Creator of the link - * @method bool getCreatesJoinRequest() True, if users joining the chat via the link need to be approved by chat administrators - * @method bool getIsPrimary() True, if the link is primary - * @method bool getIsRevoked() True, if the link is revoked - * @method string getName() Optional. Invite link name - * @method int getExpireDate() Optional. Point in time (Unix timestamp) when the link will expire or has been expired - * @method int getMemberLimit() Optional. Maximum number of users that can be members of the chat simultaneously after joining the chat via this invite link; 1-99999 - * @method int getPendingJoinRequestCount() Optional. Number of pending join requests created using this link - */ -class ChatInviteLink extends Entity -{ - /** - * {@inheritdoc} - */ - protected function subEntities(): array - { - return [ - 'creator' => User::class, - ]; - } -} diff --git a/src/Entities/ChatJoinRequest.php b/src/Entities/ChatJoinRequest.php deleted file mode 100644 index 5d2c3433a..000000000 --- a/src/Entities/ChatJoinRequest.php +++ /dev/null @@ -1,29 +0,0 @@ - Chat::class, - 'from' => User::class, - 'invite_link' => ChatInviteLink::class, - ]; - } -} diff --git a/src/Entities/ChatLocation.php b/src/Entities/ChatLocation.php deleted file mode 100644 index 2f5433dd2..000000000 --- a/src/Entities/ChatLocation.php +++ /dev/null @@ -1,35 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -/** - * Class ChatLocation - * - * Represents a location to which a chat is connected. - * - * @link https://core.telegram.org/bots/api#chatlocation - * - * @method Location getLocation() The location to which the supergroup is connected. Can't be a live location. - * @method string getAddress() Location address; 1-64 characters, as defined by the chat owner - */ -class ChatLocation extends Entity -{ - /** - * {@inheritdoc} - */ - protected function subEntities(): array - { - return [ - 'location' => Location::class, - ]; - } -} diff --git a/src/Entities/ChatMember/ChatMember.php b/src/Entities/ChatMember/ChatMember.php deleted file mode 100644 index 125a6fa63..000000000 --- a/src/Entities/ChatMember/ChatMember.php +++ /dev/null @@ -1,16 +0,0 @@ - User::class, - ]; - } -} diff --git a/src/Entities/ChatMember/ChatMemberBanned.php b/src/Entities/ChatMember/ChatMemberBanned.php deleted file mode 100644 index ed824cd33..000000000 --- a/src/Entities/ChatMember/ChatMemberBanned.php +++ /dev/null @@ -1,28 +0,0 @@ - User::class, - ]; - } -} diff --git a/src/Entities/ChatMember/ChatMemberLeft.php b/src/Entities/ChatMember/ChatMemberLeft.php deleted file mode 100644 index 495fb2bc6..000000000 --- a/src/Entities/ChatMember/ChatMemberLeft.php +++ /dev/null @@ -1,27 +0,0 @@ - User::class, - ]; - } -} diff --git a/src/Entities/ChatMember/ChatMemberMember.php b/src/Entities/ChatMember/ChatMemberMember.php deleted file mode 100644 index 15c82a9cf..000000000 --- a/src/Entities/ChatMember/ChatMemberMember.php +++ /dev/null @@ -1,27 +0,0 @@ - User::class, - ]; - } -} diff --git a/src/Entities/ChatMember/ChatMemberNotImplemented.php b/src/Entities/ChatMember/ChatMemberNotImplemented.php deleted file mode 100644 index 0b3230e17..000000000 --- a/src/Entities/ChatMember/ChatMemberNotImplemented.php +++ /dev/null @@ -1,17 +0,0 @@ - User::class, - ]; - } -} diff --git a/src/Entities/ChatMember/ChatMemberRestricted.php b/src/Entities/ChatMember/ChatMemberRestricted.php deleted file mode 100644 index d4f36b7b3..000000000 --- a/src/Entities/ChatMember/ChatMemberRestricted.php +++ /dev/null @@ -1,60 +0,0 @@ - User::class, - ]; - } - - /** - * True, if the user is allowed to send audios, documents, photos, videos, video notes OR voice notes - * - * @deprecated Use new fine-grained methods provided by Telegram Bot API. - * - * @return bool - */ - public function getCanSendMediaMessages(): bool - { - return $this->getCanSendAudios() || - $this->getCanSendDocuments() || - $this->getCanSendPhotos() || - $this->getCanSendVideos() || - $this->getCanSendVideoNotes() || - $this->getCanSendVoiceNotes(); - } -} diff --git a/src/Entities/ChatMember/Factory.php b/src/Entities/ChatMember/Factory.php deleted file mode 100644 index 4ffe5d315..000000000 --- a/src/Entities/ChatMember/Factory.php +++ /dev/null @@ -1,27 +0,0 @@ - ChatMemberOwner::class, - 'administrator' => ChatMemberAdministrator::class, - 'member' => ChatMemberMember::class, - 'restricted' => ChatMemberRestricted::class, - 'left' => ChatMemberLeft::class, - 'kicked' => ChatMemberBanned::class, - ]; - - if (!isset($type[$data['status'] ?? ''])) { - return new ChatMemberNotImplemented($data, $bot_username); - } - - $class = $type[$data['status']]; - return new $class($data, $bot_username); - } -} diff --git a/src/Entities/ChatMemberUpdated.php b/src/Entities/ChatMemberUpdated.php deleted file mode 100644 index 172ddfb0f..000000000 --- a/src/Entities/ChatMemberUpdated.php +++ /dev/null @@ -1,47 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -use Longman\TelegramBot\Entities\ChatMember\ChatMember; -use Longman\TelegramBot\Entities\ChatMember\Factory as ChatMemberFactory; - -/** - * Class ChatMemberUpdated - * - * Represents changes in the status of a chat member - * - * @link https://core.telegram.org/bots/api#chatmemberupdated - * - * @method Chat getChat() Chat the user belongs to - * @method User getFrom() Performer of the action, which resulted in the change - * @method int getDate() Date the change was done in Unix time - * @method ChatMember getOldChatMember() Previous information about the chat member - * @method ChatMember getNewChatMember() New information about the chat member - * @method ChatInviteLink getInviteLink() Optional. Chat invite link, which was used by the user to join the chat; for joining by invite link events only. - * @method bool getViaChatFolderInviteLink() Optional. True, if the user joined the chat via a chat folder invite link - */ -class ChatMemberUpdated extends Entity -{ - /** - * {@inheritdoc} - */ - protected function subEntities(): array - { - return [ - 'chat' => Chat::class, - 'from' => User::class, - 'old_chat_member' => ChatMemberFactory::class, - 'new_chat_member' => ChatMemberFactory::class, - 'invite_link' => ChatInviteLink::class, - ]; - } -} diff --git a/src/Entities/ChatPermissions.php b/src/Entities/ChatPermissions.php deleted file mode 100644 index 504ccea44..000000000 --- a/src/Entities/ChatPermissions.php +++ /dev/null @@ -1,52 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -/** - * Class ChatPermissions - * - * @link https://core.telegram.org/bots/api#chatpermissions - * - * @method bool getCanSendMessages() Optional. True, if the user is allowed to send text messages, contacts, locations and venues - * @method bool getCanSendAudios() Optional. True, if the user is allowed to send audios - * @method bool getCanSendDocuments() Optional. True, if the user is allowed to send documents - * @method bool getCanSendPhotos() Optional. True, if the user is allowed to send photos - * @method bool getCanSendVideos() Optional. True, if the user is allowed to send videos - * @method bool getCanSendVideoNotes() Optional. True, if the user is allowed to send video notes - * @method bool getCanSendVoiceNotes() Optional. True, if the user is allowed to send voice notes - * @method bool getCanSendPolls() Optional. True, if the user is allowed to send polls, implies can_send_messages - * @method bool getCanSendOtherMessages() Optional. True, if the user is allowed to send animations, games, stickers and use inline bots, implies can_send_media_messages - * @method bool getCanAddWebPagePreviews() Optional. True, if the user is allowed to add web page previews to their messages, implies can_send_media_messages - * @method bool getCanChangeInfo() Optional. True, if the user is allowed to change the chat title, photo and other settings. Ignored in public supergroups - * @method bool getCanInviteUsers() Optional. True, if the user is allowed to invite new users to the chat - * @method bool getCanPinMessages() Optional. True, if the user is allowed to pin messages. Ignored in public supergroups - * @method bool getCanManageTopics() Optional. True, if the user is allowed to create forum topics. If omitted defaults to the value of can_pin_messages - */ -class ChatPermissions extends Entity -{ - /** - * True, if the user is allowed to send audios, documents, photos, videos, video notes OR voice notes - * - * @deprecated Use new fine-grained methods provided by Telegram Bot API. - * - * @return bool - */ - public function getCanSendMediaMessages(): bool - { - return $this->getCanSendAudios() || - $this->getCanSendDocuments() || - $this->getCanSendPhotos() || - $this->getCanSendVideos() || - $this->getCanSendVideoNotes() || - $this->getCanSendVoiceNotes(); - } -} diff --git a/src/Entities/ChatPhoto.php b/src/Entities/ChatPhoto.php deleted file mode 100644 index eb5d54a8a..000000000 --- a/src/Entities/ChatPhoto.php +++ /dev/null @@ -1,27 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -/** - * Class ChatPhoto - * - * @link https://core.telegram.org/bots/api#chatphoto - * - * @method string getSmallFileId() File identifier of small (160x160) chat photo. This file_id can be used only for photo download and only for as long as the photo is not changed. - * @method string getSmallFileUniqueId() Unique file identifier of small (160x160) chat photo, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. - * @method string getBigFileId() File identifier of big (640x640) chat photo. This file_id can be used only for photo download and only for as long as the photo is not changed. - * @method string getBigFileUniqueId() Unique file identifier of big (640x640) chat photo, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. - */ -class ChatPhoto extends Entity -{ - -} diff --git a/src/Entities/ChatShared.php b/src/Entities/ChatShared.php deleted file mode 100644 index 5987d36db..000000000 --- a/src/Entities/ChatShared.php +++ /dev/null @@ -1,18 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -/** - * Class ChosenInlineResult - * - * @link https://core.telegram.org/bots/api#choseninlineresult - * - * @method string getResultId() The unique identifier for the result that was chosen - * @method User getFrom() The user that chose the result - * @method Location getLocation() Optional. Sender location, only for bots that require user location - * @method string getInlineMessageId() Optional. Identifier of the sent inline message. Available only if there is an inline keyboard attached to the message. Will be also received in callback queries and can be used to edit the message. - * @method string getQuery() The query that was used to obtain the result - */ -class ChosenInlineResult extends Entity -{ - /** - * {@inheritdoc} - */ - protected function subEntities(): array - { - return [ - 'from' => User::class, - 'location' => Location::class, - ]; - } -} diff --git a/src/Entities/Contact.php b/src/Entities/Contact.php deleted file mode 100644 index d51056d83..000000000 --- a/src/Entities/Contact.php +++ /dev/null @@ -1,28 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -/** - * Class Contact - * - * @link https://core.telegram.org/bots/api#contact - * - * @method string getPhoneNumber() Contact's phone number - * @method string getFirstName() Contact's first name - * @method string getLastName() Optional. Contact's last name - * @method int getUserId() Optional. Contact's user identifier in Telegram - * @method string getVcard() Optional. Additional data about the contact in the form of a vCard - */ -class Contact extends Entity -{ - -} diff --git a/src/Entities/Dice.php b/src/Entities/Dice.php deleted file mode 100644 index 52ee5870d..000000000 --- a/src/Entities/Dice.php +++ /dev/null @@ -1,27 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -/** - * Class Dice - * - * This entity represents a dice with random value from 1 to 6. - * - * @link https://core.telegram.org/bots/api#dice - * - * @method string getEmoji() Emoji on which the dice throw animation is based - * @method int getValue() Value of the dice, 1-6 for “🎲” and “🎯” base emoji, 1-5 for “🏀” and “⚽” base emoji, 1-64 for “🎰” base emoji - */ -class Dice extends Entity -{ - -} diff --git a/src/Entities/Document.php b/src/Entities/Document.php deleted file mode 100644 index aff0b7d93..000000000 --- a/src/Entities/Document.php +++ /dev/null @@ -1,37 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -/** - * Class Document - * - * @link https://core.telegram.org/bots/api#document - * - * @method string getFileId() Identifier for this file, which can be used to download or reuse the file - * @method string getFileUniqueId() Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. - * @method PhotoSize getThumbnail() Optional. Document thumbnail as defined by sender - * @method string getFileName() Optional. Original filename as defined by sender - * @method string getMimeType() Optional. MIME type of the file as defined by sender - * @method int getFileSize() Optional. File size - */ -class Document extends Entity -{ - /** - * {@inheritdoc} - */ - protected function subEntities(): array - { - return [ - 'thumbnail' => PhotoSize::class, - ]; - } -} diff --git a/src/Entities/EditedChannelPost.php b/src/Entities/EditedChannelPost.php deleted file mode 100644 index 66036b2c5..000000000 --- a/src/Entities/EditedChannelPost.php +++ /dev/null @@ -1,17 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -class EditedChannelPost extends Message -{ - -} diff --git a/src/Entities/EditedMessage.php b/src/Entities/EditedMessage.php deleted file mode 100644 index 3f652b5c8..000000000 --- a/src/Entities/EditedMessage.php +++ /dev/null @@ -1,17 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -class EditedMessage extends Message -{ - -} diff --git a/src/Entities/Entity.php b/src/Entities/Entity.php deleted file mode 100644 index 378d71518..000000000 --- a/src/Entities/Entity.php +++ /dev/null @@ -1,313 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -use Longman\TelegramBot\Entities\InlineQuery\InlineEntity; -use Longman\TelegramBot\Entities\InputMedia\InputMedia; - -/** - * Class Entity - * - * This is the base class for all entities. - * - * @link https://core.telegram.org/bots/api#available-types - * - * @method array getRawData() Get the raw data passed to this entity - * @method string getBotUsername() Return the bot name passed to this entity - */ -abstract class Entity implements \JsonSerializable -{ - public static $fixThumbnailRename = true; - - public $bot_username = ''; - public $raw_data = []; - - private $fields = []; - - /** - * Entity constructor. - * - * @todo Get rid of the $bot_username, it shouldn't be here! - * - * @param array $data - * @param string $bot_username - */ - public function __construct(array $data, string $bot_username = '') - { - $this->bot_username = $bot_username; - $this->raw_data = $data; - - $this->assignMemberVariables($data); - $this->validate(); - } - - /** - * Dynamically set a field. - * - * @param string $name - * @param mixed $value - * @return void - */ - public function __set(string $name, $value): void - { - $this->fields[$name] = $value; - } - - /** - * Gets a dynamic field. - * - * @param string $name - * @return mixed|null - */ - public function __get(string $name) - { - return $this->fields[$name] ?? null; - } - - /** - * Return the data that should be serialized for Telegram. - * - * @return array - */ - public function jsonSerialize(): array - { - return $this->fields; - } - - /** - * Perform to json - * - * @return string - */ - public function toJson(): string - { - return json_encode($this); - } - - /** - * Perform to string - * - * @return string - */ - public function __toString() - { - return $this->toJson(); - } - - /** - * Helper to set member variables - * - * @param array $data - */ - protected function assignMemberVariables(array $data): void - { - foreach ($data as $key => $value) { - $key = $this->fixThumbnailRename($key); - $this->$key = $value; - } - } - - /** - * Get the list of the properties that are themselves Entities - * - * @return array - */ - protected function subEntities(): array - { - return []; - } - - /** - * Perform any special entity validation - */ - protected function validate(): void - { - } - - /** - * Get a property from the current Entity - * - * @param string $property - * @param mixed $default - * - * @return mixed - */ - public function getProperty(string $property, $default = null) - { - return $this->$property ?? $default; - } - - /** - * Return the variable for the called getter or magically set properties dynamically. - * - * @param $method - * @param $args - * - * @return mixed|null - */ - public function __call($method, $args) - { - $method = $this->fixThumbnailRename($method); - - //Convert method to snake_case (which is the name of the property) - $property_name = mb_strtolower(ltrim(preg_replace('/[A-Z]/', '_$0', substr($method, 3)), '_')); - $property_name = $this->fixThumbnailRename($property_name); - - $action = substr($method, 0, 3); - if ($action === 'get') { - $property = $this->getProperty($property_name); - - if ($property !== null) { - //Get all sub-Entities of the current Entity - $sub_entities = $this->subEntities(); - - if (isset($sub_entities[$property_name])) { - $class = $sub_entities[$property_name]; - - if (is_array($class)) { - return $this->makePrettyObjectArray(reset($class), $property_name); - } - - return Factory::resolveEntityClass($class, $property, $this->getProperty('bot_username')); - } - - return $property; - } - } elseif ($action === 'set') { - // Limit setters to specific classes. - if ($this instanceof InlineEntity || $this instanceof InputMedia || $this instanceof Keyboard || $this instanceof KeyboardButton) { - $this->$property_name = $args[0]; - $this->raw_data[$property_name] = $args[0]; - - return $this; - } - } - - return null; - } - - /** - * BC for renamed thumb -> thumbnail methods and fields - * - * @todo Remove after a few versions. - * - * @param string $name - * @return string - */ - protected function fixThumbnailRename(string $name): string - { - return self::$fixThumbnailRename ? preg_replace('/([Tt])humb(nail)?/', '$1humbnail', $name, -1, $count) : $name; - - /*if ($count) { - // Notify user that there are still outdated method calls? - }*/ - } - - /** - * Return an array of nice objects from an array of object arrays - * - * This method is used to generate pretty object arrays - * mainly for PhotoSize and Entities object arrays. - * - * @param string $class - * @param string $property_name - * - * @return array - */ - protected function makePrettyObjectArray(string $class, string $property_name): array - { - $objects = []; - $bot_username = $this->getProperty('bot_username'); - - $properties = array_filter($this->getProperty($property_name) ?: []); - foreach ($properties as $property) { - $objects[] = Factory::resolveEntityClass($class, $property, $bot_username); - } - - return $objects; - } - - /** - * Escape markdown (v1) special characters - * - * @see https://core.telegram.org/bots/api#markdown-style - * - * @param string $string - * - * @return string - */ - public static function escapeMarkdown(string $string): string - { - return str_replace( - ['[', '`', '*', '_',], - ['\[', '\`', '\*', '\_',], - $string - ); - } - - /** - * Escape markdown (v2) special characters - * - * @see https://core.telegram.org/bots/api#markdownv2-style - * - * @param string $string - * - * @return string - */ - public static function escapeMarkdownV2(string $string): string - { - return str_replace( - ['_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!'], - ['\_', '\*', '\[', '\]', '\(', '\)', '\~', '\`', '\>', '\#', '\+', '\-', '\=', '\|', '\{', '\}', '\.', '\!'], - $string - ); - } - - /** - * Try to mention the user - * - * Mention the user with the username otherwise print first and last name - * if the $escape_markdown argument is true special characters are escaped from the output - * - * @todo What about MarkdownV2? - * - * @param bool $escape_markdown - * - * @return string - */ - public function tryMention($escape_markdown = false): string - { - // TryMention only makes sense for the User and Chat entity. - if (!($this instanceof User || $this instanceof Chat)) { - return ''; - } - - //Try with the username first... - $name = $this->getProperty('username'); - $is_username = $name !== null; - - if ($name === null) { - //...otherwise try with the names. - $name = $this->getProperty('first_name'); - $last_name = $this->getProperty('last_name'); - if ($last_name !== null) { - $name .= ' ' . $last_name; - } - } - - if ($escape_markdown) { - $name = self::escapeMarkdown($name); - } - - return ($is_username ? '@' : '') . $name; - } -} diff --git a/src/Entities/ExternalReplyInfo.php b/src/Entities/ExternalReplyInfo.php deleted file mode 100644 index e879973da..000000000 --- a/src/Entities/ExternalReplyInfo.php +++ /dev/null @@ -1,74 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -/** - * This object contains information about a message that is being replied to, which may come from another chat or forum topic. - * - * @link https://core.telegram.org/bots/api#externalreplyinfo - * - * @method MessageOrigin getOrigin() Origin of the message replied to by the given message - * @method Chat getChat() Optional. Chat the original message belongs to. Available only if the chat is a supergroup or a channel. - * @method int getMessageId() Optional. Unique message identifier inside the original chat. Available only if the original chat is a supergroup or a channel. - * @method LinkPreviewOptions getLinkPreviewOptions() Optional. Options used for link preview generation for the original message, if it is a text message - * @method Animation getAnimation() Optional. Message is an animation, information about the animation - * @method Audio getAudio() Optional. Message is an audio file, information about the file - * @method Document getDocument() Optional. Message is a general file, information about the file - * @method PhotoSize[] getPhoto() Optional. Message is a photo, available sizes of the photo - * @method Sticker getSticker() Optional. Message is a sticker, information about the sticker - * @method Story getStory() Optional. Message is a forwarded story - * @method Video getVideo() Optional. Message is a video, information about the video - * @method VideoNote getVideoNote() Optional. Message is a video note, information about the video message - * @method Voice getVoice() Optional. Message is a voice message, information about the file - * @method bool getHasMediaSpoiler() Optional. True, if the message media is covered by a spoiler animation - * @method Contact getContact() Optional. Message is a shared contact, information about the contact - * @method Dice getDice() Optional. Message is a dice with random value - * @method Game getGame() Optional. Message is a game, information about the game. More about games » - * @method Giveaway getGiveaway() Optional. Message is a scheduled giveaway, information about the giveaway - * @method GiveawayWinners getGiveawayWinners() Optional. A giveaway with public winners was completed - * @method Invoice getInvoice() Optional. Message is an invoice for a payment, information about the invoice. More about payments » - * @method Location getLocation() Optional. Message is a shared location, information about the location - * @method Poll getPoll() Optional. Message is a native poll, information about the poll - * @method Venue getVenue() Optional. Message is a venue, information about the venue - */ -class ExternalReplyInfo extends Entity -{ - /** - * {@inheritdoc} - */ - protected function subEntities(): array - { - return [ - 'origin' => MessageOrigin::class, - 'chat' => Chat::class, - 'link_preview_options' => LinkPreviewOptions::class, - 'animation' => Animation::class, - 'audio' => Audio::class, - 'document' => Document::class, - 'photo' => [PhotoSize::class], - 'sticker' => Sticker::class, - 'story' => Story::class, - 'video' => Video::class, - 'video_note' => VideoNote::class, - 'voice' => Voice::class, - 'contact' => Contact::class, - 'dice' => Dice::class, - 'game' => Game::class, - 'giveaway' => Giveaway::class, - 'giveaway_winners' => GiveawayWinners::class, - 'invoice' => Invoice::class, - 'location' => Location::class, - 'poll' => Poll::class, - 'venue' => Venue::class, - ]; - } -} diff --git a/src/Entities/Factory.php b/src/Entities/Factory.php deleted file mode 100644 index 0741ef999..000000000 --- a/src/Entities/Factory.php +++ /dev/null @@ -1,21 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -/** - * Class File - * - * @link https://core.telegram.org/bots/api#file - * - * @method string getFileId() Identifier for this file, which can be used to download or reuse the file - * @method string getFileUniqueId() Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. - * @method int getFileSize() Optional. File size, if known - * @method string getFilePath() Optional. File path. Use https://api.telegram.org/file/bot/ to get the file. - */ -class File extends Entity -{ - -} diff --git a/src/Entities/Games/CallbackGame.php b/src/Entities/Games/CallbackGame.php deleted file mode 100644 index 078d80746..000000000 --- a/src/Entities/Games/CallbackGame.php +++ /dev/null @@ -1,26 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities\Games; - -use Longman\TelegramBot\Entities\Entity; - -/** - * Class CallbackGame - * - * A placeholder, currently holds no information. Use BotFather to set up your game. - * - * @link https://core.telegram.org/bots/api#callbackgame - **/ -class CallbackGame extends Entity -{ - -} diff --git a/src/Entities/Games/Game.php b/src/Entities/Games/Game.php deleted file mode 100644 index cb114941c..000000000 --- a/src/Entities/Games/Game.php +++ /dev/null @@ -1,46 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities\Games; - -use Longman\TelegramBot\Entities\Animation; -use Longman\TelegramBot\Entities\Entity; -use Longman\TelegramBot\Entities\MessageEntity; -use Longman\TelegramBot\Entities\PhotoSize; - -/** - * Class Game - * - * This object represents a game. Use BotFather to create and edit games, their short names will act as unique identifiers. - * - * @link https://core.telegram.org/bots/api#game - * - * @method string getTitle() Title of the game - * @method string getDescription() Description of the game - * @method PhotoSize[] getPhoto() Photo that will be displayed in the game message in chats. - * @method string getText() Optional. Brief description of the game or high scores included in the game message. Can be automatically edited to include current high scores for the game when the bot calls setGameScore, or manually edited using editMessageText. 0-4096 characters. - * @method MessageEntity[] getTextEntities() Optional. Special entities that appear in text, such as usernames, URLs, bot commands, etc. - * @method Animation getAnimation() Optional. Animation that will be displayed in the game message in chats. Upload via BotFather - **/ -class Game extends Entity -{ - /** - * {@inheritdoc} - */ - protected function subEntities(): array - { - return [ - 'photo' => [PhotoSize::class], - 'text_entities' => [MessageEntity::class], - 'animation' => Animation::class, - ]; - } -} diff --git a/src/Entities/Games/GameHighScore.php b/src/Entities/Games/GameHighScore.php deleted file mode 100644 index 71760010a..000000000 --- a/src/Entities/Games/GameHighScore.php +++ /dev/null @@ -1,39 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities\Games; - -use Longman\TelegramBot\Entities\Entity; -use Longman\TelegramBot\Entities\User; - -/** - * Class GameHighScore - * - * This object represents one row of the high scores table for a game. - * - * @link https://core.telegram.org/bots/api#gamehighscore - * - * @method int getPosition() Position in high score table for the game - * @method User getUser() User - * @method int getScore() Score - **/ -class GameHighScore extends Entity -{ - /** - * {@inheritdoc} - */ - protected function subEntities(): array - { - return [ - 'user' => User::class, - ]; - } -} diff --git a/src/Entities/Giveaway/Giveaway.php b/src/Entities/Giveaway/Giveaway.php deleted file mode 100644 index bd7fe9311..000000000 --- a/src/Entities/Giveaway/Giveaway.php +++ /dev/null @@ -1,30 +0,0 @@ - getChats() The list of chats which the user must join to participate in the giveaway - * @method int getWinnersSelectionDate() Point in time (Unix timestamp) when winners of the giveaway will be selected - * @method int getWinnerCount() The number of users which are supposed to be selected as winners of the giveaway - * @method bool getOnlyNewMembers() Optional. True, if only users who join the chats after the giveaway started should be eligible to win - * @method bool getHasPublicWinners() Optional. True, if the list of giveaway winners will be visible to everyone - * @method string getPrizeDescription() Optional. Description of additional giveaway prize - * @method array getCountryCodes() Optional. A list of two-letter ISO 3166-1 alpha-2 country codes indicating the countries from which eligible users for the giveaway must come. If empty, then all users can participate in the giveaway. Users with a phone number that was bought on Fragment can always participate in giveaways. - * @method int getPremiumSubscriptionMonthCount() Optional. The number of months the Telegram Premium subscription won from the giveaway will be active for - */ -class Giveaway extends Entity -{ - protected function subEntities(): array - { - return [ - 'chats' => [Chat::class], - ]; - } -} diff --git a/src/Entities/Giveaway/GiveawayCompleted.php b/src/Entities/Giveaway/GiveawayCompleted.php deleted file mode 100644 index 294500723..000000000 --- a/src/Entities/Giveaway/GiveawayCompleted.php +++ /dev/null @@ -1,25 +0,0 @@ - Message::class, - ]; - } -} diff --git a/src/Entities/Giveaway/GiveawayCreated.php b/src/Entities/Giveaway/GiveawayCreated.php deleted file mode 100644 index c4ec8dee3..000000000 --- a/src/Entities/Giveaway/GiveawayCreated.php +++ /dev/null @@ -1,10 +0,0 @@ - getWinners() List of up to 100 winners of the giveaway - * @method int getAdditionalChatCount() Optional. The number of other chats the user had to join in order to be eligible for the giveaway - * @method int getPremiumSubscriptionMonthCount() Optional. The number of months the Telegram Premium subscription won from the giveaway will be active for - * @method int getUnclaimedPrizeCount() Optional. Number of undistributed prizes - * @method bool getOnlyNewMembers() Optional. True, if only users who had joined the chats after the giveaway started were eligible to win - * @method bool getWasRefunded() Optional. True, if the giveaway was canceled because the payment for it was refunded - * @method string getPrizeDescription() Optional. Description of additional giveaway prize - */ -class GiveawayWinners extends Entity -{ - protected function subEntities(): array - { - return [ - 'chat' => Chat::class, - 'winners' => [User::class], - ]; - } -} diff --git a/src/Entities/InlineKeyboard.php b/src/Entities/InlineKeyboard.php deleted file mode 100644 index 30feea28a..000000000 --- a/src/Entities/InlineKeyboard.php +++ /dev/null @@ -1,21 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -/** - * Class InlineKeyboard - * - * @link https://core.telegram.org/bots/api#inlinekeyboardmarkup - */ -class InlineKeyboard extends Keyboard -{ -} diff --git a/src/Entities/InlineKeyboardButton.php b/src/Entities/InlineKeyboardButton.php deleted file mode 100644 index e23439e5c..000000000 --- a/src/Entities/InlineKeyboardButton.php +++ /dev/null @@ -1,80 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -use Longman\TelegramBot\Entities\Games\CallbackGame; -use Longman\TelegramBot\Exception\TelegramException; - -/** - * Class InlineKeyboardButton - * - * @link https://core.telegram.org/bots/api#inlinekeyboardbutton - * - * @method string getText() Label text on the button - * @method string getUrl() Optional. HTTP url to be opened when button is pressed - * @method LoginUrl getLoginUrl() Optional. An HTTP URL used to automatically authorize the user. Can be used as a replacement for the Telegram Login Widget. - * @method string getCallbackData() Optional. Data to be sent in a callback query to the bot when button is pressed, 1-64 bytes - * @method WebAppInfo getWebApp() 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. - * @method string getSwitchInlineQuery() 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. Can be empty, in which case just the bot’s username will be inserted. - * @method string getSwitchInlineQueryCurrentChat() Optional. If set, pressing the button will insert the bot‘s username and the specified inline query in the current chat's input field. Can be empty, in which case only the bot’s username will be inserted. - * @method SwitchInlineQueryChosenChat getSwitchInlineQueryChosenChat() 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 - * @method CallbackGame getCallbackGame() Optional. Description of the game that will be launched when the user presses the button. - * @method bool getPay() Optional. Specify True, to send a Pay button. - * - * @method $this setText(string $text) Label text on the button - * @method $this setUrl(string $url) Optional. HTTP url to be opened when button is pressed - * @method $this setLoginUrl(LoginUrl $login_url) Optional. HTTP url to be opened when button is pressed - * @method $this setCallbackData(string $callback_data) Optional. Data to be sent in a callback query to the bot when button is pressed, 1-64 bytes - * @method $this setWebApp(WebAppInfo $web_app) 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. - * @method $this setSwitchInlineQuery(string $switch_inline_query) 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. Can be empty, in which case just the bot’s username will be inserted. - * @method $this setSwitchInlineQueryCurrentChat(string $switch_inline_query_current_chat) Optional. If set, pressing the button will insert the bot‘s username and the specified inline query in the current chat's input field. Can be empty, in which case only the bot’s username will be inserted. - * @method $this setSwitchInlineQueryChosenChat(SwitchInlineQueryChosenChat $switch_inline_query_chosen_chat) 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 - * @method $this setCallbackGame(CallbackGame $callback_game) Optional. Description of the game that will be launched when the user presses the button. - * @method $this setPay(bool $pay) Optional. Specify True, to send a Pay button. - */ -class InlineKeyboardButton extends KeyboardButton -{ - /** - * Check if the passed data array could be an InlineKeyboardButton. - * - * @param array $data - * - * @return bool - */ - public static function couldBe(array $data): bool - { - return array_key_exists('text', $data) && ( - array_key_exists('url', $data) || - array_key_exists('login_url', $data) || - array_key_exists('callback_data', $data) || - array_key_exists('web_app', $data) || - array_key_exists('switch_inline_query', $data) || - array_key_exists('switch_inline_query_current_chat', $data) || - array_key_exists('switch_inline_query_chosen_chat', $data) || - array_key_exists('callback_game', $data) || - array_key_exists('pay', $data) - ); - } - - /** - * {@inheritdoc} - */ - public function __call($method, $args) - { - // Only 1 of these can be set, so clear the others when setting a new one. - if (in_array($method, ['setUrl', 'setLoginUrl', 'setCallbackData', 'setWebApp', 'setSwitchInlineQuery', 'setSwitchInlineQueryCurrentChat', 'setSwitchInlineQueryChosenChat', 'setCallbackGame', 'setPay'], true)) { - unset($this->url, $this->login_url, $this->callback_data, $this->web_app, $this->switch_inline_query, $this->switch_inline_query_current_chat, $this->switch_inline_query_chosen_chat, $this->callback_game, $this->pay); - } - - return parent::__call($method, $args); - } -} diff --git a/src/Entities/InlineQuery.php b/src/Entities/InlineQuery.php deleted file mode 100644 index b6e397cfe..000000000 --- a/src/Entities/InlineQuery.php +++ /dev/null @@ -1,57 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -use Longman\TelegramBot\Entities\InlineQuery\InlineQueryResult; -use Longman\TelegramBot\Request; - -/** - * Class InlineQuery - * - * @link https://core.telegram.org/bots/api#inlinequery - * - * @method string getId() Unique identifier for this query - * @method User getFrom() Sender - * @method Location getLocation() Optional. Sender location, only for bots that request user location - * @method string getQuery() Text of the query (up to 512 characters) - * @method string getOffset() Offset of the results to be returned, can be controlled by the bot - * @method string getChatType() Optional. Type of the chat, from which the inline query was sent. Can be either “sender” for a private chat with the inline query sender, “private”, “group”, “supergroup”, or “channel”. The chat type should be always known for requests sent from official clients and most third-party clients, unless the request was sent from a secret chat - */ -class InlineQuery extends Entity -{ - /** - * {@inheritdoc} - */ - protected function subEntities(): array - { - return [ - 'from' => User::class, - 'location' => Location::class, - ]; - } - - /** - * Answer this inline query with the passed results. - * - * @param InlineQueryResult[] $results - * @param array $data - * - * @return ServerResponse - */ - public function answer(array $results, array $data = []): ServerResponse - { - return Request::answerInlineQuery(array_merge([ - 'inline_query_id' => $this->getId(), - 'results' => $results, - ], $data)); - } -} diff --git a/src/Entities/InlineQuery/InlineEntity.php b/src/Entities/InlineQuery/InlineEntity.php deleted file mode 100644 index 80e6eeb06..000000000 --- a/src/Entities/InlineQuery/InlineEntity.php +++ /dev/null @@ -1,24 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities\InlineQuery; - -use Longman\TelegramBot\Entities\Entity; - -/** - * Class InlineEntity - * - * This is the base class for all inline entities. - */ -abstract class InlineEntity extends Entity -{ - -} diff --git a/src/Entities/InlineQuery/InlineQueryResult.php b/src/Entities/InlineQuery/InlineQueryResult.php deleted file mode 100644 index f8e038ea8..000000000 --- a/src/Entities/InlineQuery/InlineQueryResult.php +++ /dev/null @@ -1,8 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities\InlineQuery; - -use Longman\TelegramBot\Entities\InlineKeyboard; -use Longman\TelegramBot\Entities\InputMessageContent\InputMessageContent; - -/** - * Class InlineQueryResultArticle - * - * @link https://core.telegram.org/bots/api#inlinequeryresultarticle - * - * - * $data = [ - * 'id' => '', - * 'title' => '', - * 'input_message_content' => , - * 'reply_markup' => , - * 'url' => '', - * 'hide_url' => true, - * 'description' => '', - * 'thumbnail_url' => '', - * 'thumbnail_width' => 30, - * 'thumbnail_height' => 30, - * ]; - * - * - * @method string getType() Type of the result, must be article - * @method string getId() Unique identifier for this result, 1-64 Bytes - * @method string getTitle() Title of the result - * @method InputMessageContent getInputMessageContent() Content of the message to be sent - * @method InlineKeyboard getReplyMarkup() Optional. Inline keyboard attached to the message - * @method string getUrl() Optional. URL of the result - * @method bool getHideUrl() Optional. Pass True, if you don't want the URL to be shown in the message - * @method string getDescription() Optional. Short description of the result - * @method string getThumbnailUrl() Optional. Url of the thumbnail for the result - * @method int getThumbnailWidth() Optional. Thumbnail width - * @method int getThumbnailHeight() Optional. Thumbnail height - * - * @method $this setId(string $id) Unique identifier for this result, 1-64 Bytes - * @method $this setTitle(string $title) Title of the result - * @method $this setInputMessageContent(InputMessageContent $input_message_content) Content of the message to be sent - * @method $this setReplyMarkup(InlineKeyboard $reply_markup) Optional. Inline keyboard attached to the message - * @method $this setUrl(string $url) Optional. URL of the result - * @method $this setHideUrl(bool $hide_url) Optional. Pass True, if you don't want the URL to be shown in the message - * @method $this setDescription(string $description) Optional. Short description of the result - * @method $this setThumbnailUrl(string $thumbnail_url) Optional. Url of the thumbnail for the result - * @method $this setThumbnailWidth(int $thumbnail_width) Optional. Thumbnail width - * @method $this setThumbnailHeight(int $thumbnail_height) Optional. Thumbnail height - */ -class InlineQueryResultArticle extends InlineEntity implements InlineQueryResult -{ - /** - * InlineQueryResultArticle constructor - * - * @param array $data - */ - public function __construct(array $data = []) - { - $data['type'] = 'article'; - parent::__construct($data); - } -} diff --git a/src/Entities/InlineQuery/InlineQueryResultAudio.php b/src/Entities/InlineQuery/InlineQueryResultAudio.php deleted file mode 100644 index c0a38e2b8..000000000 --- a/src/Entities/InlineQuery/InlineQueryResultAudio.php +++ /dev/null @@ -1,70 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities\InlineQuery; - -use Longman\TelegramBot\Entities\InlineKeyboard; -use Longman\TelegramBot\Entities\InputMessageContent\InputMessageContent; - -/** - * Class InlineQueryResultAudio - * - * @link https://core.telegram.org/bots/api#inlinequeryresultaudio - * - * - * $data = [ - * 'id' => '', - * 'audio_url' => '', - * 'title' => '', - * 'caption' => '', - * 'performer' => '', - * 'audio_duration' => 123, - * 'reply_markup' => , - * 'input_message_content' => , - * ]; - * - * - * @method string getType() Type of the result, must be audio - * @method string getId() Unique identifier for this result, 1-64 bytes - * @method string getAudioUrl() A valid URL for the audio file - * @method string getTitle() Title - * @method string getCaption() Optional. Caption, 0-200 characters - * @method string getParseMode() Optional. Mode for parsing entities in the audio caption - * @method MessageEntity[] getCaptionEntities() Optional. List of special entities that appear in the caption, which can be specified instead of parse_mode - * @method string getPerformer() Optional. Performer - * @method int getAudioDuration() Optional. Audio duration in seconds - * @method InlineKeyboard getReplyMarkup() Optional. Inline keyboard attached to the message - * @method InputMessageContent getInputMessageContent() Optional. Content of the message to be sent instead of the audio - * - * @method $this setId(string $id) Unique identifier for this result, 1-64 bytes - * @method $this setAudioUrl(string $audio_url) A valid URL for the audio file - * @method $this setTitle(string $title) Title - * @method $this setCaption(string $caption) Optional. Caption, 0-200 characters - * @method $this setParseMode(string $parse_mode) Optional. Mode for parsing entities in the audio caption - * @method $this setCaptionEntities(array $caption_entities) Optional. List of special entities that appear in the caption, which can be specified instead of parse_mode - * @method $this setPerformer(string $performer) Optional. Performer - * @method $this setAudioDuration(int $audio_duration) Optional. Audio duration in seconds - * @method $this setReplyMarkup(InlineKeyboard $reply_markup) Optional. Inline keyboard attached to the message - * @method $this setInputMessageContent(InputMessageContent $input_message_content) Optional. Content of the message to be sent instead of the audio - */ -class InlineQueryResultAudio extends InlineEntity implements InlineQueryResult -{ - /** - * InlineQueryResultAudio constructor - * - * @param array $data - */ - public function __construct(array $data = []) - { - $data['type'] = 'audio'; - parent::__construct($data); - } -} diff --git a/src/Entities/InlineQuery/InlineQueryResultCachedAudio.php b/src/Entities/InlineQuery/InlineQueryResultCachedAudio.php deleted file mode 100644 index 0f5525712..000000000 --- a/src/Entities/InlineQuery/InlineQueryResultCachedAudio.php +++ /dev/null @@ -1,61 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities\InlineQuery; - -use Longman\TelegramBot\Entities\InlineKeyboard; -use Longman\TelegramBot\Entities\InputMessageContent\InputMessageContent; - -/** - * Class InlineQueryResultCachedAudio - * - * @link https://core.telegram.org/bots/api#inlinequeryresultcachedaudio - * - * - * $data = [ - * 'id' => '', - * 'audio_file_id' => '', - * 'caption' => '', - * 'reply_markup' => , - * 'input_message_content' => , - * ]; - * - * - * @method string getType() Type of the result, must be audio - * @method string getId() Unique identifier for this result, 1-64 bytes - * @method string getAudioFileId() A valid file identifier for the audio file - * @method string getCaption() Optional. Caption, 0-200 characters - * @method string getParseMode() Optional. Mode for parsing entities in the audio caption - * @method MessageEntity[] getCaptionEntities() Optional. List of special entities that appear in the caption, which can be specified instead of parse_mode - * @method InlineKeyboard getReplyMarkup() Optional. An Inline keyboard attached to the message - * @method InputMessageContent getInputMessageContent() Optional. Content of the message to be sent instead of the audio - * - * @method $this setId(string $id) Unique identifier for this result, 1-64 bytes - * @method $this setAudioFileId(string $audio_file_id) A valid file identifier for the audio file - * @method $this setCaption(string $caption) Optional. Caption, 0-200 characters - * @method $this setParseMode(string $parse_mode) Optional. Mode for parsing entities in the audio caption - * @method $this setCaptionEntities(array $caption_entities) Optional. List of special entities that appear in the caption, which can be specified instead of parse_mode - * @method $this setReplyMarkup(InlineKeyboard $reply_markup) Optional. An Inline keyboard attached to the message - * @method $this setInputMessageContent(InputMessageContent $input_message_content) Optional. Content of the message to be sent instead of the audio - */ -class InlineQueryResultCachedAudio extends InlineEntity implements InlineQueryResult -{ - /** - * InlineQueryResultCachedAudio constructor - * - * @param array $data - */ - public function __construct(array $data = []) - { - $data['type'] = 'audio'; - parent::__construct($data); - } -} diff --git a/src/Entities/InlineQuery/InlineQueryResultCachedDocument.php b/src/Entities/InlineQuery/InlineQueryResultCachedDocument.php deleted file mode 100644 index e16b9bf64..000000000 --- a/src/Entities/InlineQuery/InlineQueryResultCachedDocument.php +++ /dev/null @@ -1,67 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities\InlineQuery; - -use Longman\TelegramBot\Entities\InlineKeyboard; -use Longman\TelegramBot\Entities\InputMessageContent\InputMessageContent; - -/** - * Class InlineQueryResultCachedDocument - * - * @link https://core.telegram.org/bots/api#inlinequeryresultcacheddocument - * - * - * $data = [ - * 'id' => '', - * 'title' => '', - * 'document_file_id' => '', - * 'description' => '', - * 'caption' => '', - * 'reply_markup' => , - * 'input_message_content' => , - * ]; - * - * - * @method string getType() Type of the result, must be document - * @method string getId() Unique identifier for this result, 1-64 bytes - * @method string getTitle() Title for the result - * @method string getDocumentFileId() A valid file identifier for the file - * @method string getDescription() Optional. Short description of the result - * @method string getCaption() Optional. Caption of the document to be sent, 0-200 characters - * @method string getParseMode() Optional. Mode for parsing entities in the document caption - * @method MessageEntity[] getCaptionEntities() Optional. List of special entities that appear in the caption, which can be specified instead of parse_mode - * @method InlineKeyboard getReplyMarkup() Optional. An Inline keyboard attached to the message - * @method InputMessageContent getInputMessageContent() Optional. Content of the message to be sent instead of the file - * - * @method $this setId(string $id) Unique identifier for this result, 1-64 bytes - * @method $this setTitle(string $title) Title for the result - * @method $this setDocumentFileId(string $document_file_id) A valid file identifier for the file - * @method $this setDescription(string $description) Optional. Short description of the result - * @method $this setCaption(string $caption) Optional. Caption of the document to be sent, 0-200 characters - * @method $this setParseMode(string $parse_mode) Optional. Mode for parsing entities in the document caption - * @method $this setCaptionEntities(array $caption_entities) Optional. List of special entities that appear in the caption, which can be specified instead of parse_mode - * @method $this setReplyMarkup(InlineKeyboard $reply_markup) Optional. An Inline keyboard attached to the message - * @method $this setInputMessageContent(InputMessageContent $input_message_content) Optional. Content of the message to be sent instead of the file - */ -class InlineQueryResultCachedDocument extends InlineEntity implements InlineQueryResult -{ - /** - * InlineQueryResultCachedDocument constructor - * - * @param array $data - */ - public function __construct(array $data = []) - { - $data['type'] = 'document'; - parent::__construct($data); - } -} diff --git a/src/Entities/InlineQuery/InlineQueryResultCachedGif.php b/src/Entities/InlineQuery/InlineQueryResultCachedGif.php deleted file mode 100644 index f1b114b3f..000000000 --- a/src/Entities/InlineQuery/InlineQueryResultCachedGif.php +++ /dev/null @@ -1,64 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities\InlineQuery; - -use Longman\TelegramBot\Entities\InlineKeyboard; -use Longman\TelegramBot\Entities\InputMessageContent\InputMessageContent; - -/** - * Class InlineQueryResultCachedGif - * - * @link https://core.telegram.org/bots/api#inlinequeryresultcachedgif - * - * - * $data = [ - * 'id' => '', - * 'gif_file_id' => '', - * 'title' => '', - * 'caption' => '', - * 'reply_markup' => , - * 'input_message_content' => , - * ]; - * - * - * @method string getType() Type of the result, must be gif - * @method string getId() Unique identifier for this result, 1-64 bytes - * @method string getGifFileId() A valid file identifier for the GIF file - * @method string getTitle() Optional. Title for the result - * @method string getCaption() Optional. Caption of the GIF file to be sent, 0-200 characters - * @method string getParseMode() Optional. Mode for parsing entities in the caption - * @method MessageEntity[] getCaptionEntities() Optional. List of special entities that appear in the caption, which can be specified instead of parse_mode - * @method InlineKeyboard getReplyMarkup() Optional. An Inline keyboard attached to the message - * @method InputMessageContent getInputMessageContent() Optional. Content of the message to be sent instead of the GIF animation - * - * @method $this setId(string $id) Unique identifier for this result, 1-64 bytes - * @method $this setGifFileId(string $gif_file_id) A valid file identifier for the GIF file - * @method $this setTitle(string $title) Optional. Title for the result - * @method $this setCaption(string $caption) Optional. Caption of the GIF file to be sent, 0-200 characters - * @method $this setParseMode(string $parse_mode) Optional. Mode for parsing entities in the caption - * @method $this setCaptionEntities(array $caption_entities) Optional. List of special entities that appear in the caption, which can be specified instead of parse_mode - * @method $this setReplyMarkup(InlineKeyboard $reply_markup) Optional. An Inline keyboard attached to the message - * @method $this setInputMessageContent(InputMessageContent $input_message_content) Optional. Content of the message to be sent instead of the GIF animation - */ -class InlineQueryResultCachedGif extends InlineEntity implements InlineQueryResult -{ - /** - * InlineQueryResultCachedGif constructor - * - * @param array $data - */ - public function __construct(array $data = []) - { - $data['type'] = 'gif'; - parent::__construct($data); - } -} diff --git a/src/Entities/InlineQuery/InlineQueryResultCachedMpeg4Gif.php b/src/Entities/InlineQuery/InlineQueryResultCachedMpeg4Gif.php deleted file mode 100644 index cf01fcf6f..000000000 --- a/src/Entities/InlineQuery/InlineQueryResultCachedMpeg4Gif.php +++ /dev/null @@ -1,64 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities\InlineQuery; - -use Longman\TelegramBot\Entities\InlineKeyboard; -use Longman\TelegramBot\Entities\InputMessageContent\InputMessageContent; - -/** - * Class InlineQueryResultCachedMpeg4Gif - * - * @link https://core.telegram.org/bots/api#inlinequeryresultcachedmpeg4gif - * - * - * $data = [ - * 'id' => '', - * 'mpeg4_file_id' => '', - * 'title' => '', - * 'caption' => '', - * 'reply_markup' => , - * 'input_message_content' => , - * ]; - * - * - * @method string getType() Type of the result, must be mpeg4_gif - * @method string getId() Unique identifier for this result, 1-64 bytes - * @method string getMpeg4FileId() A valid file identifier for the MP4 file - * @method string getTitle() Optional. Title for the result - * @method string getCaption() Optional. Caption of the MPEG-4 file to be sent, 0-200 characters - * @method string getParseMode() Optional. Mode for parsing entities in the caption - * @method MessageEntity[] getCaptionEntities() Optional. List of special entities that appear in the caption, which can be specified instead of parse_mode - * @method InlineKeyboard getReplyMarkup() Optional. An Inline keyboard attached to the message - * @method InputMessageContent getInputMessageContent() Optional. Content of the message to be sent instead of the video animation - * - * @method $this setId(string $id) Unique identifier for this result, 1-64 bytes - * @method $this setMpeg4FileId(string $mpeg4_file_id) A valid file identifier for the MP4 file - * @method $this setTitle(string $title) Optional. Title for the result - * @method $this setCaption(string $caption) Optional. Caption of the MPEG-4 file to be sent, 0-200 characters - * @method $this setParseMode(string $parse_mode) Optional. Mode for parsing entities in the caption - * @method $this setCaptionEntities(array $caption_entities) Optional. List of special entities that appear in the caption, which can be specified instead of parse_mode - * @method $this setReplyMarkup(InlineKeyboard $reply_markup) Optional. An Inline keyboard attached to the message - * @method $this setInputMessageContent(InputMessageContent $input_message_content) Optional. Content of the message to be sent instead of the video animation - */ -class InlineQueryResultCachedMpeg4Gif extends InlineEntity implements InlineQueryResult -{ - /** - * InlineQueryResultCachedMpeg4Gif constructor - * - * @param array $data - */ - public function __construct(array $data = []) - { - $data['type'] = 'mpeg4_gif'; - parent::__construct($data); - } -} diff --git a/src/Entities/InlineQuery/InlineQueryResultCachedPhoto.php b/src/Entities/InlineQuery/InlineQueryResultCachedPhoto.php deleted file mode 100644 index 0eda7b59b..000000000 --- a/src/Entities/InlineQuery/InlineQueryResultCachedPhoto.php +++ /dev/null @@ -1,67 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities\InlineQuery; - -use Longman\TelegramBot\Entities\InlineKeyboard; -use Longman\TelegramBot\Entities\InputMessageContent\InputMessageContent; - -/** - * Class InlineQueryResultCachedPhoto - * - * @link https://core.telegram.org/bots/api#inlinequeryresultcachedphoto - * - * - * $data = [ - * 'id' => '', - * 'photo_file_id' => '', - * 'title' => '', - * 'description' => '', - * 'caption' => '', - * 'reply_markup' => , - * 'input_message_content' => , - * ]; - * - * - * @method string getType() Type of the result, must be photo - * @method string getId() Unique identifier for this result, 1-64 bytes - * @method string getPhotoFileId() A valid file identifier of the photo - * @method string getTitle() Optional. Title for the result - * @method string getDescription() Optional. Short description of the result - * @method string getCaption() Optional. Caption of the photo to be sent, 0-200 characters - * @method string getParseMode() Optional. Mode for parsing entities in the photo caption - * @method MessageEntity[] getCaptionEntities() Optional. List of special entities that appear in the caption, which can be specified instead of parse_mode - * @method InlineKeyboard getReplyMarkup() Optional. Inline keyboard attached to the message - * @method InputMessageContent getInputMessageContent() Optional. Content of the message to be sent instead of the photo - * - * @method $this setId(string $id) Unique identifier for this result, 1-64 bytes - * @method $this setPhotoFileId(string $photo_file_id) A valid file identifier of the photo - * @method $this setTitle(string $title) Optional. Title for the result - * @method $this setDescription(string $description) Optional. Short description of the result - * @method $this setCaption(string $caption) Optional. Caption of the photo to be sent, 0-200 characters - * @method $this setParseMode(string $parse_mode) Optional. Mode for parsing entities in the photo caption - * @method $this setCaptionEntities(array $caption_entities) Optional. List of special entities that appear in the caption, which can be specified instead of parse_mode - * @method $this setReplyMarkup(InlineKeyboard $reply_markup) Optional. Inline keyboard attached to the message - * @method $this setInputMessageContent(InputMessageContent $input_message_content) Optional. Content of the message to be sent instead of the photo - */ -class InlineQueryResultCachedPhoto extends InlineEntity implements InlineQueryResult -{ - /** - * InlineQueryResultCachedPhoto constructor - * - * @param array $data - */ - public function __construct(array $data = []) - { - $data['type'] = 'photo'; - parent::__construct($data); - } -} diff --git a/src/Entities/InlineQuery/InlineQueryResultCachedSticker.php b/src/Entities/InlineQuery/InlineQueryResultCachedSticker.php deleted file mode 100644 index 8d7c577c0..000000000 --- a/src/Entities/InlineQuery/InlineQueryResultCachedSticker.php +++ /dev/null @@ -1,54 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities\InlineQuery; - -use Longman\TelegramBot\Entities\InlineKeyboard; -use Longman\TelegramBot\Entities\InputMessageContent\InputMessageContent; - -/** - * Class InlineQueryResultCachedSticker - * - * @link https://core.telegram.org/bots/api#inlinequeryresultcachedsticker - * - * - * $data = [ - * 'id' => '', - * 'sticker_file_id' => '', - * 'reply_markup' => , - * 'input_message_content' => , - * ]; - * - * - * @method string getType() Type of the result, must be sticker - * @method string getId() Unique identifier for this result, 1-64 bytes - * @method string getStickerFileId() A valid file identifier of the sticker - * @method InlineKeyboard getReplyMarkup() Optional. An Inline keyboard attached to the message - * @method InputMessageContent getInputMessageContent() Optional. Content of the message to be sent instead of the sticker - * - * @method $this setId(string $id) Unique identifier for this result, 1-64 bytes - * @method $this setStickerFileId(string $sticker_file_id) A valid file identifier of the sticker - * @method $this setReplyMarkup(InlineKeyboard $reply_markup) Optional. An Inline keyboard attached to the message - * @method $this setInputMessageContent(InputMessageContent $input_message_content) Optional. Content of the message to be sent instead of the sticker - */ -class InlineQueryResultCachedSticker extends InlineEntity implements InlineQueryResult -{ - /** - * InlineQueryResultCachedSticker constructor - * - * @param array $data - */ - public function __construct(array $data = []) - { - $data['type'] = 'sticker'; - parent::__construct($data); - } -} diff --git a/src/Entities/InlineQuery/InlineQueryResultCachedVideo.php b/src/Entities/InlineQuery/InlineQueryResultCachedVideo.php deleted file mode 100644 index 13163c1ac..000000000 --- a/src/Entities/InlineQuery/InlineQueryResultCachedVideo.php +++ /dev/null @@ -1,67 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities\InlineQuery; - -use Longman\TelegramBot\Entities\InlineKeyboard; -use Longman\TelegramBot\Entities\InputMessageContent\InputMessageContent; - -/** - * Class InlineQueryResultCachedVideo - * - * @link https://core.telegram.org/bots/api#inlinequeryresultcachedvideo - * - * - * $data = [ - * 'id' => '', - * 'video_file_id' => '', - * 'title' => '', - * 'description' => '', - * 'caption' => '', - * 'reply_markup' => , - * 'input_message_content' => , - * ]; - * - * - * @method string getType() Type of the result, must be video - * @method string getId() Unique identifier for this result, 1-64 bytes - * @method string getVideoFileId() A valid file identifier for the video file - * @method string getTitle() Title for the result - * @method string getDescription() Optional. Short description of the result - * @method string getCaption() Optional. Caption of the video to be sent, 0-200 characters - * @method string getParseMode() Optional. Mode for parsing entities in the video caption - * @method MessageEntity[] getCaptionEntities() Optional. List of special entities that appear in the caption, which can be specified instead of parse_mode - * @method InlineKeyboard getReplyMarkup() Optional. An Inline keyboard attached to the message - * @method InputMessageContent getInputMessageContent() Optional. Content of the message to be sent instead of the video - * - * @method $this setId(string $id) Unique identifier for this result, 1-64 bytes - * @method $this setVideoFileId(string $video_file_id) A valid file identifier for the video file - * @method $this setTitle(string $title) Title for the result - * @method $this setDescription(string $description) Optional. Short description of the result - * @method $this setCaption(string $caption) Optional. Caption of the video to be sent, 0-200 characters - * @method $this setParseMode(string $parse_mode) Optional. Mode for parsing entities in the video caption - * @method $this setCaptionEntities(array $caption_entities) Optional. List of special entities that appear in the caption, which can be specified instead of parse_mode - * @method $this setReplyMarkup(InlineKeyboard $reply_markup) Optional. An Inline keyboard attached to the message - * @method $this setInputMessageContent(InputMessageContent $input_message_content) Optional. Content of the message to be sent instead of the video - */ -class InlineQueryResultCachedVideo extends InlineEntity implements InlineQueryResult -{ - /** - * InlineQueryResultCachedVideo constructor - * - * @param array $data - */ - public function __construct(array $data = []) - { - $data['type'] = 'video'; - parent::__construct($data); - } -} diff --git a/src/Entities/InlineQuery/InlineQueryResultCachedVoice.php b/src/Entities/InlineQuery/InlineQueryResultCachedVoice.php deleted file mode 100644 index 6ab041487..000000000 --- a/src/Entities/InlineQuery/InlineQueryResultCachedVoice.php +++ /dev/null @@ -1,64 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities\InlineQuery; - -use Longman\TelegramBot\Entities\InlineKeyboard; -use Longman\TelegramBot\Entities\InputMessageContent\InputMessageContent; - -/** - * Class InlineQueryResultCachedVoice - * - * @link https://core.telegram.org/bots/api#inlinequeryresultcachedvoice - * - * - * $data = [ - * 'id' => '', - * 'voice_file_id' => '', - * 'title' => '', - * 'caption' => '', - * 'reply_markup' => , - * 'input_message_content' => , - * ]; - * - * - * @method string getType() Type of the result, must be voice - * @method string getId() Unique identifier for this result, 1-64 bytes - * @method string getVoiceFileId() A valid file identifier for the voice message - * @method string getTitle() Voice message title - * @method string getCaption() Optional. Caption, 0-200 characters - * @method string getParseMode() Optional. Mode for parsing entities in the voice caption - * @method MessageEntity[] getCaptionEntities() Optional. List of special entities that appear in the caption, which can be specified instead of parse_mode - * @method InlineKeyboard getReplyMarkup() Optional. An Inline keyboard attached to the message - * @method InputMessageContent getInputMessageContent() Optional. Content of the message to be sent instead of the voice message - * - * @method $this setId(string $id) Unique identifier for this result, 1-64 bytes - * @method $this setVoiceFileId(string $voice_file_id) A valid file identifier for the voice message - * @method $this setTitle(string $title) Voice message title - * @method $this setCaption(string $caption) Optional. Caption, 0-200 characters - * @method $this setParseMode(string $parse_mode) Optional. Mode for parsing entities in the voice caption - * @method $this setCaptionEntities(array $caption_entities) Optional. List of special entities that appear in the caption, which can be specified instead of parse_mode - * @method $this setReplyMarkup(InlineKeyboard $reply_markup) Optional. An Inline keyboard attached to the message - * @method $this setInputMessageContent(InputMessageContent $input_message_content) Optional. Content of the message to be sent instead of the voice message - */ -class InlineQueryResultCachedVoice extends InlineEntity implements InlineQueryResult -{ - /** - * InlineQueryResultCachedVoice constructor - * - * @param array $data - */ - public function __construct(array $data = []) - { - $data['type'] = 'voice'; - parent::__construct($data); - } -} diff --git a/src/Entities/InlineQuery/InlineQueryResultContact.php b/src/Entities/InlineQuery/InlineQueryResultContact.php deleted file mode 100644 index b4d6cfa85..000000000 --- a/src/Entities/InlineQuery/InlineQueryResultContact.php +++ /dev/null @@ -1,71 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities\InlineQuery; - -use Longman\TelegramBot\Entities\InlineKeyboard; -use Longman\TelegramBot\Entities\InputMessageContent\InputMessageContent; - -/** - * Class InlineQueryResultContact - * - * @link https://core.telegram.org/bots/api#inlinequeryresultcontact - * - * - * $data = [ - * 'id' => '', - * 'phone_number' => '', - * 'first_name' => '', - * 'last_name' => '', - * 'reply_markup' => , - * 'input_message_content' => , - * 'thumbnail_url' => '', - * 'thumbnail_width' => 30, - * 'thumbnail_height' => 30, - * ]; - * - * - * @method string getType() Type of the result, must be contact - * @method string getId() Unique identifier for this result, 1-64 Bytes - * @method string getPhoneNumber() Contact's phone number - * @method string getFirstName() Contact's first name - * @method string getLastName() Optional. Contact's last name - * @method string getVcard() Optional. Additional data about the contact in the form of a vCard, 0-2048 bytes - * @method InlineKeyboard getReplyMarkup() Optional. Inline keyboard attached to the message - * @method InputMessageContent getInputMessageContent() Optional. Content of the message to be sent instead of the contact - * @method string getThumbnailUrl() Optional. Url of the thumbnail for the result - * @method int getThumbnailWidth() Optional. Thumbnail width - * @method int getThumbnailHeight() Optional. Thumbnail height - * - * @method $this setId(string $id) Unique identifier for this result, 1-64 Bytes - * @method $this setPhoneNumber(string $phone_number) Contact's phone number - * @method $this setFirstName(string $first_name) Contact's first name - * @method $this setLastName(string $last_name) Optional. Contact's last name - * @method $this setVcard(string $vcard) Optional. Additional data about the contact in the form of a vCard, 0-2048 bytes - * @method $this setReplyMarkup(InlineKeyboard $reply_markup) Optional. Inline keyboard attached to the message - * @method $this setInputMessageContent(InputMessageContent $input_message_content) Optional. Content of the message to be sent instead of the contact - * @method $this setThumbnailUrl(string $thumbnail_url) Optional. Url of the thumbnail for the result - * @method $this setThumbnailWidth(int $thumbnail_width) Optional. Thumbnail width - * @method $this setThumbnailHeight(int $thumbnail_height) Optional. Thumbnail height - */ -class InlineQueryResultContact extends InlineEntity implements InlineQueryResult -{ - /** - * InlineQueryResultContact constructor - * - * @param array $data - */ - public function __construct(array $data = []) - { - $data['type'] = 'contact'; - parent::__construct($data); - } -} diff --git a/src/Entities/InlineQuery/InlineQueryResultDocument.php b/src/Entities/InlineQuery/InlineQueryResultDocument.php deleted file mode 100644 index 8db76febe..000000000 --- a/src/Entities/InlineQuery/InlineQueryResultDocument.php +++ /dev/null @@ -1,79 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities\InlineQuery; - -use Longman\TelegramBot\Entities\InlineKeyboard; -use Longman\TelegramBot\Entities\InputMessageContent\InputMessageContent; - -/** - * Class InlineQueryResultDocument - * - * @link https://core.telegram.org/bots/api#inlinequeryresultdocument - * - * - * $data = [ - * 'id' => '', - * 'title' => '', - * 'caption' => '', - * 'document_url' => '', - * 'mime_type' => '', - * 'description' => '', - * 'reply_markup' => , - * 'input_message_content' => , - * 'thumbnail_url' => '', - * 'thumbnail_width' => 30, - * 'thumbnail_height' => 30, - * ]; - * - * - * @method string getType() Type of the result, must be document - * @method string getId() Unique identifier for this result, 1-64 bytes - * @method string getTitle() Title for the result - * @method string getCaption() Optional. Caption of the document to be sent, 0-200 characters - * @method string getParseMode() Optional. Mode for parsing entities in the document caption - * @method MessageEntity[] getCaptionEntities() Optional. List of special entities that appear in the caption, which can be specified instead of parse_mode - * @method string getDocumentUrl() A valid URL for the file - * @method string getMimeType() Mime type of the content of the file, either “application/pdf” or “application/zip” - * @method string getDescription() Optional. Short description of the result - * @method InlineKeyboard getReplyMarkup() Optional. Inline keyboard attached to the message - * @method InputMessageContent getInputMessageContent() Optional. Content of the message to be sent instead of the file - * @method string getThumbnailUrl() Optional. URL of the thumbnail (jpeg only) for the file - * @method int getThumbnailWidth() Optional. Thumbnail width - * @method int getThumbnailHeight() Optional. Thumbnail height - * - * @method $this setId(string $id) Unique identifier for this result, 1-64 bytes - * @method $this setTitle(string $title) Title for the result - * @method $this setCaption(string $caption) Optional. Caption of the document to be sent, 0-200 characters - * @method $this setParseMode(string $parse_mode) Optional. Mode for parsing entities in the document caption - * @method $this setCaptionEntities(array $caption_entities) Optional. List of special entities that appear in the caption, which can be specified instead of parse_mode - * @method $this setDocumentUrl(string $document_url) A valid URL for the file - * @method $this setMimeType(string $mime_type) Mime type of the content of the file, either “application/pdf” or “application/zip” - * @method $this setDescription(string $description) Optional. Short description of the result - * @method $this setReplyMarkup(InlineKeyboard $reply_markup) Optional. Inline keyboard attached to the message - * @method $this setInputMessageContent(InputMessageContent $input_message_content) Optional. Content of the message to be sent instead of the file - * @method $this setThumbnailUrl(string $thumbnail_url) Optional. URL of the thumbnail (jpeg only) for the file - * @method $this setThumbnailWidth(int $thumbnail_width) Optional. Thumbnail width - * @method $this setThumbnailHeight(int $thumbnail_height) Optional. Thumbnail height - */ -class InlineQueryResultDocument extends InlineEntity implements InlineQueryResult -{ - /** - * InlineQueryResultDocument constructor - * - * @param array $data - */ - public function __construct(array $data = []) - { - $data['type'] = 'document'; - parent::__construct($data); - } -} diff --git a/src/Entities/InlineQuery/InlineQueryResultGame.php b/src/Entities/InlineQuery/InlineQueryResultGame.php deleted file mode 100644 index d40860fe4..000000000 --- a/src/Entities/InlineQuery/InlineQueryResultGame.php +++ /dev/null @@ -1,50 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities\InlineQuery; - -use Longman\TelegramBot\Entities\InlineKeyboard; - -/** - * Class InlineQueryResultGame - * - * @link https://core.telegram.org/bots/api#inlinequeryresultgame - * - * - * $data = [ - * 'id' => '', - * 'game_short_name' => '', - * 'reply_markup' => , - * ]; - * - * - * @method string getType() Type of the result, must be game - * @method string getId() Unique identifier for this result, 1-64 bytes - * @method string getGameShortName() Short name of the game - * @method InlineKeyboard getReplyMarkup() Optional. Inline keyboard attached to the message - * - * @method $this setId(string $id) Unique identifier for this result, 1-64 bytes - * @method $this setGameShortName(string $game_short_name) Short name of the game - * @method $this setReplyMarkup(InlineKeyboard $reply_markup) Optional. Inline keyboard attached to the message - */ -class InlineQueryResultGame extends InlineEntity implements InlineQueryResult -{ - /** - * InlineQueryResultGame constructor - * - * @param array $data - */ - public function __construct(array $data = []) - { - $data['type'] = 'game'; - parent::__construct($data); - } -} diff --git a/src/Entities/InlineQuery/InlineQueryResultGif.php b/src/Entities/InlineQuery/InlineQueryResultGif.php deleted file mode 100644 index 34c9810cc..000000000 --- a/src/Entities/InlineQuery/InlineQueryResultGif.php +++ /dev/null @@ -1,77 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities\InlineQuery; - -use Longman\TelegramBot\Entities\InlineKeyboard; -use Longman\TelegramBot\Entities\InputMessageContent\InputMessageContent; - -/** - * Class InlineQueryResultGif - * - * @link https://core.telegram.org/bots/api#inlinequeryresultgif - * - * - * $data = [ - * 'id' => '', - * 'gif_url' => '', - * 'gif_width' => 30, - * 'gif_height' => 30, - * 'thumbnail_url' => '', - * 'title' => '', - * 'caption' => '', - * 'reply_markup' => , - * 'input_message_content' => , - * ]; - * - * - * @method string getType() Type of the result, must be gif - * @method string getId() Unique identifier for this result, 1-64 bytes - * @method string getGifUrl() A valid URL for the GIF file. File size must not exceed 1MB - * @method int getGifWidth() Optional. Width of the GIF - * @method int getGifHeight() Optional. Height of the GIF - * @method int getGifDuration() Optional. Duration of the GIF - * @method string getThumbnailUrl() URL of the static thumbnail for the result (jpeg or gif) - * @method string getThumbnailMimeType() Optional. MIME type of the thumbnail, must be one of “image/jpeg”, “image/gif”, or “video/mp4”. Defaults to “image/jpeg” - * @method string getTitle() Optional. Title for the result - * @method string getCaption() Optional. Caption of the GIF file to be sent, 0-200 characters - * @method string getParseMode() Optional. Mode for parsing entities in the caption - * @method MessageEntity[] getCaptionEntities() Optional. List of special entities that appear in the caption, which can be specified instead of parse_mode - * @method InlineKeyboard getReplyMarkup() Optional. Inline keyboard attached to the message - * @method InputMessageContent getInputMessageContent() Optional. Content of the message to be sent instead of the GIF animation - * - * @method $this setId(string $id) Unique identifier for this result, 1-64 bytes - * @method $this setGifUrl(string $gif_url) A valid URL for the GIF file. File size must not exceed 1MB - * @method $this setGifWidth(int $gif_width) Optional. Width of the GIF - * @method $this setGifHeight(int $gif_height) Optional. Height of the GIF - * @method $this setGifDuration(int $gif_duration) Optional. Duration of the GIF - * @method $this setThumbnailUrl(string $thumbnail_url) URL of the static thumbnail for the result (jpeg or gif) - * @method $this setThumbnailMimeType(string $thumbnail_mime_type) Optional. MIME type of the thumbnail, must be one of “image/jpeg”, “image/gif”, or “video/mp4”. Defaults to “image/jpeg” - * @method $this setTitle(string $title) Optional. Title for the result - * @method $this setCaption(string $caption) Optional. Caption of the GIF file to be sent, 0-200 characters - * @method $this setParseMode(string $parse_mode) Optional. Mode for parsing entities in the caption - * @method $this setCaptionEntities(array $caption_entities) Optional. List of special entities that appear in the caption, which can be specified instead of parse_mode - * @method $this setReplyMarkup(InlineKeyboard $reply_markup) Optional. Inline keyboard attached to the message - * @method $this setInputMessageContent(InputMessageContent $input_message_content) Optional. Content of the message to be sent instead of the GIF animation - */ -class InlineQueryResultGif extends InlineEntity implements InlineQueryResult -{ - /** - * InlineQueryResultGif constructor - * - * @param array $data - */ - public function __construct(array $data = []) - { - $data['type'] = 'gif'; - parent::__construct($data); - } -} diff --git a/src/Entities/InlineQuery/InlineQueryResultLocation.php b/src/Entities/InlineQuery/InlineQueryResultLocation.php deleted file mode 100644 index 888556d4b..000000000 --- a/src/Entities/InlineQuery/InlineQueryResultLocation.php +++ /dev/null @@ -1,81 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities\InlineQuery; - -use Longman\TelegramBot\Entities\InlineKeyboard; -use Longman\TelegramBot\Entities\InputMessageContent\InputMessageContent; - -/** - * Class InlineQueryResultLocation - * - * @link https://core.telegram.org/bots/api#inlinequeryresultlocation - * - * - * $data = [ - * 'id' => '', - * 'latitude' => 36.0338, - * 'longitude' => 71.8601, - * 'title' => '', - * 'horizontal_accuracy' => 36.9, - * 'live_period' => 900, - * 'heading' => 88, - * 'proximity_alert_radius' => 300, - * 'reply_markup' => , - * 'input_message_content' => , - * 'thumbnail_url' => '', - * 'thumbnail_width' => 30, - * 'thumbnail_height' => 30, - * ]; - * - * - * @method string getType() Type of the result, must be location - * @method string getId() Unique identifier for this result, 1-64 Bytes - * @method float getLatitude() Location latitude in degrees - * @method float getLongitude() Location longitude in degrees - * @method string getTitle() Location title - * @method float getHorizontalAccuracy() Optional. The radius of uncertainty for the location, measured in meters; 0-1500 - * @method int getLivePeriod() Optional. Period in seconds for which the location can be updated, should be between 60 and 86400. - * @method int getHeading() Optional. For live locations, a direction in which the user is moving, in degrees. Must be between 1 and 360 if specified. - * @method int getProximityAlertRadius() Optional. For live locations, a maximum distance for proximity alerts about approaching another chat member, in meters. Must be between 1 and 100000 if specified. - * @method InlineKeyboard getReplyMarkup() Optional. Inline keyboard attached to the message - * @method InputMessageContent getInputMessageContent() Optional. Content of the message to be sent instead of the location - * @method string getThumbnailUrl() Optional. Url of the thumbnail for the result - * @method int getThumbnailWidth() Optional. Thumbnail width - * @method int getThumbnailHeight() Optional. Thumbnail height - * - * @method $this setId(string $id) Unique identifier for this result, 1-64 Bytes - * @method $this setLatitude(float $latitude) Location latitude in degrees - * @method $this setLongitude(float $longitude) Location longitude in degrees - * @method $this setTitle(string $title) Location title - * @method $this setHorizontalAccuracy(float $horizontal_accuracy) Optional. The radius of uncertainty for the location, measured in meters; 0-1500 - * @method $this setLivePeriod(int $live_period) Optional. Period in seconds for which the location can be updated, should be between 60 and 86400. - * @method $this setHeading(int $heading) Optional. For live locations, a direction in which the user is moving, in degrees. Must be between 1 and 360 if specified. - * @method $this setProximityAlertRadius(int $proximity_alert_radius) Optional. For live locations, a maximum distance for proximity alerts about approaching another chat member, in meters. Must be between 1 and 100000 if specified. - * @method $this setReplyMarkup(InlineKeyboard $reply_markup) Optional. Inline keyboard attached to the message - * @method $this setInputMessageContent(InputMessageContent $input_message_content) Optional. Content of the message to be sent instead of the location - * @method $this setThumbnailUrl(string $thumbnail_url) Optional. Url of the thumbnail for the result - * @method $this setThumbnailWidth(int $thumbnail_width) Optional. Thumbnail width - * @method $this setThumbnailHeight(int $thumbnail_height) Optional. Thumbnail height - */ -class InlineQueryResultLocation extends InlineEntity implements InlineQueryResult -{ - /** - * InlineQueryResultLocation constructor - * - * @param array $data - */ - public function __construct(array $data = []) - { - $data['type'] = 'location'; - parent::__construct($data); - } -} diff --git a/src/Entities/InlineQuery/InlineQueryResultMpeg4Gif.php b/src/Entities/InlineQuery/InlineQueryResultMpeg4Gif.php deleted file mode 100644 index 01015eeca..000000000 --- a/src/Entities/InlineQuery/InlineQueryResultMpeg4Gif.php +++ /dev/null @@ -1,77 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities\InlineQuery; - -use Longman\TelegramBot\Entities\InlineKeyboard; -use Longman\TelegramBot\Entities\InputMessageContent\InputMessageContent; - -/** - * Class InlineQueryResultMpeg4Gif - * - * @link https://core.telegram.org/bots/api#inlinequeryresultmpeg4gif - * - * - * $data = [ - * 'id' => '', - * 'mpeg4_url' => '', - * 'mpeg4_width' => 30, - * 'mpeg4_height' => 30, - * 'thumbnail_url' => '', - * 'title' => '', - * 'caption' => '', - * 'reply_markup' => , - * 'input_message_content' => , - * ]; - * - * - * @method string getType() Type of the result, must be mpeg4_gif - * @method string getId() Unique identifier for this result, 1-64 bytes - * @method string getMpeg4Url() A valid URL for the MP4 file. File size must not exceed 1MB - * @method int getMpeg4Width() Optional. Video width - * @method int getMpeg4Height() Optional. Video height - * @method int getMpeg4Duration() Optional. Video duration - * @method string getThumbnailUrl() URL of the static (JPEG or GIF) or animated (MPEG4) thumbnail for the result - * @method string getThumbnailMimeType() Optional. MIME type of the thumbnail, must be one of “image/jpeg”, “image/gif”, or “video/mp4”. Defaults to “image/jpeg” - * @method string getTitle() Optional. Title for the result - * @method string getCaption() Optional. Caption of the MPEG-4 file to be sent, 0-200 characters - * @method string getParseMode() Optional. Mode for parsing entities in the caption - * @method MessageEntity[] getCaptionEntities() Optional. List of special entities that appear in the caption, which can be specified instead of parse_mode - * @method InlineKeyboard getReplyMarkup() Optional. Inline keyboard attached to the message - * @method InputMessageContent getInputMessageContent() Optional. Content of the message to be sent instead of the video animation - * - * @method $this setId(string $id) Unique identifier for this result, 1-64 bytes - * @method $this setMpeg4Url(string $mpeg4_url) A valid URL for the MP4 file. File size must not exceed 1MB - * @method $this setMpeg4Width(int $mpeg4_width) Optional. Video width - * @method $this setMpeg4Height(int $mpeg4_height) Optional. Video height - * @method $this setMpeg4Duration(int $mpeg4_duration) Optional. Video duration - * @method $this setThumbnailUrl(string $thumbnail_url) URL of the static (JPEG or GIF) or animated (MPEG4) thumbnail for the result - * @method $this setThumbnailMimeType(string $thumbnail_mime_type) Optional. MIME type of the thumbnail, must be one of “image/jpeg”, “image/gif”, or “video/mp4”. Defaults to “image/jpeg” - * @method $this setTitle(string $title) Optional. Title for the result - * @method $this setCaption(string $caption) Optional. Caption of the MPEG-4 file to be sent, 0-200 characters - * @method $this setParseMode(string $parse_mode) Optional. Mode for parsing entities in the caption - * @method $this setCaptionEntities(array $caption_entities) Optional. List of special entities that appear in the caption, which can be specified instead of parse_mode - * @method $this setReplyMarkup(InlineKeyboard $reply_markup) Optional. Inline keyboard attached to the message - * @method $this setInputMessageContent(InputMessageContent $input_message_content) Optional. Content of the message to be sent instead of the video animation - */ -class InlineQueryResultMpeg4Gif extends InlineEntity implements InlineQueryResult -{ - /** - * InlineQueryResultMpeg4Gif constructor - * - * @param array $data - */ - public function __construct(array $data = []) - { - $data['type'] = 'mpeg4_gif'; - parent::__construct($data); - } -} diff --git a/src/Entities/InlineQuery/InlineQueryResultPhoto.php b/src/Entities/InlineQuery/InlineQueryResultPhoto.php deleted file mode 100644 index 2633ad8a8..000000000 --- a/src/Entities/InlineQuery/InlineQueryResultPhoto.php +++ /dev/null @@ -1,76 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities\InlineQuery; - -use Longman\TelegramBot\Entities\InlineKeyboard; -use Longman\TelegramBot\Entities\InputMessageContent\InputMessageContent; - -/** - * Class InlineQueryResultPhoto - * - * @link https://core.telegram.org/bots/api#inlinequeryresultphoto - * - * - * $data = [ - * 'id' => '', - * 'photo_url' => '', - * 'thumbnail_url' => '', - * 'photo_width' => 30, - * 'photo_height' => 30, - * 'title' => '', - * 'description' => '', - * 'caption' => '', - * 'reply_markup' => , - * 'input_message_content' => , - * ]; - * - * - * @method string getType() Type of the result, must be photo - * @method string getId() Unique identifier for this result, 1-64 bytes - * @method string getPhotoUrl() A valid URL of the photo. Photo must be in jpeg format. Photo size must not exceed 5MB - * @method string getThumbnailUrl() URL of the thumbnail for the photo - * @method int getPhotoWidth() Optional. Width of the photo - * @method int getPhotoHeight() Optional. Height of the photo - * @method string getTitle() Optional. Title for the result - * @method string getDescription() Optional. Short description of the result - * @method string getCaption() Optional. Caption of the photo to be sent, 0-200 characters - * @method string getParseMode() Optional. Mode for parsing entities in the photo caption - * @method MessageEntity[] getCaptionEntities() Optional. List of special entities that appear in the caption, which can be specified instead of parse_mode - * @method InlineKeyboard getReplyMarkup() Optional. Inline keyboard attached to the message - * @method InputMessageContent getInputMessageContent() Optional. Content of the message to be sent instead of the photo - * - * @method $this setId(string $id) Unique identifier for this result, 1-64 bytes - * @method $this setPhotoUrl(string $photo_url) A valid URL of the photo. Photo must be in jpeg format. Photo size must not exceed 5MB - * @method $this setThumbnailUrl(string $thumbnail_url) URL of the thumbnail for the photo - * @method $this setPhotoWidth(int $photo_width) Optional. Width of the photo - * @method $this setPhotoHeight(int $photo_height) Optional. Height of the photo - * @method $this setTitle(string $title) Optional. Title for the result - * @method $this setDescription(string $description) Optional. Short description of the result - * @method $this setCaption(string $caption) Optional. Caption of the photo to be sent, 0-200 characters - * @method $this setParseMode(string $parse_mode) Optional. Mode for parsing entities in the photo caption - * @method $this setCaptionEntities(array $caption_entities) Optional. List of special entities that appear in the caption, which can be specified instead of parse_mode - * @method $this setReplyMarkup(InlineKeyboard $reply_markup) Optional. Inline keyboard attached to the message - * @method $this setInputMessageContent(InputMessageContent $input_message_content) Optional. Content of the message to be sent instead of the photo - */ -class InlineQueryResultPhoto extends InlineEntity implements InlineQueryResult -{ - /** - * InlineQueryResultPhoto constructor - * - * @param array $data - */ - public function __construct(array $data = []) - { - $data['type'] = 'photo'; - parent::__construct($data); - } -} diff --git a/src/Entities/InlineQuery/InlineQueryResultVenue.php b/src/Entities/InlineQuery/InlineQueryResultVenue.php deleted file mode 100644 index 8ab428b00..000000000 --- a/src/Entities/InlineQuery/InlineQueryResultVenue.php +++ /dev/null @@ -1,81 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities\InlineQuery; - -use Longman\TelegramBot\Entities\InlineKeyboard; -use Longman\TelegramBot\Entities\InputMessageContent\InputMessageContent; - -/** - * Class InlineQueryResultVenue - * - * @link https://core.telegram.org/bots/api#inlinequeryresultvenue - * - * - * $data = [ - * 'id' => '', - * 'latitude' => 36.0338, - * 'longitude' => 71.8601, - * 'title' => '', - * 'address' => '', - * 'foursquare_id' => '', - * 'reply_markup' => , - * 'input_message_content' => , - * 'thumbnail_url' => '', - * 'thumbnail_width' => 30, - * 'thumbnail_height' => 30, - * ]; - * - * - * @method string getType() Type of the result, must be venue - * @method string getId() Unique identifier for this result, 1-64 Bytes - * @method float getLatitude() Latitude of the venue location in degrees - * @method float getLongitude() Longitude of the venue location in degrees - * @method string getTitle() Title of the venue - * @method string getAddress() Address of the venue - * @method string getFoursquareId() Optional. Foursquare identifier of the venue if known - * @method string getFoursquareType() Optional. Foursquare type of the venue, if known. (For example, “arts_entertainment/default”, “arts_entertainment/aquarium” or “food/icecream”.) - * @method string getGooglePlaceId() Optional. Google Places identifier of the venue - * @method string getGooglePlaceType() Optional. Google Places type of the venue - * @method InlineKeyboard getReplyMarkup() Optional. Inline keyboard attached to the message - * @method InputMessageContent getInputMessageContent() Optional. Content of the message to be sent instead of the venue - * @method string getThumbnailUrl() Optional. Url of the thumbnail for the result - * @method int getThumbnailWidth() Optional. Thumbnail width - * @method int getThumbnailHeight() Optional. Thumbnail height - * - * @method $this setId(string $id) Unique identifier for this result, 1-64 Bytes - * @method $this setLatitude(float $latitude) Latitude of the venue location in degrees - * @method $this setLongitude(float $longitude) Longitude of the venue location in degrees - * @method $this setTitle(string $title) Title of the venue - * @method $this setAddress(string $address) Address of the venue - * @method $this setFoursquareId(string $foursquare_id) Optional. Foursquare identifier of the venue if known - * @method $this setFoursquareType(string $foursquare_type) Optional. Foursquare type of the venue, if known. (For example, “arts_entertainment/default”, “arts_entertainment/aquarium” or “food/icecream”.) - * @method $this setGooglePlaceId(string $google_place_id) Optional. Google Places identifier of the venue - * @method $this setGooglePlaceType(string $google_place_type) Optional. Google Places type of the venue - * @method $this setReplyMarkup(InlineKeyboard $reply_markup) Optional. Inline keyboard attached to the message - * @method $this setInputMessageContent(InputMessageContent $input_message_content) Optional. Content of the message to be sent instead of the venue - * @method $this setThumbnailUrl(string $thumbnail_url) Optional. Url of the thumbnail for the result - * @method $this setThumbnailWidth(int $thumbnail_width) Optional. Thumbnail width - * @method $this setThumbnailHeight(int $thumbnail_height) Optional. Thumbnail height - */ -class InlineQueryResultVenue extends InlineEntity implements InlineQueryResult -{ - /** - * InlineQueryResultVenue constructor - * - * @param array $data - */ - public function __construct(array $data = []) - { - $data['type'] = 'venue'; - parent::__construct($data); - } -} diff --git a/src/Entities/InlineQuery/InlineQueryResultVideo.php b/src/Entities/InlineQuery/InlineQueryResultVideo.php deleted file mode 100644 index 9337bd7a4..000000000 --- a/src/Entities/InlineQuery/InlineQueryResultVideo.php +++ /dev/null @@ -1,82 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities\InlineQuery; - -use Longman\TelegramBot\Entities\InlineKeyboard; -use Longman\TelegramBot\Entities\InputMessageContent\InputMessageContent; - -/** - * Class InlineQueryResultVideo - * - * @link https://core.telegram.org/bots/api#inlinequeryresultvideo - * - * - * $data = [ - * 'id' => '', - * 'video_url' => '', - * 'mime_type' => '', - * 'thumbnail_url' => '', - * 'title' => '', - * 'caption' => '', - * 'video_width' => 30, - * 'video_height' => 30, - * 'video_duration' => 123, - * 'description' => '', - * 'reply_markup' => , - * 'input_message_content' => , - * ]; - * - * - * @method string getType() Type of the result, must be video - * @method string getId() Unique identifier for this result, 1-64 bytes - * @method string getVideoUrl() A valid URL for the embedded video player or video file - * @method string getMimeType() Mime type of the content of video url, “text/html” or “video/mp4” - * @method string getThumbnailUrl() URL of the thumbnail (jpeg only) for the video - * @method string getTitle() Title for the result - * @method string getCaption() Optional. Caption of the video to be sent, 0-200 characters - * @method string getParseMode() Optional. Mode for parsing entities in the video caption - * @method MessageEntity[] getCaptionEntities() Optional. List of special entities that appear in the caption, which can be specified instead of parse_mode - * @method int getVideoWidth() Optional. Video width - * @method int getVideoHeight() Optional. Video height - * @method int getVideoDuration() Optional. Video duration in seconds - * @method string getDescription() Optional. Short description of the result - * @method InlineKeyboard getReplyMarkup() Optional. Inline keyboard attached to the message - * @method InputMessageContent getInputMessageContent() Optional. Content of the message to be sent instead of the video - * - * @method $this setId(string $id) Unique identifier for this result, 1-64 bytes - * @method $this setVideoUrl(string $video_url) A valid URL for the embedded video player or video file - * @method $this setMimeType(string $mime_type) Mime type of the content of video url, “text/html” or “video/mp4” - * @method $this setThumbnailUrl(string $thumbnail_url) URL of the thumbnail (jpeg only) for the video - * @method $this setTitle(string $title) Title for the result - * @method $this setCaption(string $caption) Optional. Caption of the video to be sent, 0-200 characters - * @method $this setParseMode(string $parse_mode) Optional. Mode for parsing entities in the video caption - * @method $this setCaptionEntities(array $caption_entities) Optional. List of special entities that appear in the caption, which can be specified instead of parse_mode - * @method $this setVideoWidth(int $video_width) Optional. Video width - * @method $this setVideoHeight(int $video_height) Optional. Video height - * @method $this setVideoDuration(int $video_duration) Optional. Video duration in seconds - * @method $this setDescription(string $description) Optional. Short description of the result - * @method $this setReplyMarkup(InlineKeyboard $reply_markup) Optional. Inline keyboard attached to the message - * @method $this setInputMessageContent(InputMessageContent $input_message_content) Optional. Content of the message to be sent instead of the video - */ -class InlineQueryResultVideo extends InlineEntity implements InlineQueryResult -{ - /** - * InlineQueryResultVideo constructor - * - * @param array $data - */ - public function __construct(array $data = []) - { - $data['type'] = 'video'; - parent::__construct($data); - } -} diff --git a/src/Entities/InlineQuery/InlineQueryResultVoice.php b/src/Entities/InlineQuery/InlineQueryResultVoice.php deleted file mode 100644 index 8fbad6972..000000000 --- a/src/Entities/InlineQuery/InlineQueryResultVoice.php +++ /dev/null @@ -1,67 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities\InlineQuery; - -use Longman\TelegramBot\Entities\InlineKeyboard; -use Longman\TelegramBot\Entities\InputMessageContent\InputMessageContent; - -/** - * Class InlineQueryResultVoice - * - * @link https://core.telegram.org/bots/api#inlinequeryresultvoice - * - * - * $data = [ - * 'id' => '', - * 'voice_url' => '', - * 'title' => '', - * 'caption' => '', - * 'voice_duration' => 123, - * 'reply_markup' => , - * 'input_message_content' => , - * ]; - * - * - * @method string getType() Type of the result, must be voice - * @method string getId() Unique identifier for this result, 1-64 bytes - * @method string getVoiceUrl() A valid URL for the voice recording - * @method string getTitle() Recording title - * @method string getCaption() Optional. Caption, 0-200 characters - * @method string getParseMode() Optional. Mode for parsing entities in the voice caption - * @method MessageEntity[] getCaptionEntities() Optional. List of special entities that appear in the caption, which can be specified instead of parse_mode - * @method int getVoiceDuration() Optional. Recording duration in seconds - * @method InlineKeyboard getReplyMarkup() Optional. Inline keyboard attached to the message - * @method InputMessageContent getInputMessageContent() Optional. Content of the message to be sent instead of the voice recording - * - * @method $this setId(string $id) Unique identifier for this result, 1-64 bytes - * @method $this setVoiceUrl(string $voice_url) A valid URL for the voice recording - * @method $this setTitle(string $title) Recording title - * @method $this setCaption(string $caption) Optional. Caption, 0-200 characters - * @method $this setParseMode(string $parse_mode) Optional. Mode for parsing entities in the voice caption - * @method $this setCaptionEntities(array $caption_entities) Optional. List of special entities that appear in the caption, which can be specified instead of parse_mode - * @method $this setVoiceDuration(int $voice_duration) Optional. Recording duration in seconds - * @method $this setReplyMarkup(InlineKeyboard $reply_markup) Optional. Inline keyboard attached to the message - * @method $this setInputMessageContent(InputMessageContent $input_message_content) Optional. Content of the message to be sent instead of the voice recording - */ -class InlineQueryResultVoice extends InlineEntity implements InlineQueryResult -{ - /** - * InlineQueryResultVoice constructor - * - * @param array $data - */ - public function __construct(array $data = []) - { - $data['type'] = 'voice'; - parent::__construct($data); - } -} diff --git a/src/Entities/InlineQueryResultsButton.php b/src/Entities/InlineQueryResultsButton.php deleted file mode 100644 index a96222e9c..000000000 --- a/src/Entities/InlineQueryResultsButton.php +++ /dev/null @@ -1,27 +0,0 @@ - WebAppInfo::class, - ]; - } -} diff --git a/src/Entities/InputMedia/InputMedia.php b/src/Entities/InputMedia/InputMedia.php deleted file mode 100644 index b9bd973d1..000000000 --- a/src/Entities/InputMedia/InputMedia.php +++ /dev/null @@ -1,8 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities\InputMedia; - -use Longman\TelegramBot\Entities\Entity; - -/** - * Class InputMediaAnimation - * - * @link https://core.telegram.org/bots/api#inputmediaanimation - * - * - * $data = [ - * 'media' => '123abc', - * 'thumbnail' => '456def', - * 'caption' => '*Animation* caption', - * 'parse_mode' => 'markdown', - * 'width' => 200, - * 'height' => 150, - * 'duration' => 11, - * ]; - * - * - * @method string getType() Type of the result, must be animation - * @method string getMedia() File to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet, or pass “attach://” to upload a new one using multipart/form-data under name. More info on Sending Files » - * @method string getThumbnail() Optional. Thumbnail of the file sent. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail‘s width and height should not exceed 90. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can’t be reused and can be only uploaded as a new file, so you can pass “attach://” if the thumbnail was uploaded using multipart/form-data under . More info on Sending Files » - * @method string getCaption() Optional. Caption of the animation to be sent, 0-200 characters - * @method string getParseMode() Optional. Mode for parsing entities in the animation caption - * @method MessageEntity[] getCaptionEntities() Optional. List of special entities that appear in the caption, which can be specified instead of parse_mode - * @method int getWidth() Optional. Animation width - * @method int getHeight() Optional. Animation height - * @method int getDuration() Optional. Animation duration - * @method bool getHasSpoiler() Optional. Pass True if the animation needs to be covered with a spoiler animation - * - * @method $this setMedia(string $media) File to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet, or pass “attach://” to upload a new one using multipart/form-data under name. More info on Sending Files » - * @method $this setThumbnail(string $thumbnail) Optional. Thumbnail of the file sent. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail‘s width and height should not exceed 90. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can’t be reused and can be only uploaded as a new file, so you can pass “attach://” if the thumbnail was uploaded using multipart/form-data under . More info on Sending Files » - * @method $this setCaption(string $caption) Optional. Caption of the animation to be sent, 0-200 characters - * @method $this setParseMode(string $parse_mode) Optional. Mode for parsing entities in the animation caption - * @method $this setCaptionEntities(array $caption_entities) Optional. List of special entities that appear in the caption, which can be specified instead of parse_mode - * @method $this setWidth(int $width) Optional. Animation width - * @method $this setHeight(int $height) Optional. Animation height - * @method $this setDuration(int $duration) Optional. Animation duration - * @method $this setHasSpoiler(bool $has_spoiler) Optional. Pass True if the animation needs to be covered with a spoiler animation - */ -class InputMediaAnimation extends Entity implements InputMedia -{ - /** - * InputMediaAnimation constructor - * - * @param array $data - */ - public function __construct(array $data = []) - { - $data['type'] = 'animation'; - parent::__construct($data); - } -} diff --git a/src/Entities/InputMedia/InputMediaAudio.php b/src/Entities/InputMedia/InputMediaAudio.php deleted file mode 100644 index a992095fe..000000000 --- a/src/Entities/InputMedia/InputMediaAudio.php +++ /dev/null @@ -1,64 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities\InputMedia; - -use Longman\TelegramBot\Entities\Entity; - -/** - * Class InputMediaAudio - * - * @link https://core.telegram.org/bots/api#inputmediaaudio - * - * - * $data = [ - * 'media' => '123abc', - * 'thumbnail' => '456def', - * 'caption' => '*Audio* caption', - * 'parse_mode' => 'markdown', - * 'duration' => 42, - * 'performer' => 'John Doe', - * 'title' => 'The Song', - * ]; - * - * - * @method string getType() Type of the result, must be audio - * @method string getMedia() File to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet, or pass "attach://" to upload a new one using multipart/form-data under name. - * @method string getThumbnail() Optional. Thumbnail of the file sent. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail‘s width and height should not exceed 90. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can’t be reused and can be only uploaded as a new file, so you can pass “attach://” if the thumbnail was uploaded using multipart/form-data under . More info on Sending Files » - * @method string getCaption() Optional. Caption of the audio to be sent, 0-200 characters - * @method string getParseMode() Optional. Mode for parsing entities in the audio caption - * @method MessageEntity[] getCaptionEntities() Optional. List of special entities that appear in the caption, which can be specified instead of parse_mode - * @method int getDuration() Optional. Duration of the audio in seconds - * @method string getPerformer() Optional. Performer of the audio - * @method string getTitle() Optional. Title of the audio - * - * @method $this setMedia(string $media) File to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet, or pass "attach://" to upload a new one using multipart/form-data under name. - * @method $this setThumbnail(string $thumbnail) Optional. Thumbnail of the file sent. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail‘s width and height should not exceed 90. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can’t be reused and can be only uploaded as a new file, so you can pass “attach://” if the thumbnail was uploaded using multipart/form-data under . More info on Sending Files » - * @method $this setCaption(string $caption) Optional. Caption of the audio to be sent, 0-200 characters - * @method $this setParseMode(string $parse_mode) Optional. Mode for parsing entities in the audio caption - * @method $this setCaptionEntities(array $caption_entities) Optional. List of special entities that appear in the caption, which can be specified instead of parse_mode - * @method $this setDuration(int $duration) Optional. Duration of the audio in seconds - * @method $this setPerformer(string $performer) Optional. Performer of the audio - * @method $this setTitle(string $title) Optional. Title of the audio - */ -class InputMediaAudio extends Entity implements InputMedia -{ - /** - * InputMediaAudio constructor - * - * @param array $data - */ - public function __construct(array $data = []) - { - $data['type'] = 'audio'; - parent::__construct($data); - } -} diff --git a/src/Entities/InputMedia/InputMediaDocument.php b/src/Entities/InputMedia/InputMediaDocument.php deleted file mode 100644 index 138e43e8d..000000000 --- a/src/Entities/InputMedia/InputMediaDocument.php +++ /dev/null @@ -1,57 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities\InputMedia; - -use Longman\TelegramBot\Entities\Entity; - -/** - * Class InputMediaDocument - * - * @link https://core.telegram.org/bots/api#inputmediadocument - * - * - * $data = [ - * 'media' => '123abc', - * 'thumbnail' => '456def', - * 'caption' => '*Document* caption', - * 'parse_mode' => 'markdown', - * ]; - * - * - * @method string getType() Type of the result, must be document - * @method string getMedia() File to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet, or pass "attach://" to upload a new one using multipart/form-data under name. - * @method string getThumbnail() Optional. Thumbnail of the file sent. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail‘s width and height should not exceed 90. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can’t be reused and can be only uploaded as a new file, so you can pass “attach://” if the thumbnail was uploaded using multipart/form-data under . More info on Sending Files » - * @method string getCaption() Optional. Caption of the document to be sent, 0-200 characters - * @method string getParseMode() Optional. Mode for parsing entities in the document caption - * @method MessageEntity[] getCaptionEntities() Optional. List of special entities that appear in the caption, which can be specified instead of parse_mode - * @method bool getDisableContentTypeDetection() Optional. Disables automatic server-side content type detection for files uploaded using multipart/form-data. Always true, if the document is sent as part of an album. - * - * @method $this setMedia(string $media) File to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet, or pass "attach://" to upload a new one using multipart/form-data under name. - * @method $this setThumbnail(string $thumbnail) Optional. Thumbnail of the file sent. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail‘s width and height should not exceed 90. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can’t be reused and can be only uploaded as a new file, so you can pass “attach://” if the thumbnail was uploaded using multipart/form-data under . More info on Sending Files » - * @method $this setCaption(string $caption) Optional. Caption of the document to be sent, 0-200 characters - * @method $this setParseMode(string $parse_mode) Optional. Mode for parsing entities in the document caption - * @method $this setCaptionEntities(array $caption_entities) Optional. List of special entities that appear in the caption, which can be specified instead of parse_mode - * @method $this setDisableContentTypeDetection(bool $disable_content_type_detection) Optional. Disables automatic server-side content type detection for files uploaded using multipart/form-data. Always true, if the document is sent as part of an album. - */ -class InputMediaDocument extends Entity implements InputMedia -{ - /** - * InputMediaDocument constructor - * - * @param array $data - */ - public function __construct(array $data = []) - { - $data['type'] = 'document'; - parent::__construct($data); - } -} diff --git a/src/Entities/InputMedia/InputMediaPhoto.php b/src/Entities/InputMedia/InputMediaPhoto.php deleted file mode 100644 index 4a136fccb..000000000 --- a/src/Entities/InputMedia/InputMediaPhoto.php +++ /dev/null @@ -1,54 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities\InputMedia; - -use Longman\TelegramBot\Entities\Entity; - -/** - * Class InputMediaPhoto - * - * @link https://core.telegram.org/bots/api#inputmediaphoto - * - * - * $data = [ - * 'media' => '123abc', - * 'caption' => '*Photo* caption', - * 'parse_mode' => 'markdown', - * ]; - * - * - * @method string getType() Type of the result, must be photo - * @method string getMedia() File to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet, or pass "attach://" to upload a new one using multipart/form-data under name. - * @method string getCaption() Optional. Caption of the photo to be sent, 0-200 characters - * @method string getParseMode() Optional. Mode for parsing entities in the photo caption - * @method MessageEntity[] getCaptionEntities() Optional. List of special entities that appear in the caption, which can be specified instead of parse_mode - * @method bool getHasSpoiler() Optional. Pass True if the photo needs to be covered with a spoiler animation - * - * @method $this setMedia(string $media) File to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet, or pass "attach://" to upload a new one using multipart/form-data under name. - * @method $this setCaption(string $caption) Optional. Caption of the photo to be sent, 0-200 characters - * @method $this setParseMode(string $parse_mode) Optional. Mode for parsing entities in the photo caption - * @method $this setCaptionEntities(array $caption_entities) Optional. List of special entities that appear in the caption, which can be specified instead of parse_mode - * @method $this setHasSpoiler(bool $has_spoiler) Optional. Pass True if the photo needs to be covered with a spoiler animation - */ -class InputMediaPhoto extends Entity implements InputMedia -{ - /** - * InputMediaPhoto constructor - * - * @param array $data - */ - public function __construct(array $data = []) - { - $data['type'] = 'photo'; - parent::__construct($data); - } -} diff --git a/src/Entities/InputMedia/InputMediaVideo.php b/src/Entities/InputMedia/InputMediaVideo.php deleted file mode 100644 index 3bc10652d..000000000 --- a/src/Entities/InputMedia/InputMediaVideo.php +++ /dev/null @@ -1,69 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities\InputMedia; - -use Longman\TelegramBot\Entities\Entity; - -/** - * Class InputMediaVideo - * - * @link https://core.telegram.org/bots/api#inputmediavideo - * - * - * $data = [ - * 'media' => '123abc', - * 'thumbnail' => '456def', - * 'caption' => '*Video* caption (streamable)', - * 'parse_mode' => 'markdown', - * 'width' => 800, - * 'height' => 600, - * 'duration' => 42, - * 'supports_streaming' => true, - * ]; - * - * - * @method string getType() Type of the result, must be video - * @method string getMedia() File to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet, or pass "attach://" to upload a new one using multipart/form-data under name. - * @method string getThumbnail() Optional. Thumbnail of the file sent. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail‘s width and height should not exceed 90. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can’t be reused and can be only uploaded as a new file, so you can pass “attach://” if the thumbnail was uploaded using multipart/form-data under . More info on Sending Files » - * @method string getCaption() Optional. Caption of the video to be sent, 0-200 characters - * @method string getParseMode() Optional. Mode for parsing entities in the video caption - * @method MessageEntity[] getCaptionEntities() Optional. List of special entities that appear in the caption, which can be specified instead of parse_mode - * @method int getWidth() Optional. Video width - * @method int getHeight() Optional. Video height - * @method int getDuration() Optional. Video duration - * @method bool getSupportsStreaming() Optional. Pass True, if the uploaded video is suitable for streaming - * @method bool getHasSpoiler() Optional. Pass True if the video needs to be covered with a spoiler animation - * - * @method $this setMedia(string $media) File to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet, or pass "attach://" to upload a new one using multipart/form-data under name. - * @method $this setThumbnail(string $thumbnail) Optional. Thumbnail of the file sent. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail‘s width and height should not exceed 90. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can’t be reused and can be only uploaded as a new file, so you can pass “attach://” if the thumbnail was uploaded using multipart/form-data under . More info on Sending Files » - * @method $this setCaption(string $caption) Optional. Caption of the video to be sent, 0-200 characters - * @method $this setParseMode(string $parse_mode) Optional. Mode for parsing entities in the video caption - * @method $this setCaptionEntities(array $caption_entities) Optional. List of special entities that appear in the caption, which can be specified instead of parse_mode - * @method $this setWidth(int $width) Optional. Video width - * @method $this setHeight(int $height) Optional. Video height - * @method $this setDuration(int $duration) Optional. Video duration - * @method $this setSupportsStreaming(bool $supports_streaming) Optional. Pass True, if the uploaded video is suitable for streaming - * @method $this setHasSpoiler(bool $has_spoiler) Optional. Pass True if the video needs to be covered with a spoiler animation - */ -class InputMediaVideo extends Entity implements InputMedia -{ - /** - * InputMediaVideo constructor - * - * @param array $data - */ - public function __construct(array $data = []) - { - $data['type'] = 'video'; - parent::__construct($data); - } -} diff --git a/src/Entities/InputMessageContent/InputContactMessageContent.php b/src/Entities/InputMessageContent/InputContactMessageContent.php deleted file mode 100644 index b954196ad..000000000 --- a/src/Entities/InputMessageContent/InputContactMessageContent.php +++ /dev/null @@ -1,43 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities\InputMessageContent; - -use Longman\TelegramBot\Entities\InlineQuery\InlineEntity; - -/** - * Class InputContactMessageContent - * - * @link https://core.telegram.org/bots/api#inputcontactmessagecontent - * - * - * $data = [ - * 'phone_number' => '', - * 'first_name' => '', - * 'last_name' => '', - * 'vcard' => '', - * ]; - * - * - * @method string getPhoneNumber() Contact's phone number - * @method string getFirstName() Contact's first name - * @method string getLastName() Optional. Contact's last name - * @method string getVcard() Optional. Additional data about the contact in the form of a vCard, 0-2048 bytes - * - * @method $this setPhoneNumber(string $phone_number) Contact's phone number - * @method $this setFirstName(string $first_name) Contact's first name - * @method $this setLastName(string $last_name) Optional. Contact's last name - * @method $this setVcard(string $vcard) Optional. Additional data about the contact in the form of a vCard, 0-2048 bytes - */ -class InputContactMessageContent extends InlineEntity implements InputMessageContent -{ - -} diff --git a/src/Entities/InputMessageContent/InputInvoiceMessageContent.php b/src/Entities/InputMessageContent/InputInvoiceMessageContent.php deleted file mode 100644 index 443635994..000000000 --- a/src/Entities/InputMessageContent/InputInvoiceMessageContent.php +++ /dev/null @@ -1,66 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities\InputMessageContent; - -use Longman\TelegramBot\Entities\InlineQuery\InlineEntity; - -/** - * Class InputTextMessageContent - * - * @link https://core.telegram.org/bots/api#inputinvoicemessagecontent - * - * @method string getTitle() Product name, 1-32 characters - * @method string getDescription() Product description, 1-255 characters - * @method string getPayload() Bot-defined invoice payload, 1-128 bytes. This will not be displayed to the user, use for your internal processes. - * @method string getProviderToken() Payment provider token, obtained via Botfather - * @method string getCurrency() Three-letter ISO 4217 currency code, see more on currencies - * @method LabeledPrice[] getPrices() Price breakdown, a JSON-serialized list of components (e.g. product price, tax, discount, delivery cost, delivery tax, bonus, etc.) - * @method int getMaxTipAmount() Optional. The maximum accepted amount for tips in the smallest units of the currency (integer, not float/double). For example, for a maximum tip of US$1.45 pass max_tip_amount = 145. See the exp parameter in currencies.json, it shows the number of digits past the decimal point for each currency (2 for the majority of currencies). Defaults to 0 - * @method int[] getSuggestedTipAmounts() Optional. A JSON-serialized array of suggested amounts of tip in the smallest units of the currency (integer, not float/double). At most 4 suggested tip amounts can be specified. The suggested tip amounts must be positive, passed in a strictly increased order and must not exceed max_tip_amount. - * @method string getProviderData() Optional. A JSON-serialized object for data about the invoice, which will be shared with the payment provider. A detailed description of the required fields should be provided by the payment provider. - * @method string getPhotoUrl() Optional. URL of the product photo for the invoice. Can be a photo of the goods or a marketing image for a service. People like it better when they see what they are paying for. - * @method int getPhotoSize() Optional. Photo size - * @method int getPhotoWidth() Optional. Photo width - * @method int getPhotoHeight() Optional. Photo height - * @method bool getNeedName() Optional. Pass True, if you require the user's full name to complete the order - * @method bool getNeedPhoneNumber() Optional. Pass True, if you require the user's phone number to complete the order - * @method bool getNeedEmail() Optional. Pass True, if you require the user's email address to complete the order - * @method bool getNeedShippingAddress() Optional. Pass True, if you require the user's shipping address to complete the order - * @method bool getSendPhoneNumberToProvider() Optional. Pass True, if user's phone number should be sent to provider - * @method bool getSendEmailToProvider() Optional. Pass True, if user's email address should be sent to provider - * @method bool getIsFlexible() Optional. Pass True, if the final price depends on the shipping method - * - * @method $this setTitle(string $title) Product name, 1-32 characters - * @method $this setDescription(string $description) Product description, 1-255 characters - * @method $this setPayload(string $payload) Bot-defined invoice payload, 1-128 bytes. This will not be displayed to the user, use for your internal processes. - * @method $this setProviderToken(string $provider_token) Payment provider token, obtained via Botfather - * @method $this setCurrency(string $currency) Three-letter ISO 4217 currency code, see more on currencies - * @method $this setPrices(LabeledPrice[] $prices) Price breakdown, a JSON-serialized list of components (e.g. product price, tax, discount, delivery cost, delivery tax, bonus, etc.) - * @method $this setMaxTipAmount(int $max_tip_amount) Optional. The maximum accepted amount for tips in the smallest units of the currency (integer, not float/double). For example, for a maximum tip of US$1.45 pass max_tip_amount = 145. See the exp parameter in currencies.json, it shows the number of digits past the decimal point for each currency (2 for the majority of currencies). Defaults to 0 - * @method $this setSuggestedTipAmounts(int[] $suggested_tip_amounts) Optional. A JSON-serialized array of suggested amounts of tip in the smallest units of the currency (integer, not float/double). At most 4 suggested tip amounts can be specified. The suggested tip amounts must be positive, passed in a strictly increased order and must not exceed max_tip_amount. - * @method $this setProviderData(string $provider_data) Optional. A JSON-serialized object for data about the invoice, which will be shared with the payment provider. A detailed description of the required fields should be provided by the payment provider. - * @method $this setPhotoUrl(string $photo_url) Optional. URL of the product photo for the invoice. Can be a photo of the goods or a marketing image for a service. People like it better when they see what they are paying for. - * @method $this setPhotoSize(int $photo_size) Optional. Photo size - * @method $this setPhotoWidth(int $photo_width) Optional. Photo width - * @method $this setPhotoHeight(int $photo_height) Optional. Photo height - * @method $this setNeedName(bool $need_name) Optional. Pass True, if you require the user's full name to complete the order - * @method $this setNeedPhoneNumber(bool $need_phone_number) Optional. Pass True, if you require the user's phone number to complete the order - * @method $this setNeedEmail(bool $need_email) Optional. Pass True, if you require the user's email address to complete the order - * @method $this setNeedShippingAddress(bool $need_shipping_address) Optional. Pass True, if you require the user's shipping address to complete the order - * @method $this setSendPhoneNumberToProvider(bool $send_phone_number_to_provider) Optional. Pass True, if user's phone number should be sent to provider - * @method $this setSendEmailToProvider(bool $send_email_to_provider) Optional. Pass True, if user's email address should be sent to provider - * @method $this setIsFlexible(bool $is_flexible) Optional. Pass True, if the final price depends on the shipping method - */ -class InputInvoiceMessageContent extends InlineEntity implements InputMessageContent -{ - -} diff --git a/src/Entities/InputMessageContent/InputLocationMessageContent.php b/src/Entities/InputMessageContent/InputLocationMessageContent.php deleted file mode 100644 index 2ede0d2ae..000000000 --- a/src/Entities/InputMessageContent/InputLocationMessageContent.php +++ /dev/null @@ -1,48 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities\InputMessageContent; - -use Longman\TelegramBot\Entities\InlineQuery\InlineEntity; - -/** - * Class InputLocationMessageContent - * - * @link https://core.telegram.org/bots/api#inputlocationmessagecontent - * - * - * $data = [ - * 'latitude' => 36.0338, - * 'longitude' => 71.8601, - * 'horizontal_accuracy' => 36.9, - * 'live_period' => 900, - * 'heading' => 88, - * 'proximity_alert_radius' => 300, - * ]; - * - * @method float getLatitude() Latitude of the location in degrees - * @method float getLongitude() Longitude of the location in degrees - * @method float getHorizontalAccuracy() Optional. The radius of uncertainty for the location, measured in meters; 0-1500 - * @method int getLivePeriod() Optional. Period in seconds for which the location can be updated, should be between 60 and 86400. - * @method int getHeading() Optional. For live locations, a direction in which the user is moving, in degrees. Must be between 1 and 360 if specified. - * @method int getProximityAlertRadius() Optional. For live locations, a maximum distance for proximity alerts about approaching another chat member, in meters. Must be between 1 and 100000 if specified. - * - * @method $this setLatitude(float $latitude) Latitude of the location in degrees - * @method $this setLongitude(float $longitude) Longitude of the location in degrees - * @method $this setHorizontalAccuracy(float $horizontal_accuracy) Optional. The radius of uncertainty for the location, measured in meters; 0-1500 - * @method $this setLivePeriod(int $live_period) Optional. Period in seconds for which the location can be updated, should be between 60 and 86400. - * @method $this setHeading(int $heading) Optional. For live locations, a direction in which the user is moving, in degrees. Must be between 1 and 360 if specified. - * @method $this setProximityAlertRadius(int $proximity_alert_radius) Optional. For live locations, a maximum distance for proximity alerts about approaching another chat member, in meters. Must be between 1 and 100000 if specified. - */ -class InputLocationMessageContent extends InlineEntity implements InputMessageContent -{ - -} diff --git a/src/Entities/InputMessageContent/InputMessageContent.php b/src/Entities/InputMessageContent/InputMessageContent.php deleted file mode 100644 index 3b43be9e9..000000000 --- a/src/Entities/InputMessageContent/InputMessageContent.php +++ /dev/null @@ -1,8 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities\InputMessageContent; - -use Longman\TelegramBot\Entities\InlineQuery\InlineEntity; - -/** - * Represents the content of a text message to be sent as the result of an inline query. - * - * @link https://core.telegram.org/bots/api#inputtextmessagecontent - * - * @method string getMessageText() Text of the message to be sent, 1-4096 characters. - * @method string getParseMode() Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in your bot's message. - * @method MessageEntity[] getEntities() Optional. List of special entities that appear in the caption, which can be specified instead of parse_mode - * @method LinkPreviewOptions getLinkPreviewOptions() Optional. Link preview generation options for the message - * - * @method $this setMessageText(string $message_text) Text of the message to be sent, 1-4096 characters. - * @method $this setParseMode(string $parse_mode) Optional. Send Markdown or HTML, if you want Telegram apps to show bold, italic, fixed-width text or inline URLs in your bot's message. - * @method $this setEntities(array $entities) Optional. List of special entities that appear in the caption, which can be specified instead of parse_mode - * @method $this setLinkPreviewOptions(LinkPreviewOptions $link_preview_options) Optional. Link preview generation options for the message - */ -class InputTextMessageContent extends InlineEntity implements InputMessageContent -{ - -} diff --git a/src/Entities/InputMessageContent/InputVenueMessageContent.php b/src/Entities/InputMessageContent/InputVenueMessageContent.php deleted file mode 100644 index ec5b12a8e..000000000 --- a/src/Entities/InputMessageContent/InputVenueMessageContent.php +++ /dev/null @@ -1,53 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities\InputMessageContent; - -use Longman\TelegramBot\Entities\InlineQuery\InlineEntity; - -/** - * Class InputVenueMessageContent - * - * @link https://core.telegram.org/bots/api#inputvenuemessagecontent - * - * - * $data = [ - * 'latitude' => 36.0338, - * 'longitude' => 71.8601, - * 'title' => '', - * 'address' => '', - * 'foursquare_id' => '', - * 'foursquare_type' => '', - * ]; - * - * - * @method float getLatitude() Latitude of the location in degrees - * @method float getLongitude() Longitude of the location in degrees - * @method string getTitle() Name of the venue - * @method string getAddress() Address of the venue - * @method string getFoursquareId() Optional. Foursquare identifier of the venue, if known - * @method string getFoursquareType() Optional. Foursquare type of the venue, if known. (For example, “arts_entertainment/default”, “arts_entertainment/aquarium” or “food/icecream”.) - * @method string getGooglePlaceId() Optional. Google Places identifier of the venue - * @method string getGooglePlaceType() Optional. Google Places type of the venue - * - * @method $this setLatitude(float $latitude) Latitude of the location in degrees - * @method $this setLongitude(float $longitude) Longitude of the location in degrees - * @method $this setTitle(string $title) Name of the venue - * @method $this setAddress(string $address) Address of the venue - * @method $this setFoursquareId(string $foursquare_id) Optional. Foursquare identifier of the venue, if known - * @method $this setFoursquareType(string $foursquare_type) Optional. Foursquare type of the venue, if known. (For example, “arts_entertainment/default”, “arts_entertainment/aquarium” or “food/icecream”.) - * @method $this setGooglePlaceId(string $google_place_id) Optional. Google Places identifier of the venue - * @method $this setGooglePlaceType(string $google_place_type) Optional. Google Places type of the venue - */ -class InputVenueMessageContent extends InlineEntity implements InputMessageContent -{ - -} diff --git a/src/Entities/InputSticker.php b/src/Entities/InputSticker.php deleted file mode 100644 index 2fa40e018..000000000 --- a/src/Entities/InputSticker.php +++ /dev/null @@ -1,20 +0,0 @@ -” to upload a new one using multipart/form-data under name. Animated and video stickers can't be uploaded via HTTP URL. More information on Sending Files » - * @method string[] getEmojiList() List of 1-20 emoji associated with the sticker - * @method MaskPosition getMaskPosition() Optional. Position where the mask should be placed on faces. For “mask” stickers only. - * @method string[] getKeywords() Optional. List of 0-20 search keywords for the sticker with total length of up to 64 characters. For “regular” and “custom_emoji” stickers only. - */ -class InputSticker extends Entity -{ - -} diff --git a/src/Entities/Keyboard.php b/src/Entities/Keyboard.php deleted file mode 100644 index eda4ed71c..000000000 --- a/src/Entities/Keyboard.php +++ /dev/null @@ -1,231 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - * - * Written by Marco Boretto - */ - -namespace Longman\TelegramBot\Entities; - -use Longman\TelegramBot\Exception\TelegramException; - -/** - * Class Keyboard - * - * @link https://core.telegram.org/bots/api#replykeyboardmarkup - * - * @method bool getIsPersistent() Optional. Requests clients to always show the keyboard when the regular keyboard is hidden. Defaults to false, in which case the custom keyboard can be hidden and opened with a keyboard icon. - * @method bool getResizeKeyboard() Optional. Requests clients to resize the keyboard vertically for optimal fit (e.g., make the keyboard smaller if there are just two rows of buttons). Defaults to false, in which case the custom keyboard is always of the same height as the app's standard keyboard. - * @method bool getOneTimeKeyboard() Optional. Requests clients to remove the keyboard as soon as it's been used. The keyboard will still be available, but clients will automatically display the usual letter-keyboard in the chat – the user can press a special button in the input field to see the custom keyboard again. Defaults to false. - * @method string getInputFieldPlaceholder() Optional. The placeholder to be shown in the input field when the keyboard is active; 1-64 characters - * @method bool getSelective() Optional. Use this parameter if you want to show the keyboard to specific users only. Targets: 1) users that are @mentioned in the text of the Message object; 2) if the bot's message is a reply (has reply_to_message_id), sender of the original message. - * - * @method $this setIsPersistent(bool $is_persistent) Optional. Requests clients to always show the keyboard when the regular keyboard is hidden. Defaults to false, in which case the custom keyboard can be hidden and opened with a keyboard icon. - * @method $this setResizeKeyboard(bool $resize_keyboard) Optional. Requests clients to resize the keyboard vertically for optimal fit (e.g., make the keyboard smaller if there are just two rows of buttons). Defaults to false, in which case the custom keyboard is always of the same height as the app's standard keyboard. - * @method $this setOneTimeKeyboard(bool $one_time_keyboard) Optional. Requests clients to remove the keyboard as soon as it's been used. The keyboard will still be available, but clients will automatically display the usual letter-keyboard in the chat – the user can press a special button in the input field to see the custom keyboard again. Defaults to false. - * @method $this setInputFieldPlaceholder(string $input_field_placeholder) Optional. The placeholder to be shown in the input field when the keyboard is active; 1-64 characters - * @method $this setSelective(bool $selective) Optional. Use this parameter if you want to show the keyboard to specific users only. Targets: 1) users that are @mentioned in the text of the Message object; 2) if the bot's message is a reply (has reply_to_message_id), sender of the original message. - */ -class Keyboard extends Entity -{ - public function __construct() - { - $data = $this->createFromParams(...func_get_args()); - parent::__construct($data); - - // Remove any empty buttons. - $this->{$this->getKeyboardType()} = array_filter($this->{$this->getKeyboardType()}); - } - - /** - * If this keyboard is an inline keyboard. - * - * @return bool - */ - public function isInlineKeyboard(): bool - { - return $this instanceof InlineKeyboard; - } - - /** - * Get the proper keyboard button class for this keyboard. - * - * @return string - */ - public function getKeyboardButtonClass(): string - { - return $this->isInlineKeyboard() ? InlineKeyboardButton::class : KeyboardButton::class; - } - - /** - * Get the type of keyboard, either "inline_keyboard" or "keyboard". - * - * @return string - */ - public function getKeyboardType(): string - { - return $this->isInlineKeyboard() ? 'inline_keyboard' : 'keyboard'; - } - - /** - * If no explicit keyboard is passed, try to create one from the parameters. - * - * @return array - */ - protected function createFromParams(): array - { - $keyboard_type = $this->getKeyboardType(); - - $args = func_get_args(); - - // Force button parameters into individual rows. - foreach ($args as &$arg) { - !is_array($arg) && $arg = [$arg]; - } - unset($arg); - - $data = reset($args); - - if ($from_data = array_key_exists($keyboard_type, (array) $data)) { - $args = $data[$keyboard_type]; - - // Make sure we're working with a proper row. - if (!is_array($args)) { - $args = []; - } - } - - $new_keyboard = []; - foreach ($args as $row) { - $new_keyboard[] = $this->parseRow($row); - } - - if (!empty($new_keyboard)) { - if (!$from_data) { - $data = []; - } - $data[$keyboard_type] = $new_keyboard; - } - - // If $args was empty, $data still contains `false` - return $data ?: []; - } - - /** - * Create a new row in keyboard and add buttons. - * - * @return Keyboard - */ - public function addRow(): Keyboard - { - if (($new_row = $this->parseRow(func_get_args())) !== null) { - // Workaround for "Indirect modification of overloaded property has no effect" notice in PHP 8.2. - // https://stackoverflow.com/a/19749730/3757422 - $keyboard = $this->{$this->getKeyboardType()}; - $keyboard[] = $new_row; - $this->{$this->getKeyboardType()} = $keyboard; - } - - return $this; - } - - /** - * Parse a given row to the correct array format. - * - * @param array|string $row - * - * @return array|null - */ - protected function parseRow($row): ?array - { - if (!is_array($row)) { - return null; - } - - $new_row = []; - foreach ($row as $button) { - if (($new_button = $this->parseButton($button)) !== null) { - $new_row[] = $new_button; - } - } - - return $new_row; - } - - /** - * Parse a given button to the correct KeyboardButton object type. - * - * @param array|string|KeyboardButton $button - * - * @return KeyboardButton|null - */ - protected function parseButton($button): ?KeyboardButton - { - $button_class = $this->getKeyboardButtonClass(); - - if ($button instanceof $button_class) { - return $button; - } - - if (!$this->isInlineKeyboard() || call_user_func([$button_class, 'couldBe'], $button)) { - return new $button_class($button); - } - - return null; - } - - /** - * {@inheritdoc} - */ - protected function validate(): void - { - $keyboard_type = $this->getKeyboardType(); - $keyboard = $this->getProperty($keyboard_type); - - if ($keyboard !== null) { - if (!is_array($keyboard)) { - throw new TelegramException($keyboard_type . ' field is not an array!'); - } - - foreach ($keyboard as $item) { - if (!is_array($item)) { - throw new TelegramException($keyboard_type . ' subfield is not an array!'); - } - } - } - } - - /** - * Remove the current custom keyboard and display the default letter-keyboard. - * - * @link https://core.telegram.org/bots/api/#replykeyboardremove - * - * @param array $data - * - * @return Keyboard - */ - public static function remove(array $data = []): Keyboard - { - return new static(array_merge(['keyboard' => [], 'remove_keyboard' => true, 'selective' => false], $data)); - } - - /** - * Display a reply interface to the user (act as if the user has selected the bot's message and tapped 'Reply'). - * - * @link https://core.telegram.org/bots/api#forcereply - * - * @param array $data - * - * @return Keyboard - */ - public static function forceReply(array $data = []): Keyboard - { - return new static(array_merge(['keyboard' => [], 'force_reply' => true, 'selective' => false], $data)); - } -} diff --git a/src/Entities/KeyboardButton.php b/src/Entities/KeyboardButton.php deleted file mode 100644 index 25cc7f043..000000000 --- a/src/Entities/KeyboardButton.php +++ /dev/null @@ -1,91 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -/** - * Class KeyboardButton - * - * This object represents one button of the reply keyboard. For simple text buttons String can be used instead of this object to specify text of the button. Optional fields request_contact, request_location, and request_poll are mutually exclusive. - * - * @link https://core.telegram.org/bots/api#keyboardbutton - * - * @property KeyboardButtonRequestUsers $request_users - * @property KeyboardButtonRequestChat $request_chat - * @property bool $request_contact - * @property bool $request_location - * @property KeyboardButtonPollType $request_poll - * @property WebAppInfo $web_app - * - * @method string getText() Text of the button. If none of the optional fields are used, it will be sent to the bot as a message when the button is pressed - * @method KeyboardButtonRequestUsers getRequestUsers() Optional. If specified, pressing the button will open a list of suitable users. Identifiers of selected users will be sent to the bot in a “users_shared” service message. Available in private chats only. - * @method KeyboardButtonRequestChat getRequestChat() Optional. If specified, pressing the button will open a list of suitable chats. Tapping on a chat will send its identifier to the bot in a “chat_shared” service message. Available in private chats only. - * @method bool getRequestContact() Optional. If True, the user's phone number will be sent as a contact when the button is pressed. Available in private chats only - * @method bool getRequestLocation() Optional. If True, the user's current location will be sent when the button is pressed. Available in private chats only - * @method KeyboardButtonPollType getRequestPoll() Optional. If specified, the user will be asked to create a poll and send it to the bot when the button is pressed. Available in private chats only - * @method WebAppInfo getWebApp() Optional. If specified, the described Web App will be launched when the button is pressed. The Web App will be able to send a “web_app_data” service message. Available in private chats only. - * - * @method $this setText(string $text) Text of the button. If none of the optional fields are used, it will be sent to the bot as a message when the button is pressed - * @method $this setRequestUsers(KeyboardButtonRequestUsers $request_users) Optional. If specified, pressing the button will open a list of suitable users. Identifiers of selected users will be sent to the bot in a “users_shared” service message. Available in private chats only. - * @method $this setRequestChat(KeyboardButtonRequestChat $request_chat) Optional. If specified, pressing the button will open a list of suitable chats. Tapping on a chat will send its identifier to the bot in a “chat_shared” service message. Available in private chats only. - * @method $this setRequestContact(bool $request_contact) Optional. If True, the user's phone number will be sent as a contact when the button is pressed. Available in private chats only - * @method $this setRequestLocation(bool $request_location) Optional. If True, the user's current location will be sent when the button is pressed. Available in private chats only - * @method $this setRequestPoll(KeyboardButtonPollType $request_poll) Optional. If specified, the user will be asked to create a poll and send it to the bot when the button is pressed. Available in private chats only - * @method $this setWebApp(WebAppInfo $web_app) Optional. If specified, the described Web App will be launched when the button is pressed. The Web App will be able to send a “web_app_data” service message. Available in private chats only. - */ -class KeyboardButton extends Entity -{ - /** - * @param array|string $data - */ - public function __construct($data) - { - if (is_string($data)) { - $data = ['text' => $data]; - } - parent::__construct($data); - } - - protected function subEntities(): array - { - return [ - 'request_users' => KeyboardButtonRequestUsers::class, - 'request_chat' => KeyboardButtonRequestChat::class, - 'request_poll' => KeyboardButtonPollType::class, - 'web_app' => WebAppInfo::class, - ]; - } - - /** - * Check if the passed data array could be a KeyboardButton. - * - * @param array $data - * - * @return bool - */ - public static function couldBe(array $data): bool - { - return array_key_exists('text', $data); - } - - /** - * {@inheritdoc} - */ - public function __call($method, $args) - { - // Only 1 of these can be set, so clear the others when setting a new one. - if (in_array($method, ['setRequestUsers', 'setRequestChat', 'setRequestContact', 'setRequestLocation', 'setRequestPoll', 'setWebApp'], true)) { - unset($this->request_users, $this->request_chat, $this->request_contact, $this->request_location, $this->request_poll, $this->web_app); - } - - return parent::__call($method, $args); - } -} diff --git a/src/Entities/KeyboardButtonPollType.php b/src/Entities/KeyboardButtonPollType.php deleted file mode 100644 index f11023123..000000000 --- a/src/Entities/KeyboardButtonPollType.php +++ /dev/null @@ -1,28 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -/** - * Class KeyboardButtonPollType - * - * This entity represents type of a poll, which is allowed to be created and sent when the corresponding button is pressed. - * - * @link https://core.telegram.org/bots/api#keyboardbutton - * - * @method string getType() Optional. If 'quiz' is passed, the user will be allowed to create only polls in the quiz mode. If 'regular' is passed, only regular polls will be allowed. Otherwise, the user will be allowed to create a poll of any type. - * - * @method $this setType(string $type) Optional. If 'quiz' is passed, the user will be allowed to create only polls in the quiz mode. If 'regular' is passed, only regular polls will be allowed. Otherwise, the user will be allowed to create a poll of any type. - */ -class KeyboardButtonPollType extends Entity -{ - -} diff --git a/src/Entities/KeyboardButtonRequestChat.php b/src/Entities/KeyboardButtonRequestChat.php deleted file mode 100644 index 02cdf29fe..000000000 --- a/src/Entities/KeyboardButtonRequestChat.php +++ /dev/null @@ -1,33 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -/** - * Describes the options used for link preview generation. - * - * @link https://core.telegram.org/bots/api#linkpreviewoptions - * - * @method bool getIsDisabled() Optional. True, if the link preview is disabled - * @method string getUrl() Optional. URL to use for the link preview. If empty, then the first URL found in the message text will be used - * @method bool getPreferSmallMedia() Optional. True, if the media in the link preview is supposed to be shrunk; ignored if the URL isn't explicitly specified or media size change isn't supported for the preview - * @method bool getPreferLargeMedia() Optional. True, if the media in the link preview is supposed to be enlarged; ignored if the URL isn't explicitly specified or media size change isn't supported for the preview - * @method bool getShowAboveText() Optional. True, if the link preview must be shown above the message text; otherwise, the link preview will be shown below the message text * - */ -class LinkPreviewOptions extends Entity -{ - -} diff --git a/src/Entities/Location.php b/src/Entities/Location.php deleted file mode 100644 index b4c67fe52..000000000 --- a/src/Entities/Location.php +++ /dev/null @@ -1,29 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -/** - * Class Location - * - * @link https://core.telegram.org/bots/api#location - * - * @method float getLongitude() Longitude as defined by sender - * @method float getLatitude() Latitude as defined by sender - * @method float getHorizontalAccuracy() Optional. The radius of uncertainty for the location, measured in meters; 0-1500 - * @method int getLivePeriod() Optional. Time relative to the message sending date, during which the location can be updated, in seconds. For active live locations only. - * @method int getHeading() Optional. The direction in which user is moving, in degrees; 1-360. For active live locations only. - * @method int getProximityAlertRadius() Optional. Maximum distance for proximity alerts about approaching another chat member, in meters. For sent live locations only. - */ -class Location extends Entity -{ - -} diff --git a/src/Entities/LoginUrl.php b/src/Entities/LoginUrl.php deleted file mode 100644 index 9d4c22a6b..000000000 --- a/src/Entities/LoginUrl.php +++ /dev/null @@ -1,29 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -/** - * Class LoginUrl - * - * This object represents a parameter of the inline keyboard button used to automatically authorize a user. - * - * @link https://core.telegram.org/bots/api#loginurl - * - * @method string getUrl() An HTTP URL to be opened with user authorization data added to the query string when the button is pressed. If the user refuses to provide authorization data, the original URL without information about the user will be opened. The data added is the same as described in Receiving authorization data. - * @method string getForwardText() Optional. New text of the button in forwarded messages. - * @method string getBotUsername() Optional. Username of a bot, which will be used for user authorization. See Setting up a bot for more details. If not specified, the current bot's username will be assumed. The url's domain must be the same as the domain linked with the bot. See Linking your domain to the bot for more details. - * @method bool getRequestWriteAccess() Optional. Pass True to request the permission for your bot to send messages to the user. - */ -class LoginUrl extends Entity -{ - -} diff --git a/src/Entities/MaskPosition.php b/src/Entities/MaskPosition.php deleted file mode 100644 index 76b62425b..000000000 --- a/src/Entities/MaskPosition.php +++ /dev/null @@ -1,27 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -/** - * Class MaskPosition - * - * @link https://core.telegram.org/bots/api#maskposition - * - * @method string getPoint() The part of the face relative to which the mask should be placed. One of “forehead”, “eyes”, “mouth”, or “chin”. - * @method float getXShift() Shift by X-axis measured in widths of the mask scaled to the face size, from left to right. For example, choosing -1.0 will place mask just to the left of the default mask position. - * @method float getYShift() Shift by Y-axis measured in heights of the mask scaled to the face size, from top to bottom. For example, 1.0 will place the mask just below the default mask position. - * @method float getScale() Mask scaling coefficient. For example, 2.0 means double size. - */ -class MaskPosition extends Entity -{ - -} diff --git a/src/Entities/MenuButton/Factory.php b/src/Entities/MenuButton/Factory.php deleted file mode 100644 index 86fbf9fa4..000000000 --- a/src/Entities/MenuButton/Factory.php +++ /dev/null @@ -1,24 +0,0 @@ - MenuButtonCommands::class, - 'web_app' => MenuButtonWebApp::class, - 'default' => MenuButtonDefault::class, - ]; - - if (! isset($type[$data['type'] ?? ''])) { - return new MenuButtonNotImplemented($data, $bot_username); - } - - $class = $type[$data['type']]; - return new $class($data, $bot_username); - } -} diff --git a/src/Entities/MenuButton/MenuButton.php b/src/Entities/MenuButton/MenuButton.php deleted file mode 100644 index 216d947ae..000000000 --- a/src/Entities/MenuButton/MenuButton.php +++ /dev/null @@ -1,15 +0,0 @@ - WebAppInfo::class, - ]; - } -} diff --git a/src/Entities/Message.php b/src/Entities/Message.php deleted file mode 100644 index 8f771a305..000000000 --- a/src/Entities/Message.php +++ /dev/null @@ -1,337 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -use Longman\TelegramBot\Entities\Games\Game; -use Longman\TelegramBot\Entities\Giveaway\Giveaway; -use Longman\TelegramBot\Entities\Giveaway\GiveawayCompleted; -use Longman\TelegramBot\Entities\Giveaway\GiveawayCreated; -use Longman\TelegramBot\Entities\Giveaway\GiveawayWinners; -use Longman\TelegramBot\Entities\Message\Factory as MaybeInaccessibleMessageFactory; -use Longman\TelegramBot\Entities\Message\MaybeInaccessibleMessage; -use Longman\TelegramBot\Entities\MessageOrigin\Factory as MessageOriginFactory; -use Longman\TelegramBot\Entities\MessageOrigin\MessageOrigin; -use Longman\TelegramBot\Entities\Payments\Invoice; -use Longman\TelegramBot\Entities\Payments\SuccessfulPayment; -use Longman\TelegramBot\Entities\TelegramPassport\PassportData; -use Longman\TelegramBot\Entities\Topics\ForumTopicClosed; -use Longman\TelegramBot\Entities\Topics\ForumTopicCreated; -use Longman\TelegramBot\Entities\Topics\ForumTopicEdited; -use Longman\TelegramBot\Entities\Topics\ForumTopicReopened; -use Longman\TelegramBot\Entities\Topics\GeneralForumTopicHidden; -use Longman\TelegramBot\Entities\Topics\GeneralForumTopicUnhidden; - -/** - * Class Message - * - * Represents a message - * - * @link https://core.telegram.org/bots/api#message - * - * @method int getMessageId() Unique message identifier - * @method int getMessageThreadId() Optional. Unique identifier of a message thread to which the message belongs; for supergroups only - * @method User getFrom() Optional. Sender, can be empty for messages sent to channels - * @method Chat getSenderChat() Optional. Sender of the message, sent on behalf of a chat. The channel itself for channel messages. The supergroup itself for messages from anonymous group administrators. The linked channel for messages automatically forwarded to the discussion group - * @method int getDate() Date the message was sent in Unix time - * @method Chat getChat() Conversation the message belongs to - * @method MessageOrigin getForwardOrigin() Optional. Information about the original message for forwarded messages - * @method bool getIsTopicMessage() Optional. True, if the message is sent to a forum topic - * @method bool getIsAutomaticForward() Optional. True, if the message is a channel post that was automatically forwarded to the connected discussion group - * @method ReplyToMessage getReplyToMessage() Optional. For replies, the original message. Note that the Message object in this field will not contain further reply_to_message fields even if it itself is a reply. - * @method ExternalReplyInfo getExternalReply() Optional. Information about the message that is being replied to, which may come from another chat or forum topic - * @method TextQuote getQuote() Optional. For replies that quote part of the original message, the quoted part of the message - * @method User getViaBot() Optional. Bot through which the message was sent - * @method int getEditDate() Optional. Date the message was last edited in Unix time - * @method bool getHasProtectedContent() Optional. True, if the message can't be forwarded - * @method string getMediaGroupId() Optional. The unique identifier of a media message group this message belongs to - * @method string getAuthorSignature() Optional. Signature of the post author for messages in channels - * @method LinkPreviewOptions getLinkPreviewOptions() Optional. Options used for link preview generation for the message, if it is a text message and link preview options were changed - * @method MessageEntity[] getEntities() Optional. For text messages, special entities like usernames, URLs, bot commands, etc. that appear in the text - * @method MessageEntity[] getCaptionEntities() Optional. For messages with a caption, special entities like usernames, URLs, bot commands, etc. that appear in the caption - * @method Audio getAudio() Optional. Message is an audio file, information about the file - * @method Document getDocument() Optional. Message is a general file, information about the file - * @method Animation getAnimation() Optional. Message is an animation, information about the animation. For backward compatibility, when this field is set, the document field will also be set - * @method Game getGame() Optional. Message is a game, information about the game. - * @method PhotoSize[] getPhoto() Optional. Message is a photo, available sizes of the photo - * @method Sticker getSticker() Optional. Message is a sticker, information about the sticker - * @method Story getStory() Optional. Message is a forwarded story - * @method Video getVideo() Optional. Message is a video, information about the video - * @method Voice getVoice() Optional. Message is a voice message, information about the file - * @method VideoNote getVideoNote() Optional. Message is a video note message, information about the video - * @method string getCaption() Optional. Caption for the document, photo or video, 0-200 characters - * @method bool getHasMediaSpoiler() Optional. True, if the message media is covered by a spoiler animation - * @method Contact getContact() Optional. Message is a shared contact, information about the contact - * @method Location getLocation() Optional. Message is a shared location, information about the location - * @method Venue getVenue() Optional. Message is a venue, information about the venue - * @method Poll getPoll() Optional. Message is a native poll, information about the poll - * @method Dice getDice() Optional. Message is a dice with random value, 1-6 for “🎲” and “🎯” base emoji, 1-5 for “🏀” and “⚽” base emoji, 1-64 for “🎰” base emoji - * @method User[] getNewChatMembers() Optional. A new member(s) was added to the group, information about them (one of this members may be the bot itself) - * @method User getLeftChatMember() Optional. A member was removed from the group, information about them (this member may be the bot itself) - * @method string getNewChatTitle() Optional. A chat title was changed to this value - * @method PhotoSize[] getNewChatPhoto() Optional. A chat photo was changed to this value - * @method MessageAutoDeleteTimerChanged getMessageAutoDeleteTimerChanged() Optional. Service message: auto-delete timer settings changed in the chat - * @method bool getDeleteChatPhoto() Optional. Service message: the chat photo was deleted - * @method bool getGroupChatCreated() Optional. Service message: the group has been created - * @method bool getSupergroupChatCreated() Optional. Service message: the supergroup has been created. This field can't be received in a message coming through updates, because bot can’t be a member of a supergroup when it is created. It can only be found in reply_to_message if someone replies to a very first message in a directly created supergroup. - * @method bool getChannelChatCreated() Optional. Service message: the channel has been created. This field can't be received in a message coming through updates, because bot can’t be a member of a channel when it is created. It can only be found in reply_to_message if someone replies to a very first message in a channel. - * @method int getMigrateToChatId() Optional. The group has been migrated to a supergroup with the specified identifier. This number may be greater than 32 bits and some programming languages may have difficulty/silent defects in interpreting it. But it smaller than 52 bits, so a signed 64 bit integer or double-precision float type are safe for storing this identifier. - * @method int getMigrateFromChatId() Optional. The supergroup has been migrated from a group with the specified identifier. This number may be greater than 32 bits and some programming languages may have difficulty/silent defects in interpreting it. But it smaller than 52 bits, so a signed 64 bit integer or double-precision float type are safe for storing this identifier. - * @method MaybeInaccessibleMessage getPinnedMessage() Optional. Specified message was pinned. Note that the Message object in this field will not contain further reply_to_message fields even if it is itself a reply. - * @method Invoice getInvoice() Optional. Message is an invoice for a payment, information about the invoice. - * @method SuccessfulPayment getSuccessfulPayment() Optional. Message is a service message about a successful payment, information about the payment. - * @method UsersShared getUsersShared() Optional. Service message: users were shared with the bot - * @method ChatShared getChatShared() Optional. Service message: a chat was shared with the bot - * @method string getConnectedWebsite() Optional. The domain name of the website on which the user has logged in. - * @method WriteAccessAllowed getWriteAccessAllowed() Optional. Service message: the user allowed the bot added to the attachment menu to write messages - * @method PassportData getPassportData() Optional. Telegram Passport data - * @method ProximityAlertTriggered getProximityAlertTriggered() Optional. Service message. A user in the chat triggered another user's proximity alert while sharing Live Location. - * @method ForumTopicCreated getForumTopicCreated() Optional. Service message: forum topic created - * @method ForumTopicEdited getForumTopicEdited() Optional. Service message: forum topic edited - * @method ForumTopicClosed getForumTopicClosed() Optional. Service message: forum topic closed - * @method ForumTopicReopened getForumTopicReopened() Optional. Service message: forum topic reopened - * @method GeneralForumTopicHidden getGeneralForumTopicHidden() Optional. Service message: the 'General' forum topic hidden - * @method GeneralForumTopicUnhidden getGeneralForumTopicUnhidden() Optional. Service message: the 'General' forum topic unhidden - * @method GiveawayCreated getGiveawayCreated() Optional. Service message: a scheduled giveaway was created - * @method Giveaway getGiveaway() Optional. The message is a scheduled giveaway message - * @method GiveawayWinners getGiveawayWinners() Optional. A giveaway with public winners was completed - * @method GiveawayCompleted getGiveawayCompleted() Optional. Service message: a giveaway without public winners was completed - * @method VideoChatScheduled getVideoChatScheduled() Optional. Service message: voice chat scheduled - * @method VideoChatStarted getVideoChatStarted() Optional. Service message: voice chat started - * @method VideoChatEnded getVideoChatEnded() Optional. Service message: voice chat ended - * @method VideoChatParticipantsInvited getVideoChatParticipantsInvited() Optional. Service message: new participants invited to a voice chat - * @method WebAppData getWebAppData() Optional. Service message: data sent by a Web App - * @method InlineKeyboard getReplyMarkup() Optional. Inline keyboard attached to the message. login_url buttons are represented as ordinary url buttons. - */ -class Message extends Entity implements MaybeInaccessibleMessage -{ - /** - * {@inheritdoc} - */ - protected function subEntities(): array - { - return [ - 'from' => User::class, - 'sender_chat' => Chat::class, - 'chat' => Chat::class, - 'forward_origin' => MessageOriginFactory::class, - 'reply_to_message' => ReplyToMessage::class, - 'external_reply' => ExternalReplyInfo::class, - 'quote' => TextQuote::class, - 'via_bot' => User::class, - 'link_preview_options' => LinkPreviewOptions::class, - 'entities' => [MessageEntity::class], - 'animation' => Animation::class, - 'audio' => Audio::class, - 'document' => Document::class, - 'photo' => [PhotoSize::class], - 'sticker' => Sticker::class, - 'story' => Story::class, - 'video' => Video::class, - 'video_note' => VideoNote::class, - 'voice' => Voice::class, - 'caption_entities' => [MessageEntity::class], - 'contact' => Contact::class, - 'dice' => Dice::class, - 'game' => Game::class, - 'poll' => Poll::class, - 'venue' => Venue::class, - 'location' => Location::class, - 'new_chat_members' => [User::class], - 'left_chat_member' => User::class, - 'new_chat_photo' => [PhotoSize::class], - 'message_auto_delete_timer_changed' => MessageAutoDeleteTimerChanged::class, - 'pinned_message' => MaybeInaccessibleMessageFactory::class, - 'invoice' => Invoice::class, - 'successful_payment' => SuccessfulPayment::class, - 'users_shared' => UsersShared::class, - 'chat_shared' => ChatShared::class, - 'write_access_allowed' => WriteAccessAllowed::class, - 'passport_data' => PassportData::class, - 'proximity_alert_triggered' => ProximityAlertTriggered::class, - 'forum_topic_created' => ForumTopicCreated::class, - 'forum_topic_edited' => ForumTopicEdited::class, - 'forum_topic_closed' => ForumTopicClosed::class, - 'forum_topic_reopened' => ForumTopicReopened::class, - 'general_forum_topic_hidden' => GeneralForumTopicHidden::class, - 'general_forum_topic_unhidden' => GeneralForumTopicUnhidden::class, - 'giveaway_created' => GiveawayCreated::class, - 'giveaway' => Giveaway::class, - 'giveaway_winners' => GiveawayWinners::class, - 'giveaway_completed' => GiveawayCompleted::class, - 'video_chat_scheduled' => VideoChatScheduled::class, - 'video_chat_started' => VideoChatStarted::class, - 'video_chat_ended' => VideoChatEnded::class, - 'video_chat_participants_invited' => VideoChatParticipantsInvited::class, - 'web_app_data' => WebAppData::class, - 'reply_markup' => InlineKeyboard::class, - ]; - } - - /** - * return the entire command like /echo or /echo@bot1 if specified - * - * @return string|null - */ - public function getFullCommand(): ?string - { - $text = $this->getProperty('text') ?? ''; - if (strpos($text, '/') !== 0) { - return null; - } - - $no_EOL = strtok($text, PHP_EOL); - $no_space = strtok($text, ' '); - - //try to understand which separator \n or space divide /command from text - return strlen($no_space) < strlen($no_EOL) ? $no_space : $no_EOL; - } - - /** - * Get command - * - * @return string|null - */ - public function getCommand(): ?string - { - if ($command = $this->getProperty('command')) { - return $command; - } - - $full_command = $this->getFullCommand() ?? ''; - if (strpos($full_command, '/') !== 0) { - return null; - } - $full_command = substr($full_command, 1); - - //check if command is followed by bot username - $split_cmd = explode('@', $full_command); - if (! isset($split_cmd[1])) { - //command is not followed by name - return $full_command; - } - - if (strtolower($split_cmd[1]) === strtolower($this->getBotUsername())) { - //command is addressed to me - return $split_cmd[0]; - } - - return null; - } - - /** - * For text messages, the actual UTF-8 text of the message, 0-4096 characters. - * - * @param bool $without_cmd - * - * @return string|null - */ - public function getText($without_cmd = false): ?string - { - $text = $this->getProperty('text'); - - if ($without_cmd && $command = $this->getFullCommand()) { - if (strlen($command) + 1 < strlen($text)) { - return substr($text, strlen($command) + 1); - } - - return ''; - } - - return $text; - } - - /** - * Bot added in chat - * - * @return bool - */ - public function botAddedInChat(): bool - { - foreach ($this->getNewChatMembers() as $member) { - if ($member instanceof User && $member->getUsername() === $this->getBotUsername()) { - return true; - } - } - - return false; - } - - /** - * Detect type based on properties. - * - * @return string - */ - public function getType(): string - { - $types = [ - 'text', - 'animation', - 'audio', - 'document', - 'photo', - 'sticker', - 'video', - 'video_note', - 'voice', - 'contact', - 'dice', - 'game', - 'poll', - 'venue', - 'location', - 'new_chat_members', - 'left_chat_member', - 'new_chat_title', - 'new_chat_photo', - 'delete_chat_photo', - 'group_chat_created', - 'supergroup_chat_created', - 'channel_chat_created', - 'message_auto_delete_timer_changed', - 'migrate_to_chat_id', - 'migrate_from_chat_id', - 'pinned_message', - 'invoice', - 'successful_payment', - 'users_shared', - 'chat_shared', - 'write_access_allowed', - 'passport_data', - 'proximity_alert_triggered', - 'forum_topic_created', - 'forum_topic_edited', - 'forum_topic_closed', - 'forum_topic_reopened', - 'general_forum_topic_hidden', - 'general_forum_topic_unhidden', - 'video_chat_scheduled', - 'video_chat_started', - 'video_chat_ended', - 'video_chat_participants_invited', - 'web_app_data', - 'reply_markup', - ]; - - $is_command = $this->getCommand() !== null; - foreach ($types as $type) { - if ($this->getProperty($type) !== null) { - if ($is_command && $type === 'text') { - return 'command'; - } - - return $type; - } - } - - return 'message'; - } -} diff --git a/src/Entities/Message/Factory.php b/src/Entities/Message/Factory.php deleted file mode 100644 index 63ce362ef..000000000 --- a/src/Entities/Message/Factory.php +++ /dev/null @@ -1,21 +0,0 @@ - Chat::class, - ]; - } -} diff --git a/src/Entities/Message/MaybeInaccessibleMessage.php b/src/Entities/Message/MaybeInaccessibleMessage.php deleted file mode 100644 index 8e9d1b85c..000000000 --- a/src/Entities/Message/MaybeInaccessibleMessage.php +++ /dev/null @@ -1,15 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -/** - * Class MessageAutoDeleteTimerChanged - * - * Represents a service message about a change in auto-delete timer settings - * - * @link https://core.telegram.org/bots/api#messageautodeletetimerchanged - * - * @method int getMessageAutoDeleteTime() New auto-delete time for messages in the chat - */ -class MessageAutoDeleteTimerChanged extends Entity -{ - -} diff --git a/src/Entities/MessageEntity.php b/src/Entities/MessageEntity.php deleted file mode 100644 index d09ab4684..000000000 --- a/src/Entities/MessageEntity.php +++ /dev/null @@ -1,38 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -/** - * Class MessageEntity - * - * @link https://core.telegram.org/bots/api#messageentity - * - * @method string getType() Type of the entity. Currently, can be “mention” (@username), “hashtag” (#hashtag), “cashtag” ($USD), “bot_command” (/start@jobs_bot), “url” (https://telegram.org), “email” (do-not-reply@telegram.org), “phone_number” (+1-212-555-0123), “bold” (bold text), “italic” (italic text), “underline” (underlined text), “strikethrough” (strikethrough text), “spoiler” (spoiler message), “code” (monowidth string), “pre” (monowidth block), “text_link” (for clickable text URLs), “text_mention” (for users without usernames), “custom_emoji” (for inline custom emoji stickers) - * @method int getOffset() Offset in UTF-16 code units to the start of the entity - * @method int getLength() Length of the entity in UTF-16 code units - * @method string getUrl() Optional. For "text_link" only, url that will be opened after user taps on the text - * @method User getUser() Optional. For "text_mention" only, the mentioned user - * @method string getLanguage() Optional. For "pre" only, the programming language of the entity text - * @method string getCustomEmojiId() Optional. For “custom_emoji” only, unique identifier of the custom emoji. Use getCustomEmojiStickers to get full information about the sticker - */ -class MessageEntity extends Entity -{ - /** - * {@inheritdoc} - */ - protected function subEntities(): array - { - return [ - 'user' => User::class, - ]; - } -} diff --git a/src/Entities/MessageOrigin/Factory.php b/src/Entities/MessageOrigin/Factory.php deleted file mode 100644 index d32205062..000000000 --- a/src/Entities/MessageOrigin/Factory.php +++ /dev/null @@ -1,25 +0,0 @@ - MessageOriginUser::class, - 'hidden_user' => MessageOriginHiddenUser::class, - 'chat' => MessageOriginChat::class, - 'channel' => MessageOriginChannel::class, - ]; - - if (!isset($type[$data['type'] ?? ''])) { - return new MessageOriginNotImplemented($data, $bot_username); - } - - $class = $type[$data['type']]; - return new $class($data, $bot_username); - } -} diff --git a/src/Entities/MessageOrigin/MessageOrigin.php b/src/Entities/MessageOrigin/MessageOrigin.php deleted file mode 100644 index a8a468c0f..000000000 --- a/src/Entities/MessageOrigin/MessageOrigin.php +++ /dev/null @@ -1,14 +0,0 @@ - Chat::class, - ]; - } -} diff --git a/src/Entities/MessageOrigin/MessageOriginChat.php b/src/Entities/MessageOrigin/MessageOriginChat.php deleted file mode 100644 index a0c2b398b..000000000 --- a/src/Entities/MessageOrigin/MessageOriginChat.php +++ /dev/null @@ -1,26 +0,0 @@ - Chat::class, - ]; - } -} diff --git a/src/Entities/MessageOrigin/MessageOriginHiddenUser.php b/src/Entities/MessageOrigin/MessageOriginHiddenUser.php deleted file mode 100644 index baff892f0..000000000 --- a/src/Entities/MessageOrigin/MessageOriginHiddenUser.php +++ /dev/null @@ -1,19 +0,0 @@ - User::class, - ]; - } -} diff --git a/src/Entities/MessageReactionCountUpdated.php b/src/Entities/MessageReactionCountUpdated.php deleted file mode 100644 index 21dc848bd..000000000 --- a/src/Entities/MessageReactionCountUpdated.php +++ /dev/null @@ -1,24 +0,0 @@ - Chat::class, - 'reactions' => [ReactionCount::class], - ]; - } -} diff --git a/src/Entities/MessageReactionUpdated.php b/src/Entities/MessageReactionUpdated.php deleted file mode 100644 index c5b602147..000000000 --- a/src/Entities/MessageReactionUpdated.php +++ /dev/null @@ -1,33 +0,0 @@ - Chat::class, - 'user' => User::class, - 'actor_chat' => Chat::class, - 'old_reaction' => [ReactionTypeFactory::class], - 'new_reaction' => [ReactionTypeFactory::class], - ]; - } -} diff --git a/src/Entities/Payments/Invoice.php b/src/Entities/Payments/Invoice.php deleted file mode 100644 index b30ac5d37..000000000 --- a/src/Entities/Payments/Invoice.php +++ /dev/null @@ -1,32 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities\Payments; - -use Longman\TelegramBot\Entities\Entity; - -/** - * Class Invoice - * - * This object contains basic information about an invoice. - * - * @link https://core.telegram.org/bots/api#invoice - * - * @method string getTitle() Product name - * @method string getDescription() Product description - * @method string getStartParameter() Unique bot deep-linking parameter that can be used to generate this invoice - * @method string getCurrency() Three-letter ISO 4217 currency code - * @method int getTotalAmount() Total price in the smallest units of the currency (integer, not float/double). - **/ -class Invoice extends Entity -{ - -} diff --git a/src/Entities/Payments/LabeledPrice.php b/src/Entities/Payments/LabeledPrice.php deleted file mode 100644 index a78d647d3..000000000 --- a/src/Entities/Payments/LabeledPrice.php +++ /dev/null @@ -1,29 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities\Payments; - -use Longman\TelegramBot\Entities\Entity; - -/** - * Class LabeledPrice - * - * This object represents a portion of the price for goods or services. - * - * @link https://core.telegram.org/bots/api#labeledprice - * - * @method string getLabel() Portion label - * @method int getAmount() Price of the product in the smallest units of the currency (integer, not float/double). - **/ -class LabeledPrice extends Entity -{ - -} diff --git a/src/Entities/Payments/OrderInfo.php b/src/Entities/Payments/OrderInfo.php deleted file mode 100644 index e539d4927..000000000 --- a/src/Entities/Payments/OrderInfo.php +++ /dev/null @@ -1,39 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities\Payments; - -use Longman\TelegramBot\Entities\Entity; - -/** - * Class OrderInfo - * - * This object represents information about an order. - * - * @link https://core.telegram.org/bots/api#orderinfo - * - * @method string getName() Optional. User name - * @method string getPhoneNumber() Optional. User's phone number - * @method string getEmail() Optional. User email - * @method ShippingAddress getShippingAddress() Optional. User shipping address - **/ -class OrderInfo extends Entity -{ - /** - * {@inheritdoc} - */ - protected function subEntities(): array - { - return [ - 'shipping_address' => ShippingAddress::class, - ]; - } -} diff --git a/src/Entities/Payments/PreCheckoutQuery.php b/src/Entities/Payments/PreCheckoutQuery.php deleted file mode 100644 index b58357101..000000000 --- a/src/Entities/Payments/PreCheckoutQuery.php +++ /dev/null @@ -1,62 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities\Payments; - -use Longman\TelegramBot\Entities\Entity; -use Longman\TelegramBot\Entities\ServerResponse; -use Longman\TelegramBot\Entities\User; -use Longman\TelegramBot\Request; - -/** - * Class PreCheckoutQuery - * - * This object contains information about an incoming pre-checkout query. - * - * @link https://core.telegram.org/bots/api#precheckoutquery - * - * @method string getId() Unique query identifier - * @method User getFrom() User who sent the query - * @method string getCurrency() Three-letter ISO 4217 currency code - * @method int getTotalAmount() Total price in the smallest units of the currency (integer, not float/double). - * @method string getInvoicePayload() Bot specified invoice payload - * @method string getShippingOptionId() Optional. Identifier of the shipping option chosen by the user - * @method OrderInfo getOrderInfo() Optional. Order info provided by the user - **/ -class PreCheckoutQuery extends Entity -{ - /** - * {@inheritdoc} - */ - protected function subEntities(): array - { - return [ - 'from' => User::class, - 'order_info' => OrderInfo::class, - ]; - } - - /** - * Answer this pre-checkout query. - * - * @param bool $ok - * @param array $data - * - * @return ServerResponse - */ - public function answer(bool $ok, array $data = []): ServerResponse - { - return Request::answerPreCheckoutQuery(array_merge([ - 'pre_checkout_query_id' => $this->getId(), - 'ok' => $ok, - ], $data)); - } -} diff --git a/src/Entities/Payments/ShippingAddress.php b/src/Entities/Payments/ShippingAddress.php deleted file mode 100644 index e48f64e7f..000000000 --- a/src/Entities/Payments/ShippingAddress.php +++ /dev/null @@ -1,33 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities\Payments; - -use Longman\TelegramBot\Entities\Entity; - -/** - * Class ShippingAddress - * - * This object represents a shipping address. - * - * @link https://core.telegram.org/bots/api#shippingaddress - * - * @method string getCountryCode() ISO 3166-1 alpha-2 country code - * @method string getState() State, if applicable - * @method string getCity() City - * @method string getStreetLine1() First line for the address - * @method string getStreetLine2() Second line for the address - * @method string getPostCode() Address post code - **/ -class ShippingAddress extends Entity -{ - -} diff --git a/src/Entities/Payments/ShippingOption.php b/src/Entities/Payments/ShippingOption.php deleted file mode 100644 index f49eaf4d9..000000000 --- a/src/Entities/Payments/ShippingOption.php +++ /dev/null @@ -1,38 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities\Payments; - -use Longman\TelegramBot\Entities\Entity; - -/** - * Class ShippingOption - * - * This object represents one shipping option. - * - * @link https://core.telegram.org/bots/api#shippingoption - * - * @method string getId() Shipping option identifier - * @method string getTitle() Option title - * @method LabeledPrice[] getPrices() List of price portions - **/ -class ShippingOption extends Entity -{ - /** - * {@inheritdoc} - */ - protected function subEntities(): array - { - return [ - 'prices' => [LabeledPrice::class], - ]; - } -} diff --git a/src/Entities/Payments/ShippingQuery.php b/src/Entities/Payments/ShippingQuery.php deleted file mode 100644 index f096f6613..000000000 --- a/src/Entities/Payments/ShippingQuery.php +++ /dev/null @@ -1,59 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities\Payments; - -use Longman\TelegramBot\Entities\Entity; -use Longman\TelegramBot\Entities\ServerResponse; -use Longman\TelegramBot\Entities\User; -use Longman\TelegramBot\Request; - -/** - * Class ShippingQuery - * - * This object contains information about an incoming shipping query. - * - * @link https://core.telegram.org/bots/api#shippingquery - * - * @method string getId() Unique query identifier - * @method User getFrom() User who sent the query - * @method string getInvoicePayload() Bot specified invoice payload - * @method ShippingAddress getShippingAddress() User specified shipping address - **/ -class ShippingQuery extends Entity -{ - /** - * {@inheritdoc} - */ - protected function subEntities(): array - { - return [ - 'from' => User::class, - 'shipping_address' => ShippingAddress::class, - ]; - } - - /** - * Answer this shipping query. - * - * @param bool $ok - * @param array $data - * - * @return ServerResponse - */ - public function answer(bool $ok, array $data = []): ServerResponse - { - return Request::answerShippingQuery(array_merge([ - 'shipping_query_id' => $this->getId(), - 'ok' => $ok, - ], $data)); - } -} diff --git a/src/Entities/Payments/SuccessfulPayment.php b/src/Entities/Payments/SuccessfulPayment.php deleted file mode 100644 index 8f2b5335e..000000000 --- a/src/Entities/Payments/SuccessfulPayment.php +++ /dev/null @@ -1,42 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities\Payments; - -use Longman\TelegramBot\Entities\Entity; - -/** - * Class SuccessfulPayment - * - * This object contains basic information about a successful payment. - * - * @link https://core.telegram.org/bots/api#successfulpayment - * - * @method string getCurrency() Three-letter ISO 4217 currency code - * @method int getTotalAmount() Total price in the smallest units of the currency (integer, not float/double). - * @method string getInvoicePayload() Bot specified invoice payload - * @method string getShippingOptionId() Optional. Identifier of the shipping option chosen by the user - * @method OrderInfo getOrderInfo() Optional. Order info provided by the user - * @method string getTelegramPaymentChargeId() Telegram payment identifier - * @method string getProviderPaymentChargeId() Provider payment identifier - **/ -class SuccessfulPayment extends Entity -{ - /** - * {@inheritdoc} - */ - protected function subEntities(): array - { - return [ - 'order_info' => OrderInfo::class, - ]; - } -} diff --git a/src/Entities/PhotoSize.php b/src/Entities/PhotoSize.php deleted file mode 100644 index 21718d28f..000000000 --- a/src/Entities/PhotoSize.php +++ /dev/null @@ -1,28 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -/** - * Class PhotoSize - * - * @link https://core.telegram.org/bots/api#photosize - * - * @method string getFileId() Identifier for this file, which can be used to download or reuse the file - * @method string getFileUniqueId() Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. - * @method int getWidth() Photo width - * @method int getHeight() Photo height - * @method int getFileSize() Optional. File size - */ -class PhotoSize extends Entity -{ - -} diff --git a/src/Entities/Poll.php b/src/Entities/Poll.php deleted file mode 100644 index 35313bc00..000000000 --- a/src/Entities/Poll.php +++ /dev/null @@ -1,47 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -/** - * Class Poll - * - * This entity contains information about a poll. - * - * @link https://core.telegram.org/bots/api#poll - * - * @method string getId() Unique poll identifier - * @method string getQuestion() Poll question, 1-255 characters - * @method PollOption[] getOptions() List of poll options - * @method int getTotalVoterCount() Total number of users that voted in the poll - * @method bool getIsClosed() True, if the poll is closed - * @method bool getIsAnonymous() True, if the poll is anonymous - * @method string getType() Poll type, currently can be “regular” or “quiz” - * @method bool getAllowsMultipleAnswers() True, if the poll allows multiple answers - * @method int getCorrectOptionId() Optional. 0-based identifier of the correct answer option. Available only for polls in the quiz mode, which are closed, or was sent (not forwarded) by the bot or to the private chat with the bot. - * @method string getExplanation() Optional. Text that is shown when a user chooses an incorrect answer or taps on the lamp icon in a quiz-style poll, 0-200 characters - * @method MessageEntity[] getExplanationEntities() Optional. Special entities like usernames, URLs, bot commands, etc. that appear in the explanation - * @method int getOpenPeriod() Optional. Amount of time in seconds the poll will be active after creation - * @method int getCloseDate() Optional. Point in time (Unix timestamp) when the poll will be automatically closed - */ -class Poll extends Entity -{ - /** - * {@inheritdoc} - */ - protected function subEntities(): array - { - return [ - 'options' => [PollOption::class], - 'explanation_entities' => [MessageEntity::class], - ]; - } -} diff --git a/src/Entities/PollAnswer.php b/src/Entities/PollAnswer.php deleted file mode 100644 index f55fe916f..000000000 --- a/src/Entities/PollAnswer.php +++ /dev/null @@ -1,38 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -/** - * Class PollAnswer - * - * This entity represents an answer of a user in a non-anonymous poll. - * - * @link https://core.telegram.org/bots/api#pollanswer - * - * @method string getPollId() Unique poll identifier - * @method Chat getVoterChat() Optional. The chat that changed the answer to the poll, if the voter is anonymous - * @method User getUser() Optional. The user, who changed the answer to the poll - * @method array getOptionIds() 0-based identifiers of answer options, chosen by the user. May be empty if the user retracted their vote. - */ -class PollAnswer extends Entity -{ - /** - * {@inheritdoc} - */ - protected function subEntities(): array - { - return [ - 'voter_chat' => Chat::class, - 'user' => User::class, - ]; - } -} diff --git a/src/Entities/PollOption.php b/src/Entities/PollOption.php deleted file mode 100644 index 8af5f978b..000000000 --- a/src/Entities/PollOption.php +++ /dev/null @@ -1,27 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -/** - * Class PollOption - * - * This entity contains information about one answer option in a poll. - * - * @link https://core.telegram.org/bots/api#polloption - * - * @method string getText() Option text, 1-100 characters - * @method int getVoterCount() Number of users that voted for this option - */ -class PollOption extends Entity -{ - -} diff --git a/src/Entities/ProximityAlertTriggered.php b/src/Entities/ProximityAlertTriggered.php deleted file mode 100644 index 6aa14e312..000000000 --- a/src/Entities/ProximityAlertTriggered.php +++ /dev/null @@ -1,37 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -/** - * Class ProximityAlertTriggered - * - * Represents the content of a service message, sent whenever a user in the chat triggers a proximity alert set by another user. - * - * @link https://core.telegram.org/bots/api#proximityalerttriggered - * - * @method User getTraveler() User that triggered the alert - * @method User getWatcher() User that set the alert - * @method int getDistance() The distance between the users - */ -class ProximityAlertTriggered extends Entity -{ - /** - * {@inheritdoc} - */ - protected function subEntities(): array - { - return [ - 'traveler' => User::class, - 'watcher' => User::class, - ]; - } -} diff --git a/src/Entities/ReactionCount.php b/src/Entities/ReactionCount.php deleted file mode 100644 index c99e67d3d..000000000 --- a/src/Entities/ReactionCount.php +++ /dev/null @@ -1,24 +0,0 @@ - ReactionTypeFactory::class, - ]; - } -} diff --git a/src/Entities/ReactionType/Factory.php b/src/Entities/ReactionType/Factory.php deleted file mode 100644 index f61cf368b..000000000 --- a/src/Entities/ReactionType/Factory.php +++ /dev/null @@ -1,23 +0,0 @@ - ReactionTypeEmoji::class, - 'custom_emoji' => ReactionTypeCustomEmoji::class, - ]; - - if (!isset($type[$data['type'] ?? ''])) { - return new ReactionTypeNotImplemented($data, $bot_username); - } - - $class = $type[$data['type']]; - return new $class($data, $bot_username); - } -} diff --git a/src/Entities/ReactionType/ReactionType.php b/src/Entities/ReactionType/ReactionType.php deleted file mode 100644 index 901f951de..000000000 --- a/src/Entities/ReactionType/ReactionType.php +++ /dev/null @@ -1,15 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -/** - * Describes reply parameters for the message that is being sent. - * - * @link https://core.telegram.org/bots/api#replyparameters - * - * @method int getMessageId() Identifier of the message that will be replied to in the current chat, or in the chat chat_id if it is specified - * @method int|string getChatId() Optional. If the message to be replied to is from a different chat, unique identifier for the chat or username of the channel (in the format @channelusername) - * @method bool getAllowSendingWithoutReply() Optional. Pass True if the message should be sent even if the specified message to be replied to is not found; can be used only for replies in the same chat and forum topic. - * @method string getQuote() 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, and custom_emoji entities. The message will fail to send if the quote isn't found in the original message. - * @method string getQuoteParseMode() Optional. Mode for parsing entities in the quote. See formatting options for more details. - * @method MessageEntity[] getQuoteEntities() Optional. A JSON-serialized list of special entities that appear in the quote. It can be specified instead of quote_parse_mode. - * @method int getQuotePosition() Optional. Position of the quote in the original message in UTF-16 code units - */ -class ReplyParameters extends Entity -{ - /** - * {@inheritdoc} - */ - protected function subEntities(): array - { - return [ - 'quote_entities' => [MessageEntity::class], - ]; - } -} diff --git a/src/Entities/ReplyToMessage.php b/src/Entities/ReplyToMessage.php deleted file mode 100644 index 5dc914b0a..000000000 --- a/src/Entities/ReplyToMessage.php +++ /dev/null @@ -1,35 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -/** - * Class ReplyToMessage - * - * @todo Is this even required?! - */ -class ReplyToMessage extends Message -{ - /** - * ReplyToMessage constructor. - * - * @param array $data - * @param string $bot_username - */ - public function __construct(array $data, string $bot_username = '') - { - //As explained in the documentation - //Reply to message can't contain other reply to message entities - unset($data['reply_to_message']); - - parent::__construct($data, $bot_username); - } -} diff --git a/src/Entities/SentWebAppMessage.php b/src/Entities/SentWebAppMessage.php deleted file mode 100644 index 27359b1a0..000000000 --- a/src/Entities/SentWebAppMessage.php +++ /dev/null @@ -1,20 +0,0 @@ - - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -use Longman\TelegramBot\Entities\ChatMember\ChatMember; -use Longman\TelegramBot\Entities\ChatMember\Factory as ChatMemberFactory; -use Longman\TelegramBot\Entities\Games\GameHighScore; -use Longman\TelegramBot\Entities\MenuButton\Factory as MenuButtonFactory; -use Longman\TelegramBot\Request; - -/** - * Class ServerResponse - * - * @link https://core.telegram.org/bots/api#making-requests - * - * @method bool getOk() If the request was successful - * @method mixed getResult() The result of the query - * @method int getErrorCode() Error code of the unsuccessful request - * @method string getDescription() Human-readable description of the result / unsuccessful request - * - * @todo method ResponseParameters getParameters() Field which can help to automatically handle the error - */ -class ServerResponse extends Entity -{ - /** - * ServerResponse constructor. - * - * @param array $data - * @param string $bot_username - */ - public function __construct(array $data, string $bot_username = '') - { - $is_ok = (bool) ($data['ok'] ?? false); - $result = $data['result'] ?? null; - - if ($is_ok && is_array($result)) { - if ($this->isAssoc($result)) { - $data['result'] = $this->createResultObject($result, $bot_username); - } else { - $data['result'] = $this->createResultObjects($result, $bot_username); - } - } - - parent::__construct($data, $bot_username); - } - - /** - * Check if array is associative - * - * @link https://stackoverflow.com/a/4254008 - * - * @param array $array - * - * @return bool - */ - protected function isAssoc(array $array): bool - { - return count(array_filter(array_keys($array), 'is_string')) > 0; - } - - /** - * If response is ok - * - * @return bool - */ - public function isOk(): bool - { - return (bool) $this->getOk(); - } - - /** - * Print error - * - * @see https://secure.php.net/manual/en/function.print-r.php - * - * @param bool $return - * - * @return bool|string - */ - public function printError($return = false) - { - $error = sprintf('Error N: %s, Description: %s', $this->getErrorCode(), $this->getDescription()); - - if ($return) { - return $error; - } - - echo $error; - - return true; - } - - /** - * Create and return the object of the received result - * - * @param array $result - * @param string $bot_username - * - * @return BotDescription|BotName|BotShortDescription|Chat|ChatAdministratorRights|ChatMember|File|Message|MenuButton|Poll|SentWebAppMessage|StickerSet|User|UserProfilePhotos|WebhookInfo - */ - private function createResultObject(array $result, string $bot_username): Entity - { - $result_object_types = [ - 'getWebhookInfo' => WebhookInfo::class, - 'getMe' => User::class, - 'getUserProfilePhotos' => UserProfilePhotos::class, - 'getFile' => File::class, - 'getChat' => Chat::class, - 'getChatMember' => ChatMemberFactory::class, - 'getMyName' => BotName::class, - 'getMyDescription' => BotDescription::class, - 'getMyShortDescription' => BotShortDescription::class, - 'getChatMenuButton' => MenuButtonFactory::class, - 'getMyDefaultAdministratorRights' => ChatAdministratorRights::class, - 'getStickerSet' => StickerSet::class, - 'stopPoll' => Poll::class, - 'answerWebAppQuery' => SentWebAppMessage::class, - ]; - - $action = Request::getCurrentAction(); - $object_class = $result_object_types[$action] ?? Message::class; - - return Factory::resolveEntityClass($object_class, $result, $bot_username); - } - - /** - * Create and return the objects array of the received result - * - * @param array $results - * @param string $bot_username - * - * @return BotCommand[]|ChatMember[]|GameHighScore[]|Message[]|Sticker[]|Update[] - */ - private function createResultObjects(array $results, string $bot_username): array - { - $result_object_types = [ - 'getUpdates' => Update::class, - 'getChatAdministrators' => ChatMemberFactory::class, - 'getForumTopicIconStickers' => Sticker::class, - 'getMyCommands' => BotCommand::class, - 'getCustomEmojiStickers' => Sticker::class, - 'getGameHighScores' => GameHighScore::class, - 'sendMediaGroup' => Message::class, - ]; - - $action = Request::getCurrentAction(); - $object_class = $result_object_types[$action] ?? Update::class; - - $objects = []; - - foreach ($results as $result) { - $objects[] = Factory::resolveEntityClass($object_class, $result, $bot_username); - } - - return $objects; - } -} diff --git a/src/Entities/Sticker.php b/src/Entities/Sticker.php deleted file mode 100644 index 3671c9523..000000000 --- a/src/Entities/Sticker.php +++ /dev/null @@ -1,48 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -/** - * Class Sticker - * - * @link https://core.telegram.org/bots/api#sticker - * - * @method string getFileId() Identifier for this file, which can be used to download or reuse the file - * @method string getFileUniqueId() Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. - * @method string getType() Type of the sticker, currently one of “regular”, “mask”, “custom_emoji”. The type of the sticker is independent from its format, which is determined by the fields is_animated and is_video. - * @method int getWidth() Sticker width - * @method int getHeight() Sticker height - * @method bool getIsAnimated() True, if the sticker is animated - * @method bool getIsVideo() True, if the sticker is a video sticker - * @method PhotoSize getThumbnail() Optional. Sticker thumbnail in .webp or .jpg format - * @method string getEmoji() Optional. Emoji associated with the sticker - * @method string getSetName() Optional. Name of the sticker set to which the sticker belongs - * @method File getPremiumAnimation() Optional. Premium animation for the sticker, if the sticker is premium - * @method MaskPosition getMaskPosition() Optional. For mask stickers, the position where the mask should be placed - * @method string getCustomEmojiId() Optional. For custom emoji stickers, unique identifier of the custom emoji - * @method bool getNeedsRepainting() Optional. True, if the sticker must be repainted to a text color in messages, the color of the Telegram Premium badge in emoji status, white color on chat photos, or another appropriate color in other places - * @method int getFileSize() Optional. File size - */ -class Sticker extends Entity -{ - /** - * {@inheritdoc} - */ - protected function subEntities(): array - { - return [ - 'thumbnail' => PhotoSize::class, - 'premium_animation' => File::class, - 'mask_position' => MaskPosition::class, - ]; - } -} diff --git a/src/Entities/StickerSet.php b/src/Entities/StickerSet.php deleted file mode 100644 index 8bb35ec4d..000000000 --- a/src/Entities/StickerSet.php +++ /dev/null @@ -1,39 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -/** - * Class StickerSet - * - * @link https://core.telegram.org/bots/api#stickerset - * - * @method string getName() Sticker set name - * @method string getTitle() Sticker set title - * @method string getStickerType() Type of stickers in the set, currently one of “regular”, “mask”, “custom_emoji” - * @method bool getIsAnimated() True, if the sticker set contains animated stickers - * @method bool getIsVideo() True, if the sticker set contains video stickers - * @method Sticker[] getStickers() List of all set stickers - * @method PhotoSize getThumbnail() Optional. Sticker set thumbnail in the .WEBP or .TGS format - */ -class StickerSet extends Entity -{ - /** - * {@inheritdoc} - */ - protected function subEntities(): array - { - return [ - 'stickers' => [Sticker::class], - 'thumbnail' => PhotoSize::class, - ]; - } -} diff --git a/src/Entities/Story.php b/src/Entities/Story.php deleted file mode 100644 index 5647db224..000000000 --- a/src/Entities/Story.php +++ /dev/null @@ -1,22 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -/** - * Class Story - * - * @link https://core.telegram.org/bots/api#story - */ -class Story extends Entity -{ - -} diff --git a/src/Entities/SwitchInlineQueryChosenChat.php b/src/Entities/SwitchInlineQueryChosenChat.php deleted file mode 100644 index 49218f6df..000000000 --- a/src/Entities/SwitchInlineQueryChosenChat.php +++ /dev/null @@ -1,21 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities\TelegramPassport; - -use Longman\TelegramBot\Entities\Entity; - -/** - * Class EncryptedCredentials - * - * Contains data required for decrypting and authenticating EncryptedCredentials. See the Telegram Passport Documentation for a complete description of the data decryption and authentication processes. - * - * @link https://core.telegram.org/bots/api#encryptedcredentials - * - * @method string getData() Base64-encoded encrypted JSON-serialized data with unique user's payload, data hashes and secrets required for EncryptedPassportElement decryption and authentication - * @method string getHash() Base64-encoded data hash for data authentication - * @method string getSecret() Base64-encoded secret, encrypted with the bot's public RSA key, required for data decryption - **/ -class EncryptedCredentials extends Entity -{ - -} diff --git a/src/Entities/TelegramPassport/EncryptedPassportElement.php b/src/Entities/TelegramPassport/EncryptedPassportElement.php deleted file mode 100644 index 42a169cfd..000000000 --- a/src/Entities/TelegramPassport/EncryptedPassportElement.php +++ /dev/null @@ -1,49 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities\TelegramPassport; - -use Longman\TelegramBot\Entities\Entity; - -/** - * Class EncryptedPassportElement - * - * Contains information about documents or other Telegram Passport elements shared with the bot by the user. - * - * @link https://core.telegram.org/bots/api#encryptedpassportelement - * - * @method string getType() Element type. One of “personal_details”, “passport”, “driver_license”, “identity_card”, “internal_passport”, “address”, “utility_bill”, “bank_statement”, “rental_agreement”, “passport_registration”, “temporary_registration”, “phone_number”, “email”. - * @method string getData() Optional. Base64-encoded encrypted Telegram Passport element data provided by the user, available for “personal_details”, “passport”, “driver_license”, “identity_card”, “identity_passport” and “address” types. Can be decrypted and verified using the accompanying EncryptedCredentials. - * @method string getPhoneNumber() Optional. User's verified phone number, available only for “phone_number” type - * @method string getEmail() Optional. User's verified email address, available only for “email” type - * @method PassportFile[] getFiles() Optional. Array of encrypted files with documents provided by the user, available for “utility_bill”, “bank_statement”, “rental_agreement”, “passport_registration” and “temporary_registration” types. Files can be decrypted and verified using the accompanying EncryptedCredentials. - * @method PassportFile getFrontSide() Optional. Encrypted file with the front side of the document, provided by the user. Available for “passport”, “driver_license”, “identity_card” and “internal_passport”. The file can be decrypted and verified using the accompanying EncryptedCredentials. - * @method PassportFile getReverseSide() Optional. Encrypted file with the reverse side of the document, provided by the user. Available for “driver_license” and “identity_card”. The file can be decrypted and verified using the accompanying EncryptedCredentials. - * @method PassportFile getSelfie() Optional. Encrypted file with the selfie of the user holding a document, provided by the user; available for “passport”, “driver_license”, “identity_card” and “internal_passport”. The file can be decrypted and verified using the accompanying EncryptedCredentials. - * @method PassportFile[] getTranslation() Optional. Array of encrypted files with translated versions of documents provided by the user. Available if requested for “passport”, “driver_license”, “identity_card”, “internal_passport”, “utility_bill”, “bank_statement”, “rental_agreement”, “passport_registration” and “temporary_registration” types. Files can be decrypted and verified using the accompanying EncryptedCredentials. - * @method string getHash() Base64-encoded element hash for using in PassportElementErrorUnspecified - **/ -class EncryptedPassportElement extends Entity -{ - /** - * {@inheritdoc} - */ - protected function subEntities(): array - { - return [ - 'files' => [PassportFile::class], - 'front_side' => PassportFile::class, - 'reverse_side' => PassportFile::class, - 'selfie' => PassportFile::class, - 'translation' => [PassportFile::class], - ]; - } -} diff --git a/src/Entities/TelegramPassport/PassportData.php b/src/Entities/TelegramPassport/PassportData.php deleted file mode 100644 index a845e1ed7..000000000 --- a/src/Entities/TelegramPassport/PassportData.php +++ /dev/null @@ -1,38 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities\TelegramPassport; - -use Longman\TelegramBot\Entities\Entity; - -/** - * Class PassportData - * - * Contains information about Telegram Passport data shared with the bot by the user. - * - * @link https://core.telegram.org/bots/api#passportdata - * - * @method EncryptedPassportElement[] getData() Array with information about documents and other Telegram Passport elements that was shared with the bot - * @method EncryptedCredentials getCredentials() Encrypted credentials required to decrypt the data - **/ -class PassportData extends Entity -{ - /** - * {@inheritdoc} - */ - protected function subEntities(): array - { - return [ - 'data' => [EncryptedPassportElement::class], - 'credentials' => EncryptedCredentials::class, - ]; - } -} diff --git a/src/Entities/TelegramPassport/PassportElementError/PassportElementError.php b/src/Entities/TelegramPassport/PassportElementError/PassportElementError.php deleted file mode 100644 index e69e92de3..000000000 --- a/src/Entities/TelegramPassport/PassportElementError/PassportElementError.php +++ /dev/null @@ -1,17 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Entities\TelegramPassport\PassportElementError; - -interface PassportElementError -{ - -} diff --git a/src/Entities/TelegramPassport/PassportElementError/PassportElementErrorDataField.php b/src/Entities/TelegramPassport/PassportElementError/PassportElementErrorDataField.php deleted file mode 100644 index d39881482..000000000 --- a/src/Entities/TelegramPassport/PassportElementError/PassportElementErrorDataField.php +++ /dev/null @@ -1,41 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Entities\TelegramPassport\PassportElementError; - -use Longman\TelegramBot\Entities\Entity; - -/** - * Class PassportElementErrorDataField - * - * Represents an issue in one of the data fields that was provided by the user. The error is considered resolved when the field's value changes. - * - * @link https://core.telegram.org/bots/api#passportelementerrordatafield - * - * @method string getSource() Error source, must be data - * @method string getType() The section of the user's Telegram Passport which has the error, one of “personal_details”, “passport”, “driver_license”, “identity_card”, “internal_passport”, “address” - * @method string getFieldName() Name of the data field which has the error - * @method string getDataHash() Base64-encoded data hash - * @method string getMessage() Error message - */ -class PassportElementErrorDataField extends Entity implements PassportElementError -{ - /** - * PassportElementErrorDataField constructor - * - * @param array $data - */ - public function __construct(array $data = []) - { - $data['source'] = 'data'; - parent::__construct($data); - } -} diff --git a/src/Entities/TelegramPassport/PassportElementError/PassportElementErrorFile.php b/src/Entities/TelegramPassport/PassportElementError/PassportElementErrorFile.php deleted file mode 100644 index eb980dd62..000000000 --- a/src/Entities/TelegramPassport/PassportElementError/PassportElementErrorFile.php +++ /dev/null @@ -1,40 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Entities\TelegramPassport\PassportElementError; - -use Longman\TelegramBot\Entities\Entity; - -/** - * Class PassportElementErrorFile - * - * Represents an issue with a document scan. The error is considered resolved when the file with the document scan changes. - * - * @link https://core.telegram.org/bots/api#passportelementerrorfile - * - * @method string getSource() Error source, must be file - * @method string getType() The section of the user's Telegram Passport which has the issue, one of “utility_bill”, “bank_statement”, “rental_agreement”, “passport_registration”, “temporary_registration” - * @method string getFileHash() Base64-encoded file hash - * @method string getMessage() Error message - */ -class PassportElementErrorFile extends Entity implements PassportElementError -{ - /** - * PassportElementErrorFile constructor - * - * @param array $data - */ - public function __construct(array $data = []) - { - $data['source'] = 'file'; - parent::__construct($data); - } -} diff --git a/src/Entities/TelegramPassport/PassportElementError/PassportElementErrorFiles.php b/src/Entities/TelegramPassport/PassportElementError/PassportElementErrorFiles.php deleted file mode 100644 index 10ecc53ce..000000000 --- a/src/Entities/TelegramPassport/PassportElementError/PassportElementErrorFiles.php +++ /dev/null @@ -1,40 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Entities\TelegramPassport\PassportElementError; - -use Longman\TelegramBot\Entities\Entity; - -/** - * Class PassportElementErrorFiles - * - * Represents an issue with a list of scans. The error is considered resolved when the list of files containing the scans changes. - * - * @link https://core.telegram.org/bots/api#passportelementerrorfiles - * - * @method string getSource() Error source, must be files - * @method string getType() The section of the user's Telegram Passport which has the issue, one of “utility_bill”, “bank_statement”, “rental_agreement”, “passport_registration”, “temporary_registration” - * @method string[] getFileHashes() List of base64-encoded file hashes - * @method string getMessage() Error message - */ -class PassportElementErrorFiles extends Entity implements PassportElementError -{ - /** - * PassportElementErrorFiles constructor - * - * @param array $data - */ - public function __construct(array $data = []) - { - $data['source'] = 'files'; - parent::__construct($data); - } -} diff --git a/src/Entities/TelegramPassport/PassportElementError/PassportElementErrorFrontSide.php b/src/Entities/TelegramPassport/PassportElementError/PassportElementErrorFrontSide.php deleted file mode 100644 index a05ea4f25..000000000 --- a/src/Entities/TelegramPassport/PassportElementError/PassportElementErrorFrontSide.php +++ /dev/null @@ -1,40 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Entities\TelegramPassport\PassportElementError; - -use Longman\TelegramBot\Entities\Entity; - -/** - * Class PassportElementErrorFrontSide - * - * Represents an issue with the front side of a document. The error is considered resolved when the file with the front side of the document changes. - * - * @link https://core.telegram.org/bots/api#passportelementerrorfrontside - * - * @method string getSource() Error source, must be front_side - * @method string getType() The section of the user's Telegram Passport which has the issue, one of “passport”, “driver_license”, “identity_card”, “internal_passport” - * @method string getFileHash() Base64-encoded hash of the file with the front side of the document - * @method string getMessage() Error message - */ -class PassportElementErrorFrontSide extends Entity implements PassportElementError -{ - /** - * PassportElementErrorFrontSide constructor - * - * @param array $data - */ - public function __construct(array $data = []) - { - $data['source'] = 'front_side'; - parent::__construct($data); - } -} diff --git a/src/Entities/TelegramPassport/PassportElementError/PassportElementErrorReverseSide.php b/src/Entities/TelegramPassport/PassportElementError/PassportElementErrorReverseSide.php deleted file mode 100644 index e57d80ace..000000000 --- a/src/Entities/TelegramPassport/PassportElementError/PassportElementErrorReverseSide.php +++ /dev/null @@ -1,40 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Entities\TelegramPassport\PassportElementError; - -use Longman\TelegramBot\Entities\Entity; - -/** - * Class PassportElementErrorReverseSide - * - * Represents an issue with the reverse side of a document. The error is considered resolved when the file with reverse side of the document changes. - * - * @link https://core.telegram.org/bots/api#passportelementerrorreverseside - * - * @method string getSource() Error source, must be reverse_side - * @method string getType() The section of the user's Telegram Passport which has the issue, one of “driver_license”, “identity_card” - * @method string getFileHash() Base64-encoded hash of the file with the reverse side of the document - * @method string getMessage() Error message - */ -class PassportElementErrorReverseSide extends Entity implements PassportElementError -{ - /** - * PassportElementErrorReverseSide constructor - * - * @param array $data - */ - public function __construct(array $data = []) - { - $data['source'] = 'reverse_side'; - parent::__construct($data); - } -} diff --git a/src/Entities/TelegramPassport/PassportElementError/PassportElementErrorSelfie.php b/src/Entities/TelegramPassport/PassportElementError/PassportElementErrorSelfie.php deleted file mode 100644 index a7c9897bd..000000000 --- a/src/Entities/TelegramPassport/PassportElementError/PassportElementErrorSelfie.php +++ /dev/null @@ -1,40 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Entities\TelegramPassport\PassportElementError; - -use Longman\TelegramBot\Entities\Entity; - -/** - * Class PassportElementErrorSelfie - * - * Represents an issue with the selfie with a document. The error is considered resolved when the file with the selfie changes. - * - * @link https://core.telegram.org/bots/api#passportelementerrorselfie - * - * @method string getSource() Error source, must be selfie - * @method string getType() The section of the user's Telegram Passport which has the issue, one of “passport”, “driver_license”, “identity_card”, “internal_passport” - * @method string getFileHash() Base64-encoded hash of the file with the selfie - * @method string getMessage() Error message - */ -class PassportElementErrorSelfie extends Entity implements PassportElementError -{ - /** - * PassportElementErrorSelfie constructor - * - * @param array $data - */ - public function __construct(array $data = []) - { - $data['source'] = 'selfie'; - parent::__construct($data); - } -} diff --git a/src/Entities/TelegramPassport/PassportElementError/PassportElementErrorTranslationFile.php b/src/Entities/TelegramPassport/PassportElementError/PassportElementErrorTranslationFile.php deleted file mode 100644 index 267c342ce..000000000 --- a/src/Entities/TelegramPassport/PassportElementError/PassportElementErrorTranslationFile.php +++ /dev/null @@ -1,40 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Entities\TelegramPassport\PassportElementError; - -use Longman\TelegramBot\Entities\Entity; - -/** - * Class PassportElementErrorTranslationFile - * - * Represents an issue with one of the files that constitute the translation of a document. The error is considered resolved when the file changes. - * - * @link https://core.telegram.org/bots/api#passportelementerrortranslationfile - * - * @method string getSource() Error source, must be translation_file - * @method string getType() Type of element of the user's Telegram Passport which has the issue, one of “passport”, “driver_license”, “identity_card”, “internal_passport”, “utility_bill”, “bank_statement”, “rental_agreement”, “passport_registration”, “temporary_registration” - * @method string getFileHash() Base64-encoded translation_file hash - * @method string getMessage() Error message - */ -class PassportElementErrorTranslationFile extends Entity implements PassportElementError -{ - /** - * PassportElementErrorTranslationFile constructor - * - * @param array $data - */ - public function __construct(array $data = []) - { - $data['source'] = 'translation_file'; - parent::__construct($data); - } -} diff --git a/src/Entities/TelegramPassport/PassportElementError/PassportElementErrorTranslationFiles.php b/src/Entities/TelegramPassport/PassportElementError/PassportElementErrorTranslationFiles.php deleted file mode 100644 index dd7b4de97..000000000 --- a/src/Entities/TelegramPassport/PassportElementError/PassportElementErrorTranslationFiles.php +++ /dev/null @@ -1,40 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Entities\TelegramPassport\PassportElementError; - -use Longman\TelegramBot\Entities\Entity; - -/** - * Class PassportElementErrorTranslationFiles - * - * Represents an issue with a list of scans. The error is considered resolved when the list of files containing the scans changes. - * - * @link https://core.telegram.org/bots/api#passportelementerrortranslationfiles - * - * @method string getSource() Error source, must be translation_files - * @method string getType() Type of element of the user's Telegram Passport which has the issue, one of “passport”, “driver_license”, “identity_card”, “internal_passport”, “utility_bill”, “bank_statement”, “rental_agreement”, “passport_registration”, “temporary_registration” - * @method string[] getFileHashes() List of base64-encoded file hashes - * @method string getMessage() Error message - */ -class PassportElementErrorTranslationFiles extends Entity implements PassportElementError -{ - /** - * PassportElementErrorTranslationFiles constructor - * - * @param array $data - */ - public function __construct(array $data = []) - { - $data['source'] = 'translation_files'; - parent::__construct($data); - } -} diff --git a/src/Entities/TelegramPassport/PassportElementError/PassportElementErrorUnspecified.php b/src/Entities/TelegramPassport/PassportElementError/PassportElementErrorUnspecified.php deleted file mode 100644 index c804dd2dd..000000000 --- a/src/Entities/TelegramPassport/PassportElementError/PassportElementErrorUnspecified.php +++ /dev/null @@ -1,40 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Entities\TelegramPassport\PassportElementError; - -use Longman\TelegramBot\Entities\Entity; - -/** - * Class PassportElementErrorUnspecified - * - * Represents an issue in an unspecified place. The error is considered resolved when new data is added. - * - * @link https://core.telegram.org/bots/api#passportelementerrorunspecified - * - * @method string getSource() Error source, must be unspecified - * @method string getType() Type of element of the user's Telegram Passport which has the issue - * @method string getElementHash() Base64-encoded element hash - * @method string getMessage() Error message - */ -class PassportElementErrorUnspecified extends Entity implements PassportElementError -{ - /** - * PassportElementErrorUnspecified constructor - * - * @param array $data - */ - public function __construct(array $data = []) - { - $data['source'] = 'unspecified'; - parent::__construct($data); - } -} diff --git a/src/Entities/TelegramPassport/PassportFile.php b/src/Entities/TelegramPassport/PassportFile.php deleted file mode 100644 index 13f6fa230..000000000 --- a/src/Entities/TelegramPassport/PassportFile.php +++ /dev/null @@ -1,31 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities\TelegramPassport; - -use Longman\TelegramBot\Entities\Entity; - -/** - * Class PassportFile - * - * This object represents a file uploaded to Telegram Passport. Currently all Telegram Passport files are in JPEG format when decrypted and don't exceed 10MB. - * - * @link https://core.telegram.org/bots/api#passportfile - * - * @method string getFileId() Identifier for this file, which can be used to download or reuse the file - * @method string getFileUniqueId() Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. - * @method int getFileSize() File size - * @method int getFileDate() Unix time when the file was uploaded - **/ -class PassportFile extends Entity -{ - -} diff --git a/src/Entities/TextQuote.php b/src/Entities/TextQuote.php deleted file mode 100644 index 9e237d795..000000000 --- a/src/Entities/TextQuote.php +++ /dev/null @@ -1,35 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -/** - * This object contains information about the quoted part of a message that is replied to by the given message. - * - * @link https://core.telegram.org/bots/api#textquote - * - * @method string getText() Text of the quoted part of a message that is replied to by the given message - * @method MessageEntity[] getEntities() Optional. Special entities that appear in the quote. Currently, only bold, italic, underline, strikethrough, spoiler, and custom_emoji entities are kept in quotes. - * @method int getPosition() Approximate quote position in the original message in UTF-16 code units as specified by the sender - * @method bool getIsManual() Optional. True, if the quote was chosen manually by the message sender. Otherwise, the quote was added automatically by the server. - */ -class TextQuote extends Entity -{ - /** - * {@inheritdoc} - */ - protected function subEntities(): array - { - return [ - 'entities' => [MessageEntity::class], - ]; - } -} diff --git a/src/Entities/Topics/ForumTopicClosed.php b/src/Entities/Topics/ForumTopicClosed.php deleted file mode 100644 index 7adf25b66..000000000 --- a/src/Entities/Topics/ForumTopicClosed.php +++ /dev/null @@ -1,15 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -use Longman\TelegramBot\Entities\Payments\PreCheckoutQuery; -use Longman\TelegramBot\Entities\Payments\ShippingQuery; - -/** - * Class Update - * - * @link https://core.telegram.org/bots/api#update - * - * @method int getUpdateId() The update's unique identifier. Update identifiers start from a certain positive number and increase sequentially. This ID becomes especially handy if you’re using Webhooks, since it allows you to ignore repeated updates or to restore the correct update sequence, should they get out of order. - * @method Message getMessage() Optional. New incoming message of any kind — text, photo, sticker, etc. - * @method Message getEditedMessage() Optional. New version of a message that is known to the bot and was edited - * @method Message getChannelPost() Optional. New post in the channel, can be any kind — text, photo, sticker, etc. - * @method Message getEditedChannelPost() Optional. New version of a post in the channel that is known to the bot and was edited - * @method MessageReactionUpdated getMessageReaction() Optional. A reaction to a message was changed by a user. The bot must be an administrator in the chat and must explicitly specify "message_reaction" in the list of allowed_updates to receive these updates. The update isn't received for reactions set by bots. - * @method MessageReactionCountUpdated getMessageReactionCount() Optional. Reactions to a message with anonymous reactions were changed. The bot must be an administrator in the chat and must explicitly specify "message_reaction_count" in the list of allowed_updates to receive these updates. The updates are grouped and can be sent with delay up to a few minutes. - * @method InlineQuery getInlineQuery() Optional. New incoming inline query - * @method ChosenInlineResult getChosenInlineResult() Optional. The result of an inline query that was chosen by a user and sent to their chat partner. - * @method CallbackQuery getCallbackQuery() Optional. New incoming callback query - * @method ShippingQuery getShippingQuery() Optional. New incoming shipping query. Only for invoices with flexible price - * @method PreCheckoutQuery getPreCheckoutQuery() Optional. New incoming pre-checkout query. Contains full information about checkout - * @method Poll getPoll() Optional. New poll state. Bots receive only updates about polls, which are sent or stopped by the bot - * @method PollAnswer getPollAnswer() Optional. A user changed their answer in a non-anonymous poll. Bots receive new votes only in polls that were sent by the bot itself. - * @method ChatMemberUpdated getMyChatMember() Optional. The bot's chat member status was updated in a chat. For private chats, this update is received only when the bot is blocked or unblocked by the user. - * @method ChatMemberUpdated getChatMember() Optional. A chat member's status was updated in a chat. The bot must be an administrator in the chat and must explicitly specify “chat_member” in the list of allowed_updates to receive these updates. - * @method ChatJoinRequest getChatJoinRequest() Optional. A request to join the chat has been sent. The bot must have the can_invite_users administrator right in the chat to receive these updates. - * @method ChatBoostUpdated getChatBoost() Optional. A chat boost was added or changed. The bot must be an administrator in the chat to receive these updates. - * @method ChatBoostRemoved getRemovedChatBoost() Optional. A boost was removed from a chat. The bot must be an administrator in the chat to receive these updates. - */ -class Update extends Entity -{ - public const TYPE_MESSAGE = 'message'; - public const TYPE_EDITED_MESSAGE = 'edited_message'; - public const TYPE_CHANNEL_POST = 'channel_post'; - public const TYPE_EDITED_CHANNEL_POST = 'edited_channel_post'; - public const TYPE_MESSAGE_REACTION = 'message_reaction'; - public const TYPE_MESSAGE_REACTION_COUNT = 'message_reaction_count'; - public const TYPE_INLINE_QUERY = 'inline_query'; - public const TYPE_CHOSEN_INLINE_RESULT = 'chosen_inline_result'; - public const TYPE_CALLBACK_QUERY = 'callback_query'; - public const TYPE_SHIPPING_QUERY = 'shipping_query'; - public const TYPE_PRE_CHECKOUT_QUERY = 'pre_checkout_query'; - public const TYPE_POLL = 'poll'; - public const TYPE_POLL_ANSWER = 'poll_answer'; - public const TYPE_MY_CHAT_MEMBER = 'my_chat_member'; - public const TYPE_CHAT_MEMBER = 'chat_member'; - public const TYPE_CHAT_JOIN_REQUEST = 'chat_join_request'; - public const TYPE_CHAT_BOOST = 'chat_boost'; - public const TYPE_REMOVED_CHAT_BOOST = 'removed_chat_boost'; - - /** - * {@inheritdoc} - */ - protected function subEntities(): array - { - return [ - self::TYPE_MESSAGE => Message::class, - self::TYPE_EDITED_MESSAGE => EditedMessage::class, - self::TYPE_CHANNEL_POST => ChannelPost::class, - self::TYPE_EDITED_CHANNEL_POST => EditedChannelPost::class, - self::TYPE_MESSAGE_REACTION => MessageReactionUpdated::class, - self::TYPE_MESSAGE_REACTION_COUNT => MessageReactionCountUpdated::class, - self::TYPE_INLINE_QUERY => InlineQuery::class, - self::TYPE_CHOSEN_INLINE_RESULT => ChosenInlineResult::class, - self::TYPE_CALLBACK_QUERY => CallbackQuery::class, - self::TYPE_SHIPPING_QUERY => ShippingQuery::class, - self::TYPE_PRE_CHECKOUT_QUERY => PreCheckoutQuery::class, - self::TYPE_POLL => Poll::class, - self::TYPE_POLL_ANSWER => PollAnswer::class, - self::TYPE_MY_CHAT_MEMBER => ChatMemberUpdated::class, - self::TYPE_CHAT_MEMBER => ChatMemberUpdated::class, - self::TYPE_CHAT_JOIN_REQUEST => ChatJoinRequest::class, - self::TYPE_CHAT_BOOST => ChatBoostUpdated::class, - self::TYPE_REMOVED_CHAT_BOOST => ChatBoostRemoved::class, - ]; - } - - /** - * Get the list of all available update types - * - * @return string[] - */ - public static function getUpdateTypes(): array - { - return array_keys((new self([]))->subEntities()); - } - - /** - * Get the update type based on the set properties - * - * @return string|null - */ - public function getUpdateType(): ?string - { - foreach (self::getUpdateTypes() as $type) { - if ($this->getProperty($type)) { - return $type; - } - } - - return null; - } - - /** - * Get update content - * - * @return Message|EditedMessage|ChannelPost|EditedChannelPost|MessageReactionUpdated|MessageReactionCountUpdated|InlineQuery|ChosenInlineResult|CallbackQuery|ShippingQuery|PreCheckoutQuery|Poll|PollAnswer|ChatMemberUpdated|ChatJoinRequest|ChatBoostUpdated|ChatBoostRemoved - */ - public function getUpdateContent() - { - if ($update_type = $this->getUpdateType()) { - // Instead of just getting the property as an array, - // use the __call method to get the correct Entity object. - $method = 'get' . str_replace('_', '', ucwords($update_type, '_')); - return $this->$method(); - } - - return null; - } -} diff --git a/src/Entities/User.php b/src/Entities/User.php deleted file mode 100644 index bc412b01f..000000000 --- a/src/Entities/User.php +++ /dev/null @@ -1,34 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -/** - * Class User - * - * @link https://core.telegram.org/bots/api#user - * - * @method int getId() Unique identifier for this user or bot - * @method bool getIsBot() True, if this user is a bot - * @method string getFirstName() User's or bot’s first name - * @method string getLastName() Optional. User's or bot’s last name - * @method string getUsername() Optional. User's or bot’s username - * @method string getLanguageCode() Optional. IETF language tag of the user's language - * @method bool getIsPremium() Optional. True, if this user is a Telegram Premium user - * @method bool getAddedToAttachmentMenu() Optional. True, if this user added the bot to the attachment menu - * @method bool getCanJoinGroups() Optional. True, if the bot can be invited to groups. Returned only in getMe. - * @method bool getCanReadAllGroupMessages() Optional. True, if privacy mode is disabled for the bot. Returned only in getMe. - * @method bool getSupportsInlineQueries() Optional. True, if the bot supports inline queries. Returned only in getMe. - */ -class User extends Entity -{ - -} diff --git a/src/Entities/UserChatBoosts.php b/src/Entities/UserChatBoosts.php deleted file mode 100644 index de45ddb14..000000000 --- a/src/Entities/UserChatBoosts.php +++ /dev/null @@ -1,32 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -/** - * This object represents a list of boosts added to a chat by a user. - * - * @link https://core.telegram.org/bots/api#userchatboosts - * - * @method ChatBoost[] getBoosts() The list of boosts added to the chat by the user - */ -class UserChatBoosts extends Entity -{ - /** - * {@inheritdoc} - */ - protected function subEntities(): array - { - return [ - 'boosts' => [ChatBoost::class], - ]; - } -} diff --git a/src/Entities/UserProfilePhotos.php b/src/Entities/UserProfilePhotos.php deleted file mode 100644 index e39dfc8eb..000000000 --- a/src/Entities/UserProfilePhotos.php +++ /dev/null @@ -1,54 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -/** - * Class UserProfilePhotos - * - * @link https://core.telegram.org/bots/api#userprofilephotos - * - * @method int getTotalCount() Total number of profile pictures the target user has - */ -class UserProfilePhotos extends Entity -{ - /** - * {@inheritdoc} - */ - protected function subEntities(): array - { - return [ - 'photos' => PhotoSize::class, - ]; - } - - /** - * Requested profile pictures (in up to 4 sizes each) - * - * This method overrides the default getPhotos method and returns a nice array - * - * @return PhotoSize[][] - */ - public function getPhotos(): array - { - $all_photos = []; - - if ($these_photos = $this->getProperty('photos')) { - foreach ($these_photos as $photos) { - $all_photos[] = array_map(function ($photo) { - return new PhotoSize($photo); - }, $photos); - } - } - - return $all_photos; - } -} diff --git a/src/Entities/UsersShared.php b/src/Entities/UsersShared.php deleted file mode 100644 index ab32c2c90..000000000 --- a/src/Entities/UsersShared.php +++ /dev/null @@ -1,16 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -/** - * Class Venue - * - * @link https://core.telegram.org/bots/api#venue - * - * @method Location getLocation() Venue location - * @method string getTitle() Name of the venue - * @method string getAddress() Address of the venue - * @method string getFoursquareId() Optional. Foursquare identifier of the venue - * @method string getFoursquareType() Optional. Foursquare type of the venue. (For example, “arts_entertainment/default”, “arts_entertainment/aquarium” or “food/icecream”.) - * @method string getGooglePlaceId() Optional. Google Places identifier of the venue - * @method string getGooglePlaceType() Optional. Google Places type of the venue - */ -class Venue extends Entity -{ - /** - * {@inheritdoc} - */ - protected function subEntities(): array - { - return [ - 'location' => Location::class, - ]; - } -} diff --git a/src/Entities/Video.php b/src/Entities/Video.php deleted file mode 100644 index 972f04da8..000000000 --- a/src/Entities/Video.php +++ /dev/null @@ -1,40 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -/** - * Class Video - * - * @link https://core.telegram.org/bots/api#video - * - * @method string getFileId() Identifier for this file, which can be used to download or reuse the file - * @method string getFileUniqueId() Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. - * @method int getWidth() Video width as defined by sender - * @method int getHeight() Video height as defined by sender - * @method int getDuration() Duration of the video in seconds as defined by sender - * @method PhotoSize getThumbnail() Optional. Video thumbnail - * @method string getFileName() Optional. Original filename as defined by sender - * @method string getMimeType() Optional. Mime type of a file as defined by sender - * @method int getFileSize() Optional. File size - */ -class Video extends Entity -{ - /** - * {@inheritdoc} - */ - protected function subEntities(): array - { - return [ - 'thumbnail' => PhotoSize::class, - ]; - } -} diff --git a/src/Entities/VideoChatEnded.php b/src/Entities/VideoChatEnded.php deleted file mode 100644 index 5f3a52773..000000000 --- a/src/Entities/VideoChatEnded.php +++ /dev/null @@ -1,24 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -/** - * This object represents a service message about a video chat ended in the chat. - * - * @link https://core.telegram.org/bots/api#videochatended - * - * @method int getDuration() Video chat duration in seconds - */ -class VideoChatEnded extends Entity -{ - -} diff --git a/src/Entities/VideoChatParticipantsInvited.php b/src/Entities/VideoChatParticipantsInvited.php deleted file mode 100644 index 36a67b58e..000000000 --- a/src/Entities/VideoChatParticipantsInvited.php +++ /dev/null @@ -1,32 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -/** - * This object represents a service message about new members invited to a video chat. - * - * @link https://core.telegram.org/bots/api#videochatparticipantsinvited - * - * @method User[] getUsers() New members that were invited to the video chat - */ -class VideoChatParticipantsInvited extends Entity -{ - /** - * {@inheritdoc} - */ - protected function subEntities(): array - { - return [ - 'users' => [User::class], - ]; - } -} diff --git a/src/Entities/VideoChatScheduled.php b/src/Entities/VideoChatScheduled.php deleted file mode 100644 index 6ac750197..000000000 --- a/src/Entities/VideoChatScheduled.php +++ /dev/null @@ -1,24 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -/** - * This object represents a service message about a video chat scheduled in the chat. - * - * @link https://core.telegram.org/bots/api#videochatscheduled - * - * @method int getStartDate() Point in time (Unix timestamp) when the video chat is supposed to be started by a chat administrator - */ -class VideoChatScheduled extends Entity -{ - -} diff --git a/src/Entities/VideoChatStarted.php b/src/Entities/VideoChatStarted.php deleted file mode 100644 index 4e10aa8d5..000000000 --- a/src/Entities/VideoChatStarted.php +++ /dev/null @@ -1,22 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -/** - * This object represents a service message about a video chat started in the chat. Currently holds no information. - * - * @link https://core.telegram.org/bots/api#videochatstarted - */ -class VideoChatStarted extends Entity -{ - -} diff --git a/src/Entities/VideoNote.php b/src/Entities/VideoNote.php deleted file mode 100644 index 164934967..000000000 --- a/src/Entities/VideoNote.php +++ /dev/null @@ -1,37 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -/** - * Class VideoNote - * - * @link https://core.telegram.org/bots/api#videonote - * - * @method string getFileId() Identifier for this file, which can be used to download or reuse the file - * @method string getFileUniqueId() Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. - * @method int getLength() Video width and height as defined by sender - * @method int getDuration() Duration of the audio in seconds as defined by sender - * @method PhotoSize getThumbnail() Optional. Video thumbnail as defined by sender - * @method int getFileSize() Optional. File size - */ -class VideoNote extends Entity -{ - /** - * {@inheritdoc} - */ - protected function subEntities(): array - { - return [ - 'thumbnail' => PhotoSize::class, - ]; - } -} diff --git a/src/Entities/Voice.php b/src/Entities/Voice.php deleted file mode 100644 index 1aab3b21e..000000000 --- a/src/Entities/Voice.php +++ /dev/null @@ -1,28 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -/** - * Class Voice - * - * @link https://core.telegram.org/bots/api#voice - * - * @method string getFileId() Identifier for this file, which can be used to download or reuse the file - * @method string getFileUniqueId() Unique identifier for this file, which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. - * @method int getDuration() Duration of the audio in seconds as defined by sender - * @method string getMimeType() Optional. MIME type of the file as defined by sender - * @method int getFileSize() Optional. File size - */ -class Voice extends Entity -{ - -} diff --git a/src/Entities/WebAppData.php b/src/Entities/WebAppData.php deleted file mode 100644 index e23ec53c8..000000000 --- a/src/Entities/WebAppData.php +++ /dev/null @@ -1,21 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Entities; - -/** - * Class WebhookInfo - * - * @link https://core.telegram.org/bots/api#webhookinfo - * - * @method string getUrl() Webhook URL, may be empty if webhook is not set up - * @method bool getHasCustomCertificate() True, if a custom certificate was provided for webhook certificate checks - * @method int getPendingUpdateCount() Number of updates awaiting delivery - * @method string getIpAddress() Optional. Currently used webhook IP address - * @method int getLastErrorDate() Optional. Unix time for the most recent error that happened when trying to deliver an update via webhook - * @method string getLastErrorMessage() Optional. Error message in human-readable format for the most recent error that happened when trying to deliver an update via webhook - * @method int getLastSynchronizationErrorDate() Optional. Unix time of the most recent error that happened when trying to synchronize available updates with Telegram datacenters - * @method int getMaxConnections() Optional. Maximum allowed number of simultaneous HTTPS connections to the webhook for update delivery - * @method string[] getAllowedUpdates() Optional. A list of update types the bot is subscribed to. Defaults to all update types - */ -class WebhookInfo extends Entity -{ - -} diff --git a/src/Entities/WriteAccessAllowed.php b/src/Entities/WriteAccessAllowed.php deleted file mode 100644 index 5785732eb..000000000 --- a/src/Entities/WriteAccessAllowed.php +++ /dev/null @@ -1,19 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Exception; - -/** - * Thrown when bot token is invalid - */ -class InvalidBotTokenException extends TelegramException -{ - /** - * InvalidBotTokenException constructor - */ - public function __construct() - { - parent::__construct('Invalid bot token!'); - } -} diff --git a/src/Exception/TelegramException.php b/src/Exception/TelegramException.php deleted file mode 100644 index fb0acbec1..000000000 --- a/src/Exception/TelegramException.php +++ /dev/null @@ -1,22 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Exception; - -use Exception; - -/** - * Main exception class used for exception handling - */ -class TelegramException extends Exception -{ - -} diff --git a/src/Exception/TelegramLogException.php b/src/Exception/TelegramLogException.php deleted file mode 100644 index 282ab5557..000000000 --- a/src/Exception/TelegramLogException.php +++ /dev/null @@ -1,20 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Exception; - -/** - * Main exception class used for exception handling - */ -class TelegramLogException extends TelegramException -{ - -} diff --git a/src/Exceptions/NotYetImplementedException.php b/src/Exceptions/NotYetImplementedException.php new file mode 100644 index 000000000..f3bb54787 --- /dev/null +++ b/src/Exceptions/NotYetImplementedException.php @@ -0,0 +1,11 @@ +client ??= Psr18ClientDiscovery::find(); + $this->requestFactory ??= Psr17FactoryDiscovery::findRequestFactory(); + $this->streamFactory ??= Psr17FactoryDiscovery::findStreamFactory(); + } + + public function get(string $uri): ResponseInterface + { + return $this->client->sendRequest( + $this->requestFactory->createRequest('GET', $uri) + ); + } + + public function postJson(string $uri, array $data): ResponseInterface + { + $json = json_encode($data); + + return $this->client->sendRequest( + $this->requestFactory->createRequest('POST', $uri) + ->withHeader('Content-Type', 'application/json') + ->withBody( + $this->streamFactory->createStream($json) + ) + ); + } +} diff --git a/src/Request.php b/src/Request.php deleted file mode 100644 index 21a7a5f7a..000000000 --- a/src/Request.php +++ /dev/null @@ -1,1038 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot; - -use GuzzleHttp\Client; -use GuzzleHttp\ClientInterface; -use GuzzleHttp\Exception\ConnectException; -use GuzzleHttp\Exception\RequestException; -use GuzzleHttp\Psr7\Stream; -use Longman\TelegramBot\Entities\File; -use Longman\TelegramBot\Entities\InputMedia\InputMedia; -use Longman\TelegramBot\Entities\Message; -use Longman\TelegramBot\Entities\ServerResponse; -use Longman\TelegramBot\Exception\InvalidBotTokenException; -use Longman\TelegramBot\Exception\TelegramException; -use Throwable; - -/** - * Class Request - * - * @method static ServerResponse getUpdates(array $data) Use this method to receive incoming updates using long polling (wiki). An Array of Update objects is returned. - * @method static ServerResponse setWebhook(array $data) Use this method to specify a url and receive incoming updates via an outgoing webhook. Whenever there is an update for the bot, we will send an HTTPS POST request to the specified url, containing a JSON-serialized Update. In case of an unsuccessful request, we will give up after a reasonable amount of attempts. Returns true. - * @method static ServerResponse deleteWebhook(array $data) Use this method to remove webhook integration if you decide to switch back to getUpdates. Returns True on success. - * @method static ServerResponse getWebhookInfo() Use this method to get current webhook status. Requires no parameters. On success, returns a WebhookInfo object. If the bot is using getUpdates, will return an object with the url field empty. - * @method static ServerResponse getMe() A simple method for testing your bot's auth token. Requires no parameters. Returns basic information about the bot in form of a User object. - * @method static ServerResponse logOut() Use this method to log out from the cloud Bot API server before launching the bot locally. Requires no parameters. Returns True on success. - * @method static ServerResponse close() Use this method to close the bot instance before moving it from one local server to another. Requires no parameters. Returns True on success. - * @method static ServerResponse forwardMessage(array $data) Use this method to forward messages of any kind. On success, the sent Message is returned. - * @method static ServerResponse forwardMessages(array $data) Use this method to forward multiple messages of any kind. If some of the specified messages can't be found or forwarded, they are skipped. Service messages and messages with protected content can't be forwarded. Album grouping is kept for forwarded messages. On success, an array of MessageId of the sent messages is returned. - * @method static ServerResponse copyMessage(array $data) Use this method to copy messages of any kind. The method is analogous to the method forwardMessages, but the copied message doesn't have a link to the original message. Returns the MessageId of the sent message on success. - * @method static ServerResponse copyMessages(array $data) Use this method to copy messages of any kind. If some of the specified messages can't be found or copied, they are skipped. Service messages, giveaway messages, giveaway winners messages, and invoice messages can't be copied. A quiz poll can be copied only if the value of the field correct_option_id is known to the bot. The method is analogous to the method forwardMessages, but the copied messages don't have a link to the original message. Album grouping is kept for copied messages. On success, an array of MessageId of the sent messages is returned. - * @method static ServerResponse sendPhoto(array $data) Use this method to send photos. On success, the sent Message is returned. - * @method static ServerResponse sendAudio(array $data) Use this method to send audio files, if you want Telegram clients to display them in the music player. Your audio must be in the .mp3 format. On success, the sent Message is returned. Bots can currently send audio files of up to 50 MB in size, this limit may be changed in the future. - * @method static ServerResponse sendDocument(array $data) Use this method to send general files. On success, the sent Message is returned. Bots can currently send files of any type of up to 50 MB in size, this limit may be changed in the future. - * @method static ServerResponse sendSticker(array $data) Use this method to send .webp stickers. On success, the sent Message is returned. - * @method static ServerResponse sendVideo(array $data) Use this method to send video files, Telegram clients support mp4 videos (other formats may be sent as Document). On success, the sent Message is returned. Bots can currently send video files of up to 50 MB in size, this limit may be changed in the future. - * @method static ServerResponse sendAnimation(array $data) Use this method to send animation files (GIF or H.264/MPEG-4 AVC video without sound). On success, the sent Message is returned. Bots can currently send animation files of up to 50 MB in size, this limit may be changed in the future. - * @method static ServerResponse sendVoice(array $data) Use this method to send audio files, if you want Telegram clients to display the file as a playable voice message. For this to work, your audio must be in an .ogg file encoded with OPUS (other formats may be sent as Audio or Document). On success, the sent Message is returned. Bots can currently send voice messages of up to 50 MB in size, this limit may be changed in the future. - * @method static ServerResponse sendVideoNote(array $data) Use this method to send video messages. On success, the sent Message is returned. - * @method static ServerResponse sendMediaGroup(array $data) Use this method to send a group of photos or videos as an album. On success, an array of the sent Messages is returned. - * @method static ServerResponse sendLocation(array $data) Use this method to send point on the map. On success, the sent Message is returned. - * @method static ServerResponse editMessageLiveLocation(array $data) Use this method to edit live location messages sent by the bot or via the bot (for inline bots). A location can be edited until its live_period expires or editing is explicitly disabled by a call to stopMessageLiveLocation. On success, if the edited message was sent by the bot, the edited Message is returned, otherwise True is returned. - * @method static ServerResponse stopMessageLiveLocation(array $data) Use this method to stop updating a live location message sent by the bot or via the bot (for inline bots) before live_period expires. On success, if the message was sent by the bot, the sent Message is returned, otherwise True is returned. - * @method static ServerResponse sendVenue(array $data) Use this method to send information about a venue. On success, the sent Message is returned. - * @method static ServerResponse sendContact(array $data) Use this method to send phone contacts. On success, the sent Message is returned. - * @method static ServerResponse sendPoll(array $data) Use this method to send a native poll. A native poll can't be sent to a private chat. On success, the sent Message is returned. - * @method static ServerResponse sendDice(array $data) Use this method to send a dice, which will have a random value from 1 to 6. On success, the sent Message is returned. - * @method static ServerResponse sendChatAction(array $data) Use this method when you need to tell the user that something is happening on the bot's side. The status is set for 5 seconds or less (when a message arrives from your bot, Telegram clients clear its typing status). Returns True on success. - * @method static ServerResponse setMessageReaction(array $data) Use this method to change the chosen reactions on a message. Service messages can't be reacted to. Automatically forwarded messages from a channel to its discussion group have the same available reactions as messages in the channel. Returns True on success. - * @method static ServerResponse getUserProfilePhotos(array $data) Use this method to get a list of profile pictures for a user. Returns a UserProfilePhotos object. - * @method static ServerResponse getFile(array $data) Use this method to get basic info about a file and prepare it for downloading. For the moment, bots can download files of up to 20MB in size. On success, a File object is returned. The file can then be downloaded via the link https://api.telegram.org/file/bot/, where is taken from the response. It is guaranteed that the link will be valid for at least 1 hour. When the link expires, a new one can be requested by calling getFile again. - * @method static ServerResponse banChatMember(array $data) Use this method to kick a user from a group, a supergroup or a channel. In the case of supergroups and channels, the user will not be able to return to the group on their own using invite links, etc., unless unbanned first. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Returns True on success. - * @method static ServerResponse unbanChatMember(array $data) Use this method to unban a previously kicked user in a supergroup or channel. The user will not return to the group or channel automatically, but will be able to join via link, etc. The bot must be an administrator for this to work. Returns True on success. - * @method static ServerResponse restrictChatMember(array $data) Use this method to restrict a user in a supergroup. The bot must be an administrator in the supergroup for this to work and must have the appropriate admin rights. Pass True for all permissions to lift restrictions from a user. Returns True on success. - * @method static ServerResponse promoteChatMember(array $data) Use this method to promote or demote a user in a supergroup or a channel. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Pass False for all boolean parameters to demote a user. Returns True on success. - * @method static ServerResponse setChatAdministratorCustomTitle(array $data) Use this method to set a custom title for an administrator in a supergroup promoted by the bot. Returns True on success. - * @method static ServerResponse banChatSenderChat(array $data) Use this method to ban a channel chat in a supergroup or a channel. Until the chat is unbanned, the owner of the banned chat won't be able to send messages on behalf of any of their channels. The bot must be an administrator in the supergroup or channel for this to work and must have the appropriate administrator rights. Returns True on success. - * @method static ServerResponse unbanChatSenderChat(array $data) Use this method to unban a previously banned channel chat in a supergroup or channel. The bot must be an administrator for this to work and must have the appropriate administrator rights. Returns True on success. - * @method static ServerResponse setChatPermissions(array $data) Use this method to set default chat permissions for all members. The bot must be an administrator in the group or a supergroup for this to work and must have the can_restrict_members admin rights. Returns True on success. - * @method static ServerResponse exportChatInviteLink(array $data) Use this method to generate a new invite link for a chat. Any previously generated link is revoked. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Returns the new invite link as String on success. - * @method static ServerResponse createChatInviteLink(array $data) Use this method to create an additional invite link for a chat. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. The link can be revoked using the method revokeChatInviteLink. Returns the new invite link as ChatInviteLink object. - * @method static ServerResponse editChatInviteLink(array $data) Use this method to edit a non-primary invite link created by the bot. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Returns the edited invite link as a ChatInviteLink object. - * @method static ServerResponse revokeChatInviteLink(array $data) Use this method to revoke an invite link created by the bot. If the primary link is revoked, a new link is automatically generated. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Returns the revoked invite link as ChatInviteLink object. - * @method static ServerResponse approveChatJoinRequest(array $data) Use this method to approve a chat join request. The bot must be an administrator in the chat for this to work and must have the can_invite_users administrator right. Returns True on success. - * @method static ServerResponse declineChatJoinRequest(array $data) Use this method to decline a chat join request. The bot must be an administrator in the chat for this to work and must have the can_invite_users administrator right. Returns True on success. - * @method static ServerResponse setChatPhoto(array $data) Use this method to set a new profile photo for the chat. Photos can't be changed for private chats. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Returns True on success. - * @method static ServerResponse deleteChatPhoto(array $data) Use this method to delete a chat photo. Photos can't be changed for private chats. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Returns True on success. - * @method static ServerResponse setChatTitle(array $data) Use this method to change the title of a chat. Titles can't be changed for private chats. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Returns True on success. - * @method static ServerResponse setChatDescription(array $data) Use this method to change the description of a group, a supergroup or a channel. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Returns True on success. - * @method static ServerResponse pinChatMessage(array $data) Use this method to pin a message in a supergroup or a channel. The bot must be an administrator in the chat for this to work and must have the ‘can_pin_messages’ admin right in the supergroup or ‘can_edit_messages’ admin right in the channel. Returns True on success. - * @method static ServerResponse unpinChatMessage(array $data) Use this method to unpin a message in a supergroup or a channel. The bot must be an administrator in the chat for this to work and must have the ‘can_pin_messages’ admin right in the supergroup or ‘can_edit_messages’ admin right in the channel. Returns True on success. - * @method static ServerResponse unpinAllChatMessages(array $data) Use this method to clear the list of pinned messages in a chat. If the chat is not a private chat, the bot must be an administrator in the chat for this to work and must have the 'can_pin_messages' admin right in a supergroup or 'can_edit_messages' admin right in a channel. Returns True on success. - * @method static ServerResponse leaveChat(array $data) Use this method for your bot to leave a group, supergroup or channel. Returns True on success. - * @method static ServerResponse getChat(array $data) Use this method to get up to date information about the chat (current name of the user for one-on-one conversations, current username of a user, group or channel, etc.). Returns a Chat object on success. - * @method static ServerResponse getChatAdministrators(array $data) Use this method to get a list of administrators in a chat. On success, returns an Array of ChatMember objects that contains information about all chat administrators except other bots. If the chat is a group or a supergroup and no administrators were appointed, only the creator will be returned. - * @method static ServerResponse getChatMemberCount(array $data) Use this method to get the number of members in a chat. Returns Int on success. - * @method static ServerResponse getChatMember(array $data) Use this method to get information about a member of a chat. Returns a ChatMember object on success. - * @method static ServerResponse setChatStickerSet(array $data) Use this method to set a new group sticker set for a supergroup. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Use the field can_set_sticker_set optionally returned in getChat requests to check if the bot can use this method. Returns True on success. - * @method static ServerResponse deleteChatStickerSet(array $data) Use this method to delete a group sticker set from a supergroup. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Use the field can_set_sticker_set optionally returned in getChat requests to check if the bot can use this method. Returns True on success. - * @method static ServerResponse getForumTopicIconStickers(array $data) Use this method to get custom emoji stickers, which can be used as a forum topic icon by any user. Requires no parameters. Returns an Array of Sticker objects - * @method static ServerResponse createForumTopic(array $data) Use this method to create a topic in a forum supergroup chat. The bot must be an administrator in the chat for this to work and must have the can_manage_topics administrator rights. Returns information about the created topic as a ForumTopic object. - * @method static ServerResponse editForumTopic(array $data) Use this method to edit name and icon of a topic in a forum supergroup chat. The bot must be an administrator in the chat for this to work and must have can_manage_topics administrator rights, unless it is the creator of the topic. Returns True on success. - * @method static ServerResponse closeForumTopic(array $data) Use this method to close an open topic in a forum supergroup chat. The bot must be an administrator in the chat for this to work and must have the can_manage_topics administrator rights, unless it is the creator of the topic. Returns True on success. - * @method static ServerResponse reopenForumTopic(array $data) Use this method to reopen a closed topic in a forum supergroup chat. The bot must be an administrator in the chat for this to work and must have the can_manage_topics administrator rights, unless it is the creator of the topic. Returns True on success. - * @method static ServerResponse deleteForumTopic(array $data) Use this method to delete a forum topic along with all its messages in a forum supergroup chat. The bot must be an administrator in the chat for this to work and must have the can_delete_messages administrator rights. Returns True on success. - * @method static ServerResponse unpinAllForumTopicMessages(array $data) Use this method to clear the list of pinned messages in a forum topic. The bot must be an administrator in the chat for this to work and must have the can_pin_messages administrator right in the supergroup. Returns True on success. - * @method static ServerResponse editGeneralForumTopic(array $data) Use this method to edit the name of the 'General' topic in a forum supergroup chat. The bot must be an administrator in the chat for this to work and must have can_manage_topics administrator rights. Returns True on success. - * @method static ServerResponse closeGeneralForumTopic(array $data) Use this method to close an open 'General' topic in a forum supergroup chat. The bot must be an administrator in the chat for this to work and must have the can_manage_topics administrator rights. Returns True on success. - * @method static ServerResponse reopenGeneralForumTopic(array $data) Use this method to reopen a closed 'General' topic in a forum supergroup chat. The bot must be an administrator in the chat for this to work and must have the can_manage_topics administrator rights. The topic will be automatically unhidden if it was hidden. Returns True on success. - * @method static ServerResponse hideGeneralForumTopic(array $data) Use this method to hide the 'General' topic in a forum supergroup chat. The bot must be an administrator in the chat for this to work and must have the can_manage_topics administrator rights. The topic will be automatically closed if it was open. Returns True on success. - * @method static ServerResponse unhideGeneralForumTopic(array $data) Use this method to unhide the 'General' topic in a forum supergroup chat. The bot must be an administrator in the chat for this to work and must have the can_manage_topics administrator rights. Returns True on success. - * @method static ServerResponse unpinAllGeneralForumTopicMessages(array $data) Use this method to clear the list of pinned messages in a General forum topic. The bot must be an administrator in the chat for this to work and must have the can_pin_messages administrator right in the supergroup. Returns True on success. - * @method static ServerResponse answerCallbackQuery(array $data) Use this method to send answers to callback queries sent from inline keyboards. The answer will be displayed to the user as a notification at the top of the chat screen or as an alert. On success, True is returned. - * @method static ServerResponse answerInlineQuery(array $data) Use this method to send answers to an inline query. On success, True is returned. - * @method static ServerResponse getUserChatBoosts(array $data) Use this method to get the list of boosts added to a chat by a user. Requires administrator rights in the chat. Returns a UserChatBoosts object. - * @method static ServerResponse setMyCommands(array $data) Use this method to change the list of the bot's commands. Returns True on success. - * @method static ServerResponse deleteMyCommands(array $data) Use this method to delete the list of the bot's commands for the given scope and user language. After deletion, higher level commands will be shown to affected users. Returns True on success. - * @method static ServerResponse getMyCommands(array $data) Use this method to get the current list of the bot's commands. Requires no parameters. Returns Array of BotCommand on success. - * @method static ServerResponse setMyName(array $data) Use this method to change the bot's name. Returns True on success. - * @method static ServerResponse getMyName(array $data) Use this method to get the current bot name for the given user language. Returns BotName on success. - * @method static ServerResponse setMyDescription(array $data) Use this method to change the bot's description, which is shown in the chat with the bot if the chat is empty. Returns True on success. - * @method static ServerResponse getMyDescription(array $data) Use this method to get the current bot description for the given user language. Returns BotDescription on success. - * @method static ServerResponse setMyShortDescription(array $data) Use this method to change the bot's short description, which is shown on the bot's profile page and is sent together with the link when users share the bot. Returns True on success. - * @method static ServerResponse getMyShortDescription(array $data) Use this method to get the current bot short description for the given user language. Returns BotShortDescription on success. - * @method static ServerResponse setChatMenuButton(array $data) Use this method to change the bot's menu button in a private chat, or the default menu button. Returns True on success. - * @method static ServerResponse getChatMenuButton(array $data) Use this method to get the current value of the bot's menu button in a private chat, or the default menu button. Returns MenuButton on success. - * @method static ServerResponse setMyDefaultAdministratorRights(array $data) Use this method to change the default administrator rights requested by the bot when it's added as an administrator to groups or channels. These rights will be suggested to users, but they are are free to modify the list before adding the bot. Returns True on success. - * @method static ServerResponse getMyDefaultAdministratorRights(array $data) Use this method to get the current default administrator rights of the bot. Returns ChatAdministratorRights on success. - * @method static ServerResponse editMessageText(array $data) Use this method to edit text and game messages sent by the bot or via the bot (for inline bots). On success, if edited message is sent by the bot, the edited Message is returned, otherwise True is returned. - * @method static ServerResponse editMessageCaption(array $data) Use this method to edit captions of messages sent by the bot or via the bot (for inline bots). On success, if edited message is sent by the bot, the edited Message is returned, otherwise True is returned. - * @method static ServerResponse editMessageMedia(array $data) Use this method to edit audio, document, photo, or video messages. On success, if the edited message was sent by the bot, the edited Message is returned, otherwise True is returned. - * @method static ServerResponse editMessageReplyMarkup(array $data) Use this method to edit only the reply markup of messages sent by the bot or via the bot (for inline bots). On success, if edited message is sent by the bot, the edited Message is returned, otherwise True is returned. - * @method static ServerResponse stopPoll(array $data) Use this method to stop a poll which was sent by the bot. On success, the stopped Poll with the final results is returned. - * @method static ServerResponse deleteMessage(array $data) Use this method to delete a message, including service messages, with certain limitations. Returns True on success. - * @method static ServerResponse deleteMessages(array $data) Use this method to delete multiple messages simultaneously. If some of the specified messages can't be found, they are skipped. Returns True on success. - * @method static ServerResponse getStickerSet(array $data) Use this method to get a sticker set. On success, a StickerSet object is returned. - * @method static ServerResponse getCustomEmojiStickers(array $data) Use this method to get information about custom emoji stickers by their identifiers. Returns an Array of Sticker objects. - * @method static ServerResponse uploadStickerFile(array $data) Use this method to upload a .png file with a sticker for later use in createNewStickerSet and addStickerToSet methods (can be used multiple times). Returns the uploaded File on success. - * @method static ServerResponse createNewStickerSet(array $data) Use this method to create new sticker set owned by a user. The bot will be able to edit the created sticker set. Returns True on success. - * @method static ServerResponse addStickerToSet(array $data) Use this method to add a new sticker to a set created by the bot. Returns True on success. - * @method static ServerResponse setStickerPositionInSet(array $data) Use this method to move a sticker in a set created by the bot to a specific position. Returns True on success. - * @method static ServerResponse deleteStickerFromSet(array $data) Use this method to delete a sticker from a set created by the bot. Returns True on success. - * @method static ServerResponse setStickerEmojiList(array $data) Use this method to change the list of emoji assigned to a regular or custom emoji sticker. The sticker must belong to a sticker set created by the bot. Returns True on success. - * @method static ServerResponse setStickerKeywords(array $data) Use this method to change search keywords assigned to a regular or custom emoji sticker. The sticker must belong to a sticker set created by the bot. Returns True on success. - * @method static ServerResponse setStickerMaskPosition(array $data) Use this method to change the mask position of a mask sticker. The sticker must belong to a sticker set that was created by the bot. Returns True on success. - * @method static ServerResponse setStickerSetTitle(array $data) Use this method to set the title of a created sticker set. Returns True on success. - * @method static ServerResponse setStickerSetThumbnail(array $data) Use this method to set the thumbnail of a sticker set. Animated thumbnails can be set for animated sticker sets only. Returns True on success. - * @method static ServerResponse setCustomEmojiStickerSetThumbnail(array $data) Use this method to set the thumbnail of a custom emoji sticker set. Returns True on success. - * @method static ServerResponse deleteStickerSet(array $data) Use this method to delete a sticker set that was created by the bot. Returns True on success. - * @method static ServerResponse answerWebAppQuery(array $data) Use this method to set the result of an interaction with a Web App and send a corresponding message on behalf of the user to the chat from which the query originated. On success, a SentWebAppMessage object is returned. - * @method static ServerResponse sendInvoice(array $data) Use this method to send invoices. On success, the sent Message is returned. - * @method static ServerResponse createInvoiceLink(array $data) Use this method to create a link for an invoice. Returns the created invoice link as String on success. - * @method static ServerResponse answerShippingQuery(array $data) If you sent an invoice requesting a shipping address and the parameter is_flexible was specified, the Bot API will send an Update with a shipping_query field to the bot. Use this method to reply to shipping queries. On success, True is returned. - * @method static ServerResponse answerPreCheckoutQuery(array $data) Once the user has confirmed their payment and shipping details, the Bot API sends the final confirmation in the form of an Update with the field pre_checkout_query. Use this method to respond to such pre-checkout queries. On success, True is returned. - * @method static ServerResponse setPassportDataErrors(array $data) Informs a user that some of the Telegram Passport elements they provided contains errors. The user will not be able to re-submit their Passport to you until the errors are fixed (the contents of the field for which you returned the error must change). Returns True on success. Use this if the data submitted by the user doesn't satisfy the standards your service requires for any reason. For example, if a birthday date seems invalid, a submitted document is blurry, a scan shows evidence of tampering, etc. Supply some details in the error message to make sure the user knows how to correct the issues. - * @method static ServerResponse sendGame(array $data) Use this method to send a game. On success, the sent Message is returned. - * @method static ServerResponse setGameScore(array $data) Use this method to set the score of the specified user in a game. On success, if the message was sent by the bot, returns the edited Message, otherwise returns True. Returns an error, if the new score is not greater than the user's current score in the chat and force is False. - * @method static ServerResponse getGameHighScores(array $data) Use this method to get data for high score tables. Will return the score of the specified user and several of his neighbors in a game. On success, returns an Array of GameHighScore objects. - */ -class Request -{ - /** - * Telegram object - * - * @var Telegram - */ - private static $telegram; - - /** - * URI of the Telegram API - * - * @var string - */ - private static $api_base_uri = 'https://api.telegram.org'; - - /** - * URI of the Telegram API for downloading files (relative to $api_base_url or absolute) - * - * @var string - */ - private static $api_base_download_uri = '/file/bot{API_KEY}'; - - /** - * Guzzle Client object - * - * @var ClientInterface - */ - private static $client; - - /** - * Request limiter - * - * @var bool - */ - private static $limiter_enabled; - - /** - * Request limiter's interval between checks - * - * @var float - */ - private static $limiter_interval; - - /** - * The current action that is being executed - * - * @var string - */ - private static $current_action = ''; - - /** - * Available actions to send - * - * This is basically the list of all methods listed on the official API documentation. - * - * @link https://core.telegram.org/bots/api - * - * @var array - */ - private static $actions = [ - 'getUpdates', - 'setWebhook', - 'deleteWebhook', - 'getWebhookInfo', - 'getMe', - 'logOut', - 'close', - 'sendMessage', - 'forwardMessage', - 'forwardMessages', - 'copyMessage', - 'copyMessages', - 'sendPhoto', - 'sendAudio', - 'sendDocument', - 'sendSticker', - 'sendVideo', - 'sendAnimation', - 'sendVoice', - 'sendVideoNote', - 'sendMediaGroup', - 'sendLocation', - 'editMessageLiveLocation', - 'stopMessageLiveLocation', - 'sendVenue', - 'sendContact', - 'sendPoll', - 'sendDice', - 'sendChatAction', - 'setMessageReaction', - 'getUserProfilePhotos', - 'getFile', - 'banChatMember', - 'unbanChatMember', - 'restrictChatMember', - 'promoteChatMember', - 'setChatAdministratorCustomTitle', - 'banChatSenderChat', - 'unbanChatSenderChat', - 'setChatPermissions', - 'exportChatInviteLink', - 'createChatInviteLink', - 'editChatInviteLink', - 'revokeChatInviteLink', - 'approveChatJoinRequest', - 'declineChatJoinRequest', - 'setChatPhoto', - 'deleteChatPhoto', - 'setChatTitle', - 'setChatDescription', - 'pinChatMessage', - 'unpinChatMessage', - 'unpinAllChatMessages', - 'leaveChat', - 'getChat', - 'getChatAdministrators', - 'getChatMemberCount', - 'getChatMember', - 'setChatStickerSet', - 'deleteChatStickerSet', - 'getForumTopicIconStickers', - 'createForumTopic', - 'editForumTopic', - 'closeForumTopic', - 'reopenForumTopic', - 'deleteForumTopic', - 'unpinAllForumTopicMessages', - 'editGeneralForumTopic', - 'closeGeneralForumTopic', - 'reopenGeneralForumTopic', - 'hideGeneralForumTopic', - 'unhideGeneralForumTopic', - 'unpinAllGeneralForumTopicMessages', - 'answerCallbackQuery', - 'answerInlineQuery', - 'getUserChatBoosts', - 'setMyCommands', - 'deleteMyCommands', - 'getMyCommands', - 'setMyName', - 'getMyName', - 'setMyDescription', - 'getMyDescription', - 'setMyShortDescription', - 'getMyShortDescription', - 'setChatMenuButton', - 'getChatMenuButton', - 'setMyDefaultAdministratorRights', - 'getMyDefaultAdministratorRights', - 'editMessageText', - 'editMessageCaption', - 'editMessageMedia', - 'editMessageReplyMarkup', - 'stopPoll', - 'deleteMessage', - 'deleteMessages', - 'getStickerSet', - 'getCustomEmojiStickers', - 'uploadStickerFile', - 'createNewStickerSet', - 'addStickerToSet', - 'setStickerPositionInSet', - 'deleteStickerFromSet', - 'setStickerEmojiList', - 'setStickerKeywords', - 'setStickerMaskPosition', - 'setStickerSetTitle', - 'setStickerSetThumbnail', - 'setCustomEmojiStickerSetThumbnail', - 'deleteStickerSet', - 'answerWebAppQuery', - 'sendInvoice', - 'createInvoiceLink', - 'answerShippingQuery', - 'answerPreCheckoutQuery', - 'setPassportDataErrors', - 'sendGame', - 'setGameScore', - 'getGameHighScores', - ]; - - /** - * Methods that don't require any data need a dummy param due to certain cURL issues. - * - * @see Request::addDummyParamIfNecessary() - * - * @var array - */ - private static $actions_need_dummy_param = [ - 'deleteWebhook', - 'getWebhookInfo', - 'getMe', - 'logOut', - 'close', - 'deleteMyCommands', - 'getMyCommands', - 'setMyName', - 'getMyName', - 'setMyDescription', - 'getMyDescription', - 'setMyShortDescription', - 'getMyShortDescription', - 'setChatMenuButton', - 'getChatMenuButton', - 'setMyDefaultAdministratorRights', - 'getMyDefaultAdministratorRights', - ]; - - /** - * Available fields for InputFile helper - * - * This is basically the list of all fields that allow InputFile objects - * for which input can be simplified by providing local path directly as string. - * - * @var array - */ - private static $input_file_fields = [ - 'setWebhook' => ['certificate'], - 'sendPhoto' => ['photo'], - 'sendAudio' => ['audio', 'thumbnail'], - 'sendDocument' => ['document', 'thumbnail'], - 'sendVideo' => ['video', 'thumbnail'], - 'sendAnimation' => ['animation', 'thumbnail'], - 'sendVoice' => ['voice'], - 'sendVideoNote' => ['video_note', 'thumbnail'], - 'setChatPhoto' => ['photo'], - 'sendSticker' => ['sticker'], - 'uploadStickerFile' => ['sticker'], - // @todo Look into new InputSticker field and see if we can do the same there. - // 'createNewStickerSet' => ['png_sticker', 'tgs_sticker', 'webm_sticker'], - // 'addStickerToSet' => ['png_sticker', 'tgs_sticker', 'webm_sticker'], - 'setStickerSetThumbnail' => ['thumbnail'], - ]; - - /** - * Initialize - * - * @param Telegram $telegram - */ - public static function initialize(Telegram $telegram): void - { - self::$telegram = $telegram; - self::setClient(self::$client ?: new Client(['base_uri' => self::$api_base_uri])); - } - - /** - * Set a custom Guzzle HTTP Client object - * - * @param ClientInterface $client - */ - public static function setClient(ClientInterface $client): void - { - self::$client = $client; - } - - /** - * Set a custom Bot API URL - * - * @param string $api_base_uri - * @param string $api_base_download_uri - */ - public static function setCustomBotApiUri(string $api_base_uri, string $api_base_download_uri = ''): void - { - self::$api_base_uri = $api_base_uri; - if ($api_base_download_uri !== '') { - self::$api_base_download_uri = $api_base_download_uri; - } - } - - /** - * Get input from custom input or stdin and return it - * - * @return string - */ - public static function getInput(): string - { - // First check if a custom input has been set, else get the PHP input. - return self::$telegram->getCustomInput() - ?: file_get_contents('php://input'); - } - - /** - * Generate general fake server response - * - * @param array $data Data to add to fake response - * - * @return array Fake response data - */ - public static function generateGeneralFakeServerResponse(array $data = []): array - { - //PARAM BINDED IN PHPUNIT TEST FOR TestServerResponse.php - //Maybe this is not the best possible implementation - - //No value set in $data ie testing setWebhook - //Provided $data['chat_id'] ie testing sendMessage - - $fake_response = ['ok' => true]; // :) - - if ($data === []) { - $fake_response['result'] = true; - } - - //some data to initialize the class method SendMessage - if (isset($data['chat_id'])) { - $data['message_id'] = '1234'; - $data['date'] = '1441378360'; - $data['from'] = [ - 'id' => 123456789, - 'first_name' => 'botname', - 'username' => 'namebot', - ]; - $data['chat'] = ['id' => $data['chat_id']]; - - $fake_response['result'] = $data; - } - - return $fake_response; - } - - /** - * Properly set up the request params - * - * If any item of the array is a resource, reformat it to a multipart request. - * Else, just return the passed data as form params. - * - * @param array $data - * - * @return array - * @throws TelegramException - */ - private static function setUpRequestParams(array $data): array - { - $has_resource = false; - $multipart = []; - - foreach ($data as $key => &$item) { - if ($key === 'media') { - // Magical media input helper. - $item = self::mediaInputHelper($item, $has_resource, $multipart); - } elseif (array_key_exists(self::$current_action, self::$input_file_fields) && in_array($key, self::$input_file_fields[self::$current_action], true)) { - // Allow absolute paths to local files. - if (is_string($item) && file_exists($item)) { - $item = new Stream(self::encodeFile($item)); - } - } elseif (is_array($item) || is_object($item)) { - // Convert any nested arrays or objects into JSON strings. - $item = json_encode($item); - } - - // Reformat data array in multipart way if it contains a resource - $has_resource = $has_resource || is_resource($item) || $item instanceof Stream; - $multipart[] = ['name' => $key, 'contents' => $item]; - } - unset($item); - - if ($has_resource) { - return ['multipart' => $multipart]; - } - - return ['form_params' => $data]; - } - - /** - * Magical input media helper to simplify passing media. - * - * This allows the following: - * Request::editMessageMedia([ - * ... - * 'media' => new InputMediaPhoto([ - * 'caption' => 'Caption!', - * 'media' => Request::encodeFile($local_photo), - * ]), - * ]); - * and - * Request::sendMediaGroup([ - * 'media' => [ - * new InputMediaPhoto(['media' => Request::encodeFile($local_photo_1)]), - * new InputMediaPhoto(['media' => Request::encodeFile($local_photo_2)]), - * new InputMediaVideo(['media' => Request::encodeFile($local_video_1)]), - * ], - * ]); - * and even - * Request::sendMediaGroup([ - * 'media' => [ - * new InputMediaPhoto(['media' => $local_photo_1]), - * new InputMediaPhoto(['media' => $local_photo_2]), - * new InputMediaVideo(['media' => $local_video_1]), - * ], - * ]); - * - * @param mixed $item - * @param bool $has_resource - * @param array $multipart - * - * @return mixed - * @throws TelegramException - */ - private static function mediaInputHelper($item, bool &$has_resource, array &$multipart) - { - $was_array = is_array($item); - $was_array || $item = [$item]; - - /** @var InputMedia|null $media_item */ - foreach ($item as $media_item) { - if (!($media_item instanceof InputMedia)) { - continue; - } - - // Make a list of all possible media that can be handled by the helper. - $possible_medias = array_filter([ - 'media' => $media_item->getMedia(), - 'thumbnail' => $media_item->getThumbnail(), - ]); - - foreach ($possible_medias as $type => $media) { - // Allow absolute paths to local files. - if (is_string($media) && strpos($media, 'attach://') !== 0 && file_exists($media)) { - $media = new Stream(self::encodeFile($media)); - } - - if (is_resource($media) || $media instanceof Stream) { - $has_resource = true; - $unique_key = uniqid($type . '_', false); - $multipart[] = ['name' => $unique_key, 'contents' => $media]; - - // We're literally overwriting the passed media type data! - $media_item->$type = 'attach://' . $unique_key; - $media_item->raw_data[$type] = 'attach://' . $unique_key; - } - } - } - - $was_array || $item = reset($item); - - return json_encode($item); - } - - /** - * Get the current action that's being executed - * - * @return string - */ - public static function getCurrentAction(): string - { - return self::$current_action; - } - - /** - * Execute HTTP Request - * - * @param string $action Action to execute - * @param array $data Data to attach to the execution - * - * @return string Result of the HTTP Request - * @throws TelegramException - */ - public static function execute(string $action, array $data = []): string - { - $request_params = self::setUpRequestParams($data); - $request_params['debug'] = TelegramLog::getDebugLogTempStream(); - - try { - $response = self::$client->post( - '/bot' . self::$telegram->getApiKey() . '/' . $action, - $request_params - ); - $result = (string) $response->getBody(); - } catch (ConnectException $e) { - $response = null; - $result = $e->getMessage(); - } catch (RequestException $e) { - $response = null; - $result = $e->getResponse() ? (string) $e->getResponse()->getBody() : ''; - } - - //Logging verbose debug output - if (TelegramLog::$always_log_request_and_response || $response === null) { - TelegramLog::debug('Request data:' . PHP_EOL . print_r($data, true)); - TelegramLog::debug('Response data:' . PHP_EOL . $result); - TelegramLog::endDebugLogTempStream('Verbose HTTP Request output:' . PHP_EOL . '%s' . PHP_EOL); - } - - return $result; - } - - /** - * Download file - * - * @param File $file - * - * @return bool - * @throws TelegramException - */ - public static function downloadFile(File $file): bool - { - if (empty($download_path = self::$telegram->getDownloadPath())) { - throw new TelegramException('Download path not set!'); - } - - $tg_file_path = $file->getFilePath(); - $file_path = $download_path . '/' . $tg_file_path; - - $file_dir = dirname($file_path); - //For safety reasons, first try to create the directory, then check that it exists. - //This is in case some other process has created the folder in the meantime. - if (!@mkdir($file_dir, 0755, true) && !is_dir($file_dir)) { - throw new TelegramException('Directory ' . $file_dir . ' can\'t be created'); - } - - $debug_handle = TelegramLog::getDebugLogTempStream(); - - try { - $base_download_uri = str_replace('{API_KEY}', self::$telegram->getApiKey(), self::$api_base_download_uri); - self::$client->get( - "{$base_download_uri}/{$tg_file_path}", - ['debug' => $debug_handle, 'sink' => $file_path] - ); - - return filesize($file_path) > 0; - } catch (Throwable $e) { - return false; - } finally { - //Logging verbose debug output - TelegramLog::endDebugLogTempStream('Verbose HTTP File Download Request output:' . PHP_EOL . '%s' . PHP_EOL); - } - } - - /** - * Encode file - * - * @param string $file - * - * @return resource - * @throws TelegramException - */ - public static function encodeFile(string $file) - { - $fp = fopen($file, 'rb'); - if ($fp === false) { - throw new TelegramException('Cannot open "' . $file . '" for reading'); - } - - return $fp; - } - - /** - * Send command - * - * @todo Fake response doesn't need json encoding? - * @todo Write debug entry on failure - * - * @param string $action - * @param array $data - * - * @return ServerResponse - * @throws TelegramException - */ - public static function send(string $action, array $data = []): ServerResponse - { - self::ensureValidAction($action); - self::addDummyParamIfNecessary($action, $data); - - $bot_username = self::$telegram->getBotUsername(); - - if (defined('PHPUNIT_TESTSUITE')) { - $fake_response = self::generateGeneralFakeServerResponse($data); - - return new ServerResponse($fake_response, $bot_username); - } - - self::ensureNonEmptyData($data); - - self::limitTelegramRequests($action, $data); - - // Remember which action is currently being executed. - self::$current_action = $action; - - $raw_response = self::execute($action, $data); - $response = json_decode($raw_response, true); - - if (null === $response) { - TelegramLog::debug($raw_response); - throw new TelegramException('Telegram returned an invalid response!'); - } - - $response = new ServerResponse($response, $bot_username); - - if (!$response->isOk() && $response->getErrorCode() === 401 && $response->getDescription() === 'Unauthorized') { - throw new InvalidBotTokenException(); - } - - // Special case for sent polls, which need to be saved specially. - // @todo Take into account if DB gets extracted into separate module. - if ($response->isOk() && ($message = $response->getResult()) && ($message instanceof Message) && $poll = $message->getPoll()) { - DB::insertPollRequest($poll); - } - - // Reset current action after completion. - self::$current_action = ''; - - return $response; - } - - /** - * Add a dummy parameter if the passed action requires it. - * - * If a method doesn't require parameters, we need to add a dummy one anyway, - * because of some cURL version failed POST request without parameters. - * - * @link https://github.com/php-telegram-bot/core/pull/228 - * - * @todo Would be nice to find a better solution for this! - * - * @param string $action - * @param array $data - */ - protected static function addDummyParamIfNecessary(string $action, array &$data): void - { - if (empty($data) && in_array($action, self::$actions_need_dummy_param, true)) { - // Can be anything, using a single letter to minimise request size. - $data = ['d']; - } - } - - /** - * Make sure the data isn't empty, else throw an exception - * - * @param array $data - * - * @throws TelegramException - */ - private static function ensureNonEmptyData(array $data): void - { - if (count($data) === 0) { - throw new TelegramException('Data is empty!'); - } - } - - /** - * Make sure the action is valid, else throw an exception - * - * @param string $action - * - * @throws TelegramException - */ - private static function ensureValidAction(string $action): void - { - if (!in_array($action, self::$actions, true)) { - throw new TelegramException('The action "' . $action . '" doesn\'t exist!'); - } - } - - /** - * Use this method to send text messages. On success, the last sent Message is returned - * - * All message responses are saved in `$extras['responses']`. - * Custom encoding can be defined in `$extras['encoding']` (default: `mb_internal_encoding()`) - * Custom splitting can be defined in `$extras['split']` (default: 4096) - * `$extras['split'] = null;` // force to not split message at all! - * `$extras['split'] = 200;` // split message into 200 character chunks - * - * @link https://core.telegram.org/bots/api#sendmessage - * - * @todo Splitting formatted text may break the message. - * - * @param array $data - * @param array|null $extras - * - * @return ServerResponse - * @throws TelegramException - */ - public static function sendMessage(array $data, ?array &$extras = []): ServerResponse - { - $extras = array_merge([ - 'split' => 4096, - 'encoding' => mb_internal_encoding(), - ], (array) $extras); - - $text = $data['text']; - $encoding = $extras['encoding']; - $max_length = $extras['split'] ?: mb_strlen($text, $encoding); - - $responses = []; - - do { - // Chop off and send the first message. - $data['text'] = mb_substr($text, 0, $max_length, $encoding); - $responses[] = self::send('sendMessage', $data); - - // Prepare the next message. - $text = mb_substr($text, $max_length, null, $encoding); - } while ($text !== ''); - - // Add all response objects to referenced variable. - $extras['responses'] = $responses; - - return end($responses); - } - - /** - * Any statically called method should be relayed to the `send` method. - * - * @param string $action - * @param array $data - * - * @return ServerResponse - * @throws TelegramException - */ - public static function __callStatic(string $action, array $data): ServerResponse - { - // Only argument should be the data array, ignore any others. - return static::send($action, reset($data) ?: []); - } - - /** - * Return an empty Server Response - * - * No request is sent to Telegram. - * This function is used in commands that don't need to fire a message after execution - * - * @return ServerResponse - */ - public static function emptyResponse(): ServerResponse - { - return new ServerResponse(['ok' => true, 'result' => true]); - } - - /** - * Send message to all active chats - * - * @param string $callback_function - * @param array $data - * @param array $select_chats_params - * - * @return array - * @throws TelegramException - */ - public static function sendToActiveChats( - string $callback_function, - array $data, - array $select_chats_params - ): array { - self::ensureValidAction($callback_function); - - $chats = DB::selectChats($select_chats_params); - - $results = []; - if (is_array($chats)) { - foreach ($chats as $row) { - $data['chat_id'] = $row['chat_id']; - $results[] = self::send($callback_function, $data); - } - } - - return $results; - } - - /** - * Enable request limiter - * - * @param bool $enable - * @param array $options - * - * @throws TelegramException - */ - public static function setLimiter(bool $enable = true, array $options = []): void - { - if (DB::isDbConnected()) { - $options_default = [ - 'interval' => 1, - ]; - - $options = array_merge($options_default, $options); - - if (!is_numeric($options['interval']) || $options['interval'] <= 0) { - throw new TelegramException('Interval must be a number and must be greater than zero!'); - } - - self::$limiter_interval = $options['interval']; - self::$limiter_enabled = $enable; - } - } - - /** - * This functions delays API requests to prevent reaching Telegram API limits - * Can be disabled while in execution by 'Request::setLimiter(false)' - * - * @link https://core.telegram.org/bots/faq#my-bot-is-hitting-limits-how-do-i-avoid-this - * - * @param string $action - * @param array $data - * - * @throws TelegramException - */ - private static function limitTelegramRequests(string $action, array $data = []): void - { - if (self::$limiter_enabled) { - $limited_methods = [ - 'sendMessage', - 'forwardMessage', - 'forwardMessages', - 'copyMessage', - 'copyMessages', - 'sendPhoto', - 'sendAudio', - 'sendDocument', - 'sendSticker', - 'sendVideo', - 'sendAnimation', - 'sendVoice', - 'sendVideoNote', - 'sendMediaGroup', - 'sendLocation', - 'editMessageLiveLocation', - 'stopMessageLiveLocation', - 'sendVenue', - 'sendContact', - 'sendPoll', - 'sendDice', - 'setMessageReaction', - 'sendInvoice', - 'sendGame', - 'setGameScore', - 'setMyCommands', - 'deleteMyCommands', - 'editMessageText', - 'editMessageCaption', - 'editMessageMedia', - 'editMessageReplyMarkup', - 'stopPoll', - 'deleteMessage', - 'deleteMessages', - 'setChatTitle', - 'setChatDescription', - 'setChatStickerSet', - 'deleteChatStickerSet', - 'setPassportDataErrors', - ]; - - $chat_id = $data['chat_id'] ?? null; - $inline_message_id = $data['inline_message_id'] ?? null; - - if (($chat_id || $inline_message_id) && in_array($action, $limited_methods, true)) { - $timeout = 60; - - while (true) { - if ($timeout <= 0) { - throw new TelegramException('Timed out while waiting for a request spot!'); - } - - if (!($requests = DB::getTelegramRequestCount($chat_id, $inline_message_id))) { - break; - } - - // Make sure we're handling integers here. - $requests = array_map('intval', $requests); - - $chat_per_second = ($requests['LIMIT_PER_SEC'] === 0); // No more than one message per second inside a particular chat - $global_per_second = ($requests['LIMIT_PER_SEC_ALL'] < 30); // No more than 30 messages per second to different chats - $groups_per_minute = (((is_numeric($chat_id) && $chat_id > 0) || $inline_message_id !== null) || ((!is_numeric($chat_id) || $chat_id < 0) && $requests['LIMIT_PER_MINUTE'] < 20)); // No more than 20 messages per minute in groups and channels - - if ($chat_per_second && $global_per_second && $groups_per_minute) { - break; - } - - $timeout--; - usleep((int) (self::$limiter_interval * 1000000)); - } - - DB::insertTelegramRequest($action, $data); - } - } - } - - /** - * Use this method to kick a user from a group, a supergroup or a channel. In the case of supergroups and channels, the user will not be able to return to the group on their own using invite links, etc., unless unbanned first. The bot must be an administrator in the chat for this to work and must have the appropriate admin rights. Returns True on success. - * - * @deprecated - * @see Request::banChatMember() - * - * @param array $data - * - * @return ServerResponse - */ - public static function kickChatMember(array $data = []): ServerResponse - { - return static::banChatMember($data); - } -} diff --git a/src/Telegram.php b/src/Telegram.php index bbc948052..2734d8fa0 100644 --- a/src/Telegram.php +++ b/src/Telegram.php @@ -1,1336 +1,57 @@ - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ +namespace PhpTelegramBot\Core; -namespace Longman\TelegramBot; - -defined('TB_BASE_PATH') || define('TB_BASE_PATH', __DIR__); -defined('TB_BASE_COMMANDS_PATH') || define('TB_BASE_COMMANDS_PATH', TB_BASE_PATH . '/Commands'); - -use Exception; -use InvalidArgumentException; -use Longman\TelegramBot\Commands\AdminCommand; -use Longman\TelegramBot\Commands\Command; -use Longman\TelegramBot\Commands\SystemCommand; -use Longman\TelegramBot\Commands\UserCommand; -use Longman\TelegramBot\Entities\Chat; -use Longman\TelegramBot\Entities\ServerResponse; -use Longman\TelegramBot\Entities\Update; -use Longman\TelegramBot\Entities\User; -use Longman\TelegramBot\Exception\TelegramException; -use PDO; -use RecursiveDirectoryIterator; -use RecursiveIteratorIterator; -use RegexIterator; +use PhpTelegramBot\Core\Exceptions\NotYetImplementedException; +use PhpTelegramBot\Core\Exceptions\TelegramException; class Telegram { - /** - * Version - * - * @var string - */ - protected $version = '0.82.0'; - - /** - * Telegram API key - * - * @var string - */ - protected $api_key = ''; - - /** - * Telegram Bot username - * - * @var string - */ - protected $bot_username = ''; - - /** - * Telegram Bot id - * - * @var int - */ - protected $bot_id = 0; - - /** - * Raw request data (json) for webhook methods - * - * @var string - */ - protected $input = ''; - - /** - * Custom commands paths - * - * @var array - */ - protected $commands_paths = []; - - /** - * Custom command class names - * ``` - * [ - * 'User' => [ - * // command_name => command_class - * 'start' => 'name\space\to\StartCommand', - * ], - * 'Admin' => [], //etc - * ] - * ``` - * - * @var array - */ - protected $command_classes = [ - Command::AUTH_USER => [], - Command::AUTH_ADMIN => [], - Command::AUTH_SYSTEM => [], - ]; - - /** - * Custom commands objects - * - * @var array - */ - protected $commands_objects = []; - - /** - * Current Update object - * - * @var Update - */ - protected $update; - - /** - * Upload path - * - * @var string - */ - protected $upload_path = ''; - - /** - * Download path - * - * @var string - */ - protected $download_path = ''; - - /** - * MySQL integration - * - * @var bool - */ - protected $mysql_enabled = false; - - /** - * PDO object - * - * @var PDO - */ - protected $pdo; - - /** - * Commands config - * - * @var array - */ - protected $commands_config = []; - - /** - * Admins list - * - * @var array - */ - protected $admins_list = []; - - /** - * ServerResponse of the last Command execution - * - * @var ServerResponse - */ - protected $last_command_response; - - /** - * Check if runCommands() is running in this session - * - * @var bool - */ - protected $run_commands = false; - - /** - * Is running getUpdates without DB enabled - * - * @var bool - */ - protected $getupdates_without_database = false; - - /** - * Last update ID - * Only used when running getUpdates without a database - * - * @var int - */ - protected $last_update_id; - - /** - * The command to be executed when there's a new message update and nothing more suitable is found - */ - public const GENERIC_MESSAGE_COMMAND = 'genericmessage'; - - /** - * The command to be executed by default (when no other relevant commands are applicable) - */ - public const GENERIC_COMMAND = 'generic'; - - /** - * Update filter method - * - * @var callable - */ - protected $update_filter; - - /** - * Telegram constructor. - * - * @param string $api_key - * @param string $bot_username - * - * @throws TelegramException - */ - public function __construct(string $api_key, string $bot_username = '') - { - if (empty($api_key)) { - throw new TelegramException('API KEY not defined!'); - } - preg_match('/(\d+):[\w\-]+/', $api_key, $matches); - if (!isset($matches[1])) { - throw new TelegramException('Invalid API KEY defined!'); - } - $this->bot_id = (int) $matches[1]; - $this->api_key = $api_key; - - $this->bot_username = $bot_username; - - //Add default system commands path - $this->addCommandsPath(TB_BASE_COMMANDS_PATH . '/SystemCommands'); - - Request::initialize($this); - } - - /** - * Initialize Database connection - * - * @param array $credentials - * @param string $table_prefix - * @param string $encoding - * - * @return Telegram - * @throws TelegramException - */ - public function enableMySql(array $credentials, string $table_prefix = '', string $encoding = 'utf8mb4'): Telegram - { - $this->pdo = DB::initialize($credentials, $this, $table_prefix, $encoding); - ConversationDB::initializeConversation(); - $this->mysql_enabled = true; - - return $this; - } - - /** - * Initialize Database external connection - * - * @param PDO $external_pdo_connection PDO database object - * @param string $table_prefix - * - * @return Telegram - * @throws TelegramException - */ - public function enableExternalMySql(PDO $external_pdo_connection, string $table_prefix = ''): Telegram - { - $this->pdo = DB::externalInitialize($external_pdo_connection, $this, $table_prefix); - ConversationDB::initializeConversation(); - $this->mysql_enabled = true; - - return $this; - } - - /** - * Get commands list - * - * @return array $commands - * @throws TelegramException - */ - public function getCommandsList(): array - { - $commands = []; - - foreach ($this->commands_paths as $path) { - try { - //Get all "*Command.php" files - $files = new RegexIterator( - new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($path) - ), - '/^.+Command.php$/' - ); - - foreach ($files as $file) { - // Convert filename to command - $command = $this->classNameToCommandName(substr($file->getFilename(), 0, -4)); - - // Invalid Classname - if (is_null($command)) { - continue; - } - - // Already registered - if (array_key_exists($command, $commands)) { - continue; - } - - require_once $file->getPathname(); - - $command_obj = $this->getCommandObject($command, $file->getPathname()); - if ($command_obj instanceof Command) { - $commands[$command] = $command_obj; - } - } - } catch (Exception $e) { - throw new TelegramException('Error getting commands from path: ' . $path, 0, $e); - } - } - - return $commands; - } - - /** - * Get classname of predefined commands - * - * @see command_classes - * - * @param string $auth Auth of command - * @param string $command Command name - * @param string $filepath Path to the command file - * - * @return string|null - */ - public function getCommandClassName(string $auth, string $command, string $filepath = ''): ?string - { - $command = mb_strtolower($command); - - // Invalid command - if (trim($command) === '') { - return null; - } - - $auth = $this->ucFirstUnicode($auth); - - // First, check for directly assigned command class. - if ($command_class = $this->command_classes[$auth][$command] ?? null) { - return $command_class; - } - - // Start with default namespace. - $command_namespace = __NAMESPACE__ . '\\Commands\\' . $auth . 'Commands'; - - // Check if we can get the namespace from the file (if passed). - if ($filepath && !($command_namespace = $this->getFileNamespace($filepath))) { - return null; - } - - $command_class = $command_namespace . '\\' . $this->commandNameToClassName($command); - - if (class_exists($command_class)) { - return $command_class; - } - - return null; - } - - /** - * Get an object instance of the passed command - * - * @param string $command - * @param string $filepath - * - * @return Command|null - */ - public function getCommandObject(string $command, string $filepath = ''): ?Command - { - if (isset($this->commands_objects[$command])) { - return $this->commands_objects[$command]; - } - - $which = [Command::AUTH_SYSTEM]; - $this->isAdmin() && $which[] = Command::AUTH_ADMIN; - $which[] = Command::AUTH_USER; - - foreach ($which as $auth) { - $command_class = $this->getCommandClassName($auth, $command, $filepath); - - if ($command_class) { - $command_obj = new $command_class($this, $this->update); - - if ($auth === Command::AUTH_SYSTEM && $command_obj instanceof SystemCommand) { - return $command_obj; - } - if ($auth === Command::AUTH_ADMIN && $command_obj instanceof AdminCommand) { - return $command_obj; - } - if ($auth === Command::AUTH_USER && $command_obj instanceof UserCommand) { - return $command_obj; - } - } - } - - return null; - } - - /** - * Get namespace from php file by src path - * - * @param string $src (absolute path to file) - * - * @return string|null ("Longman\TelegramBot\Commands\SystemCommands" for example) - */ - protected function getFileNamespace(string $src): ?string - { - $content = file_get_contents($src); - if (preg_match('#^\s*namespace\s+(.+?);#m', $content, $m)) { - return $m[1]; - } - - return null; - } - - /** - * Set custom input string for debug purposes - * - * @param string $input (json format) - * - * @return Telegram - */ - public function setCustomInput(string $input): Telegram - { - $this->input = $input; - - return $this; - } - - /** - * Get custom input string for debug purposes - * - * @return string - */ - public function getCustomInput(): string - { - return $this->input; - } - - /** - * Get the ServerResponse of the last Command execution - * - * @return ServerResponse - */ - public function getLastCommandResponse(): ServerResponse - { - return $this->last_command_response; - } - - /** - * Handle getUpdates method - * - * @todo Remove backwards compatibility for old signature and force $data to be an array. - * - * @param array|int|null $data - * @param int|null $timeout - * - * @return ServerResponse - * @throws TelegramException - */ - public function handleGetUpdates($data = null, ?int $timeout = null): ServerResponse - { - if (empty($this->bot_username)) { - throw new TelegramException('Bot Username is not defined!'); - } - - if (!DB::isDbConnected() && !$this->getupdates_without_database) { - return new ServerResponse( - [ - 'ok' => false, - 'description' => 'getUpdates needs MySQL connection! (This can be overridden - see documentation)', - ], - $this->bot_username - ); - } - - $offset = 0; - $limit = null; - - // By default, get update types sent by Telegram. - $allowed_updates = []; - - // @todo Backwards compatibility for old signature, remove in next version. - if (!is_array($data)) { - $limit = $data; - - @trigger_error( - sprintf('Use of $limit and $timeout parameters in %s is deprecated. Use $data array instead.', __METHOD__), - E_USER_DEPRECATED - ); - } else { - $offset = $data['offset'] ?? $offset; - $limit = $data['limit'] ?? $limit; - $timeout = $data['timeout'] ?? $timeout; - $allowed_updates = $data['allowed_updates'] ?? $allowed_updates; - } - - // Take custom input into account. - if ($custom_input = $this->getCustomInput()) { - try { - $input = json_decode($this->input, true, 512, JSON_THROW_ON_ERROR); - if (empty($input)) { - throw new TelegramException('Custom input is empty'); - } - $response = new ServerResponse($input, $this->bot_username); - } catch (\Throwable $e) { - throw new TelegramException('Invalid custom input JSON: ' . $e->getMessage()); - } - } else { - if (DB::isDbConnected() && $last_update = DB::selectTelegramUpdate(1)) { - // Get last Update id from the database. - $last_update = reset($last_update); - $this->last_update_id = $last_update['id'] ?? null; - } - - if ($this->last_update_id !== null) { - $offset = $this->last_update_id + 1; // As explained in the telegram bot API documentation. - } - - $response = Request::getUpdates(compact('offset', 'limit', 'timeout', 'allowed_updates')); - } - - if ($response->isOk()) { - // Log update. - TelegramLog::update($response->toJson()); - - // Process all updates - /** @var Update $update */ - foreach ($response->getResult() as $update) { - $this->processUpdate($update); - } - - if (!DB::isDbConnected() && !$custom_input && $this->last_update_id !== null && $offset === 0) { - // Mark update(s) as read after handling - $offset = $this->last_update_id + 1; - $limit = 1; - - Request::getUpdates(compact('offset', 'limit', 'timeout', 'allowed_updates')); - } - } - - return $response; - } - - /** - * Handle bot request from webhook - * - * @return bool - * - * @throws TelegramException - */ - public function handle(): bool - { - if ($this->bot_username === '') { - throw new TelegramException('Bot Username is not defined!'); - } - - $input = Request::getInput(); - if (empty($input)) { - throw new TelegramException('Input is empty! The webhook must not be called manually, only by Telegram.'); - } - - // Log update. - TelegramLog::update($input); - - $post = json_decode($input, true); - if (empty($post)) { - throw new TelegramException('Invalid input JSON! The webhook must not be called manually, only by Telegram.'); - } - - if ($response = $this->processUpdate(new Update($post, $this->bot_username))) { - return $response->isOk(); - } - - return false; - } - - /** - * Get the command name from the command type - * - * @param string $type - * - * @return string - */ - protected function getCommandFromType(string $type): string - { - return $this->ucFirstUnicode(str_replace('_', '', $type)); - } - - /** - * Process bot Update request - * - * @param Update $update - * - * @return ServerResponse - * @throws TelegramException - */ - public function processUpdate(Update $update): ServerResponse - { - $this->update = $update; - $this->last_update_id = $update->getUpdateId(); - - if (is_callable($this->update_filter)) { - $reason = 'Update denied by update_filter'; - try { - $allowed = (bool) call_user_func_array($this->update_filter, [$update, $this, &$reason]); - } catch (Exception $e) { - $allowed = false; - } - - if (!$allowed) { - TelegramLog::debug($reason); - return new ServerResponse(['ok' => false, 'description' => 'denied']); - } - } - - //Load admin commands - if ($this->isAdmin()) { - $this->addCommandsPath(TB_BASE_COMMANDS_PATH . '/AdminCommands', false); - } - - //Make sure we have an up-to-date command list - //This is necessary to "require" all the necessary command files! - $this->commands_objects = $this->getCommandsList(); - - //If all else fails, it's a generic message. - $command = self::GENERIC_MESSAGE_COMMAND; - - $update_type = $this->update->getUpdateType(); - if ($update_type === 'message') { - $message = $this->update->getMessage(); - $type = $message->getType(); - - // Let's check if the message object has the type field we're looking for... - $command_tmp = $type === 'command' ? $message->getCommand() : $this->getCommandFromType($type); - // ...and if a fitting command class is available. - $command_obj = $command_tmp ? $this->getCommandObject($command_tmp) : null; - - // Empty usage string denotes a non-executable command. - // @see https://github.com/php-telegram-bot/core/issues/772#issuecomment-388616072 - if ( - ($command_obj === null && $type === 'command') - || ($command_obj !== null && $command_obj->getUsage() !== '') - ) { - $command = $command_tmp; - } - } elseif ($update_type !== null) { - $command = $this->getCommandFromType($update_type); - } - - //Make sure we don't try to process update that was already processed - $last_id = DB::selectTelegramUpdate(1, $this->update->getUpdateId()); - if ($last_id && count($last_id) === 1) { - TelegramLog::debug('Duplicate update received, processing aborted!'); - return Request::emptyResponse(); - } - - DB::insertRequest($this->update); - - return $this->executeCommand($command); - } - - /** - * Execute /command - * - * @param string $command - * - * @return ServerResponse - * @throws TelegramException - */ - public function executeCommand(string $command): ServerResponse - { - $command = mb_strtolower($command); - - $command_obj = $this->commands_objects[$command] ?? $this->getCommandObject($command); - - if (!$command_obj || !$command_obj->isEnabled()) { - //Failsafe in case the Generic command can't be found - if ($command === self::GENERIC_COMMAND) { - throw new TelegramException('Generic command missing!'); - } - - //Handle a generic command or non existing one - $this->last_command_response = $this->executeCommand(self::GENERIC_COMMAND); - } else { - //execute() method is executed after preExecute() - //This is to prevent executing a DB query without a valid connection - if ($this->update) { - $this->last_command_response = $command_obj->setUpdate($this->update)->preExecute(); - } else { - $this->last_command_response = $command_obj->preExecute(); - } - } - - return $this->last_command_response; - } - - /** - * @deprecated - * - * @param string $command - * - * @return string - */ - protected function sanitizeCommand(string $command): string - { - return str_replace(' ', '', $this->ucWordsUnicode(str_replace('_', ' ', $command))); - } - - /** - * Enable a single Admin account - * - * @param int $admin_id Single admin id - * - * @return Telegram - */ - public function enableAdmin(int $admin_id): Telegram - { - if ($admin_id <= 0) { - TelegramLog::error('Invalid value "' . $admin_id . '" for admin.'); - } elseif (!in_array($admin_id, $this->admins_list, true)) { - $this->admins_list[] = $admin_id; - } - - return $this; - } - - /** - * Enable a list of Admin Accounts - * - * @param array $admin_ids List of admin ids - * - * @return Telegram - */ - public function enableAdmins(array $admin_ids): Telegram - { - foreach ($admin_ids as $admin_id) { - $this->enableAdmin($admin_id); - } - - return $this; - } - - /** - * Get list of admins - * - * @return array - */ - public function getAdminList(): array - { - return $this->admins_list; - } - - /** - * Check if the passed user is an admin - * - * If no user id is passed, the current update is checked for a valid message sender. - * - * @param int|null $user_id - * - * @return bool - */ - public function isAdmin($user_id = null): bool - { - if ($user_id === null && $this->update !== null) { - //Try to figure out if the user is an admin - $update_methods = [ - 'getMessage', - 'getEditedMessage', - 'getChannelPost', - 'getEditedChannelPost', - 'getInlineQuery', - 'getChosenInlineResult', - 'getCallbackQuery', - ]; - foreach ($update_methods as $update_method) { - $object = call_user_func([$this->update, $update_method]); - if ($object !== null && $from = $object->getFrom()) { - $user_id = $from->getId(); - break; - } - } - } - - return ($user_id === null) ? false : in_array($user_id, $this->admins_list, true); - } - - /** - * Check if user required the db connection - * - * @return bool - */ - public function isDbEnabled(): bool - { - return $this->mysql_enabled; - } - - /** - * Add a single custom command class - * - * @param string $command_class Full command class name - * - * @return Telegram - */ - public function addCommandClass(string $command_class): Telegram - { - if (!$command_class || !class_exists($command_class)) { - $error = sprintf('Command class "%s" does not exist.', $command_class); - TelegramLog::error($error); - throw new InvalidArgumentException($error); - } - - if (!is_a($command_class, Command::class, true)) { - $error = sprintf('Command class "%s" does not extend "%s".', $command_class, Command::class); - TelegramLog::error($error); - throw new InvalidArgumentException($error); - } - - // Dummy object to get data from. - $command_object = new $command_class($this); - - $auth = null; - $command_object->isSystemCommand() && $auth = Command::AUTH_SYSTEM; - $command_object->isAdminCommand() && $auth = Command::AUTH_ADMIN; - $command_object->isUserCommand() && $auth = Command::AUTH_USER; - - if ($auth) { - $command = mb_strtolower($command_object->getName()); - - $this->command_classes[$auth][$command] = $command_class; - } - - return $this; - } - - /** - * Add multiple custom command classes - * - * @param array $command_classes List of full command class names - * - * @return Telegram - */ - public function addCommandClasses(array $command_classes): Telegram - { - foreach ($command_classes as $command_class) { - $this->addCommandClass($command_class); - } - - return $this; - } - - /** - * Set a single custom commands path - * - * @param string $path Custom commands path to set - * - * @return Telegram - */ - public function setCommandsPath(string $path): Telegram - { - $this->commands_paths = []; - - $this->addCommandsPath($path); - - return $this; - } - - /** - * Add a single custom commands path - * - * @param string $path Custom commands path to add - * @param bool $before If the path should be prepended or appended to the list - * - * @return Telegram - */ - public function addCommandsPath(string $path, bool $before = true): Telegram - { - if (!is_dir($path)) { - TelegramLog::error('Commands path "' . $path . '" does not exist.'); - } elseif (!in_array($path, $this->commands_paths, true)) { - if ($before) { - array_unshift($this->commands_paths, $path); - } else { - $this->commands_paths[] = $path; - } - } - - return $this; - } - - /** - * Set multiple custom commands paths - * - * @param array $paths Custom commands paths to add - * - * @return Telegram - */ - public function setCommandsPaths(array $paths): Telegram - { - $this->commands_paths = []; - - $this->addCommandsPaths($paths); - - return $this; - } - - /** - * Add multiple custom commands paths - * - * @param array $paths Custom commands paths to add - * @param bool $before If the paths should be prepended or appended to the list - * - * @return Telegram - */ - public function addCommandsPaths(array $paths, bool $before = true): Telegram - { - foreach ($paths as $path) { - $this->addCommandsPath($path, $before); - } - - return $this; - } - - /** - * Return the list of commands paths - * - * @return array - */ - public function getCommandsPaths(): array - { - return $this->commands_paths; - } - - /** - * Return the list of command classes - * - * @return array - */ - public function getCommandClasses(): array - { - return $this->command_classes; - } - - /** - * Set custom upload path - * - * @param string $path Custom upload path - * - * @return Telegram - */ - public function setUploadPath(string $path): Telegram - { - $this->upload_path = $path; - - return $this; - } - - /** - * Get custom upload path - * - * @return string - */ - public function getUploadPath(): string - { - return $this->upload_path; - } - - /** - * Set custom download path - * - * @param string $path Custom download path - * - * @return Telegram - */ - public function setDownloadPath(string $path): Telegram - { - $this->download_path = $path; - - return $this; - } - - /** - * Get custom download path - * - * @return string - */ - public function getDownloadPath(): string - { - return $this->download_path; - } - - /** - * Set command config - * - * Provide further variables to a particular commands. - * For example you can add the channel name at the command /sendtochannel - * Or you can add the api key for external service. - * - * @param string $command - * @param array $config - * - * @return Telegram - */ - public function setCommandConfig(string $command, array $config): Telegram - { - $this->commands_config[$command] = $config; - - return $this; - } - - /** - * Get command config - * - * @param string $command - * - * @return array - */ - public function getCommandConfig(string $command): array - { - return $this->commands_config[$command] ?? []; - } - - /** - * Get API key - * - * @return string - */ - public function getApiKey(): string - { - return $this->api_key; - } - - /** - * Get Bot name - * - * @return string - */ - public function getBotUsername(): string - { - return $this->bot_username; - } - - /** - * Get Bot Id - * - * @return int - */ - public function getBotId(): int - { - return $this->bot_id; - } + protected string $apiBaseUri = 'https://api.telegram.org'; - /** - * Get Version - * - * @return string - */ - public function getVersion(): string - { - return $this->version; + public function __construct( + #[\SensitiveParameter] + protected string $botToken, + protected ?string $botUsername = null, + protected ?HttpClient $client = null, + ) { + $this->client = new HttpClient(); } - /** - * Set Webhook for bot - * - * @param string $url - * @param array $data Optional parameters. - * - * @return ServerResponse - * @throws TelegramException - */ - public function setWebhook(string $url, array $data = []): ServerResponse + public function __call(string $methodName, array $arguments) { - if ($url === '') { - throw new TelegramException('Hook url is empty!'); - } + $requestUri = $this->apiBaseUri.'/bot'.$this->botToken.'/'.$methodName; - $data = array_intersect_key($data, array_flip([ - 'certificate', - 'ip_address', - 'max_connections', - 'allowed_updates', - 'drop_pending_updates', - 'secret_token', - ])); - $data['url'] = $url; + $data = $arguments[0] ?? null; - // If the certificate is passed as a path, encode and add the file to the data array. - if (!empty($data['certificate']) && is_string($data['certificate'])) { - $data['certificate'] = Request::encodeFile($data['certificate']); - } - - $result = Request::setWebhook($data); - - if (!$result->isOk()) { - throw new TelegramException( - 'Webhook was not set! Error: ' . $result->getErrorCode() . ' ' . $result->getDescription() - ); - } - - return $result; - } - - /** - * Delete any assigned webhook - * - * @param array $data - * - * @return ServerResponse - * @throws TelegramException - */ - public function deleteWebhook(array $data = []): ServerResponse - { - $result = Request::deleteWebhook($data); + $response = match (true) { + $data === null => $this->client->get($requestUri), + default => $this->client->postJson($requestUri, $data), + }; - if (!$result->isOk()) { + $result = json_decode($response->getBody()->getContents(), true); + if ($result['ok'] !== true) { throw new TelegramException( - 'Webhook was not deleted! Error: ' . $result->getErrorCode() . ' ' . $result->getDescription() + $result['description'], + $result['error_code'] ?? 0, ); } - return $result; - } - - /** - * Replace function `ucwords` for UTF-8 characters in the class definition and commands - * - * @param string $str - * @param string $encoding (default = 'UTF-8') - * - * @return string - */ - protected function ucWordsUnicode(string $str, string $encoding = 'UTF-8'): string - { - return mb_convert_case($str, MB_CASE_TITLE, $encoding); - } - - /** - * Replace function `ucfirst` for UTF-8 characters in the class definition and commands - * - * @param string $str - * @param string $encoding (default = 'UTF-8') - * - * @return string - */ - protected function ucFirstUnicode(string $str, string $encoding = 'UTF-8'): string - { - return mb_strtoupper(mb_substr($str, 0, 1, $encoding), $encoding) - . mb_strtolower(mb_substr($str, 1, mb_strlen($str), $encoding), $encoding); - } - - /** - * Enable requests limiter - * - * @param array $options - * - * @return Telegram - * @throws TelegramException - */ - public function enableLimiter(array $options = []): Telegram - { - Request::setLimiter(true, $options); - - return $this; - } - - /** - * Run provided commands - * - * @param array $commands - * - * @return ServerResponse[] - * - * @throws TelegramException - */ - public function runCommands(array $commands): array - { - if (empty($commands)) { - throw new TelegramException('No command(s) provided!'); - } - - $this->run_commands = true; - - // Check if this request has a user Update / comes from Telegram. - if ($userUpdate = $this->update) { - $from = $this->update->getMessage()->getFrom(); - $chat = $this->update->getMessage()->getChat(); - } else { - // Fall back to the Bot user. - $from = new User([ - 'id' => $this->getBotId(), - 'first_name' => $this->getBotUsername(), - 'username' => $this->getBotUsername(), - ]); - - // Try to get "live" Bot info. - $response = Request::getMe(); - if ($response->isOk()) { - /** @var User $result */ - $result = $response->getResult(); - - $from = new User([ - 'id' => $result->getId(), - 'first_name' => $result->getFirstName(), - 'username' => $result->getUsername(), - ]); - } - - // Give Bot access to admin commands. - $this->enableAdmin($from->getId()); - - // Lock the bot to a private chat context. - $chat = new Chat([ - 'id' => $from->getId(), - 'type' => 'private', - ]); - } - - $newUpdate = static function ($text = '') use ($from, $chat) { - return new Update([ - 'update_id' => -1, - 'message' => [ - 'message_id' => -1, - 'date' => time(), - 'from' => json_decode($from->toJson(), true), - 'chat' => json_decode($chat->toJson(), true), - 'text' => $text, - ], - ]); - }; - - $responses = []; - - foreach ($commands as $command) { - $this->update = $newUpdate($command); - - // Refresh commands list for new Update object. - $this->commands_objects = $this->getCommandsList(); - - $responses[] = $this->executeCommand($this->update->getMessage()->getCommand()); - } - - // Reset Update to initial context. - $this->update = $userUpdate; - - return $responses; - } - - /** - * Is this session initiated by runCommands() - * - * @return bool - */ - public function isRunCommands(): bool - { - return $this->run_commands; - } - - /** - * Switch to enable running getUpdates without a database - * - * @param bool $enable - * - * @return Telegram - */ - public function useGetUpdatesWithoutDatabase(bool $enable = true): Telegram - { - $this->getupdates_without_database = $enable; - - return $this; + return $result['result']; } - /** - * Return last update id - * - * @return int|null - */ - public function getLastUpdateId(): ?int + public function handleGetUpdates() { - return $this->last_update_id; + throw new NotYetImplementedException(); } - /** - * Set an update filter callback - * - * @param callable $callback - * - * @return Telegram - */ - public function setUpdateFilter(callable $callback): Telegram + public function handle() { - $this->update_filter = $callback; - - return $this; + throw new NotYetImplementedException(); } - /** - * Return update filter callback - * - * @return callable|null - */ - public function getUpdateFilter(): ?callable + protected function processUpdate() { - return $this->update_filter; - } - - /** - * Converts the name of a class into the name of a command. - * - * @param string $class For example FooBarCommand - * - * @return string|null for example foo_bar. In case of errors, returns null. - */ - protected function classNameToCommandName(string $class): ?string - { - // If $class doesn't end with 'Command' - if (substr($class, -7) !== 'Command') { - return null; - } - - return mb_strtolower(preg_replace('/(.)(?=[\p{Lu}])/u', '$1_', substr($class, 0, -7))); - } - - /** - * Converts a command name into the name of a class. - * - * @param string $command For example foo_bar - * - * @return string|null for example FooBarCommand. In case of errors, returns null. - */ - protected function commandNameToClassName(string $command): ?string - { - if (trim($command) === '') { - return null; - } - - return str_replace(' ', '', $this->ucWordsUnicode(str_replace('_', ' ', $command))) . 'Command'; + throw new NotYetImplementedException(); } } diff --git a/src/TelegramLog.php b/src/TelegramLog.php deleted file mode 100644 index 449256ee9..000000000 --- a/src/TelegramLog.php +++ /dev/null @@ -1,168 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot; - -use Psr\Log\LoggerInterface; -use Psr\Log\NullLogger; - -/** - * Class TelegramLog - * - * @method static void emergency(string $message, array $context = []) - * @method static void alert(string $message, array $context = []) - * @method static void critical(string $message, array $context = []) - * @method static void error(string $message, array $context = []) - * @method static void warning(string $message, array $context = []) - * @method static void notice(string $message, array $context = []) - * @method static void info(string $message, array $context = []) - * @method static void debug(string $message, array $context = []) - * @method static void update(string $message, array $context = []) - */ -class TelegramLog -{ - /** - * Logger instance - * - * @var LoggerInterface - */ - protected static $logger; - - /** - * Logger instance for update - * - * @var LoggerInterface - */ - protected static $update_logger; - - /** - * Always log the request and response data to the debug log, also for successful requests - * - * @var bool - */ - public static $always_log_request_and_response = false; - - /** - * Temporary stream handle for debug log - * - * @var resource|null - */ - protected static $debug_log_temp_stream_handle; - - /** - * Remove bot token from debug stream - * - * @var bool - */ - public static $remove_bot_token = true; - - /** - * Initialise logging. - * - * @param LoggerInterface|null $logger - * @param LoggerInterface|null $update_logger - */ - public static function initialize(LoggerInterface $logger = null, LoggerInterface $update_logger = null): void - { - self::$logger = $logger ?: new NullLogger(); - self::$update_logger = $update_logger ?: new NullLogger(); - } - - /** - * Get the stream handle of the temporary debug output - * - * @return mixed The stream if debug is active, else false - */ - public static function getDebugLogTempStream() - { - if ((self::$debug_log_temp_stream_handle === null) && $temp_stream_handle = fopen('php://temp', 'wb+')) { - self::$debug_log_temp_stream_handle = $temp_stream_handle; - } - - return self::$debug_log_temp_stream_handle; - } - - /** - * Write the temporary debug stream to log and close the stream handle - * - * @param string $message Message (with placeholder) to write to the debug log - */ - public static function endDebugLogTempStream($message = '%s'): void - { - if (is_resource(self::$debug_log_temp_stream_handle)) { - rewind(self::$debug_log_temp_stream_handle); - $stream_contents = stream_get_contents(self::$debug_log_temp_stream_handle); - - if (self::$remove_bot_token) { - $stream_contents = preg_replace('/\/bot(\d+):[\w\-]+\//', '/botBOT_TOKEN_REMOVED/', $stream_contents); - } - - self::debug(sprintf($message, $stream_contents)); - fclose(self::$debug_log_temp_stream_handle); - self::$debug_log_temp_stream_handle = null; - } - } - - /** - * Handle any logging method call. - * - * @param string $name - * @param array $arguments - */ - public static function __callStatic(string $name, array $arguments) - { - // Get the correct logger instance. - $logger = null; - if (in_array($name, ['emergency', 'alert', 'critical', 'error', 'warning', 'notice', 'info', 'debug',], true)) { - $logger = self::$logger; - } elseif ($name === 'update') { - $logger = self::$update_logger; - $name = 'info'; - } - - // Clearly we have no logging enabled. - if ($logger === null) { - return; - } - - // Replace any placeholders from the passed context. - if (count($arguments) >= 2) { - $arguments[0] = self::interpolate($arguments[0], $arguments[1]); - } - - call_user_func_array([$logger, $name], $arguments); - } - - /** - * Interpolates context values into the message placeholders. - * - * @see https://www.php-fig.org/psr/psr-3/#12-message - * - * @param string $message - * @param array $context - * - * @return string - */ - protected static function interpolate(string $message, array $context = []): string - { - // Build a replacement array with braces around the context keys. - $replace = []; - foreach ($context as $key => $val) { - // check that the value can be casted to string - if (!is_array($val) && (!is_object($val) || method_exists($val, '__toString'))) { - $replace["{{$key}}"] = $val; - } - } - - // Interpolate replacement values into the message and return. - return strtr($message, $replace); - } -} diff --git a/src/Types/Poll.php b/src/Types/Poll.php new file mode 100644 index 000000000..57fbdf99f --- /dev/null +++ b/src/Types/Poll.php @@ -0,0 +1,26 @@ +|null Optional. Special entities that appear in the question. Currently, only custom emoji entities are allowed in poll questions */ + public ?array $question_entites; + + /** @var array List of poll options */ + public array $options; + + /** @var int Total number of users that voted in the poll */ + public int $total_voter_count; + + /** @var bool True, if the poll is closed */ + public bool $is_closed; + + // ... +} diff --git a/src/Types/Type.php b/src/Types/Type.php new file mode 100644 index 000000000..8edfea29c --- /dev/null +++ b/src/Types/Type.php @@ -0,0 +1,18 @@ +additionalFields[$name]; + } + + public function __set(string $name, $value): void + { + $this->additionalFields[$name] = $value; + } +} diff --git a/structure.sql b/structure.sql deleted file mode 100644 index cf8061536..000000000 --- a/structure.sql +++ /dev/null @@ -1,438 +0,0 @@ -CREATE TABLE IF NOT EXISTS `user` ( - `id` bigint COMMENT 'Unique identifier for this user or bot', - `is_bot` tinyint(1) DEFAULT 0 COMMENT 'True, if this user is a bot', - `first_name` CHAR(255) NOT NULL DEFAULT '' COMMENT 'User''s or bot''s first name', - `last_name` CHAR(255) DEFAULT NULL COMMENT 'User''s or bot''s last name', - `username` CHAR(191) DEFAULT NULL COMMENT 'User''s or bot''s username', - `language_code` CHAR(10) DEFAULT NULL COMMENT 'IETF language tag of the user''s language', - `is_premium` tinyint(1) DEFAULT 0 COMMENT 'True, if this user is a Telegram Premium user', - `added_to_attachment_menu` tinyint(1) DEFAULT 0 COMMENT 'True, if this user added the bot to the attachment menu', - `created_at` timestamp NULL DEFAULT NULL COMMENT 'Entry date creation', - `updated_at` timestamp NULL DEFAULT NULL COMMENT 'Entry date update', - - PRIMARY KEY (`id`), - KEY `username` (`username`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; - -CREATE TABLE IF NOT EXISTS `chat` ( - `id` bigint COMMENT 'Unique identifier for this chat', - `type` ENUM('private', 'group', 'supergroup', 'channel') NOT NULL COMMENT 'Type of chat, can be either private, group, supergroup or channel', - `title` CHAR(255) DEFAULT '' COMMENT 'Title, for supergroups, channels and group chats', - `username` CHAR(255) DEFAULT NULL COMMENT 'Username, for private chats, supergroups and channels if available', - `first_name` CHAR(255) DEFAULT NULL COMMENT 'First name of the other party in a private chat', - `last_name` CHAR(255) DEFAULT NULL COMMENT 'Last name of the other party in a private chat', - `is_forum` TINYINT(1) DEFAULT 0 COMMENT 'True, if the supergroup chat is a forum (has topics enabled)', - `all_members_are_administrators` tinyint(1) DEFAULT 0 COMMENT 'True if a all members of this group are admins', - `created_at` timestamp NULL DEFAULT NULL COMMENT 'Entry date creation', - `updated_at` timestamp NULL DEFAULT NULL COMMENT 'Entry date update', - `old_id` bigint DEFAULT NULL COMMENT 'Unique chat identifier, this is filled when a group is converted to a supergroup', - - PRIMARY KEY (`id`), - KEY `old_id` (`old_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; - -CREATE TABLE IF NOT EXISTS `user_chat` ( - `user_id` bigint COMMENT 'Unique user identifier', - `chat_id` bigint COMMENT 'Unique user or chat identifier', - - PRIMARY KEY (`user_id`, `chat_id`), - - FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, - FOREIGN KEY (`chat_id`) REFERENCES `chat` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; - -CREATE TABLE IF NOT EXISTS `message_reaction` ( - `id` bigint UNSIGNED AUTO_INCREMENT COMMENT 'Unique identifier for this entry', - `chat_id` bigint COMMENT 'The chat containing the message the user reacted to', - `message_id` bigint COMMENT 'Unique identifier of the message inside the chat', - `user_id` bigint NULL COMMENT 'Optional. The user that changed the reaction, if the user isn''t anonymous', - `actor_chat_id` bigint NULL COMMENT 'Optional. The chat on behalf of which the reaction was changed, if the user is anonymous', - `old_reaction` TEXT NOT NULL COMMENT 'Previous list of reaction types that were set by the user', - `new_reaction` TEXT NOT NULL COMMENT 'New list of reaction types that have been set by the user', - `created_at` timestamp NULL DEFAULT NULL COMMENT 'Entry date creation', - - PRIMARY KEY (`id`), - KEY `chat_id` (`chat_id`), - KEY `user_id` (`user_id`), - KEY `actor_chat_id` (`actor_chat_id`), - - FOREIGN KEY (`chat_id`) REFERENCES `chat` (`id`), - FOREIGN KEY (`user_id`) REFERENCES `user` (`id`), - FOREIGN KEY (`actor_chat_id`) REFERENCES `chat` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; - -CREATE TABLE IF NOT EXISTS `message_reaction_count` ( - `id` bigint UNSIGNED AUTO_INCREMENT COMMENT 'Unique identifier for this entry', - `chat_id` bigint COMMENT 'The chat containing the message', - `message_id` bigint COMMENT 'Unique message identifier inside the chat', - `reactions` TEXT NOT NULL COMMENT 'List of reactions that are present on the message', - `created_at` timestamp NULL DEFAULT NULL COMMENT 'Entry date creation', - - PRIMARY KEY (`id`), - KEY `chat_id` (`chat_id`), - - FOREIGN KEY (`chat_id`) REFERENCES `chat` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; - -CREATE TABLE IF NOT EXISTS `inline_query` ( - `id` bigint UNSIGNED COMMENT 'Unique identifier for this query', - `user_id` bigint NULL COMMENT 'Unique user identifier', - `location` CHAR(255) NULL DEFAULT NULL COMMENT 'Location of the user', - `query` TEXT NOT NULL COMMENT 'Text of the query', - `offset` CHAR(255) NULL DEFAULT NULL COMMENT 'Offset of the result', - `chat_type` CHAR(255) NULL DEFAULT NULL COMMENT 'Optional. Type of the chat, from which the inline query was sent.', - `created_at` timestamp NULL DEFAULT NULL COMMENT 'Entry date creation', - - PRIMARY KEY (`id`), - KEY `user_id` (`user_id`), - - FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; - -CREATE TABLE IF NOT EXISTS `chosen_inline_result` ( - `id` bigint UNSIGNED AUTO_INCREMENT COMMENT 'Unique identifier for this entry', - `result_id` CHAR(255) NOT NULL DEFAULT '' COMMENT 'The unique identifier for the result that was chosen', - `user_id` bigint NULL COMMENT 'The user that chose the result', - `location` CHAR(255) NULL DEFAULT NULL COMMENT 'Sender location, only for bots that require user location', - `inline_message_id` CHAR(255) NULL DEFAULT NULL COMMENT 'Identifier of the sent inline message', - `query` TEXT NOT NULL COMMENT 'The query that was used to obtain the result', - `created_at` timestamp NULL DEFAULT NULL COMMENT 'Entry date creation', - - PRIMARY KEY (`id`), - KEY `user_id` (`user_id`), - - FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; - -CREATE TABLE IF NOT EXISTS `message` ( - `chat_id` bigint COMMENT 'Unique chat identifier', - `sender_chat_id` bigint COMMENT 'Sender of the message, sent on behalf of a chat', - `id` bigint UNSIGNED COMMENT 'Unique message identifier', - `message_thread_id` bigint(20) DEFAULT NULL COMMENT 'Unique identifier of a message thread to which the message belongs; for supergroups only', - `user_id` bigint NULL COMMENT 'Unique user identifier', - `date` timestamp NULL DEFAULT NULL COMMENT 'Date the message was sent in timestamp format', - `forward_from` bigint NULL DEFAULT NULL COMMENT 'Unique user identifier, sender of the original message', - `forward_from_chat` bigint NULL DEFAULT NULL COMMENT 'Unique chat identifier, chat the original message belongs to', - `forward_from_message_id` bigint NULL DEFAULT NULL COMMENT 'Unique chat identifier of the original message in the channel', - `forward_signature` TEXT NULL DEFAULT NULL COMMENT 'For messages forwarded from channels, signature of the post author if present', - `forward_sender_name` TEXT NULL DEFAULT NULL COMMENT 'Sender''s name for messages forwarded from users who disallow adding a link to their account in forwarded messages', - `forward_date` timestamp NULL DEFAULT NULL COMMENT 'date the original message was sent in timestamp format', - `is_topic_message` tinyint(1) DEFAULT 0 COMMENT 'True, if the message is sent to a forum topic', - `is_automatic_forward` tinyint(1) DEFAULT 0 COMMENT 'True, if the message is a channel post that was automatically forwarded to the connected discussion group', - `reply_to_chat` bigint NULL DEFAULT NULL COMMENT 'Unique chat identifier', - `reply_to_message` bigint UNSIGNED DEFAULT NULL COMMENT 'Message that this message is reply to', - `external_reply` TEXT NULL DEFAULT NULL COMMENT 'Optional. Information about the message that is being replied to, which may come from another chat or forum topic', - `via_bot` bigint NULL DEFAULT NULL COMMENT 'Optional. Bot through which the message was sent', - `link_preview_options` TEXT NULL DEFAULT NULL COMMENT 'Optional. Options used for link preview generation for the message, if it is a text message and link preview options were changed', - `edit_date` timestamp NULL DEFAULT NULL COMMENT 'Date the message was last edited in Unix time', - `has_protected_content` tinyint(1) DEFAULT 0 COMMENT 'True, if the message can''t be forwarded', - `media_group_id` TEXT COMMENT 'The unique identifier of a media message group this message belongs to', - `author_signature` TEXT COMMENT 'Signature of the post author for messages in channels', - `text` TEXT COMMENT 'For text messages, the actual UTF-8 text of the message max message length 4096 char utf8mb4', - `entities` TEXT COMMENT 'For text messages, special entities like usernames, URLs, bot commands, etc. that appear in the text', - `caption_entities` TEXT COMMENT 'For messages with a caption, special entities like usernames, URLs, bot commands, etc. that appear in the caption', - `audio` TEXT COMMENT 'Audio object. Message is an audio file, information about the file', - `document` TEXT COMMENT 'Document object. Message is a general file, information about the file', - `animation` TEXT COMMENT 'Message is an animation, information about the animation', - `game` TEXT COMMENT 'Game object. Message is a game, information about the game', - `photo` TEXT COMMENT 'Array of PhotoSize objects. Message is a photo, available sizes of the photo', - `sticker` TEXT COMMENT 'Sticker object. Message is a sticker, information about the sticker', - `story` TEXT COMMENT 'Story object. Message is a forwarded story', - `video` TEXT COMMENT 'Video object. Message is a video, information about the video', - `voice` TEXT COMMENT 'Voice Object. Message is a Voice, information about the Voice', - `video_note` TEXT COMMENT 'VoiceNote Object. Message is a Video Note, information about the Video Note', - `caption` TEXT COMMENT 'For message with caption, the actual UTF-8 text of the caption', - `has_media_spoiler` tinyint(1) DEFAULT 0 COMMENT 'True, if the message media is covered by a spoiler animation', - `contact` TEXT COMMENT 'Contact object. Message is a shared contact, information about the contact', - `location` TEXT COMMENT 'Location object. Message is a shared location, information about the location', - `venue` TEXT COMMENT 'Venue object. Message is a Venue, information about the Venue', - `poll` TEXT COMMENT 'Poll object. Message is a native poll, information about the poll', - `dice` TEXT COMMENT 'Message is a dice with random value from 1 to 6', - `new_chat_members` TEXT COMMENT 'List of unique user identifiers, new member(s) were added to the group, information about them (one of these members may be the bot itself)', - `left_chat_member` bigint NULL DEFAULT NULL COMMENT 'Unique user identifier, a member was removed from the group, information about them (this member may be the bot itself)', - `new_chat_title` CHAR(255) DEFAULT NULL COMMENT 'A chat title was changed to this value', - `new_chat_photo` TEXT COMMENT 'Array of PhotoSize objects. A chat photo was change to this value', - `delete_chat_photo` tinyint(1) DEFAULT 0 COMMENT 'Informs that the chat photo was deleted', - `group_chat_created` tinyint(1) DEFAULT 0 COMMENT 'Informs that the group has been created', - `supergroup_chat_created` tinyint(1) DEFAULT 0 COMMENT 'Informs that the supergroup has been created', - `channel_chat_created` tinyint(1) DEFAULT 0 COMMENT 'Informs that the channel chat has been created', - `message_auto_delete_timer_changed` TEXT COMMENT 'MessageAutoDeleteTimerChanged object. Message is a service message: auto-delete timer settings changed in the chat', - `migrate_to_chat_id` bigint NULL DEFAULT NULL COMMENT 'Migrate to chat identifier. The group has been migrated to a supergroup with the specified identifier', - `migrate_from_chat_id` bigint NULL DEFAULT NULL COMMENT 'Migrate from chat identifier. The supergroup has been migrated from a group with the specified identifier', - `pinned_message` TEXT NULL COMMENT 'Message object. Specified message was pinned', - `invoice` TEXT NULL COMMENT 'Message is an invoice for a payment, information about the invoice', - `successful_payment` TEXT NULL COMMENT 'Message is a service message about a successful payment, information about the payment', - `users_shared` TEXT NULL COMMENT 'Optional. Service message: users were shared with the bot', - `chat_shared` TEXT NULL COMMENT 'Optional. Service message: a chat was shared with the bot', - `connected_website` TEXT NULL COMMENT 'The domain name of the website on which the user has logged in.', - `write_access_allowed` TEXT DEFAULT NULL COMMENT 'Service message: the user allowed the bot added to the attachment menu to write messages', - `passport_data` TEXT NULL COMMENT 'Telegram Passport data', - `proximity_alert_triggered` TEXT NULL COMMENT 'Service message. A user in the chat triggered another user''s proximity alert while sharing Live Location.', - `forum_topic_created` TEXT DEFAULT NULL COMMENT 'Service message: forum topic created', - `forum_topic_edited` TEXT DEFAULT NULL COMMENT 'Service message: forum topic edited', - `forum_topic_closed` TEXT DEFAULT NULL COMMENT 'Service message: forum topic closed', - `forum_topic_reopened` TEXT DEFAULT NULL COMMENT 'Service message: forum topic reopened', - `general_forum_topic_hidden` TEXT DEFAULT NULL COMMENT 'Service message: the General forum topic hidden', - `general_forum_topic_unhidden` TEXT DEFAULT NULL COMMENT 'Service message: the General forum topic unhidden', - `video_chat_scheduled` TEXT COMMENT 'Service message: video chat scheduled', - `video_chat_started` TEXT COMMENT 'Service message: video chat started', - `video_chat_ended` TEXT COMMENT 'Service message: video chat ended', - `video_chat_participants_invited` TEXT COMMENT 'Service message: new participants invited to a video chat', - `web_app_data` TEXT COMMENT 'Service message: data sent by a Web App', - `reply_markup` TEXT NULL COMMENT 'Inline keyboard attached to the message', - - PRIMARY KEY (`chat_id`, `id`), - KEY `user_id` (`user_id`), - KEY `forward_from` (`forward_from`), - KEY `forward_from_chat` (`forward_from_chat`), - KEY `reply_to_chat` (`reply_to_chat`), - KEY `reply_to_message` (`reply_to_message`), - KEY `via_bot` (`via_bot`), - KEY `left_chat_member` (`left_chat_member`), - KEY `migrate_from_chat_id` (`migrate_from_chat_id`), - KEY `migrate_to_chat_id` (`migrate_to_chat_id`), - - FOREIGN KEY (`user_id`) REFERENCES `user` (`id`), - FOREIGN KEY (`chat_id`) REFERENCES `chat` (`id`), - FOREIGN KEY (`forward_from`) REFERENCES `user` (`id`), - FOREIGN KEY (`forward_from_chat`) REFERENCES `chat` (`id`), - FOREIGN KEY (`reply_to_chat`, `reply_to_message`) REFERENCES `message` (`chat_id`, `id`), - FOREIGN KEY (`via_bot`) REFERENCES `user` (`id`), - FOREIGN KEY (`left_chat_member`) REFERENCES `user` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; - -CREATE TABLE IF NOT EXISTS `edited_message` ( - `id` bigint UNSIGNED AUTO_INCREMENT COMMENT 'Unique identifier for this entry', - `chat_id` bigint COMMENT 'Unique chat identifier', - `message_id` bigint UNSIGNED COMMENT 'Unique message identifier', - `user_id` bigint NULL COMMENT 'Unique user identifier', - `edit_date` timestamp NULL DEFAULT NULL COMMENT 'Date the message was edited in timestamp format', - `text` TEXT COMMENT 'For text messages, the actual UTF-8 text of the message max message length 4096 char utf8', - `entities` TEXT COMMENT 'For text messages, special entities like usernames, URLs, bot commands, etc. that appear in the text', - `caption` TEXT COMMENT 'For message with caption, the actual UTF-8 text of the caption', - - PRIMARY KEY (`id`), - KEY `chat_id` (`chat_id`), - KEY `message_id` (`message_id`), - KEY `user_id` (`user_id`), - - FOREIGN KEY (`chat_id`) REFERENCES `chat` (`id`), - FOREIGN KEY (`chat_id`, `message_id`) REFERENCES `message` (`chat_id`, `id`), - FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; - -CREATE TABLE IF NOT EXISTS `callback_query` ( - `id` bigint UNSIGNED COMMENT 'Unique identifier for this query', - `user_id` bigint NULL COMMENT 'Unique user identifier', - `chat_id` bigint NULL COMMENT 'Unique chat identifier', - `message_id` bigint UNSIGNED COMMENT 'Unique message identifier', - `inline_message_id` CHAR(255) NULL DEFAULT NULL COMMENT 'Identifier of the message sent via the bot in inline mode, that originated the query', - `chat_instance` CHAR(255) NOT NULL DEFAULT '' COMMENT 'Global identifier, uniquely corresponding to the chat to which the message with the callback button was sent', - `data` CHAR(255) NOT NULL DEFAULT '' COMMENT 'Data associated with the callback button', - `game_short_name` CHAR(255) NOT NULL DEFAULT '' COMMENT 'Short name of a Game to be returned, serves as the unique identifier for the game', - `created_at` timestamp NULL DEFAULT NULL COMMENT 'Entry date creation', - - PRIMARY KEY (`id`), - KEY `user_id` (`user_id`), - KEY `chat_id` (`chat_id`), - KEY `message_id` (`message_id`), - - FOREIGN KEY (`user_id`) REFERENCES `user` (`id`), - FOREIGN KEY (`chat_id`, `message_id`) REFERENCES `message` (`chat_id`, `id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; - -CREATE TABLE IF NOT EXISTS `shipping_query` ( - `id` bigint UNSIGNED COMMENT 'Unique query identifier', - `user_id` bigint COMMENT 'User who sent the query', - `invoice_payload` CHAR(255) NOT NULL DEFAULT '' COMMENT 'Bot specified invoice payload', - `shipping_address` CHAR(255) NOT NULL DEFAULT '' COMMENT 'User specified shipping address', - `created_at` timestamp NULL DEFAULT NULL COMMENT 'Entry date creation', - - PRIMARY KEY (`id`), - KEY `user_id` (`user_id`), - - FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; - -CREATE TABLE IF NOT EXISTS `pre_checkout_query` ( - `id` bigint UNSIGNED COMMENT 'Unique query identifier', - `user_id` bigint COMMENT 'User who sent the query', - `currency` CHAR(3) COMMENT 'Three-letter ISO 4217 currency code', - `total_amount` bigint COMMENT 'Total price in the smallest units of the currency', - `invoice_payload` CHAR(255) NOT NULL DEFAULT '' COMMENT 'Bot specified invoice payload', - `shipping_option_id` CHAR(255) NULL COMMENT 'Identifier of the shipping option chosen by the user', - `order_info` TEXT NULL COMMENT 'Order info provided by the user', - `created_at` timestamp NULL DEFAULT NULL COMMENT 'Entry date creation', - - PRIMARY KEY (`id`), - KEY `user_id` (`user_id`), - - FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; - -CREATE TABLE IF NOT EXISTS `poll` ( - `id` bigint UNSIGNED COMMENT 'Unique poll identifier', - `question` text NOT NULL COMMENT 'Poll question', - `options` text NOT NULL COMMENT 'List of poll options', - `total_voter_count` int UNSIGNED COMMENT 'Total number of users that voted in the poll', - `is_closed` tinyint(1) DEFAULT 0 COMMENT 'True, if the poll is closed', - `is_anonymous` tinyint(1) DEFAULT 1 COMMENT 'True, if the poll is anonymous', - `type` char(255) COMMENT 'Poll type, currently can be “regular” or “quiz”', - `allows_multiple_answers` tinyint(1) DEFAULT 0 COMMENT 'True, if the poll allows multiple answers', - `correct_option_id` int UNSIGNED COMMENT '0-based identifier of the correct answer option. Available only for polls in the quiz mode, which are closed, or was sent (not forwarded) by the bot or to the private chat with the bot.', - `explanation` varchar(255) DEFAULT NULL COMMENT 'Text that is shown when a user chooses an incorrect answer or taps on the lamp icon in a quiz-style poll, 0-200 characters', - `explanation_entities` text DEFAULT NULL COMMENT 'Special entities like usernames, URLs, bot commands, etc. that appear in the explanation', - `open_period` int UNSIGNED DEFAULT NULL COMMENT 'Amount of time in seconds the poll will be active after creation', - `close_date` timestamp NULL DEFAULT NULL COMMENT 'Point in time (Unix timestamp) when the poll will be automatically closed', - `created_at` timestamp NULL DEFAULT NULL COMMENT 'Entry date creation', - - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; - -CREATE TABLE IF NOT EXISTS `poll_answer` ( - `poll_id` bigint UNSIGNED COMMENT 'Unique poll identifier', - `user_id` bigint NOT NULL COMMENT 'The user, who changed the answer to the poll', - `option_ids` text NOT NULL COMMENT '0-based identifiers of answer options, chosen by the user. May be empty if the user retracted their vote.', - `created_at` timestamp NULL DEFAULT NULL COMMENT 'Entry date creation', - - PRIMARY KEY (`poll_id`, `user_id`), - FOREIGN KEY (`poll_id`) REFERENCES `poll` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; - -CREATE TABLE IF NOT EXISTS `chat_member_updated` ( - `id` BIGINT UNSIGNED AUTO_INCREMENT COMMENT 'Unique identifier for this entry', - `chat_id` BIGINT NOT NULL COMMENT 'Chat the user belongs to', - `user_id` BIGINT NOT NULL COMMENT 'Performer of the action, which resulted in the change', - `date` TIMESTAMP NOT NULL COMMENT 'Date the change was done in Unix time', - `old_chat_member` TEXT NOT NULL COMMENT 'Previous information about the chat member', - `new_chat_member` TEXT NOT NULL COMMENT 'New information about the chat member', - `invite_link` TEXT NULL COMMENT 'Chat invite link, which was used by the user to join the chat; for joining by invite link events only', - `created_at` timestamp NULL DEFAULT NULL COMMENT 'Entry date creation', - - PRIMARY KEY (`id`), - FOREIGN KEY (`chat_id`) REFERENCES `chat` (`id`), - FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; - -CREATE TABLE IF NOT EXISTS `chat_join_request` ( - `id` BIGINT UNSIGNED AUTO_INCREMENT COMMENT 'Unique identifier for this entry', - `chat_id` BIGINT NOT NULL COMMENT 'Chat to which the request was sent', - `user_id` BIGINT NOT NULL COMMENT 'User that sent the join request', - `date` TIMESTAMP NOT NULL COMMENT 'Date the request was sent in Unix time', - `bio` TEXT NULL COMMENT 'Optional. Bio of the user', - `invite_link` TEXT NULL COMMENT 'Optional. Chat invite link that was used by the user to send the join request', - `created_at` timestamp NULL DEFAULT NULL COMMENT 'Entry date creation', - - PRIMARY KEY (`id`), - - FOREIGN KEY (`chat_id`) REFERENCES `chat` (`id`), - FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; - -CREATE TABLE IF NOT EXISTS `chat_boost_updated` ( - `id` bigint UNSIGNED AUTO_INCREMENT COMMENT 'Unique identifier for this entry', - `chat_id` bigint COMMENT 'Chat which was boosted', - `boost` TEXT NOT NULL COMMENT 'Information about the chat boost', - `created_at` timestamp NULL DEFAULT NULL COMMENT 'Entry date creation', - - PRIMARY KEY (`id`), - KEY `chat_id` (`chat_id`), - - FOREIGN KEY (`chat_id`) REFERENCES `chat` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; - -CREATE TABLE IF NOT EXISTS `chat_boost_removed` ( - `id` bigint UNSIGNED AUTO_INCREMENT COMMENT 'Unique identifier for this entry', - `chat_id` bigint COMMENT 'Chat which was boosted', - `boost_id` varchar(200) NOT NULL COMMENT 'Unique identifier of the boost', - `remove_date` timestamp NOT NULL COMMENT 'Point in time (Unix timestamp) when the boost was removed', - `source` TEXT NOT NULL COMMENT 'Source of the removed boost', - `created_at` timestamp NULL DEFAULT NULL COMMENT 'Entry date creation', - - PRIMARY KEY (`id`), - KEY `chat_id` (`chat_id`), - - FOREIGN KEY (`chat_id`) REFERENCES `chat` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; - -CREATE TABLE IF NOT EXISTS `telegram_update` ( - `id` bigint UNSIGNED COMMENT 'Update''s unique identifier', - `chat_id` bigint NULL DEFAULT NULL COMMENT 'Unique chat identifier', - `message_id` bigint UNSIGNED DEFAULT NULL COMMENT 'New incoming message of any kind - text, photo, sticker, etc.', - `edited_message_id` bigint UNSIGNED DEFAULT NULL COMMENT 'New version of a message that is known to the bot and was edited', - `channel_post_id` bigint UNSIGNED DEFAULT NULL COMMENT 'New incoming channel post of any kind - text, photo, sticker, etc.', - `edited_channel_post_id` bigint UNSIGNED DEFAULT NULL COMMENT 'New version of a channel post that is known to the bot and was edited', - `message_reaction_id` bigint UNSIGNED DEFAULT NULL COMMENT 'A reaction to a message was changed by a user', - `message_reaction_count_id` bigint UNSIGNED DEFAULT NULL COMMENT 'Reactions to a message with anonymous reactions were changed', - `inline_query_id` bigint UNSIGNED DEFAULT NULL COMMENT 'New incoming inline query', - `chosen_inline_result_id` bigint UNSIGNED DEFAULT NULL COMMENT 'The result of an inline query that was chosen by a user and sent to their chat partner', - `callback_query_id` bigint UNSIGNED DEFAULT NULL COMMENT 'New incoming callback query', - `shipping_query_id` bigint UNSIGNED DEFAULT NULL COMMENT 'New incoming shipping query. Only for invoices with flexible price', - `pre_checkout_query_id` bigint UNSIGNED DEFAULT NULL COMMENT 'New incoming pre-checkout query. Contains full information about checkout', - `poll_id` bigint UNSIGNED DEFAULT NULL COMMENT 'New poll state. Bots receive only updates about polls, which are sent or stopped by the bot', - `poll_answer_poll_id` bigint UNSIGNED DEFAULT NULL COMMENT 'A user changed their answer in a non-anonymous poll. Bots receive new votes only in polls that were sent by the bot itself.', - `my_chat_member_updated_id` BIGINT UNSIGNED NULL COMMENT 'The bot''s chat member status was updated in a chat. For private chats, this update is received only when the bot is blocked or unblocked by the user.', - `chat_member_updated_id` BIGINT UNSIGNED NULL COMMENT 'A chat member''s status was updated in a chat. The bot must be an administrator in the chat and must explicitly specify “chat_member” in the list of allowed_updates to receive these updates.', - `chat_join_request_id` BIGINT UNSIGNED NULL COMMENT 'A request to join the chat has been sent', - - PRIMARY KEY (`id`), - KEY `message_id` (`message_id`), - KEY `chat_message_id` (`chat_id`, `message_id`), - KEY `edited_message_id` (`edited_message_id`), - KEY `channel_post_id` (`channel_post_id`), - KEY `edited_channel_post_id` (`edited_channel_post_id`), - KEY `inline_query_id` (`inline_query_id`), - KEY `chosen_inline_result_id` (`chosen_inline_result_id`), - KEY `callback_query_id` (`callback_query_id`), - KEY `shipping_query_id` (`shipping_query_id`), - KEY `pre_checkout_query_id` (`pre_checkout_query_id`), - KEY `poll_id` (`poll_id`), - KEY `poll_answer_poll_id` (`poll_answer_poll_id`), - KEY `my_chat_member_updated_id` (`my_chat_member_updated_id`), - KEY `chat_member_updated_id` (`chat_member_updated_id`), - KEY `chat_join_request_id` (`chat_join_request_id`), - - FOREIGN KEY (`chat_id`, `message_id`) REFERENCES `message` (`chat_id`, `id`), - FOREIGN KEY (`edited_message_id`) REFERENCES `edited_message` (`id`), - FOREIGN KEY (`chat_id`, `channel_post_id`) REFERENCES `message` (`chat_id`, `id`), - FOREIGN KEY (`edited_channel_post_id`) REFERENCES `edited_message` (`id`), - FOREIGN KEY (`inline_query_id`) REFERENCES `inline_query` (`id`), - FOREIGN KEY (`chosen_inline_result_id`) REFERENCES `chosen_inline_result` (`id`), - FOREIGN KEY (`callback_query_id`) REFERENCES `callback_query` (`id`), - FOREIGN KEY (`shipping_query_id`) REFERENCES `shipping_query` (`id`), - FOREIGN KEY (`pre_checkout_query_id`) REFERENCES `pre_checkout_query` (`id`), - FOREIGN KEY (`poll_id`) REFERENCES `poll` (`id`), - FOREIGN KEY (`poll_answer_poll_id`) REFERENCES `poll_answer` (`poll_id`), - FOREIGN KEY (`my_chat_member_updated_id`) REFERENCES `chat_member_updated` (`id`), - FOREIGN KEY (`chat_member_updated_id`) REFERENCES `chat_member_updated` (`id`), - FOREIGN KEY (`chat_join_request_id`) REFERENCES `chat_join_request` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; - -CREATE TABLE IF NOT EXISTS `conversation` ( - `id` bigint(20) unsigned AUTO_INCREMENT COMMENT 'Unique identifier for this entry', - `user_id` bigint NULL DEFAULT NULL COMMENT 'Unique user identifier', - `chat_id` bigint NULL DEFAULT NULL COMMENT 'Unique user or chat identifier', - `status` ENUM('active', 'cancelled', 'stopped') NOT NULL DEFAULT 'active' COMMENT 'Conversation state', - `command` varchar(160) DEFAULT '' COMMENT 'Default command to execute', - `notes` text DEFAULT NULL COMMENT 'Data stored from command', - `created_at` timestamp NULL DEFAULT NULL COMMENT 'Entry date creation', - `updated_at` timestamp NULL DEFAULT NULL COMMENT 'Entry date update', - - PRIMARY KEY (`id`), - KEY `user_id` (`user_id`), - KEY `chat_id` (`chat_id`), - KEY `status` (`status`), - - FOREIGN KEY (`user_id`) REFERENCES `user` (`id`), - FOREIGN KEY (`chat_id`) REFERENCES `chat` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; - -CREATE TABLE IF NOT EXISTS `request_limiter` ( - `id` bigint UNSIGNED AUTO_INCREMENT COMMENT 'Unique identifier for this entry', - `chat_id` char(255) NULL DEFAULT NULL COMMENT 'Unique chat identifier', - `inline_message_id` char(255) NULL DEFAULT NULL COMMENT 'Identifier of the sent inline message', - `method` char(255) DEFAULT NULL COMMENT 'Request method', - `created_at` timestamp NULL DEFAULT NULL COMMENT 'Entry date creation', - - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; diff --git a/tests/Unit/Commands/CommandTest.php b/tests/Unit/Commands/CommandTest.php deleted file mode 100644 index 35be1d797..000000000 --- a/tests/Unit/Commands/CommandTest.php +++ /dev/null @@ -1,171 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Tests\Unit\Commands; - -use Longman\TelegramBot\Commands\Command; -use Longman\TelegramBot\Telegram; -use Longman\TelegramBot\Tests\Unit\TestCase; -use Longman\TelegramBot\Tests\Unit\TestHelpers; - -/** - * @link https://github.com/php-telegram-bot/core - * @author Avtandil Kikabidze - * @copyright Avtandil Kikabidze - * @license http://opensource.org/licenses/mit-license.php The MIT License (MIT) - * @package TelegramTest - */ -class CommandTest extends TestCase -{ - /** - * @var string - */ - private $command_namespace = Command::class; - - /** - * @var Telegram - */ - private $telegram; - - /** - * @var Command - */ - private $command_stub; - - /** - * @var Telegram - */ - private $telegram_with_config; - - /** - * @var Command - */ - private $command_stub_with_config; - - public function setUp(): void - { - //Default command object - $this->telegram = new Telegram(self::$dummy_api_key, 'testbot'); - $this->command_stub = $this->getMockForAbstractClass($this->command_namespace, [$this->telegram]); - - //Create separate command object that contain a command config - $this->telegram_with_config = new Telegram(self::$dummy_api_key, 'testbot'); - $this->telegram_with_config->setCommandConfig('command_name', ['config_key' => 'config_value']); - $this->command_stub_with_config = $this->getMockBuilder($this->command_namespace) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - //Set a name for the object property so that the constructor can set the config correctly - TestHelpers::setObjectProperty($this->command_stub_with_config, 'name', 'command_name'); - $this->command_stub_with_config->__construct($this->telegram_with_config); - } - - // Test idea from here: http://stackoverflow.com/a/4371606 - public function testCommandConstructorNeedsTelegramObject(): void - { - $exception_count = 0; - $params_to_test = [ - [], - [null], - [12345], - ['something'], - [new \stdClass()], - [$this->telegram], // only this one is valid - ]; - - foreach ($params_to_test as $param) { - try { - $this->getMockForAbstractClass($this->command_namespace, $param); - } catch (\Throwable $e) { - $exception_count++; - } - } - - self::assertEquals(5, $exception_count); - } - - public function testCommandHasCorrectTelegramObject(): void - { - self::assertSame($this->telegram, $this->command_stub->getTelegram()); - } - - public function testDefaultCommandName(): void - { - self::assertEmpty($this->command_stub->getName()); - } - - public function testDefaultCommandDescription(): void - { - self::assertEquals('Command description', $this->command_stub->getDescription()); - } - - public function testDefaultCommandUsage(): void - { - self::assertEquals('Command usage', $this->command_stub->getUsage()); - } - - public function testDefaultCommandVersion(): void - { - self::assertEquals('1.0.0', $this->command_stub->getVersion()); - } - - public function testDefaultCommandIsEnabled(): void - { - self::assertTrue($this->command_stub->isEnabled()); - } - - public function testDefaultCommandShownInHelp(): void - { - self::assertTrue($this->command_stub->showInHelp()); - } - - public function testDefaultCommandNeedsMysql(): void - { - self::markTestSkipped('Think about better test'); - } - - public function testDefaultCommandEmptyConfig(): void - { - self::assertSame([], $this->command_stub->getConfig()); - } - - public function testDefaultCommandUpdateNull(): void - { - self::assertNull($this->command_stub->getUpdate()); - } - - public function testCommandSetUpdateAndMessage(): void - { - $stub = $this->command_stub; - - self::assertEquals(null, $stub->getUpdate()); - self::assertEquals(null, $stub->getMessage()); - - $update = TestHelpers::getFakeUpdateObject(); - $message = $update->getMessage(); - $stub->setUpdate($update); - self::assertEquals($update, $stub->getUpdate()); - self::assertEquals($message, $stub->getMessage()); - } - - public function testCommandWithConfigNotEmptyConfig(): void - { - self::assertNotEmpty($this->command_stub_with_config->getConfig()); - } - - public function testCommandWithConfigCorrectConfig(): void - { - self::assertEquals(['config_key' => 'config_value'], $this->command_stub_with_config->getConfig()); - self::assertEquals(['config_key' => 'config_value'], $this->command_stub_with_config->getConfig(null)); - self::assertEquals(['config_key' => 'config_value'], $this->command_stub_with_config->getConfig()); - self::assertEquals('config_value', $this->command_stub_with_config->getConfig('config_key')); - self::assertEquals(null, $this->command_stub_with_config->getConfig('not_config_key')); - } -} diff --git a/tests/Unit/Commands/CommandTestCase.php b/tests/Unit/Commands/CommandTestCase.php deleted file mode 100644 index 782347d31..000000000 --- a/tests/Unit/Commands/CommandTestCase.php +++ /dev/null @@ -1,56 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Tests\Unit\Commands; - -use Longman\TelegramBot\Commands\Command; -use Longman\TelegramBot\Telegram; -use Longman\TelegramBot\Tests\Unit\TestCase; - -/** - * @package TelegramTest - * @author Avtandil Kikabidze - * @copyright Avtandil Kikabidze - * @license http://opensource.org/licenses/mit-license.php The MIT License (MIT) - * @link https://github.com/php-telegram-bot/core - */ -class CommandTestCase extends TestCase -{ - /** - * @var Telegram - */ - protected $telegram; - - /** - * @var Command - */ - protected $command; - - /** - * setUp - */ - public function setUp(): void - { - $this->telegram = new Telegram(self::$dummy_api_key, 'testbot'); - - // Add custom commands dedicated to do some tests. - $this->telegram->addCommandsPath(__DIR__ . '/CustomTestCommands'); - $this->telegram->getCommandsList(); - } - - /** - * Make sure the version number is in the format x.x.x, x.x or x - */ - public function testVersionNumberFormat(): void - { - self::assertRegExp('/^(\d+\\.)?(\d+\\.)?(\d+)$/', $this->command->getVersion()); - } -} diff --git a/tests/Unit/Commands/CustomTestCommands/DummyAdminCommand.php b/tests/Unit/Commands/CustomTestCommands/DummyAdminCommand.php deleted file mode 100644 index b81114d2a..000000000 --- a/tests/Unit/Commands/CustomTestCommands/DummyAdminCommand.php +++ /dev/null @@ -1,47 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Dummy\AdminCommands; - -use Longman\TelegramBot\Commands\AdminCommand; -use Longman\TelegramBot\Entities\ServerResponse; -use Longman\TelegramBot\Request; - -/** - * Test "/dummy_admin" command - */ -class DummyAdminCommand extends AdminCommand -{ - /** - * @var string - */ - protected $name = 'dummy_admin'; - - /** - * @var string - */ - protected $description = 'Dummy AdminCommand'; - - /** - * @var string - */ - protected $usage = '/dummy_admin'; - - /** - * Command execute method - * - * @return mixed - */ - public function execute(): ServerResponse - { - return Request::emptyResponse(); - } -} diff --git a/tests/Unit/Commands/CustomTestCommands/DummySystemCommand.php b/tests/Unit/Commands/CustomTestCommands/DummySystemCommand.php deleted file mode 100644 index d9ed58597..000000000 --- a/tests/Unit/Commands/CustomTestCommands/DummySystemCommand.php +++ /dev/null @@ -1,47 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Dummy\SystemCommands; - -use Longman\TelegramBot\Commands\SystemCommand; -use Longman\TelegramBot\Entities\ServerResponse; -use Longman\TelegramBot\Request; - -/** - * Test "/dummy_system" command - */ -class DummySystemCommand extends SystemCommand -{ - /** - * @var string - */ - protected $name = 'dummy_system'; - - /** - * @var string - */ - protected $description = 'Dummy SystemCommand'; - - /** - * @var string - */ - protected $usage = '/dummy_system'; - - /** - * Command execute method - * - * @return mixed - */ - public function execute(): ServerResponse - { - return Request::emptyResponse(); - } -} diff --git a/tests/Unit/Commands/CustomTestCommands/DummyUserCommand.php b/tests/Unit/Commands/CustomTestCommands/DummyUserCommand.php deleted file mode 100644 index 7a20b1ba8..000000000 --- a/tests/Unit/Commands/CustomTestCommands/DummyUserCommand.php +++ /dev/null @@ -1,47 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Dummy\UserCommands; - -use Longman\TelegramBot\Commands\UserCommand; -use Longman\TelegramBot\Entities\ServerResponse; -use Longman\TelegramBot\Request; - -/** - * Test "/dummy_user" command - */ -class DummyUserCommand extends UserCommand -{ - /** - * @var string - */ - protected $name = 'dummy_user'; - - /** - * @var string - */ - protected $description = 'Dummy UserCommand'; - - /** - * @var string - */ - protected $usage = '/dummy_user'; - - /** - * Command execute method - * - * @return mixed - */ - public function execute(): ServerResponse - { - return Request::emptyResponse(); - } -} diff --git a/tests/Unit/Commands/CustomTestCommands/HiddenCommand.php b/tests/Unit/Commands/CustomTestCommands/HiddenCommand.php deleted file mode 100644 index 46f11dfc8..000000000 --- a/tests/Unit/Commands/CustomTestCommands/HiddenCommand.php +++ /dev/null @@ -1,57 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Commands\UserCommands; - -use Longman\TelegramBot\Commands\UserCommand; -use Longman\TelegramBot\Entities\ServerResponse; -use Longman\TelegramBot\Request; - -/** - * Test "/hidden" command to test $show_in_help - */ -class HiddenCommand extends UserCommand -{ - /** - * @var string - */ - protected $name = 'hidden'; - - /** - * @var string - */ - protected $description = 'This command is hidden in help'; - - /** - * @var string - */ - protected $usage = '/hidden'; - - /** - * @var string - */ - protected $version = '1.0.0'; - - /** - * @var bool - */ - protected $show_in_help = false; - - /** - * Command execute method - * - * @return mixed - */ - public function execute(): ServerResponse - { - return Request::emptyResponse(); - } -} diff --git a/tests/Unit/Commands/CustomTestCommands/VisibleCommand.php b/tests/Unit/Commands/CustomTestCommands/VisibleCommand.php deleted file mode 100644 index 3900c616d..000000000 --- a/tests/Unit/Commands/CustomTestCommands/VisibleCommand.php +++ /dev/null @@ -1,57 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Commands\UserCommands; - -use Longman\TelegramBot\Commands\UserCommand; -use Longman\TelegramBot\Entities\ServerResponse; -use Longman\TelegramBot\Request; - -/** - * Test "/visible" command to test $show_in_help - */ -class VisibleCommand extends UserCommand -{ - /** - * @var string - */ - protected $name = 'visible'; - - /** - * @var string - */ - protected $description = 'This command is visible in help'; - - /** - * @var string - */ - protected $usage = '/visible'; - - /** - * @var string - */ - protected $version = '1.0.0'; - - /** - * @var bool - */ - protected $show_in_help = true; - - /** - * Command execute method - * - * @return mixed - */ - public function execute(): ServerResponse - { - return Request::emptyResponse(); - } -} diff --git a/tests/Unit/ConversationTest.php b/tests/Unit/ConversationTest.php deleted file mode 100644 index f5205ffae..000000000 --- a/tests/Unit/ConversationTest.php +++ /dev/null @@ -1,126 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Tests\Unit; - -use Longman\TelegramBot\Conversation; -use Longman\TelegramBot\Exception\TelegramException; -use Longman\TelegramBot\Telegram; - -/** - * @link https://github.com/php-telegram-bot/core - * @author Avtandil Kikabidze - * @copyright Avtandil Kikabidze - * @license http://opensource.org/licenses/mit-license.php The MIT License (MIT) - * @package TelegramTest - */ -class ConversationTest extends TestCase -{ - - protected function setUp(): void - { - $credentials = [ - 'host' => PHPUNIT_DB_HOST, - 'port' => PHPUNIT_DB_PORT, - 'database' => PHPUNIT_DB_NAME, - 'user' => PHPUNIT_DB_USER, - 'password' => PHPUNIT_DB_PASS, - ]; - - $telegram = new Telegram(self::$dummy_api_key, 'testbot'); - $telegram->enableMySql($credentials); - - //Make sure we start with an empty DB for each test. - TestHelpers::emptyDb($credentials); - } - - public function testConversationThatDoesntExistPropertiesSetCorrectly(): void - { - $conversation = new Conversation(123, 456); - self::assertSame(123, $conversation->getUserId()); - self::assertSame(456, $conversation->getChatId()); - self::assertEmpty($conversation->getCommand()); - } - - public function testConversationThatExistsPropertiesSetCorrectly(): void - { - $info = TestHelpers::startFakeConversation(); - $conversation = new Conversation($info['user_id'], $info['chat_id'], 'command'); - self::assertSame($info['user_id'], $conversation->getUserId()); - self::assertSame($info['chat_id'], $conversation->getChatId()); - self::assertSame('command', $conversation->getCommand()); - } - - public function testConversationThatDoesntExistWithoutCommand(): void - { - $conversation = new Conversation(1, 1); - self::assertFalse($conversation->exists()); - self::assertEmpty($conversation->getCommand()); - } - - public function testConversationThatDoesntExistWithCommand(): void - { - $this->expectException(TelegramException::class); - new Conversation(1, 1, 'command'); - } - - public function testNewConversationThatWontExistWithoutCommand(): void - { - TestHelpers::startFakeConversation(); - $conversation = new Conversation(0, 0); - self::assertFalse($conversation->exists()); - self::assertEmpty($conversation->getCommand()); - } - - public function testNewConversationThatWillExistWithCommand(): void - { - $info = TestHelpers::startFakeConversation(); - $conversation = new Conversation($info['user_id'], $info['chat_id'], 'command'); - self::assertTrue($conversation->exists()); - self::assertEquals('command', $conversation->getCommand()); - } - - public function testStopConversation(): void - { - $info = TestHelpers::startFakeConversation(); - $conversation = new Conversation($info['user_id'], $info['chat_id'], 'command'); - self::assertTrue($conversation->exists()); - $conversation->stop(); - - $conversation2 = new Conversation($info['user_id'], $info['chat_id']); - self::assertFalse($conversation2->exists()); - } - - public function testCancelConversation(): void - { - $info = TestHelpers::startFakeConversation(); - $conversation = new Conversation($info['user_id'], $info['chat_id'], 'command'); - self::assertTrue($conversation->exists()); - $conversation->cancel(); - - $conversation2 = new Conversation($info['user_id'], $info['chat_id']); - self::assertFalse($conversation2->exists()); - } - - public function testUpdateConversationNotes(): void - { - $info = TestHelpers::startFakeConversation(); - $conversation = new Conversation($info['user_id'], $info['chat_id'], 'command'); - $conversation->notes = 'newnote'; - $conversation->update(); - - $conversation2 = new Conversation($info['user_id'], $info['chat_id'], 'command'); - self::assertSame('newnote', $conversation2->notes); - - $conversation3 = new Conversation($info['user_id'], $info['chat_id']); - self::assertSame('newnote', $conversation3->notes); - } -} diff --git a/tests/Unit/Entities/AudioTest.php b/tests/Unit/Entities/AudioTest.php deleted file mode 100644 index ce9eb64b7..000000000 --- a/tests/Unit/Entities/AudioTest.php +++ /dev/null @@ -1,53 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Tests\Unit\Entities; - -use Longman\TelegramBot\Entities\Audio; -use Longman\TelegramBot\Tests\Unit\TestCase; -use Longman\TelegramBot\Tests\Unit\TestHelpers; - -/** - * @link https://github.com/php-telegram-bot/core - * @author Baev Nikolay - * @copyright Avtandil Kikabidze - * @license http://opensource.org/licenses/mit-license.php The MIT License (MIT) - * @package TelegramTest - */ -class AudioTest extends TestCase -{ - /** - * @var array - */ - private $record; - - public function setUp(): void - { - $this->record = TestHelpers::getFakeRecordedAudio(); - } - - public function testInstance(): void - { - $audio = new Audio($this->record); - self::assertInstanceOf(Audio::class, $audio); - } - - public function testGetProperties(): void - { - $audio = new Audio($this->record); - self::assertEquals($this->record['file_id'], $audio->getFileId()); - self::assertEquals($this->record['duration'], $audio->getDuration()); - self::assertEquals($this->record['performer'], $audio->getPerformer()); - self::assertEquals($this->record['title'], $audio->getTitle()); - self::assertEquals($this->record['mime_type'], $audio->getMimeType()); - self::assertEquals($this->record['file_size'], $audio->getFileSize()); - } -} diff --git a/tests/Unit/Entities/ChatTest.php b/tests/Unit/Entities/ChatTest.php deleted file mode 100644 index 0f2c03aa6..000000000 --- a/tests/Unit/Entities/ChatTest.php +++ /dev/null @@ -1,74 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Tests\Unit\Entities; - -use Longman\TelegramBot\Tests\Unit\TestCase; -use Longman\TelegramBot\Tests\Unit\TestHelpers; - -/** - * @link https://github.com/php-telegram-bot/core - * @author Avtandil Kikabidze - * @copyright Avtandil Kikabidze - * @license http://opensource.org/licenses/mit-license.php The MIT License (MIT) - * @package TelegramTest - */ -class ChatTest extends TestCase -{ - public function testChatType(): void - { - $chat = TestHelpers::getFakeChatObject(); - self::assertEquals('private', $chat->getType()); - - $chat = TestHelpers::getFakeChatObject(['id' => -123, 'type' => null]); - self::assertEquals('group', $chat->getType()); - - $chat = TestHelpers::getFakeChatObject(['id' => -123, 'type' => 'supergroup']); - self::assertEquals('supergroup', $chat->getType()); - - $chat = TestHelpers::getFakeChatObject(['id' => -123, 'type' => 'channel']); - self::assertEquals('channel', $chat->getType()); - } - - public function testIsChatType(): void - { - $chat = TestHelpers::getFakeChatObject(); - self::assertTrue($chat->isPrivateChat()); - - $chat = TestHelpers::getFakeChatObject(['id' => -123, 'type' => null]); - self::assertTrue($chat->isGroupChat()); - - $chat = TestHelpers::getFakeChatObject(['id' => -123, 'type' => 'supergroup']); - self::assertTrue($chat->isSuperGroup()); - - $chat = TestHelpers::getFakeChatObject(['id' => -123, 'type' => 'channel']); - self::assertTrue($chat->isChannel()); - } - - public function testTryMention(): void - { - // Username. - $chat = TestHelpers::getFakeChatObject(['id' => 1, 'first_name' => 'John', 'last_name' => 'Taylor', 'username' => 'jtaylor']); - self::assertEquals('@jtaylor', $chat->tryMention()); - - // First name. - $chat = TestHelpers::getFakeChatObject(['id' => 1, 'first_name' => 'John', 'last_name' => null, 'username' => null]); - self::assertEquals('John', $chat->tryMention()); - - // First and Last name. - $chat = TestHelpers::getFakeChatObject(['id' => 1, 'first_name' => 'John', 'last_name' => 'Taylor', 'username' => null]); - self::assertEquals('John Taylor', $chat->tryMention()); - - // Non-private chat should return title. - $chat = TestHelpers::getFakeChatObject(['id' => -123, 'type' => null, 'title' => 'My group chat']); - self::assertSame('My group chat', $chat->tryMention()); - } -} diff --git a/tests/Unit/Entities/EntityTest.php b/tests/Unit/Entities/EntityTest.php deleted file mode 100644 index 47299c878..000000000 --- a/tests/Unit/Entities/EntityTest.php +++ /dev/null @@ -1,54 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Tests\Unit\Entities; - -use Longman\TelegramBot\Entities\Entity; -use Longman\TelegramBot\Tests\Unit\TestCase; - -/** - * @link https://github.com/php-telegram-bot/core - * @author Baev Nikolay - * @copyright Avtandil Kikabidze - * @license http://opensource.org/licenses/mit-license.php The MIT License (MIT) - * @package TelegramTest - */ -class EntityTest extends TestCase -{ - public function testEscapeMarkdown(): void - { - // Make sure all characters that need escaping are escaped. - - // Markdown V1 - self::assertEquals('\[\`\*\_', Entity::escapeMarkdown('[`*_')); - self::assertEquals('\*mark\*\_down\_~test~', Entity::escapeMarkdown('*mark*_down_~test~')); - - // Markdown V2 - self::assertEquals('\_\*\[\]\(\)\~\`\>\#\+\-\=\|\{\}\.\!', Entity::escapeMarkdownV2('_*[]()~`>#+-=|{}.!')); - self::assertEquals('\*mark\*\_down\_\~test\~', Entity::escapeMarkdownV2('*mark*_down_~test~')); - } - - public function testSettingDynamicParameterWorks(): void - { - $entity = new class ( [] ) extends Entity { }; // phpcs:ignore - - $entity->newParameter = 'test'; - - $this->assertEquals('test', $entity->newParameter); - } - - public function testGettingUnknownDynamicParameterReturnsNull(): void - { - $entity = new class ( [] ) extends Entity { }; // phpcs:ignore - - $this->assertNull($entity->unknownParameter); - } -} diff --git a/tests/Unit/Entities/FileTest.php b/tests/Unit/Entities/FileTest.php deleted file mode 100644 index 822f4fac5..000000000 --- a/tests/Unit/Entities/FileTest.php +++ /dev/null @@ -1,84 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Tests\Unit\Entities; - -use Longman\TelegramBot\Entities\File; -use Longman\TelegramBot\Tests\Unit\TestCase; - -/** - * @link https://github.com/php-telegram-bot/core - * @author Baev Nikolay - * @copyright Avtandil Kikabidze - * @license http://opensource.org/licenses/mit-license.php The MIT License (MIT) - * @package TelegramTest - */ -class FileTest extends TestCase -{ - /** - * @var array - */ - private $data; - - public function setUp(): void - { - $this->data = [ - 'file_id' => (int) mt_rand(1, 99), - 'file_size' => (int) mt_rand(100, 99999), - 'file_path' => 'home' . DIRECTORY_SEPARATOR . 'phpunit', - ]; - } - - public function testBaseStageLocation(): void - { - $file = new File($this->data); - self::assertInstanceOf(File::class, $file); - } - - public function testGetFileId(): void - { - $file = new File($this->data); - $id = $file->getFileId(); - self::assertIsInt($id); - self::assertEquals($this->data['file_id'], $id); - } - - public function testGetFileSize(): void - { - $file = new File($this->data); - $size = $file->getFileSize(); - self::assertIsInt($size); - self::assertEquals($this->data['file_size'], $size); - } - - public function testGetFilePath(): void - { - $file = new File($this->data); - $path = $file->getFilePath(); - self::assertEquals($this->data['file_path'], $path); - } - - public function testGetFileSizeWithoutData(): void - { - unset($this->data['file_size']); - $file = new File($this->data); - $id = $file->getFileSize(); - self::assertNull($id); - } - - public function testGetFilePathWithoutData(): void - { - unset($this->data['file_path']); - $file = new File($this->data); - $path = $file->getFilePath(); - self::assertNull($path); - } -} diff --git a/tests/Unit/Entities/InlineKeyboardButtonTest.php b/tests/Unit/Entities/InlineKeyboardButtonTest.php deleted file mode 100644 index 384430aa5..000000000 --- a/tests/Unit/Entities/InlineKeyboardButtonTest.php +++ /dev/null @@ -1,95 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Tests\Unit\Entities; - -use Longman\TelegramBot\Entities\Games\CallbackGame; -use Longman\TelegramBot\Entities\InlineKeyboardButton; -use Longman\TelegramBot\Entities\SwitchInlineQueryChosenChat; -use Longman\TelegramBot\Exception\TelegramException; -use Longman\TelegramBot\Tests\Unit\TestCase; - -/** - * @link https://github.com/php-telegram-bot/core - * @author Avtandil Kikabidze - * @copyright Avtandil Kikabidze - * @license http://opensource.org/licenses/mit-license.php The MIT License (MIT) - * @package TelegramTest - */ -class InlineKeyboardButtonTest extends TestCase -{ - public function testInlineKeyboardButtonSuccess(): void - { - new InlineKeyboardButton(['text' => 'message', 'url' => 'url_value']); - new InlineKeyboardButton(['text' => 'message', 'callback_data' => 'callback_data_value']); - new InlineKeyboardButton(['text' => 'message', 'switch_inline_query' => 'switch_inline_query_value']); - new InlineKeyboardButton(['text' => 'message', 'switch_inline_query' => '']); // Allow empty string. - new InlineKeyboardButton(['text' => 'message', 'switch_inline_query_current_chat' => 'switch_inline_query_current_chat_value']); - new InlineKeyboardButton(['text' => 'message', 'switch_inline_query_current_chat' => '']); // Allow empty string. - new InlineKeyboardButton(['text' => 'message', 'switch_inline_query_chosen_chat' => new SwitchInlineQueryChosenChat([])]); // Allow empty string. - new InlineKeyboardButton(['text' => 'message', 'callback_game' => new CallbackGame([])]); - new InlineKeyboardButton(['text' => 'message', 'pay' => true]); - self::assertTrue(true); - } - - public function testInlineKeyboardButtonCouldBe(): void - { - self::assertTrue(InlineKeyboardButton::couldBe( - ['text' => 'message', 'url' => 'url_value'] - )); - self::assertTrue(InlineKeyboardButton::couldBe( - ['text' => 'message', 'callback_data' => 'callback_data_value'] - )); - self::assertTrue(InlineKeyboardButton::couldBe( - ['text' => 'message', 'switch_inline_query' => 'switch_inline_query_value'] - )); - self::assertTrue(InlineKeyboardButton::couldBe( - ['text' => 'message', 'switch_inline_query_current_chat' => 'switch_inline_query_current_chat_value'] - )); - self::assertTrue(InlineKeyboardButton::couldBe( - ['text' => 'message', 'switch_inline_query_chosen_chat' => new SwitchInlineQueryChosenChat([])] - )); - self::assertTrue(InlineKeyboardButton::couldBe( - ['text' => 'message', 'callback_game' => new CallbackGame([])] - )); - self::assertTrue(InlineKeyboardButton::couldBe( - ['text' => 'message', 'pay' => true] - )); - - self::assertFalse(InlineKeyboardButton::couldBe(['no_text' => 'message'])); - self::assertFalse(InlineKeyboardButton::couldBe(['text' => 'message'])); - self::assertFalse(InlineKeyboardButton::couldBe(['url' => 'url_value'])); - self::assertFalse(InlineKeyboardButton::couldBe([ - 'callback_data' => 'callback_data_value' - ])); - self::assertFalse(InlineKeyboardButton::couldBe([ - 'switch_inline_query' => 'switch_inline_query_value' - ])); - self::assertFalse(InlineKeyboardButton::couldBe([ - 'switch_inline_query_current_chat' => 'switch_inline_query_current_chat_value' - ])); - self::assertFalse(InlineKeyboardButton::couldBe([ - 'switch_inline_query_chosen_chat' => new SwitchInlineQueryChosenChat([]) - ])); - self::assertFalse(InlineKeyboardButton::couldBe(['callback_game' => new CallbackGame([])])); - self::assertFalse(InlineKeyboardButton::couldBe(['pay' => true])); - - self::assertFalse(InlineKeyboardButton::couldBe([ - 'url' => 'url_value', - 'callback_data' => 'callback_data_value', - 'switch_inline_query' => 'switch_inline_query_value', - 'switch_inline_query_current_chat' => 'switch_inline_query_current_chat_value', - 'switch_inline_query_chosen_chat' => new SwitchInlineQueryChosenChat([]), - 'callback_game' => new CallbackGame([]), - 'pay' => true, - ])); - } -} diff --git a/tests/Unit/Entities/InlineKeyboardTest.php b/tests/Unit/Entities/InlineKeyboardTest.php deleted file mode 100644 index 9e30ced4e..000000000 --- a/tests/Unit/Entities/InlineKeyboardTest.php +++ /dev/null @@ -1,137 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Tests\Unit\Entities; - -use Longman\TelegramBot\Entities\InlineKeyboard; -use Longman\TelegramBot\Entities\InlineKeyboardButton; -use Longman\TelegramBot\Exception\TelegramException; -use Longman\TelegramBot\Tests\Unit\TestCase; - -/** - * @link https://github.com/php-telegram-bot/core - * @author Avtandil Kikabidze - * @copyright Avtandil Kikabidze - * @license http://opensource.org/licenses/mit-license.php The MIT License (MIT) - * @package TelegramTest - */ -class InlineKeyboardTest extends TestCase -{ - private function getRandomButton($text): InlineKeyboardButton - { - $random_params = ['url', 'callback_data', 'switch_inline_query', 'switch_inline_query_current_chat', 'pay']; - $param = $random_params[array_rand($random_params, 1)]; - $data = [ - 'text' => $text, - $param => 'random_param', - ]; - - return new InlineKeyboardButton($data); - } - - public function testInlineKeyboardDataMalformedField(): void - { - $this->expectException(TelegramException::class); - $this->expectExceptionMessage('inline_keyboard field is not an array!'); - new InlineKeyboard(['inline_keyboard' => 'wrong']); - } - - public function testInlineKeyboardDataMalformedSubfield(): void - { - $this->expectException(TelegramException::class); - $this->expectExceptionMessage('inline_keyboard subfield is not an array!'); - new InlineKeyboard(['inline_keyboard' => ['wrong']]); - } - - public function testInlineKeyboardSingleButtonSingleRow(): void - { - $inline_keyboard = (new InlineKeyboard( - $this->getRandomButton('Button Text 1') - ))->getProperty('inline_keyboard'); - self::assertSame('Button Text 1', $inline_keyboard[0][0]->getText()); - - $inline_keyboard = (new InlineKeyboard( - [$this->getRandomButton('Button Text 2')] - ))->getProperty('inline_keyboard'); - self::assertSame('Button Text 2', $inline_keyboard[0][0]->getText()); - } - - public function testInlineKeyboardSingleButtonMultipleRows(): void - { - $keyboard = (new InlineKeyboard( - $this->getRandomButton('Button Text 1'), - $this->getRandomButton('Button Text 2'), - $this->getRandomButton('Button Text 3') - ))->getProperty('inline_keyboard'); - self::assertSame('Button Text 1', $keyboard[0][0]->getText()); - self::assertSame('Button Text 2', $keyboard[1][0]->getText()); - self::assertSame('Button Text 3', $keyboard[2][0]->getText()); - - $keyboard = (new InlineKeyboard( - [$this->getRandomButton('Button Text 4')], - [$this->getRandomButton('Button Text 5')], - [$this->getRandomButton('Button Text 6')] - ))->getProperty('inline_keyboard'); - self::assertSame('Button Text 4', $keyboard[0][0]->getText()); - self::assertSame('Button Text 5', $keyboard[1][0]->getText()); - self::assertSame('Button Text 6', $keyboard[2][0]->getText()); - } - - public function testInlineKeyboardMultipleButtonsSingleRow(): void - { - $keyboard = (new InlineKeyboard([ - $this->getRandomButton('Button Text 1'), - $this->getRandomButton('Button Text 2'), - ]))->getProperty('inline_keyboard'); - self::assertSame('Button Text 1', $keyboard[0][0]->getText()); - self::assertSame('Button Text 2', $keyboard[0][1]->getText()); - } - - public function testInlineKeyboardMultipleButtonsMultipleRows(): void - { - $keyboard = (new InlineKeyboard( - [ - $this->getRandomButton('Button Text 1'), - $this->getRandomButton('Button Text 2'), - ], - [ - $this->getRandomButton('Button Text 3'), - $this->getRandomButton('Button Text 4'), - ] - ))->getProperty('inline_keyboard'); - - self::assertSame('Button Text 1', $keyboard[0][0]->getText()); - self::assertSame('Button Text 2', $keyboard[0][1]->getText()); - self::assertSame('Button Text 3', $keyboard[1][0]->getText()); - self::assertSame('Button Text 4', $keyboard[1][1]->getText()); - } - - public function testInlineKeyboardAddRows(): void - { - $keyboard_obj = new InlineKeyboard([]); - - $keyboard_obj->addRow($this->getRandomButton('Button Text 1')); - $keyboard = $keyboard_obj->getProperty('inline_keyboard'); - self::assertSame('Button Text 1', $keyboard[0][0]->getText()); - - $keyboard_obj->addRow( - $this->getRandomButton('Button Text 2'), - $this->getRandomButton('Button Text 3') - ); - $keyboard = $keyboard_obj->getProperty('inline_keyboard'); - self::assertSame('Button Text 2', $keyboard[1][0]->getText()); - self::assertSame('Button Text 3', $keyboard[1][1]->getText()); - - $keyboard_obj->addRow($this->getRandomButton('Button Text 4')); - $keyboard = $keyboard_obj->getProperty('inline_keyboard'); - self::assertSame('Button Text 4', $keyboard[2][0]->getText()); - } -} diff --git a/tests/Unit/Entities/KeyboardButtonTest.php b/tests/Unit/Entities/KeyboardButtonTest.php deleted file mode 100644 index e7cd907b1..000000000 --- a/tests/Unit/Entities/KeyboardButtonTest.php +++ /dev/null @@ -1,67 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Tests\Unit\Entities; - -use Longman\TelegramBot\Entities\KeyboardButton; -use Longman\TelegramBot\Entities\KeyboardButtonPollType; -use Longman\TelegramBot\Entities\KeyboardButtonRequestChat; -use Longman\TelegramBot\Entities\KeyboardButtonRequestUsers; -use Longman\TelegramBot\Entities\WebAppInfo; -use Longman\TelegramBot\Exception\TelegramException; -use Longman\TelegramBot\Tests\Unit\TestCase; - -/** - * @link https://github.com/php-telegram-bot/core - * @author Avtandil Kikabidze - * @copyright Avtandil Kikabidze - * @license http://opensource.org/licenses/mit-license.php The MIT License (MIT) - * @package TelegramTest - */ -class KeyboardButtonTest extends TestCase -{ - public function testKeyboardButtonSuccess(): void - { - new KeyboardButton(['text' => 'message']); - new KeyboardButton(['text' => 'message', 'request_users' => new KeyboardButtonRequestUsers([])]); - new KeyboardButton(['text' => 'message', 'request_chat' => new KeyboardButtonRequestChat([])]); - new KeyboardButton(['text' => 'message', 'request_contact' => true]); - new KeyboardButton(['text' => 'message', 'request_location' => true]); - new KeyboardButton(['text' => 'message', 'request_poll' => new KeyboardButtonPollType([])]); - new KeyboardButton(['text' => 'message', 'web_app' => new WebAppInfo([])]); - self::assertTrue(true); - } - - public function testInlineKeyboardButtonCouldBe(): void - { - self::assertTrue(KeyboardButton::couldBe(['text' => 'message'])); - self::assertFalse(KeyboardButton::couldBe(['no_text' => 'message'])); - } - - public function testReturnsSubentitiesOnArray() - { - $button = new KeyboardButton('message'); - $button->request_users = []; - $this->assertInstanceOf(KeyboardButtonRequestUsers::class, $button->getRequestUsers()); - - $button = new KeyboardButton('message'); - $button->request_chat = []; - $this->assertInstanceOf(KeyboardButtonRequestChat::class, $button->getRequestChat()); - - $button = new KeyboardButton('message'); - $button->request_poll = []; - $this->assertInstanceOf(KeyboardButtonPollType::class, $button->getRequestPoll()); - - $button = new KeyboardButton('message'); - $button->web_app = []; - $this->assertInstanceOf(WebAppInfo::class, $button->getWebApp()); - } -} diff --git a/tests/Unit/Entities/KeyboardTest.php b/tests/Unit/Entities/KeyboardTest.php deleted file mode 100644 index c5afa90e9..000000000 --- a/tests/Unit/Entities/KeyboardTest.php +++ /dev/null @@ -1,208 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Tests\Unit\Entities; - -use Longman\TelegramBot\Entities\Keyboard; -use Longman\TelegramBot\Entities\KeyboardButton; -use Longman\TelegramBot\Exception\TelegramException; -use Longman\TelegramBot\Tests\Unit\TestCase; - -/** - * @link https://github.com/php-telegram-bot/core - * @author Avtandil Kikabidze - * @copyright Avtandil Kikabidze - * @license http://opensource.org/licenses/mit-license.php The MIT License (MIT) - * @package TelegramTest - */ -class KeyboardTest extends TestCase -{ - public function testKeyboardDataMalformedField(): void - { - $this->expectException(TelegramException::class); - $this->expectExceptionMessage('keyboard field is not an array!'); - new Keyboard(['keyboard' => 'wrong']); - } - - public function testKeyboardDataMalformedSubfield(): void - { - $this->expectException(TelegramException::class); - $this->expectExceptionMessage('keyboard subfield is not an array!'); - new Keyboard(['keyboard' => ['wrong']]); - } - - public function testKeyboardSingleButtonSingleRow(): void - { - $keyboard = (new Keyboard('Button Text 1'))->getProperty('keyboard'); - self::assertSame('Button Text 1', $keyboard[0][0]->getText()); - - $keyboard = (new Keyboard(['Button Text 2']))->getProperty('keyboard'); - self::assertSame('Button Text 2', $keyboard[0][0]->getText()); - } - - public function testKeyboardSingleButtonMultipleRows(): void - { - $keyboard = (new Keyboard( - 'Button Text 1', - 'Button Text 2', - 'Button Text 3' - ))->getProperty('keyboard'); - self::assertSame('Button Text 1', $keyboard[0][0]->getText()); - self::assertSame('Button Text 2', $keyboard[1][0]->getText()); - self::assertSame('Button Text 3', $keyboard[2][0]->getText()); - - $keyboard = (new Keyboard( - ['Button Text 4'], - ['Button Text 5'], - ['Button Text 6'] - ))->getProperty('keyboard'); - self::assertSame('Button Text 4', $keyboard[0][0]->getText()); - self::assertSame('Button Text 5', $keyboard[1][0]->getText()); - self::assertSame('Button Text 6', $keyboard[2][0]->getText()); - } - - public function testKeyboardMultipleButtonsSingleRow(): void - { - $keyboard = (new Keyboard(['Button Text 1', 'Button Text 2']))->getProperty('keyboard'); - self::assertSame('Button Text 1', $keyboard[0][0]->getText()); - self::assertSame('Button Text 2', $keyboard[0][1]->getText()); - } - - public function testKeyboardMultipleButtonsMultipleRows(): void - { - $keyboard = (new Keyboard( - ['Button Text 1', 'Button Text 2'], - ['Button Text 3', 'Button Text 4'] - ))->getProperty('keyboard'); - - self::assertSame('Button Text 1', $keyboard[0][0]->getText()); - self::assertSame('Button Text 2', $keyboard[0][1]->getText()); - self::assertSame('Button Text 3', $keyboard[1][0]->getText()); - self::assertSame('Button Text 4', $keyboard[1][1]->getText()); - } - - public function testKeyboardWithButtonObjects(): void - { - $keyboard = (new Keyboard( - new KeyboardButton('Button Text 1') - ))->getProperty('keyboard'); - self::assertSame('Button Text 1', $keyboard[0][0]->getText()); - - $keyboard = (new Keyboard( - new KeyboardButton('Button Text 2'), - new KeyboardButton('Button Text 3') - ))->getProperty('keyboard'); - self::assertSame('Button Text 2', $keyboard[0][0]->getText()); - self::assertSame('Button Text 3', $keyboard[1][0]->getText()); - - $keyboard = (new Keyboard( - [new KeyboardButton('Button Text 4')], - [new KeyboardButton('Button Text 5'), new KeyboardButton('Button Text 6')] - ))->getProperty('keyboard'); - self::assertSame('Button Text 4', $keyboard[0][0]->getText()); - self::assertSame('Button Text 5', $keyboard[1][0]->getText()); - self::assertSame('Button Text 6', $keyboard[1][1]->getText()); - } - - public function testKeyboardWithDataArray(): void - { - $resize_keyboard = (bool) mt_rand(0, 1); - $one_time_keyboard = (bool) mt_rand(0, 1); - $input_field_placeholder = 'placeholder'; - $selective = (bool) mt_rand(0, 1); - - $keyboard_obj = new Keyboard([ - 'resize_keyboard' => $resize_keyboard, - 'one_time_keyboard' => $one_time_keyboard, - 'input_field_placeholder' => $input_field_placeholder, - 'selective' => $selective, - 'keyboard' => [['Button Text 1']], - ]); - - $keyboard = $keyboard_obj->getProperty('keyboard'); - self::assertSame('Button Text 1', $keyboard[0][0]->getText()); - - self::assertSame($resize_keyboard, $keyboard_obj->getResizeKeyboard()); - self::assertSame($one_time_keyboard, $keyboard_obj->getOneTimeKeyboard()); - self::assertSame($input_field_placeholder, $keyboard_obj->getInputFieldPlaceholder()); - self::assertSame($selective, $keyboard_obj->getSelective()); - } - - public function testPredefinedKeyboards(): void - { - $keyboard_remove = Keyboard::remove(); - self::assertTrue($keyboard_remove->getProperty('remove_keyboard')); - - $keyboard_force_reply = Keyboard::forceReply(); - self::assertTrue($keyboard_force_reply->getProperty('force_reply')); - } - - public function testKeyboardMethods(): void - { - $keyboard_obj = new Keyboard([]); - - self::assertEmpty($keyboard_obj->getOneTimeKeyboard()); - self::assertEmpty($keyboard_obj->getResizeKeyboard()); - self::assertEmpty($keyboard_obj->getSelective()); - - $keyboard_obj->setOneTimeKeyboard(true); - self::assertTrue($keyboard_obj->getOneTimeKeyboard()); - $keyboard_obj->setOneTimeKeyboard(false); - self::assertFalse($keyboard_obj->getOneTimeKeyboard()); - - $keyboard_obj->setResizeKeyboard(true); - self::assertTrue($keyboard_obj->getResizeKeyboard()); - $keyboard_obj->setResizeKeyboard(false); - self::assertFalse($keyboard_obj->getResizeKeyboard()); - - $keyboard_obj->setSelective(true); - self::assertTrue($keyboard_obj->getSelective()); - $keyboard_obj->setSelective(false); - self::assertFalse($keyboard_obj->getSelective()); - } - - public function testKeyboardAddRows(): void - { - $keyboard_obj = new Keyboard([]); - - $keyboard_obj->addRow('Button Text 1'); - $keyboard = $keyboard_obj->getProperty('keyboard'); - self::assertSame('Button Text 1', $keyboard[0][0]->getText()); - - $keyboard_obj->addRow('Button Text 2', 'Button Text 3'); - $keyboard = $keyboard_obj->getProperty('keyboard'); - self::assertSame('Button Text 2', $keyboard[1][0]->getText()); - self::assertSame('Button Text 3', $keyboard[1][1]->getText()); - - $keyboard_obj->addRow(['text' => 'Button Text 4']); - $keyboard = $keyboard_obj->getProperty('keyboard'); - self::assertSame('Button Text 4', $keyboard[2][0]->getText()); - } - - public function testSetterMethods(): void - { - $keyboard = (new Keyboard( - [ - ['text' => 'One'], - ] - ))->setResizeKeyboard(true); - - $array = json_decode($keyboard->toJson(), true); - - $this->assertIsArray($array); - - $this->assertArrayHasKey('keyboard', $array); - $this->assertArrayHasKey('resize_keyboard', $array); - - $this->assertIsArray($array['keyboard']); - $this->assertEquals(true, $array['resize_keyboard']); - } -} diff --git a/tests/Unit/Entities/LocationTest.php b/tests/Unit/Entities/LocationTest.php deleted file mode 100644 index 20cb94240..000000000 --- a/tests/Unit/Entities/LocationTest.php +++ /dev/null @@ -1,57 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Tests\Unit\Entities; - -use Longman\TelegramBot\Entities\Location; -use Longman\TelegramBot\Tests\Unit\TestCase; - -/** - * @link https://github.com/php-telegram-bot/core - * @author Baev Nikolay - * @copyright Avtandil Kikabidze - * @license http://opensource.org/licenses/mit-license.php The MIT License (MIT) - * @package TelegramTest - */ -class LocationTest extends TestCase -{ - private $coordinates; - - public function setUp(): void - { - $this->coordinates = [ - 'longitude' => (float) mt_rand(10, 69), - 'latitude' => (float) mt_rand(10, 48), - ]; - } - - public function testBaseStageLocation(): void - { - $location = new Location($this->coordinates); - self::assertInstanceOf(Location::class, $location); - } - - public function testGetLongitude(): void - { - $location = new Location($this->coordinates); - $long = $location->getLongitude(); - self::assertIsFloat($long); - self::assertEquals($this->coordinates['longitude'], $long); - } - - public function testGetLatitude(): void - { - $location = new Location($this->coordinates); - $lat = $location->getLatitude(); - self::assertIsFloat($lat); - self::assertEquals($this->coordinates['latitude'], $lat); - } -} diff --git a/tests/Unit/Entities/MessageTest.php b/tests/Unit/Entities/MessageTest.php deleted file mode 100644 index ba73d8146..000000000 --- a/tests/Unit/Entities/MessageTest.php +++ /dev/null @@ -1,96 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Tests\Unit\Entities; - -use Longman\TelegramBot\Tests\Unit\TestCase; -use Longman\TelegramBot\Tests\Unit\TestHelpers; - -/** - * @link https://github.com/php-telegram-bot/core - * @author Avtandil Kikabidze - * @copyright Avtandil Kikabidze - * @license http://opensource.org/licenses/mit-license.php The MIT License (MIT) - * @package TelegramTest - */ -class MessageTest extends TestCase -{ - public function testTextAndCommandRecognise(): void - { - // /command - $message = TestHelpers::getFakeMessageObject(['text' => '/help']); - self::assertEquals('/help', $message->getFullCommand()); - self::assertEquals('help', $message->getCommand()); - self::assertEquals('/help', $message->getText()); - self::assertEquals('', $message->getText(true)); - - // text - $message = TestHelpers::getFakeMessageObject(['text' => 'some text']); - self::assertNull($message->getFullCommand()); - self::assertNull($message->getCommand()); - self::assertEquals('some text', $message->getText()); - self::assertEquals('some text', $message->getText(true)); - - // /command@bot - $message = TestHelpers::getFakeMessageObject(['text' => '/help@testbot']); - self::assertEquals('/help@testbot', $message->getFullCommand()); - self::assertEquals('help', $message->getCommand()); - self::assertEquals('/help@testbot', $message->getText()); - self::assertEquals('', $message->getText(true)); - - // /command text - $message = TestHelpers::getFakeMessageObject(['text' => '/help some text']); - self::assertEquals('/help', $message->getFullCommand()); - self::assertEquals('help', $message->getCommand()); - self::assertEquals('/help some text', $message->getText()); - self::assertEquals('some text', $message->getText(true)); - - // /command@bot some text - $message = TestHelpers::getFakeMessageObject(['text' => '/help@testbot some text']); - self::assertEquals('/help@testbot', $message->getFullCommand()); - self::assertEquals('help', $message->getCommand()); - self::assertEquals('/help@testbot some text', $message->getText()); - self::assertEquals('some text', $message->getText(true)); - - // /command\n text - $message = TestHelpers::getFakeMessageObject(['text' => "/help\n some text"]); - self::assertEquals('/help', $message->getFullCommand()); - self::assertEquals('help', $message->getCommand()); - self::assertEquals("/help\n some text", $message->getText()); - self::assertEquals(' some text', $message->getText(true)); - - // /command@bot\nsome text - $message = TestHelpers::getFakeMessageObject(['text' => "/help@testbot\nsome text"]); - self::assertEquals('/help@testbot', $message->getFullCommand()); - self::assertEquals('help', $message->getCommand()); - self::assertEquals("/help@testbot\nsome text", $message->getText()); - self::assertEquals('some text', $message->getText(true)); - - // /command@bot \nsome text - $message = TestHelpers::getFakeMessageObject(['text' => "/help@testbot \nsome text"]); - self::assertEquals('/help@testbot', $message->getFullCommand()); - self::assertEquals('help', $message->getCommand()); - self::assertEquals("/help@testbot \nsome text", $message->getText()); - self::assertEquals("\nsome text", $message->getText(true)); - } - - public function testGetType(): void - { - $message = TestHelpers::getFakeMessageObject(['text' => null]); - self::assertSame('message', $message->getType()); - - $message = TestHelpers::getFakeMessageObject(['text' => '/help']); - self::assertSame('command', $message->getType()); - - $message = TestHelpers::getFakeMessageObject(['text' => 'some text']); - self::assertSame('text', $message->getType()); - } -} diff --git a/tests/Unit/Entities/ReplyToMessageTest.php b/tests/Unit/Entities/ReplyToMessageTest.php deleted file mode 100644 index 70b6f3f7e..000000000 --- a/tests/Unit/Entities/ReplyToMessageTest.php +++ /dev/null @@ -1,51 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Tests\Unit\Entities; - -use Longman\TelegramBot\Entities\Update; -use Longman\TelegramBot\Tests\Unit\TestCase; - -/** - * @link https://github.com/php-telegram-bot/core - * @author Avtandil Kikabidze - * @copyright Avtandil Kikabidze - * @license http://opensource.org/licenses/mit-license.php The MIT License (MIT) - * @package TelegramTest - */ -class ReplyToMessageTest extends TestCase -{ - public function testChatType(): void - { - $json = '{ - "update_id":137809335, - "message":{ - "message_id":4479, - "from":{"id":123,"first_name":"John","username":"MJohn"}, - "chat":{"id":-123,"title":"MyChat","type":"group"}, - "date":1449092987, - "reply_to_message":{ - "message_id":11, - "from":{"id":121,"first_name":"Myname","username":"mybot"}, - "chat":{"id":-123,"title":"MyChat","type":"group"}, - "date":1449092984, - "text":"type some text" - }, - "text":"some text" - } - }'; - - $update = new Update(json_decode($json, true), 'mybot'); - $reply_to_message = $update->getMessage()->getReplyToMessage(); - - self::assertNull($reply_to_message->getReplyToMessage()); - } -} diff --git a/tests/Unit/Entities/ServerResponseTest.php b/tests/Unit/Entities/ServerResponseTest.php deleted file mode 100644 index 6412885d0..000000000 --- a/tests/Unit/Entities/ServerResponseTest.php +++ /dev/null @@ -1,386 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - * - * Written by Marco Boretto - */ - -namespace Longman\TelegramBot\Tests\Unit\Entities; - -use Longman\TelegramBot\Entities\File; -use Longman\TelegramBot\Entities\Message; -use Longman\TelegramBot\Entities\PhotoSize; -use Longman\TelegramBot\Entities\ServerResponse; -use Longman\TelegramBot\Entities\Sticker; -use Longman\TelegramBot\Entities\StickerSet; -use Longman\TelegramBot\Entities\Update; -use Longman\TelegramBot\Entities\UserProfilePhotos; -use Longman\TelegramBot\Request; -use Longman\TelegramBot\Tests\Unit\TestCase; -use Longman\TelegramBot\Tests\Unit\TestHelpers; - -/** - * @link https://github.com/php-telegram-bot/core - * @author Avtandil Kikabidze - * @copyright Avtandil Kikabidze - * @license http://opensource.org/licenses/mit-license.php The MIT License (MIT) - * @package TelegramTest - */ -class ServerResponseTest extends TestCase -{ - protected function setUp(): void - { - // Make sure the current action in the Request class is unset. - TestHelpers::setStaticProperty(Request::class, 'current_action', ''); - } - - public function sendMessageOk(): string - { - return '{ - "ok":true, - "result":{ - "message_id":1234, - "from":{"id":123456789,"first_name":"botname","username":"namebot"}, - "chat":{"id":123456789,"first_name":"john","username":"Mjohn"}, - "date":1441378360, - "text":"hello" - } - }'; - } - - public function testSendMessageOk(): void - { - $result = $this->sendMessageOk(); - $server = new ServerResponse(json_decode($result, true), 'testbot'); - $server_result = $server->getResult(); - - self::assertTrue($server->isOk()); - self::assertNull($server->getErrorCode()); - self::assertNull($server->getDescription()); - self::assertInstanceOf(Message::class, $server_result); - - //Message - self::assertEquals('1234', $server_result->getMessageId()); - self::assertEquals('123456789', $server_result->getFrom()->getId()); - self::assertEquals('botname', $server_result->getFrom()->getFirstName()); - self::assertEquals('namebot', $server_result->getFrom()->getUsername()); - self::assertEquals('123456789', $server_result->getChat()->getId()); - self::assertEquals('john', $server_result->getChat()->getFirstName()); - self::assertEquals('Mjohn', $server_result->getChat()->getUsername()); - self::assertEquals('1441378360', $server_result->getDate()); - self::assertEquals('hello', $server_result->getText()); - - //... they are not finished... - } - - public function sendMessageFail(): string - { - return '{ - "ok":false, - "error_code":400, - "description":"Error: Bad Request: wrong chat id" - }'; - } - - public function testSendMessageFail(): void - { - $result = $this->sendMessageFail(); - $server = new ServerResponse(json_decode($result, true), 'testbot'); - - self::assertFalse($server->isOk()); - self::assertNull($server->getResult()); - self::assertEquals('400', $server->getErrorCode()); - self::assertEquals('Error: Bad Request: wrong chat id', $server->getDescription()); - } - - public function setWebhookOk(): string - { - return '{"ok":true,"result":true,"description":"Webhook was set"}'; - } - - public function testSetWebhookOk(): void - { - $result = $this->setWebhookOk(); - $server = new ServerResponse(json_decode($result, true), 'testbot'); - - self::assertTrue($server->isOk()); - self::assertTrue($server->getResult()); - self::assertNull($server->getErrorCode()); - self::assertEquals('Webhook was set', $server->getDescription()); - } - - public function setWebhookFail(): string - { - return '{ - "ok":false, - "error_code":400, - "description":"Error: Bad request: https:\/\/domain.host.org\/dir\/hook.php" - }'; - } - - public function testSetWebhookFail(): void - { - $result = $this->setWebhookFail(); - $server = new ServerResponse(json_decode($result, true), 'testbot'); - - self::assertFalse($server->isOk()); - self::assertNull($server->getResult()); - self::assertEquals(400, $server->getErrorCode()); - self::assertEquals('Error: Bad request: https://domain.host.org/dir/hook.php', $server->getDescription()); - } - - public function getUpdatesArray(): string - { - return '{ - "ok":true, - "result":[ - { - "update_id":123, - "message":{ - "message_id":90, - "from":{"id":123456789,"first_name":"John","username":"Mjohn"}, - "chat":{"id":123456789,"first_name":"John","username":"Mjohn"}, - "date":1441569067, - "text":"\/start" - } - }, - { - "update_id":124, - "message":{ - "message_id":91, - "from":{"id":123456788,"first_name":"Patrizia","username":"Patry"}, - "chat":{"id":123456788,"first_name":"Patrizia","username":"Patry"}, - "date":1441569073, - "text":"Hello!" - } - }, - { - "update_id":125, - "message":{ - "message_id":92, - "from":{"id":123456789,"first_name":"John","username":"MJohn"}, - "chat":{"id":123456789,"first_name":"John","username":"MJohn"}, - "date":1441569094, - "text":"\/echo hello!" - } - }, - { - "update_id":126, - "message":{ - "message_id":93, - "from":{"id":123456788,"first_name":"Patrizia","username":"Patry"}, - "chat":{"id":123456788,"first_name":"Patrizia","username":"Patry"}, - "date":1441569112, - "text":"\/echo the best" - } - } - ] - }'; - } - - public function testGetUpdatesArray(): void - { - $result = $this->getUpdatesArray(); - $server = new ServerResponse(json_decode($result, true), 'testbot'); - - self::assertCount(4, $server->getResult()); - self::assertInstanceOf(Update::class, $server->getResult()[0]); - } - - public function getUpdatesEmpty(): string - { - return '{"ok":true,"result":[]}'; - } - - public function testGetUpdatesEmpty(): void - { - $result = $this->getUpdatesEmpty(); - $server = new ServerResponse(json_decode($result, true), 'testbot'); - - self::assertEmpty($server->getResult()); - } - - public function getUserProfilePhotos(): string - { - TestHelpers::setStaticProperty(Request::class, 'current_action', 'getUserProfilePhotos'); - return '{ - "ok":true, - "result":{ - "total_count":3, - "photos":[ - [ - {"file_id":"AgADBG6_vmQaVf3qOGVurBRzHqgg5uEju-8IBAAEC","file_size":7402,"width":160,"height":160}, - {"file_id":"AgADBG6_vmQaVf3qOGVurBRzHWMuphij6_MIBAAEC","file_size":15882,"width":320,"height":320}, - {"file_id":"AgADBG6_vmQaVf3qOGVurBRzHNWdpQ9jz_cIBAAEC","file_size":46680,"width":640,"height":640} - ], - [ - {"file_id":"AgADBAADr6cxG6_vmH-bksDdiYzAABO8UCGz_JLAAgI","file_size":7324,"width":160,"height":160}, - {"file_id":"AgADBAADr6cxG6_vmH-bksDdiYzAABAlhB5Q_K0AAgI","file_size":15816,"width":320,"height":320}, - {"file_id":"AgADBAADr6cxG6_vmH-bksDdiYzAABIIxOSHyayAAgI","file_size":46620,"width":640,"height":640} - ], - [ - {"file_id":"AgABxG6_vmQaL2X0CUTAABMhd1n2RLaRSj6cAAgI","file_size":2710,"width":160,"height":160}, - {"file_id":"AgADcxG6_vmQaL2X0EUTAABPXm1og0O7qwjKcAAgI","file_size":11660,"width":320,"height":320}, - {"file_id":"AgADxG6_vmQaL2X0CUTAABMOtcfUmoPrcjacAAgI","file_size":37150,"width":640,"height":640} - ] - ] - } - }'; - } - - public function testGetUserProfilePhotos(): void - { - $result = $this->getUserProfilePhotos(); - $server = new ServerResponse(json_decode($result, true), 'testbot'); - $server_result = $server->getResult(); - - $photos = $server_result->getPhotos(); - - //Photo count - self::assertEquals(3, $server_result->getTotalCount()); - self::assertCount(3, $photos); - //Photo size count - self::assertCount(3, $photos[0]); - - self::assertInstanceOf(UserProfilePhotos::class, $server_result); - self::assertInstanceOf(PhotoSize::class, $photos[0][0]); - } - - public function getFile(): string - { - TestHelpers::setStaticProperty(Request::class, 'current_action', 'getFile'); - return '{ - "ok":true, - "result":{ - "file_id":"AgADBxG6_vmQaVf3qRzHYTAABD1hNWdpQ9qz_cIBAAEC", - "file_size":46680, - "file_path":"photo\/file_1.jpg" - } - }'; - } - - public function testGetFile(): void - { - $result = $this->getFile(); - $server = new ServerResponse(json_decode($result, true), 'testbot'); - - self::assertInstanceOf(File::class, $server->getResult()); - } - - public function testSetGeneralTestFakeResponse(): void - { - //setWebhook ok - $fake_response = Request::generateGeneralFakeServerResponse(); - - $server = new ServerResponse($fake_response, 'testbot'); - - self::assertTrue($server->isOk()); - self::assertTrue($server->getResult()); - self::assertNull($server->getErrorCode()); - self::assertEquals('', $server->getDescription()); - - //sendMessage ok - $fake_response = Request::generateGeneralFakeServerResponse(['chat_id' => 123456789, 'text' => 'hello']); - - $server = new ServerResponse($fake_response, 'testbot'); - - /** @var Message $server_result */ - $server_result = $server->getResult(); - - self::assertTrue($server->isOk()); - self::assertNull($server->getErrorCode()); - self::assertNull($server->getDescription()); - self::assertInstanceOf(Message::class, $server_result); - - //Message - self::assertEquals('1234', $server_result->getMessageId()); - self::assertEquals('1441378360', $server_result->getDate()); - self::assertEquals('hello', $server_result->getText()); - - //Message //User - self::assertEquals('123456789', $server_result->getFrom()->getId()); - self::assertEquals('botname', $server_result->getFrom()->getFirstName()); - self::assertEquals('namebot', $server_result->getFrom()->getUsername()); - - //Message //Chat - self::assertEquals('123456789', $server_result->getChat()->getId()); - self::assertEquals('', $server_result->getChat()->getFirstName()); - self::assertEquals('', $server_result->getChat()->getUsername()); - - //... they are not finished... - } - - public function getStickerSet(): string - { - TestHelpers::setStaticProperty(Request::class, 'current_action', 'getStickerSet'); - return '{ - "ok":true, - "result":{ - "name":"stickerset_name", - "title":"Some name", - "contains_masks":false, - "stickers":[ - { - "width":512, - "height":324, - "emoji":"\ud83d\ude33", - "set_name":"stickerset_name", - "thumb":{"file_id":"AAQEABOKTFsZAASfA4t3pp1_VlH1AAIC","file_size":3120,"width":128,"height":81}, - "file_id":"CAADBAADzAIAAph_7gOATSb9ehxv5QI", - "file_size":14246 - }, - { - "width":419, - "height":512, - "emoji":"\u2764", - "set_name":"stickerset_name", - "thumb":{"file_id":"AAQEABMj8qoZAASePUHuDSJ2uGIKAAIC","file_size":3500,"width":105,"height":128}, - "file_id":"CAADBAADzQIAAph_7gNPFguft4qtjAI", - "file_size":17814 - }, - { - "width":512, - "height":276, - "emoji":"\ud83d\ude36", - "set_name":"stickerset_name", - "thumb":{"file_id":"AAQEABMiaWcZAATNUEPkYkd0Fh2JBAABAg","file_size":2642,"file_path":"thumbnails\/file_8.jpg","width":128,"height":69}, - "file_id":"CAADBAADzwIAAph_7gOClxA3gK5wqAI", - "file_size":12258 - }, - { - "width":512, - "height":327, - "emoji":"\ud83d\udcbb", - "set_name":"stickerset_name", - "thumb":{"file_id":"AAQEABPC3d8ZAAQUJJnFB1VfII2RAAIC","file_size":3824,"file_path":"thumbnails\/file_10.jpg","width":128,"height":82}, - "file_id":"CAADBAAD0QIAAph_7gO-vBJGkTeWqwI", - "file_size":18282 - } - ] - } - }'; - } - - public function testGetStickerSet(): void - { - $result = $this->getStickerSet(); - $server = new ServerResponse(json_decode($result, true), 'testbot'); - - $server_result = $server->getResult(); - - self::assertInstanceOf(StickerSet::class, $server_result); - self::assertEquals('stickerset_name', $server_result->getName()); - self::assertEquals('Some name', $server_result->getTitle()); - self::assertFalse($server_result->getContainsMasks()); - - $stickers = $server_result->getStickers(); - self::assertCount(4, $stickers); - self::assertInstanceOf(Sticker::class, $stickers[0]); - } -} diff --git a/tests/Unit/Entities/UpdateTest.php b/tests/Unit/Entities/UpdateTest.php deleted file mode 100644 index f05e59585..000000000 --- a/tests/Unit/Entities/UpdateTest.php +++ /dev/null @@ -1,52 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Tests\Unit\Entities; - -use Longman\TelegramBot\Entities\Update; -use Longman\TelegramBot\Tests\Unit\TestCase; - -/** - * @link https://github.com/php-telegram-bot/core - * @author Avtandil Kikabidze - * @copyright Avtandil Kikabidze - * @license http://opensource.org/licenses/mit-license.php The MIT License (MIT) - * @package TelegramTest - */ -class UpdateTest extends TestCase -{ - public function testUpdateCast(): void - { - $json = '{ - "update_id":137809336, - "message":{ - "message_id":4479, - "from":{"id":123,"first_name":"John","username":"MJohn"}, - "chat":{"id":-123,"title":"MyChat","type":"group"}, - "date":1449092987, - "reply_to_message":{ - "message_id":11, - "from":{"id":121,"first_name":"Myname","username":"mybot"}, - "chat":{"id":-123,"title":"MyChat","type":"group"}, - "date":1449092984, - "text":"type some text" - }, - "text":"some text" - } - }'; - - $struct = json_decode($json, true); - $update = new Update($struct, 'mybot'); - - $array_string_after = json_decode($update->toJson(), true); - self::assertEquals($struct, $array_string_after); - } -} diff --git a/tests/Unit/Entities/UserTest.php b/tests/Unit/Entities/UserTest.php deleted file mode 100644 index 41c957ac8..000000000 --- a/tests/Unit/Entities/UserTest.php +++ /dev/null @@ -1,85 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Tests\Unit\Entities; - -use Longman\TelegramBot\Entities\User; -use Longman\TelegramBot\Tests\Unit\TestCase; - -/** - * @link https://github.com/php-telegram-bot/core - * @author Avtandil Kikabidze - * @copyright Avtandil Kikabidze - * @license http://opensource.org/licenses/mit-license.php The MIT License (MIT) - * @package TelegramTest - */ -class UserTest extends TestCase -{ - public function testInstance(): void - { - $user = new User(['id' => 1]); - self::assertInstanceOf(User::class, $user); - } - - public function testGetId(): void - { - $user = new User(['id' => 123]); - self::assertEquals(123, $user->getId()); - } - - public function testTryMention(): void - { - // Username - $user = new User(['id' => 1, 'first_name' => 'John', 'last_name' => 'Taylor', 'username' => 'jtaylor']); - self::assertEquals('@jtaylor', $user->tryMention()); - - // First name. - $user = new User(['id' => 1, 'first_name' => 'John']); - self::assertEquals('John', $user->tryMention()); - - // First and Last name. - $user = new User(['id' => 1, 'first_name' => 'John', 'last_name' => 'Taylor']); - self::assertEquals('John Taylor', $user->tryMention()); - } - - public function testEscapeMarkdown(): void - { - // Username. - $user = new User(['id' => 1, 'first_name' => 'John', 'last_name' => 'Taylor', 'username' => 'j_taylor']); - self::assertEquals('@j_taylor', $user->tryMention()); - self::assertEquals('@j\_taylor', $user->tryMention(true)); - - // First name. - $user = new User(['id' => 1, 'first_name' => 'John[']); - self::assertEquals('John[', $user->tryMention()); - self::assertEquals('John\[', $user->tryMention(true)); - - // First and Last name. - $user = new User(['id' => 1, 'first_name' => 'John', 'last_name' => '`Taylor`']); - self::assertEquals('John `Taylor`', $user->tryMention()); - self::assertEquals('John \`Taylor\`', $user->tryMention(true)); - } - - public function testGetProperties(): void - { - // Username. - $user = new User(['id' => 1, 'username' => 'name_phpunit']); - self::assertEquals('name_phpunit', $user->getUsername()); - - // First name. - $user = new User(['id' => 1, 'first_name' => 'name_phpunit']); - self::assertEquals('name_phpunit', $user->getFirstName()); - - // Last name. - $user = new User(['id' => 1, 'last_name' => 'name_phpunit']); - self::assertEquals('name_phpunit', $user->getLastName()); - } -} diff --git a/tests/Unit/Entities/WebhookInfoTest.php b/tests/Unit/Entities/WebhookInfoTest.php deleted file mode 100644 index 6a931b019..000000000 --- a/tests/Unit/Entities/WebhookInfoTest.php +++ /dev/null @@ -1,143 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Tests\Unit\Entities; - -use Longman\TelegramBot\Entities\WebhookInfo; -use Longman\TelegramBot\Tests\Unit\TestCase; - -/** - * @link https://github.com/php-telegram-bot/core - * @author Baev Nikolay - * @copyright Avtandil Kikabidze - * @license http://opensource.org/licenses/mit-license.php The MIT License (MIT) - * @package TelegramTest - */ -class WebhookInfoTest extends TestCase -{ - /** - * @var array Webhook data - */ - public $data; - - public function setUp(): void - { - $this->data = [ - 'url' => 'http://phpunit', - 'has_custom_certificate' => (bool) mt_rand(0, 1), - 'pending_update_count' => (int) mt_rand(1, 9), - 'ip_address' => '1.2.3.4', - 'last_error_date' => time(), - 'last_error_message' => 'Some_error_message', - 'max_connections' => (int) mt_rand(1, 100), - 'allowed_updates' => ['message', 'edited_channel_post', 'callback_query'], - ]; - } - - public function testBaseStageWebhookInfo(): void - { - $webhook = new WebhookInfo($this->data); - self::assertInstanceOf(WebhookInfo::class, $webhook); - } - - public function testGetUrl(): void - { - $webhook = new WebhookInfo($this->data); - $url = $webhook->getUrl(); - self::assertEquals($this->data['url'], $url); - } - - public function testGetHasCustomCertificate(): void - { - $webhook = new WebhookInfo($this->data); - $custom_certificate = $webhook->getHasCustomCertificate(); - self::assertIsBool($custom_certificate); - self::assertEquals($this->data['has_custom_certificate'], $custom_certificate); - } - - public function testGetPendingUpdateCount(): void - { - $webhook = new WebhookInfo($this->data); - $update_count = $webhook->getPendingUpdateCount(); - self::assertIsInt($update_count); - self::assertEquals($this->data['pending_update_count'], $update_count); - } - - public function testGetIpAddress(): void - { - $webhook = new WebhookInfo($this->data); - $ip_address = $webhook->getIpAddress(); - self::assertIsString($ip_address); - self::assertEquals($this->data['ip_address'], $ip_address); - } - - public function testGetLastErrorDate(): void - { - $webhook = new WebhookInfo($this->data); - $error_date = $webhook->getLastErrorDate(); - self::assertIsInt($error_date); - self::assertEquals($this->data['last_error_date'], $error_date); - } - - public function testGetLastErrorMessage(): void - { - $webhook = new WebhookInfo($this->data); - $error_msg = $webhook->getLastErrorMessage(); - self::assertIsString($error_msg); - self::assertEquals($this->data['last_error_message'], $error_msg); - } - - public function testGetMaxConnections(): void - { - $webhook = new WebhookInfo($this->data); - $max_connections = $webhook->getMaxConnections(); - self::assertIsInt($max_connections); - self::assertEquals($this->data['max_connections'], $max_connections); - } - - public function testGetAllowedUpdates(): void - { - $webhook = new WebhookInfo($this->data); - $allowed_updates = $webhook->getAllowedUpdates(); - self::assertIsArray($allowed_updates); - self::assertEquals($this->data['allowed_updates'], $allowed_updates); - } - - public function testGetDataWithoutParams(): void - { - // Make a copy to not risk failed tests if not run in proper order. - $data = $this->data; - - unset($data['url']); - self::assertNull((new WebhookInfo($data))->getUrl()); - - unset($data['has_custom_certificate']); - self::assertNull((new WebhookInfo($data))->getHasCustomCertificate()); - - unset($data['pending_update_count']); - self::assertNull((new WebhookInfo($data))->getPendingUpdateCount()); - - unset($data['ip_address']); - self::assertNull((new WebhookInfo($data))->getIpAddress()); - - unset($data['last_error_date']); - self::assertNull((new WebhookInfo($data))->getLastErrorDate()); - - unset($data['last_error_message']); - self::assertNull((new WebhookInfo($data))->getLastErrorMessage()); - - unset($data['max_connections']); - self::assertNull((new WebhookInfo($data))->getMaxConnections()); - - unset($data['allowed_updates']); - self::assertNull((new WebhookInfo($data))->getAllowedUpdates()); - } -} diff --git a/tests/Unit/TelegramLogTest.php b/tests/Unit/TelegramLogTest.php deleted file mode 100644 index 209cac5f1..000000000 --- a/tests/Unit/TelegramLogTest.php +++ /dev/null @@ -1,116 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Tests\Unit; - -use Longman\TelegramBot\TelegramLog; -use Monolog\Formatter\LineFormatter; -use Monolog\Handler\StreamHandler; -use Monolog\Logger; - -/** - * @package TelegramTest - * @author Avtandil Kikabidze - * @copyright Avtandil Kikabidze - * @license http://opensource.org/licenses/mit-license.php The MIT License (MIT) - * @link https://github.com/php-telegram-bot/core - */ -class TelegramLogTest extends TestCase -{ - /** - * @var array Dummy logfile paths - */ - private static $logfiles = [ - 'debug' => '/tmp/php-telegram-bot-debug.log', - 'error' => '/tmp/php-telegram-bot-error.log', - 'update' => '/tmp/php-telegram-bot-update.log', - ]; - - protected function setUp(): void - { - TelegramLog::initialize( - new Logger('bot_log', [ - (new StreamHandler(self::$logfiles['debug'], Logger::DEBUG))->setFormatter(new LineFormatter(null, null, true)), - (new StreamHandler(self::$logfiles['error'], Logger::ERROR))->setFormatter(new LineFormatter(null, null, true)), - ]), - new Logger('bot_log_updates', [ - (new StreamHandler(self::$logfiles['update'], Logger::INFO))->setFormatter(new LineFormatter('%message%' . PHP_EOL)), - ]) - ); - } - - protected function tearDown(): void - { - // Make sure no logger instance is set after each test. - TestHelpers::setStaticProperty(TelegramLog::class, 'logger', null); - TestHelpers::setStaticProperty(TelegramLog::class, 'update_logger', null); - - // Make sure no logfiles exist. - foreach (self::$logfiles as $file) { - file_exists($file) && unlink($file); - } - } - - public function testNullLogger(): void - { - TelegramLog::initialize(null, null); - - TelegramLog::debug('my debug log'); - TelegramLog::error('my error log'); - TelegramLog::update('my update log'); - - foreach (self::$logfiles as $file) { - self::assertFileDoesNotExist($file); - } - } - - public function testDebugStream(): void - { - $file = self::$logfiles['debug']; - - self::assertFileDoesNotExist($file); - TelegramLog::debug('my debug log'); - TelegramLog::debug('my {place} {holder} debug log', ['place' => 'custom', 'holder' => 'placeholder']); - - self::assertFileExists($file); - $debug_log = file_get_contents($file); - self::assertStringContainsString('bot_log.DEBUG: my debug log', $debug_log); - self::assertStringContainsString('bot_log.DEBUG: my custom placeholder debug log', $debug_log); - } - - public function testErrorStream(): void - { - $file = self::$logfiles['error']; - - self::assertFileDoesNotExist($file); - TelegramLog::error('my error log'); - TelegramLog::error('my {place} {holder} error log', ['place' => 'custom', 'holder' => 'placeholder']); - - self::assertFileExists($file); - $error_log = file_get_contents($file); - self::assertStringContainsString('bot_log.ERROR: my error log', $error_log); - self::assertStringContainsString('bot_log.ERROR: my custom placeholder error log', $error_log); - } - - public function testUpdateStream(): void - { - $file = self::$logfiles['update']; - - self::assertFileDoesNotExist($file); - TelegramLog::update('my update log'); - TelegramLog::update('my {place} {holder} update log', ['place' => 'custom', 'holder' => 'placeholder']); - - self::assertFileExists($file); - $update_log = file_get_contents($file); - self::assertStringContainsString('my update log', $update_log); - self::assertStringContainsString('my custom placeholder update log', $update_log); - } -} diff --git a/tests/Unit/TelegramTest.php b/tests/Unit/TelegramTest.php deleted file mode 100644 index 58855d914..000000000 --- a/tests/Unit/TelegramTest.php +++ /dev/null @@ -1,261 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Tests\Unit; - -use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts; -use Dummy\AdminCommands\DummyAdminCommand; -use Dummy\SystemCommands\DummySystemCommand; -use Dummy\UserCommands\DummyUserCommand; -use Longman\TelegramBot\Commands\UserCommands\StartCommand; -use Longman\TelegramBot\Entities\Update; -use Longman\TelegramBot\Exception\TelegramException; -use Longman\TelegramBot\Telegram; -use Longman\TelegramBot\TelegramLog; - -/** - * @link https://github.com/php-telegram-bot/core - * @author Avtandil Kikabidze - * @copyright Avtandil Kikabidze - * @license http://opensource.org/licenses/mit-license.php The MIT License (MIT) - * @package TelegramTest - */ -class TelegramTest extends TestCase -{ - use ArraySubsetAsserts; - - /** - * @var Telegram - */ - private $telegram; - - /** - * @var array A few dummy custom commands paths - */ - private $custom_commands_paths = [ - '/tmp/php-telegram-bot-custom-commands-1', - '/tmp/php-telegram-bot-custom-commands-2', - '/tmp/php-telegram-bot-custom-commands-3', - ]; - - protected function setUp(): void - { - $this->telegram = new Telegram(self::$dummy_api_key, 'testbot'); - - // Create a few dummy custom commands paths. - foreach ($this->custom_commands_paths as $custom_path) { - mkdir($custom_path); - } - } - - protected function tearDown(): void - { - // Clean up the custom commands paths. - foreach ($this->custom_commands_paths as $custom_path) { - rmdir($custom_path); - } - } - - public function testNewInstanceWithoutApiKeyParam(): void - { - $this->expectException(TelegramException::class); - $this->expectExceptionMessage('API KEY not defined!'); - new Telegram(''); - } - - public function testNewInstanceWithInvalidApiKeyParam(): void - { - $this->expectException(TelegramException::class); - $this->expectExceptionMessage('Invalid API KEY defined!'); - new Telegram('invalid-api-key-format'); - } - - public function testGetApiKey(): void - { - self::assertEquals(self::$dummy_api_key, $this->telegram->getApiKey()); - } - - public function testGetBotUsername(): void - { - self::assertEquals('testbot', $this->telegram->getBotUsername()); - } - - public function testEnableAdmins(): void - { - $tg = $this->telegram; - - self::assertEmpty($tg->getAdminList()); - - // Single - $tg->enableAdmin(1); - self::assertCount(1, $tg->getAdminList()); - - // Multiple - $tg->enableAdmins([2, 3]); - self::assertCount(3, $tg->getAdminList()); - - // Already added - $tg->enableAdmin(2); - self::assertCount(3, $tg->getAdminList()); - } - - public function testAddCustomCommandsPaths(): void - { - $tg = $this->telegram; - - self::assertCount(1, $tg->getCommandsPaths()); - - $tg->addCommandsPath($this->custom_commands_paths[0]); - self::assertCount(2, $tg->getCommandsPaths()); - self::assertArraySubset( - [$this->custom_commands_paths[0]], - $tg->getCommandsPaths() - ); - - $tg->addCommandsPath('/invalid/path'); - self::assertCount(2, $tg->getCommandsPaths()); - - $tg->addCommandsPaths([ - $this->custom_commands_paths[1], - $this->custom_commands_paths[2], - ]); - self::assertCount(4, $tg->getCommandsPaths()); - self::assertArraySubset( - array_reverse($this->custom_commands_paths), - $tg->getCommandsPaths() - ); - - $tg->addCommandsPath($this->custom_commands_paths[0]); - self::assertCount(4, $tg->getCommandsPaths()); - } - - public function testAddCustomCommandsClass(): void - { - $tg = $this->telegram; - - // Require dummy commands to test with - require_once __DIR__ . '/Commands/CustomTestCommands/DummySystemCommand.php'; - require_once __DIR__ . '/Commands/CustomTestCommands/DummyAdminCommand.php'; - require_once __DIR__ . '/Commands/CustomTestCommands/DummyUserCommand.php'; - - // Test for base arrays (System, Admin, User) - self::assertCount(3, $tg->getCommandClasses()); - - // Test for invalid command classes - try { - $tg->addCommandClass(''); - } catch (\InvalidArgumentException $ex) { - } - self::assertEmpty(array_filter($tg->getCommandClasses())); - - try { - $tg->addCommandClass('not\exist\Class'); - } catch (\InvalidArgumentException $ex) { - } - self::assertEmpty(array_filter($tg->getCommandClasses())); - - // Add valid command classes - $tg->addCommandClass(DummySystemCommand::class); - $tg->addCommandClasses([ - DummyAdminCommand::class, - DummyUserCommand::class, - ]); - - $command_classes = $tg->getCommandClasses(); - self::assertSame(['dummy_system' => 'Dummy\SystemCommands\DummySystemCommand'], $command_classes['System']); - self::assertSame(['dummy_admin' => 'Dummy\AdminCommands\DummyAdminCommand'], $command_classes['Admin']); - self::assertSame(['dummy_user' => 'Dummy\UserCommands\DummyUserCommand'], $command_classes['User']); - } - - public function testSettingDownloadUploadPaths(): void - { - self::assertEmpty($this->telegram->getDownloadPath()); - self::assertEmpty($this->telegram->getUploadPath()); - - $this->telegram->setDownloadPath('/down/below'); - $this->telegram->setUploadPath('/up/above'); - - self::assertSame('/down/below', $this->telegram->getDownloadPath()); - self::assertSame('/up/above', $this->telegram->getUploadPath()); - } - - public function testGetCommandsList(): void - { - $commands = $this->telegram->getCommandsList(); - self::assertIsArray($commands); - self::assertNotCount(0, $commands); - } - - public function testGetCommandClass(): void - { - $className = StartCommand::class; - $commands = $this->telegram->getCommandClasses(); - self::assertIsArray($commands); - self::assertCount(3, $commands); - - $class = $this->telegram->getCommandClassName('user', 'notexist'); - self::assertNull($class); - - $this->telegram->addCommandClass($className); - $class = $this->telegram->getCommandClassName('user', 'start'); - self::assertNotNull($class); - - self::assertSame($className, $class); - } - - public function testUpdateFilter(): void - { - $rawUpdate = '{ - "update_id": 513400512, - "message": { - "message_id": 3, - "from": { - "id": 313534466, - "first_name": "first", - "last_name": "last", - "username": "username" - }, - "chat": { - "id": 313534466, - "first_name": "first", - "last_name": "last", - "username": "username", - "type": "private" - }, - "date": 1499402829, - "text": "hi" - } - }'; - - $debug_log_file = '/tmp/php-telegram-bot-update-filter-debug.log'; - TelegramLog::initialize( - new \Monolog\Logger('bot_log', [ - (new \Monolog\Handler\StreamHandler($debug_log_file, \Monolog\Logger::DEBUG))->setFormatter(new \Monolog\Formatter\LineFormatter(null, null, true)), - ]) - ); - - $update = new Update(json_decode($rawUpdate, true), $this->telegram->getBotUsername()); - $this->telegram->setUpdateFilter(function (Update $update, Telegram $telegram, &$reason) { - if ($update->getMessage()->getChat()->getId() === 313534466) { - $reason = 'Invalid user, update denied.'; - return false; - } - return true; - }); - $response = $this->telegram->processUpdate($update); - self::assertFalse($response->isOk()); - - // Check that the reason is written to the debug log. - $debug_log = file_get_contents($debug_log_file); - self::assertStringContainsString('Invalid user, update denied.', $debug_log); - file_exists($debug_log_file) && unlink($debug_log_file); - } -} diff --git a/tests/Unit/TestCase.php b/tests/Unit/TestCase.php deleted file mode 100644 index fe3ccbecb..000000000 --- a/tests/Unit/TestCase.php +++ /dev/null @@ -1,31 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Tests\Unit; - -use PHPUnit\Framework\TestCase as BaseTestCase; - -class TestCase extends BaseTestCase -{ - /** - * @var string - */ - public static $dummy_api_key = '123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11'; - - protected function skip64BitTest(): void - { - if (PHP_INT_SIZE === 4) { - self::markTestSkipped( - 'Skipping test that can run only on a 64-bit build of PHP.' - ); - } - } -} diff --git a/tests/Unit/TestHelpers.php b/tests/Unit/TestHelpers.php deleted file mode 100644 index 5daa3ec13..000000000 --- a/tests/Unit/TestHelpers.php +++ /dev/null @@ -1,251 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Longman\TelegramBot\Tests\Unit; - -use Longman\TelegramBot\DB; -use Longman\TelegramBot\Entities\Chat; -use Longman\TelegramBot\Entities\Message; -use Longman\TelegramBot\Entities\Update; -use Longman\TelegramBot\Entities\User; -use Longman\TelegramBot\Exception\TelegramException; - -/** - * @link https://github.com/php-telegram-bot/core - * @author Avtandil Kikabidze - * @copyright Avtandil Kikabidze - * @license http://opensource.org/licenses/mit-license.php The MIT License (MIT) - * @package TelegramTest - */ -class TestHelpers -{ - /** - * Data template of a user. - * - * @var array - */ - protected static $user_template = [ - 'id' => 1, - 'first_name' => 'first', - 'last_name' => 'last', - 'username' => 'user', - ]; - - /** - * Data template of a chat. - * - * @var array - */ - protected static $chat_template = [ - 'id' => 1, - 'first_name' => 'first', - 'last_name' => 'last', - 'username' => 'name', - 'type' => 'private', - 'all_members_are_administrators' => false, - ]; - - /** - * Set the value of a private/protected property of an object - * - * @param object $object Object that contains the property - * @param string $property Name of the property who's value we want to set - * @param mixed $value The value to set to the property - * - * @throws \ReflectionException - */ - public static function setObjectProperty(object $object, string $property, $value): void - { - $ref_object = new \ReflectionObject($object); - $ref_property = $ref_object->getProperty($property); - $ref_property->setAccessible(true); - $ref_property->setValue($object, $value); - } - - /** - * Set the value of a private/protected static property of a class - * - * @param string $class Class that contains the static property - * @param string $property Name of the property who's value we want to set - * @param mixed $value The value to set to the property - * - * @throws \ReflectionException - */ - public static function setStaticProperty(string $class, string $property, $value): void - { - $ref_property = new \ReflectionProperty($class, $property); - $ref_property->setAccessible(true); - $ref_property->setValue(null, $value); - } - - /** - * Return a simple fake Update object - * - * @param array $data Pass custom data array if needed - * - * @return Update - */ - public static function getFakeUpdateObject(array $data = []): Update - { - $data = $data ?: [ - 'update_id' => mt_rand(), - 'message' => [ - 'message_id' => mt_rand(), - 'chat' => [ - 'id' => mt_rand(), - ], - 'date' => time(), - ], - ]; - return new Update($data, 'testbot'); - } - - /** - * Return a fake command object for the passed command text - * - * @param string $command_text - * - * @return Update - */ - public static function getFakeUpdateCommandObject(string $command_text): Update - { - $data = [ - 'update_id' => mt_rand(), - 'message' => [ - 'message_id' => mt_rand(), - 'from' => self::$user_template, - 'chat' => self::$chat_template, - 'date' => time(), - 'text' => $command_text, - ], - ]; - return self::getFakeUpdateObject($data); - } - - /** - * Return a fake user object. - * - * @param array $data Pass custom data array if needed - * - * @return User - */ - public static function getFakeUserObject(array $data = []): User - { - ($data === null) && $data = []; - - return new User($data + self::$user_template); - } - - /** - * Return a fake chat object. - * - * @param array $data Pass custom data array if needed - * - * @return Chat - */ - public static function getFakeChatObject(array $data = []): Chat - { - return new Chat($data + self::$chat_template); - } - - /** - * Get fake recorded audio track - * - * @return array - */ - public static function getFakeRecordedAudio(): array - { - $mime_type = ['audio/ogg', 'audio/mpeg', 'audio/vnd.wave', 'audio/x-ms-wma', 'audio/basic']; - return [ - 'file_id' => mt_rand(1, 999), - 'duration' => mt_rand(1, 99) . ':' . mt_rand(1, 60), - 'performer' => 'phpunit', - 'title' => 'track from phpunit', - 'mime_type' => $mime_type[array_rand($mime_type, 1)], - 'file_size' => mt_rand(1, 99999), - ]; - } - - /** - * Return a fake message object using the passed ids. - * - * @param array $message_data Pass custom message data array if needed - * @param array $user_data Pass custom user data array if needed - * @param array $chat_data Pass custom chat data array if needed - * - * @return Message - */ - public static function getFakeMessageObject(array $message_data = [], array $user_data = [], array $chat_data = []): Message - { - return new Message($message_data + [ - 'message_id' => mt_rand(), - 'from' => $user_data + self::$user_template, - 'chat' => $chat_data + self::$chat_template, - 'date' => time(), - 'text' => 'dummy', - ], 'testbot'); - } - - /** - * Start a fake conversation for the passed command and return the randomly generated ids. - * - * @return array|false - */ - public static function startFakeConversation() - { - if (!DB::isDbConnected()) { - return false; - } - - //Just get some random values. - $message_id = mt_rand(); - $user_id = mt_rand(); - $chat_id = mt_rand(); - - try { - //Make sure we have a valid user and chat available. - $message = self::getFakeMessageObject(['message_id' => $message_id], ['id' => $user_id], ['id' => $chat_id]); - DB::insertMessageRequest($message); - DB::insertUser($message->getFrom(), null, $message->getChat()); - - return compact('message_id', 'user_id', 'chat_id'); - } catch (TelegramException $e) { - return false; - } - } - - /** - * Empty all tables for the passed database - * - * @param array $credentials - */ - public static function emptyDb(array $credentials): void - { - $dsn = 'mysql:host=' . $credentials['host'] . ';dbname=' . $credentials['database']; - if (!empty($credentials['port'])) { - $dsn .= ';port=' . $credentials['port']; - } - - $options = [\PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8']; - - $pdo = new \PDO($dsn, $credentials['user'], $credentials['password'], $options); - $pdo->prepare(' - DELETE FROM `conversation`; - DELETE FROM `telegram_update`; - DELETE FROM `chosen_inline_result`; - DELETE FROM `inline_query`; - DELETE FROM `message`; - DELETE FROM `user_chat`; - DELETE FROM `chat`; - DELETE FROM `user`; - ')->execute(); - } -} diff --git a/utils/db-schema-update/0.44.1-0.45.0.sql b/utils/db-schema-update/0.44.1-0.45.0.sql deleted file mode 100644 index 98630c2b1..000000000 --- a/utils/db-schema-update/0.44.1-0.45.0.sql +++ /dev/null @@ -1,4 +0,0 @@ -ALTER TABLE `user` ADD COLUMN `language_code` CHAR(10) DEFAULT NULL COMMENT 'User''s system language' AFTER `username`; -ALTER TABLE `message` ADD COLUMN `video_note` TEXT COMMENT 'VoiceNote Object. Message is a Video Note, information about the Video Note' AFTER `voice`; -ALTER TABLE `message` ADD COLUMN `new_chat_members` TEXT COMMENT 'List of unique user identifiers, new member(s) were added to the group, information about them (one of these members may be the bot itself)' AFTER `new_chat_member`; -UPDATE `message` SET `new_chat_members` = `new_chat_member`; diff --git a/utils/db-schema-update/0.47.1-0.48.0.sql b/utils/db-schema-update/0.47.1-0.48.0.sql deleted file mode 100644 index 85131217c..000000000 --- a/utils/db-schema-update/0.47.1-0.48.0.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE `user` ADD COLUMN `is_bot` tinyint(1) DEFAULT 0 COMMENT 'True if this user is a bot' AFTER `id`; diff --git a/utils/db-schema-update/0.50.0-0.51.0.sql b/utils/db-schema-update/0.50.0-0.51.0.sql deleted file mode 100644 index 86cae0a0e..000000000 --- a/utils/db-schema-update/0.50.0-0.51.0.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE `message` ADD COLUMN `media_group_id` TEXT COMMENT 'The unique identifier of a media message group this message belongs to' AFTER `reply_to_message`; diff --git a/utils/db-schema-update/0.52.0-0.53.0.sql b/utils/db-schema-update/0.52.0-0.53.0.sql deleted file mode 100644 index 4b95fafac..000000000 --- a/utils/db-schema-update/0.52.0-0.53.0.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE `message` ADD COLUMN `connected_website` TEXT NULL COMMENT 'The domain name of the website on which the user has logged in.' AFTER `pinned_message`; diff --git a/utils/db-schema-update/0.53.0-0.54.0.sql b/utils/db-schema-update/0.53.0-0.54.0.sql deleted file mode 100644 index 413978618..000000000 --- a/utils/db-schema-update/0.53.0-0.54.0.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE `message` ADD COLUMN `game` TEXT NULL COMMENT 'Message is a game, information about the game.' AFTER `document`; diff --git a/utils/db-schema-update/0.54.1-0.55.0.sql b/utils/db-schema-update/0.54.1-0.55.0.sql deleted file mode 100644 index 531465bee..000000000 --- a/utils/db-schema-update/0.54.1-0.55.0.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE `message` ADD COLUMN `animation` TEXT NULL COMMENT 'Message is an animation, information about the animation' AFTER `document`; -ALTER TABLE `message` ADD COLUMN `passport_data` TEXT NULL COMMENT 'Telegram Passport data' AFTER `connected_website`; diff --git a/utils/db-schema-update/0.56.0-0.57.0.sql b/utils/db-schema-update/0.56.0-0.57.0.sql deleted file mode 100644 index 120e33721..000000000 --- a/utils/db-schema-update/0.56.0-0.57.0.sql +++ /dev/null @@ -1,69 +0,0 @@ -ALTER TABLE `chat` ADD COLUMN `first_name` CHAR(255) DEFAULT NULL COMMENT 'First name of the other party in a private chat' AFTER `username`; -ALTER TABLE `chat` ADD COLUMN `last_name` CHAR(255) DEFAULT NULL COMMENT 'Last name of the other party in a private chat' AFTER `first_name`; -ALTER TABLE `message` ADD COLUMN `forward_signature` TEXT NULL DEFAULT NULL COMMENT 'For messages forwarded from channels, signature of the post author if present' AFTER `forward_from_message_id`; -ALTER TABLE `message` ADD COLUMN `forward_sender_name` TEXT NULL DEFAULT NULL COMMENT 'Sender''s name for messages forwarded from users who disallow adding a link to their account in forwarded messages' AFTER `forward_signature`; -ALTER TABLE `message` ADD COLUMN `edit_date` bigint UNSIGNED DEFAULT NULL COMMENT 'Date the message was last edited in Unix time' AFTER `reply_to_message`; -ALTER TABLE `message` ADD COLUMN `author_signature` TEXT COMMENT 'Signature of the post author for messages in channels' AFTER `media_group_id`; -ALTER TABLE `message` ADD COLUMN `caption_entities` TEXT COMMENT 'For messages with a caption, special entities like usernames, URLs, bot commands, etc. that appear in the caption'; -ALTER TABLE `message` ADD COLUMN `poll` TEXT COMMENT 'Poll object. Message is a native poll, information about the poll' AFTER `venue`; -ALTER TABLE `message` ADD COLUMN `invoice` TEXT NULL COMMENT 'Message is an invoice for a payment, information about the invoice' AFTER `pinned_message`; -ALTER TABLE `message` ADD COLUMN `successful_payment` TEXT NULL COMMENT 'Message is a service message about a successful payment, information about the payment' AFTER `invoice`; -ALTER TABLE `callback_query` ADD COLUMN `chat_instance` CHAR(255) NOT NULL DEFAULT '' COMMENT 'Global identifier, uniquely corresponding to the chat to which the message with the callback button was sent' AFTER `inline_message_id`; -ALTER TABLE `callback_query` ADD COLUMN `game_short_name` CHAR(255) NOT NULL DEFAULT '' COMMENT 'Short name of a Game to be returned, serves as the unique identifier for the game' AFTER `data`; - -CREATE TABLE IF NOT EXISTS `shipping_query` ( - `id` bigint UNSIGNED COMMENT 'Unique query identifier', - `user_id` bigint COMMENT 'User who sent the query', - `invoice_payload` CHAR(255) NOT NULL DEFAULT '' COMMENT 'Bot specified invoice payload', - `shipping_address` CHAR(255) NOT NULL DEFAULT '' COMMENT 'User specified shipping address', - `created_at` timestamp NULL DEFAULT NULL COMMENT 'Entry date creation', - - PRIMARY KEY (`id`), - KEY `user_id` (`user_id`), - - FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; - -CREATE TABLE IF NOT EXISTS `pre_checkout_query` ( - `id` bigint UNSIGNED COMMENT 'Unique query identifier', - `user_id` bigint COMMENT 'User who sent the query', - `currency` CHAR(3) COMMENT 'Three-letter ISO 4217 currency code', - `total_amount` bigint COMMENT 'Total price in the smallest units of the currency', - `invoice_payload` CHAR(255) NOT NULL DEFAULT '' COMMENT 'Bot specified invoice payload', - `shipping_option_id` CHAR(255) NULL COMMENT 'Identifier of the shipping option chosen by the user', - `order_info` TEXT NULL COMMENT 'Order info provided by the user', - `created_at` timestamp NULL DEFAULT NULL COMMENT 'Entry date creation', - - PRIMARY KEY (`id`), - KEY `user_id` (`user_id`), - - FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; - -CREATE TABLE IF NOT EXISTS `poll` ( - `id` bigint UNSIGNED COMMENT 'Unique poll identifier', - `question` char(255) NOT NULL COMMENT 'Poll question', - `options` text NOT NULL COMMENT 'List of poll options', - `is_closed` tinyint(1) DEFAULT 0 COMMENT 'True, if the poll is closed', - `created_at` timestamp NULL DEFAULT NULL COMMENT 'Entry date creation', - - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; - -ALTER TABLE `telegram_update` ADD COLUMN `channel_post_id` bigint UNSIGNED DEFAULT NULL COMMENT 'New incoming channel post of any kind - text, photo, sticker, etc.'; -ALTER TABLE `telegram_update` ADD COLUMN `edited_channel_post_id` bigint UNSIGNED DEFAULT NULL COMMENT 'New version of a channel post that is known to the bot and was edited'; -ALTER TABLE `telegram_update` ADD COLUMN `shipping_query_id` bigint UNSIGNED DEFAULT NULL COMMENT 'New incoming shipping query. Only for invoices with flexible price'; -ALTER TABLE `telegram_update` ADD COLUMN `pre_checkout_query_id` bigint UNSIGNED DEFAULT NULL COMMENT 'New incoming pre-checkout query. Contains full information about checkout'; -ALTER TABLE `telegram_update` ADD COLUMN `poll_id` bigint UNSIGNED DEFAULT NULL COMMENT 'New poll state. Bots receive only updates about polls, which are sent or stopped by the bot'; - -ALTER TABLE `telegram_update` ADD KEY `channel_post_id` (`channel_post_id`); -ALTER TABLE `telegram_update` ADD KEY `edited_channel_post_id` (`edited_channel_post_id`); -ALTER TABLE `telegram_update` ADD KEY `shipping_query_id` (`shipping_query_id`); -ALTER TABLE `telegram_update` ADD KEY `pre_checkout_query_id` (`pre_checkout_query_id`); -ALTER TABLE `telegram_update` ADD KEY `poll_id` (`poll_id`); - -ALTER TABLE `telegram_update` ADD FOREIGN KEY (`chat_id`, `channel_post_id`) REFERENCES `message` (`chat_id`, `id`); -ALTER TABLE `telegram_update` ADD FOREIGN KEY (`edited_channel_post_id`) REFERENCES `edited_message` (`id`); -ALTER TABLE `telegram_update` ADD FOREIGN KEY (`shipping_query_id`) REFERENCES `shipping_query` (`id`); -ALTER TABLE `telegram_update` ADD FOREIGN KEY (`pre_checkout_query_id`) REFERENCES `pre_checkout_query` (`id`); -ALTER TABLE `telegram_update` ADD FOREIGN KEY (`poll_id`) REFERENCES `poll` (`id`); diff --git a/utils/db-schema-update/0.57.0-0.58.0.sql b/utils/db-schema-update/0.57.0-0.58.0.sql deleted file mode 100644 index 9d48cd8e7..000000000 --- a/utils/db-schema-update/0.57.0-0.58.0.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE `message` ADD COLUMN `reply_markup` TEXT NULL COMMENT 'Inline keyboard attached to the message' AFTER `passport_data`; diff --git a/utils/db-schema-update/0.60.0-0.61.0.sql b/utils/db-schema-update/0.60.0-0.61.0.sql deleted file mode 100644 index 54b0c449c..000000000 --- a/utils/db-schema-update/0.60.0-0.61.0.sql +++ /dev/null @@ -1,10 +0,0 @@ -SET FOREIGN_KEY_CHECKS=0; - -ALTER TABLE `telegram_update` -DROP KEY `message_id`; - -ALTER TABLE `telegram_update` -ADD KEY `message_id` (`message_id`), -ADD KEY `chat_message_id` (`chat_id`, `message_id`); - -SET FOREIGN_KEY_CHECKS=1; diff --git a/utils/db-schema-update/0.61.1-0.62.0.sql b/utils/db-schema-update/0.61.1-0.62.0.sql deleted file mode 100644 index a38f361e0..000000000 --- a/utils/db-schema-update/0.61.1-0.62.0.sql +++ /dev/null @@ -1,20 +0,0 @@ -ALTER TABLE `poll` ADD COLUMN `total_voter_count` int UNSIGNED COMMENT 'Total number of users that voted in the poll' AFTER `options`; -ALTER TABLE `poll` ADD COLUMN `is_anonymous` tinyint(1) DEFAULT 1 COMMENT 'True, if the poll is anonymous' AFTER `is_closed`; -ALTER TABLE `poll` ADD COLUMN `type` char(255) COMMENT 'Poll type, currently can be “regular” or “quiz”' AFTER `is_anonymous`; -ALTER TABLE `poll` ADD COLUMN `allows_multiple_answers` tinyint(1) DEFAULT 0 COMMENT 'True, if the poll allows multiple answers' AFTER `type`; -ALTER TABLE `poll` ADD COLUMN `correct_option_id` int UNSIGNED COMMENT '0-based identifier of the correct answer option. Available only for polls in the quiz mode, which are closed, or was sent (not forwarded) by the bot or to the private chat with the bot.' AFTER `allows_multiple_answers`; -ALTER TABLE `message` ADD COLUMN `dice` TEXT COMMENT 'Message is a dice with random value from 1 to 6' AFTER `poll`; - -CREATE TABLE IF NOT EXISTS `poll_answer` ( - `poll_id` bigint UNSIGNED COMMENT 'Unique poll identifier', - `user_id` bigint NOT NULL COMMENT 'The user, who changed the answer to the poll', - `option_ids` text NOT NULL COMMENT '0-based identifiers of answer options, chosen by the user. May be empty if the user retracted their vote.', - `created_at` timestamp NULL DEFAULT NULL COMMENT 'Entry date creation', - - PRIMARY KEY (`poll_id`), - FOREIGN KEY (`poll_id`) REFERENCES `poll` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; - -ALTER TABLE `telegram_update` ADD COLUMN `poll_answer_poll_id` bigint UNSIGNED DEFAULT NULL COMMENT 'A user changed their answer in a non-anonymous poll. Bots receive new votes only in polls that were sent by the bot itself.' AFTER `poll_id`; -ALTER TABLE `telegram_update` ADD KEY `poll_answer_poll_id` (`poll_answer_poll_id`); -ALTER TABLE `telegram_update` ADD FOREIGN KEY (`poll_answer_poll_id`) REFERENCES `poll_answer` (`poll_id`); diff --git a/utils/db-schema-update/0.62.0-0.63.0.sql b/utils/db-schema-update/0.62.0-0.63.0.sql deleted file mode 100644 index 6e1ce95b3..000000000 --- a/utils/db-schema-update/0.62.0-0.63.0.sql +++ /dev/null @@ -1,15 +0,0 @@ -ALTER TABLE `poll` ADD COLUMN `explanation` varchar(255) DEFAULT NULL COMMENT 'Text that is shown when a user chooses an incorrect answer or taps on the lamp icon in a quiz-style poll, 0-200 characters' AFTER `correct_option_id`; -ALTER TABLE `poll` ADD COLUMN `explanation_entities` text DEFAULT NULL COMMENT 'Special entities like usernames, URLs, bot commands, etc. that appear in the explanation' AFTER `explanation`; -ALTER TABLE `poll` ADD COLUMN `open_period` int UNSIGNED DEFAULT NULL COMMENT 'Amount of time in seconds the poll will be active after creation' AFTER `explanation_entities`; -ALTER TABLE `poll` ADD COLUMN `close_date` timestamp NULL DEFAULT NULL COMMENT 'Point in time (Unix timestamp) when the poll will be automatically closed' AFTER `open_period`; - -ALTER TABLE `poll_answer` DROP PRIMARY KEY, ADD PRIMARY KEY (`poll_id`, `user_id`); - -ALTER TABLE `message` - DROP FOREIGN KEY IF EXISTS `message_ibfk_6`, - DROP INDEX IF EXISTS `message_ibfk_6`; - -ALTER TABLE `message` - ADD COLUMN `via_bot` bigint NULL DEFAULT NULL COMMENT 'Optional. Bot through which the message was sent' AFTER `reply_to_message`, - ADD KEY `via_bot` (`via_bot`), - ADD FOREIGN KEY (`via_bot`) REFERENCES `user` (`id`); diff --git a/utils/db-schema-update/0.64.0-0.70.0.sql b/utils/db-schema-update/0.64.0-0.70.0.sql deleted file mode 100644 index daa708720..000000000 --- a/utils/db-schema-update/0.64.0-0.70.0.sql +++ /dev/null @@ -1,3 +0,0 @@ -ALTER TABLE `message` ADD COLUMN `sender_chat_id` bigint COMMENT 'Sender of the message, sent on behalf of a chat' AFTER `chat_id`; -ALTER TABLE `message` ADD COLUMN `proximity_alert_triggered` TEXT NULL COMMENT 'Service message. A user in the chat triggered another user''s proximity alert while sharing Live Location.' AFTER `passport_data`; -ALTER TABLE `poll` MODIFY `question` text NOT NULL COMMENT 'Poll question'; diff --git a/utils/db-schema-update/0.71.0-0.72.0.sql b/utils/db-schema-update/0.71.0-0.72.0.sql deleted file mode 100644 index 0720ae591..000000000 --- a/utils/db-schema-update/0.71.0-0.72.0.sql +++ /dev/null @@ -1,27 +0,0 @@ -ALTER TABLE `message` MODIFY `edit_date` timestamp NULL DEFAULT NULL COMMENT 'Date the message was last edited in Unix time'; - -CREATE TABLE IF NOT EXISTS `chat_member_updated` ( - `id` BIGINT UNSIGNED AUTO_INCREMENT COMMENT 'Unique identifier for this entry', - `chat_id` BIGINT NOT NULL COMMENT 'Chat the user belongs to', - `user_id` BIGINT NOT NULL COMMENT 'Performer of the action, which resulted in the change', - `date` TIMESTAMP NOT NULL COMMENT 'Date the change was done in Unix time', - `old_chat_member` TEXT NOT NULL COMMENT 'Previous information about the chat member', - `new_chat_member` TEXT NOT NULL COMMENT 'New information about the chat member', - `invite_link` TEXT NULL COMMENT 'Chat invite link, which was used by the user to join the chat; for joining by invite link events only', - `created_at` timestamp NULL DEFAULT NULL COMMENT 'Entry date creation', - - PRIMARY KEY (`id`), - - FOREIGN KEY (`chat_id`) REFERENCES `chat` (`id`), - FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; - -ALTER TABLE `telegram_update` ADD COLUMN `my_chat_member_updated_id` BIGINT UNSIGNED NULL COMMENT 'The bot''s chat member status was updated in a chat. For private chats, this update is received only when the bot is blocked or unblocked by the user.'; -ALTER TABLE `telegram_update` ADD FOREIGN KEY (`my_chat_member_updated_id`) REFERENCES `chat_member_updated` (`id`); -ALTER TABLE `telegram_update` ADD COLUMN `chat_member_updated_id` BIGINT UNSIGNED NULL COMMENT 'A chat member''s status was updated in a chat. The bot must be an administrator in the chat and must explicitly specify “chat_member” in the list of allowed_updates to receive these updates.'; -ALTER TABLE `telegram_update` ADD FOREIGN KEY (`chat_member_updated_id`) REFERENCES `chat_member_updated` (`id`); - -ALTER TABLE `message` ADD COLUMN `message_auto_delete_timer_changed` TEXT COMMENT 'MessageAutoDeleteTimerChanged object. Message is a service message: auto-delete timer settings changed in the chat' AFTER `channel_chat_created`; -ALTER TABLE `message` ADD COLUMN `voice_chat_started` TEXT COMMENT 'VoiceChatStarted object. Message is a service message: voice chat started' AFTER `proximity_alert_triggered`; -ALTER TABLE `message` ADD COLUMN `voice_chat_ended` TEXT COMMENT 'VoiceChatEnded object. Message is a service message: voice chat ended' AFTER `voice_chat_started`; -ALTER TABLE `message` ADD COLUMN `voice_chat_participants_invited` TEXT COMMENT 'VoiceChatParticipantsInvited object. Message is a service message: new participants invited to a voice chat' AFTER `voice_chat_ended`; diff --git a/utils/db-schema-update/0.72.0-0.73.0.sql b/utils/db-schema-update/0.72.0-0.73.0.sql deleted file mode 100644 index 2664a990c..000000000 --- a/utils/db-schema-update/0.72.0-0.73.0.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE `message` ADD COLUMN `voice_chat_scheduled` TEXT COMMENT 'VoiceChatScheduled object. Message is a service message: voice chat scheduled' AFTER `proximity_alert_triggered`; -ALTER TABLE `inline_query` ADD COLUMN `chat_type` CHAR(255) NULL DEFAULT NULL COMMENT 'Optional. Type of the chat, from which the inline query was sent.' AFTER `offset`; diff --git a/utils/db-schema-update/0.74.0-0.75.0.sql b/utils/db-schema-update/0.74.0-0.75.0.sql deleted file mode 100644 index 6bbf51422..000000000 --- a/utils/db-schema-update/0.74.0-0.75.0.sql +++ /dev/null @@ -1,20 +0,0 @@ -CREATE TABLE IF NOT EXISTS `chat_join_request` ( - `id` BIGINT UNSIGNED AUTO_INCREMENT COMMENT 'Unique identifier for this entry', - `chat_id` BIGINT NOT NULL COMMENT 'Chat to which the request was sent', - `user_id` BIGINT NOT NULL COMMENT 'User that sent the join request', - `date` TIMESTAMP NOT NULL COMMENT 'Date the request was sent in Unix time', - `bio` TEXT NULL COMMENT 'Optional. Bio of the user', - `invite_link` TEXT NULL COMMENT 'Optional. Chat invite link that was used by the user to send the join request', - `created_at` timestamp NULL DEFAULT NULL COMMENT 'Entry date creation', - - PRIMARY KEY (`id`), - - FOREIGN KEY (`chat_id`) REFERENCES `chat` (`id`), - FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; - -ALTER TABLE `telegram_update` ADD COLUMN `chat_join_request_id` BIGINT UNSIGNED NULL COMMENT 'A request to join the chat has been sent'; -ALTER TABLE `telegram_update` ADD FOREIGN KEY (`chat_join_request_id`) REFERENCES `chat_join_request` (`id`); - -ALTER TABLE `message` ADD COLUMN `is_automatic_forward` tinyint(1) DEFAULT 0 COMMENT 'True, if the message is a channel post that was automatically forwarded to the connected discussion group' AFTER `forward_date`; -ALTER TABLE `message` ADD COLUMN `has_protected_content` tinyint(1) DEFAULT 0 COMMENT 'True, if the message can''t be forwarded' AFTER `edit_date`; diff --git a/utils/db-schema-update/0.76.1-0.77.0.sql b/utils/db-schema-update/0.76.1-0.77.0.sql deleted file mode 100644 index 56c518489..000000000 --- a/utils/db-schema-update/0.76.1-0.77.0.sql +++ /dev/null @@ -1,7 +0,0 @@ -ALTER TABLE `message` ADD COLUMN `web_app_data` TEXT NULL DEFAULT NULL COMMENT 'Service message: data sent by a Web App' AFTER `voice_chat_participants_invited`; - -ALTER TABLE `message` - CHANGE `voice_chat_scheduled` `video_chat_scheduled` TEXT COMMENT 'Service message: video chat scheduled', - CHANGE `voice_chat_started` `video_chat_started` TEXT COMMENT 'Service message: video chat started', - CHANGE `voice_chat_ended` `video_chat_ended` TEXT COMMENT 'Service message: video chat ended', - CHANGE `voice_chat_participants_invited` `video_chat_participants_invited` TEXT COMMENT 'Service message: new participants invited to a video chat'; diff --git a/utils/db-schema-update/0.77.1-0.78.0.sql b/utils/db-schema-update/0.77.1-0.78.0.sql deleted file mode 100644 index 370ee6f73..000000000 --- a/utils/db-schema-update/0.77.1-0.78.0.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE `user` ADD COLUMN `is_premium` tinyint(1) DEFAULT 0 COMMENT 'True, if this user is a Telegram Premium user' AFTER `language_code`; -ALTER TABLE `user` ADD COLUMN `added_to_attachment_menu` tinyint(1) DEFAULT 0 COMMENT 'True, if this user added the bot to the attachment menu' AFTER `is_premium`; diff --git a/utils/db-schema-update/0.79.0-0.80.0.sql b/utils/db-schema-update/0.79.0-0.80.0.sql deleted file mode 100644 index bba84eb43..000000000 --- a/utils/db-schema-update/0.79.0-0.80.0.sql +++ /dev/null @@ -1,9 +0,0 @@ -ALTER TABLE `message` - ADD COLUMN `is_topic_message` TINYINT(1) DEFAULT 0 COMMENT 'True, if the message is sent to a forum topic' AFTER `forward_date`, - ADD COLUMN `message_thread_id` BIGINT(20) NULL DEFAULT NULL COMMENT 'Unique identifier of a message thread to which the message belongs; for supergroups only' AFTER `id`, - ADD COLUMN `forum_topic_created` TEXT NULL DEFAULT NULL COMMENT 'Service message: forum topic created' AFTER `proximity_alert_triggered`, - ADD COLUMN `forum_topic_closed` TEXT NULL DEFAULT NULL COMMENT 'Service message: forum topic closed' AFTER `forum_topic_created`, - ADD COLUMN `forum_topic_reopened` TEXT NULL DEFAULT NULL COMMENT 'Service message: forum topic reopened' AFTER `forum_topic_closed`; - -ALTER TABLE `chat` - ADD COLUMN `is_forum` TINYINT(1) DEFAULT 0 COMMENT 'True, if the supergroup chat is a forum (has topics enabled)' AFTER `last_name`; diff --git a/utils/db-schema-update/0.80.0-0.81.0.sql b/utils/db-schema-update/0.80.0-0.81.0.sql deleted file mode 100644 index 4411b1201..000000000 --- a/utils/db-schema-update/0.80.0-0.81.0.sql +++ /dev/null @@ -1,8 +0,0 @@ -ALTER TABLE `message` - ADD COLUMN `has_media_spoiler` TINYINT(1) DEFAULT 0 COMMENT 'True, if the message media is covered by a spoiler animation' AFTER `caption`, - ADD COLUMN `write_access_allowed` TEXT DEFAULT NULL COMMENT 'Service message: the user allowed the bot added to the attachment menu to write messages' AFTER `connected_website`, - ADD COLUMN `forum_topic_edited` TEXT DEFAULT NULL COMMENT 'Service message: forum topic edited' AFTER `forum_topic_created`, - ADD COLUMN `general_forum_topic_hidden` TEXT DEFAULT NULL COMMENT 'Service message: the General forum topic hidden' AFTER `forum_topic_reopened`, - ADD COLUMN `general_forum_topic_unhidden` TEXT DEFAULT NULL COMMENT 'Service message: the General forum topic unhidden' AFTER `general_forum_topic_hidden`, - ADD COLUMN `user_shared` TEXT DEFAULT NULL COMMENT 'Optional. Service message: a user was shared with the bot' AFTER `successful_payment`, - ADD COLUMN `chat_shared` TEXT DEFAULT NULL COMMENT 'Optional. Service message: a chat was shared with the bot' AFTER `user_shared`; diff --git a/utils/db-schema-update/0.81.0-0.82.0.sql b/utils/db-schema-update/0.81.0-0.82.0.sql deleted file mode 100644 index bae194cb9..000000000 --- a/utils/db-schema-update/0.81.0-0.82.0.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE `message` - ADD COLUMN `story` TEXT DEFAULT NULL COMMENT 'Story object. Message is a forwarded story' AFTER `sticker`; diff --git a/utils/db-schema-update/0.82.0-unreleased.sql b/utils/db-schema-update/0.82.0-unreleased.sql deleted file mode 100644 index 3fb04f377..000000000 --- a/utils/db-schema-update/0.82.0-unreleased.sql +++ /dev/null @@ -1,67 +0,0 @@ -CREATE TABLE IF NOT EXISTS `message_reaction` ( - `id` bigint UNSIGNED AUTO_INCREMENT COMMENT 'Unique identifier for this entry', - `chat_id` bigint COMMENT 'The chat containing the message the user reacted to', - `message_id` bigint COMMENT 'Unique identifier of the message inside the chat', - `user_id` bigint NULL COMMENT 'Optional. The user that changed the reaction, if the user isn''t anonymous', - `actor_chat_id` bigint NULL COMMENT 'Optional. The chat on behalf of which the reaction was changed, if the user is anonymous', - `old_reaction` TEXT NOT NULL COMMENT 'Previous list of reaction types that were set by the user', - `new_reaction` TEXT NOT NULL COMMENT 'New list of reaction types that have been set by the user', - `created_at` timestamp NULL DEFAULT NULL COMMENT 'Entry date creation', - - PRIMARY KEY (`id`), - KEY `chat_id` (`chat_id`), - KEY `user_id` (`user_id`), - KEY `actor_chat_id` (`actor_chat_id`), - - FOREIGN KEY (`chat_id`) REFERENCES `chat` (`id`), - FOREIGN KEY (`user_id`) REFERENCES `user` (`id`), - FOREIGN KEY (`actor_chat_id`) REFERENCES `chat` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; - -CREATE TABLE IF NOT EXISTS `message_reaction_count` ( - `id` bigint UNSIGNED AUTO_INCREMENT COMMENT 'Unique identifier for this entry', - `chat_id` bigint COMMENT 'The chat containing the message', - `message_id` bigint COMMENT 'Unique message identifier inside the chat', - `reactions` TEXT NOT NULL COMMENT 'List of reactions that are present on the message', - `created_at` timestamp NULL DEFAULT NULL COMMENT 'Entry date creation', - - PRIMARY KEY (`id`), - KEY `chat_id` (`chat_id`), - - FOREIGN KEY (`chat_id`) REFERENCES `chat` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; - -CREATE TABLE IF NOT EXISTS `chat_boost_updated` ( - `id` bigint UNSIGNED AUTO_INCREMENT COMMENT 'Unique identifier for this entry', - `chat_id` bigint COMMENT 'Chat which was boosted', - `boost` TEXT NOT NULL COMMENT 'Information about the chat boost', - `created_at` timestamp NULL DEFAULT NULL COMMENT 'Entry date creation', - - PRIMARY KEY (`id`), - KEY `chat_id` (`chat_id`), - - FOREIGN KEY (`chat_id`) REFERENCES `chat` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; - -CREATE TABLE IF NOT EXISTS `chat_boost_removed` ( - `id` bigint UNSIGNED AUTO_INCREMENT COMMENT 'Unique identifier for this entry', - `chat_id` bigint COMMENT 'Chat which was boosted', - `boost_id` varchar(200) NOT NULL COMMENT 'Unique identifier of the boost', - `remove_date` timestamp NOT NULL COMMENT 'Point in time (Unix timestamp) when the boost was removed', - `source` TEXT NOT NULL COMMENT 'Source of the removed boost', - `created_at` timestamp NULL DEFAULT NULL COMMENT 'Entry date creation', - - PRIMARY KEY (`id`), - KEY `chat_id` (`chat_id`), - - FOREIGN KEY (`chat_id`) REFERENCES `chat` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; - -ALTER TABLE `message` - ADD COLUMN `external_reply` TEXT NULL DEFAULT NULL COMMENT 'Optional. Information about the message that is being replied to, which may come from another chat or forum topic' AFTER `reply_to_message`, - ADD COLUMN `link_preview_options` TEXT NULL DEFAULT NULL COMMENT 'Optional. Options used for link preview generation for the message, if it is a text message and link preview options were changed' AFTER `via_bot`, - CHANGE COLUMN `user_shared` `users_shared` TEXT; - -ALTER TABLE `telegram_update` - ADD COLUMN `message_reaction_id` bigint UNSIGNED DEFAULT NULL COMMENT 'A reaction to a message was changed by a user' AFTER `edited_channel_post_id`, - ADD COLUMN `message_reaction_count_id` bigint UNSIGNED DEFAULT NULL COMMENT 'Reactions to a message with anonymous reactions were changed' AFTER `message_reaction_id`; diff --git a/utils/importFromLog.php b/utils/importFromLog.php deleted file mode 100644 index 634df0260..000000000 --- a/utils/importFromLog.php +++ /dev/null @@ -1,59 +0,0 @@ - 'localhost', - 'port' => 3306, // optional - 'user' => 'dbuser', - 'password' => 'dbpass', - 'database' => 'dbname', -]; - -try { - // Create dummy Telegram API object and connect to MySQL database. - (new Telegram('1:A'))->enableMySql($mysql_credentials); - - // Load the updates log file to iterate over. - $updates_log_file = new SplFileObject($updates_log_file_path); - $updates_log_file->setFlags(SplFileObject::DROP_NEW_LINE | SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY); - - foreach ($updates_log_file as $update_json) { - if ($update_arr = json_decode($update_json, true)) { - echo $update_json . PHP_EOL; - - // Get all updates on this line. - $updates_data = array_filter($update_arr['result'] ?? [$update_arr]); - foreach ($updates_data as $update_data) { - $update = new Update($update_data); - printf( - 'Update ID %d %s' . PHP_EOL, - $update->getUpdateId(), - DB::insertRequest($update) ? '(success)' : '(failed) ' . implode(' ', DB::getPdo()->errorInfo()) - ); - } - } - } -} catch (Throwable $e) { - // Output any errors. - echo $e; -} From 5898a32b97a272cbc4e19058fb2fc6403ca33c5d Mon Sep 17 00:00:00 2001 From: Tii Date: Sat, 8 Jun 2024 16:51:08 +0200 Subject: [PATCH 02/20] Fixed project title in README --- README.md | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d9a1188f7..8e14c9df0 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Php Telegram Bot Logo -

project_title

+

PHP Telegram Bot

A Telegram Bot based on the official Telegram Bot API @@ -61,6 +61,7 @@ + ## About The Project _TODO_ @@ -70,6 +71,7 @@ _TODO_ + ## Getting Started _TODO_ @@ -87,6 +89,7 @@ _TODO_ + ## Usage _TODO_ @@ -96,9 +99,10 @@ _TODO_ + ## Roadmap -- [ ] Adding Telegram Types +- [ ] Adding Telegram Types - [ ] Adding base functionality to call API Methods - [ ] Adding base functionality to receive Updates - [ ] ... @@ -108,11 +112,14 @@ _TODO_ + ## Contributing -Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. +Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any +contributions you make are **greatly appreciated**. -If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". +If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also +simply open an issue with the tag "enhancement". Don't forget to give the project a star! Thanks again! 1. Fork the Project @@ -126,6 +133,7 @@ Don't forget to give the project a star! Thanks again! + ## License Distributed under the MIT License. See `LICENSE.txt` for more information. @@ -135,6 +143,7 @@ Distributed under the MIT License. See `LICENSE.txt` for more information. + ## Contact Tii - [@Tii@chaos.social](https://chaos.social/@Tii) @@ -146,6 +155,7 @@ Project Link: [https://github.com/php-telegram-bot/core](https://github.com/php- + ## Acknowledgments * Big Thanks to [Avtandil Kikabidze aka LONGMAN](https://github.com/akalongman), who created this library @@ -156,13 +166,23 @@ Project Link: [https://github.com/php-telegram-bot/core](https://github.com/php- + [contributors-shield]: https://img.shields.io/github/contributors/php-telegram-bot/core.svg?style=for-the-badge + [contributors-url]: https://github.com/php-telegram-bot/core/graphs/contributors + [forks-shield]: https://img.shields.io/github/forks/php-telegram-bot/core.svg?style=for-the-badge + [forks-url]: https://github.com/php-telegram-bot/core/network/members + [stars-shield]: https://img.shields.io/github/stars/php-telegram-bot/core.svg?style=for-the-badge + [stars-url]: https://github.com/php-telegram-bot/core/stargazers + [issues-shield]: https://img.shields.io/github/issues/php-telegram-bot/core.svg?style=for-the-badge + [issues-url]: https://github.com/php-telegram-bot/core/issues + [license-shield]: https://img.shields.io/github/license/php-telegram-bot/core.svg?style=for-the-badge + [license-url]: https://github.com/php-telegram-bot/core/blob/master/LICENSE.txt \ No newline at end of file From acc37bd8317216ab773c68ae80c676f061980fb0 Mon Sep 17 00:00:00 2001 From: Tii Date: Sun, 9 Jun 2024 20:59:21 +0200 Subject: [PATCH 03/20] Added first batch of types --- pint.json | 6 + src/Entities/Animation.php | 24 +++ src/Entities/Audio.php | 24 +++ .../BackgroundFill/BackgroundFill.php | 27 +++ .../BackgroundFillFreeformGradient.php | 11 ++ .../BackgroundFill/BackgroundFillGradient.php | 13 ++ .../BackgroundFill/BackgroundFillSolid.php | 11 ++ .../BackgroundType/BackgroundType.php | 30 ++++ .../BackgroundTypeChatTheme.php | 11 ++ .../BackgroundType/BackgroundTypeFill.php | 19 +++ .../BackgroundType/BackgroundTypePattern.php | 24 +++ .../BackgroundTypeWallpaper.php | 21 +++ src/Entities/BusinessConnection.php | 21 +++ src/Entities/BusinessMessagesDeleted.php | 18 ++ src/Entities/CallbackGame.php | 8 + src/Entities/CallbackQuery.php | 23 +++ src/Entities/Chat.php | 17 ++ src/Entities/ChatBackground.php | 18 ++ src/Entities/ChatBoost.php | 21 +++ src/Entities/ChatBoostAdded.php | 11 ++ src/Entities/ChatBoostRemoved.php | 22 +++ .../ChatBoostSource/ChatBoostSource.php | 27 +++ .../ChatBoostSourceGiftCode.php | 18 ++ .../ChatBoostSourceGiveaway.php | 20 +++ .../ChatBoostSourcePremium.php | 18 ++ src/Entities/ChatBoostUpdated.php | 18 ++ src/Entities/ChatInviteLink.php | 24 +++ src/Entities/ChatJoinRequest.php | 23 +++ src/Entities/ChatMember/ChatMember.php | 36 ++++ .../ChatMember/ChatMemberAdministrator.php | 35 ++++ src/Entities/ChatMember/ChatMemberBanned.php | 20 +++ src/Entities/ChatMember/ChatMemberLeft.php | 18 ++ src/Entities/ChatMember/ChatMemberMember.php | 18 ++ src/Entities/ChatMember/ChatMemberOwner.php | 20 +++ .../ChatMember/ChatMemberRestricted.php | 34 ++++ src/Entities/ChatMemberUpdated.php | 29 ++++ src/Entities/ChatShared.php | 20 +++ src/Entities/ChosenInlineResult.php | 21 +++ src/Entities/Contact.php | 15 ++ src/Entities/Dice.php | 22 +++ src/Entities/Document.php | 21 +++ src/Entities/EncryptedCredentials.php | 13 ++ src/Entities/EncryptedPassportElement.php | 55 ++++++ src/Entities/Entity.php | 88 ++++++++++ src/Entities/ExternalReplyInfo.php | 60 +++++++ src/Entities/Factory.php | 8 + src/Entities/File.php | 14 ++ src/Entities/ForumTopicClosed.php | 8 + src/Entities/ForumTopicCreated.php | 13 ++ src/Entities/ForumTopicEdited.php | 12 ++ src/Entities/ForumTopicReopened.php | 8 + src/Entities/Game.php | 23 +++ src/Entities/GeneralForumTopicHidden.php | 8 + src/Entities/GeneralForumTopicUnhidden.php | 8 + src/Entities/Giveaway.php | 23 +++ src/Entities/GiveawayCompleted.php | 18 ++ src/Entities/GiveawayCreated.php | 8 + src/Entities/GiveawayWinners.php | 27 +++ src/Entities/InaccessibleMessage.php | 13 ++ src/Entities/InlineKeyboardButton.php | 27 +++ src/Entities/InlineKeyboardMarkup.php | 13 ++ src/Entities/InlineQuery.php | 22 +++ src/Entities/Invoice.php | 15 ++ src/Entities/LinkPreviewOptions.php | 15 ++ src/Entities/Location.php | 16 ++ src/Entities/LoginUrl.php | 14 ++ src/Entities/MaskPosition.php | 20 +++ src/Entities/MaybeInaccessibleMessage.php | 26 +++ src/Entities/Message.php | 157 ++++++++++++++++++ .../MessageAutoDeleteTimerChanged.php | 11 ++ src/Entities/MessageEntity.php | 60 +++++++ src/Entities/MessageOrigin/MessageOrigin.php | 23 +++ .../MessageOrigin/MessageOriginChannel.php | 20 +++ .../MessageOrigin/MessageOriginChat.php | 19 +++ .../MessageOrigin/MessageOriginHiddenUser.php | 11 ++ .../MessageOrigin/MessageOriginUser.php | 18 ++ src/Entities/MessageReactionCountUpdated.php | 20 +++ src/Entities/MessageReactionUpdated.php | 28 ++++ src/Entities/OrderInfo.php | 19 +++ src/Entities/PassportData.php | 18 ++ src/Entities/PassportFile.php | 14 ++ src/Entities/PhotoSize.php | 15 ++ src/Entities/Poll.php | 35 ++++ src/Entities/PollAnswer.php | 20 +++ src/Entities/PollOption.php | 18 ++ src/Entities/PreCheckoutQuery.php | 23 +++ src/Entities/ProximityAlertTriggered.php | 19 +++ src/Entities/ReactionCount.php | 19 +++ src/Entities/ReactionType/ReactionType.php | 24 +++ .../ReactionType/ReactionTypeCustomEmoji.php | 11 ++ .../ReactionType/ReactionTypeEmoji.php | 11 ++ src/Entities/SharedUser.php | 20 +++ src/Entities/ShippingAddress.php | 16 ++ src/Entities/ShippingQuery.php | 20 +++ src/Entities/Sticker.php | 38 +++++ src/Entities/Story.php | 17 ++ src/Entities/SuccessfulPayment.php | 22 +++ src/Entities/SwitchInlineQueryChosenChat.php | 15 ++ src/Entities/TextQuote.php | 19 +++ src/Entities/Update.php | 59 +++++++ src/Entities/User.php | 22 +++ src/Entities/UsersShared.php | 17 ++ src/Entities/Venue.php | 22 +++ src/Entities/Video.php | 24 +++ src/Entities/VideoChatEnded.php | 11 ++ src/Entities/VideoChatParticipantsInvited.php | 16 ++ src/Entities/VideoChatScheduled.php | 11 ++ src/Entities/VideoChatStarted.php | 8 + src/Entities/VideoNote.php | 21 +++ src/Entities/Voice.php | 15 ++ src/Entities/WebAppData.php | 12 ++ src/Entities/WebAppInfo.php | 11 ++ src/Entities/WriteAccessAllowed.php | 13 ++ src/Types/Poll.php | 26 --- src/Types/Type.php | 18 -- 115 files changed, 2454 insertions(+), 44 deletions(-) create mode 100644 pint.json create mode 100644 src/Entities/Animation.php create mode 100644 src/Entities/Audio.php create mode 100644 src/Entities/BackgroundFill/BackgroundFill.php create mode 100644 src/Entities/BackgroundFill/BackgroundFillFreeformGradient.php create mode 100644 src/Entities/BackgroundFill/BackgroundFillGradient.php create mode 100644 src/Entities/BackgroundFill/BackgroundFillSolid.php create mode 100644 src/Entities/BackgroundType/BackgroundType.php create mode 100644 src/Entities/BackgroundType/BackgroundTypeChatTheme.php create mode 100644 src/Entities/BackgroundType/BackgroundTypeFill.php create mode 100644 src/Entities/BackgroundType/BackgroundTypePattern.php create mode 100644 src/Entities/BackgroundType/BackgroundTypeWallpaper.php create mode 100644 src/Entities/BusinessConnection.php create mode 100644 src/Entities/BusinessMessagesDeleted.php create mode 100644 src/Entities/CallbackGame.php create mode 100644 src/Entities/CallbackQuery.php create mode 100644 src/Entities/Chat.php create mode 100644 src/Entities/ChatBackground.php create mode 100644 src/Entities/ChatBoost.php create mode 100644 src/Entities/ChatBoostAdded.php create mode 100644 src/Entities/ChatBoostRemoved.php create mode 100644 src/Entities/ChatBoostSource/ChatBoostSource.php create mode 100644 src/Entities/ChatBoostSource/ChatBoostSourceGiftCode.php create mode 100644 src/Entities/ChatBoostSource/ChatBoostSourceGiveaway.php create mode 100644 src/Entities/ChatBoostSource/ChatBoostSourcePremium.php create mode 100644 src/Entities/ChatBoostUpdated.php create mode 100644 src/Entities/ChatInviteLink.php create mode 100644 src/Entities/ChatJoinRequest.php create mode 100644 src/Entities/ChatMember/ChatMember.php create mode 100644 src/Entities/ChatMember/ChatMemberAdministrator.php create mode 100644 src/Entities/ChatMember/ChatMemberBanned.php create mode 100644 src/Entities/ChatMember/ChatMemberLeft.php create mode 100644 src/Entities/ChatMember/ChatMemberMember.php create mode 100644 src/Entities/ChatMember/ChatMemberOwner.php create mode 100644 src/Entities/ChatMember/ChatMemberRestricted.php create mode 100644 src/Entities/ChatMemberUpdated.php create mode 100644 src/Entities/ChatShared.php create mode 100644 src/Entities/ChosenInlineResult.php create mode 100644 src/Entities/Contact.php create mode 100644 src/Entities/Dice.php create mode 100644 src/Entities/Document.php create mode 100644 src/Entities/EncryptedCredentials.php create mode 100644 src/Entities/EncryptedPassportElement.php create mode 100644 src/Entities/Entity.php create mode 100644 src/Entities/ExternalReplyInfo.php create mode 100644 src/Entities/Factory.php create mode 100644 src/Entities/File.php create mode 100644 src/Entities/ForumTopicClosed.php create mode 100644 src/Entities/ForumTopicCreated.php create mode 100644 src/Entities/ForumTopicEdited.php create mode 100644 src/Entities/ForumTopicReopened.php create mode 100644 src/Entities/Game.php create mode 100644 src/Entities/GeneralForumTopicHidden.php create mode 100644 src/Entities/GeneralForumTopicUnhidden.php create mode 100644 src/Entities/Giveaway.php create mode 100644 src/Entities/GiveawayCompleted.php create mode 100644 src/Entities/GiveawayCreated.php create mode 100644 src/Entities/GiveawayWinners.php create mode 100644 src/Entities/InaccessibleMessage.php create mode 100644 src/Entities/InlineKeyboardButton.php create mode 100644 src/Entities/InlineKeyboardMarkup.php create mode 100644 src/Entities/InlineQuery.php create mode 100644 src/Entities/Invoice.php create mode 100644 src/Entities/LinkPreviewOptions.php create mode 100644 src/Entities/Location.php create mode 100644 src/Entities/LoginUrl.php create mode 100644 src/Entities/MaskPosition.php create mode 100644 src/Entities/MaybeInaccessibleMessage.php create mode 100644 src/Entities/Message.php create mode 100644 src/Entities/MessageAutoDeleteTimerChanged.php create mode 100644 src/Entities/MessageEntity.php create mode 100644 src/Entities/MessageOrigin/MessageOrigin.php create mode 100644 src/Entities/MessageOrigin/MessageOriginChannel.php create mode 100644 src/Entities/MessageOrigin/MessageOriginChat.php create mode 100644 src/Entities/MessageOrigin/MessageOriginHiddenUser.php create mode 100644 src/Entities/MessageOrigin/MessageOriginUser.php create mode 100644 src/Entities/MessageReactionCountUpdated.php create mode 100644 src/Entities/MessageReactionUpdated.php create mode 100644 src/Entities/OrderInfo.php create mode 100644 src/Entities/PassportData.php create mode 100644 src/Entities/PassportFile.php create mode 100644 src/Entities/PhotoSize.php create mode 100644 src/Entities/Poll.php create mode 100644 src/Entities/PollAnswer.php create mode 100644 src/Entities/PollOption.php create mode 100644 src/Entities/PreCheckoutQuery.php create mode 100644 src/Entities/ProximityAlertTriggered.php create mode 100644 src/Entities/ReactionCount.php create mode 100644 src/Entities/ReactionType/ReactionType.php create mode 100644 src/Entities/ReactionType/ReactionTypeCustomEmoji.php create mode 100644 src/Entities/ReactionType/ReactionTypeEmoji.php create mode 100644 src/Entities/SharedUser.php create mode 100644 src/Entities/ShippingAddress.php create mode 100644 src/Entities/ShippingQuery.php create mode 100644 src/Entities/Sticker.php create mode 100644 src/Entities/Story.php create mode 100644 src/Entities/SuccessfulPayment.php create mode 100644 src/Entities/SwitchInlineQueryChosenChat.php create mode 100644 src/Entities/TextQuote.php create mode 100644 src/Entities/Update.php create mode 100644 src/Entities/User.php create mode 100644 src/Entities/UsersShared.php create mode 100644 src/Entities/Venue.php create mode 100644 src/Entities/Video.php create mode 100644 src/Entities/VideoChatEnded.php create mode 100644 src/Entities/VideoChatParticipantsInvited.php create mode 100644 src/Entities/VideoChatScheduled.php create mode 100644 src/Entities/VideoChatStarted.php create mode 100644 src/Entities/VideoNote.php create mode 100644 src/Entities/Voice.php create mode 100644 src/Entities/WebAppData.php create mode 100644 src/Entities/WebAppInfo.php create mode 100644 src/Entities/WriteAccessAllowed.php delete mode 100644 src/Types/Poll.php delete mode 100644 src/Types/Type.php diff --git a/pint.json b/pint.json new file mode 100644 index 000000000..9d6485480 --- /dev/null +++ b/pint.json @@ -0,0 +1,6 @@ +{ + "preset": "laravel", + "rules": { + "phpdoc_align": true + } +} \ No newline at end of file diff --git a/src/Entities/Animation.php b/src/Entities/Animation.php new file mode 100644 index 000000000..561a2368d --- /dev/null +++ b/src/Entities/Animation.php @@ -0,0 +1,24 @@ + PhotoSize::class, + ]; + } +} diff --git a/src/Entities/Audio.php b/src/Entities/Audio.php new file mode 100644 index 000000000..6689e4a68 --- /dev/null +++ b/src/Entities/Audio.php @@ -0,0 +1,24 @@ + PhotoSize::class, + ]; + } +} diff --git a/src/Entities/BackgroundFill/BackgroundFill.php b/src/Entities/BackgroundFill/BackgroundFill.php new file mode 100644 index 000000000..69377f576 --- /dev/null +++ b/src/Entities/BackgroundFill/BackgroundFill.php @@ -0,0 +1,27 @@ + new BackgroundFillSolid($data), + self::TYPE_GRADIENT => new BackgroundFillGradient($data), + self::TYPE_FREEFORM_GRADIENT => new BackgroundFillFreeformGradient($data), + }; + } +} diff --git a/src/Entities/BackgroundFill/BackgroundFillFreeformGradient.php b/src/Entities/BackgroundFill/BackgroundFillFreeformGradient.php new file mode 100644 index 000000000..0e9784a97 --- /dev/null +++ b/src/Entities/BackgroundFill/BackgroundFillFreeformGradient.php @@ -0,0 +1,11 @@ + new BackgroundTypeFill($data), + self::TYPE_WALLPAPER => new BackgroundTypeWallpaper($data), + self::TYPE_PATTERN => new BackgroundTypePattern($data), + self::TYPE_CHAT_THEME => new BackgroundTypeChatTheme($data), + }; + } +} diff --git a/src/Entities/BackgroundType/BackgroundTypeChatTheme.php b/src/Entities/BackgroundType/BackgroundTypeChatTheme.php new file mode 100644 index 000000000..44574f4e3 --- /dev/null +++ b/src/Entities/BackgroundType/BackgroundTypeChatTheme.php @@ -0,0 +1,11 @@ + BackgroundFill::class, + ]; + } +} diff --git a/src/Entities/BackgroundType/BackgroundTypePattern.php b/src/Entities/BackgroundType/BackgroundTypePattern.php new file mode 100644 index 000000000..a7b7863a9 --- /dev/null +++ b/src/Entities/BackgroundType/BackgroundTypePattern.php @@ -0,0 +1,24 @@ + Document::class, + 'fill' => BackgroundFill::class, + ]; + } +} diff --git a/src/Entities/BackgroundType/BackgroundTypeWallpaper.php b/src/Entities/BackgroundType/BackgroundTypeWallpaper.php new file mode 100644 index 000000000..2ffc14734 --- /dev/null +++ b/src/Entities/BackgroundType/BackgroundTypeWallpaper.php @@ -0,0 +1,21 @@ + Document::class, + ]; + } +} diff --git a/src/Entities/BusinessConnection.php b/src/Entities/BusinessConnection.php new file mode 100644 index 000000000..ee071bf90 --- /dev/null +++ b/src/Entities/BusinessConnection.php @@ -0,0 +1,21 @@ + User::class, + ]; + } +} diff --git a/src/Entities/BusinessMessagesDeleted.php b/src/Entities/BusinessMessagesDeleted.php new file mode 100644 index 000000000..371d3d5f4 --- /dev/null +++ b/src/Entities/BusinessMessagesDeleted.php @@ -0,0 +1,18 @@ + Chat::class, + ]; + } +} diff --git a/src/Entities/CallbackGame.php b/src/Entities/CallbackGame.php new file mode 100644 index 000000000..bbab9a1d5 --- /dev/null +++ b/src/Entities/CallbackGame.php @@ -0,0 +1,8 @@ + User::class, + 'message' => MaybeInaccessibleMessage::class, + ]; + } +} diff --git a/src/Entities/Chat.php b/src/Entities/Chat.php new file mode 100644 index 000000000..0617ba43e --- /dev/null +++ b/src/Entities/Chat.php @@ -0,0 +1,17 @@ + BackgroundType::class, + ]; + } +} diff --git a/src/Entities/ChatBoost.php b/src/Entities/ChatBoost.php new file mode 100644 index 000000000..7b7c980ad --- /dev/null +++ b/src/Entities/ChatBoost.php @@ -0,0 +1,21 @@ + ChatBoostSource::class, + ]; + } +} diff --git a/src/Entities/ChatBoostAdded.php b/src/Entities/ChatBoostAdded.php new file mode 100644 index 000000000..04db2148c --- /dev/null +++ b/src/Entities/ChatBoostAdded.php @@ -0,0 +1,11 @@ + Chat::class, + 'source' => ChatBoostSource::class, + ]; + } +} diff --git a/src/Entities/ChatBoostSource/ChatBoostSource.php b/src/Entities/ChatBoostSource/ChatBoostSource.php new file mode 100644 index 000000000..314818974 --- /dev/null +++ b/src/Entities/ChatBoostSource/ChatBoostSource.php @@ -0,0 +1,27 @@ + new ChatBoostSourcePremium($data), + self::SOURCE_GIFT_CODE => new ChatBoostSourceGiftCode($data), + self::SOURCE_GIVEAWAY => new ChatBoostSourceGiveaway($data), + }; + } +} diff --git a/src/Entities/ChatBoostSource/ChatBoostSourceGiftCode.php b/src/Entities/ChatBoostSource/ChatBoostSourceGiftCode.php new file mode 100644 index 000000000..f7802313d --- /dev/null +++ b/src/Entities/ChatBoostSource/ChatBoostSourceGiftCode.php @@ -0,0 +1,18 @@ + User::class, + ]; + } +} diff --git a/src/Entities/ChatBoostSource/ChatBoostSourceGiveaway.php b/src/Entities/ChatBoostSource/ChatBoostSourceGiveaway.php new file mode 100644 index 000000000..cdda544df --- /dev/null +++ b/src/Entities/ChatBoostSource/ChatBoostSourceGiveaway.php @@ -0,0 +1,20 @@ + User::class, + ]; + } +} diff --git a/src/Entities/ChatBoostSource/ChatBoostSourcePremium.php b/src/Entities/ChatBoostSource/ChatBoostSourcePremium.php new file mode 100644 index 000000000..1e1ec69e6 --- /dev/null +++ b/src/Entities/ChatBoostSource/ChatBoostSourcePremium.php @@ -0,0 +1,18 @@ + User::class, + ]; + } +} diff --git a/src/Entities/ChatBoostUpdated.php b/src/Entities/ChatBoostUpdated.php new file mode 100644 index 000000000..32cb4aa9d --- /dev/null +++ b/src/Entities/ChatBoostUpdated.php @@ -0,0 +1,18 @@ + Chat::class, + 'boost' => ChatBoost::class, + ]; + } +} diff --git a/src/Entities/ChatInviteLink.php b/src/Entities/ChatInviteLink.php new file mode 100644 index 000000000..538a85cd4 --- /dev/null +++ b/src/Entities/ChatInviteLink.php @@ -0,0 +1,24 @@ + User::class, + ]; + } +} diff --git a/src/Entities/ChatJoinRequest.php b/src/Entities/ChatJoinRequest.php new file mode 100644 index 000000000..fb082f78b --- /dev/null +++ b/src/Entities/ChatJoinRequest.php @@ -0,0 +1,23 @@ + Chat::class, + 'from' => User::class, + 'invite_link' => ChatInviteLink::class, + ]; + } +} diff --git a/src/Entities/ChatMember/ChatMember.php b/src/Entities/ChatMember/ChatMember.php new file mode 100644 index 000000000..c4aca181c --- /dev/null +++ b/src/Entities/ChatMember/ChatMember.php @@ -0,0 +1,36 @@ + new ChatMemberOwner($data), + self::TYPE_ADMINISTRATOR => new ChatMemberAdministrator($data), + self::TYPE_MEMBER => new ChatMemberMember($data), + self::TYPE_RESTRICTED => new ChatMemberRestricted($data), + self::TYPE_LEFT => new ChatMemberLeft($data), + self::TYPE_KICKED => new ChatMemberBanned($data), + }; + } +} diff --git a/src/Entities/ChatMember/ChatMemberAdministrator.php b/src/Entities/ChatMember/ChatMemberAdministrator.php new file mode 100644 index 000000000..d05bec8c7 --- /dev/null +++ b/src/Entities/ChatMember/ChatMemberAdministrator.php @@ -0,0 +1,35 @@ + User::class, + ]; + } +} diff --git a/src/Entities/ChatMember/ChatMemberBanned.php b/src/Entities/ChatMember/ChatMemberBanned.php new file mode 100644 index 000000000..280c4cf81 --- /dev/null +++ b/src/Entities/ChatMember/ChatMemberBanned.php @@ -0,0 +1,20 @@ + User::class, + ]; + } +} diff --git a/src/Entities/ChatMember/ChatMemberLeft.php b/src/Entities/ChatMember/ChatMemberLeft.php new file mode 100644 index 000000000..1b45f92c8 --- /dev/null +++ b/src/Entities/ChatMember/ChatMemberLeft.php @@ -0,0 +1,18 @@ + User::class, + ]; + } +} diff --git a/src/Entities/ChatMember/ChatMemberMember.php b/src/Entities/ChatMember/ChatMemberMember.php new file mode 100644 index 000000000..eb14c8657 --- /dev/null +++ b/src/Entities/ChatMember/ChatMemberMember.php @@ -0,0 +1,18 @@ + User::class, + ]; + } +} diff --git a/src/Entities/ChatMember/ChatMemberOwner.php b/src/Entities/ChatMember/ChatMemberOwner.php new file mode 100644 index 000000000..da09a128c --- /dev/null +++ b/src/Entities/ChatMember/ChatMemberOwner.php @@ -0,0 +1,20 @@ + User::class, + ]; + } +} diff --git a/src/Entities/ChatMember/ChatMemberRestricted.php b/src/Entities/ChatMember/ChatMemberRestricted.php new file mode 100644 index 000000000..9b542b3b3 --- /dev/null +++ b/src/Entities/ChatMember/ChatMemberRestricted.php @@ -0,0 +1,34 @@ + User::class, + ]; + } +} diff --git a/src/Entities/ChatMemberUpdated.php b/src/Entities/ChatMemberUpdated.php new file mode 100644 index 000000000..dc7693c2f --- /dev/null +++ b/src/Entities/ChatMemberUpdated.php @@ -0,0 +1,29 @@ + Chat::class, + 'from' => User::class, + 'old_chat_member' => ChatMember::class, + 'new_chat_member' => ChatMember::class, + 'invite_link' => ChatInviteLink::class, + ]; + } +} diff --git a/src/Entities/ChatShared.php b/src/Entities/ChatShared.php new file mode 100644 index 000000000..a43a78769 --- /dev/null +++ b/src/Entities/ChatShared.php @@ -0,0 +1,20 @@ + [PhotoSize::class], + ]; + } +} diff --git a/src/Entities/ChosenInlineResult.php b/src/Entities/ChosenInlineResult.php new file mode 100644 index 000000000..9c6d61b63 --- /dev/null +++ b/src/Entities/ChosenInlineResult.php @@ -0,0 +1,21 @@ + User::class, + 'location' => Location::class, + ]; + } +} diff --git a/src/Entities/Contact.php b/src/Entities/Contact.php new file mode 100644 index 000000000..b1f19d796 --- /dev/null +++ b/src/Entities/Contact.php @@ -0,0 +1,15 @@ + PhotoSize::class, + ]; + } +} diff --git a/src/Entities/EncryptedCredentials.php b/src/Entities/EncryptedCredentials.php new file mode 100644 index 000000000..1d57fcf6b --- /dev/null +++ b/src/Entities/EncryptedCredentials.php @@ -0,0 +1,13 @@ + [PassportFile::class], + 'front_side' => PassportFile::class, + 'reverse_side' => PassportFile::class, + 'selfie' => PassportFile::class, + 'translation' => [PassportFile::class], + ]; + } +} diff --git a/src/Entities/Entity.php b/src/Entities/Entity.php new file mode 100644 index 000000000..53c04ed75 --- /dev/null +++ b/src/Entities/Entity.php @@ -0,0 +1,88 @@ + + */ + protected static function subEntities(): array + { + return []; + } + + public function __construct( + array $data = [] + ) { + foreach ($data as $key => $value) { + $this->fields[$key] = $value; + } + } + + public function __get(string $name) + { + return $this->fields[$name]; + } + + public function __set(string $name, $value): void + { + $this->fields[$name] = $value; + } + + protected function getField(string $name): mixed + { + $data = $this->fields[$name]; + + $subEntities = static::subEntities(); + if (! isset($subEntities[$name])) { + return $data; + } + + if ($subEntities[$name] instanceof Entity) { + $class = $subEntities[$name]; + + return new $class($data); + } elseif (is_array($subEntities[$name]) && $subEntities[$name][0] instanceof Entity) { + // Arrays like PhotoSize[] + $class = $subEntities[$name][0]; + + return array_map(fn ($item) => new $class($item), $data); + } elseif (is_array($subEntities[$name]) && is_array($subEntities[$name][0]) && $subEntities[$name][0][0] instanceof Entity) { + // Edge case currently only needed for Keyboards: Array of Array of InlineKeyboardButton + $class = $subEntities[$name][0][0]; + + return array_map(fn ($row) => array_map(fn ($button) => new $class($button), $row), $data); + } + + return $data; + } + + protected function setField(string $name, mixed $value): void + { + $this->fields[$name] = $value; + } + + public function __call(string $name, array $arguments) + { + $snakeName = strtolower(ltrim(preg_replace('/[[:upper:]]/', '_$0', $name), '_')); + + if (str_starts_with($snakeName, 'get_')) { + return $this->getField(substr($snakeName, 4)); + } elseif (str_starts_with($snakeName, 'set_')) { + $this->setField(substr($snakeName, 4), $arguments[0] ?? null); + + return $this; + } + + $method = get_class($this).'::'.$name.'()'; + throw new \BadMethodCallException("Call to undefined method $method"); + } + + public function jsonSerialize(): mixed + { + return $this->fields; + } +} diff --git a/src/Entities/ExternalReplyInfo.php b/src/Entities/ExternalReplyInfo.php new file mode 100644 index 000000000..1602ef4dc --- /dev/null +++ b/src/Entities/ExternalReplyInfo.php @@ -0,0 +1,60 @@ + MessageOrigin::class, + 'chat' => Chat::class, + 'link_preview_options' => LinkPreviewOptions::class, + 'animation' => Animation::class, + 'audio' => Audio::class, + 'document' => Document::class, + 'photo' => [PhotoSize::class], + 'sticker' => Sticker::class, + 'story' => Story::class, + 'video' => Video::class, + 'video_note' => VideoNote::class, + 'voice' => Voice::class, + 'contact' => Contact::class, + 'dice' => Dice::class, + 'game' => Game::class, + 'giveaway' => Giveaway::class, + 'giveaway_winners' => GiveawayWinners::class, + 'invoice' => Invoice::class, + 'location' => Location::class, + 'poll' => Poll::class, + 'venue' => Venue::class, + ]; + } +} diff --git a/src/Entities/Factory.php b/src/Entities/Factory.php new file mode 100644 index 000000000..9888096e7 --- /dev/null +++ b/src/Entities/Factory.php @@ -0,0 +1,8 @@ +/ to get the file. + */ +class File extends Entity +{ + // +} diff --git a/src/Entities/ForumTopicClosed.php b/src/Entities/ForumTopicClosed.php new file mode 100644 index 000000000..170deb9ba --- /dev/null +++ b/src/Entities/ForumTopicClosed.php @@ -0,0 +1,8 @@ + [PhotoSize::class], + 'text_entities' => [MessageEntity::class], + 'animation' => Animation::class, + ]; + } +} diff --git a/src/Entities/GeneralForumTopicHidden.php b/src/Entities/GeneralForumTopicHidden.php new file mode 100644 index 000000000..078a89c19 --- /dev/null +++ b/src/Entities/GeneralForumTopicHidden.php @@ -0,0 +1,8 @@ + [Chat::class], + ]; + } +} diff --git a/src/Entities/GiveawayCompleted.php b/src/Entities/GiveawayCompleted.php new file mode 100644 index 000000000..1c6b32cea --- /dev/null +++ b/src/Entities/GiveawayCompleted.php @@ -0,0 +1,18 @@ + Message::class, + ]; + } +} diff --git a/src/Entities/GiveawayCreated.php b/src/Entities/GiveawayCreated.php new file mode 100644 index 000000000..a93c41f81 --- /dev/null +++ b/src/Entities/GiveawayCreated.php @@ -0,0 +1,8 @@ + Chat::class, + 'winners' => [User::class], + ]; + } +} diff --git a/src/Entities/InaccessibleMessage.php b/src/Entities/InaccessibleMessage.php new file mode 100644 index 000000000..9a114ed81 --- /dev/null +++ b/src/Entities/InaccessibleMessage.php @@ -0,0 +1,13 @@ + Chat::class, + ]; + } +} diff --git a/src/Entities/InlineKeyboardButton.php b/src/Entities/InlineKeyboardButton.php new file mode 100644 index 000000000..580ec88a5 --- /dev/null +++ b/src/Entities/InlineKeyboardButton.php @@ -0,0 +1,27 @@ + can be used to mention a user by their identifier without using a username, if this is allowed by their privacy settings. + * @method string|null getCallbackData() Optional. Data to be sent in a callback query to the bot when button is pressed, 1-64 bytes. Not supported for messages sent on behalf of a Telegram Business account. + * @method WebAppInfo|null getWebApp() 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 Telegram Business account. + * @method LoginUrl|null getLoginUrl() Optional. An HTTPS URL used to automatically authorize the user. Can be used as a replacement for the Telegram Login Widget. + * @method string|null getSwitchInlineQuery() 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 on behalf of a Telegram Business account. + * @method SwitchInlineQueryChosenChat|null getSwitchInlineQueryChosenChat() 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 on behalf of a Telegram Business account. + * @method CallbackGame|null getCallbackGame() Optional. Description of the game that will be launched when the user presses the button. + * @method bool|null getPay() Optional. Specify True, to send a Pay button. Substrings “⭐” and “XTR” in the buttons's text will be replaced with a Telegram Star icon. + */ +class InlineKeyboardButton extends Entity +{ + protected static function subEntities(): array + { + return [ + 'web_app' => WebAppInfo::class, + 'login_url' => LoginUrl::class, + 'switch_inline_query_chosen_chat' => SwitchInlineQueryChosenChat::class, + 'callback_game' => CallbackGame::class, + ]; + } +} diff --git a/src/Entities/InlineKeyboardMarkup.php b/src/Entities/InlineKeyboardMarkup.php new file mode 100644 index 000000000..9033b2c69 --- /dev/null +++ b/src/Entities/InlineKeyboardMarkup.php @@ -0,0 +1,13 @@ + [[InlineKeyboardButton::class]], + ]; + } +} diff --git a/src/Entities/InlineQuery.php b/src/Entities/InlineQuery.php new file mode 100644 index 000000000..f35240d78 --- /dev/null +++ b/src/Entities/InlineQuery.php @@ -0,0 +1,22 @@ + User::class, + 'location' => Location::class, + ]; + } +} diff --git a/src/Entities/Invoice.php b/src/Entities/Invoice.php new file mode 100644 index 000000000..ee34fbf24 --- /dev/null +++ b/src/Entities/Invoice.php @@ -0,0 +1,15 @@ + new InaccessibleMessage($data), + default => new Message($data), + }; + } + + protected static function subEntities(): array + { + return [ + 'chat' => Chat::class, + ]; + } +} diff --git a/src/Entities/Message.php b/src/Entities/Message.php new file mode 100644 index 000000000..2dcd5ced1 --- /dev/null +++ b/src/Entities/Message.php @@ -0,0 +1,157 @@ + User::class, + 'sender_chat' => Chat::class, + 'sender_business_bot' => User::class, + 'chat' => Chat::class, + 'forward_origin' => MessageOrigin::class, + 'reply_to_message' => Message::class, + 'external_reply' => ExternalReplyInfo::class, + 'quote' => TextQuote::class, + 'reply_to_story' => Story::class, + 'via_bot' => User::class, + 'entities' => [MessageEntity::class], + 'link_preview_options' => LinkPreviewOptions::class, + 'animation' => Animation::class, + 'audio' => Audio::class, + 'document' => Document::class, + 'photo' => [PhotoSize::class], + 'sticker' => Sticker::class, + 'story' => Story::class, + 'video' => Video::class, + 'video_note' => VideoNote::class, + 'voice' => Voice::class, + 'caption_entities' => [MessageEntity::class], + 'contact' => Contact::class, + 'dice' => Dice::class, + 'game' => Game::class, + 'poll' => Poll::class, + 'venue' => Venue::class, + 'location' => Location::class, + 'new_chat_members' => [User::class], + 'left_chat_member' => User::class, + 'new_chat_photo' => [PhotoSize::class], + 'message_auto_delete_timer_changed' => MessageAutoDeleteTimerChanged::class, + 'pinned_message' => MaybeInaccessibleMessage::class, + 'invoice' => Invoice::class, + 'successful_payment' => SuccessfulPayment::class, + 'users_shared' => UsersShared::class, + 'chat_shared' => ChatShared::class, + 'write_access_allowed' => WriteAccessAllowed::class, + 'passport_data' => PassportData::class, + 'proximity_alert_triggered' => ProximityAlertTriggered::class, + 'boost_added' => ChatBoostAdded::class, + 'chat_background_set' => ChatBackground::class, + 'forum_topic_created' => ForumTopicCreated::class, + 'forum_topic_edited' => ForumTopicEdited::class, + 'forum_topic_closed' => ForumTopicClosed::class, + 'forum_topic_reopened' => ForumTopicReopened::class, + 'general_forum_topic_hidden' => GeneralForumTopicHidden::class, + 'general_forum_topic_unhidden' => GeneralForumTopicUnhidden::class, + 'giveaway_created' => GiveawayCreated::class, + 'giveaway' => Giveaway::class, + 'giveaway_winners' => GiveawayWinners::class, + 'giveaway_completed' => GiveawayCompleted::class, + 'video_chat_scheduled' => VideoChatScheduled::class, + 'video_chat_started' => VideoChatStarted::class, + 'video_chat_ended' => VideoChatEnded::class, + 'video_chat_participants_invited' => VideoChatParticipantsInvited::class, + 'web_app_data' => WebAppData::class, + 'reply_markup' => InlineKeyboardMarkup::class, + ]; + } +} diff --git a/src/Entities/MessageAutoDeleteTimerChanged.php b/src/Entities/MessageAutoDeleteTimerChanged.php new file mode 100644 index 000000000..a84476007 --- /dev/null +++ b/src/Entities/MessageAutoDeleteTimerChanged.php @@ -0,0 +1,11 @@ + User::class, + ]; + } +} diff --git a/src/Entities/MessageOrigin/MessageOrigin.php b/src/Entities/MessageOrigin/MessageOrigin.php new file mode 100644 index 000000000..dc87e52c8 --- /dev/null +++ b/src/Entities/MessageOrigin/MessageOrigin.php @@ -0,0 +1,23 @@ + new MessageOriginUser($data), + 'hidden_user' => new MessageOriginHiddenUser($data), + 'chat' => new MessageOriginChat($data), + 'channel' => new MessageOriginChannel($data), + }; + } +} diff --git a/src/Entities/MessageOrigin/MessageOriginChannel.php b/src/Entities/MessageOrigin/MessageOriginChannel.php new file mode 100644 index 000000000..66afac5b5 --- /dev/null +++ b/src/Entities/MessageOrigin/MessageOriginChannel.php @@ -0,0 +1,20 @@ + Chat::class, + ]; + } +} diff --git a/src/Entities/MessageOrigin/MessageOriginChat.php b/src/Entities/MessageOrigin/MessageOriginChat.php new file mode 100644 index 000000000..c5a5b0f61 --- /dev/null +++ b/src/Entities/MessageOrigin/MessageOriginChat.php @@ -0,0 +1,19 @@ + Chat::class, + ]; + } +} diff --git a/src/Entities/MessageOrigin/MessageOriginHiddenUser.php b/src/Entities/MessageOrigin/MessageOriginHiddenUser.php new file mode 100644 index 000000000..935dd8c0f --- /dev/null +++ b/src/Entities/MessageOrigin/MessageOriginHiddenUser.php @@ -0,0 +1,11 @@ + User::class, + ]; + } +} diff --git a/src/Entities/MessageReactionCountUpdated.php b/src/Entities/MessageReactionCountUpdated.php new file mode 100644 index 000000000..7a88046be --- /dev/null +++ b/src/Entities/MessageReactionCountUpdated.php @@ -0,0 +1,20 @@ + Chat::class, + 'reactions' => [ReactionCount::class], + ]; + } +} diff --git a/src/Entities/MessageReactionUpdated.php b/src/Entities/MessageReactionUpdated.php new file mode 100644 index 000000000..57db5afd4 --- /dev/null +++ b/src/Entities/MessageReactionUpdated.php @@ -0,0 +1,28 @@ + Chat::class, + 'user' => User::class, + 'actor_chat' => Chat::class, + 'old_reaction' => [ReactionType::class], + 'new_reaction' => [ReactionType::class], + ]; + } +} diff --git a/src/Entities/OrderInfo.php b/src/Entities/OrderInfo.php new file mode 100644 index 000000000..6a3050c34 --- /dev/null +++ b/src/Entities/OrderInfo.php @@ -0,0 +1,19 @@ + ShippingAddress::class, + ]; + } +} diff --git a/src/Entities/PassportData.php b/src/Entities/PassportData.php new file mode 100644 index 000000000..50d2a959f --- /dev/null +++ b/src/Entities/PassportData.php @@ -0,0 +1,18 @@ + [EncryptedPassportElement::class], + 'credentials' => EncryptedCredentials::class, + ]; + } +} diff --git a/src/Entities/PassportFile.php b/src/Entities/PassportFile.php new file mode 100644 index 000000000..28feed109 --- /dev/null +++ b/src/Entities/PassportFile.php @@ -0,0 +1,14 @@ + [MessageEntity::class], + 'options' => [PollOption::class], + 'explanation_entities' => [MessageEntity::class], + ]; + } +} diff --git a/src/Entities/PollAnswer.php b/src/Entities/PollAnswer.php new file mode 100644 index 000000000..e61847529 --- /dev/null +++ b/src/Entities/PollAnswer.php @@ -0,0 +1,20 @@ + Chat::class, + 'user' => User::class, + ]; + } +} diff --git a/src/Entities/PollOption.php b/src/Entities/PollOption.php new file mode 100644 index 000000000..c1352098c --- /dev/null +++ b/src/Entities/PollOption.php @@ -0,0 +1,18 @@ + [MessageEntity::class], + ]; + } +} diff --git a/src/Entities/PreCheckoutQuery.php b/src/Entities/PreCheckoutQuery.php new file mode 100644 index 000000000..c709f6eb3 --- /dev/null +++ b/src/Entities/PreCheckoutQuery.php @@ -0,0 +1,23 @@ + User::class, + 'order_info' => OrderInfo::class, + ]; + } +} diff --git a/src/Entities/ProximityAlertTriggered.php b/src/Entities/ProximityAlertTriggered.php new file mode 100644 index 000000000..7fb7e2e82 --- /dev/null +++ b/src/Entities/ProximityAlertTriggered.php @@ -0,0 +1,19 @@ + User::class, + 'watcher' => User::class, + ]; + } +} diff --git a/src/Entities/ReactionCount.php b/src/Entities/ReactionCount.php new file mode 100644 index 000000000..dd32d35e4 --- /dev/null +++ b/src/Entities/ReactionCount.php @@ -0,0 +1,19 @@ + ReactionType::class, + ]; + } +} diff --git a/src/Entities/ReactionType/ReactionType.php b/src/Entities/ReactionType/ReactionType.php new file mode 100644 index 000000000..61c0cd16c --- /dev/null +++ b/src/Entities/ReactionType/ReactionType.php @@ -0,0 +1,24 @@ + new ReactionTypeEmoji($data), + self::TYPE_CUSTOM_EMOJI => new ReactionTypeCustomEmoji($data), + }; + } +} diff --git a/src/Entities/ReactionType/ReactionTypeCustomEmoji.php b/src/Entities/ReactionType/ReactionTypeCustomEmoji.php new file mode 100644 index 000000000..8186f0ada --- /dev/null +++ b/src/Entities/ReactionType/ReactionTypeCustomEmoji.php @@ -0,0 +1,11 @@ + [PhotoSize::class], + ]; + } +} diff --git a/src/Entities/ShippingAddress.php b/src/Entities/ShippingAddress.php new file mode 100644 index 000000000..05c69d92c --- /dev/null +++ b/src/Entities/ShippingAddress.php @@ -0,0 +1,16 @@ + User::class, + 'shipping_address' => ShippingAddress::class, + ]; + } +} diff --git a/src/Entities/Sticker.php b/src/Entities/Sticker.php new file mode 100644 index 000000000..0d19155eb --- /dev/null +++ b/src/Entities/Sticker.php @@ -0,0 +1,38 @@ + PhotoSize::class, + 'premium_animation' => File::class, + 'mask_position' => MaskPosition::class, + ]; + } +} diff --git a/src/Entities/Story.php b/src/Entities/Story.php new file mode 100644 index 000000000..263a3e28a --- /dev/null +++ b/src/Entities/Story.php @@ -0,0 +1,17 @@ + Chat::class, + ]; + } +} diff --git a/src/Entities/SuccessfulPayment.php b/src/Entities/SuccessfulPayment.php new file mode 100644 index 000000000..8a03b1d72 --- /dev/null +++ b/src/Entities/SuccessfulPayment.php @@ -0,0 +1,22 @@ + OrderInfo::class, + ]; + } +} diff --git a/src/Entities/SwitchInlineQueryChosenChat.php b/src/Entities/SwitchInlineQueryChosenChat.php new file mode 100644 index 000000000..60d9fe106 --- /dev/null +++ b/src/Entities/SwitchInlineQueryChosenChat.php @@ -0,0 +1,15 @@ + [MessageEntity::class], + ]; + } +} diff --git a/src/Entities/Update.php b/src/Entities/Update.php new file mode 100644 index 000000000..8653c6da3 --- /dev/null +++ b/src/Entities/Update.php @@ -0,0 +1,59 @@ + Message::class, + 'edited_message' => Message::class, + 'channel_post' => Message::class, + 'edited_channel_post' => Message::class, + 'business_connection' => BusinessConnection::class, + 'business_message' => Message::class, + 'edited_business_message' => Message::class, + 'deleted_business_messages' => BusinessMessagesDeleted::class, + 'message_reaction' => MessageReactionUpdated::class, + 'message_reaction_count' => MessageReactionCountUpdated::class, + 'inline_query' => InlineQuery::class, + 'chosen_inline_result' => ChosenInlineResult::class, + 'callback_query' => CallbackQuery::class, + 'shipping_query' => ShippingQuery::class, + 'pre_checkout_query' => PreCheckoutQuery::class, + 'poll' => Poll::class, + 'poll_answer' => PollAnswer::class, + 'my_chat_member' => ChatMemberUpdated::class, + 'chat_member' => ChatMemberUpdated::class, + 'chat_join_request' => ChatJoinRequest::class, + 'chat_boost' => ChatBoostUpdated::class, + 'removed_chat_boost' => ChatBoostRemoved::class, + ]; + } +} diff --git a/src/Entities/User.php b/src/Entities/User.php new file mode 100644 index 000000000..47697f584 --- /dev/null +++ b/src/Entities/User.php @@ -0,0 +1,22 @@ + [SharedUser::class], + ]; + } +} diff --git a/src/Entities/Venue.php b/src/Entities/Venue.php new file mode 100644 index 000000000..d727ef8de --- /dev/null +++ b/src/Entities/Venue.php @@ -0,0 +1,22 @@ + Location::class, + ]; + } +} diff --git a/src/Entities/Video.php b/src/Entities/Video.php new file mode 100644 index 000000000..7a46e9878 --- /dev/null +++ b/src/Entities/Video.php @@ -0,0 +1,24 @@ + PhotoSize::class, + ]; + } +} diff --git a/src/Entities/VideoChatEnded.php b/src/Entities/VideoChatEnded.php new file mode 100644 index 000000000..171c351ee --- /dev/null +++ b/src/Entities/VideoChatEnded.php @@ -0,0 +1,11 @@ + [User::class], + ]; + } +} diff --git a/src/Entities/VideoChatScheduled.php b/src/Entities/VideoChatScheduled.php new file mode 100644 index 000000000..652c7fcaf --- /dev/null +++ b/src/Entities/VideoChatScheduled.php @@ -0,0 +1,11 @@ + PhotoSize::class, + ]; + } +} diff --git a/src/Entities/Voice.php b/src/Entities/Voice.php new file mode 100644 index 000000000..ac6834e3c --- /dev/null +++ b/src/Entities/Voice.php @@ -0,0 +1,15 @@ +|null Optional. Special entities that appear in the question. Currently, only custom emoji entities are allowed in poll questions */ - public ?array $question_entites; - - /** @var array List of poll options */ - public array $options; - - /** @var int Total number of users that voted in the poll */ - public int $total_voter_count; - - /** @var bool True, if the poll is closed */ - public bool $is_closed; - - // ... -} diff --git a/src/Types/Type.php b/src/Types/Type.php deleted file mode 100644 index 8edfea29c..000000000 --- a/src/Types/Type.php +++ /dev/null @@ -1,18 +0,0 @@ -additionalFields[$name]; - } - - public function __set(string $name, $value): void - { - $this->additionalFields[$name] = $value; - } -} From 2d7d46d63a170d26b3b0627120cdccfac0f1824e Mon Sep 17 00:00:00 2001 From: Tii Date: Mon, 10 Jun 2024 22:11:53 +0200 Subject: [PATCH 04/20] Added additional types --- pint.json | 7 +- src/Contracts/AllowsBypassingGet.php | 8 + .../BackgroundFill/BackgroundFill.php | 4 +- .../BackgroundType/BackgroundType.php | 6 +- .../BackgroundType/BackgroundTypePattern.php | 23 ++- .../BackgroundTypeWallpaper.php | 19 ++- src/Entities/Birthdate.php | 13 ++ src/Entities/BotCommand.php | 12 ++ .../BotCommandScope/BotCommandScope.php | 39 +++++ .../BotCommandScopeAllChatAdministrators.php | 8 + .../BotCommandScopeAllGroupChats.php | 8 + .../BotCommandScopeAllPrivateChats.php | 8 + .../BotCommandScope/BotCommandScopeChat.php | 11 ++ .../BotCommandScopeChatAdministrators.php | 11 ++ .../BotCommandScopeChatMember.php | 12 ++ .../BotCommandScopeDefault.php | 8 + src/Entities/BotDescription.php | 11 ++ src/Entities/BotName.php | 11 ++ src/Entities/BotShortDescription.php | 11 ++ src/Entities/BusinessConnection.php | 13 +- src/Entities/BusinessIntro.php | 18 +++ src/Entities/BusinessLocation.php | 17 +++ src/Entities/BusinessOpeningHours.php | 17 +++ src/Entities/BusinessOpeningHoursInterval.php | 12 ++ src/Entities/CallbackQuery.php | 2 +- src/Entities/Chat.php | 21 ++- src/Entities/ChatAdministratorRights.php | 46 ++++++ src/Entities/ChatBoostRemoved.php | 2 +- .../ChatBoostSource/ChatBoostSource.php | 4 +- .../ChatBoostSourceGiveaway.php | 12 +- src/Entities/ChatBoostUpdated.php | 2 +- src/Entities/ChatFullInfo.php | 84 +++++++++++ src/Entities/ChatInviteLink.php | 16 +- src/Entities/ChatJoinRequest.php | 4 +- src/Entities/ChatLocation.php | 17 +++ src/Entities/ChatMember/ChatMember.php | 10 +- .../ChatMember/ChatMemberAdministrator.php | 56 ++++--- src/Entities/ChatMember/ChatMemberOwner.php | 12 +- .../ChatMember/ChatMemberRestricted.php | 54 ++++--- src/Entities/ChatMemberUpdated.php | 6 +- src/Entities/ChatPermissions.php | 42 ++++++ src/Entities/ChatPhoto.php | 14 ++ src/Entities/ChosenInlineResult.php | 2 +- src/Entities/EncryptedPassportElement.php | 8 +- src/Entities/Entity.php | 18 ++- src/Entities/ExternalReplyInfo.php | 52 ++++--- src/Entities/ForceReply.php | 13 ++ src/Entities/ForumTopic.php | 14 ++ src/Entities/Game.php | 4 +- src/Entities/Giveaway.php | 13 +- src/Entities/GiveawayWinners.php | 15 +- src/Entities/InlineKeyboardButton.php | 6 +- src/Entities/InlineKeyboardMarkup.php | 3 + src/Entities/InlineQuery.php | 2 +- src/Entities/InputFile.php | 7 + src/Entities/InputMedia/InputMedia.php | 33 +++++ .../InputMedia/InputMediaAnimation.php | 36 +++++ src/Entities/InputMedia/InputMediaAudio.php | 26 ++++ .../InputMedia/InputMediaDocument.php | 24 +++ src/Entities/InputMedia/InputMediaPhoto.php | 31 ++++ src/Entities/InputMedia/InputMediaVideo.php | 38 +++++ src/Entities/InputPollOption.php | 18 +++ src/Entities/KeyboardButton.php | 25 ++++ src/Entities/KeyboardButtonPollType.php | 11 ++ src/Entities/KeyboardButtonRequestChat.php | 27 ++++ src/Entities/KeyboardButtonRequestUsers.php | 17 +++ src/Entities/LinkPreviewOptions.php | 12 +- src/Entities/MaybeInaccessibleMessage.php | 2 +- src/Entities/MenuButton/MenuButton.php | 27 ++++ .../MenuButton/MenuButtonCommands.php | 8 + src/Entities/MenuButton/MenuButtonDefault.php | 8 + src/Entities/MenuButton/MenuButtonWebApp.php | 19 +++ src/Entities/Message.php | 138 ++++++++++-------- src/Entities/MessageId.php | 11 ++ src/Entities/MessageOrigin/MessageOrigin.php | 6 +- src/Entities/MessageReactionCountUpdated.php | 2 +- src/Entities/MessageReactionUpdated.php | 6 +- src/Entities/PassportData.php | 2 +- src/Entities/Poll.php | 20 ++- src/Entities/PollAnswer.php | 2 +- src/Entities/PreCheckoutQuery.php | 2 +- src/Entities/ProximityAlertTriggered.php | 2 +- src/Entities/ReactionType/ReactionType.php | 2 +- src/Entities/ReplyKeyboardMarkup.php | 30 ++++ src/Entities/ReplyKeyboardRemove.php | 12 ++ src/Entities/ReplyParameters.php | 22 +++ src/Entities/ResponseParameters.php | 12 ++ src/Entities/ShippingQuery.php | 2 +- src/Entities/Sticker.php | 4 +- src/Entities/TextQuote.php | 13 +- src/Entities/Update.php | 42 +++--- src/Entities/User.php | 25 +++- src/Entities/UserChatBoosts.php | 16 ++ src/Entities/UserProfilePhotos.php | 17 +++ src/Entities/WebhookInfo.php | 27 ++++ src/Telegram.php | 2 +- 96 files changed, 1409 insertions(+), 238 deletions(-) create mode 100644 src/Contracts/AllowsBypassingGet.php create mode 100644 src/Entities/Birthdate.php create mode 100644 src/Entities/BotCommand.php create mode 100644 src/Entities/BotCommandScope/BotCommandScope.php create mode 100644 src/Entities/BotCommandScope/BotCommandScopeAllChatAdministrators.php create mode 100644 src/Entities/BotCommandScope/BotCommandScopeAllGroupChats.php create mode 100644 src/Entities/BotCommandScope/BotCommandScopeAllPrivateChats.php create mode 100644 src/Entities/BotCommandScope/BotCommandScopeChat.php create mode 100644 src/Entities/BotCommandScope/BotCommandScopeChatAdministrators.php create mode 100644 src/Entities/BotCommandScope/BotCommandScopeChatMember.php create mode 100644 src/Entities/BotCommandScope/BotCommandScopeDefault.php create mode 100644 src/Entities/BotDescription.php create mode 100644 src/Entities/BotName.php create mode 100644 src/Entities/BotShortDescription.php create mode 100644 src/Entities/BusinessIntro.php create mode 100644 src/Entities/BusinessLocation.php create mode 100644 src/Entities/BusinessOpeningHours.php create mode 100644 src/Entities/BusinessOpeningHoursInterval.php create mode 100644 src/Entities/ChatAdministratorRights.php create mode 100644 src/Entities/ChatFullInfo.php create mode 100644 src/Entities/ChatLocation.php create mode 100644 src/Entities/ChatPermissions.php create mode 100644 src/Entities/ChatPhoto.php create mode 100644 src/Entities/ForceReply.php create mode 100644 src/Entities/ForumTopic.php create mode 100644 src/Entities/InputFile.php create mode 100644 src/Entities/InputMedia/InputMedia.php create mode 100644 src/Entities/InputMedia/InputMediaAnimation.php create mode 100644 src/Entities/InputMedia/InputMediaAudio.php create mode 100644 src/Entities/InputMedia/InputMediaDocument.php create mode 100644 src/Entities/InputMedia/InputMediaPhoto.php create mode 100644 src/Entities/InputMedia/InputMediaVideo.php create mode 100644 src/Entities/InputPollOption.php create mode 100644 src/Entities/KeyboardButton.php create mode 100644 src/Entities/KeyboardButtonPollType.php create mode 100644 src/Entities/KeyboardButtonRequestChat.php create mode 100644 src/Entities/KeyboardButtonRequestUsers.php create mode 100644 src/Entities/MenuButton/MenuButton.php create mode 100644 src/Entities/MenuButton/MenuButtonCommands.php create mode 100644 src/Entities/MenuButton/MenuButtonDefault.php create mode 100644 src/Entities/MenuButton/MenuButtonWebApp.php create mode 100644 src/Entities/MessageId.php create mode 100644 src/Entities/ReplyKeyboardMarkup.php create mode 100644 src/Entities/ReplyKeyboardRemove.php create mode 100644 src/Entities/ReplyParameters.php create mode 100644 src/Entities/ResponseParameters.php create mode 100644 src/Entities/UserChatBoosts.php create mode 100644 src/Entities/UserProfilePhotos.php create mode 100644 src/Entities/WebhookInfo.php diff --git a/pint.json b/pint.json index 9d6485480..97bbe7852 100644 --- a/pint.json +++ b/pint.json @@ -1,6 +1,11 @@ { "preset": "laravel", "rules": { - "phpdoc_align": true + "phpdoc_align": true, + "binary_operator_spaces": { + "operators": { + "=>": "align" + } + } } } \ No newline at end of file diff --git a/src/Contracts/AllowsBypassingGet.php b/src/Contracts/AllowsBypassingGet.php new file mode 100644 index 000000000..462abeb34 --- /dev/null +++ b/src/Contracts/AllowsBypassingGet.php @@ -0,0 +1,8 @@ + new BackgroundFillSolid($data), - self::TYPE_GRADIENT => new BackgroundFillGradient($data), + self::TYPE_SOLID => new BackgroundFillSolid($data), + self::TYPE_GRADIENT => new BackgroundFillGradient($data), self::TYPE_FREEFORM_GRADIENT => new BackgroundFillFreeformGradient($data), }; } diff --git a/src/Entities/BackgroundType/BackgroundType.php b/src/Entities/BackgroundType/BackgroundType.php index fbd8eb5c1..3236a31e2 100644 --- a/src/Entities/BackgroundType/BackgroundType.php +++ b/src/Entities/BackgroundType/BackgroundType.php @@ -21,9 +21,9 @@ abstract class BackgroundType extends Entity implements Factory public static function make(array $data): static { return match ($data['type']) { - self::TYPE_FILL => new BackgroundTypeFill($data), - self::TYPE_WALLPAPER => new BackgroundTypeWallpaper($data), - self::TYPE_PATTERN => new BackgroundTypePattern($data), + self::TYPE_FILL => new BackgroundTypeFill($data), + self::TYPE_WALLPAPER => new BackgroundTypeWallpaper($data), + self::TYPE_PATTERN => new BackgroundTypePattern($data), self::TYPE_CHAT_THEME => new BackgroundTypeChatTheme($data), }; } diff --git a/src/Entities/BackgroundType/BackgroundTypePattern.php b/src/Entities/BackgroundType/BackgroundTypePattern.php index a7b7863a9..b1338ef7a 100644 --- a/src/Entities/BackgroundType/BackgroundTypePattern.php +++ b/src/Entities/BackgroundType/BackgroundTypePattern.php @@ -2,23 +2,32 @@ namespace PhpTelegramBot\Core\Entities\BackgroundType; +use PhpTelegramBot\Core\Contracts\AllowsBypassingGet; use PhpTelegramBot\Core\Entities\BackgroundFill\BackgroundFill; use PhpTelegramBot\Core\Entities\Document; /** - * @method Document getDocument() Document with the pattern - * @method BackgroundFill getFill() The background fill that is combined with the pattern - * @method int getIntensity() Intensity of the pattern when it is shown above the filled background; 0-100 - * @method true|null getIsInverted() Optional. True, if the background fill must be applied only to the pattern itself. All other pixels are black in this case. For dark themes only - * @method true|null getIsMoving() Optional. True, if the background moves slightly when the device is tilted + * @method Document getDocument() Document with the pattern + * @method BackgroundFill getFill() The background fill that is combined with the pattern + * @method int getIntensity() Intensity of the pattern when it is shown above the filled background; 0-100 + * @method bool isInverted() Optional. True, if the background fill must be applied only to the pattern itself. All other pixels are black in this case. For dark themes only + * @method bool isMoving() Optional. True, if the background moves slightly when the device is tilted */ -class BackgroundTypePattern extends BackgroundType +class BackgroundTypePattern extends BackgroundType implements AllowsBypassingGet { protected static function subEntities(): array { return [ 'document' => Document::class, - 'fill' => BackgroundFill::class, + 'fill' => BackgroundFill::class, + ]; + } + + public static function fieldsBypassingGet(): array + { + return [ + 'is_inverted' => false, + 'is_moving' => false, ]; } } diff --git a/src/Entities/BackgroundType/BackgroundTypeWallpaper.php b/src/Entities/BackgroundType/BackgroundTypeWallpaper.php index 2ffc14734..fd3f25890 100644 --- a/src/Entities/BackgroundType/BackgroundTypeWallpaper.php +++ b/src/Entities/BackgroundType/BackgroundTypeWallpaper.php @@ -2,15 +2,16 @@ namespace PhpTelegramBot\Core\Entities\BackgroundType; +use PhpTelegramBot\Core\Contracts\AllowsBypassingGet; use PhpTelegramBot\Core\Entities\Document; /** - * @method Document getDocument() Document with the wallpaper - * @method int getDarkThemeDimming() Dimming of the background in dark themes, as a percentage; 0-100 - * @method true|null getIsBlurred() Optional. True, if the wallpaper is downscaled to fit in a 450x450 square and then box-blurred with radius 12 - * @method true|null getIsMoving() Optional. True, if the background moves slightly when the device is tilted + * @method Document getDocument() Document with the wallpaper + * @method int getDarkThemeDimming() Dimming of the background in dark themes, as a percentage; 0-100 + * @method bool isBlurred() Optional. True, if the wallpaper is downscaled to fit in a 450x450 square and then box-blurred with radius 12 + * @method bool isMoving() Optional. True, if the background moves slightly when the device is tilted */ -class BackgroundTypeWallpaper extends BackgroundType +class BackgroundTypeWallpaper extends BackgroundType implements AllowsBypassingGet { protected static function subEntities(): array { @@ -18,4 +19,12 @@ protected static function subEntities(): array 'document' => Document::class, ]; } + + public static function fieldsBypassingGet(): array + { + return [ + 'is_blurred' => false, + 'is_moving' => false, + ]; + } } diff --git a/src/Entities/Birthdate.php b/src/Entities/Birthdate.php new file mode 100644 index 000000000..1ddaf2b09 --- /dev/null +++ b/src/Entities/Birthdate.php @@ -0,0 +1,13 @@ + new BotCommandScopeDefault($data), + self::TYPE_ALL_PRIVATE_CHATS => new BotCommandScopeAllPrivateChats($data), + self::TYPE_ALL_GROUP_CHATS => new BotCommandScopeAllGroupChats($data), + self::TYPE_ALL_CHAT_ADMINISTRATORS => new BotCommandScopeAllChatAdministrators($data), + self::TYPE_CHAT => new BotCommandScopeChat($data), + self::TYPE_CHAT_ADMINISTRATORS => new BotCommandScopeChatAdministrators($data), + self::TYPE_CHAT_MEMBER => new BotCommandScopeChatMember($data), + }; + } +} diff --git a/src/Entities/BotCommandScope/BotCommandScopeAllChatAdministrators.php b/src/Entities/BotCommandScope/BotCommandScopeAllChatAdministrators.php new file mode 100644 index 000000000..2338b2d89 --- /dev/null +++ b/src/Entities/BotCommandScope/BotCommandScopeAllChatAdministrators.php @@ -0,0 +1,8 @@ + User::class, ]; } + + public static function fieldsBypassingGet(): array + { + return [ + 'is_enabled' => false, + ]; + } } diff --git a/src/Entities/BusinessIntro.php b/src/Entities/BusinessIntro.php new file mode 100644 index 000000000..9cdba8a9f --- /dev/null +++ b/src/Entities/BusinessIntro.php @@ -0,0 +1,18 @@ + Sticker::class, + ]; + } +} diff --git a/src/Entities/BusinessLocation.php b/src/Entities/BusinessLocation.php new file mode 100644 index 000000000..ac40a3f0f --- /dev/null +++ b/src/Entities/BusinessLocation.php @@ -0,0 +1,17 @@ + Location::class, + ]; + } +} diff --git a/src/Entities/BusinessOpeningHours.php b/src/Entities/BusinessOpeningHours.php new file mode 100644 index 000000000..101f03ffa --- /dev/null +++ b/src/Entities/BusinessOpeningHours.php @@ -0,0 +1,17 @@ + [BusinessOpeningHoursInterval::class], + ]; + } +} diff --git a/src/Entities/BusinessOpeningHoursInterval.php b/src/Entities/BusinessOpeningHoursInterval.php new file mode 100644 index 000000000..95355ee2c --- /dev/null +++ b/src/Entities/BusinessOpeningHoursInterval.php @@ -0,0 +1,12 @@ + User::class, + 'from' => User::class, 'message' => MaybeInaccessibleMessage::class, ]; } diff --git a/src/Entities/Chat.php b/src/Entities/Chat.php index 0617ba43e..99c11c411 100644 --- a/src/Entities/Chat.php +++ b/src/Entities/Chat.php @@ -2,6 +2,8 @@ namespace PhpTelegramBot\Core\Entities; +use PhpTelegramBot\Core\Contracts\AllowsBypassingGet; + /** * @method int getId() Unique identifier for this chat. This number may have more than 32 significant bits and some programming languages may have difficulty/silent defects in interpreting it. But it has at most 52 significant bits, so a signed 64-bit integer or double-precision float type are safe for storing this identifier. * @method string getType() Type of the chat, can be either “private”, “group”, “supergroup” or “channel” @@ -9,9 +11,22 @@ * @method string|null getUsername() Optional. Username, for private chats, supergroups and channels if available * @method string|null getFirstName() Optional. First name of the other party in a private chat * @method string|null getLastName() Optional. Last name of the other party in a private chat - * @method true|null getIsForum() Optional. True, if the supergroup chat is a forum (has topics enabled). + * @method bool isForum() Optional. True, if the supergroup chat is a forum (has topics enabled). */ -class Chat extends Entity +class Chat extends Entity implements AllowsBypassingGet { - // + public const TYPE_PRIVATE = 'private'; + + public const TYPE_GROUP = 'group'; + + public const TYPE_SUPERGROUP = 'supergroup'; + + public const TYPE_CHANNEL = 'channel'; + + public static function fieldsBypassingGet(): array + { + return [ + 'is_forum' => false, + ]; + } } diff --git a/src/Entities/ChatAdministratorRights.php b/src/Entities/ChatAdministratorRights.php new file mode 100644 index 000000000..fd0ae8d2e --- /dev/null +++ b/src/Entities/ChatAdministratorRights.php @@ -0,0 +1,46 @@ + false, + 'can_manage_chat' => false, + 'can_delete_messages' => false, + 'can_manage_video_chats' => false, + 'can_restrict_members' => false, + 'can_promote_members' => false, + 'can_change_info' => false, + 'can_invite_users' => false, + 'can_post_stories' => false, + 'can_edit_stories' => false, + 'can_delete_stories' => false, + 'can_post_messages' => false, + 'can_edit_messages' => false, + 'can_pin_messages' => false, + 'can_manage_topics' => false, + ]; + } +} diff --git a/src/Entities/ChatBoostRemoved.php b/src/Entities/ChatBoostRemoved.php index 341027d7a..725c78f26 100644 --- a/src/Entities/ChatBoostRemoved.php +++ b/src/Entities/ChatBoostRemoved.php @@ -15,7 +15,7 @@ class ChatBoostRemoved extends Entity protected static function subEntities(): array { return [ - 'chat' => Chat::class, + 'chat' => Chat::class, 'source' => ChatBoostSource::class, ]; } diff --git a/src/Entities/ChatBoostSource/ChatBoostSource.php b/src/Entities/ChatBoostSource/ChatBoostSource.php index 314818974..11c2cfe2c 100644 --- a/src/Entities/ChatBoostSource/ChatBoostSource.php +++ b/src/Entities/ChatBoostSource/ChatBoostSource.php @@ -19,9 +19,9 @@ class ChatBoostSource extends Entity implements Factory public static function make(array $data): static { return match ($data['source']) { - self::SOURCE_PREMIUM => new ChatBoostSourcePremium($data), + self::SOURCE_PREMIUM => new ChatBoostSourcePremium($data), self::SOURCE_GIFT_CODE => new ChatBoostSourceGiftCode($data), - self::SOURCE_GIVEAWAY => new ChatBoostSourceGiveaway($data), + self::SOURCE_GIVEAWAY => new ChatBoostSourceGiveaway($data), }; } } diff --git a/src/Entities/ChatBoostSource/ChatBoostSourceGiveaway.php b/src/Entities/ChatBoostSource/ChatBoostSourceGiveaway.php index cdda544df..2c98987a5 100644 --- a/src/Entities/ChatBoostSource/ChatBoostSourceGiveaway.php +++ b/src/Entities/ChatBoostSource/ChatBoostSourceGiveaway.php @@ -2,14 +2,15 @@ namespace PhpTelegramBot\Core\Entities\ChatBoostSource; +use PhpTelegramBot\Core\Contracts\AllowsBypassingGet; use PhpTelegramBot\Core\Entities\User; /** * @method int getGiveawayMessageId() Identifier of a message in the chat with the giveaway; the message could have been deleted already. May be 0 if the message isn't sent yet. * @method User|null getUser() Optional. User that won the prize in the giveaway if any - * @method true|null getIsUnclaimed() Optional. True, if the giveaway was completed, but there was no user to win the prize + * @method bool isUnclaimed() Optional. True, if the giveaway was completed, but there was no user to win the prize */ -class ChatBoostSourceGiveaway extends ChatBoostSource +class ChatBoostSourceGiveaway extends ChatBoostSource implements AllowsBypassingGet { protected static function subEntities(): array { @@ -17,4 +18,11 @@ protected static function subEntities(): array 'user' => User::class, ]; } + + public static function fieldsBypassingGet(): array + { + return [ + 'is_unclaimed' => false, + ]; + } } diff --git a/src/Entities/ChatBoostUpdated.php b/src/Entities/ChatBoostUpdated.php index 32cb4aa9d..7ef5e65a5 100644 --- a/src/Entities/ChatBoostUpdated.php +++ b/src/Entities/ChatBoostUpdated.php @@ -11,7 +11,7 @@ class ChatBoostUpdated extends Entity protected static function subEntities(): array { return [ - 'chat' => Chat::class, + 'chat' => Chat::class, 'boost' => ChatBoost::class, ]; } diff --git a/src/Entities/ChatFullInfo.php b/src/Entities/ChatFullInfo.php new file mode 100644 index 000000000..7778acbc8 --- /dev/null +++ b/src/Entities/ChatFullInfo.php @@ -0,0 +1,84 @@ + links only in chats with the user + * @method bool hasRestrictedVoiceAndVideoMessages() Optional. True, if the privacy settings of the other party restrict sending voice and video note messages in the private chat + * @method true|null getJoinToSendMessages() Optional. True, if users need to join the supergroup before they can send messages + * @method true|null getJoinByRequest() Optional. True, if all users directly joining the supergroup without using an invite link need to be approved by supergroup administrators + * @method string|null getDescription() Optional. Description, for groups, supergroups and channel chats + * @method string|null getInviteLink() Optional. Primary invite link, for groups, supergroups and channel chats + * @method Message|null getPinnedMessage() Optional. The most recent pinned message (by sending date). + * @method ChatPermissions|null getPermissions() Optional. Default chat member permissions, for groups and supergroups + * @method int|null getSlowModeDelay() Optional. For supergroups, the minimum allowed delay between consecutive messages sent by each unprivileged user; in seconds + * @method int|null getUnrestrictBoostCount() Optional. For supergroups, the minimum number of boosts that a non-administrator user needs to add in order to ignore slow mode and chat permissions + * @method int|null getMessageAutoDeleteTime() Optional. The time after which all messages sent to the chat will be automatically deleted; in seconds + * @method bool hasAggressiveAntiSpamEnabled() Optional. True, if aggressive anti-spam checks are enabled in the supergroup. The field is only available to chat administrators. + * @method bool hasHiddenMembers() Optional. True, if non-administrators can only get the list of bots and administrators in the chat + * @method bool hasProtectedContent() Optional. True, if messages from the chat can't be forwarded to other chats + * @method bool hasVisibleHistory() Optional. True, if new chat members will have access to old messages; available only to chat administrators + * @method string|null getStickerSetName() Optional. For supergroups, name of the group sticker set + * @method bool canSetStickerSet() Optional. True, if the bot can change the group sticker set + * @method string|null getCustomEmojiStickerSetName() Optional. For supergroups, the name of the group's custom emoji sticker set. Custom emoji from this set can be used by all users and bots in the group. + * @method int|null getLinkedChatId() Optional. Unique identifier for the linked chat, i.e. the discussion group identifier for a channel and vice versa; for supergroups and channel chats. This identifier may be greater than 32 bits and some programming languages may have difficulty/silent defects in interpreting it. But it is smaller than 52 bits, so a signed 64 bit integer or double-precision float type are safe for storing this identifier. + * @method ChatLocation|null getLocation() Optional. For supergroups, the location to which the supergroup is connected + */ +class ChatFullInfo extends Entity implements AllowsBypassingGet +{ + protected static function subEntities(): array + { + return [ + 'photo' => ChatPhoto::class, + 'birthdate' => Birthdate::class, + 'business_intro' => BusinessIntro::class, + 'business_location' => BusinessLocation::class, + 'business_opening_hours' => BusinessOpeningHoursInterval::class, + 'personal_chat' => Chat::class, + 'available_reactions' => [ReactionType::class], + 'pinned_message' => Message::class, + 'permissions' => ChatPermissions::class, + 'location' => ChatLocation::class, + ]; + } + + public static function fieldsBypassingGet(): array + { + return [ + 'is_forum' => false, + 'has_private_forwards' => false, + 'has_restricted_voice_and_video_messages' => false, + 'has_aggressive_anti_spam_enabled' => false, + 'has_hidden_members' => false, + 'has_protected_content' => false, + 'has_visible_history' => false, + 'can_set_sticker_set' => false, + ]; + } +} diff --git a/src/Entities/ChatInviteLink.php b/src/Entities/ChatInviteLink.php index 538a85cd4..a7691ed37 100644 --- a/src/Entities/ChatInviteLink.php +++ b/src/Entities/ChatInviteLink.php @@ -2,18 +2,20 @@ namespace PhpTelegramBot\Core\Entities; +use PhpTelegramBot\Core\Contracts\AllowsBypassingGet; + /** * @method string getInviteLink() The invite link. If the link was created by another chat administrator, then the second part of the link will be replaced with “…”. * @method User getCreator() Creator of the link * @method bool getCreatesJoinRequest() True, if users joining the chat via the link need to be approved by chat administrators - * @method bool getIsPrimary() True, if the link is primary - * @method bool getIsRevoked() True, if the link is revoked + * @method bool isPrimary() True, if the link is primary + * @method bool isRevoked() True, if the link is revoked * @method string|null getName() Optional. Invite link name * @method int|null getExpireDate() Optional. Point in time (Unix timestamp), when the link will expire or has been expired * @method int|null getMemberLimit() Optional. The maximum number of users that can be members of the chat simultaneously after joining the chat via this invite link; 1-99999 * @method int|null getPendingJoinRequestCount() Optional. Number of pending join requests created using this link */ -class ChatInviteLink extends Entity +class ChatInviteLink extends Entity implements AllowsBypassingGet { protected static function subEntities(): array { @@ -21,4 +23,12 @@ protected static function subEntities(): array 'creator' => User::class, ]; } + + public static function fieldsBypassingGet(): array + { + return [ + 'is_primary' => false, + 'is_revoked' => false, + ]; + } } diff --git a/src/Entities/ChatJoinRequest.php b/src/Entities/ChatJoinRequest.php index fb082f78b..d91251547 100644 --- a/src/Entities/ChatJoinRequest.php +++ b/src/Entities/ChatJoinRequest.php @@ -15,8 +15,8 @@ class ChatJoinRequest extends Entity protected static function subEntities(): array { return [ - 'chat' => Chat::class, - 'from' => User::class, + 'chat' => Chat::class, + 'from' => User::class, 'invite_link' => ChatInviteLink::class, ]; } diff --git a/src/Entities/ChatLocation.php b/src/Entities/ChatLocation.php new file mode 100644 index 000000000..4d02b421c --- /dev/null +++ b/src/Entities/ChatLocation.php @@ -0,0 +1,17 @@ + Location::class, + ]; + } +} diff --git a/src/Entities/ChatMember/ChatMember.php b/src/Entities/ChatMember/ChatMember.php index c4aca181c..9b4a0887b 100644 --- a/src/Entities/ChatMember/ChatMember.php +++ b/src/Entities/ChatMember/ChatMember.php @@ -25,12 +25,12 @@ class ChatMember extends Entity implements Factory public static function make(array $data): static { return match ($data['type']) { - self::TYPE_CREATOR => new ChatMemberOwner($data), + self::TYPE_CREATOR => new ChatMemberOwner($data), self::TYPE_ADMINISTRATOR => new ChatMemberAdministrator($data), - self::TYPE_MEMBER => new ChatMemberMember($data), - self::TYPE_RESTRICTED => new ChatMemberRestricted($data), - self::TYPE_LEFT => new ChatMemberLeft($data), - self::TYPE_KICKED => new ChatMemberBanned($data), + self::TYPE_MEMBER => new ChatMemberMember($data), + self::TYPE_RESTRICTED => new ChatMemberRestricted($data), + self::TYPE_LEFT => new ChatMemberLeft($data), + self::TYPE_KICKED => new ChatMemberBanned($data), }; } } diff --git a/src/Entities/ChatMember/ChatMemberAdministrator.php b/src/Entities/ChatMember/ChatMemberAdministrator.php index d05bec8c7..96b30803a 100644 --- a/src/Entities/ChatMember/ChatMemberAdministrator.php +++ b/src/Entities/ChatMember/ChatMemberAdministrator.php @@ -2,29 +2,30 @@ namespace PhpTelegramBot\Core\Entities\ChatMember; +use PhpTelegramBot\Core\Contracts\AllowsBypassingGet; use PhpTelegramBot\Core\Entities\User; /** - * @method User getUser() Information about the user - * @method bool getCanBeEdited() True, if the bot is allowed to edit administrator privileges of that user - * @method bool getIsAnonymous() True, if the user's presence in the chat is hidden - * @method bool getCanManageChat() True, if the administrator can access the chat event log, get boost list, see hidden supergroup and channel members, report spam messages and ignore slow mode. Implied by any other administrator privilege. - * @method bool getCanDeleteMessages() True, if the administrator can delete messages of other users - * @method bool getCanManageVideoChats() True, if the administrator can manage video chats - * @method bool getCanRestrictMembers() True, if the administrator can restrict, ban or unban chat members, or access supergroup statistics - * @method bool getCanPromoteMembers() True, if the administrator can add new administrators with a subset of their own privileges or demote administrators that they have promoted, directly or indirectly (promoted by administrators that were appointed by the user). - * @method bool getCanChangeInfo() True, if the user is allowed to change the chat title, photo and other settings - * @method bool getCanInviteUsers() True, if the user is allowed to invite new users to the chat - * @method bool getCanPostStories() True, if the administrator can post stories to the chat - * @method bool getCanEditStories() True, if the administrator can edit stories posted by other users, post stories to the chat page, pin chat stories, and access the chat's story archive - * @method bool getCanDeleteStories() True, if the administrator can delete stories posted by other users - * @method bool|null getCanPostMessages() Optional. True, if the administrator can post messages in the channel, or access channel statistics; for channels only - * @method bool|null getCanEditMessages() Optional. True, if the administrator can edit messages of other users and can pin messages; for channels only - * @method bool|null getCanPinMessages() Optional. True, if the user is allowed to pin messages; for groups and supergroups only - * @method bool|null getCanManageTopics() Optional. True, if the user is allowed to create, rename, close, and reopen forum topics; for supergroups only - * @method string|null getCustomTitle() Optional. Custom title for this user + * @method User getUser() Information about the user + * @method bool canBeEdited() True, if the bot is allowed to edit administrator privileges of that user + * @method bool isAnonymous() True, if the user's presence in the chat is hidden + * @method bool canManageChat() True, if the administrator can access the chat event log, get boost list, see hidden supergroup and channel members, report spam messages and ignore slow mode. Implied by any other administrator privilege. + * @method bool canDeleteMessages() True, if the administrator can delete messages of other users + * @method bool canManageVideoChats() True, if the administrator can manage video chats + * @method bool canRestrictMembers() True, if the administrator can restrict, ban or unban chat members, or access supergroup statistics + * @method bool canPromoteMembers() True, if the administrator can add new administrators with a subset of their own privileges or demote administrators that they have promoted, directly or indirectly (promoted by administrators that were appointed by the user). + * @method bool canChangeInfo() True, if the user is allowed to change the chat title, photo and other settings + * @method bool canInviteUsers() True, if the user is allowed to invite new users to the chat + * @method bool canPostStories() True, if the administrator can post stories to the chat + * @method bool canEditStories() True, if the administrator can edit stories posted by other users, post stories to the chat page, pin chat stories, and access the chat's story archive + * @method bool canDeleteStories() True, if the administrator can delete stories posted by other users + * @method bool canPostMessages() Optional. True, if the administrator can post messages in the channel, or access channel statistics; for channels only + * @method bool canEditMessages() Optional. True, if the administrator can edit messages of other users and can pin messages; for channels only + * @method bool canPinMessages() Optional. True, if the user is allowed to pin messages; for groups and supergroups only + * @method bool canManageTopics() Optional. True, if the user is allowed to create, rename, close, and reopen forum topics; for supergroups only + * @method string|null getCustomTitle() Optional. Custom title for this user */ -class ChatMemberAdministrator extends ChatMember +class ChatMemberAdministrator extends ChatMember implements AllowsBypassingGet { protected static function subEntities(): array { @@ -32,4 +33,21 @@ protected static function subEntities(): array 'user' => User::class, ]; } + + public static function fieldsBypassingGet(): array + { + return [ + 'can_be_edited' => false, + 'is_anonymous' => false, + 'can_manage_chat' => false, + 'can_delete_messages' => false, + 'can_manage_video_chats' => false, + 'can_restrict_members' => false, + 'can_promote_members' => false, + 'can_change_info' => false, + 'can_invite_users' => false, + 'can_post_stories' => false, + 'can_edit_stories' => false, + ]; + } } diff --git a/src/Entities/ChatMember/ChatMemberOwner.php b/src/Entities/ChatMember/ChatMemberOwner.php index da09a128c..9b8bb88f8 100644 --- a/src/Entities/ChatMember/ChatMemberOwner.php +++ b/src/Entities/ChatMember/ChatMemberOwner.php @@ -2,14 +2,15 @@ namespace PhpTelegramBot\Core\Entities\ChatMember; +use PhpTelegramBot\Core\Contracts\AllowsBypassingGet; use PhpTelegramBot\Core\Entities\User; /** * @method User getUser() Information about the user - * @method bool getIsAnonymous() True, if the user's presence in the chat is hidden + * @method bool isAnonymous() True, if the user's presence in the chat is hidden * @method string|null getCustomTitle() Optional. Custom title for this user */ -class ChatMemberOwner extends ChatMember +class ChatMemberOwner extends ChatMember implements AllowsBypassingGet { protected static function subEntities(): array { @@ -17,4 +18,11 @@ protected static function subEntities(): array 'user' => User::class, ]; } + + public static function fieldsBypassingGet(): array + { + return [ + 'is_anonymous' => false, + ]; + } } diff --git a/src/Entities/ChatMember/ChatMemberRestricted.php b/src/Entities/ChatMember/ChatMemberRestricted.php index 9b542b3b3..713446356 100644 --- a/src/Entities/ChatMember/ChatMemberRestricted.php +++ b/src/Entities/ChatMember/ChatMemberRestricted.php @@ -2,28 +2,29 @@ namespace PhpTelegramBot\Core\Entities\ChatMember; +use PhpTelegramBot\Core\Contracts\AllowsBypassingGet; use PhpTelegramBot\Core\Entities\User; /** - * @method User getUser() Information about the user - * @method bool getIsMember() True, if the user is a member of the chat at the moment of the request - * @method bool getCanSendMessages() True, if the user is allowed to send text messages, contacts, giveaways, giveaway winners, invoices, locations and venues - * @method bool getCanSendAudios() True, if the user is allowed to send audios - * @method bool getCanSendDocuments() True, if the user is allowed to send documents - * @method bool getCanSendPhotos() True, if the user is allowed to send photos - * @method bool getCanSendVideos() True, if the user is allowed to send videos - * @method bool getCanSendVideoNotes() True, if the user is allowed to send video notes - * @method bool getCanSendVoiceNotes() True, if the user is allowed to send voice notes - * @method bool getCanSendPolls() True, if the user is allowed to send polls - * @method bool getCanSendOtherMessages() True, if the user is allowed to send animations, games, stickers and use inline bots - * @method bool getCanAddWebPagePreviews() True, if the user is allowed to add web page previews to their messages - * @method bool getCanChangeInfo() True, if the user is allowed to change the chat title, photo and other settings - * @method bool getCanInviteUsers() True, if the user is allowed to invite new users to the chat - * @method bool getCanPinMessages() True, if the user is allowed to pin messages - * @method bool getCanManageTopics() True, if the user is allowed to create forum topics - * @method int getUntilDate() Date when restrictions will be lifted for this user; Unix time. If 0, then the user is restricted forever + * @method User getUser() Information about the user + * @method bool isMember() True, if the user is a member of the chat at the moment of the request + * @method bool canSendMessages() True, if the user is allowed to send text messages, contacts, giveaways, giveaway winners, invoices, locations and venues + * @method bool canSendAudios() True, if the user is allowed to send audios + * @method bool canSendDocuments() True, if the user is allowed to send documents + * @method bool canSendPhotos() True, if the user is allowed to send photos + * @method bool canSendVideos() True, if the user is allowed to send videos + * @method bool canSendVideoNotes() True, if the user is allowed to send video notes + * @method bool canSendVoiceNotes() True, if the user is allowed to send voice notes + * @method bool canSendPolls() True, if the user is allowed to send polls + * @method bool canSendOtherMessages() True, if the user is allowed to send animations, games, stickers and use inline bots + * @method bool canAddWebPagePreviews() True, if the user is allowed to add web page previews to their messages + * @method bool canChangeInfo() True, if the user is allowed to change the chat title, photo and other settings + * @method bool canInviteUsers() True, if the user is allowed to invite new users to the chat + * @method bool canPinMessages() True, if the user is allowed to pin messages + * @method bool canManageTopics() True, if the user is allowed to create forum topics + * @method int getUntilDate() Date when restrictions will be lifted for this user; Unix time. If 0, then the user is restricted forever */ -class ChatMemberRestricted extends ChatMember +class ChatMemberRestricted extends ChatMember implements AllowsBypassingGet { protected static function subEntities(): array { @@ -31,4 +32,21 @@ protected static function subEntities(): array 'user' => User::class, ]; } + + public static function fieldsBypassingGet(): array + { + return [ + 'is_member' => false, + 'can_send_messages' => false, + 'can_send_audios' => false, + 'can_send_documents' => false, + 'can_send_photos' => false, + 'can_send_videos' => false, + 'can_send_video_notes' => false, + 'can_send_voice_notes' => false, + 'can_send_polls' => false, + 'can_send_other_messages' => false, + 'can_add_web_page_previews' => false, + ]; + } } diff --git a/src/Entities/ChatMemberUpdated.php b/src/Entities/ChatMemberUpdated.php index dc7693c2f..e6b1cbbf9 100644 --- a/src/Entities/ChatMemberUpdated.php +++ b/src/Entities/ChatMemberUpdated.php @@ -19,11 +19,11 @@ class ChatMemberUpdated extends Entity protected static function subEntities(): array { return [ - 'chat' => Chat::class, - 'from' => User::class, + 'chat' => Chat::class, + 'from' => User::class, 'old_chat_member' => ChatMember::class, 'new_chat_member' => ChatMember::class, - 'invite_link' => ChatInviteLink::class, + 'invite_link' => ChatInviteLink::class, ]; } } diff --git a/src/Entities/ChatPermissions.php b/src/Entities/ChatPermissions.php new file mode 100644 index 000000000..4542918aa --- /dev/null +++ b/src/Entities/ChatPermissions.php @@ -0,0 +1,42 @@ + false, + 'can_send_audios' => false, + 'can_send_documents' => false, + 'can_send_photos' => false, + 'can_send_videos' => false, + 'can_send_video_notes' => false, + 'can_send_voice_notes' => false, + 'can_send_polls' => false, + 'can_send_other_messages' => false, + 'can_add_web_page_previews' => false, + 'can_change_info' => false, + ]; + } +} diff --git a/src/Entities/ChatPhoto.php b/src/Entities/ChatPhoto.php new file mode 100644 index 000000000..4c2ea3f94 --- /dev/null +++ b/src/Entities/ChatPhoto.php @@ -0,0 +1,14 @@ + User::class, + 'from' => User::class, 'location' => Location::class, ]; } diff --git a/src/Entities/EncryptedPassportElement.php b/src/Entities/EncryptedPassportElement.php index 11a7d2a70..64f039aa6 100644 --- a/src/Entities/EncryptedPassportElement.php +++ b/src/Entities/EncryptedPassportElement.php @@ -45,11 +45,11 @@ class EncryptedPassportElement extends Entity protected static function subEntities(): array { return [ - 'files' => [PassportFile::class], - 'front_side' => PassportFile::class, + 'files' => [PassportFile::class], + 'front_side' => PassportFile::class, 'reverse_side' => PassportFile::class, - 'selfie' => PassportFile::class, - 'translation' => [PassportFile::class], + 'selfie' => PassportFile::class, + 'translation' => [PassportFile::class], ]; } } diff --git a/src/Entities/Entity.php b/src/Entities/Entity.php index 53c04ed75..a45d328c2 100644 --- a/src/Entities/Entity.php +++ b/src/Entities/Entity.php @@ -2,6 +2,8 @@ namespace PhpTelegramBot\Core\Entities; +use PhpTelegramBot\Core\Contracts\AllowsBypassingGet; + abstract class Entity implements \JsonSerializable { protected array $fields = []; @@ -32,7 +34,7 @@ public function __set(string $name, $value): void $this->fields[$name] = $value; } - protected function getField(string $name): mixed + private function getField(string $name): mixed { $data = $this->fields[$name]; @@ -70,11 +72,25 @@ public function __call(string $name, array $arguments) $snakeName = strtolower(ltrim(preg_replace('/[[:upper:]]/', '_$0', $name), '_')); if (str_starts_with($snakeName, 'get_')) { + // Getter for fields return $this->getField(substr($snakeName, 4)); } elseif (str_starts_with($snakeName, 'set_')) { + // Setter for fields $this->setField(substr($snakeName, 4), $arguments[0] ?? null); return $this; + } elseif (is_subclass_of($this, AllowsBypassingGet::class)) { + $fields = $this::fieldsBypassingGet(); + + if (array_key_exists($snakeName, $fields)) { + + return $this->getField($snakeName) ?? $fields[$snakeName]; + + } elseif (in_array($snakeName, $fields)) { + + return $this->getField($snakeName); + + } } $method = get_class($this).'::'.$name.'()'; diff --git a/src/Entities/ExternalReplyInfo.php b/src/Entities/ExternalReplyInfo.php index 1602ef4dc..3dde0106a 100644 --- a/src/Entities/ExternalReplyInfo.php +++ b/src/Entities/ExternalReplyInfo.php @@ -2,6 +2,7 @@ namespace PhpTelegramBot\Core\Entities; +use PhpTelegramBot\Core\Contracts\AllowsBypassingGet; use PhpTelegramBot\Core\Entities\MessageOrigin\MessageOrigin; /** @@ -18,7 +19,7 @@ * @method Video|null getVideo() Optional. Message is a video, information about the video * @method VideoNote|null getVideoNote() Optional. Message is a video note, information about the video message * @method Voice|null getVoice() Optional. Message is a voice message, information about the file - * @method true|null getHasMediaSpoiler() Optional. True, if the message media is covered by a spoiler animation + * @method bool hasMediaSpoiler() Optional. True, if the message media is covered by a spoiler animation * @method Contact|null getContact() Optional. Message is a shared contact, information about the contact * @method Dice|null getDice() Optional. Message is a dice with random value * @method Game|null getGame() Optional. Message is a game, information about the game. More about games » @@ -29,32 +30,39 @@ * @method Poll|null getPoll() Optional. Message is a native poll, information about the poll * @method Venue|null getVenue() Optional. Message is a venue, information about the venue */ -class ExternalReplyInfo extends Entity +class ExternalReplyInfo extends Entity implements AllowsBypassingGet { protected static function subEntities(): array { return [ - 'origin' => MessageOrigin::class, - 'chat' => Chat::class, + 'origin' => MessageOrigin::class, + 'chat' => Chat::class, 'link_preview_options' => LinkPreviewOptions::class, - 'animation' => Animation::class, - 'audio' => Audio::class, - 'document' => Document::class, - 'photo' => [PhotoSize::class], - 'sticker' => Sticker::class, - 'story' => Story::class, - 'video' => Video::class, - 'video_note' => VideoNote::class, - 'voice' => Voice::class, - 'contact' => Contact::class, - 'dice' => Dice::class, - 'game' => Game::class, - 'giveaway' => Giveaway::class, - 'giveaway_winners' => GiveawayWinners::class, - 'invoice' => Invoice::class, - 'location' => Location::class, - 'poll' => Poll::class, - 'venue' => Venue::class, + 'animation' => Animation::class, + 'audio' => Audio::class, + 'document' => Document::class, + 'photo' => [PhotoSize::class], + 'sticker' => Sticker::class, + 'story' => Story::class, + 'video' => Video::class, + 'video_note' => VideoNote::class, + 'voice' => Voice::class, + 'contact' => Contact::class, + 'dice' => Dice::class, + 'game' => Game::class, + 'giveaway' => Giveaway::class, + 'giveaway_winners' => GiveawayWinners::class, + 'invoice' => Invoice::class, + 'location' => Location::class, + 'poll' => Poll::class, + 'venue' => Venue::class, + ]; + } + + public static function fieldsBypassingGet(): array + { + return [ + 'has_media_spoiler' => false, ]; } } diff --git a/src/Entities/ForceReply.php b/src/Entities/ForceReply.php new file mode 100644 index 000000000..87592faa2 --- /dev/null +++ b/src/Entities/ForceReply.php @@ -0,0 +1,13 @@ + [PhotoSize::class], + 'photo' => [PhotoSize::class], 'text_entities' => [MessageEntity::class], - 'animation' => Animation::class, + 'animation' => Animation::class, ]; } } diff --git a/src/Entities/Giveaway.php b/src/Entities/Giveaway.php index ef39b0a18..938238f32 100644 --- a/src/Entities/Giveaway.php +++ b/src/Entities/Giveaway.php @@ -2,17 +2,19 @@ namespace PhpTelegramBot\Core\Entities; +use PhpTelegramBot\Core\Contracts\AllowsBypassingGet; + /** * @method Chat[] getChats() The list of chats which the user must join to participate in the giveaway * @method int getWinnersSelectionDate() Point in time (Unix timestamp) when winners of the giveaway will be selected * @method int getWinnerCount() The number of users which are supposed to be selected as winners of the giveaway * @method true|null getOnlyNewMembers() Optional. True, if only users who join the chats after the giveaway started should be eligible to win - * @method true|null getHasPublicWinners() Optional. True, if the list of giveaway winners will be visible to everyone + * @method bool hasPublicWinners() Optional. True, if the list of giveaway winners will be visible to everyone * @method string|null getPrizeDescription() Optional. Description of additional giveaway prize * @method string[]|null getCountryCodes() Optional. A list of two-letter ISO 3166-1 alpha-2 country codes indicating the countries from which eligible users for the giveaway must come. If empty, then all users can participate in the giveaway. Users with a phone number that was bought on Fragment can always participate in giveaways. * @method int|null getPremiumSubscriptionMonthCount() Optional. The number of months the Telegram Premium subscription won from the giveaway will be active for */ -class Giveaway extends Entity +class Giveaway extends Entity implements AllowsBypassingGet { protected static function subEntities(): array { @@ -20,4 +22,11 @@ protected static function subEntities(): array 'chats' => [Chat::class], ]; } + + public static function fieldsBypassingGet(): array + { + return [ + 'has_public_winners' => false, + ]; + } } diff --git a/src/Entities/GiveawayWinners.php b/src/Entities/GiveawayWinners.php index eec4cca02..4d00b6e96 100644 --- a/src/Entities/GiveawayWinners.php +++ b/src/Entities/GiveawayWinners.php @@ -2,6 +2,8 @@ namespace PhpTelegramBot\Core\Entities; +use PhpTelegramBot\Core\Contracts\AllowsBypassingGet; + /** * @method Chat getChat() The chat that created the giveaway * @method int getGiveawayMessageId() Identifier of the message with the giveaway in the chat @@ -12,16 +14,23 @@ * @method int|null getPremiumSubscriptionMonthCount() Optional. The number of months the Telegram Premium subscription won from the giveaway will be active for * @method int|null getUnclaimedPrizeCount() Optional. Number of undistributed prizes * @method true|null getOnlyNewMembers() Optional. True, if only users who had joined the chats after the giveaway started were eligible to win - * @method true|null getWasRefunded() Optional. True, if the giveaway was canceled because the payment for it was refunded + * @method bool wasRefunded() Optional. True, if the giveaway was canceled because the payment for it was refunded * @method string|null getPrizeDescription() Optional. Description of additional giveaway prize */ -class GiveawayWinners extends Entity +class GiveawayWinners extends Entity implements AllowsBypassingGet { protected static function subEntities(): array { return [ - 'chat' => Chat::class, + 'chat' => Chat::class, 'winners' => [User::class], ]; } + + public static function fieldsBypassingGet(): array + { + return [ + 'was_refunded' => false, + ]; + } } diff --git a/src/Entities/InlineKeyboardButton.php b/src/Entities/InlineKeyboardButton.php index 580ec88a5..c57400027 100644 --- a/src/Entities/InlineKeyboardButton.php +++ b/src/Entities/InlineKeyboardButton.php @@ -18,10 +18,10 @@ class InlineKeyboardButton extends Entity protected static function subEntities(): array { return [ - 'web_app' => WebAppInfo::class, - 'login_url' => LoginUrl::class, + 'web_app' => WebAppInfo::class, + 'login_url' => LoginUrl::class, 'switch_inline_query_chosen_chat' => SwitchInlineQueryChosenChat::class, - 'callback_game' => CallbackGame::class, + 'callback_game' => CallbackGame::class, ]; } } diff --git a/src/Entities/InlineKeyboardMarkup.php b/src/Entities/InlineKeyboardMarkup.php index 9033b2c69..a33fc9d03 100644 --- a/src/Entities/InlineKeyboardMarkup.php +++ b/src/Entities/InlineKeyboardMarkup.php @@ -2,6 +2,9 @@ namespace PhpTelegramBot\Core\Entities; +/** + * @method array getInlineKeyboard() Array of button rows, each represented by an Array of InlineKeyboardButton objects + */ class InlineKeyboardMarkup extends Entity { protected static function subEntities(): array diff --git a/src/Entities/InlineQuery.php b/src/Entities/InlineQuery.php index f35240d78..179654c55 100644 --- a/src/Entities/InlineQuery.php +++ b/src/Entities/InlineQuery.php @@ -15,7 +15,7 @@ class InlineQuery extends Entity protected static function subEntities(): array { return [ - 'from' => User::class, + 'from' => User::class, 'location' => Location::class, ]; } diff --git a/src/Entities/InputFile.php b/src/Entities/InputFile.php new file mode 100644 index 000000000..342b8c2c8 --- /dev/null +++ b/src/Entities/InputFile.php @@ -0,0 +1,7 @@ + new InputMediaPhoto($data), + self::TYPE_VIDEO => new InputMediaVideo($data), + self::TYPE_ANIMATION => new InputMediaAnimation($data), + self::TYPE_AUDIO => new InputMediaAudio($data), + self::TYPE_DOCUMENT => new InputMediaDocument($data), + }; + } +} diff --git a/src/Entities/InputMedia/InputMediaAnimation.php b/src/Entities/InputMedia/InputMediaAnimation.php new file mode 100644 index 000000000..c47890b81 --- /dev/null +++ b/src/Entities/InputMedia/InputMediaAnimation.php @@ -0,0 +1,36 @@ +” to upload a new one using multipart/form-data under name. + * @method string|InputFile getThumbnail() Optional. Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail's width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can't be reused and can be only uploaded as a new file, so you can pass “attach://” if the thumbnail was uploaded using multipart/form-data under . + * @method string|null getCaption() Optional. Caption of the animation to be sent, 0-1024 characters after entities parsing + * @method string|null getParseMode() Optional. Mode for parsing entities in the animation caption. See formatting options for more details. + * @method MessageEntity[]|null getCaptionEntites() Optional. List of special entities that appear in the caption, which can be specified instead of parse_mode + * @method bool|null getShowCaptionAboveMedia() Optional. Pass True, if the caption must be shown above the message media + * @method int|null getWidth() Optional. Animation width + * @method int|null getHeight() Optional. Animation height + * @method int|null getDuration() Optional. Animation duration in seconds + * @method bool hasSpoiler() Optional. Pass True if the animation needs to be covered with a spoiler animation + */ +class InputMediaAnimation extends InputMedia implements AllowsBypassingGet +{ + protected static function subEntities(): array + { + return [ + 'caption_entities' => [MessageEntity::class], + ]; + } + + public static function fieldsBypassingGet(): array + { + return [ + 'has_spoiler' => false, + ]; + } +} diff --git a/src/Entities/InputMedia/InputMediaAudio.php b/src/Entities/InputMedia/InputMediaAudio.php new file mode 100644 index 000000000..cd3eb8eda --- /dev/null +++ b/src/Entities/InputMedia/InputMediaAudio.php @@ -0,0 +1,26 @@ +” to upload a new one using multipart/form-data under name. + * @method string|InputFile getThumbnail() Optional. Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail's width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can't be reused and can be only uploaded as a new file, so you can pass “attach://” if the thumbnail was uploaded using multipart/form-data under . + * @method string|null getCaption() Optional. Caption of the audio to be sent, 0-1024 characters after entities parsing + * @method string|null getParseMode() Optional. Mode for parsing entities in the audio caption. See formatting options for more details. + * @method MessageEntity[]|null getCaptionEntities() Optional. List of special entities that appear in the caption, which can be specified instead of parse_mode + * @method int|null getDuration() Optional. Duration of the audio in seconds + * @method string|null getPerformer() Optional. Performer of the audio + * @method string|null getTitle() Optional. Title of the audio + */ +class InputMediaAudio extends InputMedia +{ + protected static function subEntities(): array + { + return [ + 'caption_entities' => [MessageEntity::class], + ]; + } +} diff --git a/src/Entities/InputMedia/InputMediaDocument.php b/src/Entities/InputMedia/InputMediaDocument.php new file mode 100644 index 000000000..89c614fa9 --- /dev/null +++ b/src/Entities/InputMedia/InputMediaDocument.php @@ -0,0 +1,24 @@ +” to upload a new one using multipart/form-data under name. + * @method string|InputFile getThumbnail() Optional. Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail's width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can't be reused and can be only uploaded as a new file, so you can pass “attach://” if the thumbnail was uploaded using multipart/form-data under . + * @method string|null getCaption() Optional. Caption of the document to be sent, 0-1024 characters after entities parsing + * @method string|null getParseMode() Optional. Mode for parsing entities in the document caption. See formatting options for more details. + * @method MessageEntity[]|null getCaptionEntities() Optional. List of special entities that appear in the caption, which can be specified instead of parse_mode + * @method bool|null getDisableContentTypeDetection() Optional. Disables automatic server-side content type detection for files uploaded using multipart/form-data. Always True, if the document is sent as part of an album. + */ +class InputMediaDocument extends InputMedia +{ + protected static function subEntities(): array + { + return [ + 'caption_entites' => [MessageEntity::class], + ]; + } +} diff --git a/src/Entities/InputMedia/InputMediaPhoto.php b/src/Entities/InputMedia/InputMediaPhoto.php new file mode 100644 index 000000000..bbe60ac9c --- /dev/null +++ b/src/Entities/InputMedia/InputMediaPhoto.php @@ -0,0 +1,31 @@ +” to upload a new one using multipart/form-data under name. More information on Sending Files » + * @method string|null getCaption() Optional. Caption of the photo to be sent, 0-1024 characters after entities parsing + * @method string|null getParseMode() Optional. Mode for parsing entities in the photo caption. See formatting options for more details. + * @method MessageEntity[]|null getCaptionEntities() Optional. List of special entities that appear in the caption, which can be specified instead of parse_mode + * @method bool|null getShowCaptionAboveMedia() Optional. Pass True, if the caption must be shown above the message media + * @method bool hasSpoiler() Optional. Pass True if the photo needs to be covered with a spoiler animation + */ +class InputMediaPhoto extends InputMedia implements AllowsBypassingGet +{ + protected static function subEntities(): array + { + return [ + 'caption_entities' => [MessageEntity::class], + ]; + } + + public static function fieldsBypassingGet(): array + { + return [ + 'has_spoiler' => false, + ]; + } +} diff --git a/src/Entities/InputMedia/InputMediaVideo.php b/src/Entities/InputMedia/InputMediaVideo.php new file mode 100644 index 000000000..f92acd445 --- /dev/null +++ b/src/Entities/InputMedia/InputMediaVideo.php @@ -0,0 +1,38 @@ +” to upload a new one using multipart/form-data under name. + * @method string|InputFile getThumbnail() Optional. Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail's width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can't be reused and can be only uploaded as a new file, so you can pass “attach://” if the thumbnail was uploaded using multipart/form-data under . + * @method string|null getCaption() Optional. Caption of the video to be sent, 0-1024 characters after entities parsing + * @method string|null getParseMode() Optional. Mode for parsing entities in the video caption. See formatting options for more details. + * @method MessageEntity[]|null getCaptionEntities() Optional. List of special entities that appear in the caption, which can be specified instead of parse_mode + * @method bool|null getShowCaptionAboveMedia() Optional. Pass True, if the caption must be shown above the message media + * @method int|null getWidth() Optional. Video width + * @method int|null getHeight() Optional. Video height + * @method int|null getDuration() Optional. Video duration in seconds + * @method bool|null getSupportsStreaming() Optional. Pass True if the uploaded video is suitable for streaming + * @method bool hasSpoiler() Optional. Pass True if the video needs to be covered with a spoiler animation + */ +class InputMediaVideo extends Entity implements AllowsBypassingGet +{ + protected static function subEntities(): array + { + return [ + 'caption_entities' => [MessageEntity::class], + ]; + } + + public static function fieldsBypassingGet(): array + { + return [ + 'has_spoiler' => false, + ]; + } +} diff --git a/src/Entities/InputPollOption.php b/src/Entities/InputPollOption.php new file mode 100644 index 000000000..9b8957a09 --- /dev/null +++ b/src/Entities/InputPollOption.php @@ -0,0 +1,18 @@ + [MessageEntity::class], + ]; + } +} diff --git a/src/Entities/KeyboardButton.php b/src/Entities/KeyboardButton.php new file mode 100644 index 000000000..fc9a90813 --- /dev/null +++ b/src/Entities/KeyboardButton.php @@ -0,0 +1,25 @@ + KeyboardButtonRequestUsers::class, + 'request_chat' => KeyboardButtonRequestChat::class, + 'request_poll' => KeyboardButtonPollType::class, + 'web_app' => WebAppInfo::class, + ]; + } +} diff --git a/src/Entities/KeyboardButtonPollType.php b/src/Entities/KeyboardButtonPollType.php new file mode 100644 index 000000000..361ee418b --- /dev/null +++ b/src/Entities/KeyboardButtonPollType.php @@ -0,0 +1,11 @@ + ChatAdministratorRights::class, + 'bot_administrator_rights' => ChatAdministratorRights::class, + ]; + } +} diff --git a/src/Entities/KeyboardButtonRequestUsers.php b/src/Entities/KeyboardButtonRequestUsers.php new file mode 100644 index 000000000..adbb0902c --- /dev/null +++ b/src/Entities/KeyboardButtonRequestUsers.php @@ -0,0 +1,17 @@ + false, + ]; + } } diff --git a/src/Entities/MaybeInaccessibleMessage.php b/src/Entities/MaybeInaccessibleMessage.php index a0ba20b15..e372bd823 100644 --- a/src/Entities/MaybeInaccessibleMessage.php +++ b/src/Entities/MaybeInaccessibleMessage.php @@ -13,7 +13,7 @@ public static function make(array $data): static { return match (true) { $data['date'] === 0 => new InaccessibleMessage($data), - default => new Message($data), + default => new Message($data), }; } diff --git a/src/Entities/MenuButton/MenuButton.php b/src/Entities/MenuButton/MenuButton.php new file mode 100644 index 000000000..ffc6b6acb --- /dev/null +++ b/src/Entities/MenuButton/MenuButton.php @@ -0,0 +1,27 @@ + new MenuButtonCommands($data), + self::TYPE_WEB_APP => new MenuButtonWebApp($data), + self::TYPE_DEFAULT => new MenuButtonDefault($data), + }; + } +} diff --git a/src/Entities/MenuButton/MenuButtonCommands.php b/src/Entities/MenuButton/MenuButtonCommands.php new file mode 100644 index 000000000..d0394fba2 --- /dev/null +++ b/src/Entities/MenuButton/MenuButtonCommands.php @@ -0,0 +1,8 @@ + WebAppInfo::class, + ]; + } +} diff --git a/src/Entities/Message.php b/src/Entities/Message.php index 2dcd5ced1..08c1a0b9b 100644 --- a/src/Entities/Message.php +++ b/src/Entities/Message.php @@ -2,6 +2,7 @@ namespace PhpTelegramBot\Core\Entities; +use PhpTelegramBot\Core\Contracts\AllowsBypassingGet; use PhpTelegramBot\Core\Entities\MessageOrigin\MessageOrigin; /** @@ -15,16 +16,16 @@ * @method string|null getBusinessConnectionId() Optional. Unique identifier of the business connection from which the message was received. If non-empty, the message belongs to a chat of the corresponding business account that is independent from any potential bot chat which might share the same identifier. * @method Chat getChat() Chat the message belongs to * @method MessageOrigin|null getForwardOrigin() Optional. Information about the original message for forwarded messages - * @method true|null getIsTopicMessage() Optional. True, if the message is sent to a forum topic - * @method true|null getIsAutomaticForward() Optional. True, if the message is a channel post that was automatically forwarded to the connected discussion group + * @method bool isTopicMessage() Optional. True, if the message is sent to a forum topic + * @method bool isAutomaticForward() Optional. True, if the message is a channel post that was automatically forwarded to the connected discussion group * @method Message|null getReplyToMessage() Optional. For replies in the same chat and message thread, the original message. Note that the Message object in this field will not contain further reply_to_message fields even if it itself is a reply. * @method ExternalReplyInfo|null getExternalReply() Optional. Information about the message that is being replied to, which may come from another chat or forum topic * @method TextQuote|null getQuote() Optional. For replies that quote part of the original message, the quoted part of the message * @method Story|null getReplyToStory() Optional. For replies to a story, the original story * @method User|null getViaBot() Optional. Bot through which the message was sent * @method int|null getEditDate() Optional. Date the message was last edited in Unix time - * @method true|null getHasProtectedContent() Optional. True, if the message can't be forwarded - * @method true|null getIsFromOffline() Optional. True, if the message was sent by an implicit action, for example, as an away or a greeting business message, or as a scheduled message + * @method bool hasProtectedContent() Optional. True, if the message can't be forwarded + * @method bool isFromOffline() Optional. True, if the message was sent by an implicit action, for example, as an away or a greeting business message, or as a scheduled message * @method string|null getMediaGroupId() Optional. The unique identifier of a media message group this message belongs to * @method string|null getAuthorSignature() Optional. Signature of the post author for messages in channels, or the custom title of an anonymous group administrator * @method string|null getText() Optional. For text messages, the actual UTF-8 text of the message @@ -43,7 +44,7 @@ * @method string|null getCaption() Optional. Caption for the animation, audio, document, photo, video or voice * @method MessageEntity[]|null getCaptionEntities() Optional. For messages with a caption, special entities like usernames, URLs, bot commands, etc. that appear in the caption * @method true|null getShowCaptionAboveMedia() Optional. True, if the caption must be shown above the message media - * @method true|null getHasMediaSpoiler() Optional. True, if the message media is covered by a spoiler animation + * @method bool hasMediaSpoiler() Optional. True, if the message media is covered by a spoiler animation * @method Contact|null getContact() Optional. Message is a shared contact, information about the contact * @method Dice|null getDice() Optional. Message is a dice with random value * @method Game|null getGame() Optional. Message is a game, information about the game. @@ -89,69 +90,80 @@ * @method WebAppData|null getWebAppData() Optional. Service message: data sent by a Web App * @method InlineKeyboardMarkup getReplyMarkup() Optional. Inline keyboard attached to the message. login_url buttons are represented as ordinary url buttons. */ -class Message extends MaybeInaccessibleMessage +class Message extends MaybeInaccessibleMessage implements AllowsBypassingGet { protected static function subEntities(): array { return [ - 'from' => User::class, - 'sender_chat' => Chat::class, - 'sender_business_bot' => User::class, - 'chat' => Chat::class, - 'forward_origin' => MessageOrigin::class, - 'reply_to_message' => Message::class, - 'external_reply' => ExternalReplyInfo::class, - 'quote' => TextQuote::class, - 'reply_to_story' => Story::class, - 'via_bot' => User::class, - 'entities' => [MessageEntity::class], - 'link_preview_options' => LinkPreviewOptions::class, - 'animation' => Animation::class, - 'audio' => Audio::class, - 'document' => Document::class, - 'photo' => [PhotoSize::class], - 'sticker' => Sticker::class, - 'story' => Story::class, - 'video' => Video::class, - 'video_note' => VideoNote::class, - 'voice' => Voice::class, - 'caption_entities' => [MessageEntity::class], - 'contact' => Contact::class, - 'dice' => Dice::class, - 'game' => Game::class, - 'poll' => Poll::class, - 'venue' => Venue::class, - 'location' => Location::class, - 'new_chat_members' => [User::class], - 'left_chat_member' => User::class, - 'new_chat_photo' => [PhotoSize::class], + 'from' => User::class, + 'sender_chat' => Chat::class, + 'sender_business_bot' => User::class, + 'chat' => Chat::class, + 'forward_origin' => MessageOrigin::class, + 'reply_to_message' => Message::class, + 'external_reply' => ExternalReplyInfo::class, + 'quote' => TextQuote::class, + 'reply_to_story' => Story::class, + 'via_bot' => User::class, + 'entities' => [MessageEntity::class], + 'link_preview_options' => LinkPreviewOptions::class, + 'animation' => Animation::class, + 'audio' => Audio::class, + 'document' => Document::class, + 'photo' => [PhotoSize::class], + 'sticker' => Sticker::class, + 'story' => Story::class, + 'video' => Video::class, + 'video_note' => VideoNote::class, + 'voice' => Voice::class, + 'caption_entities' => [MessageEntity::class], + 'contact' => Contact::class, + 'dice' => Dice::class, + 'game' => Game::class, + 'poll' => Poll::class, + 'venue' => Venue::class, + 'location' => Location::class, + 'new_chat_members' => [User::class], + 'left_chat_member' => User::class, + 'new_chat_photo' => [PhotoSize::class], 'message_auto_delete_timer_changed' => MessageAutoDeleteTimerChanged::class, - 'pinned_message' => MaybeInaccessibleMessage::class, - 'invoice' => Invoice::class, - 'successful_payment' => SuccessfulPayment::class, - 'users_shared' => UsersShared::class, - 'chat_shared' => ChatShared::class, - 'write_access_allowed' => WriteAccessAllowed::class, - 'passport_data' => PassportData::class, - 'proximity_alert_triggered' => ProximityAlertTriggered::class, - 'boost_added' => ChatBoostAdded::class, - 'chat_background_set' => ChatBackground::class, - 'forum_topic_created' => ForumTopicCreated::class, - 'forum_topic_edited' => ForumTopicEdited::class, - 'forum_topic_closed' => ForumTopicClosed::class, - 'forum_topic_reopened' => ForumTopicReopened::class, - 'general_forum_topic_hidden' => GeneralForumTopicHidden::class, - 'general_forum_topic_unhidden' => GeneralForumTopicUnhidden::class, - 'giveaway_created' => GiveawayCreated::class, - 'giveaway' => Giveaway::class, - 'giveaway_winners' => GiveawayWinners::class, - 'giveaway_completed' => GiveawayCompleted::class, - 'video_chat_scheduled' => VideoChatScheduled::class, - 'video_chat_started' => VideoChatStarted::class, - 'video_chat_ended' => VideoChatEnded::class, - 'video_chat_participants_invited' => VideoChatParticipantsInvited::class, - 'web_app_data' => WebAppData::class, - 'reply_markup' => InlineKeyboardMarkup::class, + 'pinned_message' => MaybeInaccessibleMessage::class, + 'invoice' => Invoice::class, + 'successful_payment' => SuccessfulPayment::class, + 'users_shared' => UsersShared::class, + 'chat_shared' => ChatShared::class, + 'write_access_allowed' => WriteAccessAllowed::class, + 'passport_data' => PassportData::class, + 'proximity_alert_triggered' => ProximityAlertTriggered::class, + 'boost_added' => ChatBoostAdded::class, + 'chat_background_set' => ChatBackground::class, + 'forum_topic_created' => ForumTopicCreated::class, + 'forum_topic_edited' => ForumTopicEdited::class, + 'forum_topic_closed' => ForumTopicClosed::class, + 'forum_topic_reopened' => ForumTopicReopened::class, + 'general_forum_topic_hidden' => GeneralForumTopicHidden::class, + 'general_forum_topic_unhidden' => GeneralForumTopicUnhidden::class, + 'giveaway_created' => GiveawayCreated::class, + 'giveaway' => Giveaway::class, + 'giveaway_winners' => GiveawayWinners::class, + 'giveaway_completed' => GiveawayCompleted::class, + 'video_chat_scheduled' => VideoChatScheduled::class, + 'video_chat_started' => VideoChatStarted::class, + 'video_chat_ended' => VideoChatEnded::class, + 'video_chat_participants_invited' => VideoChatParticipantsInvited::class, + 'web_app_data' => WebAppData::class, + 'reply_markup' => InlineKeyboardMarkup::class, + ]; + } + + public static function fieldsBypassingGet(): array + { + return [ + 'is_topic_message' => false, + 'is_automatic_forward' => false, + 'has_protected_content' => false, + 'is_from_offline' => false, + 'has_media_spoiler' => false, ]; } } diff --git a/src/Entities/MessageId.php b/src/Entities/MessageId.php new file mode 100644 index 000000000..2ca56e183 --- /dev/null +++ b/src/Entities/MessageId.php @@ -0,0 +1,11 @@ + new MessageOriginUser($data), + 'user' => new MessageOriginUser($data), 'hidden_user' => new MessageOriginHiddenUser($data), - 'chat' => new MessageOriginChat($data), - 'channel' => new MessageOriginChannel($data), + 'chat' => new MessageOriginChat($data), + 'channel' => new MessageOriginChannel($data), }; } } diff --git a/src/Entities/MessageReactionCountUpdated.php b/src/Entities/MessageReactionCountUpdated.php index 7a88046be..35d0f8db6 100644 --- a/src/Entities/MessageReactionCountUpdated.php +++ b/src/Entities/MessageReactionCountUpdated.php @@ -13,7 +13,7 @@ class MessageReactionCountUpdated extends Entity protected static function subEntities(): array { return [ - 'chat' => Chat::class, + 'chat' => Chat::class, 'reactions' => [ReactionCount::class], ]; } diff --git a/src/Entities/MessageReactionUpdated.php b/src/Entities/MessageReactionUpdated.php index 57db5afd4..bb7043869 100644 --- a/src/Entities/MessageReactionUpdated.php +++ b/src/Entities/MessageReactionUpdated.php @@ -18,9 +18,9 @@ class MessageReactionUpdated extends Entity protected static function subEntities(): array { return [ - 'chat' => Chat::class, - 'user' => User::class, - 'actor_chat' => Chat::class, + 'chat' => Chat::class, + 'user' => User::class, + 'actor_chat' => Chat::class, 'old_reaction' => [ReactionType::class], 'new_reaction' => [ReactionType::class], ]; diff --git a/src/Entities/PassportData.php b/src/Entities/PassportData.php index 50d2a959f..ed7758999 100644 --- a/src/Entities/PassportData.php +++ b/src/Entities/PassportData.php @@ -11,7 +11,7 @@ class PassportData extends Entity protected static function subEntities(): array { return [ - 'data' => [EncryptedPassportElement::class], + 'data' => [EncryptedPassportElement::class], 'credentials' => EncryptedCredentials::class, ]; } diff --git a/src/Entities/Poll.php b/src/Entities/Poll.php index 87bf112e5..ace697074 100644 --- a/src/Entities/Poll.php +++ b/src/Entities/Poll.php @@ -2,14 +2,16 @@ namespace PhpTelegramBot\Core\Entities; +use PhpTelegramBot\Core\Contracts\AllowsBypassingGet; + /** * @method string getId() Unique poll identifier * @method string getQuestion() Poll question, 1-300 characters * @method MessageEntity[]|null getQuestionEntities() Optional. Special entities that appear in the question. Currently, only custom emoji entities are allowed in poll questions * @method PollOption[] getOptions() List of poll options * @method int getTotalVoterCount() Total number of users that voted in the poll - * @method bool getIsClosed() True, if the poll is closed - * @method bool getIsAnonymous() True, if the poll is anonymous + * @method bool isClosed() True, if the poll is closed + * @method bool isAnonymous() True, if the poll is anonymous * @method string getType() Poll type, currently can be “regular” or “quiz” * @method bool getAllowsMultipleAnswers() True, if the poll allows multiple answers * @method int|null getCorrectOptionId() Optional. 0-based identifier of the correct answer option. Available only for polls in the quiz mode, which are closed, or was sent (not forwarded), by the bot or to the private chat with the bot. @@ -18,7 +20,7 @@ * @method int|null getOpenPeriod() Optional. Amount of time in seconds the poll will be active after creation * @method int|null getCloseDate() Optional. Point in time (Unix timestamp), when the poll will be automatically closed */ -class Poll extends Entity +class Poll extends Entity implements AllowsBypassingGet { public const TYPE_REGULAR = 'regular'; @@ -27,9 +29,17 @@ class Poll extends Entity protected static function subEntities(): array { return [ - 'question_entities' => [MessageEntity::class], - 'options' => [PollOption::class], + 'question_entities' => [MessageEntity::class], + 'options' => [PollOption::class], 'explanation_entities' => [MessageEntity::class], ]; } + + public static function fieldsBypassingGet(): array + { + return [ + 'is_closed' => false, + 'is_anonymous' => false, + ]; + } } diff --git a/src/Entities/PollAnswer.php b/src/Entities/PollAnswer.php index e61847529..1031d5ff8 100644 --- a/src/Entities/PollAnswer.php +++ b/src/Entities/PollAnswer.php @@ -14,7 +14,7 @@ protected static function subEntities(): array { return [ 'voter_chat' => Chat::class, - 'user' => User::class, + 'user' => User::class, ]; } } diff --git a/src/Entities/PreCheckoutQuery.php b/src/Entities/PreCheckoutQuery.php index c709f6eb3..77d13e02d 100644 --- a/src/Entities/PreCheckoutQuery.php +++ b/src/Entities/PreCheckoutQuery.php @@ -16,7 +16,7 @@ class PreCheckoutQuery extends Entity protected static function subEntities(): array { return [ - 'from' => User::class, + 'from' => User::class, 'order_info' => OrderInfo::class, ]; } diff --git a/src/Entities/ProximityAlertTriggered.php b/src/Entities/ProximityAlertTriggered.php index 7fb7e2e82..eaf0c885f 100644 --- a/src/Entities/ProximityAlertTriggered.php +++ b/src/Entities/ProximityAlertTriggered.php @@ -13,7 +13,7 @@ protected static function subEntities(): array { return [ 'traveler' => User::class, - 'watcher' => User::class, + 'watcher' => User::class, ]; } } diff --git a/src/Entities/ReactionType/ReactionType.php b/src/Entities/ReactionType/ReactionType.php index 61c0cd16c..4be271f7f 100644 --- a/src/Entities/ReactionType/ReactionType.php +++ b/src/Entities/ReactionType/ReactionType.php @@ -17,7 +17,7 @@ class ReactionType extends Entity implements Factory public static function make(array $data): static { return match ($data['type']) { - self::TYPE_EMOJI => new ReactionTypeEmoji($data), + self::TYPE_EMOJI => new ReactionTypeEmoji($data), self::TYPE_CUSTOM_EMOJI => new ReactionTypeCustomEmoji($data), }; } diff --git a/src/Entities/ReplyKeyboardMarkup.php b/src/Entities/ReplyKeyboardMarkup.php new file mode 100644 index 000000000..7ede7dc7b --- /dev/null +++ b/src/Entities/ReplyKeyboardMarkup.php @@ -0,0 +1,30 @@ + [[KeyboardButton::class]], + ]; + } + + public static function fieldsBypassingGet(): array + { + return [ + 'is_persistent' => false, + ]; + } +} diff --git a/src/Entities/ReplyKeyboardRemove.php b/src/Entities/ReplyKeyboardRemove.php new file mode 100644 index 000000000..0980dcc3a --- /dev/null +++ b/src/Entities/ReplyKeyboardRemove.php @@ -0,0 +1,12 @@ + [MessageEntity::class], + ]; + } +} diff --git a/src/Entities/ResponseParameters.php b/src/Entities/ResponseParameters.php new file mode 100644 index 000000000..38dbd1c9c --- /dev/null +++ b/src/Entities/ResponseParameters.php @@ -0,0 +1,12 @@ + User::class, + 'from' => User::class, 'shipping_address' => ShippingAddress::class, ]; } diff --git a/src/Entities/Sticker.php b/src/Entities/Sticker.php index 0d19155eb..69e727605 100644 --- a/src/Entities/Sticker.php +++ b/src/Entities/Sticker.php @@ -30,9 +30,9 @@ class Sticker extends Entity protected static function subEntities(): array { return [ - 'thumbnail' => PhotoSize::class, + 'thumbnail' => PhotoSize::class, 'premium_animation' => File::class, - 'mask_position' => MaskPosition::class, + 'mask_position' => MaskPosition::class, ]; } } diff --git a/src/Entities/TextQuote.php b/src/Entities/TextQuote.php index a20c23f15..2136e6884 100644 --- a/src/Entities/TextQuote.php +++ b/src/Entities/TextQuote.php @@ -2,13 +2,15 @@ namespace PhpTelegramBot\Core\Entities; +use PhpTelegramBot\Core\Contracts\AllowsBypassingGet; + /** * @method string getText() Text of the quoted part of a message that is replied to by the given message * @method MessageEntity[]|null getEntities() Optional. Special entities that appear in the quote. Currently, only bold, italic, underline, strikethrough, spoiler, and custom_emoji entities are kept in quotes. * @method int getPosition() Approximate quote position in the original message in UTF-16 code units as specified by the sender - * @method true|null getIsManual() Optional. True, if the quote was chosen manually by the message sender. Otherwise, the quote was added automatically by the server. + * @method bool isManual() Optional. True, if the quote was chosen manually by the message sender. Otherwise, the quote was added automatically by the server. */ -class TextQuote extends Entity +class TextQuote extends Entity implements AllowsBypassingGet { protected static function subEntities(): array { @@ -16,4 +18,11 @@ protected static function subEntities(): array 'entities' => [MessageEntity::class], ]; } + + public static function fieldsBypassingGet(): array + { + return [ + 'is_manual' => false, + ]; + } } diff --git a/src/Entities/Update.php b/src/Entities/Update.php index 8653c6da3..65b1b8e2e 100644 --- a/src/Entities/Update.php +++ b/src/Entities/Update.php @@ -32,28 +32,28 @@ class Update extends Entity protected static function subEntities(): array { return [ - 'message' => Message::class, - 'edited_message' => Message::class, - 'channel_post' => Message::class, - 'edited_channel_post' => Message::class, - 'business_connection' => BusinessConnection::class, - 'business_message' => Message::class, - 'edited_business_message' => Message::class, + 'message' => Message::class, + 'edited_message' => Message::class, + 'channel_post' => Message::class, + 'edited_channel_post' => Message::class, + 'business_connection' => BusinessConnection::class, + 'business_message' => Message::class, + 'edited_business_message' => Message::class, 'deleted_business_messages' => BusinessMessagesDeleted::class, - 'message_reaction' => MessageReactionUpdated::class, - 'message_reaction_count' => MessageReactionCountUpdated::class, - 'inline_query' => InlineQuery::class, - 'chosen_inline_result' => ChosenInlineResult::class, - 'callback_query' => CallbackQuery::class, - 'shipping_query' => ShippingQuery::class, - 'pre_checkout_query' => PreCheckoutQuery::class, - 'poll' => Poll::class, - 'poll_answer' => PollAnswer::class, - 'my_chat_member' => ChatMemberUpdated::class, - 'chat_member' => ChatMemberUpdated::class, - 'chat_join_request' => ChatJoinRequest::class, - 'chat_boost' => ChatBoostUpdated::class, - 'removed_chat_boost' => ChatBoostRemoved::class, + 'message_reaction' => MessageReactionUpdated::class, + 'message_reaction_count' => MessageReactionCountUpdated::class, + 'inline_query' => InlineQuery::class, + 'chosen_inline_result' => ChosenInlineResult::class, + 'callback_query' => CallbackQuery::class, + 'shipping_query' => ShippingQuery::class, + 'pre_checkout_query' => PreCheckoutQuery::class, + 'poll' => Poll::class, + 'poll_answer' => PollAnswer::class, + 'my_chat_member' => ChatMemberUpdated::class, + 'chat_member' => ChatMemberUpdated::class, + 'chat_join_request' => ChatJoinRequest::class, + 'chat_boost' => ChatBoostUpdated::class, + 'removed_chat_boost' => ChatBoostRemoved::class, ]; } } diff --git a/src/Entities/User.php b/src/Entities/User.php index 47697f584..f124796ba 100644 --- a/src/Entities/User.php +++ b/src/Entities/User.php @@ -2,21 +2,32 @@ namespace PhpTelegramBot\Core\Entities; +use PhpTelegramBot\Core\Contracts\AllowsBypassingGet; + /** * @method int getId() Unique identifier for this user or bot. This number may have more than 32 significant bits and some programming languages may have difficulty/silent defects in interpreting it. But it has at most 52 significant bits, so a 64-bit integer or double-precision float type are safe for storing this identifier. - * @method bool getIsBot() True, if this user is a bot + * @method bool isBot() True, if this user is a bot * @method string getFirstName() User's or bot's first name * @method string|null getLastName() Optional. User's or bot's last name * @method string|null getUsername() Optional. User's or bot's username * @method string|null getLanguageCode() Optional. IETF language tag of the user's language - * @method true|null getIsPremium() Optional. True, if this user is a Telegram Premium user + * @method bool isPremium() Optional. True, if this user is a Telegram Premium user * @method true|null getAddedToAttachmentMenu() Optional. True, if this user added the bot to the attachment menu - * @method bool|null getCanJoinGroups() Optional. True, if the bot can be invited to groups. Returned only in getMe. - * @method bool|null getReadAllGroupMessages() Optional. True, if privacy mode is disabled for the bot. Returned only in getMe. + * @method bool canJoinGroups() Optional. True, if the bot can be invited to groups. Returned only in getMe. + * @method bool canReadAllGroupMessages() Optional. True, if privacy mode is disabled for the bot. Returned only in getMe. * @method bool|null getSupportsInlineQueries() Optional. True, if the bot supports inline queries. Returned only in getMe. - * @method bool|null getCanConnectToBusiness() Optional. True, if the bot can be connected to a Telegram Business account to receive its messages. Returned only in getMe. + * @method bool canConnectToBusiness() Optional. True, if the bot can be connected to a Telegram Business account to receive its messages. Returned only in getMe. */ -class User extends Entity +class User extends Entity implements AllowsBypassingGet { - // + public static function fieldsBypassingGet(): array + { + return [ + 'is_bot' => false, + 'is_premium' => false, + 'can_join_groups' => false, + 'can_read_all_group_messages' => false, + 'can_connect_to_business' => false, + ]; + } } diff --git a/src/Entities/UserChatBoosts.php b/src/Entities/UserChatBoosts.php new file mode 100644 index 000000000..32d013e25 --- /dev/null +++ b/src/Entities/UserChatBoosts.php @@ -0,0 +1,16 @@ + [ChatBoost::class], + ]; + } +} diff --git a/src/Entities/UserProfilePhotos.php b/src/Entities/UserProfilePhotos.php new file mode 100644 index 000000000..03d27987e --- /dev/null +++ b/src/Entities/UserProfilePhotos.php @@ -0,0 +1,17 @@ + [[PhotoSize::class]], + ]; + } +} diff --git a/src/Entities/WebhookInfo.php b/src/Entities/WebhookInfo.php new file mode 100644 index 000000000..f4f733a1f --- /dev/null +++ b/src/Entities/WebhookInfo.php @@ -0,0 +1,27 @@ + false, + ]; + } +} diff --git a/src/Telegram.php b/src/Telegram.php index 2734d8fa0..86abde8bb 100644 --- a/src/Telegram.php +++ b/src/Telegram.php @@ -26,7 +26,7 @@ public function __call(string $methodName, array $arguments) $response = match (true) { $data === null => $this->client->get($requestUri), - default => $this->client->postJson($requestUri, $data), + default => $this->client->postJson($requestUri, $data), }; $result = json_decode($response->getBody()->getContents(), true); From b9eac527c29166301c8bc2d5ca6822a5f16d83d2 Mon Sep 17 00:00:00 2001 From: Tii Date: Tue, 11 Jun 2024 10:31:04 +0200 Subject: [PATCH 05/20] intermediate result adding methods via traits --- README.md | 11 +- composer.json | 2 +- src/Entities/Entity.php | 93 +- .../InlineQueryResult/InlineQueryResult.php | 14 + .../InlineQueryResultArticle.php | 35 + .../InlineQueryResultAudio.php | 37 + .../InlineQueryResultCachedAudio.php | 34 + .../InlineQueryResultCachedDocument.php | 36 + .../InlineQueryResultCachedGif.php | 36 + .../InlineQueryResultCachedMpeg4Gif.php | 36 + .../InlineQueryResultCachedPhoto.php | 37 + .../InlineQueryResultCachedSticker.php | 29 + .../InlineQueryResultCachedVideo.php | 37 + .../InlineQueryResultCachedVoice.php | 35 + .../InlineQueryResultContact.php | 35 + .../InlineQueryResultDocument.php | 40 + .../InlineQueryResultGame.php | 26 + .../InlineQueryResultGif.php | 41 + .../InlineQueryResultLocation.php | 38 + .../InlineQueryResultMpeg4Gif.php | 41 + .../InlineQueryResultPhoto.php | 40 + .../InlineQueryResultVenue.php | 39 + .../InlineQueryResultVideo.php | 42 + .../InlineQueryResultVoice.php | 36 + src/Entities/InlineQueryResultsButton.php | 18 + .../InputContactMessageContent.php | 14 + .../InputInvoiceMessageContent.php | 45 + .../InputLocationMessageContent.php | 16 + .../InputMessageContent.php | 10 + .../InputTextMessageContent.php | 23 + .../InputVenueMessageContent.php | 18 + src/Entities/InputSticker.php | 21 + src/Entities/LabeledPrice.php | 12 + src/Entities/SentWebAppMessage.php | 11 + src/Entities/ShippingOption.php | 18 + src/Entities/StickerSet.php | 21 + src/Methods/AnswersInlineQueries.php | 40 + src/Methods/SendsInvoices.php | 124 ++ src/Methods/SendsMessages.php | 1365 +++++++++++++++++ src/Methods/SendsStickers.php | 242 +++ src/Methods/UpdatesMessages.php | 159 ++ src/Telegram.php | 50 +- 42 files changed, 3005 insertions(+), 52 deletions(-) create mode 100644 src/Entities/InlineQueryResult/InlineQueryResult.php create mode 100644 src/Entities/InlineQueryResult/InlineQueryResultArticle.php create mode 100644 src/Entities/InlineQueryResult/InlineQueryResultAudio.php create mode 100644 src/Entities/InlineQueryResult/InlineQueryResultCachedAudio.php create mode 100644 src/Entities/InlineQueryResult/InlineQueryResultCachedDocument.php create mode 100644 src/Entities/InlineQueryResult/InlineQueryResultCachedGif.php create mode 100644 src/Entities/InlineQueryResult/InlineQueryResultCachedMpeg4Gif.php create mode 100644 src/Entities/InlineQueryResult/InlineQueryResultCachedPhoto.php create mode 100644 src/Entities/InlineQueryResult/InlineQueryResultCachedSticker.php create mode 100644 src/Entities/InlineQueryResult/InlineQueryResultCachedVideo.php create mode 100644 src/Entities/InlineQueryResult/InlineQueryResultCachedVoice.php create mode 100644 src/Entities/InlineQueryResult/InlineQueryResultContact.php create mode 100644 src/Entities/InlineQueryResult/InlineQueryResultDocument.php create mode 100644 src/Entities/InlineQueryResult/InlineQueryResultGame.php create mode 100644 src/Entities/InlineQueryResult/InlineQueryResultGif.php create mode 100644 src/Entities/InlineQueryResult/InlineQueryResultLocation.php create mode 100644 src/Entities/InlineQueryResult/InlineQueryResultMpeg4Gif.php create mode 100644 src/Entities/InlineQueryResult/InlineQueryResultPhoto.php create mode 100644 src/Entities/InlineQueryResult/InlineQueryResultVenue.php create mode 100644 src/Entities/InlineQueryResult/InlineQueryResultVideo.php create mode 100644 src/Entities/InlineQueryResult/InlineQueryResultVoice.php create mode 100644 src/Entities/InlineQueryResultsButton.php create mode 100644 src/Entities/InputMessageContent/InputContactMessageContent.php create mode 100644 src/Entities/InputMessageContent/InputInvoiceMessageContent.php create mode 100644 src/Entities/InputMessageContent/InputLocationMessageContent.php create mode 100644 src/Entities/InputMessageContent/InputMessageContent.php create mode 100644 src/Entities/InputMessageContent/InputTextMessageContent.php create mode 100644 src/Entities/InputMessageContent/InputVenueMessageContent.php create mode 100644 src/Entities/InputSticker.php create mode 100644 src/Entities/LabeledPrice.php create mode 100644 src/Entities/SentWebAppMessage.php create mode 100644 src/Entities/ShippingOption.php create mode 100644 src/Entities/StickerSet.php create mode 100644 src/Methods/AnswersInlineQueries.php create mode 100644 src/Methods/SendsInvoices.php create mode 100644 src/Methods/SendsMessages.php create mode 100644 src/Methods/SendsStickers.php create mode 100644 src/Methods/UpdatesMessages.php diff --git a/README.md b/README.md index 8e14c9df0..34ae59a13 100644 --- a/README.md +++ b/README.md @@ -102,10 +102,13 @@ _TODO_ ## Roadmap -- [ ] Adding Telegram Types -- [ ] Adding base functionality to call API Methods -- [ ] Adding base functionality to receive Updates -- [ ] ... +- [x] Adding Telegram Types +- [x] Adding base functionality to call API Methods +- [x] Adding base functionality to parse Telegram Entities +- [ ] Test editMessageText with its return types Message or bool +- [ ] "must be article" etc should be set via presetData static function +- [ ] File Downloads/Uploads +- [ ] Custom Bot API Server

(back to top)

diff --git a/composer.json b/composer.json index f23593347..1958567b3 100644 --- a/composer.json +++ b/composer.json @@ -67,7 +67,7 @@ }, "extra": { "branch-alias": { - "dev-develop": "1.x-dev" + "dev-v1": "1.x-dev" } } } diff --git a/src/Entities/Entity.php b/src/Entities/Entity.php index a45d328c2..0494c64c4 100644 --- a/src/Entities/Entity.php +++ b/src/Entities/Entity.php @@ -2,26 +2,24 @@ namespace PhpTelegramBot\Core\Entities; +use BadMethodCallException; +use JsonSerializable; use PhpTelegramBot\Core\Contracts\AllowsBypassingGet; -abstract class Entity implements \JsonSerializable +abstract class Entity implements JsonSerializable { protected array $fields = []; - /** - * @return array - */ - protected static function subEntities(): array - { - return []; - } - public function __construct( array $data = [] ) { foreach ($data as $key => $value) { $this->fields[$key] = $value; } + + foreach (static::presetData() as $key => $value) { + $this->fields[$key] = $value; + } } public function __get(string $name) @@ -34,6 +32,36 @@ public function __set(string $name, $value): void $this->fields[$name] = $value; } + public function __call(string $name, array $arguments) + { + $snakeName = strtolower(ltrim(preg_replace('/[[:upper:]]/', '_$0', $name), '_')); + + if (str_starts_with($snakeName, 'get_')) { + // Getter for fields + return $this->getField(substr($snakeName, 4)); + } elseif (str_starts_with($snakeName, 'set_')) { + // Setter for fields + $this->setField(substr($snakeName, 4), $arguments[0] ?? null); + + return $this; + } elseif (is_subclass_of($this, AllowsBypassingGet::class)) { + $fields = $this::fieldsBypassingGet(); + + if (array_key_exists($snakeName, $fields)) { + + return $this->getField($snakeName) ?? $fields[$snakeName]; + + } elseif (in_array($snakeName, $fields)) { + + return $this->getField($snakeName); + + } + } + + $method = get_class($this).'::'.$name.'()'; + throw new BadMethodCallException("Call to undefined method $method"); + } + private function getField(string $name): mixed { $data = $this->fields[$name]; @@ -43,16 +71,16 @@ private function getField(string $name): mixed return $data; } - if ($subEntities[$name] instanceof Entity) { + if (is_subclass_of($subEntities[$name], Entity::class)) { $class = $subEntities[$name]; return new $class($data); - } elseif (is_array($subEntities[$name]) && $subEntities[$name][0] instanceof Entity) { + } elseif (is_array($subEntities[$name]) && is_subclass_of($subEntities[$name][0], Entity::class)) { // Arrays like PhotoSize[] $class = $subEntities[$name][0]; return array_map(fn ($item) => new $class($item), $data); - } elseif (is_array($subEntities[$name]) && is_array($subEntities[$name][0]) && $subEntities[$name][0][0] instanceof Entity) { + } elseif (is_array($subEntities[$name]) && is_array($subEntities[$name][0]) && is_subclass_of($subEntities[$name][0][0], Entity::class)) { // Edge case currently only needed for Keyboards: Array of Array of InlineKeyboardButton $class = $subEntities[$name][0][0]; @@ -62,43 +90,26 @@ private function getField(string $name): mixed return $data; } - protected function setField(string $name, mixed $value): void + /** + * @return array + */ + protected static function subEntities(): array { - $this->fields[$name] = $value; + return []; } - public function __call(string $name, array $arguments) + protected static function presetData(): array { - $snakeName = strtolower(ltrim(preg_replace('/[[:upper:]]/', '_$0', $name), '_')); - - if (str_starts_with($snakeName, 'get_')) { - // Getter for fields - return $this->getField(substr($snakeName, 4)); - } elseif (str_starts_with($snakeName, 'set_')) { - // Setter for fields - $this->setField(substr($snakeName, 4), $arguments[0] ?? null); - - return $this; - } elseif (is_subclass_of($this, AllowsBypassingGet::class)) { - $fields = $this::fieldsBypassingGet(); - - if (array_key_exists($snakeName, $fields)) { - - return $this->getField($snakeName) ?? $fields[$snakeName]; - - } elseif (in_array($snakeName, $fields)) { - - return $this->getField($snakeName); - - } - } + return []; + } - $method = get_class($this).'::'.$name.'()'; - throw new \BadMethodCallException("Call to undefined method $method"); + private function setField(string $name, mixed $value): void + { + $this->fields[$name] = $value; } public function jsonSerialize(): mixed { - return $this->fields; + return array_filter($this->fields, fn ($item) => ! is_null($item)); } } diff --git a/src/Entities/InlineQueryResult/InlineQueryResult.php b/src/Entities/InlineQueryResult/InlineQueryResult.php new file mode 100644 index 000000000..72f42de4e --- /dev/null +++ b/src/Entities/InlineQueryResult/InlineQueryResult.php @@ -0,0 +1,14 @@ + InputMessageContent::class, + 'reply_markup' => InlineKeyboardMarkup::class, + ]; + } + + protected static function presetData(): array + { + return [ + 'type' => 'article', + ]; + } +} diff --git a/src/Entities/InlineQueryResult/InlineQueryResultAudio.php b/src/Entities/InlineQueryResult/InlineQueryResultAudio.php new file mode 100644 index 000000000..8101ff156 --- /dev/null +++ b/src/Entities/InlineQueryResult/InlineQueryResultAudio.php @@ -0,0 +1,37 @@ + [MessageEntity::class], + 'reply_markup' => InlineKeyboardMarkup::class, + 'input_message_content' => InputMessageContent::class, + ]; + } + + protected static function presetData(): array + { + return [ + 'type' => 'audio', + ]; + } +} diff --git a/src/Entities/InlineQueryResult/InlineQueryResultCachedAudio.php b/src/Entities/InlineQueryResult/InlineQueryResultCachedAudio.php new file mode 100644 index 000000000..a6ebf9bdb --- /dev/null +++ b/src/Entities/InlineQueryResult/InlineQueryResultCachedAudio.php @@ -0,0 +1,34 @@ + [MessageEntity::class], + 'reply_markup' => InlineKeyboardMarkup::class, + 'input_message_content' => InputMessageContent::class, + ]; + } + + protected static function presetData(): array + { + return [ + 'type' => 'audio', + ]; + } +} diff --git a/src/Entities/InlineQueryResult/InlineQueryResultCachedDocument.php b/src/Entities/InlineQueryResult/InlineQueryResultCachedDocument.php new file mode 100644 index 000000000..3976b0a7c --- /dev/null +++ b/src/Entities/InlineQueryResult/InlineQueryResultCachedDocument.php @@ -0,0 +1,36 @@ + [MessageEntity::class], + 'reply_markup' => InlineKeyboardMarkup::class, + 'input_message_content' => InputMessageContent::class, + ]; + } + + protected static function presetData(): array + { + return [ + 'type' => 'document', + ]; + } +} diff --git a/src/Entities/InlineQueryResult/InlineQueryResultCachedGif.php b/src/Entities/InlineQueryResult/InlineQueryResultCachedGif.php new file mode 100644 index 000000000..b96e576d9 --- /dev/null +++ b/src/Entities/InlineQueryResult/InlineQueryResultCachedGif.php @@ -0,0 +1,36 @@ + [MessageEntity::class], + 'reply_markup' => InlineKeyboardMarkup::class, + 'input_message_content' => InputMessageContent::class, + ]; + } + + protected static function presetData(): array + { + return [ + 'type' => 'gif', + ]; + } +} diff --git a/src/Entities/InlineQueryResult/InlineQueryResultCachedMpeg4Gif.php b/src/Entities/InlineQueryResult/InlineQueryResultCachedMpeg4Gif.php new file mode 100644 index 000000000..4c67194c5 --- /dev/null +++ b/src/Entities/InlineQueryResult/InlineQueryResultCachedMpeg4Gif.php @@ -0,0 +1,36 @@ + [MessageEntity::class], + 'reply_markup' => InlineKeyboardMarkup::class, + 'input_message_content' => InputMessageContent::class, + ]; + } + + protected static function presetData(): array + { + return [ + 'type' => 'mpeg4_gif', + ]; + } +} diff --git a/src/Entities/InlineQueryResult/InlineQueryResultCachedPhoto.php b/src/Entities/InlineQueryResult/InlineQueryResultCachedPhoto.php new file mode 100644 index 000000000..057a5ca1d --- /dev/null +++ b/src/Entities/InlineQueryResult/InlineQueryResultCachedPhoto.php @@ -0,0 +1,37 @@ + [MessageEntity::class], + 'reply_markup' => InlineKeyboardMarkup::class, + 'input_message_content' => InputMessageContent::class, + ]; + } + + protected static function presetData(): array + { + return [ + 'type' => 'photo', + ]; + } +} diff --git a/src/Entities/InlineQueryResult/InlineQueryResultCachedSticker.php b/src/Entities/InlineQueryResult/InlineQueryResultCachedSticker.php new file mode 100644 index 000000000..8d4e09eff --- /dev/null +++ b/src/Entities/InlineQueryResult/InlineQueryResultCachedSticker.php @@ -0,0 +1,29 @@ + InlineKeyboardMarkup::class, + 'input_message_content' => InputMessageContent::class, + ]; + } + + protected static function presetData(): array + { + return [ + 'type' => 'sticker', + ]; + } +} diff --git a/src/Entities/InlineQueryResult/InlineQueryResultCachedVideo.php b/src/Entities/InlineQueryResult/InlineQueryResultCachedVideo.php new file mode 100644 index 000000000..de2794838 --- /dev/null +++ b/src/Entities/InlineQueryResult/InlineQueryResultCachedVideo.php @@ -0,0 +1,37 @@ + [MessageEntity::class], + 'reply_markup' => InlineKeyboardMarkup::class, + 'input_message_content' => InputMessageContent::class, + ]; + } + + protected static function presetData(): array + { + return [ + 'type' => 'video', + ]; + } +} diff --git a/src/Entities/InlineQueryResult/InlineQueryResultCachedVoice.php b/src/Entities/InlineQueryResult/InlineQueryResultCachedVoice.php new file mode 100644 index 000000000..41910617f --- /dev/null +++ b/src/Entities/InlineQueryResult/InlineQueryResultCachedVoice.php @@ -0,0 +1,35 @@ + [MessageEntity::class], + 'reply_markup' => InlineKeyboardMarkup::class, + 'input_message_content' => InputMessageContent::class, + ]; + } + + protected static function presetData(): array + { + return [ + 'type' => 'voice', + ]; + } +} diff --git a/src/Entities/InlineQueryResult/InlineQueryResultContact.php b/src/Entities/InlineQueryResult/InlineQueryResultContact.php new file mode 100644 index 000000000..2d2558214 --- /dev/null +++ b/src/Entities/InlineQueryResult/InlineQueryResultContact.php @@ -0,0 +1,35 @@ + InlineKeyboardMarkup::class, + 'input_message_content' => InputMessageContent::class, + ]; + } + + protected static function presetData(): array + { + return [ + 'type' => 'contact', + ]; + } +} diff --git a/src/Entities/InlineQueryResult/InlineQueryResultDocument.php b/src/Entities/InlineQueryResult/InlineQueryResultDocument.php new file mode 100644 index 000000000..64ffb4e8d --- /dev/null +++ b/src/Entities/InlineQueryResult/InlineQueryResultDocument.php @@ -0,0 +1,40 @@ + [MessageEntity::class], + 'reply_markup' => InlineKeyboardMarkup::class, + 'input_message_content' => InputMessageContent::class, + ]; + } + + protected static function presetData(): array + { + return [ + 'type' => 'document', + ]; + } +} diff --git a/src/Entities/InlineQueryResult/InlineQueryResultGame.php b/src/Entities/InlineQueryResult/InlineQueryResultGame.php new file mode 100644 index 000000000..e2bb1df9c --- /dev/null +++ b/src/Entities/InlineQueryResult/InlineQueryResultGame.php @@ -0,0 +1,26 @@ + InlineKeyboardMarkup::class, + ]; + } + + protected static function presetData(): array + { + return [ + 'type' => 'game', + ]; + } +} diff --git a/src/Entities/InlineQueryResult/InlineQueryResultGif.php b/src/Entities/InlineQueryResult/InlineQueryResultGif.php new file mode 100644 index 000000000..d6030f5ba --- /dev/null +++ b/src/Entities/InlineQueryResult/InlineQueryResultGif.php @@ -0,0 +1,41 @@ + [MessageEntity::class], + 'reply_markup' => InlineKeyboardMarkup::class, + 'input_message_content' => InputMessageContent::class, + ]; + } + + protected static function presetData(): array + { + return [ + 'type' => 'gif', + ]; + } +} diff --git a/src/Entities/InlineQueryResult/InlineQueryResultLocation.php b/src/Entities/InlineQueryResult/InlineQueryResultLocation.php new file mode 100644 index 000000000..041f2bf8f --- /dev/null +++ b/src/Entities/InlineQueryResult/InlineQueryResultLocation.php @@ -0,0 +1,38 @@ + InlineKeyboardMarkup::class, + 'input_message_content' => InputMessageContent::class, + ]; + } + + protected static function presetData(): array + { + return [ + 'type' => 'location', + ]; + } +} diff --git a/src/Entities/InlineQueryResult/InlineQueryResultMpeg4Gif.php b/src/Entities/InlineQueryResult/InlineQueryResultMpeg4Gif.php new file mode 100644 index 000000000..d164758f3 --- /dev/null +++ b/src/Entities/InlineQueryResult/InlineQueryResultMpeg4Gif.php @@ -0,0 +1,41 @@ + [MessageEntity::class], + 'reply_markup' => InlineKeyboardMarkup::class, + 'input_message_content' => InputMessageContent::class, + ]; + } + + protected static function presetData(): array + { + return [ + 'type' => 'mpeg4_gif', + ]; + } +} diff --git a/src/Entities/InlineQueryResult/InlineQueryResultPhoto.php b/src/Entities/InlineQueryResult/InlineQueryResultPhoto.php new file mode 100644 index 000000000..277663c2e --- /dev/null +++ b/src/Entities/InlineQueryResult/InlineQueryResultPhoto.php @@ -0,0 +1,40 @@ + [MessageEntity::class], + 'reply_markup' => InlineKeyboardMarkup::class, + 'input_message_content' => InputMessageContent::class, + ]; + } + + protected static function presetData(): array + { + return [ + 'type' => 'photo', + ]; + } +} diff --git a/src/Entities/InlineQueryResult/InlineQueryResultVenue.php b/src/Entities/InlineQueryResult/InlineQueryResultVenue.php new file mode 100644 index 000000000..d82f81f15 --- /dev/null +++ b/src/Entities/InlineQueryResult/InlineQueryResultVenue.php @@ -0,0 +1,39 @@ + InlineKeyboardMarkup::class, + 'input_message_content' => InputMessageContent::class, + ]; + } + + protected static function presetData(): array + { + return [ + 'type' => 'venue', + ]; + } +} diff --git a/src/Entities/InlineQueryResult/InlineQueryResultVideo.php b/src/Entities/InlineQueryResult/InlineQueryResultVideo.php new file mode 100644 index 000000000..e0b92974b --- /dev/null +++ b/src/Entities/InlineQueryResult/InlineQueryResultVideo.php @@ -0,0 +1,42 @@ + [MessageEntity::class], + 'reply_markup' => InlineKeyboardMarkup::class, + 'input_message_content' => InputMessageContent::class, + ]; + } + + protected static function presetData(): array + { + return [ + 'type' => 'video', + ]; + } +} diff --git a/src/Entities/InlineQueryResult/InlineQueryResultVoice.php b/src/Entities/InlineQueryResult/InlineQueryResultVoice.php new file mode 100644 index 000000000..4094e73c3 --- /dev/null +++ b/src/Entities/InlineQueryResult/InlineQueryResultVoice.php @@ -0,0 +1,36 @@ + [MessageEntity::class], + 'reply_markup' => InlineKeyboardMarkup::class, + 'input_message_content' => InputMessageContent::class, + ]; + } + + protected static function presetData(): array + { + return [ + 'type' => 'voice', + ]; + } +} diff --git a/src/Entities/InlineQueryResultsButton.php b/src/Entities/InlineQueryResultsButton.php new file mode 100644 index 000000000..b85049fc6 --- /dev/null +++ b/src/Entities/InlineQueryResultsButton.php @@ -0,0 +1,18 @@ + WebAppInfo::class, + ]; + } +} diff --git a/src/Entities/InputMessageContent/InputContactMessageContent.php b/src/Entities/InputMessageContent/InputContactMessageContent.php new file mode 100644 index 000000000..d3cb97038 --- /dev/null +++ b/src/Entities/InputMessageContent/InputContactMessageContent.php @@ -0,0 +1,14 @@ + [LabeledPrice::class], + ]; + } + + public static function fieldsBypassingGet(): array + { + return [ + 'is_flexible' => false, + ]; + } +} diff --git a/src/Entities/InputMessageContent/InputLocationMessageContent.php b/src/Entities/InputMessageContent/InputLocationMessageContent.php new file mode 100644 index 000000000..c6a61109c --- /dev/null +++ b/src/Entities/InputMessageContent/InputLocationMessageContent.php @@ -0,0 +1,16 @@ + [MessageEntity::class], + 'link_preview_options' => LinkPreviewOptions::class, + ]; + } +} diff --git a/src/Entities/InputMessageContent/InputVenueMessageContent.php b/src/Entities/InputMessageContent/InputVenueMessageContent.php new file mode 100644 index 000000000..d17fb4f61 --- /dev/null +++ b/src/Entities/InputMessageContent/InputVenueMessageContent.php @@ -0,0 +1,18 @@ +” to upload a new one using multipart/form-data under name. Animated and video stickers can't be uploaded via HTTP URL. + * @method string getFormat() Format of the added sticker, must be one of “static” for a .WEBP or .PNG image, “animated” for a .TGS animation, “video” for a WEBM video + * @method string[] getEmojiList() List of 1-20 emoji associated with the sticker + * @method MaskPosition|null getMaskPosition() Optional. Position where the mask should be placed on faces. For “mask” stickers only. + * @method string[]|null getKeywords() Optional. List of 0-20 search keywords for the sticker with total length of up to 64 characters. For “regular” and “custom_emoji” stickers only. + */ +class InputSticker extends Entity +{ + protected static function subEntities(): array + { + return [ + 'sticker' => InputFile::class, + 'mask_position' => MaskPosition::class, + ]; + } +} diff --git a/src/Entities/LabeledPrice.php b/src/Entities/LabeledPrice.php new file mode 100644 index 000000000..6369cd027 --- /dev/null +++ b/src/Entities/LabeledPrice.php @@ -0,0 +1,12 @@ + [LabeledPrice::class], + ]; + } +} diff --git a/src/Entities/StickerSet.php b/src/Entities/StickerSet.php new file mode 100644 index 000000000..c76387e3b --- /dev/null +++ b/src/Entities/StickerSet.php @@ -0,0 +1,21 @@ + [Sticker::class], + 'thumbnail' => PhotoSize::class, + ]; + } +} diff --git a/src/Methods/AnswersInlineQueries.php b/src/Methods/AnswersInlineQueries.php new file mode 100644 index 000000000..892fbb8e8 --- /dev/null +++ b/src/Methods/AnswersInlineQueries.php @@ -0,0 +1,40 @@ +send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * web_app_query_id: string, + * result: InlineQueryResult, + * } $data + * + * @throws \PhpTelegramBot\Core\Exceptions\TelegramException + */ + public function answerWebAppQuery(array $data = []): SentWebAppMessage + { + return $this->send(__FUNCTION__, $data, SentWebAppMessage::class); + } +} diff --git a/src/Methods/SendsInvoices.php b/src/Methods/SendsInvoices.php new file mode 100644 index 000000000..01369e025 --- /dev/null +++ b/src/Methods/SendsInvoices.php @@ -0,0 +1,124 @@ +send(__FUNCTION__, $data, Message::class); + } + + /** + * @param array{ + * title: string, + * description: string, + * payload: string, + * provider_token: string, + * currency: string, + * prices: LabeledPrice[], + * max_tip_amount: int, + * suggested_tip_amounts: int[], + * provider_data: string, + * photo_url: string, + * photo_size: int, + * photo_width: int, + * photo_height: int, + * need_name: bool, + * need_phone_number: bool, + * need_email: bool, + * need_shipping_address: bool, + * send_phone_number_to_provider: bool, + * send_email_to_provider: bool, + * is_flexible: bool, + * } $data + * + * @throws \PhpTelegramBot\Core\Exceptions\TelegramException + */ + public function createInvoiceLink(array $data = []): string + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * shipping_query_id: string, + * ok: bool, + * shipping_options: ShippingOption[], + * error_message: string, + * } $data + * + * @throws \PhpTelegramBot\Core\Exceptions\TelegramException + */ + public function answerShippingQuery(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * pre_checkout_query_id: string, + * ok: bool, + * error_message: string, + * } $data + * + * @throws \PhpTelegramBot\Core\Exceptions\TelegramException + */ + public function answerPreCheckoutQuery(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * user_id: int, + * telegram_payment_charge_id: string, + * } $data + * + * @throws \PhpTelegramBot\Core\Exceptions\TelegramException + */ + public function refundStarPayment(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } +} diff --git a/src/Methods/SendsMessages.php b/src/Methods/SendsMessages.php new file mode 100644 index 000000000..d31e95a4b --- /dev/null +++ b/src/Methods/SendsMessages.php @@ -0,0 +1,1365 @@ + + * } $data + * @return array + * + * @throws Exceptions\TelegramException + */ + public function getUpdates(array $data = []): array + { + return $this->send(__FUNCTION__, $data, [Update::class]); + } + + /** + * @param array{ + * url: string, + * certificate: ?InputFile, + * ip_address: ?string, + * max_connections: ?int, + * allowed_updates: ?array, + * drop_pending_updates: ?bool, + * secret_token: ?string + * } $data + * + * @throws Exceptions\TelegramException + */ + public function setWebhook(array $data = []): bool + { + return $this->send(__FUNCTION__, $data); + } + + /** + * @param array{ + * drop_pending_updates: ?bool + * } $data + * + * @throws Exceptions\TelegramException + */ + public function deleteWebhook(array $data = []): bool + { + return $this->send(__FUNCTION__, $data); + } + + /** + * @throws Exceptions\TelegramException + */ + public function getWebhookInfo(array $data = []): WebhookInfo + { + return $this->send(__FUNCTION__, $data, WebhookInfo::class); + } + + /** + * @throws Exceptions\TelegramException + */ + public function getMe(array $data = []): User + { + return $this->send(__FUNCTION__, $data, User::class); + } + + public function logOut(array $data = []): bool + { + return $this->send(__FUNCTION__, $data); + } + + /** + * @throws Exceptions\TelegramException + */ + public function close(array $data = []): bool + { + return $this->send(__FUNCTION__, $data); + } + + /** + * @param array{ + * business_connection_id: ?string, + * chat_id: int|string, + * message_thread_id: ?int, + * text: string, + * parse_mode: ?string, + * entities: MessageEntity[]|null, + * link_preview_options: ?LinkPreviewOptions, + * disable_notification: ?bool, + * protect_content: ?bool, + * message_effect_id: ?string, + * reply_parameters: ?ReplyParameters, + * reply_markup: InlineKeyboardMarkup|ReplyKeyboardMarkup|ReplyKeyboardRemove|ForceReply|null, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function sendMessage(array $data = []): Message + { + return $this->send(__FUNCTION__, $data, Message::class); + } + + /** + * @param array{ + * chat_id: int|string, + * message_thread_id: ?int, + * from_chat_id: int|string, + * disable_notification: ?bool, + * protect_content: ?bool, + * message_id: int, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function forwardMessage(array $data = []): Message + { + return $this->send(__FUNCTION__, $data, Message::class); + } + + /** + * @param array{ + * chat_id: int|string, + * message_thread_id: ?int, + * from_chat_id: int|string, + * message_ids: int[], + * disable_notification: ?bool, + * protect_content: ?bool, + * } $data + * @return MessageId[] + * + * @throws Exceptions\TelegramException + */ + public function forwardMessages(array $data = []): array + { + return $this->send(__FUNCTION__, $data, [MessageId::class]); + } + + /** + * @param array{ + * chat_id: int|string, + * message_thread_id: int, + * from_chat_id: int|string, + * message_id: int, + * caption: string, + * parse_mode: string, + * caption_entities: MessageEntity[], + * show_caption_above_media: bool, + * disable_notification: bool, + * protect_content: bool, + * reply_parameters: ReplyParameters, + * reply_markup: InlineKeyboardMarkup|ReplyKeyboardMarkup|ReplyKeyboardRemove|ForceReply + * } $data + * + * @throws Exceptions\TelegramException + */ + public function copyMessage(array $data = []): MessageId + { + return $this->send(__FUNCTION__, $data, MessageId::class); + } + + /** + * @param array{ + * chat_id: int|string, + * message_thread_id: int, + * from_chat_id: int|string, + * message_ids: int[], + * disable_notification: bool, + * protect_content: bool, + * remove_caption: bool, + * } $data + * @return MessageId[] + * + * @throws Exceptions\TelegramException + */ + public function copyMessages(array $data = []): array + { + return $this->send(__FUNCTION__, $data, [MessageId::class]); + } + + /** + * @param array{ + * business_connection_id: string, + * chat_id: int|string, + * message_thread_id: int, + * photo: string|InputFile, + * caption: string, + * parse_mode: string, + * caption_entities: MessageEntity[], + * show_caption_above_media: bool, + * has_spoiler: bool, + * disable_notification: bool, + * protect_content: bool, + * message_effect_id: string, + * reply_parameters: ReplyParameters, + * reply_markup: InlineKeyboardMarkup|ReplyKeyboardMarkup|ReplyKeyboardRemove|ForceReply + * } $data + * + * @throws Exceptions\TelegramException + */ + public function sendPhoto(array $data = []): Message + { + return $this->send(__FUNCTION__, $data, Message::class); + } + + /** + * @param array{ + * business_connection_id: string, + * chat_id: int|string, + * message_thread_id: int, + * audio: string|InputFile, + * caption: string, + * parse_mode: string, + * caption_entities: MessageEntity[], + * duration: int, + * performer: string, + * title: string, + * thumbnail: string|InputFile, + * disable_notification: bool, + * protect_content: bool, + * message_effect_id: string, + * reply_parameters: ReplyParameters, + * reply_markup: InlineKeyboardMarkup|ReplyKeyboardMarkup|ReplyKeyboardRemove|ForceReply, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function sendAudio(array $data = []): Message + { + return $this->send(__FUNCTION__, $data, Message::class); + } + + /** + * @param array{ + * business_connection_id: string, + * chat_id: int|string, + * message_thread_id: int, + * document: string|InputFile, + * thumbnail: string|InputFile, + * caption: string, + * parse_mode: string, + * caption_entities: MessageEntity[], + * disable_content_type_detection: bool, + * disable_notification: bool, + * protect_content: bool, + * message_effect_id: string, + * reply_parameters: ReplyParameters, + * reply_markup: InlineKeyboardMarkup|ReplyKeyboardMarkup|ReplyKeyboardRemove|ForceReply, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function sendDocument(array $data = []): Message + { + return $this->send(__FUNCTION__, $data, Message::class); + } + + /** + * @param array{ + * business_connection_id: string, + * chat_id: int|string, + * message_thread_id: int, + * video: string|InputFile, + * duration: int, + * width: int, + * height: int, + * thumbnail: string|InputFile, + * caption: string, + * parse_mode: string, + * caption_entities: MessageEntity[], + * show_caption_above_media: bool, + * has_spoiler: bool, + * supports_streaming: bool, + * disable_notification: bool, + * protect_content: bool, + * message_effect_id: string, + * reply_parameters: ReplyParameters, + * reply_markup: InlineKeyboardMarkup|ReplyKeyboardMarkup|ReplyKeyboardRemove|ForceReply, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function sendVideo(array $data = []): Message + { + return $this->send(__FUNCTION__, $data, Message::class); + } + + /** + * @param array{ + * business_connection_id: string, + * chat_id: int|string, + * message_thread_id: int, + * animation: string|InputFile, + * duration: int, + * width: int, + * height: int, + * thumbnail: string|InputFile, + * caption: string, + * parse_mode: string, + * caption_entities: MessageEntity[], + * show_caption_above_media: bool, + * has_spoiler: bool, + * disable_notification: bool, + * protect_content: bool, + * message_effect_id: string, + * reply_parameters: ReplyParameters, + * reply_markup: InlineKeyboardMarkup|ReplyKeyboardMarkup|ReplyKeyboardRemove|ForceReply, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function sendAnimation(array $data = []): Message + { + return $this->send(__FUNCTION__, $data, Message::class); + } + + /** + * @param array{ + * business_connection_id: string, + * chat_id: int|string, + * message_thread_id: int, + * voice: string|InputFile, + * caption: string, + * parse_mode: string, + * caption_entities: MessageEntity[], + * duration: int, + * disable_notification: bool, + * protect_content: bool, + * message_effect_id: string, + * reply_parameters: ReplyParameters, + * reply_markup: InlineKeyboardMarkup|ReplyKeyboardMarkup|ReplyKeyboardRemove|ForceReply, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function sendVoice(array $data = []): Message + { + return $this->send(__FUNCTION__, $data, Message::class); + } + + /** + * @param array{ + * business_connection_id: string, + * chat_id: int|string, + * message_thread_id: int, + * video_note: string|InputFile, + * duration: int, + * length: int, + * thumbnail: string|InputFile, + * disable_notification: bool, + * protect_content: bool, + * message_effect_id: string, + * reply_parameters: ReplyParameters, + * reply_markup: InlineKeyboardMarkup|ReplyKeyboardMarkup|ReplyKeyboardRemove|ForceReply, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function sendVideoNote(array $data = []): Message + { + return $this->send(__FUNCTION__, $data, Message::class); + } + + /** + * @param array{ + * business_connection_id: string, + * chat_id: int|string, + * message_thread_id: int, + * media: InputMedia[], + * disable_notification: bool, + * protect_content: bool, + * message_effect_id: string, + * reply_parameters: ReplyParameters, + * } $data + * @return Message[] + * + * @throws Exceptions\TelegramException + */ + public function sendMediaGroup(array $data = []): array + { + return $this->send(__FUNCTION__, $data, [Message::class]); + } + + /** + * @param array{ + * business_connection_id: string, + * chat_id: int|string, + * message_thread_id: int, + * latitude: float, + * longitude: float, + * horizontal_accuracy: float, + * live_period: int, + * heading: int, + * proximity_alert_radius: int, + * disable_notification: bool, + * protect_content: bool, + * message_effect_id: string, + * reply_parameters: ReplyParameters, + * reply_markup: InlineKeyboardMarkup|ReplyKeyboardMarkup|ReplyKeyboardRemove|ForceReply + * } $data + * + * @throws Exceptions\TelegramException + */ + public function sendLocation(array $data = []): Message + { + return $this->send(__FUNCTION__, $data, Message::class); + } + + /** + * @param array{ + * business_connection_id: string, + * chat_id: int|string, + * message_thread_id: int, + * latitude: float, + * longitude: float, + * title: string, + * address: string, + * foursquare_id: string, + * foursquare_type: string, + * google_place_id: string, + * google_place_type: string, + * disable_notification: bool, + * protect_content: bool, + * message_effect_id: string, + * reply_parameters: ReplyParameters, + * reply_markup: InlineKeyboardMarkup|ReplyKeyboardMarkup|ReplyKeyboardRemove|ForceReply + * } $data + * + * @throws Exceptions\TelegramException + */ + public function sendVenue(array $data = []): Message + { + return $this->send(__FUNCTION__, $data, Message::class); + } + + /** + * @param array{ + * business_connection_id: string, + * chat_id: int|string, + * message_thread_id: int, + * phone_number: string, + * first_name: string, + * last_name: string, + * vcard: string, + * disable_notification: bool, + * protect_content: bool, + * message_effect_id: string, + * reply_parameters: ReplyParameters, + * reply_markup: InlineKeyboardMarkup|ReplyKeyboardMarkup|ReplyKeyboardRemove|ForceReply + * } $data + * + * @throws Exceptions\TelegramException + */ + public function sendContact(array $data = []): Message + { + return $this->send(__FUNCTION__, $data, Message::class); + } + + /** + * @param array{ + * business_connection_id: string, + * chat_id: int|string, + * message_thread_id: int, + * question: string, + * question_parse_mode: string, + * question_entities: array, + * options: array, + * is_anonymous: bool, + * type: string, + * allows_multiple_answers: bool, + * correct_option_id: int, + * explanation: string, + * explanation_parse_mode: string, + * explanation_entities: array, + * open_period: int, + * close_date: int, + * is_closed: bool, + * disable_notification: bool, + * protect_content: bool, + * message_effect_id: string, + * reply_parameters: ReplyParameters, + * reply_markup: InlineKeyboardMarkup|ReplyKeyboardMarkup|ReplyKeyboardRemove|ForceReply + * } $data + * + * @throws Exceptions\TelegramException + */ + public function sendPoll(array $data = []): Message + { + return $this->send(__FUNCTION__, $data, Message::class); + } + + /** + * @param array{ + * business_connection_id: string, + * chat_id: int|string, + * message_thread_id: int, + * emoji: string, + * disable_notification: bool, + * protect_content: bool, + * message_effect_id: string, + * reply_parameters: ReplyParameters, + * reply_markup: InlineKeyboardMarkup|ReplyKeyboardMarkup|ReplyKeyboardRemove|ForceReply + * } $data + * + * @throws Exceptions\TelegramException + */ + public function sendDice(array $data = []): Message + { + return $this->send(__FUNCTION__, $data, Message::class); + } + + /** + * @param array{ + * business_connection_id: string, + * chat_id: int|string, + * message_thread_id: int, + * action: string, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function sendChatAction(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * chat_id: int|string, + * message_id: int, + * reaction: ReactionType[], + * is_big: bool + * } $data + * + * @throws Exceptions\TelegramException + */ + public function sendMessageReaction(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * user_id: int, + * offset: int, + * limit: int, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function getUserProfilePhotos(array $data = []): UserProfilePhotos + { + return $this->send(__FUNCTION__, $data, UserProfilePhotos::class); + } + + /** + * @param array{ + * file_id: string, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function getFile(array $data = []): File + { + return $this->send(__FUNCTION__, $data, File::class); + } + + /** + * @param array{ + * chat_id: int|string, + * user_id: int, + * until_date: int, + * revoke_messages: bool, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function banChatMember(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * chat_id: int|string, + * user_id: int, + * only_if_banned: bool, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function unbanChatMember(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * chat_id: int|string, + * user_id: int, + * permissions: ChatPermissions, + * use_independent_chat_permissions: bool, + * until_date: int, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function restrictChatMember(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * chat_id: int|string, + * user_id: int, + * is_anonymous: bool, + * can_manage_chat: bool, + * can_delete_messages: bool, + * can_manage_video_chats: bool, + * can_restrict_members: bool, + * can_promote_members: bool, + * can_change_info: bool, + * can_invite_users: bool, + * can_post_stories: bool, + * can_edit_stories: bool, + * can_delete_stories: bool, + * can_post_messages: bool, + * can_edit_messages: bool, + * can_pin_messages: bool, + * can_manage_topics: bool + * } $data + * + * @throws Exceptions\TelegramException + */ + public function promoteChatMember(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * chat_id: int|string, + * user_id: int, + * custom_title: string, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function setChatAdministratorCustomTitle(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * chat_id: int|string, + * sender_chat_id: int, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function banChatSenderChat(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * chat_id: int|string, + * sender_chat_id: int, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function unbanChatSenderChat(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * chat_id: int|string, + * permissions: ChatPermissions, + * use_independent_chat_permissions: bool, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function setChatPermissions(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * chat_id: int|string, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function exportChatInviteLink(array $data = []): string + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * chat_id: int|string, + * name: string, + * expire_date: int, + * member_limit: int, + * creates_join_request: bool, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function createChatInviteLink(array $data = []): ChatInviteLink + { + return $this->send(__FUNCTION__, $data, ChatInviteLink::class); + } + + /** + * @param array{ + * chat_id: int|string, + * invite_link: string, + * name: string, + * expire_date: int, + * member_limit: int, + * creates_join_request: bool, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function editChatInviteLink(array $data = []): ChatInviteLink + { + return $this->send(__FUNCTION__, $data, ChatInviteLink::class); + } + + /** + * @param array{ + * chat_id: int|string, + * invite_link: string, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function revokeChatInviteLink(array $data = []): ChatInviteLink + { + return $this->send(__FUNCTION__, $data, ChatInviteLink::class); + } + + /** + * @param array{ + * chat_id: int|string, + * user_id: int, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function approveChatJoinRequest(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * chat_id: int|string, + * user_id: int, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function declineChatJoinRequest(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * chat_id: int|string, + * photo: InputFile, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function setChatPhoto(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * chat_id: int|string, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function deleteChatPhoto(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * chat_id: int|string, + * title: string, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function setChatTitle(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * chat_id: int|string, + * description: string, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function setChatDescription(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * chat_id: int|string, + * message_id: int, + * disable_notification: bool, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function pinChatMessage(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * chat_id: int|string, + * message_id: int, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function unpinChatMessage(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * chat_id: int|string, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function unpinAllChatMessages(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * chat_id: int|string, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function leaveChat(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * chat_id: int|string, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function getChat(array $data = []): ChatFullInfo + { + return $this->send(__FUNCTION__, $data, ChatFullInfo::class); + } + + /** + * @param array{ + * chat_id: int|string, + * } $data + * @return ChatMember[] + * + * @throws Exceptions\TelegramException + */ + public function getChatAdministrator(array $data = []): array + { + return $this->send(__FUNCTION__, $data, [ChatMember::class]); + } + + /** + * @param array{ + * chat_id: int|string, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function getChatMemberCount(array $data = []): int + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * chat_id: int|string, + * user_id: int, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function getChatMember(array $data = []): ChatMember + { + return $this->send(__FUNCTION__, $data, ChatMember::class); + } + + /** + * @param array{ + * chat_id: int|string, + * sticker_set_name: string, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function setChatStickerSet(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * chat_id: int|string, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function deleteChatStickerSet(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * + * } $data + * @return Sticker[] + * + * @throws Exceptions\TelegramException + */ + public function getForumTopicIconStickers(array $data = []): array + { + return $this->send(__FUNCTION__, $data, [Sticker::class]); + } + + /** + * @param array{ + * chat_id: int|string, + * name: string, + * icon_color: int, + * icon_custom_emoji_id: string, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function createForumTopic(array $data = []): ForumTopic + { + return $this->send(__FUNCTION__, $data, ForumTopic::class); + } + + /** + * @param array{ + * chat_id: int|string, + * message_thread_id: int, + * name: string, + * icon_custom_emoji_id: string, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function editForumTopic(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * chat_id: int|string, + * message_thread_id: int, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function closeForumTopic(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * chat_id: int|string, + * message_thread_id: int, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function reopenForumTopic(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * chat_id: int|string, + * message_thread_id: int, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function deleteForumTopic(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * chat_id: int|string, + * message_thread_id: int, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function unpinAllForumTopicMessages(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * chat_id: int|string, + * name: string, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function editGeneralForumTopic(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * chat_id: int|string, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function closeGeneralForumTopic(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * chat_id: int|string, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function reopenGeneralForumTopic(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * chat_id: int|string, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function hideGeneralForumTopic(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * chat_id: int|string + * } $data + * + * @throws Exceptions\TelegramException + */ + public function unhideGeneralForumTopic(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * chat_id: int|string, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function unpinAllGeneralForumTopicMessages(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * callback_query_id: string, + * text: string, + * show_alert: bool, + * url: string, + * cache_time: int, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function answerCallbackQuery(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * chat_id: int|string, + * user_id: int, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function getUserChatBoosts(array $data = []): UserChatBoosts + { + return $this->send(__FUNCTION__, $data, UserChatBoosts::class); + } + + /** + * @param array{ + * business_connection_id: string, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function getBusinessConnection(array $data = []): BusinessConnection + { + return $this->send(__FUNCTION__, $data, BusinessConnection::class); + } + + /** + * @param array{ + * commands: BotCommand[], + * scope: BotCommandScope, + * language_code: string, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function setMyCommands(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * scope: BotCommandScope, + * language_code: string, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function deleteMyCommands(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * scope: BotCommandScope, + * language_code: string, + * } $data + * @return BotCommand[] + * + * @throws Exceptions\TelegramException + */ + public function getMyCommands(array $data = []): array + { + return $this->send(__FUNCTION__, $data, [BotCommand::class]); + } + + /** + * @param array{ + * name: string, + * language_code: string, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function setMyName(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * language_code: string, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function getMyName(array $data = []): BotName + { + return $this->send(__FUNCTION__, $data, BotName::class); + } + + /** + * @param array{ + * description: string, + * language_code: string, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function setMyDescription(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * language_code: string, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function getMyDescription(array $data = []): BotDescription + { + return $this->send(__FUNCTION__, $data, BotDescription::class); + } + + /** + * @param array{ + * short_description: string, + * language_code: string, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function setMyShortDescription(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * language_code: string, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function getMyShortDescription(array $data = []): BotShortDescription + { + return $this->send(__FUNCTION__, $data, BotShortDescription::class); + } + + /** + * @param array{ + * chat_id: int, + * menu_button: MenuButton, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function setChatMenuButton(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * chat_id: int, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function getChatMenuButton(array $data = []): MenuButton + { + return $this->send(__FUNCTION__, $data, MenuButton::class); + } + + /** + * @param array{ + * rights: ChatAdministratorRights, + * for_channels: bool, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function setMyDefaultAdministratorRights(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * for_channels: bool, + * } $data + * + * @throws Exceptions\TelegramException + */ + public function getMyDefaultAdministratorRights(array $data = []): ChatAdministratorRights + { + return $this->send(__FUNCTION__, $data, ChatAdministratorRights::class); + } +} diff --git a/src/Methods/SendsStickers.php b/src/Methods/SendsStickers.php new file mode 100644 index 000000000..d9857f948 --- /dev/null +++ b/src/Methods/SendsStickers.php @@ -0,0 +1,242 @@ +send(__FUNCTION__, $data, Message::class); + } + + /** + * @param array{ + * name: string, + * } $data + * + * @throws \PhpTelegramBot\Core\Exceptions\TelegramException + */ + public function getStickerSet(array $data = []): StickerSet + { + return $this->send(__FUNCTION__, $data, StickerSet::class); + } + + /** + * @param array{ + * custom_emoji_ids: string[], + * } $data + * @return Sticker[] + * + * @throws \PhpTelegramBot\Core\Exceptions\TelegramException + */ + public function getCustomEmojiStickers(array $data = []): array + { + return $this->send(__FUNCTION__, $data, [Sticker::class]); + } + + /** + * @param array{ + * user_id: int, + * sticker: InputFile, + * sticker_format: string, + * } $data + * + * @throws \PhpTelegramBot\Core\Exceptions\TelegramException + */ + public function uploadStickerFile(array $data = []): File + { + return $this->send(__FUNCTION__, $data, File::class); + } + + /** + * @param array{ + * user_id: int, + * name: string, + * title: string, + * stickers: InputSticker[], + * sticker_type: string, + * needs_repainting: bool, + * } $data + * + * @throws \PhpTelegramBot\Core\Exceptions\TelegramException + */ + public function createNewStickerSet(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * user_id: int, + * name: string, + * sticker: InputSticker, + * } $data + * + * @throws \PhpTelegramBot\Core\Exceptions\TelegramException + */ + public function addStickerToSet(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * sticker: string, + * position: int, + * } $data + * + * @throws \PhpTelegramBot\Core\Exceptions\TelegramException + */ + public function setStickerPositionInSet(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * sticker: string, + * } $data + * + * @throws \PhpTelegramBot\Core\Exceptions\TelegramException + */ + public function deleteStickerFromSet(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * user_id: int, + * name: string, + * old_sticker: string, + * sticker: InputSticker, + * } $data + * + * @throws \PhpTelegramBot\Core\Exceptions\TelegramException + */ + public function replaceStickerInSet(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * sticker: string, + * emoji_list: string[], + * } $data + * + * @throws \PhpTelegramBot\Core\Exceptions\TelegramException + */ + public function setStickerEmojiList(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * sticker: string, + * keywords: string[], + * } $data + * + * @throws \PhpTelegramBot\Core\Exceptions\TelegramException + */ + public function setStickerKeywords(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * sticker: string, + * mask_position: MaskPosition, + * } $data + * + * @throws \PhpTelegramBot\Core\Exceptions\TelegramException + */ + public function setStickerMaskPosition(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * name: string, + * title: string, + * } $data + * + * @throws \PhpTelegramBot\Core\Exceptions\TelegramException + */ + public function setStickerSetTitle(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * name: string, + * user_id: int, + * thumbnail: InputFile|string, + * format: string, + * } $data + * + * @throws \PhpTelegramBot\Core\Exceptions\TelegramException + */ + public function setStickerSetThumbnail(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * name: string, + * custom_emoji_id: string, + * } $data + * + * @throws \PhpTelegramBot\Core\Exceptions\TelegramException + */ + public function setCustomEmojiStickerSetThumbnail(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * name: string, + * } $data + * + * @throws \PhpTelegramBot\Core\Exceptions\TelegramException + */ + public function deleteStickerSet(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } +} diff --git a/src/Methods/UpdatesMessages.php b/src/Methods/UpdatesMessages.php new file mode 100644 index 000000000..fe5ad8573 --- /dev/null +++ b/src/Methods/UpdatesMessages.php @@ -0,0 +1,159 @@ +send(__FUNCTION__, $data, Message::class); + } + + /** + * @param array{ + * chat_id: int|string, + * message_id: int, + * inline_message_id: string, + * caption: string, + * parse_mode: string, + * caption_entities: MessageEntity[], + * show_caption_above_media:bool, + * reply_markup: InlineKeyboardMarkup, + * } $data + * + * @throws TelegramException + */ + public function editMessageCaption(array $data = []): Message|bool + { + return $this->send(__FUNCTION__, $data, Message::class); + } + + /** + * @param array{ + * chat_id: int|string, + * message_id: int, + * inline_message_id: string, + * media: InputMedia, + * reply_markup: InlineKeyboardMarkup, + * } $data + * + * @throws \PhpTelegramBot\Core\Exceptions\TelegramException + */ + public function editMessageMedia(array $data = []): Message|bool + { + return $this->send(__FUNCTION__, $data, Message::class); + } + + /** + * @param array{ + * chat_id: int|string, + * message_id: int, + * inline_message_id: string, + * latitude: float, + * longitude: float, + * live_period: int, + * horizontal_accuracy: float, + * heading: int, + * proximity_alert_radius: int, + * reply_markup: InlineKeyboardMarkup, + * } $data + * + * @throws \PhpTelegramBot\Core\Exceptions\TelegramException + */ + public function editMessageLiveLocation(array $data = []): Message|bool + { + return $this->send(__FUNCTION__, $data, Message::class); + } + + /** + * @param array{ + * chat_id: int|string, + * message_id: int, + * inline_message_id: string, + * reply_markup: InlineKeyboardMarkup, + * } $data + * + * @throws \PhpTelegramBot\Core\Exceptions\TelegramException + */ + public function stopMessageLiveLocation(array $data = []): Message|bool + { + return $this->send(__FUNCTION__, $data, Message::class); + } + + /** + * @param array{ + * chat_id: int|string, + * message_id: int, + * inline_message_id: string, + * reply_markup: InlineKeyboardMarkup, + * } $data + * + * @throws \PhpTelegramBot\Core\Exceptions\TelegramException + */ + public function editMessageReplyMarkup(array $data = []): Message|bool + { + return $this->send(__FUNCTION__, $data, Message::class); + } + + /** + * @param array{ + * chat_id: int|string, + * message_id: int, + * reply_markup: InlineKeyboardMarkup, + * } $data + * + * @throws \PhpTelegramBot\Core\Exceptions\TelegramException + */ + public function stopPoll(array $data = []): Poll + { + return $this->send(__FUNCTION__, $data, Poll::class); + } + + /** + * @param array{ + * chat_id: int|string, + * message_id: int, + * } $data + * + * @throws \PhpTelegramBot\Core\Exceptions\TelegramException + */ + public function deleteMessage(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } + + /** + * @param array{ + * chat_id: int|string, + * message_ids: int[], + * } $data + * + * @throws \PhpTelegramBot\Core\Exceptions\TelegramException + */ + public function deleteMessages(array $data = []): bool + { + return $this->send(__FUNCTION__, $data, null); + } +} diff --git a/src/Telegram.php b/src/Telegram.php index 86abde8bb..5a29d966d 100644 --- a/src/Telegram.php +++ b/src/Telegram.php @@ -2,11 +2,23 @@ namespace PhpTelegramBot\Core; +use PhpTelegramBot\Core\Entities\Factory; use PhpTelegramBot\Core\Exceptions\NotYetImplementedException; use PhpTelegramBot\Core\Exceptions\TelegramException; +use PhpTelegramBot\Core\Methods\AnswersInlineQueries; +use PhpTelegramBot\Core\Methods\SendsInvoices; +use PhpTelegramBot\Core\Methods\SendsMessages; +use PhpTelegramBot\Core\Methods\SendsStickers; +use PhpTelegramBot\Core\Methods\UpdatesMessages; class Telegram { + use AnswersInlineQueries; + use SendsInvoices; + use SendsMessages; + use SendsStickers; + use UpdatesMessages; + protected string $apiBaseUri = 'https://api.telegram.org'; public function __construct( @@ -18,15 +30,18 @@ public function __construct( $this->client = new HttpClient(); } - public function __call(string $methodName, array $arguments) + public function __call(string $methodName, array $arguments): mixed { - $requestUri = $this->apiBaseUri.'/bot'.$this->botToken.'/'.$methodName; + return $this->send($methodName, $arguments[0] ?? null, $arguments[1] ?? null); + } - $data = $arguments[0] ?? null; + protected function send(string $methodName, ?array $data = null, string|array|null $returnType = null): mixed + { + $requestUri = $this->apiBaseUri.'/bot'.$this->botToken.'/'.$methodName; $response = match (true) { - $data === null => $this->client->get($requestUri), - default => $this->client->postJson($requestUri, $data), + empty($data) => $this->client->get($requestUri), + default => $this->client->postJson($requestUri, $data), }; $result = json_decode($response->getBody()->getContents(), true); @@ -37,7 +52,30 @@ public function __call(string $methodName, array $arguments) ); } - return $result['result']; + if ($returnType === null) { + return $result['result']; + } + + if (is_array($returnType)) { + $returnType = $returnType[0]; + + return array_map(fn ($item) => $this->makeResultObject($item, $returnType), $result['result']); + } + + return $this->makeResultObject($result['result'], $returnType); + } + + protected function makeResultObject(mixed $result, string|array|null $returnType = null): mixed + { + if (! is_array($result)) { + return $result; + } + + if (is_subclass_of($returnType, Factory::class)) { + return $returnType::make($result); + } + + return new $returnType($result); } public function handleGetUpdates() From ed35363e946b6d43eaa5484afdb51a2394a5767b Mon Sep 17 00:00:00 2001 From: Tii Date: Wed, 12 Jun 2024 07:31:57 +0200 Subject: [PATCH 06/20] Polling --- src/Telegram.php | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/Telegram.php b/src/Telegram.php index 5a29d966d..63783d9f5 100644 --- a/src/Telegram.php +++ b/src/Telegram.php @@ -3,6 +3,7 @@ namespace PhpTelegramBot\Core; use PhpTelegramBot\Core\Entities\Factory; +use PhpTelegramBot\Core\Entities\Update; use PhpTelegramBot\Core\Exceptions\NotYetImplementedException; use PhpTelegramBot\Core\Exceptions\TelegramException; use PhpTelegramBot\Core\Methods\AnswersInlineQueries; @@ -78,9 +79,21 @@ protected function makeResultObject(mixed $result, string|array|null $returnType return new $returnType($result); } - public function handleGetUpdates() + public function handleGetUpdates(int $pollingInterval = 30, ?array $allowedUpdates = null) { - throw new NotYetImplementedException(); + $offset = null; + while (true) { + $updates = $this->getUpdates([ + 'offset' => $offset, + 'timeout' => 30, + 'allowed_updates' => $allowedUpdates, + ]); + + foreach ($updates as $update) { + $this->processUpdate($update); + $offset = $update->getUpdateId() + 1; + } + } } public function handle() @@ -88,8 +101,8 @@ public function handle() throw new NotYetImplementedException(); } - protected function processUpdate() + protected function processUpdate(Update $update) { - throw new NotYetImplementedException(); + // } } From bea9e8576a019fe28e926f5a13ae301b596f0a2c Mon Sep 17 00:00:00 2001 From: Tii Date: Wed, 12 Jun 2024 22:16:25 +0200 Subject: [PATCH 07/20] Handle incoming webhook --- src/Telegram.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Telegram.php b/src/Telegram.php index 63783d9f5..917c8c70f 100644 --- a/src/Telegram.php +++ b/src/Telegram.php @@ -4,7 +4,6 @@ use PhpTelegramBot\Core\Entities\Factory; use PhpTelegramBot\Core\Entities\Update; -use PhpTelegramBot\Core\Exceptions\NotYetImplementedException; use PhpTelegramBot\Core\Exceptions\TelegramException; use PhpTelegramBot\Core\Methods\AnswersInlineQueries; use PhpTelegramBot\Core\Methods\SendsInvoices; @@ -98,7 +97,12 @@ public function handleGetUpdates(int $pollingInterval = 30, ?array $allowedUpdat public function handle() { - throw new NotYetImplementedException(); + $data = file_get_contents('php://input'); + $json = json_decode($data, true); + + $update = new Update($json); + + $this->processUpdate($update); } protected function processUpdate(Update $update) From b7a5edde8241bc673cbe0105859b569cdceed316 Mon Sep 17 00:00:00 2001 From: Tii Date: Thu, 13 Jun 2024 18:28:02 +0200 Subject: [PATCH 08/20] Added ability to send files --- .gitignore | 4 +- README.md | 5 +-- composer.json | 3 +- examples/.env.example | 2 + examples/.gitignore | 1 + examples/bootstrap.php | 7 ++++ examples/files/example_photo.jpeg | Bin 0 -> 454026 bytes examples/sendPhoto.php | 16 ++++++++ .../AnswersInlineQueries.php | 2 +- src/{Methods => ApiMethods}/SendsInvoices.php | 2 +- src/{Methods => ApiMethods}/SendsMessages.php | 2 +- src/{Methods => ApiMethods}/SendsStickers.php | 2 +- .../UpdatesMessages.php | 2 +- src/Entities/Entity.php | 2 +- src/Entities/InputFile.php | 15 ++++++++ src/Exceptions/InvalidArgumentException.php | 8 ++++ src/Exceptions/NotYetImplementedException.php | 11 ------ src/HttpClient.php | 30 +++++++++++++++ src/Telegram.php | 35 ++++++++++++------ 19 files changed, 116 insertions(+), 33 deletions(-) create mode 100644 examples/.env.example create mode 100644 examples/.gitignore create mode 100644 examples/bootstrap.php create mode 100644 examples/files/example_photo.jpeg create mode 100644 examples/sendPhoto.php rename src/{Methods => ApiMethods}/AnswersInlineQueries.php (96%) rename src/{Methods => ApiMethods}/SendsInvoices.php (98%) rename src/{Methods => ApiMethods}/SendsMessages.php (99%) rename src/{Methods => ApiMethods}/SendsStickers.php (99%) rename src/{Methods => ApiMethods}/UpdatesMessages.php (99%) create mode 100644 src/Exceptions/InvalidArgumentException.php delete mode 100644 src/Exceptions/NotYetImplementedException.php diff --git a/.gitignore b/.gitignore index 95d0d25fa..179871170 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,6 @@ phpdoc-* # OSX ._* .Spotlight-V100 -.Trashes \ No newline at end of file +.Trashes + +/public \ No newline at end of file diff --git a/README.md b/README.md index 34ae59a13..82b079ba7 100644 --- a/README.md +++ b/README.md @@ -105,9 +105,8 @@ _TODO_ - [x] Adding Telegram Types - [x] Adding base functionality to call API Methods - [x] Adding base functionality to parse Telegram Entities -- [ ] Test editMessageText with its return types Message or bool -- [ ] "must be article" etc should be set via presetData static function -- [ ] File Downloads/Uploads +- [x] Test editMessageText with its return types Message or bool +- [x] File Downloads/Uploads - [ ] Custom Bot API Server

(back to top)

diff --git a/composer.json b/composer.json index 1958567b3..e48a16fb4 100644 --- a/composer.json +++ b/composer.json @@ -49,7 +49,8 @@ "guzzlehttp/psr7": "^2.6", "laravel/pint": "^1.16", "php-http/guzzle7-adapter": "^1.0", - "spatie/ray": "^1.41" + "spatie/ray": "^1.41", + "vlucas/phpdotenv": "^5.6" }, "autoload": { "psr-4": { diff --git a/examples/.env.example b/examples/.env.example new file mode 100644 index 000000000..068ec4ccf --- /dev/null +++ b/examples/.env.example @@ -0,0 +1,2 @@ +BOT_TOKEN= +RECIPIENT_CHAT_ID= \ No newline at end of file diff --git a/examples/.gitignore b/examples/.gitignore new file mode 100644 index 000000000..4cb512ec1 --- /dev/null +++ b/examples/.gitignore @@ -0,0 +1 @@ +/.env \ No newline at end of file diff --git a/examples/bootstrap.php b/examples/bootstrap.php new file mode 100644 index 000000000..d4b0a0a2c --- /dev/null +++ b/examples/bootstrap.php @@ -0,0 +1,7 @@ +safeLoad(); diff --git a/examples/files/example_photo.jpeg b/examples/files/example_photo.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..b2efc26a657ceb0d72a8dee62657ed734f4670d6 GIT binary patch literal 454026 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

Y3ZQm~>9!Fa3wP|i`<&)*f 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%0A)aQ_>2R|s;dO) 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%0D^k=t2b+5Vf~kX z3=)|v^=o4≮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 a-~ci|9M?avr|q3d>e{!-7Mr!3KmXY?r4U2_ literal 0 HcmV?d00001 diff --git a/examples/sendPhoto.php b/examples/sendPhoto.php new file mode 100644 index 000000000..ee074afe1 --- /dev/null +++ b/examples/sendPhoto.php @@ -0,0 +1,16 @@ +sendPhoto([ + 'chat_id' => $_SERVER['RECIPIENT_CHAT_ID'], + ...\PhpTelegramBot\Core\Entities\InputFile::attachFile('photo', __DIR__.'/files/example_photo.jpeg'), + ]); + + echo 'Photo sent.'; +} catch (\PhpTelegramBot\Core\Exceptions\TelegramException $e) { + echo 'ERROR: '.$e->getMessage(); +} diff --git a/src/Methods/AnswersInlineQueries.php b/src/ApiMethods/AnswersInlineQueries.php similarity index 96% rename from src/Methods/AnswersInlineQueries.php rename to src/ApiMethods/AnswersInlineQueries.php index 892fbb8e8..4c56308b4 100644 --- a/src/Methods/AnswersInlineQueries.php +++ b/src/ApiMethods/AnswersInlineQueries.php @@ -1,6 +1,6 @@ fields, fn ($item) => ! is_null($item)); + return array_filter($this->fields, fn ($value, $key) => ! is_null($value) && str_starts_with($key, '__'), ARRAY_FILTER_USE_BOTH); } } diff --git a/src/Entities/InputFile.php b/src/Entities/InputFile.php index 342b8c2c8..6e6dbc5ca 100644 --- a/src/Entities/InputFile.php +++ b/src/Entities/InputFile.php @@ -2,6 +2,21 @@ namespace PhpTelegramBot\Core\Entities; +use PhpTelegramBot\Core\Exceptions\InvalidArgumentException; + class InputFile { + public static function attachFile(string $field, string $filepath): array + { + $file_id = uniqid($field.'_'); + + if (! is_file($filepath)) { + throw new InvalidArgumentException("Cannot attach file to '$field'. $filepath must be a valid filepath."); + } + + return [ + $field => 'attach://'.$file_id, + '__file_'.$file_id => fopen($filepath, 'r'), + ]; + } } diff --git a/src/Exceptions/InvalidArgumentException.php b/src/Exceptions/InvalidArgumentException.php new file mode 100644 index 000000000..b3217772e --- /dev/null +++ b/src/Exceptions/InvalidArgumentException.php @@ -0,0 +1,8 @@ + $value) { + if (str_starts_with($key, '__file_')) { + $key = substr($key, 7); + } + + $value = match (true) { + is_array($value) => json_encode($value), + is_resource($value) => $value, + default => (string) $value, + }; + + $builder->addResource($key, $value); + } + + $boundary = $builder->getBoundary(); + + return $this->client->sendRequest( + $this->requestFactory->createRequest('POST', $uri) + ->withHeader('Content-Type', "multipart/form-data; boundary=\"$boundary\"") + ->withBody($builder->build()) + ); + } } diff --git a/src/Telegram.php b/src/Telegram.php index 917c8c70f..e571b4f5b 100644 --- a/src/Telegram.php +++ b/src/Telegram.php @@ -2,14 +2,14 @@ namespace PhpTelegramBot\Core; +use PhpTelegramBot\Core\ApiMethods\AnswersInlineQueries; +use PhpTelegramBot\Core\ApiMethods\SendsInvoices; +use PhpTelegramBot\Core\ApiMethods\SendsMessages; +use PhpTelegramBot\Core\ApiMethods\SendsStickers; +use PhpTelegramBot\Core\ApiMethods\UpdatesMessages; use PhpTelegramBot\Core\Entities\Factory; use PhpTelegramBot\Core\Entities\Update; use PhpTelegramBot\Core\Exceptions\TelegramException; -use PhpTelegramBot\Core\Methods\AnswersInlineQueries; -use PhpTelegramBot\Core\Methods\SendsInvoices; -use PhpTelegramBot\Core\Methods\SendsMessages; -use PhpTelegramBot\Core\Methods\SendsStickers; -use PhpTelegramBot\Core\Methods\UpdatesMessages; class Telegram { @@ -35,20 +35,33 @@ public function __call(string $methodName, array $arguments): mixed return $this->send($methodName, $arguments[0] ?? null, $arguments[1] ?? null); } + protected function dataContainsFiles(array $data): bool + { + foreach ($data as $key => $value) { + if (str_starts_with($key, '__file_')) { + return true; + } + } + + return false; + } + protected function send(string $methodName, ?array $data = null, string|array|null $returnType = null): mixed { $requestUri = $this->apiBaseUri.'/bot'.$this->botToken.'/'.$methodName; $response = match (true) { - empty($data) => $this->client->get($requestUri), - default => $this->client->postJson($requestUri, $data), + empty($data) => $this->client->get($requestUri), + $this->dataContainsFiles($data) => $this->client->postMultipart($requestUri, $data), + default => $this->client->postJson($requestUri, $data), }; $result = json_decode($response->getBody()->getContents(), true); - if ($result['ok'] !== true) { + + if ($result === null || $result['ok'] !== true) { throw new TelegramException( - $result['description'], - $result['error_code'] ?? 0, + $result['description'] ?? $response->getReasonPhrase(), + $result['error_code'] ?? $response->getStatusCode(), ); } @@ -84,7 +97,7 @@ public function handleGetUpdates(int $pollingInterval = 30, ?array $allowedUpdat while (true) { $updates = $this->getUpdates([ 'offset' => $offset, - 'timeout' => 30, + 'timeout' => $pollingInterval, 'allowed_updates' => $allowedUpdates, ]); From 193e7b3d0c8f1dc01ddeab2d23600663028603ce Mon Sep 17 00:00:00 2001 From: Tii Date: Thu, 13 Jun 2024 18:31:31 +0200 Subject: [PATCH 09/20] Added additional sendPhoto example --- examples/sendPhoto.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/examples/sendPhoto.php b/examples/sendPhoto.php index ee074afe1..bde87a905 100644 --- a/examples/sendPhoto.php +++ b/examples/sendPhoto.php @@ -5,12 +5,22 @@ $bot = new \PhpTelegramBot\Core\Telegram($_SERVER['BOT_TOKEN']); try { + $bot->sendPhoto([ 'chat_id' => $_SERVER['RECIPIENT_CHAT_ID'], ...\PhpTelegramBot\Core\Entities\InputFile::attachFile('photo', __DIR__.'/files/example_photo.jpeg'), ]); + // or + + // $bot->sendPhoto([ + // 'chat_id' => $_SERVER['RECIPIENT_CHAT_ID'], + // ] + \PhpTelegramBot\Core\Entities\InputFile::attachFile('photo', __DIR__.'/files/example_photo.jpeg')); + echo 'Photo sent.'; + } catch (\PhpTelegramBot\Core\Exceptions\TelegramException $e) { + echo 'ERROR: '.$e->getMessage(); + } From b50e6435f745bd49e70826c1c0738f748ad5d380 Mon Sep 17 00:00:00 2001 From: Tii Date: Fri, 14 Jun 2024 09:54:05 +0200 Subject: [PATCH 10/20] Removed optionals in array shape --- src/ApiMethods/SendsMessages.php | 54 ++++++++++++++++---------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/src/ApiMethods/SendsMessages.php b/src/ApiMethods/SendsMessages.php index 46ec415c6..25de98e9f 100644 --- a/src/ApiMethods/SendsMessages.php +++ b/src/ApiMethods/SendsMessages.php @@ -40,10 +40,10 @@ trait SendsMessages { /** * @param array{ - * offset: ?int, - * limit: ?int, - * timeout: ?int, - * allowed_updates: ?array + * offset: int, + * limit: int, + * timeout: int, + * allowed_updates: array * } $data * @return array * @@ -57,12 +57,12 @@ public function getUpdates(array $data = []): array /** * @param array{ * url: string, - * certificate: ?InputFile, - * ip_address: ?string, - * max_connections: ?int, - * allowed_updates: ?array, - * drop_pending_updates: ?bool, - * secret_token: ?string + * certificate: InputFile, + * ip_address: string, + * max_connections: int, + * allowed_updates: array, + * drop_pending_updates: bool, + * secret_token: string * } $data * * @throws Exceptions\TelegramException @@ -74,7 +74,7 @@ public function setWebhook(array $data = []): bool /** * @param array{ - * drop_pending_updates: ?bool + * drop_pending_updates: bool * } $data * * @throws Exceptions\TelegramException @@ -115,18 +115,18 @@ public function close(array $data = []): bool /** * @param array{ - * business_connection_id: ?string, + * business_connection_id: string, * chat_id: int|string, - * message_thread_id: ?int, + * message_thread_id: int, * text: string, - * parse_mode: ?string, - * entities: MessageEntity[]|null, - * link_preview_options: ?LinkPreviewOptions, - * disable_notification: ?bool, - * protect_content: ?bool, - * message_effect_id: ?string, - * reply_parameters: ?ReplyParameters, - * reply_markup: InlineKeyboardMarkup|ReplyKeyboardMarkup|ReplyKeyboardRemove|ForceReply|null, + * parse_mode: string, + * entities: MessageEntity[], + * link_preview_options: LinkPreviewOptions, + * disable_notification: bool, + * protect_content: bool, + * message_effect_id: string, + * reply_parameters: ReplyParameters, + * reply_markup: InlineKeyboardMarkup|ReplyKeyboardMarkup|ReplyKeyboardRemove|ForceReply * } $data * * @throws Exceptions\TelegramException @@ -139,10 +139,10 @@ public function sendMessage(array $data = []): Message /** * @param array{ * chat_id: int|string, - * message_thread_id: ?int, + * message_thread_id: int, * from_chat_id: int|string, - * disable_notification: ?bool, - * protect_content: ?bool, + * disable_notification: bool, + * protect_content: bool, * message_id: int, * } $data * @@ -156,11 +156,11 @@ public function forwardMessage(array $data = []): Message /** * @param array{ * chat_id: int|string, - * message_thread_id: ?int, + * message_thread_id: int, * from_chat_id: int|string, * message_ids: int[], - * disable_notification: ?bool, - * protect_content: ?bool, + * disable_notification: bool, + * protect_content: bool, * } $data * @return MessageId[] * From 8395bcc136a4a789a58ab34c1d5a4a91f0ff820e Mon Sep 17 00:00:00 2001 From: Tii Date: Mon, 17 Jun 2024 17:36:41 +0200 Subject: [PATCH 11/20] Fixed preset data --- .../BotCommandScopeAllChatAdministrators.php | 7 +++++++ .../BotCommandScope/BotCommandScopeAllGroupChats.php | 7 ++++++- .../BotCommandScope/BotCommandScopeAllPrivateChats.php | 7 ++++++- src/Entities/BotCommandScope/BotCommandScopeChat.php | 7 ++++++- .../BotCommandScopeChatAdministrators.php | 7 ++++++- .../BotCommandScope/BotCommandScopeChatMember.php | 7 ++++++- .../BotCommandScope/BotCommandScopeDefault.php | 7 ++++++- src/Entities/InputMedia/InputMediaAnimation.php | 7 +++++++ src/Entities/InputMedia/InputMediaAudio.php | 7 +++++++ src/Entities/InputMedia/InputMediaDocument.php | 7 +++++++ src/Entities/InputMedia/InputMediaPhoto.php | 7 +++++++ src/Entities/InputMedia/InputMediaVideo.php | 10 ++++++++-- src/Entities/MenuButton/MenuButtonCommands.php | 7 ++++++- src/Entities/MenuButton/MenuButtonDefault.php | 7 ++++++- src/Entities/MenuButton/MenuButtonWebApp.php | 7 +++++++ 15 files changed, 98 insertions(+), 10 deletions(-) diff --git a/src/Entities/BotCommandScope/BotCommandScopeAllChatAdministrators.php b/src/Entities/BotCommandScope/BotCommandScopeAllChatAdministrators.php index 2338b2d89..4277179a0 100644 --- a/src/Entities/BotCommandScope/BotCommandScopeAllChatAdministrators.php +++ b/src/Entities/BotCommandScope/BotCommandScopeAllChatAdministrators.php @@ -5,4 +5,11 @@ class BotCommandScopeAllChatAdministrators extends BotCommandScope { // + + protected static function presetData(): array + { + return [ + 'type' => self::TYPE_ALL_CHAT_ADMINISTRATORS, + ]; + } } diff --git a/src/Entities/BotCommandScope/BotCommandScopeAllGroupChats.php b/src/Entities/BotCommandScope/BotCommandScopeAllGroupChats.php index ceb57ff33..3dfffb5a6 100644 --- a/src/Entities/BotCommandScope/BotCommandScopeAllGroupChats.php +++ b/src/Entities/BotCommandScope/BotCommandScopeAllGroupChats.php @@ -4,5 +4,10 @@ class BotCommandScopeAllGroupChats extends BotCommandScope { - // + protected static function presetData(): array + { + return [ + 'type' => self::TYPE_ALL_GROUP_CHATS, + ]; + } } diff --git a/src/Entities/BotCommandScope/BotCommandScopeAllPrivateChats.php b/src/Entities/BotCommandScope/BotCommandScopeAllPrivateChats.php index d6a9b2f59..08f62201a 100644 --- a/src/Entities/BotCommandScope/BotCommandScopeAllPrivateChats.php +++ b/src/Entities/BotCommandScope/BotCommandScopeAllPrivateChats.php @@ -4,5 +4,10 @@ class BotCommandScopeAllPrivateChats extends BotCommandScope { - // + protected static function presetData(): array + { + return [ + 'type' => self::TYPE_ALL_PRIVATE_CHATS, + ]; + } } diff --git a/src/Entities/BotCommandScope/BotCommandScopeChat.php b/src/Entities/BotCommandScope/BotCommandScopeChat.php index a86f58f4a..22357cef8 100644 --- a/src/Entities/BotCommandScope/BotCommandScopeChat.php +++ b/src/Entities/BotCommandScope/BotCommandScopeChat.php @@ -7,5 +7,10 @@ */ class BotCommandScopeChat extends BotCommandScope { - // + protected static function presetData(): array + { + return [ + 'type' => self::TYPE_CHAT, + ]; + } } diff --git a/src/Entities/BotCommandScope/BotCommandScopeChatAdministrators.php b/src/Entities/BotCommandScope/BotCommandScopeChatAdministrators.php index 9bd0ec65e..3b8d626d3 100644 --- a/src/Entities/BotCommandScope/BotCommandScopeChatAdministrators.php +++ b/src/Entities/BotCommandScope/BotCommandScopeChatAdministrators.php @@ -7,5 +7,10 @@ */ class BotCommandScopeChatAdministrators extends BotCommandScope { - // + protected static function presetData(): array + { + return [ + 'type' => self::TYPE_CHAT_ADMINISTRATORS, + ]; + } } diff --git a/src/Entities/BotCommandScope/BotCommandScopeChatMember.php b/src/Entities/BotCommandScope/BotCommandScopeChatMember.php index 81db5ec69..ab7c9038f 100644 --- a/src/Entities/BotCommandScope/BotCommandScopeChatMember.php +++ b/src/Entities/BotCommandScope/BotCommandScopeChatMember.php @@ -8,5 +8,10 @@ */ class BotCommandScopeChatMember extends BotCommandScope { - // + protected static function presetData(): array + { + return [ + 'type' => self::TYPE_CHAT_MEMBER, + ]; + } } diff --git a/src/Entities/BotCommandScope/BotCommandScopeDefault.php b/src/Entities/BotCommandScope/BotCommandScopeDefault.php index bede4fdb4..680ba3c25 100644 --- a/src/Entities/BotCommandScope/BotCommandScopeDefault.php +++ b/src/Entities/BotCommandScope/BotCommandScopeDefault.php @@ -4,5 +4,10 @@ class BotCommandScopeDefault extends BotCommandScope { - // + protected static function presetData(): array + { + return [ + 'type' => self::TYPE_DEFAULT, + ]; + } } diff --git a/src/Entities/InputMedia/InputMediaAnimation.php b/src/Entities/InputMedia/InputMediaAnimation.php index c47890b81..c475cf944 100644 --- a/src/Entities/InputMedia/InputMediaAnimation.php +++ b/src/Entities/InputMedia/InputMediaAnimation.php @@ -27,6 +27,13 @@ protected static function subEntities(): array ]; } + protected static function presetData(): array + { + return [ + 'type' => self::TYPE_ANIMATION, + ]; + } + public static function fieldsBypassingGet(): array { return [ diff --git a/src/Entities/InputMedia/InputMediaAudio.php b/src/Entities/InputMedia/InputMediaAudio.php index cd3eb8eda..37e04449f 100644 --- a/src/Entities/InputMedia/InputMediaAudio.php +++ b/src/Entities/InputMedia/InputMediaAudio.php @@ -23,4 +23,11 @@ protected static function subEntities(): array 'caption_entities' => [MessageEntity::class], ]; } + + protected static function presetData(): array + { + return [ + 'type' => self::TYPE_AUDIO, + ]; + } } diff --git a/src/Entities/InputMedia/InputMediaDocument.php b/src/Entities/InputMedia/InputMediaDocument.php index 89c614fa9..e3dff1ff7 100644 --- a/src/Entities/InputMedia/InputMediaDocument.php +++ b/src/Entities/InputMedia/InputMediaDocument.php @@ -21,4 +21,11 @@ protected static function subEntities(): array 'caption_entites' => [MessageEntity::class], ]; } + + protected static function presetData(): array + { + return [ + 'type' => self::TYPE_DOCUMENT, + ]; + } } diff --git a/src/Entities/InputMedia/InputMediaPhoto.php b/src/Entities/InputMedia/InputMediaPhoto.php index bbe60ac9c..4d13ae097 100644 --- a/src/Entities/InputMedia/InputMediaPhoto.php +++ b/src/Entities/InputMedia/InputMediaPhoto.php @@ -22,6 +22,13 @@ protected static function subEntities(): array ]; } + protected static function presetData(): array + { + return [ + 'type' => self::TYPE_PHOTO, + ]; + } + public static function fieldsBypassingGet(): array { return [ diff --git a/src/Entities/InputMedia/InputMediaVideo.php b/src/Entities/InputMedia/InputMediaVideo.php index f92acd445..70bbd1735 100644 --- a/src/Entities/InputMedia/InputMediaVideo.php +++ b/src/Entities/InputMedia/InputMediaVideo.php @@ -3,7 +3,6 @@ namespace PhpTelegramBot\Core\Entities\InputMedia; use PhpTelegramBot\Core\Contracts\AllowsBypassingGet; -use PhpTelegramBot\Core\Entities\Entity; use PhpTelegramBot\Core\Entities\InputFile; use PhpTelegramBot\Core\Entities\MessageEntity; @@ -20,7 +19,7 @@ * @method bool|null getSupportsStreaming() Optional. Pass True if the uploaded video is suitable for streaming * @method bool hasSpoiler() Optional. Pass True if the video needs to be covered with a spoiler animation */ -class InputMediaVideo extends Entity implements AllowsBypassingGet +class InputMediaVideo extends InputMedia implements AllowsBypassingGet { protected static function subEntities(): array { @@ -29,6 +28,13 @@ protected static function subEntities(): array ]; } + protected static function presetData(): array + { + return [ + 'type' => self::TYPE_VIDEO, + ]; + } + public static function fieldsBypassingGet(): array { return [ diff --git a/src/Entities/MenuButton/MenuButtonCommands.php b/src/Entities/MenuButton/MenuButtonCommands.php index d0394fba2..21dee21a1 100644 --- a/src/Entities/MenuButton/MenuButtonCommands.php +++ b/src/Entities/MenuButton/MenuButtonCommands.php @@ -4,5 +4,10 @@ class MenuButtonCommands extends MenuButton { - // + protected static function presetData(): array + { + return [ + 'type' => self::TYPE_COMMANDS, + ]; + } } diff --git a/src/Entities/MenuButton/MenuButtonDefault.php b/src/Entities/MenuButton/MenuButtonDefault.php index 124cf56be..7ce86c2e4 100644 --- a/src/Entities/MenuButton/MenuButtonDefault.php +++ b/src/Entities/MenuButton/MenuButtonDefault.php @@ -4,5 +4,10 @@ class MenuButtonDefault extends MenuButton { - // + protected static function presetData(): array + { + return [ + 'type' => self::TYPE_DEFAULT, + ]; + } } diff --git a/src/Entities/MenuButton/MenuButtonWebApp.php b/src/Entities/MenuButton/MenuButtonWebApp.php index 31c3cf5a6..45b025cd6 100644 --- a/src/Entities/MenuButton/MenuButtonWebApp.php +++ b/src/Entities/MenuButton/MenuButtonWebApp.php @@ -16,4 +16,11 @@ protected static function subEntities(): array 'web_app' => WebAppInfo::class, ]; } + + protected static function presetData(): array + { + return [ + 'type' => self::TYPE_WEB_APP, + ]; + } } From 3965ff63c3bafc7e25cba756dc2ed1c6a488c1a2 Mon Sep 17 00:00:00 2001 From: Tii Date: Tue, 18 Jun 2024 11:42:02 +0000 Subject: [PATCH 12/20] Bot API 7.5 Implementation --- src/ApiMethods/SendsInvoices.php | 14 ++++++++++ src/ApiMethods/UpdatesMessages.php | 7 +++++ .../RevenueWithdrawalState.php | 27 +++++++++++++++++++ .../RevenueWithdrawalStateFailed.php | 8 ++++++ .../RevenueWithdrawalStatePending.php | 8 ++++++ .../RevenueWithdrawalStateSucceeded.php | 12 +++++++++ src/Entities/StarTransaction.php | 23 ++++++++++++++++ src/Entities/StarTransactions.php | 13 +++++++++ .../TransactionPartner/TransactionPartner.php | 27 +++++++++++++++++++ .../TransactionPartnerFragment.php | 18 +++++++++++++ .../TransactionPartnerOther.php | 8 ++++++ .../TransactionPartnerUser.php | 18 +++++++++++++ 12 files changed, 183 insertions(+) create mode 100644 src/Entities/RevenueWithdrawalState/RevenueWithdrawalState.php create mode 100644 src/Entities/RevenueWithdrawalState/RevenueWithdrawalStateFailed.php create mode 100644 src/Entities/RevenueWithdrawalState/RevenueWithdrawalStatePending.php create mode 100644 src/Entities/RevenueWithdrawalState/RevenueWithdrawalStateSucceeded.php create mode 100644 src/Entities/StarTransaction.php create mode 100644 src/Entities/StarTransactions.php create mode 100644 src/Entities/TransactionPartner/TransactionPartner.php create mode 100644 src/Entities/TransactionPartner/TransactionPartnerFragment.php create mode 100644 src/Entities/TransactionPartner/TransactionPartnerOther.php create mode 100644 src/Entities/TransactionPartner/TransactionPartnerUser.php diff --git a/src/ApiMethods/SendsInvoices.php b/src/ApiMethods/SendsInvoices.php index 07f0568d7..4f2045c2c 100644 --- a/src/ApiMethods/SendsInvoices.php +++ b/src/ApiMethods/SendsInvoices.php @@ -7,6 +7,7 @@ use PhpTelegramBot\Core\Entities\Message; use PhpTelegramBot\Core\Entities\ReplyParameters; use PhpTelegramBot\Core\Entities\ShippingOption; +use PhpTelegramBot\Core\Entities\StarTransactions; trait SendsInvoices { @@ -109,6 +110,19 @@ public function answerPreCheckoutQuery(array $data = []): bool return $this->send(__FUNCTION__, $data, null); } + /** + * @param array{ + * offset: int, + * limit: int, + * } $data + * + * @throws \PhpTelegramBot\Core\Exceptions\TelegramException + */ + public function getStarTransactions(array $data = []): StarTransactions + { + return $this->send(__FUNCTION__, $data, StarTransactions::class); + } + /** * @param array{ * user_id: int, diff --git a/src/ApiMethods/UpdatesMessages.php b/src/ApiMethods/UpdatesMessages.php index ef1f8fe83..771b3439c 100644 --- a/src/ApiMethods/UpdatesMessages.php +++ b/src/ApiMethods/UpdatesMessages.php @@ -14,6 +14,7 @@ trait UpdatesMessages { /** * @param array{ + * business_connection_id: string, * chat_id: int|string, * message_id: int, * inline_message_id: string, @@ -33,6 +34,7 @@ public function editMessageText(array $data = []): Message|bool /** * @param array{ + * business_connection_id: string, * chat_id: int|string, * message_id: int, * inline_message_id: string, @@ -52,6 +54,7 @@ public function editMessageCaption(array $data = []): Message|bool /** * @param array{ + * business_connection_id: string, * chat_id: int|string, * message_id: int, * inline_message_id: string, @@ -68,6 +71,7 @@ public function editMessageMedia(array $data = []): Message|bool /** * @param array{ + * business_connection_id: string, * chat_id: int|string, * message_id: int, * inline_message_id: string, @@ -89,6 +93,7 @@ public function editMessageLiveLocation(array $data = []): Message|bool /** * @param array{ + * business_connection_id: string, * chat_id: int|string, * message_id: int, * inline_message_id: string, @@ -104,6 +109,7 @@ public function stopMessageLiveLocation(array $data = []): Message|bool /** * @param array{ + * businss_connection_id: string, * chat_id: int|string, * message_id: int, * inline_message_id: string, @@ -119,6 +125,7 @@ public function editMessageReplyMarkup(array $data = []): Message|bool /** * @param array{ + * business_connection_id: string, * chat_id: int|string, * message_id: int, * reply_markup: InlineKeyboardMarkup, diff --git a/src/Entities/RevenueWithdrawalState/RevenueWithdrawalState.php b/src/Entities/RevenueWithdrawalState/RevenueWithdrawalState.php new file mode 100644 index 000000000..826ac78a7 --- /dev/null +++ b/src/Entities/RevenueWithdrawalState/RevenueWithdrawalState.php @@ -0,0 +1,27 @@ + new RevenueWithdrawalStatePending($data), + self::TYPE_SUCCEEDED => new RevenueWithdrawalStateSucceeded($data), + self::TYPE_FAILED => new RevenueWithdrawalStateFailed($data), + }; + } +} diff --git a/src/Entities/RevenueWithdrawalState/RevenueWithdrawalStateFailed.php b/src/Entities/RevenueWithdrawalState/RevenueWithdrawalStateFailed.php new file mode 100644 index 000000000..f19e40017 --- /dev/null +++ b/src/Entities/RevenueWithdrawalState/RevenueWithdrawalStateFailed.php @@ -0,0 +1,8 @@ + TransactionPartner::class, + 'receiver' => TransactionPartner::class, + ]; + } +} diff --git a/src/Entities/StarTransactions.php b/src/Entities/StarTransactions.php new file mode 100644 index 000000000..bd2e4bf19 --- /dev/null +++ b/src/Entities/StarTransactions.php @@ -0,0 +1,13 @@ + [StarTransaction::class], + ]; + } +} diff --git a/src/Entities/TransactionPartner/TransactionPartner.php b/src/Entities/TransactionPartner/TransactionPartner.php new file mode 100644 index 000000000..ebde6bdf2 --- /dev/null +++ b/src/Entities/TransactionPartner/TransactionPartner.php @@ -0,0 +1,27 @@ + new TransactionPartnerFragment($data), + self::TYPE_USER => new TransactionPartnerUser($data), + self::TYPE_OTHER => new TransactionPartnerOther($data), + }; + } +} diff --git a/src/Entities/TransactionPartner/TransactionPartnerFragment.php b/src/Entities/TransactionPartner/TransactionPartnerFragment.php new file mode 100644 index 000000000..c63b2694c --- /dev/null +++ b/src/Entities/TransactionPartner/TransactionPartnerFragment.php @@ -0,0 +1,18 @@ + RevenueWithdrawalState::class, + ]; + } +} diff --git a/src/Entities/TransactionPartner/TransactionPartnerOther.php b/src/Entities/TransactionPartner/TransactionPartnerOther.php new file mode 100644 index 000000000..51e2a5ad6 --- /dev/null +++ b/src/Entities/TransactionPartner/TransactionPartnerOther.php @@ -0,0 +1,8 @@ + User::class, + ]; + } +} From e4294d0f23d77f1b95a4dcd6e04957408855b7b5 Mon Sep 17 00:00:00 2001 From: Tii Date: Tue, 18 Jun 2024 15:44:50 +0200 Subject: [PATCH 13/20] Refactored file uploads... --- composer.json | 5 ++- examples/sendPhoto.php | 32 +++++++++++------ src/HttpClient.php | 28 ++++++++++----- src/Telegram.php | 78 +++++++++++++++++++++++++++++++++++++----- src/helpers.php | 10 ++++++ 5 files changed, 124 insertions(+), 29 deletions(-) create mode 100644 src/helpers.php diff --git a/composer.json b/composer.json index e48a16fb4..7c7126202 100644 --- a/composer.json +++ b/composer.json @@ -55,7 +55,10 @@ "autoload": { "psr-4": { "PhpTelegramBot\\Core\\": "src" - } + }, + "files": [ + "src/helpers.php" + ] }, "config": { "optimize-autoloader": true, diff --git a/examples/sendPhoto.php b/examples/sendPhoto.php index bde87a905..b708f4f86 100644 --- a/examples/sendPhoto.php +++ b/examples/sendPhoto.php @@ -3,21 +3,33 @@ require 'bootstrap.php'; $bot = new \PhpTelegramBot\Core\Telegram($_SERVER['BOT_TOKEN']); +$recipient = $_SERVER['RECIPIENT_CHAT_ID']; +$filepath = __DIR__.'/files/example_photo.jpeg'; try { - $bot->sendPhoto([ - 'chat_id' => $_SERVER['RECIPIENT_CHAT_ID'], - ...\PhpTelegramBot\Core\Entities\InputFile::attachFile('photo', __DIR__.'/files/example_photo.jpeg'), + $bot->sendMediaGroup([ + 'chat_id' => $recipient, + 'media' => [ + [ + 'type' => 'photo', + 'media' => $filepath, + 'caption' => 'Photo #1', + ], + [ + 'type' => 'photo', + 'media' => fopen($filepath, 'r'), + 'caption' => 'Photo #2', + ], + [ + 'type' => 'photo', + 'media' => new \GuzzleHttp\Psr7\Stream(fopen($filepath, 'r')), + 'caption' => 'Photo #3', + ], + ], ]); - // or - - // $bot->sendPhoto([ - // 'chat_id' => $_SERVER['RECIPIENT_CHAT_ID'], - // ] + \PhpTelegramBot\Core\Entities\InputFile::attachFile('photo', __DIR__.'/files/example_photo.jpeg')); - - echo 'Photo sent.'; + echo 'Photos sent.'; } catch (\PhpTelegramBot\Core\Exceptions\TelegramException $e) { diff --git a/src/HttpClient.php b/src/HttpClient.php index 5840e210f..90b777524 100644 --- a/src/HttpClient.php +++ b/src/HttpClient.php @@ -9,6 +9,7 @@ use Psr\Http\Message\RequestFactoryInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\StreamFactoryInterface; +use Psr\Http\Message\StreamInterface; class HttpClient { @@ -42,26 +43,30 @@ public function postJson(string $uri, array $data): ResponseInterface ); } - public function postMultipart(string $uri, array $data): ResponseInterface + /** + * @param array $streams + * + * @throws \Psr\Http\Client\ClientExceptionInterface + */ + public function postMultipart(string $uri, array $data, array $streams): ResponseInterface { - $builder = new MultipartStreamBuilder( - Psr17FactoryDiscovery::findStreamFactory() - ); + $builder = new MultipartStreamBuilder(); + // Add data foreach ($data as $key => $value) { - if (str_starts_with($key, '__file_')) { - $key = substr($key, 7); - } - $value = match (true) { is_array($value) => json_encode($value), - is_resource($value) => $value, default => (string) $value, }; $builder->addResource($key, $value); } + // Add file streams + foreach ($streams as $fileId => $stream) { + $builder->addResource($fileId, $stream); + } + $boundary = $builder->getBoundary(); return $this->client->sendRequest( @@ -70,4 +75,9 @@ public function postMultipart(string $uri, array $data): ResponseInterface ->withBody($builder->build()) ); } + + public function streamFactory(): StreamFactoryInterface + { + return $this->streamFactory; + } } diff --git a/src/Telegram.php b/src/Telegram.php index e571b4f5b..387587cd3 100644 --- a/src/Telegram.php +++ b/src/Telegram.php @@ -10,6 +10,9 @@ use PhpTelegramBot\Core\Entities\Factory; use PhpTelegramBot\Core\Entities\Update; use PhpTelegramBot\Core\Exceptions\TelegramException; +use Psr\Http\Message\StreamInterface; + +use function PhpTelegramBot\Core\Helpers\array_is_assoc; class Telegram { @@ -21,13 +24,36 @@ class Telegram protected string $apiBaseUri = 'https://api.telegram.org'; + public static function inputFileFields(): array + { + return [ + 'addStickerToSet' => ['sticker' => ['sticker']], + 'createNewStickerSet' => ['stickers' => ['sticker']], + 'editMessageMedia' => ['media' => ['media']], + 'replaceStickerInSet' => ['sticker' => ['sticker']], + 'sendAnimation' => ['animation', 'thumbnail'], + 'sendAudio' => ['audio', 'thumbnail'], + 'sendDocument' => ['document', 'thumbnail'], + 'sendMediaGroup' => ['media' => ['media', 'thumbnail']], + 'sendPhoto' => ['photo'], + 'sendSticker' => ['sticker'], + 'sendVideo' => ['video', 'thumbnail'], + 'sendVideoNote' => ['video_note', 'thumbnail'], + 'sendVoice' => ['voice'], + 'setChatPhoto' => ['photo'], + 'setStickerSetThumbnail' => ['thumbnail'], + 'setWebhook' => ['certificate'], + 'uploadStickerFile' => ['sticker'], + ]; + } + public function __construct( #[\SensitiveParameter] protected string $botToken, protected ?string $botUsername = null, protected ?HttpClient $client = null, ) { - $this->client = new HttpClient(); + $this->client ??= new HttpClient(); } public function __call(string $methodName, array $arguments): mixed @@ -35,25 +61,59 @@ public function __call(string $methodName, array $arguments): mixed return $this->send($methodName, $arguments[0] ?? null, $arguments[1] ?? null); } - protected function dataContainsFiles(array $data): bool + /** + * @return StreamInterface[] + */ + public function extractFiles(array $fields, array &$data): array { - foreach ($data as $key => $value) { - if (str_starts_with($key, '__file_')) { - return true; + $streams = []; + foreach ($fields as $key => $value) { + + if (is_string($value)) { + if (! isset($data[$value])) { + continue; + } + + $file = $data[$value]; + + if (is_string($file) && is_file($file) || is_resource($file) || $file instanceof StreamInterface) { + $fileId = uniqid($value.'_'); + $data[$value] = 'attach://'.$fileId; + + $streams[$fileId] = match (true) { + is_string($file) && is_file($file) => $this->client->streamFactory()->createStreamFromFile($file), + is_resource($file) => $this->client->streamFactory()->createStreamFromResource($file), + $file instanceof StreamInterface => $file, + }; + } + + } elseif (! array_is_assoc($data[$key])) { + + foreach ($data[$key] as &$item) { + $streams += $this->extractFiles($value, $item); + } + + } else { + + $streams += $this->extractFiles($value, $data[$key]); + } + } - return false; + return $streams; } protected function send(string $methodName, ?array $data = null, string|array|null $returnType = null): mixed { $requestUri = $this->apiBaseUri.'/bot'.$this->botToken.'/'.$methodName; + $streams = $this->extractFiles(self::inputFileFields()[$methodName] ?? null, $data); + $response = match (true) { - empty($data) => $this->client->get($requestUri), - $this->dataContainsFiles($data) => $this->client->postMultipart($requestUri, $data), - default => $this->client->postJson($requestUri, $data), + empty($data) => $this->client->get($requestUri), + count($streams) > 0 => $this->client->postMultipart($requestUri, $data, $streams), + default => $this->client->postJson($requestUri, $data), }; $result = json_decode($response->getBody()->getContents(), true); diff --git a/src/helpers.php b/src/helpers.php new file mode 100644 index 000000000..0c1d793f2 --- /dev/null +++ b/src/helpers.php @@ -0,0 +1,10 @@ + Date: Tue, 18 Jun 2024 15:45:54 +0200 Subject: [PATCH 14/20] Add pint config for spaces around dot notation --- examples/bootstrap.php | 2 +- examples/sendPhoto.php | 4 ++-- pint.json | 3 +++ src/Entities/Entity.php | 2 +- src/Entities/InputFile.php | 6 +++--- src/Telegram.php | 6 +++--- 6 files changed, 13 insertions(+), 10 deletions(-) diff --git a/examples/bootstrap.php b/examples/bootstrap.php index d4b0a0a2c..238434140 100644 --- a/examples/bootstrap.php +++ b/examples/bootstrap.php @@ -2,6 +2,6 @@ use Dotenv\Dotenv; -require __DIR__.'/../vendor/autoload.php'; +require __DIR__ . '/../vendor/autoload.php'; Dotenv::createImmutable(__DIR__)->safeLoad(); diff --git a/examples/sendPhoto.php b/examples/sendPhoto.php index b708f4f86..1465f1328 100644 --- a/examples/sendPhoto.php +++ b/examples/sendPhoto.php @@ -4,7 +4,7 @@ $bot = new \PhpTelegramBot\Core\Telegram($_SERVER['BOT_TOKEN']); $recipient = $_SERVER['RECIPIENT_CHAT_ID']; -$filepath = __DIR__.'/files/example_photo.jpeg'; +$filepath = __DIR__ . '/files/example_photo.jpeg'; try { @@ -33,6 +33,6 @@ } catch (\PhpTelegramBot\Core\Exceptions\TelegramException $e) { - echo 'ERROR: '.$e->getMessage(); + echo 'ERROR: ' . $e->getMessage(); } diff --git a/pint.json b/pint.json index 97bbe7852..e5818b4e4 100644 --- a/pint.json +++ b/pint.json @@ -6,6 +6,9 @@ "operators": { "=>": "align" } + }, + "concat_space": { + "spacing": "one" } } } \ No newline at end of file diff --git a/src/Entities/Entity.php b/src/Entities/Entity.php index ae53e73c5..e4006890b 100644 --- a/src/Entities/Entity.php +++ b/src/Entities/Entity.php @@ -58,7 +58,7 @@ public function __call(string $name, array $arguments) } } - $method = get_class($this).'::'.$name.'()'; + $method = get_class($this) . '::' . $name . '()'; throw new BadMethodCallException("Call to undefined method $method"); } diff --git a/src/Entities/InputFile.php b/src/Entities/InputFile.php index 6e6dbc5ca..aad2a1bd5 100644 --- a/src/Entities/InputFile.php +++ b/src/Entities/InputFile.php @@ -8,15 +8,15 @@ class InputFile { public static function attachFile(string $field, string $filepath): array { - $file_id = uniqid($field.'_'); + $file_id = uniqid($field . '_'); if (! is_file($filepath)) { throw new InvalidArgumentException("Cannot attach file to '$field'. $filepath must be a valid filepath."); } return [ - $field => 'attach://'.$file_id, - '__file_'.$file_id => fopen($filepath, 'r'), + $field => 'attach://' . $file_id, + '__file_' . $file_id => fopen($filepath, 'r'), ]; } } diff --git a/src/Telegram.php b/src/Telegram.php index 387587cd3..e7e645a47 100644 --- a/src/Telegram.php +++ b/src/Telegram.php @@ -77,8 +77,8 @@ public function extractFiles(array $fields, array &$data): array $file = $data[$value]; if (is_string($file) && is_file($file) || is_resource($file) || $file instanceof StreamInterface) { - $fileId = uniqid($value.'_'); - $data[$value] = 'attach://'.$fileId; + $fileId = uniqid($value . '_'); + $data[$value] = 'attach://' . $fileId; $streams[$fileId] = match (true) { is_string($file) && is_file($file) => $this->client->streamFactory()->createStreamFromFile($file), @@ -106,7 +106,7 @@ public function extractFiles(array $fields, array &$data): array protected function send(string $methodName, ?array $data = null, string|array|null $returnType = null): mixed { - $requestUri = $this->apiBaseUri.'/bot'.$this->botToken.'/'.$methodName; + $requestUri = $this->apiBaseUri . '/bot' . $this->botToken . '/' . $methodName; $streams = $this->extractFiles(self::inputFileFields()[$methodName] ?? null, $data); From 720a4de0a55d0cc755da8b914db9c7c9c358fad8 Mon Sep 17 00:00:00 2001 From: Tii Date: Tue, 18 Jun 2024 16:00:51 +0200 Subject: [PATCH 15/20] Cleanup --- src/HttpClient.php | 2 +- src/Telegram.php | 20 +++++++++++++++----- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/HttpClient.php b/src/HttpClient.php index 90b777524..78acda05b 100644 --- a/src/HttpClient.php +++ b/src/HttpClient.php @@ -50,7 +50,7 @@ public function postJson(string $uri, array $data): ResponseInterface */ public function postMultipart(string $uri, array $data, array $streams): ResponseInterface { - $builder = new MultipartStreamBuilder(); + $builder = new MultipartStreamBuilder($this->streamFactory); // Add data foreach ($data as $key => $value) { diff --git a/src/Telegram.php b/src/Telegram.php index e7e645a47..4a866a514 100644 --- a/src/Telegram.php +++ b/src/Telegram.php @@ -27,14 +27,24 @@ class Telegram public static function inputFileFields(): array { return [ - 'addStickerToSet' => ['sticker' => ['sticker']], - 'createNewStickerSet' => ['stickers' => ['sticker']], - 'editMessageMedia' => ['media' => ['media']], - 'replaceStickerInSet' => ['sticker' => ['sticker']], + 'addStickerToSet' => [ + 'sticker' => ['sticker'] + ], + 'createNewStickerSet' => [ + 'stickers' => ['sticker'] + ], + 'editMessageMedia' => [ + 'media' => ['media'] + ], + 'replaceStickerInSet' => [ + 'sticker' => ['sticker'] + ], 'sendAnimation' => ['animation', 'thumbnail'], 'sendAudio' => ['audio', 'thumbnail'], 'sendDocument' => ['document', 'thumbnail'], - 'sendMediaGroup' => ['media' => ['media', 'thumbnail']], + 'sendMediaGroup' => [ + 'media' => ['media', 'thumbnail'] + ], 'sendPhoto' => ['photo'], 'sendSticker' => ['sticker'], 'sendVideo' => ['video', 'thumbnail'], From 03ddf31afa28dd27ec1d3ea75f38662c78ab64e6 Mon Sep 17 00:00:00 2001 From: Tii Date: Tue, 18 Jun 2024 18:15:10 +0000 Subject: [PATCH 16/20] Make method protected --- src/Telegram.php | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/Telegram.php b/src/Telegram.php index 4a866a514..b91be5387 100644 --- a/src/Telegram.php +++ b/src/Telegram.php @@ -74,13 +74,13 @@ public function __call(string $methodName, array $arguments): mixed /** * @return StreamInterface[] */ - public function extractFiles(array $fields, array &$data): array + protected function extractFiles(array $fields, array &$data): array { $streams = []; foreach ($fields as $key => $value) { if (is_string($value)) { - if (! isset($data[$value])) { + if (!isset($data[$value])) { continue; } @@ -96,19 +96,15 @@ public function extractFiles(array $fields, array &$data): array $file instanceof StreamInterface => $file, }; } - - } elseif (! array_is_assoc($data[$key])) { + } elseif (!array_is_assoc($data[$key])) { foreach ($data[$key] as &$item) { $streams += $this->extractFiles($value, $item); } - } else { $streams += $this->extractFiles($value, $data[$key]); - } - } return $streams; @@ -150,7 +146,7 @@ protected function send(string $methodName, ?array $data = null, string|array|nu protected function makeResultObject(mixed $result, string|array|null $returnType = null): mixed { - if (! is_array($result)) { + if (!is_array($result)) { return $result; } From dae977b8b0d1d167fa895fcf1b0ad93d04d41762 Mon Sep 17 00:00:00 2001 From: Tii Date: Wed, 19 Jun 2024 13:15:57 +0200 Subject: [PATCH 17/20] Move Factory to Contracts --- src/{Entities => Contracts}/Factory.php | 2 +- src/Entities/BackgroundFill/BackgroundFill.php | 2 +- src/Entities/BackgroundType/BackgroundType.php | 2 +- .../BotCommandScope/BotCommandScope.php | 2 +- .../ChatBoostSource/ChatBoostSource.php | 2 +- src/Entities/ChatMember/ChatMember.php | 2 +- src/Entities/InputMedia/InputMedia.php | 2 +- src/Entities/MaybeInaccessibleMessage.php | 2 ++ src/Entities/MenuButton/MenuButton.php | 2 +- src/Entities/MessageOrigin/MessageOrigin.php | 2 +- src/Entities/ReactionType/ReactionType.php | 2 +- .../RevenueWithdrawalState.php | 2 +- .../TransactionPartner/TransactionPartner.php | 2 +- src/Telegram.php | 18 +++++++++--------- 14 files changed, 23 insertions(+), 21 deletions(-) rename src/{Entities => Contracts}/Factory.php (67%) diff --git a/src/Entities/Factory.php b/src/Contracts/Factory.php similarity index 67% rename from src/Entities/Factory.php rename to src/Contracts/Factory.php index 9888096e7..b673dc34c 100644 --- a/src/Entities/Factory.php +++ b/src/Contracts/Factory.php @@ -1,6 +1,6 @@ [ - 'sticker' => ['sticker'] + 'sticker' => ['sticker'], ], 'createNewStickerSet' => [ - 'stickers' => ['sticker'] + 'stickers' => ['sticker'], ], 'editMessageMedia' => [ - 'media' => ['media'] + 'media' => ['media'], ], 'replaceStickerInSet' => [ - 'sticker' => ['sticker'] + 'sticker' => ['sticker'], ], 'sendAnimation' => ['animation', 'thumbnail'], 'sendAudio' => ['audio', 'thumbnail'], 'sendDocument' => ['document', 'thumbnail'], 'sendMediaGroup' => [ - 'media' => ['media', 'thumbnail'] + 'media' => ['media', 'thumbnail'], ], 'sendPhoto' => ['photo'], 'sendSticker' => ['sticker'], @@ -80,7 +80,7 @@ protected function extractFiles(array $fields, array &$data): array foreach ($fields as $key => $value) { if (is_string($value)) { - if (!isset($data[$value])) { + if (! isset($data[$value])) { continue; } @@ -96,7 +96,7 @@ protected function extractFiles(array $fields, array &$data): array $file instanceof StreamInterface => $file, }; } - } elseif (!array_is_assoc($data[$key])) { + } elseif (! array_is_assoc($data[$key])) { foreach ($data[$key] as &$item) { $streams += $this->extractFiles($value, $item); @@ -146,7 +146,7 @@ protected function send(string $methodName, ?array $data = null, string|array|nu protected function makeResultObject(mixed $result, string|array|null $returnType = null): mixed { - if (!is_array($result)) { + if (! is_array($result)) { return $result; } From 177dc65f6f909166b9d345a0aaf86c57f593dd96 Mon Sep 17 00:00:00 2001 From: Tii Date: Wed, 19 Jun 2024 17:45:43 +0200 Subject: [PATCH 18/20] Register Commands and stuff process Updates accordingly --- examples/logs/.gitignore | 2 + examples/run.php | 48 ++++++++ src/Entities/Entity.php | 8 +- src/Events/Event.php | 7 ++ src/Events/IncomingUpdate.php | 13 +++ src/Telegram.php | 203 +++++++++++++++++++++++++++++++++- 6 files changed, 275 insertions(+), 6 deletions(-) create mode 100644 examples/logs/.gitignore create mode 100644 examples/run.php create mode 100644 src/Events/Event.php create mode 100644 src/Events/IncomingUpdate.php diff --git a/examples/logs/.gitignore b/examples/logs/.gitignore new file mode 100644 index 000000000..c96a04f00 --- /dev/null +++ b/examples/logs/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/examples/run.php b/examples/run.php new file mode 100644 index 000000000..1ee636dae --- /dev/null +++ b/examples/run.php @@ -0,0 +1,48 @@ +registerEventListener(IncomingUpdate::class, function (IncomingUpdate $event, Telegram $bot) { + // Gets called on every incoming `Update` no matter what type + dump('IncomingUpdate'); +}); + +$bot->registerUpdateTypes([ + 'edited_message' => function (Update $update, Telegram $bot) { + // Gets called on every `Update` of type "edited_message" + dump('edited_message'); + }, +]); + +$bot->registerMessageTypes([ + 'text' => [ + function (Update $update, Telegram $bot) { + // Gets called on every `Update` with a `Message` of type "text" + dump('message.text'); + dump($update->getMessage()->getText()); + }, + 'save_dump', + ], + + 'voice' => function (Update $update, Telegram $bot) { + // Gets called on every `Update` with a `Message` of type "voice". + dump('message.voice'); + dump($update->getMessage()->getVoice()); + }, +]); + +// Start 30s long polling in an infinite loop +// See https://en.wikipedia.org/wiki/Push_technology#Long_polling +$bot->handleGetUpdates(); diff --git a/src/Entities/Entity.php b/src/Entities/Entity.php index e4006890b..7c20b7108 100644 --- a/src/Entities/Entity.php +++ b/src/Entities/Entity.php @@ -24,7 +24,7 @@ public function __construct( public function __get(string $name) { - return $this->fields[$name]; + return $this->fields[$name] ?? null; } public function __set(string $name, $value): void @@ -64,10 +64,10 @@ public function __call(string $name, array $arguments) private function getField(string $name): mixed { - $data = $this->fields[$name]; + $data = $this->fields[$name] ?? null; $subEntities = static::subEntities(); - if (! isset($subEntities[$name])) { + if (is_null($data) || ! isset($subEntities[$name])) { return $data; } @@ -110,6 +110,6 @@ private function setField(string $name, mixed $value): void public function jsonSerialize(): mixed { - return array_filter($this->fields, fn ($value, $key) => ! is_null($value) && str_starts_with($key, '__'), ARRAY_FILTER_USE_BOTH); + return array_filter($this->fields, fn ($value) => ! is_null($value)); } } diff --git a/src/Events/Event.php b/src/Events/Event.php new file mode 100644 index 000000000..df825e371 --- /dev/null +++ b/src/Events/Event.php @@ -0,0 +1,7 @@ +extractFiles($value, $item); } + } else { $streams += $this->extractFiles($value, $data[$key]); @@ -114,7 +118,7 @@ protected function send(string $methodName, ?array $data = null, string|array|nu { $requestUri = $this->apiBaseUri . '/bot' . $this->botToken . '/' . $methodName; - $streams = $this->extractFiles(self::inputFileFields()[$methodName] ?? null, $data); + $streams = $this->extractFiles(self::inputFileFields()[$methodName] ?? [], $data); $response = match (true) { empty($data) => $this->client->get($requestUri), @@ -177,6 +181,11 @@ public function handleGetUpdates(int $pollingInterval = 30, ?array $allowedUpdat public function handle() { $data = file_get_contents('php://input'); + + if (empty($data)) { + return; + } + $json = json_decode($data, true); $update = new Update($json); @@ -184,8 +193,198 @@ public function handle() $this->processUpdate($update); } + protected function dispatch(Event $event) + { + $listeners = $this->eventListeners[$event::class] ?? []; + + foreach ($listeners as $callback) { + $callback($event, $this); + } + } + protected function processUpdate(Update $update) { - // + // IncomingUpdate Event + $this->dispatch(new IncomingUpdate($update)); + + // Update Types + foreach ($this->updateTypeCallbacks as $key => $callbacks) { + if (! is_null($update->$key)) { + foreach ($callbacks as $callback) { + $callback($update, $this); + } + } + } + + // Message Types + $message = $update->getMessage(); + if ($message !== null) { + foreach ($this->messageTypeCallbacks as $key => $callbacks) { + if (! is_null($message->$key)) { + foreach ($callbacks as $callback) { + $callback($update, $this); + } + } + } + } + + // Commands + $command = $update->getMessage()?->getText() ?? null; + if ($command !== null && str_starts_with($command, '/')) { + // Cut / from beginning + $command = ltrim($command, '/'); + + // Cut username + $command = explode(' ', $command, 2)[0]; + [$command, $username] = explode('@', $command, 2) + [null, null]; + + // TODO: Check if the username belongs to this bot... + + // Get callbacks + $callbacks = $this->commandCallbacks[$command] ?? []; + + foreach ($callbacks as $callback) { + $callback($update, $this); + } + } + } + + /** + * @var array, array> + */ + protected array $eventListeners = []; + + /** + * @param class-string $event + * @param callable|array $callback + * @return $this + */ + public function registerEventListener(string $event, callable|array $callback): static + { + if (is_callable($callback)) { + $callback = [$callback]; + } + + if (! is_subclass_of($event, Event::class)) { + throw new InvalidArgumentException("$event is not a PhpTelegramBot Event"); + } + + $this->eventListeners[$event] = array_merge($this->eventListeners[$event] ?? [], $callback); + + return $this; + } + + /** + * @param array, callable|array> $callbacks + * @return $this + */ + public function registerEventListeners(array $callbacks): static + { + foreach ($callbacks as $event => $callback) { + $this->registerEventListener($event, $callback); + } + + return $this; + } + + /** + * @var array> + */ + protected array $updateTypeCallbacks = []; + + /** + * @param callable|array $callback + * @return $this + */ + public function registerUpdateType(string $updateType, callable|array $callback): static + { + if (is_callable($callback)) { + $callback = [$callback]; + } + + $this->updateTypeCallbacks[$updateType] = array_merge($this->updateTypeCallbacks[$updateType] ?? [], $callback); + + return $this; + } + + /** + * @param array> $callbacks + * @return $this + */ + public function registerUpdateTypes(array $callbacks): Telegram + { + foreach ($callbacks as $type => $callback) { + $this->registerUpdateType($type, $callback); + } + + return $this; + } + + /** + * @var array> + */ + protected array $messageTypeCallbacks = []; + + /** + * @param callable|array $callback + * @return $this + */ + public function registerMessageType(string $messageType, array|callable $callback): static + { + if (is_callable($callback)) { + $callback = [$callback]; + } + + $this->messageTypeCallbacks[$messageType] = array_merge($this->messageTypeCallbacks[$messageType] ?? [], $callback); + + return $this; + } + + /** + * @param array> $callbacks + * @return $this + */ + public function registerMessageTypes(array $callbacks): static + { + foreach ($callbacks as $messageType => $callback) { + $this->registerMessageType($messageType, $callback); + } + + return $this; + } + + /** + * @var array> + */ + protected array $commandCallbacks = []; + + /** + * @param callable|array $callback + * @return $this + */ + public function registerCommand(string $command, callable|array $callback): static + { + $command = ltrim($command, '/'); + + if (is_callable($callback)) { + $callback = [$callback]; + } + + $this->commandCallbacks[$command] = array_merge($this->commandCallbacks[$command] ?? [], $callback); + + return $this; + } + + /** + * @param array> $callbacks + * @return $this + */ + public function registerCommands(array $callbacks): static + { + foreach ($callbacks as $command => $callback) { + $this->registerCommand($command, $callback); + } + + return $this; } } From 47eeb428696735bed1846bf5c3733e37d67f871b Mon Sep 17 00:00:00 2001 From: Tii Date: Thu, 20 Jun 2024 09:08:22 +0200 Subject: [PATCH 19/20] Added Events for unregistered commands, update and message types --- src/Events/UnregisteredCommand.php | 13 +++++++++++++ src/Events/UnregisteredMessageType.php | 13 +++++++++++++ src/Events/UnregisteredUpdateType.php | 13 +++++++++++++ src/Telegram.php | 24 ++++++++++++++++++++++++ 4 files changed, 63 insertions(+) create mode 100644 src/Events/UnregisteredCommand.php create mode 100644 src/Events/UnregisteredMessageType.php create mode 100644 src/Events/UnregisteredUpdateType.php diff --git a/src/Events/UnregisteredCommand.php b/src/Events/UnregisteredCommand.php new file mode 100644 index 000000000..832e21c08 --- /dev/null +++ b/src/Events/UnregisteredCommand.php @@ -0,0 +1,13 @@ +dispatch(new IncomingUpdate($update)); // Update Types + $registeredUpdateType = false; foreach ($this->updateTypeCallbacks as $key => $callbacks) { if (! is_null($update->$key)) { + $registeredUpdateType = true; foreach ($callbacks as $callback) { $callback($update, $this); } } } + if (! $registeredUpdateType) { + $this->dispatch(new UnregisteredUpdateType($update)); + } + // Message Types + $registeredMessageType = false; $message = $update->getMessage(); if ($message !== null) { foreach ($this->messageTypeCallbacks as $key => $callbacks) { if (! is_null($message->$key)) { + $registeredMessageType = true; foreach ($callbacks as $callback) { $callback($update, $this); } @@ -228,7 +239,12 @@ protected function processUpdate(Update $update) } } + if (! $registeredMessageType) { + $this->dispatch(new UnregisteredMessageType($update)); + } + // Commands + $registeredCommand = false; $command = $update->getMessage()?->getText() ?? null; if ($command !== null && str_starts_with($command, '/')) { // Cut / from beginning @@ -243,10 +259,18 @@ protected function processUpdate(Update $update) // Get callbacks $callbacks = $this->commandCallbacks[$command] ?? []; + if (! empty($callbacks)) { + $registeredCommand = true; + } + foreach ($callbacks as $callback) { $callback($update, $this); } } + + if (! $registeredCommand) { + $this->dispatch(new UnregisteredCommand($update)); + } } /** From 90b9c44f58a7288c0ab505f4cbef9a5cab32e6b7 Mon Sep 17 00:00:00 2001 From: Tii Date: Wed, 3 Jul 2024 11:57:46 +0200 Subject: [PATCH 20/20] Added Bot API Version 7.5 --- src/ApiMethods/SendsMessages.php | 22 +++++++++++ src/Entities/ChatFullInfo.php | 2 + src/Entities/ExternalReplyInfo.php | 2 + .../InputPaidMedia/InputPaidMedia.php | 15 ++++++++ .../InputPaidMedia/InputPaidMediaPhoto.php | 16 ++++++++ .../InputPaidMedia/InputPaidMediaVideo.php | 38 +++++++++++++++++++ src/Entities/MenuButton/MenuButtonWebApp.php | 2 +- src/Entities/Message.php | 2 + src/Entities/PaidMedia/PaidMedia.php | 27 +++++++++++++ src/Entities/PaidMedia/PaidMediaPhoto.php | 25 ++++++++++++ src/Entities/PaidMedia/PaidMediaPreview.php | 18 +++++++++ src/Entities/PaidMedia/PaidMediaVideo.php | 25 ++++++++++++ src/Entities/PaidMediaInfo.php | 19 ++++++++++ .../TransactionPartner/TransactionPartner.php | 11 ++++-- .../TransactionPartnerTelegramAds.php | 8 ++++ .../TransactionPartnerUser.php | 3 +- src/Telegram.php | 4 ++ 17 files changed, 233 insertions(+), 6 deletions(-) create mode 100644 src/Entities/InputPaidMedia/InputPaidMedia.php create mode 100644 src/Entities/InputPaidMedia/InputPaidMediaPhoto.php create mode 100644 src/Entities/InputPaidMedia/InputPaidMediaVideo.php create mode 100644 src/Entities/PaidMedia/PaidMedia.php create mode 100644 src/Entities/PaidMedia/PaidMediaPhoto.php create mode 100644 src/Entities/PaidMedia/PaidMediaPreview.php create mode 100644 src/Entities/PaidMedia/PaidMediaVideo.php create mode 100644 src/Entities/PaidMediaInfo.php create mode 100644 src/Entities/TransactionPartner/TransactionPartnerTelegramAds.php diff --git a/src/ApiMethods/SendsMessages.php b/src/ApiMethods/SendsMessages.php index 25de98e9f..9e863de0e 100644 --- a/src/ApiMethods/SendsMessages.php +++ b/src/ApiMethods/SendsMessages.php @@ -396,6 +396,28 @@ public function sendVideoNote(array $data = []): Message return $this->send(__FUNCTION__, $data, Message::class); } + /** + * @param array{ + * chat_id: int|string, + * star_count: int, + * media: InputPaidMedia[], + * caption: string, + * parse_mode: string, + * caption_entities: MessageEntity[], + * show_caption_above_media: bool, + * disable_notification: bool, + * parse_content: bool, + * reply_parameters: ReplyParameters, + * reply_markup: InlineKeyboardMarkup|ReplyKeyboardMarkup|ReplyKeyboardRemove|ForceReply, + * } $data + * + * @throws \PhpTelegramBot\Core\Exceptions\TelegramException + */ + public function sendPaidMedia(array $data = []): Message + { + return $this->send(__FUNCTION__, $data, Message::class); + } + /** * @param array{ * business_connection_id: string, diff --git a/src/Entities/ChatFullInfo.php b/src/Entities/ChatFullInfo.php index 7778acbc8..47dc1423a 100644 --- a/src/Entities/ChatFullInfo.php +++ b/src/Entities/ChatFullInfo.php @@ -37,6 +37,7 @@ * @method string|null getInviteLink() Optional. Primary invite link, for groups, supergroups and channel chats * @method Message|null getPinnedMessage() Optional. The most recent pinned message (by sending date). * @method ChatPermissions|null getPermissions() Optional. Default chat member permissions, for groups and supergroups + * @method bool canSendPaidMedia() Optional. True, if paid media messages can be sent or forwarded to the channel chat. The field is available only for channel chats. * @method int|null getSlowModeDelay() Optional. For supergroups, the minimum allowed delay between consecutive messages sent by each unprivileged user; in seconds * @method int|null getUnrestrictBoostCount() Optional. For supergroups, the minimum number of boosts that a non-administrator user needs to add in order to ignore slow mode and chat permissions * @method int|null getMessageAutoDeleteTime() Optional. The time after which all messages sent to the chat will be automatically deleted; in seconds @@ -74,6 +75,7 @@ public static function fieldsBypassingGet(): array 'is_forum' => false, 'has_private_forwards' => false, 'has_restricted_voice_and_video_messages' => false, + 'can_send_paid_media' => false, 'has_aggressive_anti_spam_enabled' => false, 'has_hidden_members' => false, 'has_protected_content' => false, diff --git a/src/Entities/ExternalReplyInfo.php b/src/Entities/ExternalReplyInfo.php index 3dde0106a..dda94b4ae 100644 --- a/src/Entities/ExternalReplyInfo.php +++ b/src/Entities/ExternalReplyInfo.php @@ -13,6 +13,7 @@ * @method Animation|null getAnimation() Optional. Message is an animation, information about the animation * @method Audio|null getAudio() Optional. Message is an audio file, information about the file * @method Document|null getDocument() Optional. Message is a general file, information about the file + * @method PaidMediaInfo|null getPaidMedia() Optional. Message contains paid media; information about the paid media * @method PhotoSize[]|null getPhoto() Optional. Message is a photo, available sizes of the photo * @method Sticker|null getSticker() Optional. Message is a sticker, information about the sticker * @method Story|null getStory() Optional. Message is a forwarded story @@ -41,6 +42,7 @@ protected static function subEntities(): array 'animation' => Animation::class, 'audio' => Audio::class, 'document' => Document::class, + 'paid_media' => PaidMediaInfo::class, 'photo' => [PhotoSize::class], 'sticker' => Sticker::class, 'story' => Story::class, diff --git a/src/Entities/InputPaidMedia/InputPaidMedia.php b/src/Entities/InputPaidMedia/InputPaidMedia.php new file mode 100644 index 000000000..17ab2221d --- /dev/null +++ b/src/Entities/InputPaidMedia/InputPaidMedia.php @@ -0,0 +1,15 @@ +” to upload a new one using multipart/form-data under name. + */ +class InputPaidMediaPhoto extends InputPaidMedia +{ + protected static function presetData(): array + { + return [ + 'type' => self::TYPE_PHOTO, + ]; + } +} diff --git a/src/Entities/InputPaidMedia/InputPaidMediaVideo.php b/src/Entities/InputPaidMedia/InputPaidMediaVideo.php new file mode 100644 index 000000000..549bdc4e7 --- /dev/null +++ b/src/Entities/InputPaidMedia/InputPaidMediaVideo.php @@ -0,0 +1,38 @@ +” to upload a new one using multipart/form-data under name. More information on Sending Files » + * @method InputFile|string getThumbnail() Optional. Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail's width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can't be reused and can be only uploaded as a new file, so you can pass “attach://” if the thumbnail was uploaded using multipart/form-data under . More information on Sending Files » + * @method int|null getWidth() Optional. Video width + * @method int|null getHeight() Optional. Video height + * @method int|null getDuration() Optional. Video duration in seconds + * @method bool|null supportsStreaming() Optional. Pass True if the uploaded video is suitable for streaming + */ +class InputPaidMediaVideo extends InputPaidMedia implements AllowsBypassingGet +{ + protected static function presetData(): array + { + return [ + 'type' => self::TYPE_VIDEO, + ]; + } + + protected static function subEntities(): array + { + return [ + 'thumbnail' => InputFile::class, + ]; + } + + public static function fieldsBypassingGet(): array + { + return [ + 'supports_streaming' => false, + ]; + } +} diff --git a/src/Entities/MenuButton/MenuButtonWebApp.php b/src/Entities/MenuButton/MenuButtonWebApp.php index 45b025cd6..bec75ae56 100644 --- a/src/Entities/MenuButton/MenuButtonWebApp.php +++ b/src/Entities/MenuButton/MenuButtonWebApp.php @@ -6,7 +6,7 @@ /** * @method string getText() Text on the button - * @method WebAppInfo getWebApp() 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. + * @method WebAppInfo getWebApp() 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. Alternatively, a t.me link to a Web App of the bot can be specified in the object instead of the Web App's URL, in which case the Web App will be opened as if the user pressed the link. */ class MenuButtonWebApp extends MenuButton { diff --git a/src/Entities/Message.php b/src/Entities/Message.php index 08c1a0b9b..b8c0b6cbe 100644 --- a/src/Entities/Message.php +++ b/src/Entities/Message.php @@ -35,6 +35,7 @@ * @method Animation|null getAnimation() Optional. Message is an animation, information about the animation. For backward compatibility, when this field is set, the document field will also be set * @method Audio|null getAudio() Optional. Message is an audio file, information about the file * @method Document|null getDocument() Optional. Message is a general file, information about the file + * @method PaidMediaInfo|null getPaidMedia() Optional. Message contains paid media; information about the paid media * @method PhotoSize[]|null getPhoto() Optional. Message is a photo, available sizes of the photo * @method Sticker|null getSticker() Optional. Message is a sticker, information about the sticker * @method Story|null getStory() Optional. Message is a forwarded story @@ -110,6 +111,7 @@ protected static function subEntities(): array 'animation' => Animation::class, 'audio' => Audio::class, 'document' => Document::class, + 'paid_media' => PaidMediaInfo::class, 'photo' => [PhotoSize::class], 'sticker' => Sticker::class, 'story' => Story::class, diff --git a/src/Entities/PaidMedia/PaidMedia.php b/src/Entities/PaidMedia/PaidMedia.php new file mode 100644 index 000000000..30d01e95d --- /dev/null +++ b/src/Entities/PaidMedia/PaidMedia.php @@ -0,0 +1,27 @@ + new PaidMediaPreview($data), + self::TYPE_PHOTO => new PaidMediaPhoto($data), + self::TYPE_VIDEO => new PaidMediaVideo($data), + }; + } +} diff --git a/src/Entities/PaidMedia/PaidMediaPhoto.php b/src/Entities/PaidMedia/PaidMediaPhoto.php new file mode 100644 index 000000000..ea5a0b239 --- /dev/null +++ b/src/Entities/PaidMedia/PaidMediaPhoto.php @@ -0,0 +1,25 @@ + [PhotoSize::class], + ]; + } + + protected static function presetData(): array + { + return [ + 'type' => self::TYPE_PHOTO, + ]; + } +} diff --git a/src/Entities/PaidMedia/PaidMediaPreview.php b/src/Entities/PaidMedia/PaidMediaPreview.php new file mode 100644 index 000000000..4753a30d5 --- /dev/null +++ b/src/Entities/PaidMedia/PaidMediaPreview.php @@ -0,0 +1,18 @@ + self::TYPE_PREVIEW, + ]; + } +} diff --git a/src/Entities/PaidMedia/PaidMediaVideo.php b/src/Entities/PaidMedia/PaidMediaVideo.php new file mode 100644 index 000000000..bc5f4bf0f --- /dev/null +++ b/src/Entities/PaidMedia/PaidMediaVideo.php @@ -0,0 +1,25 @@ + Video::class, + ]; + } + + protected static function presetData(): array + { + return [ + 'type' => self::TYPE_VIDEO, + ]; + } +} diff --git a/src/Entities/PaidMediaInfo.php b/src/Entities/PaidMediaInfo.php new file mode 100644 index 000000000..40f727232 --- /dev/null +++ b/src/Entities/PaidMediaInfo.php @@ -0,0 +1,19 @@ + [PaidMedia::class], + ]; + } +} diff --git a/src/Entities/TransactionPartner/TransactionPartner.php b/src/Entities/TransactionPartner/TransactionPartner.php index 433a156c9..92aa58250 100644 --- a/src/Entities/TransactionPartner/TransactionPartner.php +++ b/src/Entities/TransactionPartner/TransactionPartner.php @@ -10,18 +10,21 @@ */ abstract class TransactionPartner extends Entity implements Factory { + public const TYPE_USER = 'user'; + public const TYPE_FRAGMENT = 'fragment'; - public const TYPE_USER = 'user'; + public const TYPE_TELEGRAM_ADS = 'telegram_ads'; public const TYPE_OTHER = 'other'; public static function make(array $data): static { return match ($data['type']) { - self::TYPE_FRAGMENT => new TransactionPartnerFragment($data), - self::TYPE_USER => new TransactionPartnerUser($data), - self::TYPE_OTHER => new TransactionPartnerOther($data), + self::TYPE_USER => new TransactionPartnerUser($data), + self::TYPE_FRAGMENT => new TransactionPartnerFragment($data), + self::TYPE_TELEGRAM_ADS => new TransactionPartnerTelegramAds($data), + self::TYPE_OTHER => new TransactionPartnerOther($data), }; } } diff --git a/src/Entities/TransactionPartner/TransactionPartnerTelegramAds.php b/src/Entities/TransactionPartner/TransactionPartnerTelegramAds.php new file mode 100644 index 000000000..ffbdf0091 --- /dev/null +++ b/src/Entities/TransactionPartner/TransactionPartnerTelegramAds.php @@ -0,0 +1,8 @@ + ['sticker'], 'sendVideo' => ['video', 'thumbnail'], 'sendVideoNote' => ['video_note', 'thumbnail'], + 'sendPaidMedia' => [ + 'media' => ['media', 'thumbnail'], + ], 'sendVoice' => ['voice'], 'setChatPhoto' => ['photo'], 'setStickerSetThumbnail' => ['thumbnail'], @@ -62,6 +65,7 @@ public static function inputFileFields(): array 'uploadStickerFile' => ['sticker'], ]; } + // .media public function __construct( #[\SensitiveParameter]