Drupal 9: Auto Tweeting From A Drupal Site When Content Is Published

Normally, when creating Tweets from Drupal 8 I use the Social Post Twitter module. This module is part of the Drupal Social Initiative and has been my go-to module when I've needed to read or send Tweets from a Drupal site. Since the release of Drupal 9, however, these modules have not been receiving the support needed and as of writing this article there is no easy way to install them on a Drupal 9 site. I've looked into the issue queues and can't see why the delay is there.

The Social Post Twitter module does have a lot of features that I didn't need for what I was looking for, which was to send Tweets when items of content are created. I decided to see how difficult it would be to send Tweets from a Drupal site as an item of content is published.

As it happens, creating your own Twitter integration to post status updates to Twitter isn't that difficult. The Social Post Twitter module is build around the abraham/twitteroauth package. Aside from the boilerplate code of setting up services and hooks I only needed a few lines of code to start that package and then send the needed data to Twitter. I decided it would be helpful to go through every step along the way from getting authentication details to writing code and creating a Drupal module.

Before getting into how to get the code working we need to get some authentication details.

Getting Your Access Tokens

In order to send Tweets from your Drupal site you'll need to get some OAuth details. Do find these you need to head over to https://developer.twitter.com/ and apply for developer access. This isn't that easy to figure out how to do this, so I've added some steps here to go through.

To find the entry point to accessing the Twitter API go to the page at https://developer.twitter.com/en/products/twitter-api and click on the Apply for access link. You can also find this page by going to the top left menu and selecting the link at Products and then to the Twitter API link.

Twitter API Apply

From this page you should see a link allowing you to "Apply for a developer account".

Twitter API Apply Part 2

From here, Twitter will ask a little about what you need to use the API for. Depending on what you need to use the API for will depend on the options you select. When I was testing things out I selected "Hobbyist", but you should select the option relevant to your situation and needs. I'd advise you to use the Professional option if you are sending Tweets from any form of site that makes money, even if that is from advertising.

Twitter API Apply use cases

After a couple of questions and the verification of your details you will see a form like this. This allows you to inform Twitter exactly what you will be using the API for. For my purposes I selected "no" to quite a few of the questions as I will not be pulling information or analysing Tweets at all. Everything will be going from our Drupal site to Twitter.

Twitter API usage

Once you have entered your information and answered the questions you will be asked to review everything before a final step of agreeing to the Twitter terms and conditions.

After completing the final step you will need to verify your email address. Once done you will see this screen, allowing you to create your first project.

Twitter API create app

The project will be largely the same questions as you already answered, but once you complete that you will see something like this. This is a project and an app that will be used to integrate with that project. The name of the project is important as it will appear after every Tweet generated by your code. My advise, therefore, is not to call it something daft as that will be very publicly visible.

Twitter API new project and app created.

From here, you need to click on the key icon within the app you have created. This, finally, is where we can find the authentication details for our Twitter app.

The Twitter API uses OAuth and the key values to this are the API Key and the API Secret Key. These two keys are used to encrypt authentications between the application and the Twitter API. You should have been shown the authentication keys when you created the app, but you can easily regenerate them whenever you need.

To regenerate your keys for your application click on the Regenerate button. You will need to make a record of these keys or you will need to regenerate them again.

Twitter API key and secret.

As the Twitter API uses OAuth it means that if we want to allow an application to Tweet on the behalf of a user we need to go through the OAuth access granting workflow. This means that the user must visit Twitter and see exactly what it is that we will be doing on the users behalf. You've probably seen this when connecting Twitter to third party apps.

There is, though, a way around this. As the app is attached to a single user you can generate the needed access tokens for the user running the app. This means that yo can get access to the API straight away, without needing to go through the permissions check. To generate access tokens for the user you used to create the Twitter app click on the Generate link at the bottom of the page, it should look a little like this.

Twitter API Token and secret

This will give you an Access Token and an Access Token Secret keys that you can use to authenticate this user with the Twitter API. Make a note of these as you will need to re-generate them again if you lose them.

Once you have created your user tokens the dialog should change to look a little like this.

Twitter API token and secret regeneration

After following through all of those steps you should now have the following tokens.

  • API Key
  • API Secret Key
  • User Access Token
  • User Access Token Secret

With these values in hand we can now set about the code needed to post the Tweets from the Drupal site.

Posting To Twitter

Before we add any code we need to include the library that we will use to authenticate and interact with the Twitter API. This library is the abraham/twitteroauth library. Run the following composer require statement in your codebase in order to pull in the TwitterOAuth library.

composer require abraham/twitteroauth

With that library available in our Drupal codebase we just need to instantiate the TwitterOAuth object by supplying the correct credentials. This is where you add in your Twitter API credentials.

use Abraham\TwitterOAuth\TwitterOAuth;

$consumerKey = 'api key';
$consumerSecret = 'api secret key';
$accessToken = 'access token';
$accessTokenSecret = 'access token secret';
$connection = new TwitterOAuth($consumerKey, $consumerSecret, $accessToken, $accessTokenSecret);

With that connection object created we can perform any number of actions. The simplest thing we can do is simply check that the credentials are correct. We do this using the "account/verify_credentials" method of the API. The return value here may contain an 'errors' property. If it does contain this property then something went wrong and we need to investigate what.

$content = $connection->get("account/verify_credentials");

if (isset($content->errors)) {
  $message = $content->errors[0]->message;
  print 'An error occurred verifying credentials. Error: ' . $message;
}

If we want to send a Tweet then we just use the "statuses/update" method of the API. Similarly, if the return of this method contains an errors property then something went wrong and we need to investigate what happened. The payload we sent to Twitter can contain a few items

$tweet = [];
$tweet['status'] = 'The tweet';
$post = $connection->post('statuses/update', $tweet);

The "status" part of this payload is the simplest thing to send to Twitter in order to create a Tweet. There are a number of other options available here, but they are only needed if you want to integrate with things like location or attaching media items. When you are starting out, using the status option is the best way to ensure you application works. Be careful though as it will send live Tweets to Twitter.

Integrating With Drupal

With the library in place we need to create a module that will contain our code within the Drupal site. I've called this module "mymodule_social_push" in these examples, but you can name this whatever you want.

In order to perform our needed action, of posting to Twitter when a page is published, we just need to create two hooks. These are hook_ENTITY_TYPE_create() for a node being created and hook_ENTITY_TYPE_update() for a node being updated, and they apply only to the node entity type. Since we need to perform the same action in each we will be passing the responsibility of posting to Twitter to another function. In the code below, we are also checking the type of node to make sure it is an 'article' content type. It's always a good idea to check the type of content as you don't want to be posting to Twitter on the creation of a webform or other structural content type.

<?php

use Drupal\Core\Entity\EntityInterface;

/**
 * Implements hook_ENTITY_TYPE_create().
 */
function mymodule_social_push_node_insert(EntityInterface $entity) {
  if ($entity->getType() == 'article') {
    mymodule_social_push_publish($entity);
  }
}

/**
 * Implements hook_ENTITY_TYPE_update().
 */
function mymodule_social_push_node_update(EntityInterface $entity) {
  if ($entity->getType() == 'article') {
    mymodule_social_push_publish($entity);
  }
}

The mymodule_social_push_publish() function takes the node as an argument and extracts the needed information from it before sending to Twitter as a post. There are a few things going on in this function so I have added comments for each step along the way. The is_first_published() function will detect the first time a post is saved in a published state, which I have written about in a previous post.

<?php

use Drupal\node\NodeInterface;
use Abraham\TwitterOAuth\TwitterOAuth;

/**
 * @param NodeInterface $node
 */
function mymodule_social_push_publish(NodeInterface $entity) {
  if ($entity->isPublished() && is_first_published($entity)) {
    // Build an array of term labels.
    $tagLabels = [];
    $tags = $entity->get('field_tags')->referencedEntities();
    foreach ($tags as $tag) {
      $tagLabels[] = '#' . $tag->label();
    }

    // Grab the URL for the current page.
    $urlAlias = $entity->toUrl('canonical', ['absolute' => TRUE])->toString();
    
    // Combine into a twitter status.
    $tweetText = $entity->getTitle() . ' ' . $urlAlias . ' ' . implode(' ', $tagLabels);

    // Set up the connection to TwitterOAuth.
    $consumerKey = 'api key';
    $consumerSecret = 'api secret key';
    $accessToken = 'access token';
    $accessTokenSecret = 'access token secret';
    $connection = new TwitterOAuth($consumerKey, $consumerSecret, $accessToken, $accessTokenSecret);
    
    // Create Tweet payload.
    $tweet = [];
    $tweet['status'] = $tweetText;

    // Post Tweet.
    $post = $connection->post('statuses/update', $tweet);

    // Log the response from the API.
    $logger = \Drupal::logger('mymodule_social_push');

    if (isset($post->errors)) {
      $message = $post->errors[0]->message;
      $logger->error('An error occurred when sending the tweet. Error: %error', ['%error' => $message]);
    }
    else {
      $logger->info('Post sent. Contents: %contents, Response: %response', ['%contents' => $tweet['status'], '%response' => print_r($post, true)]);
    }
  }
}

Your module should have the following structure.

mymodule.info.yml
mymodule.module

With all that in place the next time an article is published a Tweet will be generated on the account connected to the authentication details passed to the TwitterOAuth constructor. The above code will add together the title of the article, the url and any taxonomy terms created in a tags field. We prepend each tag with a hash symbol in order to create them as hashtags on Twitter.

Drupal Best Practice

Of course, there are a couple of poor decisions in the above code that need to be addressed. These are:

  • Hard coding of the authentication details into the codebase. This is a bad idea if the codebase is ever leaked as the attacker would get full access to our Twitter account.
  • Not using services to encapsulate the posting of items to Twitter. This is less of an issue, but it is important to enforce the separation of concerns so that those separate services can be tested.

There is, therefore, some work to be done in correcting these issues.

First, we need to create a service that we can use to encapsulate the TwitterOAuth integration.

services:
  mymodule_social_push.twitter_social_push:
    class: Drupal\mymodule_social_push\TwitterSocialPush
    arguments: ['@config.factory', '@state', '@logger.factory']

This service takes the config factory so that we can store some of the authentication details as configuration, but we are also passing the state service. Using the Drupal state API means that we can store some of the authentication details outside of the codebase entirely (including configuration). I have chosen to store the app keys in configuration and the user authentication tokens in the state API. This means that if the codebase was to be leaked then the user in question can't be compromised.

The final item added to the service is the Drupal logging service, this service allows us to write to the Drupal logs when we publish or fail to publish to Twitter.

Here is the final service class in full. The post() method here will accept a string and post this string as post to Twitter, the service method handles everything from that point onwards.

<?php

namespace Drupal\mymodule_social_push;

use Drupal\Core\Config\ConfigFactoryInterface;
use Abraham\TwitterOAuth\TwitterOAuth;
use Psr\Log\LoggerInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\State\StateInterface;

/**
 * Class TwitterSocialPush.
 *
 * @package Drupal\mymodule_social_push
 */
class TwitterSocialPush {

  /**
   * The config name.
   *
   * @var string
   */
  protected $configName = 'mymodule_social_push.settings';

  /**
   * The menu link module config object.
   *
   * @var \Drupal\Core\Config\Config
   */
  protected $config;

  /**
   * The config factory object.
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
  protected $configFactory;

  /**
   * The state API.
   *
   * @var \Drupal\Core\State\StateInterface
   */
  protected $state;

  /**
   * A logger instance.
   *
   * @var \Psr\Log\LoggerInterface
   */
  protected $logger;

  /**
   * Constructs a StaticMenuLinkOverrides object.
   *
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   A configuration factory instance.
   * @param \Drupal\Core\State\StateInterface $state
   *   The state API.
   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger
   *   A logger instance.
   */
  public function __construct(ConfigFactoryInterface $config_factory, StateInterface $state, LoggerChannelFactoryInterface $logger) {
    $this->configFactory = $config_factory;
    $this->state = $state;
    $this->logger = $logger->get('mymodule_social_push');
  }

  /**
   * {@inheritdoc}
   */
  public function post(string $tweetPayload) {

    $consumerKey = $config->get('twitter_auth_consumer_key');
    $consumerSecret = $config->get('twitter_auth_consumer_secret');

    $accessToken = $this->state->get('mymodule_social_push.twitter_auth_access_token');
    $accessTokenSecret = $this->state->get('mymodule_social_push.twitter_auth_access_token_secret');

    $connection = new TwitterOAuth($consumerKey, $consumerSecret, $accessToken, $accessTokenSecret);

    $content = $connection->get("account/verify_credentials");
    if (isset($content->errors)) {
      $message = $content->errors[0]->message;
      $this->logger->error('An error occurred verifying credentials. Error: %error', ['%error' => $message]);
      return;
    }

    $tweet = [];

    $tweet['status'] = $tweetPayload;
    $post = $connection->post('statuses/update', $tweet);

    if (isset($post->errors)) {
      $message = $post->errors[0]->message;
      $this->logger->error('An error occurred when sending the tweet. Error: %error', ['%error' => $message]);
    }
    else {
      $this->logger->info('Post sent. Contents: %contents, Response: %response', ['%contents' => $tweet['status'], '%response' => print_r($post, true)]);
    }
  }

}

We could further separate out the TwitterOAuth object here in order to allow for better testing, but this works as a first step in sorting out the two issues discussed above.

To create the configuration and state items needed for the service to function we just create a Drupal form class. This form class will extend the ConfigFormBase class so that we can utilise the configuration functions built into the from. We do also need to inject the state service into the form class so that we can add the needed items to the state.

As much of the form class is boilerplate code I decided to only post the submit handler for the form. This extracts the values from the form and either saves them to the configuration or the state.

public function submitForm(array &$form, FormStateInterface $form_state) {
  $config = $this->config('mymodule_social_push.settings');

  $config
    ->set('twitter_auth_consumer_key', $form_state->getValue('twitter_auth_consumer_key'))
    ->set('twitter_auth_consumer_secret', $form_state->getValue('twitter_auth_consumer_secret'));
   
  $config->save();

  $state = [
    'mymodule_social_push.twitter_auth_access_token' => $form_state->getValue('twitter_auth_access_token'),
    'mymodule_social_push.twitter_auth_access_token_secret' => $form_state->getValue('twitter_auth_access_token_secret'),
  ];
  $this->state->setMultiple($state);

  parent::submitForm($form, $form_state);
}

With all of those items in place we just need to alter the original function in order to use the service to send the Tweet.

/**
 * @param NodeInterface $node
 */
function mymodule_social_push_publish(NodeInterface $entity) {
  if ($entity->isPublished() && is_first_published($entity)) {
    // Build an array of term labels.
    $tagLabels = [];
    $tags = $entity->get('field_tags')->referencedEntities();
    foreach ($tags as $tag) {
      $tagLabels[] = '#' . $tag->label();
    }

    // Grab the URL for the current page.
    $urlAlias = $entity->toUrl('canonical', ['absolute' => TRUE])->toString();

    // Combine into a twitter status.
    $tweetText = $entity->getTitle() . ' ' . $urlAlias . ' ' . implode(' ', $tagLabels);
    $socialPushService = \Drupal::service('mymodule_social_push.twitter_social_push');
    $socialPushService->post($tweetText);
  }
}

These changes allow Tweets to be posted to Twitter when we publish articles without the fear of compromising the Twitter account if the code fell into the wrong hands. We are also a step further down the road of ensuring that we can unit test all of the items involved.

All of these files should form the following structure in your module.

mymodule.info.yml
mymodule.module
mymodule.routing.yml
/src
- TwitterSocialPush.php
- /Form
-  -  SocialPushSettingsForm.php

The SocialPushSettingForm.php is where the configuration form lives that contains the submitForm() handler mentioned above. The mymodule.routing.yml file is used to allow the settings form to be visited, although it won't be present in your admin menu as we are missing a links.yml file.

I hope this article has been useful to you. Please let me know in the comments below.

Comments

Hi, thanks a lot for sharing this workaround for the social API module. Same problem for me, it won't work with D9. 

Just wondered if you could share in which files to put the code you wrote in this article ? Thanks a lot for helping.

Permalink

It looks like the Social Post Twitter modules (and it's upstream dependencies) are now available in Drupal 9 versions. I can still supply the files if needed. Would a github repo be acceptable?

Name
Philip Norton
Permalink

Great Explanation on how to programatically setup the tweets. Looks like the Social Post Twitter modules is not working in D9.3... As much as I would like to read the code in an article, it's much easier to follow the code in a repo.  

Permalink

Hi there, would it be possible to get this in a Github repo as you mentioned in another comment?

The Social Post Twitter module doesn't seem to be working for the latest Drupal 9 release, and in your article above it's quite hard to find where you are adding these bits of code.

Thanks

Permalink

Hmm...  I need to try to figure this out at some point unless someone else has a solution ?

Eric

Permalink

Sorry everyone, creating this module IS on my todo list, I am just a little busy with other projects at the moment. I'll make a start on it soon!

Name
Philip Norton
Permalink

Add new comment

The content of this field is kept private and will not be shown publicly.
CAPTCHA
1 + 0 =
Solve this simple math problem and enter the result. E.g. for 1+3, enter 4.
This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.