17.09.2019

Кастомная служба отправки SMS в Bitrix

Не так давно в Bitrix появилась возможность отправлять SMS из "коробки". Ее работу обеспечивает новый модуль Служба сообщений (messageservice). В списке модулей он описывается так:

Модуль осуществляет отправку текстовых и иных сообщений

Для отправки SMS используется \Bitrix\Main\Sms\Event. Пример

$sms = new \Bitrix\Main\Sms\Event(
	"SMS_USER_CONFIRM_NUMBER",
	[
		"USER_PHONE" => $phoneNumber,
		"CODE" => $code,
	]
);
$smsResult = $sms->send(true);

  • Первый параметр конструктора это Тип события, который создается так же как и почтовые типы событий тут Настройки > Настройки продукта > Почтовые и СМС события > Типы событий
  • Второй параметр конструктора это массив полей, которые будут переданы в шаблон в качестве макроподстановок.

По умолчанию модуль messageservice предоставляет ряд уже готовых Службы отправки SMS: SMS.RU, SMS-ассистент, Twilio.com. Мне понадобилось на одном из проектов сделать отправку SMS через существующий аккаунт SMS.RU. И тут возникли проблемы. После заполнения всех полей формы, на соответсвующей странице настроек модуля, в SMS.RU почему то зарегистрировался новый аккаунт. После повторного ввода данных, не приходили SMS с подтверждением. После копания в недрах модуля messageservice нашел, что для работы с SMS.RU используется не документрированное API, о котором ТП SMS.RU ни чего не может сказать.

В итоге оказалось проще написать собственную реализацию Службы отправки SMS.RU для модуля messageservice. И вот что у меня получилось:

<?php

namespace Gricuk\MessageService;

use Bitrix\Main\Application;
use Bitrix\Main\Error;
use Bitrix\Main\Result;
use Bitrix\Main\Web\HttpClient;
use Bitrix\Main\Web\Json;
use Bitrix\Main\Loader;

use Bitrix\MessageService\Sender\Result\MessageStatus;
use Bitrix\MessageService\Sender\Result\SendMessage;

use Bitrix\MessageService;

class SmsRu extends \Bitrix\MessageService\Sender\Base
{
    const API_ID = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";

    public static function isSupported()
    {
        return true;
    }

    public function getId()
    {
        return 'customsmsru';
    }

    public function getName()
    {
        return "Custom Sms.ru";
    }

    public function getShortName()
    {
        return 'sms.ru';
    }

    public function isDemo()
    {
        return false;
    }

    public function canUse()
    {
        return true;
    }


    public function sendMessage(array $messageFields)
    {
        if (!$this->canUse()) {
            $result = new SendMessage();
            $result->addError(new Error(self::getMessage('MESSAGESERVICE_SENDER_SMS_SMSRU_CAN_USE_ERROR')));
            return $result;
        }

        $params = array(
            'to' => $messageFields['MESSAGE_TO'],
            'msg' => $messageFields['MESSAGE_BODY']
        );

        $result = new SendMessage();
        $apiResult = $this->callExternalMethod('sms/send', $params);
        $resultData = $apiResult->getData();

        if (!$apiResult->isSuccess()) {
            if ((int)$resultData['status_code'] == 206) {
                $result->setStatus(MessageService\MessageStatus::DEFERRED);
                $result->addError(new Error($this->getErrorMessage($resultData['status_code'])));
            } else {
                $result->addErrors($apiResult->getErrors());
            }
        } else {
            $smsData = current($resultData['sms']);

            if (isset($smsData['sms_id'])) {
                $result->setExternalId($smsData['sms_id']);
            }

            if ((int)$smsData['status_code'] !== 100) {
                $result->addError(new Error($this->getErrorMessage($smsData['status_code'])));
            } elseif ((int)$smsData['status_code'] == 206) {
                $result->setStatus(MessageService\MessageStatus::DEFERRED);
                $result->addError(new Error($this->getErrorMessage($smsData['status_code'])));
            } else {
                $result->setAccepted();
            }
        }

        return $result;
    }

    public function getMessageStatus(array $messageFields)
    {
        $result = new MessageStatus();
        $result->setId($messageFields['ID']);
        $result->setExternalId($messageFields['EXTERNAL_ID']);

        if (!$this->canUse()) {
            $result->addError(new Error(self::getMessage('MESSAGESERVICE_SENDER_SMS_SMSRU_CAN_USE_ERROR')));
            return $result;
        }

        $params = array(
            'sms_id' => $result->getExternalId()
        );

        $apiResult = $this->callExternalMethod('sms/status', $params);
        if (!$apiResult->isSuccess()) {
            $result->addErrors($apiResult->getErrors());
        } else {
            $resultData = $apiResult->getData();
            $smsData = current($resultData['sms']);

            $result->setStatusCode($smsData['status_code']);
            $result->setStatusText($smsData['status_text']);

            if ((int)$resultData['status_code'] !== 100) {
                $result->addError(new Error($this->getErrorMessage($smsData['status_code'])));
            }
        }

        return $result;
    }

    public static function resolveStatus($serviceStatus)
    {
        $status = parent::resolveStatus($serviceStatus);

        switch ((int)$serviceStatus) {
            case 100:
                return MessageService\MessageStatus::ACCEPTED;
                break;
            case 101:
                return MessageService\MessageStatus::SENDING;
                break;
            case 102:
                return MessageService\MessageStatus::SENT;
                break;
            case 103:
                return MessageService\MessageStatus::DELIVERED;
                break;
            case 104: //timeout
            case 105: //removed by moderator
            case 106: //error on receiver`s side
            case 107: //unknown reason
            case 108: //rejected
                return MessageService\MessageStatus::UNDELIVERED;
                break;
            case 110:
                return MessageService\MessageStatus::READ;
                break;
        }

        return $status;
    }

    private function callExternalMethod($method, $params = [])
    {
        $url = 'https://sms.ru/' . $method;
        $params["api_id"] = self::API_ID;

        $httpClient = new HttpClient(array(
            "socketTimeout" => 10,
            "streamTimeout" => 30,
            "waitResponse" => true,
        ));
        $httpClient->setHeader('User-Agent', 'Bitrix24');
        $httpClient->setCharset('UTF-8');

        $isUtf = Application::getInstance()->isUtfMode();

        if (!$isUtf) {
            $params = \Bitrix\Main\Text\Encoding::convertEncoding($params, SITE_CHARSET, 'UTF-8');
        }
        $params['json'] = 1;

        $result = new Result();
        $answer = array();

        if ($httpClient->query(HttpClient::HTTP_POST, $url, $params) && $httpClient->getStatus() == '200') {
            $answer = $this->parseExternalAnswer($httpClient->getResult());
        }

        $answerCode = isset($answer['status_code']) ? (int)$answer['status_code'] : 0;

        if ($answerCode !== 100) {
            $result->addError(new Error($this->getErrorMessage($answerCode, $answer)));
        }
        $result->setData($answer);

        return $result;
    }

    private function parseExternalAnswer($httpResult)
    {
        try {
            $answer = Json::decode($httpResult);
        } catch (\Bitrix\Main\ArgumentException $e) {
            $data = explode(PHP_EOL, $httpResult);
            $code = (int)array_shift($data);
            $answer = $data;
            $answer['status_code'] = $code;
            $answer['status'] = $code === 100 ? 'OK' : 'ERROR';
        }

        if (!is_array($answer) && is_numeric($answer)) {
            $answer = array(
                'status' => $answer === 100 ? 'OK' : 'ERROR',
                'status_code' => $answer
            );
        }

        return $answer;
    }

    private function getErrorMessage($errorCode, $answer = null)
    {
        $message = self::getMessage('MESSAGESERVICE_SENDER_SMS_SMSRU_ERROR_' . $errorCode);
        if (!$message && $answer && !empty($answer['errors'])) {
            $errorCode = $answer['errors'][0]['status_code'];
            $message = self::getMessage('MESSAGESERVICE_SENDER_SMS_SMSRU_ERROR_' . $errorCode);
            if (!$message) {
                $message = $answer['errors'][0]['status_text'];
            }
        }

        return $message ?: self::getMessage('MESSAGESERVICE_SENDER_SMS_SMSRU_ERROR_OTHER');
    }


    public static function getMessage($code)
    {
        $MESS = [];
        $MESS["MESSAGESERVICE_SENDER_SMS_SMSRU_NAME"] = "Компания SMS.RU";
        $MESS["MESSAGESERVICE_SENDER_SMS_SMSRU_CAN_USE_ERROR"] = "Провайдер компании SMS.RU не настроен";
        $MESS["MESSAGESERVICE_SENDER_SMS_SMSRU_ERROR_201"] = "Не хватает средств на лицевом счету";
        $MESS["MESSAGESERVICE_SENDER_SMS_SMSRU_ERROR_202"] = "Неправильно указан получатель";
        $MESS["MESSAGESERVICE_SENDER_SMS_SMSRU_ERROR_203"] = "Нет текста сообщения";
        $MESS["MESSAGESERVICE_SENDER_SMS_SMSRU_ERROR_204"] = "Имя отправителя не согласовано с администрацией";
        $MESS["MESSAGESERVICE_SENDER_SMS_SMSRU_ERROR_205"] = "Сообщение слишком длинное (превышает 8 СМС)";
        $MESS["MESSAGESERVICE_SENDER_SMS_SMSRU_ERROR_206"] = "Будет превышен или уже превышен дневной лимит на отправку сообщений";
        $MESS["MESSAGESERVICE_SENDER_SMS_SMSRU_ERROR_207"] = "На этот номер (или один из номеров) нельзя отправлять сообщения, либо указано более 100 номеров в списке получателей";
        $MESS["MESSAGESERVICE_SENDER_SMS_SMSRU_ERROR_209"] = "Вы добавили этот номер (или один из номеров) в стоп-лист";
        $MESS["MESSAGESERVICE_SENDER_SMS_SMSRU_ERROR_230"] = "Превышен общий лимит количества сообщений на этот номер в день";
        $MESS["MESSAGESERVICE_SENDER_SMS_SMSRU_ERROR_231"] = "Превышен лимит одинаковых сообщений на этот номер в минуту";
        $MESS["MESSAGESERVICE_SENDER_SMS_SMSRU_ERROR_232"] = "Превышен лимит одинаковых сообщений на этот номер в день";
        $MESS["MESSAGESERVICE_SENDER_SMS_SMSRU_ERROR_301"] = "Неправильный пароль, либо пользователь не найден";
        $MESS["MESSAGESERVICE_SENDER_SMS_SMSRU_ERROR_302"] = "Аккаунт не подтвержден (пользователь не ввел код, присланный в регистрационной смс)";
        $MESS["MESSAGESERVICE_SENDER_SMS_SMSRU_ERROR_303"] = "Код подтверждения неверен";
        $MESS["MESSAGESERVICE_SENDER_SMS_SMSRU_ERROR_304"] = "Отправлено слишком много кодов подтверждения. Пожалуйста, повторите запрос позднее";
        $MESS["MESSAGESERVICE_SENDER_SMS_SMSRU_ERROR_305"] = "Слишком много неверных вводов кода, повторите попытку позднее";
        $MESS["MESSAGESERVICE_SENDER_SMS_SMSRU_ERROR_703"] = "Опечатка в номере мобильного телефона или номер из неподдерживаемой страны";
        $MESS["MESSAGESERVICE_SENDER_SMS_SMSRU_ERROR_900"] = "Код подтверждения указан неверно";
        $MESS["MESSAGESERVICE_SENDER_SMS_SMSRU_ERROR_901"] = "Неверно указан номер телефона, ошибка в формате";
        $MESS["MESSAGESERVICE_SENDER_SMS_SMSRU_ERROR_903"] = "В данный момент регистрация номера с таким кодом недоступна";
        $MESS["MESSAGESERVICE_SENDER_SMS_SMSRU_ERROR_905"] = "Пользователь не указал имя (или оно меньше 2х символов)";
        $MESS["MESSAGESERVICE_SENDER_SMS_SMSRU_ERROR_906"] = "Пользователь не указал фамилию (или оно меньше 2х символов)";
        $MESS["MESSAGESERVICE_SENDER_SMS_SMSRU_ERROR_OTHER"] = "Системная ошибка. Необходимо повторить запрос еще раз";


        return $MESS[$code];
    }


    public static function onGetSmsSenders()
    {
        $class = __CLASS__;
        return [new $class()];
    }

    public function getFromList()
    {
        $result = $this->callExternalMethod('my/senders');

        if ($result->isSuccess()) {
            $from = array();
            $resultData = $result->getData();
            foreach ($resultData['senders'] as $sender) {
                if (!empty($sender)) {
                    $from[] = array(
                        'id' => $sender,
                        'name' => $sender
                    );
                }
            }

            return $from;
        }
        return [];
    }
}
?>

А вот так выполняется подключение данной Службы отправки SMS

<?php
	
$eventManager = \Bitrix\Main\EventManager::getInstance();
$eventManager->addEventHandler(
    "messageservice",
    "onGetSmsSenders",
    array(
        Gricuk\MessageService\SmsRu::class,
        "onGetSmsSenders",
    )
);
?>

Как видно для кастомной службы отправки SMS.RU необходимо написать класс, наследованный от \Bitrix\MessageService\Sender\Base. Так же существует класс \Bitrix\MessageService\Sender\BaseConfigurable. Он является наследником \Bitrix\MessageService\Sender\Base и отличается от него тем, что позволяет вывести в настройки модуля messageservice форму с параметрами службы.