Initial commit: CloudOps infrastructure platform

This commit is contained in:
root
2026-04-09 19:58:57 +02:00
commit 1166a52f26
7762 changed files with 839452 additions and 0 deletions

View File

@@ -0,0 +1,26 @@
# Workflow name:
name: Close Pull Requests
# Workflow triggers:
on:
pull_request_target:
types: [opened]
# Workflow jobs:
jobs:
run:
runs-on: ubuntu-latest
steps:
- uses: superbrothers/close-pull-request@v3
with:
comment: |
Thank you for submitting a pull request. :raised_hands:
We greatly appreciate your willingness to submit a contribution. However, we are not accepting pull requests against this repository, as all development happens on the [main project repository](https://github.com/mautic/mautic).
We kindly request that you submit this pull request against the [respective directory](https://github.com/mautic/mautic/blob/head/plugins/MauticSocialBundle) of the main repository where we'll review and provide feedback. If this is your first Mautic contribution, be sure to read the [contributing guide](https://github.com/mautic/mautic/blob/4.x/.github/CONTRIBUTING.md) which provides guidelines and instructions for submitting contributions.
Thank you again, and we look forward to receiving your contribution! :smiley:
Best,
The Mautic team

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -0,0 +1,122 @@
Mautic.getNetworkFormAction = function(networkType) {
// removes errors when network type properties has changed
if (networkType && mQuery(networkType).val() && mQuery(networkType).closest('.form-group').hasClass('has-error')) {
mQuery(networkType).closest('.form-group').removeClass('has-error');
if (mQuery(networkType).next().hasClass('help-block')) {
mQuery(networkType).next().remove();
}
}
Mautic.activateLabelLoadingIndicator('monitoring_networkType');
var query = "action=plugin:mauticSocial:getNetworkForm&networkType=" + mQuery(networkType).val();
mQuery.ajax({
url: mauticAjaxUrl,
type: "POST",
data: query,
dataType: "json",
success: function (response) {
if (typeof response.html != 'undefined') {
// pushes response into container element
mQuery('#properties-container').html(response.html);
// sends markup through core js parsers
if (response.html != '') {
Mautic.onPageLoad('#properties-container', response);
}
}
},
error: function (request, textStatus, errorThrown) {
Mautic.processAjaxError(request, textStatus, errorThrown);
},
complete: function() {
Mautic.removeLabelLoadingIndicator();
}
});
};
/*
* watches the compose field and updates various parts of the modal and text area
*/
Mautic.composeSocialWatcher = function() {
// the text area
var input = mQuery('textarea.tweet-message');
// on load
Mautic.updateCharacterCount();
// watch the text area keyup
input.on('keyup', function(){
Mautic.updateCharacterCount();
});
var pageId = mQuery('select.tweet-insert-page');
var assetId = mQuery('select.tweet-insert-asset');
var handle = mQuery('button.tweet-insert-handle');
pageId.on('change', function() {
Mautic.insertSocialLink(pageId.val(), 'pagelink', false);
});
assetId.on('change', function() {
Mautic.insertSocialLink(assetId.val(), 'assetlink', false);
});
handle.on('click', function() {
Mautic.insertSocialLink(false, 'twitter_handle', true);
});
};
/*
* gets the count of the text area and returns (140 - count)
*/
Mautic.getCharacterCount = function() {
var tweetLenght = 280;
var currentLength = mQuery('textarea#twitter_tweet_text');
return (tweetLenght - currentLength.val().length);
};
/*
* sets the content of the character count span
*/
Mautic.updateCharacterCount = function() {
var tweetCount = Mautic.getCharacterCount();
var countContainer = mQuery('#character-count span');
countContainer.text(tweetCount);
};
/*
* inserts a link placeholder into the text box.
*
* @id the id of the link placeholder
* @type the type of link to insert
* @skipId if the id is blank and this is true it'll still insert the link
*/
Mautic.insertSocialLink = function(id, type, skipId) {
// if there is no id and skipID is false then exit
if (! id && ! skipId) {
return;
}
// if we need to skip the id state just leave it out
if (skipId) {
var link = '{' + type + '}';
}
else {
var link = '{' + type + '=' + id + '}';
}
var textarea = mQuery('textarea.tweet-message');
var currentVal = textarea.val();
var newVal = (currentVal) ? currentVal + ' ' + link : link;
textarea.val(newVal);
Mautic.updateCharacterCount();
};

View File

@@ -0,0 +1,145 @@
<?php
namespace MauticPlugin\MauticSocialBundle\Command;
use MauticPlugin\MauticSocialBundle\Entity\MonitoringRepository;
use MauticPlugin\MauticSocialBundle\Model\MonitoringModel;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand(
name: 'mautic:social:monitoring',
description: 'Looks at the records of monitors and iterates through them.'
)]
class MauticSocialMonitoringCommand extends Command
{
public function __construct(
private MonitoringModel $monitoringModel,
) {
parent::__construct();
}
protected function configure()
{
$this
->addOption('mid', 'i', InputOption::VALUE_OPTIONAL, 'The id of a specific monitor record to process')
->addOption(
'batch-size',
null,
InputOption::VALUE_REQUIRED,
'The maximum number of iterations the cron runs per cycle. This value gets distributed by the number of monitor records published'
)
->addOption('query-count', null, InputOption::VALUE_OPTIONAL, 'The number of records to search for per iteration. Default is 100.', 100);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
// get the mid from the cli
$batchSize = $input->getOption('batch-size');
// monitor record
$monitorId = $input->getOption('mid');
$monitorList = $this->getMonitors($monitorId);
// no mid found, quit now
if (!$monitorList->count()) {
$output->writeln('No published monitors found. Make sure the id you supplied is published');
return Command::SUCCESS;
}
if (!is_numeric($batchSize)) {
$output->writeln('batch-size is not number.');
return self::FAILURE;
}
// max iterations
$maxPerIterations = ceil((int) $batchSize / count($monitorList));
foreach ($monitorList as $monitor) {
$output->writeln('Executing Monitor Item '.$monitor->getId());
$resultCode = $this->processMonitorListItem($monitor, $maxPerIterations, $input, $output);
$output->writeln('Result Code: '.$resultCode);
}
return Command::SUCCESS;
}
/**
* @return \Doctrine\ORM\Tools\Pagination\Paginator
*/
protected function getMonitors($id = null)
{
$filter = [
'start' => 0,
'limit' => 100,
];
/** @var MonitoringRepository $repository */
$repository = $this->monitoringModel->getRepository();
if (null !== $id) {
$filter['filter'] = [
'force' => [
[
'column' => $repository->getTableAlias().'.id',
'expr' => 'eq',
'value' => (int) $id,
],
],
];
}
return $repository->getPublishedEntities($filter);
}
/**
* @return bool|int
*
* @throws \Exception
*/
protected function processMonitorListItem($listItem, float $maxPerIterations, InputInterface $input, OutputInterface $output)
{
// @todo set this up to use the command type per-monitor record.
$networkType = $listItem->getNetworkType();
$commandName = '';
// hashtag command
if ('twitter_hashtag' == $networkType) {
$commandName = 'social:monitor:twitter:hashtags';
}
// mention command
if ('twitter_handle' == $networkType) {
$commandName = 'social:monitor:twitter:mentions';
}
if ('' == $commandName) {
$output->writeln('Matching command not found.');
return 1;
}
// monitor hash command
$command = $this->getApplication()->find($commandName);
// create command options
$cliArgs = [
'command' => $commandName,
'--mid' => $listItem->getId(),
'--max-runs' => $maxPerIterations,
'--query-count' => $input->getOption('query-count'),
];
// execute the command
$returnCode = $command->run(new ArrayInput($cliArgs), $output);
return $returnCode;
}
}

View File

@@ -0,0 +1,300 @@
<?php
namespace MauticPlugin\MauticSocialBundle\Command;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\CoreBundle\Translation\Translator;
use Mautic\PluginBundle\Helper\IntegrationHelper;
use MauticPlugin\MauticSocialBundle\Entity\Monitoring;
use MauticPlugin\MauticSocialBundle\Event\SocialMonitorEvent;
use MauticPlugin\MauticSocialBundle\Helper\TwitterCommandHelper;
use MauticPlugin\MauticSocialBundle\Integration\TwitterIntegration;
use MauticPlugin\MauticSocialBundle\SocialEvents;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
abstract class MonitorTwitterBaseCommand extends Command
{
/**
* @var TwitterIntegration
*/
protected $twitter;
/**
* @var InputInterface
*/
protected $input;
/**
* @var OutputInterface
*/
protected $output;
/**
* @var int
*/
protected $maxRuns = 5;
/**
* @var int
*/
protected $runCount = 0;
/**
* @var int
*/
protected $queryCount = 100;
public function __construct(
protected EventDispatcherInterface $dispatcher,
protected Translator $translator,
protected IntegrationHelper $integrationHelper,
private TwitterCommandHelper $twitterCommandHelper,
CoreParametersHelper $coreParametersHelper,
) {
$this->translator->setLocale($coreParametersHelper->get('locale', 'en_US'));
parent::__construct();
}
/**
* Command configuration. Set the name, description, and options here.
*/
protected function configure()
{
$this
->addOption(
'mid',
'i',
InputOption::VALUE_REQUIRED,
'The id of the monitor record'
)
->addOption(
'max-runs',
null,
InputOption::VALUE_REQUIRED,
'The maximum number of recursive iterations permitted',
5
)
->addOption(
'query-count',
null,
InputOption::VALUE_OPTIONAL,
'The number of records to search for per iteration.',
100
)
->addOption(
'show-posts',
null,
InputOption::VALUE_NONE,
'Use this option to display the posts retrieved'
)
->addOption(
'show-stats',
null,
InputOption::VALUE_NONE,
'Use this option to display the stats of the tweets fetched'
);
}
/**
* Used in various areas to set name of the network being searched.
*
* @return string twitter|facebook etc..
*/
abstract public function getNetworkName();
/**
* Search for tweets by creating your own search criteria.
*
* @param Monitoring $monitor
*
* @return array The results of makeRequest
*/
abstract protected function getTweets($monitor);
/**
* Main execution method. Gets the integration settings, processes the search criteria.
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->input = $input;
$this->output = $output;
$this->maxRuns = $this->input->getOption('max-runs');
$this->queryCount = $this->input->getOption('query-count');
$twitterIntegration = $this->integrationHelper->getIntegrationObject('Twitter');
if (false === $twitterIntegration || false === $twitterIntegration->getIntegrationSettings()->getIsPublished()) {
$this->output->writeln($this->translator->trans('mautic.social.monitoring.twitter.not.published'));
return Command::FAILURE;
}
\assert($twitterIntegration instanceof TwitterIntegration);
$this->twitter = $twitterIntegration;
if (!$this->twitter->isAuthorized()) {
$this->output->writeln($this->translator->trans('mautic.social.monitoring.twitter.not.configured'));
return Command::FAILURE;
}
// get the mid from the cli
$mid = (int) $input->getOption('mid');
if (!$mid) {
$this->output->writeln($this->translator->trans('mautic.social.monitoring.twitter.mid.empty'));
return Command::FAILURE;
}
$this->twitterCommandHelper->setOutput($output);
$monitor = $this->twitterCommandHelper->getMonitor($mid);
if (!$monitor || !$monitor->getId()) {
$this->output->writeln($this->translator->trans('mautic.social.monitoring.twitter.monitor.does.not.exist', ['%id%' => $mid]));
return Command::FAILURE;
}
// process the monitor
$this->processMonitor($monitor);
$this->dispatcher->dispatch(
new SocialMonitorEvent($this->getNetworkName(), $monitor, $this->twitterCommandHelper->getManipulatedLeads(), $this->twitterCommandHelper->getNewLeadsCount(), $this->twitterCommandHelper->getUpdatedLeadsCount()),
SocialEvents::MONITOR_POST_PROCESS
);
return Command::SUCCESS;
}
/**
* Process the monitor record.
*
* @Note: Keeping this method here instead of in the twitterCommandHelper
* so that the hashtag and mention commands can easily extend it.
*
* @param Monitoring $monitor
*
* @return bool
*/
protected function processMonitor($monitor)
{
$results = $this->getTweets($monitor);
if (false === $results || !isset($results['statuses'])) {
$this->output->writeln('No statuses found');
if (!empty($results['errors'])) {
foreach ($results['errors'] as $error) {
$this->output->writeln($error['code'].': '.$error['message']);
}
}
return 0;
}
if (count($results['statuses'])) {
$this->twitterCommandHelper->createLeadsFromStatuses($results['statuses'], $monitor);
} else {
$this->output->writeln($this->translator->trans('mautic.social.monitoring.twitter.no.new.tweets'));
}
$this->twitterCommandHelper->setMonitorStats($monitor, $results['search_metadata']);
$this->printInformation($monitor, $results);
// get stats after being updated
$stats = $monitor->getStats();
++$this->runCount;
// if we have stats and a next results request, process it here
// @todo add a check for max iterations
if (is_array($stats) && array_key_exists('max_id_str', $stats)
&& ($this->runCount < $this->maxRuns)
&& count($results['statuses'])
) {
// recursive
$this->processMonitor($monitor);
}
return 0;
}
/**
* Prints all the returned tweets.
*
* @param array $statuses
*/
protected function printTweets($statuses)
{
if (!$this->input->getOption('show-posts') && $this->output->getVerbosity() < OutputInterface::VERBOSITY_VERY_VERBOSE) {
return;
}
foreach ($statuses as $status) {
$this->output->writeln('-- tweet -- ');
$this->output->writeln('ID: '.$status['id']);
$this->output->writeln('Message: '.$status['text']);
$this->output->writeln('Handle: '.$status['user']['screen_name']);
$this->output->writeln('Name: '.$status['user']['name']);
$this->output->writeln('Location: '.$status['user']['location']);
$this->output->writeln('Profile Img: '.$status['user']['profile_image_url']);
$this->output->writeln('Profile Description: '.$status['user']['description']);
$this->output->writeln('// tweet // ');
}
}
/**
* Prints the search query metadata from twitter.
* Only shows stats if explicitly requested or if we're in verbose mode.
*
* @param array $metadata
*/
protected function printQueryMetadata($metadata)
{
if (!$this->input->getOption('show-stats') && $this->output->getVerbosity() < OutputInterface::VERBOSITY_VERBOSE) {
return;
}
$this->output->writeln('-- search meta -- ');
$this->output->writeln('max_id_str: '.$metadata['max_id_str']);
$this->output->writeln('since_id_str: '.$metadata['since_id_str']);
$this->output->writeln('Page Count: '.$metadata['count']);
$this->output->writeln('query: '.$metadata['query']);
if (array_key_exists('next_results', $metadata)) {
$this->output->writeln('next results: '.$metadata['next_results']);
}
$this->output->writeln('// search meta // ');
}
/**
* Prints a summary of the search query.
* Only shows stats if explicitly requested or if we're in verbose mode.
*
* @param Monitoring $monitor
* @param array $results
*/
protected function printInformation($monitor, $results)
{
if (!$this->input->getOption('show-stats') && $this->output->getVerbosity() < OutputInterface::VERBOSITY_VERBOSE) {
return;
}
$this->output->writeln('------------------------');
$this->output->writeln($monitor->getTitle());
$this->output->writeln('Published '.$monitor->isPublished());
$this->output->writeln($monitor->getNetworkType());
$this->output->writeln('New Leads '.$this->twitterCommandHelper->getNewLeadsCount());
$this->output->writeln('Updated Leads '.$this->twitterCommandHelper->getUpdatedLeadsCount());
$this->printQueryMetadata($results['search_metadata']);
$this->printTweets($results['statuses']);
$this->output->writeln('------------------------');
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace MauticPlugin\MauticSocialBundle\Command;
use MauticPlugin\MauticSocialBundle\Entity\Monitoring;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand(
name: 'social:monitor:twitter:hashtags',
description: 'Looks at our monitoring records and finds hashtags'
)]
class MonitorTwitterHashtagsCommand extends MonitorTwitterBaseCommand
{
/**
* Search for tweets by hashtag.
*
* @param Monitoring $monitor
*
* @return bool|array False if missing the hashtag, otherwise the array response from Twitter
*/
protected function getTweets($monitor)
{
$params = $monitor->getProperties();
$stats = $monitor->getStats();
if (!array_key_exists('hashtag', $params)) {
$this->output->writeln('No hashtag was found!');
return false;
}
$searchUrl = $this->twitter->getApiUrl('search/tweets');
$requestQuery = [
'q' => '#'.$params['hashtag'],
'count' => $this->queryCount,
];
// if we have a max id string use it here
if (is_array($stats) && array_key_exists('max_id_str', $stats) && $stats['max_id_str']) {
$requestQuery['since_id'] = $stats['max_id_str'];
}
return $this->twitter->makeRequest($searchUrl, $requestQuery);
}
public function getNetworkName(): string
{
return 'twitter';
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace MauticPlugin\MauticSocialBundle\Command;
use MauticPlugin\MauticSocialBundle\Entity\Monitoring;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand(
name: 'social:monitor:twitter:mentions',
description: 'Searches for mentioned tweets'
)]
class MonitorTwitterMentionsCommand extends MonitorTwitterBaseCommand
{
/**
* Search for tweets by mention.
*
* @param Monitoring $monitor
*
* @return bool|array False if missing the twitter handle, otherwise the array response from Twitter
*/
protected function getTweets($monitor)
{
$params = $monitor->getProperties();
$stats = $monitor->getStats();
if (!array_key_exists('handle', $params)) {
$this->output->writeln('No twitter handle was found!');
return false;
}
$mentionsUrl = $this->twitter->getApiUrl('search/tweets');
$requestQuery = [
'q' => '@'.$params['handle'],
'count' => $this->queryCount,
];
// if we have a max id string use it here
if (is_array($stats) && array_key_exists('max_id_str', $stats) && $stats['max_id_str']) {
$requestQuery['since_id'] = $stats['max_id_str'];
}
return $this->twitter->makeRequest($mentionsUrl, $requestQuery);
}
public function getNetworkName(): string
{
return 'twitter';
}
}

View File

@@ -0,0 +1,213 @@
<?php
return [
'name' => 'Social Media',
'description' => 'Enables integrations with Mautic supported social media services.',
'version' => '1.0',
'author' => 'Mautic',
'routes' => [
'main' => [
'mautic_social_index' => [
'path' => '/monitoring/{page}',
'controller' => 'MauticPlugin\MauticSocialBundle\Controller\MonitoringController::indexAction',
],
'mautic_social_action' => [
'path' => '/monitoring/{objectAction}/{objectId}',
'controller' => 'MauticPlugin\MauticSocialBundle\Controller\MonitoringController::executeAction',
],
'mautic_social_contacts' => [
'path' => '/monitoring/view/{objectId}/contacts/{page}',
'controller' => 'MauticPlugin\MauticSocialBundle\Controller\MonitoringController::contactsAction',
],
'mautic_tweet_index' => [
'path' => '/tweets/{page}',
'controller' => 'MauticPlugin\MauticSocialBundle\Controller\TweetController::indexAction',
],
'mautic_tweet_action' => [
'path' => '/tweets/{objectAction}/{objectId}',
'controller' => 'MauticPlugin\MauticSocialBundle\Controller\TweetController::executeAction',
],
],
'api' => [
'mautic_api_tweetsstandard' => [
'standard_entity' => true,
'name' => 'tweets',
'path' => '/tweets',
'controller' => MauticPlugin\MauticSocialBundle\Controller\Api\TweetApiController::class,
],
],
'public' => [
'mautic_social_js_generate' => [
'path' => '/social/generate/{formName}.js',
'controller' => 'MauticPlugin\MauticSocialBundle\Controller\JsController::generateAction',
],
],
],
'services' => [
'others' => [
'mautic.social.helper.campaign' => [
'class' => MauticPlugin\MauticSocialBundle\Helper\CampaignEventHelper::class,
'arguments' => [
'mautic.helper.integration',
'mautic.page.model.trackable',
'mautic.page.helper.token',
'mautic.asset.helper.token',
'mautic.social.model.tweet',
],
],
'mautic.social.helper.twitter_command' => [
'class' => MauticPlugin\MauticSocialBundle\Helper\TwitterCommandHelper::class,
'arguments' => [
'mautic.lead.model.lead',
'mautic.lead.model.field',
'mautic.social.model.monitoring',
'mautic.social.model.postcount',
'translator',
'doctrine.orm.entity_manager',
'mautic.helper.core_parameters',
],
],
],
'integrations' => [
'mautic.integration.facebook' => [
'class' => MauticPlugin\MauticSocialBundle\Integration\FacebookIntegration::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.lead.field.fields_with_unique_identifier',
'mautic.plugin.model.integration_entity',
'mautic.lead.model.dnc',
'mautic.helper.integration',
'mautic.lead.field.fields_with_unique_identifier',
],
],
'mautic.integration.foursquare' => [
'class' => MauticPlugin\MauticSocialBundle\Integration\FoursquareIntegration::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.lead.field.fields_with_unique_identifier',
'mautic.plugin.model.integration_entity',
'mautic.lead.model.dnc',
'mautic.helper.integration',
'mautic.lead.field.fields_with_unique_identifier',
],
],
'mautic.integration.instagram' => [
'class' => MauticPlugin\MauticSocialBundle\Integration\InstagramIntegration::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.lead.field.fields_with_unique_identifier',
'mautic.plugin.model.integration_entity',
'mautic.lead.model.dnc',
'mautic.helper.integration',
'mautic.lead.field.fields_with_unique_identifier',
],
],
'mautic.integration.twitter' => [
'class' => MauticPlugin\MauticSocialBundle\Integration\TwitterIntegration::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.lead.field.fields_with_unique_identifier',
'mautic.plugin.model.integration_entity',
'mautic.lead.model.dnc',
'mautic.helper.integration',
'mautic.lead.field.fields_with_unique_identifier',
],
],
],
],
'menu' => [
'main' => [
'mautic.social.monitoring' => [
'route' => 'mautic_social_index',
'parent' => 'mautic.core.channels',
'access' => 'mauticSocial:monitoring:view',
'priority' => 0,
'checks' => [
'integration' => [
'Twitter' => [
'enabled' => true,
],
],
],
],
'mautic.social.tweets' => [
'route' => 'mautic_tweet_index',
'access' => ['mauticSocial:tweets:viewown', 'mauticSocial:tweets:viewother'],
'parent' => 'mautic.core.channels',
'priority' => 80,
'checks' => [
'integration' => [
'Twitter' => [
'enabled' => true,
],
],
],
],
],
],
'categories' => [
'plugin:mauticSocial' => [
'label' => 'mautic.social.monitoring',
'class' => MauticPlugin\MauticSocialBundle\Entity\Monitoring::class,
],
],
'twitter' => [
'tweet_request_count' => 100,
],
'parameters' => [
'twitter_handle_field' => 'twitter',
],
];

View File

@@ -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('MauticPlugin\\MauticSocialBundle\\', '../')
->exclude('../{'.implode(',', array_merge(MauticCoreExtension::DEFAULT_EXCLUDES, $excludes)).'}');
$services->load('MauticPlugin\\MauticSocialBundle\\Entity\\', '../Entity/*Repository.php');
$services->alias('mautic.social.model.monitoring', MauticPlugin\MauticSocialBundle\Model\MonitoringModel::class);
$services->alias('mautic.social.model.postcount', MauticPlugin\MauticSocialBundle\Model\PostCountModel::class);
$services->alias('mautic.social.model.tweet', MauticPlugin\MauticSocialBundle\Model\TweetModel::class);
};

View File

@@ -0,0 +1,60 @@
<?php
namespace MauticPlugin\MauticSocialBundle\Controller;
use Mautic\CoreBundle\Controller\AjaxController as CommonAjaxController;
use Mautic\CoreBundle\Controller\AjaxLookupControllerTrait;
use Mautic\CoreBundle\Helper\InputHelper;
use MauticPlugin\MauticSocialBundle\Model\MonitoringModel;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\Request;
class AjaxController extends CommonAjaxController
{
use AjaxLookupControllerTrait;
public function getNetworkFormAction(Request $request, MonitoringModel $monitoringModel, FormFactoryInterface $formFactory): \Symfony\Component\HttpFoundation\JsonResponse
{
// get the form type
$type = InputHelper::clean($request->request->get('networkType'));
// default to empty
$dataArray = [
'html' => '',
'success' => 0,
];
if (!empty($type)) {
// get the HTML for the form
$formType = $monitoringModel->getFormByType($type);
// get the network type form
$form = $formFactory->create($formType, [], ['label' => false, 'csrf_protection' => false]);
$html = $this->renderView(
'@MauticSocial/FormTheme/'.$type.'_widget.html.twig',
['form' => $form->createView()]
);
$html = str_replace(
[
$type.'[', // this is going to generate twitter_hashtag[ or twitter_mention[
$type.'_', // this is going to generate twitter_hashtag_ or twitter_mention_
$type,
],
[
'monitoring[properties][',
'monitoring_properties_',
'monitoring',
],
$html
);
$dataArray['html'] = $html;
$dataArray['success'] = 1;
}
return $this->sendJsonResponse($dataArray);
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace MauticPlugin\MauticSocialBundle\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 MauticPlugin\MauticSocialBundle\Entity\Tweet;
use MauticPlugin\MauticSocialBundle\Model\TweetModel;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Routing\RouterInterface;
/**
* @extends CommonApiController<Tweet>
*/
class TweetApiController extends CommonApiController
{
/**
* @var TweetModel|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)
{
$tweetModel = $modelFactory->getModel('social.tweet');
\assert($tweetModel instanceof TweetModel);
$this->model = $tweetModel;
$this->entityClass = Tweet::class;
$this->entityNameOne = 'tweet';
$this->entityNameMulti = 'tweets';
parent::__construct($security, $translator, $entityResultHelper, $router, $formFactory, $appVersion, $requestStack, $doctrine, $modelFactory, $dispatcher, $coreParametersHelper);
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace MauticPlugin\MauticSocialBundle\Controller;
use Mautic\CoreBundle\Controller\CommonController;
use Symfony\Component\HttpFoundation\Response;
class JsController extends CommonController
{
public function generateAction($formName): Response
{
$js = <<<JS
function openOAuthWindow(authUrl){
if (authUrl) {
var generator = window.open(authUrl, 'integrationauth', 'height=500,width=500');
if (!generator || generator.closed || typeof generator.closed == 'undefined') {
alert(mauticLang.popupBlockerMessage);
}
}
}
function postAuthCallback(response){
var elements = document.getElementById("mauticform_{$formName}").elements;
var field, fieldName;
values = JSON.parse(JSON.stringify(response));
for (var i = 0, element; element = elements[i++];) {
field = element.name
fieldName = field.replace("mauticform[","");
fieldName = fieldName.replace("]","");
var element = document.getElementsByName("mauticform["+fieldName+"]");
// Remove underscores, dashes, and f_ prefix for comparison
fieldName = fieldName.replace("f_", "").replace(/_/g,"").replace(/-/g, "");
for(var key in values) {
var compareKey = key.replace(/_/g,"").replace(/-/g, "");
if (key != 'id' && (key.indexOf(fieldName) >= 0 || fieldName.indexOf(key) >= 0) && element[0].value == "") {
if (values[key].constructor === Array && values[key][0].value) {
element[0].value = values[key][0].value;
} else {
element[0].value = values[key];
}
break;
}
}
}
}
JS;
return new Response(
$js,
200,
[
'Content-Type' => 'application/javascript',
]
);
}
}

View File

@@ -0,0 +1,677 @@
<?php
namespace MauticPlugin\MauticSocialBundle\Controller;
use Mautic\CoreBundle\Controller\FormController;
use Mautic\CoreBundle\Factory\PageHelperFactoryInterface;
use Mautic\CoreBundle\Form\Type\DateRangeType;
use Mautic\CoreBundle\Helper\Chart\LineChart;
use Mautic\CoreBundle\Helper\IpLookupHelper;
use Mautic\CoreBundle\Model\AuditLogModel;
use Mautic\LeadBundle\Controller\EntityContactsTrait;
use MauticPlugin\MauticSocialBundle\Entity\Monitoring;
use MauticPlugin\MauticSocialBundle\Model\MonitoringModel;
use Symfony\Component\Form\SubmitButton;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class MonitoringController extends FormController
{
use EntityContactsTrait;
/*
* @param int $page
*/
public function indexAction(Request $request, MonitoringModel $model, $page = 1)
{
if (!$this->security->isGranted('mauticSocial:monitoring:view')) {
return $this->accessDenied();
}
$session = $request->getSession();
$this->setListFilters();
// set limits
$limit = $session->get('mautic.social.monitoring.limit', $this->getParameter('mautic.default_pagelimit'));
$start = (1 === $page) ? 0 : (($page - 1) * $limit);
if ($start < 0) {
$start = 0;
}
$search = $request->get('search', $session->get('mautic.social.monitoring.filter', ''));
$session->set('mautic.social.monitoring.filter', $search);
$filter = ['string' => $search, 'force' => []];
$orderBy = $session->get('mautic.social.monitoring.orderby', 'e.title');
$orderByDir = $session->get('mautic.social.monitoring.orderbydir', 'DESC');
$monitoringList = $model->getEntities(
[
'start' => $start,
'limit' => $limit,
'filter' => $filter,
'orderBy' => $orderBy,
'orderByDir' => $orderByDir,
]
);
$count = count($monitoringList);
if ($count && $count < ($start + 1)) {
// the number of entities are now less then the current asset so redirect to the last asset
if (1 === $count) {
$lastPage = 1;
} else {
$lastPage = (floor($limit / $count)) ?: 1;
}
$session->set('mautic.social.monitoring.page', $lastPage);
$returnUrl = $this->generateUrl('mautic_social_index', ['page' => $lastPage]);
return $this->postActionRedirect(
[
'returnUrl' => $returnUrl,
'viewParameters' => ['page' => $lastPage],
'contentTemplate' => 'MauticPlugin\MauticSocialBundle\Controller\MonitoringController::indexAction',
'passthroughVars' => [
'activeLink' => '#mautic_social_index',
'mauticContent' => 'monitoring',
],
]
);
}
// set what asset currently on so that we can return here after form submission/cancellation
$session->set('mautic.social.monitoring.page', $page);
$tmpl = $request->isXmlHttpRequest() ? $request->get('tmpl', 'index') : 'index';
return $this->delegateView(
[
'viewParameters' => [
'searchValue' => $search,
'items' => $monitoringList,
'limit' => $limit,
'model' => $model,
'tmpl' => $tmpl,
'page' => $page,
],
'contentTemplate' => '@MauticSocial/Monitoring/list.html.twig',
'passthroughVars' => [
'activeLink' => '#mautic_social_index',
'mauticContent' => 'monitoring',
'route' => $this->generateUrl('mautic_social_index', ['page' => $page]),
],
]
);
}
/**
* Generates new form and processes post data.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse|Response
*/
public function newAction(Request $request, MonitoringModel $model, IpLookupHelper $ipLookupHelper)
{
if (!$this->security->isGranted('mauticSocial:monitoring:create')) {
return $this->accessDenied();
}
$action = $this->generateUrl('mautic_social_action', ['objectAction' => 'new']);
$entity = $model->getEntity();
$method = $request->getMethod();
$session = $request->getSession();
// get the list of types from the model
$networkTypes = $model->getNetworkTypes();
// get the network type from the request on submit. helpful for validation error
// rebuilds structure of the form when it gets updated on submit
$monitoring = $request->request->all()['monitoring'] ?? [];
$networkType = 'POST' === $method ? ($monitoring['networkType'] ?? '') : '';
// build the form
$form = $model->createForm(
$entity,
$this->formFactory,
$action,
[
// pass through the types and the selected default type
'networkTypes' => $networkTypes,
'networkType' => $networkType,
]
);
// Set the page we came from
$page = $session->get('mautic.social.monitoring.page', 1);
// /Check for a submitted form and process it
if ('POST' === $method) {
$viewParameters = ['page' => $page];
$template = 'MauticPlugin\MauticSocialBundle\Controller\MonitoringController::indexAction';
$valid = false;
if (!$cancelled = $this->isFormCancelled($form)) {
if ($valid = $this->isFormValid($form)) {
// form is valid so process the data
$model->saveEntity($entity);
// update the audit log
$this->updateAuditLog($entity, $ipLookupHelper, 'create');
$this->addFlashMessage(
'mautic.core.notice.created',
[
'%name%' => $entity->getTitle(),
'%menu_link%' => 'mautic_social_index',
'%url%' => $this->generateUrl(
'mautic_social_action',
[
'objectAction' => 'edit',
'objectId' => $entity->getId(),
]
),
]
);
if (!$this->getFormButton($form, ['buttons', 'save'])->isClicked()) {
// return edit view so that all the session stuff is loaded
return $this->editAction($request, $ipLookupHelper, $entity->getId(), true);
}
$viewParameters = [
'objectAction' => 'view',
'objectId' => $entity->getId(),
];
$template = 'MauticPlugin\MauticSocialBundle\Controller\MonitoringController::viewAction';
}
}
$returnUrl = $this->generateUrl('mautic_social_index', $viewParameters);
/** @var SubmitButton $saveSubmitButton */
$saveSubmitButton = $form->get('buttons')->get('save');
if ($cancelled || ($valid && $saveSubmitButton->isClicked())) {
return $this->postActionRedirect(
[
'returnUrl' => $returnUrl,
'viewParameters' => $viewParameters,
'contentTemplate' => $template,
'passthroughVars' => [
'activeLink' => 'mautic_social_index',
'mauticContent' => 'monitoring',
],
]
);
}
}
return $this->delegateView(
[
'viewParameters' => [
'tmpl' => $request->isXmlHttpRequest() ? $request->get('tmpl', 'index') : 'index',
'entity' => $entity,
'form' => $form->createView(),
],
'contentTemplate' => '@MauticSocial/Monitoring/form.html.twig',
'passthroughVars' => [
'activeLink' => '#mautic_social_index',
'mauticContent' => 'monitoring',
'route' => $this->generateUrl(
'mautic_social_action',
[
'objectAction' => 'new',
'objectId' => $entity->getId(),
]
),
],
]
);
}
/**
* @return \Symfony\Component\HttpFoundation\JsonResponse|\Symfony\Component\HttpFoundation\RedirectResponse|Response
*/
public function editAction(Request $request, IpLookupHelper $ipLookupHelper, $objectId, bool $ignorePost = false)
{
if (!$this->security->isGranted('mauticSocial:monitoring:edit')) {
return $this->accessDenied();
}
$action = $this->generateUrl('mautic_social_action', ['objectAction' => 'edit', 'objectId' => $objectId]);
/** @var MonitoringModel $model */
$model = $this->getModel('social.monitoring');
$entity = $model->getEntity($objectId);
$session = $request->getSession();
// Set the page we came from
$page = $session->get('mautic.social.monitoring.page', 1);
// set the return URL
$returnUrl = $this->generateUrl('mautic_social_index', ['page' => $page]);
$postActionVars = [
'returnUrl' => $returnUrl,
'viewParameters' => ['page' => $page],
'contentTemplate' => 'MauticSocial:Monitoring:index',
'passthroughVars' => [
'activeLink' => 'mautic_social_index',
'mauticContent' => 'monitoring',
],
];
// not found
if (null === $entity) {
return $this->postActionRedirect(
array_merge(
$postActionVars,
[
'flashes' => [
[
'type' => 'error',
'msg' => 'mautic.social.monitoring.error.notfound',
'msgVars' => ['%id%' => $objectId],
],
],
]
)
);
}
// get the list of types from the model
$networkTypes = $model->getNetworkTypes();
// get the network type from the request on submit. helpful for validation error
// rebuilds structure of the form when it gets updated on submit
$method = $request->getMethod();
$monitoring = $request->request->all()['monitoring'] ?? [];
$networkType = 'POST' === $method ? ($monitoring['networkType'] ?? '') : $entity->getNetworkType();
// build the form
$form = $model->createForm(
$entity,
$this->formFactory,
$action,
[
// pass through the types and the selected default type
'networkTypes' => $networkTypes,
'networkType' => $networkType,
]
);
// /Check for a submitted form and process it
if (!$ignorePost && 'POST' === $method) {
$valid = false;
/** @var SubmitButton $saveSubmitButton */
$saveSubmitButton = $form->get('buttons')->get('save');
if (!$cancelled = $this->isFormCancelled($form)) {
if ($valid = $this->isFormValid($form)) {
// form is valid so process the data
$model->saveEntity($entity, $saveSubmitButton->isClicked());
// update the audit log
$this->updateAuditLog($entity, $ipLookupHelper, 'update');
$this->addFlashMessage(
'mautic.core.notice.updated',
[
'%name%' => $entity->getTitle(),
'%menu_link%' => 'mautic_email_index',
'%url%' => $this->generateUrl(
'mautic_social_action',
[
'objectAction' => 'edit',
'objectId' => $entity->getId(),
]
),
],
'warning'
);
}
} else {
$model->unlockEntity($entity);
}
if ($cancelled || ($valid && $saveSubmitButton->isClicked())) {
$viewParameters = [
'objectAction' => 'view',
'objectId' => $entity->getId(),
];
return $this->postActionRedirect(
array_merge(
$postActionVars,
[
'returnUrl' => $this->generateUrl('mautic_social_action', $viewParameters),
'viewParameters' => $viewParameters,
'contentTemplate' => 'MauticPlugin\MauticSocialBundle\Controller\MonitoringController::viewAction',
]
)
);
}
} else {
// lock the entity
$model->lockEntity($entity);
}
return $this->delegateView(
[
'viewParameters' => [
'tmpl' => $request->isXmlHttpRequest() ? $request->get('tmpl', 'index') : 'index',
'entity' => $entity,
'form' => $form->createView(),
],
'contentTemplate' => '@MauticSocial/Monitoring/form.html.twig',
'passthroughVars' => [
'activeLink' => '#mautic_social_index',
'mauticContent' => 'monitoring',
'route' => $this->generateUrl(
'mautic_social_action',
[
'objectAction' => 'edit',
'objectId' => $entity->getId(),
]
),
],
]
);
}
/**
* Loads a specific form into the detailed panel.
*
* @param int $objectId
*
* @return JsonResponse|Response
*/
public function viewAction(Request $request, $objectId)
{
if (!$this->security->isGranted('mauticSocial:monitoring:view')) {
return $this->accessDenied();
}
$session = $request->getSession();
/** @var MonitoringModel $model */
$model = $this->getModel('social.monitoring');
/** @var \MauticPlugin\MauticSocialBundle\Entity\PostCountRepository $postCountRepo */
$postCountRepo = $this->getModel('social.postcount')->getRepository();
$security = $this->security;
$monitoringEntity = $model->getEntity($objectId);
// set the asset we came from
$page = $session->get('mautic.social.monitoring.page', 1);
$tmpl = $request->isXmlHttpRequest() ? $request->get('tmpl', 'details') : 'details';
if (null === $monitoringEntity) {
// set the return URL
$returnUrl = $this->generateUrl('mautic_social_index', ['page' => $page]);
return $this->postActionRedirect(
[
'returnUrl' => $returnUrl,
'viewParameters' => ['page' => $page],
'contentTemplate' => 'MauticPlugin\MauticSocialBundle\Controller\MonitoringController::indexAction',
'passthroughVars' => [
'activeLink' => '#mautic_social_index',
'mauticContent' => 'monitoring',
],
'flashes' => [
[
'type' => 'error',
'msg' => 'mautic.social.monitoring.error.notfound',
'msgVars' => ['%id%' => $objectId],
],
],
]
);
}
// Audit Log
$auditLogModel = $this->getModel('core.auditlog');
\assert($auditLogModel instanceof AuditLogModel);
$logs = $auditLogModel->getLogForObject('monitoring', $objectId);
$returnUrl = $this->generateUrl(
'mautic_social_action',
[
'objectAction' => 'view',
'objectId' => $monitoringEntity->getId(),
]
);
// Init the date range filter form
$dateRangeValues = $request->get('daterange', []);
$dateRangeForm = $this->formFactory->create(DateRangeType::class, $dateRangeValues, ['action' => $returnUrl]);
$dateFrom = new \DateTime($dateRangeForm['date_from']->getData());
$dateTo = new \DateTime($dateRangeForm['date_to']->getData());
$chart = new LineChart(null, $dateFrom, $dateTo);
$leadStats = $postCountRepo->getLeadStatsPost(
$dateFrom,
$dateTo,
['monitor_id' => $monitoringEntity->getId()]
);
$chart->setDataset($this->translator->trans('mautic.social.twitter.tweet.count'), $leadStats);
return $this->delegateView(
[
'returnUrl' => $returnUrl,
'viewParameters' => [
'activeMonitoring' => $monitoringEntity,
'logs' => $logs,
'isEmbedded' => $request->get('isEmbedded') ?: false,
'tmpl' => $tmpl,
'security' => $security,
'leadStats' => $chart->render(),
'monitorLeads' => $this->forward(
'MauticPlugin\MauticSocialBundle\Controller\MonitoringController::contactsAction',
[
'objectId' => $monitoringEntity->getId(),
'page' => $page,
'ignoreAjax' => true,
]
)->getContent(),
'dateRangeForm' => $dateRangeForm->createView(),
],
'contentTemplate' => '@MauticSocial/Monitoring/'.$tmpl.'.html.twig',
'passthroughVars' => [
'activeLink' => '#mautic_social_index',
'mauticContent' => 'monitoring',
],
]
);
}
/**
* Deletes the entity.
*
* @param int $objectId
*
* @return Response
*/
public function deleteAction(Request $request, IpLookupHelper $ipLookupHelper, $objectId)
{
if (!$this->security->isGranted('mauticSocial:monitoring:delete')) {
return $this->accessDenied();
}
$session = $request->getSession();
$page = $session->get('mautic.social.monitoring.page', 1);
$returnUrl = $this->generateUrl('mautic_social_index', ['page' => $page]);
$flashes = [];
$postActionVars = [
'returnUrl' => $returnUrl,
'viewParameters' => ['page' => $page],
'contentTemplate' => 'MauticPlugin\MauticSocialBundle\Controller\MonitoringController::indexAction',
'passthroughVars' => [
'activeLink' => 'mautic_social_index',
'mauticContent' => 'monitoring',
],
];
if ('POST' === $request->getMethod()) {
/** @var MonitoringModel $model */
$model = $this->getModel('social.monitoring');
$entity = $model->getEntity($objectId);
if (null === $entity) {
$flashes[] = [
'type' => 'error',
'msg' => 'mautic.social.monitoring.error.notfound',
'msgVars' => ['%id%' => $objectId],
];
} elseif ($model->isLocked($entity)) {
return $this->isLocked($postActionVars, $entity, 'plugin.mauticSocial.monitoring');
}
// update the audit log
$this->updateAuditLog($entity, $ipLookupHelper, 'delete');
// then delete the record
$model->deleteEntity($entity);
$flashes[] = [
'type' => 'notice',
'msg' => 'mautic.core.notice.deleted',
'msgVars' => [
'%name%' => $entity->getTitle(),
'%id%' => $objectId,
],
];
} // else don't do anything
return $this->postActionRedirect(
array_merge(
$postActionVars,
[
'flashes' => $flashes,
]
)
);
}
/**
* Deletes a group of entities.
*
* @return Response
*/
public function batchDeleteAction(Request $request)
{
if (!$this->security->isGranted('mauticSocial:monitoring:delete')) {
return $this->accessDenied();
}
$session = $request->getSession();
$page = $session->get('mautic.social.monitoring.page', 1);
$returnUrl = $this->generateUrl('mautic_social_index', ['page' => $page]);
$flashes = [];
$postActionVars = [
'returnUrl' => $returnUrl,
'viewParameters' => ['page' => $page],
'contentTemplate' => 'MauticPlugin\MauticSocialBundle\Controller\MonitoringController::indexAction',
'passthroughVars' => [
'activeLink' => '#mautic_social_index',
'mauticContent' => 'monitoring',
],
];
if ('POST' === $request->getMethod()) {
/** @var MonitoringModel $model */
$model = $this->getModel('social.monitoring');
$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.social.monitoring.error.notfound',
'msgVars' => ['%id%' => $objectId],
];
} elseif ($model->isLocked($entity)) {
$flashes[] = $this->isLocked($postActionVars, $entity, 'monitoring', true);
} else {
$deleteIds[] = $objectId;
}
}
// Delete everything we are able to
if (!empty($deleteIds)) {
$entities = $model->deleteEntities($deleteIds);
$flashes[] = [
'type' => 'notice',
'msg' => 'mautic.social.monitoring.notice.batch_deleted',
'msgVars' => [
'%count%' => count($entities),
],
];
}
} // else don't do anything
return $this->postActionRedirect(
array_merge(
$postActionVars,
[
'flashes' => $flashes,
]
)
);
}
/**
* @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,
'mauticSocial:monitoring:view',
'social',
'monitoring_leads',
null, // @todo - implement when individual social channels are supported by the plugin
'monitor_id'
);
}
/*
* Update the audit log
*/
public function updateAuditLog(Monitoring $monitoring, IpLookupHelper $ipLookupHelper, $action): void
{
$log = [
'bundle' => 'plugin.mauticSocial',
'object' => 'monitoring',
'objectId' => $monitoring->getId(),
'action' => $action,
'details' => ['name' => $monitoring->getTitle()],
'ipAddress' => $ipLookupHelper->getIpAddressFromRequest(),
];
$auditLog = $this->getModel('core.auditlog');
\assert($auditLog instanceof AuditLogModel);
$auditLog->writeToLog($log);
}
}

View File

@@ -0,0 +1,158 @@
<?php
namespace MauticPlugin\MauticSocialBundle\Controller;
use Mautic\CoreBundle\Controller\AbstractStandardFormController;
use Mautic\CoreBundle\Controller\FormController;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class TweetController extends FormController
{
protected function getModelName(): string
{
return 'social.tweet';
}
protected function getJsLoadMethodPrefix(): string
{
return 'socialTweet';
}
protected function getRouteBase(): string
{
return 'mautic_tweet';
}
protected function getSessionBase($objectId = null): string
{
return 'mautic_tweet';
}
protected function getTemplateBase(): string
{
return '@MauticSocial/Tweet';
}
protected function getTranslationBase(): string
{
return 'mautic.integration.Twitter';
}
protected function getPermissionBase(): string
{
return 'mauticSocial:tweets';
}
/**
* Define options to pass to the form when it's being created.
*/
protected function getEntityFormOptions(): array
{
return [
'update_select' => $this->getUpdateSelect(),
'allow_extra_fields' => true,
];
}
/**
* Get updateSelect value from request.
*
* @return string|bool
*/
public function getUpdateSelect()
{
$request = $this->getCurrentRequest();
return ('POST' === $request->getMethod())
? ($request->request->all()['twitter_tweet']['updateSelect'] ?? false)
: $request->get('updateSelect', false);
}
/**
* Set custom form themes, etc.
*
* @param string $action
*/
protected function getFormView(FormInterface $form, $action): FormView
{
return $form->createView();
}
/**
* @param int $page
*/
public function indexAction(Request $request, $page = 1): Response
{
return parent::indexStandard($request, $page);
}
/**
* Generates new form and processes post data.
*
* @return \Symfony\Component\HttpFoundation\JsonResponse|Response
*/
public function newAction(Request $request)
{
return parent::newStandard($request);
}
/**
* Get the template file.
*/
protected function getTemplateName($file): string
{
if (('form.html.twig' === $file) && 1 == $this->getCurrentRequest()->get('modal')) {
return '@MauticSocial/Tweet/form_modal.html.twig';
}
return AbstractStandardFormController::getTemplateName($file);
}
/**
* Generates edit form and processes post data.
*
* @param int $objectId
* @param bool $ignorePost
*
* @return \Symfony\Component\HttpFoundation\JsonResponse|Response
*/
public function editAction(Request $request, $objectId, $ignorePost = false)
{
return parent::editStandard($request, $objectId, $ignorePost);
}
/**
* @param int $objectId
*
* @return \Symfony\Component\HttpFoundation\JsonResponse|\Symfony\Component\HttpFoundation\RedirectResponse|Response
*/
public function cloneAction(Request $request, $objectId)
{
return parent::cloneStandard($request, $objectId);
}
/**
* Deletes the entity.
*
* @param int $objectId
*
* @return \Symfony\Component\HttpFoundation\JsonResponse|\Symfony\Component\HttpFoundation\RedirectResponse
*/
public function deleteAction(Request $request, $objectId)
{
return parent::deleteStandard($request, $objectId);
}
/**
* Deletes a group of entities.
*
* @return \Symfony\Component\HttpFoundation\JsonResponse|\Symfony\Component\HttpFoundation\RedirectResponse
*/
public function batchDeleteAction(Request $request)
{
return parent::batchDeleteStandard($request);
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace MauticPlugin\MauticSocialBundle\DependencyInjection;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
class MauticSocialExtension 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');
}
}

View File

@@ -0,0 +1,97 @@
<?php
namespace MauticPlugin\MauticSocialBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
#[ORM\Table(name: 'monitoring_leads')]
#[ORM\Entity(repositoryClass: LeadRepository::class)]
class Lead
{
/**
* @var Monitoring
*/
private $monitor;
/**
* @var \Mautic\LeadBundle\Entity\Lead
*/
private $lead;
/**
* @var \DateTimeInterface
*/
private $dateAdded;
public static function loadMetadata(ORM\ClassMetadata $metadata): void
{
$builder = new ClassMetadataBuilder($metadata);
$builder->setTable('monitoring_leads')
->setCustomRepositoryClass(LeadRepository::class);
$builder->createManyToOne('monitor', 'Monitoring')
->isPrimaryKey()
->addJoinColumn('monitor_id', 'id', false, false, 'CASCADE')
->build();
$builder->addLead(false, 'CASCADE', true);
$builder->addNamedField('dateAdded', 'datetime', 'date_added');
}
/**
* @return mixed
*/
public function getDateAdded()
{
return $this->dateAdded;
}
/**
* @return $this
*/
public function setDateAdded($dateAdded)
{
$this->dateAdded = $dateAdded;
return $this;
}
/**
* @return mixed
*/
public function getLead()
{
return $this->lead;
}
/**
* @return $this
*/
public function setLead($lead)
{
$this->lead = $lead;
return $this;
}
/**
* @return mixed
*/
public function getMonitor()
{
return $this->monitor;
}
/**
* @return $this
*/
public function setMonitor($monitor)
{
$this->monitor = $monitor;
return $this;
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace MauticPlugin\MauticSocialBundle\Entity;
use Mautic\CoreBundle\Entity\CommonRepository;
/**
* @extends CommonRepository<Lead>
*/
class LeadRepository extends CommonRepository
{
}

View File

@@ -0,0 +1,404 @@
<?php
namespace MauticPlugin\MauticSocialBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
use Mautic\CoreBundle\Entity\FormEntity;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Mapping\ClassMetadata;
class Monitoring extends FormEntity
{
/**
* @var int
*/
private $id;
/**
* @var string
*/
private $title;
/**
* @var string|null
*/
private $description;
/**
* @var \Mautic\CategoryBundle\Entity\Category|null
*/
private $category;
/**
* @var array
*/
private $lists = [];
/**
* @var string|null
*/
private $networkType;
/**
* @var int
*/
private $revision = 1;
/**
* @var array
*/
private $stats = [];
/**
* @var array
*/
private $properties = [];
/**
* @var \DateTimeInterface
*/
private $publishDown;
/**
* @var \DateTimeInterface
*/
private $publishUp;
public static function loadMetadata(ORM\ClassMetadata $metadata): void
{
$builder = new ClassMetadataBuilder($metadata);
$builder->setTable('monitoring')
->setCustomRepositoryClass(MonitoringRepository::class)
->addLifecycleEvent('cleanMonitorData', 'preUpdate')
->addLifecycleEvent('cleanMonitorData', 'prePersist');
$builder->addCategory();
$builder->addIdColumns('title');
$builder->addNullableField('lists', 'array');
$builder->addNamedField('networkType', 'string', 'network_type', true);
$builder->addField('revision', 'integer');
$builder->addNullableField('stats', 'array');
$builder->addNullableField('properties', 'array');
$builder->addPublishDates();
}
/**
* Constraints for required fields.
*/
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
$metadata->addPropertyConstraint('title', new Assert\NotBlank(
['message' => 'mautic.core.title.required']
));
$metadata->addPropertyConstraint('networkType', new Assert\NotBlank(
['message' => 'mautic.social.network.type']
));
}
/**
* @return mixed
*/
public function getCategory()
{
return $this->category;
}
/**
* Get description.
*
* @return string
*/
public function getDescription()
{
return $this->description;
}
/**
* Get id.
*
* @return int
*/
public function getId()
{
return $this->id;
}
/**
* Get lists.
*
* @return array
*/
public function getLists()
{
return $this->lists;
}
/**
* Get network type.
*
* @return string
*/
public function getNetworkType()
{
return $this->networkType;
}
/**
* Get revision.
*
* @return int
*/
public function getRevision()
{
return $this->revision;
}
/**
* Get statistics.
*
* @return array
*/
public function getStats()
{
return $this->stats;
}
/**
* Get title.
*
* @return string
*/
public function getTitle()
{
return $this->title;
}
/**
* Get properties.
*
* @return array
*/
public function getProperties()
{
return $this->properties;
}
/**
* Get publishDown.
*
* @return \DateTimeInterface
*/
public function getPublishDown()
{
return $this->publishDown;
}
/**
* Get publishUp.
*
* @return \DateTimeInterface
*/
public function getPublishUp()
{
return $this->publishUp;
}
/**
* Set the category id.
*
* @param \Mautic\CategoryBundle\Entity\Category|null $category
*/
public function setCategory($category): void
{
$this->isChanged('category', $category);
$this->category = $category;
}
/**
* Set description.
*
* @param string $description
*
* @return Monitoring
*/
public function setDescription($description)
{
$this->isChanged('description', $description);
$this->description = $description;
return $this;
}
/**
* Set the monitor lists.
*
* @return Monitoring
*/
public function setLists($lists)
{
$this->isChanged('lists', $lists);
$this->lists = $lists;
return $this;
}
/**
* Set the network type.
*
* @return Monitoring
*/
public function setNetworkType($networkType)
{
$this->isChanged('networkType', $networkType);
$this->networkType = $networkType;
return $this;
}
/**
* Set the revision counter.
*
* @param int $revision
*
* @return Monitoring
*/
public function setRevision($revision)
{
$this->isChanged('revision', $revision);
$this->revision = $revision;
return $this;
}
/**
* Set the statistics.
*
* @param array $stats
*
* @return Monitoring
*/
public function setStats($stats)
{
$this->isChanged('stats', $stats);
$this->stats = $stats;
return $this;
}
/**
* Set name.
*
* @param string $title
*
* @return Monitoring
*/
public function setTitle($title)
{
$this->isChanged('title', $title);
$this->title = $title;
return $this;
}
/**
* Set properties.
*
* @param array $properties
*
* @return Monitoring
*/
public function setProperties($properties)
{
$this->isChanged('properties', $properties);
$this->properties = $properties;
return $this;
}
/**
* Set publishDown.
*
* @param \DateTime $publishDown
*
* @return Monitoring
*/
public function setPublishDown($publishDown)
{
$this->isChanged('publishDown', $publishDown);
$this->publishDown = $publishDown;
return $this;
}
/**
* Set publishUp.
*
* @param \DateTime $publishUp
*
* @return Monitoring
*/
public function setPublishUp($publishUp)
{
$this->isChanged('publishUp', $publishUp);
$this->publishUp = $publishUp;
return $this;
}
/**
* Clear out old properties data.
*/
public function cleanMonitorData(): void
{
$property = $this->getProperties();
if (!array_key_exists('checknames', $property)) {
$property['checknames'] = 0;
}
// clean up property array for the twitter handle
if ('twitter_handle' == $this->getNetworkType()) {
$this->setProperties(
[
'handle' => $property['handle'],
'checknames' => $property['checknames'],
]
);
}
// clean up property array for the hashtag
if ('twitter_hashtag' == $this->getNetworkType()) {
$this->setProperties(
[
'hashtag' => $property['hashtag'],
'checknames' => $property['checknames'],
]
);
}
// clean up clean up property array for the custom action
if ('twitter_custom' == $this->getNetworkType()) {
$this->setProperties(
[
'custom' => $property['custom'],
]
);
}
// if the property is not new and the old property doesn't match the new one
if (!$this->isNew() && $property != $this->getProperties()) {
// reset stats on save of edited
$this->setStats([]);
}
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace MauticPlugin\MauticSocialBundle\Entity;
use Doctrine\ORM\Tools\Pagination\Paginator;
use Mautic\CoreBundle\Entity\CommonRepository;
/**
* @extends CommonRepository<Monitoring>
*/
class MonitoringRepository extends CommonRepository
{
/**
* @param array $args
*
* @return Paginator
*/
public function getPublishedEntities($args = [])
{
$q = $this->createQueryBuilder($this->getTableAlias());
$expr = $this->getPublishedByDateExpression($q);
$q->where($expr);
$args['qb'] = $q;
return parent::getEntities($args);
}
public function getPublishedEntitiesCount(): int
{
$q = $this->createQueryBuilder($this->getTableAlias());
$expr = $this->getPublishedByDateExpression($q);
$q->where($expr);
$args['qb'] = $q;
return count(parent::getEntities($args));
}
/**
* @param \Doctrine\ORM\QueryBuilder|\Doctrine\DBAL\Query\QueryBuilder $q
*/
protected function addCatchAllWhereClause($q, $filter): array
{
return $this->addStandardCatchAllWhereClause(
$q,
$filter,
[
$this->getTableAlias().'.title',
$this->getTableAlias().'.description',
]
);
}
/**
* @param \Doctrine\ORM\QueryBuilder|\Doctrine\DBAL\Query\QueryBuilder $q
*/
protected function addSearchCommandWhereClause($q, $filter): array
{
return $this->addStandardSearchCommandWhereClause($q, $filter);
}
public function getTableAlias(): string
{
return 'e';
}
/**
* @return string[]
*/
public function getSearchCommands(): array
{
return $this->getStandardSearchCommands();
}
}

View File

@@ -0,0 +1,113 @@
<?php
namespace MauticPlugin\MauticSocialBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
class PostCount
{
/**
* @var int
*/
private $id;
/**
* @var Monitoring|null
*/
private $monitor;
/**
* @var \DateTimeInterface
*/
private $postDate;
/**
* @var int
*/
private $postCount;
public static function loadMetadata(ORM\ClassMetadata $metadata): void
{
$builder = new ClassMetadataBuilder($metadata);
$builder->setTable('monitor_post_count')
->setCustomRepositoryClass(PostCountRepository::class);
$builder->addId();
$builder->createManyToOne('monitor', 'Monitoring')
->addJoinColumn('monitor_id', 'id', true, false, 'CASCADE')
->build();
$builder->addNamedField('postDate', 'date', 'post_date');
$builder->addNamedField('postCount', 'integer', 'post_count');
}
/**
* @return mixed
*/
public function getId()
{
return $this->id;
}
/**
* @return Monitoring
*/
public function getMonitor()
{
return $this->monitor;
}
/**
* @param Monitoring $monitor
*
* @return $this
*/
public function setMonitor($monitor)
{
$this->monitor = $monitor;
return $this;
}
/**
* @return int
*/
public function getPostCount()
{
return $this->postCount;
}
/**
* @param int $postCount
*
* @return $this
*/
public function setPostCount($postCount)
{
$this->postCount = $postCount;
return $this;
}
/**
* @return \DateTimeInterface
*/
public function getPostDate()
{
return $this->postDate;
}
/**
* @return $this
*/
public function setPostDate($postDate)
{
$this->postDate = $postDate;
return $this;
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace MauticPlugin\MauticSocialBundle\Entity;
use Mautic\CoreBundle\Entity\CommonRepository;
use Mautic\CoreBundle\Helper\Chart\ChartQuery;
/**
* @extends CommonRepository<PostCount>
*/
class PostCountRepository extends CommonRepository
{
/**
* Fetch Lead stats for some period of time.
*
* @param array $options
*
* @return PostCount[]
*
* @throws \Doctrine\ORM\NoResultException
* @throws \Doctrine\ORM\NonUniqueResultException
*/
public function getLeadStatsPost($dateFrom, $dateTo, $options): array
{
$chartQuery = new ChartQuery($this->getEntityManager()->getConnection(), $dateFrom, $dateTo);
// Load points for selected periods
$q = $chartQuery->prepareTimeDataQuery(MAUTIC_TABLE_PREFIX.'monitor_post_count', 'post_date', $options, 'post_count', 'sum');
if (isset($options['monitor_id'])) {
$q->andwhere($q->expr()->eq('t.monitor_id', (int) $options['monitor_id']));
}
return $chartQuery->loadAndBuildTimeData($q);
}
}

View File

@@ -0,0 +1,475 @@
<?php
namespace MauticPlugin\MauticSocialBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
use Mautic\AssetBundle\Entity\Asset;
use Mautic\CategoryBundle\Entity\Category;
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
use Mautic\CoreBundle\Entity\FormEntity;
use Mautic\PageBundle\Entity\Page;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Mapping\ClassMetadata;
#[ORM\Table(name: 'tweets')]
#[ORM\Entity(repositoryClass: TweetRepository::class)]
class Tweet extends FormEntity
{
/**
* Internal Mautic ID of the tweet.
*
* @var int
*/
private $id;
/**
* ID of the Twitter media object attached to the tweet.
*
* @var string|null
*/
private $mediaId;
/**
* Path to the local media file.
*
* @var string|null
*/
private $mediaPath;
/**
* Internal Mautic name of the tweet.
*
* @var string
*/
private $name;
/**
* The actual messge of the tweet.
*
* @var string
*/
private $text;
/**
* Internal Mautic description.
*
* @var string|null
*/
private $description;
/**
* @var string|null
*/
private $language = 'en';
/**
* @var int|null
*/
private $sentCount = 0;
/**
* @var int|null
*/
private $favoriteCount = 0;
/**
* @var int|null
*/
private $retweetCount = 0;
/**
* @var Page|null
*/
private $page;
/**
* @var Asset|null
*/
private $asset;
/**
* @var Category|null
**/
private $category;
/**
* @var ArrayCollection<int, TweetStat>
*/
private $stats;
public function __construct()
{
$this->stats = new ArrayCollection();
}
public function __clone()
{
$this->id = null;
$this->sentCount = 0;
$this->favoriteCount = 0;
$this->retweetCount = 0;
$this->stats = new ArrayCollection();
parent::__clone();
}
public static function loadMetadata(ORM\ClassMetadata $metadata): void
{
$builder = new ClassMetadataBuilder($metadata);
$builder->setTable('tweets')
->setCustomRepositoryClass(TweetRepository::class)
->addIndex(['sent_count'], 'sent_count_index')
->addIndex(['favorite_count'], 'favorite_count_index')
->addIndex(['retweet_count'], 'retweet_count_index');
$builder->addIdColumns();
$builder->addCategory();
$builder->addNullableField('mediaId', Types::STRING, 'media_id');
$builder->addNullableField('mediaPath', Types::STRING, 'media_path');
$builder->addField('text', Types::STRING, ['length' => 280]);
$builder->addNullableField('sentCount', Types::INTEGER, 'sent_count');
$builder->addNullableField('favoriteCount', Types::INTEGER, 'favorite_count');
$builder->addNullableField('retweetCount', Types::INTEGER, 'retweet_count');
$builder->addNullableField('language', Types::STRING, 'lang');
$builder->createManyToOne('page', Page::class)
->addJoinColumn('page_id', 'id', true, false, 'SET NULL')
->build();
$builder->createManyToOne('asset', Asset::class)
->addJoinColumn('asset_id', 'id', true, false, 'SET NULL')
->build();
$builder->createOneToMany('stats', 'TweetStat')
->setIndexBy('id')
->mappedBy('tweet')
->cascadePersist()
->fetchExtraLazy()
->build();
}
/**
* Prepares the metadata for API usage.
*/
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
{
$metadata->setGroupPrefix('tweet')
->addListProperties(
[
'id',
'name',
'text',
'language',
'category',
]
)
->addProperties(
[
'mediaId',
'mediaPath',
'sentCount',
'favoriteCount',
'retweetCount',
'description',
]
)
->build();
}
/**
* Constraints for required fields.
*/
public static function loadValidatorMetadata(ClassMetadata $metadata): void
{
$metadata->addPropertyConstraint('text', new Assert\Length(
[
'max' => 280,
]
));
}
/**
* @return int|null
*/
public function getId()
{
return $this->id;
}
/**
* @param int $id
*
* @return $this
*/
public function setId($id)
{
$this->id = $id;
return $this;
}
/**
* @return string
*/
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|null $description
*
* @return $this
*/
public function setDescription($description)
{
$this->isChanged('description', $description);
$this->description = $description;
return $this;
}
/**
* @return string|null
*/
public function getMediaId()
{
return $this->mediaId;
}
/**
* @param string $mediaId
*
* @return $this
*/
public function setMediaId($mediaId)
{
$this->isChanged('mediaId', $mediaId);
$this->mediaId = $mediaId;
return $this;
}
/**
* @return string|null
*/
public function getMediaPath()
{
return $this->mediaPath;
}
/**
* @param string $mediaPath
*
* @return $this
*/
public function setMediaPath($mediaPath)
{
$this->isChanged('mediaPath', $mediaPath);
$this->mediaPath = $mediaPath;
return $this;
}
/**
* @return string
*/
public function getText()
{
return $this->text;
}
/**
* @param string $text
*
* @return $this
*/
public function setText($text)
{
$this->isChanged('text', $text);
$this->text = $text;
return $this;
}
/**
* @return int|null
*/
public function getSentCount()
{
return $this->sentCount;
}
/**
* @return $this
*/
public function setSentCount($sentCount)
{
$this->isChanged('sentCount', $sentCount);
$this->sentCount = $sentCount;
return $this;
}
/**
* Add 1 to sentCount.
*
* @return $this
*/
public function sentCountUp()
{
$this->setSentCount($this->getSentCount() + 1);
return $this;
}
/**
* @return int
*/
public function getFavoriteCount()
{
return $this->favoriteCount;
}
/**
* @param int $favoriteCount
*
* @return $this
*/
public function setFavoriteCount($favoriteCount)
{
$this->isChanged('favoriteCount', $favoriteCount);
$this->favoriteCount = $favoriteCount;
return $this;
}
/**
* @return int
*/
public function getRetweetCount()
{
return $this->retweetCount;
}
/**
* @param int $retweetCount
*
* @return $this
*/
public function setRetweetCount($retweetCount)
{
$this->isChanged('retweetCount', $retweetCount);
$this->retweetCount = $retweetCount;
return $this;
}
/**
* @return string
*/
public function getLanguage()
{
return $this->language;
}
/**
* @param string $language
*
* @return $this
*/
public function setLanguage($language)
{
$this->isChanged('language', $language);
$this->language = $language;
return $this;
}
/**
* @return Asset|null
*/
public function getAsset()
{
return $this->asset;
}
/**
* @return $this
*/
public function setAsset(Asset $asset)
{
$this->asset = $asset;
return $this;
}
/**
* @return Page|null
*/
public function getPage()
{
return $this->page;
}
/**
* @return $this
*/
public function setPage(Page $page)
{
$this->page = $page;
return $this;
}
/**
* @return Category|null
*/
public function getCategory()
{
return $this->category;
}
/**
* @return $this
*/
public function setCategory(Category $category)
{
$this->category = $category;
return $this;
}
/**
* @return mixed
*/
public function getStats()
{
return $this->stats;
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace MauticPlugin\MauticSocialBundle\Entity;
use Mautic\CoreBundle\Entity\CommonRepository;
/**
* @extends CommonRepository<Tweet>
*/
class TweetRepository extends CommonRepository
{
/**
* @param string $search
* @param int $limit
* @param int $start
* @param bool $viewOther
*
* @return array
*/
public function getTweetList($search = '', $limit = 10, $start = 0, $viewOther = false, array $ignoreIds = [])
{
$qb = $this->createQueryBuilder('t');
$qb->select('partial t.{id, text, name, language}');
if (!empty($search)) {
if (is_array($search)) {
$search = array_map('intval', $search);
$qb->andWhere($qb->expr()->in('t.id', ':search'))
->setParameter('search', $search);
} else {
$qb->andWhere($qb->expr()->like('t.name', ':search'))
->setParameter('search', "%{$search}%");
}
}
if (!$viewOther) {
$qb->andWhere($qb->expr()->eq('t.createdBy', ':id'))
->setParameter('id', $this->currentUser->getId());
}
if (!empty($ignoreIds)) {
$qb->andWhere($qb->expr()->notIn('t.id', ':ignoreIds'))
->setParameter('ignoreIds', $ignoreIds);
}
$qb->orderBy('t.name');
if (!empty($limit)) {
$qb->setFirstResult($start)
->setMaxResults($limit);
}
return $qb->getQuery()->getArrayResult();
}
}

View File

@@ -0,0 +1,395 @@
<?php
namespace MauticPlugin\MauticSocialBundle\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\LeadBundle\Entity\Lead as TheLead;
class TweetStat
{
/**
* @var int
*/
private $id;
/**
* ID of the tweet from Twitter.
*
* @var string|null
*/
private $twitterTweetId;
/**
* @var Tweet|null
*/
private $tweet;
/**
* @var TheLead|null
*/
private $lead;
/**
* @var string
*/
private $handle;
/**
* @var DateTime
*/
private $dateSent;
/**
* @var bool|null
*/
private $isFailed = false;
/**
* @var int|null
*/
private $retryCount = 0;
/**
* @var string|null
*/
private $source;
/**
* @var int|null
*/
private $sourceId;
/**
* @var int|null
*/
private $favoriteCount = 0;
/**
* @var int|null
*/
private $retweetCount = 0;
/**
* @var array|null
*/
private $responseDetails = [];
public static function loadMetadata(ORM\ClassMetadata $metadata): void
{
$builder = new ClassMetadataBuilder($metadata);
$builder->setTable('tweet_stats')
->setCustomRepositoryClass(TweetStatRepository::class)
->addIndex(['tweet_id', 'lead_id'], 'stat_tweet_search')
->addIndex(['lead_id', 'tweet_id'], 'stat_tweet_search2')
->addIndex(['is_failed'], 'stat_tweet_failed_search')
->addIndex(['source', 'source_id'], 'stat_tweet_source_search')
->addIndex(['favorite_count'], 'favorite_count_index')
->addIndex(['retweet_count'], 'retweet_count_index')
->addIndex(['date_sent'], 'tweet_date_sent')
->addIndex(['twitter_tweet_id'], 'twitter_tweet_id_index');
$builder->addId();
$builder->createManyToOne('tweet', 'Tweet')
->inversedBy('stats')
->addJoinColumn('tweet_id', 'id', true, false, 'SET NULL')
->build();
$builder->createField('twitterTweetId', 'string')
->columnName('twitter_tweet_id')
->nullable()
->build();
$builder->addLead(true, 'SET NULL');
$builder->createField('handle', 'string')
->build();
$builder->createField('dateSent', 'datetime')
->columnName('date_sent')
->nullable()
->build();
$builder->createField('isFailed', 'boolean')
->columnName('is_failed')
->nullable()
->build();
$builder->createField('retryCount', 'integer')
->columnName('retry_count')
->nullable()
->build();
$builder->createField('source', 'string')
->nullable()
->build();
$builder->createField('sourceId', 'integer')
->columnName('source_id')
->nullable()
->build();
$builder->addNullableField('favoriteCount', 'integer', 'favorite_count');
$builder->addNullableField('retweetCount', 'integer', 'retweet_count');
$builder->addNullableField('responseDetails', Types::JSON, 'response_details');
}
/**
* Prepares the metadata for API usage.
*/
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
{
$metadata->setGroupPrefix('stat')
->addProperties(
[
'id',
'tweetId',
'handle',
'dateSent',
'isFailed',
'retryCount',
'favoriteCount',
'retweetCount',
'source',
'sourceId',
'lead',
'tweet',
'responseDetails',
]
)
->build();
}
/**
* @return mixed
*/
public function getId()
{
return $this->id;
}
/**
* @return string|null
*/
public function getTwitterTweetId()
{
return $this->twitterTweetId;
}
/**
* @param string $twitterTweetId
*
* @return $this
*/
public function setTwitterTweetId($twitterTweetId)
{
$this->twitterTweetId = $twitterTweetId;
return $this;
}
/**
* @return mixed
*/
public function getDateSent()
{
return $this->dateSent;
}
/**
* @param mixed $dateSent
*/
public function setDateSent($dateSent): void
{
$this->dateSent = $dateSent;
}
/**
* @return Tweet
*/
public function getTweet()
{
return $this->tweet;
}
/**
* @param mixed $tweet
*/
public function setTweet(?Tweet $tweet = null): void
{
$this->tweet = $tweet;
}
/**
* @return TheLead
*/
public function getLead()
{
return $this->lead;
}
/**
* @param mixed $lead
*/
public function setLead(?TheLead $lead = null): void
{
$this->lead = $lead;
}
/**
* @return mixed
*/
public function getRetryCount()
{
return $this->retryCount;
}
/**
* @param mixed $retryCount
*/
public function setRetryCount($retryCount): void
{
$this->retryCount = $retryCount;
}
public function retryCountUp(): void
{
$this->setRetryCount($this->getRetryCount() + 1);
}
/**
* @return int
*/
public function getFavoriteCount()
{
return $this->favoriteCount;
}
/**
* @param int $favoriteCount
*
* @return $this
*/
public function setFavoriteCount($favoriteCount)
{
$this->favoriteCount = $favoriteCount;
return $this;
}
/**
* @return int
*/
public function getRetweetCount()
{
return $this->retweetCount;
}
/**
* @param int $retweetCount
*
* @return $this
*/
public function setRetweetCount($retweetCount)
{
$this->retweetCount = $retweetCount;
return $this;
}
/**
* @return mixed
*/
public function getIsFailed()
{
return $this->isFailed;
}
/**
* @param mixed $isFailed
*/
public function setIsFailed($isFailed): void
{
$this->isFailed = $isFailed;
}
/**
* @return mixed
*/
public function isFailed()
{
return $this->getIsFailed();
}
/**
* @return string|null
*/
public function getHandle()
{
return $this->handle;
}
/**
* @param mixed $handle
*/
public function setHandle($handle): void
{
$this->handle = $handle;
}
/**
* @return mixed
*/
public function getSource()
{
return $this->source;
}
/**
* @param mixed $source
*/
public function setSource($source): void
{
$this->source = $source;
}
/**
* @return mixed
*/
public function getSourceId()
{
return $this->sourceId;
}
/**
* @param mixed $sourceId
*/
public function setSourceId($sourceId): void
{
$this->sourceId = (int) $sourceId;
}
/**
* @return mixed
*/
public function getResponseDetails()
{
return $this->responseDetails;
}
/**
* @param mixed $responseDetails
*
* @return Stat
*/
public function setResponseDetails($responseDetails)
{
$this->responseDetails = $responseDetails;
return $this;
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace MauticPlugin\MauticSocialBundle\Entity;
use Mautic\CoreBundle\Entity\CommonRepository;
/**
* @extends CommonRepository<TweetStat>
*/
class TweetStatRepository extends CommonRepository
{
}

View File

@@ -0,0 +1,36 @@
<?php
namespace MauticPlugin\MauticSocialBundle\Event;
use Mautic\CoreBundle\Event\CommonEvent;
use MauticPlugin\MauticSocialBundle\Entity\Monitoring;
class SocialEvent extends CommonEvent
{
/**
* @param bool $isNew
*/
public function __construct(Monitoring $monitoring, $isNew = false)
{
$this->entity = $monitoring;
$this->isNew = $isNew;
}
/**
* Returns the Monitoring entity.
*
* @return Monitoring
*/
public function getMonitoring()
{
return $this->entity;
}
/**
* Sets the Monitoring entity.
*/
public function setMonitoring(Monitoring $monitoring): void
{
$this->entity = $monitoring;
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace MauticPlugin\MauticSocialBundle\Event;
use Mautic\CoreBundle\Event\CommonEvent;
use MauticPlugin\MauticSocialBundle\Entity\Monitoring;
class SocialMonitorEvent extends CommonEvent
{
protected int $newLeadCount;
protected int $updatedLeadCount;
/**
* @param string $integrationName
* @param int $newLeadCount
* @param int $updatedLeadCount
*/
public function __construct(
protected $integrationName,
Monitoring $monitoring,
protected array $leadIds,
$newLeadCount,
$updatedLeadCount,
) {
$this->entity = $monitoring;
$this->newLeadCount = (int) $newLeadCount;
$this->updatedLeadCount = (int) $updatedLeadCount;
}
/**
* Returns the Monitoring entity.
*
* @return Monitoring
*/
public function getMonitoring()
{
return $this->entity;
}
/**
* Get count of new leads.
*/
public function getNewLeadCount(): int
{
return $this->newLeadCount;
}
/**
* Get count of updated leads.
*/
public function getUpdatedLeadCount(): int
{
return $this->updatedLeadCount;
}
public function getTotalLeadCount(): int
{
return $this->updatedLeadCount + $this->newLeadCount;
}
/**
* @return array
*/
public function getLeadIds()
{
return $this->leadIds;
}
/**
* @return mixed
*/
public function getIntegrationName()
{
return $this->integrationName;
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace MauticPlugin\MauticSocialBundle\EventListener;
use Mautic\CampaignBundle\CampaignEvents;
use Mautic\CampaignBundle\Event\CampaignBuilderEvent;
use Mautic\CampaignBundle\Event\CampaignExecutionEvent;
use Mautic\PluginBundle\Helper\IntegrationHelper;
use MauticPlugin\MauticSocialBundle\Form\Type\TweetSendType;
use MauticPlugin\MauticSocialBundle\Helper\CampaignEventHelper;
use MauticPlugin\MauticSocialBundle\SocialEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class CampaignSubscriber implements EventSubscriberInterface
{
public function __construct(
private CampaignEventHelper $campaignEventHelper,
private IntegrationHelper $integrationHelper,
private TranslatorInterface $translator,
) {
}
public static function getSubscribedEvents(): array
{
return [
CampaignEvents::CAMPAIGN_ON_BUILD => ['onCampaignBuild', 0],
SocialEvents::ON_CAMPAIGN_TRIGGER_ACTION => ['onCampaignAction', 0],
];
}
public function onCampaignBuild(CampaignBuilderEvent $event): void
{
$integration = $this->integrationHelper->getIntegrationObject('Twitter');
if ($integration && $integration->getIntegrationSettings()->isPublished()) {
$action = [
'label' => 'mautic.social.twitter.tweet.event.open',
'description' => 'mautic.social.twitter.tweet.event.open_desc',
'eventName' => SocialEvents::ON_CAMPAIGN_TRIGGER_ACTION,
'formTypeOptions' => ['update_select' => 'campaignevent_properties_channelId'],
'formType' => TweetSendType::class,
'channel' => 'social.tweet',
'channelIdField' => 'channelId',
];
$event->addAction('twitter.tweet', $action);
}
}
public function onCampaignAction(CampaignExecutionEvent $event)
{
$event->setChannel('social.twitter');
if ($response = $this->campaignEventHelper->sendTweetAction($event->getLead(), $event->getEvent())) {
return $event->setResult($response);
}
return $event->setFailed(
$this->translator->trans('mautic.social.twitter.error.handle_not_found')
);
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace MauticPlugin\MauticSocialBundle\EventListener;
use Mautic\ChannelBundle\ChannelEvents;
use Mautic\ChannelBundle\Event\ChannelEvent;
use Mautic\ChannelBundle\Model\MessageModel;
use Mautic\PluginBundle\Helper\IntegrationHelper;
use MauticPlugin\MauticSocialBundle\Form\Type\TweetListType;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class ChannelSubscriber implements EventSubscriberInterface
{
public function __construct(
private IntegrationHelper $helper,
) {
}
public static function getSubscribedEvents(): array
{
return [
ChannelEvents::ADD_CHANNEL => ['onAddChannel', 80],
];
}
public function onAddChannel(ChannelEvent $event): void
{
$integration = $this->helper->getIntegrationObject('Twitter');
if ($integration && $integration->getIntegrationSettings()->isPublished()) {
$event->addChannel(
'tweet',
[
MessageModel::CHANNEL_FEATURE => [
'campaignAction' => 'twitter.tweet',
'campaignDecisionsSupported' => [
'page.pagehit',
'asset.download',
'form.submit',
],
'lookupFormType' => TweetListType::class,
'repository' => 'MauticSocialBundle:Tweet',
],
]
);
}
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace MauticPlugin\MauticSocialBundle\EventListener;
use Mautic\ConfigBundle\ConfigEvents;
use Mautic\ConfigBundle\Event\ConfigBuilderEvent;
use Mautic\ConfigBundle\Event\ConfigEvent;
use MauticPlugin\MauticSocialBundle\Form\Type\ConfigType;
use MauticPlugin\MauticSocialBundle\Integration\Config;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class ConfigSubscriber implements EventSubscriberInterface
{
public function __construct(private Config $config)
{
}
public static function getSubscribedEvents(): array
{
return [
ConfigEvents::CONFIG_ON_GENERATE => ['onConfigGenerate', 0],
ConfigEvents::CONFIG_PRE_SAVE => ['onConfigSave', 0],
];
}
public function onConfigGenerate(ConfigBuilderEvent $event): void
{
if (!$this->config->isPublished()) {
return;
}
$event->addForm(
[
'formAlias' => 'social_config',
'formTheme' => '@MauticSocial/FormTheme/Config/_config_social_config_widget.html.twig',
'formType' => ConfigType::class,
'parameters' => $event->getParametersFromConfig('MauticSocialBundle'),
]
);
}
public function onConfigSave(ConfigEvent $event): void
{
/** @var array $values */
$values = $event->getConfig();
// Manipulate the values
if (!empty($values['social_config']['twitter_handle_field'])) {
$values['social_config']['twitter_handle_field'] = htmlspecialchars($values['social_config']['twitter_handle_field']);
}
// Set updated values
$event->setConfig($values);
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace MauticPlugin\MauticSocialBundle\EventListener;
use Mautic\FormBundle\Event\FormBuilderEvent;
use Mautic\FormBundle\FormEvents;
use MauticPlugin\MauticSocialBundle\Form\Type\SocialLoginType;
use MauticPlugin\MauticSocialBundle\Integration\Config;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class FormSubscriber implements EventSubscriberInterface
{
public function __construct(private Config $config)
{
}
public static function getSubscribedEvents(): array
{
return [
FormEvents::FORM_ON_BUILD => ['onFormBuild', 0],
];
}
public function onFormBuild(FormBuilderEvent $event): void
{
if (!$this->config->isPublished()) {
return;
}
$action = [
'label' => 'mautic.plugin.actions.socialLogin',
'formType' => SocialLoginType::class,
'template' => '@MauticSocial/Integration/login.html.twig',
'builderOptions' => [
'addLeadFieldList' => false,
'addIsRequired' => false,
'addDefaultValue' => false,
'addSaveResult' => false,
],
];
$event->addFormField('plugin.loginSocial', $action);
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace MauticPlugin\MauticSocialBundle\EventListener;
use Doctrine\ORM\EntityManager;
use Mautic\CoreBundle\EventListener\CommonStatsSubscriber;
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
use MauticPlugin\MauticSocialBundle\Entity\TweetStat;
use MauticPlugin\MauticSocialBundle\Entity\TweetStatRepository;
class StatsSubscriber extends CommonStatsSubscriber
{
public function __construct(CorePermissions $security, EntityManager $entityManager)
{
parent::__construct($security, $entityManager);
/** @var TweetStatRepository $repo */
$repo = $entityManager->getRepository(TweetStat::class);
$table = $repo->getTableName();
$this->repositories[] = $repo;
$this->permissions[$table] = ['tweet' => 'mauticSocial:tweets'];
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace MauticPlugin\MauticSocialBundle\Exception;
class ExitMonitorException extends \Exception
{
public function __construct($message = 'Exit monitor requested', $code = 0, ?\Exception $previous = null)
{
parent::__construct($message, $code, $previous);
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace MauticPlugin\MauticSocialBundle\Form\Type;
use Mautic\LeadBundle\Field\FieldList;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
/**
* @extends AbstractType<array<mixed>>
*/
class ConfigType extends AbstractType
{
public function __construct(
private FieldList $fieldList,
) {
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$leadFields = $this->fieldList->getFieldList(false, false);
$builder->add(
'twitter_handle_field',
ChoiceType::class,
[
'choices' => array_flip($leadFields),
'label' => 'mautic.social.config.twitter.field.label',
'required' => false,
'label_attr' => ['class' => 'control-label'],
'attr' => ['class' => 'form-control'],
]
);
}
public function getBlockPrefix(): string
{
return 'social_config';
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace MauticPlugin\MauticSocialBundle\Form\Type;
use Mautic\CoreBundle\Form\Type\YesNoButtonGroupType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
/**
* @extends AbstractType<array<mixed>>
*/
class FacebookType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('layout', ChoiceType::class, [
'choices' => [
'mautic.integration.Facebook.share.layout.standard' => 'standard',
'mautic.integration.Facebook.share.layout.buttoncount' => 'button_count',
'mautic.integration.Facebook.share.layout.button' => 'button',
'mautic.integration.Facebook.share.layout.boxcount' => 'box_count',
'mautic.integration.Facebook.share.layout.icon' => 'icon',
],
'label' => 'mautic.integration.Facebook.share.layout',
'required' => false,
'placeholder' => false,
'label_attr' => ['class' => 'control-label'],
'attr' => ['class' => 'form-control'],
]);
$builder->add('action', ChoiceType::class, [
'choices' => [
'mautic.integration.Facebook.share.action.like' => 'like',
'mautic.integration.Facebook.share.action.recommend' => 'recommend',
'mautic.integration.Facebook.share.action.share' => 'share',
],
'label' => 'mautic.integration.Facebook.share.action',
'required' => false,
'placeholder' => false,
'label_attr' => ['class' => 'control-label'],
'attr' => ['class' => 'form-control'],
]);
$builder->add('showFaces', YesNoButtonGroupType::class, [
'label' => 'mautic.integration.Facebook.share.showfaces',
'data' => (!isset($options['data']['showFaces'])) ? 1 : $options['data']['showFaces'],
]);
$builder->add('showShare', YesNoButtonGroupType::class, [
'label' => 'mautic.integration.Facebook.share.showshare',
'data' => (!isset($options['data']['showShare'])) ? 1 : $options['data']['showShare'],
]);
}
public function getBlockPrefix(): string
{
return 'socialmedia_facebook';
}
}

View File

@@ -0,0 +1,110 @@
<?php
namespace MauticPlugin\MauticSocialBundle\Form\Type;
use Mautic\CategoryBundle\Form\Type\CategoryListType;
use Mautic\CoreBundle\Form\EventListener\CleanFormSubscriber;
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 MauticPlugin\MauticSocialBundle\Model\MonitoringModel;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @extends AbstractType<array<mixed>>
*/
class MonitoringType extends AbstractType
{
public function __construct(
private MonitoringModel $monitoringModel,
) {
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->addEventSubscriber(new CleanFormSubscriber(['description' => 'html']));
$builder->add('title', TextType::class, [
'label' => 'mautic.core.name',
'label_attr' => ['class' => 'control-label'],
'attr' => ['class' => 'form-control'],
]);
$builder->add('description', TextareaType::class, [
'label' => 'mautic.core.description',
'label_attr' => ['class' => 'control-label'],
'attr' => ['class' => 'form-control editor'],
'required' => false,
]);
$builder->add('isPublished', YesNoButtonGroupType::class);
$builder->add('publishUp', PublishUpDateType::class);
$builder->add('publishDown', PublishDownDateType::class);
$builder->add('networkType', ChoiceType::class, [
'label' => 'mautic.social.monitoring.type.list',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
'onchange' => 'Mautic.getNetworkFormAction(this)',
],
'choices' => array_flip((array) $options['networkTypes']), // passed from the controller
'placeholder' => 'mautic.core.form.chooseone',
]);
// if we have a network type value add in the form
if (!empty($options['networkType']) && array_key_exists($options['networkType'], $options['networkTypes'])) {
// get the values from the entity function
$properties = $options['data']->getProperties();
$formType = $this->monitoringModel->getFormByType($options['networkType']);
$builder->add('properties', $formType,
[
'label' => false,
'data' => $properties,
]
);
}
$builder->add(
'lists',
LeadListType::class,
[
'label' => 'mautic.lead.lead.events.addtolists',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
],
'multiple' => true,
'expanded' => false,
]
);
// add category
$builder->add('category', CategoryListType::class, [
'bundle' => 'plugin:mauticSocial',
]);
$builder->add('buttons', FormButtonsType::class);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => \MauticPlugin\MauticSocialBundle\Entity\Monitoring::class,
]);
// allow network types to be sent through - list
$resolver->setRequired(['networkTypes']);
// allow the specific network type - single
$resolver->setDefined(['networkType']);
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace MauticPlugin\MauticSocialBundle\Form\Type;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\FormBundle\Model\FormModel;
use Mautic\PluginBundle\Helper\IntegrationHelper;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\FormBuilderInterface;
/**
* @extends AbstractType<array<mixed>>
*/
class SocialLoginType extends AbstractType
{
public function __construct(
private IntegrationHelper $helper,
private FormModel $formModel,
private CoreParametersHelper $coreParametersHelper,
) {
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$integrations = '';
$integrationObjects = $this->helper->getIntegrationObjects(null, 'login_button');
foreach ($integrationObjects as $integrationObject) {
if ($integrationObject->getIntegrationSettings()->isPublished()) {
$model = $this->formModel;
$integrations .= $integrationObject->getName().',';
$integration = [
'integration' => $integrationObject->getName(),
];
$builder->add(
'authUrl_'.$integrationObject->getName(),
HiddenType::class,
[
'data' => $model->buildUrl('mautic_integration_auth_user', $integration, true, []),
]
);
$builder->add(
'buttonImageUrl',
HiddenType::class,
[
'data' => $this->coreParametersHelper->get('site_url').'/'.$this->coreParametersHelper->get('image_path').'/',
]
);
}
}
$builder->add(
'integrations',
HiddenType::class,
[
'data' => $integrations,
]
);
}
public function getBlockPrefix(): string
{
return 'sociallogin';
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace MauticPlugin\MauticSocialBundle\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 TweetListType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults(
[
'modal_route' => 'mautic_tweet_action',
'modal_header' => 'mautic.integration.Twitter.new.tweet',
'model' => 'social.tweet',
'model_lookup_method' => 'getLookupResults',
'lookup_arguments' => fn (Options $options): array => [
'type' => 'tweet',
'filter' => '$data',
'limit' => 0,
'start' => 0,
],
'ajax_lookup_action' => fn (Options $options) => 'mauticSocial:getLookupChoiceList',
'multiple' => true,
'required' => false,
]
);
}
public function getParent(): ?string
{
return EntityLookupType::class;
}
}

View File

@@ -0,0 +1,109 @@
<?php
namespace MauticPlugin\MauticSocialBundle\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 TweetSendType extends AbstractType
{
public function __construct(
protected RouterInterface $router,
) {
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add(
'channelId',
TweetListType::class,
[
'label' => 'mautic.integration.Twitter.send.selecttweet',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
'tooltip' => 'mautic.integration.Twitter.send.selecttweet.desc',
'onchange' => 'Mautic.disabledTweetAction()',
],
'multiple' => false,
'required' => true,
'constraints' => [
new NotBlank(
['message' => 'mautic.integration.Twitter.send.selecttweet.notblank']
),
],
]
);
if (!empty($options['update_select'])) {
$windowUrl = $this->router->generate(
'mautic_tweet_action',
[
'objectAction' => 'new',
'contentOnly' => 1,
'updateSelect' => $options['update_select'],
]
);
$builder->add(
'newTweetButton',
ButtonType::class,
[
'attr' => [
'class' => 'btn btn-primary btn-nospin',
'onclick' => 'Mautic.loadNewWindow({
"windowUrl": "'.$windowUrl.'"
})',
'icon' => 'ri-add-line',
],
'label' => 'mautic.integration.Twitter.new.tweet',
]
);
// $tweet = $options['data']['channelId'];
// create button edit tweet
// @todo: this button requires a JS to be injected to the campaign builder
// $windowUrlEdit = $this->router->generate(
// 'mautic_tweet_action',
// [
// 'objectAction' => 'edit',
// 'objectId' => 'tweetId',
// 'contentOnly' => 1,
// 'updateSelect' => $options['update_select'],
// ]
// );
// $builder->add(
// 'editTweetButton',
// 'button',
// [
// 'attr' => [
// 'class' => 'btn btn-primary btn-nospin',
// 'onclick' => 'Mautic.loadNewWindow(Mautic.standardTweetUrl({"windowUrl": "'.$windowUrlEdit.'"}))',
// 'disabled' => !isset($tweet),
// 'icon' => 'ri-edit-line',
// ],
// 'label' => 'mautic.integration.Twitter.edit.tweet',
// ]
// );
}
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefined(['update_select']);
}
public function getBlockPrefix(): string
{
return 'tweetsend_list';
}
}

View File

@@ -0,0 +1,178 @@
<?php
namespace MauticPlugin\MauticSocialBundle\Form\Type;
use Doctrine\ORM\EntityManager;
use Mautic\AssetBundle\Form\Type\AssetListType;
use Mautic\CategoryBundle\Form\Type\CategoryListType;
use Mautic\CoreBundle\Form\DataTransformer\IdToEntityModelTransformer;
use Mautic\CoreBundle\Form\Type\FormButtonsType;
use Mautic\PageBundle\Form\Type\PageListType;
use MauticPlugin\MauticSocialBundle\Entity\Tweet;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ButtonType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\NotBlank;
/**
* @extends AbstractType<Tweet>
*/
class TweetType extends AbstractType
{
public function __construct(
protected EntityManager $em,
) {
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add(
'name',
TextType::class,
[
'label' => 'mautic.social.monitoring.twitter.tweet.name',
'required' => true,
'label_attr' => ['class' => 'control-label'],
'attr' => [
'tooltip' => 'mautic.social.monitoring.twitter.tweet.name.tooltip',
'class' => 'form-control',
],
'constraints' => [
new NotBlank(
[
'message' => 'mautic.core.name.required',
]
),
],
]
);
$builder->add(
'description',
TextareaType::class,
[
'label' => 'mautic.social.monitoring.twitter.tweet.description',
'required' => false,
'label_attr' => ['class' => 'control-label'],
'attr' => [
'tooltip' => 'mautic.social.monitoring.twitter.tweet.description.tooltip',
'class' => 'form-control',
],
]
);
$builder->add(
'text',
TextareaType::class,
[
'label' => 'mautic.social.monitoring.twitter.tweet.text',
'required' => true,
'label_attr' => ['class' => 'control-label'],
'attr' => [
'tooltip' => 'mautic.social.monitoring.twitter.tweet.text.tooltip',
'class' => 'form-control tweet-message',
],
'constraints' => [
new NotBlank(
[
'message' => 'mautic.core.value.required',
]
),
],
]
);
$transformer = new IdToEntityModelTransformer($this->em, \Mautic\AssetBundle\Entity\Asset::class, 'id');
$builder->add(
$builder->create(
'asset',
AssetListType::class,
[
'label' => 'mautic.social.monitoring.twitter.assets',
'placeholder' => 'mautic.social.monitoring.list.choose',
'label_attr' => ['class' => 'control-label'],
'multiple' => false,
'attr' => [
'class' => 'form-control tweet-insert-asset',
'tooltip' => 'mautic.social.monitoring.twitter.assets.descr',
],
]
)->addModelTransformer($transformer)
);
$transformer = new IdToEntityModelTransformer($this->em, \Mautic\PageBundle\Entity\Page::class, 'id');
$builder->add(
$builder->create(
'page',
PageListType::class,
[
'label' => 'mautic.social.monitoring.twitter.pages',
'placeholder' => 'mautic.social.monitoring.list.choose',
'label_attr' => ['class' => 'control-label'],
'multiple' => false,
'attr' => [
'class' => 'form-control tweet-insert-page',
'tooltip' => 'mautic.social.monitoring.twitter.pages.descr',
],
]
)->addModelTransformer($transformer)
);
$builder->add(
'handle',
ButtonType::class,
[
'label' => 'mautic.social.twitter.handle',
'attr' => [
'class' => 'form-control btn-primary tweet-insert-handle',
],
]
);
// add category
$builder->add('category', CategoryListType::class, [
'bundle' => 'plugin:mauticSocial',
]);
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->setDefined(['update_select']);
}
public function getBlockPrefix(): string
{
return 'twitter_tweet';
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace MauticPlugin\MauticSocialBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
/**
* @extends AbstractType<array<mixed>>
*/
class TwitterAbstractType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace MauticPlugin\MauticSocialBundle\Form\Type;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
class TwitterHashtagType extends TwitterAbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('hashtag', TextType::class, [
'label' => 'mautic.social.monitoring.twitter.hashtag',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'tooltip' => 'mautic.social.monitoring.twitter.hashtag.tooltip',
'class' => 'form-control',
'preaddon' => 'symbol-hashtag',
],
]);
$builder->add('checknames', ChoiceType::class, [
'choices' => [
'mautic.social.monitoring.twitter.no' => '0',
'mautic.social.monitoring.twitter.yes' => '1',
],
'label' => 'mautic.social.monitoring.twitter.namematching',
'required' => false,
'placeholder' => false,
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
'tooltip' => 'mautic.social.monitoring.twitter.namematching.tooltip',
],
]);
// pull in the parent type's form builder
parent::buildForm($builder, $options);
}
public function getBlockPrefix(): string
{
return 'twitter_hashtag';
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace MauticPlugin\MauticSocialBundle\Form\Type;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
class TwitterMentionType extends TwitterAbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('handle', TextType::class, [
'label' => 'mautic.social.monitoring.twitter.handle',
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
'tooltip' => 'mautic.social.monitoring.twitter.handle.tooltip',
'preaddon' => 'ri-at-line',
],
]);
$builder->add('checknames', ChoiceType::class, [
'choices' => [
'mautic.social.monitoring.twitter.no' => '0',
'mautic.social.monitoring.twitter.yes' => '1',
],
'label' => 'mautic.social.monitoring.twitter.namematching',
'required' => false,
'placeholder' => false,
'label_attr' => ['class' => 'control-label'],
'attr' => [
'class' => 'form-control',
'tooltip' => 'mautic.social.monitoring.twitter.namematching.tooltip',
],
]);
// pull in the parent type's form builder
parent::buildForm($builder, $options);
}
public function getBlockPrefix(): string
{
return 'twitter_handle';
}
}

View File

@@ -0,0 +1,86 @@
<?php
namespace MauticPlugin\MauticSocialBundle\Form\Type;
use Mautic\CoreBundle\Form\Type\YesNoButtonGroupType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
/**
* @extends AbstractType<array<mixed>>
*/
class TwitterType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('count', ChoiceType::class, [
'choices' => [
'mautic.integration.Twitter.share.layout.horizontal' => 'horizontal',
'mautic.integration.Twitter.share.layout.vertical' => 'vertical',
'mautic.integration.Twitter.share.layout.none' => 'none',
],
'label' => 'mautic.integration.Twitter.share.layout',
'required' => false,
'placeholder' => false,
'label_attr' => ['class' => 'control-label'],
'attr' => ['class' => 'form-control'],
]);
$builder->add('text', TextType::class, [
'label_attr' => ['class' => 'control-label'],
'label' => 'mautic.integration.Twitter.share.text',
'required' => false,
'attr' => [
'class' => 'form-control',
'placeholder' => 'mautic.integration.Twitter.share.text.pagetitle',
],
]);
$builder->add('via', TextType::class, [
'label_attr' => ['class' => 'control-label'],
'label' => 'mautic.integration.Twitter.share.via',
'required' => false,
'attr' => [
'class' => 'form-control',
'placeholder' => 'mautic.integration.Twitter.share.username',
'preaddon' => 'ri-at-line',
],
]);
$builder->add('related', TextType::class, [
'label_attr' => ['class' => 'control-label'],
'label' => 'mautic.integration.Twitter.share.related',
'required' => false,
'attr' => [
'class' => 'form-control',
'placeholder' => 'mautic.integration.Twitter.share.username',
'preaddon' => 'ri-at-line',
],
]);
$builder->add('hashtags', TextType::class, [
'label_attr' => ['class' => 'control-label'],
'label' => 'mautic.integration.Twitter.share.hashtag',
'required' => false,
'attr' => [
'class' => 'form-control',
'placeholder' => 'mautic.integration.Twitter.share.hashtag.placeholder',
'preaddon' => 'symbol-hashtag',
],
]);
$builder->add('size', YesNoButtonGroupType::class, [
'no_value' => 'medium',
'yes_value' => 'large',
'label' => 'mautic.integration.Twitter.share.largesize',
'data' => (!empty($options['data']['size'])) ? $options['data']['size'] : 'medium',
]);
}
public function getBlockPrefix(): string
{
return 'socialmedia_twitter';
}
}

View File

@@ -0,0 +1,122 @@
<?php
namespace MauticPlugin\MauticSocialBundle\Helper;
use Mautic\AssetBundle\Helper\TokenHelper as AssetTokenHelper;
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\PluginBundle\Helper\IntegrationHelper;
use MauticPlugin\MauticSocialBundle\Model\TweetModel;
class CampaignEventHelper
{
/**
* @var array
*/
protected $clickthrough = [];
public function __construct(
protected IntegrationHelper $integrationHelper,
protected TrackableModel $trackableModel,
protected PageTokenHelper $pageTokenHelper,
protected AssetTokenHelper $assetTokenHelper,
protected TweetModel $tweetModel,
) {
}
/**
* @return array|false
*/
public function sendTweetAction(Lead $lead, array $event)
{
$tweetSent = false;
$tweetEntity = $this->tweetModel->getEntity($event['channelId']);
if (!$tweetEntity) {
return ['failed' => 1, 'response' => 'Tweet entity '.$event['channelId'].' not found'];
}
/** @var \MauticPlugin\MauticSocialBundle\Integration\TwitterIntegration $twitterIntegration */
$twitterIntegration = $this->integrationHelper->getIntegrationObject('Twitter');
// Setup clickthrough for URLs in tweet
$this->clickthrough = [
'source' => ['campaign', $event['campaign']['id']],
];
$leadArray = $lead->getProfileFields();
if (empty($leadArray['twitter'])) {
return false;
}
$tweetText = $tweetEntity->getText();
$tweetText = $this->parseTweetText($tweetText, $leadArray, $tweetEntity->getId());
$tweetUrl = $twitterIntegration->getApiUrl('statuses/update');
$status = ['status' => $tweetText];
// fire the tweet
$sendResponse = $twitterIntegration->makeRequest($tweetUrl, $status, 'POST', ['append_callback' => false]);
// verify the tweet was sent by checking for a tweet id
if (is_array($sendResponse) && array_key_exists('id_str', $sendResponse)) {
$tweetSent = true;
}
if ($tweetSent) {
$this->tweetModel->registerSend($tweetEntity, $lead, $sendResponse, 'campaign.event', $event['id']);
return ['timeline' => $tweetText, 'response' => $sendResponse];
}
$response = ['failed' => 1, 'response' => $sendResponse];
if (!empty($sendResponse['error']['message'])) {
$response['reason'] = $sendResponse['error']['message'];
}
return $response;
}
/**
* PreParse the twitter message and replace placeholders with values.
*
* @param string $text
* @param array $lead
* @param int $channelId
*
* @return string|string[]
*/
protected function parseTweetText($text, $lead, $channelId = -1): array|string
{
$tweetHandle = $lead['twitter'];
$tokens = [
'{twitter_handle}' => (str_contains($tweetHandle, '@')) ? $tweetHandle : "@$tweetHandle",
];
$tokens = array_merge(
$tokens,
TokenHelper::findLeadTokens($text, $lead),
$this->pageTokenHelper->findPageTokens($text, $this->clickthrough),
$this->assetTokenHelper->findAssetTokens($text, $this->clickthrough)
);
[$text, $trackables] = $this->trackableModel->parseContentForTrackables(
$text,
$tokens,
'social_twitter',
$channelId
);
/**
* @var string $token
* @var Trackable $trackable
*/
foreach ($trackables as $token => $trackable) {
$tokens[$token] = $this->trackableModel->generateTrackableUrl($trackable, array_merge($this->clickthrough, ['lead' => $lead['id']]));
}
return str_replace(array_keys($tokens), array_values($tokens), $text);
}
}

View File

@@ -0,0 +1,366 @@
<?php
namespace MauticPlugin\MauticSocialBundle\Helper;
use Doctrine\ORM\EntityManagerInterface;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\CoreBundle\Translation\Translator;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\LeadBundle\Model\FieldModel;
use Mautic\LeadBundle\Model\LeadModel;
use MauticPlugin\MauticSocialBundle\Entity\Monitoring;
use MauticPlugin\MauticSocialBundle\Exception\ExitMonitorException;
use MauticPlugin\MauticSocialBundle\Model\MonitoringModel;
use MauticPlugin\MauticSocialBundle\Model\PostCountModel;
use Symfony\Component\Console\Output\OutputInterface;
class TwitterCommandHelper
{
private ?OutputInterface $output = null;
private int $updatedLeads = 0;
private int $newLeads = 0;
private array $manipulatedLeads = [];
/**
* @var string
*/
private $twitterHandleField;
public function __construct(
private LeadModel $leadModel,
private FieldModel $fieldModel,
private MonitoringModel $monitoringModel,
private PostCountModel $postCountModel,
private Translator $translator,
private EntityManagerInterface $em,
CoreParametersHelper $coreParametersHelper,
) {
$this->translator->setLocale($coreParametersHelper->get('locale', 'en_US'));
$this->twitterHandleField = $coreParametersHelper->get('twitter_handle_field', 'twitter');
}
public function getNewLeadsCount(): int
{
return $this->newLeads;
}
public function getUpdatedLeadsCount(): int
{
return $this->updatedLeads;
}
/**
* @return array
*/
public function getManipulatedLeads()
{
return $this->manipulatedLeads;
}
public function setOutput(OutputInterface $output): void
{
$this->output = $output;
}
/**
* @param string $message
* @param bool $newLine
*/
private function output($message, $newLine = true): void
{
if ($newLine) {
$this->output->writeln($message);
} else {
$this->output->write($message);
}
}
/**
* Processes a list of tweets and creates / updates leads in Mautic.
*
* @param array $statusList
* @param Monitoring $monitor
*/
public function createLeadsFromStatuses($statusList, $monitor): int
{
$leadField = $this->fieldModel->getRepository()->findOneBy(['alias' => $this->twitterHandleField]);
if (!$leadField) {
// Field has been deleted or something
$this->output($this->translator->trans('mautic.social.monitoring.twitter.field.not.found'));
return 0;
}
$handleFieldGroup = $leadField->getGroup();
// Just a means to let any LeadEvents listeners know that many leads are likely coming in case that matters to their logic
defined('MASS_LEADS_MANIPULATION') or define('MASS_LEADS_MANIPULATION', 1);
defined('SOCIAL_MONITOR_IMPORT') or define('SOCIAL_MONITOR_IMPORT', 1);
// Get a list of existing leads to tone down on queries
$usersByHandles = [];
$usersByName = ['firstnames' => [], 'lastnames' => []];
$expr = $this->leadModel->getRepository()->createQueryBuilder('f')->expr();
$monitorProperties = $monitor->getProperties();
if (!array_key_exists('checknames', $monitorProperties)) {
$monitorProperties['checknames'] = 0;
}
foreach ($statusList as $i => $status) {
// If we don't have a screen_name, the rest is irrelevant. Remove from further processing
if (empty($status['user']['screen_name'])) {
unset($statusList[$i]);
continue;
}
$usersByHandles[] = $expr->literal($status['user']['screen_name']);
// Split the twitter user's name into its parts if we're matching to contacts by name
if ($monitorProperties['checknames'] && $status['user']['name'] && str_contains($status['user']['name'], ' ')) {
[$firstName, $lastName] = $this->splitName($status['user']['name']);
if (!empty($firstName) && !empty($lastName)) {
$usersByName['firstnames'][] = $expr->literal($firstName);
$usersByName['lastnames'][] = $expr->literal($lastName);
}
unset($firstName, $lastName);
}
}
unset($expr);
if (!empty($usersByHandles)) {
$leads = $this->leadModel->getRepository()->getEntities(
[
'filter' => [
'force' => [
[
'column' => 'l.'.$this->twitterHandleField,
'expr' => 'in',
'value' => $usersByHandles,
],
],
],
]
);
// Key by twitter handle
$twitterLeads = [];
foreach ($leads as $lead) {
$fields = $lead->getFields();
$twitterHandle = strtolower($fields[$handleFieldGroup][$this->twitterHandleField]['value']);
$twitterLeads[$twitterHandle] = $lead;
}
unset($leads);
}
if ($monitorProperties['checknames']) {
// Fetch existing contacts who have an unknown twitter
// handle in Mautic but are found during monitoring.
$leadsByName = $this->leadModel->getRepository()->getEntities(
[
'filter' => [
'force' => [
[
'column' => 'l.firstname',
'expr' => 'in',
'value' => $usersByName['firstnames'],
],
[
'column' => 'l.lastname',
'expr' => 'in',
'value' => $usersByName['lastnames'],
],
[
'column' => 'l.'.$this->twitterHandleField,
'expr' => 'isNull',
],
],
],
]
);
// key by name
$namedLeads = [];
/** @var Lead $lead */
foreach ($leadsByName as $lead) {
$firstName = $lead->getFirstname();
$lastName = $lead->getLastname();
$namedLeads[$firstName.' '.$lastName] = $lead;
}
unset($leadsByName, $firstName, $lastName);
}
$processedLeads = [];
foreach ($statusList as $status) {
$handle = strtolower($status['user']['screen_name']);
/* @var \Mautic\LeadBundle\Entity\Lead $leadEntity */
if (!isset($processedLeads[$handle])) {
$processedLeads[$handle] = 1;
$lastActive = new \DateTime($status['created_at']);
if (isset($namedLeads[$status['user']['name']])) {
++$this->updatedLeads;
$isNew = false;
$leadEntity = $namedLeads[$status['user']['name']];
$fields = [
$this->twitterHandleField => $handle,
];
$this->leadModel->setFieldValues($leadEntity, $fields, false);
$this->output('Updating existing lead ID #'.$leadEntity->getId().' ('.$handle.'). Matched by first and last names.');
} elseif (isset($twitterLeads[$handle])) {
++$this->updatedLeads;
$isNew = false;
$leadEntity = $twitterLeads[$handle];
$this->output('Updating existing lead ID #'.$leadEntity->getId().' ('.$handle.')');
} else {
++$this->newLeads;
$this->output('Creating new lead');
$isNew = true;
$leadEntity = new Lead();
$leadEntity->setNewlyCreated(true);
[$firstName, $lastName] = $this->splitName($status['user']['name']);
// build new lead fields
$fields = [
$this->twitterHandleField => $handle,
'firstname' => $firstName,
'lastname' => $lastName,
'country' => $status['user']['location'],
];
$this->leadModel->setFieldValues($leadEntity, $fields, false);
// mark as identified just to be sure
$leadEntity->setDateIdentified(new \DateTime());
}
$leadEntity->setPreferredProfileImage('Twitter');
// save the lead now
$leadEntity->setLastActive($lastActive->format('Y-m-d H:i:s'));
try {
// save the lead entity
$this->leadModel->saveEntity($leadEntity);
// Note lead ids
$this->manipulatedLeads[$leadEntity->getId()] = 1;
// add lead entity to the lead list
$this->leadModel->addToLists($leadEntity, $monitor->getLists());
if ($isNew) {
$this->setMonitorLeadStat($monitor, $leadEntity);
}
} catch (ExitMonitorException $e) {
$this->output($e->getMessage());
return 0;
} catch (\Exception $e) {
$this->output($e->getMessage());
continue;
}
}
// Increment the post count
$this->incrementPostCount($monitor, $status);
}
unset($processedLeads);
return 1;
}
/**
* Set the monitor's stat record with the metadata.
*
* @param array $searchMeta
*/
public function setMonitorStats(Monitoring $monitor, $searchMeta): void
{
$monitor->setStats($searchMeta);
$this->monitoringModel->saveEntity($monitor);
}
/**
* Get monitor record entity.
*
* @param int $mid
*/
public function getMonitor($mid): ?Monitoring
{
return $this->monitoringModel->getEntity($mid);
}
/**
* handles splitting a string handle into first / last name based on a space.
*
* @param string $name Space separated first & last name. Supports multiple first names
*
* @return array{0: string, 1?: string}
*/
private function splitName(string $name): array
{
// array the entire name
$nameParts = explode(' ', $name);
// last part of the array is our last
$lastName = array_pop($nameParts);
// push the rest of the name into first name
$firstName = implode(' ', $nameParts);
return [$firstName, $lastName];
}
/**
* Add new monitoring_leads record to track leads found via the search.
*
* @param Monitoring $monitor
* @param Lead $lead
*/
private function setMonitorLeadStat($monitor, $lead): void
{
// track the lead in our monitor_leads table
$monitorLead = new \MauticPlugin\MauticSocialBundle\Entity\Lead();
$monitorLead->setMonitor($monitor);
$monitorLead->setLead($lead);
$monitorLead->setDateAdded(new \DateTime());
/* @var \MauticPlugin\MauticSocialBundle\Entity\LeadRepository $monitorRepository */
$monitorRepository = $this->em->getRepository(\MauticPlugin\MauticSocialBundle\Entity\Lead::class);
$monitorRepository->saveEntity($monitorLead);
}
/**
* Increment the post counter.
*
* @param Monitoring $monitor
*/
private function incrementPostCount($monitor, $tweet): void
{
$date = new \DateTime($tweet['created_at']);
$this->postCountModel->updatePostCount($monitor, $date);
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace MauticPlugin\MauticSocialBundle\Integration;
use Mautic\PluginBundle\Helper\IntegrationHelper;
final class Config
{
public function __construct(private IntegrationHelper $integrationsHelper)
{
}
public function isPublished(): bool
{
$integration = $this->integrationsHelper->getIntegrationObject(TwitterIntegration::NAME);
return $integration && $integration->getIntegrationSettings()->getIsPublished();
}
}

View File

@@ -0,0 +1,149 @@
<?php
namespace MauticPlugin\MauticSocialBundle\Integration;
use MauticPlugin\MauticSocialBundle\Form\Type\FacebookType;
class FacebookIntegration extends SocialIntegration
{
public function getName(): string
{
return 'Facebook';
}
/**
* @return string[]
*/
public function getIdentifierFields(): array
{
return [
'facebook',
];
}
public function getSupportedFeatures(): array
{
return [
'share_button',
'login_button',
'public_profile',
];
}
public function getAuthenticationUrl(): string
{
return 'https://www.facebook.com/dialog/oauth';
}
public function getAccessTokenUrl(): string
{
return 'https://graph.facebook.com/oauth/access_token';
}
public function getAuthScope(): string
{
return 'email';
}
/**
* @param string $data
* @param bool $postAuthorization
*
* @return mixed
*/
public function parseCallbackResponse($data, $postAuthorization = false)
{
// Facebook is inconsistent in that it returns errors as json and data as parameter list
$values = parent::parseCallbackResponse($data, $postAuthorization);
if (null === $values) {
parse_str($data, $values);
$this->requestStack->getSession()->set($this->getName().'_tokenResponse', $values);
}
return $values;
}
public function getApiUrl($endpoint): string
{
return "https://graph.facebook.com/$endpoint";
}
/**
* Get public data.
*
* @return array
*/
public function getUserData($identifier, &$socialCache)
{
$this->persistNewLead = false;
$accessToken = $this->getContactAccessToken($socialCache);
if (!isset($accessToken['access_token'])) {
return;
}
$url = $this->getApiUrl('v2.8/me');
$fields = array_keys($this->getAvailableLeadFields());
$parameters = [
'access_token' => $accessToken['access_token'],
'fields' => implode(',', $fields),
];
$data = $this->makeRequest($url, $parameters, 'GET', ['auth_type' => 'rest']);
if (is_object($data) && isset($data->id)) {
$info = $this->matchUpData($data);
if (isset($data->username)) {
$info['profileHandle'] = $data->username;
} elseif (isset($data->link)) {
if (preg_match("/www.facebook.com\/(app_scoped_user_id\/)?(.*?)($|\/)/", $data->link, $matches)) {
$info['profileHandle'] = $matches[2];
}
} else {
$info['profileHandle'] = $data->id;
}
$info['profileImage'] = "https://graph.facebook.com/{$data->id}/picture?type=large";
$socialCache['id'] = $data->id;
$socialCache['profile'] = $info;
$socialCache['lastRefresh'] = new \DateTime();
$socialCache['accessToken'] = $this->encryptApiKeys($accessToken);
$this->getMauticLead($info, $this->persistNewLead, $socialCache, $identifier);
return $data;
}
return null;
}
public function getAvailableLeadFields($settings = []): array
{
return [
'about' => ['type' => 'string'],
'birthday' => ['type' => 'string'],
'email' => ['type' => 'string'],
'first_name' => ['type' => 'string'],
'gender' => ['type' => 'string'],
'last_name' => ['type' => 'string'],
'link' => ['type' => 'string'],
'locale' => ['type' => 'string'],
'middle_name' => ['type' => 'string'],
'name' => ['type' => 'string'],
'political' => ['type' => 'string'],
'quotes' => ['type' => 'string'],
'religion' => ['type' => 'string'],
'timezone' => ['type' => 'string'],
'website' => ['type' => 'string'],
];
}
public function getFormType(): string
{
return FacebookType::class;
}
}

View File

@@ -0,0 +1,290 @@
<?php
namespace MauticPlugin\MauticSocialBundle\Integration;
class FoursquareIntegration extends SocialIntegration
{
public function getName(): string
{
return 'Foursquare';
}
public function getPriority(): int
{
return 2;
}
/**
* @return string[]
*/
public function getIdentifierFields(): array
{
return [
'email',
'twitter', // foursquare allows searching directly by twitter handle
];
}
public function getAuthenticationUrl(): string
{
return 'https://foursquare.com/oauth2/authenticate';
}
public function getAccessTokenUrl(): string
{
return 'https://foursquare.com/oauth2/access_token';
}
public function getAuthenticationType(): string
{
return 'oauth2';
}
/**
* @param string $endpoint
* @param string $m
*/
public function getApiUrl($endpoint, $m = 'foursquare'): string
{
return "https://api.foursquare.com/v2/$endpoint?v=20140806&m={$m}";
}
/**
* @param array $parameters
* @param string $method
* @param array $settings
*
* @return mixed|string
*/
public function makeRequest($url, $parameters = [], $method = 'GET', $settings = [])
{
$settings[$this->getAuthTokenKey()] = 'oauth_token';
return parent::makeRequest($url, $parameters, $method, $settings);
}
/**
* Get public data.
*/
public function getUserData($identifier, &$socialCache): void
{
if ($id = $this->getContactUserId($identifier, $socialCache)) {
$url = $this->getApiUrl("users/{$id}");
$data = $this->makeRequest($url);
if (!empty($data) && isset($data->response->user)) {
$result = $data->response->user;
$socialCache['profile'] = $this->matchUpData($result);
if (isset($result->photo)) {
$socialCache['profile']['profileImage'] = $result->photo->prefix.'300x300'.$result->photo->suffix;
}
$socialCache['profile']['profileHandle'] = $id;
}
}
}
public function getPublicActivity($identifier, &$socialCache): void
{
if ($id = $this->getContactUserId($identifier, $socialCache)) {
$activity = [
// 'mayorships' => array(),
'tips' => [],
// 'lists' => array()
];
/*
//mayorships
$url = $this->getApiUrl("users/{$id}/mayorships");
$data = $this->makeRequest($url);
if (isset($data->response->mayorships) && count($data->response->mayorships->items)) {
$limit = 5;
foreach ($data->response->mayorships->items as $m) {
if (empty($limit)) {
break;
}
//find main category of venue
$category = '';
foreach ($m->venue->categories as $c) {
if ($c->primary) {
$category = $c->name;
break;
}
}
$contact = (!empty($m->contact->formattedPhone)) ? $m->contact->formattedPhone : '';
$activity['mayorships'][] = array(
'venueName' => $m->venue->name,
'venueLocation' => $m->venue->location->formattedAddress,
'venueContact' => $contact,
'venueCategory' => $category
);
$limit--;
}
}
*/
// tips
$url = $this->getApiUrl("users/{$id}/tips").'&limit=5&sort=recent';
$data = $this->makeRequest($url);
if (isset($data->response->tips) && count($data->response->tips->items)) {
foreach ($data->response->tips->items as $t) {
// find main category of venue
$category = '';
foreach ($t->venue->categories as $c) {
if ($c->primary) {
$category = $c->name;
break;
}
}
$contact = (!empty($t->contact->formattedPhone)) ? $t->contact->formattedPhone : '';
$activity['tips'][] = [
'createdAt' => $t->createdAt,
'tipText' => $t->text,
'tipUrl' => $t->canonicalUrl,
'venueName' => $t->venue->name,
'venueLocation' => $t->venue->location->formattedAddress,
'venueContact' => $contact,
'venueCategory' => $category,
];
}
}
/*
//lists
$url = $this->getApiUrl("users/{$id}/lists") . "&limit=5&group=created";
$data = $this->makeRequest($url);
if (isset($data->response->lists) && count($data->response->lists->items)) {
foreach ($data->response->lists->items as $l) {
if (!$l->listItems->count) {
continue;
}
$item = array(
'listName' => $l->name,
'listDescription' => $l->description,
'listUrl' => $l->canonicalUrl,
'listCreatedAt' => (isset($l->createdAt)) ? $l->createdAt : '',
'listUpdatedAt' => (isset($l->updatedAt)) ? $l->updatedAt : '',
'listItems' => array()
);
//get a sample of the list items
$url = "https://api.foursquare.com/v2/lists/{$l->id}?limit=5&sort=recent&v=20140719&oauth_token={$keys['access_token']}";
$listData = $this->makeRequest($url);
if (isset($listData->response->list->listItems) && count($listData->response->list->listItems->items)) {
foreach ($listData->response->list->listItems->items as $li) {
//find main category of venue
$category = '';
foreach ($li->venue->categories as $c) {
if ($c->primary) {
$category = $c->name;
break;
}
}
$contact = (!empty($li->contact->formattedPhone)) ? $li->contact->formattedPhone : '';
$item['listItems'][] = array(
'createdAt' => $li->createdAt,
'venueName' => $li->venue->name,
'venueLocation' => $li->venue->location->formattedAddress,
'venueContact' => $contact,
'venueCategory' => $category
);
}
}
$activity['lists'][] = $item;
}
}
*/
if (!empty($activity)) {
$socialCache['activity'] = $activity;
}
}
}
public function getErrorsFromResponse($response): string
{
if (is_object($response) && isset($response->meta->errorDetail)) {
return $response->meta->errorDetail.' ('.$response->meta->code.')';
}
return '';
}
public function matchFieldName($field, $subfield = '')
{
if ('contact' == $field && in_array($subfield, ['facebook', 'twitter'])) {
return $subfield.'ProfileHandle';
}
return parent::matchFieldName($field, $subfield);
}
public function getAvailableLeadFields($settings = []): array
{
return [
'profileHandle' => ['type' => 'string'],
'firstName' => ['type' => 'string'],
'lastName' => ['type' => 'string'],
'gender' => ['type' => 'string'],
'homeCity' => ['type' => 'string'],
'bio' => ['type' => 'string'],
'contact' => [
'type' => 'object',
'fields' => [
'twitter',
'facebook',
'phone',
],
],
];
}
public function getSupportedFeatures(): array
{
return [
'public_profile',
'public_activity',
];
}
/**
* @return bool
*/
private function getContactUserId(&$identifier, &$socialCache)
{
if (!empty($socialCache['id'])) {
return $socialCache['id'];
} elseif (empty($identifier)) {
return false;
}
$cleaned = $this->cleanIdentifier($identifier);
if (!is_array($cleaned)) {
$cleaned = [$cleaned];
}
foreach ($cleaned as $type => $c) {
$url = $this->getApiUrl('users/search')."&{$type}={$c}";
$data = $this->makeRequest($url);
if (!empty($data) && isset($data->response->results) && count($data->response->results)) {
$socialCache['id'] = $data->response->results[0]->id;
return $socialCache['id'];
}
}
return false;
}
public function getFormType(): null
{
return null;
}
}

View File

@@ -0,0 +1,140 @@
<?php
namespace MauticPlugin\MauticSocialBundle\Integration;
class InstagramIntegration extends SocialIntegration
{
public function getName(): string
{
return 'Instagram';
}
public function getSupportedFeatures(): array
{
return [
'public_profile',
'public_activity',
];
}
public function getIdentifierFields(): string
{
return 'instagram';
}
public function getAuthenticationUrl(): string
{
return 'https://api.instagram.com/oauth/authorize';
}
public function getAccessTokenUrl(): string
{
return 'https://api.instagram.com/oauth/access_token';
}
public function getApiUrl($endpoint): string
{
return "https://api.instagram.com/v1/$endpoint";
}
public function getUserData($identifier, &$socialCache): void
{
if ($id = $this->getContactUserId($identifier, $socialCache)) {
$url = $this->getApiUrl('users/'.$id);
$data = $this->makeRequest($url);
if (isset($data->data)) {
$info = $this->matchUpData($data->data);
$info['profileImage'] = $data->data->profile_picture;
$info['profileHandle'] = $data->data->username;
$socialCache['profile'] = $info;
}
}
}
public function getPublicActivity($identifier, &$socialCache): void
{
$socialCache['has']['activity'] = false;
if ($id = $this->getContactUserId($identifier, $socialCache)) {
// get more than 10 so we can weed out videos
$data = $this->makeRequest($this->getApiUrl("users/$id/media/recent"), ['count' => 20]);
$socialCache['activity'] = [
'photos' => [],
'tags' => [],
];
if (!empty($data->data)) {
$socialCache['has']['activity'] = true;
$count = 1;
foreach ($data->data as $m) {
if ($count > 10) {
break;
}
if ('image' == $m->type) {
$socialCache['activity']['photos'][] = [
'url' => $m->images->standard_resolution->url,
];
if (!empty($m->caption->text)) {
preg_match_all("/#(\w+)/", $m->caption->text, $tags);
foreach ($tags[1] as $tag) {
if (isset($socialCache['activity']['tags'][$tag])) {
++$socialCache['activity']['tags'][$tag]['count'];
} else {
$socialCache['activity']['tags'][$tag] = [
'count' => 1,
'url' => 'http://searchinstagram.com/'.$tag,
];
}
}
}
}
++$count;
}
}
}
}
public function getAvailableLeadFields($settings = []): array
{
return [
'full_name' => ['type' => 'string'],
'bio' => ['type' => 'string'],
'website' => ['type' => 'string'],
];
}
private function getContactUserId(&$identifier, &$socialCache)
{
if (!empty($socialCache['id'])) {
return $socialCache['id'];
} elseif (empty($identifier)) {
return false;
}
$data = $this->makeRequest($this->getApiUrl('users/search'), ['q' => $identifier]);
if (!empty($data->data)) {
foreach ($data->data as $user) {
// its possible that instagram may return multiple users if the username is a base of another
// for example, search for alan may return alanh, alanhartless, etc
if (strtolower($user->username) == strtolower($identifier)) {
$socialCache['id'] = $user->id;
break;
}
}
return (!empty($socialCache['id'])) ? $socialCache['id'] : false;
}
return false;
}
public function getFormType(): null
{
return null;
}
}

View File

@@ -0,0 +1,284 @@
<?php
namespace MauticPlugin\MauticSocialBundle\Integration;
use Doctrine\ORM\EntityManager;
use Mautic\CoreBundle\Helper\CacheStorageHelper;
use Mautic\CoreBundle\Helper\EncryptionHelper;
use Mautic\CoreBundle\Helper\PathsHelper;
use Mautic\CoreBundle\Model\NotificationModel;
use Mautic\CoreBundle\Translation\Translator;
use Mautic\LeadBundle\Field\FieldsWithUniqueIdentifier;
use Mautic\LeadBundle\Model\CompanyModel;
use Mautic\LeadBundle\Model\DoNotContact;
use Mautic\LeadBundle\Model\FieldModel;
use Mautic\LeadBundle\Model\LeadModel;
use Mautic\PluginBundle\Helper\IntegrationHelper;
use Mautic\PluginBundle\Integration\AbstractIntegration;
use Mautic\PluginBundle\Model\IntegrationEntityModel;
use Monolog\Logger;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\FormBuilder;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Routing\Router;
use Symfony\Contracts\Translation\TranslatorInterface;
abstract class SocialIntegration extends AbstractIntegration
{
protected $persistNewLead = false;
/**
* @var Translator
*/
protected TranslatorInterface $translator;
public function __construct(
EventDispatcherInterface $eventDispatcher,
CacheStorageHelper $cacheStorageHelper,
EntityManager $entityManager,
RequestStack $requestStack,
Router $router,
Translator $translator,
Logger $logger,
EncryptionHelper $encryptionHelper,
LeadModel $leadModel,
CompanyModel $companyModel,
PathsHelper $pathsHelper,
NotificationModel $notificationModel,
FieldModel $fieldModel,
FieldsWithUniqueIdentifier $fieldsWithUniqueIdentifier,
IntegrationEntityModel $integrationEntityModel,
DoNotContact $doNotContact,
protected IntegrationHelper $integrationHelper,
) {
parent::__construct(
$eventDispatcher,
$cacheStorageHelper,
$entityManager,
$requestStack,
$router,
$translator,
$logger,
$encryptionHelper,
$leadModel,
$companyModel,
$pathsHelper,
$notificationModel,
$fieldModel,
$integrationEntityModel,
$doNotContact,
$fieldsWithUniqueIdentifier
);
}
/**
* @param \Mautic\PluginBundle\Integration\Form|FormBuilder $builder
* @param array $data
* @param string $formArea
*/
public function appendToForm(&$builder, $data, $formArea): void
{
if ('features' == $formArea) {
$name = strtolower($this->getName());
$formType = $this->getFormType();
if ($formType) {
$builder->add('shareButton', $formType, [
'label' => 'mautic.integration.form.sharebutton',
'required' => false,
'data' => $data['shareButton'] ?? [],
]);
}
}
}
/**
* @param array $settings
*
* @return array
*/
public function getFormLeadFields($settings = [])
{
static $fields = [];
if (empty($fields)) {
$s = $this->getName();
$available = $this->getAvailableLeadFields($settings);
if (empty($available)) {
return [];
}
// create social profile fields
$socialProfileUrls = $this->integrationHelper->getSocialProfileUrlRegex();
foreach ($available as $field => $details) {
$label = (!empty($details['label'])) ? $details['label'] : false;
$fn = $this->matchFieldName($field);
switch ($details['type']) {
case 'string':
case 'boolean':
$fields[$fn] = (!$label)
? $this->translator->transConditional("mautic.integration.common.{$fn}", "mautic.integration.{$s}.{$fn}")
: $label;
break;
case 'object':
if (isset($details['fields'])) {
foreach ($details['fields'] as $f) {
$fn = $this->matchFieldName($field, $f);
$fields[$fn] = (!$label)
? $this->translator->transConditional("mautic.integration.common.{$fn}", "mautic.integration.{$s}.{$fn}")
: $label;
}
} else {
$fields[$field] = (!$label)
? $this->translator->transConditional("mautic.integration.common.{$fn}", "mautic.integration.{$s}.{$fn}")
: $label;
}
break;
case 'array_object':
if ('urls' == $field || 'url' == $field) {
foreach ($socialProfileUrls as $p => $d) {
$fields["{$p}ProfileHandle"] = (!$label)
? $this->translator->transConditional("mautic.integration.common.{$p}ProfileHandle", "mautic.integration.{$s}.{$p}ProfileHandle")
: $label;
}
foreach ($details['fields'] as $f) {
$fields["{$p}Urls"] = (!$label)
? $this->translator->transConditional("mautic.integration.common.{$f}Urls", "mautic.integration.{$s}.{$f}Urls")
: $label;
}
} elseif (isset($details['fields'])) {
foreach ($details['fields'] as $f) {
$fn = $this->matchFieldName($field, $f);
$fields[$fn] = (!$label)
? $this->translator->transConditional("mautic.integration.common.{$fn}", "mautic.integration.{$s}.{$fn}")
: $label;
}
} else {
$fields[$fn] = (!$label)
? $this->translator->transConditional("mautic.integration.common.{$fn}", "mautic.integration.{$s}.{$fn}")
: $label;
}
break;
}
}
if ($this->sortFieldsAlphabetically()) {
uasort($fields, 'strnatcmp');
}
}
return $fields;
}
public function getFormCompanyFields($settings = [])
{
$settings['feature_settings']['objects'] = ['Company'];
return [];
}
public function getAuthenticationType()
{
return 'oauth2';
}
public function getRequiredKeyFields()
{
return [
'client_id' => 'mautic.integration.keyfield.clientid',
'client_secret' => 'mautic.integration.keyfield.clientsecret',
];
}
/**
* Get the array key for clientId.
*
* @return string
*/
public function getClientIdKey()
{
return 'client_id';
}
/**
* Get the array key for client secret.
*
* @return string
*/
public function getClientSecretKey()
{
return 'client_secret';
}
/**
* @param string $data
* @param bool $postAuthorization
*
* @return mixed
*/
public function parseCallbackResponse($data, $postAuthorization = false)
{
if ($postAuthorization) {
return json_decode($data, true);
} else {
return json_decode($data);
}
}
/**
* Returns notes specific to sections of the integration form (if applicable).
*
* @return array<mixed>
*/
public function getFormNotes($section)
{
return ['', 'info'];
}
/**
* Get the template for social profiles.
*
* @return string
*/
public function getSocialProfileTemplate()
{
return "MauticSocialBundle:Integration/{$this->getName()}/Profile:view.html.twig";
}
/**
* Get the access token from session or socialCache.
*
* @return array|mixed|null
*/
protected function getContactAccessToken(&$socialCache)
{
if (!$this->requestStack->getCurrentRequest()->hasSession()) {
return null;
}
if (!$this->requestStack->getSession()->isStarted()) {
return (isset($socialCache['accessToken'])) ? $this->decryptApiKeys($socialCache['accessToken']) : null;
}
$accessToken = $this->requestStack->getSession()->get($this->getName().'_tokenResponse', []);
if (!isset($accessToken[$this->getAuthTokenKey()])) {
if (isset($socialCache['accessToken'])) {
$accessToken = $this->decryptApiKeys($socialCache['accessToken']);
} else {
return null;
}
} else {
$this->requestStack->getSession()->remove($this->getName().'_tokenResponse');
$socialCache['accessToken'] = $this->encryptApiKeys($accessToken);
$this->persistNewLead = true;
}
return $accessToken;
}
/**
* Returns form type.
*
* @return string|null
*/
abstract public function getFormType();
}

View File

@@ -0,0 +1,260 @@
<?php
namespace MauticPlugin\MauticSocialBundle\Integration;
use MauticPlugin\MauticSocialBundle\Form\Type\TwitterType;
class TwitterIntegration extends SocialIntegration
{
public const NAME = 'Twitter';
public function getName(): string
{
return self::NAME;
}
public function getPriority(): int
{
return 5000;
}
public function getIdentifierFields(): string
{
return 'twitter';
}
public function getSupportedFeatures(): array
{
return [
'public_profile',
'public_activity',
'share_button',
'login_button',
];
}
public function getAccessTokenUrl(): string
{
return 'https://api.twitter.com/oauth/access_token';
}
public function getAuthLoginUrl(): string
{
$url = 'https://api.twitter.com/oauth/authorize';
// Get request token
$requestToken = $this->getRequestToken();
if (isset($requestToken['oauth_token'])) {
$url .= '?oauth_token='.$requestToken['oauth_token'];
}
return $url;
}
public function getRequestTokenUrl(): string
{
return 'https://api.twitter.com/oauth/request_token';
}
public function getAuthenticationType(): string
{
return 'oauth1a';
}
public function prepareRequest($url, $parameters, $method, $settings, $authType)
{
// Prevent SSL issues
$settings['ssl_verifypeer'] = false;
if (empty($settings['authorize_session']) && 'access_token' != $authType) {
// Twitter requires oauth_token_secret to be part of composite key
if (isset($this->keys['oauth_token_secret'])) {
$settings['token_secret'] = $this->keys['oauth_token_secret'];
}
// Twitter also requires double encoding of parameters in building base string
$settings['double_encode_basestring_parameters'] = true;
}
return parent::prepareRequest($url, $parameters, $method, $settings, $authType);
}
public function getApiUrl($endpoint): string
{
return "https://api.twitter.com/1.1/$endpoint.json";
}
public function getUserData($identifier, &$socialCache)
{
$accessToken = $this->getContactAccessToken($socialCache);
// Contact SSO
if (isset($accessToken['oauth_token'])) {
// note twitter requires params to be passed as strings
$data = $this->makeRequest(
$this->getApiUrl('account/verify_credentials'),
[
'include_email' => 'true',
'include_entities' => 'false',
'oauth_token' => $accessToken['oauth_token'],
],
'GET',
['auth_type' => 'oauth1a']
);
}
if (empty($data)) {
// Try via user lookup
$data = $this->makeRequest(
$this->getApiUrl('users/lookup'),
[
'screen_name' => $this->cleanIdentifier($identifier),
'include_entities' => 'false',
]
);
if (isset($data[0])) {
$data = $data[0];
}
}
if (isset($data['id'])) {
$socialCache['id'] = $data['id'];
$info = $this->matchUpData($data);
$info['profileHandle'] = $data['screen_name'];
// remove the size variant
$image = $data['profile_image_url_https'];
$image = str_replace(['_normal', '_bigger', '_mini'], '', $image);
$info['profileImage'] = $image;
$socialCache['profile'] = $info;
$socialCache['lastRefresh'] = new \DateTime();
$this->getMauticLead($info, $this->persistNewLead, $socialCache, $identifier);
}
return $data;
}
public function getPublicActivity($identifier, &$socialCache): void
{
if (!isset($socialCache['id'])) {
$this->getUserData($identifier, $socialCache);
if (!isset($socialCache['id'])) {
return;
}
}
$id = $socialCache['id'];
// due to the way Twitter filters, get more than 10 tweets
$data = $this->makeRequest($this->getApiUrl('/statuses/user_timeline'), [
'user_id' => $id,
'exclude_replies' => 'true',
'count' => 25,
'trim_user' => 'true',
]);
if (!empty($data) && count($data)) {
$socialCache['has']['activity'] = true;
$socialCache['activity'] = [
'tweets' => [],
'photos' => [],
'tags' => [],
];
foreach ($data as $k => $d) {
if (10 == $k) {
break;
}
$tweet = [
'tweet' => $d['text'],
'url' => "https://twitter.com/{$id}/status/{$d['id']}",
'coordinates' => $d['coordinates'],
'published' => $d['created_at'],
];
$socialCache['activity']['tweets'][] = $tweet;
// images
if (isset($d['entities']['media'])) {
foreach ($d['entities']['media'] as $m) {
if ('photo' == $m['type']) {
$photo = [
'url' => ($m['media_url_https'] ?? $m['media_url']),
];
$socialCache['activity']['photos'][] = $photo;
}
}
}
// hastags
if (isset($d['entities']['hashtags'])) {
foreach ($d['entities']['hashtags'] as $h) {
if (isset($socialCache['activity']['tags'][$h['text']])) {
++$socialCache['activity']['tags'][$h['text']]['count'];
} else {
$socialCache['activity']['tags'][$h['text']] = [
'count' => 1,
'url' => 'https://twitter.com/search?q=%23'.$h['text'],
];
}
}
}
}
}
}
public function getAvailableLeadFields($settings = []): array
{
return [
'profileHandle' => ['type' => 'string'],
'name' => ['type' => 'string'],
'location' => ['type' => 'string'],
'description' => ['type' => 'string'],
'url' => ['type' => 'string'],
'time_zone' => ['type' => 'string'],
'lang' => ['type' => 'string'],
'email' => ['type' => 'string'],
];
}
public function cleanIdentifier($identifier): string
{
if (preg_match('#https?://twitter.com/(.*?)(/.*?|$)#i', $identifier, $match)) {
// extract the handle
$identifier = $match[1];
} elseif (str_starts_with($identifier, '@')) {
$identifier = substr($identifier, 1);
}
return urlencode($identifier);
}
/**
* @param string $data
* @param bool $postAuthorization
*
* @return mixed
*/
public function parseCallbackResponse($data, $postAuthorization = false)
{
if ($postAuthorization) {
parse_str($data, $parsed);
return $parsed;
}
return json_decode($data, true);
}
public function getFormType(): string
{
return TwitterType::class;
}
}

View File

@@ -0,0 +1,9 @@
<?php
namespace MauticPlugin\MauticSocialBundle;
use Mautic\PluginBundle\Bundle\PluginBundleBase;
class MauticSocialBundle extends PluginBundleBase
{
}

View File

@@ -0,0 +1,156 @@
<?php
namespace MauticPlugin\MauticSocialBundle\Model;
use Mautic\CoreBundle\Model\FormModel;
use MauticPlugin\MauticSocialBundle\Entity\Monitoring;
use MauticPlugin\MauticSocialBundle\Event as Events;
use MauticPlugin\MauticSocialBundle\Form\Type\MonitoringType;
use MauticPlugin\MauticSocialBundle\Form\Type\TwitterHashtagType;
use MauticPlugin\MauticSocialBundle\Form\Type\TwitterMentionType;
use MauticPlugin\MauticSocialBundle\SocialEvents;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Contracts\EventDispatcher\Event;
/**
* @extends FormModel<Monitoring>
*/
class MonitoringModel extends FormModel
{
/**
* @var array<string, mixed>
*/
private array $networkTypes = [
'twitter_handle' => [
'label' => 'mautic.social.monitoring.type.list.twitter.handle',
'form' => TwitterMentionType::class,
],
'twitter_hashtag' => [
'label' => 'mautic.social.monitoring.type.list.twitter.hashtag',
'form' => TwitterHashtagType::class,
],
];
/**
* @param object $entity
* @param string|null $action
* @param mixed[] $options
*/
public function createForm($entity, FormFactoryInterface $formFactory, $action = null, $options = []): \Symfony\Component\Form\FormInterface
{
if (!$entity instanceof Monitoring) {
throw new MethodNotAllowedHttpException(['Monitoring']);
}
if (!empty($action)) {
$options['action'] = $action;
}
return $formFactory->create(MonitoringType::class, $entity, $options);
}
/**
* Get a specific entity or generate a new one if id is empty.
*/
public function getEntity($id = null): ?Monitoring
{
return $id ? parent::getEntity($id) : new Monitoring();
}
/**
* @throws MethodNotAllowedHttpException
*/
protected function dispatchEvent($action, &$entity, $isNew = false, ?Event $event = null): ?Event
{
if (!$entity instanceof Monitoring) {
throw new MethodNotAllowedHttpException(['Monitoring']);
}
switch ($action) {
case 'pre_save':
$name = SocialEvents::MONITOR_PRE_SAVE;
break;
case 'post_save':
$name = SocialEvents::MONITOR_POST_SAVE;
break;
case 'pre_delete':
$name = SocialEvents::MONITOR_PRE_DELETE;
break;
case 'post_delete':
$name = SocialEvents::MONITOR_POST_DELETE;
break;
default:
return null;
}
if ($this->dispatcher->hasListeners($name)) {
if (empty($event)) {
$event = new Events\SocialEvent($entity, $isNew);
}
$this->dispatcher->dispatch($event, $name);
return $event;
} else {
return null;
}
}
/**
* @param Monitoring $monitoringEntity
* @param bool $unlock
*/
public function saveEntity($monitoringEntity, $unlock = true): void
{
// we're editing an existing record
if (!$monitoringEntity->isNew()) {
// increase the revision
$revision = $monitoringEntity->getRevision();
++$revision;
$monitoringEntity->setRevision($revision);
} // is new
else {
$now = new \DateTime();
$monitoringEntity->setDateAdded($now);
}
parent::saveEntity($monitoringEntity, $unlock);
}
/**
* @return \MauticPlugin\MauticSocialBundle\Entity\MonitoringRepository
*/
public function getRepository()
{
return $this->em->getRepository(Monitoring::class);
}
public function getPermissionBase(): string
{
return 'mauticSocial:monitoring';
}
/**
* @return string[]
*/
public function getNetworkTypes(): array
{
$types = [];
foreach ($this->networkTypes as $type => $data) {
$types[$type] = $data['label'];
}
return $types;
}
/**
* @param string $type
*
* @return string|null
*/
public function getFormByType($type)
{
return array_key_exists($type, $this->networkTypes) ? $this->networkTypes[$type]['form'] : null;
}
}

View File

@@ -0,0 +1,84 @@
<?php
namespace MauticPlugin\MauticSocialBundle\Model;
use Mautic\CoreBundle\Model\AbstractCommonModel;
use MauticPlugin\MauticSocialBundle\Entity\PostCount;
/**
* @extends AbstractCommonModel<PostCount>
*/
class PostCountModel extends AbstractCommonModel
{
/**
* Get a specific entity or generate a new one if id is empty.
*/
public function getEntity($id = null): ?PostCount
{
if (null !== $id) {
$repo = $this->getRepository();
if (method_exists($repo, 'getEntity')) {
return $repo->getEntity($id);
}
return $repo->find($id);
}
return new PostCount();
}
/**
* Get this model's repository.
*
* @return \MauticPlugin\MauticSocialBundle\Entity\PostCountRepository
*/
public function getRepository()
{
return $this->em->getRepository(PostCount::class);
}
/*
* Updates a monitor record's post count on a daily basis
*
* @return boolean
*/
public function updatePostCount($monitor, \DateTime $postDate): bool
{
// query the db for posts on this date
$q = $this->getRepository()->createQueryBuilder($this->getRepository()->getTableAlias());
$expr = $q->expr()->eq($this->getRepository()->getTableAlias().'.postDate', ':date');
$q->setParameter('date', $postDate, 'date');
$q->where($expr);
$args['qb'] = $q;
// ignore paginator so we can use the array later
$args['ignore_paginator'] = true;
/** @var \MauticPlugin\MauticSocialBundle\Entity\PostCountRepository $postCountsRepository */
$postCountsRepository = $this->getRepository();
// get any existing records
$postCounts = $postCountsRepository->getEntities($args);
// if there isn't anything then create it
if (!count($postCounts)) {
/** @var PostCount $postCount */
$postCount = $this->getEntity();
$postCount->setMonitor($monitor);
$postCount->setPostDate($postDate); // $postDate->format('m-d-Y')
} else {
// use the first record to increment it.
$postCount = $this->getEntity($postCounts[0]->getId());
}
// increment
$postCount->setPostCount($postCount->getPostCount() + 1);
// now save it
$postCountsRepository->saveEntity($postCount);
// nothing went wrong so return true here
return true;
}
}

View File

@@ -0,0 +1,209 @@
<?php
namespace MauticPlugin\MauticSocialBundle\Model;
use Mautic\CoreBundle\Model\AjaxLookupModelInterface;
use Mautic\CoreBundle\Model\FormModel;
use Mautic\LeadBundle\Entity\Lead;
use MauticPlugin\MauticSocialBundle\Entity\Tweet;
use MauticPlugin\MauticSocialBundle\Entity\TweetRepository;
use MauticPlugin\MauticSocialBundle\Entity\TweetStat;
use MauticPlugin\MauticSocialBundle\Entity\TweetStatRepository;
use MauticPlugin\MauticSocialBundle\Event as Events;
use MauticPlugin\MauticSocialBundle\Form\Type\TweetType;
use MauticPlugin\MauticSocialBundle\SocialEvents;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Contracts\EventDispatcher\Event;
/**
* @extends FormModel<Tweet>
*
* @implements AjaxLookupModelInterface<Tweet>
*/
class TweetModel extends FormModel implements AjaxLookupModelInterface
{
/**
* @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 'social.tweet':
case 'tweet':
if (isset($filter['tweet_text'])) {
// This tweet was created as the campaign action param and these params are not the filter. Clear the filter.
$filter = '';
}
$tweetRepo = $this->getRepository();
$tweetRepo->setCurrentUser($this->userHelper->getUser());
$entities = $tweetRepo->getTweetList(
$filter,
$limit,
$start,
$this->security->isGranted($this->getPermissionBase().':viewother')
);
foreach ($entities as $entity) {
$results[$entity['language']][$entity['id']] = $entity['name'];
}
// sort by language
ksort($results);
unset($entities);
break;
}
return $results;
}
/**
* Create/update Tweet Stat and update sent count for Tweet.
*
* @param string $source
* @param int $sourceId
*
* @return $this
*/
public function registerSend(Tweet $tweet, Lead $lead, array $sendResponse, $source = null, $sourceId = null)
{
$statRepo = $this->getStatRepository();
// Update failed tweet
$stat = $statRepo->findOneBy(
[
'lead' => $lead->getId(),
'tweet' => $tweet->getId(),
'source' => $source,
'sourceId' => $sourceId,
'isFailed' => true,
]
);
if (!$stat) {
// Create new entity
$stat = new TweetStat();
} else {
// Or add 1 to the retry count
$stat->retryCountUp();
}
$stat->setTweet($tweet);
$stat->setLead($lead);
$stat->setResponseDetails($sendResponse);
$stat->setSource($source);
$stat->setSourceId($sourceId);
$fields = $lead->getProfileFields();
if (!empty($fields['twitter'])) {
$stat->setHandle($fields['twitter']);
}
if (!empty($sendResponse['id_str'])) {
$stat->setDateSent(new \DateTime());
$stat->setTwitterTweetId($sendResponse['id_str']);
$tweet->sentCountUp();
$this->saveEntity($tweet);
} else {
$stat->setIsFailed(true);
}
$statRepo->saveEntity($stat);
return $this;
}
/**
* @param Tweet $entity
* @param array<mixed> $options
*/
public function createForm($entity, FormFactoryInterface $formFactory, $action = null, $options = []): \Symfony\Component\Form\FormInterface
{
if (!$entity instanceof Tweet) {
throw new MethodNotAllowedHttpException(['Tweet']);
}
if (!empty($action)) {
$options['action'] = $action;
}
return $formFactory->create(TweetType::class, $entity, $options);
}
/**
* Get a specific entity or generate a new one if id is empty.
*
* @param int $id
*/
public function getEntity($id = null): ?Tweet
{
if (null === $id) {
return new Tweet();
}
return parent::getEntity($id);
}
/**
* @throws MethodNotAllowedHttpException
*/
protected function dispatchEvent($action, &$entity, $isNew = false, ?Event $event = null): ?Event
{
if (!$entity instanceof Tweet) {
throw new MethodNotAllowedHttpException(['Tweet']);
}
switch ($action) {
case 'pre_save':
$name = SocialEvents::TWEET_PRE_SAVE;
break;
case 'post_save':
$name = SocialEvents::TWEET_POST_SAVE;
break;
case 'pre_delete':
$name = SocialEvents::TWEET_PRE_DELETE;
break;
case 'post_delete':
$name = SocialEvents::TWEET_POST_DELETE;
break;
default:
return null;
}
if ($this->dispatcher->hasListeners($name)) {
if (empty($event)) {
$event = new Events\SocialEvent($entity, $isNew);
}
$this->dispatcher->dispatch($event, $name);
return $event;
} else {
return null;
}
}
public function getRepository(): TweetRepository
{
return $this->em->getRepository(Tweet::class);
}
public function getStatRepository(): TweetStatRepository
{
return $this->em->getRepository(TweetStat::class);
}
public function getPermissionBase(): string
{
return 'mauticSocial:tweets';
}
}

View File

@@ -0,0 +1,5 @@
# Mautic bundle for Social plugin
## This plugin is managed centrally in https://github.com/mautic/mautic/blob/head/plugins/MauticSocialBundle and this is a read-only mirror repository.
**📣 Please make PRs and issues against Mautic Core, not here!**

View File

@@ -0,0 +1,25 @@
<div id="compose-tweet">
<div id="character-count" class="text-right small">
{{ 'mautic.social.twitter.tweet.count'|trans }}
<span></span>
</div>
{{ form_row(form.tweet_text) }}
<div class="row">
<div id="handle" class="col-md-2">
<label class="control-label">
{{ 'mautic.social.twitter.tweet.handle'|trans }}
</label>
{{ form_row(form.handle) }}
</div>
<div id="asset" class="col-md-5">
{{ form_row(form.asset_link) }}
</div>
<div id="page" class="col-md-5">
{{ form_row(form.page_link) }}
</div>
</div>
</div>
{{ includeScript('plugins/MauticSocialBundle/Assets/js/social.js', 'composeSocialWatcher', 'composeSocialWatcher') }}

View File

@@ -0,0 +1,16 @@
{% block _config_social_config_widget %}
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">{{ 'mautic.config.tab.social_config'|trans }}</h3>
</div>
<div class="panel-body">
{% for f in form.children %}
<div class="row">
<div class="col-md-6">
{{ form_row(f) }}
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,9 @@
<div class="row">
<div class="col-md-6">
<div class="row">
<div class="form-group col-xs-12">
{{ form_row(form.custom) }}
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,16 @@
<div class="row">
<div class="col-md-6">
<div class="row">
<div class="form-group col-xs-12">
{{ form_row(form.handle) }}
</div>
</div>
</div>
<div class="col-md-6">
<div class="row">
<div class="form-group col-xs-12">
{{ form_row(form.checknames) }}
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,16 @@
<div class="row">
<div class="col-md-6">
<div class="row">
<div class="form-group col-xs-12">
{{ form_row(form.hashtag) }}
</div>
</div>
</div>
<div class="col-md-6">
<div class="row">
<div class="form-group col-xs-12">
{{ form_row(form.checknames) }}
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,25 @@
<div id="compose-tweet">
<div id="character-count" class="text-right small">
{{ 'mautic.social.twitter.tweet.count'|trans }}
<span></span>
</div>
{{ form_row(form.tweet_text) }}
<div class="row">
<div id="handle" class="col-md-2">
<label class="control-label">
{{ 'mautic.social.twitter.tweet.handle'|trans }}
</label>
{{ form_row(form.handle) }}
</div>
<div id="asset" class="col-md-5">
{{ form_row(form.asset_link) }}
</div>
<div id="page" class="col-md-5">
{{ form_row(form.page_link) }}
</div>
</div>
</div>
{{ includeScript('plugins/MauticSocialBundle/Assets/js/social.js', 'composeSocialWatcher', 'composeSocialWatcher') }}

View File

@@ -0,0 +1,8 @@
{% import '@MauticSocial//macros.html.twig' as social %}
<div class="media">
{{ social.profileImage(profile) }}
<div class="media-body">
<h4 class="media-heading">{{ profile.name }}</h4>
<p class="text-secondary"><a href="https://facebook.com/{{ profile.profileHandle }}" target="_blank">{{ profile.profileHandle }}</a></p>
</div>
</div>

View File

@@ -0,0 +1,6 @@
<div class="panel-body">
{{ include('@MauticSocial/Integration/Facebook/Profile/profile.html.twig', {
'lead': lead,
'profile': details.profile,
}) }}
</div>

View File

@@ -0,0 +1,26 @@
{% set locale = app.request.locale %}
{% set settings = field.properties|default([]) %}
{% set layout = settings.layout|default('standard') %}
{% set action = settings.action|default('like') %}
{% set showFaces = settings.showFaces is not empty ? 'true' : 'false' %}
{% set showShare = settings.showShare is not empty ? 'true' : 'false' %}
{% set clientId = settings.keys.clientId|default('') %}
{# add FB's required OG tag #}
<meta property="og:type" content="website" />
<div class="fb-{% if 'share' == action %}share-button{% else %}like{% endif %} share-button facebook-share-button layout-{{ layout }} action-{{ action }}"
data-{% if 'share' == action %}type{% else %}layout{% endif %}="{{ layout }}"
{% if 'share' != action %}
data-action="{{ action }}"
data-show-faces="{{ showFaces }}"
data-share="{{ showShare }}"
{% endif %}
</div>
<div id="fb-root"></div>
<script>(function(d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = '//connect.facebook.net/{$locale}/sdk.js#xfbml=1&appId={{ clientId }}&version=v2.0';
fjs.parentNode.insertBefore(js, fjs);
}(document, 'script', 'facebook-jssdk'));</script>

View File

@@ -0,0 +1,17 @@
{% import '@MauticSocial//macros.html.twig' as social %}
{% set tableFields = ['gender', 'homeCity', 'bio'] %}
<div class="media">
{{ social.profileImage(profile) }}
<div class="media-body">
<h4 class="media-heading">{{ profile.firstName }} {{ profile.lastName }}</h4>
<p class="text-secondary"><a href="https://foursquare.com/user/{{ profile.profileHandle }}" target="_blank">{{ profile.profileHandle }}</a></p>
<table class="table table-condensed">
{% for t in tableFields|filter(v => profile[v] is defined and profile[v] is not empty) %}
<tr>
<td>{{ translatorConditional('mautic.integration.common.' ~ t, 'mautic.integration.Foursquare.' ~ t) }}</td>
<td>{{ profile[t] }}</td>
</tr>
{% endfor %}
</table>
</div>
</div>

View File

@@ -0,0 +1,15 @@
<ul class="list-group">
{% for item in activity %}
<li class="bdr-w-0 list-group-item">
<h4 class="mt-10 mb-10 pb-10"><i class="ri-check-line-circle-o"></i> {{ item.tipText }}</h4>
<p class="alert alert-warning">
{{ item.venueName }}<br />
{% for l in item.venueLocation %}
{{ l }}<br />
{% endfor %}
</p>
<p class="text-secondary"><i class="ri-time-line"></i> {{ dateToFull(item.createdAt, 'UTC', 'U') }}</p>
{% if not loop.first %}<hr />{% endif %}
</li>
{% endfor %}
</ul>

View File

@@ -0,0 +1,29 @@
<div class="panel-toolbar np">
<ul class="nav nav-tabs nav-tabs-contained">
<li class="active">
<a href="#FoursquareProfile" role="tab" data-toggle="tab">
{{ 'mautic.lead.lead.social.profile'|trans }}
</a>
</li>
<li>
<a href="#FoursquareTips" role="tab" data-toggle="tab">
{{ 'mautic.lead.lead.social.foursquare.tips'|trans }}
</a>
</li>
</ul>
</div>
<div class="np panel-body tab-content">
<div class="pa-20 tab-pane active" id="FoursquareProfile">
{{ include('@MauticSocial/Integration/Foursquare/Profile/profile.html.twig', {
'lead': lead,
'profile': details.profile,
}) }}
</div>
<div class="tab-pane" id="FoursquareTips">
{{ include('@MauticSocial/Integration/Foursquare/Profile/tips.html.twig', {
'lead': lead,
'activity': details.activity.tips|default([]),
}) }}
</div>
</div>

View File

@@ -0,0 +1,12 @@
<div class="container-fluid">
<div class="img-grid row">
{% for a in activity %}
{% if loop.index0 is divisible by(3) %}</div><div class="row">{% endif %}
<div class="col-xs-4 social-image">
<a href="javascript: void(0);" onclick="Mautic.showSocialMediaImageModal('{{ a.url }}');">
<img class="img-responsive img-thumbnail" src="{{ a.url }}" />
</a>
</div>
{% endfor %}
</div>
</div>

View File

@@ -0,0 +1,14 @@
{% import '@MauticSocial//macros.html.twig' as social %}
<div class="media">
{{ social.profileImage(profile) }}
<div class="media-body">
<h4 class="media-title">{{ profile.full_name }}</h4>
<p><a href="https://instagram.com/{{ profile.profileHandle }}" target="_blank">{{ profile.profileHandle }}</a></p>
<p class="text-secondary">
{{ profile.website }}
</p>
<p class="text-secondary">
{{ profile.bio }}
</p>
</div>
</div>

View File

@@ -0,0 +1,26 @@
<div class="panel-toolbar np">
<ul class="nav nav-tabs nav-tabs-contained">
<li class="active">
<a href="#InstagramProfile" role="tab" data-toggle="tab">
{{ 'mautic.lead.lead.social.profile'|trans }}
</a>
</li>
<li>
<a href="#InstagramPhotos" role="tab" data-toggle="tab">
{{ 'mautic.lead.lead.social.photos'|trans }}
</a>
</li>
</ul>
</div>
<div class="np panel-body tab-content">
<div class="pa-20 tab-pane active" id="InstagramProfile">
{{ include('@MauticSocial/Integration/Instagram/Profile/profile.html.twig', {
'profile': details.profile,
}) }}
</div>
<div class="pa-20 tab-pane" id="InstagramPhotos">
{{ include('@MauticSocial/Integration/Instagram/Profile/photos.html.twig', {
'activity': details.activity.photos|default([]),
}) }}
</div>
</div>

View File

@@ -0,0 +1,14 @@
{% import '@MauticSocial//macros.html.twig' as social %}
<div class="media">
{{ social.profileImage(profile) }}
<div class="media-body">
<h4 class="media-title">{{ profile.formattedName }}</h4>
<p><a href="https://www.linkedin.com/{{ profile.profileHandle }}" target="_blank">{{ profile.profileHandle }}</a></p>
<p class="text-secondary">
{{ profile.headline }}
</p>
<p class="text-secondary">
{{ profile.summary }}
</p>
</div>
</div>

View File

@@ -0,0 +1,21 @@
<div class="panel-toolbar np">
<ul class="nav nav-tabs nav-tabs-contained">
<li class="active">
<a href="#LinkedInProfile" role="tab" data-toggle="tab">
{{ 'mautic.lead.lead.social.profile'|trans }}
</a>
</li>
<li>
<a href="#LinkedInPhotos" role="tab" data-toggle="tab">
{{ 'mautic.lead.lead.social.photos'|trans }}
</a>
</li>
</ul>
</div>
<div class="np panel-body tab-content">
<div class="pa-20 tab-pane active" id="LinkedInProfile">
{{ include('@MauticSocial/Integration/LinkedIn/Profile/profile.html.twig', {
'profile': details.profile,
}) }}
</div>
</div>

View File

@@ -0,0 +1,8 @@
{% set locale = app.request.locale %}
{% set counter = settings.counter|default('none') %}
<div class="share-button linkedin-share-button layout-{{ counter }}">
<script type="IN/Share" {% if 'none' != counter %}data-counter="{{ settings.counter }}"{% endif %}></script>
</div>
<script src="//platform.linkedin.com/in.js" type="text/javascript">
lang: {{ locale }}
</script>

View File

@@ -0,0 +1,10 @@
<div class="container-fluid">
<div class="img-grid row">
{% for a in activity %}
{% if not loop.first and loop.index0 is divisible by(3) %}</div><div class="row">{% endif %}
<div class="col-xs-4 social-image">
<a href="javascript: void(0);" onclick="Mautic.showSocialMediaImageModal('{{ a.url }}');"><img class="img-responsive img-thumbnail" src="{{ a.url }}" /></a>
</div>
{% endfor %}
</div>
</div>

View File

@@ -0,0 +1,14 @@
{% import '@MauticSocial//macros.html.twig' as social %}
<div class="media">
{{ social.profileImage(profile) }}
<div class="media-body">
<h4 class="media-title">{{ profile.name }}</h4>
<p><a href="https://twitter.com/{{ profile.profileHandle }}" target="_blank">{{ profile.profileHandle }}</a></p>
<p class="text-secondary">
{{ profile.location }}
</p>
<p class="text-secondary">
{{ profile.description|purify }}
</p>
</div>
</div>

View File

@@ -0,0 +1,12 @@
<ul class="twitter-tags tag-cloud">
{% for tag, t in activity %}
{% if (t.count / 10) < 1 %}
{% set fontSize = (t.count / 10) + 1 %}
{% elseif (t.count / 10) > 2 %}
{% set fontSize = 2 %}
{% else %}
{% set fontSize = t.count / 10 %}
{% endif %}
<li style="font-size: {{ fontSize }}em"><a href="{{ t.url }}" target="_new">{{ tag }}</a></li>
{% endfor %}
</ul>

View File

@@ -0,0 +1,10 @@
<ul class="list-group">
{% for item in activity %}
{% set border = 'bdr-b bdr-l-wdh-0 bdr-r-wdh-0' %}
{% if loop.first or loop.last %}{% set border = 'bdr-w-0' %}{% endif %}
<li class="{{ border }} pa-15 list-group-item">
<p>{{ item.tweet }}</p>
<span class="text-secondary"><i class="ri-time-line"></i> {{ dateToFull(item.published) }}</span>
</li>
{% endfor %}
</ul>

View File

@@ -0,0 +1,50 @@
<div class="panel-toolbar np">
<ul class="nav nav-tabs nav-tabs-contained">
<li class="active">
<a href="#TwitterProfile" role="tab" data-toggle="tab">
{{ 'mautic.lead.lead.social.profile'|trans }}
</a>
</li>
<li>
<a href="#TwitterTweets" role="tab" data-toggle="tab">
{{ 'mautic.lead.lead.social.twitter.tweets'|trans }}
</a>
</li>
<li>
<a href="#TwitterPhotos" role="tab" data-toggle="tab">
{{ 'mautic.lead.lead.social.photos'|trans }}
</a>
</li>
<li>
<a href="#TwitterTags" role="tab" data-toggle="tab">
{{ 'mautic.lead.lead.social.tags'|trans }}
</a>
</li>
</ul>
</div>
<div class="np panel-body tab-content">
<div class="pa-20 tab-pane active" id="TwitterProfile">
{{ include('@MauticSocial/Integration/Twitter/Profile/profile.html.twig', {
'lead': lead,
'profile': details.profile,
}) }}
</div>
<div class="tab-pane" id="TwitterTweets">
{{ include('@MauticSocial/Integration/Twitter/Profile/tweets.html.twig', {
'lead': lead,
'activity': details.activity.tweets|default([]),
}) }}
</div>
<div class="pa-20 tab-pane" id="TwitterPhotos">
{{ include('@MauticSocial/Integration/Twitter/Profile/photos.html.twig', {
'lead': lead,
'activity': details.activity.photos|default([]),
}) }}
</div>
<div class="pa-20 tab-pane" id="TwitterTags">
{{ include('@MauticSocial/Integration/Twitter/Profile/tags.html.twig', {
'lead': lead,
'activity': details.activity.tags|default([]),
}) }}
</div>
</div>

View File

@@ -0,0 +1,11 @@
<div class="share-button twitter-share-button layout-{{ settings.count|default('0') }}">
<a href="https://twitter.com/share"
{% if settings.text is defined and settings.text is not empty %}data-text="{{ settings.text }}"{% endif %}
{% if settings.via is defined and settings.via is not empty %}data-via="{{ settings.via }}"{% endif %}
{% if settings.related is defined and settings.related is not empty %}data-related="{{ settings.related }}"{% endif %}
{% if settings.hashtags is defined and settings.hashtags is not empty %}data-hashtags="{{ settings.hashtags }}"{% endif %}
{% if settings.size is defined and settings.size is not empty %}data-size="{{ settings.size }}"{% endif %}
{% if settings.count is defined and settings.count is not empty %}data-count="{{ settings.count }}"{% endif %}
class="twitter-share-button share-button">{{ 'mautic.integration.Twitter.share.tweet'|trans }}</a>
</div>
<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+'://platform.twitter.com/widgets.js';fjs.parentNode.insertBefore(js,fjs);}}(document, 'script', 'twitter-wjs');</script>

View File

@@ -0,0 +1,169 @@
{#
Varables
- field
- formName (optional, string)
- fieldPage
- contactFields
- companyFields
- inBuilder
- fields
- inForm (optional, bool)
- required (optional, bool)
#}
{% set defaultInputClass = 'button' %}
{% set containerType = 'div-wrapper' %}
{# start: field_helper #}
{% set defaultInputFormClass = defaultInputFormClass|default('') %}
{% set defaultLabelClass = defaultLabelClass|default('label') %}
{% set formName = formName|default('') %}
{% set defaultInputClass = 'mauticform-' ~ defaultInputClass %}
{% set defaultLabelClass = 'mauticform-' ~ defaultLabelClass %}
{% set containerClass = containerClass|default(containerType) %}
{% set order = field.order|default(0) %}
{% set validationMessage = '' %}
{% set inputAttributes = htmlAttributesStringToArray(field.inputAttributes|default('')) %}
{% set labelAttributes = htmlAttributesStringToArray(field.labelAttributes|default('')) %}
{% set containerAttributes = htmlAttributesStringToArray(field.containerAttributes|default('')) %}
{% if ignoreName is not defined or (ignoreName is defined and ignoreName is empty) %}
{% set inputName = 'mauticform[' ~ field.alias ~ ']' %}
{% if field.properties.multiple is defined %}
{% set inputName = inputName ~ '[]' %}
{% endif %}
{% set inputAttributes = inputAttributes|merge({
'name': inputName,
}) %}
{% endif %}
{% if field.type not in ['checkboxgrp', 'radiogrp', 'textarea'] %}
{% set inputAttributes = inputAttributes|merge({
'value': field.defaultValue|default(''),
}) %}
{% endif %}
{% if ignoreId is not defined or (ignoreId is defined and ignoreId is empty) %}
{% set inputAttributes = inputAttributes|merge({
'id': 'mauticform_input' ~ formName ~ '_' ~ field.alias,
}) %}
{% set labelAttributes = labelAttributes|merge({
'id': 'mauticform_label' ~ formName ~ '_' ~ field.alias,
'for': 'mauticform_input' ~ formName ~ '_' ~ field.alias,
}) %}
{% endif %}
{% if field.properties.placeholder is defined and field.properties.placeholder is not empty %}
{% set inputAttributes = inputAttributes|merge({
'placeholder': field.properties.placeholder,
}) %}
{% endif %}
{# Label and input #}
{% if inForm is defined and (true == inForm or inForm is not empty) %}
{% if field.type in ['button', 'pagebreak'] %}
{% set defaultInputFormClass = defaultInputFormClass ~ ' btn btn-ghost' %}
{% endif %}
{% set labelAttributes = labelAttributes|merge({
'class': labelAttributes.class|default([])|merge([defaultLabelClass]),
}) %}
{% set inputAttributes = inputAttributes|merge({
'disabled': 'disabled',
'class': inputAttributes.class|default([])|merge([defaultInputClass, defaultInputFormClass]),
}) %}
{% else %}
{% set labelAttributes = labelAttributes|merge({
'class': labelAttributes.class|default([])|merge([defaultLabelClass]),
}) %}
{% set inputAttributes = inputAttributes|merge({
'class': inputAttributes.class|default([])|merge([defaultInputClass]),
}) %}
{% endif %}
{# Container #}
{% set containerAttributes = containerAttributes|merge({
'id': 'mauticform' ~ formName|default('') ~ '_' ~ id,
'class': containerAttributes.class|default([])|merge([
'mauticform-row',
'mauticform-' ~ containerClass,
'mauticform-field-' ~ order,
]),
}) %}
{% if field.parent and fields[field.parent] is defined %}
{% set values = field.conditions.values|join('|') %}
{% if field.conditions.any is not empty and 'notIn' != field.conditions.expr %}
{% set values = '*' %}
{% endif %}
{% set containerAttributes = containerAttributes|merge({
'data-mautic-form-show-on': fields[field.parent].alias ~ ':' ~ values,
'data-mautic-form-expr': field.conditions.expr,
'class': containerAttributes.class|merge([
'mauticform-field-hidden',
]),
}) %}
{% endif %}
{# Field is required #}
{% if field.isRequired is defined and field.isRequired %}
{% set required = true %}
{% set validationMessage = field.validationMessage %}
{% if validationMessage is empty %}
{% set validationMessage = 'mautic.form.field.generic.required'|trans([], 'validators') %}
{% endif %}
{% set containerAttributes = containerAttributes|merge({
'class': containerAttributes.class|default([])|merge([
'mauticform-required',
]),
'data-validate': field.alias,
'data-validation-type': field.type,
}) %}
{% if field.properties.multiple is defined and field.properties.multiple is not empty %}
{% set containerAttributes = containerAttributes|merge({
'data-validate-multiple': 'true',
}) %}
{% endif %}
{% elseif required is defined and true == required %}
{# Forced to be required #}
{% set containerAttributes = containerAttributes|merge({
'class': containerAttributes.class|default([])|merge([
'mauticform-required',
]),
}) %}
{% endif %}
{# end: field_helper #}
{% set action = app.request.get('objectAction') %}
{% set settings = field.properties %}
{% set integrations = [] %}
{% if settings.integrations is defined and settings.integrations is not empty %}
{% set integrations = settings.integrations[0:-1]|split(',') %}
{% endif %}
{% set formName = formName|replace({'_': ''})|default('mauticform') %}
<script src="{{ url('mautic_social_js_generate', {'formName': formName}) }}" type="text/javascript" charset="utf-8" async="async"></script>
<div {% for attrName, attrValue in containerAttributes %}{{ attrName }}="{% if attrValue is iterable %}{{ attrValue|join(' ') }}{% else %}{{ attrValue }}{% endif %}"{% endfor %}>
{% if inForm is defined and (true == inForm or inForm is not empty) %}
{{ include('@MauticForm/Builder/_actions.html.twig', {
'deleted': false,
'id': id,
'formId': formId,
'formName': formName,
'disallowDelete': false,
}, with_context=false) }}
{% endif %}
{% if field.showLabel %}<label {% for attrName, attrValue in labelAttributes %}{{ attrName }}="{% if attrValue is iterable %}{{ attrValue|join(' ') }}{% else %}{{ attrValue }}{% endif %}"{% endfor %}>{{ field.label }}</label>{% endif %}
{% for integration in integrations %}
{% if settings.buttonImageUrl is defined and settings.buttonImageUrl is not empty and integration is not empty %}
<a href="#" onclick="openOAuthWindow('{{ settings['authUrl_' ~ integration ~ ''] }}')"><img src="{{ settings['buttonImageUrl'] }}btn_{{ integration }}.png"></a>
{% endif %}
{% endfor %}
</div>

View File

@@ -0,0 +1,81 @@
{% if items|length > 0 %}
<div class="table-responsive">
<table class="table table-hover monitoring-list" id="monitoringTable">
<thead>
<tr>
{{ include('@MauticCore/Helper/tableheader.html.twig', {
'checkall': 'true',
'target': '#monitoringTable',
}) }}
{{ include('@MauticCore/Helper/tableheader.html.twig', {
'sessionVar': 'social.monitoring',
'orderBy': 'e.title',
'text': 'mautic.core.title',
'class': 'col-monitoring-title',
'default': true,
}) }}
{{ include('@MauticCore/Helper/tableheader.html.twig', {
'sessionVar': 'social.monitoring',
'orderBy': 'e.id',
'text': 'mautic.core.id',
'class': 'visible-md visible-lg col-asset-id',
}) }}
</tr>
</thead>
<tbody>
{% for k, item in items %}
<tr>
<td>
{{ include('@MauticCore/Helper/list_actions.html.twig', {
'item': item,
'templateButtons': {
'edit': securityIsGranted('mauticSocial:monitoring:edit'),
'delete': securityIsGranted('mauticSocial:monitoring:delete'),
},
'routeBase': 'social',
'langVar': 'mautic.social.monitoring',
'nameGetter': 'getTitle',
}) }}
</td>
<td>
<div>
{{ include('@MauticCore/Helper/publishstatus_icon.html.twig', {
'item': item,
'model': 'social.monitoring',
}) }}
<a href="{{ path('mautic_social_action', {'objectAction': 'view', 'objectId': item.id}) }}" data-toggle="ajax">
{{ item.title }}
</a>
</div>
{{ include('@MauticCore/Helper/description--inline.html.twig', {
'description': item.description
}) }}
</td>
<td class="visible-md visible-lg">{{ item.id }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="panel-footer">
{{ include('@MauticCore/Helper/pagination.html.twig', {
'totalItems': items|length,
'page': page,
'limit': limit,
'menuLinkId': 'mautic_campaign_index',
'baseUrl': path('mautic_social_index'),
'sessionVar': 'social.monitoring',
'routeBase': 'social',
}) }}
</div>
{% else %}
{{ include('@MauticCore/Helper/noresults.html.twig', {'tip': 'mautic.mautic.social.monitoring.noresults.tip'}) }}
{% endif %}
{{ include('@MauticCore/Helper/modal.html.twig', {
'id': 'MonitoringPreviewModal',
'header': false,
}) }}

View File

@@ -0,0 +1,128 @@
{#
Variables
- isEmbeded
- activeMonitoring
#}
{% extends isEmbedded ? '@MauticCore/Default/raw_output.html.twig' : '@MauticCore/Default/content.html.twig' %}
{% block mauticContent 'monitoring' %}
{% block headerTitle activeMonitoring.title %}
{% block preHeader %}
{{- include('@MauticCore/Helper/page_actions.html.twig',
{
'item' : activeMonitoring,
'templateButtons' : {
'close' : securityIsGranted('mauticSocial:monitoring:view'),
},
'routeBase' : 'social',
'langVar' : 'monitoring',
'targetLabel' : 'mautic.social.monitoring'|trans
}
) -}}
{% endblock %}
{% block actions %}
{{- include('@MauticCore/Helper/page_actions.html.twig', {
'item': activeMonitoring,
'templateButtons': {
'edit': securityIsGranted('mauticSocial:monitoring:edit'),
'delete': securityIsGranted('mauticSocial:monitoring:delete'),
},
'routeBase': 'social',
'langVar': 'monitoring',
'nameGetter': 'getTitle',
}) -}}
{% endblock %}
{% block publishStatus %}
{{ include('@MauticCore/Helper/publishstatus_badge.html.twig', {'entity': activeMonitoring}) }}
{% endblock %}
{% block content %}
{{ includeScript('plugins/MauticSocialBundle/Assets/js/social.js') }}
<!-- start: box layout -->
<div class="box-layout">
<!-- left section -->
<div class="col-md-9 height-auto">
<div>
<!-- monitoring detail header -->
{% include '@MauticCore/Helper/description--expanded.html.twig' with {'description': activeMonitoring.description} %}
<!--/ monitoring detail header -->
<!-- monitoring detail collapseable -->
<div class="collapse pr-md pl-md" id="asset-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': activeMonitoring}) }}
</tbody>
</table>
</div>
</div>
</div>
<!--/ monitoring collapseable -->
</div>
<div>
<!-- stats -->
<div class="pa-md">
<div class="row">
<div class="col-sm-12">
<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-twitter-x-line"></span>
{{ ('mautic.social.monitoring.' ~ activeMonitoring.networkType ~ '.popularity')|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': leadStats, '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="#leads-container" role="tab" data-toggle="tab">
{{ 'mautic.lead.leads'|trans }}
</a>
</li>
</ul>
<!--/ tabs controls -->
</div>
<!-- start: tab-content -->
<div class="tab-content pa-md">
<!-- #events-container -->
<div class="tab-pane active fade in bdr-w-0 page-list" id="leads-container">
{{ monitorLeads|raw }}
</div>
</div>
</div>
<!--/ left section -->
<!-- right section -->
<div class="col-md-3 bdr-l height-auto">
<!-- recent activity -->
{{ include('@MauticCore/Helper/recentactivity.html.twig', {'logs': logs}) }}
</div>
<!--/ right section -->
<input id="itemId" type="hidden" value="{{ activeMonitoring.id }}" />
</div>
<!--/ end: box layout -->
{% endblock %}

View File

@@ -0,0 +1,68 @@
{#
Variables
- entity
- form
#}
{% extends '@MauticCore/Default/content.html.twig' %}
{% block mauticContent 'monitoring' %}
{% block headerTitle %}
{% if entity.id %}
{{ 'mautic.social.monitoring.menu.edit'|trans({'%name%': entity.title|trans}) }}
{% else %}
{{ 'mautic.social.monitoring.menu.new'|trans }}
{% endif %}
{% endblock %}
{% block modal %}
{{ include('@MauticCore/Helper/modal.html.twig', {
'id': 'formComponentModal',
'header': false,
'footerButtons': true,
}) }}
{% endblock %}
{% block content %}
{{ includeScript('plugins/MauticSocialBundle/Assets/js/social.js') }}
{{ form_start(form) }}
<!-- start: box layout -->
<div class="box-layout">
<!-- container -->
<div class="col-md-9 height-auto bdr-r">
<div class="pa-md">
<div class="row">
<div class="col-md-12">
<div class="row">
<div class="col-md-6">{{ form_row(form.title) }}</div>
<div class="col-md-6">{{ form_row(form.networkType) }}</div>
</div>
<div id="properties-container">
<div class="row">
{% if form.properties is defined %}
{% for child in form.properties %}
<div class="col-md-6">
{{ form_row(child) }}
</div>
{% endfor %}
{% endif %}
</div>
</div>
{{ form_row(form.description) }}
</div>
</div>
</div>
</div>
<div class="col-md-3 height-auto">
<div class="pr-lg pl-lg pt-md pb-md">
{{ form_row(form.isPublished) }}
{{ form_row(form.publishUp) }}
{{ form_row(form.publishDown) }}
{{ form_row(form.category) }}
{{ form_row(form.lists) }}
</div>
</div>
</div>
{{ form_end(form) }}
{% endblock %}

View File

@@ -0,0 +1,65 @@
{% set isIndex = 'index' == tmpl ? true : false %}
{% set tmpl = 'list' %}
{% extends isIndex ? '@MauticCore/Default/content.html.twig' : '@MauticCore/Default/raw_output.html.twig' %}
{% block mauticContent 'monitoring' %}
{% block headerTitle %}{{ 'mautic.social.monitoring'|trans }}{% endblock %}
{% block content %}
{% if isIndex %}
<div id="page-list-wrapper" class="panel panel-default">
{{ include('@MauticCore/Helper/list_toolbar.html.twig', {
'searchValue': searchValue,
'action': currentRoute,
'page_actions': {
'templateButtons': {
'new': securityIsGranted('mauticSocial:monitoring:create'),
},
'routeBase': 'social',
'langVar': 'monitoring',
},
'bulk_actions': {
'langVar': 'mautic.social.monitoring',
'routeBase': 'social',
'templateButtons': {
'delete': securityIsGranted('mauticSocial:monitoring:delete'),
},
},
'quickFilters': [
{
'search': 'mautic.core.searchcommand.ispublished',
'label': 'mautic.core.form.active',
'tooltip': 'mautic.core.searchcommand.ispublished.description',
'icon': 'ri-check-line'
},
{
'search': 'mautic.core.searchcommand.isunpublished',
'label': 'mautic.core.form.inactive',
'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">
{% endif %}
{{ include('@MauticSocial/Monitoring/_list.html.twig') }}
{% if isIndex %}
</div>
</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1 @@
{{ includeScript('plugins/MauticSocialBundle/Assets/js/social.js', 'composeSocialWatcher', 'composeSocialWatcher') }}

View File

@@ -0,0 +1,71 @@
<!-- Contact Monitoring tokens -->
<li class="panel">
<a role="button" id="headingContactMonitoring" class="accordion-heading collapsed" data-toggle="collapse"
data-parent="#tokensAccordion" href="#collapseContactMonitoring" aria-expanded="false"
aria-controls="collapseContactMonitoring">
<i class="ri-arrow-down-s-line accordion-arrow"></i>
<span class="accordion-title">{{ 'mautic.placeholder_tokens.contact_monitoring'|trans }}</span>
</a>
<div id="collapseContactMonitoring" class="collapse accordion-wrapper" role="tabpanel"
aria-labelledby="headingContactMonitoring">
<table class="table table-hover">
<thead>
<tr>
<th>{{ 'mautic.placeholder_tokens.variable_name'|trans }}</th>
<th>{{ 'mautic.placeholder_tokens.variable_syntax'|trans }}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ 'mautic.placeholder_tokens.monitoring.language'|trans }}</td>
<td><code>{language}</code></td>
</tr>
<tr>
<td>{{ 'mautic.placeholder_tokens.monitoring.title'|trans }}</td>
<td><code>{title}</code></td>
</tr>
<tr>
<td>{{ 'mautic.placeholder_tokens.monitoring.landing_page_title'|trans }}</td>
<td><code>{page_title}</code></td>
</tr>
<tr>
<td>{{ 'mautic.placeholder_tokens.monitoring.url'|trans }}</td>
<td><code>{url}</code></td>
</tr>
<tr>
<td>{{ 'mautic.placeholder_tokens.monitoring.landing_page_url'|trans }}</td>
<td><code>{page_url}</code></td>
</tr>
<tr>
<td>{{ 'mautic.placeholder_tokens.monitoring.referrer'|trans }}</td>
<td><code>{referrer}</code></td>
</tr>
<tr>
<td>{{ 'mautic.placeholder_tokens.monitoring.tracking_pixel'|trans }}</td>
<td><code>{tracking_pixel}</code></td>
</tr>
<tr>
<td>{{ 'mautic.placeholder_tokens.monitoring.utm_campaign'|trans }}</td>
<td><code>{utm_campaign}</code></td>
</tr>
<tr>
<td>{{ 'mautic.placeholder_tokens.monitoring.utm_content'|trans }}</td>
<td><code>{utm_content}</code></td>
</tr>
<tr>
<td>{{ 'mautic.placeholder_tokens.monitoring.utm_medium'|trans }}</td>
<td><code>{utm_medium}</code></td>
</tr>
<tr>
<td>{{ 'mautic.placeholder_tokens.monitoring.utm_source'|trans }}</td>
<td><code>{utm_source}</code></td>
</tr>
<tr>
<td>{{ 'mautic.placeholder_tokens.monitoring.utm_term'|trans }}</td>
<td><code>{utm_term}</code></td>
</tr>
</tbody>
</table>
</div>
</li>

View File

@@ -0,0 +1,75 @@
{% if items|length > 0 %}
<div class="table-responsive">
<table class="table table-hover tweet-list" id="tweetTable">
<thead>
<tr>
{{ include('@MauticCore/Helper/tableheader.html.twig', {
'checkall': 'true',
'target': '#tweetTable',
}) }}
{{ include('@MauticCore/Helper/tableheader.html.twig', {
'sessionVar': 'social.tweet',
'orderBy': 'e.name',
'text': 'mautic.core.name',
'class': 'col-tweet-name',
'default': true,
}) }}
{{ include('@MauticCore/Helper/tableheader.html.twig', {
'sessionVar': 'social.tweet',
'orderBy': 'e.id',
'text': 'mautic.core.id',
'class': 'visible-md visible-lg col-asset-id',
}) }}
</tr>
</thead>
<tbody>
{% for item in items %}
<tr>
<td>
{{ include('@MauticCore/Helper/list_actions.html.twig', {
'item': item,
'templateButtons': {
'edit': securityIsGranted('mauticSocial:tweets:edit'),
'delete': securityIsGranted('mauticSocial:tweets:delete'),
},
'routeBase': 'mautic_tweet',
'langVar': 'mautic.integration.Twitter',
'nameGetter': 'getName',
}) }}
</td>
<td>
<div>
{{ include('@MauticCore/Helper/publishstatus_icon.html.twig', {
'item': item,
'model': 'social.tweet',
}) }}
<a href="{{ path('mautic_tweet_action', {'objectAction': 'edit', 'objectId': item.id}) }}" data-toggle="ajax">
{{ item.name }}
</a>
</div>
{{ include('@MauticCore/Helper/description--inline.html.twig', {
'description': item.description
}) }}
</td>
<td class="visible-md visible-lg">{{ item.id }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="panel-footer">
{{ include('@MauticCore/Helper/pagination.html.twig', {
'totalItems': items|length,
'page': page,
'limit': limit,
'menuLinkId': 'mautic_tweet_index',
'baseUrl': path('mautic_tweet_index'),
'sessionVar': 'social.tweet',
'routeBase': 'tweet',
}) }}
</div>
{% else %}
{{ include('@MauticCore/Helper/noresults.html.twig', {'tip': 'mautic.mautic.social.tweet.noresults.tip'}) }}
{% endif %}

View File

@@ -0,0 +1,62 @@
{% extends '@MauticCore/Default/content.html.twig' %}
{% block mauticContent 'tweet' %}
{% block headerTitle %}
{% if entity.id %}
{{ 'mautic.social.tweet.menu.edit'|trans({'%name%': entity.name|trans}) }}
{% else %}
{{ 'mautic.social.tweet.menu.new'|trans }}
{% endif %}
{% endblock %}
{% block content %}
{{ includeScript('plugins/MauticSocialBundle/Assets/js/social.js', 'composeSocialWatcher', 'composeSocialWatcher') }}
{{ form_start(form) }}
<!-- start: box layout -->
<div class="box-layout">
<!-- container -->
<div class="col-md-9 height-auto bdr-r">
<div class="pa-md">
<div class="row">
<div class="col-md-4">
{{ form_row(form.name) }}
{{ form_row(form.description) }}
</div>
<div class="col-md-8">
{{ form_row(form.text) }}
<div class="row">
<div class="col-md-3">
<label class="control-label">
{{ 'mautic.social.twitter.tweet.handle'|trans }}
</label>
{{ form_row(form.handle) }}
</div>
<div class="col-md-3">
{{ form_row(form.asset) }}
</div>
<div class="col-md-3">
{{ form_row(form.page) }}
</div>
<div class="col-md-3">
<div id="character-count" class="text-right small">
{{ 'mautic.social.twitter.tweet.count'|trans }}
<span></span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3 height-auto">
<div class="pr-lg pl-lg pt-md pb-md">
{{ form_row(form.category) }}
</div>
</div>
</div>
{{ form_end(form) }}
{% endblock %}

View File

@@ -0,0 +1,30 @@
{% extends '@MauticCore/Default/content.html.twig' %}
{% block mauticContent 'tweet' %}
{% block content %}
{{ includeScript('plugins/MauticSocialBundle/Assets/js/social.js', 'composeSocialWatcher', 'composeSocialWatcher') }}
{{ form_start(form) }}
{{ form_row(form.name) }}
{{ form_row(form.text) }}
<div id="character-count" class="text-right small">
{{ 'mautic.social.twitter.tweet.count'|trans }}
<span></span>
</div>
<div class="row">
<div class="col-md-4">
<label class="control-label">
{{ 'mautic.social.twitter.tweet.handle'|trans }}
</label>
{{ form_row(form.handle) }}
</div>
<div class="col-md-4">{{ form_row(form.asset) }}</div>
<div class="col-md-4">{{ form_row(form.page) }}</div>
</div>
{{ form_row(form.description) }}
{{ form_row(form.category) }}
{{ form_end(form) }}
{% endblock %}

View File

@@ -0,0 +1,37 @@
{%- set isIndex = 'index' == tmpl -%}
{%- set tmpl = 'list' -%}
{% extends isIndex ? '@MauticCore/Default/content.html.twig' : '@MauticCore/Default/raw_output.html.twig' %}
{% block mauticContent 'tweet' %}
{% block headerTitle 'mautic.social.tweets'|trans %}
{% block content %}
{% if isIndex %}
<div id="page-list-wrapper" class="panel panel-default">
{{ include('@MauticCore/Helper/list_toolbar.html.twig', {
'searchValue': searchValue,
'action': currentRoute,
'page_actions': {
'templateButtons': {
'new': securityIsGranted('mauticSocial:tweets:create'),
},
'routeBase': 'mautic_tweet',
'langVar': 'tweet',
},
'bulk_actions': {
'langVar': 'mautic.social.tweets',
'routeBase': 'mautic_tweet',
'templateButtons': {
'delete': securityIsGranted('mauticSocial:tweets:delete'),
},
},
}) }}
<div class="page-list">
{{- include('@MauticSocial/Tweet/_list.html.twig') -}}
</div>
</div>
{% else %}
{{- include('@MauticSocial/Tweet/_list.html.twig') -}}
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,7 @@
{% macro profileImage(profile) %}
{% if profile.profileImage is defined and profile.profileImage is not empty %}
<div class="pull-left thumbnail">
<img src="{{ profile.profileImage }}" width="100px" class="media-object img-rounded" />
</div>
{% endif %}
{% endmacro %}

View File

@@ -0,0 +1,29 @@
<?php
namespace MauticPlugin\MauticSocialBundle\Security\Permissions;
use Mautic\CoreBundle\Security\Permissions\AbstractPermissions;
use Symfony\Component\Form\FormBuilderInterface;
class MauticSocialPermissions extends AbstractPermissions
{
public function __construct($params)
{
parent::__construct($params);
$this->addStandardPermissions('categories');
$this->addStandardPermissions('monitoring');
$this->addExtendedPermissions('tweets');
}
public function getName(): string
{
return 'mauticSocial';
}
public function buildForm(FormBuilderInterface &$builder, array $options, array $data): void
{
$this->addStandardFormFields('mauticSocial', 'categories', $builder, $data);
$this->addStandardFormFields('mauticSocial', 'monitoring', $builder, $data);
$this->addExtendedFormFields('mauticSocial', 'tweets', $builder, $data);
}
}

View File

@@ -0,0 +1,109 @@
<?php
namespace MauticPlugin\MauticSocialBundle;
/**
* Events available for MauticSocialBundle.
*/
final class SocialEvents
{
/**
* The mautic.monitor_pre_save event is dispatched right before a monitor is persisted.
*
* The event listener receives a
* MauticPlugin\MauticSocialBundle\Event\SocialEvent instance.
*
* @var string
*/
public const MONITOR_PRE_SAVE = 'mautic.monitor_pre_save';
/**
* The mautic.monitor_post_save event is dispatched right after a monitor is persisted.
*
* The event listener receives a
* MauticPlugin\MauticSocialBundle\Event\SocialEvent instance.
*
* @var string
*/
public const MONITOR_POST_SAVE = 'mautic.monitor_post_save';
/**
* The mautic.monitor_pre_delete event is dispatched before a monitor item is deleted.
*
* The event listener receives a
* MauticPlugin\MauticSocialBundle\Event\SocialEvent instance.
*
* @var string
*/
public const MONITOR_PRE_DELETE = 'mautic.monitor_pre_delete';
/**
* The mautic.monitor_post_delete event is dispatched after a monitor is deleted.
*
* The event listener receives a
* MauticPlugin\MauticSocialBundle\Event\SocialEvent instance.
*
* @var string
*/
public const MONITOR_POST_DELETE = 'mautic.monitor_post_delete';
/**
* The mautic.monitor_post_process event is dispatched after a monitor is processed passing along the data gleaned.
*
* The event listener receives a
* MauticPlugin\MauticSocialBundle\Event\SocialEvent instance.
*
* @var string
*/
public const MONITOR_POST_PROCESS = 'mautic.monitor_post_process';
/**
* The mautic.tweet_pre_save event is dispatched right before a tweet is persisted.
*
* The event listener receives a
* MauticPlugin\MauticSocialBundle\Event\SocialEvent instance.
*
* @var string
*/
public const TWEET_PRE_SAVE = 'mautic.tweet_pre_save';
/**
* The mautic.tweet_post_save event is dispatched right after a tweet is persisted.
*
* The event listener receives a
* MauticPlugin\MauticSocialBundle\Event\SocialEvent instance.
*
* @var string
*/
public const TWEET_POST_SAVE = 'mautic.tweet_post_save';
/**
* The mautic.tweet_pre_delete event is dispatched before a tweet item is deleted.
*
* The event listener receives a
* MauticPlugin\MauticSocialBundle\Event\SocialEvent instance.
*
* @var string
*/
public const TWEET_PRE_DELETE = 'mautic.tweet_pre_delete';
/**
* The mautic.tweet_post_delete event is dispatched after a tweet is deleted.
*
* The event listener receives a
* MauticPlugin\MauticSocialBundle\Event\SocialEvent instance.
*
* @var string
*/
public const TWEET_POST_DELETE = 'mautic.tweet_post_delete';
/**
* The mautic.social.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.social.on_campaign_trigger_action';
}

View File

@@ -0,0 +1,103 @@
<?php
namespace MauticPlugin\MauticSocialBundle\Tests\Functional\Controller;
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
use Mautic\UserBundle\Entity\Role;
use Mautic\UserBundle\Entity\User;
use Symfony\Component\PasswordHasher\PasswordHasherInterface;
class MonitoringControllerTest extends MauticMysqlTestCase
{
public const USERNAME = 'jhony';
public function testIndex(): void
{
$this->client->request('GET', '/s/monitoring');
$response = $this->client->getResponse();
$this->assertEquals(200, $response->getStatusCode());
}
public function testNew(): void
{
$this->client->request('GET', '/s/monitoring/new');
$response = $this->client->getResponse();
$this->assertEquals(200, $response->getStatusCode());
}
public function testEdit(): void
{
$this->client->request('GET', '/s/monitoring/edit/1');
$response = $this->client->getResponse();
$this->assertEquals(200, $response->getStatusCode());
}
public function testIndexWithoutPermission(): void
{
$this->createAndLoginUser();
$this->client->request('GET', '/s/monitoring');
$response = $this->client->getResponse();
$this->assertEquals(403, $response->getStatusCode());
}
public function testNewWithoutPermission(): void
{
$this->createAndLoginUser();
$this->client->request('GET', '/s/monitoring/new');
$response = $this->client->getResponse();
$this->assertEquals(403, $response->getStatusCode());
}
public function testEditWithoutPermission(): void
{
$this->createAndLoginUser();
$this->client->request('GET', '/s/monitoring/edit/1');
$response = $this->client->getResponse();
$this->assertEquals(403, $response->getStatusCode());
}
private function createAndLoginUser(): User
{
// Create non-admin role
$role = $this->createRole();
// Create non-admin user
$user = $this->createUser($role);
$this->em->flush();
$this->em->detach($role);
$this->loginUser($user);
$this->client->setServerParameter('PHP_AUTH_USER', self::USERNAME);
$this->client->setServerParameter('PHP_AUTH_PW', 'Maut1cR0cks!');
return $user;
}
private function createRole(bool $isAdmin = false): Role
{
$role = new Role();
$role->setName('Role');
$role->setIsAdmin($isAdmin);
$this->em->persist($role);
return $role;
}
private function createUser(Role $role): User
{
$user = new User();
$user->setFirstName('John');
$user->setLastName('Doe');
$user->setUsername(self::USERNAME);
$user->setEmail('john.doe@email.com');
$hasher = self::getContainer()->get('security.password_hasher_factory')->getPasswordHasher($user);
\assert($hasher instanceof PasswordHasherInterface);
$user->setPassword($hasher->hash('Maut1cR0cks!'));
$user->setRole($role);
$this->em->persist($user);
return $user;
}
}

Some files were not shown because too many files have changed in this diff Show More