Compare commits
11 Commits
064d6fd941
...
473fb297f3
Author | SHA1 | Date |
---|---|---|
osaajani | 473fb297f3 | |
osaajani | 36c5d7ec0c | |
osaajani | 5c697b5240 | |
osaajani | 2be8242d5e | |
osaajani | 52c849e043 | |
osaajani | 44b855dd48 | |
osaajani | d0e17567c3 | |
osaajani | 50dff066d0 | |
osaajani | ae4f3bd9a7 | |
osaajani | 54f5be7523 | |
osaajani | 4f717ef849 |
|
@ -53,6 +53,98 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
/* Custom utility classes for padding */
|
||||
.py-1 { padding-top: 0.25rem !important; padding-bottom: 0.25rem !important; }
|
||||
.py-2 { padding-top: 0.5rem !important; padding-bottom: 0.5rem !important; }
|
||||
.py-3 { padding-top: 1rem !important; padding-bottom: 1rem !important; }
|
||||
.py-4 { padding-top: 1.5rem !important; padding-bottom: 1.5rem !important; }
|
||||
.py-5 { padding-top: 3rem !important; padding-bottom: 3rem !important; }
|
||||
|
||||
.px-1 { padding-left: 0.25rem !important; padding-right: 0.25rem !important; }
|
||||
.px-2 { padding-left: 0.5rem !important; padding-right: 0.5rem !important; }
|
||||
.px-3 { padding-left: 1rem !important; padding-right: 1rem !important; }
|
||||
.px-4 { padding-left: 1.5rem !important; padding-right: 1.5rem !important; }
|
||||
.px-5 { padding-left: 3rem !important; padding-right: 3rem !important; }
|
||||
|
||||
.pt-1 { padding-top: 0.25rem !important; }
|
||||
.pt-2 { padding-top: 0.5rem !important; }
|
||||
.pt-3 { padding-top: 1rem !important; }
|
||||
.pt-4 { padding-top: 1.5rem !important; }
|
||||
.pt-5 { padding-top: 3rem !important; }
|
||||
|
||||
.pb-1 { padding-bottom: 0.25rem !important; }
|
||||
.pb-2 { padding-bottom: 0.5rem !important; }
|
||||
.pb-3 { padding-bottom: 1rem !important; }
|
||||
.pb-4 { padding-bottom: 1.5rem !important; }
|
||||
.pb-5 { padding-bottom: 3rem !important; }
|
||||
|
||||
.pl-1 { padding-left: 0.25rem !important; }
|
||||
.pl-2 { padding-left: 0.5rem !important; }
|
||||
.pl-3 { padding-left: 1rem !important; }
|
||||
.pl-4 { padding-left: 1.5rem !important; }
|
||||
.pl-5 { padding-left: 3rem !important; }
|
||||
|
||||
.pr-1 { padding-right: 0.25rem !important; }
|
||||
.pr-2 { padding-right: 0.5rem !important; }
|
||||
.pr-3 { padding-right: 1rem !important; }
|
||||
.pr-4 { padding-right: 1.5rem !important; }
|
||||
.pr-5 { padding-right: 3rem !important; }
|
||||
|
||||
/* Custom utility classes for margin */
|
||||
.my-1 { margin-top: 0.25rem !important; margin-bottom: 0.25rem !important; }
|
||||
.my-2 { margin-top: 0.5rem !important; margin-bottom: 0.5rem !important; }
|
||||
.my-3 { margin-top: 1rem !important; margin-bottom: 1rem !important; }
|
||||
.my-4 { margin-top: 1.5rem !important; margin-bottom: 1.5rem !important; }
|
||||
.my-5 { margin-top: 3rem !important; margin-bottom: 3rem !important; }
|
||||
|
||||
.mx-1 { margin-left: 0.25rem !important; margin-right: 0.25rem !important; }
|
||||
.mx-2 { margin-left: 0.5rem !important; margin-right: 0.5rem !important; }
|
||||
.mx-3 { margin-left: 1rem !important; margin-right: 1rem !important; }
|
||||
.mx-4 { margin-left: 1.5rem !important; margin-right: 1.5rem !important; }
|
||||
.mx-5 { margin-left: 3rem !important; margin-right: 3rem !important; }
|
||||
|
||||
.mt-1 { margin-top: 0.25rem !important; }
|
||||
.mt-2 { margin-top: 0.5rem !important; }
|
||||
.mt-3 { margin-top: 1rem !important; }
|
||||
.mt-4 { margin-top: 1.5rem !important; }
|
||||
.mt-5 { margin-top: 3rem !important; }
|
||||
|
||||
.mb-1 { margin-bottom: 0.25rem !important; }
|
||||
.mb-2 { margin-bottom: 0.5rem !important; }
|
||||
.mb-3 { margin-bottom: 1rem !important; }
|
||||
.mb-4 { margin-bottom: 1.5rem !important; }
|
||||
.mb-5 { margin-bottom: 3rem !important; }
|
||||
|
||||
.ml-1 { margin-left: 0.25rem !important; }
|
||||
.ml-2 { margin-left: 0.5rem !important; }
|
||||
.ml-3 { margin-left: 1rem !important; }
|
||||
.ml-4 { margin-left: 1.5rem !important; }
|
||||
.ml-5 { margin-left: 3rem !important; }
|
||||
|
||||
.mr-1 { margin-right: 0.25rem !important; }
|
||||
.mr-2 { margin-right: 0.5rem !important; }
|
||||
.mr-3 { margin-right: 1rem !important; }
|
||||
.mr-4 { margin-right: 1.5rem !important; }
|
||||
.mr-5 { margin-right: 3rem !important; }
|
||||
|
||||
/* HTML: <div class="loader"></div> */
|
||||
.loader {
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
aspect-ratio: 1;
|
||||
border-radius: 50%;
|
||||
background:
|
||||
radial-gradient(farthest-side,#999 94%,#0000) top/8px 8px no-repeat,
|
||||
conic-gradient(#0000 30%,#999);
|
||||
-webkit-mask: radial-gradient(farthest-side,#0000 calc(100% - 8px),#000 0);
|
||||
animation: l13 1s infinite linear;
|
||||
}
|
||||
|
||||
@keyframes l13{
|
||||
100%{transform: rotate(1turn)}
|
||||
}
|
||||
|
||||
|
||||
/** POPUPS ALERT **/
|
||||
.popup-alerts-container
|
||||
{
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -9,12 +9,16 @@
|
|||
"symfony/expression-language": "^5.0",
|
||||
"robmorgan/phinx": "^0.11.1",
|
||||
"monolog/monolog": "^2.0",
|
||||
"ovh/ovh": "^2.0",
|
||||
"ovh/ovh": "^3.0",
|
||||
"twilio/sdk": "^6.1",
|
||||
"symfony/yaml": "^5.0",
|
||||
"phpmailer/phpmailer": "^6.1",
|
||||
"xantios/mimey": ">=2.1",
|
||||
"kreait/firebase-php": "^5.14",
|
||||
"benmorel/gsm-charset-converter": "^0.3.0"
|
||||
"kreait/firebase-php": "^7.0",
|
||||
"benmorel/gsm-charset-converter": "^0.3.0",
|
||||
"google/cloud-pubsub": "^1.46"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.21"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
namespace controllers\internals;
|
||||
|
||||
use DateInterval;
|
||||
use Faker\Factory;
|
||||
|
||||
/**
|
||||
* Class to call the console scripts.
|
||||
|
@ -216,44 +217,261 @@ use DateInterval;
|
|||
}
|
||||
|
||||
/**
|
||||
* Do some fake population renewal.
|
||||
* Do phone reliability verifications
|
||||
*/
|
||||
public function f()
|
||||
public function phone_reliability()
|
||||
{
|
||||
$bdd = \descartes\Model::_connect(DATABASE_HOST, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD);
|
||||
$internal_user = new \controllers\internals\User($bdd);
|
||||
$internal_settings = new \controllers\internals\Setting($bdd);
|
||||
$internal_sended = new \controllers\internals\Sended($bdd);
|
||||
$internal_phone_reliability = new \controllers\internals\PhoneReliability($bdd);
|
||||
$internal_phone = new \controllers\internals\Phone($bdd);
|
||||
$internal_webhook = new \controllers\internals\Webhook($bdd);
|
||||
$internal_mailer = new \controllers\internals\Mailer();
|
||||
|
||||
$users = $internal_user->get_all_active();
|
||||
foreach ($users as $user)
|
||||
{
|
||||
$settings = $internal_settings->gets_for_user($user['id']);
|
||||
echo "\nCheck phone reliability for user " . $user['id'] . ":\n";
|
||||
if ($settings['phone_reliability_failed'])
|
||||
{
|
||||
$rate_limit = intval($settings['phone_reliability_failed_rate_limit']) / 100;
|
||||
$min_volume = intval($settings['phone_reliability_failed_volume']);
|
||||
$period = intval($settings['phone_reliability_failed_period']);
|
||||
$grace_period = intval($settings['phone_reliability_failed_grace_period']);
|
||||
|
||||
echo " Check for failed SMS with rate > " . $rate_limit . " and volume > " . $min_volume . " on period " . $period . "s with grace period of " . $grace_period . "s.\n";
|
||||
|
||||
$unreliable_phones = $internal_phone_reliability->find_unreliable_phones($user['id'], \models\Sended::STATUS_FAILED, $rate_limit, $min_volume, $period, $grace_period);
|
||||
foreach ($unreliable_phones as $unreliable_phone)
|
||||
{
|
||||
$phone = $internal_phone->get($unreliable_phone['id_phone']);
|
||||
if (!$phone)
|
||||
{
|
||||
echo ' Cannot find phone: ' . $unreliable_phone['id_phone'] . "\n";
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
echo "\n Phone " . $phone['id'] . ' - ' . $phone['name'] . " failed rate = " . $unreliable_phone['rate'] . " > " . $rate_limit . " and volume " . $unreliable_phone['total'] . " > " . $min_volume . "\n";
|
||||
|
||||
$internal_phone_reliability->create($user['id'], $phone['id'], \models\Sended::STATUS_FAILED);
|
||||
|
||||
if ($settings['phone_reliability_failed_email'])
|
||||
{
|
||||
$success = $internal_mailer->enqueue($user['email'], EMAIL_PHONE_RELIABILITY_FAILED, [
|
||||
'phone' => $phone,
|
||||
'period' => $period,
|
||||
'total' => $unreliable_phone['total'],
|
||||
'unreliable' => $unreliable_phone['unreliable'],
|
||||
'rate' => $unreliable_phone['rate'],
|
||||
]);
|
||||
|
||||
if (!$success)
|
||||
{
|
||||
echo ' Cannot enqueue alert for unreliable failed phone: ' . $unreliable_phone['id_phone'] . "\n";
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
echo " Alert mail for unreliable failed phone " . $phone['id'] . ' - ' . $phone['name'] . " added\n";
|
||||
}
|
||||
|
||||
if ($settings['phone_reliability_failed_webhook'])
|
||||
{
|
||||
$webhook = [
|
||||
'reliability_type' => \models\Sended::STATUS_FAILED,
|
||||
'id_phone' => $unreliable_phone['id_phone'],
|
||||
'period' => $period,
|
||||
'total' => $unreliable_phone['total'],
|
||||
'unreliable' => $unreliable_phone['unreliable'],
|
||||
'rate' => $unreliable_phone['rate'],
|
||||
];
|
||||
|
||||
$internal_webhook->trigger($user['id'], \models\Webhook::TYPE_PHONE_RELIABILITY, $webhook);
|
||||
|
||||
echo " Webhook for unreliable failed phone " . $phone['id'] . ' - ' . $phone['name'] . " triggered\n";
|
||||
}
|
||||
|
||||
if ($settings['phone_reliability_failed_auto_disable'])
|
||||
{
|
||||
$internal_phone->update_status($unreliable_phone['id_phone'], \models\Phone::STATUS_DISABLED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($settings['phone_reliability_unknown'])
|
||||
{
|
||||
$rate_limit = intval($settings['phone_reliability_unknown_rate_limit']) / 100;
|
||||
$min_volume = intval($settings['phone_reliability_unknown_volume']);
|
||||
$period = intval($settings['phone_reliability_unknown_period']);
|
||||
$grace_period = intval($settings['phone_reliability_unknown_grace_period']);
|
||||
|
||||
echo "\n Check for unknown SMS with rate > " . $rate_limit . " and volume > " . $min_volume . " on period " . $period . "s with grace period of " . $grace_period . "s.\n";
|
||||
|
||||
$unreliable_phones = $internal_phone_reliability->find_unreliable_phones($user['id'], \models\Sended::STATUS_UNKNOWN, $rate_limit, $min_volume, $period, $grace_period);
|
||||
foreach ($unreliable_phones as $unreliable_phone)
|
||||
{
|
||||
$phone = $internal_phone->get($unreliable_phone['id_phone']);
|
||||
if (!$phone)
|
||||
{
|
||||
echo ' Cannot find phone: ' . $unreliable_phone['id_phone'] . "\n";
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
echo "\n Phone " . $phone['id'] . ' - ' . $phone['name'] . " unknown rate = " . $unreliable_phone['rate'] . " > " . $rate_limit . "\n";
|
||||
|
||||
$internal_phone_reliability->create($user['id'], $phone['id'], \models\Sended::STATUS_UNKNOWN);
|
||||
|
||||
if ($settings['phone_reliability_unknown_email'])
|
||||
{
|
||||
$success = $internal_mailer->enqueue($user['email'], EMAIL_PHONE_RELIABILITY_UNKNOWN, [
|
||||
'phone' => $phone,
|
||||
'period' => $period,
|
||||
'total' => $unreliable_phone['total'],
|
||||
'unreliable' => $unreliable_phone['unreliable'],
|
||||
'rate' => $unreliable_phone['rate'],
|
||||
]);
|
||||
|
||||
if (!$success)
|
||||
{
|
||||
echo ' Cannot enqueue alert for unreliable unknown phone: ' . $unreliable_phone['id_phone'] . "\n";
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
echo " Alert mail for unreliable unknown phone " . $phone['id'] . ' - ' . $phone['name'] . " added\n";
|
||||
}
|
||||
|
||||
if ($settings['phone_reliability_unknown_webhook'])
|
||||
{
|
||||
$webhook = [
|
||||
'reliability_type' => \models\Sended::STATUS_UNKNOWN,
|
||||
'id_phone' => $unreliable_phone['id_phone'],
|
||||
'period' => $period,
|
||||
'total' => $unreliable_phone['total'],
|
||||
'unreliable' => $unreliable_phone['unreliable'],
|
||||
'rate' => $unreliable_phone['rate'],
|
||||
];
|
||||
|
||||
$internal_webhook->trigger($user['id'], \models\Webhook::TYPE_PHONE_RELIABILITY, $webhook);
|
||||
|
||||
echo " Webhook for unreliable unknown phone " . $phone['id'] . ' - ' . $phone['name'] . " triggered\n";
|
||||
}
|
||||
|
||||
if ($settings['phone_reliability_unknown_auto_disable'])
|
||||
{
|
||||
$internal_phone->update_status($unreliable_phone['id_phone'], \models\Phone::STATUS_DISABLED);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to easily populate the database with fake data for testing.
|
||||
*
|
||||
* @param int $id_user : User ID for whom data is to be generated
|
||||
* @param int $received_entries : Number of entries to add to the received table
|
||||
* @param int $sended_entries : Number of entries to add to the sended table
|
||||
* @param int $contact_entries : Number of entries to add to the contact table
|
||||
*/
|
||||
public function seed_database(int $id_user, int $received_entries, int $sended_entries, int $contact_entries)
|
||||
{
|
||||
$this->seed_received($id_user, $received_entries);
|
||||
$this->seed_sended($id_user, $sended_entries);
|
||||
$this->seed_contact($id_user, $contact_entries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill table received with fake data
|
||||
*
|
||||
* @param int $id_user : User to insert received for
|
||||
* @param int $entries : How many received to insert
|
||||
*/
|
||||
public function seed_received(int $id_user, int $entries)
|
||||
{
|
||||
$bdd = \descartes\Model::_connect(DATABASE_HOST, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD);
|
||||
$internal_received = new \controllers\internals\Received($bdd);
|
||||
$internal_phone = new \controllers\internals\Phone($bdd);
|
||||
$faker = Factory::create();
|
||||
|
||||
$phones = $internal_phone->gets_for_user($id_user);
|
||||
|
||||
for ($i = 0; $i < $entries; $i++)
|
||||
{
|
||||
$id_phone = $faker->randomElement($phones)['id'];
|
||||
$at = $faker->dateTimeBetween('-1 year', 'now')->format('Y-m-d H:i:s');
|
||||
$text = $faker->sentence(rand(5,10), true);
|
||||
$origin = $faker->e164PhoneNumber;
|
||||
$status = $faker->randomElement(['read', 'unread']);
|
||||
$command = false;
|
||||
$mms = false;
|
||||
$media_ids = [];
|
||||
|
||||
|
||||
$internal_received->create($id_user, $id_phone, $at, $text, $origin, $status, $command, $mms, $media_ids);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill table sended with fake data
|
||||
*
|
||||
* @param int $id_user : User to insert sended entries for
|
||||
* @param int $entries : Number of entries to insert
|
||||
*/
|
||||
public function seed_sended(int $id_user, int $entries)
|
||||
{
|
||||
$bdd = \descartes\Model::_connect(DATABASE_HOST, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD);
|
||||
$internal_sended = new \controllers\internals\Sended($bdd);
|
||||
$internal_phone = new \controllers\internals\Phone($bdd);
|
||||
$faker = Factory::create();
|
||||
|
||||
$destinations = ['+33612345678','+33612345679','+33612345680',];
|
||||
$statuses = [\models\Sended::STATUS_DELIVERED, \models\Sended::STATUS_FAILED, \models\Sended::STATUS_UNKNOWN];
|
||||
$day = new \DateTime();
|
||||
$day->sub(new DateInterval('P30D'));
|
||||
for ($i = 0; $i < 30; $i++)
|
||||
$phones = $internal_phone->gets_for_user($id_user);
|
||||
|
||||
for ($i = 0; $i < $entries; $i++)
|
||||
{
|
||||
$day->add(new DateInterval('P1D'));
|
||||
$n = rand(0, 100);
|
||||
for ($j = 0; $j < $n; $j++)
|
||||
{
|
||||
$id_user = 1;
|
||||
$id_phone = rand(1, 2);
|
||||
$destination = $destinations[array_rand($destinations)];
|
||||
$status = $statuses[array_rand($statuses)];
|
||||
$internal_sended->create(
|
||||
$id_user,
|
||||
$id_phone,
|
||||
$day->format('Y-m-d H:i:s'),
|
||||
"TEST N°$i:$j",
|
||||
$destination,
|
||||
uniqid(),
|
||||
'adapters\TestAdapter',
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
[],
|
||||
null,
|
||||
$status,
|
||||
);
|
||||
echo $i."\n";
|
||||
$phone = $faker->randomElement($phones);
|
||||
$id_phone = $phone['id'];
|
||||
$at = $faker->dateTimeBetween('-1 year', 'now')->format('Y-m-d H:i:s');
|
||||
$text = $faker->sentence(rand(5, 10), true);
|
||||
$destination = $faker->e164PhoneNumber;
|
||||
$uid = $faker->uuid;
|
||||
$adapter = $phone['adapter'];
|
||||
$flash = $faker->boolean;
|
||||
$mms = $faker->boolean;
|
||||
$tag = $faker->optional()->word;
|
||||
$medias = []; // Add logic for media IDs if needed
|
||||
$originating_scheduled = $faker->numberBetween(1, 100);
|
||||
$status = $faker->randomElement([\models\Sended::STATUS_UNKNOWN, \models\Sended::STATUS_DELIVERED, \models\Sended::STATUS_FAILED]);
|
||||
|
||||
$internal_sended->create($id_user, $id_phone, $at, $text, $destination, $uid, $adapter, $flash, $mms, $tag, $medias, $originating_scheduled, $status);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill table contact with fake data
|
||||
*
|
||||
* @param int $id_user : User to insert contacts for
|
||||
* @param int $entries : Number of contacts to insert
|
||||
*/
|
||||
public function seed_contact(int $id_user, int $entries)
|
||||
{
|
||||
$bdd = \descartes\Model::_connect(DATABASE_HOST, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD);
|
||||
$internal_contact = new \controllers\internals\Contact($bdd);
|
||||
$faker = Factory::create();
|
||||
|
||||
for ($i = 0; $i < $entries; $i++)
|
||||
{
|
||||
$name = $faker->name;
|
||||
$number = $faker->e164PhoneNumber;
|
||||
$data = '[]';
|
||||
|
||||
$internal_contact->create($id_user, $number, $name, $data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -117,11 +117,10 @@ class Mailer extends \descartes\Controller
|
|||
'attachments' => $attachments,
|
||||
];
|
||||
|
||||
$error_code = null;
|
||||
$queue = msg_get_queue(QUEUE_ID_EMAIL);
|
||||
$success = msg_send($queue, QUEUE_TYPE_EMAIL, $message, true, true, $error_code);
|
||||
$queue = new Queue(QUEUE_ID_EMAIL);
|
||||
$queue->push(json_encode($message), QUEUE_TYPE_EMAIL);
|
||||
|
||||
return (bool) $success;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of RaspiSMS.
|
||||
*
|
||||
* (c) Pierre-Lin Bonnemaison <plebwebsas@gmail.com>
|
||||
*
|
||||
* This source file is subject to the GPL-3.0 license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace controllers\internals;
|
||||
|
||||
use Exception;
|
||||
|
||||
class PhoneReliability extends StandardController
|
||||
{
|
||||
protected $model;
|
||||
|
||||
/**
|
||||
* Create a phone reliability history entry.
|
||||
*
|
||||
* @param int $id_user : Id of user to create sended message for
|
||||
* @param int $id_phone : Id of the number the message was send with
|
||||
* @param $type : Type of reliability alert
|
||||
* @return mixed : false on error, new sended id else
|
||||
*/
|
||||
public function create(int $id_user, int $id_phone, string $type)
|
||||
{
|
||||
return $this->get_model()->insert([
|
||||
'id_user' => $id_user,
|
||||
'id_phone' => $id_phone,
|
||||
'type' => $type,
|
||||
]);
|
||||
|
||||
return $id_sended;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all unreliable phones for a user, based on sended sms status, rate limit, etc.
|
||||
*
|
||||
* @param int $id_user : User id
|
||||
* @param string $sms_status : Status of SMS to use to calculate rate
|
||||
* @param float $rate_limit : Percentage of SMS matching status after which we consider the phone unreliable
|
||||
* @param int $min_volume : Minimum number of sms we need to have to consider the statistic relevent
|
||||
* @param int $period : The time span in minutes from which SMS counting should begin.
|
||||
* @param int $grace_period : How long in minutes should we wait before including a SMS in counting
|
||||
*
|
||||
* @return array : A list of unreliable phone for the user, with phone id, total number of sms, and rate of failed sms
|
||||
*/
|
||||
public function find_unreliable_phones (int $id_user, string $sms_status, float $rate_limit, int $min_volume, int $period, int $grace_period)
|
||||
{
|
||||
return $this->get_model()->find_unreliable_phones($id_user, $sms_status, $rate_limit, $min_volume, $period, $grace_period);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the model for the Controller.
|
||||
*/
|
||||
protected function get_model(): \models\PhoneReliabilityHistory
|
||||
{
|
||||
$this->model = $this->model ?? new \models\PhoneReliabilityHistory($this->bdd);
|
||||
|
||||
return $this->model;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of RaspiSMS.
|
||||
*
|
||||
* (c) Pierre-Lin Bonnemaison <plebwebsas@gmail.com>
|
||||
*
|
||||
* This source file is subject to the GPL-3.0 license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace controllers\internals;
|
||||
|
||||
use Exception;
|
||||
use models\RedisQueue;
|
||||
use models\SystemVQueue;
|
||||
|
||||
class Queue extends \descartes\InternalController
|
||||
{
|
||||
private $queue;
|
||||
|
||||
/**
|
||||
* A class to interact with queue, the class is in charge to choose the type of queue (redis/system v) to use
|
||||
*/
|
||||
public function __construct($id)
|
||||
{
|
||||
if (USE_REDIS_QUEUES ?? false)
|
||||
{
|
||||
$params = [];
|
||||
if (REDIS_HOST ?? false)
|
||||
{
|
||||
$params['host'] = REDIS_HOST;
|
||||
}
|
||||
|
||||
if (REDIS_PORT ?? false)
|
||||
{
|
||||
$params['port'] = REDIS_PORT;
|
||||
}
|
||||
|
||||
if (REDIS_PASSWORD ?? false)
|
||||
{
|
||||
$params['auth'] = REDIS_PASSWORD;
|
||||
}
|
||||
|
||||
$this->queue = new RedisQueue($id, $params, 'raspisms', 'raspisms');
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->queue = new SystemVQueue($id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a message to the queue
|
||||
*
|
||||
* @param string $message : The message to add to the queue
|
||||
* @param ?string $tag : A tag to associate to the message for routing purposes, if null will add to general queue
|
||||
*/
|
||||
public function push($message, ?string $tag = null)
|
||||
{
|
||||
return $this->queue->push($message, $tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the older message in the queue
|
||||
*
|
||||
* @return mixed $message : The oldest message or null if no message found, can be anything
|
||||
* @param ?string $tag : A tag to associate to the message for routing purposes, if null will read from general queue
|
||||
* @param mixed : The message to add to the queue, can be anything, the queue will have to treat it by itself
|
||||
*/
|
||||
public function read(?string $tag = null)
|
||||
{
|
||||
return $this->queue->read($tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to close system V queue for cleaning resources, usefull only if system V queue
|
||||
*/
|
||||
public function close()
|
||||
{
|
||||
if ($this->queue instanceof SystemVQueue)
|
||||
{
|
||||
$this->queue->close();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -253,9 +253,9 @@ use Exception;
|
|||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_discussions_for_user(int $id_user)
|
||||
public function get_discussions_for_user(int $id_user, ?int $nb_entry = null, ?int $page = null)
|
||||
{
|
||||
return $this->get_model()->get_discussions_for_user($id_user);
|
||||
return $this->get_model()->get_discussions_for_user($id_user, $nb_entry, $page);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -225,6 +225,22 @@ use Exception;
|
|||
return $this->get_model()->get_last_for_destination_and_user($id_user, $destination);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get number of sended SMS by day and status between two dates, possibly by sending phone.
|
||||
*
|
||||
* @param int $id_user : user id
|
||||
* @param \DateTime $start_date : Date since which we want the messages
|
||||
* @param \DateTime $end_date : Date until which we want the messages
|
||||
* @param ?int $id_phone : Id of the phone to search sended for, null by default get all phones
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_sended_status_stats ($id_user, $start_date, $end_date, ?int $id_phone = null)
|
||||
{
|
||||
return $this->get_model()->get_sended_status_stats($id_user, $start_date, $end_date, $id_phone);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send a SMS message.
|
||||
*
|
||||
|
@ -360,6 +376,22 @@ use Exception;
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of invalid phone number we've sent message to
|
||||
*
|
||||
* @param int $id_user : user id
|
||||
* @param int $volume : Minimum number of sms sent to the number
|
||||
* @param float $percent_failed : Minimum ratio of failed message
|
||||
* @param float $percent_unknown : Minimum ratio of unknown message
|
||||
* @param int $limit : Limit of results
|
||||
* @param int $page : Page of results (offset = page * limit)
|
||||
*
|
||||
*/
|
||||
public function get_invalid_numbers (int $id_user, int $volume, float $percent_failed, float $percent_unknown, int $limit, int $page)
|
||||
{
|
||||
return $this->get_model()->get_invalid_numbers($id_user, $volume, $percent_failed, $percent_unknown, $limit, $page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the model for the Controller.
|
||||
*/
|
||||
|
|
|
@ -91,6 +91,12 @@ namespace controllers\internals;
|
|||
$all_success = true;
|
||||
foreach (USER_DEFAULT_SETTINGS as $name => $value)
|
||||
{
|
||||
// Ignore if already existing settings
|
||||
if (count($this->get_by_name_for_user($id_user, $name)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$success = $this->create($id_user, $name, $value);
|
||||
$all_success = ($all_success && $success);
|
||||
}
|
||||
|
|
|
@ -168,6 +168,26 @@ use BenMorel\GsmCharsetConverter\Converter;
|
|||
return $logo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a string is a valid PHP date
|
||||
*
|
||||
* @param string $date : Datestring to validate
|
||||
*
|
||||
* @return bool : True if a valid date, false else
|
||||
*/
|
||||
public static function is_valid_date($date)
|
||||
{
|
||||
try
|
||||
{
|
||||
new \DateTime($date);
|
||||
return true;
|
||||
}
|
||||
catch (\Exception $e)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cette fonction vérifie une date.
|
||||
*
|
||||
|
|
|
@ -31,6 +31,17 @@ namespace controllers\internals;
|
|||
$this->internal_phone = new Phone($bdd);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all active users.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_all_active()
|
||||
{
|
||||
return $this->model_user->get_all_active();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return a list of users by their ids.
|
||||
*
|
||||
|
|
|
@ -135,12 +135,11 @@ class Webhook extends StandardController
|
|||
],
|
||||
];
|
||||
|
||||
$error_code = null;
|
||||
$queue = msg_get_queue(QUEUE_ID_WEBHOOK);
|
||||
msg_send($queue, QUEUE_TYPE_WEBHOOK, $message, true, true, $error_code);
|
||||
$queue = new Queue(QUEUE_ID_WEBHOOK);
|
||||
$success = $queue->push(json_encode($message), QUEUE_TYPE_WEBHOOK);
|
||||
}
|
||||
|
||||
return true;
|
||||
return (bool) $success;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -91,6 +91,10 @@ namespace controllers\publics;
|
|||
{
|
||||
$this->user = $this->internal_user->get_by_api_key($api_key);
|
||||
}
|
||||
elseif ($_SESSION['user'] ?? false)
|
||||
{
|
||||
$this->user = $this->internal_user->get($_SESSION['user']['id']);
|
||||
}
|
||||
|
||||
if (!$this->user)
|
||||
{
|
||||
|
@ -300,6 +304,47 @@ namespace controllers\publics;
|
|||
return $this->json($return);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplest method to send a SMS immediately with nothing but a URL and a GET query
|
||||
* @param string $_GET['to'] = Phone number to send sms to
|
||||
* @param string $_GET['text'] = Text of the SMS
|
||||
* @param ?int $_GET['id_phone'] = Id of the phone to use, if null use a random phone
|
||||
*/
|
||||
public function get_send_sms()
|
||||
{
|
||||
$to = \controllers\internals\Tool::parse_phone($_GET['to'] ?? '');
|
||||
$text = $_GET['text'] ?? false;
|
||||
$id_phone = empty($_GET['id_phone']) ? null : $_GET['id_phone'];
|
||||
|
||||
if (!$to || !$text)
|
||||
{
|
||||
$return = self::DEFAULT_RETURN;
|
||||
$return['error'] = self::ERROR_CODES['MISSING_PARAMETER'];
|
||||
$return['message'] = self::ERROR_MESSAGES['MISSING_PARAMETER'] . ($to ? '' : 'to ') . ($text ? '' : 'text');
|
||||
$this->auto_http_code(false);
|
||||
|
||||
return $this->json($return);
|
||||
}
|
||||
$at = (new \DateTime())->format('Y-m-d H:i:s');
|
||||
|
||||
$scheduled_id = $this->internal_scheduled->create($this->user['id'], $at, $text, $id_phone);
|
||||
if (!$scheduled_id)
|
||||
{
|
||||
$return = self::DEFAULT_RETURN;
|
||||
$return['error'] = self::ERROR_CODES['CANNOT_CREATE'];
|
||||
$return['message'] = self::ERROR_MESSAGES['CANNOT_CREATE'];
|
||||
$this->auto_http_code(false);
|
||||
|
||||
return $this->json($return);
|
||||
}
|
||||
|
||||
$return = self::DEFAULT_RETURN;
|
||||
$return['response'] = $scheduled_id;
|
||||
$this->auto_http_code(true);
|
||||
|
||||
return $this->json($return);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule a message to be send.
|
||||
*
|
||||
|
@ -1030,6 +1075,15 @@ namespace controllers\publics;
|
|||
return $this->json($return);
|
||||
}
|
||||
|
||||
if ($phone['status'] === \models\Phone::STATUS_DISABLED)
|
||||
{
|
||||
$return['error'] = self::ERROR_CODES['CANNOT_UPDATE'];
|
||||
$return['message'] = self::ERROR_MESSAGES['CANNOT_UPDATE'] . 'Phone have been manually disabled, you need to re-enable it manually.';
|
||||
$this->auto_http_code(false);
|
||||
|
||||
return $this->json($return);
|
||||
}
|
||||
|
||||
// If user have activated phone limits, check if RaspiSMS phone limit have already been reached
|
||||
$limit_reached = false;
|
||||
if ((int) ($this->user['settings']['phone_limit'] ?? false))
|
||||
|
@ -1068,4 +1122,176 @@ namespace controllers\publics;
|
|||
|
||||
return $this->json($return);
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually disable/enable phones
|
||||
* @param int id : id of phone we want to update status
|
||||
* @param string $_POST['new_status'] : New status of the phone, either 'disabled' or 'available'
|
||||
* @param $csrf : CSRF token
|
||||
*/
|
||||
public function post_change_phone_status ($id)
|
||||
{
|
||||
$new_status = $_POST['status'] ?? '';
|
||||
|
||||
if (!in_array($new_status, [\models\Phone::STATUS_AVAILABLE, \models\Phone::STATUS_DISABLED]))
|
||||
{
|
||||
$return['error'] = self::ERROR_CODES['INVALID_PARAMETER'];
|
||||
$return['message'] = self::ERROR_MESSAGES['INVALID_PARAMETER'] . ' "status" must be "disabled" or "available".';
|
||||
$this->auto_http_code(false);
|
||||
|
||||
return $this->json($return);
|
||||
}
|
||||
|
||||
$phone = $this->internal_phone->get_for_user($this->user['id'], $id);
|
||||
if (!$phone)
|
||||
{
|
||||
$return['error'] = self::ERROR_CODES['CANNOT_UPDATE'];
|
||||
$return['message'] = self::ERROR_MESSAGES['CANNOT_UPDATE'];
|
||||
$this->auto_http_code(false);
|
||||
|
||||
return $this->json($return);
|
||||
}
|
||||
|
||||
$status_update = $this->internal_phone->update_status($id, $new_status);
|
||||
$return['response'] = $new_status;
|
||||
$this->auto_http_code(true);
|
||||
|
||||
return $this->json($return);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return statistics about status of sended sms for a period by phone
|
||||
*
|
||||
* @param string $_GET['start'] : Date from which to get sms volume, format Y-m-d H:i:s.
|
||||
* @param string $_GET['end'] : Date up to which to get sms volume, format Y-m-d H:i:s.
|
||||
* @param ?int $_GET['id_phone'] : Id of the phone we want to check the status for. Default to null will return stats for all phone.
|
||||
*
|
||||
* @return : List of entries
|
||||
*/
|
||||
public function get_sms_status_stats()
|
||||
{
|
||||
$start = $_GET['start'] ?? null;
|
||||
$end = $_GET['end'] ?? null;
|
||||
$id_phone = $_GET['id_phone'] ?? null;
|
||||
|
||||
if (!$start || !$end)
|
||||
{
|
||||
$return = self::DEFAULT_RETURN;
|
||||
$return['error'] = self::ERROR_CODES['MISSING_PARAMETER'];
|
||||
$return['message'] = self::ERROR_MESSAGES['MISSING_PARAMETER'] . 'start and end date are required.';
|
||||
$this->auto_http_code(false);
|
||||
|
||||
return $this->json($return);
|
||||
}
|
||||
|
||||
$return = self::DEFAULT_RETURN;
|
||||
|
||||
if (!\controllers\internals\Tool::is_valid_date($start))
|
||||
{
|
||||
$return = self::DEFAULT_RETURN;
|
||||
$return['error'] = self::ERROR_CODES['INVALID_PARAMETER'];
|
||||
$return['message'] = self::ERROR_MESSAGES['INVALID_PARAMETER'] . 'start must be a date of format "Y-m-d H:i:s".';
|
||||
$this->auto_http_code(false);
|
||||
|
||||
return $this->json($return);
|
||||
}
|
||||
$start = new \DateTime($start);
|
||||
|
||||
if (!\controllers\internals\Tool::is_valid_date($end))
|
||||
{
|
||||
$return = self::DEFAULT_RETURN;
|
||||
$return['error'] = self::ERROR_CODES['INVALID_PARAMETER'];
|
||||
$return['message'] = self::ERROR_MESSAGES['INVALID_PARAMETER'] . 'end must be a date of format "Y-m-d H:i:s".';
|
||||
$this->auto_http_code(false);
|
||||
|
||||
return $this->json($return);
|
||||
}
|
||||
$end = new \DateTime($end);
|
||||
|
||||
if ($id_phone)
|
||||
{
|
||||
$phone = $this->internal_phone->get_for_user($this->user['id'], $id_phone);
|
||||
if (!$phone)
|
||||
{
|
||||
$return['error'] = self::ERROR_CODES['INVALID_PARAMETER'];
|
||||
$return['message'] = self::ERROR_MESSAGES['INVALID_PARAMETER'] . 'phone with id ' . $id_phone . ' does not exists.';
|
||||
$this->auto_http_code(false);
|
||||
|
||||
return $this->json($return);
|
||||
}
|
||||
}
|
||||
|
||||
$stats = $this->internal_sended->get_sended_status_stats($this->user['id'], $start, $end, $id_phone);
|
||||
|
||||
$return = self::DEFAULT_RETURN;
|
||||
$return['response'] = $stats;
|
||||
$this->auto_http_code(true);
|
||||
|
||||
return $this->json($return);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return statistics about invalid numbers
|
||||
*
|
||||
* @param int $page : Pagination number, Default = 0. Group of 25 results.
|
||||
* @param int $_GET['volume'] : Minimum number of SMS sent to the number
|
||||
* @param int $_GET['percent_failed'] : Minimum percentage of failed SMS to the number
|
||||
* @param int $_GET['percent_unknown'] : Minimum percentage of unknown SMS to the number
|
||||
*
|
||||
* @return : List of entries
|
||||
*/
|
||||
public function get_invalid_numbers($page = 0)
|
||||
{
|
||||
$page = (int) $page;
|
||||
$limit = 25;
|
||||
$volume = $_GET['volume'] ?? false;
|
||||
$percent_failed = $_GET['percent_failed'] ?? false;
|
||||
$percent_unknown = $_GET['percent_unknown'] ?? false;
|
||||
|
||||
if ($volume === false || $percent_failed === false || $percent_unknown === false)
|
||||
{
|
||||
$return = self::DEFAULT_RETURN;
|
||||
$return['error'] = self::ERROR_CODES['MISSING_PARAMETER'];
|
||||
$return['message'] = self::ERROR_MESSAGES['MISSING_PARAMETER'] . 'volume, percent_failed and percent_unknown are required.';
|
||||
$this->auto_http_code(false);
|
||||
|
||||
return $this->json($return);
|
||||
}
|
||||
|
||||
$volume = (int) $volume;
|
||||
$percent_failed = ((float) $percent_failed) / 100;
|
||||
$percent_unknown = ((float) $percent_unknown) / 100;
|
||||
|
||||
$return = self::DEFAULT_RETURN;
|
||||
|
||||
$invalid_numbers = $this->internal_sended->get_invalid_numbers($this->user['id'], $volume, $percent_failed, $percent_unknown, $limit, $page);
|
||||
|
||||
$return = self::DEFAULT_RETURN;
|
||||
|
||||
if (\count($invalid_numbers) === $limit)
|
||||
{
|
||||
$return['next'] = \descartes\Router::url('Api', __FUNCTION__, ['page' => $page + 1], [
|
||||
'api_key' => $this->user['api_key'],
|
||||
'volume' => $volume,
|
||||
'percent_failed' => $percent_failed * 100,
|
||||
'percent_unknown' => $percent_unknown * 100
|
||||
]);
|
||||
}
|
||||
|
||||
if ($page > 0)
|
||||
{
|
||||
$return['prev'] = \descartes\Router::url('Api', __FUNCTION__, ['page' => $page - 1], [
|
||||
'api_key' => $this->user['api_key'],
|
||||
'volume' => $volume,
|
||||
'percent_failed' => $percent_failed * 100,
|
||||
'percent_unknown' => $percent_unknown * 100
|
||||
]);
|
||||
}
|
||||
|
||||
$return['response'] = $invalid_numbers;
|
||||
$this->auto_http_code(true);
|
||||
|
||||
return $this->json($return, false);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -87,16 +87,50 @@ namespace controllers\publics;
|
|||
$stats_start_date_formated = $stats_start_date->format('Y-m-d');
|
||||
}
|
||||
|
||||
$this->render('dashboard/show', [
|
||||
'nb_contacts' => $nb_contacts,
|
||||
'nb_groups' => $nb_groups,
|
||||
'nb_scheduleds' => $nb_scheduleds,
|
||||
'nb_sendeds' => $nb_sendeds,
|
||||
'nb_receiveds' => $nb_receiveds,
|
||||
'nb_unreads' => $nb_unreads,
|
||||
'quota_unused' => $quota_unused,
|
||||
'sendeds' => $sendeds,
|
||||
'receiveds' => $receiveds,
|
||||
'events' => $events,
|
||||
'stats_start_date_formated' => $stats_start_date_formated,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return stats about sended sms
|
||||
*/
|
||||
public function stats_sended()
|
||||
{
|
||||
$id_user = $_SESSION['user']['id'];
|
||||
|
||||
//Création de la date d'il y a 30 jours
|
||||
$now = new \DateTime();
|
||||
$one_month = new \DateInterval('P1M');
|
||||
$stats_start_date = clone $now;
|
||||
$stats_start_date->sub($one_month);
|
||||
$stats_start_date_formated = $stats_start_date->format('Y-m-d');
|
||||
|
||||
//If user have a quota and the quota start before today, use quota start date instead
|
||||
$quota = $this->internal_quota->get_user_quota($id_user);
|
||||
if ($quota && (new \DateTime($quota['start_date']) <= $now) && (new \DateTime($quota['expiration_date']) > $now))
|
||||
{
|
||||
$stats_start_date = new \DateTime($quota['start_date']);
|
||||
$stats_start_date_formated = $stats_start_date->format('Y-m-d');
|
||||
}
|
||||
|
||||
$nb_sendeds_by_day = $this->internal_sended->count_by_day_and_status_since_for_user($id_user, $stats_start_date_formated);
|
||||
$nb_receiveds_by_day = $this->internal_received->count_by_day_since_for_user($id_user, $stats_start_date_formated);
|
||||
|
||||
//On va traduire ces données pour les afficher en graphique
|
||||
$array_bar_chart_sended = [];
|
||||
$array_bar_chart_received = [];
|
||||
|
||||
$date = clone $stats_start_date;
|
||||
$one_day = new \DateInterval('P1D');
|
||||
$i = 0;
|
||||
|
||||
//On va construire un tableau avec la date en clef, et les données pour chaque date
|
||||
while ($date <= $now)
|
||||
|
@ -109,15 +143,13 @@ namespace controllers\publics;
|
|||
'sendeds_delivered' => 0,
|
||||
];
|
||||
|
||||
$array_bar_chart_received[$date_f] = ['period' => $date_f, 'receiveds' => 0];
|
||||
|
||||
$date->add($one_day);
|
||||
}
|
||||
|
||||
$total_sendeds = 0;
|
||||
$total_receiveds = 0;
|
||||
|
||||
//0n remplie le tableau avec les données adaptées
|
||||
//On remplie le tableau avec les données adaptées
|
||||
foreach ($nb_sendeds_by_day as $nb_sended)
|
||||
{
|
||||
$array_bar_chart_sended[$nb_sended['at_ymd']]['sendeds_' . $nb_sended['status']] = $nb_sended['nb'];
|
||||
|
@ -125,6 +157,59 @@ namespace controllers\publics;
|
|||
$total_sendeds += $nb_sended['nb'];
|
||||
}
|
||||
|
||||
$nb_days = $stats_start_date->diff($now)->days + 1;
|
||||
$avg_sendeds = round($total_sendeds / $nb_days, 2);
|
||||
|
||||
$array_bar_chart_sended = array_values($array_bar_chart_sended);
|
||||
|
||||
header('content-type:application/json');
|
||||
echo json_encode([
|
||||
'data_bar_chart_sended' => $array_bar_chart_sended,
|
||||
'avg_sendeds' => $avg_sendeds,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return stats about received sms
|
||||
*/
|
||||
public function stats_received()
|
||||
{
|
||||
$id_user = $_SESSION['user']['id'];
|
||||
|
||||
//Création de la date d'il y a 30 jours
|
||||
$now = new \DateTime();
|
||||
$one_month = new \DateInterval('P1M');
|
||||
$stats_start_date = clone $now;
|
||||
$stats_start_date->sub($one_month);
|
||||
$stats_start_date_formated = $stats_start_date->format('Y-m-d');
|
||||
|
||||
$quota = $this->internal_quota->get_user_quota($id_user);
|
||||
if ($quota && (new \DateTime($quota['start_date']) <= $now) && (new \DateTime($quota['expiration_date']) > $now))
|
||||
{
|
||||
$stats_start_date = new \DateTime($quota['start_date']);
|
||||
$stats_start_date_formated = $stats_start_date->format('Y-m-d');
|
||||
}
|
||||
|
||||
$nb_receiveds_by_day = $this->internal_received->count_by_day_since_for_user($id_user, $stats_start_date_formated);
|
||||
|
||||
//On va traduire ces données pour les afficher en graphique
|
||||
$array_bar_chart_received = [];
|
||||
|
||||
$date = clone $stats_start_date;
|
||||
$one_day = new \DateInterval('P1D');
|
||||
|
||||
//On va construire un tableau avec la date en clef, et les données pour chaque date
|
||||
while ($date <= $now)
|
||||
{
|
||||
$date_f = $date->format('Y-m-d');
|
||||
$array_bar_chart_received[$date_f] = ['period' => $date_f, 'receiveds' => 0];
|
||||
|
||||
$date->add($one_day);
|
||||
}
|
||||
|
||||
$total_receiveds = 0;
|
||||
|
||||
foreach ($nb_receiveds_by_day as $date => $nb_received)
|
||||
{
|
||||
$array_bar_chart_received[$date]['receiveds'] = $nb_received;
|
||||
|
@ -132,28 +217,15 @@ namespace controllers\publics;
|
|||
}
|
||||
|
||||
$nb_days = $stats_start_date->diff($now)->days + 1;
|
||||
$avg_sendeds = round($total_sendeds / $nb_days, 2);
|
||||
$avg_receiveds = round($total_receiveds / $nb_days, 2);
|
||||
|
||||
$array_bar_chart_sended = array_values($array_bar_chart_sended);
|
||||
$array_bar_chart_received = array_values($array_bar_chart_received);
|
||||
|
||||
$this->render('dashboard/show', [
|
||||
'nb_contacts' => $nb_contacts,
|
||||
'nb_groups' => $nb_groups,
|
||||
'nb_scheduleds' => $nb_scheduleds,
|
||||
'nb_sendeds' => $nb_sendeds,
|
||||
'nb_receiveds' => $nb_receiveds,
|
||||
'nb_unreads' => $nb_unreads,
|
||||
'avg_sendeds' => $avg_sendeds,
|
||||
header('content-type:application/json');
|
||||
echo json_encode([
|
||||
'data_bar_chart_received' => $array_bar_chart_received,
|
||||
'avg_receiveds' => $avg_receiveds,
|
||||
'quota_unused' => $quota_unused,
|
||||
'sendeds' => $sendeds,
|
||||
'receiveds' => $receiveds,
|
||||
'events' => $events,
|
||||
'data_bar_chart_sended' => json_encode($array_bar_chart_sended),
|
||||
'data_bar_chart_received' => json_encode($array_bar_chart_received),
|
||||
'stats_start_date_formated' => $stats_start_date_formated,
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -56,7 +56,7 @@ namespace controllers\publics;
|
|||
*/
|
||||
public function list_json()
|
||||
{
|
||||
$entities = $this->internal_received->get_discussions_for_user($_SESSION['user']['id']);
|
||||
$entities = $this->internal_received->get_discussions_for_user($_SESSION['user']['id'], 1000);
|
||||
|
||||
foreach ($entities as &$entity)
|
||||
{
|
||||
|
|
|
@ -535,6 +535,16 @@ class Phone extends \descartes\Controller
|
|||
foreach ($ids as $id)
|
||||
{
|
||||
$phone = $this->internal_phone->get_for_user($id_user, $id);
|
||||
if (!$phone)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($phone['status'] === \models\Phone::STATUS_DISABLED)
|
||||
{
|
||||
\FlashMessage\FlashMessage::push('error', 'Certains téléphones ont été désactivés manuellements, vous devez les réactiver manuellement.');
|
||||
continue;
|
||||
}
|
||||
|
||||
// If user have activated phone limits, check if RaspiSMS phone limit have already been reached
|
||||
$limit_reached = false;
|
||||
|
@ -581,6 +591,48 @@ class Phone extends \descartes\Controller
|
|||
return $this->redirect(\descartes\Router::url('Phone', 'list'));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Manually disable/enable phones
|
||||
* @param array int $_GET['ids'] : ids of phones we want to update status
|
||||
* @param string $new_status : New status of the phone, either 'disabled' or 'available'
|
||||
* @param $csrf : CSRF token
|
||||
*/
|
||||
public function change_status ($new_status, $csrf)
|
||||
{
|
||||
if (!$this->verify_csrf($csrf))
|
||||
{
|
||||
\FlashMessage\FlashMessage::push('danger', 'Jeton CSRF invalid !');
|
||||
|
||||
return $this->redirect(\descartes\Router::url('Phone', 'add'));
|
||||
}
|
||||
|
||||
if (!in_array($new_status, [\models\Phone::STATUS_AVAILABLE, \models\Phone::STATUS_DISABLED]))
|
||||
{
|
||||
\FlashMessage\FlashMessage::push('danger', 'Seul les status disponibles et désactivés peuvent être définis manuellement.');
|
||||
|
||||
return $this->redirect(\descartes\Router::url('Phone', 'add'));
|
||||
}
|
||||
|
||||
$ids = $_GET['ids'] ?? [];
|
||||
$id_user = $_SESSION['user']['id'];
|
||||
|
||||
foreach ($ids as $id)
|
||||
{
|
||||
$phone = $this->internal_phone->get_for_user($id_user, $id);
|
||||
if (!$phone)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$status_update = $this->internal_phone->update_status($id, $new_status);
|
||||
}
|
||||
|
||||
\FlashMessage\FlashMessage::push('success', 'Les status des téléphones ont bien été mis à jour manuellement.');
|
||||
|
||||
return $this->redirect(\descartes\Router::url('Phone', 'list'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of phones as a JSON array
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of RaspiSMS.
|
||||
*
|
||||
* (c) Pierre-Lin Bonnemaison <plebwebsas@gmail.com>
|
||||
*
|
||||
* This source file is subject to the GPL-3.0 license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace controllers\publics;
|
||||
|
||||
/**
|
||||
* Statistics pages
|
||||
*/
|
||||
class Stat extends \descartes\Controller
|
||||
{
|
||||
private $internal_sended;
|
||||
private $internal_phone;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$bdd = \descartes\Model::_connect(DATABASE_HOST, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD);
|
||||
|
||||
$this->internal_sended = new \controllers\internals\Sended($bdd);
|
||||
$this->internal_phone = new \controllers\internals\Phone($bdd);
|
||||
|
||||
\controllers\internals\Tool::verifyconnect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the stats about sms status for a period by phone
|
||||
*
|
||||
* @return void;
|
||||
*/
|
||||
public function sms_status()
|
||||
{
|
||||
$id_user = $_SESSION['user']['id'];
|
||||
$phones = $this->internal_phone->gets_for_user($id_user);
|
||||
|
||||
$now = new \DateTime();
|
||||
$seven_days_interval = new \DateInterval('P7D');
|
||||
$seven_days_ago = clone($now);
|
||||
$seven_days_ago->sub($seven_days_interval);
|
||||
|
||||
$this->render('stat/sms-status', [
|
||||
'phones' => $phones,
|
||||
'now' => $now,
|
||||
'seven_days_ago' => $seven_days_ago,
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -11,6 +11,7 @@
|
|||
|
||||
namespace daemons;
|
||||
|
||||
use controllers\internals\Queue;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
use Monolog\Logger;
|
||||
|
||||
|
@ -19,7 +20,7 @@ use Monolog\Logger;
|
|||
*/
|
||||
class Mailer extends AbstractDaemon
|
||||
{
|
||||
private $mailer_queue;
|
||||
private ?Queue $mailer_queue;
|
||||
private $last_message_at;
|
||||
private $bdd;
|
||||
|
||||
|
@ -49,27 +50,15 @@ class Mailer extends AbstractDaemon
|
|||
$find_message = true;
|
||||
while ($find_message)
|
||||
{
|
||||
//Call message
|
||||
$msgtype = null;
|
||||
$maxsize = 409600;
|
||||
$message = null;
|
||||
$message = $this->mailer_queue->read(QUEUE_TYPE_EMAIL);
|
||||
|
||||
$error_code = null;
|
||||
$success = msg_receive($this->mailer_queue, QUEUE_TYPE_EMAIL, $msgtype, $maxsize, $message, true, MSG_IPC_NOWAIT, $error_code); //MSG_IPC_NOWAIT == dont wait if no message found
|
||||
if (!$success && MSG_ENOMSG !== $error_code)
|
||||
if ($message === null)
|
||||
{
|
||||
$this->logger->critical('Error for mailer queue reading, error code : ' . $error_code);
|
||||
$find_message = false;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$message)
|
||||
{
|
||||
$find_message = false;
|
||||
|
||||
continue;
|
||||
}
|
||||
$message = json_decode($message, true);
|
||||
|
||||
$this->logger->info('Try sending email : ' . json_encode($message));
|
||||
|
||||
|
@ -92,7 +81,7 @@ class Mailer extends AbstractDaemon
|
|||
public function on_start()
|
||||
{
|
||||
//Set last message at to construct time
|
||||
$this->mailer_queue = msg_get_queue(QUEUE_ID_EMAIL);
|
||||
$this->mailer_queue = new Queue(QUEUE_ID_EMAIL);
|
||||
|
||||
$this->logger->info('Starting Mailer daemon with pid ' . getmypid());
|
||||
}
|
||||
|
@ -101,8 +90,6 @@ class Mailer extends AbstractDaemon
|
|||
{
|
||||
//Delete queue on daemon close
|
||||
$this->logger->info('Closing queue : ' . QUEUE_ID_EMAIL);
|
||||
msg_remove_queue($this->mailer_queue);
|
||||
|
||||
$this->logger->info('Stopping Mailer daemon with pid ' . getmypid());
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
|
||||
namespace daemons;
|
||||
|
||||
use controllers\internals\Queue;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
use Monolog\Logger;
|
||||
|
||||
|
@ -22,7 +23,7 @@ class Phone extends AbstractDaemon
|
|||
private $max_inactivity = 5 * 60;
|
||||
private $read_delay = 20 / 0.5;
|
||||
private $read_tick = 0;
|
||||
private $msg_queue;
|
||||
private ?Queue $queue;
|
||||
private $webhook_queue;
|
||||
private $last_message_at;
|
||||
private $phone;
|
||||
|
@ -85,7 +86,7 @@ class Phone extends AbstractDaemon
|
|||
//Set last message at to construct time
|
||||
$this->last_message_at = microtime(true);
|
||||
|
||||
$this->msg_queue = msg_get_queue(QUEUE_ID_PHONE);
|
||||
$this->queue = new Queue(QUEUE_ID_PHONE);
|
||||
|
||||
//Instanciate adapter
|
||||
$adapter_class = $this->phone['adapter'];
|
||||
|
@ -114,30 +115,17 @@ class Phone extends AbstractDaemon
|
|||
$find_message = true;
|
||||
while ($find_message)
|
||||
{
|
||||
//Call message
|
||||
$msgtype = null;
|
||||
$maxsize = 409600;
|
||||
$message = null;
|
||||
|
||||
// Message type is forged from a prefix concat with the phone ID
|
||||
$message_type = (int) QUEUE_TYPE_SEND_MSG_PREFIX . $this->phone['id'];
|
||||
$error_code = null;
|
||||
$success = msg_receive($this->msg_queue, $message_type, $msgtype, $maxsize, $message, true, MSG_IPC_NOWAIT, $error_code); //MSG_IPC_NOWAIT == dont wait if no message found
|
||||
$message = $this->queue->read($message_type);
|
||||
|
||||
if (!$success && MSG_ENOMSG !== $error_code)
|
||||
{
|
||||
$this->logger->critical('Error reading MSG SEND Queue, error code : ' . $error_code);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$message)
|
||||
if ($message === null)
|
||||
{
|
||||
$find_message = false;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$message = json_decode($message, true);
|
||||
|
||||
//Update last message time
|
||||
$this->last_message_at = microtime(true);
|
||||
|
||||
|
|
|
@ -11,6 +11,8 @@
|
|||
|
||||
namespace daemons;
|
||||
|
||||
use controllers\internals\Queue;
|
||||
use Exception;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
use Monolog\Logger;
|
||||
|
||||
|
@ -19,12 +21,9 @@ use Monolog\Logger;
|
|||
*/
|
||||
class Sender extends AbstractDaemon
|
||||
{
|
||||
private $internal_phone;
|
||||
private $internal_scheduled;
|
||||
private $internal_received;
|
||||
private $internal_sended;
|
||||
private $bdd;
|
||||
private $msg_queue;
|
||||
private ?Queue $queue;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
|
@ -44,9 +43,7 @@ class Sender extends AbstractDaemon
|
|||
|
||||
public function run()
|
||||
{
|
||||
//Create the internal controllers
|
||||
$this->internal_scheduled = new \controllers\internals\Scheduled($this->bdd);
|
||||
$this->internal_sended = new \controllers\internals\Sended($this->bdd);
|
||||
|
||||
//Get smss and transmit order to send to appropriate phone daemon
|
||||
$smss_per_scheduled = $this->internal_scheduled->get_smss_to_send();
|
||||
|
@ -64,12 +61,6 @@ class Sender extends AbstractDaemon
|
|||
{
|
||||
foreach ($smss_per_scheduled as $id_scheduled => $smss)
|
||||
{
|
||||
//If queue not already exists
|
||||
if (!msg_queue_exists(QUEUE_ID_PHONE) || !isset($this->msg_queue))
|
||||
{
|
||||
$this->msg_queue = msg_get_queue(QUEUE_ID_PHONE);
|
||||
}
|
||||
|
||||
foreach ($smss as $sms)
|
||||
{
|
||||
$msg = [
|
||||
|
@ -84,9 +75,10 @@ class Sender extends AbstractDaemon
|
|||
'medias' => $sms['medias'] ?? [],
|
||||
];
|
||||
|
||||
|
||||
// Message type is forged from a prefix concat with the phone ID
|
||||
$message_type = (int) QUEUE_TYPE_SEND_MSG_PREFIX . $sms['id_phone'];
|
||||
msg_send($this->msg_queue, $message_type, $msg);
|
||||
$this->queue->push(json_encode($msg), $message_type);
|
||||
$this->logger->info('Transmit sms send signal to phone ' . $sms['id_phone'] . ' on queue ' . QUEUE_ID_PHONE . ' with message type ' . $message_type . '.');
|
||||
}
|
||||
|
||||
|
@ -96,16 +88,25 @@ class Sender extends AbstractDaemon
|
|||
}
|
||||
|
||||
public function on_start()
|
||||
{
|
||||
try
|
||||
{
|
||||
$this->logger->info('Starting Sender with pid ' . getmypid());
|
||||
$this->bdd = \descartes\Model::_connect(DATABASE_HOST, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD);
|
||||
$this->queue = new Queue(QUEUE_ID_PHONE);
|
||||
}
|
||||
catch (Exception $e)
|
||||
{
|
||||
$this->logger->error('Failed to start sender daemon : ' . $e->getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function on_stop()
|
||||
{
|
||||
//Delete queue on daemon close
|
||||
$this->logger->info('Closing queue : ' . $this->msg_queue);
|
||||
msg_remove_queue($this->msg_queue);
|
||||
$this->logger->info('Closing queue : ' . QUEUE_ID_PHONE);
|
||||
$this->queue->close();
|
||||
|
||||
$this->logger->info('Stopping Sender with pid ' . getmypid());
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
|
||||
namespace daemons;
|
||||
|
||||
use controllers\internals\Queue;
|
||||
use GuzzleHttp\Promise\Utils;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
use Monolog\Logger;
|
||||
|
@ -20,11 +21,7 @@ use Monolog\Logger;
|
|||
*/
|
||||
class Webhook extends AbstractDaemon
|
||||
{
|
||||
private $webhook_queue;
|
||||
private $last_message_at;
|
||||
private $phone;
|
||||
private $adapter;
|
||||
private $bdd;
|
||||
private ?Queue $webhook_queue;
|
||||
private $guzzle_client;
|
||||
|
||||
/**
|
||||
|
@ -56,30 +53,17 @@ class Webhook extends AbstractDaemon
|
|||
$promises = [];
|
||||
while ($find_message)
|
||||
{
|
||||
//Call message
|
||||
$msgtype = null;
|
||||
$maxsize = 409600;
|
||||
$message = null;
|
||||
$message = $this->webhook_queue->read(QUEUE_TYPE_WEBHOOK);
|
||||
|
||||
$error_code = null;
|
||||
$success = msg_receive($this->webhook_queue, QUEUE_TYPE_WEBHOOK, $msgtype, $maxsize, $message, true, MSG_IPC_NOWAIT, $error_code); //MSG_IPC_NOWAIT == dont wait if no message found
|
||||
if (!$success && MSG_ENOMSG !== $error_code)
|
||||
if ($message === null)
|
||||
{
|
||||
$this->logger->critical('Error for webhook queue reading, error code : ' . $error_code);
|
||||
$find_message = false;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$message)
|
||||
{
|
||||
$find_message = false;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->logger->info('Trigger webhook : ' . json_encode($message));
|
||||
$this->logger->info('Trigger webhook : ' . $message);
|
||||
|
||||
$message = json_decode($message, true);
|
||||
$promises[] = $this->guzzle_client->postAsync($message['url'], ['form_params' => $message['data']]);
|
||||
}
|
||||
|
||||
|
@ -97,10 +81,13 @@ class Webhook extends AbstractDaemon
|
|||
|
||||
public function on_start()
|
||||
{
|
||||
//Set last message at to construct time
|
||||
$this->last_message_at = microtime(true);
|
||||
|
||||
$this->webhook_queue = msg_get_queue(QUEUE_ID_WEBHOOK);
|
||||
try{
|
||||
$this->webhook_queue = new Queue(QUEUE_ID_WEBHOOK);
|
||||
}
|
||||
catch (\Exception $e)
|
||||
{
|
||||
$this->logger->info('Webhook : failed with ' . $e->getMessage());
|
||||
}
|
||||
|
||||
$this->logger->info('Starting Webhook daemon with pid ' . getmypid());
|
||||
}
|
||||
|
@ -109,7 +96,7 @@ class Webhook extends AbstractDaemon
|
|||
{
|
||||
//Delete queue on daemon close
|
||||
$this->logger->info('Closing queue : ' . QUEUE_ID_WEBHOOK);
|
||||
msg_remove_queue($this->webhook_queue);
|
||||
unset($this->webhook_queue);
|
||||
|
||||
$this->logger->info('Stopping Webhook daemon with pid ' . getmypid());
|
||||
}
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
class AddStatusDisabledPhone extends AbstractMigration
|
||||
{
|
||||
/**
|
||||
* Change Method.
|
||||
*
|
||||
* Write your reversible migrations using this method.
|
||||
*
|
||||
* More information on writing migrations is available here:
|
||||
* https://book.cakephp.org/phinx/0/en/migrations.html
|
||||
*
|
||||
* The following commands can be used in this method and Phinx will
|
||||
* automatically reverse them when rolling back:
|
||||
*
|
||||
* createTable
|
||||
* renameTable
|
||||
* addColumn
|
||||
* addCustomColumn
|
||||
* renameColumn
|
||||
* addIndex
|
||||
* addForeignKey
|
||||
*
|
||||
* Any other destructive changes will result in an error when trying to
|
||||
* rollback the migration.
|
||||
*
|
||||
* Remember to call "create()" or "update()" and NOT "save()" when working
|
||||
* with the Table class.
|
||||
*/
|
||||
public function change()
|
||||
{
|
||||
$table = $this->table('phone');
|
||||
$table->changeColumn('status', 'enum', ['values' => ['available', 'unavailable', 'no_credit', 'limit_reached', 'disabled'], 'default' => 'available']);
|
||||
$table->save();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
class AddWebhookPhoneReliability extends AbstractMigration
|
||||
{
|
||||
/**
|
||||
* Change Method.
|
||||
*
|
||||
* Write your reversible migrations using this method.
|
||||
*
|
||||
* More information on writing migrations is available here:
|
||||
* https://book.cakephp.org/phinx/0/en/migrations.html
|
||||
*
|
||||
* The following commands can be used in this method and Phinx will
|
||||
* automatically reverse them when rolling back:
|
||||
*
|
||||
* createTable
|
||||
* renameTable
|
||||
* addColumn
|
||||
* addCustomColumn
|
||||
* renameColumn
|
||||
* addIndex
|
||||
* addForeignKey
|
||||
*
|
||||
* Any other destructive changes will result in an error when trying to
|
||||
* rollback the migration.
|
||||
*
|
||||
* Remember to call "create()" or "update()" and NOT "save()" when working
|
||||
* with the Table class.
|
||||
*/
|
||||
public function change()
|
||||
{
|
||||
$table = $this->table('webhook');
|
||||
$table->changeColumn('type', 'enum', ['values' => ['send_sms','send_sms_status_change','receive_sms','inbound_call', 'phone_reliability']]);
|
||||
$table->save();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
class AddTablePhoneReliabilityHistory extends AbstractMigration
|
||||
{
|
||||
public function change()
|
||||
{
|
||||
// Create the phone_reliability_history table
|
||||
// This table store history of reliability alert for phones, so we can use last alert as min date
|
||||
// for surveillance periode, preventing triggering same alert in a loop
|
||||
$this->table('phone_reliability_history')
|
||||
->addColumn('id_user', 'integer', ['null' => false])
|
||||
->addColumn('id_phone', 'integer', ['null' => false])
|
||||
->addColumn('type', 'string', ['null' => false, 'limit' => 100])
|
||||
->addColumn('created_at', 'timestamp', ['null' => false, 'default' => 'CURRENT_TIMESTAMP'])
|
||||
->addColumn('updated_at', 'timestamp', ['null' => true, 'update' => 'CURRENT_TIMESTAMP'])
|
||||
->addForeignKey('id_user', 'user', 'id', ['delete' => 'CASCADE', 'update' => 'CASCADE'])
|
||||
->addForeignKey('id_phone', 'phone', 'id', ['delete' => 'CASCADE', 'update' => 'CASCADE'])
|
||||
->create();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
class AddIndexSendedStatus extends AbstractMigration
|
||||
{
|
||||
/**
|
||||
* Change Method.
|
||||
*
|
||||
* Write your reversible migrations using this method.
|
||||
*
|
||||
* More information on writing migrations is available here:
|
||||
* https://book.cakephp.org/phinx/0/en/migrations.html
|
||||
*
|
||||
* The following commands can be used in this method and Phinx will
|
||||
* automatically reverse them when rolling back:
|
||||
*
|
||||
* createTable
|
||||
* renameTable
|
||||
* addColumn
|
||||
* addCustomColumn
|
||||
* renameColumn
|
||||
* addIndex
|
||||
* addForeignKey
|
||||
*
|
||||
* Any other destructive changes will result in an error when trying to
|
||||
* rollback the migration.
|
||||
*
|
||||
* Remember to call "create()" or "update()" and NOT "save()" when working
|
||||
* with the Table class.
|
||||
*/
|
||||
public function change()
|
||||
{
|
||||
$table = $this->table('sended');
|
||||
$table->addIndex(['id_user', 'status']);
|
||||
$table->update();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
class AddIndexDestinationStatusSended extends AbstractMigration
|
||||
{
|
||||
/**
|
||||
* Change Method.
|
||||
*
|
||||
* Write your reversible migrations using this method.
|
||||
*
|
||||
* More information on writing migrations is available here:
|
||||
* https://book.cakephp.org/phinx/0/en/migrations.html
|
||||
*
|
||||
* The following commands can be used in this method and Phinx will
|
||||
* automatically reverse them when rolling back:
|
||||
*
|
||||
* createTable
|
||||
* renameTable
|
||||
* addColumn
|
||||
* addCustomColumn
|
||||
* renameColumn
|
||||
* addIndex
|
||||
* addForeignKey
|
||||
*
|
||||
* Any other destructive changes will result in an error when trying to
|
||||
* rollback the migration.
|
||||
*
|
||||
* Remember to call "create()" or "update()" and NOT "save()" when working
|
||||
* with the Table class.
|
||||
*/
|
||||
public function change()
|
||||
{
|
||||
$table = $this->table('sended');
|
||||
$table->addIndex(['destination', 'status']);
|
||||
$table->update();
|
||||
}
|
||||
}
|
16
env.php.dist
16
env.php.dist
|
@ -91,6 +91,22 @@
|
|||
'shorten_url' => 0,
|
||||
'smsstop_respond' => 1,
|
||||
'smsstop_response' => 'Demande prise en compte, vous ne recevrez plus de messages.',
|
||||
'phone_reliability_failed' => 1,
|
||||
'phone_reliability_failed_rate_limit' => 10,
|
||||
'phone_reliability_failed_volume' => 25,
|
||||
'phone_reliability_failed_period' => 120,
|
||||
'phone_reliability_failed_grace_period' => 1,
|
||||
'phone_reliability_failed_email' => 0,
|
||||
'phone_reliability_failed_webhook' => 1,
|
||||
'phone_reliability_failed_auto_disable' => 0,
|
||||
'phone_reliability_unknown' => 0,
|
||||
'phone_reliability_unknown_rate_limit' => 25,
|
||||
'phone_reliability_unknown_volume' => 25,
|
||||
'phone_reliability_unknown_period' => 120,
|
||||
'phone_reliability_unknown_grace_period' => 1,
|
||||
'phone_reliability_unknown_email' => 0,
|
||||
'phone_reliability_unknown_webhook' => 1,
|
||||
'phone_reliability_unknown_auto_disable' => 0,
|
||||
],
|
||||
];
|
||||
|
||||
|
|
|
@ -26,6 +26,11 @@
|
|||
'HOST' => '%APP_URL_SHORTENER_HOST%',
|
||||
'USER' => '%APP_URL_SHORTENER_USER%',
|
||||
'PASS' => '%APP_URL_SHORTENER_PASS%',
|
||||
]
|
||||
],
|
||||
|
||||
// Define if we should use a Redis instance instead of System V Queues
|
||||
'USE_REDIS_QUEUES' => false,
|
||||
'REDIS_HOST' => '%APP_REDIS_HOST%',
|
||||
'REDIS_PORT' => '%APP_REDIS_PORT%',
|
||||
'REDIS_PASSWORD' => '%APP_REDIS_PASSWORD%',
|
||||
];
|
||||
|
|
|
@ -14,10 +14,11 @@ namespace models;
|
|||
class Phone extends StandardModel
|
||||
{
|
||||
|
||||
const STATUS_AVAILABLE = 'available';
|
||||
const STATUS_UNAVAILABLE = 'unavailable';
|
||||
const STATUS_NO_CREDIT = 'no_credit';
|
||||
const STATUS_LIMIT_REACHED = 'limit_reached';
|
||||
const STATUS_AVAILABLE = 'available'; # Everything OK
|
||||
const STATUS_UNAVAILABLE = 'unavailable'; # RaspiSMS cannot communication with the phone
|
||||
const STATUS_DISABLED = 'disabled'; # Phone have been manually or automatically disabled by user/system
|
||||
const STATUS_NO_CREDIT = 'no_credit'; # Phone have no more credit available
|
||||
const STATUS_LIMIT_REACHED = 'limit_reached'; # We reached the limit in of SMS in RaspiSMS for this phone
|
||||
|
||||
/**
|
||||
* Return all phones that belongs to active users
|
||||
|
@ -109,7 +110,6 @@ namespace models;
|
|||
return $this->_delete('phone_limit', ['id_phone' => $id_phone]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return table name.
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of RaspiSMS.
|
||||
*
|
||||
* (c) Pierre-Lin Bonnemaison <plebwebsas@gmail.com>
|
||||
*
|
||||
* This source file is subject to the GPL-3.0 license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace models;
|
||||
|
||||
class PhoneReliabilityHistory extends StandardModel
|
||||
{
|
||||
/**
|
||||
* Find all unreliable phones for a user, based on sended sms status, rate limit, etc.
|
||||
*
|
||||
* @param int $id_user : User id
|
||||
* @param string $sms_status : Status of SMS to use to calculate rate
|
||||
* @param float $rate_limit : Percentage of SMS matching status after which we consider the phone unreliable
|
||||
* @param int $min_volume : Minimum number of sms we need to have to consider the statistic relevent
|
||||
* @param int $period : The time span in minutes from which SMS counting should begin.
|
||||
* @param int $grace_period : How long in minutes should we wait before including a SMS in counting
|
||||
*
|
||||
* @return array : A list of unreliable phone for the user, with phone id, total number of sms, and rate of failed sms
|
||||
*/
|
||||
public function find_unreliable_phones (int $id_user, string $sms_status, float $rate_limit, int $min_volume, int $period, int $grace_period)
|
||||
{
|
||||
return $this->_run_query("
|
||||
WITH recent_messages AS (
|
||||
SELECT
|
||||
sended.id_phone AS id_phone,
|
||||
COUNT(sended.id) AS total,
|
||||
SUM(sended.status = :sms_status) AS unreliable
|
||||
FROM
|
||||
sended
|
||||
JOIN
|
||||
phone
|
||||
ON
|
||||
sended.id_phone = phone.id
|
||||
LEFT JOIN
|
||||
(
|
||||
SELECT
|
||||
id_phone,
|
||||
MAX(created_at) AS last_alert_time
|
||||
FROM
|
||||
phone_reliability_history
|
||||
WHERE
|
||||
type = :sms_status
|
||||
GROUP BY
|
||||
id_phone
|
||||
) AS last_alerts
|
||||
ON
|
||||
sended.id_phone = last_alerts.id_phone
|
||||
WHERE
|
||||
sended.id_user = :id_user
|
||||
AND
|
||||
phone.status != 'disabled'
|
||||
AND
|
||||
sended.at > IFNULL(last_alerts.last_alert_time, '1970-01-01')
|
||||
AND
|
||||
sended.at BETWEEN NOW() - INTERVAL :period MINUTE AND NOW() - INTERVAL :grace_period MINUTE
|
||||
GROUP BY
|
||||
id_phone
|
||||
)
|
||||
SELECT
|
||||
id_phone,
|
||||
total,
|
||||
unreliable,
|
||||
(unreliable / total) AS rate
|
||||
FROM
|
||||
recent_messages
|
||||
WHERE
|
||||
total >= :min_volume
|
||||
AND
|
||||
(unreliable / total) >= :rate_limit;
|
||||
", [
|
||||
'id_user' => $id_user,
|
||||
'sms_status' => $sms_status,
|
||||
'period' => $period,
|
||||
'grace_period' => $grace_period,
|
||||
'min_volume' => $min_volume,
|
||||
'rate_limit' => $rate_limit,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return table name.
|
||||
*/
|
||||
protected function get_table_name(): string
|
||||
{
|
||||
return 'phone_reliability_history';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of RaspiSMS.
|
||||
*
|
||||
* (c) Pierre-Lin Bonnemaison <plebwebsas@gmail.com>
|
||||
*
|
||||
* This source file is subject to the GPL-3.0 license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace models;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
interface Queue
|
||||
{
|
||||
/**
|
||||
* A FIFO Queue to exchange messages, the backend mechanism can be whatever we want, but the queue take message, tag for routing is optionnal
|
||||
* @param string $id : A unique identifier for the queue
|
||||
*/
|
||||
public function __construct($id);
|
||||
|
||||
/**
|
||||
* Add a message to the queue
|
||||
*
|
||||
* @param string $message : The message to add to the queue, must be a string, for complex data just use json
|
||||
* @param ?string $tag : A tag to associate to the message for routing purposes, if not set will add to general queue
|
||||
*/
|
||||
public function push($message, ?string $tag = null);
|
||||
|
||||
/**
|
||||
* Read the older message in the queue (non-blocking)
|
||||
* @param ?string $tag : A tag to associate to the message for routing purposes, if not set will read from general queue
|
||||
* @return ?string $message : The oldest message or null if no message found, can be anything
|
||||
*/
|
||||
public function read(?string $tag = null);
|
||||
}
|
|
@ -267,27 +267,33 @@ namespace models;
|
|||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_discussions_for_user(int $id_user)
|
||||
public function get_discussions_for_user(int $id_user, ?int $nb_entry = null, ?int $page = null)
|
||||
{
|
||||
$query = '
|
||||
SELECT discussions.at, discussions.number, contact.name as contact_name
|
||||
FROM (
|
||||
SELECT at, destination as number FROM sended
|
||||
WHERE id_user = :id_user
|
||||
UNION (
|
||||
SELECT at, origin as number FROM received
|
||||
WHERE id_user = :id_user
|
||||
)
|
||||
) as discussions
|
||||
LEFT JOIN contact
|
||||
ON discussions.number = contact.number AND id_user = :id_user
|
||||
GROUP BY number
|
||||
SELECT at, destination AS number, contact.name AS contact_name
|
||||
FROM sended
|
||||
LEFT JOIN contact ON contact.number = sended.destination
|
||||
WHERE sended.id_user = :id_user
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT at, origin AS number, contact.name AS contact_name
|
||||
FROM received
|
||||
LEFT JOIN contact ON contact.number = received.origin
|
||||
WHERE received.id_user = :id_user
|
||||
|
||||
ORDER BY at DESC
|
||||
';
|
||||
|
||||
$params = ['id_user' => $id_user];
|
||||
|
||||
return $this->_run_query($query, $params);
|
||||
if ($nb_entry !== null)
|
||||
{
|
||||
$query .= 'LIMIT ' . intval($nb_entry) * intval($page) . ', ' . intval($nb_entry);
|
||||
}
|
||||
|
||||
$results = $this->_run_query($query, $params);
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of RaspiSMS.
|
||||
*
|
||||
* (c) Pierre-Lin Bonnemaison <plebwebsas@gmail.com>
|
||||
*
|
||||
* This source file is subject to the GPL-3.0 license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace models;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
class RedisQueue implements Queue
|
||||
{
|
||||
private \Redis $redis;
|
||||
private $group;
|
||||
private $consumer;
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* A Redis queue to store and exchange messages using redis streams
|
||||
* routing is based on queue uniq id as stream name, combined with ':tag' if routing is needed, messages are stored as json
|
||||
* @param string $id : A unique identifier for the queue
|
||||
* @param array $redis_parameters : Parameters for the redis server, such as host, port, etc. Default to a basic local redis on port 6379
|
||||
* @param string $group : Name to use for the redis group that must read this queue, default to 'default'
|
||||
* @param string $consumer : Name to use for the redis consumer in the group that must read this queue, default to 'default'
|
||||
*/
|
||||
public function __construct($id, $redis_parameters = [], $group = 'default', $consumer = 'default')
|
||||
{
|
||||
$this->id = $id;
|
||||
$this->redis = new \Redis();
|
||||
$success = $this->redis->connect($redis_parameters['host'], intval($redis_parameters['port']), 1, '', 0, 0, ['auth' => $redis_parameters['auth']]);
|
||||
|
||||
if (!$success)
|
||||
{
|
||||
throw new \Exception('Failed to connect to redis server !');
|
||||
}
|
||||
|
||||
$this->group = $group;
|
||||
$this->consumer = $consumer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a message to the queue
|
||||
*
|
||||
* @param string $message : The message to add to the queue
|
||||
* @param ?string $tag : A tag to associate to the message for routing purposes, if null will add to general queue
|
||||
*/
|
||||
public function push($message, ?string $tag = null)
|
||||
{
|
||||
$stream = $this->id . ($tag !== null ? ":$tag" : '');
|
||||
$success = $this->redis->xAdd($stream, '*', ['message' => $message]);
|
||||
|
||||
if (!$success)
|
||||
{
|
||||
throw new \Exception('Failed to push a message !');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the older message in the queue
|
||||
*
|
||||
* @return mixed $message : The oldest message or null if no message found, can be anything
|
||||
* @param ?string $tag : A tag to associate to the message for routing purposes, if null will read from general queue
|
||||
* @param mixed : The message to add to the queue, can be anything, the queue will have to treat it by itself
|
||||
*/
|
||||
public function read(?string $tag = null)
|
||||
{
|
||||
$stream = $this->id . ($tag !== null ? ":$tag" : '');
|
||||
|
||||
// Create the consumer group if it doesn't already exist
|
||||
try
|
||||
{
|
||||
$this->redis->xGroup('CREATE', $stream, $this->group, '$', true);
|
||||
}
|
||||
catch (Exception $e)
|
||||
{
|
||||
// Ignore error if the group already exists
|
||||
}
|
||||
|
||||
// Read a single message starting from the oldest (>)
|
||||
$messages = $this->redis->xReadGroup($this->group, $this->consumer, [$stream => '>'], 1);
|
||||
if (!count($messages))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find the message, acknowledge it and return it
|
||||
foreach ($messages as $stream_name => $entries)
|
||||
{
|
||||
foreach ($entries as $message_id => $message)
|
||||
{
|
||||
$success = $this->redis->xAck($stream, $this->group, [$message_id]);
|
||||
return $message['message'];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -294,6 +294,90 @@ namespace models;
|
|||
return $result[0] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get number of sended SMS by day and status between two dates, possibly by sending phone.
|
||||
*
|
||||
* @param int $id_user : user id
|
||||
* @param \DateTime $start_date : Date since which we want the messages
|
||||
* @param \DateTime $end_date : Date until which we want the messages
|
||||
* @param ?int $id_phone : Id of the phone to search sended for, null by default get all phones
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_sended_status_stats ($id_user, $start_date, $end_date, ?int $id_phone = null)
|
||||
{
|
||||
$params = [
|
||||
'start_date' => $start_date->format('y-m-d H:i:s'),
|
||||
'end_date' => $end_date->format('y-m-d H:i:s'),
|
||||
'id_user' => $id_user,
|
||||
];
|
||||
|
||||
$query = "
|
||||
SELECT DATE_FORMAT(at, '%Y-%m-%d') as at_ymd, id_phone, status, COUNT(id) as nb
|
||||
FROM sended
|
||||
WHERE id_user = :id_user
|
||||
AND id_phone IS NOT NULL
|
||||
AND at >= :start_date
|
||||
AND at <= :end_date
|
||||
";
|
||||
|
||||
if ($id_phone)
|
||||
{
|
||||
$params['id_phone'] = $id_phone;
|
||||
$query .= "
|
||||
AND id_phone = :id_phone
|
||||
";
|
||||
}
|
||||
|
||||
$query .= "
|
||||
GROUP BY at_ymd, status, id_phone
|
||||
ORDER BY at_ymd, id_phone, status
|
||||
";
|
||||
|
||||
|
||||
return $this->_run_query($query, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of invalid phone number we've sent message to
|
||||
*
|
||||
* @param int $id_user : user id
|
||||
* @param int $volume : Minimum number of sms sent to the number
|
||||
* @param float $percent_failed : Minimum ratio of failed message
|
||||
* @param float $percent_unknown : Minimum ratio of unknown message
|
||||
* @param int $limit : Limit of results
|
||||
* @param int $page : Page of results (offset = page * limit)
|
||||
*
|
||||
*/
|
||||
public function get_invalid_numbers (int $id_user, int $volume, float $percent_failed, float $percent_unknown, int $limit, int $page)
|
||||
{
|
||||
$query = "
|
||||
SELECT
|
||||
destination,
|
||||
COUNT(*) AS total_sms_sent,
|
||||
ROUND(SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) / COUNT(*), 2) AS failed_percentage,
|
||||
ROUND(SUM(CASE WHEN status = 'unknown' THEN 1 ELSE 0 END) / COUNT(*), 2) AS unknown_percentage
|
||||
FROM
|
||||
sended
|
||||
GROUP BY
|
||||
destination
|
||||
HAVING
|
||||
total_sms_sent >= :volume
|
||||
AND failed_percentage >= :percent_failed
|
||||
AND unknown_percentage >= :percent_unknown
|
||||
LIMIT " . intval($page * $limit) . "," . intval($limit) . "
|
||||
";
|
||||
|
||||
$params = [
|
||||
'volume' => $volume,
|
||||
'percent_failed' => $percent_failed,
|
||||
'percent_unknown' => $percent_unknown
|
||||
];
|
||||
|
||||
return $this->_run_query($query, $params);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return table name.
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,115 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of RaspiSMS.
|
||||
*
|
||||
* (c) Pierre-Lin Bonnemaison <plebwebsas@gmail.com>
|
||||
*
|
||||
* This source file is subject to the GPL-3.0 license that is bundled
|
||||
* with this source code in the file LICENSE.
|
||||
*/
|
||||
|
||||
namespace models;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
class SystemVQueue implements Queue
|
||||
{
|
||||
private $id;
|
||||
private $queue;
|
||||
|
||||
/**
|
||||
* A queue using System V message queues to store and exchange messages
|
||||
* routing is based on queue id and message type
|
||||
*
|
||||
* ** Attention : Instead of string, all ids and tags must be numbers, its the system v queues works, no reliable way arround it**
|
||||
* @param int $id : A unique identifier for the queue, *this must be generated with ftok*
|
||||
|
||||
*/
|
||||
public function __construct($id)
|
||||
{
|
||||
$this->id = (int) $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to close the system v queue on destruction
|
||||
*/
|
||||
public function close()
|
||||
{
|
||||
if ($this->queue)
|
||||
{
|
||||
msg_remove_queue($this->queue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to get the message queue and ensure it is open, we should always call it during push/read just to
|
||||
* make sure another process didn't close the queue
|
||||
*/
|
||||
private function get_queue()
|
||||
{
|
||||
$this->queue = msg_get_queue($this->id);
|
||||
|
||||
if (!$this->queue)
|
||||
{
|
||||
throw new \Exception('Impossible to get a System V message queue for id ' . $this->id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a message to the queue
|
||||
*
|
||||
* @param string $message : The message to add to the queue
|
||||
* @param ?string $tag : A tag to associate to the message for routing purposes.
|
||||
* Though this is a string, we MUST pass a valid number, its the way System V queue works
|
||||
*/
|
||||
public function push($message, ?string $tag = '0')
|
||||
{
|
||||
$tag = (int) $tag;
|
||||
|
||||
$this->get_queue();
|
||||
$error_code = null;
|
||||
$success = msg_send($this->queue, $tag, $message, true, false, $error_code);
|
||||
if (!$success)
|
||||
{
|
||||
throw new \Exception('Impossible to send the message on system V queue, error code : ' . $error_code);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the older message in the queue
|
||||
*
|
||||
* @param ?string $tag : A tag to associate to the message for routing purposes
|
||||
* Though this is a string, we MUST pass a valid number, its the way System V queue works
|
||||
*
|
||||
* @return mixed $message : The oldest message or null if no message found, can be anything
|
||||
*/
|
||||
public function read(?string $tag = '0')
|
||||
{
|
||||
$tag = (int) $tag;
|
||||
|
||||
$msgtype = null;
|
||||
$maxsize = 409600;
|
||||
$message = null;
|
||||
|
||||
// Message type is forged from a prefix concat with the phone ID
|
||||
$error_code = null;
|
||||
$this->get_queue();
|
||||
$success = msg_receive($this->queue, $tag, $msgtype, $maxsize, $message, true, MSG_IPC_NOWAIT, $error_code); //MSG_IPC_NOWAIT == dont wait if no message found
|
||||
|
||||
if (!$success && MSG_ENOMSG !== $error_code)
|
||||
{
|
||||
throw new \Exception('Impossible to read messages on system V queue, error code : ' . $error_code);
|
||||
}
|
||||
|
||||
if (!$message)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return $message;
|
||||
}
|
||||
}
|
|
@ -31,6 +31,16 @@ namespace models;
|
|||
return $this->_select_one('user', ['id' => $id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all active users.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_all_active()
|
||||
{
|
||||
return $this->_select('user', ['status' => self::STATUS_ACTIVE]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find user by ids.
|
||||
*
|
||||
|
|
|
@ -19,6 +19,7 @@ namespace models;
|
|||
const TYPE_INBOUND_CALL = 'inbound_call';
|
||||
const TYPE_QUOTA_LEVEL_ALERT = 'quota_level';
|
||||
const TYPE_QUOTA_REACHED = 'quota_reached';
|
||||
const TYPE_PHONE_RELIABILITY = 'phone_reliability';
|
||||
|
||||
/**
|
||||
* Find all webhooks for a user and for a type of webhook.
|
||||
|
|
18
routes.php
18
routes.php
|
@ -11,6 +11,8 @@
|
|||
|
||||
'Dashboard' => [
|
||||
'show' => '/dashboard/',
|
||||
'stats_sended' => '/dashboard/stats/sended.json/',
|
||||
'stats_received' => '/dashboard/stats/received.json/',
|
||||
],
|
||||
|
||||
'Account' => [
|
||||
|
@ -165,6 +167,7 @@
|
|||
'edit' => '/phone/edit/',
|
||||
'update' => '/phone/update/{csrf}/',
|
||||
'update_status' => '/phone/update_status/{csrf}/',
|
||||
'change_status' => '/phone/change_status/{new_status}/{csrf}/',
|
||||
'json_list' => '/phones.json/',
|
||||
],
|
||||
|
||||
|
@ -203,12 +206,24 @@
|
|||
'end_call' => '/callback/end_call/{id_phone}/',
|
||||
],
|
||||
|
||||
'Stat' => [
|
||||
'sms_status' => '/stats/sms-status/',
|
||||
],
|
||||
|
||||
'Api' => [
|
||||
'get_entries' => [
|
||||
'/api/list/{entry_type}/',
|
||||
'/api/list/{entry_type}/{page}/',
|
||||
],
|
||||
'get_usage' => '/api/usage/',
|
||||
'get_sms_status_stats' => '/api/stats/sms-status/',
|
||||
'get_invalid_numbers' => [
|
||||
'/api/invalid_number/',
|
||||
'/api/invalid_number/{page}/',
|
||||
],
|
||||
'get_send_sms' => [
|
||||
'/api/send-sms/',
|
||||
],
|
||||
'post_scheduled' => [
|
||||
'/api/scheduled/',
|
||||
],
|
||||
|
@ -224,6 +239,9 @@
|
|||
'post_update_phone_status' => [
|
||||
'/api/phone/{id}/status/',
|
||||
],
|
||||
'post_change_phone_status' => [
|
||||
'/api/phone/{id}/status/force/',
|
||||
],
|
||||
'delete_phone' => [
|
||||
'/api/phone/{id}/',
|
||||
],
|
||||
|
|
|
@ -121,13 +121,14 @@
|
|||
<div class="panel panel-default dashboard-panel-chart">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title"><i class="fa fa-area-chart fa-fw"></i> SMS envoyés depuis le <?= $stats_start_date_formated; ?> : </h3>
|
||||
<span style="color: #5CB85C;">SMS envoyés (moyenne = <?php echo $avg_sendeds; ?> par jour).</span><br/>
|
||||
<span style="color: #5CB85C;">SMS envoyés (moyenne = <span id="avg_sendeds">0</span> par jour).</span><br/>
|
||||
<?php if ($quota_unused) { ?>
|
||||
<br/>
|
||||
<span style="color: #d9534f">Crédits restants : <?= $quota_unused; ?>.</span>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div id="morris-bar-chart-sended-loader" class="text-center"><div class="loader"></div></div>
|
||||
<div id="morris-bar-chart-sended"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -139,9 +140,10 @@
|
|||
<div class="panel panel-default dashboard-panel-chart">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title"><i class="fa fa-area-chart fa-fw"></i> SMS reçus depuis le <?= $stats_start_date_formated; ?> : </h3>
|
||||
<span style="color: #EDAB4D">SMS reçus (moyenne = <?php echo $avg_receiveds; ?> par jour).</span>
|
||||
<span style="color: #EDAB4D">SMS reçus (moyenne = <span id="avg_receiveds">0</span> par jour).</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div id="morris-bar-chart-received-loader" class="text-center"><div class="loader"></div></div>
|
||||
<div id="morris-bar-chart-received"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -255,18 +257,23 @@
|
|||
|
||||
</div>
|
||||
<script>
|
||||
jQuery(document).ready(function()
|
||||
{
|
||||
async function drawChartSended() {
|
||||
let url = <?= json_encode(\descartes\Router::url('Dashboard', 'stats_sended'))?>;
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
|
||||
document.getElementById('avg_sendeds').textContent = data.avg_sendeds;
|
||||
|
||||
Morris.Bar({
|
||||
element: 'morris-bar-chart-sended',
|
||||
fillOpacity: 0.4,
|
||||
data: <?php echo $data_bar_chart_sended;?>,
|
||||
data: data.data_bar_chart_sended,
|
||||
xkey: 'period',
|
||||
parseTime: false,
|
||||
ykeys: ['sendeds_failed', 'sendeds_unknown', 'sendeds_delivered'],
|
||||
labels: ['SMS échoués', 'SMS inconnus', 'SMS délivrés'],
|
||||
barColors: ['#D9534F', '#337AB7', '#5CB85C'],
|
||||
goals: [<?php echo $avg_sendeds; ?>,],
|
||||
goals: [data.avg_sendeds],
|
||||
goalLineColors: ['#5CB85C'],
|
||||
goalStrokeWidth: 2,
|
||||
pointSize: 4,
|
||||
|
@ -290,22 +297,42 @@
|
|||
}
|
||||
});
|
||||
|
||||
document.getElementById('morris-bar-chart-sended-loader').classList.add('hidden');
|
||||
}
|
||||
|
||||
async function drawChartReceived() {
|
||||
let url = <?= json_encode(\descartes\Router::url('Dashboard', 'stats_received'))?>;
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
console.log(data);
|
||||
|
||||
|
||||
document.getElementById('avg_receiveds').textContent = data.avg_receiveds;
|
||||
Morris.Bar({
|
||||
element: 'morris-bar-chart-received',
|
||||
fillOpacity: 0.4,
|
||||
data: <?php echo $data_bar_chart_received;?>,
|
||||
data: data.data_bar_chart_received,
|
||||
xkey: 'period',
|
||||
parseTime: false,
|
||||
ykeys: ['receiveds'],
|
||||
labels: ['SMS reçus'],
|
||||
barColors: ['#EDAB4D'],
|
||||
goals: [<?php echo $avg_receiveds; ?>],
|
||||
goals: [data.avg_receiveds],
|
||||
goalLineColors: ['#EDAB4D'],
|
||||
goalStrokeWidth: 2,
|
||||
pointSize: 4,
|
||||
hideHover: 'auto',
|
||||
resize: true,
|
||||
});
|
||||
|
||||
document.getElementById('morris-bar-chart-received-loader').classList.add('hidden');
|
||||
}
|
||||
|
||||
|
||||
jQuery(document).ready(function()
|
||||
{
|
||||
drawChartSended();
|
||||
drawChartReceived();
|
||||
});
|
||||
</script>
|
||||
<!-- /#wrapper -->
|
||||
|
|
|
@ -64,7 +64,7 @@ jQuery(document).ready(function ()
|
|||
{
|
||||
jQuery('.datatable').DataTable({
|
||||
"pageLength": 25,
|
||||
"lengthMenu": [[25, 50, 100, 1000, 10000, -1], [25, 50, 100, 1000, 10000, "All"]],
|
||||
"lengthMenu": [[25, 50, 100, 1000], [25, 50, 100, 1000]],
|
||||
"language": {
|
||||
"url": HTTP_PWD + "/assets/js/datatables/french.json",
|
||||
},
|
||||
|
@ -73,7 +73,6 @@ jQuery(document).ready(function ()
|
|||
'targets': 'checkcolumn',
|
||||
'orderable': false,
|
||||
}],
|
||||
|
||||
"ajax": {
|
||||
'url': '<?php echo \descartes\Router::url('Discussion', 'list_json'); ?>',
|
||||
'dataSrc': 'data',
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
Le téléphone <?php $this->s($phone['name']); ?> semble rencontrer un taux de SMS échoués anormalement élevé.
|
||||
|
||||
Période prise en compte : <?php $this->s($period); ?> dernières minutes
|
||||
Total de SMS : <?php $this->s($total); ?>
|
||||
Nombre d'échecs : <?php $this->s($unreliable); ?>
|
||||
Taux d'échecs : <?php $this->s($rate); ?>%
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------------------
|
||||
Pour plus d'informations sur le système RaspiSMS, rendez-vous sur le site https://raspisms.fr
|
|
@ -0,0 +1,10 @@
|
|||
Le téléphone <?php $this->s($phone['name']); ?> semble rencontrer un taux de SMS incconnus anormalement élevé.
|
||||
|
||||
Période prise en compte : <?php $this->s($period); ?> dernières minutes
|
||||
Total de SMS : <?php $this->s($total); ?>
|
||||
Nombre d'inconnus : <?php $this->s($unreliable); ?>
|
||||
Taux d'inconnus : <?php $this->s($rate); ?>%
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------------------
|
||||
Pour plus d'informations sur le système RaspiSMS, rendez-vous sur le site https://raspisms.fr
|
|
@ -42,6 +42,9 @@
|
|||
<script src="<?php echo HTTP_PWD_JS; ?>/datatables/datatables.min.js"></script>
|
||||
<!-- Qrcode lib -->
|
||||
<script src="<?php echo HTTP_PWD_JS; ?>/qrcode.min.js"></script>
|
||||
<!-- Chartjs -->
|
||||
<script src="<?php echo HTTP_PWD_JS; ?>/chart.js"></script>
|
||||
|
||||
|
||||
<!-- Custom JS -->
|
||||
<script src="<?php echo HTTP_PWD_JS; ?>/custom.js"></script>
|
||||
|
|
|
@ -122,6 +122,11 @@
|
|||
</ul>
|
||||
</li>
|
||||
<?php } ?>
|
||||
<?php if (!in_array('stats', json_decode($_SESSION['user']['settings']['hide_menus'], true) ?? [])) { ?>
|
||||
<li <?php echo $page == 'stats' ? 'class="active"' : ''; ?>>
|
||||
<a href="<?php echo \descartes\Router::url('Stat', 'sms_status'); ?>"><i class="fa fa-fw fa-area-chart"></i> Statistiques</a>
|
||||
</li>
|
||||
<?php } ?>
|
||||
<?php if (!in_array('settings', json_decode($_SESSION['user']['settings']['hide_menus'], true) ?? [])) { ?>
|
||||
<li <?php echo $page == 'settings' ? 'class="active"' : ''; ?>>
|
||||
<a href="<?php echo \descartes\Router::url('Setting', 'show'); ?>"><i class="fa fa-fw fa-cogs"></i> Réglages</a>
|
||||
|
|
|
@ -62,9 +62,11 @@
|
|||
</div>
|
||||
<div class="text-right col-xs-6 no-padding">
|
||||
<strong>Action pour la séléction :</strong>
|
||||
<button class="btn btn-default" type="submit" formaction="<?php echo \descartes\Router::url('Phone', 'update_status', ['csrf' => $_SESSION['csrf']]); ?>"><span class="fa fa-refresh"></span> Rafraichir le status</button>
|
||||
<button class="btn btn-default" type="submit" formaction="<?php echo \descartes\Router::url('Phone', 'edit'); ?>"><span class="fa fa-edit"></span> Modifier</button>
|
||||
<button class="btn btn-default btn-confirm" type="submit" formaction="<?php echo \descartes\Router::url('Phone', 'delete', ['csrf' => $_SESSION['csrf']]); ?>"><span class="fa fa-trash-o"></span> Supprimer</button>
|
||||
<button class="btn btn-default mb-2" type="submit" formaction="<?php echo \descartes\Router::url('Phone', 'update_status', ['csrf' => $_SESSION['csrf']]); ?>"><span class="fa fa-refresh"></span> Rafraichir le status</button>
|
||||
<button class="btn btn-default mb-2" type="submit" formaction="<?php echo \descartes\Router::url('Phone', 'change_status', ['csrf' => $_SESSION['csrf'], 'new_status' => 'available']); ?>"><span class="fa fa-toggle-on"></span> Activer</button>
|
||||
<button class="btn btn-default mb-2" type="submit" formaction="<?php echo \descartes\Router::url('Phone', 'change_status', ['csrf' => $_SESSION['csrf'], 'new_status' => 'disabled']); ?>"><span class="fa fa-toggle-off"></span> Désactiver</button>
|
||||
<button class="btn btn-default mb-2" type="submit" formaction="<?php echo \descartes\Router::url('Phone', 'edit'); ?>"><span class="fa fa-edit"></span> Modifier</button>
|
||||
<button class="btn btn-default btn-confirm mb-2" type="submit" formaction="<?php echo \descartes\Router::url('Phone', 'delete', ['csrf' => $_SESSION['csrf']]); ?>"><span class="fa fa-trash-o"></span> Supprimer</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -105,6 +107,10 @@ jQuery(document).ready(function ()
|
|||
html += ' - <span class="text-success">Disponible</span>'
|
||||
break;
|
||||
|
||||
case 'disabled':
|
||||
html += ' - <span class="text-warning">Désactivé</span>'
|
||||
break;
|
||||
|
||||
case 'unavailable':
|
||||
html += ' - <span class="text-danger">Indisponible</span>'
|
||||
break;
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
<div class="col-lg-12">
|
||||
<h1 class="page-header">
|
||||
Dashboard <small>SMS envoyés</small>
|
||||
<a class="btn btn-warning float-right" id="btn-invalid-numbers" href="#"><span class="fa fa-eraser"></span> Télécharger les numéros invalides</a>
|
||||
</h1>
|
||||
<ol class="breadcrumb">
|
||||
<li>
|
||||
|
@ -65,9 +66,122 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" tabindex="-1" id="invalid-numbers-modal">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<form id="invalid-numbers-form" action="<?php $this->s(\descartes\Router::url('Api', 'get_invalid_numbers')); ?>" method="GET">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title">Télécharger les numéros invalides</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="help">Vous pouvez téléchager une liste de destinataires qui affichent un taux d'erreur anormal selon les critères de votre choix (liste limitée à 25 000 numéros).</p>
|
||||
<div class="form-group">
|
||||
<label>Volume minimum de SMS envoyés au numéros</label>
|
||||
<div class="form-group input-group">
|
||||
<span class="input-group-addon"><span class="fa fa-arrow-circle-up"></span></span>
|
||||
<input name="volume" class="form-control" type="number" min="1" step="1" placeholder="" autofocus required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Pourcentage d'échecs minimum</label>
|
||||
<div class="form-group input-group">
|
||||
<span class="input-group-addon"><span class="fa fa-percent"></span></span>
|
||||
<input name="percent_failed" class="form-control" type="number" min="0" step="1" placeholder="" autofocus required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Pourcentage d'inconnus minimum</label>
|
||||
<div class="form-group input-group">
|
||||
<span class="input-group-addon"><span class="fa fa-percent"></span></span>
|
||||
<input name="percent_unknown" class="form-control" type="number" min="0" step="1" placeholder="" autofocus required>
|
||||
</div>
|
||||
</div>
|
||||
<div id="invalid-numbers-loader" class="text-center hidden"><div class="loader"></div></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a type="button" class="btn btn-danger" data-dismiss="modal">Annuler</a>
|
||||
<input type="submit" class="btn btn-success" value="Valider" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
jQuery(document).ready(function ()
|
||||
{
|
||||
jQuery('body').on('click', '#btn-invalid-numbers', function ()
|
||||
{
|
||||
jQuery('#invalid-numbers-modal').modal({'keyboard': true});
|
||||
});
|
||||
|
||||
jQuery('body').on('submit', '#invalid-numbers-form', function (e)
|
||||
{
|
||||
e.preventDefault();
|
||||
|
||||
jQuery('#invalid-numbers-loader').removeClass('hidden');
|
||||
|
||||
const form = this;
|
||||
const formData = jQuery(form).serialize();
|
||||
|
||||
let invalidNumbers = []; // Array to store cumulative results
|
||||
|
||||
// Function to fetch data and handle pagination
|
||||
const fetchData = (url, limit = -1, params = null) => {
|
||||
if (params) {
|
||||
url += '?' + params;
|
||||
}
|
||||
|
||||
fetch(url)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(jsonResponse => {
|
||||
invalidNumbers = invalidNumbers.concat(jsonResponse.response);
|
||||
|
||||
// Check if there is a "next" URL to fetch more data
|
||||
if (jsonResponse.next && limit != 0) {
|
||||
fetchData(jsonResponse.next, limit - 1); // Recursive call for next page
|
||||
} else {
|
||||
exportToCSV(invalidNumbers);
|
||||
jQuery('#invalid-numbers-loader').addClass('hidden');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('There was a problem with the fetch operation:', error);
|
||||
});
|
||||
};
|
||||
|
||||
// Function to export data to CSV
|
||||
const exportToCSV = (results) => {
|
||||
// Define the CSV headers
|
||||
let csvContent = "Destination,Total SMS Sent,Failed Percentage,Unknown Percentage\n";
|
||||
|
||||
// Append each row of data to the CSV content
|
||||
results.forEach(item => {
|
||||
csvContent += `${item.destination},${item.total_sms_sent},${item.failed_percentage},${item.unknown_percentage}\n`;
|
||||
});
|
||||
|
||||
// Create a downloadable link for the CSV file
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const downloadLink = document.createElement('a');
|
||||
downloadLink.href = url;
|
||||
downloadLink.download = 'invalid_numbers.csv';
|
||||
|
||||
// Trigger download
|
||||
document.body.appendChild(downloadLink);
|
||||
downloadLink.click();
|
||||
document.body.removeChild(downloadLink); // Clean up
|
||||
};
|
||||
|
||||
// Initial call to fetch data
|
||||
fetchData(form.action, 1000, formData);
|
||||
});
|
||||
|
||||
jQuery('.datatable').DataTable({
|
||||
"pageLength": 25,
|
||||
"lengthMenu": [[25, 50, 100, 1000, 10000, Math.pow(10, 10)], [25, 50, 100, 1000, 10000, "All"]],
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,217 @@
|
|||
<?php
|
||||
//Template dashboard
|
||||
|
||||
$this->render('incs/head')
|
||||
?>
|
||||
<div id="wrapper">
|
||||
<?php
|
||||
$this->render('incs/nav', ['page' => 'stats'])
|
||||
?>
|
||||
<div id="page-wrapper">
|
||||
<div class="container-fluid">
|
||||
<!-- Page Heading -->
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<h1 class="page-header">
|
||||
Statistiques <small>Statistiques avancées</small>
|
||||
</h1>
|
||||
<ol class="breadcrumb">
|
||||
<li class="active">
|
||||
<i class="fa fa-dashboard"></i> Statistiques avancées
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.row -->
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<div class="panel panel-default dashboard-panel-chart">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title"><i class="fa fa-area-chart fa-fw"></i> Status des SMS envoyés par téléphone : </h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<form id="sms-status-form" class="form-inline text-right mb-3" action="" method="POST">
|
||||
<div class="form-group">
|
||||
<label for="id_phone">Téléphone : </label>
|
||||
<div class="form-group">
|
||||
<select id="id_phone" name="id_phone" class="form-control">
|
||||
<option value="">Tous les téléphones</option>
|
||||
<?php foreach ($phones as $phone) { ?>
|
||||
<option value="<?php $this->s($phone['id']); ?>"><?php $this->s($phone['name']); ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group ml-4">
|
||||
<label for="start">Période : </label>
|
||||
<input id="start" name="start" class="form-control form-date auto-width" type="date" value="<?php $this->s($seven_days_ago->format('Y-m-d')) ?>">
|
||||
- <input id="end" name="end" class="form-control form-date auto-width" type="date" value="<?php $this->s($now->format('Y-m-d')) ?>">
|
||||
</div>
|
||||
|
||||
<input type="submit" class="btn btn-success ml-4" value="Valider" />
|
||||
</form>
|
||||
<canvas id="bar-chart-sms-status"></canvas>
|
||||
<div id="bar-chart-sms-status-loader" class="text-center mb-5"><div class="loader"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- /.row -->
|
||||
</div>
|
||||
<!-- /.container-fluid -->
|
||||
|
||||
</div>
|
||||
<!-- /#page-wrapper -->
|
||||
|
||||
</div>
|
||||
<script>
|
||||
smsStatusChart = null;
|
||||
const phones = {};
|
||||
for (const phone of <?= json_encode($phones); ?>) {
|
||||
phones[phone.id] = phone;
|
||||
};
|
||||
|
||||
|
||||
async function drawChart(e = null) {
|
||||
const startDate = new Date(document.getElementById('start').value);
|
||||
const formatedStartDate = startDate.toISOString().split('T')[0]
|
||||
const endDate = new Date(document.getElementById('end').value);
|
||||
const formatedEndDate = endDate.toISOString().split('T')[0]
|
||||
const id_phone = document.getElementById('id_phone').value;
|
||||
|
||||
let url = <?= json_encode(\descartes\Router::url('Api', 'get_sms_status_stats'))?>;
|
||||
url += `?start=${formatedStartDate}&end=${formatedEndDate}`;
|
||||
url += id_phone ? `&id_phone=${id_phone}` : '';
|
||||
const response = await fetch(url);
|
||||
const data = (await response.json()).response;
|
||||
|
||||
// Get all dates to avoid holes in data
|
||||
const dates = [];
|
||||
let currentDate = new Date(startDate);
|
||||
while (currentDate <= endDate) {
|
||||
const formated_date = (new Date(currentDate)).toISOString().split('T')[0]
|
||||
dates.push(formated_date);
|
||||
currentDate.setDate(currentDate.getDate() + 1);
|
||||
}
|
||||
const empty_dataset = Array(dates.length + 1).fill(0)
|
||||
|
||||
const colors = {'failed': '#d9534f', 'unknown': '#337ab7', 'delivered': '#5cb85c'};
|
||||
|
||||
|
||||
|
||||
let datasets = {};
|
||||
for (const entry of data) {
|
||||
if (!datasets[entry.id_phone]) {
|
||||
datasets[entry.id_phone] = {
|
||||
'failed': {
|
||||
'data': [...empty_dataset],
|
||||
'label': `Phone ${phones[entry.id_phone]['name']} - Failed`,
|
||||
'backgroundColor': colors['failed'],
|
||||
'stack': entry.id_phone,
|
||||
},
|
||||
'unknown': {
|
||||
'data': [...empty_dataset],
|
||||
'label': `Phone ${phones[entry.id_phone]['name']} - Unknown`,
|
||||
'backgroundColor': colors['unknown'],
|
||||
'stack': entry.id_phone,
|
||||
},
|
||||
'delivered': {
|
||||
'data': [...empty_dataset],
|
||||
'label': `Phone ${phones[entry.id_phone]['name']} - Delivered`,
|
||||
'backgroundColor': colors['delivered'],
|
||||
'stack': entry.id_phone,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const date_index = dates.indexOf(entry.at_ymd);
|
||||
|
||||
// This should never happen, but better be sure
|
||||
if (date_index == -1) {
|
||||
throw Error('Data for a date not in dates array');
|
||||
}
|
||||
|
||||
datasets[entry.id_phone][entry.status]['data'][date_index] = entry.nb;
|
||||
}
|
||||
// Pass all from dict to array
|
||||
const formated_datasets = [];
|
||||
for (const key in datasets) {
|
||||
formated_datasets.push(datasets[key]['failed']);
|
||||
formated_datasets.push(datasets[key]['unknown']);
|
||||
formated_datasets.push(datasets[key]['delivered']);
|
||||
}
|
||||
|
||||
// Custom plugin to display "Pas de données sur cette période"
|
||||
const noDataPlugin = {
|
||||
id: 'noDataPlugin',
|
||||
afterDraw: (chart) => {
|
||||
const datasets = chart.data.datasets;
|
||||
const hasData = datasets.some(dataset => dataset.data.some(value => value !== null && value !== undefined && value !== 0));
|
||||
|
||||
if (!hasData) {
|
||||
const ctx = chart.ctx;
|
||||
const { width, height } = chart;
|
||||
ctx.save();
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.font = '3em Helvetica';
|
||||
ctx.fillText('Pas de données sur cette période', width / 2, height / 2);
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Create the chart
|
||||
const ctx = document.getElementById('bar-chart-sms-status');
|
||||
const config = {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: dates,
|
||||
datasets: formated_datasets,
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
scales: {
|
||||
x: {
|
||||
stacked: true,
|
||||
},
|
||||
y: {
|
||||
stacked: true,
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [noDataPlugin],
|
||||
};
|
||||
|
||||
document.getElementById('bar-chart-sms-status-loader').classList.add('hidden');
|
||||
|
||||
// On first run create chart, after update
|
||||
if (!smsStatusChart) {
|
||||
smsStatusChart = new Chart(ctx, config);
|
||||
} else {
|
||||
for (const key in config) {
|
||||
smsStatusChart[key] = config[key];
|
||||
}
|
||||
smsStatusChart.update();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
jQuery(document).ready(function()
|
||||
{
|
||||
drawChart();
|
||||
});
|
||||
|
||||
jQuery('#sms-status-form').on('submit', (e) => {
|
||||
e.preventDefault();
|
||||
drawChart();
|
||||
return false;
|
||||
});
|
||||
</script>
|
||||
<!-- /#wrapper -->
|
||||
<?php
|
||||
$this->render('incs/footer');
|
|
@ -49,6 +49,7 @@
|
|||
<option value="send_sms" <?= ($_SESSION['previous_http_post']['type'] ?? '') == 'send_sms' ? 'selected' : '' ?>>Envoi d'un SMS</option>
|
||||
<option value="send_sms_status_change" <?= ($_SESSION['previous_http_post']['type'] ?? '') == 'send_sms_status_change' ? 'selected' : '' ?>>Mise à jour du statut d'un SMS envoyé</option>
|
||||
<option value="inbound_call" <?= ($_SESSION['previous_http_post']['type'] ?? '') == 'inbound_call' ? 'selected' : '' ?>>Réception d'un appel téléphonique</option>
|
||||
<option value="phone_reliability" <?= ($_SESSION['previous_http_post']['type'] ?? '') == 'phone_reliability' ? 'selected' : '' ?>>Détection d'un problème de fiabilité sur un téléphone</option>
|
||||
</select>
|
||||
</div>
|
||||
<a class="btn btn-danger" href="<?php echo \descartes\Router::url('Webhook', 'list'); ?>">Annuler</a>
|
||||
|
|
|
@ -52,6 +52,7 @@
|
|||
<option <?php echo $webhook['type'] == 'send_sms' ? 'selected="selected"' : '' ?> value="send_sms">Envoi d'un SMS</option>
|
||||
<option <?php echo $webhook['type'] == 'send_sms_status_change' ? 'selected="selected"' : '' ?> value="send_sms_status_change">Mise à jour du statut d'un SMS envoyé</option>
|
||||
<option <?php echo $webhook['type'] == 'inbound_call' ? 'selected="selected"' : '' ?> value="inbound_call">Réception d'un appel téléphonique</option>
|
||||
<option <?php echo $webhook['type'] == 'phone_reliability' ? 'selected="selected"' : '' ?> value="phone_reliability">Détection d'un problème de fiabilité sur un téléphone</option>
|
||||
</select>
|
||||
</div>
|
||||
<hr/>
|
||||
|
|
|
@ -96,6 +96,8 @@ jQuery(document).ready(function ()
|
|||
return 'Réception de SMS';
|
||||
case 'inbound_call':
|
||||
return 'Réception d\'un appel téléphonique';
|
||||
case 'phone_reliability':
|
||||
return 'Détection d\'un problème de fiabilité sur un téléphone';
|
||||
default:
|
||||
return 'Inconnu';
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue