This commit is contained in:
root
2025-11-13 19:04:05 +03:00
commit 240d0aba5f
75129 changed files with 11118122 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
<?php
namespace Bitrix\MessageService\Providers\Base;
use Bitrix\MessageService\Providers;
use Bitrix\MessageService\Providers\OptionManager;
class DemoManager implements Providers\DemoManager
{
protected OptionManager $optionManager;
public function __construct(OptionManager $optionManager)
{
$this->optionManager = $optionManager;
}
public function isDemo(): bool
{
return ($this->optionManager->getOption(self::IS_DEMO) === true);
}
public function disableDemo(): DemoManager
{
$this->optionManager->setOption(self::IS_DEMO, false);
return $this;
}
public function enableDemo(): DemoManager
{
$this->optionManager->setOption(self::IS_DEMO, true);
return $this;
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Bitrix\MessageService\Providers\Base;
use Bitrix\MessageService\MessageType;
use Bitrix\MessageService\Providers;
abstract class Informant implements Providers\Informant
{
public function getType(): string
{
return MessageType::SMS;
}
public function getExternalId(): string
{
return $this->getType() . ':' . $this->getId();
}
public function getManageUrl(): string
{
if (defined('ADMIN_SECTION') && ADMIN_SECTION === true)
{
return 'messageservice_sender_sms.php?sender_id='.$this->getId();
}
return $this->isConfigurable() ? '/crm/configs/sms/?sender=' . $this->getId() : '';
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Bitrix\MessageService\Providers\Base;
abstract class Initiator implements \Bitrix\MessageService\Providers\Initiator
{
public function getDefaultFrom(): ?string
{
$fromList = $this->getFromList();
$from = isset($fromList[0]) ? $fromList[0]['id'] : null;
//Try to find alphanumeric from
foreach ($fromList as $item)
{
if (!preg_match('#^[0-9]+$#', $item['id']))
{
$from = $item['id'];
break;
}
}
return $from;
}
public function getFirstFromList()
{
$fromList = $this->getFromList();
if (!is_array($fromList))
{
return null;
}
foreach ($fromList as $item)
{
if (isset($item['id']) && $item['id'])
{
return $item['id'];
}
}
return null;
}
public function isCorrectFrom($from): bool
{
$fromList = $this->getFromList();
foreach ($fromList as $item)
{
if ($from === $item['id'])
{
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,139 @@
<?php
namespace Bitrix\MessageService\Providers\Base;
use Bitrix\Main;
use Bitrix\Main\Security\Cipher;
use Bitrix\MessageService\Providers\Encryptor;
use Bitrix\MessageService\Providers\OptionManager;
class Option implements OptionManager
{
use Encryptor;
protected ?array $options;
protected string $providerType;
protected string $providerId;
protected string $dbOptionName;
protected int $socketTimeout = 10;
protected int $streamTimeout = 30;
public function __construct(string $providerType, string $providerId)
{
$this->options = null;
$this->providerType = mb_strtolower($providerType);
$this->providerId = $providerId;
$this->dbOptionName = 'sender.' . $this->providerType . '.' . $this->providerId;
}
/**
* @return int
*/
public function getSocketTimeout(): int
{
return $this->socketTimeout;
}
/**
* @param int $socketTimeout
* @return Option
*/
public function setSocketTimeout(int $socketTimeout): OptionManager
{
$this->socketTimeout = $socketTimeout;
return $this;
}
/**
* @return int
*/
public function getStreamTimeout(): int
{
return $this->streamTimeout;
}
/**
* @param int $streamTimeout
* @return Option
*/
public function setStreamTimeout(int $streamTimeout): OptionManager
{
$this->streamTimeout = $streamTimeout;
return $this;
}
public function setOptions(array $options): OptionManager
{
$this->options = $options;
$data = serialize($options);
$encryptedData = [
'crypto' => 'Y',
'data' => self::encrypt($data, $this->providerType . '-' . $this->providerId)
];
Main\Config\Option::set('messageservice', $this->dbOptionName, serialize($encryptedData));
return $this;
}
public function setOption(string $optionName, $optionValue): OptionManager
{
$options = $this->getOptions();
if (!isset($options[$optionName]) || $options[$optionName] !== $optionValue)
{
$options[$optionName] = $optionValue;
$this->setOptions($options);
}
return $this;
}
public function getOptions(): array
{
$this->options ??= $this->loadOptions();
return $this->options;
}
public function getOption(string $optionName, $defaultValue = null)
{
$this->getOptions();
return $this->options[$optionName] ?? $defaultValue;
}
public function clearOptions(): OptionManager
{
$this->options = [];
Main\Config\Option::delete('messageservice', array('name' => $this->dbOptionName));
return $this;
}
protected function loadOptions(): array
{
$data = Main\Config\Option::get('messageservice', $this->dbOptionName);
$data = unserialize($data, ['allowed_classes' => false]);
if (!isset($data['crypto']) && !isset($data['data']))
{
return is_array($data) ? $data : [];
}
$decryptedData = self::decrypt($data['data'], $this->providerType . '-' . $this->providerId);
$options = unserialize($decryptedData, ['allowed_classes' => false]);
return is_array($options) ? $options : [];
}
public function getProviderId(): string
{
return $this->providerId;
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace Bitrix\MessageService\Providers\Base;
use Bitrix\Main\Context;
use Bitrix\Main\Result;
use Bitrix\MessageService\Providers;
abstract class Registrar implements Providers\Registrar
{
protected Providers\OptionManager $optionManager;
protected string $providerId;
public function __construct(string $providerId, Providers\OptionManager $optionManager)
{
$this->providerId = $providerId;
$this->optionManager = $optionManager;
}
public function isConfirmed(): bool
{
return $this->isRegistered();
}
public function confirmRegistration(array $fields): Result
{
return new Result();
}
public function sendConfirmationCode(): Result
{
return new Result();
}
public function sync(): Registrar
{
return $this;
}
public function getCallbackUrl(): string
{
return $this->getHostUrl() . '/bitrix/tools/messageservice/callback_' . $this->providerId . '.php';
}
public function getHostUrl(): string
{
$protocol = (Context::getCurrent()->getRequest()->isHttps() ? 'https' : 'http');
if (defined("SITE_SERVER_NAME") && SITE_SERVER_NAME)
{
$host = SITE_SERVER_NAME;
}
else
{
$host =
\Bitrix\Main\Config\Option::get('main', 'server_name', Context::getCurrent()->getServer()->getHttpHost())
?: Context::getCurrent()->getServer()->getHttpHost()
;
}
$port = Context::getCurrent()->getServer()->getServerPort();
if($port != 80 && $port != 443 && $port > 0 && mb_strpos($host, ':') === false)
{
$host .= ':'.$port;
}
elseif($protocol === 'http' && $port == 80)
{
$host = str_replace(':80', '', $host);
}
elseif($protocol === 'https' && $port == 443)
{
$host = str_replace(':443', '', $host);
}
return $protocol . '://' . $host;
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Bitrix\MessageService\Providers\Base;
use Bitrix\Main\Text\Emoji;
abstract class Sender implements \Bitrix\MessageService\Providers\Sender
{
public function prepareMessageBodyForSave(string $text): string
{
return Emoji::encode($text);
}
public function prepareMessageBodyForSend(string $text): string
{
return Emoji::decode($text);
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Bitrix\MessageService\Providers\Base;
class TemplateManager implements \Bitrix\MessageService\Providers\TemplateManager
{
protected string $providerId;
public function __construct(string $providerId)
{
$this->providerId = $providerId;
}
public function getTemplatesList(array $context = null): array
{
return [];
}
public function prepareTemplate($templateData): array
{
return $templateData;
}
public function isTemplatesBased(): bool
{
return false;
}
public function getConfigComponentTemplatePageName(): string
{
return $this->providerId;
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace Bitrix\MessageService\Providers;
use Bitrix\Main\Data\Cache;
class CacheManager
{
public const CHANNEL_CACHE_ENTITY_ID = 'channel';
private const BASE_CACHE_DIR = '/messageservice/';
private const CACHE_TTL = 86400; //one hour
private Cache $cache;
private string $providerId;
/**
* @param string $providerId
*/
public function __construct(string $providerId)
{
$this->cache = Cache::createInstance();
$this->providerId = $providerId;
}
/**
* @param string $entityId
* @return int
*/
public function getTtl(string $entityId): int
{
return self::CACHE_TTL;
}
/**
* @param string $entityId
*
* @return array
*/
public function getValue(string $entityId): array
{
$result = [];
if ($this->cache->initCache($this->getTtl($entityId), $entityId, $this->getCacheDir()))
{
$result = $this->cache->getVars();
}
return $result;
}
private function getCacheDir(): string
{
return self::BASE_CACHE_DIR . $this->providerId;
}
public function setValue(string $entityId, array $value): CacheManager
{
$cacheName = $entityId;
$this->cache->clean($cacheName, $this->getCacheDir());
$this->cache->initCache($this->getTtl($entityId), $cacheName, $this->getCacheDir());
$this->cache->startDataCache();
$this->cache->endDataCache($value);
return $this;
}
public function deleteValue(string $entityId): CacheManager
{
$cacheName = $entityId;
$this->cache->clean($cacheName, $this->getCacheDir());
return $this;
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace Bitrix\MessageService\Providers\Constants;
class InternalOption
{
public const SENDER_ID = 'sender_id';
public const API_KEY = 'api_key';
public const EMOJI_DECODE = 'decode';
public const EMOJI_ENCODE = 'encode';
//endregion
public const NEW_API_AVAILABLE = 'is_migrated_to_new_api';
public const MIGRATED_TO_STANDART_SETTING_NAMES = 'is_migrated_to_standart_settings';
}

View File

@@ -0,0 +1,12 @@
<?php
namespace Bitrix\MessageService\Providers;
interface DemoManager
{
public const IS_DEMO = 'is_demo';
public function isDemo(): bool;
public function disableDemo(): self;
public function enableDemo(): self;
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Bitrix\MessageService\Providers\Edna\Constants;
class CallbackType
{
public const MESSAGE_STATUS = 'statusCallbackUrl';
public const INCOMING_MESSAGE = 'inMessageCallbackUrl';
public const TEMPLATE_REGISTER_STATUS = 'messageMatcherCallbackUrl';
public static function getAllTypeList(): array
{
return [
self::MESSAGE_STATUS,
self::INCOMING_MESSAGE,
self::TEMPLATE_REGISTER_STATUS
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Bitrix\MessageService\Providers\Edna\Constants;
class ChannelType
{
public const WHATSAPP = 'WHATSAPP';
public const SMS = 'SMS';
public const VIBER = 'VIBER';
public static function getAllTypeList(): array
{
return [
self::WHATSAPP,
self::SMS,
self::VIBER,
];
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Bitrix\MessageService\Providers\Edna\Constants;
/**
* Class
* @see https://docs.edna.ru/kb/api-sending-messages/
*/
class ContentType
{
public const TEXT = 'TEXT';
public const IMAGE = 'IMAGE';
public const DOCUMENT = 'DOCUMENT';
public const VIDEO = 'VIDEO';
public const AUDIO = 'AUDIO';
public const BUTTON = 'BUTTON';
public const LOCATION = 'LOCATION';
public const LIST_PICKER = 'LIST_PICKER';
public static function getAllTypeList(): array
{
return [
self::TEXT,
self::IMAGE,
self::DOCUMENT,
self::VIDEO,
self::AUDIO,
self::BUTTON,
self::LOCATION,
self::LIST_PICKER,
];
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace Bitrix\MessageService\Providers\Edna\Constants;
class Method
{
public const GET_CHANNELS = 'channel-profile';
public const GET_CASCADES = 'cascade/get-all/';
public const SEND_MESSAGE = 'cascade/schedule';
public const GET_TEMPLATES = 'message-matchers/get-by-request';
public const SEND_TEMPLATE = 'message-matchers';
}

View File

@@ -0,0 +1,15 @@
<?php
namespace Bitrix\MessageService\Providers\Edna\Constants;
/** @see https://docs.edna.ru/kb/message-matchers-get-by-request/ */
final class TemplateStatus
{
public const APPROVED = 'APPROVED';
public const REJECTED = 'REJECTED';
public const PENDING = 'PENDING';
public const NOT_SENT = 'NOT_SENT';
public const ARCHIVED = 'ARCHIVED';
public const PAUSED = 'PAUSED';
public const DISABLED = 'DISABLED';
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Bitrix\MessageService\Providers\Edna;
use Bitrix\Main\Result;
interface EdnaRu
{
public function getMessageTemplates(string $subject = ''): Result;
public function getSentTemplateMessage(string $from, string $to): string;
public function getChannelList(string $imType): Result;
public function getCascadeList(): Result;
public function setCallback(string $callbackUrl, array $callbackTypes, ?int $subjectId = null): Result;
public function checkActiveChannelBySubjectIdList(array $subjectIdList, string $imType): bool;
public function getActiveChannelList(string $imType): Result;
}

View File

@@ -0,0 +1,310 @@
<?php
namespace Bitrix\MessageService\Providers\Edna;
use Bitrix\Main\Error;
use Bitrix\Main\Result;
use Bitrix\MessageService\Providers;
use Bitrix\MessageService\Providers\Constants\InternalOption;
use Bitrix\MessageService\Providers\Edna\Constants;
use Bitrix\MessageService\Internal\Entity\ChannelTable;
abstract class EdnaUtils implements EdnaRu
{
protected string $providerId;
protected Providers\ExternalSender $externalSender;
protected Providers\OptionManager $optionManager;
abstract public function getMessageTemplates(string $subject = ''): Result;
abstract protected function initializeDefaultExternalSender(): Providers\ExternalSender;
public function __construct(string $providerId, Providers\OptionManager $optionManager)
{
$this->providerId = $providerId;
$this->optionManager = $optionManager;
$this->externalSender = $this->initializeDefaultExternalSender();
}
/**
* @see https://docs.edna.ru/kb/channel-profile/
* @param string $imType
* @see \Bitrix\MessageService\Providers\Edna\Constants\ChannelType
* @return Result
*/
public function getChannelList(string $imType): Result
{
if (!in_array($imType, Constants\ChannelType::getAllTypeList(), true))
{
return (new Result())->addError(new Error('Incorrect imType'));
}
$channelResult = $this->gelAllChannelList();
if (!$channelResult->isSuccess())
{
return (new Result())->addError(new Error('Edna service error'));
}
$channelList = [];
foreach ($channelResult->getData() as $channel)
{
if (is_array($channel) && isset($channel['type']) && $channel['type'] === $imType)
{
$channelList[] = $channel;
}
}
if (empty($channelList))
{
return (new Result())->addError(new Error("There are no $imType channels in your profile"));
}
$result = new Result();
$result->setData($channelList);
return $result;
}
/**
* @see https://docs.edna.ru/kb/poluchenie-informacii-o-kaskadah/
* @return Result
*/
public function getCascadeList(): Result
{
$apiResult = $this->externalSender->callExternalMethod(Constants\Method::GET_CASCADES, [
'offset' => 0,
'limit' => 0
]);
return $apiResult;
}
/**
* @see https://docs.edna.ru/kb/callback-set/
* @param string $callbackUrl
* @param array $callbackTypes
* @param int|null $subjectId
* @return Result
*/
public function setCallback(string $callbackUrl, array $callbackTypes, ?int $subjectId = null): Result
{
$typeList = Constants\CallbackType::getAllTypeList();
$requestParams = [];
foreach ($callbackTypes as $callbackType)
{
if (in_array($callbackType, $typeList, true))
{
$requestParams[$callbackType] = $callbackUrl;
}
}
if (empty($requestParams))
{
return (new Result())->addError(new Error('Invalid callback types passed'));
}
if ($subjectId)
{
$requestParams['subjectId'] = $subjectId;
}
$this->externalSender->setApiKey($this->optionManager->getOption(InternalOption::API_KEY));
return $this->externalSender->callExternalMethod('callback/set', $requestParams);
}
public function getActiveChannelList(string $imType): Result
{
$channelListResult = $this->getChannelList($imType);
if (!$channelListResult->isSuccess())
{
return $channelListResult;
}
$activeChannelList = [];
foreach ($channelListResult->getData() as $channel)
{
if (isset($channel['active'], $channel['subjectId']) && $channel['active'] === true)
{
$activeChannelList[] = $channel;
}
}
if (empty($activeChannelList))
{
return (new Result())->addError(new Error('There are no active channels'));
}
return (new Result())->setData($activeChannelList);
}
public function checkActiveChannelBySubjectIdList(array $subjectIdList, string $imType): bool
{
if (empty($subjectIdList))
{
return false;
}
$channelResult = $this->getChannelList($imType);
if (!$channelResult->isSuccess())
{
return false;
}
$checkedChannels = [];
foreach ($channelResult->getData() as $channel)
{
if (
isset($channel['active'], $channel['subjectId'])
&& $channel['active'] === true
&& in_array($channel['subjectId'], $subjectIdList, true)
)
{
$checkedChannels[] = $channel['subjectId'];
}
}
return count($checkedChannels) === count($subjectIdList);
}
/**
* @param int|string $subject
* @param callable $subjectComparator
* @return Result
*/
public function getCascadeIdFromSubject($subject, callable $subjectComparator): Result
{
$apiResult = $this->getCascadeList();
if (!$apiResult->isSuccess())
{
return $apiResult;
}
$apiData = $apiResult->getData();
$result = new Result();
foreach ($apiData as $cascade)
{
if (is_array($cascade))
{
if ($cascade['status'] !== 'ACTIVE' || $cascade['stagesCount'] > 1)
{
continue;
}
if ($subjectComparator($cascade['stages'][0]['subject'], $subject))
{
$result->setData(['cascadeId' => $cascade['id']]);
return $result;
}
}
}
$result->addError(new Error('Not cascade'));
return $result;
}
private function gelAllChannelList(): Result
{
$this->externalSender->setApiKey($this->optionManager->getOption(InternalOption::API_KEY));
return $this->externalSender->callExternalMethod(Constants\Method::GET_CHANNELS);
}
/**
* Loads channels from provider.
*
* @param string $channelType
* @return array
*/
public function updateSavedChannelList(string $channelType): array
{
$fromList = [];
$activeChannelListResult = $this->getActiveChannelList($channelType);
if ($activeChannelListResult->isSuccess())
{
$registeredSubjectIdList = $this->optionManager->getOption(Providers\Constants\InternalOption::SENDER_ID, []);
$channels = [];
foreach ($activeChannelListResult->getData() as $channel)
{
if (in_array((int)$channel['subjectId'], $registeredSubjectIdList, true))
{
$fromList[] = [
'id' => $channel['subjectId'],
'name' => $channel['name'],
'channelPhone' => $channel['channelAttribute'] ?? '',
];
$channels[] = [
'SENDER_ID' => $this->providerId,
'EXTERNAL_ID' => $channel['subjectId'],
'TYPE' => $channelType,
'NAME' => $channel['name'] ?? '',
'ADDITIONAL_PARAMS' => [
'channelAttribute' => $channel['channelAttribute'] ?? ''
],
];
}
}
if (count($channels) > 0)
{
ChannelTable::reloadChannels($this->providerId, $channelType, $channels);
}
else
{
ChannelTable::deleteByFilter([
'=SENDER_ID' => $this->providerId,
'=TYPE' => $channelType,
]);
}
}
return $fromList;
}
public function sendTemplate(string $name, string $text, array $examples = [], ?string $langCode = null): Result
{
return (new Result())->addError(new Error('This provider does not support template creation'));
}
protected function validateLanguage(string $langCode): bool
{
$langs = [
'af', 'sq', 'ar', 'az', 'bn',
'bg', 'ca','zh_CN', 'zh_HK', 'zh_TW',
'hr', 'cs', 'da', 'nl', 'en',
'en_GB', 'en_US', 'et', 'fil', 'fi',
'fr', 'ka', 'de', 'el', 'gu',
'ha', 'he', 'hi', 'hu', 'id',
'ga', 'it', 'ja', 'kn', 'kk',
'rw_RW', 'ko', 'ky_KG', 'lo', 'lv',
'lt', 'mk', 'ms', 'ml', 'mr',
'nb', 'fa', 'pl', 'pt_BR', 'pt_PT',
'pa', 'ro', 'ru', 'sr', 'sk',
'sl', 'es', 'es_AR', 'es_ES', 'es_MX',
'sw', 'sv', 'ta', 'te', 'th',
'tr', 'uk', 'ur', 'uz', 'vi', 'zu',
];
if (in_array($langCode, $langs, true))
{
return true;
}
return false;
}
protected function validateTemplateName(string $name): Result
{
$result = new Result();
if (!preg_match('/^[0-9a-z_]{1,60}$/i', $name))
{
return $result->addError(new Error('The template name can only contain Latin letters, numbers and underscore (_). The maximum number of characters is 60'));
}
return $result;
}
public function clearCache(string $key): void
{
$cacheManager = new Providers\CacheManager($this->providerId);
$cacheManager->deleteValue($key);
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Bitrix\MessageService\Providers\Edna;
use Bitrix\MessageService\Sender\Result\HttpRequestResult;
use Bitrix\MessageService\Providers\ExternalSender as IExternalSender;
abstract class ExternalSender implements IExternalSender
{
protected const USER_AGENT = 'Bitrix24';
protected const CONTENT_TYPE = 'application/json';
protected const CHARSET = 'UTF-8';
protected const WAIT_RESPONSE = true;
protected string $apiKey;
protected string $apiEndpoint;
protected int $socketTimeout;
protected int $streamTimeout;
public function setApiKey(string $apiKey) : self
{
$this->apiKey = $apiKey;
return $this;
}
}

View File

@@ -0,0 +1,95 @@
<?php
namespace Bitrix\MessageService\Providers\Edna;
use Bitrix\MessageService\Providers;
use Bitrix\MessageService\Internal\Entity\ChannelTable;
class Initiator extends Providers\Base\Initiator
{
protected Providers\OptionManager $optionManager;
protected Providers\SupportChecker $supportChecker;
protected EdnaRu $utils;
protected Providers\CacheManager $cacheManager;
protected string $providerId;
protected string $channelType = '';
public function __construct(
Providers\OptionManager $optionManager,
Providers\SupportChecker $supportChecker,
EdnaRu $utils,
string $providerId
)
{
$this->optionManager = $optionManager;
$this->supportChecker = $supportChecker;
$this->utils = $utils;
$this->providerId = $providerId;
$this->cacheManager = new Providers\CacheManager($this->providerId);
}
/**
* @return string
*/
public function getChannelType(): string
{
return $this->channelType;
}
/**
* @return array<array{id: int, name: string, channelPhone: string}>
*/
public function getFromList(): array
{
if (!$this->supportChecker->canUse())
{
return [];
}
// load from cache
$cachedChannels = $this->cacheManager->getValue(Providers\CacheManager::CHANNEL_CACHE_ENTITY_ID);
if (!empty($cachedChannels))
{
return $cachedChannels;
}
$fromList = [];
// load from db
$res = ChannelTable::getChannelsByType($this->providerId, $this->getChannelType());
while ($channel = $res->fetch())
{
$fromList[] = [
'id' => (int)$channel['EXTERNAL_ID'],
'name' => $channel['NAME'],
'channelPhone' => $channel['ADDITIONAL_PARAMS']['channelAttribute'] ?? '',
];
}
if (empty($fromList))
{
// get channels from provider
//$fromList = $this->utils->updateSavedChannelList($this->getChannelType());
\Bitrix\Main\Application::getInstance()->addBackgroundJob([$this->utils, 'updateSavedChannelList'], [$this->getChannelType()]);
}
// update cache
$this->cacheManager->setValue(Providers\CacheManager::CHANNEL_CACHE_ENTITY_ID, $fromList);
return $fromList;
}
public function isCorrectFrom($from): bool
{
$fromList = $this->getFromList();
foreach ($fromList as $item)
{
if ((int)$from === $item['id'])
{
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Bitrix\MessageService\Providers\Edna;
abstract class RegionHelper
{
public const REGION_RU = ['ru'];
public const REGION_OPTION_FORCE = 'force_region';
public const REGION_PHRASE_POSTFIX = '_IO';
public static function isInternational(): bool
{
$region = \Bitrix\Main\Config\Option::get('messageservice', self::REGION_OPTION_FORCE, false);
if (!$region)
{
$region = \Bitrix\Main\Application::getInstance()->getLicense()->getRegion();
\Bitrix\Main\Config\Option::set('messageservice', self::REGION_OPTION_FORCE, $region);
}
return !in_array($region, self::REGION_RU, true);
}
public static function getPhrase(string $phraseCode): string
{
return self::isInternational() ? $phraseCode . self::REGION_PHRASE_POSTFIX : $phraseCode;
}
abstract static function getApiEndPoint();
}

View File

@@ -0,0 +1,174 @@
<?php
namespace Bitrix\MessageService\Providers\Edna;
use Bitrix\Main\Error;
use Bitrix\Main\Localization\Loc;
use Bitrix\Main\Result;
use Bitrix\MessageService\Providers\Constants\InternalOption;
use Bitrix\MessageService\Providers\Edna\EdnaRu;
use Bitrix\MessageService\Providers;
use Bitrix\MessageService\Providers\Edna;
use Bitrix\MessageService\Sender\Traits\RussianProvider;
abstract class Registrar extends Providers\Base\Registrar implements Providers\SupportChecker
{
use RussianProvider { RussianProvider::isSupported as isRussianRegion; }
protected EdnaRu $utils;
protected string $channelType = '';
abstract protected function getCallbackTypeList(): array;
public function __construct(string $providerId, Providers\OptionManager $optionManager, EdnaRu $utils)
{
parent::__construct($providerId, $optionManager);
$this->utils = $utils;
}
public function isRegistered(): bool
{
return
!is_null($this->optionManager->getOption(InternalOption::API_KEY))
&& !is_null($this->optionManager->getOption(InternalOption::SENDER_ID));
}
public function register(array $fields): Result
{
if (isset($fields['subject_id']))
{
$fields[InternalOption::SENDER_ID] = $fields['subject_id'];
}
if (!isset($fields[InternalOption::API_KEY], $fields[InternalOption::SENDER_ID]))
{
return (new Result())->addError(new Error(Loc::getMessage('MESSAGESERVICE_SENDER_SMS_EDNARU_EMPTY_REQUIRED_FIELDS')));
}
$this->optionManager->setOption(InternalOption::API_KEY, (string)$fields[InternalOption::API_KEY]);
$subjectIdList = [];
foreach (explode(';', (string)$fields[InternalOption::SENDER_ID]) as $senderId)
{
$senderId = trim($senderId);
if ($senderId !== '')
{
$subjectIdList[] = (int)$senderId;
}
}
if (!$this->utils->checkActiveChannelBySubjectIdList($subjectIdList, $this->channelType))
{
$this->optionManager->clearOptions();
return (new Result())->addError(new Error(Loc::getMessage('MESSAGESERVICE_EDNARU_INACTIVE_CHANNEL_ERROR')));
}
foreach ($subjectIdList as $subjectId)
{
$setCallbackResult = $this->utils->setCallback(
$this->getCallbackUrl(),
$this->getCallbackTypeList(),
$subjectId
);
if (!$setCallbackResult->isSuccess())
{
$this->optionManager->clearOptions();
$errorData = $setCallbackResult->getData();
if (isset($errorData['detail']))
{
return (new Result())->addError(new Error($errorData['detail']));
}
return $setCallbackResult;
}
}
$this->optionManager->setOption(InternalOption::SENDER_ID, $subjectIdList);
$this->optionManager->setOption(InternalOption::MIGRATED_TO_STANDART_SETTING_NAMES, 'Y');
return new Result();
}
/**
* @return array{api_key: string, sender_id: array}
*/
public function getOwnerInfo(): array
{
return [
InternalOption::API_KEY => $this->optionManager->getOption(InternalOption::API_KEY),
InternalOption::SENDER_ID => $this->optionManager->getOption(InternalOption::SENDER_ID),
];
}
public function isSupported(): bool
{
return self::isRussianRegion();
}
public function canUse(): bool
{
return ($this->isRegistered() && $this->isConfirmed());
}
public function getExternalManageUrl(): string
{
if (RegionHelper::isInternational())
{
return 'https://app.edna.io/';
}
return 'https://app.edna.ru/';
}
public function resetCallback(): Result
{
\Bitrix\MessageService\Providers\Edna\WhatsApp\Utils::cleanTemplatesCache();
if (!$this->isRegistered())
{
return (new Result())->addError(new Error('It is impossible to reinstall the callback url because the provider is not registered'));
}
$fields = $this->getOwnerInfo();
$subjectIdList = [];
foreach (explode(';', (string)$fields[InternalOption::SENDER_ID][0]) as $senderId)
{
$senderId = trim($senderId);
if ($senderId !== '')
{
$subjectIdList[] = (int)$senderId;
}
}
if (!$this->utils->checkActiveChannelBySubjectIdList($subjectIdList, $this->channelType))
{
return (new Result())->addError(new Error(Loc::getMessage('MESSAGESERVICE_EDNARU_INACTIVE_CHANNEL_ERROR')));
}
foreach ($subjectIdList as $subjectId)
{
$setCallbackResult = $this->utils->setCallback(
$this->getCallbackUrl(),
$this->getCallbackTypeList(),
$subjectId
);
if (!$setCallbackResult->isSuccess())
{
$errorData = $setCallbackResult->getData();
if (isset($errorData['detail']))
{
return (new Result())->addError(new Error($errorData['detail']));
}
return $setCallbackResult;
}
}
return new Result();
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Bitrix\MessageService\Providers\Edna\SMS;
use Bitrix\MessageService\Providers\Constants\InternalOption;
class Constants extends InternalOption
{
public const ID = 'smsednaru';
public const API_ENDPOINT = 'https://app.edna.ru/api/';
public const API_ENDPOINT_IO = 'https://app.edna.io/api/';
public const API_KEY = 'apiKey';
}

View File

@@ -0,0 +1,124 @@
<?php
namespace Bitrix\MessageService\Providers\Edna\SMS;
use Bitrix\Main\ArgumentException;
use Bitrix\Main\Error;
use Bitrix\Main\Localization\Loc;
use Bitrix\Main\Text\StringHelper;
use Bitrix\Main\Web\HttpClient;
use Bitrix\Main\Web\Json;
use Bitrix\MessageService\DTO;
use Bitrix\MessageService\Sender\Result\HttpRequestResult;
use Bitrix\MessageService\Sender\Util;
class ExternalSender extends \Bitrix\MessageService\Providers\Edna\ExternalSender
{
protected string $apiKey;
protected string $apiEndpoint;
public function __construct(?string $apiKey, string $apiEndpoint, int $socketTimeout = 10, int $streamTimeout = 30)
{
$this->apiKey = $apiKey ?? '';
$this->apiEndpoint = $apiEndpoint;
$this->socketTimeout = $socketTimeout;
$this->streamTimeout = $streamTimeout;
}
public function callExternalMethod(string $method, ?array $requestParams = null, string $httpMethod = ''): HttpRequestResult
{
if ($this->apiKey === '')
{
$result = new HttpRequestResult();
$result->addError(new Error('Missing API key when requesting a service.'));
return $result;
}
$url = $this->apiEndpoint . $method;
$queryMethod = HttpClient::HTTP_GET;
$httpClient = new HttpClient([
'socketTimeout' => $this->socketTimeout,
'streamTimeout' => $this->streamTimeout,
'waitResponse' => true,
'version' => HttpClient::HTTP_1_1,
]);
$httpClient->setHeader('User-Agent', static::USER_AGENT);
$httpClient->setHeader('Content-type', static::CONTENT_TYPE);
$httpClient->setHeader('X-API-KEY', $this->apiKey);
$httpClient->setCharset(static::CHARSET);
if (is_array($requestParams))
{
$queryMethod = HttpClient::HTTP_POST;
$requestParams = Json::encode($requestParams);
}
$result = new HttpRequestResult();
$result->setHttpRequest(new DTO\Request([
'method' => $queryMethod,
'uri' => $url,
'headers' => method_exists($httpClient, 'getRequestHeaders') ? $httpClient->getRequestHeaders()->toArray() : [],
'body' => $requestParams
]));
$answer = [];
$errorInfo = [];
if ($httpClient->query($queryMethod, $url, $requestParams))
{
$answer = $this->parseExternalAnswer($httpClient->getResult());
if ($httpClient->getStatus() !== 200)
{
$errorInfo = [
'code' => $httpClient->getStatus(),
'error' => $this->getMessageByErrorCode('error-' . $httpClient->getStatus()),
];
}
}
else
{
$error = $httpClient->getError();
$errorInfo = [
'code' => key($error),
'error' => current($error),
];
}
$result->setHttpResponse(new DTO\Response([
'statusCode' => $httpClient->getStatus(),
'headers' => $httpClient->getHeaders()->toArray(),
'body' => $httpClient->getResult(),
'error' => Util::getHttpClientErrorString($httpClient)
]));
$result->setData($answer);
if (array_key_exists('code', $errorInfo) && $errorInfo['code'] !== 'ok')
{
$result->addError(new Error($errorInfo['error'], $errorInfo['code'], $errorInfo));
}
return $result;
}
protected function getMessageByErrorCode(string $code)
{
$locCode = 'MESSAGESERVICE_SENDER_SMS_SMSEDNARU_';
$locCode .= StringHelper::str_replace('-', '_', mb_strtoupper($code));
return Loc::getMessage($locCode) ?? $code;
}
protected function parseExternalAnswer(string $httpResult): array
{
try
{
return Json::decode($httpResult);
}
catch (ArgumentException $exception)
{
return ['error' => 'error-json-parsing'];
}
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Bitrix\MessageService\Providers\Edna\SMS;
use Bitrix\Main\Localization\Loc;
use Bitrix\MessageService\Providers\Edna\RegionHelper;
class Informant extends \Bitrix\MessageService\Providers\Base\Informant
{
public function isConfigurable(): bool
{
return true;
}
public function getId(): string
{
return Constants::ID;
}
public function getName(): string
{
return Loc::getMessage(RegionHelper::getPhrase('MESSAGESERVICE_SENDER_SMS_SMSEDNARU_NAME'));
}
public function getShortName(): string
{
if (RegionHelper::isInternational())
{
return 'sms.edna.io';
}
return 'sms.edna.ru';
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Bitrix\MessageService\Providers\Edna\SMS;
use Bitrix\MessageService\Providers\Edna\Constants\ChannelType;
class Initiator extends \Bitrix\MessageService\Providers\Edna\Initiator
{
protected string $channelType = ChannelType::SMS;
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Bitrix\MessageService\Providers\Edna\SMS\Old;
class Constants extends \Bitrix\MessageService\Providers\Edna\SMS\Constants
{
public const API_ENDPOINT = 'https://sms.edna.ru/connector_sme/api/';
}

View File

@@ -0,0 +1,62 @@
<?php
namespace Bitrix\MessageService\Providers\Edna\SMS\Old;
use Bitrix\MessageService\Providers\Constants\InternalOption;
use Bitrix\MessageService\Providers\Edna\SMS\ExternalSender;
class Initiator extends \Bitrix\MessageService\Providers\Edna\Initiator
{
/**
* @return array{array{id: int, name: string}}
*/
public function getFromList(): array
{
$fromList = [];
if (!$this->supportChecker->canUse())
{
return $fromList;
}
$externalSender = new ExternalSender(
$this->optionManager->getOption(InternalOption::API_KEY, ''),
Constants::API_ENDPOINT
);
$apiResult = $externalSender->callExternalMethod('smsSubject/');
if (!$apiResult->isSuccess())
{
return $fromList;
}
foreach ($apiResult->getData() as $subjectInfo)
{
if ($subjectInfo['active'])
{
$fromList[] = [
'id' => $subjectInfo['subject'],
'name' => $subjectInfo['subject'],
];
}
}
return $fromList;
}
/**
* @param string $from
* @return bool
*/
public function isCorrectFrom($from): bool
{
$fromList = $this->getFromList();
foreach ($fromList as $item)
{
if ($from === $item['id'])
{
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace Bitrix\MessageService\Providers\Edna\SMS\Old;
use Bitrix\Main\Result;
use Bitrix\MessageService\Providers\Constants\InternalOption;
use Bitrix\MessageService\Providers\Edna\SMS\ExternalSender;
class Registrar extends \Bitrix\MessageService\Providers\Edna\SMS\Registrar
{
public function register(array $fields): Result
{
$this->optionManager->setOption(InternalOption::API_KEY, $fields[InternalOption::API_KEY]);
$externalSender = new ExternalSender($fields[InternalOption::API_KEY], Constants::API_ENDPOINT);
return $externalSender->callExternalMethod('smsSubject/');
}
/**
* @return array{apiKey: string, subject: array}
*/
public function getOwnerInfo(): array
{
$initiator = new Initiator($this->optionManager,$this, $this->utils);
return [
InternalOption::API_KEY => $this->optionManager->getOption(InternalOption::API_KEY),
InternalOption::SENDER_ID => array_column($initiator->getFromList(), 'name'),
];
}
public function getExternalManageUrl(): string
{
return 'https://sms.edna.ru/';
}
public function isRegistered(): bool
{
return $this->optionManager->getOption(InternalOption::API_KEY, '') !== '';
}
}

View File

@@ -0,0 +1,125 @@
<?php
namespace Bitrix\MessageService\Providers\Edna\SMS\Old;
use Bitrix\Main\Error;
use Bitrix\Main\Localization\Loc;
use Bitrix\Main\Result;
use Bitrix\Main\Text\Emoji;
use Bitrix\MessageService\Providers\Constants\InternalOption;
use Bitrix\MessageService\Providers\Edna\SMS\ExternalSender;
use Bitrix\MessageService\Providers\Edna\SMS\StatusResolver;
use Bitrix\MessageService\Sender\Result\MessageStatus;
use Bitrix\MessageService\Sender\Result\SendMessage;
class Sender extends \Bitrix\MessageService\Providers\Edna\SMS\Sender
{
public function sendMessage(array $messageFields): SendMessage
{
if (!$this->supportChecker->canUse())
{
$result = new SendMessage();
$result->addError(new Error('Cant use'));
return $result;
}
$validationResult = $this->validatePhoneNumber($messageFields['MESSAGE_TO']);
if (!$validationResult->isSuccess())
{
$result = new SendMessage();
$result->addErrors($validationResult->getErrors());
return $result;
}
$phoneNumber = $validationResult->getData()['validNumber'];
$params = [
'id' => uniqid('', true),
'subject' => $messageFields['MESSAGE_FROM'],
'address' => $phoneNumber,
'priority' => 'high',
'contentType' => 'text',
'content' => Emoji::decode($messageFields['MESSAGE_BODY']),
];
$externalSender = new ExternalSender(
$this->optionManager->getOption(InternalOption::API_KEY, ''),
Constants::API_ENDPOINT
);
$apiResult = $externalSender->callExternalMethod('smsOutMessage', $params);
$result = new SendMessage();
$result->setServiceRequest($apiResult->getHttpRequest());
$result->setServiceResponse($apiResult->getHttpResponse());
if (!$apiResult->isSuccess())
{
$result->addErrors($apiResult->getErrors());
return $result;
}
$apiData = $apiResult->getData();
$result->setExternalId($apiData['id']);
$result->setAccepted();
return $result;
}
protected function validatePhoneNumber(string $number): Result
{
$result = new Result();
$number = str_replace('+', '', $number);
$externalSender = new ExternalSender(
$this->optionManager->getOption(InternalOption::API_KEY, ''),
Constants::API_ENDPOINT
);
$apiResult = $externalSender->callExternalMethod("validatePhoneNumber/{$number}");
if ($apiResult->isSuccess())
{
$result->setData(['validNumber' => $number]);
}
else
{
$result->addErrors($apiResult->getErrors());
}
return $result;
}
public function getMessageStatus(array $messageFields): MessageStatus
{
$result = new MessageStatus();
$result->setId($messageFields['ID']);
$result->setExternalId($messageFields['ID']);
if (!$this->supportChecker->canUse())
{
$result->addError(new Error(Loc::getMessage('MESSAGESERVICE_SENDER_SMS_SMSEDNARU_USE_ERROR')));
return $result;
}
$externalSender = new ExternalSender($this->optionManager->getOption(InternalOption::API_KEY, ''), Constants::API_ENDPOINT);
$apiResult = $externalSender->callExternalMethod("smsOutMessage/{$messageFields['ID']}");
if (!$apiResult->isSuccess())
{
$result->addErrors($apiResult->getErrors());
}
else
{
$apiData = $apiResult->getData();
$result->setStatusText($apiData['dlvStatus']);
$result->setStatusCode((new StatusResolver())->resolveStatus($apiData['dlvStatus']));
}
return $result;
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Bitrix\MessageService\Providers\Edna\SMS;
class RegionHelper extends \Bitrix\MessageService\Providers\Edna\RegionHelper
{
public static function getApiEndPoint(): string
{
return self::isInternational() ? Constants::API_ENDPOINT_IO : Constants::API_ENDPOINT;
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Bitrix\MessageService\Providers\Edna\SMS;
use Bitrix\MessageService\Providers\Constants\InternalOption;
use Bitrix\MessageService\Providers\Edna\Constants\CallbackType;
use Bitrix\MessageService\Providers\Edna\Constants\ChannelType;
use Bitrix\MessageService\Providers\Edna\EdnaRu;
use Bitrix\MessageService\Providers\OptionManager;
class Registrar extends \Bitrix\MessageService\Providers\Edna\Registrar
{
protected string $channelType = ChannelType::SMS;
public function __construct(string $providerId, OptionManager $optionManager, EdnaRu $utils)
{
parent::__construct($providerId, $optionManager, $utils);
if ($this->isRegistered() && !$this->isMigratedToStandartSettingNames())
{
$this->migrateToStandartSettingNames();
}
}
protected function getCallbackTypeList(): array
{
return [
CallbackType::MESSAGE_STATUS,
];
}
private function isMigratedToStandartSettingNames(): bool
{
return $this->optionManager->getOption(InternalOption::MIGRATED_TO_STANDART_SETTING_NAMES, 'N') === 'Y';
}
private function migrateToStandartSettingNames(): void
{
$options = $this->optionManager->getOptions();
if (isset($options['apiKey']))
{
$migratedOptions = [
InternalOption::API_KEY => $options['apiKey'],
InternalOption::MIGRATED_TO_STANDART_SETTING_NAMES => 'Y'
];
$this->optionManager->setOptions($migratedOptions);
}
}
}

View File

@@ -0,0 +1,114 @@
<?php
namespace Bitrix\MessageService\Providers\Edna\SMS;
use Bitrix\Main\Error;
use Bitrix\Main\Result;
use Bitrix\Main\Text\Emoji;
use Bitrix\MessageService\Providers;
use Bitrix\MessageService\Sender\Result\MessageStatus;
class Sender extends Providers\Edna\Sender
{
public function getMessageStatus(array $messageFields): MessageStatus
{
return new MessageStatus();
}
public function prepareMessageBodyForSave(string $text): string
{
return Emoji::encode($text);
}
protected function initializeDefaultExternalSender(): Providers\ExternalSender
{
return new ExternalSender(
$this->optionManager->getOption(Providers\Constants\InternalOption::API_KEY),
RegionHelper::getApiEndPoint(),
$this->optionManager->getSocketTimeout(),
$this->optionManager->getStreamTimeout()
);
}
protected function getSendMessageParams(array $messageFields): Result
{
$cascadeResult = $this->getSenderFromSubject($messageFields['MESSAGE_FROM']);
if (!$cascadeResult->isSuccess())
{
return $cascadeResult;
}
$params = [
'requestId' => uniqid('', true),
'cascadeId' => $cascadeResult->getData()['cascadeId'],
'subscriberFilter' => [
'address' => str_replace('+', '', $messageFields['MESSAGE_TO']),
'type' => 'PHONE',
],
];
$params['content'] = $this->getMessageContent($messageFields);
return (new Result())->setData($params);
}
protected function getSendMessageMethod(array $messageFields): string
{
return Providers\Edna\Constants\Method::SEND_MESSAGE;
}
protected function isTemplateMessage(array $messageFields): bool
{
return false;
}
protected function sendHSMtoChat(array $messageFields): Result
{
return new Result();
}
/**
* @param array $messageFields
* @return array{contentType:string, text:string}
*/
private function getMessageContent(array $messageFields): array
{
return [
'smsContent' => [
'contentType' => Providers\Edna\Constants\ContentType::TEXT,
'text' => $this->prepareMessageBodyForSend($messageFields['MESSAGE_BODY']),
],
];
}
private function getSenderFromSubject($subject): Result
{
$cascadeResult = new Result();
if (is_numeric($subject))
{
$cascadeResult = $this->utils->getCascadeIdFromSubject(
(int)$subject,
static function(array $externalSubjectData, int $internalSubject)
{
return $externalSubjectData['id'] === $internalSubject;
}
);
}
elseif (is_string($subject))
{
$cascadeResult = $this->utils->getCascadeIdFromSubject(
$subject,
static function(array $externalSubjectData, string $internalSubject)
{
return $externalSubjectData['subject'] === $internalSubject;
}
);
}
else
{
$cascadeResult->addError(new Error('Invalid subject id'));
}
return $cascadeResult;
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Bitrix\MessageService\Providers\Edna\SMS;
use Bitrix\MessageService\MessageStatus;
class StatusResolver implements \Bitrix\MessageService\Providers\StatusResolver
{
public function resolveStatus(string $serviceStatus): ?int
{
$serviceStatus = mb_strtolower($serviceStatus);
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':
return MessageStatus::FAILED;
default:
return
mb_strpos($serviceStatus, 'error') === 0
? MessageStatus::ERROR
: null
;
}
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Bitrix\MessageService\Providers\Edna\SMS;
use Bitrix\Main\Result;
use Bitrix\MessageService\Providers;
use Bitrix\MessageService\Providers\Edna\EdnaUtils;
class Utils extends EdnaUtils
{
public function getSentTemplateMessage(string $from, string $to): string
{
return '';
}
public function initializeDefaultExternalSender(): Providers\ExternalSender
{
return new ExternalSender(
$this->optionManager->getOption(Providers\Constants\InternalOption::API_KEY),
RegionHelper::getApiEndPoint(),
$this->optionManager->getSocketTimeout(),
$this->optionManager->getStreamTimeout()
);
}
public function getMessageTemplates(string $subject = ''): Result
{
return new Result();
}
}

View File

@@ -0,0 +1,99 @@
<?php
namespace Bitrix\MessageService\Providers\Edna;
use Bitrix\Main\Error;
use Bitrix\Main\Result;
use Bitrix\MessageService;
use Bitrix\MessageService\Providers;
use Bitrix\MessageService\Sender\Result\MessageStatus;
use Bitrix\MessageService\Sender\Result\SendMessage;
abstract class Sender extends Providers\Base\Sender
{
protected Providers\OptionManager $optionManager;
protected Providers\SupportChecker $supportChecker;
protected EdnaRu $utils;
protected Providers\ExternalSender $externalSender;
abstract protected function initializeDefaultExternalSender(): Providers\ExternalSender;
abstract protected function getSendMessageParams(array $messageFields): Result;
abstract protected function getSendMessageMethod(array $messageFields): string;
abstract protected function isTemplateMessage(array $messageFields): bool;
abstract protected function sendHSMtoChat(array $messageFields): Result;
/**
* @param Providers\OptionManager $optionManager
* @param Providers\SupportChecker $supportChecker
*/
public function __construct(
Providers\OptionManager $optionManager,
Providers\SupportChecker $supportChecker,
EdnaRu $utils
)
{
$this->optionManager = $optionManager;
$this->supportChecker = $supportChecker;
$this->utils = $utils;
$this->externalSender = $this->initializeDefaultExternalSender();
}
public function sendMessage(array $messageFields): SendMessage
{
if (!$this->supportChecker->canUse())
{
$result = new SendMessage();
$result->addError(new Error('Service is unavailable'));
return $result;
}
$paramsResult = $this->getSendMessageParams($messageFields);
if (!$paramsResult->isSuccess())
{
$providerId = $this->optionManager->getProviderId();
$cacheManager = new Providers\CacheManager($providerId);
$cacheManager->deleteValue(Providers\CacheManager::CHANNEL_CACHE_ENTITY_ID);
$sender = MessageService\Sender\SmsManager::getSenderById($providerId);
\Bitrix\Main\Application::getInstance()->addBackgroundJob([$sender, 'refreshFromList']);
$result = new SendMessage();
$result->addErrors($paramsResult->getErrors());
return $result;
}
$requestParams = $paramsResult->getData();
$method = $this->getSendMessageMethod($messageFields);
if ($this->isTemplateMessage($messageFields))
{
$this->sendHSMtoChat($messageFields);
}
$result = new SendMessage();
$requestResult = $this->externalSender->callExternalMethod($method, $requestParams);
if (!$requestResult->isSuccess())
{
$result->addErrors($requestResult->getErrors());
return $result;
}
$apiData = $requestResult->getData();
$result->setExternalId($apiData['requestId']);
$result->setAccepted();
return $result;
}
public function getMessageStatus(array $messageFields): MessageStatus
{
return new MessageStatus();
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Bitrix\MessageService\Providers\Edna\WhatsApp;
use Bitrix\ImConnector\Library;
use Bitrix\Main\Loader;
use Bitrix\Main\Result;
use Bitrix\MessageService\Providers\Edna\Constants\ChannelType;
class ConnectorLine
{
protected \Bitrix\MessageService\Providers\Edna\EdnaRu $utils;
public function __construct(\Bitrix\MessageService\Providers\Edna\EdnaRu $utils)
{
$this->utils = $utils;
}
public function getLineId(?int $subjectId = null): ?int
{
if (!Loader::includeModule('imconnector'))
{
return null;
}
$statuses = \Bitrix\ImConnector\Status::getInstanceAllLine(Library::ID_EDNA_WHATSAPP_CONNECTOR);
foreach ($statuses as $status)
{
if ($status->isConfigured())
{
$data = $status->getData();
if (isset($data['subjectId']) && $data['subjectId'] == $subjectId)
{
return (int)$status->getLine();
}
elseif (!isset($data['subjectId']))
{
$commonLine = (int)$status->getLine();
}
}
}
return $commonLine ?? null;
}
public function testConnection(): Result
{
return $this->utils->getChannelList(ChannelType::WHATSAPP);
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Bitrix\MessageService\Providers\Edna\WhatsApp;
class Constants
{
//region Shared
public const
ID = 'ednaru',
API_ENDPOINT = 'https://app.edna.ru/api/',
API_ENDPOINT_IO = 'https://app.edna.io/api/';
public const
CONTENT_TYPE_TEXT = 'TEXT',
CONTENT_TYPE_IMAGE = 'IMAGE',
CONTENT_TYPE_DOCUMENT = 'DOCUMENT',
CONTENT_TYPE_VIDEO = 'VIDEO',
CONTENT_TYPE_AUDIO = 'AUDIO';
/* @see \Bitrix\Disk\TypeFile */
public const CONTENT_TYPE_MAP = [
2 => self::CONTENT_TYPE_IMAGE,
3 => self::CONTENT_TYPE_VIDEO,
4 => self::CONTENT_TYPE_DOCUMENT,
5 => self::CONTENT_TYPE_DOCUMENT,
8 => self::CONTENT_TYPE_DOCUMENT,
9 => self::CONTENT_TYPE_AUDIO,
];
}

View File

@@ -0,0 +1,63 @@
<?php
namespace Bitrix\MessageService\Providers\Edna\WhatsApp;
use Bitrix\ImConnector\Input;
use Bitrix\ImConnector\Library;
use Bitrix\Main\Text\Emoji;
use Bitrix\MessageService\IncomingMessage;
use Bitrix\Messageservice\Internal\Entity\IncomingMessageTable;
class EdnaRuIncomingMessage extends IncomingMessage
{
public static function sendMessageToChat(array $message): void
{
$message = self::prepareMessageFields($message);
$portal = new Input($message);
$portal->reception();
self::confirmSendingMessage($message['internalId']);
}
public static function prepareMessageFields(array $messageFields): array
{
$messageFields['CONNECTOR'] = Library::ID_EDNA_WHATSAPP_CONNECTOR;
$messageFields['imSubject'] = $messageFields['subjectId'];
$messageFields['address'] = $messageFields['subscriber']['identifier'];
$messageFields['userName'] = $messageFields['userInfo']['userName'];
$messageFields['firstName'] = $messageFields['userInfo']['firstName'];
$messageFields['lastName'] = $messageFields['userInfo']['lastName'];
$messageFields['avatarUrl'] = $messageFields['userInfo']['avatarUrl'];
$messageFields['imType'] = 'whatsapp';
$messageFields['contentType'] = mb_strtolower($messageFields['messageContent']['type']);
if (isset($messageFields['messageContent']['text']))
{
$messageFields['text'] = Emoji::decode($messageFields['messageContent']['text']);
}
if (isset($messageFields['messageContent']['attachment']) && is_array($messageFields['messageContent']['attachment']))
{
$messageFields['attachmentUrl'] = $messageFields['messageContent']['attachment']['url'];
$messageFields['attachmentName'] = $messageFields['messageContent']['attachment']['name'];
}
if (isset($messageFields['messageContent']['caption']) && !is_null($messageFields['messageContent']['caption']))
{
$messageFields['caption'] = Emoji::decode($messageFields['messageContent']['caption']);
}
return $messageFields;
}
protected static function prepareBodyForSave(array $body): array
{
$body['messageContent']['text'] = Emoji::encode($body['messageContent']['text']);
$body['messageContent']['caption'] = Emoji::encode($body['messageContent']['caption']);
return $body;
}
}

View File

@@ -0,0 +1,119 @@
<?php
namespace Bitrix\MessageService\Providers\Edna\WhatsApp;
use Bitrix\MessageService\Providers\Constants\InternalOption;
class EmojiConverter
{
public function convertEmojiInTemplate(array $messageTemplate, string $type): array
{
$template = $messageTemplate;
if (isset($template['text']))
{
$template['text'] = $this->convertTextSection($template['text'], $type);
}
if (isset($template['header']))
{
$template['header'] = $this->convertHeaderSection($template['header'], $type);
}
if (isset($template['footer']))
{
$template['footer'] = $this->convertFooterSection($template['footer'], $type);
}
if (isset($template['keyboard']))
{
$template['keyboard'] = $this->convertKeyboardSection($template['keyboard'], $type);
}
return $template;
}
public function convertEmoji(string $text, string $type): string
{
if (!in_array($type, [InternalOption::EMOJI_DECODE, InternalOption::EMOJI_ENCODE], true))
{
return $text;
}
return \Bitrix\Main\Text\Emoji::$type($text);
}
protected function convertTextSection(?string $textSection, string $type): string
{
if (is_string($textSection))
{
$textSection = $this->convertEmoji($textSection, $type);
}
return $textSection;
}
protected function convertHeaderSection(?array $headerSection, string $type): array
{
if (isset($headerSection['text']))
{
$headerSection['text'] = $this->convertEmoji($headerSection['text'], $type);
}
return $headerSection;
}
protected function convertFooterSection(?array $footerSection, string $type): array
{
if (isset($footerSection['text']))
{
$footerSection['text'] = $this->convertEmoji($footerSection['text'], $type);
}
return $footerSection;
}
/**
* Example:
*
* 'keyboard' => [
* 'rows' => [
* [
* 'buttons' => [
* [
* 'text' => 'Red',
* 'payload' => '1',
* ],
* [
* 'text' => 'blue',
* 'payload' => '2',
* ]
* ]
* ]
* ]
* ]
*
* @see https://docs.edna.ru/kb/message-matchers-get-by-request/
* @param array|null $keyboardSection
* @param string $type
* @return array
*/
protected function convertKeyboardSection(?array $keyboardSection, string $type): array
{
if (isset($keyboardSection['rows']) && is_array($keyboardSection['rows']))
{
foreach ($keyboardSection['rows'] as $rowIndex => $row)
{
if (isset($row['buttons']) && is_array($row['buttons']))
{
foreach ($row['buttons'] as $buttonIndex => $button)
{
if (isset($button['text']))
{
$keyboardSection['rows'][$rowIndex]['buttons'][$buttonIndex]['text'] =
$this->convertEmoji($button['text'], $type);
}
}
}
}
}
return $keyboardSection;
}
}

View File

@@ -0,0 +1,172 @@
<?php
namespace Bitrix\MessageService\Providers\Edna\WhatsApp;
use Bitrix\Main\ArgumentException;
use Bitrix\Main\Error;
use Bitrix\Main\Localization\Loc;
use Bitrix\Main\Web\HttpClient;
use Bitrix\Main\Web\Json;
use Bitrix\MessageService\DTO\Request;
use Bitrix\MessageService\DTO\Response;
use Bitrix\MessageService\Sender\Result\HttpRequestResult;
use Bitrix\MessageService\Sender\Util;
class ExternalSender extends \Bitrix\MessageService\Providers\Edna\ExternalSender
{
public function __construct(?string $apiKey, string $apiEndpoint, int $socketTimeout = 10, int $streamTimeout = 30)
{
$this->apiKey = $apiKey ?? '';
$this->apiEndpoint = $apiEndpoint;
$this->socketTimeout = $socketTimeout;
$this->streamTimeout = $streamTimeout;
}
/**
* @param string $method
* @param array|null $requestParams
* @param string $httpMethod see class constants \Bitrix\Main\Web\HttpClient
* @return HttpRequestResult
*/
public function callExternalMethod(string $method, ?array $requestParams = null, string $httpMethod = ''): HttpRequestResult
{
if ($this->apiKey === '')
{
$result = new HttpRequestResult();
$result->addError(new Error(Loc::getMessage('MESSAGESERVICE_SENDER_SMS_EDNARU_ERROR_SYSTEM')));
return $result;
}
$url = $this->apiEndpoint . $method;
$queryMethod = HttpClient::HTTP_GET;
$httpClient = new HttpClient([
'socketTimeout' => $this->socketTimeout,
'streamTimeout' => $this->streamTimeout,
'waitResponse' => static::WAIT_RESPONSE,
'version' => HttpClient::HTTP_1_1,
]);
$httpClient->setHeader('User-Agent', static::USER_AGENT);
$httpClient->setHeader('Content-type', static::CONTENT_TYPE);
$httpClient->setHeader('X-API-KEY', $this->apiKey);
$httpClient->setCharset(static::CHARSET);
if (isset($requestParams) && $httpMethod !== HttpClient::HTTP_GET)
{
$queryMethod = HttpClient::HTTP_POST;
}
$queryMethod = $httpMethod ?: $queryMethod;
if (isset($requestParams) && $queryMethod === HttpClient::HTTP_POST)
{
$requestParams = Json::encode($requestParams, JSON_UNESCAPED_UNICODE);
}
if (isset($requestParams) && $queryMethod === HttpClient::HTTP_GET)
{
$url .= '?' . http_build_query($requestParams);
}
$result = new HttpRequestResult();
$result->setHttpRequest(new Request([
'method' => $queryMethod,
'uri' => $url,
'headers' => method_exists($httpClient, 'getRequestHeaders') ? $httpClient->getRequestHeaders()->toArray() : [],
'body' => $requestParams,
]));
if ($httpClient->query($queryMethod, $url, $requestParams))
{
$response = $this->parseExternalResponse($httpClient->getResult());
}
else
{
$result->setHttpResponse(new Response([
'error' => Util::getHttpClientErrorString($httpClient)
]));
$error = $httpClient->getError();
$response = ['code' => current($error)];
}
$result->setHttpResponse(new Response([
'statusCode' => $httpClient->getStatus(),
'headers' => $httpClient->getHeaders()->toArray(),
'body' => $httpClient->getResult(),
]));
if (!$this->checkResponse($response))
{
$errorMessage = '';
if (isset($response['title']))
{
$errorMessage = $response['title'];
}
if (isset($response['code']))
{
$errorMessage = $this->getErrorMessageByCode($response['code']);
}
if (isset($response['detail']))
{
$errorMessage = $response['detail'];
}
$result->addError(new Error($errorMessage));
return $result;
}
$result->setData($response);
return $result;
}
protected function parseExternalResponse(string $httpResult): array
{
try
{
return Json::decode($httpResult);
}
catch (ArgumentException $exception)
{
return ['code' => 'error-json-parsing'];
}
}
protected function checkResponse(array $response): bool
{
// Success response without "code" parameter https://edna.docs.apiary.io/#reference/api/by-apikey
if ($this->apiEndpoint === Old\Constants::API_ENDPOINT)
{
return (isset($response['code']) && $response['code'] === 'ok') || !isset($response['code']);
}
if (isset($response['title']) && $response['title'] === 'system-error')
{
return false;
}
return (isset($response['status']) && (int)$response['status'] === 200) || !isset($response['status']);
}
/**
* Mapping from the docs https://edna.docs.apiary.io/#reference/0
*
* @param string $errorCode
*
* @return string
*/
protected function getErrorMessageByCode(?string $errorCode): string
{
$errorCode = mb_strtoupper($errorCode);
$errorCode = str_replace("-", "_", $errorCode);
$errorMessage = Loc::getMessage('MESSAGESERVICE_SENDER_SMS_EDNARU_'.$errorCode);
return $errorMessage ? : Loc::getMessage('MESSAGESERVICE_SENDER_SMS_EDNARU_UNKNOWN_ERROR');
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace Bitrix\MessageService\Providers\Edna\WhatsApp;
use Bitrix\ImConnector\Library;
use Bitrix\Main\Loader;
class Informant extends \Bitrix\MessageService\Providers\Base\Informant
{
public function isConfigurable(): bool
{
return true;
}
public function getId(): string
{
return Constants::ID;
}
public function getName(): string
{
if (RegionHelper::isInternational())
{
return 'Edna.io WhatsApp';
}
return 'Edna.ru WhatsApp';
}
public function getShortName(): string
{
if (RegionHelper::isInternational())
{
return 'Edna.io WhatsApp';
}
return 'Edna.ru WhatsApp';
}
public function getManageUrl(): string
{
if (defined('ADMIN_SECTION') && ADMIN_SECTION === true)
{
return parent::getManageUrl();
}
if (!Loader::includeModule('imopenlines') || !Loader::includeModule('imconnector'))
{
return '';
}
$contactCenterUrl = \Bitrix\ImOpenLines\Common::getContactCenterPublicFolder();
return $contactCenterUrl . 'connector/?ID=' . Library::ID_EDNA_WHATSAPP_CONNECTOR;
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Bitrix\MessageService\Providers\Edna\WhatsApp;
use Bitrix\MessageService\Providers\Edna\Constants\ChannelType;
class Initiator extends \Bitrix\MessageService\Providers\Edna\Initiator
{
protected string $channelType = ChannelType::WHATSAPP;
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Bitrix\MessageService\Providers\Edna\WhatsApp\Old;
class Constants extends \Bitrix\MessageService\Providers\Edna\WhatsApp\Constants
{
//region Old API
public const API_ENDPOINT = 'https://im.edna.ru/api/';
//endregion
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Bitrix\MessageService\Providers\Edna\WhatsApp\Old;
class EmojiConverter extends \Bitrix\MessageService\Providers\Edna\WhatsApp\EmojiConverter
{
protected function convertKeyboardSection(?array $keyboardSection, string $type): array
{
if (
isset($keyboardSection['row']['buttons'])
&& is_array($keyboardSection['row']['buttons'])
)
{
foreach ($keyboardSection['row']['buttons'] as $index => $button)
{
if (isset($button['text']))
{
$keyboardSection['row']['buttons'][$index]['text'] = $this->convertEmoji($button['text'], $type);
}
}
}
return $keyboardSection;
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace Bitrix\MessageService\Providers\Edna\WhatsApp\Old;
class Registrar extends \Bitrix\MessageService\Providers\Edna\WhatsApp\Registrar
{
public function getExternalManageUrl(): string
{
return 'https://im.edna.ru/';
}
}

View File

@@ -0,0 +1,177 @@
<?php
namespace Bitrix\MessageService\Providers\Edna\WhatsApp\Old;
use Bitrix\Main\Error;
use Bitrix\Main\Result;
use Bitrix\Main\Localization\Loc;
use Bitrix\MessageService\Providers;
use Bitrix\MessageService\Providers\Constants\InternalOption;
use Bitrix\MessageService\Providers\Edna\WhatsApp\ExternalSender;
use Bitrix\MessageService\Providers\Edna\WhatsApp\StatusResolver;
use Bitrix\MessageService\Sender\Result\MessageStatus;
use Bitrix\MessageService\Sender\Result\SendMessage;
class Sender extends Providers\Edna\WhatsApp\Sender
{
public function __construct(
Providers\OptionManager $optionManager,
Providers\SupportChecker $supportChecker,
Providers\Edna\EdnaRu $utils,
EmojiConverter $emoji
)
{
parent::__construct($optionManager, $supportChecker, $utils, $emoji);
$this->emoji = $emoji;
}
public function sendMessage(array $messageFields): SendMessage
{
if (!$this->supportChecker->canUse())
{
$result = new SendMessage();
$result->addError(new Error('Service is unavailable'));
return $result;
}
$requestParams = $this->getSendMessageParams($messageFields)->getData();
$method = $this->getSendMessageMethod($messageFields);
if ($method === 'imOutHSM')
{
$this->sendHSMtoChat($messageFields);
}
$result = new SendMessage();
$externalSender =
new ExternalSender(
$this->optionManager->getOption(InternalOption::API_KEY),
Constants::API_ENDPOINT
)
;
$requestResult = $externalSender->callExternalMethod($method, $requestParams);
if (!$requestResult->isSuccess())
{
$result->addErrors($requestResult->getErrors());
return $result;
}
$apiData = $requestResult->getData();
$result->setExternalId($apiData['id']);
$result->setAccepted();
return $result;
}
public function getMessageStatus(array $messageFields): MessageStatus
{
$result = new MessageStatus();
$result->setId($messageFields['ID']);
$result->setExternalId($messageFields['ID']);
if (!$this->supportChecker->canUse())
{
$result->addError(new Error(Loc::getMessage('MESSAGESERVICE_SENDER_SMS_EDNARU_USE_ERROR')));
return $result;
}
$externalSender =
new ExternalSender(
$this->optionManager->getOption(InternalOption::API_KEY),
Constants::API_ENDPOINT
)
;
$apiResult = $externalSender->callExternalMethod("imOutMessage/{$messageFields['ID']}");
if (!$apiResult->isSuccess())
{
$result->addErrors($apiResult->getErrors());
}
else
{
$apiData = $apiResult->getData();
$result->setStatusText($apiData['dlvStatus']);
$result->setStatusCode((new StatusResolver())->resolveStatus($apiData['dlvStatus']));
}
return $result;
}
/**
* Converts message body text. Encodes emoji in the text, if there are any emoji.
*
* @param string $text
*
* @return string
*/
public function prepareMessageBodyForSave(string $text): string
{
return $this->emoji->convertEmoji($text, Providers\Constants\InternalOption::EMOJI_ENCODE);
}
/**
* Returns request params for sending template or simple message.
* @param array $messageFields Message fields.
*
* @return Result
*/
protected function getSendMessageParams(array $messageFields): Result
{
$messageFields['MESSAGE_BODY'] = $this->emoji->convertEmoji($messageFields['MESSAGE_BODY'], Providers\Constants\InternalOption::EMOJI_DECODE);
$params = [
'id' => uniqid('', true),
'subject' => $messageFields['MESSAGE_FROM'],
'address' => str_replace('+', '', $messageFields['MESSAGE_TO']),
'contentType' => 'text',
'text' => $messageFields['MESSAGE_BODY'],
];
if ($this->isTemplateMessage($messageFields))
{
$params['imType'] = 'whatsapp';
$params['text'] = $messageFields['MESSAGE_HEADERS']['template']['text'];
$templateFields = ['header', 'footer', 'keyboard'];
foreach ($templateFields as $templateField)
{
if (
isset($messageFields['MESSAGE_HEADERS']['template'][$templateField])
&& count($messageFields['MESSAGE_HEADERS']['template'][$templateField]) > 0
)
{
$params[$templateField] = $messageFields['MESSAGE_HEADERS']['template'][$templateField];
}
}
$params = $this->emoji->convertEmojiInTemplate($params, InternalOption::EMOJI_DECODE);
}
return (new Result)->setData($params);
}
/**
* Returns method for sending template or simple message.
*
* @param array $messageFields Message fields.
*
* @return string
*/
protected function getSendMessageMethod(array $messageFields): string
{
$method = 'imOutMessage';
if ($this->isTemplateMessage($messageFields))
{
$method = 'imOutHSM';
}
return $method;
}
}

View File

@@ -0,0 +1,141 @@
<?php
namespace Bitrix\MessageService\Providers\Edna\WhatsApp\Old;
use Bitrix\Main\Result;
use Bitrix\MessageService\Providers\Constants\InternalOption;
use Bitrix\MessageService\Providers\Edna\WhatsApp;
use Bitrix\MessageService\Providers\Edna\WhatsApp\ExternalSender;
use Bitrix\MessageService\Providers\OptionManager;
class Utils extends WhatsApp\Utils
{
public function testConnection(): Result
{
$requestParams = ['imType' => 'WHATSAPP'];
$externalSender =
new ExternalSender(
$this->optionManager->getOption(InternalOption::API_KEY),
Constants::API_ENDPOINT
)
;
return $externalSender->callExternalMethod('im-subject/by-apikey', $requestParams);
}
public function getMessageTemplates(string $subject = ''): Result
{
$result = new Result();
if (defined('WA_EDNA_RU_TEMPLATES_STUB') && WA_EDNA_RU_TEMPLATES_STUB === true)
{
return $result->setData($this->getMessageTemplatesStub());
}
$params = ['imType' => 'whatsapp'];
if ($subject !== '')
{
$params['subject'] = $subject;
}
$externalSender =
new ExternalSender(
$this->optionManager->getOption(InternalOption::API_KEY),
Constants::API_ENDPOINT
);
$templatesRequestResult = $externalSender->callExternalMethod('getOutMessageMatchers', $params);
if ($templatesRequestResult->isSuccess())
{
$templates = $templatesRequestResult->getData();
$checkErrors = $this->checkForErrors($templates);
if ($checkErrors->isSuccess())
{
$templates = $this->removeUnsupportedTemplates($templates);
$result->setData($templates);
}
else
{
$result->addErrors($checkErrors->getErrors());
}
}
else
{
$result->addErrors($templatesRequestResult->getErrors());
}
return $result;
}
/**
* Returns stub with HSM template from docs:
* https://edna.docs.apiary.io/#reference/api/getoutmessagematchers
*
* @return array
*/
protected function getMessageTemplatesStub(): array
{
return [
'result' => [
[
'id' => 206,
'name' => 'test template',
'imType' => 'whatsapp',
'language' => 'AU',
'content' => [
'header' => [],
'text' => 'whatsapp text',
'footer' => [
'text' => 'footer text'
],
'keyboard' => [
'row' => [
'buttons' => [
[
'text' => 'button1',
'payload' => 'button1',
'buttonType' => 'QUICK_REPLY'
]
]
]
]
],
'category' => 'ISSUE_UPDATE',
'status' => 'PENDING',
'createdAt' => '2020-11-12T11:31:39.000+0000',
'updatedAt' => '2020-11-12T11:31:39.000+0000'
],
[
'id' => 207,
'name' => 'one more template',
'imType' => 'whatsapp',
'language' => 'AU',
'content' => [
'header' => [],
'text' => 'one more template',
'footer' => [
'text' => 'footer text'
],
'keyboard' => [
'row' => [
'buttons' => [
[
'text' => 'button1',
'payload' => 'button1',
'buttonType' => 'QUICK_REPLY'
]
]
]
]
],
'category' => 'ISSUE_UPDATE',
'status' => 'PENDING',
'createdAt' => '2020-11-12T11:31:39.000+0000',
'updatedAt' => '2020-11-12T11:31:39.000+0000'
]
],
'code' => 'ok'
];
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Bitrix\MessageService\Providers\Edna\WhatsApp;
class RegionHelper extends \Bitrix\MessageService\Providers\Edna\RegionHelper
{
public static function getApiEndPoint(): string
{
return self::isInternational() ? Constants::API_ENDPOINT_IO : Constants::API_ENDPOINT;
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Bitrix\MessageService\Providers\Edna\WhatsApp;
use Bitrix\MessageService\Providers\Edna;
class Registrar extends Edna\Registrar
{
protected string $channelType = Edna\Constants\ChannelType::WHATSAPP;
protected function getCallbackTypeList(): array
{
return [
Edna\Constants\CallbackType::MESSAGE_STATUS,
Edna\Constants\CallbackType::INCOMING_MESSAGE,
Edna\Constants\CallbackType::TEMPLATE_REGISTER_STATUS,
];
}
}

View File

@@ -0,0 +1,353 @@
<?php
namespace Bitrix\MessageService\Providers\Edna\WhatsApp;
use Bitrix\Disk\File;
use Bitrix\ImConnector\Library;
use Bitrix\ImOpenLines\Im;
use Bitrix\ImOpenLines\Session;
use Bitrix\Main\Error;
use Bitrix\Main\Loader;
use Bitrix\Main\Result;
use Bitrix\MessageService\Providers;
use Bitrix\MessageService\Providers\Constants\InternalOption;
class Sender extends Providers\Edna\Sender
{
public const AVAILABLE_CONTENT_TYPES = [
'image/jpeg' => 5 * 1024 * 1024,
'image/png' => 5 * 1024 * 1024,
'audio/aac' => 16 * 1024 * 1024,
'audio/mp4' => 16 * 1024 * 1024,
'audio/amr' => 16 * 1024 * 1024,
'audio/mpeg' => 16 * 1024 * 1024,
'audio/ogg' => 16 * 1024 * 1024,
'video/mp4' => 16 * 1024 * 1024,
'video/3gpp' => 16 * 1024 * 1024,
];
public const DOCUMENT_MAX_FILE_SIZE = 100 * 1024 * 1024;
protected Providers\OptionManager $optionManager;
protected Providers\SupportChecker $supportChecker;
protected Providers\Edna\EdnaRu $utils;
protected EmojiConverter $emoji;
protected ConnectorLine $connectorLine;
public function __construct(
Providers\OptionManager $optionManager,
Providers\SupportChecker $supportChecker,
Providers\Edna\EdnaRu $utils,
EmojiConverter $emoji
)
{
parent::__construct($optionManager, $supportChecker, $utils);
$this->emoji = $emoji;
$this->connectorLine = new ConnectorLine($this->utils);
}
protected function getSendMessageMethod(array $messageFields): string
{
return Providers\Edna\Constants\Method::SEND_MESSAGE;
}
/**
* Converts message body text. Encodes emoji in the text, if there are any emoji.
*
* @param string $text
*
* @return string
*/
public function prepareMessageBodyForSave(string $text): string
{
return $this->emoji->convertEmoji($text, InternalOption::EMOJI_ENCODE);
}
protected function getSendMessageParams(array $messageFields): Result
{
$cascadeResult = new Result();
if (is_numeric($messageFields['MESSAGE_FROM']))
{
$cascadeResult = $this->utils->getCascadeIdFromSubject(
(int)$messageFields['MESSAGE_FROM'],
static function(array $externalSubjectData, int $internalSubject)
{
return $externalSubjectData['id'] === $internalSubject;
}
);
}
elseif (is_string($messageFields['MESSAGE_FROM']))
{
$cascadeResult = $this->utils->getCascadeIdFromSubject(
$messageFields['MESSAGE_FROM'],
static function(array $externalSubjectData, string $internalSubject)
{
return $externalSubjectData['subject'] === $internalSubject;
}
);
}
else
{
return $cascadeResult->addError(new Error('Invalid subject id'));
}
if (!$cascadeResult->isSuccess())
{
return $cascadeResult;
}
$messageFields['MESSAGE_BODY'] = $this->emoji->convertEmoji($messageFields['MESSAGE_BODY'], InternalOption::EMOJI_DECODE);
$params = [
'requestId' => uniqid('', true),
'cascadeId' => $cascadeResult->getData()['cascadeId'],
'subscriberFilter' => [
'address' => str_replace('+', '', $messageFields['MESSAGE_TO']),
'type' => 'PHONE',
],
];
$params['content'] = $this->getMessageContent($messageFields);
$result = new Result();
$result->setData($params);
return $result;
}
/**
* Checks if message is HSM template by message fields.
* We consider that it is template by mandatory text field.
* https://edna.docs.apiary.io/#reference/api/imouthsm
*
* @param array $messageFields Message fields.
*
* @return bool
*/
public function isTemplateMessage(array $messageFields): bool
{
if (isset($messageFields['MESSAGE_HEADERS']['template']['text']))
{
return true;
}
return false;
}
/**
* @param array $messageFields
* @return array{whatsappContent: array}
*/
protected function getMessageContent(array $messageFields): array
{
$whatsAppContent =
$this->isTemplateMessage($messageFields)
? $this->getHSMContent($messageFields)
: $this->getSimpleMessageContent($messageFields)
;
return [
'whatsappContent' => $whatsAppContent
];
}
/**
* @param array $messageFields
* @return array{contentType:string, text:string}
*/
private function getHSMContent(array $messageFields): array
{
$params = [
'contentType' => Providers\Edna\Constants\ContentType::TEXT,
'text' => $messageFields['MESSAGE_HEADERS']['template']['text']
];
foreach (['header', 'footer', 'keyboard'] as $templateField)
{
if (
isset($messageFields['MESSAGE_HEADERS']['template'][$templateField])
&& count($messageFields['MESSAGE_HEADERS']['template'][$templateField]) > 0
)
{
$params[$templateField] = $messageFields['MESSAGE_HEADERS']['template'][$templateField];
}
}
return $this->emoji->convertEmojiInTemplate($params, InternalOption::EMOJI_DECODE);
}
/**
* @param array $messageFields
* @return array{contentType:string, text:string}
*/
private function getSimpleMessageContent(array $messageFields): array
{
$contentType = Constants::CONTENT_TYPE_TEXT;
$messageBody = $messageFields['MESSAGE_BODY'];
if (Loader::includeModule('disk') && preg_match('/^http.+~.+$/', trim($messageBody)))
{
$fileUri = \CBXShortUri::GetUri($messageBody);
if ($fileUri)
{
$parsedUrl = parse_url($fileUri['URI']);
$queryParams = [];
parse_str($parsedUrl['query'], $queryParams);
if (isset($queryParams['FILE_ID']))
{
$diskFile = \Bitrix\Disk\File::getById((int)$queryParams['FILE_ID']);
if ($diskFile)
{
$contentType = $this->determineContentType($diskFile);
$messageBody = $fileUri['URI'];
}
}
}
}
$content = [
'contentType' => $contentType
];
switch ($contentType)
{
case Constants::CONTENT_TYPE_IMAGE:
case Constants::CONTENT_TYPE_AUDIO:
case Constants::CONTENT_TYPE_VIDEO:
case Constants::CONTENT_TYPE_DOCUMENT:
$content['attachment'] = [
'url' => $messageBody
];
break;
case Constants::CONTENT_TYPE_TEXT:
default:
$content['text'] = $messageBody;
}
return $content;
}
private function determineContentType(File $diskFile): string
{
$contentType = Constants::CONTENT_TYPE_TEXT;
$file = $diskFile->getFile();
if (is_array($file) && isset($file['CONTENT_TYPE']))
{
if (isset(self::AVAILABLE_CONTENT_TYPES[$file['CONTENT_TYPE']]))
{
$maxSize = self::AVAILABLE_CONTENT_TYPES[$file['CONTENT_TYPE']];
if ($diskFile->getSize() <= $maxSize)
{
$contentType = Constants::CONTENT_TYPE_MAP[$diskFile->getTypeFile()] ?? Constants::CONTENT_TYPE_DOCUMENT;
}
elseif ($diskFile->getSize() <= self::DOCUMENT_MAX_FILE_SIZE)
{
$contentType = Constants::CONTENT_TYPE_DOCUMENT;
}
}
elseif ($diskFile->getSize() <= self::DOCUMENT_MAX_FILE_SIZE)
{
$contentType = Constants::CONTENT_TYPE_DOCUMENT;
}
}
return $contentType;
}
protected function sendHSMtoChat(array $messageFields): Result
{
if (!Loader::includeModule('imopenlines') || !Loader::includeModule('imconnector'))
{
return (new Result())->addError(new Error('Missing modules imopenlines and imconnector'));
}
$externalChatId = str_replace('+', '', $messageFields['MESSAGE_TO']);
$userId = $this->getImconnectorUserId($externalChatId);
if (!$userId)
{
return (new Result())->addError(new Error('Missing User Id'));
}
$from = $messageFields['MESSAGE_FROM'];
$lineId = $this->connectorLine->getLineId((int)$from);
if (!$lineId)
{
return (new Result())->addError(new Error('Missing Line Id. Please reconfigure the open line'));
}
$userSessionCode = $this->getSessionUserCode($lineId, $externalChatId, $from, $userId);
$chatId = $this->getOpenedSessionChatId($userSessionCode);
if (!$chatId)
{
return (new Result())->addError(new Error('Missing Chat Id'));
}
$messageId = Im::addMessage([
'TO_CHAT_ID' => $chatId,
'MESSAGE' => $this->utils->prepareTemplateMessageText($messageFields),
'SYSTEM' => 'Y',
'SKIP_COMMAND' => 'Y',
'NO_SESSION_OL' => 'Y',
'PARAMS' => [
'CLASS' => 'bx-messenger-content-item-ol-output'
],
]);
$result = new Result();
$resultData = $messageFields;
$resultData['messageId'] = $messageId;
$resultData['chatId'] = $chatId;
$result->setData($resultData);
if (!$messageId)
{
$result->addError(new Error('Error sending a message to the chat'));
}
return $result;
}
protected function getImconnectorUserId(string $externalChatId): ?string
{
$userXmlId = Library::ID_EDNA_WHATSAPP_CONNECTOR . '|' . $externalChatId;
$user = \Bitrix\Main\UserTable::getRow([
'select' => ['ID'],
'filter' => ['=XML_ID' => $userXmlId],
]);
return $user ? $user['ID'] : null;
}
protected function getSessionUserCode(string $lineId, string $externalChatId, string $from, string $userId): string
{
return Library::ID_EDNA_WHATSAPP_CONNECTOR. '|'. $lineId. '|'. $externalChatId. '@'. $from. '|' . $userId;
}
protected function getOpenedSessionChatId(string $userSessionCode): ?string
{
$session = new Session();
$sessionLoadResult = $session->getLast(['USER_CODE' => $userSessionCode]);
if (!$sessionLoadResult->isSuccess())
{
return null;
}
$sessionData = $session->getData();
$chatId = $sessionData['CHAT_ID'];
$closed = $sessionData['CLOSED'] === 'Y';
if ($closed)
{
return null;
}
return $chatId;
}
protected function initializeDefaultExternalSender(): Providers\ExternalSender
{
return new ExternalSender(
$this->optionManager->getOption(InternalOption::API_KEY),
RegionHelper::getApiEndPoint(),
$this->optionManager->getSocketTimeout(),
$this->optionManager->getStreamTimeout()
);
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Bitrix\MessageService\Providers\Edna\WhatsApp;
use Bitrix\MessageService\MessageStatus;
class StatusResolver implements \Bitrix\MessageService\Providers\StatusResolver
{
public function resolveStatus(string $serviceStatus): ?int
{
$serviceStatus = mb_strtolower($serviceStatus);
switch ($serviceStatus)
{
case 'read':
return MessageStatus::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;
}
}
}

View File

@@ -0,0 +1,108 @@
<?php
namespace Bitrix\MessageService\Providers\Edna\WhatsApp;
use Bitrix\Main\ArgumentException;
use Bitrix\Main\Web\Json;
use Bitrix\MessageService\Providers\Constants\InternalOption;
use Bitrix\MessageService\Providers\Edna\EdnaRu;
class TemplateManager extends \Bitrix\MessageService\Providers\Base\TemplateManager
{
protected EdnaRu $utils;
protected EmojiConverter $emoji;
public function __construct(string $providerId, EdnaRu $utils, EmojiConverter $emoji)
{
parent::__construct($providerId);
$this->utils = $utils;
$this->emoji = $emoji;
}
/**
* @param array|null $context
* @return array<int, array{ID: string, TITLE: string, PREVIEW: string, HEADER: string, FOOTER: string, PLACEHOLDERS: array, KEYBOARD: array}>
*/
public function getTemplatesList(array $context = null): array
{
$templatesResult = $this->utils->getMessageTemplates();
if (!$templatesResult->isSuccess())
{
return [];
}
$templates = $templatesResult->getData();
if (!is_array($templates))
{
return [];
}
$result = [];
foreach ($templates as $template)
{
$tmp = [
'ID' => Json::encode($template['content']),
'ORIGINAL_ID' => (int)$template['id'],
'TITLE' => $template['name'],
'PREVIEW' => $template['content']['text'] ?? '',
];
if (!empty($template['content']['header']['text']))
{
$tmp['HEADER'] = $template['content']['header']['text'];
}
if (!empty($template['content']['footer']['text']))
{
$tmp['FOOTER'] = $template['content']['footer']['text'];
}
if (!empty($template['content']['keyboard']['rows']))
{
$tmp['KEYBOARD'] = $template['content']['keyboard'];
}
if (!empty($template['placeholders']))
{
$tmp['PLACEHOLDERS'] = [];
}
if (!empty($template['placeholders']['text']))
{
$tmp['PLACEHOLDERS']['PREVIEW'] = $template['placeholders']['text'];
}
if (!empty($template['placeholders']['header']))
{
$tmp['PLACEHOLDERS']['HEADER'] = $template['placeholders']['header'];
}
if (!empty($template['placeholders']['footer']))
{
$tmp['PLACEHOLDERS']['FOOTER'] = $template['placeholders']['footer'];
}
$result[] = $tmp;
}
return $result;
}
public function prepareTemplate($templateData): array
{
try
{
$messageTemplateDecoded = Json::decode($templateData);
$messageTemplateDecoded =
$this->emoji->convertEmojiInTemplate($messageTemplateDecoded, InternalOption::EMOJI_ENCODE);
}
catch (\Bitrix\Main\ArgumentException $e)
{
throw new ArgumentException('Incorrect message template');
}
return $messageTemplateDecoded;
}
public function isTemplatesBased(): bool
{
return true;
}
}

View File

@@ -0,0 +1,532 @@
<?php
namespace Bitrix\MessageService\Providers\Edna\WhatsApp;
use Bitrix\Main\Application;
use Bitrix\Main\Data\Cache;
use Bitrix\Main\Error;
use Bitrix\Main\Result;
use Bitrix\MessageService\Internal\Entity\MessageTable;
use Bitrix\MessageService\Internal\Entity\TemplateTable;
use Bitrix\MessageService\Providers;
use Bitrix\MessageService\Providers\Edna\EdnaUtils;
class Utils extends EdnaUtils
{
public const CACHE_KEY_TEMPLATES = 'whatsapp_templates_cache';
public const CACHE_DIR_TEMPLATES = '/messageservice/templates/';
protected const CACHE_TIME_TEMPLATES = 14400;
protected function initializeDefaultExternalSender(): Providers\ExternalSender
{
return new ExternalSender(
$this->optionManager->getOption(Providers\Constants\InternalOption::API_KEY),
RegionHelper::getApiEndPoint(),
$this->optionManager->getSocketTimeout(),
$this->optionManager->getStreamTimeout()
);
}
public function getMessageTemplates(string $subject = ''): Result
{
$result = new Result();
if ($this->optionManager->getOption('enable_templates_stub', 'N') === 'Y')
{
$templates = $this->removeUnsupportedTemplates($this->getMessageTemplatesStub());
return $result->setData($templates);
}
$subjectList = [$subject];
if ($subject === '')
{
$subjectList = $this->optionManager->getOption('sender_id', []);
}
$verifiedSubjectIdResult = $this->getVerifiedSubjectIdList($subjectList);
if (!$verifiedSubjectIdResult->isSuccess())
{
return $verifiedSubjectIdResult;
}
$verifiedSubjectIdList = $verifiedSubjectIdResult->getData();
$templates = [];
$cache = Cache::createInstance();
if ($cache->initCache(self::CACHE_TIME_TEMPLATES, self::CACHE_KEY_TEMPLATES, self::CACHE_DIR_TEMPLATES))
{
$templates = $cache->getVars();
}
elseif ($cache->startDataCache())
{
foreach ($verifiedSubjectIdList as $subjectId)
{
$requestParams = [
'subjectId' => $subjectId
];
$templatesRequestResult =
$this->externalSender->callExternalMethod(Providers\Edna\Constants\Method::GET_TEMPLATES, $requestParams);
if ($templatesRequestResult->isSuccess())
{
$templates = array_merge($templates, $templatesRequestResult->getData());
}
}
$templates = $this->excludeOutdatedTemplates($templates);
$templates = $this->replaceNameToTitle($templates);
$cache->endDataCache($templates);
}
$checkErrors = $this->checkForErrors($templates);
if ($checkErrors->isSuccess())
{
$templates = $this->removeUnsupportedTemplates($templates);
$templates = $this->checkForPlaceholders($templates);
$result->setData($templates);
}
else
{
$result->addErrors($checkErrors->getErrors());
}
return $result;
}
public function prepareTemplateMessageText(array $message): string
{
$latestMessage = '';
if (isset($message['MESSAGE_HEADERS']['template']['header']['text']))
{
$latestMessage .= $message['MESSAGE_HEADERS']['template']['header']['text'] . '#BR#';
}
if (isset($message['MESSAGE_HEADERS']['template']['text']))
{
$latestMessage .= $message['MESSAGE_HEADERS']['template']['text'] . '#BR#';
}
if (isset($message['MESSAGE_HEADERS']['template']['footer']['text']))
{
$latestMessage .= $message['MESSAGE_HEADERS']['template']['footer']['text'];
}
return $latestMessage;
}
public function getSentTemplateMessage(string $from, string $to): string
{
$message = MessageTable::getList([
'select' => ['ID', 'MESSAGE_HEADERS'],
'filter' => [
'=SENDER_ID' => $this->providerId,
'=MESSAGE_FROM' => $from,
'=MESSAGE_TO' => '+' . $to,
],
'limit' => 1,
'order' => ['ID' => 'DESC'],
])->fetch();
if (!$message)
{
return '';
}
return $this->prepareTemplateMessageText($message);
}
protected function getMessageTemplatesStub(): array
{
return [
[
'id' => 242,
'name' => 'only text',
'channelType' => 'whatsapp',
'language' => 'RU',
'content' => [
'attachment' => null,
'action' => null,
'caption' => null,
'header' => null,
'text' => "Hello! Welcome to our platform.",
'footer' => null,
'keyboard' => null,
],
'category' => 'ACCOUNT_UPDATE',
'status' => Providers\Edna\Constants\TemplateStatus::PENDING,
'locked' => false,
'type' => 'OPERATOR',
'createdAt' => '2021-07-15T14:16:54.417024Z',
'updatedAt' => '2021-07-16T13:08:26.275414Z',
],
[
'id' => 267,
'name' => 'text + header + footer',
'channelType' => 'whatsapp',
'language' => 'RU',
'content' => [
'attachment' => null,
'action' => null,
'caption' => null,
'header' => [
'text' => 'Greetings',
],
'text' => 'Hello! Welcome to our platform.',
'footer' => [
'text' => 'Have a nice day',
],
'keyboard' => null,
],
'category' => 'ACCOUNT_UPDATE',
'status' => Providers\Edna\Constants\TemplateStatus::APPROVED,
'locked' => false,
'type' => 'USER',
'createdAt' => '2021-07-20T09:21:42.444454Z',
'updatedAt' => '2021-07-20T09:21:42.444454Z',
],
[
'id' => 268,
'name' => 'text + buttons',
'channelType' => 'whatsapp',
'language' => 'RU',
'content' => [
'attachment' => null,
'action' => null,
'caption' => null,
'header' => null,
'text' => "Hello! Welcome to our platform. Have you already tried it?",
'footer' => null,
'keyboard' => [
'rows' => [
[
'buttons' => [
[
'text' => 'Yes',
'buttonType' => "QUICK_REPLY",
'payload' => '1'
],
[
'text' => 'No',
'buttonType' => "QUICK_REPLY",
'payload' => '2'
],
],
],
],
],
],
'category' => 'ACCOUNT_UPDATE',
'status' => Providers\Edna\Constants\TemplateStatus::APPROVED,
'locked' => false,
'type' => 'USER',
'createdAt' => '2021-07-20T09:21:42.444454Z',
'updatedAt' => '2021-07-20T09:21:42.444454Z',
],
[
'id' => 269,
'name' => 'text + button-link',
'channelType' => 'whatsapp',
'language' => 'RU',
'content' => [
'attachment' => null,
'action' => null,
'caption' => null,
'header' => null,
'text' => 'Hello! Welcome to our platform. Follow the link bellow to read manuals:',
'footer' => null,
'keyboard' => [
'rows' => [
[
'buttons' => [
[
'text' => 'Manual',
'buttonType' => "URL",
'url' => "https://docs.edna.io/"
],
],
],
],
],
],
'category' => 'ACCOUNT_UPDATE',
'status' => Providers\Edna\Constants\TemplateStatus::DISABLED,
'locked' => false,
'type' => 'USER',
'createdAt' => '2021-07-20T09:21:42.444454Z',
'updatedAt' => '2021-07-20T09:21:42.444454Z',
],
];
}
private function getVerifiedSubjectIdList(array $subjectList): Result
{
$channelListResult = $this->getChannelList(Providers\Edna\Constants\ChannelType::WHATSAPP);
if (!$channelListResult->isSuccess())
{
return $channelListResult;
}
$filteredSubjectList = [];
foreach ($channelListResult->getData() as $channel)
{
if (isset($channel['subjectId']) && in_array($channel['subjectId'], $subjectList, true))
{
$filteredSubjectList[] = $channel['subjectId'];
}
}
$result = new Result();
if (empty($filteredSubjectList))
{
$result->addError(new Error('Verified subjects are missing'));
return $result;
}
$result->setData($filteredSubjectList);
return $result;
}
/**
* @param array{status: string} $template
* @return bool
*/
protected function checkApprovedStatus(array $template): bool
{
return isset($template['status']) && $template['status'] === Providers\Edna\Constants\TemplateStatus::APPROVED;
}
protected function checkForPlaceholders(array $templates): array
{
foreach ($templates as &$template)
{
$template['placeholders'] = [];
if (
!empty($template['content']['header']['text'])
&& $this->hasPlaceholder($template['content']['header']['text'])
)
{
$template['placeholders']['header'] = $this->extractPlaceholders($template['content']['header']['text']);
}
if (
!empty($template['content']['text'])
&& $this->hasPlaceholder($template['content']['text'])
)
{
$template['placeholders']['text'] = $this->extractPlaceholders($template['content']['text']);
}
if (
!empty($template['content']['footer']['text'])
&& $this->hasPlaceholder($template['content']['footer']['text'])
)
{
$template['placeholders']['footer'] = $this->extractPlaceholders($template['content']['footer']['text']);
}
}
return $templates;
}
protected function hasPlaceholder(string $text): bool
{
return !empty($text) && preg_match("/{{[\d]+}}/", $text);
}
protected function extractPlaceholders(string $text): array
{
preg_match_all("/({{[\d]+}})/", $text, $matches);
return !empty($matches[0]) ? $matches[0] : [];
}
protected function checkForErrors(array $templates): Result
{
$checkResult = new Result();
foreach ($templates as $template)
{
if (!is_array($template))
{
$exception = new \Bitrix\Main\SystemException(
'Incorrect response from the Edna service: ' . var_export($templates, true)
);
\Bitrix\Main\Application::getInstance()->getExceptionHandler()->writeToLog($exception);
return $checkResult->addError(
new Error('Incorrect response from the Edna service.', 400, $templates)
);
}
}
return $checkResult;
}
protected function removeUnsupportedTemplates(array $templates): array
{
$filteredTemplates = [];
foreach ($templates as $template)
{
if (!is_array($template))
{
continue;
}
if (!$this->checkApprovedStatus($template))
{
continue;
}
$filteredTemplates[] = $template;
}
return $filteredTemplates;
}
protected function replaceNameToTitle(array $templates = []): array
{
$autoTemplates = TemplateTable::getList([
'filter' => ['=ACTIVE' => 'Y']
])->fetchAll();
$titles = [];
foreach ($autoTemplates as $autoTemplate)
{
$titles[$autoTemplate['NAME']] = $autoTemplate['TITLE'];
}
foreach ($templates as $key => $template)
{
$templates[$key]['name'] = $titles[$template['name']] ?? $template['name'];
}
return $templates;
}
protected function excludeOutdatedTemplates(array $templates = []): array
{
$outdatedTemplatesResult = TemplateTable::getList([
'filter' => ['=ACTIVE' => 'N']
])->fetchAll();
$outdatedTemplates = [];
foreach ($outdatedTemplatesResult as $outdatedTemplate)
{
$outdatedTemplates[$outdatedTemplate['NAME']] = true;
}
$activeTemplates = array_filter($templates, function ($template) use ($outdatedTemplates) {
return is_array($template) && isset($template['name']) && !isset($outdatedTemplates[$template['name']]);
});
return array_values($activeTemplates);
}
public function sendTemplate(string $name, string $text, array $examples = [], ?string $langCode = null): Result
{
if (is_null($langCode))
{
$langCode = Application::getInstance()->getContext()->getLanguage();
}
if (!$this->validateLanguage($langCode))
{
return (new Result)->addError(new Error('Unknown language code'));
}
$validateTemplateName = $this->validateTemplateName($name);
if (!$validateTemplateName->isSuccess())
{
return $validateTemplateName;
}
$validateTemplateText = $this->validateTemplateText($text);
if (!$validateTemplateText->isSuccess())
{
return $validateTemplateText;
}
$validateExamples = $this->validateExamples($text, $examples);
if (!$validateExamples->isSuccess())
{
return $validateExamples;
}
$subjectList = $this->optionManager->getOption('sender_id', []);
$verifiedSubjectIdResult = $this->getVerifiedSubjectIdList($subjectList);
if (!$verifiedSubjectIdResult->isSuccess())
{
return $verifiedSubjectIdResult;
}
$verifiedSubjectIdList = $verifiedSubjectIdResult->getData();
$requestParams = [
'messageMatcher' => [
'name' => $name,
'channelType' => $this->getChannelType(),
'language' => $langCode,
'category' => 'UTILITY',
'type' => 'OPERATOR',
'contentType' => 'TEXT',
'content' => [
'text' => $text,
'textExampleParams' => $examples
]
],
'subjectIds' => $verifiedSubjectIdList,
];
return $this->externalSender->callExternalMethod(Providers\Edna\Constants\Method::SEND_TEMPLATE, $requestParams);
}
protected function getChannelType(): string
{
return Providers\Edna\Constants\ChannelType::WHATSAPP;
}
protected function validateTemplateText(string $text): Result
{
$result = new Result();
if (mb_strlen($text) > 1024)
{
return $result->addError(new Error('The maximum number of characters is 1024'));
}
if (!preg_match('/^(?!.* {4,}).*$/ui', $text))
{
return $result->addError(new Error('The text cannot contain newlines and 4 spaces in a row'));
}
return $result;
}
protected function validateExamples(string $text, array $examples): Result
{
$result = new Result();
$variables = [];
preg_match_all('/{{[0-9]+}}/ui', $text, $variables);
if (count($variables[0]) !== count($examples))
{
return $result->addError(new Error('The number of examples differs from the number of variables'));
}
return $result;
}
public static function cleanTemplatesCache(): void
{
$cache = Cache::createInstance();
$cache->clean(
\Bitrix\MessageService\Providers\Edna\WhatsApp\Utils::CACHE_KEY_TEMPLATES,
\Bitrix\MessageService\Providers\Edna\WhatsApp\Utils::CACHE_DIR_TEMPLATES
);
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Bitrix\MessageService\Providers;
use Bitrix\Main\Security\Cipher;
trait Encryptor
{
protected static function encrypt(string $data, string $cryptoKey): string
{
$encryptedData = '';
try
{
$cipher = new Cipher();
$encryptedData = $cipher->encrypt($data, $cryptoKey);
$encryptedData = \base64_encode($encryptedData);
}
catch (\Bitrix\Main\Security\SecurityException $e)
{}
return $encryptedData;
}
protected static function decrypt(string $encryptedData, string $cryptoKey): string
{
$decryptedData = '';
try
{
$cipher = new Cipher();
$decryptedData = base64_decode($encryptedData);
$decryptedData = $cipher->decrypt($decryptedData, $cryptoKey);
}
catch(\Bitrix\Main\Security\SecurityException $e)
{}
return $decryptedData;
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Bitrix\MessageService\Providers;
use Bitrix\MessageService\Sender\Result\HttpRequestResult;
interface ExternalSender
{
public function callExternalMethod(string $method, ?array $requestParams = null, string $httpMethod = ''): HttpRequestResult;
public function setApiKey(string $apiKey): self;
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Bitrix\MessageService\Providers;
interface Informant
{
public function isConfigurable(): bool;
public function getType(): string;
public function getId(): string;
public function getName(): string;
public function getShortName(): string;
public function getManageUrl(): string;
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Bitrix\MessageService\Providers;
interface Initiator
{
public function getFromList(): array;
public function getDefaultFrom(): ?string;
public function getFirstFromList();
public function isCorrectFrom(string $from): bool;
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Bitrix\MessageService\Providers;
interface OptionManager
{
public function setOptions(array $options): self;
public function setOption(string $optionName, $optionValue): self;
public function getOptions(): array;
public function getOption(string $optionName, $defaultValue = null);
public function clearOptions(): self;
public function setStreamTimeout(int $streamTimeout): self;
public function getStreamTimeout(): int;
public function setSocketTimeout(int $socketTimeout): self;
public function getSocketTimeout(): int;
public function getProviderId(): string;
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Bitrix\MessageService\Providers;
use \Bitrix\Main\Result;
interface Registrar
{
public function isRegistered(): bool;
public function isConfirmed(): bool;
public function register(array $fields): Result;
public function confirmRegistration(array $fields): Result;
public function sendConfirmationCode(): Result;
public function sync(): Registrar;
public function getCallbackUrl(): string;
public function getOwnerInfo(): array;
public function getExternalManageUrl(): string;
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Bitrix\MessageService\Providers;
use Bitrix\MessageService\Sender\Result\MessageStatus;
use Bitrix\MessageService\Sender\Result\SendMessage;
interface Sender
{
public function sendMessage(array $messageFields): SendMessage;
public function prepareMessageBodyForSave(string $text): string;
public function prepareMessageBodyForSend(string $text): string;
public function getMessageStatus(array $messageFields): MessageStatus;
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Bitrix\MessageService\Providers;
interface StatusResolver
{
public function resolveStatus(string $serviceStatus): ?int;
}

View File

@@ -0,0 +1,9 @@
<?php
namespace Bitrix\MessageService\Providers;
interface SupportChecker
{
public function isSupported(): bool;
public function canUse(): bool;
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Bitrix\MessageService\Providers;
interface TemplateManager
{
public function getTemplatesList(array $context = null): array;
public function prepareTemplate($templateData): array;
public function isTemplatesBased(): bool;
public function getConfigComponentTemplatePageName(): string;
}

View File

@@ -0,0 +1,67 @@
<?php
namespace Bitrix\MessageService\Providers\Twilio;
use Bitrix\Main\Error;
use Bitrix\Main\Localization\Loc;
class ErrorInformant
{
protected ?int $code;
protected int $httpStatus;
protected ?string $message;
protected ?string $moreInfo;
/**
* @param int|null $code
* @param string|null $message
* @param string|null $moreInfo
*/
public function __construct(?string $message, ?int $code, ?string $moreInfo, int $httpStatus)
{
$this->code = $code;
$this->message = $message;
$this->moreInfo = $moreInfo;
$this->httpStatus = $httpStatus;
}
public function getError(): Error
{
return new Error($this->getErrorMessage(), $this->code ?? 0);
}
protected function getErrorMessage(): string
{
$str = Loc::getMessage('MESSAGESERVICE_PROVIDER_TWILIO_ERROR_INFORMANT_ERROR', [
'#BR#' => '<br>',
]);
if (isset($this->moreInfo))
{
$str .= Loc::getMessage('MESSAGESERVICE_PROVIDER_TWILIO_ERROR_INFORMANT_ERROR_MORE', [
'#LINKSTART#' => '<a href="' . $this->moreInfo . '" target="_blank">',
'#INFO#' => $this->moreInfo,
'#LINKEND#' => '</a>',
]);
return $str;
}
if (isset($this->message, $this->code))
{
$str .= Loc::getMessage('MESSAGESERVICE_PROVIDER_TWILIO_ERROR_INFORMANT_ERROR_CODE', [
'#CODE#' => $this->code,
'#MESSAGE#' => $this->message,
]);
return $str;
}
$str .= Loc::getMessage('MESSAGESERVICE_PROVIDER_TWILIO_ERROR_INFORMANT_ERROR_HTTP_STATUS', [
'#STATUS#' => $this->httpStatus,
]);
return $str;
}
}