From 177b5b8ce281ea36091f4385e3a5cfb92f14f58c Mon Sep 17 00:00:00 2001 From: Irfaq Syed Date: Wed, 16 Oct 2024 18:56:12 +0530 Subject: [PATCH] 6.0 --- .gitignore | 2 +- CHANGELOG.md | 19 +++ README.md | 23 ++- composer.json | 16 +- phpstan.neon.dist | 1 - phpunit.xml.dist.bak | 37 ----- src/Enums/ParseMode.php | 10 ++ src/Telegram.php | 17 +- src/TelegramChannel.php | 43 ++--- src/TelegramFile.php | 37 ++--- src/TelegramMessage.php | 147 +++++++---------- src/TelegramUpdates.php | 3 +- src/Traits/HasSharedLogic.php | 224 +++++++++++++++++++------- tests/Feature/TelegramMessageTest.php | 4 +- 14 files changed, 329 insertions(+), 254 deletions(-) delete mode 100644 phpunit.xml.dist.bak create mode 100644 src/Enums/ParseMode.php diff --git a/.gitignore b/.gitignore index 89f080d..6b94cc9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ vendor build -.phpunit.result.cache +.phpunit.cache composer.lock phpunit.xml phpstan.neon diff --git a/CHANGELOG.md b/CHANGELOG.md index b03bc25..a37fc37 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,25 @@ All notable changes to `telegram` will be documented in this file +## 6.0 - (Unreleased) + +### What's Changed + +* Drop support for Laravel 10. +* Drop support for PHP 8.1. +* Add `sendWhen` method to conditionally send a message. +* Add ParseMode Enum and refactor parsing mode setting logic. +* Add `buttonWithWebApp` method to open web app from a button. +* Add `onError` method to handle exceptions. Based of https://github.com/laravel-notification-channels/telegram/pull/201 +* Refactor `escapedLine` method. +* Refactor `HasSharedLogic` trait. +* Refactor classes to use PHP 8.2 features. +* Revise `keyboard` method parameters to `$requestLocation` and `$requestContact` to be consistent. + +### New Contributors + +* @Hesammousavi made their first contribution in https://github.com/laravel-notification-channels/telegram/pull/201 + ## 5.0 - 2024-03-12 ### What's Changed diff --git a/README.md b/README.md index 803551d..63c3f36 100644 --- a/README.md +++ b/README.md @@ -87,10 +87,10 @@ use NotificationChannels\Telegram\TelegramUpdates; $updates = TelegramUpdates::create() // (Optional). Get's the latest update. NOTE: All previous updates will be forgotten using this method. // ->latest() - + // (Optional). Limit to 2 updates (By default, updates starting with the earliest unconfirmed update are returned). ->limit(2) - + // (Optional). Add more params to the request. ->options([ 'timeout' => 0, @@ -154,6 +154,7 @@ class InvoicePaid extends Notification return TelegramMessage::create() // Optional recipient user id. ->to($notifiable->telegram_user_id) + // Markdown supported. ->content("Hello there!") ->line("Your invoice has been *PAID*") @@ -165,9 +166,16 @@ class InvoicePaid extends Notification // (Optional) Inline Buttons ->button('View Invoice', $url) - ->button('Download Invoice', $url) + ->button('Download Invoice', $url); + + // (Optional) Conditional notification. Only send if amount is greater than 0. Otherwise, don't send. + // ->sendWhen($notifiable->amount > 0) + + // (Optional) Inline Button with Web App + // ->buttonWithWebApp('Open Web App', $url) + // (Optional) Inline Button with callback. You can handle callback in your bot instance - ->buttonWithCallback('Confirm', 'confirm_invoice ' . $this->invoice->id); + // ->buttonWithCallback('Confirm', 'confirm_invoice ' . $this->invoice->id) } } ``` @@ -362,10 +370,10 @@ Notification::route('telegram', 'TELEGRAM_CHAT_ID') Using the [notification facade][link-notification-facade] you can send a notification to multiple recipients at once. -> If you're sending bulk notifications to multiple users, the Telegram Bot API will not allow more than 30 messages per second or so. +> If you're sending bulk notifications to multiple users, the Telegram Bot API will not allow more than 30 messages per second or so. > Consider spreading out notifications over large intervals of 8—12 hours for best results. > -> Also note that your bot will not be able to send more than 20 messages per minute to the same group. +> Also note that your bot will not be able to send more than 20 messages per minute to the same group. > > If you go over the limit, you'll start getting `429` errors. For more details, refer Telegram Bots [FAQ](https://core.telegram.org/bots/faq#broadcasting-to-users). @@ -386,8 +394,11 @@ Notification::send($recipients, new InvoicePaid()); - `token(string $token)`: Bot token if you wish to override the default token for a specific notification. - `button(string $text, string $url, int $columns = 2)`: Adds an inline "Call to Action" button. You can add as many as you want, and they'll be placed 2 in a row by default. - `buttonWithCallback(string $text, string $callback_data, int $columns = 2)`: Adds an inline button with the given callback data. You can add as many as you want, and they'll be placed 2 in a row by default. +- `buttonWithWebApp(string $text, string $url, int $columns = 2)`: Adds an inline button that opens a web application when clicked. This button can be used to direct users to a specific web app. - `disableNotification(bool $disableNotification = true)`: Send the message silently. Users will receive a notification with no sound. - `options(array $options)`: Allows you to add additional params or override the payload. +- `sendWhen(bool $condition)`: Sets a condition for sending the notification. If the condition is true, the notification will be sent; otherwise, it will not. +- `onError(Closure $callback)`: Sets a callback function to handle exceptions. Callback receives an array with `to`, `request`, `exception` keys. - `getPayloadValue(string $key)`: Get payload value for given key. ### Telegram Message methods diff --git a/composer.json b/composer.json index d5c94e7..295f999 100644 --- a/composer.json +++ b/composer.json @@ -19,23 +19,23 @@ ], "homepage": "https://github.com/laravel-notification-channels/telegram", "require": { - "php": "^8.1", + "php": "^8.2", "ext-json": "*", - "guzzlehttp/guzzle": "^7.2", - "illuminate/contracts": "^10 || ^11.0", - "illuminate/notifications": "^10 || ^11.0", - "illuminate/support": "^10 || ^11.0" + "guzzlehttp/guzzle": "^7.8", + "illuminate/contracts": "^11.0", + "illuminate/notifications": "^11.0", + "illuminate/support": "^11.0" }, "require-dev": { "larastan/larastan": "^2.9", "mockery/mockery": "^1.4.4", "orchestra/testbench": "^8.0 || ^9.0", - "pestphp/pest": "^2.34", - "pestphp/pest-plugin-laravel": "^2.3", + "pestphp/pest": "^3.0", + "pestphp/pest-plugin-laravel": "^3.0", "phpstan/extension-installer": "^1.2", "phpstan/phpstan-deprecation-rules": "^1.1", "phpstan/phpstan-phpunit": "^1.3", - "phpunit/phpunit": "^10.5 || ^11.0" + "phpunit/phpunit": "^11.0" }, "minimum-stability": "dev", "prefer-stable": true, diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 0935fa6..8793aab 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -6,4 +6,3 @@ parameters: paths: - src tmpDir: build/phpstan - checkMissingIterableValueType: false diff --git a/phpunit.xml.dist.bak b/phpunit.xml.dist.bak deleted file mode 100644 index b8c462d..0000000 --- a/phpunit.xml.dist.bak +++ /dev/null @@ -1,37 +0,0 @@ - - - - - tests - - - - - src - - - - - - - - - - - diff --git a/src/Enums/ParseMode.php b/src/Enums/ParseMode.php new file mode 100644 index 0000000..94c917b --- /dev/null +++ b/src/Enums/ParseMode.php @@ -0,0 +1,10 @@ +token = $token; - $this->http = $httpClient ?? new HttpClient(); - $this->setApiBaseUri($apiBaseUri ?? 'https://api.telegram.org'); + $this->http = $httpClient; + $this->setApiBaseUri($apiBaseUri); } /** @@ -112,9 +115,9 @@ public function sendMessage(array $params): ?ResponseInterface * * @throws CouldNotSendNotification */ - public function sendFile(array $params, string $type, bool $multipart = false): ?ResponseInterface + public function sendFile(array $params, string|array $type, bool $multipart = false): ?ResponseInterface { - return $this->sendRequest('send'.Str::studly($type), $params, $multipart); + return $this->sendRequest('send'.Str::studly((array)$type[0]), $params, $multipart); } /** diff --git a/src/TelegramChannel.php b/src/TelegramChannel.php index 90b4e99..14f5736 100644 --- a/src/TelegramChannel.php +++ b/src/TelegramChannel.php @@ -13,15 +13,9 @@ */ class TelegramChannel { - private Dispatcher $dispatcher; - - /** - * Channel constructor. - */ - public function __construct(Dispatcher $dispatcher) - { - $this->dispatcher = $dispatcher; - } + public function __construct( + private readonly Dispatcher $dispatcher + ) {} /** * Send the given notification. @@ -38,17 +32,20 @@ public function send(mixed $notifiable, Notification $notification): ?array $message = TelegramMessage::create($message); } - if ($message->toNotGiven()) { - $to = $notifiable->routeNotificationFor('telegram', $notification) - ?? $notifiable->routeNotificationFor(self::class, $notification); + if (!$message->canSend()) { + return null; + } - if (! $to) { - return null; - } + $to = $message->getPayloadValue('chat_id') ?: + ($notifiable->routeNotificationFor('telegram', $notification) ?: + $notifiable->routeNotificationFor(self::class, $notification)); - $message->to($to); + if (! $to) { + return null; } + $message->to($to); + if ($message->hasToken()) { $message->telegram->setToken($message->token); } @@ -56,15 +53,23 @@ public function send(mixed $notifiable, Notification $notification): ?array try { $response = $message->send(); } catch (CouldNotSendNotification $exception) { - $this->dispatcher->dispatch(new NotificationFailed($notifiable, $notification, 'telegram', [ + $data = [ 'to' => $message->getPayloadValue('chat_id'), 'request' => $message->toArray(), 'exception' => $exception, - ])); + ]; + + if ($message->exceptionHandler) { + ($message->exceptionHandler)($data); + } + + $this->dispatcher->dispatch(new NotificationFailed($notifiable, $notification, 'telegram', $data)); throw $exception; } - return $response instanceof Response ? json_decode($response->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR) : $response; + return $response instanceof Response + ? json_decode($response->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR) + : $response; } } diff --git a/src/TelegramFile.php b/src/TelegramFile.php index 1745ab4..1b9ef9a 100644 --- a/src/TelegramFile.php +++ b/src/TelegramFile.php @@ -7,6 +7,7 @@ use NotificationChannels\Telegram\Exceptions\CouldNotSendNotification; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\StreamInterface; +use NotificationChannels\Telegram\Enums\ParseMode; /** * Class TelegramFile. @@ -20,7 +21,7 @@ public function __construct(string $content = '') { parent::__construct(); $this->content($content); - $this->payload['parse_mode'] = 'Markdown'; + $this->parseMode(ParseMode::Markdown); } public static function create(string $content = ''): self @@ -46,6 +47,8 @@ public function content(string $content): self * Generic method to attach files of any type based on API. * * @param resource|StreamInterface|string $file + * @param string $type + * @param string|null $filename * @return $this */ public function file(mixed $file, string $type, ?string $filename = null): self @@ -72,9 +75,7 @@ public function file(mixed $file, string $type, ?string $filename = null): self /** * Attach an image. - * - * Use this method to send photos. - * + * @param string $file * @return $this */ public function photo(string $file): self @@ -84,10 +85,7 @@ public function photo(string $file): self /** * Attach an audio file. - * - * 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. - * + * @param string $file * @return $this */ public function audio(string $file): self @@ -97,9 +95,8 @@ public function audio(string $file): self /** * Attach a document or any file as document. - * - * Use this method to send general files. - * + * @param string $file + * @param string|null $filename * @return $this */ public function document(string $file, ?string $filename = null): self @@ -109,9 +106,7 @@ public function document(string $file, ?string $filename = null): self /** * Attach a video file. - * - * Use this method to send video files, Telegram clients support mp4 videos. - * + * @param string $file * @return $this */ public function video(string $file): self @@ -121,9 +116,7 @@ public function video(string $file): self /** * Attach an animation file. - * - * Use this method to send animation files (GIF or H.264/MPEG-4 AVC video without sound). - * + * @param string $file * @return $this */ public function animation(string $file): self @@ -133,10 +126,7 @@ public function animation(string $file): self /** * Attach a voice file. - * - * 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. - * + * @param string $file * @return $this */ public function voice(string $file): self @@ -146,10 +136,7 @@ public function voice(string $file): self /** * Attach a video note file. - * - * Telegram clients support rounded square mp4 videos of up to 1 minute long. - * Use this method to send video messages. - * + * @param string $file * @return $this */ public function videoNote(string $file): self diff --git a/src/TelegramMessage.php b/src/TelegramMessage.php index d3aeeea..6d1b6bf 100644 --- a/src/TelegramMessage.php +++ b/src/TelegramMessage.php @@ -1,26 +1,29 @@ content($content); - $this->payload['parse_mode'] = 'Markdown'; + $this->parseMode(ParseMode::Markdown); } public static function create(string $content = ''): self @@ -28,16 +31,10 @@ public static function create(string $content = ''): self return new self($content); } - /** - * Notification message (Supports Markdown). - * - * @return $this - */ public function content(string $content, ?int $limit = null): self { $this->payload['text'] = $content; - - if ($limit) { + if ($limit !== null) { $this->chunkSize = $limit; } @@ -46,57 +43,41 @@ public function content(string $content, ?int $limit = null): self public function line(string $content): self { - $this->payload['text'] .= $content."\n"; + $this->payload['text'] .= "$content\n"; return $this; } - public function lineIf(bool $boolean, string $line): self + public function lineIf(bool $condition, string $line): self { - if ($boolean) { - return $this->line($line); - } - - return $this; + return $condition ? $this->line($line) : $this; } public function escapedLine(string $content): self { - // code taken from public gist https://gist.github.com/vijinho/3d66fab3270fc377b8485387ce7e7455 - $content = str_replace([ - '\\', '-', '#', '*', '+', '`', '.', '[', ']', '(', ')', '!', '&', '<', '>', '_', '{', '}', ], [ - '\\\\', '\-', '\#', '\*', '\+', '\`', '\.', '\[', '\]', '\(', '\)', '\!', '\&', '\<', '\>', '\_', '\{', '\}', - ], $content); + $content = str_replace('\\', '\\\\', $content); - return $this->line($content); + $escapedContent = preg_replace_callback( + '/[_*[\]()~`>#\+\-=|{}.!]/', + fn ($matches): string => "\\$matches[0]", + $content + ); + + return $this->line($escapedContent ?? $content); } - /** - * Attach a view file as the content for the notification. - * Supports Laravel blade template. - * - * @return $this - */ public function view(string $view, array $data = [], array $mergeData = []): self { return $this->content(View::make($view, $data, $mergeData)->render()); } - /** - * Chunk message to given size. - * - * @return $this - */ - public function chunk(int $limit = 4096): self + public function chunk(int $limit = self::DEFAULT_CHUNK_SIZE): self { $this->chunkSize = $limit; return $this; } - /** - * Should the message be chunked. - */ public function shouldChunk(): bool { return $this->chunkSize > 0; @@ -105,19 +86,18 @@ public function shouldChunk(): bool /** * @throws CouldNotSendNotification * @throws JsonException + * @return array>|ResponseInterface|null */ - public function send(): ResponseInterface|array|null + public function send(): array|ResponseInterface|null { - $params = $this->toArray(); - - if ($this->shouldChunk()) { - return $this->sendChunkedMessage($params); - } - - return $this->telegram->sendMessage($params); + return $this->shouldChunk() + ? $this->sendChunkedMessage($this->toArray()) + : $this->telegram->sendMessage($this->toArray()); } /** + * @param array $params + * @return array> * @throws CouldNotSendNotification * @throws JsonException */ @@ -129,52 +109,47 @@ private function sendChunkedMessage(array $params): array unset($params['reply_markup']); } - $messages = $this->chunkStrings($this->getPayloadValue('text'), $this->chunkSize); + $messages = $this->chunkStrings($params['text'], $this->chunkSize); + $lastIndex = count($messages) - 1; - $payloads = collect($messages) + return Collection::make($messages) ->filter() - ->map(fn ($text) => array_merge($params, ['text' => $text])); - - if ($replyMarkup) { - $lastMessage = $payloads->pop(); - $lastMessage['reply_markup'] = $replyMarkup; - $payloads->push($lastMessage); - } - - return $payloads->map(function ($payload) { - $response = $this->telegram->sendMessage($payload); - - // To avoid rate limit of one message per second. - sleep(1); - - if ($response) { - return json_decode($response->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR); - } - - return $response; - })->toArray(); + ->map(function (string $text, int $index) use ($params, $replyMarkup, $lastIndex): ?array { + $payload = [...$params, 'text' => $text]; + if ($index === $lastIndex && $replyMarkup !== null) { + $payload['reply_markup'] = $replyMarkup; + } + + $response = $this->telegram->sendMessage($payload); + sleep(1); // Rate limiting + + return $response ? json_decode( + $response->getBody()->getContents(), + true, + 512, + JSON_THROW_ON_ERROR + ) : null; + }) + ->filter() + ->values() + ->all(); } /** - * Chunk the given string into an array of strings. + * @return array */ - private function chunkStrings(string $value, int $limit = 4096): array + private function chunkStrings(string $value, int $limit = self::DEFAULT_CHUNK_SIZE): array { if (mb_strwidth($value, 'UTF-8') <= $limit) { return [$value]; } - if ($limit > 4096) { - $limit = 4096; - } - - $output = explode('%#TGMSG#%', wordwrap($value, $limit, '%#TGMSG#%')); + $limit = min($limit, self::DEFAULT_CHUNK_SIZE); - // Fallback for when the string is too long and wordwrap doesn't cut it. - if (count($output) <= 1) { - $output = mb_str_split($value, $limit, 'UTF-8'); - } + $output = explode(self::CHUNK_SEPARATOR, wordwrap($value, $limit, self::CHUNK_SEPARATOR)); - return $output; + return count($output) <= 1 + ? mb_str_split($value, $limit, 'UTF-8') ?? [$value] + : $output; } } diff --git a/src/TelegramUpdates.php b/src/TelegramUpdates.php index 12b1c20..f6f0cf2 100644 --- a/src/TelegramUpdates.php +++ b/src/TelegramUpdates.php @@ -7,8 +7,7 @@ */ class TelegramUpdates { - /** @var array Params payload. */ - protected array $payload = []; + public function __construct(protected array $payload = []) {} public static function create(): self { diff --git a/src/Traits/HasSharedLogic.php b/src/Traits/HasSharedLogic.php index fba6ee9..fbb7a55 100644 --- a/src/Traits/HasSharedLogic.php +++ b/src/Traits/HasSharedLogic.php @@ -1,35 +1,50 @@ Params payload */ protected array $payload = []; - /** @var array Keyboard Buttons. */ + /** @var array> Keyboard Buttons */ protected array $keyboards = []; - /** @var array Inline Keyboard Buttons. */ + /** @var array> Inline Keyboard Buttons */ protected array $buttons = []; + /** @var bool|null Condition for sending the message */ + private ?bool $sendCondition = null; + + /** @var Closure|null Callback function to handle exceptions */ + public ?Closure $exceptionHandler = null; + /** - * Recipient's Chat ID. + * Set the recipient's Chat ID. * + * @param int|string $chatId The unique identifier for the target chat * @return static */ - public function to(int|string $chatId): self + public function to(int|string $chatId): static { $this->payload['chat_id'] = $chatId; @@ -37,14 +52,13 @@ public function to(int|string $chatId): self } /** - * sets reply markup for payload - * + * Set the keyboard markup for the message. * + * @param array $markup The keyboard markup array + * @throws JsonException When JSON encoding fails * @return static - * - * @throws \JsonException */ - public function keyboardMarkup(array $markup): self + public function keyboardMarkup(array $markup): static { $this->payload['reply_markup'] = json_encode($markup, JSON_THROW_ON_ERROR); @@ -52,11 +66,11 @@ public function keyboardMarkup(array $markup): self } /** - * unsets parse mode of the message. + * Unset parse mode of the message. * * @return static */ - public function normal() + public function normal(): static { unset($this->payload['parse_mode']); @@ -64,17 +78,14 @@ public function normal() } /** - * Sets parse mode of the message. + * Set the parse mode of the message. * + * @param ParseMode|null $mode The parse mode to use * @return static */ - public function parseMode(?string $mode = null) + public function parseMode(?ParseMode $mode = null): static { - if (isset($mode) and ! in_array($mode, $allowed = ['Markdown', 'HTML', 'MarkdownV2'])) { - throw new InvalidArgumentException("Invalid aggregate type [$mode], allowed types: [".implode(', ', $allowed).'].'); - } - - $this->payload['parse_mode'] = $mode; + $this->payload['parse_mode'] = $mode?->value; return $this; } @@ -82,79 +93,108 @@ public function parseMode(?string $mode = null) /** * Add a normal keyboard button. * + * @param string $text The text to display on the button + * @param int $columns Number of columns for button layout + * @param bool $requestContact Whether to request user's contact + * @param bool $requestLocation Whether to request user's location + * @throws JsonException When JSON encoding fails * @return static - * - * @throws \JsonException */ - public function keyboard(string $text, int $columns = 2, bool $request_contact = false, bool $request_location = false): self - { - $this->keyboards[] = compact('text', 'request_contact', 'request_location'); + public function keyboard( + string $text, + int $columns = 2, + bool $requestContact = false, + bool $requestLocation = false + ): static { + $this->keyboards[] = [ + 'text' => $text, + 'request_contact' => $requestContact, + 'request_location' => $requestLocation, + ]; $this->keyboardMarkup([ 'keyboard' => array_chunk($this->keyboards, $columns), - 'one_time_keyboard' => true, // Hide the keyboard after the user makes a selection - 'resize_keyboard' => true, // Allow the keyboard to be resized + 'one_time_keyboard' => true, + 'resize_keyboard' => true, ]); return $this; } /** - * Add an inline button. + * Add an inline button with URL. * + * @param string $text The text to display on the button + * @param string $url The URL to open when button is pressed + * @param int $columns Number of columns for button layout + * @throws JsonException When JSON encoding fails * @return static - * - * @throws \JsonException */ - public function button(string $text, string $url, int $columns = 2): self + public function button(string $text, string $url, int $columns = 2): static { $this->buttons[] = compact('text', 'url'); - $this->keyboardMarkup([ - 'inline_keyboard' => array_chunk($this->buttons, $columns), - ]); - - return $this; + return $this->updateInlineKeyboard($columns); } /** - * Add an inline button with callback_data. + * Add an inline button with callback data. * + * @param string $text The text to display on the button + * @param string $callbackData The data to send when button is pressed + * @param int $columns Number of columns for button layout + * @throws JsonException When JSON encoding fails * @return static - * - * @throws \JsonException */ - public function buttonWithCallback(string $text, string $callback_data, int $columns = 2): self + public function buttonWithCallback(string $text, string $callbackData, int $columns = 2): static { - $this->buttons[] = compact('text', 'callback_data'); + $this->buttons[] = [ + 'text' => $text, + 'callback_data' => $callbackData, + ]; - $this->keyboardMarkup([ - 'inline_keyboard' => array_chunk($this->buttons, $columns), - ]); + return $this->updateInlineKeyboard($columns); + } - return $this; + /** + * Add an inline button with web app. + * + * @param string $text The text to display on the button + * @param string $url The URL of the Web App to open + * @param int $columns Number of columns for button layout + * @throws JsonException When JSON encoding fails + * @return static + */ + public function buttonWithWebApp(string $text, string $url, int $columns = 2): static + { + $this->buttons[] = [ + 'text' => $text, + 'web_app' => ['url' => $url], + ]; + + return $this->updateInlineKeyboard($columns); } /** - * Send the message silently. - * Users will receive a notification with no sound. + * Send the message silently. Users will receive a notification with no sound. * + * @param bool $disable Whether to disable the notification sound * @return static */ - public function disableNotification(bool $disableNotification = true): self + public function disableNotification(bool $disable = true): static { - $this->payload['disable_notification'] = $disableNotification; + $this->payload['disable_notification'] = $disable; return $this; } /** - * Bot Token. - * Overrides default bot token with the given value for this notification. + * Set the Bot Token. Overrides default bot token with the given value for this notification. * + * @param string $token The bot token to use * @return static */ - public function token(string $token): self + public function token(string $token): static { $this->token = $token; @@ -163,6 +203,8 @@ public function token(string $token): self /** * Determine if bot token is given for this notification. + * + * @return bool */ public function hasToken(): bool { @@ -170,29 +212,73 @@ public function hasToken(): bool } /** - * Additional options to pass to sendMessage method. + * Set additional options to pass to sendMessage method. * + * @param array $options Additional options * @return static */ - public function options(array $options): self + public function options(array $options): static { - $this->payload = array_merge($this->payload, $options); + $this->payload = [...$this->payload, ...$options]; return $this; } + /** + * Registers a callback function to handle exceptions. + * + * This method allows you to define a custom error handler, + * which will be invoked if an exception occurs during the + * notification process. The callback must be a valid Closure. + * + * @param Closure $callback The closure that will handle exceptions. + * @return self + */ + public function onError(Closure $callback): self + { + $this->exceptionHandler = $callback; + + return $this; + } + + /** + * Set a condition for sending the message. + * + * @param bool|callable $condition The condition to evaluate + * @return static + */ + public function sendWhen(bool|callable $condition): static + { + $this->sendCondition = $this->when($condition, fn() => true, fn() => false); + + return $this; + } + + /** + * Determine if the message can be sent based on the condition. + * + * @return bool + */ + public function canSend(): bool + { + return $this->sendCondition ?? true; + } + /** * Determine if chat id is not given. + * + * @return bool */ public function toNotGiven(): bool { - return ! isset($this->payload['chat_id']); + return !isset($this->payload['chat_id']); } /** * Get payload value for given key. * - * @return null|mixed + * @param string $key The key to retrieve from payload + * @return mixed The value from payload or null if not found */ public function getPayloadValue(string $key): mixed { @@ -200,7 +286,9 @@ public function getPayloadValue(string $key): mixed } /** - * Returns params payload. + * Get the complete payload as array. + * + * @return array */ public function toArray(): array { @@ -209,9 +297,25 @@ public function toArray(): array /** * Convert the object into something JSON serializable. + * + * @return array */ public function jsonSerialize(): array { return $this->toArray(); } + + /** + * Update the inline keyboard markup. + * + * @param int $columns Number of columns for button layout + * @throws JsonException When JSON encoding fails + * @return static + */ + private function updateInlineKeyboard(int $columns): static + { + return $this->keyboardMarkup([ + 'inline_keyboard' => array_chunk($this->buttons, $columns), + ]); + } } diff --git a/tests/Feature/TelegramMessageTest.php b/tests/Feature/TelegramMessageTest.php index 43fd5d3..63cbdff 100644 --- a/tests/Feature/TelegramMessageTest.php +++ b/tests/Feature/TelegramMessageTest.php @@ -155,14 +155,14 @@ }); test('a request phone keyboard button can be added to the message', function () { - $message = TelegramMessage::create()->keyboard('Laravel', request_contact: true); + $message = TelegramMessage::create()->keyboard('Laravel', requestContact: true); expect($message->getPayloadValue('reply_markup'))->toEqual( '{"keyboard":[[{"text":"Laravel","request_contact":true,"request_location":false}]],"one_time_keyboard":true,"resize_keyboard":true}' ); }); test('a request location keyboard button can be added to the message', function () { - $message = TelegramMessage::create()->keyboard('Laravel', request_location: true); + $message = TelegramMessage::create()->keyboard('Laravel', requestLocation: true); expect($message->getPayloadValue('reply_markup'))->toEqual( '{"keyboard":[[{"text":"Laravel","request_contact":false,"request_location":true}]],"one_time_keyboard":true,"resize_keyboard":true}' );