<?php
namespace Products\NotificationsBundle\Service;
use App\Service\Vendors\Slack\Slacker;
use App\Util\Locales;
use Cms\CoreBundle\Service\LocaleManager;
use Cms\CoreBundle\Util\Doctrine\EntityManager;
use Cms\Modules\AlertBundle\Model\Alert\AlertData;
use DateTimeInterface;
use Doctrine\ORM\NonUniqueResultException;
use Products\NotificationsBundle\Doctrine\Repository\RecordingRepository;
use Products\NotificationsBundle\Entity\AbstractContactAttempt;
use Products\NotificationsBundle\Entity\AbstractNotification;
use Products\NotificationsBundle\Entity\Job;
use Products\NotificationsBundle\Entity\Notifications\Channels\ChannelsInterface;
use Products\NotificationsBundle\Entity\Notifications\Channels\Service\FacebookChannelInterface;
use Products\NotificationsBundle\Entity\Notifications\Channels\Service\InstagramChannelInterface;
use Products\NotificationsBundle\Entity\Notifications\Channels\Service\TwitterChannelInterface;
use Products\NotificationsBundle\Entity\Notifications\Channels\Service\WebsiteChannelInterface;
use Products\NotificationsBundle\Entity\Notifications\Channels\Transactional\AppChannelInterface;
use Products\NotificationsBundle\Entity\Notifications\Channels\Transactional\EmailChannelInterface;
use Products\NotificationsBundle\Entity\Notifications\Channels\Transactional\SmsChannelInterface;
use Products\NotificationsBundle\Entity\Notifications\Channels\Transactional\VoiceChannelInterface;
use Products\NotificationsBundle\Entity\Notifications\ContentInterface;
use Products\NotificationsBundle\Entity\Notifications\Invocation;
use Products\NotificationsBundle\Entity\Notifications\ListInterface;
use Products\NotificationsBundle\Entity\Notifications\Message;
use Products\NotificationsBundle\Entity\Notifications\NotificationInterface;
use Products\NotificationsBundle\Entity\Notifications\RecordingInterface;
use Products\NotificationsBundle\Entity\Notifications\Translations\TranslatableInterface;
use Products\NotificationsBundle\Entity\Notifications\Translations\Translation;
use Products\NotificationsBundle\Entity\Recording;
use Products\NotificationsBundle\Model\Testing\AbstractTester;
use Products\NotificationsBundle\Model\Testing\NotificationsTest;
use Products\NotificationsBundle\Util\MessageContentGenerator;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Routing\RouterInterface;
use Twig\Environment;
use Twig\Loader\ArrayLoader;
/**
* Class MessageLogic
* @package Products\NotificationsBundle\Service
*/
final class MessageLogic
{
protected EntityManager $em;
protected RouterInterface $router;
protected ListLogic $listLogic;
protected LocaleManager $lm;
protected Switchboard $switchboard;
protected Slacker $slacker;
protected ParameterBagInterface $params;
protected ?array $lastPrefillParams = null;
/**
* @var Environment
*/
protected Environment $twig;
/**
* @param EntityManager $em
* @param RouterInterface $router
* @param ListLogic $listLogic
* @param LocaleManager $lm
* @param Switchboard $switchboard
* @param Slacker $slacker
* @param ParameterBagInterface $params
*/
public function __construct(
EntityManager $em,
RouterInterface $router,
ListLogic $listLogic,
LocaleManager $lm,
Switchboard $switchboard,
Slacker $slacker,
ParameterBagInterface $params,
)
{
$this->em = $em;
$this->router = $router;
$this->listLogic = $listLogic;
$this->lm = $lm;
$this->twig = new Environment(new ArrayLoader(), [
'strict_variables' => true,
'optimizations' => 0,
]);
$this->switchboard = $switchboard;
$this->slacker = $slacker;
$this->params = $params;
}
/**
* @param AbstractNotification $message
* @param object|null $starter
* @param array|null $prefillParams
* @return AbstractNotification|Message|Invocation
*/
public function init(
AbstractNotification $message,
?object $starter = null,
?array $prefillParams = null
): AbstractNotification {
$message->setLocale(Locales::RFC4646_DEFAULT);
return ($starter) ? $this->prefill($message, $starter, $prefillParams) : $message;
}
/**
* @param AbstractNotification $message
* @param object $starter
* @param array|null $overridePrefillParams
* @return AbstractNotification
*/
public function prefill(AbstractNotification $message, object $starter, ?array $overridePrefillParams = null): AbstractNotification
{
$generator = (new MessageContentGenerator())
// if we have an invocation, we don't want to merge the content as that needs done per individual message...
->togglePassthrough($message instanceof Invocation)
->setTimezone($starter->getTenant()->getLocale()->getTimezone())
->setLocale($starter->getLocale())
->setTiming();
$this->lastPrefillParams = !empty($overridePrefillParams) ? $overridePrefillParams : $generator->getParams();
if ($starter instanceof NotificationInterface) {
$message->setUrgent($starter->isUrgent());
}
if ($message instanceof ListInterface && $starter instanceof ListInterface) {
$message->setLists($starter->getLists());
}
if ($starter instanceof ChannelsInterface) {
$message->setStarterChannels($starter->getChannels());
}
if ($message instanceof ChannelsInterface && $starter instanceof ChannelsInterface) {
$message->setChannels($starter->getChannels());
}
if ($message instanceof ContentInterface && $starter instanceof ContentInterface) {
$message
->setTitle(
$generator->render($starter->getTitle(), $overridePrefillParams) ?: null,
)
->setDescription(
$generator->render($starter->getDescription(), $overridePrefillParams) ?: null,
)
->setScript(
$generator->render($starter->getScript(), $overridePrefillParams) ?: null,
)
->setHtml($starter->isHtml())
->setMedia($starter->getMedia());
}
if ($message instanceof AppChannelInterface && $starter instanceof AppChannelInterface) {
// NOOP
}
if ($message instanceof EmailChannelInterface && $starter instanceof EmailChannelInterface) {
$message
->setEmailFrom($starter->getEmailFrom())
->setEmailName($starter->getEmailName());
}
if ($message instanceof SmsChannelInterface && $starter instanceof SmsChannelInterface) {
// NOOP
}
if ($message instanceof VoiceChannelInterface && $starter instanceof VoiceChannelInterface) {
$message->setVoiceCallerId($starter->getVoiceCallerId());
}
if ($message instanceof FacebookChannelInterface && $starter instanceof FacebookChannelInterface) {
$message
->setFacebookSocialAccounts($starter->getFacebookSocialAccounts())
->setFacebookNote($starter->getFacebookNote());
}
if ($message instanceof InstagramChannelInterface && $starter instanceof InstagramChannelInterface) {
$message->setInstagramSocialAccounts($starter->getInstagramSocialAccounts());
}
if ($message instanceof TwitterChannelInterface && $starter instanceof TwitterChannelInterface) {
$message
->setTwitterSocialAccounts($starter->getTwitterSocialAccounts())
->setTwitterNote($starter->getTwitterNote());
}
if ($message instanceof WebsiteChannelInterface && $starter instanceof WebsiteChannelInterface) {
$message
->setWebsiteDepartments($starter->getWebsiteDepartments())
->setWebsiteLevel($starter->getWebsiteLevel())
->setWebsiteBehavior($starter->getWebsiteBehavior())
->setWebsiteEndDateTime($starter->getWebsiteEndDateTime());
}
if ($message instanceof RecordingInterface && $starter instanceof RecordingInterface) {
$message->setRecording($starter->getRecording());
if ($starter->getRecording() && $starter->getRecording()->isMethod(Recording::METHODS__SPEECH)) {
$merged = $generator->render($starter->getRecording()->getSpeechText(), $overridePrefillParams) ?: null;
if ($starter->getRecording()->getSpeechText() !== $merged) {
$message->setRecording(
(new Recording())
->setMethod($starter->getRecording()->getMethod())
->setSpeechText($merged)
);
}
}
}
// if the starter has any translations, copy them over
if ($message instanceof TranslatableInterface && $starter instanceof TranslatableInterface) {
foreach ($starter->getTranslations() as $translation) {
// we really only care about non-automation translations
// if they were automatic once, then they can be automatically generated again
if ($translation->hasFlag(Translation::FLAGS__MANUAL)) {
// need to make a new copy of the translation
// also mark it has having come from a starter
$translation = $translation->fork()
->markFlag(Translation::FLAGS__STARTER)
;
$message->addTranslation($translation);
$this->em->persist($translation);
}
}
}
return $message;
}
/**
* @param AbstractNotification $message
* @return AbstractNotification
*/
public function create(AbstractNotification $message): AbstractNotification
{
return $this->update($message);
}
/**
* @param AbstractNotification $message
* @return AbstractNotification
*/
public function update(AbstractNotification $message): AbstractNotification
{
// if we don't have the website channel set, unset all of it's stuff
if ( ! $message->hasWebsiteChannel()) {
$message
->setWebsiteEndDateTime(null)
->setWebsiteLevel($message->isUrgent() ? AlertData::LEVELS__URGENT : AlertData::LEVELS__INFORMATIVE)
->setWebsiteBehavior($message->isUrgent() ? AlertData::BEHAVIORS__POPUP : AlertData::BEHAVIORS__NONE);
}
// if the message doesn't have a voice channel, that stuff needs wiped
if ( ! $message->isUrgent()) {
$message
->setScript(null)
->setRecording(null)
->removeChannel(ChannelsInterface::CHANNELS__VOICE);
}
// if there is no recording, make sure the voice channel is turned off
// TODO: is this needed any more with recent logic changes?
if ( ! $message->getRecording()) {
$message->removeChannel(ChannelsInterface::CHANNELS__VOICE);
}
// save to database
$this->em->save($message);
return $message;
}
/**
* @param AbstractNotification $message
* @return int
*/
public function delete(AbstractNotification $message): int
{
return $this->em->transactional(
function (EntityManager $em) use ($message) {
$id = $message->getId();
// TODO: handle with fk constraints
$em->getRepository(AbstractContactAttempt::class)->deleteByMessage($message);
$em->delete($message);
return $id;
}
);
}
/**
* @param Message $message
* @param DateTimeInterface|null $timestamp
* @return Job|null
*/
public function schedule(Message $message, ?DateTimeInterface $timestamp): ?Job
{
// if we have a timestamp, we just need to flip some settings
if ($timestamp) {
// set status to ready and set the scheduled timestamp
$this->em->save(
$message
->setStatus(AbstractNotification::STATUSES__READY)
->setScheduledAt($timestamp),
);
// no job was created
return null;
}
// since we are not scheduling, go ahead and broadcast the message
return $this->broadcast($message);
}
/**
* @param Message $message
* @return bool
*/
public function unschedule(Message $message): bool
{
// message must be in a scheduled state
if ($message->isScheduled()) {
// set status back to draft and clear the scheduled timestamp
$this->em->save(
$message
->setStatus(AbstractNotification::STATUSES__DRAFT)
->setScheduledAt(null),
);
// we did actually unschedule
return true;
}
// was unable to unschedule for some reason, so no action was taken
return false;
}
/**
* @param AbstractNotification $notification
* @param bool $force
* @return Job|null
*/
public function broadcast(AbstractNotification $notification, bool $force = false): ?Job
{
// update the message
$this->em->save(
$notification->setStatus(
AbstractNotification::STATUSES__READY,
),
);
// run the job immediately if it is not scheduled
// otherwise, the lambda will have to kick in to send the notification out
$job = null;
if ($force || ($notification->isReady() && ! $notification->isScheduled())) {
$job = $this->switchboard->start($notification);
}
// return the job in case we are interested
return $job;
}
/**
* @param AbstractNotification $message
* @param array|AbstractTester[] $contacts
* @return NotificationsTest
*/
public function test(AbstractNotification $message, array $contacts = []): NotificationsTest
{
return $this->switchboard->test(new NotificationsTest(
$message,
$contacts
));
}
/**
* @param AbstractNotification $message
* @return bool
* @throws NonUniqueResultException
*/
public function isTranslatableFieldsModified(AbstractNotification $message): bool
{
return (
$this->isPropertyModified('title', $message) ||
$this->isPropertyModified('description', $message) ||
$this->isPropertyModified('script', $message) ||
$this->isRecordingModified($message)
);
}
/**
* @param AbstractNotification $message
* @return bool
* @throws NonUniqueResultException
*/
public function isRecordingModified(AbstractNotification $message): bool
{
/** @var RecordingRepository $recordingRepository */
$recordingRepository = $this->em->getRepository(Recording::class);
$existingRecording = $recordingRepository->findOneByMessage($message);
$isRecordingModified = false;
if (($message->getRecording() instanceof Recording) && !empty($message->getRecording()->getId())) {
$isRecordingModified = ( !($existingRecording instanceof Recording) || ($existingRecording->getId() !== $message->getRecording()->getId()) );
}
return $isRecordingModified;
}
/**
* @param AbstractNotification $message
* @return string
*/
public static function generateVisibilityKey(AbstractNotification $message): string
{
return hash('md5', "{$message->getId()}-{$message->getCreatedAt()->getTimestamp()}");
}
/**
* @param string $propertyName
* @param AbstractNotification $message
* @return bool
*/
public function isPropertyModified(string $propertyName, AbstractNotification $message): bool
{
$originalMessage = $this->em->getUnitOfWork()->getOriginalEntityData($message);
switch ($propertyName) {
case 'title':
if (isset($originalMessage['title']) && $this->isPropertyValueModified($originalMessage['title'], $message->getTitle())) {
return true;
}
break;
case 'description':
if (isset($originalMessage['description']) && $this->isPropertyValueModified($originalMessage['description'], $message->getDescription())) {
return true;
}
break;
case 'script':
if (isset($originalMessage['script']) && $this->isPropertyValueModified($originalMessage['script'], $message->getScript())) {
return true;
}
break;
default:
throw new \LogicException();
}
return false;
}
/**
* @return array|null
*/
public function getLastPrefillParams(): ?array
{
return $this->lastPrefillParams;
}
/**
* @param string|null $originalValue
* @param string|null $currentValue
* @return bool
*/
private function isPropertyValueModified(?string $originalValue, ?string $currentValue): bool
{
$strippedOriginalValue = trim(preg_replace('/\s+/', ' ', $originalValue));
$strippedCurrentValue = trim(preg_replace('/\s+/', ' ', $currentValue));
return ($strippedOriginalValue !== $strippedCurrentValue);
}
}