Improve adapters datas interface by defining fields and implement real OVH SMS API adapter !

This commit is contained in:
osaajani 2020-01-09 22:23:58 +01:00
parent 4cd1105ae6
commit c4bc7d94c1
7 changed files with 591 additions and 48 deletions

View File

@ -31,6 +31,12 @@
* Description of the datas expected by the adapter to help the user. (e.g : A list of expecteds Api credentials fields, with name and value)
*/
public static function meta_datas_help() : string;
/**
* List of entries we want in datas for the adapter
* @return array : Eachline line is a field as an array with keys : name, title, description, required
*/
public static function meta_datas_fields() : array;
/**
* Does the implemented service support flash smss
@ -58,7 +64,7 @@
* @param bool $flash : Is the SMS a Flash SMS
* @return mixed Uid of the sended message if send, False else
*/
public function send (string $destination, string $text, bool $flash);
public function send (string $destination, string $text, bool $flash = false);
/**
@ -68,6 +74,14 @@
public function read () : array;
/**
* Method called to verify if the adapter is working correctly
* should be use for exemple to verify that credentials and number are both valid
* @return boolean : False on error, true else
*/
public function test () : bool;
/**
* Method called on reception of a status update notification for a SMS
* @return mixed : False on error, else array ['uid' => uid of the sms, 'status' => New status of the sms ('unknown', 'delivered', 'failed')]

View File

@ -1,5 +1,6 @@
<?php
namespace adapters;
use \Ovh\Api;
/**
* Interface for phones adapters
@ -30,7 +31,47 @@
/**
* Description of the datas expected by the adapter to help the user. (e.g : A list of expecteds Api credentials fields, with name and value)
*/
public static function meta_datas_help() : string { return 'See later.'; }
public static function meta_datas_help() : string { return 'Clefs API OVH, https://api.ovh.com/createToken/index.cgi.'; }
/**
* List of entries we want in datas for the adapter
* @return array : Every line is a field as an array with keys : name, title, description, required
*/
public static function meta_datas_fields() : array
{
return [
[
'name' => 'app_key',
'title' => 'Application Key',
'description' => 'Paramètre "Application Key" obtenu lors de la génération de la clef API OVH.',
'required' => true,
],
[
'name' => 'app_secret',
'title' => 'Application Secret',
'description' => 'Paramètre "Application Secret" obtenu lors de la génération de la clef API OVH.',
'required' => true,
],
[
'name' => 'consumer_key',
'title' => 'Consumer Key',
'description' => 'Paramètre "Consumer Key" obtenu lors de la génération de la clef API OVH.',
'required' => true,
],
[
'name' => 'endpoint',
'title' => 'Endpoint',
'description' => 'Endpoint de l\'API OVH, voir https://github.com/ovh/php-ovh/#supported-apis.',
'required' => true,
],
[
'name' => 'service_name',
'title' => 'Service Name',
'description' => 'Service Name de votre service SMS chez OVH. Il s\'agit du nom associé à votre service SMS dans la console OVH, probablement quelque chose comme "sms-xxxxx-1" ou "xxxx" est votre identifiant client OVH.',
'required' => true,
],
];
}
/**
* Does the implemented service support flash smss
@ -53,6 +94,15 @@
*/
private $datas;
/**
* OVH Api instance
*/
private $api;
/**
* Number formated to be compatible with http query according to the ovh way
*/
private $formatted_number;
/**
* Adapter constructor, called when instanciated by RaspiSMS
@ -62,7 +112,15 @@
public function __construct (string $number, string $datas)
{
$this->number = $number;
$this->datas = $datas;
$this->formatted_number = str_replace('+', '00', $number);
$this->datas = json_decode($datas, true);
$this->api = new Api(
$this->datas['app_key'],
$this->datas['app_secret'],
$this->datas['endpoint'],
$this->datas['consumer_key']
);
}
@ -73,9 +131,33 @@
* @param bool $flash : Is the SMS a Flash SMS
* @return mixed Uid of the sended message if send, False else
*/
public function send (string $destination, string $text, bool $flash)
public function send (string $destination, string $text, bool $flash = false)
{
return uniqid();
try
{
$success = true;
$endpoint = '/sms/' . $this->datas['service_name'] . '/virtualNumbers/' . $this->formatted_number . '/jobs';
$params = [
'message' => $text,
'receivers' => [$destination],
];
$response = $this->api->post($endpoint, $params);
$nb_invalid_receivers = count(($response['invalidReceivers'] ?? []));
if ($nb_invalid_receivers > 0)
{
return false;
}
$uids = $response['ids'] ?? [];
return ($uids[0] ?? false);
}
catch (\Exception $e)
{
return false;
}
}
@ -85,7 +167,77 @@
*/
public function read () : array
{
return [];
try
{
$success = true;
$endpoint = '/sms/' . $this->datas['service_name'] . '/virtualNumbers/' . $this->formatted_number . '/incoming';
$uids = $this->api->get($endpoint);
if (!is_array($uids) || !$uids)
{
return [];
}
$received_smss = [];
foreach ($uids as $uid)
{
$endpoint = '/sms/' . $this->datas['service_name'] . '/virtualNumbers/' . $this->formatted_number . '/incoming/' . $uid;
$sms_details = $this->api->get($endpoint);
if (!isset($sms_details['creationDatetime'], $sms_details['message'], $sms_details['sender']))
{
continue;
}
$received_smss[] = [
'at' => (new \DateTime($sms_details['creationDatetime']))->format('Y-m-d H:i:s'),
'text' => $sms_details['message'],
'origin' => $sms_details['sender'],
'destination' => $this->number,
];
//Remove the sms to prevent double reading as ovh do not offer a filter for unread messages only
$endpoint = '/sms/' . $this->datas['service_name'] . '/virtualNumbers/' . $this->formatted_number . '/incoming/' . $uid;
$this->api->delete($endpoint);
}
return $received_smss;
}
catch (\Exception $e)
{
return [];
}
}
/**
* Method called to verify if the adapter is working correctly
* should be use for exemple to verify that credentials and number are both valid
* @return boolean : False on error, true else
*/
public function test () : bool
{
try
{
$success = true;
//Check service name
$endpoint = '/sms/' . $this->datas['service_name'];
$response = $this->api->get($endpoint);
$success = $success && (bool) $response;
//Check virtualnumber
$endpoint = '/sms/virtualNumbers/' . $this->formatted_number;
$response = $this->api->get($endpoint);
$success = $success && (bool) $response;
return $success;
}
catch (\Exception $e)
{
return false;
}
}
@ -95,6 +247,30 @@
*/
public static function status_change_callback ()
{
return false;
$uid = $_GET['id'] ?? false;
$dlr = $_GET['dlr'] ?? false;
if ($uid === false || $dlr ==== false)
{
return false;
}
switch ($dlr)
{
case 1:
$status = 'delivered';
break;
case 2:
case 16:
$status = 'failed';
break;
default:
$status = 'unknown';
break;
}
return ['uid' => $uid, 'status' => $status];
}
}

View File

@ -31,6 +31,12 @@
* Description of the datas expected by the adapter to help the user. (e.g : A list of expecteds Api credentials fields, with name and value)
*/
public static function meta_datas_help() : string { return 'No datas.'; }
/**
* List of entries we want in datas for the adapter
* @return array : Eachline line is a field as an array with keys : name, title, description, required
*/
public static function meta_datas_fields() : array { return []; }
/**
* Does the implemented service support flash smss
@ -84,7 +90,7 @@
* @param bool $flash : Is the SMS a Flash SMS
* @return mixed Uid of the sended message if send, False else
*/
public function send (string $destination, string $text, bool $flash)
public function send (string $destination, string $text, bool $flash = false)
{
$uid = uniqid();
@ -125,6 +131,17 @@
}
/**
* Method called to verify if the adapter is working correctly
* should be use for exemple to verify that credentials and number are both valid
* @return boolean : False on error, true else
*/
public function test () : bool
{
return true;
}
/**
* Method called on reception of a status update notification for a SMS
* @return mixed : False on error, else array ['uid' => uid of the sms, 'status' => New status of the sms ('unknown', 'delivered', 'failed')]

View File

@ -6,7 +6,8 @@
"twig/twig": "^3.0",
"symfony/expression-language": "^5.0",
"robmorgan/phinx": "^0.11.1",
"monolog/monolog": "^2.0"
"monolog/monolog": "^2.0",
"ovh/ovh": "^2.0"
},
"require-dev": {
}

316
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"content-hash": "2c1bb9c8a11ff3457ba8db4d5d1d33ae",
"content-hash": "c1b16bab1d0052b5ccb38c18a08ee0d2",
"packages": [
{
"name": "ajani/flash-message",
@ -495,6 +495,195 @@
"description": "Locale functions required by libphonenumber-for-php",
"time": "2019-10-09T18:53:14+00:00"
},
{
"name": "guzzlehttp/guzzle",
"version": "6.5.2",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
"reference": "43ece0e75098b7ecd8d13918293029e555a50f82"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/43ece0e75098b7ecd8d13918293029e555a50f82",
"reference": "43ece0e75098b7ecd8d13918293029e555a50f82",
"shasum": ""
},
"require": {
"ext-json": "*",
"guzzlehttp/promises": "^1.0",
"guzzlehttp/psr7": "^1.6.1",
"php": ">=5.5"
},
"require-dev": {
"ext-curl": "*",
"phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0",
"psr/log": "^1.1"
},
"suggest": {
"ext-intl": "Required for Internationalized Domain Name (IDN) support",
"psr/log": "Required for using the Log middleware"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "6.5-dev"
}
},
"autoload": {
"psr-4": {
"GuzzleHttp\\": "src/"
},
"files": [
"src/functions_include.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
}
],
"description": "Guzzle is a PHP HTTP client library",
"homepage": "http://guzzlephp.org/",
"keywords": [
"client",
"curl",
"framework",
"http",
"http client",
"rest",
"web service"
],
"time": "2019-12-23T11:57:10+00:00"
},
{
"name": "guzzlehttp/promises",
"version": "v1.3.1",
"source": {
"type": "git",
"url": "https://github.com/guzzle/promises.git",
"reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646",
"reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646",
"shasum": ""
},
"require": {
"php": ">=5.5.0"
},
"require-dev": {
"phpunit/phpunit": "^4.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.4-dev"
}
},
"autoload": {
"psr-4": {
"GuzzleHttp\\Promise\\": "src/"
},
"files": [
"src/functions_include.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
}
],
"description": "Guzzle promises library",
"keywords": [
"promise"
],
"time": "2016-12-20T10:07:11+00:00"
},
{
"name": "guzzlehttp/psr7",
"version": "1.6.1",
"source": {
"type": "git",
"url": "https://github.com/guzzle/psr7.git",
"reference": "239400de7a173fe9901b9ac7c06497751f00727a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/239400de7a173fe9901b9ac7c06497751f00727a",
"reference": "239400de7a173fe9901b9ac7c06497751f00727a",
"shasum": ""
},
"require": {
"php": ">=5.4.0",
"psr/http-message": "~1.0",
"ralouphie/getallheaders": "^2.0.5 || ^3.0.0"
},
"provide": {
"psr/http-message-implementation": "1.0"
},
"require-dev": {
"ext-zlib": "*",
"phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8"
},
"suggest": {
"zendframework/zend-httphandlerrunner": "Emit PSR-7 responses"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.6-dev"
}
},
"autoload": {
"psr-4": {
"GuzzleHttp\\Psr7\\": "src/"
},
"files": [
"src/functions_include.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
},
{
"name": "Tobias Schultze",
"homepage": "https://github.com/Tobion"
}
],
"description": "PSR-7 message implementation that also provides common utility methods",
"keywords": [
"http",
"message",
"psr-7",
"request",
"response",
"stream",
"uri",
"url"
],
"time": "2019-07-01T23:21:34+00:00"
},
{
"name": "ingenerator/tokenista",
"version": "v1.1.0",
@ -629,6 +818,41 @@
],
"time": "2019-11-13T10:27:43+00:00"
},
{
"name": "ovh/ovh",
"version": "v2.0.1",
"source": {
"type": "git",
"url": "https://github.com/ovh/php-ovh.git",
"reference": "25a97501a150dff8a7848c372b5826395aab4df2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ovh/php-ovh/zipball/25a97501a150dff8a7848c372b5826395aab4df2",
"reference": "25a97501a150dff8a7848c372b5826395aab4df2",
"shasum": ""
},
"require": {
"guzzlehttp/guzzle": "^6.0"
},
"require-dev": {
"phpdocumentor/phpdocumentor": "2.*",
"phpunit/phpunit": "4.*",
"squizlabs/php_codesniffer": "1.*"
},
"type": "library",
"autoload": {
"psr-4": {
"Ovh\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"description": "Wrapper for OVH APIs",
"time": "2016-02-10T13:16:38+00:00"
},
{
"name": "psr/cache",
"version": "1.0.1",
@ -724,6 +948,56 @@
],
"time": "2017-02-14T16:28:37+00:00"
},
{
"name": "psr/http-message",
"version": "1.0.1",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-message.git",
"reference": "f6561bf28d520154e4b0ec72be95418abe6d9363"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363",
"reference": "f6561bf28d520154e4b0ec72be95418abe6d9363",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Http\\Message\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"description": "Common interface for HTTP messages",
"homepage": "https://github.com/php-fig/http-message",
"keywords": [
"http",
"http-message",
"psr",
"psr-7",
"request",
"response"
],
"time": "2016-08-06T14:39:51+00:00"
},
{
"name": "psr/log",
"version": "1.1.2",
@ -819,6 +1093,46 @@
],
"time": "2017-10-23T01:57:42+00:00"
},
{
"name": "ralouphie/getallheaders",
"version": "3.0.3",
"source": {
"type": "git",
"url": "https://github.com/ralouphie/getallheaders.git",
"reference": "120b605dfeb996808c31b6477290a714d356e822"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822",
"reference": "120b605dfeb996808c31b6477290a714d356e822",
"shasum": ""
},
"require": {
"php": ">=5.6"
},
"require-dev": {
"php-coveralls/php-coveralls": "^2.1",
"phpunit/phpunit": "^5 || ^6.5"
},
"type": "library",
"autoload": {
"files": [
"src/getallheaders.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ralph Khattar",
"email": "ralph.khattar@gmail.com"
}
],
"description": "A polyfill for getallheaders.",
"time": "2019-03-08T08:55:37+00:00"
},
{
"name": "robmorgan/phinx",
"version": "0.11.1",

View File

@ -101,7 +101,7 @@ class Phone extends \descartes\Controller
* @param $csrf : CSRF token
* @param string $_POST['number'] : Phone number
* @param string $_POST['adapter'] : Phone adapter
* @param string $_POST['adapter_datas'] : Phone adapter datas
* @param array $_POST['adapter_datas'] : Phone adapter datas
*/
public function create($csrf)
{
@ -114,7 +114,7 @@ class Phone extends \descartes\Controller
$id_user = $_SESSION['user']['id'];
$number = $_POST['number'] ?? false;
$adapter = $_POST['adapter'] ?? false;
$adapter_datas = !empty($_POST['adapter_datas']) ? $_POST['adapter_datas'] : '{}';
$adapter_datas = !empty($_POST['adapter_datas']) ? $_POST['adapter_datas'] : [];
if (!$number || !$adapter)
{
@ -139,26 +139,49 @@ class Phone extends \descartes\Controller
$adapters = $this->internal_adapter->list_adapters();
$adapter_exists = false;
$find_adapter = false;
foreach ($adapters as $metas)
{
if ($metas['meta_classname'] === $adapter)
{
$adapter_exists = true;
$find_adapter = $metas;
break;
}
}
if (!$adapter_exists)
if (!$find_adapter)
{
\FlashMessage\FlashMessage::push('danger', 'Cet adaptateur n\'existe pas.');
return $this->redirect(\descartes\Router::url('Phone', 'add'));
}
if (NULL === json_decode($adapter_datas))
//If missing required data fields, error
foreach ($find_adapter['meta_datas_fields'] as $field)
{
\FlashMessage\FlashMessage::push('danger', 'La chaîne de configuration n\'est pas valide.');
if ($field['required'] === false)
{
continue;
}
if (!empty($adapter_datas[$field['name']]))
{
continue;
}
\FlashMessage\FlashMessage::push('danger', 'Vous n\'avez pas rempli certains champs obligatoires pour l\'adaptateur choisis.');
return $this->redirect(\descartes\Router::url('Phone', 'add'));
}
$adapter_datas = json_encode($adapter_datas);
//Check adapter is working correctly with thoses numbers and datas
$adapter_classname = $find_adapter['meta_classname'];
$adapter_instance = new $adapter_classname($number, $adapter_datas);
$adapter_working = $adapter_instance->test();
if (!$adapter_working)
{
\FlashMessage\FlashMessage::push('danger', 'Impossible d\'utiliser l\'adaptateur choisis avec les données fournies. Vérifiez le numéro de téléphone et les réglages.');
return $this->redirect(\descartes\Router::url('Phone', 'add'));
}

View File

@ -59,20 +59,15 @@
title="<?php $this->s($adapter['meta_description']); ?>"
data-description="<?php $this->s($adapter['meta_description']); ?>"
data-datas-help="<?php $this->s($adapter['meta_datas_help']); ?>"
data-datas-fields="<?php $this->s(json_encode($adapter['meta_datas_fields'])); ?>"
>
<?php $this->s($adapter['meta_name']); ?>
</option>
<?php } ?>
</select>
</div>
<div class="form-group" id="adapter-datas-container">
<label>Configuration de l'adaptateur</label>
<p class="italic small help" id="description-adapter-datas">
Les données à fournir à l'adaptateur pour lui permettre de faire la liaison avec le téléphone. Par exemple des identifiants d'API.<br/>
</p>
<textarea id="adapter-datas" name="adapter_datas" class="form-control has-error"></textarea>
<p class="hidden help-block" id="adapter-datas-error-message">La configuration doit être un JSON valide.</p>
</div>
<div id="adapter-datas-fields-container">
</div>
<a class="btn btn-danger" href="<?php echo \descartes\Router::url('Phone', 'list'); ?>">Annuler</a>
<input type="submit" class="btn btn-success" value="Enregistrer le phone" />
</form>
@ -84,36 +79,39 @@
</div>
</div>
<script>
jQuery('document').ready(function($)
function change_adapter ()
{
var option = jQuery('#adapter-select').find('option:selected');
jQuery('#description-adapter').text(option.attr('data-description'));
jQuery('#description-adapter-datas').text(option.attr('data-datas-help'));
jQuery('#adapter-select').on('change', function (e)
var datas_fields = option.attr('data-datas-fields');
datas_fields = JSON.parse(datas_fields);
var html = '';
jQuery.each(datas_fields, function (index, field)
{
var option = jQuery(this).find('option:selected');
jQuery('#description-adapter').text(option.attr('data-description'));
jQuery('#description-adapter-datas').text(option.attr('data-datas-help'));
html += '<div class="form-group">' +
'<label>' + field.title + '</label>' +
'<p class="italic small help">' + field.description + '</p>' +
'<div class="form-group">' +
'<input name="adapter_datas[' + field.name + ']" class="form-control" ' + (field.required ? 'required' : '') + ' >' +
'</div>' +
'</div>';
});
jQuery('#adapter-datas').on('input', function (e)
{
try
{
if (jQuery(this).val() !== '')
{
JSON.parse(jQuery(this).val());
}
jQuery('#adapter-datas-fields-container').html(html);
}
jQuery('#adapter-datas-container').removeClass('has-error');
jQuery('#adapter-datas-error-message').addClass('hidden');
}
catch (err)
{
jQuery('#adapter-datas-container').addClass('has-error');
jQuery('#adapter-datas-error-message').removeClass('hidden');
}
jQuery('document').ready(function($)
{
change_adapter();
jQuery('#adapter-select').on('change', function (e)
{
change_adapter();
});
var number_input = jQuery('#phone-international-input')[0];