Server IP : 80.87.202.40 / Your IP : 216.73.216.169 Web Server : Apache System : Linux rospirotorg.ru 5.14.0-539.el9.x86_64 #1 SMP PREEMPT_DYNAMIC Thu Dec 5 22:26:13 UTC 2024 x86_64 User : bitrix ( 600) PHP Version : 8.2.27 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : ON Directory : /home/bitrix/ext_www/rospirotorg.ru/bitrix/modules/messageservice/lib/sender/sms/ |
Upload File : |
<?php namespace Bitrix\MessageService\Sender\Sms; use Bitrix\Main\Error; use Bitrix\Main\Result; use Bitrix\Main\Type\Collection; use Bitrix\Main\Web\HttpClient; use Bitrix\Main\Web\Json; use Bitrix\MessageService\DTO; use Bitrix\MessageService\Message; use Bitrix\MessageService\MessageStatus; use Bitrix\MessageService\Sender; class EdnaruImHpx extends Sender\BaseConfigurable { public const ID = 'ednaruimhpx'; protected const MESSAGE_TYPE = "whatsapp"; protected const DEFAULT_PRIORITY = "normal"; protected const CONTENT_TEXT = "text"; private const ENDPOINT_OPTION = 'connector_endpoint'; private const LOGIN_OPTION = 'login'; private const PASSWORD_OPTION = 'password'; private const SUBJECT_OPTION = 'subject_id'; public static function isSupported() { return defined('MESSAGESERIVICE_ALLOW_IMHPX') && MESSAGESERIVICE_ALLOW_IMHPX === true; } public function getId() { return static::ID; } public function getName() { return 'Edna.ru IMHPX'; } public function getShortName() { return 'Edna.ru IMHPX'; } public function isRegistered() { return defined('MESSAGESERIVICE_ALLOW_IMHPX') && MESSAGESERIVICE_ALLOW_IMHPX === true; } public function canUse() { return $this->isRegistered(); } public function getFromList() { $id = $this->getOption(self::SUBJECT_OPTION); return [ [ 'id' => $id, 'name' => $id, ], ]; } public function register(array $fields): Result { $this->setOption(static::ENDPOINT_OPTION, $fields['connector_endpoint']); $this->setOption(static::LOGIN_OPTION, $fields['login']); $this->setOption(static::PASSWORD_OPTION, $fields['password']); $this->setOption(static::SUBJECT_OPTION, $fields['subject_id']); return new Result(); } public function getOwnerInfo() { return [ self::ENDPOINT_OPTION => $this->getOption(self::ENDPOINT_OPTION), self::LOGIN_OPTION => $this->getOption(self::LOGIN_OPTION), self::PASSWORD_OPTION => $this->getOption(self::PASSWORD_OPTION), self::SUBJECT_OPTION => $this->getOption(self::SUBJECT_OPTION), ]; } public function getCallbackUrl(): string { return parent::getCallbackUrl(); } public function getExternalManageUrl() { // TODO: Implement getExternalManageUrl() method. } public function sendMessage(array $messageFields): Sender\Result\SendMessage { $result = new Sender\Result\SendMessage(); if (!$this->canUse()) { return $result->addError(new Error('Service is unavailable')); } $body = $this->makeBodyOutgoingMessage($messageFields); $requestResult = $this->callExternalMethod($body); if (!$requestResult->isSuccess()) { $result->addErrors($requestResult->getErrors()); return $result; } $response = $requestResult->getHttpResponse(); $this->processServiceResponse($response, $result); return $result; } public function getMessageStatus(array $messageFields) { // TODO: Implement getMessageStatus() method. } public static function resolveStatus($serviceStatus): ?int { switch ($serviceStatus) { case 'read': case 'sent': return MessageStatus::SENT; case 'enqueued': return MessageStatus::QUEUED; case 'delayed': return MessageStatus::ACCEPTED; case 'delivered': return MessageStatus::DELIVERED; case 'undelivered': return MessageStatus::UNDELIVERED; case 'failed': case 'cancelled': case 'expired': case 'no-match-template': return MessageStatus::FAILED; default: return mb_strpos($serviceStatus, 'error') === 0 ? MessageStatus::ERROR : MessageStatus::UNKNOWN; } } protected function makeBodyOutgoingMessage(array $messageFields): string { $messageText = $messageFields['MESSAGE_BODY']; $messageText = htmlspecialcharsbx($messageText); $smsBlock = "<sms> <subject>Bitrix24</subject> <priority>{$this->getPriority()}</priority> <content>{$messageText}</content> <sendTimeoutSeconds>60</sendTimeoutSeconds> <validityPeriodMinutes>30</validityPeriodMinutes> </sms>"; $address = static::normalizePhoneNumberForOutgoing($messageFields['MESSAGE_TO']); $ownerInfo = $this->getOwnerInfo(); $login = $ownerInfo[static::LOGIN_OPTION]; $password = $ownerInfo[static::PASSWORD_OPTION]; $mailbox = $ownerInfo[static::SUBJECT_OPTION]; $template = <<<XML <?xml version="1.0" encoding="UTF-8" standalone="no"?> <consumeInstantMessageRequest> <header> <auth> <login>{$login}</login> <password>{$password}</password> </auth> </header> <payload> <instantMessageList> <instantMessage clientId="{$messageFields['ID']}"> <address>{$address}</address> <subject>{$mailbox}</subject> <priority>{$this->getPriority()}</priority> <instantMessageType>{$this->getMessageType()}</instantMessageType> <contentType>text</contentType> <content> <text>{$messageText}</text> </content> $smsBlock </instantMessage> </instantMessageList> </payload> </consumeInstantMessageRequest> XML; return $template; } protected function callExternalMethod(string $body): Sender\Result\HttpRequestResult { $httpClient = new HttpClient([ "socketTimeout" => $this->socketTimeout, "streamTimeout" => $this->streamTimeout, 'waitResponse' => true, ]); $httpClient->setHeader('User-Agent', 'Bitrix24'); $httpClient->setHeader('Content-type', 'text/xml'); $result = new Sender\Result\HttpRequestResult(); $result->setHttpRequest(new DTO\Request([ 'method' => HttpClient::HTTP_POST, 'uri' => $this->getServiceEndpoint(), 'headers' => method_exists($httpClient, 'getRequestHeaders') ? $httpClient->getRequestHeaders()->toArray() : [], 'body' => $body ])); if (!$httpClient->query(HttpClient::HTTP_POST, $this->getServiceEndpoint(), $body)) { $result->setHttpResponse(new DTO\Response([ 'error' => Sender\Util::getHttpClientErrorString($httpClient) ])); $httpError = $httpClient->getError(); $errorCode = array_key_first($httpError); $result->addError(new Error($httpError[$errorCode], $errorCode)); return $result; } $httpResponse = new DTO\Response([ 'statusCode' => $httpClient->getStatus(), 'headers' => $httpClient->getHeaders()->toArray(), 'body' => $httpClient->getResult(), ]); $result->setHttpResponse($httpResponse); return $result; } /** * * Response body example: * <?xml version="1.0" encoding="UTF-8" standalone="no"?> * <consumeInstantMessageResponse> * <header/> * <payload> * <code>ok</code> * <instantMessageList> * <instantMessage clientId="9991144" providerId="87952036"> * <code>ok</code> * </instantMessage> * </instantMessageList> * </payload> * </consumeInstantMessageResponse> * * Where: * - clientId: our message id * - providerId: their message id * - code: status code ("ok" or one of the error codes, like "error-syntax", "error-auth", "error-permission", etc.) * * @param DTO\Response $response Response for our send request. * @param Sender\Result\SendMessage $result Request to be filled. */ public function processServiceResponse(DTO\Response $response, Sender\Result\SendMessage $result): void { if ($response->statusCode !== 200) { $result->addError(new Error("Response status code is {$response->statusCode}", 'WRONG_SERVICE_RESPONSE_CODE')); return; } $parseResult = $this->parseXml($response->body); if (!$parseResult->isSuccess()) { $result->addError( new Error( 'XML parse error: ' . implode('; ', $parseResult->getErrorMessages()), 'XML_PARSE_ERROR' ) ); return; } /** @var \SimpleXMLElement $instantMessageResponse */ $instantMessageResponse = $parseResult->getData()['root']; // hack to convert SimpleXMLElement to array $instantMessageResponse = Json::decode(Json::encode($instantMessageResponse)); // response structure if ( !isset($instantMessageResponse['payload']) || (!isset($instantMessageResponse['payload']['code']) && !isset($instantMessageResponse['payload']['instantMessageList']) ) ) { $result->addError(new Error('Wrong xml response structure', 'SERVICE_RESPONSE_PARSE_ERROR')); return; } if ($instantMessageResponse['payload']['code'] !== 'ok') { $result->setStatus(\Bitrix\MessageService\MessageStatus::ERROR); $result->addError(new Error($instantMessageResponse['payload']['code'], $instantMessageResponse['payload']['code'])); return; } foreach ($instantMessageResponse['payload']['instantMessageList'] as $instantMessage) { if ($instantMessage['code'] === 'ok' && isset($instantMessage['@attributes']['providerId'])) { $result->setExternalId($instantMessage['@attributes']['providerId']); $result->setAccepted(); } else { $result->setStatus(\Bitrix\MessageService\MessageStatus::ERROR); $result->addError(new Error('', $instantMessage['code'])); } // we expect only one message response here return; } $result->addError(new Error('Could not find message status in response', 'SERVICE_RESPONSE_PARSE_ERROR')); } /** * @param string $incomingRequestBody (see example) * <?xml version="1.0" encoding="UTF-8" standalone="no"?> * <provideInstantMessageDlvStatusResponse> * <header/> * <payload> * <code>ok</code> * <instantMessageList> * <instantMessage providerId="6042"> * <code>ok</code> * <instantMessageDlvStatus> * <dlvStatus>read</dlvStatus> * <dlvStatusAt>2021-03-24 10:53:55</dlvStatusAt> * </instantMessageDlvStatus> * </instantMessage> * </instantMessageList> * </payload> * </provideInstantMessageDlvStatusResponse> * * @return DTO\Response */ public function processIncomingRequest(string $incomingRequestBody): DTO\Response { $response = new DTO\Response([ 'statusCode' => 200 ]); $parseResult = $this->parseIncomingRequest($incomingRequestBody); if (!$parseResult->isSuccess()) { $response->statusCode = 400; $response->body = 'Parse error'; return $response; } /** @var DTO\StatusUpdate[] $statusUpdateList */ $statusUpdateList = $parseResult->getData(); foreach ($statusUpdateList as $statusUpdate) { $message = Message::loadByExternalId(static::ID, $statusUpdate->externalId); if ($message && $statusUpdate->providerStatus != '') { $message->updateStatusByExternalStatus($statusUpdate->providerStatus); } } return $response; } /** * @param string $incomingRequestBody (see example) * <?xml version="1.0" encoding="UTF-8" standalone="no"?> * <provideInstantMessageDlvStatusResponse> * <header/> * <payload> * <code>ok</code> * <instantMessageList> * <instantMessage providerId="6042"> * <code>ok</code> * <instantMessageDlvStatus> * <dlvStatus>read</dlvStatus> * <dlvStatusAt>2021-03-24 10:53:55</dlvStatusAt> * </instantMessageDlvStatus> * </instantMessage> * </instantMessageList> * </payload> * </provideInstantMessageDlvStatusResponse> * * @return Result Result with []DTO\StatusUpdate */ public function parseIncomingRequest(string $incomingRequestBody): Result { $result = new Result(); $parseResult = $this->parseXml($incomingRequestBody); if (!$parseResult->isSuccess()) { return $result->addErrors($parseResult->getErrors()); } /** @var \SimpleXMLElement $incomingRequest */ $incomingRequest = $parseResult->getData()['root']; // incoming messages are not supported yet if ($incomingRequest->getName() != 'provideInstantMessageDlvStatusResponse') { return $result; } // hack to convert SimpleXMLElement to array $incomingRequest = Json::decode(Json::encode($incomingRequest)); if ( !isset($incomingRequest['payload']) || (!isset($incomingRequest['payload']['code']) && !isset($incomingRequest['payload']['instantMessageList']) ) ) { return $result->addError(new Error('Wrong XML structure')); } $statusUpdateList = []; // If response contains only one message delivery report - <instantMessage> element will contain the report // If response contains more than on message delivery report - <instantMessage> element will contain array of reports $instantMessageList = $incomingRequest['payload']['instantMessageList']['instantMessage']; if (!is_array($instantMessageList)) { //empty list return $result->setData($statusUpdateList); } if (Collection::isAssociative($instantMessageList)) { $instantMessageList = [$instantMessageList]; } foreach ($instantMessageList as $instantMessage) { $statusUpdateList[] = new DTO\StatusUpdate([ 'externalId' => (int)$instantMessage['@attributes']['providerId'], 'providerStatus' => $instantMessage['instantMessageDlvStatus']['dlvStatus'], 'deliveryStatus' => static::resolveStatus($instantMessage['instantMessageDlvStatus']['dlvStatus']), 'deliveryError' => $instantMessage['instantMessageDlvStatus']['dlvError'] ]); } return $result->setData($statusUpdateList); } /** * @param string $xmlString XML document. * @return Result Returns Result with the following keys (in the case of success) * - root {SimpleXMLElement} Root element of the XML document. */ protected function parseXml(string $xmlString): Result { $result = new Result(); if ($xmlString === '') { return $result->addError(new Error('Empty XML')); } libxml_use_internal_errors(true); libxml_clear_errors(); $parsedBody = simplexml_load_string($xmlString); $parseErrors = libxml_get_errors(); if (!empty($parseErrors)) { /** @var \LibXMLError $parseError */ foreach ($parseErrors as $parseError) { $result->addError(new Error($parseError->message, $parseError->code)); } return $result; } $result->setData([ 'root' => $parsedBody ]); return $result; } protected function getPriority() { return $this::DEFAULT_PRIORITY; } protected function getMessageType() { return $this::MESSAGE_TYPE; } public function getServiceEndpoint(): string { return $this->getOption(static::ENDPOINT_OPTION); } public static function normalizePhoneNumberForOutgoing(string $phoneNumber): string { // remove + if (mb_strpos($phoneNumber, '+') === 0) { return mb_substr($phoneNumber, 1); } return $phoneNumber; } }