diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a7489df4..96856c632 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ Exclamation symbols (:exclamation:) note something of importance e.g. breaking c ## [Unreleased] ### 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)`. ### Changed - All Message field types dynamically search for an existing Command class that can handle them. ### Deprecated @@ -13,6 +15,7 @@ Exclamation symbols (:exclamation:) note something of importance e.g. breaking c ### Removed ### Fixed - Constraint errors in `/cleanup` command. +- Return correct objects for requests. ### Security ## [0.55.1] - 2019-01-06 diff --git a/src/Entities/Entity.php b/src/Entities/Entity.php index 23e95a153..77665442a 100644 --- a/src/Entities/Entity.php +++ b/src/Entities/Entity.php @@ -12,7 +12,7 @@ use Exception; use Longman\TelegramBot\Entities\InlineQuery\InlineEntity; -use Longman\TelegramBot\TelegramLog; +use Longman\TelegramBot\Entities\InputMedia\InputMedia; /** * Class Entity @@ -149,7 +149,7 @@ public function __call($method, $args) } } elseif ($action === 'set') { // Limit setters to specific classes. - if ($this instanceof InlineEntity || $this instanceof Keyboard || $this instanceof KeyboardButton) { + if ($this instanceof InlineEntity || $this instanceof InputMedia || $this instanceof Keyboard || $this instanceof KeyboardButton) { $this->$property_name = $args[0]; return $this; diff --git a/src/Entities/ServerResponse.php b/src/Entities/ServerResponse.php index 11ee995bb..8115b3e79 100644 --- a/src/Entities/ServerResponse.php +++ b/src/Entities/ServerResponse.php @@ -8,6 +8,9 @@ namespace Longman\TelegramBot\Entities; +use Longman\TelegramBot\Entities\Games\GameHighScore; +use Longman\TelegramBot\Request; + /** * Class ServerResponse * @@ -103,32 +106,27 @@ public function printError($return = false) * @param string $bot_username * * @return \Longman\TelegramBot\Entities\Chat|\Longman\TelegramBot\Entities\ChatMember|\Longman\TelegramBot\Entities\File|\Longman\TelegramBot\Entities\Message|\Longman\TelegramBot\Entities\User|\Longman\TelegramBot\Entities\UserProfilePhotos|\Longman\TelegramBot\Entities\WebhookInfo - * @throws \Longman\TelegramBot\Exception\TelegramException */ - private function createResultObject($result, $bot_username) + private function createResultObject(array $result, $bot_username) { + $action = Request::getCurrentAction(); + // We don't need to save the raw_data of the response object! $result['raw_data'] = null; $result_object_types = [ - 'total_count' => 'UserProfilePhotos', //Response from getUserProfilePhotos - 'stickers' => 'StickerSet', //Response from getStickerSet - 'file_id' => 'File', //Response from getFile - 'title' => 'Chat', //Response from getChat - 'username' => 'User', //Response from getMe - 'user' => 'ChatMember', //Response from getChatMember - 'url' => 'WebhookInfo', //Response from getWebhookInfo + 'getChat' => Chat::class, + 'getChatMember' => ChatMember::class, + 'getFile' => File::class, + 'getMe' => User::class, + 'getStickerSet' => StickerSet::class, + 'getUserProfilePhotos' => UserProfilePhotos::class, + 'getWebhookInfo' => WebhookInfo::class, ]; - foreach ($result_object_types as $type => $object_class) { - if (isset($result[$type])) { - $object_class = __NAMESPACE__ . '\\' . $object_class; - return new $object_class($result); - } - } + $object_class = array_key_exists($action, $result_object_types) ? $result_object_types[$action] : Message::class; - //Response from sendMessage - return new Message($result, $bot_username); + return new $object_class($result, $bot_username); } /** @@ -137,28 +135,26 @@ private function createResultObject($result, $bot_username) * @param array $result * @param string $bot_username * - * @return null|\Longman\TelegramBot\Entities\ChatMember[]|\Longman\TelegramBot\Entities\Update[] - * @throws \Longman\TelegramBot\Exception\TelegramException + * @return \Longman\TelegramBot\Entities\ChatMember[]|\Longman\TelegramBot\Entities\Games\GameHighScore[]|\Longman\TelegramBot\Entities\Message[]|\Longman\TelegramBot\Entities\Update[] */ - private function createResultObjects($result, $bot_username) + private function createResultObjects(array $result, $bot_username) { $results = []; - if (isset($result[0]['user'])) { - //Response from getChatAdministrators - foreach ($result as $user) { - // We don't need to save the raw_data of the response object! - $user['raw_data'] = null; + $action = Request::getCurrentAction(); - $results[] = new ChatMember($user); - } - } else { - //Get Update - foreach ($result as $update) { - // We don't need to save the raw_data of the response object! - $update['raw_data'] = null; + $result_object_types = [ + 'getChatAdministrators' => ChatMember::class, + 'getGameHighScores' => GameHighScore::class, + 'sendMediaGroup' => Message::class, + ]; - $results[] = new Update($update, $bot_username); - } + $object_class = array_key_exists($action, $result_object_types) ? $result_object_types[$action] : Update::class; + + foreach ($result as $data) { + // We don't need to save the raw_data of the response object! + $data['raw_data'] = null; + + $results[] = new $object_class($data, $bot_username); } return $results; diff --git a/src/Request.php b/src/Request.php index 4d294ab07..136158d07 100644 --- a/src/Request.php +++ b/src/Request.php @@ -12,7 +12,9 @@ use GuzzleHttp\Client; use GuzzleHttp\Exception\RequestException; +use GuzzleHttp\Psr7\Stream; use Longman\TelegramBot\Entities\File; +use Longman\TelegramBot\Entities\InputMedia\InputMedia; use Longman\TelegramBot\Entities\ServerResponse; use Longman\TelegramBot\Exception\InvalidBotTokenException; use Longman\TelegramBot\Exception\TelegramException; @@ -126,6 +128,13 @@ class Request */ private static $limiter_interval; + /** + * Get the current action that is being executed + * + * @var string + */ + private static $current_action; + /** * Available actions to send * @@ -213,6 +222,30 @@ class Request 'getMe', ]; + /** + * 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', 'thumb'], + 'sendDocument' => ['document', 'thumb'], + 'sendVideo' => ['video', 'thumb'], + 'sendAnimation' => ['animation', 'thumb'], + 'sendVoice' => ['voice', 'thumb'], + 'sendVideoNote' => ['video_note', 'thumb'], + 'setChatPhoto' => ['photo'], + 'sendSticker' => ['sticker'], + 'uploadStickerFile' => ['png_sticker'], + 'createNewStickerSet' => ['png_sticker'], + 'addStickerToSet' => ['png_sticker'], + ]; + /** * Initialize * @@ -318,22 +351,32 @@ public static function generateGeneralFakeServerResponse(array $data = []) * @param array $data * * @return array + * @throws TelegramException */ private static function setUpRequestParams(array $data) { $has_resource = false; $multipart = []; - // Convert any nested arrays into JSON strings. - array_walk($data, function (&$item) { - is_array($item) && $item = json_encode($item); - }); + 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)) { + // Convert any nested arrays into JSON strings. + $item = json_encode($item); + } - //Reformat data array in multipart way if it contains a resource - foreach ($data as $key => $item) { - $has_resource |= (is_resource($item) || $item instanceof \GuzzleHttp\Psr7\Stream); + // Reformat data array in multipart way if it contains a resource + $has_resource |= (is_resource($item) || $item instanceof Stream); $multipart[] = ['name' => $key, 'contents' => $item]; } + if ($has_resource) { return ['multipart' => $multipart]; } @@ -341,6 +384,83 @@ private static function setUpRequestParams(array $data) 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 + */ + private static function mediaInputHelper($item, &$has_resource, array &$multipart) + { + $was_array = is_array($item); + $was_array || $item = [$item]; + + foreach ($item as $media_item) { + if (!($media_item instanceof InputMedia)) { + continue; + } + + $media = $media_item->getMedia(); + + // Allow absolute paths to local files. + if (is_string($media) && file_exists($media)) { + $media = new Stream(self::encodeFile($media)); + } + + if (is_resource($media) || $media instanceof Stream) { + $has_resource = true; + $rnd_key = uniqid('media_', false); + $multipart[] = ['name' => $rnd_key, 'contents' => $media]; + + // We're literally overwriting the passed media data! + $media_item->media = 'attach://' . $rnd_key; + $media_item->raw_data['media'] = 'attach://' . $rnd_key; + } + } + + $was_array || $item = reset($item); + + return json_encode($item); + } + + /** + * Get the current action that's being executed + * + * @return string + */ + public static function getCurrentAction() + { + return self::$current_action; + } + /** * Execute HTTP Request * @@ -373,7 +493,7 @@ public static function execute($action, array $data = []) TelegramLog::update($result); } } catch (RequestException $e) { - $result = ($e->getResponse()) ? (string) $e->getResponse()->getBody() : ''; + $result = $e->getResponse() ? (string) $e->getResponse()->getBody() : ''; } finally { //Logging verbose debug output TelegramLog::endDebugLogTempStream('Verbose HTTP Request output:' . PHP_EOL . '%s' . PHP_EOL); @@ -470,8 +590,11 @@ public static function send($action, array $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); + $response = json_decode($raw_response, true); if (null === $response) { TelegramLog::debug($raw_response); @@ -484,6 +607,9 @@ public static function send($action, array $data = []) throw new InvalidBotTokenException(); } + // Reset current action after completion. + self::$current_action = null; + return $response; } diff --git a/tests/unit/Entities/ServerResponseTest.php b/tests/unit/Entities/ServerResponseTest.php index e89bd064f..ffe0ab119 100644 --- a/tests/unit/Entities/ServerResponseTest.php +++ b/tests/unit/Entities/ServerResponseTest.php @@ -25,6 +25,12 @@ */ class ServerResponseTest extends TestCase { + protected function setUp() + { + // Make sure the current action in the Request class is unset. + TestHelpers::setStaticProperty(Request::class, 'current_action', null); + } + public function sendMessageOk() { return '{ @@ -193,6 +199,7 @@ public function testGetUpdatesEmpty() public function getUserProfilePhotos() { + TestHelpers::setStaticProperty(Request::class, 'current_action', 'getUserProfilePhotos'); return '{ "ok":true, "result":{ @@ -238,6 +245,7 @@ public function testGetUserProfilePhotos() public function getFile() { + TestHelpers::setStaticProperty(Request::class, 'current_action', 'getFile'); return '{ "ok":true, "result":{ @@ -301,6 +309,7 @@ public function testSetGeneralTestFakeResponse() public function getStickerSet() { + TestHelpers::setStaticProperty(Request::class, 'current_action', 'getStickerSet'); return '{ "ok":true, "result":{