Initial commit: CloudOps infrastructure platform
This commit is contained in:
@@ -0,0 +1,389 @@
|
||||
.phone-preview__button--outer {
|
||||
background: var(--text-primary);
|
||||
}
|
||||
|
||||
.phone-preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: solid 10px var(--text-primary);
|
||||
width: 336px;
|
||||
height: 725px;
|
||||
padding: 0;
|
||||
border-radius: 50px;
|
||||
background: var(--background);
|
||||
box-shadow: 0 0 0 5px var(--text-primary);
|
||||
position: relative;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.phone-preview__silence-switch,
|
||||
.phone-preview__volume-rocker--top,
|
||||
.phone-preview__volume-rocker--bottom {
|
||||
position: absolute;
|
||||
margin-left: -19px;
|
||||
width: 4px;
|
||||
border-radius: 10px 0 0 10px;
|
||||
}
|
||||
|
||||
.phone-preview__silence-switch {
|
||||
margin-top: 90px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.phone-preview__volume-rocker--top {
|
||||
margin-top: 140px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.phone-preview__volume-rocker--bottom {
|
||||
margin-top: 200px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.phone-preview__power-button {
|
||||
position: absolute;
|
||||
margin-top: 200px;
|
||||
width: 4px;
|
||||
height: 70px;
|
||||
border-radius: 0 10px 10px 0;
|
||||
right: -18px;
|
||||
}
|
||||
|
||||
.phone-preview__top-section {
|
||||
background: var(--layer-01);
|
||||
min-height: 100px;
|
||||
margin: 0;
|
||||
border-radius: 40px 40px 0 0;
|
||||
border-bottom: 1px solid var(--border-subtle-01);
|
||||
}
|
||||
|
||||
.phone-preview__top-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-inline: 20px;
|
||||
}
|
||||
|
||||
.phone-preview__time {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.phone-preview__notch {
|
||||
width: 180px;
|
||||
background: var(--text-primary);
|
||||
height: 20px;
|
||||
border-radius: 0 0 20px 20px;
|
||||
padding-top: 5px;
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
.phone-preview__user-avatar,
|
||||
.phone-preview__user-name {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.phone-preview__user-avatar {
|
||||
margin-top: 10px;
|
||||
width: 30px;
|
||||
height: 40px;
|
||||
border-radius: 100%;
|
||||
font-size: 40px;
|
||||
color: var(--text-placeholder);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.phone-preview__user-name {
|
||||
margin-top: 3px;
|
||||
height: 10px;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.phone-preview__speaker,
|
||||
.phone-preview__front-camera {
|
||||
height: 5px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.phone-preview__speaker {
|
||||
width: 60px;
|
||||
background: var(--text-secondary);
|
||||
margin-left: 55px;
|
||||
}
|
||||
|
||||
.phone-preview__front-camera {
|
||||
width: 5px;
|
||||
background: var(--support-info);
|
||||
margin-left: 70px;
|
||||
}
|
||||
|
||||
.phone-preview__status-icons {
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.phone-preview__back-arrow {
|
||||
position: absolute;
|
||||
border: solid var(--primary-60);
|
||||
border-width: 0 3px 3px 0;
|
||||
display: inline-block;
|
||||
padding: 5px;
|
||||
margin: 45px 0 0 13px;
|
||||
border-radius: 2px;
|
||||
opacity: 0.9;
|
||||
transform: rotate(135deg);
|
||||
}
|
||||
|
||||
.phone-preview__back-arrow:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.phone-preview__messages {
|
||||
margin-top: 18px;
|
||||
background: var(--background);
|
||||
height: 305px;
|
||||
overflow: scroll;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.phone-preview__messages::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.phone-preview__keyboard {
|
||||
background: var(--layer-accent-01);
|
||||
border-radius: 0 0 40px 40px;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.phone-preview__keyboard-input {
|
||||
background: var(--background);
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.phone-preview__input-field {
|
||||
border-radius: 35px;
|
||||
border: 1px solid var(--border-subtle-01);
|
||||
padding: 8px;
|
||||
width: 100%;
|
||||
height: 28px;
|
||||
margin: 0 10px;
|
||||
overflow: scroll;
|
||||
caret-color: var(--primary-60);
|
||||
}
|
||||
|
||||
.phone-preview__input-addon {
|
||||
margin-left: 10px;
|
||||
color: var(--text-placeholder);
|
||||
font-size: 25px;
|
||||
}
|
||||
|
||||
.phone-preview__bottom-symbols {
|
||||
margin: 20px auto 0;
|
||||
font-size: 25px;
|
||||
color: var(--text-secondary);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding-inline: 20px;
|
||||
}
|
||||
|
||||
.phone-preview__keypad {
|
||||
background: var(--layer-accent-01);
|
||||
width: 100%;
|
||||
height: 190px;
|
||||
padding-top: 16px;
|
||||
user-select: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.phone-preview__key-spacer {
|
||||
width: 336px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.phone-preview__key--white,
|
||||
.phone-preview__key--space {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
background: var(--background);
|
||||
border-radius: 6px;
|
||||
padding: 9px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
border-bottom: 1px solid var(--border-strong-01);
|
||||
height: 35px;
|
||||
transition: var(--transition-all-productive);
|
||||
}
|
||||
|
||||
.phone-preview__key--white {
|
||||
margin: 0;
|
||||
width: 27px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.phone-preview__key--space {
|
||||
margin: 1px;
|
||||
width: 142px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.phone-preview__key-row {
|
||||
display: flex;
|
||||
gap: 3px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.phone-preview__key--grey {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--border-tile-01);
|
||||
border-radius: 6px;
|
||||
margin: 0;
|
||||
padding: 9px;
|
||||
width: auto;
|
||||
height: 35px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
border-bottom: 1px solid var(--border-strong-01);
|
||||
font-size: 13px;
|
||||
transition: var(--transition-all-productive);
|
||||
}
|
||||
|
||||
.phone-preview__key--grey:hover,
|
||||
.phone-preview__key--clickable:hover {
|
||||
background: var(--layer-accent-hover-02);
|
||||
}
|
||||
|
||||
.phone-preview__key--backspace {
|
||||
width: 40px;
|
||||
padding: 8px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.phone-preview__key--backspace > i {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.phone-preview__home-button {
|
||||
margin: 15px auto 4px;
|
||||
border-radius: 50px;
|
||||
width: 125px;
|
||||
height: 5px;
|
||||
background: var(--text-primary);
|
||||
}
|
||||
|
||||
.phone-preview__cursor {
|
||||
height: 25px;
|
||||
position: absolute;
|
||||
opacity: 1;
|
||||
color: var(--primary-60);
|
||||
padding: 2px;
|
||||
font-size: 22px;
|
||||
right: 10px;
|
||||
top: 26%;
|
||||
}
|
||||
|
||||
.phone-preview__send-button:hover,
|
||||
.phone-preview__cursor:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.phone-preview__send-button:active,
|
||||
.phone-preview__cursor:active {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.phone-preview__message {
|
||||
border-radius: 20px;
|
||||
margin: 0 15px 10px;
|
||||
padding: 10px 15px;
|
||||
position: relative;
|
||||
animation: fadeInOpacity 0.4s ease-in;
|
||||
}
|
||||
|
||||
@keyframes fadeInOpacity {
|
||||
0% { opacity: 0; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
.phone-preview__message--to {
|
||||
background-color: var(--primary-60);
|
||||
color: var(--text-on-color);
|
||||
margin-left: 80px;
|
||||
transition: width 2s;
|
||||
}
|
||||
|
||||
.phone-preview__message--from {
|
||||
background-color: var(--layer-accent-01);
|
||||
color: var(--text-primary);
|
||||
margin-right: 80px;
|
||||
}
|
||||
|
||||
.phone-preview__message-content {
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.phone-preview__message--to:before {
|
||||
content: "";
|
||||
width: 0;
|
||||
height: 0;
|
||||
left: 93%;
|
||||
top: 30%;
|
||||
position: absolute;
|
||||
border-top: 13px solid transparent;
|
||||
border-left: 26px solid var(--primary-60);
|
||||
border-bottom: 13px solid transparent;
|
||||
border-radius: 40px;
|
||||
transform: rotate(40deg);
|
||||
}
|
||||
|
||||
.phone-preview__message--from:before {
|
||||
content: "";
|
||||
width: 0;
|
||||
height: 0;
|
||||
right: 92%;
|
||||
top: 35%;
|
||||
position: absolute;
|
||||
border-top: 13px solid transparent;
|
||||
border-right: 26px solid var(--layer-accent-01);
|
||||
border-bottom: 13px solid transparent;
|
||||
border-radius: 40px;
|
||||
transform: rotate(-40deg);
|
||||
}
|
||||
|
||||
textarea:focus,
|
||||
input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.phone-preview__key--shift {
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.phone-preview__key--emoji {
|
||||
font-size: 16px;
|
||||
padding: 5px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.phone-preview__key--return {
|
||||
padding: 0 10px;
|
||||
width: 60px;
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 8.2 KiB |
@@ -0,0 +1,115 @@
|
||||
/** SmsBundle **/
|
||||
Mautic.smsOnLoad = function (container, response) {
|
||||
const smsMessage = mQuery('#sms_message');
|
||||
|
||||
if (smsMessage.length) {
|
||||
Mautic.setSmsCharactersCount(smsMessage);
|
||||
smsMessage.on('input', () => {
|
||||
Mautic.setSmsCharactersCount(smsMessage);
|
||||
});
|
||||
}
|
||||
|
||||
if (mQuery(container + ' #list-search').length) {
|
||||
Mautic.activateSearchAutocomplete('list-search', 'sms');
|
||||
}
|
||||
|
||||
if (mQuery('table.sms-list').length) {
|
||||
var ids = [];
|
||||
mQuery('td.col-stats').each(function () {
|
||||
var id = mQuery(this).attr('data-stats');
|
||||
ids.push(id);
|
||||
});
|
||||
|
||||
// Get all stats numbers in batches of 10
|
||||
while (ids.length > 0) {
|
||||
let batchIds = ids.splice(0, 10);
|
||||
Mautic.ajaxActionRequest(
|
||||
'sms:getSmsCountStats',
|
||||
{ids: batchIds},
|
||||
function (response) {
|
||||
if (response.success && response.stats) {
|
||||
for (var i = 0; i < response.stats.length; i++) {
|
||||
var stat = response.stats[i];
|
||||
if (mQuery('#pending-' + stat.id).length) {
|
||||
if (stat.pending) {
|
||||
mQuery('#pending-' + stat.id + ' > a').html(stat.pending);
|
||||
mQuery('#pending-' + stat.id).removeClass('hide');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
false,
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Mautic.initSmsAtWho();
|
||||
};
|
||||
|
||||
Mautic.setSmsCharactersCount = function (smsMessage) {
|
||||
mQuery('#sms_nb_char').text((smsMessage.val().length))
|
||||
};
|
||||
|
||||
|
||||
Mautic.initSmsAtWho = function () {
|
||||
var smsMessage = mQuery('#sms_message, #send_sms_message');
|
||||
smsMessage.each(function () {
|
||||
var obj = mQuery(this);
|
||||
var callbackAttr = obj.attr('data-token-callback');
|
||||
if (typeof callbackAttr == 'undefined') {
|
||||
obj.attr('data-token-callback', 'sms:getBuilderTokens');
|
||||
obj.attr('data-token-activator', '{');
|
||||
obj.attr('data-token-visual', 'false');
|
||||
Mautic.initAtWho(obj, obj.attr('data-token-callback'));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Mautic.selectSmsType = function(smsType) {
|
||||
if (smsType == 'list') {
|
||||
mQuery('#leadList').removeClass('hide');
|
||||
mQuery('#publishStatus').addClass('hide');
|
||||
mQuery('.page-header h3').text(mauticLang.newListSms);
|
||||
} else {
|
||||
mQuery('#publishStatus').removeClass('hide');
|
||||
mQuery('#leadList').addClass('hide');
|
||||
mQuery('.page-header h3').text(mauticLang.newTemplateSms);
|
||||
}
|
||||
|
||||
mQuery('#sms_smsType').val(smsType);
|
||||
|
||||
mQuery('body').removeClass('noscroll');
|
||||
|
||||
mQuery('.sms-type-modal').remove();
|
||||
mQuery('.sms-type-modal-backdrop').remove();
|
||||
};
|
||||
|
||||
Mautic.standardSmsUrl = function(options) {
|
||||
if (!options) {
|
||||
return;
|
||||
}
|
||||
|
||||
var url = options.windowUrl;
|
||||
if (url) {
|
||||
var editEmailKey = '/sms/edit/smsId';
|
||||
if (url.indexOf(editEmailKey) > -1) {
|
||||
options.windowUrl = url.replace('smsId', mQuery('#campaignevent_properties_sms').val());
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
};
|
||||
|
||||
Mautic.disabledSmsAction = function(opener) {
|
||||
if (typeof opener == 'undefined') {
|
||||
opener = window;
|
||||
}
|
||||
|
||||
var sms = opener.mQuery('#campaignevent_properties_sms').val();
|
||||
|
||||
var disabled = sms === '' || sms === null;
|
||||
|
||||
opener.mQuery('#campaignevent_properties_editSmsButton').prop('disabled', disabled);
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\Broadcast;
|
||||
|
||||
use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter;
|
||||
use Mautic\ChannelBundle\Event\ChannelBroadcastEvent;
|
||||
use Mautic\LeadBundle\Entity\LeadRepository;
|
||||
use Mautic\SmsBundle\Entity\Sms;
|
||||
use Mautic\SmsBundle\Model\SmsModel;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class BroadcastExecutioner
|
||||
{
|
||||
private ?ContactLimiter $contactLimiter = null;
|
||||
|
||||
private ?BroadcastResult $result = null;
|
||||
|
||||
public function __construct(
|
||||
private SmsModel $smsModel,
|
||||
private BroadcastQuery $broadcastQuery,
|
||||
private TranslatorInterface $translator,
|
||||
private LeadRepository $leadRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
public function execute(ChannelBroadcastEvent $event): void
|
||||
{
|
||||
// Get list of published broadcasts or broadcast if there is only a single ID
|
||||
$smses = $this->smsModel->getRepository()->getPublishedBroadcastsIterable($event->getId());
|
||||
foreach ($smses as $sms) {
|
||||
$this->contactLimiter = new ContactLimiter($event->getBatch(), null, $event->getMinContactIdFilter(), $event->getMaxContactIdFilter(), [], $event->getThreadId(), $event->getMaxThreads(), $event->getLimit());
|
||||
$this->result = new BroadcastResult();
|
||||
try {
|
||||
$this->send($sms);
|
||||
} catch (\Exception) {
|
||||
}
|
||||
$event->setResults(
|
||||
sprintf('%s: %s', $this->translator->trans('mautic.sms.sms'), $sms->getName()),
|
||||
$this->result->getSentCount(),
|
||||
$this->result->getFailedCount()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws LimitQuotaException
|
||||
* @throws \Mautic\CampaignBundle\Executioner\Exception\NoContactsFoundException
|
||||
*/
|
||||
private function send(Sms $sms): void
|
||||
{
|
||||
$contacts = $this->broadcastQuery->getPendingContacts($sms, $this->contactLimiter);
|
||||
while (!empty($contacts)) {
|
||||
$reduction = 0;
|
||||
$leads = [];
|
||||
foreach ($contacts as $contact) {
|
||||
$contactId = $contact['id'];
|
||||
$results = $this->smsModel->sendSms($sms, $contactId, [
|
||||
'channel'=> [
|
||||
'sms', $sms->getId(),
|
||||
],
|
||||
'listId'=> $contact['listId'],
|
||||
], $leads);
|
||||
$this->result->process($results);
|
||||
$reduction += count($results);
|
||||
}
|
||||
|
||||
$this->contactLimiter->setBatchMinContactId($contactId + 1);
|
||||
|
||||
if ($this->contactLimiter->hasCampaignLimit()) {
|
||||
$this->contactLimiter->reduceCampaignLimitRemaining($reduction);
|
||||
}
|
||||
|
||||
$this->leadRepository->detachEntities($leads);
|
||||
|
||||
// Next batch
|
||||
$contacts = $this->broadcastQuery->getPendingContacts($sms, $this->contactLimiter);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\Broadcast;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Mautic\CampaignBundle\Entity\ContactLimiterTrait;
|
||||
use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter;
|
||||
use Mautic\ChannelBundle\Entity\MessageQueue;
|
||||
use Mautic\SmsBundle\Entity\Sms;
|
||||
use Mautic\SmsBundle\Model\SmsModel;
|
||||
|
||||
class BroadcastQuery
|
||||
{
|
||||
use ContactLimiterTrait;
|
||||
|
||||
/**
|
||||
* @var \Doctrine\DBAL\Query\QueryBuilder
|
||||
*/
|
||||
private $query;
|
||||
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
private SmsModel $smsModel,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getPendingContacts(Sms $sms, ContactLimiter $contactLimiter): array
|
||||
{
|
||||
$query = $this->getBasicQuery($sms);
|
||||
$query->select('DISTINCT l.id, ll.id as listId');
|
||||
$this->updateQueryFromContactLimiter('lll', $query, $contactLimiter);
|
||||
|
||||
return $query->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool|string
|
||||
*/
|
||||
public function getPendingCount(Sms $sms)
|
||||
{
|
||||
$query = $this->getBasicQuery($sms);
|
||||
$query->select('COUNT(DISTINCT l.id)');
|
||||
|
||||
return $query->executeQuery()->fetchOne();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Doctrine\DBAL\Query\QueryBuilder
|
||||
*/
|
||||
private function getBasicQuery(Sms $sms)
|
||||
{
|
||||
$this->query = $this->smsModel->getRepository()->getSegmentsContactsQuery($sms->getId());
|
||||
$this->query->andWhere(
|
||||
$this->query->expr()->or(
|
||||
$this->query->expr()->or(
|
||||
$this->query->expr()->isNotNull('l.mobile'),
|
||||
$this->query->expr()->neq('l.mobile', $this->query->expr()->literal(''))
|
||||
),
|
||||
$this->query->expr()->or(
|
||||
$this->query->expr()->isNotNull('l.phone'),
|
||||
$this->query->expr()->neq('l.phone', $this->query->expr()->literal(''))
|
||||
)
|
||||
)
|
||||
);
|
||||
$this->excludeStatsRecords($sms->getId());
|
||||
$this->excludeDnc();
|
||||
$this->excludeQueue();
|
||||
|
||||
return $this->query;
|
||||
}
|
||||
|
||||
private function excludeStatsRecords(int $smsId): void
|
||||
{
|
||||
// Do not include leads that have already received text message
|
||||
$statQb = $this->entityManager->getConnection()->createQueryBuilder();
|
||||
$statQb->select('null')
|
||||
->from(MAUTIC_TABLE_PREFIX.'sms_message_stats', 'stat')
|
||||
->where(
|
||||
$statQb->expr()->and(
|
||||
$statQb->expr()->eq('stat.lead_id', 'l.id'),
|
||||
$statQb->expr()->eq('stat.sms_id', $smsId)
|
||||
)
|
||||
);
|
||||
|
||||
$this->query->andWhere(sprintf('NOT EXISTS (%s)', $statQb->getSQL()));
|
||||
}
|
||||
|
||||
private function excludeDnc(): void
|
||||
{
|
||||
// Do not include leads in the do not contact table
|
||||
$dncQb = $this->entityManager->getConnection()->createQueryBuilder();
|
||||
$dncQb->select('null')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_donotcontact', 'dnc')
|
||||
->where(
|
||||
$dncQb->expr()->and(
|
||||
$dncQb->expr()->eq('dnc.lead_id', 'l.id'),
|
||||
$dncQb->expr()->eq('dnc.channel', $dncQb->expr()->literal('sms'))
|
||||
)
|
||||
);
|
||||
$this->query->andWhere(sprintf('NOT EXISTS (%s)', $dncQb->getSQL()));
|
||||
}
|
||||
|
||||
private function excludeQueue(): void
|
||||
{
|
||||
// Do not include contacts where the message is pending in the message queue
|
||||
$mqQb = $this->entityManager->getConnection()->createQueryBuilder();
|
||||
$mqQb->select('null')
|
||||
->from(MAUTIC_TABLE_PREFIX.'message_queue', 'mq')
|
||||
->where(
|
||||
$mqQb->expr()->and(
|
||||
$mqQb->expr()->eq('mq.lead_id', 'l.id'),
|
||||
$mqQb->expr()->neq('mq.status', $mqQb->expr()->literal(MessageQueue::STATUS_SENT)),
|
||||
$mqQb->expr()->eq('mq.channel', $mqQb->expr()->literal('sms'))
|
||||
)
|
||||
);
|
||||
$this->query->andWhere(sprintf('NOT EXISTS (%s)', $mqQb->getSQL()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\Broadcast;
|
||||
|
||||
class BroadcastResult
|
||||
{
|
||||
private int $sentCount = 0;
|
||||
|
||||
private int $failedCount = 0;
|
||||
|
||||
public function process(array $results): void
|
||||
{
|
||||
foreach ($results as $result) {
|
||||
if (isset($result['sent']) && true === $result['sent']) {
|
||||
$this->sent();
|
||||
} else {
|
||||
$this->failed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function sent(): void
|
||||
{
|
||||
++$this->sentCount;
|
||||
}
|
||||
|
||||
public function failed(): void
|
||||
{
|
||||
++$this->failedCount;
|
||||
}
|
||||
|
||||
public function getSentCount(): int
|
||||
{
|
||||
return $this->sentCount;
|
||||
}
|
||||
|
||||
public function getFailedCount(): int
|
||||
{
|
||||
return $this->failedCount;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\Callback;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Mautic\SmsBundle\Exception\NumberNotFoundException;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
interface CallbackInterface
|
||||
{
|
||||
/**
|
||||
* Returns a "transport" string to match the URL path /sms/{transport}/callback.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTransportName();
|
||||
|
||||
/**
|
||||
* Return all contacts that match whatever identifiers the service provides (likely number).
|
||||
*
|
||||
* @return ArrayCollection
|
||||
*
|
||||
* @throws NumberNotFoundException
|
||||
* @throws BadRequestHttpException
|
||||
* @throws NotFoundHttpException
|
||||
*/
|
||||
public function getContacts(Request $request);
|
||||
|
||||
/**
|
||||
* Extract the message in the reply from the request.
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @throws BadRequestHttpException
|
||||
* @throws NotFoundHttpException
|
||||
*/
|
||||
public function getMessage(Request $request);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\Callback;
|
||||
|
||||
use Mautic\SmsBundle\Exception\CallbackHandlerNotFound;
|
||||
|
||||
class HandlerContainer
|
||||
{
|
||||
/**
|
||||
* @var CallbackInterface[]
|
||||
*/
|
||||
private ?array $handlers = null;
|
||||
|
||||
public function registerHandler(CallbackInterface $handler): void
|
||||
{
|
||||
$this->handlers[$handler->getTransportName()] = $handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return CallbackInterface
|
||||
*
|
||||
* @throws CallbackHandlerNotFound
|
||||
*/
|
||||
public function getHandler($transportName)
|
||||
{
|
||||
if (!isset($this->handlers[$transportName])) {
|
||||
throw new CallbackHandlerNotFound("$transportName has not been registered");
|
||||
}
|
||||
|
||||
return $this->handlers[$transportName];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'services' => [
|
||||
'helpers' => [
|
||||
'mautic.helper.sms' => [
|
||||
'class' => Mautic\SmsBundle\Helper\SmsHelper::class,
|
||||
'arguments' => [
|
||||
'doctrine.orm.entity_manager',
|
||||
'mautic.lead.model.lead',
|
||||
'mautic.helper.phone_number',
|
||||
'mautic.sms.model.sms',
|
||||
'mautic.helper.integration',
|
||||
'mautic.lead.model.dnc',
|
||||
'mautic.helper.core_parameters',
|
||||
],
|
||||
'alias' => 'sms_helper',
|
||||
],
|
||||
],
|
||||
'other' => [
|
||||
'mautic.sms.transport_chain' => [
|
||||
'class' => Mautic\SmsBundle\Sms\TransportChain::class,
|
||||
'arguments' => [
|
||||
'%mautic.sms_transport%',
|
||||
'mautic.helper.integration',
|
||||
'monolog.logger.mautic',
|
||||
],
|
||||
],
|
||||
'mautic.sms.callback_handler_container' => [
|
||||
'class' => Mautic\SmsBundle\Callback\HandlerContainer::class,
|
||||
],
|
||||
'mautic.sms.helper.contact' => [
|
||||
'class' => Mautic\SmsBundle\Helper\ContactHelper::class,
|
||||
'arguments' => [
|
||||
'mautic.lead.repository.lead',
|
||||
'doctrine.dbal.default_connection',
|
||||
'mautic.helper.phone_number',
|
||||
],
|
||||
],
|
||||
'mautic.sms.helper.reply' => [
|
||||
'class' => Mautic\SmsBundle\Helper\ReplyHelper::class,
|
||||
'arguments' => [
|
||||
'event_dispatcher',
|
||||
'monolog.logger.mautic',
|
||||
'mautic.tracker.contact',
|
||||
],
|
||||
],
|
||||
'mautic.sms.twilio.configuration' => [
|
||||
'class' => Mautic\SmsBundle\Integration\Twilio\Configuration::class,
|
||||
'arguments' => [
|
||||
'mautic.helper.integration',
|
||||
],
|
||||
],
|
||||
'mautic.sms.twilio.transport' => [
|
||||
'class' => Mautic\SmsBundle\Integration\Twilio\TwilioTransport::class,
|
||||
'arguments' => [
|
||||
'mautic.sms.twilio.configuration',
|
||||
'monolog.logger.mautic',
|
||||
],
|
||||
'tag' => 'mautic.sms_transport',
|
||||
'tagArguments' => [
|
||||
'integrationAlias' => 'Twilio',
|
||||
],
|
||||
'serviceAliases' => [
|
||||
'sms_api',
|
||||
'mautic.sms.api',
|
||||
],
|
||||
],
|
||||
'mautic.sms.twilio.callback' => [
|
||||
'class' => Mautic\SmsBundle\Integration\Twilio\TwilioCallback::class,
|
||||
'arguments' => [
|
||||
'mautic.sms.helper.contact',
|
||||
'mautic.sms.twilio.configuration',
|
||||
],
|
||||
'tag' => 'mautic.sms_callback_handler',
|
||||
],
|
||||
'mautic.sms.broadcast.executioner' => [
|
||||
'class' => Mautic\SmsBundle\Broadcast\BroadcastExecutioner::class,
|
||||
'arguments' => [
|
||||
'mautic.sms.model.sms',
|
||||
'mautic.sms.broadcast.query',
|
||||
'translator',
|
||||
'mautic.lead.repository.lead',
|
||||
],
|
||||
],
|
||||
'mautic.sms.broadcast.query' => [
|
||||
'class' => Mautic\SmsBundle\Broadcast\BroadcastQuery::class,
|
||||
'arguments' => [
|
||||
'doctrine.orm.entity_manager',
|
||||
'mautic.sms.model.sms',
|
||||
],
|
||||
],
|
||||
],
|
||||
'integrations' => [
|
||||
'mautic.integration.twilio' => [
|
||||
'class' => Mautic\SmsBundle\Integration\TwilioIntegration::class,
|
||||
'arguments' => [
|
||||
'event_dispatcher',
|
||||
'mautic.helper.cache_storage',
|
||||
'doctrine.orm.entity_manager',
|
||||
'request_stack',
|
||||
'router',
|
||||
'translator',
|
||||
'monolog.logger.mautic',
|
||||
'mautic.helper.encryption',
|
||||
'mautic.lead.model.lead',
|
||||
'mautic.lead.model.company',
|
||||
'mautic.helper.paths',
|
||||
'mautic.core.model.notification',
|
||||
'mautic.lead.model.field',
|
||||
'mautic.plugin.model.integration_entity',
|
||||
'mautic.lead.model.dnc',
|
||||
'mautic.lead.field.fields_with_unique_identifier',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'routes' => [
|
||||
'main' => [
|
||||
'mautic_sms_index' => [
|
||||
'path' => '/sms/{page}',
|
||||
'controller' => 'Mautic\SmsBundle\Controller\SmsController::indexAction',
|
||||
],
|
||||
'mautic_sms_action' => [
|
||||
'path' => '/sms/{objectAction}/{objectId}',
|
||||
'controller' => 'Mautic\SmsBundle\Controller\SmsController::executeAction',
|
||||
],
|
||||
'mautic_sms_contacts' => [
|
||||
'path' => '/sms/view/{objectId}/contact/{page}',
|
||||
'controller' => 'Mautic\SmsBundle\Controller\SmsController::contactsAction',
|
||||
],
|
||||
],
|
||||
'public' => [
|
||||
'mautic_sms_callback' => [
|
||||
'path' => '/sms/{transport}/callback',
|
||||
'controller' => 'Mautic\SmsBundle\Controller\ReplyController::callbackAction',
|
||||
],
|
||||
/* @deprecated as this was Twilio specific */
|
||||
'mautic_receive_sms' => [
|
||||
'path' => '/sms/receive',
|
||||
'controller' => 'Mautic\SmsBundle\Controller\ReplyController::callbackAction',
|
||||
'defaults' => [
|
||||
'transport' => 'twilio',
|
||||
],
|
||||
],
|
||||
],
|
||||
'api' => [
|
||||
'mautic_api_smsesstandard' => [
|
||||
'standard_entity' => true,
|
||||
'name' => 'smses',
|
||||
'path' => '/smses',
|
||||
'controller' => Mautic\SmsBundle\Controller\Api\SmsApiController::class,
|
||||
],
|
||||
'mautic_api_smses_send' => [
|
||||
'path' => '/smses/{id}/contact/{contactId}/send',
|
||||
'controller' => 'Mautic\SmsBundle\Controller\Api\SmsApiController::sendAction',
|
||||
],
|
||||
],
|
||||
],
|
||||
'menu' => [
|
||||
'main' => [
|
||||
'items' => [
|
||||
'mautic.sms.smses' => [
|
||||
'route' => 'mautic_sms_index',
|
||||
'access' => ['sms:smses:viewown', 'sms:smses:viewother'],
|
||||
'parent' => 'mautic.core.channels',
|
||||
'checks' => [
|
||||
'integration' => [
|
||||
'Twilio' => [
|
||||
'enabled' => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
'priority' => 70,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'categories' => [
|
||||
'sms' => null,
|
||||
],
|
||||
'parameters' => [
|
||||
'sms_enabled' => false,
|
||||
'sms_username' => null,
|
||||
'sms_password' => null,
|
||||
'sms_messaging_service_sid' => null,
|
||||
'sms_frequency_number' => 0,
|
||||
'sms_frequency_time' => 'DAY',
|
||||
'sms_transport' => 'mautic.sms.twilio.transport',
|
||||
Mautic\SmsBundle\Form\Type\ConfigType::SMS_DISABLE_TRACKABLE_URLS => false,
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Mautic\CoreBundle\DependencyInjection\MauticCoreExtension;
|
||||
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
|
||||
|
||||
return function (ContainerConfigurator $configurator): void {
|
||||
$services = $configurator->services()
|
||||
->defaults()
|
||||
->autowire()
|
||||
->autoconfigure()
|
||||
->public();
|
||||
|
||||
$excludes = [
|
||||
];
|
||||
|
||||
$services->load('Mautic\\SmsBundle\\', '../')
|
||||
->exclude('../{'.implode(',', array_merge(MauticCoreExtension::DEFAULT_EXCLUDES, $excludes)).'}');
|
||||
|
||||
$services->load('Mautic\\SmsBundle\\Entity\\', '../Entity/*Repository.php')
|
||||
->tag(Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\ServiceRepositoryCompilerPass::REPOSITORY_SERVICE_TAG);
|
||||
|
||||
$services->alias('mautic.sms.model.sms', Mautic\SmsBundle\Model\SmsModel::class);
|
||||
$services->alias('mautic.sms.repository.stat', Mautic\SmsBundle\Entity\StatRepository::class);
|
||||
};
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Controller\AjaxController as CommonAjaxController;
|
||||
use Mautic\CoreBundle\Controller\AjaxLookupControllerTrait;
|
||||
use Mautic\CoreBundle\Helper\CacheStorageHelper;
|
||||
use Mautic\EmailBundle\Model\EmailModel;
|
||||
use Mautic\SmsBundle\Broadcast\BroadcastQuery;
|
||||
use Mautic\SmsBundle\Event\TokensBuildEvent;
|
||||
use Mautic\SmsBundle\Model\SmsModel;
|
||||
use Mautic\SmsBundle\SmsEvents;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class AjaxController extends CommonAjaxController
|
||||
{
|
||||
use AjaxLookupControllerTrait;
|
||||
|
||||
public function getSmsCountStatsAction(Request $request, BroadcastQuery $broadcastQuery, CacheStorageHelper $cacheStorageHelper): JsonResponse
|
||||
{
|
||||
/** @var SmsModel $model */
|
||||
$model = $this->getModel('sms');
|
||||
|
||||
$id = $request->get('id');
|
||||
$ids = $request->query->all()['ids'] ?? [];
|
||||
|
||||
// Support for legacy calls
|
||||
if (!$ids && $id) {
|
||||
$ids = [$id];
|
||||
}
|
||||
|
||||
$data = [];
|
||||
foreach ($ids as $id) {
|
||||
if ($sms = $model->getEntity($id)) {
|
||||
if ('list' !== $sms->getSmsType()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$pending = $broadcastQuery->getPendingCount($sms);
|
||||
$cacheStorageHelper->set(sprintf('%s|%s|%s', 'sms', $sms->getId(), 'pending'), $pending);
|
||||
if (!$pending) {
|
||||
continue;
|
||||
}
|
||||
$data[] = [
|
||||
'id' => $id,
|
||||
'pending' => $this->translator->trans(
|
||||
'mautic.sms.stat.leadcount',
|
||||
['%count%' => $pending]
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Support for legacy calls
|
||||
if ($request->get('id')) {
|
||||
$data = $data[0];
|
||||
} else {
|
||||
$data = [
|
||||
'success' => 1,
|
||||
'stats' => $data,
|
||||
];
|
||||
}
|
||||
|
||||
return new JsonResponse($data);
|
||||
}
|
||||
|
||||
public function getBuilderTokensAction(Request $request, ?EventDispatcherInterface $eventDispatcher = null): JsonResponse
|
||||
{
|
||||
$query = $request->get('query', '');
|
||||
$tokens = $this->getBuilderTokens($query);
|
||||
$event = new TokensBuildEvent($tokens);
|
||||
$eventDispatcher->dispatch($event, SmsEvents::ON_SMS_TOKENS_BUILD);
|
||||
|
||||
return $this->sendJsonResponse(['tokens'=>$event->getTokens()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Just selected get tokens from email builder.
|
||||
*
|
||||
* @param string|null $query
|
||||
*
|
||||
* @return array<string,array<int|string>>
|
||||
*/
|
||||
protected function getBuilderTokens($query): array
|
||||
{
|
||||
/** @var EmailModel $model */
|
||||
$model = $this->getModel('email');
|
||||
$components = $model->getBuilderComponents(null, ['tokens'], $query);
|
||||
$findTokens = ['{contactfield=', '{assetlink', '{pagelink'];
|
||||
$returnTokens = [];
|
||||
$tokens = $components['tokens'];
|
||||
|
||||
array_map(
|
||||
function ($token, $value) use ($findTokens, &$returnTokens): void {
|
||||
foreach ($findTokens as $findToken) {
|
||||
if (str_starts_with($token, $findToken)) {
|
||||
$returnTokens[$token] = $value;
|
||||
}
|
||||
}
|
||||
}, array_keys($tokens), $tokens);
|
||||
|
||||
return $returnTokens;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\Controller\Api;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Mautic\ApiBundle\Controller\CommonApiController;
|
||||
use Mautic\ApiBundle\Helper\EntityResultHelper;
|
||||
use Mautic\CoreBundle\Factory\ModelFactory;
|
||||
use Mautic\CoreBundle\Helper\AppVersion;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\LeadBundle\Controller\LeadAccessTrait;
|
||||
use Mautic\SmsBundle\Entity\Sms;
|
||||
use Mautic\SmsBundle\Model\SmsModel;
|
||||
use Mautic\SmsBundle\Sms\TransportChain;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
|
||||
/**
|
||||
* @extends CommonApiController<Sms>
|
||||
*/
|
||||
class SmsApiController extends CommonApiController
|
||||
{
|
||||
use LeadAccessTrait;
|
||||
|
||||
/**
|
||||
* @var SmsModel|null
|
||||
*/
|
||||
protected $model;
|
||||
|
||||
public function __construct(CorePermissions $security, Translator $translator, EntityResultHelper $entityResultHelper, RouterInterface $router, FormFactoryInterface $formFactory, AppVersion $appVersion, RequestStack $requestStack, ManagerRegistry $doctrine, ModelFactory $modelFactory, EventDispatcherInterface $dispatcher, CoreParametersHelper $coreParametersHelper)
|
||||
{
|
||||
$smsModel = $modelFactory->getModel('sms');
|
||||
\assert($smsModel instanceof SmsModel);
|
||||
|
||||
$this->model = $smsModel;
|
||||
$this->entityClass = Sms::class;
|
||||
$this->entityNameOne = 'sms';
|
||||
$this->entityNameMulti = 'smses';
|
||||
|
||||
$this->serializerGroups = [
|
||||
'smsDetails',
|
||||
'categoryList',
|
||||
'publishDetails',
|
||||
'leadListList',
|
||||
];
|
||||
|
||||
parent::__construct($security, $translator, $entityResultHelper, $router, $formFactory, $appVersion, $requestStack, $doctrine, $modelFactory, $dispatcher, $coreParametersHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return JsonResponse|Response
|
||||
*/
|
||||
public function sendAction(TransportChain $transportChain, LoggerInterface $mauticLogger, $id, $contactId)
|
||||
{
|
||||
if (!$transportChain->getEnabledTransports()) {
|
||||
return new JsonResponse(json_encode(['error' => ['message' => 'SMS transport is disabled.', 'code' => Response::HTTP_EXPECTATION_FAILED]]));
|
||||
}
|
||||
|
||||
$message = $this->model->getEntity((int) $id);
|
||||
|
||||
if (is_null($message)) {
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
$contact = $this->checkLeadAccess($contactId, 'edit');
|
||||
|
||||
if ($contact instanceof Response) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$mauticLogger->debug("Sending SMS #{$id} to contact #{$contactId}", ['originator' => 'api']);
|
||||
|
||||
try {
|
||||
$response = $this->model->sendSms($message, $contact, ['channel' => 'api'])[$contact->getId()];
|
||||
} catch (\Exception $e) {
|
||||
$mauticLogger->error($e->getMessage(), ['error' => (array) $e]);
|
||||
|
||||
return new Response('Interval server error', Response::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
$success = !empty($response['sent']);
|
||||
|
||||
if (!$success) {
|
||||
$mauticLogger->error('Failed to send SMS.', ['error' => $response['status']]);
|
||||
}
|
||||
|
||||
$view = $this->view(
|
||||
[
|
||||
'success' => $success,
|
||||
'status' => $this->translator->trans($response['status']),
|
||||
'result' => $response,
|
||||
'errors' => $success ? [] : [['message' => $response['status']]],
|
||||
],
|
||||
Response::HTTP_OK // 200 - is legacy, we cannot change it yet
|
||||
);
|
||||
|
||||
return $this->handleView($view);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\Controller;
|
||||
|
||||
use Mautic\SmsBundle\Callback\HandlerContainer;
|
||||
use Mautic\SmsBundle\Exception\CallbackHandlerNotFound;
|
||||
use Mautic\SmsBundle\Helper\ReplyHelper;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class ReplyController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private HandlerContainer $callbackHandler,
|
||||
private ReplyHelper $replyHelper,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Response
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function callbackAction(Request $request, $transport)
|
||||
{
|
||||
define('MAUTIC_NON_TRACKABLE_REQUEST', 1);
|
||||
|
||||
try {
|
||||
$handler = $this->callbackHandler->getHandler($transport);
|
||||
} catch (CallbackHandlerNotFound) {
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
||||
return $this->replyHelper->handleRequest($handler, $request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,768 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\Controller;
|
||||
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Mautic\CoreBundle\Controller\FormController;
|
||||
use Mautic\CoreBundle\Factory\PageHelperFactoryInterface;
|
||||
use Mautic\CoreBundle\Form\Type\DateRangeType;
|
||||
use Mautic\CoreBundle\Helper\InputHelper;
|
||||
use Mautic\CoreBundle\Model\AuditLogModel;
|
||||
use Mautic\LeadBundle\Controller\EntityContactsTrait;
|
||||
use Mautic\SmsBundle\Entity\Sms;
|
||||
use Mautic\SmsBundle\Model\SmsModel;
|
||||
use Mautic\SmsBundle\Sms\TransportChain;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class SmsController extends FormController
|
||||
{
|
||||
use EntityContactsTrait;
|
||||
|
||||
/**
|
||||
* @param int $page
|
||||
*
|
||||
* @return JsonResponse|Response
|
||||
*/
|
||||
public function indexAction(Request $request, TransportChain $transportChain, $page = 1)
|
||||
{
|
||||
/** @var SmsModel $model */
|
||||
$model = $this->getModel('sms');
|
||||
|
||||
// set some permissions
|
||||
$permissions = $this->security->isGranted(
|
||||
[
|
||||
'sms:smses:viewown',
|
||||
'sms:smses:viewother',
|
||||
'sms:smses:create',
|
||||
'sms:smses:editown',
|
||||
'sms:smses:editother',
|
||||
'sms:smses:deleteown',
|
||||
'sms:smses:deleteother',
|
||||
'sms:smses:publishown',
|
||||
'sms:smses:publishother',
|
||||
],
|
||||
'RETURN_ARRAY'
|
||||
);
|
||||
|
||||
if (!$permissions['sms:smses:viewown'] && !$permissions['sms:smses:viewother']) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$this->setListFilters();
|
||||
|
||||
$session = $request->getSession();
|
||||
|
||||
// set limits
|
||||
$limit = $session->get('mautic.sms.limit', $this->coreParametersHelper->get('default_pagelimit'));
|
||||
$start = (1 === $page) ? 0 : (($page - 1) * $limit);
|
||||
if ($start < 0) {
|
||||
$start = 0;
|
||||
}
|
||||
|
||||
$search = $request->get('search', $session->get('mautic.sms.filter', ''));
|
||||
$session->set('mautic.sms.filter', $search);
|
||||
|
||||
$filter = ['string' => $search];
|
||||
|
||||
if (!$permissions['sms:smses:viewother']) {
|
||||
$filter['force'][] =
|
||||
[
|
||||
'column' => 'e.createdBy',
|
||||
'expr' => 'eq',
|
||||
'value' => $this->user->getId(),
|
||||
];
|
||||
}
|
||||
|
||||
// Not to include translations
|
||||
$filter['force'][] = ['column' => 'e.translationParent', 'expr' => 'isNull'];
|
||||
|
||||
$orderBy = $session->get('mautic.sms.orderby', 'e.name');
|
||||
$orderByDir = $session->get('mautic.sms.orderbydir', $this->getDefaultOrderDirection());
|
||||
|
||||
$smss = $model->getEntities([
|
||||
'start' => $start,
|
||||
'limit' => $limit,
|
||||
'filter' => $filter,
|
||||
'orderBy' => $orderBy,
|
||||
'orderByDir' => $orderByDir,
|
||||
]);
|
||||
|
||||
$count = count($smss);
|
||||
if ($count && $count < ($start + 1)) {
|
||||
// the number of entities are now less then the current page so redirect to the last page
|
||||
if (1 === $count) {
|
||||
$lastPage = 1;
|
||||
} else {
|
||||
$lastPage = (floor($count / $limit)) ?: 1;
|
||||
}
|
||||
|
||||
$session->set('mautic.sms.page', $lastPage);
|
||||
$returnUrl = $this->generateUrl('mautic_sms_index', ['page' => $lastPage]);
|
||||
|
||||
return $this->postActionRedirect([
|
||||
'returnUrl' => $returnUrl,
|
||||
'viewParameters' => ['page' => $lastPage],
|
||||
'contentTemplate' => 'Mautic\SmsBundle\Controller\SmsController::indexAction',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_sms_index',
|
||||
'mauticContent' => 'sms',
|
||||
],
|
||||
]);
|
||||
}
|
||||
$session->set('mautic.sms.page', $page);
|
||||
|
||||
return $this->delegateView([
|
||||
'viewParameters' => [
|
||||
'searchValue' => $search,
|
||||
'items' => $smss,
|
||||
'totalItems' => $count,
|
||||
'page' => $page,
|
||||
'limit' => $limit,
|
||||
'tmpl' => $request->get('tmpl', 'index'),
|
||||
'permissions' => $permissions,
|
||||
'model' => $model,
|
||||
'security' => $this->security,
|
||||
'configured' => count($transportChain->getEnabledTransports()) > 0,
|
||||
],
|
||||
'contentTemplate' => '@MauticSms/Sms/list.html.twig',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_sms_index',
|
||||
'mauticContent' => 'sms',
|
||||
'route' => $this->generateUrl('mautic_sms_index', ['page' => $page]),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a specific form into the detailed panel.
|
||||
*
|
||||
* @return JsonResponse|Response
|
||||
*/
|
||||
public function viewAction(Request $request, $objectId)
|
||||
{
|
||||
/** @var SmsModel $model */
|
||||
$model = $this->getModel('sms');
|
||||
$security = $this->security;
|
||||
|
||||
/** @var Sms $sms */
|
||||
$sms = $model->getEntity($objectId);
|
||||
// set the page we came from
|
||||
$page = $request->getSession()->get('mautic.sms.page', 1);
|
||||
|
||||
if (null === $sms) {
|
||||
// set the return URL
|
||||
$returnUrl = $this->generateUrl('mautic_sms_index', ['page' => $page]);
|
||||
|
||||
return $this->postActionRedirect([
|
||||
'returnUrl' => $returnUrl,
|
||||
'viewParameters' => ['page' => $page],
|
||||
'contentTemplate' => 'Mautic\SmsBundle\Controller\SmsController::indexAction',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_sms_index',
|
||||
'mauticContent' => 'sms',
|
||||
],
|
||||
'flashes' => [
|
||||
[
|
||||
'type' => 'error',
|
||||
'msg' => 'mautic.sms.error.notfound',
|
||||
'msgVars' => ['%id%' => $objectId],
|
||||
],
|
||||
],
|
||||
]);
|
||||
} elseif (!$this->security->hasEntityAccess(
|
||||
'sms:smses:viewown',
|
||||
'sms:smses:viewother',
|
||||
$sms->getCreatedBy()
|
||||
)
|
||||
) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
// Audit Log
|
||||
$auditLogModel = $this->getModel('core.auditlog');
|
||||
\assert($auditLogModel instanceof AuditLogModel);
|
||||
$logs = $auditLogModel->getLogForObject('sms', $sms->getId(), $sms->getDateAdded());
|
||||
|
||||
// Init the date range filter form
|
||||
$dateRangeValues = $request->query->all()['daterange'] ?? $request->request->all()['daterange'] ?? [];
|
||||
$action = $this->generateUrl('mautic_sms_action', ['objectAction' => 'view', 'objectId' => $objectId]);
|
||||
$dateRangeForm = $this->formFactory->create(DateRangeType::class, $dateRangeValues, ['action' => $action]);
|
||||
$entityViews = $model->getHitsLineChartData(
|
||||
null,
|
||||
new \DateTime($dateRangeForm->get('date_from')->getData()),
|
||||
new \DateTime($dateRangeForm->get('date_to')->getData()),
|
||||
null,
|
||||
['sms_id' => $sms->getId()]
|
||||
);
|
||||
|
||||
// Get click through stats
|
||||
$trackableLinks = $model->getSmsClickStats($sms->getId());
|
||||
|
||||
$translations = $sms->getTranslations();
|
||||
if ($translations instanceof Collection) {
|
||||
$translations = $translations->toArray();
|
||||
}
|
||||
|
||||
[$translationParent, $translationChildren] = $translations;
|
||||
|
||||
return $this->delegateView([
|
||||
'returnUrl' => $this->generateUrl('mautic_sms_action', ['objectAction' => 'view', 'objectId' => $sms->getId()]),
|
||||
'viewParameters' => [
|
||||
'sms' => $sms,
|
||||
'trackables' => $trackableLinks,
|
||||
'logs' => $logs,
|
||||
'isEmbedded' => $request->get('isEmbedded') ?: false,
|
||||
'permissions' => $security->isGranted([
|
||||
'sms:smses:viewown',
|
||||
'sms:smses:viewother',
|
||||
'sms:smses:create',
|
||||
'sms:smses:editown',
|
||||
'sms:smses:editother',
|
||||
'sms:smses:deleteown',
|
||||
'sms:smses:deleteother',
|
||||
'sms:smses:publishown',
|
||||
'sms:smses:publishother',
|
||||
], 'RETURN_ARRAY'),
|
||||
'security' => $security,
|
||||
'entityViews' => $entityViews,
|
||||
'contacts' => $this->forward(
|
||||
'Mautic\SmsBundle\Controller\SmsController::contactsAction',
|
||||
[
|
||||
'objectId' => $sms->getId(),
|
||||
'page' => $request->getSession()->get('mautic.sms.contact.page', 1),
|
||||
'ignoreAjax' => true,
|
||||
]
|
||||
)->getContent(),
|
||||
'dateRangeForm' => $dateRangeForm->createView(),
|
||||
'translations' => [
|
||||
'parent' => $translationParent,
|
||||
'children' => $translationChildren,
|
||||
],
|
||||
],
|
||||
'contentTemplate' => '@MauticSms/Sms/details.html.twig',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_sms_index',
|
||||
'mauticContent' => 'sms',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates new form and processes post data.
|
||||
*
|
||||
* @param Sms $entity
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|Response
|
||||
*/
|
||||
public function newAction(Request $request, $entity = null)
|
||||
{
|
||||
/** @var SmsModel $model */
|
||||
$model = $this->getModel('sms');
|
||||
|
||||
if (!$entity instanceof Sms) {
|
||||
/** @var Sms $entity */
|
||||
$entity = $model->getEntity();
|
||||
}
|
||||
|
||||
$method = $request->getMethod();
|
||||
$session = $request->getSession();
|
||||
|
||||
if (!$this->security->isGranted('sms:smses:create')) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
// set the page we came from
|
||||
$page = $session->get('mautic.sms.page', 1);
|
||||
$action = $this->generateUrl('mautic_sms_action', ['objectAction' => 'new']);
|
||||
$sms = $request->request->all()['sms'] ?? [];
|
||||
$updateSelect = 'POST' === $method
|
||||
? ($sms['updateSelect'] ?? false)
|
||||
: $request->get('updateSelect', false);
|
||||
|
||||
if ($updateSelect) {
|
||||
$entity->setSmsType('template');
|
||||
}
|
||||
|
||||
// create the form
|
||||
$form = $model->createForm($entity, $this->formFactory, $action, ['update_select' => $updateSelect]);
|
||||
|
||||
// /Check for a submitted form and process it
|
||||
if ('POST' == $method) {
|
||||
$valid = false;
|
||||
if (!$cancelled = $this->isFormCancelled($form)) {
|
||||
if ($valid = $this->isFormValid($form)) {
|
||||
// form is valid so process the data
|
||||
$model->saveEntity($entity);
|
||||
|
||||
$this->addFlashMessage(
|
||||
'mautic.core.notice.created',
|
||||
[
|
||||
'%name%' => $entity->getName(),
|
||||
'%menu_link%' => 'mautic_sms_index',
|
||||
'%url%' => $this->generateUrl(
|
||||
'mautic_sms_action',
|
||||
[
|
||||
'objectAction' => 'edit',
|
||||
'objectId' => $entity->getId(),
|
||||
]
|
||||
),
|
||||
]
|
||||
);
|
||||
|
||||
if ($this->getFormButton($form, ['buttons', 'save'])->isClicked()) {
|
||||
$viewParameters = [
|
||||
'objectAction' => 'view',
|
||||
'objectId' => $entity->getId(),
|
||||
];
|
||||
$returnUrl = $this->generateUrl('mautic_sms_action', $viewParameters);
|
||||
$template = 'Mautic\SmsBundle\Controller\SmsController::viewAction';
|
||||
} else {
|
||||
// return edit view so that all the session stuff is loaded
|
||||
return $this->editAction($request, $entity->getId(), true);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$viewParameters = ['page' => $page];
|
||||
$returnUrl = $this->generateUrl('mautic_sms_index', $viewParameters);
|
||||
$template = 'Mautic\SmsBundle\Controller\SmsController::indexAction';
|
||||
// clear any modified content
|
||||
$session->remove('mautic.sms.'.$entity->getId().'.content');
|
||||
}
|
||||
|
||||
$passthrough = [
|
||||
'activeLink' => 'mautic_sms_index',
|
||||
'mauticContent' => 'sms',
|
||||
];
|
||||
|
||||
// Check to see if this is a popup
|
||||
if (isset($form['updateSelect'])) {
|
||||
$template = false;
|
||||
$passthrough = array_merge(
|
||||
$passthrough,
|
||||
[
|
||||
'updateSelect' => $form['updateSelect']->getData(),
|
||||
'id' => $entity->getId(),
|
||||
'name' => $entity->getName(),
|
||||
'group' => $entity->getLanguage(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
if ($cancelled || ($valid && $this->getFormButton($form, ['buttons', 'save'])->isClicked())) {
|
||||
return $this->postActionRedirect(
|
||||
[
|
||||
'returnUrl' => $returnUrl,
|
||||
'viewParameters' => $viewParameters,
|
||||
'contentTemplate' => $template,
|
||||
'passthroughVars' => $passthrough,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->delegateView(
|
||||
[
|
||||
'viewParameters' => [
|
||||
'form' => $form->createView(),
|
||||
'sms' => $entity,
|
||||
],
|
||||
'contentTemplate' => '@MauticSms/Sms/form.html.twig',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_sms_index',
|
||||
'mauticContent' => 'sms',
|
||||
'updateSelect' => InputHelper::clean($request->query->get('updateSelect')),
|
||||
'route' => $this->generateUrl(
|
||||
'mautic_sms_action',
|
||||
[
|
||||
'objectAction' => 'new',
|
||||
]
|
||||
),
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $ignorePost
|
||||
* @param bool $forceTypeSelection
|
||||
*
|
||||
* @return array|JsonResponse|\Symfony\Component\HttpFoundation\RedirectResponse|Response
|
||||
*/
|
||||
public function editAction(Request $request, $objectId, $ignorePost = false, $forceTypeSelection = false)
|
||||
{
|
||||
/** @var SmsModel $model */
|
||||
$model = $this->getModel('sms');
|
||||
$method = $request->getMethod();
|
||||
$entity = $model->getEntity($objectId);
|
||||
$session = $request->getSession();
|
||||
$page = $session->get('mautic.sms.page', 1);
|
||||
|
||||
// set the return URL
|
||||
$returnUrl = $this->generateUrl('mautic_sms_index', ['page' => $page]);
|
||||
|
||||
$postActionVars = [
|
||||
'returnUrl' => $returnUrl,
|
||||
'viewParameters' => ['page' => $page],
|
||||
'contentTemplate' => 'Mautic\SmsBundle\Controller\SmsController::indexAction',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => 'mautic_sms_index',
|
||||
'mauticContent' => 'sms',
|
||||
],
|
||||
];
|
||||
|
||||
// not found
|
||||
if (null === $entity) {
|
||||
return $this->postActionRedirect(
|
||||
array_merge(
|
||||
$postActionVars,
|
||||
[
|
||||
'flashes' => [
|
||||
[
|
||||
'type' => 'error',
|
||||
'msg' => 'mautic.sms.error.notfound',
|
||||
'msgVars' => ['%id%' => $objectId],
|
||||
],
|
||||
],
|
||||
]
|
||||
)
|
||||
);
|
||||
} elseif (!$this->security->hasEntityAccess(
|
||||
'sms:smses:viewown',
|
||||
'sms:smses:viewother',
|
||||
$entity->getCreatedBy()
|
||||
)
|
||||
) {
|
||||
return $this->accessDenied();
|
||||
} elseif ($model->isLocked($entity)) {
|
||||
// deny access if the entity is locked
|
||||
return $this->isLocked($postActionVars, $entity, 'sms');
|
||||
}
|
||||
|
||||
// Create the form
|
||||
$action = $this->generateUrl('mautic_sms_action', ['objectAction' => 'edit', 'objectId' => $objectId]);
|
||||
$sms = $request->request->all()['sms'] ?? [];
|
||||
$updateSelect = 'POST' === $method
|
||||
? ($sms['updateSelect'] ?? false)
|
||||
: $request->get('updateSelect', false);
|
||||
|
||||
$form = $model->createForm($entity, $this->formFactory, $action, ['update_select' => $updateSelect]);
|
||||
|
||||
// /Check for a submitted form and process it
|
||||
if (!$ignorePost && 'POST' == $method) {
|
||||
$valid = false;
|
||||
if (!$cancelled = $this->isFormCancelled($form)) {
|
||||
if ($valid = $this->isFormValid($form)) {
|
||||
// form is valid so process the data
|
||||
$model->saveEntity($entity, $this->getFormButton($form, ['buttons', 'save'])->isClicked());
|
||||
|
||||
$this->addFlashMessage(
|
||||
'mautic.core.notice.updated',
|
||||
[
|
||||
'%name%' => $entity->getName(),
|
||||
'%menu_link%' => 'mautic_sms_index',
|
||||
'%url%' => $this->generateUrl(
|
||||
'mautic_sms_action',
|
||||
[
|
||||
'objectAction' => 'edit',
|
||||
'objectId' => $entity->getId(),
|
||||
]
|
||||
),
|
||||
],
|
||||
'warning'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// clear any modified content
|
||||
$session->remove('mautic.sms.'.$objectId.'.content');
|
||||
// unlock the entity
|
||||
$model->unlockEntity($entity);
|
||||
}
|
||||
|
||||
$passthrough = [
|
||||
'activeLink' => 'mautic_sms_index',
|
||||
'mauticContent' => 'sms',
|
||||
];
|
||||
|
||||
$template = 'Mautic\SmsBundle\Controller\SmsController::viewAction';
|
||||
|
||||
// Check to see if this is a popup
|
||||
if (isset($form['updateSelect'])) {
|
||||
$template = false;
|
||||
$passthrough = array_merge(
|
||||
$passthrough,
|
||||
[
|
||||
'updateSelect' => $form['updateSelect']->getData(),
|
||||
'id' => $entity->getId(),
|
||||
'name' => $entity->getName(),
|
||||
'group' => $entity->getLanguage(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
if ($cancelled || ($valid && $this->getFormButton($form, ['buttons', 'save'])->isClicked())) {
|
||||
$viewParameters = [
|
||||
'objectAction' => 'view',
|
||||
'objectId' => $entity->getId(),
|
||||
];
|
||||
|
||||
return $this->postActionRedirect(
|
||||
array_merge(
|
||||
$postActionVars,
|
||||
[
|
||||
'returnUrl' => $this->generateUrl('mautic_sms_action', $viewParameters),
|
||||
'viewParameters' => $viewParameters,
|
||||
'contentTemplate' => $template,
|
||||
'passthroughVars' => $passthrough,
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// lock the entity
|
||||
$model->lockEntity($entity);
|
||||
}
|
||||
|
||||
return $this->delegateView(
|
||||
[
|
||||
'viewParameters' => [
|
||||
'form' => $form->createView(),
|
||||
'sms' => $entity,
|
||||
'forceTypeSelection' => $forceTypeSelection,
|
||||
],
|
||||
'contentTemplate' => '@MauticSms/Sms/form.html.twig',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_sms_index',
|
||||
'mauticContent' => 'sms',
|
||||
'updateSelect' => InputHelper::clean($request->query->get('updateSelect')),
|
||||
'route' => $this->generateUrl(
|
||||
'mautic_sms_action',
|
||||
[
|
||||
'objectAction' => 'edit',
|
||||
'objectId' => $entity->getId(),
|
||||
]
|
||||
),
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone an entity.
|
||||
*
|
||||
* @return JsonResponse|\Symfony\Component\HttpFoundation\RedirectResponse|Response
|
||||
*/
|
||||
public function cloneAction(Request $request, $objectId)
|
||||
{
|
||||
$model = $this->getModel('sms');
|
||||
$entity = $model->getEntity($objectId);
|
||||
|
||||
if (null != $entity) {
|
||||
if (!$this->security->isGranted('sms:smses:create')
|
||||
|| !$this->security->hasEntityAccess(
|
||||
'sms:smses:viewown',
|
||||
'sms:smses:viewother',
|
||||
$entity->getCreatedBy()
|
||||
)
|
||||
) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$entity = clone $entity;
|
||||
}
|
||||
|
||||
return $this->newAction($request, $entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the entity.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function deleteAction(Request $request, $objectId)
|
||||
{
|
||||
$page = $request->getSession()->get('mautic.sms.page', 1);
|
||||
$returnUrl = $this->generateUrl('mautic_sms_index', ['page' => $page]);
|
||||
$flashes = [];
|
||||
|
||||
$postActionVars = [
|
||||
'returnUrl' => $returnUrl,
|
||||
'viewParameters' => ['page' => $page],
|
||||
'contentTemplate' => 'Mautic\SmsBundle\Controller\SmsController::indexAction',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => 'mautic_sms_index',
|
||||
'mauticContent' => 'sms',
|
||||
],
|
||||
];
|
||||
|
||||
if (Request::METHOD_POST === $request->getMethod()) {
|
||||
$model = $this->getModel('sms');
|
||||
\assert($model instanceof SmsModel);
|
||||
$entity = $model->getEntity($objectId);
|
||||
|
||||
if (null === $entity) {
|
||||
$flashes[] = [
|
||||
'type' => 'error',
|
||||
'msg' => 'mautic.sms.error.notfound',
|
||||
'msgVars' => ['%id%' => $objectId],
|
||||
];
|
||||
} elseif (!$this->security->hasEntityAccess(
|
||||
'sms:smses:deleteown',
|
||||
'sms:smses:deleteother',
|
||||
$entity->getCreatedBy()
|
||||
)
|
||||
) {
|
||||
return $this->accessDenied();
|
||||
} elseif ($model->isLocked($entity)) {
|
||||
return $this->isLocked($postActionVars, $entity, 'sms');
|
||||
}
|
||||
|
||||
$model->deleteEntity($entity);
|
||||
|
||||
$flashes[] = [
|
||||
'type' => 'notice',
|
||||
'msg' => 'mautic.core.notice.deleted',
|
||||
'msgVars' => [
|
||||
'%name%' => $entity->getName(),
|
||||
'%id%' => $objectId,
|
||||
],
|
||||
];
|
||||
} // else don't do anything
|
||||
|
||||
return $this->postActionRedirect(
|
||||
array_merge(
|
||||
$postActionVars,
|
||||
['flashes' => $flashes]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a group of entities.
|
||||
*/
|
||||
public function batchDeleteAction(Request $request): Response
|
||||
{
|
||||
$page = $request->getSession()->get('mautic.sms.page', 1);
|
||||
$returnUrl = $this->generateUrl('mautic_sms_index', ['page' => $page]);
|
||||
$flashes = [];
|
||||
|
||||
$postActionVars = [
|
||||
'returnUrl' => $returnUrl,
|
||||
'viewParameters' => ['page' => $page],
|
||||
'contentTemplate' => 'Mautic\SmsBundle\Controller\SmsController::indexAction',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_sms_index',
|
||||
'mauticContent' => 'sms',
|
||||
],
|
||||
];
|
||||
|
||||
if (Request::METHOD_POST == $request->getMethod()) {
|
||||
$model = $this->getModel('sms');
|
||||
\assert($model instanceof SmsModel);
|
||||
$ids = json_decode($request->query->get('ids', '{}'));
|
||||
|
||||
$deleteIds = [];
|
||||
|
||||
// Loop over the IDs to perform access checks pre-delete
|
||||
foreach ($ids as $objectId) {
|
||||
$entity = $model->getEntity($objectId);
|
||||
|
||||
if (null === $entity) {
|
||||
$flashes[] = [
|
||||
'type' => 'error',
|
||||
'msg' => 'mautic.sms.error.notfound',
|
||||
'msgVars' => ['%id%' => $objectId],
|
||||
];
|
||||
} elseif (!$this->security->hasEntityAccess(
|
||||
'sms:smses:viewown',
|
||||
'sms:smses:viewother',
|
||||
$entity->getCreatedBy()
|
||||
)
|
||||
) {
|
||||
$flashes[] = $this->accessDenied(true);
|
||||
} elseif ($model->isLocked($entity)) {
|
||||
$flashes[] = $this->isLocked($postActionVars, $entity, 'sms', true);
|
||||
} else {
|
||||
$deleteIds[] = $objectId;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete everything we are able to
|
||||
if (!empty($deleteIds)) {
|
||||
$entities = $model->deleteEntities($deleteIds);
|
||||
|
||||
$flashes[] = [
|
||||
'type' => 'notice',
|
||||
'msg' => 'mautic.sms.notice.batch_deleted',
|
||||
'msgVars' => [
|
||||
'%count%' => count($entities),
|
||||
],
|
||||
];
|
||||
}
|
||||
} // else don't do anything
|
||||
|
||||
return $this->postActionRedirect(
|
||||
array_merge(
|
||||
$postActionVars,
|
||||
['flashes' => $flashes]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return JsonResponse|Response
|
||||
*/
|
||||
public function previewAction($objectId)
|
||||
{
|
||||
/** @var SmsModel $model */
|
||||
$model = $this->getModel('sms');
|
||||
$sms = $model->getEntity($objectId);
|
||||
$security = $this->security;
|
||||
|
||||
if (null !== $sms && $security->hasEntityAccess('sms:smses:viewown', 'sms:smses:viewother')) {
|
||||
return $this->delegateView([
|
||||
'viewParameters' => [
|
||||
'sms' => $sms,
|
||||
],
|
||||
'contentTemplate' => '@MauticSms/Sms/preview.html.twig',
|
||||
]);
|
||||
}
|
||||
|
||||
return new Response('', Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $page
|
||||
*
|
||||
* @return JsonResponse|\Symfony\Component\HttpFoundation\RedirectResponse|Response
|
||||
*/
|
||||
public function contactsAction(
|
||||
Request $request,
|
||||
PageHelperFactoryInterface $pageHelperFactory,
|
||||
$objectId,
|
||||
$page = 1,
|
||||
) {
|
||||
return $this->generateContactsGrid(
|
||||
$request,
|
||||
$pageHelperFactory,
|
||||
$objectId,
|
||||
$page,
|
||||
'sms:smses:view',
|
||||
'sms',
|
||||
'sms_message_stats',
|
||||
'sms',
|
||||
'sms_id'
|
||||
);
|
||||
}
|
||||
|
||||
protected function getModelName(): string
|
||||
{
|
||||
return 'sms';
|
||||
}
|
||||
|
||||
protected function getDefaultOrderDirection(): string
|
||||
{
|
||||
return 'DESC';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\DependencyInjection\Compiler;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
|
||||
class SmsTransportPass implements CompilerPassInterface
|
||||
{
|
||||
private ?ContainerBuilder $container = null;
|
||||
|
||||
public function process(ContainerBuilder $container): void
|
||||
{
|
||||
$this->container = $container;
|
||||
|
||||
$this->registerTransports();
|
||||
$this->registerCallbacks();
|
||||
}
|
||||
|
||||
private function registerTransports(): void
|
||||
{
|
||||
if (!$this->container->has('mautic.sms.transport_chain')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$definition = $this->container->getDefinition('mautic.sms.transport_chain');
|
||||
$taggedServices = $this->container->findTaggedServiceIds('mautic.sms_transport');
|
||||
foreach ($taggedServices as $id => $tags) {
|
||||
$definition->addMethodCall('addTransport', [
|
||||
$id,
|
||||
new Reference($id),
|
||||
!empty($tags[0]['alias']) ? $tags[0]['alias'] : $id,
|
||||
!empty($tags[0]['integrationAlias']) ? $tags[0]['integrationAlias'] : $id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function registerCallbacks(): void
|
||||
{
|
||||
if (!$this->container->has('mautic.sms.callback_handler_container')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$definition = $this->container->getDefinition('mautic.sms.callback_handler_container');
|
||||
$taggedServices = $this->container->findTaggedServiceIds('mautic.sms_callback_handler');
|
||||
foreach ($taggedServices as $id => $tags) {
|
||||
$definition->addMethodCall('registerHandler', [
|
||||
new Reference($id),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\SmsBundle\DependencyInjection;
|
||||
|
||||
use Symfony\Component\Config\FileLocator;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Extension\Extension;
|
||||
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
|
||||
|
||||
class MauticSmsExtension extends Extension
|
||||
{
|
||||
/**
|
||||
* @param mixed[] $configs
|
||||
*/
|
||||
public function load(array $configs, ContainerBuilder $container): void
|
||||
{
|
||||
$loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/../Config'));
|
||||
$loader->load('services.php');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,490 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use ApiPlatform\Metadata\Put;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
|
||||
use Mautic\CategoryBundle\Entity\Category;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Mautic\CoreBundle\Entity\FormEntity;
|
||||
use Mautic\CoreBundle\Entity\TranslationEntityInterface;
|
||||
use Mautic\CoreBundle\Entity\TranslationEntityTrait;
|
||||
use Mautic\CoreBundle\Entity\UuidInterface;
|
||||
use Mautic\CoreBundle\Entity\UuidTrait;
|
||||
use Mautic\CoreBundle\Entity\VariantEntityInterface;
|
||||
use Mautic\CoreBundle\Entity\VariantEntityTrait;
|
||||
use Mautic\CoreBundle\Validator\EntityEvent;
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
use Mautic\LeadBundle\Form\Validator\Constraints\LeadListAccess;
|
||||
use Mautic\ProjectBundle\Entity\ProjectTrait;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
use Symfony\Component\Validator\Constraints\Callback;
|
||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
use Symfony\Component\Validator\Mapping\ClassMetadata;
|
||||
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new GetCollection(security: "is_granted('sms:smses:viewown')"),
|
||||
new Post(security: "is_granted('sms:smses:create')"),
|
||||
new Get(security: "is_granted('sms:smses:viewown')"),
|
||||
new Put(security: "is_granted('sms:smses:editown')"),
|
||||
new Patch(security: "is_granted('sms:smses:editother')"),
|
||||
new Delete(security: "is_granted('sms:smses:deleteown')"),
|
||||
],
|
||||
normalizationContext: [
|
||||
'groups' => ['sms:read'],
|
||||
'swagger_definition_name' => 'Read',
|
||||
'api_included' => ['category'],
|
||||
],
|
||||
denormalizationContext: [
|
||||
'groups' => ['sms:write'],
|
||||
'swagger_definition_name' => 'Write',
|
||||
]
|
||||
)]
|
||||
/**
|
||||
* @use TranslationEntityTrait<Sms>
|
||||
* @use VariantEntityTrait<Sms>
|
||||
*/
|
||||
class Sms extends FormEntity implements UuidInterface, TranslationEntityInterface, VariantEntityInterface
|
||||
{
|
||||
use UuidTrait;
|
||||
use ProjectTrait;
|
||||
use TranslationEntityTrait;
|
||||
use VariantEntityTrait;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
#[Groups(['sms:read'])]
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
#[Groups(['sms:read', 'sms:write'])]
|
||||
private $name;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
#[Groups(['sms:read', 'sms:write'])]
|
||||
private $description;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
#[Groups(['sms:read', 'sms:write'])]
|
||||
private $message;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
#[Groups(['sms:read', 'sms:write'])]
|
||||
private $publishUp;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
#[Groups(['sms:read', 'sms:write'])]
|
||||
private $publishDown;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
#[Groups(['sms:read'])]
|
||||
private $sentCount = 0;
|
||||
|
||||
/**
|
||||
* @var Category|null
|
||||
**/
|
||||
#[Groups(['sms:read', 'sms:write'])]
|
||||
private $category;
|
||||
|
||||
/**
|
||||
* @var ArrayCollection<int, LeadList>
|
||||
*/
|
||||
#[Groups(['sms:read', 'sms:write'])]
|
||||
private $lists;
|
||||
|
||||
/**
|
||||
* @var ArrayCollection<int, Stat>
|
||||
*/
|
||||
private $stats;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
#[Groups(['sms:read', 'sms:write'])]
|
||||
private $smsType = 'template';
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
#[Groups(['sms:read'])]
|
||||
private $pendingCount = 0;
|
||||
|
||||
public function __clone()
|
||||
{
|
||||
$this->id = null;
|
||||
$this->stats = new ArrayCollection();
|
||||
$this->sentCount = 0;
|
||||
|
||||
$this->clearTranslations();
|
||||
|
||||
parent::__clone();
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->lists = new ArrayCollection();
|
||||
$this->stats = new ArrayCollection();
|
||||
$this->initializeProjects();
|
||||
$this->translationChildren = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function clearStats(): void
|
||||
{
|
||||
$this->stats = new ArrayCollection();
|
||||
}
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable('sms_messages')
|
||||
->setCustomRepositoryClass(SmsRepository::class);
|
||||
|
||||
$builder->addIdColumns();
|
||||
|
||||
$builder->createField('message', 'text')
|
||||
->build();
|
||||
|
||||
$builder->createField('smsType', 'text')
|
||||
->columnName('sms_type')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->addPublishDates();
|
||||
|
||||
$builder->createField('sentCount', 'integer')
|
||||
->columnName('sent_count')
|
||||
->build();
|
||||
|
||||
$builder->addCategory();
|
||||
|
||||
$builder->createManyToMany('lists', LeadList::class)
|
||||
->setJoinTable('sms_message_list_xref')
|
||||
->setIndexBy('id')
|
||||
->addInverseJoinColumn('leadlist_id', 'id', false, false, 'CASCADE')
|
||||
->addJoinColumn('sms_id', 'id', false, false, 'CASCADE')
|
||||
->fetchExtraLazy()
|
||||
->build();
|
||||
|
||||
$builder->createOneToMany('stats', 'Stat')
|
||||
->setIndexBy('id')
|
||||
->mappedBy('sms')
|
||||
->cascadePersist()
|
||||
->fetchExtraLazy()
|
||||
->build();
|
||||
|
||||
self::addTranslationMetadata($builder, self::class);
|
||||
|
||||
static::addUuidField($builder);
|
||||
self::addProjectsField($builder, 'sms_projects_xref', 'sms_id');
|
||||
}
|
||||
|
||||
public static function loadValidatorMetadata(ClassMetadata $metadata): void
|
||||
{
|
||||
$metadata->addPropertyConstraint(
|
||||
'name',
|
||||
new NotBlank(
|
||||
[
|
||||
'message' => 'mautic.core.name.required',
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
$metadata->addConstraint(new Callback(
|
||||
function (Sms $sms, ExecutionContextInterface $context): void {
|
||||
$type = $sms->getSmsType();
|
||||
if ('list' == $type) {
|
||||
$validator = $context->getValidator();
|
||||
$violations = $validator->validate(
|
||||
$sms->getLists(),
|
||||
[
|
||||
new NotBlank(
|
||||
[
|
||||
'message' => 'mautic.lead.lists.required',
|
||||
]
|
||||
),
|
||||
new LeadListAccess(),
|
||||
]
|
||||
);
|
||||
|
||||
foreach ($violations as $violation) {
|
||||
$context->buildViolation($violation->getMessage())
|
||||
->atPath('lists')
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
},
|
||||
));
|
||||
|
||||
$metadata->addConstraint(new EntityEvent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the metadata for API usage.
|
||||
*/
|
||||
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
|
||||
{
|
||||
$metadata->setGroupPrefix('sms')
|
||||
->addListProperties(
|
||||
[
|
||||
'id',
|
||||
'name',
|
||||
'message',
|
||||
'language',
|
||||
'category',
|
||||
]
|
||||
)
|
||||
->addProperties(
|
||||
[
|
||||
'publishUp',
|
||||
'publishDown',
|
||||
'sentCount',
|
||||
]
|
||||
)
|
||||
->build();
|
||||
|
||||
self::addProjectsInLoadApiMetadata($metadata, 'sms');
|
||||
}
|
||||
|
||||
protected function isChanged($prop, $val)
|
||||
{
|
||||
$getter = 'get'.ucfirst($prop);
|
||||
$current = $this->$getter();
|
||||
|
||||
if ('category' == $prop || 'list' == $prop) {
|
||||
$currentId = ($current) ? $current->getId() : '';
|
||||
$newId = ($val) ? $val->getId() : null;
|
||||
if ($currentId != $newId) {
|
||||
$this->changes[$prop] = [$currentId, $newId];
|
||||
}
|
||||
} else {
|
||||
parent::isChanged($prop, $val);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setName($name)
|
||||
{
|
||||
$this->isChanged('name', $name);
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $description
|
||||
*/
|
||||
public function setDescription($description): void
|
||||
{
|
||||
$this->isChanged('description', $description);
|
||||
$this->description = $description;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getCategory()
|
||||
{
|
||||
return $this->category;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setCategory($category)
|
||||
{
|
||||
$this->isChanged('category', $category);
|
||||
$this->category = $category;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getMessage()
|
||||
{
|
||||
return $this->message;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $message
|
||||
*/
|
||||
public function setMessage($message): void
|
||||
{
|
||||
$this->isChanged('message', $message);
|
||||
$this->message = $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getPublishDown()
|
||||
{
|
||||
return $this->publishDown;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setPublishDown($publishDown)
|
||||
{
|
||||
$this->isChanged('publishDown', $publishDown);
|
||||
$this->publishDown = $publishDown;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getPublishUp()
|
||||
{
|
||||
return $this->publishUp;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setPublishUp($publishUp)
|
||||
{
|
||||
$this->isChanged('publishUp', $publishUp);
|
||||
$this->publishUp = $publishUp;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSentCount(bool $includeVariants = false): mixed
|
||||
{
|
||||
return ($includeVariants) ? $this->getAccumulativeTranslationCount('getSentCount') : $this->sentCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setSentCount($sentCount)
|
||||
{
|
||||
$this->sentCount = $sentCount;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getLists()
|
||||
{
|
||||
return $this->lists;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Sms
|
||||
*/
|
||||
public function addList(LeadList $list)
|
||||
{
|
||||
$this->lists[] = $list;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeList(LeadList $list): void
|
||||
{
|
||||
$this->lists->removeElement($list);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getStats()
|
||||
{
|
||||
return $this->stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getSmsType()
|
||||
{
|
||||
return $this->smsType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $smsType
|
||||
*/
|
||||
public function setSmsType($smsType): void
|
||||
{
|
||||
$this->isChanged('smsType', $smsType);
|
||||
$this->smsType = $smsType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $pendingCount
|
||||
*
|
||||
* @return Sms
|
||||
*/
|
||||
public function setPendingCount($pendingCount)
|
||||
{
|
||||
$this->pendingCount = $pendingCount;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getPendingCount()
|
||||
{
|
||||
return $this->pendingCount;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\Tools\Pagination\Paginator;
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
use Mautic\ProjectBundle\Entity\ProjectRepositoryTrait;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<Sms>
|
||||
*/
|
||||
class SmsRepository extends CommonRepository
|
||||
{
|
||||
use ProjectRepositoryTrait;
|
||||
|
||||
/**
|
||||
* Get a list of entities.
|
||||
*
|
||||
* @return Paginator
|
||||
*/
|
||||
public function getEntities(array $args = [])
|
||||
{
|
||||
$q = $this->_em
|
||||
->createQueryBuilder()
|
||||
->select($this->getTableAlias())
|
||||
->from(Sms::class, $this->getTableAlias(), $this->getTableAlias().'.id');
|
||||
|
||||
if (empty($args['iterable_mode'])) {
|
||||
$q->leftJoin($this->getTableAlias().'.category', 'c');
|
||||
}
|
||||
|
||||
$args['qb'] = $q;
|
||||
|
||||
return parent::getEntities($args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<Sms>
|
||||
*/
|
||||
public function getPublishedBroadcastsIterable(?int $id = null): iterable
|
||||
{
|
||||
return $this->getPublishedBroadcastsQuery($id)->toIterable();
|
||||
}
|
||||
|
||||
private function getPublishedBroadcastsQuery(?int $id = null): Query
|
||||
{
|
||||
$qb = $this->createQueryBuilder($this->getTableAlias());
|
||||
$expr = $this->getPublishedByDateExpression($qb, null, true, true, false);
|
||||
|
||||
$expr->add(
|
||||
$qb->expr()->eq($this->getTableAlias().'.smsType', $qb->expr()->literal('list'))
|
||||
);
|
||||
|
||||
if (null !== $id && 0 !== $id) {
|
||||
$expr->add(
|
||||
$qb->expr()->eq($this->getTableAlias().'.id', (int) $id)
|
||||
);
|
||||
}
|
||||
$qb->where($expr);
|
||||
|
||||
return $qb->getQuery();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Doctrine\DBAL\Query\QueryBuilder
|
||||
*/
|
||||
public function getSegmentsContactsQuery(int $smsId)
|
||||
{
|
||||
// Main query
|
||||
$q = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$q->from(MAUTIC_TABLE_PREFIX.'sms_message_list_xref', 'sml')
|
||||
->join('sml', MAUTIC_TABLE_PREFIX.'lead_lists', 'll', 'll.id = sml.leadlist_id and ll.is_published = 1')
|
||||
->join('ll', MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'lll', 'lll.leadlist_id = sml.leadlist_id and lll.manually_removed = 0')
|
||||
->join('lll', MAUTIC_TABLE_PREFIX.'leads', 'l', 'lll.lead_id = l.id')
|
||||
->where(
|
||||
$q->expr()->and(
|
||||
$q->expr()->eq('sml.sms_id', ':smsId')
|
||||
)
|
||||
)
|
||||
->setParameter('smsId', $smsId)
|
||||
// Order by ID so we can query by greater than X contact ID when batching
|
||||
->orderBy('lll.lead_id');
|
||||
|
||||
return $q;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get amounts of sent and read emails.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getSentCount()
|
||||
{
|
||||
$q = $this->_em->createQueryBuilder();
|
||||
$q->select('SUM(e.sentCount) as sent_count')
|
||||
->from(Sms::class, 'e');
|
||||
$results = $q->getQuery()->getSingleResult(Query::HYDRATE_ARRAY);
|
||||
|
||||
if (!isset($results['sent_count'])) {
|
||||
$results['sent_count'] = 0;
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Doctrine\ORM\QueryBuilder|\Doctrine\DBAL\Query\QueryBuilder $q
|
||||
*/
|
||||
protected function addSearchCommandWhereClause($q, $filter): array
|
||||
{
|
||||
[$expr, $parameters] = $this->addStandardSearchCommandWhereClause($q, $filter);
|
||||
if ($expr) {
|
||||
return [$expr, $parameters];
|
||||
}
|
||||
|
||||
$command = $filter->command;
|
||||
$unique = $this->generateRandomParameterName();
|
||||
$returnParameter = false; // returning a parameter that is not used will lead to a Doctrine error
|
||||
|
||||
switch ($command) {
|
||||
case $this->translator->trans('mautic.core.searchcommand.lang'):
|
||||
$langUnique = $this->generateRandomParameterName();
|
||||
$langValue = $filter->string.'_%';
|
||||
$forceParameters = [
|
||||
$langUnique => $langValue,
|
||||
$unique => $filter->string,
|
||||
];
|
||||
$expr = $q->expr()->or(
|
||||
$q->expr()->eq('e.language', ":$unique"),
|
||||
$q->expr()->like('e.language', ":$langUnique")
|
||||
);
|
||||
$returnParameter = true;
|
||||
break;
|
||||
case $this->translator->trans('mautic.project.searchcommand.name'):
|
||||
case $this->translator->trans('mautic.project.searchcommand.name', [], null, 'en_US'):
|
||||
return $this->handleProjectFilter(
|
||||
$this->_em->getConnection()->createQueryBuilder(),
|
||||
'sms_id',
|
||||
'sms_projects_xref',
|
||||
$this->getTableAlias(),
|
||||
$filter->string,
|
||||
$filter->not
|
||||
);
|
||||
}
|
||||
|
||||
if ($expr && $filter->not) {
|
||||
$expr = $q->expr()->not($expr);
|
||||
}
|
||||
|
||||
if (!empty($forceParameters)) {
|
||||
$parameters = $forceParameters;
|
||||
} elseif ($returnParameter) {
|
||||
$string = ($filter->strict) ? $filter->string : "%{$filter->string}%";
|
||||
$parameters = ["$unique" => $string];
|
||||
}
|
||||
|
||||
return [$expr, $parameters];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getSearchCommands(): array
|
||||
{
|
||||
$commands = [
|
||||
'mautic.core.searchcommand.ispublished',
|
||||
'mautic.core.searchcommand.isunpublished',
|
||||
'mautic.core.searchcommand.isuncategorized',
|
||||
'mautic.core.searchcommand.ismine',
|
||||
'mautic.core.searchcommand.category',
|
||||
'mautic.core.searchcommand.lang',
|
||||
'mautic.project.searchcommand.name',
|
||||
];
|
||||
|
||||
return array_merge($commands, parent::getSearchCommands());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<array<string>>
|
||||
*/
|
||||
protected function getDefaultOrder(): array
|
||||
{
|
||||
return [
|
||||
['e.name', 'ASC'],
|
||||
];
|
||||
}
|
||||
|
||||
public function getTableAlias(): string
|
||||
{
|
||||
return 'e';
|
||||
}
|
||||
|
||||
/**
|
||||
* Up the click/sent counts.
|
||||
*
|
||||
* @param string $type
|
||||
* @param int $increaseBy
|
||||
*/
|
||||
public function upCount($id, $type = 'sent', $increaseBy = 1): void
|
||||
{
|
||||
try {
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
|
||||
$q->update(MAUTIC_TABLE_PREFIX.'sms_messages')
|
||||
->set($type.'_count', $type.'_count + '.(int) $increaseBy)
|
||||
->where('id = '.(int) $id);
|
||||
|
||||
$q->executeStatement();
|
||||
} catch (\Exception) {
|
||||
// not important
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int> $ignoreIds
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getSmsList(
|
||||
mixed $search = '',
|
||||
int $limit = 10,
|
||||
int $start = 0,
|
||||
bool $viewOther = false,
|
||||
?string $smsType = null,
|
||||
?string $topLevel = null,
|
||||
array $ignoreIds = [],
|
||||
) {
|
||||
$q = $this->createQueryBuilder('e');
|
||||
$q->select('partial e.{id, name, language}');
|
||||
|
||||
if (!empty($search)) {
|
||||
if (is_array($search)) {
|
||||
$search = array_map('intval', $search);
|
||||
$q->andWhere($q->expr()->in('e.id', ':search'))
|
||||
->setParameter('search', $search);
|
||||
} else {
|
||||
$q->andWhere($q->expr()->like('e.name', ':search'))
|
||||
->setParameter('search', "%{$search}%");
|
||||
}
|
||||
}
|
||||
|
||||
if (!$viewOther) {
|
||||
$q->andWhere($q->expr()->eq('e.createdBy', ':id'))
|
||||
->setParameter('id', $this->currentUser->getId());
|
||||
}
|
||||
|
||||
if (!empty($smsType)) {
|
||||
$q->andWhere(
|
||||
$q->expr()->eq('e.smsType', $q->expr()->literal($smsType))
|
||||
);
|
||||
}
|
||||
|
||||
if ('translation' === $topLevel) {
|
||||
$q->andWhere($q->expr()->isNull('e.translationParent'));
|
||||
}
|
||||
|
||||
if (!empty($ignoreIds)) {
|
||||
$q->andWhere($q->expr()->notIn('e.id', ':smsIds'))
|
||||
->setParameter('smsIds', $ignoreIds);
|
||||
}
|
||||
|
||||
$q->orderBy('e.name');
|
||||
|
||||
if (!empty($limit)) {
|
||||
$q->setFirstResult($start)
|
||||
->setMaxResults($limit);
|
||||
}
|
||||
|
||||
return $q->getQuery()->getArrayResult();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\Entity;
|
||||
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Mautic\CoreBundle\Entity\IpAddress;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
|
||||
class Stat
|
||||
{
|
||||
public const TABLE_NAME = 'sms_message_stats';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var Sms|null
|
||||
*/
|
||||
private $sms;
|
||||
|
||||
/**
|
||||
* @var Lead|null
|
||||
*/
|
||||
private $lead;
|
||||
|
||||
/**
|
||||
* @var LeadList|null
|
||||
*/
|
||||
private $list;
|
||||
|
||||
/**
|
||||
* @var IpAddress|null
|
||||
*/
|
||||
private $ipAddress;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
private $dateSent;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $trackingHash;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $source;
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
private $sourceId;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $tokens = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $details = [];
|
||||
|
||||
/**
|
||||
* @var bool|null
|
||||
*/
|
||||
private $isFailed = false;
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable(self::TABLE_NAME)
|
||||
->setCustomRepositoryClass(StatRepository::class)
|
||||
->addIndex(['sms_id', 'lead_id'], 'stat_sms_search')
|
||||
->addIndex(['tracking_hash'], 'stat_sms_hash_search')
|
||||
->addIndex(['source', 'source_id'], 'stat_sms_source_search')
|
||||
->addIndex(['is_failed'], 'stat_sms_failed_search');
|
||||
|
||||
$builder->addBigIntIdField();
|
||||
|
||||
$builder->createManyToOne('sms', 'Sms')
|
||||
->inversedBy('stats')
|
||||
->addJoinColumn('sms_id', 'id', true, false, 'SET NULL')
|
||||
->build();
|
||||
|
||||
$builder->addLead(true, 'SET NULL');
|
||||
|
||||
$builder->createManyToOne('list', LeadList::class)
|
||||
->addJoinColumn('list_id', 'id', true, false, 'SET NULL')
|
||||
->build();
|
||||
|
||||
$builder->addIpAddress(true);
|
||||
|
||||
$builder->createField('dateSent', 'datetime')
|
||||
->columnName('date_sent')
|
||||
->build();
|
||||
|
||||
$builder->createField('isFailed', 'boolean')
|
||||
->columnName('is_failed')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('trackingHash', 'string')
|
||||
->columnName('tracking_hash')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('source', 'string')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('sourceId', 'integer')
|
||||
->columnName('source_id')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('tokens', 'array')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->addField('details', Types::JSON);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the metadata for API usage.
|
||||
*/
|
||||
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
|
||||
{
|
||||
$metadata->setGroupPrefix('stat')
|
||||
->addProperties(
|
||||
[
|
||||
'id',
|
||||
'ipAddress',
|
||||
'dateSent',
|
||||
'isFailed',
|
||||
'source',
|
||||
'sourceId',
|
||||
'trackingHash',
|
||||
'lead',
|
||||
'sms',
|
||||
'details',
|
||||
]
|
||||
)
|
||||
->build();
|
||||
}
|
||||
|
||||
public function getId(): int
|
||||
{
|
||||
return (int) $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Sms
|
||||
*/
|
||||
public function getSms()
|
||||
{
|
||||
return $this->sms;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Stat
|
||||
*/
|
||||
public function setSms(Sms $sms)
|
||||
{
|
||||
$this->sms = $sms;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Lead
|
||||
*/
|
||||
public function getLead()
|
||||
{
|
||||
return $this->lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Stat
|
||||
*/
|
||||
public function setLead(Lead $lead)
|
||||
{
|
||||
$this->lead = $lead;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LeadList
|
||||
*/
|
||||
public function getList()
|
||||
{
|
||||
return $this->list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Stat
|
||||
*/
|
||||
public function setList(LeadList $list)
|
||||
{
|
||||
$this->list = $list;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return IpAddress
|
||||
*/
|
||||
public function getIpAddress()
|
||||
{
|
||||
return $this->ipAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Stat
|
||||
*/
|
||||
public function setIpAddress(IpAddress $ipAddress)
|
||||
{
|
||||
$this->ipAddress = $ipAddress;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getDateSent()
|
||||
{
|
||||
return $this->dateSent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $dateSent
|
||||
*
|
||||
* @return Stat
|
||||
*/
|
||||
public function setDateSent($dateSent)
|
||||
{
|
||||
$this->dateSent = $dateSent;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTrackingHash()
|
||||
{
|
||||
return $this->trackingHash;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $trackingHash
|
||||
*
|
||||
* @return Stat
|
||||
*/
|
||||
public function setTrackingHash($trackingHash)
|
||||
{
|
||||
$this->trackingHash = $trackingHash;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getSource()
|
||||
{
|
||||
return $this->source;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $source
|
||||
*
|
||||
* @return Stat
|
||||
*/
|
||||
public function setSource($source)
|
||||
{
|
||||
$this->source = $source;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getSourceId()
|
||||
{
|
||||
return $this->sourceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $sourceId
|
||||
*
|
||||
* @return Stat
|
||||
*/
|
||||
public function setSourceId($sourceId)
|
||||
{
|
||||
$this->sourceId = $sourceId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getTokens()
|
||||
{
|
||||
return $this->tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Stat
|
||||
*/
|
||||
public function setTokens(array $tokens)
|
||||
{
|
||||
$this->tokens = $tokens;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $isFailed
|
||||
*
|
||||
* @return Stat
|
||||
*/
|
||||
public function setIsFailed($isFailed)
|
||||
{
|
||||
$this->isFailed = $isFailed;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isFailed()
|
||||
{
|
||||
return $this->isFailed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getDetails()
|
||||
{
|
||||
return $this->details;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $details
|
||||
*
|
||||
* @return Stat
|
||||
*/
|
||||
public function setDetails($details)
|
||||
{
|
||||
$this->details = $details;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
* @param string $detail
|
||||
*
|
||||
* @return Stat
|
||||
*/
|
||||
public function addDetail($type, $detail)
|
||||
{
|
||||
$this->details[$type][] = $detail;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
use Mautic\CoreBundle\Helper\DateTimeHelper;
|
||||
use Mautic\LeadBundle\Entity\TimelineTrait;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<Stat>
|
||||
*/
|
||||
class StatRepository extends CommonRepository
|
||||
{
|
||||
use TimelineTrait;
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*
|
||||
* @throws \Doctrine\ORM\NoResultException
|
||||
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||
*/
|
||||
public function getSmsStatus($trackingHash)
|
||||
{
|
||||
$q = $this->createQueryBuilder('s');
|
||||
$q->select('s')
|
||||
->leftJoin('s.lead', 'l')
|
||||
->leftJoin('s.sms', 'e')
|
||||
->where(
|
||||
$q->expr()->eq('s.trackingHash', ':hash')
|
||||
)
|
||||
->setParameter('hash', $trackingHash);
|
||||
|
||||
$result = $q->getQuery()->getResult();
|
||||
|
||||
return (!empty($result)) ? $result[0] : null;
|
||||
}
|
||||
|
||||
public function getSentStats($smsId, $listId = null): array
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
$q->select('s.lead_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'sms_messages_stats', 's')
|
||||
->where('s.sms_id = :sms')
|
||||
->setParameter('sms', $smsId);
|
||||
|
||||
if ($listId) {
|
||||
$q->andWhere('s.list_id = :list')
|
||||
->setParameter('list', $listId);
|
||||
}
|
||||
|
||||
$result = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
// index by lead
|
||||
$stats = [];
|
||||
foreach ($result as $r) {
|
||||
$stats[$r['lead_id']] = $r['lead_id'];
|
||||
}
|
||||
|
||||
unset($result);
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|array $smsIds
|
||||
* @param int $listId
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getSentCount($smsIds = null, $listId = null)
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
|
||||
$q->select('count(s.id) as sent_count')
|
||||
->from(MAUTIC_TABLE_PREFIX.'sms_message_stats', 's');
|
||||
|
||||
if ($smsIds) {
|
||||
if (!is_array($smsIds)) {
|
||||
$smsIds = [(int) $smsIds];
|
||||
}
|
||||
$q->where(
|
||||
$q->expr()->in('s.sms_id', $smsIds)
|
||||
);
|
||||
}
|
||||
|
||||
if ($listId) {
|
||||
$q->andWhere('s.list_id = '.(int) $listId);
|
||||
}
|
||||
|
||||
$q->andWhere('s.is_failed = :false')
|
||||
->setParameter('false', false, 'boolean');
|
||||
|
||||
$results = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
return (isset($results[0])) ? $results[0]['sent_count'] : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a lead's email stat.
|
||||
*
|
||||
* @param int $leadId
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @throws \Doctrine\ORM\NoResultException
|
||||
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||
*/
|
||||
public function getLeadStats($leadId, array $options = [])
|
||||
{
|
||||
$query = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$query->from(MAUTIC_TABLE_PREFIX.'sms_message_stats', 's')
|
||||
->leftJoin('s', MAUTIC_TABLE_PREFIX.'sms_messages', 'e', 's.sms_id = e.id');
|
||||
|
||||
if ($leadId) {
|
||||
$query->andWhere(
|
||||
$query->expr()->eq('s.lead_id', ':leadId')
|
||||
)->setParameter('leadId', $leadId);
|
||||
}
|
||||
|
||||
if (!empty($options['basic_select'])) {
|
||||
$query->select(
|
||||
's.sms_id, s.id, s.date_sent as dateSent, e.name, e.name as sms_name, s.is_failed as isFailed'
|
||||
);
|
||||
} else {
|
||||
$query->select(
|
||||
's.sms_id, s.id, s.date_sent as dateSent, e.name, e.name as sms_name, e.message, e.sms_type as type, s.is_failed as isFailed, s.list_id, l.name as list_name, s.tracking_hash as idHash, s.lead_id, s.details'
|
||||
)
|
||||
->leftJoin('s', MAUTIC_TABLE_PREFIX.'lead_lists', 'l', 's.list_id = l.id');
|
||||
}
|
||||
|
||||
if (isset($options['state'])) {
|
||||
$state = $options['state'];
|
||||
if ('failed' == $state) {
|
||||
$query->andWhere(
|
||||
$query->expr()->eq('s.is_failed', 1)
|
||||
);
|
||||
}
|
||||
}
|
||||
$state = 'sent';
|
||||
|
||||
if (isset($options['search']) && $options['search']) {
|
||||
$query->andWhere(
|
||||
$query->expr()->or(
|
||||
$query->expr()->like('e.name', $query->expr()->literal('%'.$options['search'].'%'))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (isset($options['fromDate']) && $options['fromDate']) {
|
||||
$dt = new DateTimeHelper($options['fromDate']);
|
||||
$query->andWhere(
|
||||
$query->expr()->gte('s.date_sent', $query->expr()->literal($dt->toUtcString()))
|
||||
);
|
||||
}
|
||||
|
||||
return $this->getTimelineResults(
|
||||
$query,
|
||||
$options,
|
||||
'e.name',
|
||||
's.date_'.$state
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates lead ID (e.g. after a lead merge).
|
||||
*/
|
||||
public function updateLead($fromLeadId, $toLeadId): void
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
$q->update(MAUTIC_TABLE_PREFIX.'sms_message_stats')
|
||||
->set('sms_id', (int) $toLeadId)
|
||||
->where('sms_id = '.(int) $fromLeadId)
|
||||
->executeStatement();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a stat.
|
||||
*/
|
||||
public function deleteStat($id): void
|
||||
{
|
||||
$this->_em->getConnection()->delete(MAUTIC_TABLE_PREFIX.'sms_message_stats', ['id' => (int) $id]);
|
||||
}
|
||||
|
||||
public function getTableAlias(): string
|
||||
{
|
||||
return 's';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\Event;
|
||||
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadEventLog;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class ReplyEvent extends \Symfony\Contracts\EventDispatcher\Event
|
||||
{
|
||||
private ?Response $response = null;
|
||||
|
||||
private ?LeadEventLog $eventLog = null;
|
||||
|
||||
/**
|
||||
* ReplyEvent constructor.
|
||||
*
|
||||
* @param string $message
|
||||
*/
|
||||
public function __construct(
|
||||
private Lead $contact,
|
||||
private $message,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Lead
|
||||
*/
|
||||
public function getContact()
|
||||
{
|
||||
return $this->contact;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getMessage()
|
||||
{
|
||||
return $this->message;
|
||||
}
|
||||
|
||||
public function setResponse(Response $response): void
|
||||
{
|
||||
$this->response = $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Response|null
|
||||
*/
|
||||
public function getResponse()
|
||||
{
|
||||
return $this->response;
|
||||
}
|
||||
|
||||
public function getEventLog(): ?LeadEventLog
|
||||
{
|
||||
return $this->eventLog;
|
||||
}
|
||||
|
||||
public function setEventLog(LeadEventLog $eventLog): void
|
||||
{
|
||||
$this->eventLog = $eventLog;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\Event;
|
||||
|
||||
use Mautic\CoreBundle\Event\CommonEvent;
|
||||
use Mautic\SmsBundle\Entity\Sms;
|
||||
|
||||
class SmsEvent extends CommonEvent
|
||||
{
|
||||
/**
|
||||
* @param bool $isNew
|
||||
*/
|
||||
public function __construct(Sms $sms, $isNew = false)
|
||||
{
|
||||
$this->entity = $sms;
|
||||
$this->isNew = $isNew;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Sms entity.
|
||||
*
|
||||
* @return Sms
|
||||
*/
|
||||
public function getSms()
|
||||
{
|
||||
return $this->entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Sms entity.
|
||||
*/
|
||||
public function setSms(Sms $sms): void
|
||||
{
|
||||
$this->entity = $sms;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\Event;
|
||||
|
||||
use Mautic\CoreBundle\Event\CommonEvent;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
|
||||
class SmsSendEvent extends CommonEvent
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $smsId;
|
||||
|
||||
/**
|
||||
* @param string $content
|
||||
*/
|
||||
public function __construct(
|
||||
protected $content,
|
||||
protected Lead $lead,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getContent()
|
||||
{
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $content
|
||||
*/
|
||||
public function setContent($content): void
|
||||
{
|
||||
$this->content = $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Lead
|
||||
*/
|
||||
public function getLead()
|
||||
{
|
||||
return $this->lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Lead $lead
|
||||
*/
|
||||
public function setLead($lead): void
|
||||
{
|
||||
$this->lead = $lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getSmsId()
|
||||
{
|
||||
return $this->smsId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $smsId
|
||||
*/
|
||||
public function setSmsId($smsId): void
|
||||
{
|
||||
$this->smsId = $smsId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\Event;
|
||||
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
class TokensBuildEvent extends Event
|
||||
{
|
||||
/**
|
||||
* @param array<string,array<int|string>> $tokens
|
||||
*/
|
||||
public function __construct(private array $tokens)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,array<int|string>>
|
||||
*/
|
||||
public function getTokens(): array
|
||||
{
|
||||
return $this->tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,array<int|string>> $tokens
|
||||
*/
|
||||
public function setTokens($tokens): void
|
||||
{
|
||||
$this->tokens = $tokens;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\EventListener;
|
||||
|
||||
use Mautic\ChannelBundle\ChannelEvents;
|
||||
use Mautic\ChannelBundle\Event\ChannelBroadcastEvent;
|
||||
use Mautic\SmsBundle\Broadcast\BroadcastExecutioner;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class BroadcastSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private BroadcastExecutioner $broadcastExecutioner,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
ChannelEvents::CHANNEL_BROADCAST => ['onBroadcast', 0],
|
||||
];
|
||||
}
|
||||
|
||||
public function onBroadcast(ChannelBroadcastEvent $event): void
|
||||
{
|
||||
if (!$event->checkContext('sms')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->broadcastExecutioner->execute($event);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\EventListener;
|
||||
|
||||
use Mautic\CampaignBundle\CampaignEvents;
|
||||
use Mautic\CampaignBundle\Event\CampaignBuilderEvent;
|
||||
use Mautic\CampaignBundle\Event\DecisionEvent;
|
||||
use Mautic\CampaignBundle\Executioner\RealTimeExecutioner;
|
||||
use Mautic\SmsBundle\Event\ReplyEvent;
|
||||
use Mautic\SmsBundle\Form\Type\CampaignReplyType;
|
||||
use Mautic\SmsBundle\Helper\ReplyHelper;
|
||||
use Mautic\SmsBundle\Sms\TransportChain;
|
||||
use Mautic\SmsBundle\SmsEvents;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class CampaignReplySubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public const TYPE = 'sms.reply';
|
||||
|
||||
public function __construct(
|
||||
private TransportChain $transportChain,
|
||||
private RealTimeExecutioner $realTimeExecutioner,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
CampaignEvents::CAMPAIGN_ON_BUILD => ['onCampaignBuild', 0],
|
||||
SmsEvents::ON_CAMPAIGN_REPLY => ['onCampaignReply', 0],
|
||||
SmsEvents::ON_REPLY => ['onReply', 0],
|
||||
];
|
||||
}
|
||||
|
||||
public function onCampaignBuild(CampaignBuilderEvent $event): void
|
||||
{
|
||||
if (0 === count($this->transportChain->getEnabledTransports())) {
|
||||
return;
|
||||
}
|
||||
|
||||
$event->addDecision(
|
||||
self::TYPE,
|
||||
[
|
||||
'label' => 'mautic.campaign.sms.reply',
|
||||
'description' => 'mautic.campaign.sms.reply.tooltip',
|
||||
'eventName' => SmsEvents::ON_CAMPAIGN_REPLY,
|
||||
'formType' => CampaignReplyType::class,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function onCampaignReply(DecisionEvent $decisionEvent): void
|
||||
{
|
||||
/** @var ReplyEvent $replyEvent */
|
||||
$replyEvent = $decisionEvent->getPassthrough();
|
||||
$pattern = $decisionEvent->getLog()->getEvent()->getProperties()['pattern'];
|
||||
|
||||
if (empty($pattern)) {
|
||||
// Assume any reply
|
||||
$decisionEvent->setAsApplicable();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ReplyHelper::matches($pattern, $replyEvent->getMessage())) {
|
||||
// It does not match so ignore
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$decisionEvent->setChannel('sms');
|
||||
$decisionEvent->setAsApplicable();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogNotProcessedException
|
||||
* @throws \Mautic\CampaignBundle\Executioner\Dispatcher\Exception\LogPassedAndFailedException
|
||||
* @throws \Mautic\CampaignBundle\Executioner\Exception\CannotProcessEventException
|
||||
* @throws \Mautic\CampaignBundle\Executioner\Scheduler\Exception\NotSchedulableException
|
||||
*/
|
||||
public function onReply(ReplyEvent $event): void
|
||||
{
|
||||
$this->realTimeExecutioner->execute(self::TYPE, $event, 'sms');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\EventListener;
|
||||
|
||||
use Mautic\CampaignBundle\CampaignEvents;
|
||||
use Mautic\CampaignBundle\Event\CampaignBuilderEvent;
|
||||
use Mautic\CampaignBundle\Event\CampaignExecutionEvent;
|
||||
use Mautic\SmsBundle\Form\Type\SmsSendType;
|
||||
use Mautic\SmsBundle\Model\SmsModel;
|
||||
use Mautic\SmsBundle\Sms\TransportChain;
|
||||
use Mautic\SmsBundle\SmsEvents;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class CampaignSendSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private SmsModel $smsModel,
|
||||
private TransportChain $transportChain,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
CampaignEvents::CAMPAIGN_ON_BUILD => ['onCampaignBuild', 0],
|
||||
SmsEvents::ON_CAMPAIGN_TRIGGER_ACTION => ['onCampaignTriggerAction', 0],
|
||||
];
|
||||
}
|
||||
|
||||
public function onCampaignBuild(CampaignBuilderEvent $event): void
|
||||
{
|
||||
if (count($this->transportChain->getEnabledTransports()) > 0) {
|
||||
$event->addAction(
|
||||
'sms.send_text_sms',
|
||||
[
|
||||
'label' => 'mautic.campaign.sms.send_text_sms',
|
||||
'description' => 'mautic.campaign.sms.send_text_sms.tooltip',
|
||||
'eventName' => SmsEvents::ON_CAMPAIGN_TRIGGER_ACTION,
|
||||
'formType' => SmsSendType::class,
|
||||
'formTypeOptions' => ['update_select' => 'campaignevent_properties_sms'],
|
||||
'formTheme' => '@MauticSms/FormTheme/SmsSendList/smssend_list_row.html.twig',
|
||||
'channel' => 'sms',
|
||||
'channelIdField' => 'sms',
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function onCampaignTriggerAction(CampaignExecutionEvent $event): void
|
||||
{
|
||||
$lead = $event->getLead();
|
||||
$smsId = (int) $event->getConfig()['sms'];
|
||||
$sms = $this->smsModel->getEntity($smsId);
|
||||
|
||||
if (!$sms) {
|
||||
$event->setFailed('mautic.sms.campaign.failed.missing_entity');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$sms->isPublished()) {
|
||||
$event->setFailed('mautic.sms.campaign.failed.unpublished');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$result = $this->smsModel->sendSms($sms, $lead, ['channel' => ['campaign.event', $event->getEvent()['id']]])[$lead->getId()];
|
||||
|
||||
if ('Authenticate' === $result['status']) {
|
||||
// Don't fail the event but reschedule it for later
|
||||
$event->setResult(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!empty($result['sent'])) {
|
||||
$event->setChannel('sms', $sms->getId());
|
||||
$event->setResult($result);
|
||||
} else {
|
||||
$result['failed'] = true;
|
||||
$result['reason'] = $result['status'];
|
||||
$event->setResult($result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\EventListener;
|
||||
|
||||
use Mautic\ChannelBundle\ChannelEvents;
|
||||
use Mautic\ChannelBundle\Event\ChannelEvent;
|
||||
use Mautic\ChannelBundle\Model\MessageModel;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Mautic\ReportBundle\Model\ReportModel;
|
||||
use Mautic\SmsBundle\Form\Type\SmsListType;
|
||||
use Mautic\SmsBundle\Sms\TransportChain;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class ChannelSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private TransportChain $transportChain,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
ChannelEvents::ADD_CHANNEL => ['onAddChannel', 90],
|
||||
];
|
||||
}
|
||||
|
||||
public function onAddChannel(ChannelEvent $event): void
|
||||
{
|
||||
if (count($this->transportChain->getEnabledTransports()) > 0) {
|
||||
$event->addChannel(
|
||||
'sms',
|
||||
[
|
||||
MessageModel::CHANNEL_FEATURE => [
|
||||
'campaignAction' => 'sms.send_text_sms',
|
||||
'campaignDecisionsSupported' => [
|
||||
'page.pagehit',
|
||||
'asset.download',
|
||||
'form.submit',
|
||||
],
|
||||
'lookupFormType' => SmsListType::class,
|
||||
'repository' => \Mautic\SmsBundle\Entity\Sms::class,
|
||||
],
|
||||
LeadModel::CHANNEL_FEATURE => [],
|
||||
ReportModel::CHANNEL_FEATURE => [
|
||||
'table' => 'sms_messages',
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\EventListener;
|
||||
|
||||
use Mautic\ConfigBundle\ConfigEvents;
|
||||
use Mautic\ConfigBundle\Event\ConfigBuilderEvent;
|
||||
use Mautic\SmsBundle\Form\Type\ConfigType;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class ConfigSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
ConfigEvents::CONFIG_ON_GENERATE => ['onConfigGenerate', 0],
|
||||
];
|
||||
}
|
||||
|
||||
public function onConfigGenerate(ConfigBuilderEvent $event): void
|
||||
{
|
||||
$event->addForm([
|
||||
'bundle' => 'SmsBundle',
|
||||
'formAlias' => 'smsconfig',
|
||||
'formType' => ConfigType::class,
|
||||
'formTheme' => '@MauticSms/FormTheme/Config/_config_smsconfig_widget.html.twig',
|
||||
'parameters' => $event->getParametersFromConfig('MauticSmsBundle'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\EventListener;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Mautic\LeadBundle\Event\LeadTimelineEvent;
|
||||
use Mautic\LeadBundle\LeadEvents;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class LeadSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private TranslatorInterface $translator,
|
||||
private RouterInterface $router,
|
||||
private EntityManager $em,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
LeadEvents::TIMELINE_ON_GENERATE => ['onTimelineGenerate', 0],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile events for the lead timeline.
|
||||
*/
|
||||
public function onTimelineGenerate(LeadTimelineEvent $event): void
|
||||
{
|
||||
$this->addSmsEvents($event, 'sent');
|
||||
$this->addSmsEvents($event, 'failed');
|
||||
}
|
||||
|
||||
protected function addSmsEvents(LeadTimelineEvent $event, $state)
|
||||
{
|
||||
// Set available event types
|
||||
$eventTypeKey = 'sms.'.$state;
|
||||
$eventTypeName = $this->translator->trans('mautic.sms.timeline.status.'.$state);
|
||||
$event->addEventType($eventTypeKey, $eventTypeName);
|
||||
$event->addSerializerGroup('smsList');
|
||||
|
||||
// Decide if those events are filtered
|
||||
if (!$event->isApplicable($eventTypeKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var \Mautic\SmsBundle\Entity\StatRepository $statRepository */
|
||||
$statRepository = $this->em->getRepository(\Mautic\SmsBundle\Entity\Stat::class);
|
||||
$queryOptions = $event->getQueryOptions();
|
||||
$queryOptions['state'] = $state;
|
||||
$stats = $statRepository->getLeadStats($event->getLeadId(), $queryOptions);
|
||||
|
||||
// Add total to counter
|
||||
$event->addToCounter($eventTypeKey, $stats);
|
||||
|
||||
if (!$event->isEngagementCount()) {
|
||||
// Add the events to the event array
|
||||
foreach ($stats['results'] as $stat) {
|
||||
if (!empty($stat['sms_name'])) {
|
||||
$label = $stat['sms_name'];
|
||||
} else {
|
||||
$label = $this->translator->trans('mautic.sms.timeline.event.custom_sms');
|
||||
}
|
||||
|
||||
$eventName = [
|
||||
'label' => $label,
|
||||
'href' => $this->router->generate('mautic_sms_action', ['objectAction'=>'view', 'objectId' => $stat['sms_id']]),
|
||||
];
|
||||
if ('failed' == $state or 'sent' == $state) { // this is to get the correct column for date dateSent
|
||||
$dateSent = 'sent';
|
||||
}
|
||||
|
||||
$contactId = $stat['lead_id'];
|
||||
unset($stat['lead_id']);
|
||||
$event->addEvent(
|
||||
[
|
||||
'event' => $eventTypeKey,
|
||||
'eventId' => $eventTypeKey.$stat['id'],
|
||||
'eventLabel' => $eventName,
|
||||
'eventType' => $eventTypeName,
|
||||
'timestamp' => $stat['date'.ucfirst($dateSent)],
|
||||
'extra' => [
|
||||
'stat' => $stat,
|
||||
'type' => $state,
|
||||
],
|
||||
'contentTemplate' => '@MauticSms/SubscribedEvents/Timeline/index.html.twig',
|
||||
'icon' => ('read' == $state) ? 'ri-chat-1-fill' : 'ri-message-2-fill',
|
||||
'contactId' => $contactId,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\EventListener;
|
||||
|
||||
use Mautic\ChannelBundle\ChannelEvents;
|
||||
use Mautic\ChannelBundle\Entity\MessageQueue;
|
||||
use Mautic\ChannelBundle\Event\MessageQueueBatchProcessEvent;
|
||||
use Mautic\SmsBundle\Model\SmsModel;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class MessageQueueSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private SmsModel $model,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
ChannelEvents::PROCESS_MESSAGE_QUEUE_BATCH => ['onProcessMessageQueueBatch', 0],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends campaign emails.
|
||||
*/
|
||||
public function onProcessMessageQueueBatch(MessageQueueBatchProcessEvent $event): void
|
||||
{
|
||||
if (!$event->checkContext('sms')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$messages = $event->getMessages();
|
||||
$id = $event->getChannelId();
|
||||
$sms = $this->model->getEntity($id);
|
||||
$sendTo = [];
|
||||
$messagesByContact = [];
|
||||
|
||||
/** @var MessageQueue $message */
|
||||
foreach ($messages as $message) {
|
||||
if ($sms && $message->getLead() && $sms->isPublished()) {
|
||||
$contact = $message->getLead();
|
||||
$mobile = $contact->getMobile();
|
||||
$phone = $contact->getPhone();
|
||||
if (empty($mobile) && empty($phone)) {
|
||||
$message->setProcessed();
|
||||
$message->setSuccess();
|
||||
}
|
||||
$sendTo[$contact->getId()] = $contact;
|
||||
$messagesByContact[$contact->getId()] = $message;
|
||||
} else {
|
||||
$message->setFailed();
|
||||
}
|
||||
}
|
||||
|
||||
if (count($sendTo)) {
|
||||
$options['resend_message_queue'] = $messagesByContact;
|
||||
$results = $this->model->sendSms($sms, $sendTo, $options);
|
||||
|
||||
foreach ($messagesByContact as $contactId => $message) {
|
||||
if (!$message->isProcessed()) {
|
||||
$message->setProcessed();
|
||||
$message->setMetadata($results[$contactId]);
|
||||
if ($results[$contactId]['sent']) {
|
||||
$message->setSuccess();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$event->stopPropagation();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\EventListener;
|
||||
|
||||
use Mautic\CoreBundle\Helper\InputHelper;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\LeadBundle\Entity\LeadEventLog;
|
||||
use Mautic\LeadBundle\Entity\LeadEventLogRepository;
|
||||
use Mautic\LeadBundle\Event\LeadTimelineEvent;
|
||||
use Mautic\LeadBundle\EventListener\TimelineEventLogTrait;
|
||||
use Mautic\LeadBundle\LeadEvents;
|
||||
use Mautic\SmsBundle\Event\ReplyEvent;
|
||||
use Mautic\SmsBundle\SmsEvents;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class ReplySubscriber implements EventSubscriberInterface
|
||||
{
|
||||
use TimelineEventLogTrait;
|
||||
|
||||
public function __construct(Translator $translator, LeadEventLogRepository $eventLogRepository)
|
||||
{
|
||||
$this->translator = $translator;
|
||||
$this->eventLogRepository = $eventLogRepository;
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
SmsEvents::ON_REPLY => ['onReply', 0],
|
||||
LeadEvents::TIMELINE_ON_GENERATE => 'onTimelineGenerate',
|
||||
];
|
||||
}
|
||||
|
||||
public function onReply(ReplyEvent $event): void
|
||||
{
|
||||
$message = $event->getMessage();
|
||||
$contact = $event->getContact();
|
||||
|
||||
$log = new LeadEventLog();
|
||||
$log
|
||||
->setLead($contact)
|
||||
->setBundle('sms')
|
||||
->setObject('sms')
|
||||
->setAction('reply')
|
||||
->setProperties(
|
||||
[
|
||||
'message' => InputHelper::clean($message),
|
||||
]
|
||||
);
|
||||
|
||||
$this->eventLogRepository->saveEntity($log);
|
||||
$this->eventLogRepository->detachEntity($log);
|
||||
$event->setEventLog($log);
|
||||
}
|
||||
|
||||
public function onTimelineGenerate(LeadTimelineEvent $event): void
|
||||
{
|
||||
$this->addEvents(
|
||||
$event,
|
||||
'sms_reply',
|
||||
'mautic.sms.timeline.reply',
|
||||
'ri-smartphone-line',
|
||||
'sms',
|
||||
'sms',
|
||||
'reply',
|
||||
'@MauticSms/SubscribedEvents/Timeline/reply.html.twig'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\SmsBundle\EventListener;
|
||||
|
||||
use Mautic\CoreBundle\CoreEvents;
|
||||
use Mautic\CoreBundle\DTO\GlobalSearchFilterDTO;
|
||||
use Mautic\CoreBundle\Event as MauticEvents;
|
||||
use Mautic\CoreBundle\Service\GlobalSearch;
|
||||
use Mautic\SmsBundle\Model\SmsModel;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class SearchSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private SmsModel $model,
|
||||
private GlobalSearch $globalSearch,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
CoreEvents::GLOBAL_SEARCH => ['onGlobalSearch', 0],
|
||||
];
|
||||
}
|
||||
|
||||
public function onGlobalSearch(MauticEvents\GlobalSearchEvent $event): void
|
||||
{
|
||||
$filterDTO = new GlobalSearchFilterDTO($event->getSearchString());
|
||||
$results = $this->globalSearch->performSearch(
|
||||
$filterDTO,
|
||||
$this->model,
|
||||
'@MauticSms/SubscribedEvents/Search/global.html.twig'
|
||||
);
|
||||
|
||||
if (!empty($results)) {
|
||||
$event->addResults('mautic.sms.smses.header', $results);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\EventListener;
|
||||
|
||||
use Mautic\AssetBundle\Helper\TokenHelper as AssetTokenHelper;
|
||||
use Mautic\CoreBundle\Event\TokenReplacementEvent;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Model\AuditLogModel;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Helper\TokenHelper;
|
||||
use Mautic\PageBundle\Entity\Trackable;
|
||||
use Mautic\PageBundle\Helper\TokenHelper as PageTokenHelper;
|
||||
use Mautic\PageBundle\Model\TrackableModel;
|
||||
use Mautic\SmsBundle\Event\SmsEvent;
|
||||
use Mautic\SmsBundle\Helper\SmsHelper;
|
||||
use Mautic\SmsBundle\SmsEvents;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class SmsSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private AuditLogModel $auditLogModel,
|
||||
private TrackableModel $trackableModel,
|
||||
private PageTokenHelper $pageTokenHelper,
|
||||
private AssetTokenHelper $assetTokenHelper,
|
||||
private SmsHelper $smsHelper,
|
||||
private CoreParametersHelper $coreParametersHelper,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
SmsEvents::SMS_POST_SAVE => ['onPostSave', 0],
|
||||
SmsEvents::SMS_POST_DELETE => ['onDelete', 0],
|
||||
SmsEvents::TOKEN_REPLACEMENT => ['onTokenReplacement', 0],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an entry to the audit log.
|
||||
*/
|
||||
public function onPostSave(SmsEvent $event): void
|
||||
{
|
||||
$entity = $event->getSms();
|
||||
if ($details = $event->getChanges()) {
|
||||
$log = [
|
||||
'bundle' => 'sms',
|
||||
'object' => 'sms',
|
||||
'objectId' => $entity->getId(),
|
||||
'action' => ($event->isNew()) ? 'create' : 'update',
|
||||
'details' => $details,
|
||||
];
|
||||
$this->auditLogModel->writeToLog($log);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a delete entry to the audit log.
|
||||
*/
|
||||
public function onDelete(SmsEvent $event): void
|
||||
{
|
||||
$entity = $event->getSms();
|
||||
$log = [
|
||||
'bundle' => 'sms',
|
||||
'object' => 'sms',
|
||||
'objectId' => $entity->deletedId,
|
||||
'action' => 'delete',
|
||||
'details' => ['name' => $entity->getName()],
|
||||
];
|
||||
$this->auditLogModel->writeToLog($log);
|
||||
}
|
||||
|
||||
public function onTokenReplacement(TokenReplacementEvent $event): void
|
||||
{
|
||||
/** @var Lead $lead */
|
||||
$lead = $event->getLead();
|
||||
$content = $event->getContent();
|
||||
$clickthrough = $event->getClickthrough();
|
||||
|
||||
if ($content) {
|
||||
$tokens = array_merge(
|
||||
TokenHelper::findLeadTokens($content, $lead->getProfileFields()),
|
||||
$this->pageTokenHelper->findPageTokens($content, $clickthrough),
|
||||
$this->assetTokenHelper->findAssetTokens($content, $clickthrough)
|
||||
);
|
||||
|
||||
// Disable trackable urls
|
||||
if (!$this->smsHelper->getDisableTrackableUrls()) {
|
||||
[$content, $trackables] = $this->trackableModel->parseContentForTrackables(
|
||||
$content,
|
||||
$tokens,
|
||||
'sms',
|
||||
$clickthrough['channel'][1]
|
||||
);
|
||||
|
||||
$shortenEnabled = $this->coreParametersHelper->get('shortener_sms_enable', false);
|
||||
|
||||
/**
|
||||
* @var string $token
|
||||
* @var Trackable $trackable
|
||||
*/
|
||||
foreach ($trackables as $token => $trackable) {
|
||||
$tokens[$token] = $this->trackableModel->generateTrackableUrl($trackable, $clickthrough, $shortenEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
$content = str_replace(array_keys($tokens), array_values($tokens), $content);
|
||||
foreach ($tokens as $token => $value) {
|
||||
$event->addToken($token, $value);
|
||||
}
|
||||
|
||||
$event->setContent($content);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\EventListener;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Mautic\CoreBundle\EventListener\CommonStatsSubscriber;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\SmsBundle\Entity\Stat;
|
||||
|
||||
class StatsSubscriber extends CommonStatsSubscriber
|
||||
{
|
||||
public function __construct(CorePermissions $security, EntityManager $entityManager)
|
||||
{
|
||||
parent::__construct($security, $entityManager);
|
||||
$this->addContactRestrictedRepositories([Stat::class]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\EventListener;
|
||||
|
||||
use Mautic\LeadBundle\Entity\DoNotContact;
|
||||
use Mautic\LeadBundle\Model\DoNotContact as DoNotContactModel;
|
||||
use Mautic\SmsBundle\Event\ReplyEvent;
|
||||
use Mautic\SmsBundle\SmsEvents;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class StopSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private DoNotContactModel $doNotContactModel,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
SmsEvents::ON_REPLY => ['onReply', 0],
|
||||
];
|
||||
}
|
||||
|
||||
public function onReply(ReplyEvent $event): void
|
||||
{
|
||||
$message = $event->getMessage();
|
||||
|
||||
if ('stop' === strtolower($message)) {
|
||||
// Unsubscribe the contact
|
||||
$this->doNotContactModel->addDncForContact($event->getContact()->getId(), 'sms', DoNotContact::UNSUBSCRIBED);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\EventListener;
|
||||
|
||||
use Mautic\LeadBundle\Event\ContactIdentificationEvent;
|
||||
use Mautic\LeadBundle\LeadEvents;
|
||||
use Mautic\SmsBundle\Entity\Stat;
|
||||
use Mautic\SmsBundle\Entity\StatRepository;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class TrackingSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private StatRepository $statRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
LeadEvents::ON_CLICKTHROUGH_IDENTIFICATION => ['onIdentifyContact', 0],
|
||||
];
|
||||
}
|
||||
|
||||
public function onIdentifyContact(ContactIdentificationEvent $event): void
|
||||
{
|
||||
$clickthrough = $event->getClickthrough();
|
||||
|
||||
// Nothing left to identify by so stick to the tracked lead
|
||||
if (empty($clickthrough['channel']['sms']) && empty($clickthrough['stat'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var Stat $stat */
|
||||
$stat = $this->statRepository->findOneBy(['trackingHash' => $clickthrough['stat']]);
|
||||
|
||||
if (!$stat) {
|
||||
// Stat doesn't exist so use the tracked lead
|
||||
return;
|
||||
}
|
||||
|
||||
if ($stat->getSms() && (int) $stat->getSms()->getId() !== (int) $clickthrough['channel']['sms']) {
|
||||
// ID mismatch - fishy so use tracked lead
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$contact = $stat->getLead()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$event->setIdentifiedContact($contact, 'sms');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\SmsBundle\EventListener;
|
||||
|
||||
use Mautic\SmsBundle\Event\SmsSendEvent;
|
||||
use Mautic\SmsBundle\SmsEvents;
|
||||
use Mautic\WebhookBundle\Event\WebhookBuilderEvent;
|
||||
use Mautic\WebhookBundle\Model\WebhookModel;
|
||||
use Mautic\WebhookBundle\WebhookEvents;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
final class WebhookSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private WebhookModel $webhookModel,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
SmsEvents::SMS_ON_SEND => 'onSend',
|
||||
WebhookEvents::WEBHOOK_ON_BUILD => 'onWebhookBuild',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add event triggers and actions.
|
||||
*/
|
||||
public function onWebhookBuild(WebhookBuilderEvent $event): void
|
||||
{
|
||||
$event->addEvent(
|
||||
SmsEvents::SMS_ON_SEND,
|
||||
[
|
||||
'label' => 'mautic.sms.webhook.event.send',
|
||||
'description' => 'mautic.sms.webhook.event.send_desc',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function onSend(SmsSendEvent $event): void
|
||||
{
|
||||
$this->webhookModel->queueWebhooksByType(
|
||||
SmsEvents::SMS_ON_SEND,
|
||||
[
|
||||
'smsId' => $event->getSmsId(),
|
||||
'contact' => $event->getLead(),
|
||||
'content' => $event->getContent(),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\Exception;
|
||||
|
||||
class CallbackHandlerNotFound extends \Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\Exception;
|
||||
|
||||
class NumberNotFoundException extends \Exception
|
||||
{
|
||||
/**
|
||||
* @param string $number
|
||||
* @param string $message
|
||||
* @param int $code
|
||||
*/
|
||||
public function __construct(
|
||||
private $number,
|
||||
$message = '',
|
||||
$code = 0,
|
||||
?\Throwable $previous = null,
|
||||
) {
|
||||
if (!$message) {
|
||||
$message = "Phone number '{$number}' not found";
|
||||
}
|
||||
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getNumber()
|
||||
{
|
||||
return $this->number;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\Exception;
|
||||
|
||||
class PrimaryTransportNotEnabledException extends \Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\Form\Type;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<array<mixed>>
|
||||
*/
|
||||
class CampaignReplyType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->add(
|
||||
'pattern',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'mautic.sms.reply_pattern',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'tooltip' => 'mautic.sms.reply_pattern.tooltip',
|
||||
],
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\Form\Type;
|
||||
|
||||
use Mautic\CoreBundle\Form\Type\YesNoButtonGroupType;
|
||||
use Mautic\SmsBundle\Sms\TransportChain;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<array<mixed>>
|
||||
*/
|
||||
class ConfigType extends AbstractType
|
||||
{
|
||||
public const SMS_DISABLE_TRACKABLE_URLS = 'sms_disable_trackable_urls';
|
||||
|
||||
public function __construct(
|
||||
private TransportChain $transportChain,
|
||||
private TranslatorInterface $translator,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface<array<mixed>|null> $builder
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$choices = [];
|
||||
$transports = $this->transportChain->getEnabledTransports();
|
||||
foreach ($transports as $transportServiceId=>$transport) {
|
||||
$choices[$this->translator->trans($transportServiceId)] = $transportServiceId;
|
||||
}
|
||||
|
||||
$builder->add('sms_transport', ChoiceType::class, [
|
||||
'label' => 'mautic.sms.config.select_default_transport',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'tooltip' => 'mautic.sms.config.select_default_transport',
|
||||
],
|
||||
'choices' => $choices,
|
||||
]);
|
||||
|
||||
$builder->add(
|
||||
self::SMS_DISABLE_TRACKABLE_URLS,
|
||||
YesNoButtonGroupType::class,
|
||||
[
|
||||
'label' => 'mautic.sms.config.form.sms.disable_trackable_urls',
|
||||
'attr' => [
|
||||
'tooltip' => 'mautic.sms.config.form.sms.disable_trackable_urls.tooltip',
|
||||
],
|
||||
'data'=> !empty($options['data'][self::SMS_DISABLE_TRACKABLE_URLS]) ? true : false,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function getBlockPrefix(): string
|
||||
{
|
||||
return 'smsconfig';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\Form\Type;
|
||||
|
||||
use Mautic\CoreBundle\Form\Type\EntityLookupType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\OptionsResolver\Options;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<array<mixed>>
|
||||
*/
|
||||
class SmsListType extends AbstractType
|
||||
{
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults(
|
||||
[
|
||||
'modal_route' => 'mautic_sms_action',
|
||||
'modal_header' => 'mautic.sms.header.new',
|
||||
'model' => 'sms',
|
||||
'model_lookup_method' => 'getLookupResults',
|
||||
'lookup_arguments' => fn (Options $options): array => [
|
||||
'type' => SmsType::class,
|
||||
'filter' => '$data',
|
||||
'limit' => 0,
|
||||
'start' => 0,
|
||||
'options' => [
|
||||
'sms_type' => $options['sms_type'],
|
||||
'top_level' => $options['top_level'],
|
||||
'ignore_ids' => $options['ignore_ids'],
|
||||
],
|
||||
],
|
||||
'ajax_lookup_action' => function (Options $options): string {
|
||||
$query = [
|
||||
'sms_type' => $options['sms_type'],
|
||||
'top_level' => $options['top_level'],
|
||||
'ignore_ids' => $options['ignore_ids'],
|
||||
];
|
||||
|
||||
return 'sms:getLookupChoiceList&'.http_build_query($query);
|
||||
},
|
||||
'multiple' => true,
|
||||
'required' => false,
|
||||
'sms_type' => 'template',
|
||||
'top_level' => 'translation',
|
||||
'ignore_ids' => [],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function getParent(): ?string
|
||||
{
|
||||
return EntityLookupType::class;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\Form\Type;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ButtonType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<array<mixed>>
|
||||
*/
|
||||
class SmsSendType extends AbstractType
|
||||
{
|
||||
public function __construct(
|
||||
protected RouterInterface $router,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface<array<mixed>|null> $builder
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->add(
|
||||
'sms',
|
||||
SmsListType::class,
|
||||
[
|
||||
'label' => 'mautic.sms.send.selectsmss',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'tooltip' => 'mautic.sms.choose.smss',
|
||||
'onchange' => 'Mautic.disabledSmsAction()',
|
||||
],
|
||||
'multiple' => false,
|
||||
'required' => true,
|
||||
'constraints' => [
|
||||
new NotBlank(
|
||||
['message' => 'mautic.sms.choosesms.notblank']
|
||||
),
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
if (!empty($options['update_select'])) {
|
||||
$windowUrl = $this->router->generate(
|
||||
'mautic_sms_action',
|
||||
[
|
||||
'objectAction' => 'new',
|
||||
'contentOnly' => 1,
|
||||
'updateSelect' => $options['update_select'],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'newSmsButton',
|
||||
ButtonType::class,
|
||||
[
|
||||
'attr' => [
|
||||
'class' => 'btn btn-primary btn-nospin',
|
||||
'onclick' => 'Mautic.loadNewWindow({
|
||||
"windowUrl": "'.$windowUrl.'"
|
||||
})',
|
||||
'icon' => 'ri-add-line',
|
||||
],
|
||||
'label' => 'mautic.sms.send.new.sms',
|
||||
]
|
||||
);
|
||||
|
||||
// create button edit sms
|
||||
$windowUrlEdit = $this->router->generate(
|
||||
'mautic_sms_action',
|
||||
[
|
||||
'objectAction' => 'edit',
|
||||
'objectId' => 'smsId',
|
||||
'contentOnly' => 1,
|
||||
'updateSelect' => $options['update_select'],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'editSmsButton',
|
||||
ButtonType::class,
|
||||
[
|
||||
'attr' => [
|
||||
'class' => 'btn btn-primary btn-nospin',
|
||||
'onclick' => 'Mautic.loadNewWindow(Mautic.standardSmsUrl({"windowUrl": "'.$windowUrlEdit.'"}))',
|
||||
'disabled' => !isset($options['data']['sms']),
|
||||
'icon' => 'ri-edit-line',
|
||||
],
|
||||
'label' => 'mautic.sms.send.edit.sms',
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefined(['update_select']);
|
||||
}
|
||||
|
||||
public function getBlockPrefix(): string
|
||||
{
|
||||
return 'smssend_list';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\Form\Type;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Mautic\CategoryBundle\Form\Type\CategoryListType;
|
||||
use Mautic\CoreBundle\Form\DataTransformer\IdToEntityModelTransformer;
|
||||
use Mautic\CoreBundle\Form\EventListener\CleanFormSubscriber;
|
||||
use Mautic\CoreBundle\Form\EventListener\FormExitSubscriber;
|
||||
use Mautic\CoreBundle\Form\Type\FormButtonsType;
|
||||
use Mautic\CoreBundle\Form\Type\PublishDownDateType;
|
||||
use Mautic\CoreBundle\Form\Type\PublishUpDateType;
|
||||
use Mautic\CoreBundle\Form\Type\YesNoButtonGroupType;
|
||||
use Mautic\LeadBundle\Form\Type\LeadListType;
|
||||
use Mautic\ProjectBundle\Form\Type\ProjectType;
|
||||
use Mautic\SmsBundle\Entity\Sms;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\LocaleType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<Sms>
|
||||
*/
|
||||
class SmsType extends AbstractType
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManager $em,
|
||||
) {
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->addEventSubscriber(new CleanFormSubscriber(['content' => 'html', 'customHtml' => 'html']));
|
||||
$builder->addEventSubscriber(new FormExitSubscriber('sms.sms', $options));
|
||||
|
||||
$builder->add(
|
||||
'name',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'mautic.sms.form.internal.name',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => ['class' => 'form-control'],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'description',
|
||||
TextareaType::class,
|
||||
[
|
||||
'label' => 'mautic.sms.form.internal.description',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => ['class' => 'form-control'],
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'message',
|
||||
TextareaType::class,
|
||||
[
|
||||
'label' => 'mautic.sms.form.message',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'data-token-activator' => '{',
|
||||
'data-token-visual' => 'false',
|
||||
'rows' => 6,
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add('isPublished', YesNoButtonGroupType::class, [
|
||||
'label' => 'mautic.core.form.available',
|
||||
]);
|
||||
|
||||
// add lead lists
|
||||
$transformer = new IdToEntityModelTransformer($this->em, \Mautic\LeadBundle\Entity\LeadList::class, 'id', true);
|
||||
$builder->add(
|
||||
$builder->create(
|
||||
'lists',
|
||||
LeadListType::class,
|
||||
[
|
||||
'label' => 'mautic.email.form.list',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
],
|
||||
'multiple' => true,
|
||||
'expanded' => false,
|
||||
'required' => true,
|
||||
]
|
||||
)
|
||||
->addModelTransformer($transformer)
|
||||
);
|
||||
|
||||
$builder->add('publishUp', PublishUpDateType::class);
|
||||
$builder->add('publishDown', PublishDownDateType::class);
|
||||
|
||||
// add category
|
||||
$builder->add(
|
||||
'category',
|
||||
CategoryListType::class,
|
||||
[
|
||||
'bundle' => 'sms',
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add('projects', ProjectType::class);
|
||||
|
||||
$builder->add(
|
||||
'language',
|
||||
LocaleType::class,
|
||||
[
|
||||
'label' => 'mautic.core.language',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
],
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
|
||||
$transformer = new IdToEntityModelTransformer($this->em, Sms::class);
|
||||
$builder->add(
|
||||
$builder->create(
|
||||
'translationParent',
|
||||
HiddenType::class
|
||||
)->addModelTransformer($transformer)
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'translationParentSelector', // This is a non-mapped field
|
||||
SmsListType::class, // A new form type to be created
|
||||
[
|
||||
'label' => 'mautic.core.form.translation_parent',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'tooltip' => 'mautic.core.form.translation_parent.help',
|
||||
],
|
||||
'required' => false,
|
||||
'multiple' => false,
|
||||
'placeholder' => 'mautic.core.form.translation_parent.empty',
|
||||
'top_level' => 'translation',
|
||||
'ignore_ids' => [(int) $options['data']->getId()],
|
||||
'mapped' => false,
|
||||
'data' => ($options['data']->getTranslationParent()) ? $options['data']->getTranslationParent()->getId() : null,
|
||||
]
|
||||
);
|
||||
|
||||
$builder->addEventListener(
|
||||
FormEvents::PRE_SUBMIT,
|
||||
function (FormEvent $event) {
|
||||
$data = $event->getData();
|
||||
if (isset($data['translationParentSelector'])) {
|
||||
$data['translationParent'] = $data['translationParentSelector'];
|
||||
}
|
||||
$event->setData($data);
|
||||
}
|
||||
);
|
||||
|
||||
$builder->add('smsType', HiddenType::class);
|
||||
$builder->add('buttons', FormButtonsType::class);
|
||||
|
||||
if (!empty($options['update_select'])) {
|
||||
$builder->add(
|
||||
'buttons',
|
||||
FormButtonsType::class,
|
||||
[
|
||||
'apply_text' => false,
|
||||
]
|
||||
);
|
||||
$builder->add(
|
||||
'updateSelect',
|
||||
HiddenType::class,
|
||||
[
|
||||
'data' => $options['update_select'],
|
||||
'mapped' => false,
|
||||
]
|
||||
);
|
||||
} else {
|
||||
$builder->add(
|
||||
'buttons',
|
||||
FormButtonsType::class
|
||||
);
|
||||
}
|
||||
|
||||
if (!empty($options['action'])) {
|
||||
$builder->setAction($options['action']);
|
||||
}
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults(
|
||||
[
|
||||
'data_class' => Sms::class,
|
||||
]
|
||||
);
|
||||
|
||||
$resolver->setDefined(['update_select']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\Helper;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\DBAL\ArrayParameterType;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Mautic\CoreBundle\Helper\PhoneNumberHelper;
|
||||
use Mautic\LeadBundle\Entity\LeadRepository;
|
||||
use Mautic\SmsBundle\Exception\NumberNotFoundException;
|
||||
|
||||
class ContactHelper
|
||||
{
|
||||
public function __construct(
|
||||
private LeadRepository $leadRepository,
|
||||
private Connection $connection,
|
||||
private PhoneNumberHelper $phoneNumberHelper,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $number
|
||||
*
|
||||
* @throws NumberNotFoundException
|
||||
*/
|
||||
public function findContactsByNumber($number): ArrayCollection
|
||||
{
|
||||
// Who knows what the number was originally formatted as so let's try a few
|
||||
$searchForNumbers = $this->phoneNumberHelper->getFormattedNumberList($number);
|
||||
|
||||
$qb = $this->connection->createQueryBuilder();
|
||||
|
||||
$foundContacts = $qb->select('l.id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'leads', 'l')
|
||||
->where(
|
||||
$qb->expr()->or(
|
||||
'l.mobile IN (:numbers)',
|
||||
'l.phone IN (:numbers)'
|
||||
)
|
||||
)
|
||||
->setParameter('numbers', $searchForNumbers, ArrayParameterType::STRING)
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
|
||||
$ids = array_column($foundContacts, 'id');
|
||||
if (0 === count($ids)) {
|
||||
throw new NumberNotFoundException($number);
|
||||
}
|
||||
|
||||
$collection = new ArrayCollection();
|
||||
/** @var Lead[] $contacts */
|
||||
$contacts = $this->leadRepository->getEntities(['ids' => $ids]);
|
||||
foreach ($contacts as $contact) {
|
||||
$collection->set($contact->getId(), $contact);
|
||||
}
|
||||
|
||||
return $collection;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\Helper;
|
||||
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Tracker\ContactTracker;
|
||||
use Mautic\SmsBundle\Callback\CallbackInterface;
|
||||
use Mautic\SmsBundle\Event\ReplyEvent;
|
||||
use Mautic\SmsBundle\Exception\NumberNotFoundException;
|
||||
use Mautic\SmsBundle\SmsEvents;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class ReplyHelper
|
||||
{
|
||||
public function __construct(
|
||||
private EventDispatcherInterface $eventDispatcher,
|
||||
private LoggerInterface $logger,
|
||||
private ContactTracker $contactTracker,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $pattern
|
||||
* @param string $replyBody
|
||||
*/
|
||||
public static function matches($pattern, $replyBody): bool
|
||||
{
|
||||
return fnmatch($pattern, $replyBody, FNM_CASEFOLD);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Response
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function handleRequest(CallbackInterface $handler, Request $request)
|
||||
{
|
||||
// Set the default response
|
||||
$response = new Response();
|
||||
|
||||
try {
|
||||
$message = $handler->getMessage($request);
|
||||
$contacts = $handler->getContacts($request);
|
||||
|
||||
$this->logger->debug(sprintf('SMS REPLY: Processing message "%s"', $message));
|
||||
$this->logger->debug(sprintf('SMS REPLY: Found IDs %s', implode(',', $contacts->getKeys())));
|
||||
|
||||
foreach ($contacts as $contact) {
|
||||
// Set the contact for campaign decisions
|
||||
$this->contactTracker->setSystemContact($contact);
|
||||
|
||||
$eventResponse = $this->dispatchReplyEvent($contact, $message);
|
||||
|
||||
if ($eventResponse instanceof Response) {
|
||||
// Last one wins
|
||||
$response = $eventResponse;
|
||||
}
|
||||
}
|
||||
} catch (BadRequestHttpException) {
|
||||
return new Response('invalid request', 400);
|
||||
} catch (NotFoundHttpException) {
|
||||
return new Response('', 404);
|
||||
} catch (NumberNotFoundException $exception) {
|
||||
$this->logger->debug(
|
||||
sprintf(
|
||||
'%s: %s was not found. The message sent was "%s"',
|
||||
$handler->getTransportName(),
|
||||
$exception->getNumber(),
|
||||
!empty($message) ? $message : 'unknown'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
private function dispatchReplyEvent(Lead $contact, string $message): ?Response
|
||||
{
|
||||
$replyEvent = new ReplyEvent($contact, trim($message));
|
||||
|
||||
$this->eventDispatcher->dispatch($replyEvent, SmsEvents::ON_REPLY);
|
||||
|
||||
return $replyEvent->getResponse();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\Helper;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use libphonenumber\PhoneNumberFormat;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\PhoneNumberHelper;
|
||||
use Mautic\LeadBundle\Entity\DoNotContact as DoNotContactEntity;
|
||||
use Mautic\LeadBundle\Entity\LeadRepository;
|
||||
use Mautic\LeadBundle\Model\DoNotContact;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Mautic\PluginBundle\Helper\IntegrationHelper;
|
||||
use Mautic\SmsBundle\Form\Type\ConfigType;
|
||||
use Mautic\SmsBundle\Model\SmsModel;
|
||||
|
||||
class SmsHelper
|
||||
{
|
||||
public function __construct(
|
||||
protected EntityManager $em,
|
||||
protected LeadModel $leadModel,
|
||||
protected PhoneNumberHelper $phoneNumberHelper,
|
||||
protected SmsModel $smsModel,
|
||||
protected IntegrationHelper $integrationHelper,
|
||||
private DoNotContact $doNotContact,
|
||||
private CoreParametersHelper $coreParametersHelper,
|
||||
) {
|
||||
}
|
||||
|
||||
public function unsubscribe($number)
|
||||
{
|
||||
$number = $this->phoneNumberHelper->format($number, PhoneNumberFormat::E164);
|
||||
|
||||
/** @var LeadRepository $repo */
|
||||
$repo = $this->em->getRepository(\Mautic\LeadBundle\Entity\Lead::class);
|
||||
|
||||
$args = [
|
||||
'filter' => [
|
||||
'force' => [
|
||||
[
|
||||
'column' => 'mobile',
|
||||
'expr' => 'eq',
|
||||
'value' => $number,
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$leads = $repo->getEntities($args);
|
||||
|
||||
if (!empty($leads)) {
|
||||
$lead = array_shift($leads);
|
||||
} else {
|
||||
// Try to find the lead based on the given phone number
|
||||
$args['filter']['force'][0]['column'] = 'phone';
|
||||
|
||||
$leads = $repo->getEntities($args);
|
||||
|
||||
if (!empty($leads)) {
|
||||
$lead = array_shift($leads);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->doNotContact->addDncForContact($lead->getId(), 'sms', DoNotContactEntity::UNSUBSCRIBED);
|
||||
}
|
||||
|
||||
public function getDisableTrackableUrls(): bool
|
||||
{
|
||||
return $this->coreParametersHelper->get(ConfigType::SMS_DISABLE_TRACKABLE_URLS);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\Integration\Twilio;
|
||||
|
||||
use Mautic\PluginBundle\Helper\IntegrationHelper;
|
||||
use Twilio\Exceptions\ConfigurationException;
|
||||
|
||||
class Configuration
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $messagingServiceSid;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $accountSid;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $authToken;
|
||||
|
||||
public function __construct(
|
||||
private IntegrationHelper $integrationHelper,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*
|
||||
* @throws ConfigurationException
|
||||
*/
|
||||
public function getMessagingServiceSid()
|
||||
{
|
||||
$this->setConfiguration();
|
||||
|
||||
return $this->messagingServiceSid;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*
|
||||
* @throws ConfigurationException
|
||||
*/
|
||||
public function getAccountSid()
|
||||
{
|
||||
$this->setConfiguration();
|
||||
|
||||
return $this->accountSid;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*
|
||||
* @throws ConfigurationException
|
||||
*/
|
||||
public function getAuthToken()
|
||||
{
|
||||
$this->setConfiguration();
|
||||
|
||||
return $this->authToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ConfigurationException
|
||||
*/
|
||||
private function setConfiguration(): void
|
||||
{
|
||||
if ($this->accountSid) {
|
||||
return;
|
||||
}
|
||||
|
||||
$integration = $this->integrationHelper->getIntegrationObject('Twilio');
|
||||
|
||||
if (!$integration || !$integration->getIntegrationSettings()->getIsPublished()) {
|
||||
throw new ConfigurationException();
|
||||
}
|
||||
|
||||
$this->messagingServiceSid = $integration->getIntegrationSettings()->getFeatureSettings()['messaging_service_sid'];
|
||||
if (empty($this->messagingServiceSid)) {
|
||||
throw new ConfigurationException();
|
||||
}
|
||||
|
||||
$keys = $integration->getDecryptedApiKeys();
|
||||
if (empty($keys['username']) || empty($keys['password'])) {
|
||||
throw new ConfigurationException();
|
||||
}
|
||||
|
||||
$this->accountSid = $keys['username'];
|
||||
$this->authToken = $keys['password'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\Integration\Twilio;
|
||||
|
||||
use Mautic\SmsBundle\Callback\CallbackInterface;
|
||||
use Mautic\SmsBundle\Exception\NumberNotFoundException;
|
||||
use Mautic\SmsBundle\Helper\ContactHelper;
|
||||
use Symfony\Component\HttpFoundation\InputBag;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Twilio\Exceptions\ConfigurationException;
|
||||
|
||||
class TwilioCallback implements CallbackInterface
|
||||
{
|
||||
public function __construct(
|
||||
private ContactHelper $contactHelper,
|
||||
private Configuration $configuration,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getTransportName(): string
|
||||
{
|
||||
return 'twilio';
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NumberNotFoundException
|
||||
*/
|
||||
public function getContacts(Request $request): \Doctrine\Common\Collections\ArrayCollection
|
||||
{
|
||||
$this->validateRequest($request->request);
|
||||
|
||||
$number = $request->get('From');
|
||||
|
||||
return $this->contactHelper->findContactsByNumber($number);
|
||||
}
|
||||
|
||||
public function getMessage(Request $request): string
|
||||
{
|
||||
$this->validateRequest($request->request);
|
||||
|
||||
return trim($request->get('Body'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InputBag<bool|float|int|string> $request
|
||||
*/
|
||||
private function validateRequest(InputBag $request): void
|
||||
{
|
||||
try {
|
||||
$accountSid = $this->configuration->getAccountSid();
|
||||
} catch (ConfigurationException) {
|
||||
// Not published or not configured
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
||||
// Validate this is a request from Twilio
|
||||
if ($accountSid !== $request->get('AccountSid')) {
|
||||
throw new BadRequestHttpException();
|
||||
}
|
||||
|
||||
// Who is the message from?
|
||||
$number = $request->get('From');
|
||||
if (empty($number)) {
|
||||
throw new BadRequestHttpException();
|
||||
}
|
||||
|
||||
// What did they say?
|
||||
$message = trim($request->get('Body'));
|
||||
if (empty($message)) {
|
||||
throw new BadRequestHttpException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\Integration\Twilio;
|
||||
|
||||
use libphonenumber\NumberParseException;
|
||||
use libphonenumber\PhoneNumberFormat;
|
||||
use libphonenumber\PhoneNumberUtil;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\SmsBundle\Sms\TransportInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Twilio\Exceptions\ConfigurationException;
|
||||
use Twilio\Exceptions\TwilioException;
|
||||
use Twilio\Rest\Client;
|
||||
|
||||
class TwilioTransport implements TransportInterface
|
||||
{
|
||||
private ?Client $client = null;
|
||||
|
||||
public function __construct(
|
||||
private Configuration $configuration,
|
||||
private LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $content
|
||||
*
|
||||
* @return bool|string
|
||||
*/
|
||||
public function sendSms(Lead $lead, $content)
|
||||
{
|
||||
$number = $lead->getLeadPhoneNumber();
|
||||
|
||||
if (null === $number) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$messagingServiceSid = $this->configuration->getMessagingServiceSid();
|
||||
$this->configureClient();
|
||||
|
||||
$this->client->messages->create(
|
||||
$this->sanitizeNumber($number),
|
||||
$this->createPayload($messagingServiceSid, $content)
|
||||
);
|
||||
|
||||
return true;
|
||||
} catch (NumberParseException $numberParseException) {
|
||||
$this->logger->warning(
|
||||
$numberParseException->getMessage(),
|
||||
['exception' => $numberParseException]
|
||||
);
|
||||
|
||||
return $numberParseException->getMessage();
|
||||
} catch (ConfigurationException $configurationException) {
|
||||
$message = $configurationException->getMessage() ?: 'mautic.sms.transport.twilio.not_configured';
|
||||
$this->logger->warning(
|
||||
$message,
|
||||
['exception' => $configurationException]
|
||||
);
|
||||
|
||||
return $message;
|
||||
} catch (TwilioException $twilioException) {
|
||||
$this->logger->warning(
|
||||
$twilioException->getMessage(),
|
||||
['exception' => $twilioException]
|
||||
);
|
||||
|
||||
return $twilioException->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $number
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @throws NumberParseException
|
||||
*/
|
||||
private function sanitizeNumber($number)
|
||||
{
|
||||
$util = PhoneNumberUtil::getInstance();
|
||||
$parsed = $util->parse($number, 'US');
|
||||
|
||||
return $util->format($parsed, PhoneNumberFormat::E164);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
private function createPayload(string $messagingServiceSid, string $content): array
|
||||
{
|
||||
return [
|
||||
'messagingServiceSid' => $messagingServiceSid,
|
||||
'body' => $content,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ConfigurationException
|
||||
*/
|
||||
private function configureClient(): void
|
||||
{
|
||||
if ($this->client) {
|
||||
// Already configured
|
||||
return;
|
||||
}
|
||||
|
||||
$this->client = new Client(
|
||||
$this->configuration->getAccountSid(),
|
||||
$this->configuration->getAuthToken()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\Integration;
|
||||
|
||||
use Mautic\PluginBundle\Integration\AbstractIntegration;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\NumberType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
|
||||
class TwilioIntegration extends AbstractIntegration
|
||||
{
|
||||
protected bool $coreIntegration = true;
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'Twilio';
|
||||
}
|
||||
|
||||
public function getIcon(): string
|
||||
{
|
||||
return 'app/bundles/SmsBundle/Assets/img/Twilio.png';
|
||||
}
|
||||
|
||||
public function getSecretKeys(): array
|
||||
{
|
||||
return ['password'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getRequiredKeyFields(): array
|
||||
{
|
||||
return [
|
||||
'username' => 'mautic.sms.config.form.sms.username',
|
||||
'password' => 'mautic.sms.config.form.sms.password',
|
||||
];
|
||||
}
|
||||
|
||||
public function getAuthenticationType(): string
|
||||
{
|
||||
return 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Mautic\PluginBundle\Integration\Form|FormBuilder $builder
|
||||
* @param array $data
|
||||
* @param string $formArea
|
||||
*/
|
||||
public function appendToForm(&$builder, $data, $formArea): void
|
||||
{
|
||||
if ('features' == $formArea) {
|
||||
$builder->add(
|
||||
'messaging_service_sid',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'mautic.sms.config.form.sms.messaging_service_sid',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'required' => false,
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'tooltip' => 'mautic.sms.config.form.sms.messaging_service_sid.tooltip',
|
||||
],
|
||||
]
|
||||
);
|
||||
$builder->add('frequency_number', NumberType::class,
|
||||
[
|
||||
'scale' => 0,
|
||||
'label' => 'mautic.sms.list.frequency.number',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'required' => false,
|
||||
'attr' => [
|
||||
'class' => 'form-control frequency',
|
||||
],
|
||||
]);
|
||||
$builder->add('frequency_time', ChoiceType::class,
|
||||
[
|
||||
'choices' => [
|
||||
'day' => 'DAY',
|
||||
'week' => 'WEEK',
|
||||
'month' => 'MONTH',
|
||||
],
|
||||
'label' => 'mautic.lead.list.frequency.times',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'required' => false,
|
||||
'multiple' => false,
|
||||
'attr' => [
|
||||
'class' => 'form-control frequency',
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle;
|
||||
|
||||
use Mautic\PluginBundle\Bundle\PluginBundleBase;
|
||||
use Mautic\SmsBundle\DependencyInjection\Compiler\SmsTransportPass;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
|
||||
class MauticSmsBundle extends PluginBundleBase
|
||||
{
|
||||
public function build(ContainerBuilder $container): void
|
||||
{
|
||||
$container->addCompilerPass(new SmsTransportPass(), \Symfony\Component\DependencyInjection\Compiler\PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,554 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\Model;
|
||||
|
||||
use Doctrine\DBAL\Query\QueryBuilder;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Mautic\ChannelBundle\Entity\MessageQueue;
|
||||
use Mautic\ChannelBundle\Model\MessageQueueModel;
|
||||
use Mautic\CoreBundle\Event\TokenReplacementEvent;
|
||||
use Mautic\CoreBundle\Helper\CacheStorageHelper;
|
||||
use Mautic\CoreBundle\Helper\Chart\ChartQuery;
|
||||
use Mautic\CoreBundle\Helper\Chart\LineChart;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Model\AjaxLookupModelInterface;
|
||||
use Mautic\CoreBundle\Model\FormModel;
|
||||
use Mautic\CoreBundle\Model\GlobalSearchInterface;
|
||||
use Mautic\CoreBundle\Model\TranslationModelTrait;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\LeadBundle\Entity\DoNotContactRepository;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Mautic\PageBundle\Model\TrackableModel;
|
||||
use Mautic\SmsBundle\Entity\Sms;
|
||||
use Mautic\SmsBundle\Entity\Stat;
|
||||
use Mautic\SmsBundle\Event\SmsEvent;
|
||||
use Mautic\SmsBundle\Event\SmsSendEvent;
|
||||
use Mautic\SmsBundle\Form\Type\SmsType;
|
||||
use Mautic\SmsBundle\Sms\TransportChain;
|
||||
use Mautic\SmsBundle\SmsEvents;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* @extends FormModel<Sms>
|
||||
*
|
||||
* @implements AjaxLookupModelInterface<Sms>
|
||||
*/
|
||||
class SmsModel extends FormModel implements AjaxLookupModelInterface, GlobalSearchInterface
|
||||
{
|
||||
use TranslationModelTrait;
|
||||
|
||||
public function __construct(
|
||||
protected TrackableModel $pageTrackableModel,
|
||||
protected LeadModel $leadModel,
|
||||
protected MessageQueueModel $messageQueueModel,
|
||||
protected TransportChain $transport,
|
||||
private CacheStorageHelper $cacheStorageHelper,
|
||||
EntityManagerInterface $em,
|
||||
CorePermissions $security,
|
||||
EventDispatcherInterface $dispatcher,
|
||||
UrlGeneratorInterface $router,
|
||||
Translator $translator,
|
||||
UserHelper $userHelper,
|
||||
LoggerInterface $mauticLogger,
|
||||
CoreParametersHelper $coreParametersHelper,
|
||||
) {
|
||||
parent::__construct($em, $security, $dispatcher, $router, $translator, $userHelper, $mauticLogger, $coreParametersHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Mautic\SmsBundle\Entity\SmsRepository
|
||||
*/
|
||||
public function getRepository()
|
||||
{
|
||||
return $this->em->getRepository(Sms::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Mautic\SmsBundle\Entity\StatRepository
|
||||
*/
|
||||
public function getStatRepository()
|
||||
{
|
||||
return $this->em->getRepository(Stat::class);
|
||||
}
|
||||
|
||||
public function getPermissionBase(): string
|
||||
{
|
||||
return 'sms:smses';
|
||||
}
|
||||
|
||||
public function saveEntity($entity, $unlock = true): void
|
||||
{
|
||||
parent::saveEntity($entity, $unlock);
|
||||
|
||||
$this->postTranslationEntitySave($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save an array of entities.
|
||||
*
|
||||
* @param iterable<Sms> $entities
|
||||
*/
|
||||
public function saveEntities($entities, $unlock = true): void
|
||||
{
|
||||
// iterate over the results so the events are dispatched on each delete
|
||||
$batchSize = 20;
|
||||
$i = 0;
|
||||
foreach ($entities as $entity) {
|
||||
$isNew = ($entity->getId()) ? false : true;
|
||||
|
||||
// set some defaults
|
||||
$this->setTimestamps($entity, $isNew, $unlock);
|
||||
|
||||
if ($dispatchEvent = $entity instanceof Sms) {
|
||||
$event = $this->dispatchEvent('pre_save', $entity, $isNew);
|
||||
}
|
||||
|
||||
$this->getRepository()->saveEntity($entity, false);
|
||||
|
||||
if ($dispatchEvent) {
|
||||
$this->dispatchEvent('post_save', $entity, $isNew, $event);
|
||||
}
|
||||
|
||||
if (0 === ++$i % $batchSize) {
|
||||
$this->em->flush();
|
||||
}
|
||||
}
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $options
|
||||
*
|
||||
* @throws MethodNotAllowedHttpException
|
||||
*/
|
||||
public function createForm($entity, FormFactoryInterface $formFactory, $action = null, $options = []): FormInterface
|
||||
{
|
||||
if (!$entity instanceof Sms) {
|
||||
throw new MethodNotAllowedHttpException(['Sms']);
|
||||
}
|
||||
if (!empty($action)) {
|
||||
$options['action'] = $action;
|
||||
}
|
||||
|
||||
return $formFactory->create(SmsType::class, $entity, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific entity or generate a new one if id is empty.
|
||||
*/
|
||||
public function getEntity($id = null): ?Sms
|
||||
{
|
||||
if (null === $id) {
|
||||
$entity = new Sms();
|
||||
} else {
|
||||
$entity = parent::getEntity($id);
|
||||
}
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of entities.
|
||||
*
|
||||
* @param array $args [start, limit, filter, orderBy, orderByDir]
|
||||
*
|
||||
* @return \Doctrine\ORM\Tools\Pagination\Paginator|array
|
||||
*/
|
||||
public function getEntities(array $args = [])
|
||||
{
|
||||
$entities = parent::getEntities($args);
|
||||
|
||||
foreach ($entities as $entity) {
|
||||
$pending = $this->cacheStorageHelper->get(sprintf('%s|%s|%s', 'sms', $entity->getId(), 'pending'));
|
||||
|
||||
if (false !== $pending) {
|
||||
$entity->setPendingCount($pending);
|
||||
}
|
||||
}
|
||||
|
||||
return $entities;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $options
|
||||
* @param array<int, Lead> $leads
|
||||
*/
|
||||
public function sendSms(Sms $sms, $sendTo, $options = [], array &$leads = []): array
|
||||
{
|
||||
$channel = $options['channel'] ?? null;
|
||||
$listId = $options['listId'] ?? null;
|
||||
|
||||
if ($sendTo instanceof Lead) {
|
||||
$sendTo = [$sendTo];
|
||||
} elseif (!is_array($sendTo)) {
|
||||
$sendTo = [$sendTo];
|
||||
}
|
||||
|
||||
$sentCount = 0;
|
||||
$failedCount = 0;
|
||||
$results = [];
|
||||
$contacts = [];
|
||||
$fetchContacts = [];
|
||||
foreach ($sendTo as $lead) {
|
||||
if (!$lead instanceof Lead) {
|
||||
$fetchContacts[] = $lead;
|
||||
} else {
|
||||
$contacts[$lead->getId()] = $lead;
|
||||
$leads[$lead->getId()] = $lead;
|
||||
}
|
||||
}
|
||||
|
||||
if ($fetchContacts) {
|
||||
/** @var Lead[] $foundContacts */
|
||||
$foundContacts = $this->leadModel->getEntities(
|
||||
[
|
||||
'ids' => $fetchContacts,
|
||||
]
|
||||
);
|
||||
|
||||
foreach ($foundContacts as $contact) {
|
||||
$contacts[$contact->getId()] = $contact;
|
||||
$leads[$contact->getId()] = $contact;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$sms->isPublished()) {
|
||||
foreach ($contacts as $leadId => $lead) {
|
||||
$results[$leadId] = [
|
||||
'sent' => false,
|
||||
'status' => 'mautic.sms.campaign.failed.unpublished',
|
||||
];
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
$contactIds = array_keys($contacts);
|
||||
|
||||
/** @var DoNotContactRepository $dncRepo */
|
||||
$dncRepo = $this->em->getRepository(\Mautic\LeadBundle\Entity\DoNotContact::class);
|
||||
$dnc = $dncRepo->getChannelList('sms', $contactIds);
|
||||
|
||||
foreach ($dnc as $removeMeId => $removeMeReason) {
|
||||
$results[$removeMeId] = [
|
||||
'sent' => false,
|
||||
'status' => 'mautic.sms.campaign.failed.not_contactable',
|
||||
];
|
||||
|
||||
unset($contacts[$removeMeId], $contactIds[$removeMeId]);
|
||||
}
|
||||
|
||||
if (!empty($contacts)) {
|
||||
$messageQueue = $options['resend_message_queue'] ?? null;
|
||||
$campaignEventId = (is_array($channel) && 'campaign.event' === $channel[0] && !empty($channel[1])) ? $channel[1] : null;
|
||||
|
||||
$queued = $this->messageQueueModel->processFrequencyRules(
|
||||
$contacts,
|
||||
'sms',
|
||||
$sms->getId(),
|
||||
$campaignEventId,
|
||||
3,
|
||||
MessageQueue::PRIORITY_NORMAL,
|
||||
$messageQueue,
|
||||
'sms_message_stats'
|
||||
);
|
||||
|
||||
foreach ($queued as $queue) {
|
||||
$results[$queue] = [
|
||||
'sent' => false,
|
||||
'status' => 'mautic.sms.timeline.status.scheduled',
|
||||
];
|
||||
|
||||
unset($contacts[$queue]);
|
||||
}
|
||||
|
||||
$stats = [];
|
||||
// @todo we should allow batch sending based on transport, MessageBird does support 20 SMS at once
|
||||
// the transport chain is already prepared for it
|
||||
if (count($contacts)) {
|
||||
/** @var Lead $lead */
|
||||
foreach ($contacts as $lead) {
|
||||
$leadId = $lead->getId();
|
||||
$stat = $this->createStatEntry($sms, $lead, $channel, false, $listId);
|
||||
|
||||
$leadPhoneNumber = $lead->getLeadPhoneNumber();
|
||||
|
||||
if (empty($leadPhoneNumber)) {
|
||||
$results[$leadId] = [
|
||||
'sent' => false,
|
||||
'status' => 'mautic.sms.campaign.failed.missing_number',
|
||||
];
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
list($ignore, $sms) = $this->getTranslatedEntity($sms, $lead);
|
||||
\assert($sms instanceof Sms);
|
||||
|
||||
$smsEvent = new SmsSendEvent($sms->getMessage(), $lead);
|
||||
$smsEvent->setSmsId($sms->getId());
|
||||
$this->dispatcher->dispatch($smsEvent, SmsEvents::SMS_ON_SEND);
|
||||
|
||||
$tokenEvent = $this->dispatcher->dispatch(
|
||||
new TokenReplacementEvent(
|
||||
$smsEvent->getContent(),
|
||||
$lead,
|
||||
[
|
||||
'channel' => [
|
||||
'sms', // Keep BC pre 2.14.1
|
||||
$sms->getId(), // Keep BC pre 2.14.1
|
||||
'sms' => $sms->getId(),
|
||||
],
|
||||
'stat' => $stat->getTrackingHash(),
|
||||
]
|
||||
),
|
||||
SmsEvents::TOKEN_REPLACEMENT
|
||||
);
|
||||
|
||||
$sendResult = [
|
||||
'sent' => false,
|
||||
'type' => 'mautic.sms.sms',
|
||||
'status' => 'mautic.sms.timeline.status.delivered',
|
||||
'id' => $sms->getId(),
|
||||
'name' => $sms->getName(),
|
||||
'content' => $tokenEvent->getContent(),
|
||||
];
|
||||
|
||||
$metadata = $this->transport->sendSms($lead, $tokenEvent->getContent(), $stat);
|
||||
if (true !== $metadata) {
|
||||
$sendResult['status'] = $metadata;
|
||||
$stat->setIsFailed(true);
|
||||
if (is_string($metadata)) {
|
||||
$stat->addDetail('failed', $metadata);
|
||||
}
|
||||
++$failedCount;
|
||||
} else {
|
||||
$sendResult['sent'] = true;
|
||||
++$sentCount;
|
||||
}
|
||||
|
||||
$stats[] = $stat;
|
||||
unset($stat);
|
||||
$results[$leadId] = $sendResult;
|
||||
|
||||
unset($smsEvent, $tokenEvent, $sendResult, $metadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($sentCount || $failedCount) {
|
||||
$this->getRepository()->upCount($sms->getId(), 'sent', $sentCount);
|
||||
$this->getStatRepository()->saveEntities($stats);
|
||||
|
||||
foreach ($stats as $stat) {
|
||||
if (!$stat->isFailed()) {
|
||||
$results[$stat->getLead()->getId()]['statId'] = $stat->getId();
|
||||
}
|
||||
|
||||
$this->getRepository()->detachEntity($stat);
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $persist
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function createStatEntry(Sms $sms, Lead $lead, $source = null, $persist = true, $listId = null): Stat
|
||||
{
|
||||
$stat = new Stat();
|
||||
$stat->setDateSent(new \DateTime());
|
||||
$stat->setLead($lead);
|
||||
$stat->setSms($sms);
|
||||
if (null !== $listId) {
|
||||
$stat->setList($this->leadModel->getLeadListRepository()->getEntity($listId));
|
||||
}
|
||||
if (is_array($source)) {
|
||||
$stat->setSourceId($source[1]);
|
||||
$source = $source[0];
|
||||
}
|
||||
$stat->setSource($source);
|
||||
$stat->setTrackingHash(str_replace('.', '', uniqid('', true)));
|
||||
|
||||
if ($persist) {
|
||||
$this->getStatRepository()->saveEntity($stat);
|
||||
}
|
||||
|
||||
return $stat;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws MethodNotAllowedHttpException
|
||||
*/
|
||||
protected function dispatchEvent($action, &$entity, $isNew = false, ?Event $event = null): ?Event
|
||||
{
|
||||
if (!$entity instanceof Sms) {
|
||||
throw new MethodNotAllowedHttpException(['Sms']);
|
||||
}
|
||||
|
||||
switch ($action) {
|
||||
case 'pre_save':
|
||||
$name = SmsEvents::SMS_PRE_SAVE;
|
||||
break;
|
||||
case 'post_save':
|
||||
$name = SmsEvents::SMS_POST_SAVE;
|
||||
break;
|
||||
case 'pre_delete':
|
||||
$name = SmsEvents::SMS_PRE_DELETE;
|
||||
break;
|
||||
case 'post_delete':
|
||||
$name = SmsEvents::SMS_POST_DELETE;
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->dispatcher->hasListeners($name)) {
|
||||
if (empty($event)) {
|
||||
$event = new SmsEvent($entity, $isNew);
|
||||
$event->setEntityManager($this->em);
|
||||
}
|
||||
|
||||
$this->dispatcher->dispatch($event, $name);
|
||||
|
||||
return $event;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins the page table and limits created_by to currently logged in user.
|
||||
*/
|
||||
public function limitQueryToCreator(QueryBuilder &$q): void
|
||||
{
|
||||
$q->join('t', MAUTIC_TABLE_PREFIX.'sms_messages', 's', 's.id = t.sms_id')
|
||||
->andWhere('s.created_by = :userId')
|
||||
->setParameter('userId', $this->userHelper->getUser()->getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get line chart data of hits.
|
||||
*
|
||||
* @param char $unit {@link php.net/manual/en/function.date.php#refsect1-function.date-parameters}
|
||||
* @param string $dateFormat
|
||||
* @param array $filter
|
||||
* @param bool $canViewOthers
|
||||
*/
|
||||
public function getHitsLineChartData($unit, \DateTime $dateFrom, \DateTime $dateTo, $dateFormat = null, $filter = [], $canViewOthers = true): array
|
||||
{
|
||||
$flag = null;
|
||||
|
||||
if (isset($filter['flag'])) {
|
||||
$flag = $filter['flag'];
|
||||
unset($filter['flag']);
|
||||
}
|
||||
|
||||
$chart = new LineChart($unit, $dateFrom, $dateTo, $dateFormat);
|
||||
$query = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo);
|
||||
|
||||
if (!$flag || 'total_and_unique' === $flag) {
|
||||
$filter['is_failed'] = 0;
|
||||
$q = $query->prepareTimeDataQuery('sms_message_stats', 'date_sent', $filter);
|
||||
|
||||
if (!$canViewOthers) {
|
||||
$this->limitQueryToCreator($q);
|
||||
}
|
||||
|
||||
$data = $query->loadAndBuildTimeData($q);
|
||||
$chart->setDataset($this->translator->trans('mautic.sms.show.total.sent'), $data);
|
||||
}
|
||||
|
||||
if (!$flag || 'failed' === $flag) {
|
||||
$filter['is_failed'] = 1;
|
||||
$q = $query->prepareTimeDataQuery('sms_message_stats', 'date_sent', $filter);
|
||||
if (!$canViewOthers) {
|
||||
$this->limitQueryToCreator($q);
|
||||
}
|
||||
|
||||
$data = $query->loadAndBuildTimeData($q);
|
||||
$chart->setDataset($this->translator->trans('mautic.sms.show.failed'), $data);
|
||||
}
|
||||
|
||||
return $chart->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Stat
|
||||
*/
|
||||
public function getSmsStatus($idHash)
|
||||
{
|
||||
return $this->getStatRepository()->getSmsStatus($idHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for an sms stat by sms and lead IDs.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getSmsStatByLeadId($smsId, $leadId)
|
||||
{
|
||||
return $this->getStatRepository()->findBy(
|
||||
[
|
||||
'sms' => (int) $smsId,
|
||||
'lead' => (int) $leadId,
|
||||
],
|
||||
['dateSent' => 'DESC']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of tracked links.
|
||||
*/
|
||||
public function getSmsClickStats($smsId): array
|
||||
{
|
||||
return $this->pageTrackableModel->getTrackableList('sms', $smsId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $filter
|
||||
* @param int $limit
|
||||
* @param int $start
|
||||
* @param array $options
|
||||
*/
|
||||
public function getLookupResults($type, $filter = '', $limit = 10, $start = 0, $options = []): array
|
||||
{
|
||||
$results = [];
|
||||
switch ($type) {
|
||||
case 'sms':
|
||||
case SmsType::class:
|
||||
$entities = $this->getRepository()->getSmsList(
|
||||
$filter,
|
||||
$limit,
|
||||
$start,
|
||||
$this->security->isGranted($this->getPermissionBase().':viewother'),
|
||||
$options['sms_type'] ?? null,
|
||||
$options['top_level'] ?? '',
|
||||
$options['ignore_ids'] ?? [],
|
||||
);
|
||||
|
||||
foreach ($entities as $entity) {
|
||||
$results[$entity['language']][$entity['id']] = $entity['name'];
|
||||
}
|
||||
|
||||
// sort by language
|
||||
ksort($results);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{% block _config_smsconfig_widget %}
|
||||
<h4 class="fw-sb mt-48 mb-xs">{{ 'mautic.config.tab.smsconfig'|trans }}</h4>
|
||||
<div class="text-muted small pb-md">{{ 'mautic.core.config.header.smsconfig.description'|trans }}</div>
|
||||
<div class="row">
|
||||
<div class="panel panel-default mb-md">
|
||||
<div class="panel-body">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
{% if form.sms_transport.vars.choices|length %}
|
||||
{{ form_row(form.sms_transport) }}
|
||||
{% else %}
|
||||
{{ 'mautic.sms.config.smsconfig'|trans }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<hr>
|
||||
<div class="col-xs-12">
|
||||
{{ form_row(form.sms_disable_trackable_urls) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,12 @@
|
||||
{% block _smssend_list_row %}
|
||||
<div class="row">
|
||||
<div class="col-xs-8">
|
||||
{{ form_row(form.sms) }}
|
||||
</div>
|
||||
<div class="col-xs-4 mt-lg">
|
||||
<div class="mt-3">
|
||||
{{ form_row(form.newSmsButton) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,230 @@
|
||||
{% if not isEmbedded %}
|
||||
{% extends '@MauticCore/Default/content.html.twig' %}
|
||||
{% endif %}
|
||||
|
||||
{% block mauticContent %}sms{% endblock %}
|
||||
{% block headerTitle %}{{sms.getName()}}{% endblock %}
|
||||
|
||||
{% block preHeader %}
|
||||
{{- include('@MauticCore/Helper/page_actions.html.twig',
|
||||
{
|
||||
'item' : sms,
|
||||
'templateButtons' : {
|
||||
'close' : securityHasEntityAccess(
|
||||
permissions['sms:smses:viewown'],
|
||||
permissions['sms:smses:viewother'],
|
||||
sms.getCreatedBy()
|
||||
),
|
||||
},
|
||||
'routeBase' : 'sms',
|
||||
'targetLabel': 'mautic.sms.smses'|trans
|
||||
}
|
||||
) -}}
|
||||
{{ include('@MauticCore/Modules/category--inline.html.twig', {'category': sms.category}) }}
|
||||
{{ include('@MauticProject/Modules/projects.html.twig', {'item': sms}) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block actions %}
|
||||
{% set customButtons = [] %}
|
||||
{{- include('@MauticCore/Helper/page_actions.html.twig',
|
||||
{
|
||||
'item' : sms,
|
||||
'customButtons' : customButtons is defined ? customButtons : [],
|
||||
'templateButtons' : {
|
||||
'edit' : securityHasEntityAccess(
|
||||
permissions['sms:smses:editown'],
|
||||
permissions['sms:smses:editother'],
|
||||
sms.getCreatedBy()
|
||||
),
|
||||
'clone' : permissions['sms:smses:create'],
|
||||
'delete' : securityHasEntityAccess(
|
||||
permissions['sms:smses:deleteown'],
|
||||
permissions['sms:smses:deleteother'],
|
||||
sms.getCreatedBy()
|
||||
),
|
||||
},
|
||||
'routeBase' : 'sms',
|
||||
}
|
||||
) -}}
|
||||
{% endblock %}
|
||||
|
||||
{% block publishStatus %}
|
||||
{{- include('@MauticCore/Helper/publishstatus_badge.html.twig', {
|
||||
'entity': sms,
|
||||
'status': sms.getPublishUp() is not null ? 'sending' : 'available'
|
||||
}) -}}
|
||||
<div class="label__divider"></div>
|
||||
{% set tags = [
|
||||
{
|
||||
color: 'warm-gray',
|
||||
label: smsType == 'list'
|
||||
? 'mautic.sms.type.list.header'
|
||||
: (smsType == 'template' ? 'mautic.sms.type.template.header' : type)
|
||||
}
|
||||
] %}
|
||||
|
||||
{# Translation tags #}
|
||||
{% set tags = tags
|
||||
|merge(sms.isTranslation and not sms.isTranslation(true)
|
||||
? [{ color: 'warm-gray', label: 'mautic.core.icon_tooltip.translation' }]
|
||||
: [])
|
||||
|merge(sms.isTranslation(true)
|
||||
? [{ color: 'warm-gray', label: 'mautic.core.translation_of'|trans({'%parent%' : translations.parent.name}), icon: 'ri-translate' }]
|
||||
: [])
|
||||
%}
|
||||
|
||||
{% include '@MauticCore/Helper/_tag.html.twig' with { tags: tags } %}
|
||||
{% endblock %}
|
||||
|
||||
{% set smsType = sms.getSmsType() %}
|
||||
{% if smsType is empty %}
|
||||
{% set smsType = 'template' %}
|
||||
{% endif %}
|
||||
|
||||
{% set translationContent = include('@MauticCore/Translation/index.html.twig',
|
||||
{
|
||||
'activeEntity' : sms,
|
||||
'translations' : translations,
|
||||
'model' : 'sms',
|
||||
'actionRoute' : 'mautic_sms_action',
|
||||
}
|
||||
) %}
|
||||
{% set showTranslations = translationContent|trim is not empty %}
|
||||
|
||||
{% block content %}
|
||||
<!-- start: box layout -->
|
||||
<div class="box-layout">
|
||||
<!-- left section -->
|
||||
<div class="col-md-7 col-lg-9 height-auto">
|
||||
<div>
|
||||
<!-- page detail header -->
|
||||
<!-- sms detail collapseable toggler -->
|
||||
<div class="collapse pr-md pl-md" id="sms-details">
|
||||
<div class="pr-md pl-md pb-md">
|
||||
<div class="panel shd-none mb-0">
|
||||
<table class="table table-hover mb-0">
|
||||
<tbody>
|
||||
{{- include('@MauticCore/Helper/details.html.twig', {'entity' : sms}) -}}
|
||||
{% if sms.getSmsType() == 'list' %}
|
||||
<tr>
|
||||
<td width="20%"><span class="fw-b textTitle">{{ 'mautic.lead.lead.lists'|trans }}</span></td>
|
||||
<td>
|
||||
{% set segments = sms.getLists %}
|
||||
{% set segmentNames = segments.toArray|map(segment => segment.getName) %}
|
||||
{{ segmentNames|join(', ') }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--/ sms detail collapseable toggler -->
|
||||
<div>
|
||||
<div class="hr-expand nm">
|
||||
<span data-toggle="tooltip" title="Detail">
|
||||
<a href="javascript:void(0)" class="arrow text-secondary collapsed" data-toggle="collapse" data-target="#sms-details">
|
||||
<span class="caret"></span> {{ 'mautic.core.details'|trans }}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<!-- some stats -->
|
||||
<div class="pa-md">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
{% if security.isGranted('lead:leads:viewown') %}
|
||||
{{ include('@MauticCore/Modules/stat--icon.html.twig', {'stats': [
|
||||
{
|
||||
'title': 'mautic.lead.lead.contacts.sms_sent',
|
||||
'value': sms.sentCount,
|
||||
'link': path('mautic_contact_index', {
|
||||
'search': ('mautic.lead.lead.searchcommand.sms_sent'|trans) ~ ':' ~ sms.id
|
||||
}),
|
||||
'icon': 'ri-message-2-line'
|
||||
}
|
||||
]}) }}
|
||||
{% endif %}
|
||||
<div class="panel">
|
||||
<div class="panel-body box-layout">
|
||||
<div class="col-md-3 va-m">
|
||||
<h5 class="text-white dark-md fw-sb mb-xs">
|
||||
<span class="ri-line-chart-fill"></span>
|
||||
{{ 'mautic.core.stats'|trans }}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="col-md-9 va-m">
|
||||
{{- include('@MauticCore/Helper/graph_dateselect.html.twig', {'dateRangeForm' : dateRangeForm, 'class' : 'pull-right'}) -}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex fd-column pt-0 pl-15 pb-15 pr-15 min-h-256">
|
||||
{{- include('@MauticCore/Helper/chart.html.twig', {'chartData' : entityViews, 'chartType' : 'line', 'chartHeight' : 300}) -}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--/ stats -->
|
||||
|
||||
{{ customContent('details.stats.graph.below', _context) }}
|
||||
|
||||
<!-- tabs controls -->
|
||||
<ul class="nav nav-tabs nav-tabs-contained">
|
||||
<li class="active">
|
||||
<a href="#clicks-container" role="tab" data-toggle="tab">
|
||||
{{ 'mautic.trackable.click_counts'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="">
|
||||
<a href="#contacts-container" role="tab" data-toggle="tab">
|
||||
{{ 'mautic.lead.leads'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{% if showTranslations %}
|
||||
<li>
|
||||
<a href="#translation-container" role="tab" data-toggle="tab">
|
||||
{{ 'mautic.core.translations'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<!--/ tabs controls -->
|
||||
</div>
|
||||
|
||||
<!-- start: tab-content -->
|
||||
<div class="tab-content pa-md">
|
||||
<div class="tab-pane active bdr-w-0" id="clicks-container">
|
||||
{{- include('@MauticPage/Trackable/click_counts.html.twig', {
|
||||
'trackables' : trackables,
|
||||
'entity' : sms,
|
||||
'channel' : 'sms'
|
||||
}) -}}
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade in bdr-w-0 page-list" id="contacts-container">
|
||||
{{ contacts|raw }}
|
||||
</div>
|
||||
|
||||
{% if showTranslations %}
|
||||
<div class="tab-pane bdr-w-0" id="translation-container">
|
||||
{{ translationContent|raw }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<!-- end: tab-content -->
|
||||
</div>
|
||||
<!--/ left section -->
|
||||
|
||||
<!-- right section -->
|
||||
<div class="col-md-5 col-lg-3 height-auto">
|
||||
<!-- activity feed -->
|
||||
<div class="mb-48" style="zoom: 0.8;">{{ include('@MauticSms/Sms/preview.html.twig') }}</div>
|
||||
{{- include('@MauticCore/Helper/recentactivity.html.twig', {'logs' : logs}) -}}
|
||||
</div>
|
||||
<!--/ right section -->
|
||||
<input name="entityId" id="entityId" type="hidden" value="{{ sms.getId() }}" />
|
||||
</div>
|
||||
<!--/ end: box layout -->
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,83 @@
|
||||
{% extends '@MauticCore/FormTheme/form_simple.html.twig' %}
|
||||
{% form_theme form with [
|
||||
'@MauticSms/FormTheme/SmsSendList/smssend_list_row.html.twig'
|
||||
] %}
|
||||
|
||||
{% block mauticContent %}sms{% endblock %}
|
||||
{# @var sms \Mautic\SmsBundle\Entity\Sms #}
|
||||
{% set type = sms.getSmsType() %}
|
||||
{% set isExisting = sms.getId() %}
|
||||
{% set translationBase = 'mautic.sms' %}
|
||||
{% set mauticContent = 'sms' %}
|
||||
|
||||
{% block primaryFormContent %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
{{ form_row(form.name) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="characters-count">
|
||||
<label class="control-label" for="" data-toggle="tooltip" data-container="body" data-placement="top" title="" data-original-title="{{ 'mautic.sms.form.nbcharacter.infobox'|trans }}">
|
||||
{{ 'mautic.sms.form.nbcharacter.counter'|trans }}
|
||||
<span size="sm" class="label label-gray" id="sms_nb_char">0</span>
|
||||
</label>
|
||||
</div>
|
||||
{{ form_row(form.message) }}
|
||||
<i class="text-secondary">{{ 'mautic.sms.token.dropdown.hint'|trans|purify }}</i>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block rightFormContent %}
|
||||
{{ form_row(form.category) }}
|
||||
{{ form_row(form.projects) }}
|
||||
{{ form_row(form.language) }}
|
||||
{{ form_row(form.translationParentSelector) }}
|
||||
{{ form_row(form.isPublished, {
|
||||
'attr': {
|
||||
'data-none': 'mautic.core.form.sending_paused',
|
||||
'data-start': 'mautic.core.form.sending_on_scheduled_start',
|
||||
'data-both': 'mautic.core.form.sending_during_scheduled_period',
|
||||
'data-end': 'mautic.core.form.available_until_scheduled_end'
|
||||
}
|
||||
}) }}
|
||||
|
||||
<div id="leadList"{% if 'template' == type %} class="hide"{% endif %}>
|
||||
{{ form_row(form.lists) }}
|
||||
{{ form_row(form.publishUp, {'label': 'mautic.core.form.sending.start_at'}) }}
|
||||
{{ form_row(form.publishDown, {'label': 'mautic.core.form.sending.stop_at'}) }}
|
||||
</div>
|
||||
|
||||
<div class="hide">
|
||||
{{ form_rest(form) }}
|
||||
</div>
|
||||
|
||||
{% if (updateSelect is not defined or updateSelect is empty) and not isExisting and not formContainsErrors(form) or type is empty %}
|
||||
{{- include('@MauticCore/Helper/form_selecttype.html.twig',
|
||||
{
|
||||
'item' : sms,
|
||||
'mauticLang' : {
|
||||
'newListSms' : 'mautic.sms.type.list.header',
|
||||
'newTemplateSms' : 'mautic.sms.type.template.header',
|
||||
},
|
||||
'typePrefix' : 'sms',
|
||||
'cancelUrl' : 'mautic_sms_index',
|
||||
'header' : 'mautic.sms.type.header',
|
||||
'typeOneHeader' : 'mautic.sms.type.template.header',
|
||||
'typeOnePictogram': 'process--automation',
|
||||
'typeOneTag' : 'mautic.sms.type.template.tag',
|
||||
'typeOneDifferences' : ['mautic.sms.type.template.differences.1st', 'mautic.sms.type.template.differences.2nd', 'mautic.sms.type.template.differences.3rd', 'mautic.sms.type.template.differences.4th', 'mautic.sms.type.template.differences.5th'],
|
||||
'typeOneOnClick' : "Mautic.selectSmsType('template');",
|
||||
'typeTwoHeader' : 'mautic.sms.type.list.header',
|
||||
'typeTwoTag' : 'mautic.sms.type.list.tag',
|
||||
'typeTwoPictogram': 'chart--pie',
|
||||
'typeTwoDifferences' : ['mautic.sms.type.list.differences.1st', 'mautic.sms.type.list.differences.2nd', 'mautic.sms.type.list.differences.3rd', 'mautic.sms.type.list.differences.4th', 'mautic.sms.type.list.differences.5th'],
|
||||
'typeTwoOnClick' : "Mautic.selectSmsType('list');",
|
||||
}
|
||||
) -}}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,26 @@
|
||||
{% extends '@MauticCore/Default/content.html.twig' %}
|
||||
{% block mauticContent %}sms{% endblock %}
|
||||
{% block headerTitle %}{{ 'mautic.sms.smses'|trans }}{% endblock %}
|
||||
{% block actions %}
|
||||
{{- include('@MauticCore/Helper/page_actions.html.twig',
|
||||
{
|
||||
'templateButtons' : {
|
||||
'new' : permissions['sms:smses:create'],
|
||||
},
|
||||
'routeBase' : 'sms',
|
||||
}
|
||||
) -}}
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div id="page-list-wrapper" class="panel panel-default">
|
||||
{{- include('@MauticCore/Helper/list_toolbar.html.twig', {
|
||||
'searchValue' : searchValue,
|
||||
'searchId' : 'sms-search',
|
||||
'action' : currentRoute,
|
||||
}) -}}
|
||||
|
||||
<div class="page-list">
|
||||
{{ block('listResults') }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,218 @@
|
||||
{% set isIndex = tmpl == 'index' ? true : false %}
|
||||
{% set tmpl = 'list' %}
|
||||
{% extends isIndex ? '@MauticCore/Default/content.html.twig' : '@MauticCore/Default/raw_output.html.twig' %}
|
||||
{% block mauticContent %}sms{% endblock %}
|
||||
{% block headerTitle %}{{ 'mautic.sms.smses'|trans }}{% endblock %}
|
||||
{% block content %}
|
||||
{% if isIndex %}
|
||||
<div id="page-list-wrapper" class="{% if items|length > 0 or searchValue is not empty %}panel {% endif %}panel-default">
|
||||
{{- include('@MauticCore/Helper/list_toolbar.html.twig', {
|
||||
'searchValue': searchValue,
|
||||
'searchId': 'sms-search',
|
||||
'action': currentRoute,
|
||||
'page_actions': {
|
||||
'templateButtons': {
|
||||
'new': permissions['sms:smses:create']
|
||||
},
|
||||
'routeBase': 'sms'
|
||||
},
|
||||
'quickFilters': [
|
||||
{
|
||||
'search': 'mautic.core.searchcommand.ispublished',
|
||||
'label': 'mautic.core.form.available',
|
||||
'tooltip': 'mautic.core.searchcommand.ispublished.description',
|
||||
'icon': 'ri-check-line'
|
||||
},
|
||||
{
|
||||
'search': 'mautic.core.searchcommand.isunpublished',
|
||||
'label': 'mautic.core.form.unavailable',
|
||||
'tooltip': 'mautic.core.searchcommand.isunpublished.description',
|
||||
'icon': 'ri-close-line'
|
||||
},
|
||||
{
|
||||
'search': 'mautic.core.searchcommand.isuncategorized',
|
||||
'label': 'mautic.core.form.uncategorized',
|
||||
'tooltip': 'mautic.core.searchcommand.isuncategorized.description',
|
||||
'icon': 'ri-folder-unknow-line'
|
||||
},
|
||||
{
|
||||
'search': 'mautic.core.searchcommand.ismine',
|
||||
'label': 'mautic.core.searchcommand.ismine.label',
|
||||
'tooltip': 'mautic.core.searchcommand.ismine.description',
|
||||
'icon': 'ri-user-line'
|
||||
}
|
||||
]
|
||||
}) -}}
|
||||
|
||||
<div class="page-list">
|
||||
{{ block('listResults') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ include('@MauticCore/Modules/protip.html.twig', {
|
||||
tip: random(['mautic.protip.sms.concise', 'mautic.protip.sms.timesensitive', 'mautic.protip.sms.compliance'])
|
||||
}) }}
|
||||
{% else %}
|
||||
{{ block('listResults') }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block listResults %}
|
||||
{% if items|length > 0 %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover sms-list">
|
||||
<thead>
|
||||
<tr>
|
||||
{{- include(
|
||||
'@MauticCore/Helper/tableheader.html.twig',
|
||||
{
|
||||
'checkall' : 'true',
|
||||
'routeBase' : 'sms',
|
||||
'templateButtons' : {
|
||||
'delete' : permissions['sms:smses:deleteown'] or permissions['sms:smses:deleteother'],
|
||||
}
|
||||
}
|
||||
) -}}
|
||||
|
||||
{{- include(
|
||||
'@MauticCore/Helper/tableheader.html.twig',
|
||||
{
|
||||
'sessionVar' : 'sms',
|
||||
'orderBy' : 'e.name',
|
||||
'text' : 'mautic.core.name',
|
||||
'class' : 'col-sms-name',
|
||||
'default' : true,
|
||||
}
|
||||
) -}}
|
||||
|
||||
{{- include(
|
||||
'@MauticCore/Helper/tableheader.html.twig',
|
||||
{
|
||||
'sessionVar' : 'sms',
|
||||
'orderBy' : 'c.title',
|
||||
'text' : 'mautic.core.category',
|
||||
'class' : 'visible-md visible-lg col-sms-category',
|
||||
}
|
||||
) -}}
|
||||
|
||||
<th class="visible-sm visible-md visible-lg col-sms-stats">{{ 'mautic.core.stats'|trans }}</th>
|
||||
|
||||
{{- include(
|
||||
'@MauticCore/Helper/tableheader.html.twig',
|
||||
{
|
||||
'sessionVar' : 'sms',
|
||||
'orderBy' : 'e.id',
|
||||
'text' : 'mautic.core.id',
|
||||
'class' : 'visible-md visible-lg col-sms-id',
|
||||
}
|
||||
) -}}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{# @var item \Mautic\SmsBundle\Entity\Sms #}
|
||||
{% for item in items %}
|
||||
{% set type = item.getSmsType() %}
|
||||
<tr>
|
||||
<td>
|
||||
{% set edit = securityHasEntityAccess(
|
||||
permissions['sms:smses:editown'],
|
||||
permissions['sms:smses:editother'],
|
||||
item.getCreatedBy()
|
||||
) %}
|
||||
{% set customButtons = [
|
||||
{
|
||||
'attr' : {
|
||||
'data-toggle' : 'ajaxmodal',
|
||||
'data-target' : '#MauticSharedModal',
|
||||
'data-header' : 'mautic.sms.smses.header.preview'|trans,
|
||||
'data-footer' : 'false',
|
||||
'href' : path('mautic_sms_action', { 'objectId' : item.getId(), 'objectAction' : 'preview' }),
|
||||
},
|
||||
'btnText' : 'mautic.sms.preview'|trans,
|
||||
'iconClass' : 'ri-share-forward-box-fill',
|
||||
}
|
||||
] %}
|
||||
{{- include(
|
||||
'@MauticCore/Helper/list_actions.html.twig',
|
||||
{
|
||||
'item' : item,
|
||||
'templateButtons' : {
|
||||
'edit' : edit,
|
||||
'clone' : permissions['sms:smses:create'],
|
||||
'delete' : securityHasEntityAccess(
|
||||
permissions['sms:smses:deleteown'],
|
||||
permissions['sms:smses:deleteother'],
|
||||
item.getCreatedBy()
|
||||
),
|
||||
},
|
||||
'routeBase' : 'sms',
|
||||
'customButtons' : customButtons,
|
||||
}
|
||||
) -}}
|
||||
</td>
|
||||
<td>
|
||||
<div>
|
||||
{{- include('@MauticCore/Helper/publishstatus_icon.html.twig',
|
||||
{
|
||||
'item' : item,
|
||||
'model' : 'sms',
|
||||
}
|
||||
) -}}
|
||||
<a href="{{ path('mautic_sms_action', {'objectAction' : 'view', 'objectId' : item.getId()}) }}" data-toggle="ajax">
|
||||
{{ item.getName() }}
|
||||
{% if item.isTranslation() %}
|
||||
<span data-toggle="tooltip" title="{{ 'mautic.core.icon_tooltip.translation'|trans }}"><i class="ri-fw ri-translate fs-14"></i></span>
|
||||
{% endif %}
|
||||
{% if 'list' == type %}
|
||||
<span data-toggle="tooltip" title="{{ 'mautic.sms.icon_tooltip.list_sms'|trans }}">
|
||||
<i class="ri-pie-chart-line fs-14"></i>
|
||||
</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
{{ include('@MauticProject/Modules/projects.html.twig') }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="visible-md visible-lg">
|
||||
{{ include('@MauticCore/Modules/category--expanded.html.twig', {'category': item.getCategory()}) }}
|
||||
</td>
|
||||
{{- include('@MauticSms/Sms/list_stats.html.twig', { 'item' : item }) -}}
|
||||
<td class="visible-md visible-lg">{{ item.getId() }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
{{- include('@MauticCore/Helper/pagination.html.twig', {
|
||||
'totalItems' : totalItems,
|
||||
'page' : page,
|
||||
'limit' : limit,
|
||||
'baseUrl' : path('mautic_sms_index'),
|
||||
'sessionVar' : 'sms',
|
||||
}) -}}
|
||||
</div>
|
||||
{% elseif not configured %}
|
||||
{{- include('@MauticCore/Helper/noresults.html.twig', { 'header' : 'mautic.sms.disabled', 'message' : 'mautic.sms.enable.in.configuration' }) -}}
|
||||
{% else %}
|
||||
{% if searchValue is not empty %}
|
||||
{{- include('@MauticCore/Helper/noresults.html.twig', {'tip' : 'mautic.category.noresults.tip'}) -}}
|
||||
{% else %}
|
||||
<div class="mt-80 col-md-offset-2 col-lg-offset-3 col-md-8 col-lg-5 height-auto">
|
||||
{% set childContainer %}
|
||||
<div class="mt-32 mb-md">
|
||||
{% include '@MauticCore/Components/pictogram.html.twig' with {
|
||||
'pictogram': 'mobile--chat',
|
||||
'size': '80'
|
||||
} %}
|
||||
</div>
|
||||
{% endset %}
|
||||
{% include '@MauticCore/Components/content-block.html.twig' with {
|
||||
heading: 'mautic.sms.contentblock.heading',
|
||||
subheading: 'mautic.sms.contentblock.subheading',
|
||||
copy: 'mautic.sms.contentblock.copy',
|
||||
childContainer: childContainer
|
||||
} %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,23 @@
|
||||
<div class="pa-md">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="panel">
|
||||
<div class="panel-body box-layout">
|
||||
<div class="col-xs-4 va-m">
|
||||
<h5 class="text-white dark-md fw-sb mb-xs">
|
||||
<span class="ri-mail-line"></span>
|
||||
{{ 'mautic.sms.lead.list.comparison'|trans }}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="col-xs-8 va-m" id="legend"></div>
|
||||
</div>
|
||||
<div class="pt-0 pl-15 pb-10 pr-15">
|
||||
<div>
|
||||
<canvas id="list-compare-chart" height="300"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div id="list-compare-chart-data" class="hide">{{ stats|json_encode|raw }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,23 @@
|
||||
{# @var item \Mautic\SmsBundle\Entity\Sms #}
|
||||
{% set type = item.getSmsType() %}
|
||||
<td class="visible-sm visible-md visible-lg col-stats" data-stats="{{ item.getId() }}">
|
||||
{% if 'list' == type %}
|
||||
<span size="sm" class="mt-xs label label-gray clickable-stat{{ item.getPendingCount() > 0 and 'list' == item.getSmsType() ? '' : ' hide' }}"
|
||||
id="pending-{{ item.getId() }}"
|
||||
data-toggle="tooltip"
|
||||
title="{{ 'mautic.channel.stat.leadcount.tooltip'|trans }}">
|
||||
<a href="{{ path('mautic_contact_index', { 'search' : 'mautic.lead.lead.searchcommand.sms_pending'|trans ~ ':' ~ item.getId() }) }}">
|
||||
{{ 'mautic.sms.stat.leadcount'|trans({'%count%' : item.getPendingCount()}) }}
|
||||
</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
<span class="mt-xs label label-green"
|
||||
id="sent-count-{{ item.getId() }}"
|
||||
data-toggle="tooltip"
|
||||
title="{{ 'mautic.channel.stat.leadcount.tooltip'|trans }}">
|
||||
<a href="{{ path('mautic_contact_index',{'search' : 'mautic.lead.lead.searchcommand.sms_sent'|trans ~ ':' ~ item.getId()}) }}">
|
||||
{{ 'mautic.sms.stat.sentcount'|trans({'%count%' : item.getSentCount(true)}) }}
|
||||
</a>
|
||||
</span>
|
||||
</td>
|
||||
@@ -0,0 +1,88 @@
|
||||
{# sms @var \Mautic\SmsBundle\Entity\Sms #}
|
||||
<div class="d-flex jc-center pa-md">
|
||||
<div class="phone-preview animation--slide-in-right">
|
||||
<div class="phone-preview__silence-switch phone-preview__button--outer"></div>
|
||||
<div class="phone-preview__volume-rocker--top phone-preview__button--outer"></div>
|
||||
<div class="phone-preview__volume-rocker--bottom phone-preview__button--outer"></div>
|
||||
<div class="phone-preview__power-button phone-preview__button--outer"></div>
|
||||
|
||||
<div class="phone-preview__top-section">
|
||||
<div class="phone-preview__top-bar">
|
||||
<div class="phone-preview__time">12:00</div>
|
||||
<div class="phone-preview__notch">
|
||||
<div class="phone-preview__speaker">
|
||||
<div class="phone-preview__front-camera"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="phone-preview__status-icons">
|
||||
<i class="ri-signal-tower-line"></i>
|
||||
<i class="ri-wifi-fill"></i>
|
||||
<i class="ri-battery-fill"></i>
|
||||
</div>
|
||||
</div>
|
||||
<i class="phone-preview__back-arrow phone-preview__back-arrow--left"></i>
|
||||
<div class="phone-preview__user-avatar">
|
||||
<i class="ri-account-circle-fill"></i>
|
||||
</div>
|
||||
<div class="phone-preview__user-name">{{ configGetParameter('brand_name') }}</div>
|
||||
</div>
|
||||
|
||||
<div class="phone-preview__messages">
|
||||
<div class="phone-preview__message phone-preview__message--from">
|
||||
<span class="phone-preview__message-content">{{ sms.getMessage() }}</span>
|
||||
</div>
|
||||
<div class="phone-preview__message phone-preview__message--to">{{ 'mautic.sms.preview.demo.message'|trans }}</div>
|
||||
</div>
|
||||
|
||||
<div class="phone-preview__keyboard">
|
||||
<div class="phone-preview__keyboard-input">
|
||||
<div class="phone-preview__input-addon">
|
||||
<i class="ri-camera-fill"></i>
|
||||
</div>
|
||||
<div class="phone-preview__cursor">
|
||||
<i class="ri-arrow-up-circle-fill phone-preview__send-button"></i>
|
||||
</div>
|
||||
<input type="text" class="phone-preview__input-field" placeholder="{{ 'mautic.sms.preview.input.placeholder'|trans }}" maxlength="1000"/>
|
||||
</div>
|
||||
|
||||
<div class="phone-preview__keypad">
|
||||
<div class="phone-preview__key-row">
|
||||
{% for letter in ['Q','W','E','R','T','Y','U','I','O','P'] %}
|
||||
<div class="phone-preview__key--white phone-preview__key--clickable">{{ letter }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="phone-preview__key-spacer"></div>
|
||||
<div class="phone-preview__key-row">
|
||||
{% for letter in ['A','S','D','F','G','H','J','K','L'] %}
|
||||
<div class="phone-preview__key--white phone-preview__key--clickable">{{ letter }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="phone-preview__key-spacer"></div>
|
||||
<div class="phone-preview__key-row">
|
||||
<div class="phone-preview__key--white phone-preview__key--shift">⇧</div>
|
||||
{% for letter in ['Z','X','C','V','B','N','M'] %}
|
||||
<div class="phone-preview__key--white phone-preview__key--clickable">{{ letter }}</div>
|
||||
{% endfor %}
|
||||
<div class="phone-preview__key--grey phone-preview__key--backspace">
|
||||
<i class="ri-delete-back-2-fill"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="phone-preview__key-spacer"></div>
|
||||
<div class="phone-preview__key-row">
|
||||
<div class="phone-preview__key--grey">123</div>
|
||||
<div class="phone-preview__key--grey phone-preview__key--emoji">
|
||||
<i class="ri-emoji-sticker-line"></i>
|
||||
</div>
|
||||
<div class="phone-preview__key--space">space</div>
|
||||
<div class="phone-preview__key--grey phone-preview__key--return">return</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="phone-preview__bottom-symbols">
|
||||
<i class="ri-global-fill phone-preview__symbol--globe"></i>
|
||||
<i class="ri-mic-fill phone-preview__symbol--mic"></i>
|
||||
</div>
|
||||
<div class="phone-preview__home-button"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,26 @@
|
||||
<div class="pa-md">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="panel">
|
||||
<div class="panel-body box-layout">
|
||||
<div class="col-xs-4 va-m">
|
||||
<h5 class="text-white dark-md fw-sb mb-xs">
|
||||
<span class="ri-mail-line"></span>
|
||||
{{ 'mautic.sms.stats'|trans }}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="col-xs-6 va-m" id="legend"></div>
|
||||
<div class="col-xs-2 va-m">
|
||||
{{- include('@MauticCore/Helper/graph_dateselect.htm.twig', {'mescallbacksage' : 'updateSmsStatsChart'}) -}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pt-0 pl-15 pb-10 pr-15">
|
||||
<div>
|
||||
<canvas id="stat-chart" height="300"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div id="stat-chart-data" class="hide">{{ stats|json_encode|raw }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,22 @@
|
||||
{% if showMore is defined %}
|
||||
<a href="{{ url('mautic_sms_index', {'search': searchString}) }}" data-toggle="ajax">
|
||||
<span>{{ 'mautic.core.search.more'|trans({'%count%': remaining}) }}</span>
|
||||
</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<a href="{{ url('mautic_sms_action', {'objectAction': 'view', 'objectId': item.id}) }}" data-toggle="ajax">
|
||||
<span class="fw-sb">{{ item.name }}</span>
|
||||
<span class="ml-4 mr-sm">#{{ item.id }}</span>
|
||||
{{- include('@MauticCore/Helper/publishstatus_badge.html.twig', {
|
||||
'entity': item,
|
||||
'status': 'active',
|
||||
'simplified': 'true'
|
||||
}) -}}
|
||||
</a>
|
||||
<span class="pull-right" data-toggle="tooltip" title="{{ 'mautic.sms.stat.sentcount.label'|trans }}"
|
||||
data-placement="left">
|
||||
<i class="ri-send-plane-line"></i>
|
||||
{{ item.sentCount }}
|
||||
</span>
|
||||
<div class="clearfix"></div>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,56 @@
|
||||
{% set item = event.extra.stat is defined ? event.extra.stat : false %}
|
||||
{% if (item) %}
|
||||
{% set type = event.extra.type is defined ? event.extra.type : null %}
|
||||
<p>
|
||||
{% if item.isFailed is defined and item.isFailed is not empty and 'failed' == type %}
|
||||
{% set details = item.details|json_decode|raw %}
|
||||
{% set errors = '' %}
|
||||
{% if details.failed is defined %}
|
||||
{% set failedDetails = details.failed %}
|
||||
{% if failedDetails is not iterable %}
|
||||
{% set failedDetails = [failedDetails] %}
|
||||
{% endif %}
|
||||
{% set errors = failedDetails|join('<br />') %}
|
||||
{% endif %}
|
||||
<span class="text-danger mt-0 mb-10"><i class="ri-alert-line"></i>
|
||||
{% if errors is defined and errors is not empty %}
|
||||
{{ errors }}
|
||||
{% else %}
|
||||
{{ 'mautic.sms.timeline.event.failed'|trans }}
|
||||
{% endif %}
|
||||
</span>
|
||||
|
||||
{% endif %}
|
||||
{% if item.list_name is defined and item.list_name is not empty and 'failed' != type %}
|
||||
<br />{{ 'mautic.sms.timeline.event.list'|trans({ '%list%' : item.list_name }) }}
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% if errors is defined %}
|
||||
<p class="text-danger mt-0 mb-10">
|
||||
<i class="ri-alert-line"></i> {{ 'mautic.campaign.event.last_error'|trans ~ ': ' ~ errors }}
|
||||
</p>
|
||||
{% else %}
|
||||
<dl class="dl-horizontal">
|
||||
<dt>{{ 'mautic.sms.timeline.status'|trans }}</dt>
|
||||
<dd>
|
||||
{% if item.is_failed is defined and item.is_failed is not empty %}
|
||||
{{ 'mautic.email.stat.failed'|trans }}
|
||||
{% else %}
|
||||
{{ 'mautic.email.send'|trans }}
|
||||
{% endif %}
|
||||
</dd>
|
||||
<dt>{{ 'mautic.sms.timeline.type'|trans }}</dt>
|
||||
<dd>{{ item.type|trans }}</dd>
|
||||
</dl>
|
||||
{% endif %}
|
||||
|
||||
{% if item.message is defined and item.message is not empty %}
|
||||
<div class="small">
|
||||
<hr />
|
||||
<strong>{{ 'mautic.sms.timeline.content.heading'|trans }}</strong>
|
||||
<br />
|
||||
{{ item.message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -0,0 +1 @@
|
||||
<strong>{{ event.extra.message }}</strong>
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\Security\Permissions;
|
||||
|
||||
use Mautic\CoreBundle\Security\Permissions\AbstractPermissions;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
class SmsPermissions extends AbstractPermissions
|
||||
{
|
||||
public function __construct($params)
|
||||
{
|
||||
parent::__construct($params);
|
||||
$this->addStandardPermissions('categories');
|
||||
$this->addExtendedPermissions('smses');
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'sms';
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface &$builder, array $options, array $data): void
|
||||
{
|
||||
$this->addStandardFormFields('sms', 'categories', $builder, $data);
|
||||
$this->addExtendedFormFields('sms', 'smses', $builder, $data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\Sms;
|
||||
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\PluginBundle\Helper\IntegrationHelper;
|
||||
use Mautic\SmsBundle\Entity\Stat;
|
||||
use Mautic\SmsBundle\Exception\PrimaryTransportNotEnabledException;
|
||||
|
||||
class TransportChain
|
||||
{
|
||||
/**
|
||||
* @var TransportInterface[]
|
||||
*/
|
||||
private array $transports;
|
||||
|
||||
/**
|
||||
* @param string $primaryTransport
|
||||
*/
|
||||
public function __construct(
|
||||
private $primaryTransport,
|
||||
private IntegrationHelper $integrationHelper,
|
||||
) {
|
||||
$this->transports = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $alias
|
||||
* @param string $translatableAlias
|
||||
* @param string $integrationAlias
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addTransport($alias, TransportInterface $transport, $translatableAlias, $integrationAlias)
|
||||
{
|
||||
$this->transports[$alias]['alias'] = $translatableAlias;
|
||||
$this->transports[$alias]['integrationAlias'] = $integrationAlias;
|
||||
$this->transports[$alias]['service'] = $transport;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the transport defined in parameters.
|
||||
*
|
||||
* @return TransportInterface
|
||||
*
|
||||
* @throws PrimaryTransportNotEnabledException
|
||||
*/
|
||||
public function getPrimaryTransport()
|
||||
{
|
||||
$enabled = $this->getEnabledTransports();
|
||||
|
||||
// If there no primary transport selected and there is just one available we will use it as primary
|
||||
if (1 === count($enabled)) {
|
||||
return array_shift($enabled);
|
||||
}
|
||||
|
||||
if (0 === count($enabled)) {
|
||||
throw new PrimaryTransportNotEnabledException('Primary SMS transport is not enabled');
|
||||
}
|
||||
|
||||
if (!array_key_exists($this->primaryTransport, $enabled)) {
|
||||
throw new PrimaryTransportNotEnabledException('Primary SMS transport is not enabled. '.$this->primaryTransport);
|
||||
}
|
||||
|
||||
return $enabled[$this->primaryTransport];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $content
|
||||
*
|
||||
* @return mixed
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function sendSms(Lead $lead, $content, ?Stat $stat = null)
|
||||
{
|
||||
return $this->getPrimaryTransport()->sendSms($lead, $content, $stat);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all transports registered in service container.
|
||||
*
|
||||
* @return TransportInterface[]
|
||||
*/
|
||||
public function getTransports()
|
||||
{
|
||||
return $this->transports;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $transport
|
||||
*
|
||||
* @return TransportInterface
|
||||
*
|
||||
* @throws PrimaryTransportNotEnabledException
|
||||
*/
|
||||
public function getTransport($transport)
|
||||
{
|
||||
$enabled = $this->getEnabledTransports();
|
||||
|
||||
if (!array_key_exists($transport, $enabled)) {
|
||||
throw new PrimaryTransportNotEnabledException($transport.' SMS transport is not enabled or does not exist');
|
||||
}
|
||||
|
||||
return $enabled[$transport];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get published transports.
|
||||
*
|
||||
* @return TransportInterface[]
|
||||
*/
|
||||
public function getEnabledTransports(): array
|
||||
{
|
||||
$enabled = [];
|
||||
foreach ($this->transports as $alias => $transport) {
|
||||
if (!isset($transport['published'])) {
|
||||
$integration = $this->integrationHelper->getIntegrationObject($transport['integrationAlias']);
|
||||
if (!$integration) {
|
||||
continue;
|
||||
}
|
||||
$transport['published'] = $integration->getIntegrationSettings()->getIsPublished();
|
||||
$this->transports[$alias] = $transport;
|
||||
}
|
||||
if ($transport['published']) {
|
||||
$enabled[$alias] = $transport['service'];
|
||||
}
|
||||
}
|
||||
|
||||
return $enabled;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\Sms;
|
||||
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
|
||||
interface TransportInterface
|
||||
{
|
||||
/**
|
||||
* @param string $content
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function sendSms(Lead $lead, $content);
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle;
|
||||
|
||||
/**
|
||||
* Events available for SmsBundle.
|
||||
*/
|
||||
final class SmsEvents
|
||||
{
|
||||
/**
|
||||
* The mautic.sms_token_replacement event is thrown right before the content is returned.
|
||||
*
|
||||
* The event listener receives a
|
||||
* Mautic\CoreBundle\Event\TokenReplacementEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const TOKEN_REPLACEMENT = 'mautic.sms_token_replacement';
|
||||
|
||||
/**
|
||||
* The mautic.sms_on_send event is thrown when a sms is sent.
|
||||
*
|
||||
* The event listener receives a
|
||||
* Mautic\SmsBundle\Event\SmsSendEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const SMS_ON_SEND = 'mautic.sms_on_send';
|
||||
|
||||
/**
|
||||
* The mautic.sms_pre_save event is thrown right before a sms is persisted.
|
||||
*
|
||||
* The event listener receives a
|
||||
* Mautic\SmsBundle\Event\SmsEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const SMS_PRE_SAVE = 'mautic.sms_pre_save';
|
||||
|
||||
/**
|
||||
* The mautic.sms_post_save event is thrown right after a sms is persisted.
|
||||
*
|
||||
* The event listener receives a
|
||||
* Mautic\SmsBundle\Event\SmsEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const SMS_POST_SAVE = 'mautic.sms_post_save';
|
||||
|
||||
/**
|
||||
* The mautic.sms_pre_delete event is thrown prior to when a sms is deleted.
|
||||
*
|
||||
* The event listener receives a
|
||||
* Mautic\SmsBundle\Event\SmsEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const SMS_PRE_DELETE = 'mautic.sms_pre_delete';
|
||||
|
||||
/**
|
||||
* The mautic.sms_post_delete event is thrown after a sms is deleted.
|
||||
*
|
||||
* The event listener receives a
|
||||
* Mautic\SmsBundle\Event\SmsEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const SMS_POST_DELETE = 'mautic.sms_post_delete';
|
||||
|
||||
/**
|
||||
* The mautic.sms.on_campaign_trigger_action event is fired when the campaign action triggers.
|
||||
*
|
||||
* The event listener receives a
|
||||
* Mautic\CampaignBundle\Event\CampaignExecutionEvent
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const ON_CAMPAIGN_TRIGGER_ACTION = 'mautic.sms.on_campaign_trigger_action';
|
||||
|
||||
/**
|
||||
* The mautic.sms.on_reply event is dispatched when a SMS service receives a reply.
|
||||
*
|
||||
* The event listener receives a Mautic\SmsBundle\Event\ReplyEvent
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const ON_REPLY = 'mautic.sms.on_reply';
|
||||
|
||||
/**
|
||||
* The mautic.sms.on_campaign_reply event is dispatched when a SMS reply campaign decision is processed.
|
||||
*
|
||||
* The event listener receives a Mautic\SmsBundle\Event\ReplyEvent
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const ON_CAMPAIGN_REPLY = 'mautic.sms.on_campaign_reply';
|
||||
|
||||
/**
|
||||
* The mautic.sms.on_tokens_build event is dispatched when a contact generate tokens are build.
|
||||
*
|
||||
* The event listener receives a
|
||||
* Mautic\SmsBundle\Event\TokensBuildEvent
|
||||
*/
|
||||
public const ON_SMS_TOKENS_BUILD = 'mautic.sms.on_tokens_build';
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\SmsBundle\Tests;
|
||||
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\SmsBundle\Sms\TransportInterface;
|
||||
|
||||
class ArrayTransport implements TransportInterface
|
||||
{
|
||||
/**
|
||||
* @var array<array{'contact': Lead, 'content': string}>
|
||||
*/
|
||||
public array $smses = [];
|
||||
|
||||
/**
|
||||
* @var array<array{'contact': Lead, 'content': string}>
|
||||
*/
|
||||
public array $mmses = [];
|
||||
|
||||
public function sendSms(Lead $lead, $content): bool
|
||||
{
|
||||
$this->smses[] = ['contact' => $lead, 'content' => $content];
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\SmsBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class AjaxControllerFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
public function testGetBuilderTokensAction(): void
|
||||
{
|
||||
$this->client->request(Request::METHOD_POST, '/s/ajax?action=sms:getBuilderTokens');
|
||||
Assert::assertTrue($this->client->getResponse()->isOk());
|
||||
$tokens = json_decode($this->client->getResponse()->getContent(), true);
|
||||
$this->assertArrayHasKey('tokens', $tokens);
|
||||
$this->assertArrayHasKey('{contactfield=email}', $tokens['tokens']);
|
||||
$this->assertArrayHasKey('{ownerfield=email}', $tokens['tokens']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\SmsBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\ProjectBundle\Entity\Project;
|
||||
use Mautic\SmsBundle\Entity\Sms;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
final class SMSControllerFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->configParams['site_url'] = 'https://localhost';
|
||||
parent::setUp();
|
||||
}
|
||||
|
||||
public function testSmsWithProject(): void
|
||||
{
|
||||
$sms = $this->CreateSms();
|
||||
|
||||
$project = new Project();
|
||||
$project->setName('Test Project');
|
||||
$this->em->persist($project);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$crawler = $this->client->request('GET', '/s/sms/edit/'.$sms->getId());
|
||||
$form = $crawler->selectButton('Save')->form();
|
||||
$form['sms[projects]']->setValue((string) $project->getId());
|
||||
|
||||
$this->client->submit($form);
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$savedSms = $this->em->find(Sms::class, $sms->getId());
|
||||
Assert::assertSame($project->getId(), $savedSms->getProjects()->first()->getId());
|
||||
}
|
||||
|
||||
private function CreateSms(string $name = 'sms', string $message = 'sms body'): Sms
|
||||
{
|
||||
$sms = new Sms();
|
||||
$sms->setName($name);
|
||||
$sms->setMessage($message);
|
||||
$sms->setSmsType('template');
|
||||
$this->em->persist($sms);
|
||||
$this->em->flush();
|
||||
|
||||
return $sms;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\SmsBundle\Tests\Controller;
|
||||
|
||||
use Mautic\ProjectBundle\Tests\Functional\AbstractProjectSearchTestCase;
|
||||
use Mautic\SmsBundle\Entity\Sms;
|
||||
|
||||
final class SmsProjectSearchFunctionalTest extends AbstractProjectSearchTestCase
|
||||
{
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('searchDataProvider')]
|
||||
public function testProjectSearch(string $searchTerm, array $expectedEntities, array $unexpectedEntities): void
|
||||
{
|
||||
$projectOne = $this->createProject('Project One');
|
||||
$projectTwo = $this->createProject('Project Two');
|
||||
$projectThree = $this->createProject('Project Three');
|
||||
|
||||
$smsAlpha = $this->createSms('Sms Alpha');
|
||||
$smsBeta = $this->createSms('Sms Beta');
|
||||
$this->createSms('Sms Gamma');
|
||||
$this->createSms('Sms Delta');
|
||||
|
||||
$smsAlpha->addProject($projectOne);
|
||||
$smsAlpha->addProject($projectTwo);
|
||||
$smsBeta->addProject($projectTwo);
|
||||
$smsBeta->addProject($projectThree);
|
||||
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
$this->searchAndAssert($searchTerm, $expectedEntities, $unexpectedEntities, ['/api/smses', '/s/sms']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Generator<string, array{searchTerm: string, expectedEntities: array<string>, unexpectedEntities: array<string>}>
|
||||
*/
|
||||
public static function searchDataProvider(): \Generator
|
||||
{
|
||||
yield 'search by one project' => [
|
||||
'searchTerm' => 'project:"Project Two"',
|
||||
'expectedEntities' => ['Sms Alpha', 'Sms Beta'],
|
||||
'unexpectedEntities' => ['Sms Gamma', 'Sms Delta'],
|
||||
];
|
||||
|
||||
yield 'search by one project AND sms name' => [
|
||||
'searchTerm' => 'project:"Project Two" AND Beta',
|
||||
'expectedEntities' => ['Sms Beta'],
|
||||
'unexpectedEntities' => ['Sms Alpha', 'Sms Gamma', 'Sms Delta'],
|
||||
];
|
||||
|
||||
yield 'search by one project OR sms name' => [
|
||||
'searchTerm' => 'project:"Project Two" OR Gamma',
|
||||
'expectedEntities' => ['Sms Alpha', 'Sms Beta', 'Sms Gamma'],
|
||||
'unexpectedEntities' => ['Sms Delta'],
|
||||
];
|
||||
|
||||
yield 'search by NOT one project' => [
|
||||
'searchTerm' => '!project:"Project Two"',
|
||||
'expectedEntities' => ['Sms Gamma', 'Sms Delta'],
|
||||
'unexpectedEntities' => ['Sms Alpha', 'Sms Beta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with AND' => [
|
||||
'searchTerm' => 'project:"Project Two" AND project:"Project Three"',
|
||||
'expectedEntities' => ['Sms Beta'],
|
||||
'unexpectedEntities' => ['Sms Alpha', 'Sms Gamma', 'Sms Delta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with NOT AND' => [
|
||||
'searchTerm' => '!project:"Project Two" AND !project:"Project Three"',
|
||||
'expectedEntities' => ['Sms Gamma', 'Sms Delta'],
|
||||
'unexpectedEntities' => ['Sms Alpha', 'Sms Beta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with OR' => [
|
||||
'searchTerm' => 'project:"Project Two" OR project:"Project Three"',
|
||||
'expectedEntities' => ['Sms Alpha', 'Sms Beta'],
|
||||
'unexpectedEntities' => ['Sms Gamma', 'Sms Delta'],
|
||||
];
|
||||
|
||||
yield 'search by two projects with NOT OR' => [
|
||||
'searchTerm' => '!project:"Project Two" OR !project:"Project Three"',
|
||||
'expectedEntities' => ['Sms Alpha', 'Sms Gamma', 'Sms Delta'],
|
||||
'unexpectedEntities' => ['Sms Beta'],
|
||||
];
|
||||
}
|
||||
|
||||
private function createSms(string $name): Sms
|
||||
{
|
||||
$sms = new Sms();
|
||||
$sms->setName($name);
|
||||
$sms->setMessage('Message for '.$name);
|
||||
$this->em->persist($sms);
|
||||
|
||||
return $sms;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\Tests\DependencyInjection\Compiler;
|
||||
|
||||
use Mautic\PluginBundle\Helper\IntegrationHelper;
|
||||
use Mautic\SmsBundle\DependencyInjection\Compiler\SmsTransportPass;
|
||||
use Mautic\SmsBundle\Sms\TransportChain;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
|
||||
class SmsTransportPassTest extends TestCase
|
||||
{
|
||||
public function testProcess(): void
|
||||
{
|
||||
$container = new ContainerBuilder();
|
||||
$container->addCompilerPass(new SmsTransportPass());
|
||||
$container
|
||||
->register('foo')
|
||||
->setPublic(true)
|
||||
->setAbstract(true)
|
||||
->addTag('mautic.sms_transport', ['alias'=>'fakeAliasDefault', 'integrationAlias' => 'fakeIntegrationDefault']);
|
||||
|
||||
$container
|
||||
->register('chocolate')
|
||||
->setPublic(true)
|
||||
->setAbstract(true);
|
||||
|
||||
$container
|
||||
->register('bar')
|
||||
->setPublic(true)
|
||||
->setAbstract(true)
|
||||
->addTag('mautic.sms_transport');
|
||||
|
||||
$transport = $this->getMockBuilder(TransportChain::class)
|
||||
->disableOriginalConstructor()
|
||||
->onlyMethods(['addTransport'])
|
||||
->getMock();
|
||||
|
||||
$container
|
||||
->register('mautic.sms.transport_chain')
|
||||
->setClass($transport::class)
|
||||
->setArguments(['foo', $this->createMock(IntegrationHelper::class)])
|
||||
->setShared(false)
|
||||
->setSynthetic(true)
|
||||
->setAbstract(true);
|
||||
|
||||
$pass = new SmsTransportPass();
|
||||
$pass->process($container);
|
||||
|
||||
$this->assertEquals(2, count($container->findTaggedServiceIds('mautic.sms_transport')));
|
||||
|
||||
$methodCalls = $container->getDefinition('mautic.sms.transport_chain')->getMethodCalls();
|
||||
$this->assertCount(count($methodCalls), $container->findTaggedServiceIds('mautic.sms_transport'));
|
||||
|
||||
// Translation string
|
||||
$this->assertEquals('fakeAliasDefault', $methodCalls[0][1][2]);
|
||||
// Integration name/alias
|
||||
$this->assertEquals('fakeIntegrationDefault', $methodCalls[0][1][3]);
|
||||
|
||||
// Translation string is set as service ID by default
|
||||
$this->assertEquals('bar', $methodCalls[1][1][2]);
|
||||
// Integration name/alias is set to service ID by default
|
||||
$this->assertEquals('bar', $methodCalls[1][1][3]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\Tests\EventListener;
|
||||
|
||||
use Mautic\CampaignBundle\Event\CampaignExecutionEvent;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\SmsBundle\Entity\Sms;
|
||||
use Mautic\SmsBundle\EventListener\CampaignSendSubscriber;
|
||||
use Mautic\SmsBundle\Model\SmsModel;
|
||||
use Mautic\SmsBundle\Sms\TransportChain;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
|
||||
class CampaignSendSubscriberTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @var mixed[]
|
||||
*/
|
||||
private $args;
|
||||
|
||||
/**
|
||||
* @var MockObject|SmsModel
|
||||
*/
|
||||
private MockObject $smsModel;
|
||||
|
||||
/**
|
||||
* @var MockObject|TransportChain
|
||||
*/
|
||||
private MockObject $transportChain;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->smsModel = $this->createMock(SmsModel::class);
|
||||
$this->transportChain = $this->createMock(TransportChain::class);
|
||||
|
||||
$lead = new Lead();
|
||||
$lead->setId(1);
|
||||
$this->args = [
|
||||
'lead' => $lead,
|
||||
'event' => [
|
||||
'type' => 'sms.send_text_sms',
|
||||
'properties' => ['sms' => 1],
|
||||
],
|
||||
'eventDetails' => [],
|
||||
'systemTriggered' => true,
|
||||
'eventSettings' => [],
|
||||
];
|
||||
}
|
||||
|
||||
public function testSendDeletedSms(): void
|
||||
{
|
||||
$this->smsModel->expects(self::once())->method('getEntity')->willReturn(null);
|
||||
|
||||
$event = new CampaignExecutionEvent($this->args, false, null);
|
||||
|
||||
$this->CampaignSendSubscriber()->onCampaignTriggerAction($event);
|
||||
self::assertTrue((bool) $event->getResult()['failed']);
|
||||
self::assertSame('mautic.sms.campaign.failed.missing_entity', $event->getResult()['reason']);
|
||||
}
|
||||
|
||||
public function testSendUnpublishedSms(): void
|
||||
{
|
||||
$lead = new Lead();
|
||||
$lead->setId(1);
|
||||
$sms = new Sms();
|
||||
$sms->setIsPublished(false);
|
||||
$this->smsModel->expects(self::once())->method('getEntity')->willReturn($sms);
|
||||
|
||||
$event = new CampaignExecutionEvent($this->args, false, null);
|
||||
|
||||
$this->CampaignSendSubscriber()->onCampaignTriggerAction($event);
|
||||
self::assertTrue((bool) $event->getResult()['failed']);
|
||||
self::assertSame('mautic.sms.campaign.failed.unpublished', $event->getResult()['reason']);
|
||||
}
|
||||
|
||||
private function CampaignSendSubscriber(): CampaignSendSubscriber
|
||||
{
|
||||
return new CampaignSendSubscriber($this->smsModel, $this->transportChain);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\Tests\EventListener;
|
||||
|
||||
use Mautic\CoreBundle\Event\TokenReplacementEvent;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Model\AuditLogModel;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\PageBundle\Entity\Trackable;
|
||||
use Mautic\PageBundle\Helper\TokenHelper;
|
||||
use Mautic\PageBundle\Model\TrackableModel;
|
||||
use Mautic\SmsBundle\EventListener\SmsSubscriber;
|
||||
use Mautic\SmsBundle\Helper\SmsHelper;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class SmsSubscriberTest extends TestCase
|
||||
{
|
||||
private CoreParametersHelper|\PHPUnit\Framework\MockObject\MockObject $coreParametersHelper;
|
||||
|
||||
private $messageText = 'custom http://mautic.com text';
|
||||
|
||||
private $messageUrl = 'http://mautic.com';
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->coreParametersHelper = $this->createMock(CoreParametersHelper::class);
|
||||
parent::setUp();
|
||||
}
|
||||
|
||||
public function testOnTokenReplacementWithTrackableUrls(): void
|
||||
{
|
||||
$mockAuditLogModel = $this->createMock(AuditLogModel::class);
|
||||
|
||||
$mockTrackableModel = $this->createMock(TrackableModel::class);
|
||||
$mockTrackableModel->expects($this->any())->method('parseContentForTrackables')->willReturn([
|
||||
$this->messageUrl,
|
||||
new Trackable(),
|
||||
]);
|
||||
$mockTrackableModel->expects($this->any())->method('generateTrackableUrl')->willReturn('custom');
|
||||
|
||||
$mockPageTokenHelper = $this->createMock(TokenHelper::class);
|
||||
$mockPageTokenHelper->expects($this->any())->method('findPageTokens')->willReturn([]);
|
||||
|
||||
$mockAssetTokenHelper = $this->createMock(\Mautic\AssetBundle\Helper\TokenHelper::class);
|
||||
$mockAssetTokenHelper->expects($this->any())->method('findAssetTokens')->willReturn([]);
|
||||
|
||||
$mockSmsHelper = $this->createMock(SmsHelper::class);
|
||||
$mockSmsHelper->expects($this->any())->method('getDisableTrackableUrls')->willReturn(false);
|
||||
|
||||
$lead = new Lead();
|
||||
$tokenReplacementEvent = new TokenReplacementEvent($this->messageText, $lead, ['channel' => [1 => 'sms']]);
|
||||
$subscriber = new SmsSubscriber(
|
||||
$mockAuditLogModel,
|
||||
$mockTrackableModel,
|
||||
$mockPageTokenHelper,
|
||||
$mockAssetTokenHelper,
|
||||
$mockSmsHelper,
|
||||
$this->coreParametersHelper
|
||||
);
|
||||
$subscriber->onTokenReplacement($tokenReplacementEvent);
|
||||
$this->assertNotSame($this->messageText, $tokenReplacementEvent->getContent());
|
||||
}
|
||||
|
||||
public function testOnTokenReplacementWithDisableTrackableUrls(): void
|
||||
{
|
||||
$mockAuditLogModel = $this->createMock(AuditLogModel::class);
|
||||
|
||||
$mockTrackableModel = $this->createMock(TrackableModel::class);
|
||||
$mockTrackableModel->expects($this->any())->method('parseContentForTrackables')->willReturn([
|
||||
$this->messageUrl,
|
||||
new Trackable(),
|
||||
]);
|
||||
$mockTrackableModel->expects($this->any())->method('generateTrackableUrl')->willReturn('custom');
|
||||
|
||||
$mockPageTokenHelper = $this->createMock(TokenHelper::class);
|
||||
$mockPageTokenHelper->expects($this->any())->method('findPageTokens')->willReturn([]);
|
||||
|
||||
$mockAssetTokenHelper = $this->createMock(\Mautic\AssetBundle\Helper\TokenHelper::class);
|
||||
$mockAssetTokenHelper->expects($this->any())->method('findAssetTokens')->willReturn([]);
|
||||
|
||||
$mockSmsHelper = $this->createMock(SmsHelper::class);
|
||||
$mockSmsHelper->expects($this->any())->method('getDisableTrackableUrls')->willReturn(true);
|
||||
|
||||
$lead = new Lead();
|
||||
$tokenReplacementEvent = new TokenReplacementEvent($this->messageText, $lead, ['channel' => ['sms', 1]]);
|
||||
$subscriber = new SmsSubscriber(
|
||||
$mockAuditLogModel,
|
||||
$mockTrackableModel,
|
||||
$mockPageTokenHelper,
|
||||
$mockAssetTokenHelper,
|
||||
$mockSmsHelper,
|
||||
$this->coreParametersHelper
|
||||
);
|
||||
$subscriber->onTokenReplacement($tokenReplacementEvent);
|
||||
$this->assertSame($this->messageText, $tokenReplacementEvent->getContent());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\SmsBundle\Tests\EventListener;
|
||||
|
||||
use Mautic\AssetBundle\Entity\Asset;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Mautic\PageBundle\Entity\Page;
|
||||
use Mautic\SmsBundle\Entity\Sms;
|
||||
use Mautic\SmsBundle\Model\SmsModel;
|
||||
use Mautic\SmsBundle\Tests\SmsTestHelperTrait;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
final class SmsSubscriberTokenTest extends MauticMysqlTestCase
|
||||
{
|
||||
use SmsTestHelperTrait;
|
||||
|
||||
public function testSmsTokenReplacement(): void
|
||||
{
|
||||
$transport = $this->configureTwilioWithArrayTransport();
|
||||
$smsModel = $this->getContainer()->get('mautic.sms.model.sms');
|
||||
\assert($smsModel instanceof SmsModel);
|
||||
|
||||
$contactModel = $this->getContainer()->get('mautic.lead.model.lead');
|
||||
\assert($contactModel instanceof LeadModel);
|
||||
|
||||
$page = new Page();
|
||||
$page->setTitle('Test Page');
|
||||
$page->setAlias('test-page');
|
||||
|
||||
$this->em->persist($page);
|
||||
|
||||
$asset = new Asset();
|
||||
$asset->setPath('test.jpg');
|
||||
$asset->setTitle('test');
|
||||
$asset->setAlias('test');
|
||||
|
||||
$this->em->persist($asset);
|
||||
|
||||
$contact = new Lead();
|
||||
$contact->setFirstname('John');
|
||||
$contact->setPhone('1234567890');
|
||||
|
||||
$this->em->persist($contact);
|
||||
$this->em->flush();
|
||||
|
||||
$sms = new Sms();
|
||||
$sms->setName('Test SMS');
|
||||
$sms->setMessage("Hello {contactfield=firstname}, download {assetlink={$asset->getId()}} or visit {pagelink={$page->getId()}} or https://mautic.org");
|
||||
|
||||
$smsModel->saveEntity($sms);
|
||||
$smsModel->sendSms($sms, $contactModel->getEntity($contact->getId()));
|
||||
|
||||
Assert::assertCount(1, $transport->smses);
|
||||
|
||||
$ctRegex = 'ct=([a-zA-Z0-9%]+)';
|
||||
$domainRegex = 'https?:\/\/([a-zA-Z0-9.-]+)';
|
||||
$assetLinkRegex = $domainRegex.'\/asset\/'.$asset->getId().':test\?'.$ctRegex;
|
||||
$pageLinkregex = $domainRegex.'\/test-page\?'.$ctRegex;
|
||||
$trackingRegex = $domainRegex.'\/r\/([a-zA-Z0-9]+)\?'.$ctRegex;
|
||||
|
||||
Assert::assertMatchesRegularExpression(
|
||||
"/Hello John, download {$assetLinkRegex} or visit {$pageLinkregex} or {$trackingRegex}/",
|
||||
$transport->smses[0]['content']
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\Tests\EventListener;
|
||||
|
||||
use Mautic\LeadBundle\Entity\DoNotContact;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Model\DoNotContact as DoNotContactModel;
|
||||
use Mautic\SmsBundle\Event\ReplyEvent;
|
||||
use Mautic\SmsBundle\EventListener\StopSubscriber;
|
||||
|
||||
class StopSubscriberTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @var \PHPUnit\Framework\MockObject\MockObject&DoNotContactModel
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $doNotContactModel;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->doNotContactModel = $this->createMock(DoNotContactModel::class);
|
||||
}
|
||||
|
||||
public function testLeadAddedToDNC(): void
|
||||
{
|
||||
$lead = new Lead();
|
||||
$lead->setId(1);
|
||||
$event = new ReplyEvent($lead, 'stop');
|
||||
|
||||
$this->doNotContactModel->expects($this->once())
|
||||
->method('addDncForContact')
|
||||
->with(1, 'sms', DoNotContact::UNSUBSCRIBED);
|
||||
|
||||
$this->StopSubscriber()->onReply($event);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return StopSubscriber
|
||||
*/
|
||||
private function StopSubscriber()
|
||||
{
|
||||
return new StopSubscriber($this->doNotContactModel);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\Tests\EventListener;
|
||||
|
||||
use Mautic\EmailBundle\Entity\Email;
|
||||
use Mautic\EmailBundle\Entity\Stat;
|
||||
use Mautic\EmailBundle\Entity\StatRepository;
|
||||
use Mautic\EmailBundle\EventListener\TrackingSubscriber;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Event\ContactIdentificationEvent;
|
||||
|
||||
class TrackingSubscriberTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @var \PHPUnit\Framework\MockObject\MockObject|StatRepository
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $statRepository;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->statRepository = $this->createMock(StatRepository::class);
|
||||
}
|
||||
|
||||
public function testIdentifyContactByStat(): void
|
||||
{
|
||||
$ct = [
|
||||
'lead' => 2,
|
||||
'channel' => [
|
||||
'email' => 1,
|
||||
],
|
||||
'stat' => 'abc123',
|
||||
];
|
||||
|
||||
$email = $this->createMock(Email::class);
|
||||
$email->method('getId')
|
||||
->willReturn(1);
|
||||
|
||||
$lead = $this->createMock(Lead::class);
|
||||
$lead->method('getId')
|
||||
->willReturn(2);
|
||||
|
||||
$stat = new Stat();
|
||||
$stat->setEmail($email);
|
||||
$stat->setLead($lead);
|
||||
|
||||
$this->statRepository->expects($this->once())
|
||||
->method('findOneBy')
|
||||
->with(['trackingHash' => 'abc123'])
|
||||
->willReturn($stat);
|
||||
|
||||
$event = new ContactIdentificationEvent($ct);
|
||||
|
||||
$this->getSubscriber()->onIdentifyContact($event);
|
||||
|
||||
$this->assertEquals($lead->getId(), $event->getIdentifiedContact()->getId());
|
||||
}
|
||||
|
||||
public function testChannelMismatchDoesNotIdentify(): void
|
||||
{
|
||||
$ct = [
|
||||
'lead' => 2,
|
||||
'channel' => [
|
||||
'sms' => 1,
|
||||
],
|
||||
'stat' => 'abc123',
|
||||
];
|
||||
|
||||
$event = new ContactIdentificationEvent($ct);
|
||||
|
||||
$this->getSubscriber()->onIdentifyContact($event);
|
||||
|
||||
$this->assertNull($event->getIdentifiedContact());
|
||||
}
|
||||
|
||||
public function testChannelIdMismatchDoesNotIdentify(): void
|
||||
{
|
||||
$ct = [
|
||||
'lead' => 2,
|
||||
'channel' => [
|
||||
'email' => 2,
|
||||
],
|
||||
'stat' => 'abc123',
|
||||
];
|
||||
|
||||
$email = $this->createMock(Email::class);
|
||||
$email->method('getId')
|
||||
->willReturn(1);
|
||||
|
||||
$lead = $this->createMock(Lead::class);
|
||||
$lead->method('getId')
|
||||
->willReturn(2);
|
||||
|
||||
$stat = new Stat();
|
||||
$stat->setEmail($email);
|
||||
$stat->setLead($lead);
|
||||
|
||||
$this->statRepository->expects($this->once())
|
||||
->method('findOneBy')
|
||||
->with(['trackingHash' => 'abc123'])
|
||||
->willReturn($stat);
|
||||
|
||||
$event = new ContactIdentificationEvent($ct);
|
||||
|
||||
$this->getSubscriber()->onIdentifyContact($event);
|
||||
|
||||
$this->assertNull($event->getIdentifiedContact());
|
||||
}
|
||||
|
||||
public function testStatEmptyLeadDoesNotIdentify(): void
|
||||
{
|
||||
$ct = [
|
||||
'lead' => 2,
|
||||
'channel' => [
|
||||
'email' => 2,
|
||||
],
|
||||
'stat' => 'abc123',
|
||||
];
|
||||
|
||||
$email = $this->createMock(Email::class);
|
||||
$email->method('getId')
|
||||
->willReturn(1);
|
||||
|
||||
$stat = new Stat();
|
||||
$stat->setEmail($email);
|
||||
|
||||
$this->statRepository->expects($this->once())
|
||||
->method('findOneBy')
|
||||
->with(['trackingHash' => 'abc123'])
|
||||
->willReturn($stat);
|
||||
|
||||
$event = new ContactIdentificationEvent($ct);
|
||||
|
||||
$this->getSubscriber()->onIdentifyContact($event);
|
||||
|
||||
$this->assertNull($event->getIdentifiedContact());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return TrackingSubscriber
|
||||
*/
|
||||
private function getSubscriber()
|
||||
{
|
||||
return new TrackingSubscriber($this->statRepository);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\SmsBundle\Tests\EventListener;
|
||||
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\SmsBundle\Event\SmsSendEvent;
|
||||
use Mautic\SmsBundle\EventListener\WebhookSubscriber;
|
||||
use Mautic\SmsBundle\SmsEvents;
|
||||
use Mautic\WebhookBundle\Event\WebhookBuilderEvent;
|
||||
use Mautic\WebhookBundle\Model\WebhookModel;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
|
||||
final class WebhookSubscriberTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @var MockObject|WebhookModel
|
||||
*/
|
||||
private MockObject $webhookModel;
|
||||
|
||||
private WebhookSubscriber $subscriber;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->webhookModel = $this->createMock(WebhookModel::class);
|
||||
$this->subscriber = new WebhookSubscriber($this->webhookModel);
|
||||
}
|
||||
|
||||
public function testOnWebhookBuild(): void
|
||||
{
|
||||
$event = $this->createMock(WebhookBuilderEvent::class);
|
||||
|
||||
$event->expects($this->once())
|
||||
->method('addEvent')
|
||||
->with(
|
||||
SmsEvents::SMS_ON_SEND,
|
||||
[
|
||||
'label' => 'mautic.sms.webhook.event.send',
|
||||
'description' => 'mautic.sms.webhook.event.send_desc',
|
||||
]
|
||||
);
|
||||
|
||||
$this->subscriber->onWebhookBuild($event);
|
||||
}
|
||||
|
||||
public function testOnSend(): void
|
||||
{
|
||||
$event = $this->createMock(SmsSendEvent::class);
|
||||
$contact = $this->createMock(Lead::class);
|
||||
|
||||
$event->expects($this->once())
|
||||
->method('getSmsId')
|
||||
->willReturn(343);
|
||||
|
||||
$event->expects($this->once())
|
||||
->method('getLead')
|
||||
->willReturn($contact);
|
||||
|
||||
$event->expects($this->once())
|
||||
->method('getContent')
|
||||
->willReturn('The SMS content.');
|
||||
|
||||
$this->webhookModel->expects($this->once())
|
||||
->method('queueWebhooksByType')
|
||||
->with(
|
||||
SmsEvents::SMS_ON_SEND,
|
||||
[
|
||||
'smsId' => 343,
|
||||
'contact' => $contact,
|
||||
'content' => 'The SMS content.',
|
||||
]
|
||||
);
|
||||
|
||||
$this->subscriber->onSend($event);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\SmsBundle\Tests\Functional;
|
||||
|
||||
use Mautic\SmsBundle\Entity\Sms;
|
||||
|
||||
trait CreateEntitiesTrait
|
||||
{
|
||||
private function createAnSms(string $name, string $message, bool $isPublished = true, string $locale = 'en'): Sms
|
||||
{
|
||||
$sms = new Sms();
|
||||
$sms->setName($name);
|
||||
$sms->setMessage($message);
|
||||
$sms->setLanguage($locale);
|
||||
$sms->setIsPublished($isPublished);
|
||||
|
||||
return $sms;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\SmsBundle\Tests\Functional;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\SmsBundle\Entity\Sms;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
final class SmsControllerFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
use CreateEntitiesTrait;
|
||||
|
||||
public function testSmsCanBeCreatedWithTranslationParent(): void
|
||||
{
|
||||
// Arrange
|
||||
$parentSms = $this->createAndPersistSms('Parent SMS', 'Parent SMS message');
|
||||
|
||||
// Act
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/sms/new');
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$form = $crawler->selectButton('Save')->form();
|
||||
$form['sms[name]'] = 'Child SMS';
|
||||
$form['sms[message]'] = 'Child SMS message';
|
||||
$form['sms[translationParentSelector]'] = (string) $parentSms->getId();
|
||||
|
||||
$this->client->submit($form);
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
// Assert
|
||||
$childSms = $this->em->getRepository(Sms::class)->findOneBy(['name' => 'Child SMS']);
|
||||
$this->assertInstanceOf(Sms::class, $childSms);
|
||||
$this->assertInstanceOf(Sms::class, $childSms->getTranslationParent());
|
||||
$this->assertSame($parentSms->getId(), $childSms->getTranslationParent()->getId());
|
||||
}
|
||||
|
||||
public function testSmsCannotBeItsOwnTranslationParent(): void
|
||||
{
|
||||
// Arrange
|
||||
$sms = $this->createAndPersistSms('Test SMS', 'Test SMS message');
|
||||
|
||||
// Act
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/sms/edit/'.$sms->getId());
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
// Assert
|
||||
$options = $crawler->filter('#sms_translationParentSelector option');
|
||||
$this->assertCount(2, $options);
|
||||
$this->assertSame('Choose a translated item...', $options->eq(0)->text());
|
||||
$this->assertSame('Create new...', $options->eq(1)->text());
|
||||
|
||||
// Ensure the SMS itself is not in the dropdown
|
||||
$optionValues = $options->each(fn ($node) => $node->attr('value'));
|
||||
$this->assertNotContains((string) $sms->getId(), $optionValues);
|
||||
}
|
||||
|
||||
public function testSmsWithTranslationParentCanBeEdited(): void
|
||||
{
|
||||
// Arrange
|
||||
$parentSms = $this->createAndPersistSms('Parent SMS', 'Parent SMS message');
|
||||
$childSms = $this->createAndPersistSms('Child SMS', 'Child SMS message');
|
||||
$childSms->setTranslationParent($parentSms);
|
||||
|
||||
$newParentSms = $this->createAndPersistSms('New Parent SMS', 'New Parent SMS message');
|
||||
$this->em->flush();
|
||||
|
||||
// Act
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/sms/edit/'.$childSms->getId());
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
// Assert original parent is selected
|
||||
$this->assertSame(
|
||||
(string) $parentSms->getId(),
|
||||
$crawler->filter('#sms_translationParentSelector option[selected]')->attr('value')
|
||||
);
|
||||
|
||||
// Change parent
|
||||
$form = $crawler->selectButton('Save')->form();
|
||||
$form['sms[translationParentSelector]'] = (string) $newParentSms->getId();
|
||||
$this->client->submit($form);
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
// Assert parent updated
|
||||
$this->em->refresh($childSms);
|
||||
$this->assertInstanceOf(Sms::class, $childSms->getTranslationParent());
|
||||
$this->assertSame($newParentSms->getId(), $childSms->getTranslationParent()->getId());
|
||||
}
|
||||
|
||||
public function testTranslationParentCanBeRemovedFromSms(): void
|
||||
{
|
||||
// Arrange
|
||||
$parentSms = $this->createAndPersistSms('Parent SMS', 'Parent SMS message');
|
||||
$childSms = $this->createAndPersistSms('Child SMS', 'Child SMS message');
|
||||
$childSms->setTranslationParent($parentSms);
|
||||
$this->em->flush();
|
||||
|
||||
// Act
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/sms/edit/'.$childSms->getId());
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$form = $crawler->selectButton('Save')->form();
|
||||
$form['sms[translationParentSelector]'] = '';
|
||||
$this->client->submit($form);
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
// Assert
|
||||
$this->em->refresh($childSms);
|
||||
$this->assertNull($childSms->getTranslationParent());
|
||||
}
|
||||
|
||||
public function testTranslationsAreDisplayedOnViewPage(): void
|
||||
{
|
||||
// Arrange
|
||||
$parentSms = $this->createAndPersistSms('Parent SMS', 'Parent SMS message', 'en');
|
||||
$childSms = $this->createAndPersistSms('Child SMS', 'Child SMS message', 'fr');
|
||||
$childSms->setTranslationParent($parentSms);
|
||||
$parentSms->addTranslationChild($childSms);
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
// Act & Assert - Parent view
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/sms/view/'.$parentSms->getId());
|
||||
$this->assertResponseIsSuccessful();
|
||||
$this->assertCount(1, $crawler->filter('a[href="#translation-container"]'));
|
||||
$this->client->click($crawler->selectLink('Translations')->link());
|
||||
$this->assertSelectorTextContains('#translation-container', 'Child SMS');
|
||||
|
||||
// Act & Assert - Child view
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/sms/view/'.$childSms->getId());
|
||||
$this->assertResponseIsSuccessful();
|
||||
$this->assertCount(1, $crawler->filter('a[href="#translation-container"]'));
|
||||
$this->client->click($crawler->selectLink('Translations')->link());
|
||||
$this->assertSelectorTextContains('#translation-container', 'Parent SMS');
|
||||
}
|
||||
|
||||
private function createAndPersistSms(string $name, string $message, string $locale = 'en'): Sms
|
||||
{
|
||||
$sms = $this->createAnSms($name, $message, true, $locale);
|
||||
$this->em->persist($sms);
|
||||
$this->em->flush();
|
||||
|
||||
return $sms;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\SmsBundle\Tests\Functional;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\SmsBundle\Entity\Sms;
|
||||
use Mautic\SmsBundle\Model\SmsModel;
|
||||
use Mautic\SmsBundle\Sms\TransportChain;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
|
||||
final class SmsModelFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
use CreateEntitiesTrait;
|
||||
|
||||
#[DataProvider('smsTranslationDataProvider')]
|
||||
public function testSmsTranslationBasedOnLocale(string $contactLocale, string $expectedMessage): void
|
||||
{
|
||||
// 1. Create SMS with translation
|
||||
$sms = $this->createAnSms('English SMS', 'Hello');
|
||||
$smsFr = $this->createAnSms('French SMS', 'Bonjour', true, 'fr_FR');
|
||||
$smsFr->setTranslationParent($sms);
|
||||
|
||||
$this->em->persist($sms);
|
||||
$this->em->persist($smsFr);
|
||||
|
||||
// 2. Create contact
|
||||
$contact = new Lead();
|
||||
$contact->setFirstname('Test');
|
||||
$contact->setLastname('Contact');
|
||||
$contact->setMobile('123456789');
|
||||
$this->em->persist($contact);
|
||||
$this->em->flush();
|
||||
|
||||
$contactId = $contact->getId();
|
||||
|
||||
// Clear the EM and fetch the entities
|
||||
$this->em->clear();
|
||||
$contact = $this->em->find(Lead::class, $contactId);
|
||||
$sms = $this->em->find(Sms::class, $sms->getId());
|
||||
|
||||
// Set locale
|
||||
$contact->addUpdatedField('preferred_locale', $contactLocale);
|
||||
$this->em->persist($contact);
|
||||
$this->em->flush();
|
||||
|
||||
// 3. Mock transport
|
||||
$transportMock = $this->createMock(TransportChain::class);
|
||||
$transportMock->expects($this->once())
|
||||
->method('sendSms')
|
||||
->with(
|
||||
$this->anything(),
|
||||
$this->callback(function (string $message) use ($expectedMessage) {
|
||||
$this->assertSame($expectedMessage, $message);
|
||||
|
||||
return true;
|
||||
}),
|
||||
$this->anything()
|
||||
)
|
||||
->willReturn(true);
|
||||
|
||||
$this->getContainer()->set('mautic.sms.transport_chain', $transportMock);
|
||||
|
||||
/** @var SmsModel $smsModel */
|
||||
$smsModel = $this->getContainer()->get('mautic.sms.model.sms');
|
||||
|
||||
// 4. Send SMS
|
||||
$smsModel->sendSms($sms, $contact);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string, string[]>
|
||||
*/
|
||||
public static function smsTranslationDataProvider(): iterable
|
||||
{
|
||||
yield 'translation exists' => ['fr_FR', 'Bonjour'];
|
||||
yield 'translation not available (fallback)' => ['de_DE', 'Hello'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\Tests\Helper;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Tracker\ContactTracker;
|
||||
use Mautic\SmsBundle\Callback\CallbackInterface;
|
||||
use Mautic\SmsBundle\Exception\NumberNotFoundException;
|
||||
use Mautic\SmsBundle\Helper\ReplyHelper;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class ReplyHelperTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @var EventDispatcherInterface|\PHPUnit\Framework\MockObject\MockObject
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $eventDispatcher;
|
||||
|
||||
private NullLogger $logger;
|
||||
|
||||
/**
|
||||
* @var ContactTracker|\PHPUnit\Framework\MockObject\MockObject
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $contactTracker;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->eventDispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||
$this->logger = new NullLogger();
|
||||
$this->contactTracker = $this->createMock(ContactTracker::class);
|
||||
}
|
||||
|
||||
public function testFoundContactsDispatchEvent(): void
|
||||
{
|
||||
$handler = $this->createMock(CallbackInterface::class);
|
||||
$handler->expects($this->once())
|
||||
->method('getContacts')
|
||||
->willReturn(new ArrayCollection([new Lead()]));
|
||||
|
||||
$handler->method('getMessage')->willReturn('some message');
|
||||
|
||||
$this->contactTracker->expects($this->once())
|
||||
->method('setSystemContact');
|
||||
|
||||
$this->eventDispatcher->expects($this->once())
|
||||
->method('dispatch');
|
||||
|
||||
$this->getHelper()->handleRequest($handler, new Request());
|
||||
}
|
||||
|
||||
public function testContactsNotFoundDoesNotDispatchEvent(): void
|
||||
{
|
||||
$handler = $this->createMock(CallbackInterface::class);
|
||||
$handler->expects($this->once())
|
||||
->method('getContacts')
|
||||
->willReturnCallback(
|
||||
function (): void {
|
||||
throw new NumberNotFoundException('');
|
||||
}
|
||||
);
|
||||
|
||||
$this->contactTracker->expects($this->never())
|
||||
->method('setSystemContact');
|
||||
|
||||
$this->eventDispatcher->expects($this->never())
|
||||
->method('dispatch');
|
||||
|
||||
$this->getHelper()->handleRequest($handler, new Request());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ReplyHelper
|
||||
*/
|
||||
private function getHelper()
|
||||
{
|
||||
return new ReplyHelper($this->eventDispatcher, $this->logger, $this->contactTracker);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\Tests\Integration\Twilio;
|
||||
|
||||
use Mautic\PluginBundle\Entity\Integration;
|
||||
use Mautic\PluginBundle\Helper\IntegrationHelper;
|
||||
use Mautic\PluginBundle\Integration\AbstractIntegration;
|
||||
use Mautic\SmsBundle\Integration\Twilio\Configuration;
|
||||
use Twilio\Exceptions\ConfigurationException;
|
||||
|
||||
class ConfigurationTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @var IntegrationHelper|\PHPUnit\Framework\MockObject\MockObject
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $integrationHelper;
|
||||
|
||||
/**
|
||||
* @var AbstractIntegration|\PHPUnit\Framework\MockObject\MockObject
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $integrationObject;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->integrationHelper = $this->createMock(IntegrationHelper::class);
|
||||
|
||||
$integrationSettings = new Integration();
|
||||
$integrationSettings->setIsPublished(true);
|
||||
$integrationSettings->setFeatureSettings(['messaging_service_sid' => '123']);
|
||||
$this->integrationObject = $this->createMock(AbstractIntegration::class);
|
||||
$this->integrationObject->method('getIntegrationSettings')
|
||||
->willReturn($integrationSettings);
|
||||
|
||||
$this->integrationHelper->method('getIntegrationObject')
|
||||
->with('Twilio')
|
||||
->willReturn($this->integrationObject);
|
||||
}
|
||||
|
||||
public function testGetMessagingServiceSid(): void
|
||||
{
|
||||
$this->integrationObject->method('getDecryptedApiKeys')
|
||||
->willReturn(
|
||||
[
|
||||
'username' => 'username',
|
||||
'password' => 'password',
|
||||
]
|
||||
);
|
||||
$this->assertEquals('123', $this->getConfiguration()->getMessagingServiceSid());
|
||||
}
|
||||
|
||||
public function testGetAccountSid(): void
|
||||
{
|
||||
$this->integrationObject->method('getDecryptedApiKeys')
|
||||
->willReturn(
|
||||
[
|
||||
'username' => 'username',
|
||||
'password' => 'password',
|
||||
]
|
||||
);
|
||||
$this->assertEquals('username', $this->getConfiguration()->getAccountSid());
|
||||
}
|
||||
|
||||
public function testGetAuthToken(): void
|
||||
{
|
||||
$this->integrationObject->method('getDecryptedApiKeys')
|
||||
->willReturn(
|
||||
[
|
||||
'username' => 'username',
|
||||
'password' => 'password',
|
||||
]
|
||||
);
|
||||
$this->assertEquals('password', $this->getConfiguration()->getAuthToken());
|
||||
}
|
||||
|
||||
public function testConfigurationExceptionThrownIfNotPublished(): void
|
||||
{
|
||||
$this->expectException(ConfigurationException::class);
|
||||
|
||||
$integrationSettings = new Integration();
|
||||
$integrationSettings->setIsPublished(false);
|
||||
$integrationSettings->setFeatureSettings(['messaging_service_sid' => '123']);
|
||||
|
||||
$this->integrationObject->method('getIntegrationSettings')
|
||||
->willReturn($integrationSettings);
|
||||
|
||||
$this->getConfiguration()->getMessagingServiceSid();
|
||||
}
|
||||
|
||||
public function testConfigurationExceptionThrownWithoutMessagingServiceSId(): void
|
||||
{
|
||||
$this->expectException(ConfigurationException::class);
|
||||
|
||||
$this->integrationObject->getIntegrationSettings()->setFeatureSettings(['messaging_service_sid' => '']);
|
||||
|
||||
$this->getConfiguration()->getMessagingServiceSid();
|
||||
}
|
||||
|
||||
public function testConfigurationExceptionThrownWithoutUsername(): void
|
||||
{
|
||||
$this->expectException(ConfigurationException::class);
|
||||
$this->integrationObject->method('getDecryptedApiKeys')
|
||||
->willReturn(
|
||||
[
|
||||
'username' => '',
|
||||
'password' => 'password',
|
||||
]
|
||||
);
|
||||
$this->getConfiguration()->getMessagingServiceSid();
|
||||
}
|
||||
|
||||
public function testConfigurationExceptionThrownWithoutPassword(): void
|
||||
{
|
||||
$this->expectException(ConfigurationException::class);
|
||||
$this->integrationObject->method('getDecryptedApiKeys')
|
||||
->willReturn(
|
||||
[
|
||||
'username' => 'username',
|
||||
'password' => '',
|
||||
]
|
||||
);
|
||||
$this->getConfiguration()->getMessagingServiceSid();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Configuration
|
||||
*/
|
||||
private function getConfiguration()
|
||||
{
|
||||
return new Configuration($this->integrationHelper);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\Tests\Integration\Twilio;
|
||||
|
||||
use Mautic\SmsBundle\Helper\ContactHelper;
|
||||
use Mautic\SmsBundle\Integration\Twilio\Configuration;
|
||||
use Mautic\SmsBundle\Integration\Twilio\TwilioCallback;
|
||||
use Symfony\Component\HttpFoundation\InputBag;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
|
||||
class TwilioCallbackTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @var ContactHelper|\PHPUnit\Framework\MockObject\MockObject
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $contactHelper;
|
||||
|
||||
/**
|
||||
* @var Configuration|\PHPUnit\Framework\MockObject\MockObject
|
||||
*/
|
||||
private \PHPUnit\Framework\MockObject\MockObject $configuration;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->contactHelper = $this->createMock(ContactHelper::class);
|
||||
$this->configuration = $this->createMock(Configuration::class);
|
||||
$this->configuration->method('getAccountSid')
|
||||
->willReturn('123');
|
||||
}
|
||||
|
||||
public function testMissingFromThrowsBadRequestException(): void
|
||||
{
|
||||
$this->expectException(BadRequestHttpException::class);
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
$inputBag = new InputBag([
|
||||
'AccountSid' => '123',
|
||||
'From' => '',
|
||||
]);
|
||||
|
||||
$request->request = $inputBag;
|
||||
|
||||
$this->getCallback()->getMessage($request);
|
||||
}
|
||||
|
||||
public function testMissingBodyThrowsBadRequestException(): void
|
||||
{
|
||||
$this->expectException(BadRequestHttpException::class);
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
$inputBag = new InputBag([
|
||||
'AccountSid' => '123',
|
||||
'From' => '321',
|
||||
'Body' => '',
|
||||
]);
|
||||
|
||||
$request->request = $inputBag;
|
||||
|
||||
$this->getCallback()->getMessage($request);
|
||||
}
|
||||
|
||||
public function testMismatchedAccountSidThrowsBadRequestException(): void
|
||||
{
|
||||
$this->expectException(BadRequestHttpException::class);
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
$inputBag = new InputBag([
|
||||
'AccountSid' => '321',
|
||||
]);
|
||||
|
||||
$request->request = $inputBag;
|
||||
|
||||
$this->getCallback()->getMessage($request);
|
||||
}
|
||||
|
||||
public function testMessageIsReturned(): void
|
||||
{
|
||||
$request = $this->createMock(Request::class);
|
||||
$request->method('get')
|
||||
->willReturn('Hello');
|
||||
|
||||
$inputBag = new InputBag([
|
||||
'AccountSid' => '123',
|
||||
'From' => '321',
|
||||
'Body' => 'Hello',
|
||||
]);
|
||||
|
||||
$request->request = $inputBag;
|
||||
|
||||
$this->assertEquals('Hello', $this->getCallback()->getMessage($request));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return TwilioCallback
|
||||
*/
|
||||
private function getCallback()
|
||||
{
|
||||
return new TwilioCallback($this->contactHelper, $this->configuration);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\SmsBundle\Tests\Integration\Twilio;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\PluginBundle\Entity\Integration;
|
||||
use Mautic\SmsBundle\Integration\TwilioIntegration;
|
||||
use Mautic\SmsBundle\Tests\SmsTestHelperTrait;
|
||||
use PHPUnit\Framework\Assert;
|
||||
|
||||
final class TwilioConfigurationFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
use SmsTestHelperTrait;
|
||||
|
||||
public function testSaveTwilioConfig(): void
|
||||
{
|
||||
$this->configureTwilioWithArrayTransport();
|
||||
|
||||
$integration = $this->getContainer()->get('mautic.integration.twilio');
|
||||
\assert($integration instanceof TwilioIntegration);
|
||||
|
||||
$integrationRepository = $this->em->getRepository(Integration::class);
|
||||
|
||||
$integrationConfig = $integrationRepository->findOneBy(['name' => $integration->getName()]);
|
||||
\assert($integrationConfig instanceof Integration);
|
||||
Assert::assertSame('messaging_sid', $integrationConfig->getFeatureSettings()['messaging_service_sid']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\SmsBundle\Tests\Integration\Twilio;
|
||||
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\PluginBundle\Helper\IntegrationHelper;
|
||||
use Mautic\SmsBundle\Integration\Twilio\Configuration;
|
||||
use Mautic\SmsBundle\Integration\Twilio\TwilioTransport;
|
||||
use Monolog\Logger;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class TwilioTransportTest extends TestCase
|
||||
{
|
||||
private TwilioTransport $twilioTransport;
|
||||
|
||||
/**
|
||||
* @var MockObject&Logger
|
||||
*/
|
||||
private MockObject $logger;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->logger = $this->createMock(Logger::class);
|
||||
$integrationHelper = $this->createMock(IntegrationHelper::class);
|
||||
$configuration = new Configuration($integrationHelper);
|
||||
|
||||
$this->twilioTransport = new TwilioTransport($configuration, $this->logger);
|
||||
}
|
||||
|
||||
public function testSendSMS(): void
|
||||
{
|
||||
$lead = new Lead();
|
||||
$lead->setMobile('123456');
|
||||
$this->logger->expects($this->once())
|
||||
->method('warning')
|
||||
->with('mautic.sms.transport.twilio.not_configured');
|
||||
|
||||
$this->twilioTransport->sendSms($lead, 'some_content');
|
||||
}
|
||||
|
||||
public function testCreatePayload(): void
|
||||
{
|
||||
$reflection = new \ReflectionClass($this->twilioTransport::class);
|
||||
$method = $reflection->getMethod('createPayload');
|
||||
$method->setAccessible(true);
|
||||
|
||||
$payload = ['messagingServiceSid' => 'MS1234', 'body' => 'some_content'];
|
||||
|
||||
$result = $method->invokeArgs($this->twilioTransport, array_values($payload));
|
||||
Assert::assertSame($payload, $result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\SmsBundle\Tests\Model;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Mautic\ChannelBundle\Model\MessageQueueModel;
|
||||
use Mautic\CoreBundle\Helper\CacheStorageHelper;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Mautic\PageBundle\Model\TrackableModel;
|
||||
use Mautic\SmsBundle\Entity\Sms;
|
||||
use Mautic\SmsBundle\Entity\SmsRepository;
|
||||
use Mautic\SmsBundle\Form\Type\SmsType;
|
||||
use Mautic\SmsBundle\Model\SmsModel;
|
||||
use Mautic\SmsBundle\Sms\TransportChain;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
class SmsModelTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @var MockObject|CacheStorageHelper
|
||||
*/
|
||||
private MockObject $cacheStorageHelper;
|
||||
|
||||
/**
|
||||
* @var MockObject|EntityManager
|
||||
*/
|
||||
private MockObject $entityManger;
|
||||
|
||||
/**
|
||||
* @var MockObject|LeadModel
|
||||
*/
|
||||
private MockObject $leadModel;
|
||||
|
||||
/**
|
||||
* @var MockObject|MessageQueueModel
|
||||
*/
|
||||
private MockObject $messageQueueModel;
|
||||
|
||||
/**
|
||||
* @var MockObject|TrackableModel
|
||||
*/
|
||||
private MockObject $pageTrackableModel;
|
||||
|
||||
/**
|
||||
* @var MockObject|TransportChain
|
||||
*/
|
||||
private MockObject $transport;
|
||||
|
||||
/**
|
||||
* @var MockObject&CorePermissions
|
||||
*/
|
||||
private MockObject $security;
|
||||
|
||||
private SmsModel $smsModel;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->pageTrackableModel = $this->createMock(TrackableModel::class);
|
||||
$this->leadModel = $this->createMock(LeadModel::class);
|
||||
$this->messageQueueModel = $this->createMock(MessageQueueModel::class);
|
||||
$this->transport = $this->createMock(TransportChain::class);
|
||||
$this->cacheStorageHelper = $this->createMock(CacheStorageHelper::class);
|
||||
$this->entityManger = $this->createMock(EntityManager::class);
|
||||
$this->security = $this->createMock(CorePermissions::class);
|
||||
$this->smsModel = new SmsModel(
|
||||
$this->pageTrackableModel,
|
||||
$this->leadModel,
|
||||
$this->messageQueueModel,
|
||||
$this->transport,
|
||||
$this->cacheStorageHelper,
|
||||
$this->entityManger,
|
||||
$this->security,
|
||||
$this->createMock(EventDispatcherInterface::class),
|
||||
$this->createMock(UrlGeneratorInterface::class),
|
||||
$this->createMock(Translator::class),
|
||||
$this->createMock(UserHelper::class),
|
||||
$this->createMock(LoggerInterface::class),
|
||||
$this->createMock(CoreParametersHelper::class)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test to get lookup results when class name is sent as a parameter.
|
||||
*/
|
||||
public function testGetLookupResultsWhenTypeIsClass(): void
|
||||
{
|
||||
$entities = [['name' => 'Mautic', 'id' => 1, 'language' => 'cs']];
|
||||
|
||||
/** @var MockObject|SmsRepository $repositoryMock */
|
||||
$repositoryMock = $this->createMock(SmsRepository::class);
|
||||
$repositoryMock->method('getSmsList')
|
||||
->with('', 10, 0, true, false)
|
||||
->willReturn($entities);
|
||||
|
||||
$this->entityManger->method('getRepository')
|
||||
->with(Sms::class)
|
||||
->willReturn($repositoryMock);
|
||||
|
||||
$this->security->method('isGranted')
|
||||
->with('sms:smses:viewother')
|
||||
->willReturn(true);
|
||||
|
||||
$textMessages = $this->smsModel->getLookupResults(SmsType::class);
|
||||
$this->assertSame('Mautic', $textMessages['cs'][1], 'Mautic is the right text message name');
|
||||
}
|
||||
|
||||
public function testSendSmsNotPublished(): void
|
||||
{
|
||||
$sms = new Sms();
|
||||
$sms->setIsPublished(false);
|
||||
$lead = new Lead();
|
||||
$lead->setId(1);
|
||||
$results = $this->smsModel->sendSms($sms, $lead);
|
||||
self::assertFalse((bool) $results[1]['sent']);
|
||||
self::assertSame('mautic.sms.campaign.failed.unpublished', $results[1]['status']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\SmsBundle\Tests\Sms;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\SmsBundle\Integration\Twilio\TwilioTransport;
|
||||
use Mautic\SmsBundle\Sms\TransportChain;
|
||||
use Mautic\SmsBundle\Sms\TransportInterface;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
|
||||
class TransportChainTest extends MauticMysqlTestCase
|
||||
{
|
||||
private TransportChain $transportChain;
|
||||
|
||||
/**
|
||||
* @var TransportInterface|MockObject
|
||||
*/
|
||||
private MockObject $twilioTransport;
|
||||
|
||||
/**
|
||||
* Call protected/private method of a class.
|
||||
*
|
||||
* @param object &$object Instantiated object that we will run method on
|
||||
* @param string $methodName Method name to call
|
||||
* @param array $parameters array of parameters to pass into method
|
||||
*
|
||||
* @return mixed method return
|
||||
*
|
||||
* @throws \ReflectionException
|
||||
*/
|
||||
public function invokeMethod(&$object, $methodName, array $parameters = [])
|
||||
{
|
||||
$reflection = new \ReflectionClass($object::class);
|
||||
$method = $reflection->getMethod($methodName);
|
||||
$method->setAccessible(true);
|
||||
|
||||
return $method->invokeArgs($object, $parameters);
|
||||
}
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->transportChain = new TransportChain(
|
||||
'mautic.test.twilio.mock',
|
||||
static::getContainer()->get('mautic.helper.integration')
|
||||
);
|
||||
|
||||
$this->twilioTransport = $this->createMock(TwilioTransport::class);
|
||||
|
||||
$this->twilioTransport
|
||||
->method('sendSMS')
|
||||
->willReturn('lol');
|
||||
}
|
||||
|
||||
public function testAddTransport(): void
|
||||
{
|
||||
$count = count($this->transportChain->getTransports());
|
||||
|
||||
$this->transportChain->addTransport('mautic.transport.test', static::getContainer()->get('mautic.sms.twilio.transport'), 'mautic.transport.test', 'Twilio');
|
||||
|
||||
$this->assertCount($count + 1, $this->transportChain->getTransports());
|
||||
}
|
||||
|
||||
public function testSendSms(): void
|
||||
{
|
||||
$this->testAddTransport();
|
||||
|
||||
$this->transportChain->addTransport('mautic.test.twilio.mock', $this->twilioTransport, 'mautic.test.twilio.mock', 'Twilio');
|
||||
|
||||
$lead = new Lead();
|
||||
$lead->setMobile('+123456789');
|
||||
|
||||
try {
|
||||
$this->transportChain->sendSms($lead, 'Yeah');
|
||||
} catch (\Exception $e) {
|
||||
$message = $e->getMessage();
|
||||
$this->assertEquals('Primary SMS transport is not enabled', $message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\SmsBundle\Tests;
|
||||
|
||||
use Mautic\SmsBundle\Integration\TwilioIntegration;
|
||||
use Mautic\SmsBundle\Sms\TransportChain;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
trait SmsTestHelperTrait
|
||||
{
|
||||
private function configureTwilioWithArrayTransport(): ArrayTransport
|
||||
{
|
||||
$this->testSymfonyCommand('mautic:plugins:install');
|
||||
$messagingServiceSid = 'messaging_sid';
|
||||
|
||||
$integration = $this->getContainer()->get('mautic.integration.twilio');
|
||||
\assert($integration instanceof TwilioIntegration);
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, 's/plugins/config/'.$integration->getName());
|
||||
$response = $this->client->getResponse();
|
||||
|
||||
Assert::assertSame(Response::HTTP_OK, $response->getStatusCode(), $response->getContent());
|
||||
|
||||
$saveButton = $crawler->selectButton('integration_details[buttons][save]');
|
||||
$form = $saveButton->form();
|
||||
|
||||
$form['integration_details[apiKeys][username]']->setValue('test_username');
|
||||
$form['integration_details[apiKeys][password]']->setValue('test_password');
|
||||
$form['integration_details[isPublished]']->setValue('1');
|
||||
$form['integration_details[featureSettings][messaging_service_sid]']->setValue($messagingServiceSid);
|
||||
|
||||
$this->client->submit($form);
|
||||
|
||||
$response = $this->client->getResponse();
|
||||
Assert::assertSame(Response::HTTP_OK, $response->getStatusCode(), $response->getContent());
|
||||
|
||||
$transportChain = $this->getContainer()->get('mautic.sms.transport_chain');
|
||||
\assert($transportChain instanceof TransportChain);
|
||||
|
||||
// Replaces Twilio transport with ArrayTransport
|
||||
$transport = new ArrayTransport();
|
||||
$transportChain->addTransport('mautic.sms.twilio.transport', $transport, 'Array SMS Transport', 'Twilio');
|
||||
|
||||
return $transport;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
mautic.sms.notice.batch_deleted="%count% text messages have been deleted!"
|
||||
mautic.sms.error.notfound="No SMS with id %id% was found!"
|
||||
@@ -0,0 +1,148 @@
|
||||
mautic.campaign.sms.reply="Sends a text message"
|
||||
mautic.campaign.sms.reply.tooltip="Contact sent or replied to a text message"
|
||||
mautic.campaign.sms.send_text_sms="Send text message"
|
||||
mautic.sms.timeline.event.custom_sms="Custom text message"
|
||||
mautic.campaign.sms.send_text_sms.tooltip="Sends a text/sms to the contact."
|
||||
mautic.lead.list.filter.dnc_bounced_sms="Bounced - SMS"
|
||||
mautic.lead.list.filter.dnc_unsubscribed_sms="Unsubscribed - SMS"
|
||||
mautic.lead.timeline.sms.sms.reply="Text message received from contact"
|
||||
mautic.sms.timeline.reply="Text message received"
|
||||
|
||||
mautic.sms.text="Text Message Content"
|
||||
mautic.sms.placeholder="Message content here..."
|
||||
mautic.config.tab.smsconfig="Text Message Settings"
|
||||
mautic.core.config.header.smsconfig.description="Configure SMS transport settings and tracking options for text message campaigns."
|
||||
mautic.sms.config.form.sms.enabled="Text Message Enabled?"
|
||||
mautic.sms.config.form.sms.enabled.tooltip="Enable Text Message support in your campaigns?"
|
||||
mautic.sms.config.form.sms.username="Account SID"
|
||||
mautic.sms.config.form.sms.username.tooltip="Twilio Account SID"
|
||||
mautic.sms.config.form.sms.password="Auth Token"
|
||||
mautic.sms.config.form.sms.password.tooltip="Twilio Auth Token"
|
||||
mautic.sms.config.form.sms.messaging_service_sid="Messaging Service SID"
|
||||
mautic.sms.config.form.sms.messaging_service_sid.tooltip="The messaging service SID given by your provider that you use to send and receive Text Message messages."
|
||||
mautic.sms.config.form.sms.disable_trackable_urls="Disable click tracking"
|
||||
mautic.sms.config.form.sms.disable_trackable_urls.tooltip="This option will disable click tracking for URLs in the text message."
|
||||
|
||||
mautic.protip.sms.concise="Keep messages concise and action-oriented to respect the personal nature of SMS"
|
||||
mautic.protip.sms.timesensitive="Use SMS for time-sensitive information like appointment reminders or limited-time offers"
|
||||
mautic.protip.sms.compliance="Comply with regulations by including opt-out instructions"
|
||||
|
||||
mautic.sms.sms="Text Message"
|
||||
mautic.sms.smses="Text Messages"
|
||||
mautic.sms.campaign.send_sms="Send Push Text Message"
|
||||
mautic.sms.campaign.send_sms.tooltip="Sends a push sms to the user."
|
||||
mautic.sms.choose.smss="Select a text message to send."
|
||||
|
||||
mautic.sms.form.action.sendsms.admin="Send sms to user"
|
||||
mautic.sms.form.action.sendsms.admin.descr="Send the selected sms to the selected user(s) upon form submission."
|
||||
mautic.sms.form.action.sendsms.lead="Send sms to contact"
|
||||
mautic.sms.form.action.sendsms.lead.descr="Send the selected sms to the contact upon form submission."
|
||||
mautic.sms.form.body="Body"
|
||||
mautic.sms.form.confirmbatchdelete="Delete the selected smses?"
|
||||
mautic.sms.form.confirmdelete="Delete the sms, %name%?"
|
||||
mautic.sms.form.confirmsend="Queue, %name%, for sending?"
|
||||
mautic.sms.form.internal.name="Internal Name"
|
||||
mautic.sms.form.list="Contact list"
|
||||
mautic.sms.form.nbcharacter.counter="Character count: "
|
||||
mautic.sms.form.nbcharacter.infobox="One SMS is built with 160 characters. Most of the time, your SMS router will charge you 2 or more SMS if you go over this amount of characters."
|
||||
|
||||
mautic.sms.header.edit="Edit Text Message - %name%"
|
||||
mautic.sms.header.new="New Text Message"
|
||||
mautic.sms.contentblock.heading="Reach contacts via instant messaging"
|
||||
mautic.sms.contentblock.subheading="Direct, high-impact communication sent via SMS to contacts' mobile numbers."
|
||||
mautic.sms.contentblock.copy="Ideal for time-sensitive alerts, quick confirmations, and concise promotions, SMS marketing offers high open rates."
|
||||
|
||||
mautic.sms.text.tooltip="Your sms content"
|
||||
mautic.sms.headings="Text Message Title"
|
||||
mautic.sms.headings.tooltip="Your sms title"
|
||||
mautic.sms.link="Link"
|
||||
mautic.sms.link.placeholder="http://"
|
||||
mautic.sms.link.tooltip="When the user clicks the sms, where do you want to send them?"
|
||||
|
||||
mautic.sms.reply_pattern="Pattern the reply should match"
|
||||
mautic.sms.reply_pattern.tooltip="Type the pattern the reply must match in order for this decision to be acknowledged. Leave blank to match any message. You can use an asterisk * for wildcard string matching or ? for a single character. To match the asterisks or question marks themselves, surround them in bars like [?]."
|
||||
|
||||
mautic.sms.stats="Text Message Stats"
|
||||
mautic.sms.stats.report.table="Text Messages Sent"
|
||||
mautic.sms.stat.leadcount="%count% Pending"
|
||||
mautic.sms.stat.readcount="%count% Read"
|
||||
mautic.sms.stat.sentcount="%count% Sent"
|
||||
|
||||
mautic.sms.type.header="What type of SMS do you want to create?"
|
||||
mautic.sms.type.list="Segment Text Messages"
|
||||
mautic.sms.type.list.tag="Independent send"
|
||||
mautic.sms.type.list.differences.1st="For marketing messages, alerts, updates etc."
|
||||
mautic.sms.type.list.differences.2nd="Allows manual sending to selected segments"
|
||||
mautic.sms.type.list.differences.3rd="Designed for marketing/broadcast use"
|
||||
mautic.sms.type.list.differences.4th="Used for targeted mass SMS sending"
|
||||
mautic.sms.type.list.differences.5th="Non-editable once sent"
|
||||
mautic.sms.type.list.header="Segment text message"
|
||||
mautic.sms.icon_tooltip.list_sms="Is a segment (broadcast) SMS"
|
||||
mautic.sms.type.list.description="A segment SMS can be manually sent to selected contact segments. Once the SMS has been sent, it cannot be edited. However, it can be sent to new contacts as they are added to the associated segments."
|
||||
mautic.sms.type.template="Triggered Text Messages"
|
||||
mautic.sms.type.template.header="Triggered text message"
|
||||
mautic.sms.type.template.tag="Trigger required"
|
||||
mautic.sms.type.template.differences.1st="For campaigns, forms, point events etc."
|
||||
mautic.sms.type.template.differences.2nd="Allows sending multiple times (based on triggers)"
|
||||
mautic.sms.type.template.differences.3rd="Suited for transactional/notification use"
|
||||
mautic.sms.type.template.differences.4th="Based on users' specific actions or events"
|
||||
mautic.sms.type.template.differences.5th="Template can be edited"
|
||||
mautic.sms.type.template.description="A triggered SMS is automatically sent by campaigns, forms, point events, etc. These can be edited but cannot be sent to a contact list."
|
||||
|
||||
mautic.sms.form.internal.description="Description"
|
||||
mautic.sms.form.heading="Heading"
|
||||
mautic.sms.form.url="Link"
|
||||
mautic.sms.form.url.tooltip="The destination the user is sent to when they click the SMS."
|
||||
mautic.sms.form.message="Text Message"
|
||||
|
||||
mautic.sms.send.selectsmses="Select Text Message"
|
||||
mautic.sms.choose.smses="Select the SMS to send to the user."
|
||||
mautic.sms.send.new.sms="New Text Message"
|
||||
mautic.sms.send.edit.sms="Edit Text Message"
|
||||
mautic.sms.send.preview.sms="Preview Text Message"
|
||||
|
||||
mautic.sms.send.selectsmss="Select Message"
|
||||
mautic.sms.no.smses.note="Please create a new Text Message message to select."
|
||||
|
||||
mautic.sms.smses.header.preview="Preview Text Message"
|
||||
mautic.sms.preview="Preview"
|
||||
|
||||
mautic.sms.timeline.status="Status"
|
||||
mautic.sms.timeline.type="Type"
|
||||
mautic.sms.timeline.status.delivered="Delivered"
|
||||
mautic.sms.timeline.status.sent="Sent"
|
||||
mautic.sms.timeline.status.failed="Failed"
|
||||
mautic.sms.timeline.status.scheduled="Scheduled"
|
||||
mautic.sms.timeline.content.heading="Message Content"
|
||||
mautic.sms.timeline.event.list="Text message was sent to this contact because they belong to the '%list%' segment."
|
||||
mautic.sms.timeline.event.failed="Sending text message failed"
|
||||
|
||||
mautic.sms.disabled="Text Messages are currently disabled"
|
||||
mautic.sms.enable.in.configuration="Enable and configure Text Messages in the Mautic configuration."
|
||||
mautic.sms.create.in.campaign.builder="Seems there are none! Try changing a filter (if applicable) or create a new one in the Campaign Builder."
|
||||
|
||||
mautic.sms.campaign.failed.not_contactable="Contact is not contactable on the SMS channel."
|
||||
mautic.sms.campaign.failed.missing_number="Missing phone number for contact."
|
||||
mautic.sms.campaign.failed.missing_entity="The specified SMS entity does not exist."
|
||||
mautic.sms.campaign.failed.unpublished="The specified SMS was unpublished."
|
||||
mautic.sms.show.total.sent="Total sent"
|
||||
mautic.sms.show.failed="Failed"
|
||||
mautic.sms.permissions.header="Text Message Permissions"
|
||||
mautic.sms.permissions.smses="Text Messages - User has access to"
|
||||
mautic.sms.list.frequency.number="Do not send text message more than"
|
||||
|
||||
mautic.sms.config.select_default_transport="Select default transport to use"
|
||||
mautic.sms.config.no_transport="No transport enabled"
|
||||
mautic.sms.config.choose_transport="Select transport"
|
||||
mautic.sms.config.smsconfig="Please, install messaging transport first."
|
||||
|
||||
mautic.sms.transport.twilio="Twilio"
|
||||
mautic.sms.transport.twilio.not_configured="Twilio is not configured."
|
||||
|
||||
mautic.sms.webhook.event.send="Text Send Event"
|
||||
mautic.sms.token.dropdown.hint="Hint: In text message area, entering { shows a token dropdown."
|
||||
|
||||
mautic.sms.smses.header="Text Messages"
|
||||
mautic.sms.stat.sentcount.label="Sent"
|
||||
mautic.sms.preview.demo.message="Got it! Thanks for keeping me in the loop."
|
||||
mautic.sms.preview.input.placeholder="Message"
|
||||
@@ -0,0 +1 @@
|
||||
mautic.sms.choosesms.notblank="Please select a sms"
|
||||
Reference in New Issue
Block a user