src/Products/NotificationsBundle/Service/MessageLogic.php line 100

Open in your IDE?
  1. <?php
  2. namespace Products\NotificationsBundle\Service;
  3. use App\Service\Vendors\Slack\Slacker;
  4. use App\Util\Locales;
  5. use Cms\CoreBundle\Service\LocaleManager;
  6. use Cms\CoreBundle\Util\Doctrine\EntityManager;
  7. use Cms\Modules\AlertBundle\Model\Alert\AlertData;
  8. use DateTimeInterface;
  9. use Doctrine\ORM\NonUniqueResultException;
  10. use Products\NotificationsBundle\Doctrine\Repository\RecordingRepository;
  11. use Products\NotificationsBundle\Entity\AbstractContactAttempt;
  12. use Products\NotificationsBundle\Entity\AbstractNotification;
  13. use Products\NotificationsBundle\Entity\Job;
  14. use Products\NotificationsBundle\Entity\Notifications\Channels\ChannelsInterface;
  15. use Products\NotificationsBundle\Entity\Notifications\Channels\Service\FacebookChannelInterface;
  16. use Products\NotificationsBundle\Entity\Notifications\Channels\Service\InstagramChannelInterface;
  17. use Products\NotificationsBundle\Entity\Notifications\Channels\Service\TwitterChannelInterface;
  18. use Products\NotificationsBundle\Entity\Notifications\Channels\Service\WebsiteChannelInterface;
  19. use Products\NotificationsBundle\Entity\Notifications\Channels\Transactional\AppChannelInterface;
  20. use Products\NotificationsBundle\Entity\Notifications\Channels\Transactional\EmailChannelInterface;
  21. use Products\NotificationsBundle\Entity\Notifications\Channels\Transactional\SmsChannelInterface;
  22. use Products\NotificationsBundle\Entity\Notifications\Channels\Transactional\VoiceChannelInterface;
  23. use Products\NotificationsBundle\Entity\Notifications\ContentInterface;
  24. use Products\NotificationsBundle\Entity\Notifications\Invocation;
  25. use Products\NotificationsBundle\Entity\Notifications\ListInterface;
  26. use Products\NotificationsBundle\Entity\Notifications\Message;
  27. use Products\NotificationsBundle\Entity\Notifications\NotificationInterface;
  28. use Products\NotificationsBundle\Entity\Notifications\RecordingInterface;
  29. use Products\NotificationsBundle\Entity\Notifications\Translations\TranslatableInterface;
  30. use Products\NotificationsBundle\Entity\Notifications\Translations\Translation;
  31. use Products\NotificationsBundle\Entity\Recording;
  32. use Products\NotificationsBundle\Model\Testing\AbstractTester;
  33. use Products\NotificationsBundle\Model\Testing\NotificationsTest;
  34. use Products\NotificationsBundle\Util\MessageContentGenerator;
  35. use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
  36. use Symfony\Component\Routing\RouterInterface;
  37. use Twig\Environment;
  38. use Twig\Loader\ArrayLoader;
  39. /**
  40.  * Class MessageLogic
  41.  * @package Products\NotificationsBundle\Service
  42.  */
  43. final class MessageLogic
  44. {
  45.     protected EntityManager $em;
  46.     protected RouterInterface $router;
  47.     protected ListLogic $listLogic;
  48.     protected LocaleManager $lm;
  49.     protected Switchboard $switchboard;
  50.     protected Slacker $slacker;
  51.     protected ParameterBagInterface $params;
  52.     protected ?array $lastPrefillParams null;
  53.     /**
  54.      * @var Environment
  55.      */
  56.     protected Environment $twig;
  57.     /**
  58.      * @param EntityManager $em
  59.      * @param RouterInterface $router
  60.      * @param ListLogic $listLogic
  61.      * @param LocaleManager $lm
  62.      * @param Switchboard $switchboard
  63.      * @param Slacker $slacker
  64.      * @param ParameterBagInterface $params
  65.      */
  66.     public function __construct(
  67.         EntityManager $em,
  68.         RouterInterface $router,
  69.         ListLogic $listLogic,
  70.         LocaleManager $lm,
  71.         Switchboard $switchboard,
  72.         Slacker $slacker,
  73.         ParameterBagInterface $params,
  74.     )
  75.     {
  76.         $this->em $em;
  77.         $this->router $router;
  78.         $this->listLogic $listLogic;
  79.         $this->lm $lm;
  80.         $this->twig = new Environment(new ArrayLoader(), [
  81.             'strict_variables' => true,
  82.             'optimizations' => 0,
  83.         ]);
  84.         $this->switchboard $switchboard;
  85.         $this->slacker $slacker;
  86.         $this->params $params;
  87.     }
  88.     /**
  89.      * @param AbstractNotification $message
  90.      * @param object|null $starter
  91.      * @param array|null $prefillParams
  92.      * @return AbstractNotification|Message|Invocation
  93.      */
  94.     public function init(
  95.         AbstractNotification $message,
  96.         ?object $starter null,
  97.         ?array $prefillParams null
  98.     ): AbstractNotification {
  99.         $message->setLocale(Locales::RFC4646_DEFAULT);
  100.         return ($starter) ? $this->prefill($message$starter$prefillParams) : $message;
  101.     }
  102.     /**
  103.      * @param AbstractNotification $message
  104.      * @param object $starter
  105.      * @param array|null $overridePrefillParams
  106.      * @return AbstractNotification
  107.      */
  108.     public function prefill(AbstractNotification $messageobject $starter, ?array $overridePrefillParams null): AbstractNotification
  109.     {
  110.         $generator = (new MessageContentGenerator())
  111.             // if we have an invocation, we don't want to merge the content as that needs done per individual message...
  112.             ->togglePassthrough($message instanceof Invocation)
  113.             ->setTimezone($starter->getTenant()->getLocale()->getTimezone())
  114.             ->setLocale($starter->getLocale())
  115.             ->setTiming();
  116.         $this->lastPrefillParams = !empty($overridePrefillParams) ? $overridePrefillParams $generator->getParams();
  117.         if ($starter instanceof NotificationInterface) {
  118.             $message->setUrgent($starter->isUrgent());
  119.         }
  120.         if ($message instanceof ListInterface && $starter instanceof ListInterface) {
  121.             $message->setLists($starter->getLists());
  122.         }
  123.         if ($starter instanceof ChannelsInterface) {
  124.             $message->setStarterChannels($starter->getChannels());
  125.         }
  126.         if ($message instanceof ChannelsInterface && $starter instanceof ChannelsInterface) {
  127.             $message->setChannels($starter->getChannels());
  128.         }
  129.         if ($message instanceof ContentInterface && $starter instanceof ContentInterface) {
  130.             $message
  131.                 ->setTitle(
  132.                     $generator->render($starter->getTitle(), $overridePrefillParams) ?: null,
  133.                 )
  134.                 ->setDescription(
  135.                     $generator->render($starter->getDescription(), $overridePrefillParams) ?: null,
  136.                 )
  137.                 ->setScript(
  138.                     $generator->render($starter->getScript(), $overridePrefillParams) ?: null,
  139.                 )
  140.                 ->setHtml($starter->isHtml())
  141.                 ->setMedia($starter->getMedia());
  142.         }
  143.         if ($message instanceof AppChannelInterface && $starter instanceof AppChannelInterface) {
  144.             // NOOP
  145.         }
  146.         if ($message instanceof EmailChannelInterface && $starter instanceof EmailChannelInterface) {
  147.             $message
  148.                 ->setEmailFrom($starter->getEmailFrom())
  149.                 ->setEmailName($starter->getEmailName());
  150.         }
  151.         if ($message instanceof SmsChannelInterface && $starter instanceof SmsChannelInterface) {
  152.             // NOOP
  153.         }
  154.         if ($message instanceof VoiceChannelInterface && $starter instanceof VoiceChannelInterface) {
  155.             $message->setVoiceCallerId($starter->getVoiceCallerId());
  156.         }
  157.         if ($message instanceof FacebookChannelInterface && $starter instanceof FacebookChannelInterface) {
  158.             $message
  159.                 ->setFacebookSocialAccounts($starter->getFacebookSocialAccounts())
  160.                 ->setFacebookNote($starter->getFacebookNote());
  161.         }
  162.         if ($message instanceof InstagramChannelInterface && $starter instanceof InstagramChannelInterface) {
  163.             $message->setInstagramSocialAccounts($starter->getInstagramSocialAccounts());
  164.         }
  165.         if ($message instanceof TwitterChannelInterface && $starter instanceof TwitterChannelInterface) {
  166.             $message
  167.                 ->setTwitterSocialAccounts($starter->getTwitterSocialAccounts())
  168.                 ->setTwitterNote($starter->getTwitterNote());
  169.         }
  170.         if ($message instanceof WebsiteChannelInterface && $starter instanceof WebsiteChannelInterface) {
  171.             $message
  172.                 ->setWebsiteDepartments($starter->getWebsiteDepartments())
  173.                 ->setWebsiteLevel($starter->getWebsiteLevel())
  174.                 ->setWebsiteBehavior($starter->getWebsiteBehavior())
  175.                 ->setWebsiteEndDateTime($starter->getWebsiteEndDateTime());
  176.         }
  177.         if ($message instanceof RecordingInterface && $starter instanceof RecordingInterface) {
  178.             $message->setRecording($starter->getRecording());
  179.             if ($starter->getRecording() && $starter->getRecording()->isMethod(Recording::METHODS__SPEECH)) {
  180.                 $merged $generator->render($starter->getRecording()->getSpeechText(), $overridePrefillParams) ?: null;
  181.                 if ($starter->getRecording()->getSpeechText() !== $merged) {
  182.                     $message->setRecording(
  183.                         (new Recording())
  184.                             ->setMethod($starter->getRecording()->getMethod())
  185.                             ->setSpeechText($merged)
  186.                     );
  187.                 }
  188.             }
  189.         }
  190.         // if the starter has any translations, copy them over
  191.         if ($message instanceof TranslatableInterface && $starter instanceof TranslatableInterface) {
  192.             foreach ($starter->getTranslations() as $translation) {
  193.                 // we really only care about non-automation translations
  194.                 // if they were automatic once, then they can be automatically generated again
  195.                 if ($translation->hasFlag(Translation::FLAGS__MANUAL)) {
  196.                     // need to make a new copy of the translation
  197.                     // also mark it has having come from a starter
  198.                     $translation $translation->fork()
  199.                         ->markFlag(Translation::FLAGS__STARTER)
  200.                     ;
  201.                     $message->addTranslation($translation);
  202.                     $this->em->persist($translation);
  203.                 }
  204.             }
  205.         }
  206.         return $message;
  207.     }
  208.     /**
  209.      * @param AbstractNotification $message
  210.      * @return AbstractNotification
  211.      */
  212.     public function create(AbstractNotification $message): AbstractNotification
  213.     {
  214.         return $this->update($message);
  215.     }
  216.     /**
  217.      * @param AbstractNotification $message
  218.      * @return AbstractNotification
  219.      */
  220.     public function update(AbstractNotification $message): AbstractNotification
  221.     {
  222.         // if we don't have the website channel set, unset all of it's stuff
  223.         if ( ! $message->hasWebsiteChannel()) {
  224.             $message
  225.                 ->setWebsiteEndDateTime(null)
  226.                 ->setWebsiteLevel($message->isUrgent() ? AlertData::LEVELS__URGENT AlertData::LEVELS__INFORMATIVE)
  227.                 ->setWebsiteBehavior($message->isUrgent() ? AlertData::BEHAVIORS__POPUP AlertData::BEHAVIORS__NONE);
  228.         }
  229.         // if the message doesn't have a voice channel, that stuff needs wiped
  230.         if ( ! $message->isUrgent()) {
  231.             $message
  232.                 ->setScript(null)
  233.                 ->setRecording(null)
  234.                 ->removeChannel(ChannelsInterface::CHANNELS__VOICE);
  235.         }
  236.         // if there is no recording, make sure the voice channel is turned off
  237.         // TODO: is this needed any more with recent logic changes?
  238.         if ( ! $message->getRecording()) {
  239.             $message->removeChannel(ChannelsInterface::CHANNELS__VOICE);
  240.         }
  241.         // save to database
  242.         $this->em->save($message);
  243.         return $message;
  244.     }
  245.     /**
  246.      * @param AbstractNotification $message
  247.      * @return int
  248.      */
  249.     public function delete(AbstractNotification $message): int
  250.     {
  251.         return $this->em->transactional(
  252.             function (EntityManager $em) use ($message) {
  253.                 $id $message->getId();
  254.                 // TODO: handle with fk constraints
  255.                 $em->getRepository(AbstractContactAttempt::class)->deleteByMessage($message);
  256.                 $em->delete($message);
  257.                 return $id;
  258.             }
  259.         );
  260.     }
  261.     /**
  262.      * @param Message $message
  263.      * @param DateTimeInterface|null $timestamp
  264.      * @return Job|null
  265.      */
  266.     public function schedule(Message $message, ?DateTimeInterface $timestamp): ?Job
  267.     {
  268.         // if we have a timestamp, we just need to flip some settings
  269.         if ($timestamp) {
  270.             // set status to ready and set the scheduled timestamp
  271.             $this->em->save(
  272.                 $message
  273.                     ->setStatus(AbstractNotification::STATUSES__READY)
  274.                     ->setScheduledAt($timestamp),
  275.             );
  276.             // no job was created
  277.             return null;
  278.         }
  279.         // since we are not scheduling, go ahead and broadcast the message
  280.         return $this->broadcast($message);
  281.     }
  282.     /**
  283.      * @param Message $message
  284.      * @return bool
  285.      */
  286.     public function unschedule(Message $message): bool
  287.     {
  288.         // message must be in a scheduled state
  289.         if ($message->isScheduled()) {
  290.             // set status back to draft and clear the scheduled timestamp
  291.             $this->em->save(
  292.                 $message
  293.                     ->setStatus(AbstractNotification::STATUSES__DRAFT)
  294.                     ->setScheduledAt(null),
  295.             );
  296.             // we did actually unschedule
  297.             return true;
  298.         }
  299.         // was unable to unschedule for some reason, so no action was taken
  300.         return false;
  301.     }
  302.     /**
  303.      * @param AbstractNotification $notification
  304.      * @param bool $force
  305.      * @return Job|null
  306.      */
  307.     public function broadcast(AbstractNotification $notificationbool $force false): ?Job
  308.     {
  309.         // update the message
  310.         $this->em->save(
  311.             $notification->setStatus(
  312.                 AbstractNotification::STATUSES__READY,
  313.             ),
  314.         );
  315.         // run the job immediately if it is not scheduled
  316.         // otherwise, the lambda will have to kick in to send the notification out
  317.         $job null;
  318.         if ($force || ($notification->isReady() && ! $notification->isScheduled())) {
  319.             $job $this->switchboard->start($notification);
  320.         }
  321.         // return the job in case we are interested
  322.         return $job;
  323.     }
  324.     /**
  325.      * @param AbstractNotification $message
  326.      * @param array|AbstractTester[] $contacts
  327.      * @return NotificationsTest
  328.      */
  329.     public function test(AbstractNotification $message, array $contacts = []): NotificationsTest
  330.     {
  331.         return $this->switchboard->test(new NotificationsTest(
  332.             $message,
  333.             $contacts
  334.         ));
  335.     }
  336.     /**
  337.      * @param AbstractNotification $message
  338.      * @return bool
  339.      * @throws NonUniqueResultException
  340.      */
  341.     public function isTranslatableFieldsModified(AbstractNotification $message): bool
  342.     {
  343.         return (
  344.             $this->isPropertyModified('title'$message) ||
  345.             $this->isPropertyModified('description'$message) ||
  346.             $this->isPropertyModified('script'$message) ||
  347.             $this->isRecordingModified($message)
  348.         );
  349.     }
  350.     /**
  351.      * @param AbstractNotification $message
  352.      * @return bool
  353.      * @throws NonUniqueResultException
  354.      */
  355.     public function isRecordingModified(AbstractNotification $message): bool
  356.     {
  357.         /** @var RecordingRepository $recordingRepository */
  358.         $recordingRepository $this->em->getRepository(Recording::class);
  359.         $existingRecording $recordingRepository->findOneByMessage($message);
  360.         $isRecordingModified false;
  361.         if (($message->getRecording() instanceof Recording) && !empty($message->getRecording()->getId())) {
  362.             $isRecordingModified = ( !($existingRecording instanceof Recording) || ($existingRecording->getId() !== $message->getRecording()->getId()) );
  363.         }
  364.         return $isRecordingModified;
  365.     }
  366.     /**
  367.      * @param AbstractNotification $message
  368.      * @return string
  369.      */
  370.     public static function generateVisibilityKey(AbstractNotification $message): string
  371.     {
  372.         return hash('md5'"{$message->getId()}-{$message->getCreatedAt()->getTimestamp()}");
  373.     }
  374.     /**
  375.      * @param string $propertyName
  376.      * @param AbstractNotification $message
  377.      * @return bool
  378.      */
  379.     public function isPropertyModified(string $propertyNameAbstractNotification $message): bool
  380.     {
  381.         $originalMessage $this->em->getUnitOfWork()->getOriginalEntityData($message);
  382.         switch ($propertyName) {
  383.             case 'title':
  384.                 if (isset($originalMessage['title']) && $this->isPropertyValueModified($originalMessage['title'], $message->getTitle())) {
  385.                     return true;
  386.                 }
  387.                 break;
  388.             case 'description':
  389.                 if (isset($originalMessage['description']) && $this->isPropertyValueModified($originalMessage['description'], $message->getDescription())) {
  390.                     return true;
  391.                 }
  392.                 break;
  393.             case 'script':
  394.                 if (isset($originalMessage['script']) && $this->isPropertyValueModified($originalMessage['script'], $message->getScript())) {
  395.                     return true;
  396.                 }
  397.                 break;
  398.             default:
  399.                 throw new \LogicException();
  400.         }
  401.         return false;
  402.     }
  403.     /**
  404.      * @return array|null
  405.      */
  406.     public function getLastPrefillParams(): ?array
  407.     {
  408.         return $this->lastPrefillParams;
  409.     }
  410.     /**
  411.      * @param string|null $originalValue
  412.      * @param string|null $currentValue
  413.      * @return bool
  414.      */
  415.     private function isPropertyValueModified(?string $originalValue, ?string $currentValue): bool
  416.     {
  417.         $strippedOriginalValue trim(preg_replace('/\s+/'' '$originalValue));
  418.         $strippedCurrentValue trim(preg_replace('/\s+/'' '$currentValue));
  419.         return ($strippedOriginalValue !== $strippedCurrentValue);
  420.     }
  421. }