diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a7c44dd --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +indent_size = 4 +indent_style = space +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..9e9519b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,19 @@ +# Path-based git attributes +# https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html + +# Ignore all test and documentation with "export-ignore". +/.github export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/phpunit.xml.dist export-ignore +/art export-ignore +/docs export-ignore +/tests export-ignore +/.editorconfig export-ignore +/.php_cs.dist.php export-ignore +/psalm.xml export-ignore +/psalm.xml.dist export-ignore +/testbench.yaml export-ignore +/UPGRADING.md export-ignore +/phpstan.neon.dist export-ignore +/phpstan-baseline.neon export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..83c9b9f --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +.idea +.phpunit.result.cache +build +composer.lock +coverage +docs +phpunit.xml +phpstan.neon +testbench.yaml +vendor +node_modules diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8c002f9 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +### v1.0 + +Initial release diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..b4915ba --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) niladam + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +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 new file mode 100644 index 0000000..3da936a --- /dev/null +++ b/README.md @@ -0,0 +1,166 @@ +# Laravel SendSMS integration + +[![Latest Version on Packagist](https://img.shields.io/packagist/v/niladam/laravel-sendsms.svg?style=flat-square)](https://packagist.org/packages/niladam/laravel-sendsms) +[![GitHub Tests Action Status](https://img.shields.io/github/workflow/status/niladam/laravel-sendsms/run-tests?label=tests)](https://github.com/niladam/laravel-sendsms/actions?query=workflow%3Arun-tests+branch%3Amain) +[![GitHub Code Style Action Status](https://img.shields.io/github/workflow/status/niladam/laravel-sendsms/Check%20&%20fix%20styling?label=code%20style)](https://github.com/niladam/laravel-sendsms/actions?query=workflow%3A"Check+%26+fix+styling"+branch%3Amain) +[![Total Downloads](https://img.shields.io/packagist/dt/niladam/laravel-sendsms.svg?style=flat-square)](https://packagist.org/packages/niladam/laravel-sendsms) + +Just a small Laravel package that allows you to use [sendsms.ro](https://www.sendsms.ro/ro/) API. + +## Installation + +You can install the package via composer: + +```bash +composer require niladam/laravel-sendsms +``` + +You can publish the config file with: + +```bash +php artisan vendor:publish --provider="Niladam\LaravelSendsms\LaravelSendsmsServiceProvider" --tag=config +``` + +This is the contents of the published config file: + +```php + env("LARAVEL_SENDSMS_USERNAME", null), + + /** + * This is your main password. + */ + "password" => env("LARAVEL_SENDSMS_PASSWORD", null), + + /** + * This is the base URL that the package will use. + * + * It has already been filled with a default value. + * + */ + "url" => env("LARAVEL_SENDSMS_URL", "https://api.sendsms.ro/json"), + + /** + * If this package should have debug turned on + * please set this here. + * + */ + "debug" => env("LARAVEL_SENDSMS_DEBUG", false), + + "messages" => [ + "from" => env("LARAVEL_SENDSMS_FROM", null), + "callback_url" => env("LARAVEL_SENDSMS_CALLBACK", null), + "charset" => env("LARAVEL_SENDSMS_CHARSET", null), + "coding" => env("LARAVEL_SENDSMS_CODING", null), + "class" => env("LARAVEL_SENDSMS_CLASS", -1), + "auto_detect_encoding" => env( + "LARAVEL_SENDSMS_AUTODETECT_ENCODING", + null + ), + /** + * Information on the report mask: + * + * 1 Delivered + * 2 Undelivered + * 4 Queued at network + * 8 Sent to network + * 16 Failed at network + * + * So, 19 means: + * + * (Delivered + Undelivered + Failed at network) + * 1 + 2 + 16 = 19 + */ + "report_mask" => env("LARAVEL_SENDSMS_MASK", 19) + ], + + /** + * This is basically a mapping of the operations + * that the API will use. + * + */ + "operations" => [ + "balance" => "user_get_balance", + "ping" => "ping", + "price" => "route_check_price", + "info" => "user_get_info", + "number" => "user_get_phone_number", + "send" => "message_send", + ], +]; +``` + + +## Usage + +```php +use Niladam\LaravelSendsms\SendSmsMessage; + +$message = SendSmsMessage::create(); + +$message->to('0744123123') + ->message('Example message here.') + // You can also use the alias ->text('example text') + // the following is optional, as it'll use your default settings + ->from('0744123456') + ->send(); +``` + +OR + +```php +use Niladam\LaravelSendsms\SendSmsMessage; + +SendSmsMessage::create() + ->to('0744123123') + // You can also use the alias ->text('example text') + ->message('Example message here.') + // the following is optional, as it'll use your default settings + ->from('0744123456') + ->send(); +``` + +OR + +```php +SendSmsMessage::create(to: 0744123123, message: 'Example message here')->send(); + +// Or by specifying the from. + +SendSmsMessage::create(to: '0744123123', message: 'Example message here', from: '0744123456')->send(); +``` + +### Command +```shell +# The package also publishes a command with the following signature: +# laravel:sendsms {to?} {message?} {from?} +# so you can use it with your tinker. +# +php artisan laravel:sendsms +# +# You will be asked to provide the required details. + +# Or you can easily provide them yourself. +php artisan laravel:sendsms "0744123123" "Example message here." "0744123456" +``` + +## Changelog + +Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. + +## Credits + +- [Madalin Tache](https://github.com/niladam) + +## License + +The MIT License (MIT). Please see [License File](LICENSE.md) for more information. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..fad02b4 --- /dev/null +++ b/composer.json @@ -0,0 +1,47 @@ +{ + "name": "niladam/laravel-sendsms", + "description": "Laravel SendSMS integration", + "keywords": [ + "niladam", + "laravel", + "laravel-sendsms" + ], + "homepage": "https://github.com/niladam/laravel-sendsms", + "license": "MIT", + "authors": [ + { + "name": "Madalin Tache", + "email": "madalin@madalin.eu", + "role": "Developer" + } + ], + "require": { + "php": "^8.1", + "illuminate/http": "^8.0|^9.0" + }, + "autoload": { + "psr-4": { + "Niladam\\LaravelSendsms\\": "src" + } + }, + "config": { + "sort-packages": true, + "allow-plugins": { + "pestphp/pest-plugin": true, + "phpstan/extension-installer": true + } + }, + "extra": { + "laravel": { + "providers": [ + "Niladam\\LaravelSendsms\\LaravelSendsmsServiceProvider" + ], + "aliases": { + "LaravelSendsms": "Niladam\\LaravelSendsms\\Facades\\LaravelSendsms", + "SendSmsMessage": "Niladam\\LaravelSendsms\\SendSmsMessage\\" + } + } + }, + "minimum-stability": "dev", + "prefer-stable": true +} diff --git a/config/sendsms.php b/config/sendsms.php new file mode 100644 index 0000000..216c511 --- /dev/null +++ b/config/sendsms.php @@ -0,0 +1,69 @@ + env('LARAVEL_SENDSMS_USERNAME', null), + + /** + * This is your main password. + */ + 'password' => env('LARAVEL_SENDSMS_PASSWORD', null), + + /** + * This is the base URL that the package will use. + * + * It has already been filled with a default value. + */ + 'url' => env('LARAVEL_SENDSMS_URL', 'https://api.sendsms.ro/json'), + + /** + * If this package should have debug turned on + * please set this here. + */ + 'debug' => env('LARAVEL_SENDSMS_DEBUG', false), + + 'messages' => [ + 'from' => env('LARAVEL_SENDSMS_FROM', null), + 'callback_url' => env('LARAVEL_SENDSMS_CALLBACK', null), + 'charset' => env('LARAVEL_SENDSMS_CHARSET', null), + 'coding' => env('LARAVEL_SENDSMS_CODING', null), + 'class' => env('LARAVEL_SENDSMS_CLASS', -1), + 'auto_detect_encoding' => env( + 'LARAVEL_SENDSMS_AUTODETECT_ENCODING', + null + ), + /** + * Information on the report mask: + * + * 1 Delivered + * 2 Undelivered + * 4 Queued at network + * 8 Sent to network + * 16 Failed at network + * + * So, 19 means: + * + * (Delivered + Undelivered + Failed at network) + * 1 + 2 + 16 = 19 + */ + 'report_mask' => env('LARAVEL_SENDSMS_MASK', 19), + ], + + /** + * This is basically a mapping of the operations + * that the API will use. + */ + 'operations' => [ + 'balance' => 'user_get_balance', + 'ping' => 'ping', + 'price' => 'route_check_price', + 'info' => 'user_get_info', + 'number' => 'user_get_phone_number', + 'send' => 'message_send', + ], +]; diff --git a/src/Commands/LaravelSendsmsCommand.php b/src/Commands/LaravelSendsmsCommand.php new file mode 100644 index 0000000..548f0d6 --- /dev/null +++ b/src/Commands/LaravelSendsmsCommand.php @@ -0,0 +1,45 @@ +argument("to") ?: + $this->ask("Please enter a destination phone number"); + $message = + $this->argument("message") ?: $this->ask("Please enter a message"); + $from = + $this->argument("from") ?: + $this->ask( + "Please enter a FROM phone number (optional, can be left empty)" + ); + + try { + $message = SendSmsMessage::create( + to: $to, + message: $message, + from: $from + )->send(); + } catch (Exception $exception) { + $this->error($exception->getMessage()); + } + + $this->info("Message sent."); + $this->newLine(2); + $this->info($message); + $this->newLine(2); + } +} diff --git a/src/Exceptions/InValidPhoneNumberProvidedException.php b/src/Exceptions/InValidPhoneNumberProvidedException.php new file mode 100644 index 0000000..4bfd6be --- /dev/null +++ b/src/Exceptions/InValidPhoneNumberProvidedException.php @@ -0,0 +1,9 @@ +username = $this->config["username"]; + $this->password = $this->config["password"]; + $this->url = $this->config["url"]; + $this->operations = $this->config["operations"]; + } + + public function price(string $to = "") + { + throw_if( + !$to, + InValidPhoneNumberProvidedException::class, + "No valid phone number provided." + ); + + $operationName = __FUNCTION__; + + throw_if( + !array_key_exists($operationName, $this->operations), + UnknownOperationException::class, + "No operation called $operationName found." + ); + + $operation = $this->operations[$operationName]; + + $args = [ + "to" => $to, + ]; + + return $this->call_api_action($operation, $args); + } + + public function call_api_action($method, $params): array|string + { + $url = $this->url . "?action=" . urlencode($method); + $url .= "&username=" . urlencode($this->username); + $url .= "&password=" . urlencode($this->password); + + foreach ($params as $key => $value) { + if (is_null($value)) { + continue; + } + + if (is_bool($value)) { + $url .= + "&" . + urlencode($key) . + "=" . + urlencode($value ? "true" : "false"); + } else { + $url .= "&" . urlencode($key) . "=" . urlencode($value); + } + } + + return $this->sendRequest($url); + } + + public function sendRequest($url): array|string + { + try { + $response = Http::post($url) + ->throw() + ->json(); + + return array_merge($response, $this->extractDataFromUrl($url)); + } catch (Exception $exception) { + return $exception->getMessage(); + } + } + + private function extractDataFromUrl(string $url): array + { + $data = []; + + parse_str(parse_url(urldecode($url))["query"], $data); + + return $data["action"] === "message_send" + ? [ + "to" => $data["to"], + "from" => $data["from"], + "message" => $data["text"], + ] + : []; + } + + public function dump($str) + { + if ($this->$debug) { + dump($str); + } + } + + /** + * This action allows you to check the price you can expect to pay for a message to the destination in 'to' + * + * @param string $to : A phone number + * + * @global string $password + * @global string $username + */ + public function route_check_price($to) + { + $args = func_get_args(); + + return $this->call_api_action( + new ReflectionMethod(__CLASS__, __FUNCTION__), + $args + ); + } + + /** + * Gets the user balance + * + * @global string $username + * @global string $password + */ + public function user_get_balance() + { + $args = func_get_args(); + + return $this->call_api_action( + new ReflectionMethod(__CLASS__, __FUNCTION__), + $args + ); + } + + public function __call(string $name, array $arguments) + { + $operationName = $name; + + throw_if( + !array_key_exists($operationName, $this->operations), + UnknownOperationException::class, + "No operation called $operationName found." + ); + + $operation = $this->operations[$operationName]; + + $args = func_get_args() ?: []; + + return $this->call_api_action($operation, $args); + } + + public function balance() + { + $operationName = __FUNCTION__; + + throw_if( + !array_key_exists($operationName, $this->operations), + UnknownOperationException::class, + "No operation called $operationName found." + ); + + $operation = $this->operations[$operationName]; + + $args = func_get_args() ?: []; + + return $this->call_api_action($operation, $args); + } + + /** + * Gets the user details + * + * @global string $username + * @global string $password + */ + public function user_get_info() + { + $args = func_get_args(); + + return $this->call_api_action( + new ReflectionMethod(__CLASS__, __FUNCTION__), + $args + ); + } + + /** + * This function returns the verified phone number for the given user + * + * @global string $username + * @global string $password + */ + public function user_get_phone_number() + { + $args = func_get_args(); + + return $this->call_api_action( + new ReflectionMethod(__CLASS__, __FUNCTION__), + $args + ); + } + + public function send(string $to, string $message, ?string $from = "") + { + $operationName = __FUNCTION__; + + throw_if( + !array_key_exists($operationName, $this->operations), + UnknownOperationException::class, + "No operation called $operationName found." + ); + + $operation = $this->operations[$operationName]; + + throw_if( + !$to, + InValidPhoneNumberProvidedException::class, + "Invalid, or no phone number provided. Got: $to" + ); + + throw_if( + !$message, + InvalidMessageProvidedException::class, + "No message provided." + ); + + $args = [ + "to" => $to, + "text" => $message, + "from" => $from ?: $this->config["messages"]["from"], + "report_mask" => $this->config["messages"]["report_mask"], + ]; + + if ($this->config["messages"]["callback_url"]) { + $args["callback_url"] = $this->config["messages"]["callback_url"]; + } + + return $this->call_api_action($operation, $args); + } + + /** + * Send an SMS message + * + * @param string $to + * @param string $text : The body of your message + * @param string $from (optional): The expeditor's label + * @param int $report_mask (optional): Delivery report request bitmask + * @param string $report_url (optional): URL to call when delivery status changes + * @param string $charset (optional): Character set to use + * @param int $data_coding (optional): Data coding + * @param int $message_class (optional): Message class + * @param int $auto_detect_encoding (optional): Auto detect the encoding and send appropriately 1 = on, 0 = off. + * @param string/boolean $short (optional): 1. "string" Add sort url at the end of message or search for key {short} in message and replace with short url when parameter contain URL + * 2. "boolean" Searches long url and replaces them with coresponding sort url when shrot parameter is "true" + * + * @global string $username + * @global string $password + */ + public function message_send( + $to, + $text, + $from = null, + $report_mask = 19, + $report_url = null, + $charset = null, + $data_coding = null, + $message_class = -1, + $auto_detect_encoding = null, + $short = false + ) { + $args = func_get_args(); + + return $this->call_api_action( + new ReflectionMethod(__CLASS__, __FUNCTION__), + $args + ); + } +} diff --git a/src/LaravelSendsmsServiceProvider.php b/src/LaravelSendsmsServiceProvider.php new file mode 100644 index 0000000..4661136 --- /dev/null +++ b/src/LaravelSendsmsServiceProvider.php @@ -0,0 +1,52 @@ +app->singleton( + LaravelSendsms::class, + fn () => new LaravelSendsms(config('laravel-sendsms')) + ); + + $this->app->bind('laravel-sendsms', LaravelSendsms::class); + + $this->app->singleton("command.sendsms", function () { + return new LaravelSendsmsCommand(); + }); + + $this->commands(["command.sendsms"]); + } + + public function boot() + { + if ($this->app->runningInConsole()) { + $this->publishes( + [ + __DIR__.'/../config/sendsms.php' => config_path( + 'laravel-sendsms.php' + ), + ], + 'config' + ); + // $this->commands([LaravelSendsmsCommand::class]); + } + } + + /** + * Get the services provided by the provider. + * + * @return array + */ + public function provides() + { + return ["command.tinker"]; + } +} diff --git a/src/SendSmsMessage.php b/src/SendSmsMessage.php new file mode 100644 index 0000000..30209c5 --- /dev/null +++ b/src/SendSmsMessage.php @@ -0,0 +1,69 @@ +to = $to; + + return $this; + } + + public function message(string $message): self + { + $this->message = $message; + + return $this; + } + + public function text(string $text): self + { + $this->message = $text; + + return $this; + } + + public function from(string $from): self + { + $this->from = $from; + + return $this; + } + + /** + * @return array|string + */ + public function send(): array|string + { + throw_if( + ! $this->to || ! $this->message, + InvalidRequiredParametersException::class, + "Unable to send message, as the required parameters as invalid: Destination: {$this->to} / Message: {$this->message}" + ); + + return app(LaravelSendsms::class)->send( + $this->to, + $this->message, + $this->from + ); + } +}