diff --git a/adapters/AdapterInterface.php b/adapters/AdapterInterface.php index 3a0dc85..c967bb2 100644 --- a/adapters/AdapterInterface.php +++ b/adapters/AdapterInterface.php @@ -119,9 +119,9 @@ interface AdapterInterface * @param array $medias : Array of medias to link to the MMS, [['http_url' => HTTP public url of the media et 'local_uri' => local uri to media file]] * * @return array : [ - * bool 'error' => false if no error, true else - * ?string 'error_message' => null if no error, else error message - * array 'uid' => Uid of the sms created on success + * bool 'error' => false if no error, true else, + * ?string 'error_message' => null if no error, else error message, + * array 'uid' => Uid of the sms created on success, * ] */ public function send(string $destination, string $text, bool $flash = false, bool $mms = false, array $medias = []): array; diff --git a/assets/css/style.css b/assets/css/style.css index ea2f87d..204950f 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -311,7 +311,8 @@ footer img } /* SCHEDULEDS */ -.add-number-button +.add-number-button, +.add-phone-limit-button { display: inline-block; color: #DADFE1; @@ -319,7 +320,8 @@ footer img vertical-align: top; } -.add-number-button:hover +.add-number-button:hover, +.add-phone-limit-button:hover { color: #3498DB; cursor: pointer; @@ -373,7 +375,8 @@ footer img text-align: right; } -.scheduleds-number-groupe +.scheduleds-number-groupe, +.phone-limits-group { padding-top: 15px; padding-bottom: 15px; @@ -383,7 +386,8 @@ footer img position: relative; } -.scheduleds-number-groupe-remove +.scheduleds-number-groupe-remove, +.phone-limits-group-remove { position: absolute; top: 15px; @@ -391,7 +395,8 @@ footer img color: #888; } -.scheduleds-number-groupe-remove:hover +.scheduleds-number-groupe-remove:hover, +.phone-limits-group-remove:hover { color: #555; } @@ -466,6 +471,16 @@ footer img color: #9b2420; } +/* PREVIEW CONTACT */ +.preview-contact-name +{ + font-weight: bold; +} + +.preview-contact-number +{ + font-style: italic; +} /* PHONE */ #adapter-data-container diff --git a/composer.json b/composer.json index 935f689..403cedf 100644 --- a/composer.json +++ b/composer.json @@ -14,8 +14,7 @@ "symfony/yaml": "^5.0", "phpmailer/phpmailer": "^6.1", "xantios/mimey": ">=2.1", - "kreait/firebase-php": "^5.14" - }, - "require-dev": { + "kreait/firebase-php": "^5.14", + "benmorel/gsm-charset-converter": "^0.3.0" } } diff --git a/controllers/internals/Event.php b/controllers/internals/Event.php index 29f76cb..003fc8d 100644 --- a/controllers/internals/Event.php +++ b/controllers/internals/Event.php @@ -73,21 +73,6 @@ 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 function 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. */ diff --git a/controllers/internals/ExpressionProvider.php b/controllers/internals/ExpressionProvider.php index 9d056ab..b26ff41 100644 --- a/controllers/internals/ExpressionProvider.php +++ b/controllers/internals/ExpressionProvider.php @@ -44,11 +44,6 @@ class ExpressionProvider implements ExpressionFunctionProviderInterface return sprintf('isset(%1$s) && is_a(%1$s, \'DateTime\') && %1$s->format(\'m-d\') == (new \\DateTime())->format(\'m-d\')', $birthdate); }, function ($arguments, DateTime $birthdate) { - if (!($birthdate ?? false)) - { - return false; - } - return $birthdate->format('m-d') == (new DateTime())->format('m-d'); }); diff --git a/controllers/internals/Phone.php b/controllers/internals/Phone.php index 0cf8354..9bdeac7 100644 --- a/controllers/internals/Phone.php +++ b/controllers/internals/Phone.php @@ -44,15 +44,15 @@ namespace controllers\internals; } /** - * Return a phone by his name. + * Return a list of phone limits * - * @param string $name : Phone name + * @param int $id_phone : Phone id * * @return array */ - public function get_by_name(string $name) + public function get_limits(int $id_phone) { - return $this->get_model()->get_by_name($name); + return $this->get_model()->get_limits($id_phone); } /** @@ -137,19 +137,46 @@ namespace controllers\internals; * @param string $name : The name of the phone * @param string $adapter : The adapter to use the phone * @param string json $adapter_data : A JSON string representing adapter's data (for example credentials for an api) + * @param int $priority : Priority with which to use phone to send SMS. Default 0. + * @param array $limits : An array of limits for this phone. Each limit must be an array with a key volume and a key startpoint * * @return bool|int : false on error, new id on success */ - public function create(int $id_user, string $name, string $adapter, string $adapter_data) + public function create(int $id_user, string $name, string $adapter, string $adapter_data, int $priority = 0, array $limits = []) { $phone = [ 'id_user' => $id_user, 'name' => $name, + 'priority' => $priority, 'adapter' => $adapter, 'adapter_data' => $adapter_data, ]; - return $this->get_model()->insert($phone); + //Use transaction to garanty atomicity + $this->bdd->beginTransaction(); + + $new_phone_id = $this->get_model()->insert($phone); + if (!$new_phone_id) + { + $this->bdd->rollBack(); + + return false; + } + + foreach ($limits as $limit) + { + $limit_id = $this->get_model()->insert_phone_limit($new_phone_id, $limit['volume'], $limit['startpoint']); + + if (!$limit_id) + { + $this->bdd->rollBack(); + + return false; + } + } + + $success = $this->bdd->commit(); + return ($success ? $new_phone_id : false); } /** @@ -159,20 +186,54 @@ namespace controllers\internals; * @param int $id : Phone id * @param string $name : The name of the phone * @param string $adapter : The adapter to use the phone - * @param array $adapter_data : An array of the data of the adapter (for example credentials for an api) + * @param string json $adapter_data : A JSON string representing adapter's data (for example credentials for an api) + * @param int $priority : Priority with which to use phone to send SMS. Default 0. + * @param array $limits : An array of limits for this phone. Each limit must be an array with a key volume and a key startpoint * * @return bool : false on error, true on success */ - public function update_for_user(int $id_user, int $id, string $name, string $adapter, array $adapter_data): bool + public function update_for_user(int $id_user, int $id, string $name, string $adapter, string $adapter_data, int $priority = 0, array $limits = []): bool { $phone = [ 'id_user' => $id_user, 'name' => $name, 'adapter' => $adapter, - 'adapter_data' => json_encode($adapter_data), + 'adapter_data' => $adapter_data, + 'priority' => $priority, ]; - return (bool) $this->get_model()->update_for_user($id_user, $id, $phone); + //Use transaction to garanty atomicity + $this->bdd->beginTransaction(); + + $nb_delete = $this->get_model()->delete_phone_limits($id); + + foreach ($limits as $limit) + { + $limit_id = $this->get_model()->insert_phone_limit($id, $limit['volume'], $limit['startpoint']); + + if (!$limit_id) + { + $this->bdd->rollBack(); + + return false; + } + } + + $nb_update = $this->get_model()->update_for_user($id_user, $id, $phone); + + $success = $this->bdd->commit(); + + if (!$success) + { + return false; + } + + if ($nb_update == 0 && count($limits) == 0) + { + return false; + } + + return true; } /** diff --git a/controllers/internals/Scheduled.php b/controllers/internals/Scheduled.php index 25ce51e..4f0c3f7 100644 --- a/controllers/internals/Scheduled.php +++ b/controllers/internals/Scheduled.php @@ -274,236 +274,6 @@ use Monolog\Logger; return $this->get_model()->gets_after_date_for_number_and_user($id_user, $date, $number); } - /** - * Get all messages to send and the number to use to send theme. - * - * @return array : List of smss to send at this time per scheduled id ['1' => [['id_scheduled', 'text', 'id_phone', 'destination', 'flash', 'mms', 'medias'], ...], ...] - */ - public function get_smss_to_send() - { - $smss_to_send_per_scheduled = []; - - $internal_templating = new \controllers\internals\Templating(); - $internal_setting = new \controllers\internals\Setting($this->bdd); - $internal_group = new \controllers\internals\Group($this->bdd); - $internal_conditional_group = new \controllers\internals\ConditionalGroup($this->bdd); - $internal_phone = new \controllers\internals\Phone($this->bdd); - $internal_smsstop = new \controllers\internals\SmsStop($this->bdd); - - $users_smsstops = []; - $users_settings = []; - $users_phones = []; - $users_mms_phones = []; - - $now = new \DateTime(); - $now = $now->format('Y-m-d H:i:s'); - $scheduleds = $this->get_model()->gets_before_date($now); - foreach ($scheduleds as $scheduled) - { - $smss_to_send_per_scheduled[$scheduled['id']] = []; - - if (!isset($users_settings[$scheduled['id_user']])) - { - $users_settings[$scheduled['id_user']] = []; - - $settings = $internal_setting->gets_for_user($scheduled['id_user']); - foreach ($settings as $name => $value) - { - $users_settings[$scheduled['id_user']][$name] = $value; - } - } - - if (!isset($users_smsstops[$scheduled['id_user']]) && $users_settings[$scheduled['id_user']]['smsstop']) - { - $users_smsstops[$scheduled['id_user']] = []; - - $smsstops = $internal_smsstop->gets_for_user($scheduled['id_user']); - foreach ($smsstops as $smsstop) - { - $users_smsstops[$scheduled['id_user']][] = $smsstop['number']; - } - } - - if (!isset($users_phones[$scheduled['id_user']])) - { - $phones = $internal_phone->gets_for_user($scheduled['id_user']); - $mms_phones = $internal_phone->gets_phone_supporting_mms_for_user($scheduled['id_user'], $internal_phone::MMS_SENDING); - $users_phones[$scheduled['id_user']] = $phones ?: []; - $users_mms_phones[$scheduled['id_user']] = $mms_phones ?: []; - } - - //Add medias to mms - $scheduled['medias'] = []; - if ($scheduled['mms']) - { - $internal_media = new Media($this->bdd); - $scheduled['medias'] = $internal_media->gets_for_scheduled($scheduled['id']); - } - - $phone_to_use = null; - foreach ($users_phones[$scheduled['id_user']] as $phone) - { - if ($phone['id'] !== $scheduled['id_phone']) - { - continue; - } - - $phone_to_use = $phone; - } - - $messages = []; - - //Add messages for numbers - $numbers = $this->get_numbers($scheduled['id']); - foreach ($numbers as $number) - { - if (null === $phone_to_use) - { - if ($scheduled['mms'] && count($users_mms_phones)) - { - $rnd_key = array_rand($users_mms_phones[$scheduled['id_user']]); - $random_phone = $users_mms_phones[$scheduled['id_user']][$rnd_key]; - } - else - { - $rnd_key = array_rand($users_phones[$scheduled['id_user']]); - $random_phone = $users_phones[$scheduled['id_user']][$rnd_key]; - } - } - - $message = [ - 'id_user' => $scheduled['id_user'], - 'id_scheduled' => $scheduled['id'], - 'id_phone' => $phone_to_use['id'] ?? $random_phone['id'], - 'destination' => $number['number'], - 'flash' => $scheduled['flash'], - 'mms' => $scheduled['mms'], - 'medias' => $scheduled['medias'], - ]; - - if ((int) ($users_settings[$scheduled['id_user']]['templating'] ?? false)) - { - $number['data'] = json_decode($number['data'] ?? '[]', true); - - $metas = ['number' => $number['number']]; - $data = ['contact' => $number['data'], 'contact_metas' => $metas]; - - $render = $internal_templating->render($scheduled['text'], $data); - - if (!$render['success']) - { - continue; - } - - $message['text'] = $render['result']; - } - else - { - $message['text'] = $scheduled['text']; - } - - $messages[] = $message; - } - - //Add messages for contacts - $contacts = $this->get_contacts($scheduled['id']); - - $groups = $this->get_groups($scheduled['id']); - foreach ($groups as $group) - { - $contacts_to_add = $internal_group->get_contacts($group['id']); - $contacts = array_merge($contacts, $contacts_to_add); - } - - $conditional_groups = $this->get_conditional_groups($scheduled['id']); - foreach ($conditional_groups as $conditional_group) - { - $contacts_to_add = $internal_conditional_group->get_contacts_for_condition_and_user($scheduled['id_user'], $conditional_group['condition']); - $contacts = array_merge($contacts, $contacts_to_add); - } - - $added_contacts = []; - foreach ($contacts as $contact) - { - if ($added_contacts[$contact['id']] ?? false) - { - continue; - } - - $added_contacts[$contact['id']] = true; - - if (null === $phone_to_use) - { - if ($scheduled['mms'] && count($users_mms_phones)) - { - $rnd_key = array_rand($users_mms_phones[$scheduled['id_user']]); - $random_phone = $users_mms_phones[$scheduled['id_user']][$rnd_key]; - } - else - { - $rnd_key = array_rand($users_phones[$scheduled['id_user']]); - $random_phone = $users_phones[$scheduled['id_user']][$rnd_key]; - } - } - - $message = [ - 'id_user' => $scheduled['id_user'], - 'id_scheduled' => $scheduled['id'], - 'id_phone' => $phone_to_use['id'] ?? $random_phone['id'], - 'destination' => $contact['number'], - 'flash' => $scheduled['flash'], - 'mms' => $scheduled['mms'], - 'medias' => $scheduled['medias'], - ]; - - if ((int) ($users_settings[$scheduled['id_user']]['templating'] ?? false)) - { - $contact['data'] = json_decode($contact['data'], true); - - //Add metas of contact by adding contact without data - $metas = $contact; - unset($metas['data'], $metas['id_user']); - - $data = ['contact' => $contact['data'], 'contact_metas' => $metas]; - - $render = $internal_templating->render($scheduled['text'], $data); - - if (!$render['success']) - { - continue; - } - - $message['text'] = $render['result']; - } - else - { - $message['text'] = $scheduled['text']; - } - - $messages[] = $message; - } - - foreach ($messages as $message) - { - //Remove empty messages - if ('' === trim($message['text']) && !$message['medias']) - { - continue; - } - - //Remove messages to smsstops numbers - if (($users_smsstops[$scheduled['id_user']] ?? false) && in_array($message['destination'], $users_smsstops[$scheduled['id_user']])) - { - continue; - } - - $smss_to_send_per_scheduled[$scheduled['id']][] = $message; - } - } - - return $smss_to_send_per_scheduled; - } - /** * Parse a CSV file of numbers, potentially associated with datas. * @@ -627,4 +397,292 @@ use Monolog\Logger; return $this->model; } + + + /** + * Get all messages to send and the number to use to send theme. + * + * @return array : List of smss to send at this time per scheduled id ['1' => [['id_scheduled', 'text', 'id_phone', 'destination', 'flash', 'mms', 'medias'], ...], ...] + */ + public function get_smss_to_send() + { + $sms_per_scheduled = []; + + $internal_templating = new \controllers\internals\Templating(); + $internal_setting = new \controllers\internals\Setting($this->bdd); + $internal_group = new \controllers\internals\Group($this->bdd); + $internal_conditional_group = new \controllers\internals\ConditionalGroup($this->bdd); + $internal_phone = new \controllers\internals\Phone($this->bdd); + $internal_smsstop = new \controllers\internals\SmsStop($this->bdd); + $internal_sended = new \controllers\internals\Sended($this->bdd); + + $users_smsstops = []; + $users_settings = []; + $users_phones = []; + $users_mms_phones = []; + + $now = new \DateTime(); + $now = $now->format('Y-m-d H:i:s'); + $scheduleds = $this->get_model()->gets_before_date($now); + foreach ($scheduleds as $scheduled) + { + $id_scheduled = $scheduled['id']; + $id_user = $scheduled['id_user']; + + $sms_per_scheduled[$id_scheduled] = []; + + // Forge cache of data about users, sms stops, phones, etc. + if (!isset($users_settings[$id_user])) + { + $users_settings[$id_user] = []; + + $settings = $internal_setting->gets_for_user($id_user); + foreach ($settings as $name => $value) + { + $users_settings[$id_user][$name] = $value; + } + } + + if (!isset($users_smsstops[$id_user]) && $users_settings[$id_user]['smsstop']) + { + $users_smsstops[$id_user] = []; + + $smsstops = $internal_smsstop->gets_for_user($id_user); + foreach ($smsstops as $smsstop) + { + $users_smsstops[$id_user][] = $smsstop['number']; + } + } + + if (!isset($users_phones[$id_user])) + { + $users_phones[$id_user] = []; + $users_mms_phones[$id_user] = []; + + $phones = $internal_phone->gets_for_user($id_user); + foreach ($phones as &$phone) + { + $limits = $internal_phone->get_limits($phone['id']); + + $remaining_volume = PHP_INT_MAX; + foreach ($limits as $limit) + { + $startpoint = new \DateTime($limit['startpoint']); + $consumed = $internal_sended->count_since_for_phone_and_user($id_user, $phone['id'], $startpoint); + $remaining_volume = min(($limit['volume'] - $consumed), $remaining_volume); + } + + $phone['remaining_volume'] = $remaining_volume; + $users_phones[$id_user][$phone['id']] = $phone; + } + + $mms_phones = $internal_phone->gets_phone_supporting_mms_for_user($id_user, $internal_phone::MMS_SENDING); + foreach ($mms_phones as &$mms_phone) + { + $limits = $internal_phone->get_limits($mms_phone['id']); + + $remaining_volume = PHP_INT_MAX; + foreach ($limits as $limit) + { + $startpoint = new \DateTime($limit['startpoint']); + $consumed = $internal_sended->count_since_for_phone_and_user($id_user, $mms_phone['id'], $startpoint); + $remaining_volume = min(($limit['volume'] - $consumed), $remaining_volume); + } + + $mms_phone['remaining_volume'] = $remaining_volume; + $users_mms_phones[$id_user][$mms_phone['id']] = $mms_phone; + } + } + + + //Add medias to mms + $scheduled['medias'] = []; + if ($scheduled['mms']) + { + $internal_media = new Media($this->bdd); + $scheduled['medias'] = $internal_media->gets_for_scheduled($id_scheduled); + } + + $phone_to_use = null; + if ($scheduled['id_phone']) + { + $phone_to_use = $users_phones[$id_user][$scheduled['id_phone']] ?? null; + } + + + // We turn all contacts, groups and conditional groups into just contacts + $contacts = $this->get_contacts($id_scheduled); + + $groups = $this->get_groups($id_scheduled); + foreach ($groups as $group) + { + $contacts_to_add = $internal_group->get_contacts($group['id']); + $contacts = array_merge($contacts, $contacts_to_add); + } + + $conditional_groups = $this->get_conditional_groups($id_scheduled); + foreach ($conditional_groups as $conditional_group) + { + $contacts_to_add = $internal_conditional_group->get_contacts_for_condition_and_user($id_user, $conditional_group['condition']); + $contacts = array_merge($contacts, $contacts_to_add); + } + + + // We turn all numbers and contacts into simple targets with number, data and meta so we can forge all messages from onlye one data source + $targets = []; + + $numbers = $this->get_numbers($id_scheduled); + foreach ($numbers as $number) + { + $metas = ['number' => $number['number']]; + $targets[] = [ + 'number' => $number['number'], + 'data' => $number['data'], + 'metas' => $metas, + ]; + } + + foreach ($contacts as $contact) + { + $metas = $contact; + unset($metas['data'], $metas['id_user']); + + $targets[] = [ + 'number' => $contact['number'], + 'data' => $contact['data'], + 'metas' => $metas, + ]; + } + + + // Pass on all targets to deduplicate destinations, remove number in sms stops, etc. + $used_destinations = []; + foreach ($targets as $key => $target) + { + if (in_array($target['number'], $used_destinations)) + { + unset($targets[$key]); + continue; + } + + //Remove messages to smsstops numbers + if (($users_smsstops[$id_user] ?? false) && in_array($target['number'], $users_smsstops[$id_user])) + { + continue; + } + + $used_destinations[] = $target['number']; + } + + + // Finally, we forge all messages and select phone to use + foreach ($targets as $target) + { + // Forge message if templating enable + $text = $scheduled['text']; + if ((int) ($users_settings[$id_user]['templating'] ?? false)) // Cast to int because it is more reliable than bool on strings + { + $target['data'] = json_decode($target['data'], true); + $data = ['contact' => $target['data'], 'contact_metas' => $target['metas']]; + + $render = $internal_templating->render($scheduled['text'], $data); + + if (!$render['success']) + { + continue; + } + + $text = $render['result']; + } + + // Ignore empty messages + if ('' === trim($text) && !$scheduled['medias']) + { + continue; + } + + // If we must force GSM 7 alphabet + if ((int) ($users_settings[$id_user]['force_gsm_alphabet'] ?? false)) + { + $text = Tool::convert_to_gsm0338($text); + } + + /* + Choose phone if no phone defined for message + Phones are choosen using type, priority and remaining volume : + 1 - If sms is a mms, try to use mms phone if any available. If mms phone available use mms phone, else use default. + 2 - In group of phones, keep only phones with remaining volume. If no phones with remaining volume, use all phones instead. + 3 - Groupe keeped phones by priority get group with biggest priority. + 4 - Get a random phone in this group. + 5 - If their is no phone matching, keep phone at null so sender will directly mark it as failed + */ + $random_phone = null; + if (null === $phone_to_use) + { + $phones_subset = $users_phones[$id_user]; + if ($scheduled['mms']) + { + $phones_subset = $users_mms_phones[$id_user] ?: $phones_subset; + } + + $remaining_volume_phones = array_filter($phones_subset, function ($phone) { + return $phone['remaining_volume'] > 0; + }); + $phones_subset = $remaining_volume_phones ?: $phones_subset; + + $max_priority_phones = []; + $max_priority = PHP_INT_MIN; + foreach ($phones_subset as $phone) + { + if ($phone['priority'] < $max_priority) + { + continue; + } + elseif ($phone['priority'] == $max_priority) + { + $max_priority_phones[] = $phone; + } + elseif ($phone['priority'] > $max_priority) + { + $max_priority_phones = [$phone]; + $max_priority = $phone['priority']; + } + } + + $phones_subset = $max_priority_phones; + if ($phones_subset) + { + $random_phone = $phones_subset[array_rand($phones_subset)]; + } + } + + // This should only happen if the user try to send a message without any phone in his account, then we simply ignore. + if (!$random_phone && !$phone_to_use) + { + continue; + } + + $id_phone = $phone_to_use['id'] ?? $random_phone['id']; + $sms_per_scheduled[$id_scheduled][] = [ + 'id_user' => $id_user, + 'id_scheduled' => $id_scheduled, + 'id_phone' => $id_phone, + 'destination' => $target['number'], + 'flash' => $scheduled['flash'], + 'mms' => $scheduled['mms'], + 'medias' => $scheduled['medias'], + 'text' => $text, + ]; + + // Consume one sms from remaining volume of phone, dont forget to do the same for the entry in mms phones + $users_phones[$id_user][$id_phone]['remaining_volume'] --; + if ($users_mms_phones[$id_user][$id_phone] ?? false) + { + $users_mms_phones[$id_user][$id_phone] --; + } + } + } + + return $sms_per_scheduled; + } } diff --git a/controllers/internals/Sended.php b/controllers/internals/Sended.php index 9cce107..6a9276c 100644 --- a/controllers/internals/Sended.php +++ b/controllers/internals/Sended.php @@ -179,6 +179,20 @@ namespace controllers\internals; return $this->get_model()->get_by_uid_and_adapter_for_user($id_user, $uid, $adapter); } + /** + * Get number of sended SMS since a date for a phone + * + * @param int $id_user : User id + * @param int $id_phone : Phone id we want the number of sended message for + * @param \DateTime $since : Date since which we want sended number + * + * @return int + */ + public function count_since_for_phone_and_user(int $id_user, int $id_phone, \DateTime $since): int + { + return $this->get_model()->count_since_for_phone_and_user($id_user, $id_phone, $since); + } + /** * Get number of sended SMS for every date since a date for a specific user. * @@ -239,17 +253,6 @@ 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) @@ -272,38 +275,56 @@ namespace controllers\internals; $text .= "\n" . join(' - ', $media_urls); } - $response = $adapter->send($destination, $text, $flash, $mms, $media_uris); - - if ($response['error']) + //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'] = true; - $return['error_message'] = $response['error_message']; - $status = \models\Sended::STATUS_FAILED; - $sended_id = $this->create($id_user, $id_phone, $at, $text, $destination, $response['uid'] ?? uniqid(), $adapter->meta_classname(), $flash, $mms, $medias, $originating_scheduled, $status); - - $sended = [ - 'id' => $sended_id, - 'at' => $at, - 'status' => $status, - 'text' => $text, - 'destination' => $destination, - 'origin' => $id_phone, - 'mms' => $mms, - 'medias' => $medias, - 'originating_scheduled' => $originating_scheduled, - ]; - - $internal_webhook = new Webhook($this->bdd); - $internal_webhook->trigger($id_user, \models\Webhook::TYPE_SEND_SMS, $sended); - - return $return; + $return['error_message'] = 'Not enough credit to send message.'; } - $internal_quota->consume_credit($id_user, $nb_credits); + //If we reached limit for this phone, do not send the message + $internal_phone = new Phone($this->bdd); + $internal_sended = new Sended($this->bdd); + $limits = $internal_phone->get_limits($id_phone); - $sended_id = $this->create($id_user, $id_phone, $at, $text, $destination, $response['uid'] ?? uniqid(), $adapter->meta_classname(), $flash, $mms, $medias, $originating_scheduled, $status); + $remaining_volume = PHP_INT_MAX; + foreach ($limits as $limit) + { + $startpoint = new \DateTime($limit['startpoint']); + $consumed = $internal_sended->count_since_for_phone_and_user($id_user, $id_phone, $startpoint); + $remaining_volume = min(($limit['volume'] - $consumed), $remaining_volume); + } - $sended = [ + if ($remaining_volume < 1) + { + $return['error'] = true; + $return['error_message'] = 'Phone send limit have been reached.'; + } + + $uid = uniqid(); + if (!$return['error']) + { + $response = $adapter->send($destination, $text, $flash, $mms, $media_uris); + $uid = $response['uid'] ?? $uid; + + if ($response['error']) + { + $return['error'] = true; + $return['error_message'] = $response['error_message']; + } + else // If send with success, consume credit + { + $internal_quota->consume_credit($id_user, $nb_credits); + } + } + + // If we fail to send or not, we will always save message as sended, only the status will change. + $status = $return['error'] ? \models\Sended::STATUS_FAILED : \models\Sended::STATUS_UNKNOWN; + $sended_id = $this->create($id_user, $id_phone, $at, $text, $destination, $uid, $adapter->meta_classname(), $flash, $mms, $medias, $originating_scheduled, $status); + + $webhook_body = [ 'id' => $sended_id, 'at' => $at, 'status' => $status, @@ -316,7 +337,7 @@ namespace controllers\internals; ]; $internal_webhook = new Webhook($this->bdd); - $internal_webhook->trigger($id_user, \models\Webhook::TYPE_SEND_SMS, $sended); + $internal_webhook->trigger($id_user, \models\Webhook::TYPE_SEND_SMS, $webhook_body); return $return; } diff --git a/controllers/internals/Tool.php b/controllers/internals/Tool.php index e6b8994..a09fab6 100644 --- a/controllers/internals/Tool.php +++ b/controllers/internals/Tool.php @@ -11,6 +11,8 @@ namespace controllers\internals; +use BenMorel\GsmCharsetConverter\Converter; + /** * Some tools frequently used. * Not a standard controller as it's not linked to a model in any way. @@ -165,6 +167,24 @@ namespace controllers\internals; return $objectDate && $objectDate->format($format) === $date; } + /** + * Check if a relative format date (see https://www.php.net/manual/en/datetime.formats.relative.php) is valid. + * + * @param string $date : Relative date + * + * @return bool : True if valid, false else + */ + public static function validate_relative_date($date) + { + try { + $d = new \DateTime($date); + } catch (\Throwable $th) { + return false; + } + + return true; + } + /** * Check if a sting represent a valid PHP period for creating an interval. * @@ -394,4 +414,17 @@ namespace controllers\internals; return "{$scheme}{$user}{$pass}{$host}{$port}{$path}{$query}{$fragment}"; } + + + /** + * Transform an UTF-8 string into a valid GSM 7 string (GSM 03.38), remplacing invalid chars with best equivalent whenever possible + * + * @param string $text : Input text to convert into gsm string + * @return string : An UTF-8 string with GSM alphabet only + */ + public static function convert_to_gsm0338(string $text): string + { + $converter = new Converter(); + return $converter->cleanUpUtf8String($text, true, '?'); + } } diff --git a/controllers/internals/Webhook.php b/controllers/internals/Webhook.php index bb2c3bd..6a2e8cf 100644 --- a/controllers/internals/Webhook.php +++ b/controllers/internals/Webhook.php @@ -137,10 +137,10 @@ class Webhook extends StandardController $error_code = null; $queue = msg_get_queue(QUEUE_ID_WEBHOOK); - $success = msg_send($queue, QUEUE_TYPE_WEBHOOK, $message, true, true, $error_code); - - return (bool) $success; + msg_send($queue, QUEUE_TYPE_WEBHOOK, $message, true, true, $error_code); } + + return true; } /** diff --git a/controllers/publics/Api.php b/controllers/publics/Api.php index 92fb2ea..7ee0d36 100644 --- a/controllers/publics/Api.php +++ b/controllers/publics/Api.php @@ -506,6 +506,8 @@ namespace controllers\publics; * @param string $_POST['name'] : Phone name * @param string $_POST['adapter'] : Phone adapter * @param array $_POST['adapter_data'] : Phone adapter data + * @param int $priority : Priority with which to use phone to send SMS. Default 0. + * @param ?array $_POST['limits'] : Array of limits in number of SMS for a period to be applied to this phone. * * @return int : id phone the new phone on success */ @@ -516,6 +518,10 @@ namespace controllers\publics; $name = $_POST['name'] ?? false; $adapter = $_POST['adapter'] ?? false; $adapter_data = !empty($_POST['adapter_data']) ? $_POST['adapter_data'] : []; + $priority = $_POST['priority'] ?? 0; + $priority = max(((int) $priority), 0); + $limits = $_POST['limits'] ?? []; + $limits = is_array($limits) ? $limits : [$limits]; if (!$name) { @@ -535,7 +541,7 @@ namespace controllers\publics; return $this->json($return); } - $name_exist = $this->internal_phone->get_by_name($name); + $name_exist = $this->internal_phone->get_by_name_and_user($this->user['id'], $name); if ($name_exist) { $return['error'] = self::ERROR_CODES['INVALID_PARAMETER']; @@ -545,6 +551,36 @@ namespace controllers\publics; return $this->json($return); } + if ($limits) + { + foreach ($limits as $key => $limit) + { + if (!is_array($limit)) + { + unset($limits[$key]); + continue; + } + + $startpoint = $limit['startpoint'] ?? false; + $volume = $limit['volume'] ?? false; + + if (!$startpoint || !$volume) + { + unset($limits[$key]); + continue; + } + + $volume = (int) $volume; + $limits[$key]['volume'] = max($volume, 1); + + if (!\controllers\internals\Tool::validate_relative_date($startpoint)) + { + unset($limits[$key]); + continue; + } + } + } + $adapters = $this->internal_adapter->list_adapters(); $find_adapter = false; foreach ($adapters as $metas) @@ -627,7 +663,7 @@ namespace controllers\publics; return $this->json($return); } - $phone_id = $this->internal_phone->create($this->user['id'], $name, $adapter, $adapter_data); + $phone_id = $this->internal_phone->create($this->user['id'], $name, $adapter, $adapter_data, $priority, $limits); if (false === $phone_id) { $return['error'] = self::ERROR_CODES['CANNOT_CREATE']; @@ -650,6 +686,7 @@ namespace controllers\publics; * @param string (optionnal) $_POST['name'] : New phone name * @param string (optionnal) $_POST['adapter'] : New phone adapter * @param array (optionnal) $_POST['adapter_data'] : New phone adapter data + * @param int $priority : Priority with which to use phone to send SMS. Default 0. * * @return int : id phone the new phone on success */ @@ -667,10 +704,16 @@ namespace controllers\publics; return $this->json($return); } + $limits = $this->internal_phone->get_limits(($phone['id'])); + $name = $_POST['name'] ?? $phone['name']; + $priority = $_POST['priority'] ?? $phone['priority']; + $priority = max(((int) $priority), 0); $adapter = $_POST['adapter'] ?? $phone['adapter']; $adapter_data = !empty($_POST['adapter_data']) ? $_POST['adapter_data'] : json_decode($phone['adapter_data']); $adapter_data = is_array($adapter_data) ? $adapter_data : [$adapter_data]; + $limits = $_POST['limits'] ?? $limits; + $limits = is_array($limits) ? $limits : [$limits]; if (!$name && !$adapter && !$adapter_data) @@ -683,7 +726,7 @@ namespace controllers\publics; } - $phone_with_same_name = $this->internal_phone->get_by_name($name); + $phone_with_same_name = $this->internal_phone->get_by_name_and_user($this->user['id'], $name); if ($phone_with_same_name && $phone_with_same_name['id'] != $phone['id']) { $return['error'] = self::ERROR_CODES['INVALID_PARAMETER']; @@ -693,6 +736,36 @@ namespace controllers\publics; return $this->json($return); } + if ($limits) + { + foreach ($limits as $key => $limit) + { + if (!is_array($limit)) + { + unset($limits[$key]); + continue; + } + + $startpoint = $limit['startpoint'] ?? false; + $volume = $limit['volume'] ?? false; + + if (!$startpoint || !$volume) + { + unset($limits[$key]); + continue; + } + + $volume = (int) $volume; + $limits[$key]['volume'] = max($volume, 1); + + if (!\controllers\internals\Tool::validate_relative_date($startpoint)) + { + unset($limits[$key]); + continue; + } + } + } + $adapters = $this->internal_adapter->list_adapters(); $find_adapter = false; foreach ($adapters as $metas) @@ -775,7 +848,7 @@ namespace controllers\publics; return $this->json($return); } - $success = $this->internal_phone->update_for_user($this->user['id'], $phone['id'], $name, $adapter, $adapter_data); + $success = $this->internal_phone->update_for_user($this->user['id'], $phone['id'], $name, $adapter, $adapter_data_json, $priority, $limits); if (!$success) { $return['error'] = self::ERROR_CODES['CANNOT_UPDATE']; diff --git a/controllers/publics/ConditionalGroup.php b/controllers/publics/ConditionalGroup.php index 2a1aad8..8f1f3ea 100644 --- a/controllers/publics/ConditionalGroup.php +++ b/controllers/publics/ConditionalGroup.php @@ -185,6 +185,45 @@ namespace controllers\publics; return $this->redirect(\descartes\Router::url('ConditionalGroup', 'list')); } + /** + * Return contacts of a group as json array + * @param int $id_group = Group id + * + * @return json + */ + public function preview (int $id_group) + { + $return = [ + 'success' => false, + 'result' => 'Une erreur inconnue est survenue.', + ]; + + $group = $this->internal_conditional_group->get_for_user($_SESSION['user']['id'], $id_group); + + if (!$group) + { + $return['result'] = 'Ce groupe n\'existe pas.'; + echo json_encode($return); + + return false; + } + + $contacts = $this->internal_conditional_group->get_contacts_for_condition_and_user($_SESSION['user']['id'], $group['condition']); + if (!$contacts) + { + $return['result'] = 'Aucun contact dans le groupe.'; + echo json_encode($return); + + return false; + } + + $return['success'] = true; + $return['result'] = $contacts; + echo json_encode($return); + + return true; + } + /** * Try to get the preview of contacts for a conditionnal group. * diff --git a/controllers/publics/Contact.php b/controllers/publics/Contact.php index 78da100..69b0cc8 100644 --- a/controllers/publics/Contact.php +++ b/controllers/publics/Contact.php @@ -372,6 +372,7 @@ namespace controllers\publics; else { $invalid_type = true; + $result = false; } } diff --git a/controllers/publics/Group.php b/controllers/publics/Group.php index fa8b8af..2bd2c4f 100644 --- a/controllers/publics/Group.php +++ b/controllers/publics/Group.php @@ -191,6 +191,45 @@ namespace controllers\publics; return $this->redirect(\descartes\Router::url('Group', 'list')); } + /** + * Return contacts of a group as json array + * @param int $id_group = Group id + * + * @return json + */ + public function preview (int $id_group) + { + $return = [ + 'success' => false, + 'result' => 'Une erreur inconnue est survenue.', + ]; + + $group = $this->internal_group->get_for_user($_SESSION['user']['id'], $id_group); + + if (!$group) + { + $return['result'] = 'Ce groupe n\'existe pas.'; + echo json_encode($return); + + return false; + } + + $contacts = $this->internal_group->get_contacts($id_group); + if (!$contacts) + { + $return['result'] = 'Aucun contact dans le groupe.'; + echo json_encode($return); + + return false; + } + + $return['success'] = true; + $return['result'] = $contacts; + echo json_encode($return); + + return true; + } + /** * Cette fonction retourne la liste des groups sous forme JSON. */ diff --git a/controllers/publics/Phone.php b/controllers/publics/Phone.php index 14028ea..7451288 100644 --- a/controllers/publics/Phone.php +++ b/controllers/publics/Phone.php @@ -55,6 +55,9 @@ class Phone extends \descartes\Controller foreach ($phones as &$phone) { + $limits = $this->internal_phone->get_limits($phone['id']); + $phone['limits'] = $limits; + $adapter = $adapters[$phone['adapter']] ?? false; if (!$adapter) @@ -131,9 +134,11 @@ class Phone extends \descartes\Controller * Create a new phone. * * @param $csrf : CSRF token - * @param string $_POST['name'] : Phone name - * @param string $_POST['adapter'] : Phone adapter - * @param array $_POST['adapter_data'] : Phone adapter data + * @param string $_POST['name'] : Phone name + * @param string $_POST['adapter'] : Phone adapter + * @param ?array $_POST['adapter_data'] : Phone adapter data + * @param ?array $_POST['limits'] : Array of limits in number of SMS for a period to be applied to this phone. + * @param int $_POST['priority'] : Priority with which to use phone to send SMS. Default 0. */ public function create($csrf) { @@ -146,8 +151,12 @@ class Phone extends \descartes\Controller $id_user = $_SESSION['user']['id']; $name = $_POST['name'] ?? false; + $priority = $_POST['priority'] ?? 0; + $priority = max(((int) $priority), 0); $adapter = $_POST['adapter'] ?? false; $adapter_data = !empty($_POST['adapter_data']) ? $_POST['adapter_data'] : []; + $limits = $_POST['limits'] ?? []; + $limits = is_array($limits) ? $limits : [$limits]; if (!$name || !$adapter) { @@ -156,7 +165,7 @@ class Phone extends \descartes\Controller return $this->redirect(\descartes\Router::url('Phone', 'add')); } - $name_exist = $this->internal_phone->get_by_name($name); + $name_exist = $this->internal_phone->get_by_name_and_user($id_user, $name); if ($name_exist) { \FlashMessage\FlashMessage::push('danger', 'Ce nom est déjà utilisé pour un autre téléphone.'); @@ -164,6 +173,36 @@ class Phone extends \descartes\Controller return $this->redirect(\descartes\Router::url('Phone', 'add')); } + if ($limits) + { + foreach ($limits as $key => $limit) + { + if (!is_array($limit)) + { + unset($limits[$key]); + continue; + } + + $startpoint = $limit['startpoint'] ?? false; + $volume = $limit['volume'] ?? false; + + if (!$startpoint || !$volume) + { + unset($limits[$key]); + continue; + } + + $volume = (int) $volume; + $limits[$key]['volume'] = max($volume, 1); + + if (!\controllers\internals\Tool::validate_relative_date($startpoint)) + { + unset($limits[$key]); + continue; + } + } + } + $adapters = $this->internal_adapter->list_adapters(); $find_adapter = false; foreach ($adapters as $metas) @@ -245,7 +284,7 @@ class Phone extends \descartes\Controller return $this->redirect(\descartes\Router::url('Phone', 'add')); } - $success = $this->internal_phone->create($id_user, $name, $adapter, $adapter_data); + $success = $this->internal_phone->create($id_user, $name, $adapter, $adapter_data, $priority, $limits); if (!$success) { \FlashMessage\FlashMessage::push('danger', 'Impossible de créer ce téléphone.'); @@ -257,4 +296,207 @@ class Phone extends \descartes\Controller return $this->redirect(\descartes\Router::url('Phone', 'list')); } + + + /** + * Return the edit page for phones + * + * @param int... $ids : Phones ids + */ + public function edit() + { + $ids = $_GET['ids'] ?? []; + $id_user = $_SESSION['user']['id']; + + $phones = $this->internal_phone->gets_in_for_user($id_user, $ids); + + if (!$phones) + { + return $this->redirect(\descartes\Router::url('Phone', 'list')); + } + + $adapters = $this->internal_adapter->list_adapters(); + + foreach ($phones as &$phone) + { + $limits = $this->internal_phone->get_limits($phone['id']); + $phone['limits'] = $limits; + } + + $this->render('phone/edit', [ + 'phones' => $phones, + 'adapters' => $adapters, + ]); + } + + + /** + * Update multiple phones. + * + * @param $csrf : CSRF token + * @param string $_POST['phones']['id']['name'] : Phone name + * @param string $_POST['phones']['id']['adapter'] : Phone adapter + * @param ?array $_POST['phones']['id']['adapter_data'] : Phone adapter data + * @param ?array $_POST['phones']['id']['limits'] : Array of limits in number of SMS for a period to be applied to this phone. + * @param int $_POST['phones']['id']['priority'] : Priority with which to use phone to send SMS. Default 0. + */ + public function update($csrf) + { + if (!$this->verify_csrf($csrf)) + { + \FlashMessage\FlashMessage::push('danger', 'Jeton CSRF invalid !'); + + return $this->redirect(\descartes\Router::url('Phone', 'add')); + } + + if (!$_POST['phones']) + { + return $this->redirect(\descartes\Router::url('Phone', 'list')); + } + + $id_user = $_SESSION['user']['id']; + + $nb_update = 0; + foreach ($_POST['phones'] as $id_phone => $phone) + { + $name = $phone['name'] ?? false; + $priority = $phone['priority'] ?? 0; + $priority = max(((int) $priority), 0); + $adapter = $phone['adapter'] ?? false; + $adapter_data = !empty($phone['adapter_data']) ? $phone['adapter_data'] : []; + $limits = $phone['limits'] ?? []; + $limits = is_array($limits) ? $limits : [$limits]; + + if (!$name || !$adapter) + { + continue; + } + + $phone_with_same_name = $this->internal_phone->get_by_name_and_user($id_user, $name); + if ($phone_with_same_name && $phone_with_same_name['id'] != $id_phone) + { + continue; + } + + if ($limits) + { + foreach ($limits as $key => $limit) + { + if (!is_array($limit)) + { + unset($limits[$key]); + continue; + } + + $startpoint = $limit['startpoint'] ?? false; + $volume = $limit['volume'] ?? false; + + if (!$startpoint || !$volume) + { + unset($limits[$key]); + continue; + } + + $volume = (int) $volume; + $limits[$key]['volume'] = max($volume, 1); + + if (!\controllers\internals\Tool::validate_relative_date($startpoint)) + { + unset($limits[$key]); + continue; + } + } + } + + $adapters = $this->internal_adapter->list_adapters(); + $find_adapter = false; + foreach ($adapters as $metas) + { + if ($metas['meta_classname'] === $adapter) + { + $find_adapter = $metas; + + break; + } + } + + if (!$find_adapter) + { + continue; + } + + if ($find_adapter['meta_hidden']) + { + continue; + } + + //If missing required data fields, error + foreach ($find_adapter['meta_data_fields'] as $field) + { + if (false === $field['required']) + { + continue; + } + + if (!empty($adapter_data[$field['name']])) + { + continue; + } + + continue 2; + } + + //If field phone number is invalid + foreach ($find_adapter['meta_data_fields'] as $field) + { + if ('phone_number' !== ($field['type'] ?? false)) + { + continue; + } + + if (!empty($adapter_data[$field['name']])) + { + $adapter_data[$field['name']] = \controllers\internals\Tool::parse_phone($adapter_data[$field['name']]); + + if ($adapter_data[$field['name']]) + { + continue; + } + } + + continue 2; + } + + $adapter_data = json_encode($adapter_data); + + //Check adapter is working correctly with thoses names and data + $adapter_classname = $find_adapter['meta_classname']; + $adapter_instance = new $adapter_classname($adapter_data); + $adapter_working = $adapter_instance->test(); + + if (!$adapter_working) + { + continue; + } + + $success = $this->internal_phone->update_for_user($id_user, $id_phone, $name, $adapter, $adapter_data, $priority, $limits); + if (!$success) + { + continue; + } + + $nb_update ++; + } + + if ($nb_update !== \count($_POST['phones'])) + { + \FlashMessage\FlashMessage::push('danger', 'Certains téléphones n\'ont pas pu êtres mis à jour.'); + + return $this->redirect(\descartes\Router::url('Phone', 'list')); + } + + \FlashMessage\FlashMessage::push('success', 'Tous les téléphones ont été modifiés avec succès.'); + + return $this->redirect(\descartes\Router::url('Phone', 'list')); + } } diff --git a/controllers/publics/Templating.php b/controllers/publics/Templating.php index 026622e..a667ee7 100644 --- a/controllers/publics/Templating.php +++ b/controllers/publics/Templating.php @@ -83,6 +83,12 @@ namespace controllers\publics; $result = $this->internal_templating->render($template, $data); $return = $result; + // If we must force GSM 7 alphabet + if ((int) ($_SESSION['user']['settings']['force_gsm_alphabet'] ?? false)) + { + $return['result'] = \controllers\internals\Tool::convert_to_gsm0338($return['result']); + } + if (!trim($result['result'])) { $return['result'] = 'Message vide, il ne sera pas envoyé.'; diff --git a/daemons/Sender.php b/daemons/Sender.php index 6f0176b..d462739 100644 --- a/daemons/Sender.php +++ b/daemons/Sender.php @@ -22,6 +22,7 @@ class Sender extends AbstractDaemon private $internal_phone; private $internal_scheduled; private $internal_received; + private $internal_sended; private $bdd; private $msg_queue; @@ -45,6 +46,7 @@ class Sender extends AbstractDaemon { //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(); @@ -62,8 +64,8 @@ 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->queue)) + //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); } diff --git a/db/migrations/20230201152658_add_phone_limits.php b/db/migrations/20230201152658_add_phone_limits.php new file mode 100644 index 0000000..debf233 --- /dev/null +++ b/db/migrations/20230201152658_add_phone_limits.php @@ -0,0 +1,43 @@ +table('phone_limit'); + $table->addColumn('id_phone', 'integer', ['null' => false]) + ->addColumn('volume', 'integer', ['null' => false]) + ->addColumn('startpoint', 'string', ['null' => false, 'limit' => 254]) # A relative time to use as startpoint for counting volume. See https://www.php.net/manual/en/datetime.formats.relative.php + ->addColumn('created_at', 'timestamp', ['null' => false, 'default' => 'CURRENT_TIMESTAMP']) + ->addColumn('updated_at', 'timestamp', ['null' => true, 'update' => 'CURRENT_TIMESTAMP']) + ->addForeignKey('id_phone', 'phone', 'id', ['delete' => 'CASCADE', 'update' => 'CASCADE']) + ->create(); + } +} diff --git a/db/migrations/20230205214319_add_phone_priority.php b/db/migrations/20230205214319_add_phone_priority.php new file mode 100644 index 0000000..3ce343b --- /dev/null +++ b/db/migrations/20230205214319_add_phone_priority.php @@ -0,0 +1,43 @@ +table('phone'); + $table->addColumn('priority', 'integer', [ + 'null' => false, + 'default' => 0, + 'comment' => 'Priority with which the phone will be used. The higher the more prioritary.', + 'after' => 'name' + ]) + ->update(); + } +} diff --git a/env.php.dist b/env.php.dist index 8f7091b..c544ba0 100644 --- a/env.php.dist +++ b/env.php.dist @@ -82,6 +82,7 @@ 'alert_quota_limit_reached' => 1, 'alert_quota_limit_close' => 0.9, 'hide_menus' => '', + 'force_gsm_alphabet' => 0, ], ]; diff --git a/models/Event.php b/models/Event.php index 843f442..166d46e 100644 --- a/models/Event.php +++ b/models/Event.php @@ -84,32 +84,6 @@ 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 function 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 (null !== $until) - { - $where['<=at'] = $until->format('Y-m-d H:i:s'); - } - - return $this->_select('event', $where, 'at'); - } - /** * Return table name. */ diff --git a/models/Phone.php b/models/Phone.php index 14447f4..4579b22 100644 --- a/models/Phone.php +++ b/models/Phone.php @@ -15,7 +15,7 @@ namespace models; { /** - * Return all hones that belongs to active users + * Return all phones that belongs to active users * * @return array */ @@ -63,6 +63,48 @@ namespace models; return $this->_select_one('phone', ['name' => $name]); } + + /** + * Return a list of phone limits + * + * @param int $id_phone : Phone id + * + * @return array + */ + public function get_limits(int $id_phone) + { + return $this->_select('phone_limit', ['id_phone' => $id_phone]); + } + + /** + * Add a limit for a phone. + * + * @param int $id_phone : Phone id + * @param int $volume : Limit in volume of SMS + * @param string $startpoint : A relative time to use as startpoint for counting volume. See https://www.php.net/manual/en/datetime.formats.relative.php + * + * @return mixed (bool|int) : False on error, new row id else + */ + public function insert_phone_limit(int $id_phone, int $volume, string $startpoint) + { + $success = $this->_insert('phone_limit', ['id_phone' => $id_phone, 'volume' => $volume, 'startpoint' => $startpoint]); + + return $success ? $this->_last_id() : false; + } + + /** + * Delete limits for a phone + * + * @param array $id_phone : Phone id + * + * @return array + */ + public function delete_phone_limits(int $id_phone) + { + return $this->_delete('phone_limit', ['id_phone' => $id_phone]); + } + + /** * Return table name. */ diff --git a/models/Sended.php b/models/Sended.php index b401ccc..0acdb20 100644 --- a/models/Sended.php +++ b/models/Sended.php @@ -178,6 +178,20 @@ namespace models; return $this->_select_one('sended', ['id_user' => $id_user, 'uid' => $uid, 'adapter' => $adapter]); } + /** + * Get number of sended SMS since a date for a phone + * + * @param int $id_user : User id + * @param int $id_phone : Phone id we want the number of sended message for + * @param \DateTime $since : Date since which we want sended number + * + * @return int + */ + public function count_since_for_phone_and_user(int $id_user, int $id_phone, \DateTime $since) : int + { + return $this->_count('sended', ['id_user' => $id_user, 'id_phone' => $id_phone, '>=at' => $since->format('c')]); + } + /** * Get number of sended SMS for every date since a date for a specific user. * diff --git a/routes.php b/routes.php index 3eefebd..a5e87cd 100644 --- a/routes.php +++ b/routes.php @@ -76,6 +76,7 @@ 'delete' => '/group/delete/{csrf}/', 'edit' => '/group/edit/', 'update' => '/group/update/{csrf}/', + 'preview' => '/group/preview/{id_group}/', 'json_list' => '/groups.json/', ], @@ -88,6 +89,7 @@ 'edit' => '/conditional_group/edit/', 'update' => '/conditional_group/update/{csrf}/', 'contacts_preview' => '/conditional_group/preview/', + 'preview' => '/conditional_group/preview/{id_group}/', 'json_list' => '/conditional_groups.json/', ], @@ -160,6 +162,8 @@ 'add' => '/phone/add/', 'create' => '/phone/create/{csrf}/', 'delete' => '/phone/delete/{csrf}/', + 'edit' => '/phone/edit/', + 'update' => '/phone/update/{csrf}/', ], 'Call' => [ diff --git a/templates/conditional_group/list.php b/templates/conditional_group/list.php index b688bc4..0ef7726 100644 --- a/templates/conditional_group/list.php +++ b/templates/conditional_group/list.php @@ -43,6 +43,7 @@ Condition Date de création Dernière modification + Preview @@ -69,9 +70,56 @@ + Nombre de contacts Date de création Dernière modification + Preview @@ -69,6 +70,21 @@ +
- + +

+ Lors de l'envoi de SMS sans téléphone spécifié, les téléphones avec la plus haute priorité seront utilisés en premier. +

+
+ +
+
+
+

Le type de téléphone utilisé par RaspiSMS pour envoyer ou recevoir les SMS. Pour plus d'information, consultez la documentation de RaspiSMS concernant les différents types de téléphones.

@@ -67,7 +76,7 @@
-
+

Description du téléphone

@@ -77,6 +86,15 @@

Réglages du téléphone

+
+
+ +

+ Défini le nombre maximum de SMS qui pourront être envoyés avec ce téléphone sur des périodes de temps données. +

+
+
+
Annuler @@ -174,6 +192,47 @@ { change_adapter(); }); + + jQuery('body').on('click', '.phone-limits-group-remove', function (e) + { + e.preventDefault(); + jQuery(this).parent('.phone-limits-group').remove(); + return false; + }); + + jQuery('body').on('click', '.add-phone-limit-button', function(e) + { + var random_id = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); + var newLimit = '' + + '
'+ + '
'+ + '
'+ + ''+ + '
'+ + '
'+ + ''+ + '
'+ + ''+ + '
'+ + '
'+ + ''+ + '
'; + + jQuery(this).parent('div').before(newLimit); + }); }); render('incs/head', ['title' => 'Phones - Edit']) +?> +
+render('incs/nav', ['page' => 'phones']) +?> +
+
+ +
+
+

+ Modification téléphones +

+ +
+
+ + +
+
+
+
+

Modification de téléphones

+
+
+
+ +
+
+ +

+ Le nom du téléphone qui enverra et recevra les messages. +

+
+ +
+
+
+ +

+ Lors de l'envoi de SMS sans téléphone spécifié, les téléphones avec la plus haute priorité seront utilisés en premier. +

+
+ +
+
+
+ +

+ Le type de téléphone utilisé par RaspiSMS pour envoyer ou recevoir les SMS. Pour plus d'information, consultez la documentation de RaspiSMS concernant les différents types de téléphones. +

+ +
+
+
+

Description du téléphone

+
+
+ +
+

Réglages du téléphone

+
+
+
+
+ +

+ Défini le nombre maximum de SMS qui pourront être envoyés avec ce téléphone sur des périodes de temps données. +

+
+ +
+
+
+ + +
+
+ +
+ +
+
+ +
+ +
+
+
+
+
+ + Annuler + +
+
+
+
+
+
+
+
+ +render('incs/footer'); diff --git a/templates/phone/list.php b/templates/phone/list.php index c505a45..59a701c 100644 --- a/templates/phone/list.php +++ b/templates/phone/list.php @@ -41,8 +41,10 @@ ID Nom + Priorité Type de téléphone Callbacks + Limites @@ -56,6 +58,7 @@
Action pour la séléction : +
@@ -88,6 +91,7 @@ jQuery(document).ready(function () "columns" : [ {data: 'id', render: jQuery.fn.dataTable.render.text()}, {data: 'name', render: jQuery.fn.dataTable.render.text()}, + {data: 'priority', render: jQuery.fn.dataTable.render.text()}, {data: 'adapter', render: jQuery.fn.dataTable.render.text()}, { data: '_', @@ -123,6 +127,61 @@ jQuery(document).ready(function () return html; }, }, + { + data: 'limits', + render: function (limits) { + if (!limits.length) + { + return 'Pas de limites.'; + } + + var html = ''; + for (limit of limits) + { + switch (limit.startpoint) + { + case "today" : + var startpoint = 'Par jour'; + break; + case "-24 hours" : + var startpoint = '24 heures glissantes'; + break; + case "this week midnight" : + var startpoint = 'Cette semaine'; + break; + case "-7 days" : + var startpoint = '7 jours glissants'; + break; + case "this week midnight -1 week" : + var startpoint = 'Ces deux dernières semaines'; + break; + case "-14 days" : + var startpoint = '14 jours glissants'; + break; + case "this month midnight" : + var startpoint = 'Ce mois'; + break; + case "-1 month" : + var startpoint = '1 mois glissant'; + break; + case "-28 days" : + var startpoint = '28 jours glissants'; + break; + case "-30 days" : + var startpoint = '30 jours glissants'; + break; + case "-31 days" : + var startpoint = '31 jours glissants'; + break; + default : + var startpoint = 'Inconnu' + } + html += '
' + jQuery.fn.dataTable.render.text().display(startpoint) + ' : ' + jQuery.fn.dataTable.render.text().display(limit.volume) + '
'; + } + + return html; + }, + }, { data: 'id', render: function (data, type, row, meta) { diff --git a/templates/setting/show.php b/templates/setting/show.php index 7a6325a..afe8cff 100644 --- a/templates/setting/show.php +++ b/templates/setting/show.php @@ -54,6 +54,25 @@ +
+
+

Alphabet SMS optimisé

+
+
+
+
+ + +
+
+ +
+
+
+

Support des MMS

diff --git a/tests/phpstan/phpstan.phar b/tests/phpstan/phpstan.phar index 19bead9..aee0930 100644 Binary files a/tests/phpstan/phpstan.phar and b/tests/phpstan/phpstan.phar differ