Initial commit: CloudOps infrastructure platform
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user