diff --git a/config/install/disqus.settings.yml b/config/install/disqus.settings.yml index 5852b86..9f2473b 100644 --- a/config/install/disqus.settings.yml +++ b/config/install/disqus.settings.yml @@ -4,6 +4,7 @@ behavior: disqus_inherit_login: false disqus_disable_mobile: false disqus_track_newcomment_ga: false + disqus_notify_newcomment: false advanced: disqus_useraccesstoken: '' disqus_publickey: '' diff --git a/config/schema/disqus.schema.yml b/config/schema/disqus.schema.yml index 0f780bf..ad50678 100644 --- a/config/schema/disqus.schema.yml +++ b/config/schema/disqus.schema.yml @@ -23,6 +23,9 @@ disqus.settings: disqus_track_newcomment_ga: type: boolean label: 'Track new comments in Google Analytics' + disqus_notify_newcomment: + type: boolean + label: 'Notify of new comments by email' advanced: type: mapping label: 'Advanced' diff --git a/disqus.libraries.yml b/disqus.libraries.yml index d358f7f..d6053dd 100644 --- a/disqus.libraries.yml +++ b/disqus.libraries.yml @@ -14,3 +14,9 @@ ga: js/disqus_ga.js: {} dependencies: - disqus/disqus +notification: + version: 1.x + js: + js/disqus_notification.js: {} + dependencies: + - disqus/disqus diff --git a/disqus.module b/disqus.module index 8c0d476..9d947f4 100644 --- a/disqus.module +++ b/disqus.module @@ -233,6 +233,26 @@ function disqus_entity_update(EntityInterface $entity) { } /** + * Implements hook_mail(). + */ +function disqus_mail($key, &$message, $params) { + switch ($key) { + case 'new_comment': + $post = $params['post']; + $message['subject'] = t('New comment posted to @title', ['@title' => $post->thread->title]); + $message['body'] = array( + t('New comment posted on :url by @author:',[ + ':url' => $post->url, + '@author' => $post->author->name, + ]), + $post->raw_message, + ); + + break; + } +} + +/** * Implements hook_field_views_data(). */ function disqus_field_views_data(FieldStorageConfigInterface $field_storage) { diff --git a/disqus.routing.yml b/disqus.routing.yml index 36be6ae..b16f985 100644 --- a/disqus.routing.yml +++ b/disqus.routing.yml @@ -12,3 +12,11 @@ disqus.close_window: _controller: '\Drupal\disqus\Controller\DisqusController::closeWindow' requirements: _permission: 'access content' + +disqus.new_comment: + path: '/disqus/new-comment/{comment_id}' + defaults: + _controller: '\Drupal\disqus\Controller\NewCommentController::receiver' + _title: 'New comment' + requirements: + _access: 'TRUE' diff --git a/disqus.services.yml b/disqus.services.yml index 97febf9..5979160 100644 --- a/disqus.services.yml +++ b/disqus.services.yml @@ -2,3 +2,8 @@ services: disqus.manager: class: Drupal\disqus\DisqusCommentManager arguments: ['@entity_type.manager', '@entity_field.manager', '@current_user', '@module_handler'] + disqus.new_comment_subscriber: + class: Drupal\disqus\EventSubscriber\NewCommentSubscriber + arguments: ['@plugin.manager.mail', '@entity_type.manager', '@logger.factory', '@language_manager', '@router.no_access_checks'] + tags: + - { name: 'event_subscriber' } diff --git a/js/disqus_notification.js b/js/disqus_notification.js new file mode 100644 index 0000000..1d139d9 --- /dev/null +++ b/js/disqus_notification.js @@ -0,0 +1,21 @@ +/** + * @file + * JavaScript for the Disqus module's new comment notification. + */ + +(function ($) { + +"use strict"; + +/** + * Notify of new comments. + */ +Drupal.disqus.disqusNotifyNewComment = function (comment) { + $.post({ + url: drupalSettings.path.baseUrl + drupalSettings.path.pathPrefix + 'disqus/new-comment/' + comment.id, + data: {}, + dataType: 'json', + }); +}; + +})(jQuery, Drupal, drupalSettings); diff --git a/src/Controller/NewCommentController.php b/src/Controller/NewCommentController.php new file mode 100644 index 0000000..2060f96 --- /dev/null +++ b/src/Controller/NewCommentController.php @@ -0,0 +1,149 @@ +flood = $flood; + $this->tempStore = $temp_store_factory->get('disqus_new_comment'); + $this->eventDispatcher = $event_dispatcher; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('flood'), + $container->get('tempstore.shared'), + $container->get('event_dispatcher') + ); + } + + /** + * Receives notification of a new comment. + * + * @param string $comment_id + * The comment ID. + * + * @return \Symfony\Component\HttpFoundation\Response + * A Symfony response object. + */ + public function receiver($comment_id) { + // Do not process requests from the current user's IP if the limit for + // invalid requests has been reached. Default is 5 invalid requests allowed + // every 6 hours. This is arbitrarily based on the existing user login flood + // settings. + $flood_config = $this->config('user.flood'); + $limit = $flood_config->get('user_limit'); + $interval = $flood_config->get('user_window'); + + if ($this->flood->isAllowed('disqus.new_comment', $limit, $interval)) { + // Register a flood event; but it will be cleared if the request turns out + // to be genuine. + $this->flood->register('disqus.new_comment', $interval); + } + else { + return new Response('', 400); + } + + // Only process comments that have not been processed before. + if ($this->tempStore->get($comment_id)) { + // Comment has already been processed, return response as "Gone". + return new Response('', 410); + } + /** @var \DisqusAPI $disqus */ + elseif ($disqus = disqus_api()) { + $disqus_config = $this->config('disqus.settings'); + try { + $post = $disqus->posts->details([ + 'post' => $comment_id, + 'related' => 'thread', + ]); + + if ($post && !empty($post->forum) && $post->forum === $disqus_config->get('disqus_domain')) { + // Check that the post was created recently (in the last hour) before + // sending any notification to avoid notifying of old posts. The + // createdAt property is formatted like '2018-03-28T12:51:57'. + $created = DrupalDateTime::createFromFormat('Y-m-d\TH:i:s', $post->createdAt, 'UTC') + ->format('U'); + if (\Drupal::time()->getRequestTime() <= strtotime('+1 hour', $created)) { + $this->tempStore->set($comment_id, TRUE); + $this->eventDispatcher->dispatch(NewCommentEvent::NEW_COMMENT, new NewCommentEvent($post)); + + // Clear flood control for this user as this was a genuine comment. + $this->flood->clear('disqus.new_comment'); + + // HTTP 204 is "No content", meaning "Processed succesfully, now done". + return new Response('', 204); + } + else { + // Too late to notify of this comment, return response as "Gone". + return new Response('', 410); + } + } + else { + // Comment not actually for this site. + return new Response('', 404); + } + } + catch (\DisqusAPIError $e) { + // Pass along whatever the error code was from Disqus. + return new Response('', $e->getCode()); + } + catch (\Exception $exception) { + // Any other error, consider the post not found. + return new Response('', 404); + } + } + else { + // Service unavailable. + return new Response('', 503); + } + } + +} diff --git a/src/Element/Disqus.php b/src/Element/Disqus.php index 264643f..283b5d3 100644 --- a/src/Element/Disqus.php +++ b/src/Element/Disqus.php @@ -121,6 +121,14 @@ public static function displayDisqusComments($title, $url, $identifier, $callbac // Attach the js with the callback implementation. $element['#attached']['library'][] = 'disqus/ga'; } + // Check if we want to notify of new comments. This can only work if a + // secret key has been set for API requests. + if ($disqus_settings->get('advanced.disqus_secretkey') && $disqus_settings->get('behavior.disqus_notify_newcomment')) { + // Add a callback when a new comment is posted. + $disqus['callbacks']['onNewComment'][] = 'Drupal.disqus.disqusNotifyNewComment'; + // Attach the js with the callback implementation. + $element['#attached']['library'][] = 'disqus/notification'; + } // Add the disqus.js and all the settings to process the JavaScript and load // Disqus. $element['#attached']['library'][] = 'disqus/disqus'; diff --git a/src/Event/NewCommentEvent.php b/src/Event/NewCommentEvent.php new file mode 100644 index 0000000..fbc4ed6 --- /dev/null +++ b/src/Event/NewCommentEvent.php @@ -0,0 +1,42 @@ +post = $post; + } + + /** + * Get the new post. + * + * @return object + */ + public function getPost() { + return $this->post; + } +} diff --git a/src/EventSubscriber/NewCommentSubscriber.php b/src/EventSubscriber/NewCommentSubscriber.php new file mode 100644 index 0000000..9c157ab --- /dev/null +++ b/src/EventSubscriber/NewCommentSubscriber.php @@ -0,0 +1,154 @@ +mailManager = $mail_manager; + $this->entityTypeManager = $entity_type_manager; + $this->logger = $logger_factory->get('mail'); + $this->languageManager = $language_manager; + $this->accessUnawareRouter = $access_unaware_router; + } + + /** + * Send email notification of new comment. + * + * @param \Drupal\disqus\Event\NewCommentEvent $event + */ + public function onNewComment(NewCommentEvent $event) { + $post = $event->getPost(); + + // Try and match a thread ID to an entity first. + $entity = NULL; + $identifiers = $post->thread->identifiers; + foreach ($identifiers as $identifier) { + $parts = explode('/', $identifier); + if (count($parts) === 2) { + list($entity_type_id, $entity_id) = $parts; + try { + if ($storage = $this->entityTypeManager->getStorage($entity_type_id)) { + if ($entity = $storage->load($entity_id)) { + break; + } + } + } + catch (\Exception $e) { + // No problem; try the next identifier. + continue; + } + } + } + + // Fall back to matching the post URL itself to an entity. + if (!$entity) { + try { + $result = $this->accessUnawareRouter->match($post->url); + if (!empty($result['_route_object'])) { + /** @var \Symfony\Component\Routing\Route $route */ + $route = $result['_route_object']; + if ($parameters = $route->getOption('parameters')) { + foreach ($parameters as $name => $options) { + if (isset($options['type']) && strpos($options['type'], 'entity:') === 0 && !empty($result[$name])) { + if ($result[$name] instanceof EntityOwnerInterface) { + $entity = $result[$name]; + break; + } + } + } + } + } + } + catch (\Exception $e) { + // This is not business-critical logic; no need to let the exception + // bubble further. + } + } + + if ($entity && $entity instanceof EntityOwnerInterface) { + $owner = $entity->getOwner(); + if ($to = $owner->getEmail()) { + $langcode = $owner->getPreferredLangcode(); + + $message = $this->mailManager->mail('disqus', 'new_comment', $to, $langcode, [ + 'post' => $post, + ]); + + // Error logging is handled by \Drupal\Core\Mail\MailManager::mail(). + if ($message['result']) { + $this->logger->notice('Sent email to %recipient', ['%recipient' => $to]); + } + } + } + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + $events[NewCommentEvent::NEW_COMMENT][] = ['onNewComment']; + return $events; + } +} diff --git a/src/Form/DisqusSettingsForm.php b/src/Form/DisqusSettingsForm.php index 07832d4..2833b37 100644 --- a/src/Form/DisqusSettingsForm.php +++ b/src/Form/DisqusSettingsForm.php @@ -136,6 +136,18 @@ public function buildForm(array $form, FormStateInterface $form_state) { ), '#default_value' => $disqus_config->get('behavior.disqus_track_newcomment_ga'), ]; + $form['behavior']['disqus_notify_newcomment'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Notify authors of new comments'), + '#description' => $this->t('When enabled, a notification email will be sent to the author of a post when a new comment is added. This will work only if you have a secret key set in the advanced section.'), + '#default_value' => $disqus_config->get('behavior.disqus_notify_newcomment'), + '#states' => [ + 'visible' => [ + 'input[name="disqus_publickey"]' => ['empty' => FALSE], + 'input[name="disqus_secretkey"]' => ['empty' => FALSE], + ], + ], + ]; // Advanced settings. $form['advanced'] = [ @@ -280,6 +292,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) { ->set('behavior.disqus_inherit_login', $form_state->getValue('disqus_inherit_login')) ->set('behavior.disqus_disable_mobile', $form_state->getValue('disqus_disable_mobile')) ->set('behavior.disqus_track_newcomment_ga', $form_state->getValue('disqus_track_newcomment_ga')) + ->set('behavior.disqus_notify_newcomment', $form_state->getValue('disqus_notify_newcomment')) ->set('advanced.disqus_useraccesstoken', $form_state->getValue('disqus_useraccesstoken')) ->set('advanced.disqus_publickey', $form_state->getValue('disqus_publickey')) ->set('advanced.disqus_secretkey', $form_state->getValue('disqus_secretkey'))