First step of quota and using daemon

This commit is contained in:
osaajani 2021-06-08 02:00:48 +02:00
parent 3d19c4decb
commit 120f56fad7
12 changed files with 552 additions and 0 deletions

Binary file not shown.

View File

@ -55,6 +55,21 @@ namespace controllers\internals;
return $this->get_model()->insert($event);
}
/**
* Gets events for a type, since a date and eventually until a date (both included)
*
* @param int $id_user : User id
* @param string $type : Event type we want
* @param \DateTime $since : Date to get events since
* @param ?\DateTime $until (optional) : Date until wich we want events, if not specified no limit
*
* @return array
*/
public get_events_by_type_and_date_for_user (int $id_user, string $type, \DateTime $since, ?\DateTime $until = null)
{
$this->get_model->get_events_by_type_and_date_for_user ($id_user, $type, $since, $until);
}
/**
* Get the model for the Controller.

View File

@ -0,0 +1,180 @@
<?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;
class Quota extends StandardController
{
protected $model;
/**
* Create a new quota.
*
* @param int $id_user : User id
* @param int $credit : Credit for this quota
* @param bool $report_unused : Should unused credits be re-credited
* @param bool $report_unused_additional : Should unused additional credits be re-credited
* @param \DateTime $start_date : Starting date for the quota
* @param ?\DateTime $expiration_date (optional) : Ending date for the quota
* @param bool $auto_renew (optional) : Should the quota be automatically renewed after expiration_date
* @param ?\DateInterval $renew_interval (optional) : Period to use for setting expiration_date on renewal
* @param int $additional (optional) : Additionals credits
*
* @return mixed bool|int : False if cannot create smsstop, id of the new smsstop else
*/
public function create(int $id_user, int $credit, bool $report_unused, bool $report_unused_additional, \DateTime $start_date, ?\DateTime $expiration_date = null, bool $auto_renew= false, ?\DateInterval $renew_interval = null, int $additional = 0)
{
$quota = [
'id_user' => $id_user,
'credit' => $credit,
'report_unused' => $report_unused,
'report_unused_additional' => $report_unused_additional,
'start_date' => $start_date,
'expiration_date' => $expiration_date,
'auto_renew' => $auto_renew,
'renew_interval' => $renew_interval,
'additional' => $additional,
];
return $this->get_model()->insert($quota);
}
/**
* Update a quota.
*
*
* @param int $id_user : User id
* @param int $id_quota : Id of the quota to update
* @param int $credit : Credit for this quota
* @param bool $report_unused : Should unused credits be re-credited
* @param bool $report_unused_additional : Should unused additional credits be re-credited
* @param \DateTime $start_date : Starting date for the quota
* @param ?\DateTime $expiration_date (optional) : Ending date for the quota
* @param bool $auto_renew (optional) : Should the quota be automatically renewed after expiration_date
* @param ?\DateInterval $renew_interval (optional) : Period to use for setting expiration_date on renewal
* @param int $additional (optional) : Additionals credits
* @param int $consumed (optional) : Number of consumed credits
*
* @return mixed bool|int : False if cannot create smsstop, id of the new smsstop else
*/
public function update_for_user(int $id_user, int $id_quota, int $credit, bool $report_unused, bool $report_unused_additional, \DateTime $start_date, ?\DateTime $expiration_date = null, bool $auto_renew= false, ?\DateInterval $renew_interval = null, int $additional = 0, int $consumed = 0)
{
$quota = [
'id_user' => $id_user,
'id_quota' => $id_quota,
'credit' => $credit,
'report_unused' => $report_unused,
'report_unused_additional' => $report_unused_additional,
'start_date' => $start_date,
'expiration_date' => $expiration_date,
'auto_renew' => $auto_renew,
'renew_interval' => $renew_interval,
'additional' => $additional,
'consumed' => $consumed,
];
return $this->get_model()->insert($quota);
}
/**
* Check if we have enough credit
* @param int $id_user : User id
* @param int $needed : Number of credits we need
* @return bool : true if we have enough credit, false else
*/
public function has_enough_credit(int $id_user, int $needed)
{
$remaining_credit = $this->get_model()->get_remaining_credit($id_user, new \DateTime());
return $remaining_credit >= $needed;
}
/**
* Consume some credit
* @param int $id_user : User id
* @param int $quantity : Number of credits to consume
* @return bool : True on success, false else
*/
public function consume_credit (int $id_user, int $quantity)
{
$result = $this->get_model()->consume_credit($id_user, $quantity);
//Enqueue verifications for quotas alerting
$queue = msg_get_queue(QUEUE_ID_QUOTA);
$message = ['id_user' => $id_user];
msg_send($queue, QUEUE_TYPE_QUOTA, $message, true, true);
return $result;
}
/**
* Get quota usage percentage
* @param int $id_user : User id
* @return float : percentage of quota used
*/
public function get_usage_percentage (int $id_user)
{
return $this->get_model()->get_usage_percentage($id_user, new \DateTime());
}
/**
* Compute how many credit a message represent
* this function count 160 chars per SMS if it can be send as GSM 03.38 encoding and 70 chars per SMS if it can only be send as UTF8
* @param string $text : Message to send
* @return int : Number of credit to send this message
*/
public static function compute_credits_for_message ($text)
{
//Gsm 03.38 charset to detect if message is compatible or must use utf8
$gsm0338 = array(
'@','Δ',' ','0','¡','P','¿','p',
'£','_','!','1','A','Q','a','q',
'$','Φ','"','2','B','R','b','r',
'¥','Γ','#','3','C','S','c','s',
'è','Λ','¤','4','D','T','d','t',
'é','Ω','%','5','E','U','e','u',
'ù','Π','&','6','F','V','f','v',
'ì','Ψ','\'','7','G','W','g','w',
'ò','Σ','(','8','H','X','h','x',
'Ç','Θ',')','9','I','Y','i','y',
"\n",'Ξ','*',':','J','Z','j','z',
'Ø',"\x1B",'+',';','K','Ä','k','ä',
'ø','Æ',',','<','L','Ö','l','ö',
"\r",'æ','-','=','M','Ñ','m','ñ',
'Å','ß','.','>','N','Ü','n','ü',
'å','É','/','?','O','§','o','à'
);
$is_gsm0338 = true;
$len = mb_strlen($text);
for ($i = 0; $i < $len; $i++)
{
if (!in_array(mb_substr($utf8_string, $i, 1), $gsm0338))
{
$is_gsm0338 = false;
break;
}
}
return ($is_gsm0338 ? ceil($len / 160) : ceil($len / 70));
}
/**
* Get the model for the Controller.
*/
protected function get_model(): \descartes\Model
{
$this->model = $this->model ?? new \models\Quota($this->bdd);
return $this->model;
}
}

View File

@ -219,6 +219,18 @@ namespace controllers\internals;
'error_message' => null,
];
//If we reached our max quota, do not send the message
$internal_quota = new Quota($this->bdd);
$nb_credits = $internal_quota::compute_credits_for_message($text); //Calculate how much credit the message require
if ($internal_quota->has_enough_credit($id_user, $nb_credits))
{
$return['error'] = false;
$return['error_message'] = 'Not enough credit to send message.';
return $return;
}
$at = (new \DateTime())->format('Y-m-d H:i:s');
$media_uris = [];
foreach ($medias as $media)
@ -253,6 +265,8 @@ namespace controllers\internals;
return $return;
}
$internal_quota->consume_credit($id_user, $nb_credits);
$sended_id = $this->create($id_user, $id_phone, $at, $text, $destination, $response['uid'] ?? uniqid(), $adapter->meta_classname(), $flash, $mms, $medias, $status);
$sended = [

BIN
daemons/.Quota.php.swp Normal file

Binary file not shown.

159
daemons/Quota.php Normal file
View File

@ -0,0 +1,159 @@
<?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 daemons;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
/**
* Quota daemon class.
*/
class Quota extends AbstractDaemon
{
private $quota_queue;
private $last_message_at;
private $bdd;
/**
* Constructor.
*
* @param array $phone : A phone table entry
*/
public function __construct()
{
$name = 'RaspiSMS Daemon Quota';
$logger = new Logger($name);
$logger->pushHandler(new StreamHandler(PWD_LOGS . '/daemons.log', Logger::DEBUG));
$pid_dir = PWD_PID;
$no_parent = false; //Rattach to parent so parent can stop it
$additional_signals = [];
$uniq = true; //Quota should be uniq
//Construct the daemon
parent::__construct($name, $logger, $pid_dir, $no_parent, $additional_signals, $uniq);
parent::start();
}
public function run()
{
$this->bdd = \descartes\Model::_connect(DATABASE_HOST, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD, 'UTF8');
$find_message = true;
while ($find_message)
{
//Call message
$maxsize = 409600;
$message = null;
$error_code = null;
$success = msg_receive($this->quota_queue, QUEUE_TYPE_QUOTA, $msgtype, $maxsize, $message, true, MSG_IPC_NOWAIT, $error_code); //MSG_IPC_NOWAIT == dont wait if no message found
if (!$success && MSG_ENOMSG !== $error_code)
{
$this->logger->critical('Error for quota queue reading, error code : ' . $error_code);
$find_message = false;
continue;
}
if (!$message)
{
$find_message = false;
continue;
}
$this->logger->info('Check alert level for quota : ' . json_encode($message['id']));
$internal_settings = new \controllers\internals\Setting($this->bdd);
$settings = $internal_user->gets_for_user($message['id_user']);
$quota_alert_level = false;
foreach ($settings as $name => $value)
{
if ('quota_alert_level', $name)
{
$quota_alert_level = (float) $value;
break;
}
}
if (!$quota_alert_level)
{
$this->logger->info('Alert is disabled for quota : ' . json_encode($message['id']));
continue;
}
$internal_quota = new \controllers\internals\Quota($this->bdd);
$usage_percentage = $internal_quota->get_usage_percentage($message['id_user']);
if ($usage_percentage < $quota_alert_level)
{
continue;
}
//If already an alert event since quota start_date, then ignore alert
$internal_event = new \controllers\internals\Event($this->bdd);
$alert_events = $internal_event->get_events_by_type_and_date_for_user($message['id_user'], 'QUOTA_USAGE_CLOSE', new \DateTime($message['start_date']));
if (count($alert_events))
{
continue;
}
//Alert level reached and no previous alert, we create a new alert
$this->logger->info('Trigger alert for quota : ' . json_encode($message['id']));
$internal_event->create($message['id_user'], 'QUOTA_USAGE_CLOSE', 'Reached ' . ($usage_percentage * 100) . '% of SMS quota.');
$user = $internal_user->get($message['id_user']);
if (!$user)
{
$this->logger->info('Cannot find user with id : ' . json_encode($message['id_user']));
continue;
}
$mailer = new \controllers\internals\Mailer();
$success = $mailer->enqueue($user['email'], EMAIL_QUOTA_USAGE_CLOSE, ['percent' => $usage_percentage]);
if (!$success)
{
$this->logger->error('Cannot enqueue alerting email for quota usage.');
continue;
}
$this->logger->info('Success sending email');
}
//Check quotas every 60 seconds
usleep(60 * 1000000);
}
public function on_start()
{
//Set last message at to construct time
$this->quota_queue = msg_get_queue(QUEUE_ID_QUOTA);
$this->logger->info('Starting Quota daemon with pid ' . getmypid());
}
public function on_stop()
{
//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());
}
public function handle_other_signals($signal)
{
$this->logger->info('Signal not handled by ' . $this->name . ' Daemon : ' . $signal);
}
}

View File

@ -0,0 +1,51 @@
<?php
use Phinx\Migration\AbstractMigration;
class AddQuotas 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('quota');
$table->addColumn('id_user', 'integer', ['null' => false])
->addColumn('consumed', 'integer', ['null' => false, 'default' => 0])
->addColumn('credit', 'integer', ['null' => false])
->addColumn('additional', 'integer', ['null' => false, 'default' => 0])
->addColumn('report_unused', 'boolean', ['null' => false])
->addColumn('report_unused_additional', 'boolean', ['null' => false])
->addColumn('auto_renew', 'boolean', ['null' => false, 'default' => false])
->addColumn('renew_interval', 'string', ['null' => true, 'default' => NULL])
->addColumn('start_date', 'datetime', ['null' => false])
->addColumn('expiration_date', 'datetime', ['null' => true])
->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'])
->create();
}
}

View File

@ -26,6 +26,32 @@ namespace models;
return $this->_select('event', ['id_user' => $id_user], 'at', true, $nb_entry);
}
/**
* Gets events for a type, since a date and eventually until a date (both included)
*
* @param int $id_user : User id
* @param string $type : Event type we want
* @param \DateTime $since : Date to get events since
* @param ?\DateTime $until (optional) : Date until wich we want events, if not specified no limit
*
* @return array
*/
public get_events_by_type_and_date_for_user (int $id_user, string $type, \DateTime $since, ?\DateTime $until = null)
{
$where = [
'id_user' => $id_user,
'type' => $type,
'>=at' => $since->format('Y-m-d H:i:s'),
];
if ($until !== null)
{
$where['<=at' => $until->format('Y-m-d H:i:s')];
}
return $this->_select('event', $where, 'at');
}
/**
* Return table name.
*/

96
models/Quota.php Normal file
View File

@ -0,0 +1,96 @@
<?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 Quota extends StandardModel
{
/**
* Get remaining credit for a date
* if no quota for this user return max int
* @param int $id_user : User id
* @param \DateTime $at : date to get credit at
* @return int : number of remaining credits
*/
public function get_remaining_credit (int $id_user, \DateTime $at): int
{
$query = '
SELECT (credit + additional - consumed) AS remaining_credit
FROM quota
WHERE id_user = :id_user
AND start_date <= :at
AND end_date > :at';
$params = [
'id_user' => $id_user,
'at' => $at->format('Y-m-d H:i:s'),
];
$result = $this->_run_query($query, $params);
return ($result[0]['remaining_credit'] ?? PHP_INT_MAX);
}
/**
* Get credit usage percent for a date
* if no quota for this user return 0
* @param int $id_user : User id
* @param \DateTime $at : date to get usage percent at
* @return float : percent of used credits
*/
public function get_usage_percentage (int $id_user, \DateTime $at): int
{
$query = '
SELECT (consumed / (credit + additional)) AS usage_percentage
FROM quota
WHERE id_user = :id_user
AND start_date <= :at
AND end_date > :at';
$params = [
'id_user' => $id_user,
'at' => $at->format('Y-m-d H:i:s'),
];
$result = $this->_run_query($query, $params);
return ($result[0]['usage_percentage'] ?? 0);
}
/**
* Consume some credit for a user
* @param int $id_user : User id
* @param int $quantity : Number of credits to consume
* @return bool
*/
public function consume_credit (int $id_user, int $quantity): int
{
$query = '
UPDATE quota
SET consumed = consumed + :quantity
WHERE id_user = :id_user';
$params = [
'id_user' => $id_user,
'quantity' => $quantity,
];
return (bool) $this->_run_query($query, $params, \descartes\Model::ROWCOUNT);
}
/**
* Return table name.
*/
protected function get_table_name(): string
{
return 'quota';
}
}

View File

@ -16,6 +16,9 @@ namespace models;
const TYPE_SEND_SMS = 'send_sms';
const TYPE_RECEIVE_SMS = 'receive_sms';
const TYPE_INBOUND_CALL = 'inbound_call';
const TYPE_QUOTA_LEVEL_ALERT = 'quota_level';
const TYPE_QUOTA_REACHED = 'quota_reached';
/**
* Find all webhooks for a user and for a type of webhook.

View File

@ -0,0 +1,4 @@
Vous avez atteint <?php echo $percent * 100; ?>% de votre quota de SMS.
--------------------------------------------------------------------------------------------
Pour plus d'informations sur le système RaspiSMS, rendez-vous sur le site https://raspisms.fr

View File

@ -0,0 +1,4 @@
Vous avez épuisé votre quota de SMS, vous ne pourrez plus envoyer de SMS tant que votre quota de SMS n'aura pas été augmenté ou remis à zéro.
--------------------------------------------------------------------------------------------
Pour plus d'informations sur le système RaspiSMS, rendez-vous sur le site https://raspisms.fr