It is common that tweets have media attached (image, animated GIF, or video). Many Drupal sites also use at least an image for a post, or you can attach a media file.

The Twitter API allows for uploading of media (https://developer.twitter.com/en/docs/media/upload-media/overview), but it would be a two step process:

  1. Upload the media either via simple upload (https://developer.twitter.com/en/docs/media/upload-media/api-reference/p...) or via chunked upload (https://developer.twitter.com/en/docs/media/upload-media/api-reference/p...), depending on media type and/or size. We would then get the media_id from the response.
  2. Include the media_ids in the status update

Would people think that's useful?

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Anonymous’s picture

Andreas Speck created an issue. See original summary.

gvso’s picture

Yes, I agree that we should allow this kind of tweets. It might be nice if we can actually customize tweets in the content editor.

But since the module currently doesn't support that, there are always ways around to get the SDK (the library used by Social Post Twitter) and perform any requests to Twitter API #2949855: Port to Social Post 2.x

Hint: \Drupal::service('plugin.network.manager')->createInstance('social_post_twitter')->getSdk2($access_token, $access_token_secret); should return the library and allow you to execute any requests.

DamienMcKenna’s picture

There's some code in #1981472: Support media/upload endpoint that might help with this.

joshua.boltz’s picture

Attached is a patch that adds support for media with status text.
Mainly, it just allows $this->status to be the entire array Twitter needs to send a Media + Text.

$params = [
  'status' => 'My tweet text',
  'media_ids' => $media->media_id_string,
];

So, it's on the developer to update their own custom implementation logic that does the posting and to make make use of the ->getSdk2() method (per @gvso's comment above) to post the media up to Twitter, and then pass along the media IDs like Twitter needs to include them in the post.

In my case, I am getting an image field URL path, pushing the media up to Twitter, then after media has been added, construct the $params array with the status and media_ids to actually create the Twitter post.

This is currently only against 8.x-1.x. I have not had much luck with the 8.x-2.x versions of the Social post modules.

DamienMcKenna’s picture

Status: Active » Needs review
DamienMcKenna’s picture

Status: Needs review » Needs work

Thanks for putting that together.

A few things about the patch:

  • The change to the info.yml file is unnecessary, it's just the verbose syntax allowed since core 7.42 (forget the D8 version that supported it).
  • The args change to $this->connection->post() needs to be backwards compatible, I suspect this change would break the module for sites that using it as-is.
joshua.boltz’s picture

Status: Needs work » Active
FileSize
1.51 KB
joshua.boltz’s picture

The patch was updated to be backward-compatible, and also removed the unnecessary changes to the info.yml.
With this, an implementer can supply a simple text string (as is the case without the patch), or an array that contains the media information + the tweet text in their own ->doPost() call.

DamienMcKenna’s picture

Status: Active » Needs review
Anonymous’s picture

Do I understand this correctly - the patch supports to provide the media ids as part of the array, which means I need to deal with uploading the media to Twitter to obtain the ids separately?

DamienMcKenna’s picture

Status: Needs review » Needs work

I think some documentation should be added showing how to use the API for attaching media.

joshua.boltz’s picture

Yes, the patch updates the ->doPost() method in social post twitter to allow an array to be passed in instead of just a string like pre-patch.

The Twitter API needs an array with a status key, which is for the text, and a media_ids keys, which is a comma separated list of media ids, so yes, before doing that doPost, you need to perform the API call to Twitter to upload the media and get their IDs back in the request.

You would need to apply the patch, and then in your implementation logic when the node inserts/updates, you need to add logic to upload the media to Twitter and build an array like

$twitterSdk = $twitterPost->getSdk2($account->getAccessToken(), $account->getAccessTokenSecret());`
`$media = $twitterSdk->upload('media/upload', ['media' => $image_path]);`

$params = [
  'status' => 'My tweet text',
  'media_ids' => $media->media_id_string,
];

Hope that helps.

joshua.boltz’s picture

Here's what I've done.

I have a custom function I run inside of hook_entity_insert() and hook_entity_update that does some stuff, including posting to Twitter.
In that function, I have this logic, which is essentially what you can find in the Social Post Twitter docs (https://www.drupal.org/node/2780771):

/* @var \Drupal\social_post_twitter\Plugin\Network\TwitterPostInterface $twitterPost */
  // Gets instance of Social Post Twitter.
  $twitterPost = \Drupal::service('plugin.network.manager')
    ->createInstance('social_post_twitter');

  // Gets the Twitter entity storage.
  $twitterEntity = \Drupal::entityTypeManager()
    ->getStorage('social_post_twitter_user');

  /* @var \Drupal\social_post_twitter\Entity\TwitterUserInterface[] $accounts */
  // Gets the Twitter accounts associated with the current user.
  $accounts = $twitterEntity->loadByProperties([
    'uid' => Drupal::currentUser()->id(),
  ]);

  $parameters = [];

  // Check for and grab the field value of a custom field on my node edit form.
  if ($node->hasField('field_twitter_post')) {
    try {
      $twitter_post_value = $node->get('field_twitter_post')->first();
    }
    catch (MissingDataException $e) {
      \Drupal::logger('my_module')->error($e->getMessage());
    }

    if (!empty($twitter_post_value)) {
      // Set the main key that Twitter needs to post something, just standard text string.
      // This was the same value that was sent pre-patch, but now instead of passing it as a single variable, the patch allows us to pass it as an array so we can include the media_ids next.
      $parameters['status'] = $twitter_post_value->getValue();
    }
  }

  // Check for and grab image field data.
  $images = ($node->hasField('field_image') && !empty($node->get('field_image')
    ->getValue())) ? $node->get('field_image')
    ->referencedEntities() : [];

  if (!empty($parameters)) {
    // Loop through each Twitter accounts.
    foreach ($accounts as $account) {

      // Per gvso's comment in https://www.drupal.org/project/social_post_twitter/issues/2975062#comment-12627648
      // We can make use of ->sdk2() to get the Twitter API.
      $twitterSdk = $twitterPost->getSdk2($account->getAccessToken(), $account->getAccessTokenSecret());

      $media_ids = [];

      // For each image, get the image path.
      foreach ($images as $image) {
        $image_path = drupal_realpath($image->getFileUri());

        // Upload the image to Twitter via the media/upload API.
        $media = $twitterSdk->upload('media/upload', ['media' => $image_path]);

        // Once the image is uploaded, the response contains the media_id_string we append to media_ids array.
        $media_ids[] = $media->media_id_string;
      }

      // The media_ids to attach to the post.
      $parameters['media_ids'] = implode(',', $media_ids);

      // Call ->doPost() to call the Twitter API and send the post data, which includes the status (plain text) and media_ids (media)
      if (!$twitterPost->doPost($account->getAccessToken(), $account->getAccessTokenSecret(), $parameters)) {
        drupal_set_message('There was an error while updating Twitter status for ' . $account->getScreenName() . ', please review the logs.', 'error');
      }
    }
  }
}
gvso’s picture

Thanks @joshua.boltz! It might be nicer if we have a helper method to upload images in Social Post Twitter itself. The ideal API might allow the following:

$tweet['status'] = 'This is a tweet!';
$tweet['media_ids'] = $twitterPost->uploadMedia($images);

$twitterPost->doPost($account->getAccessToken(), $account->getAccessTokenSecret(), $tweet)
joshua.boltz’s picture

New patch that includes a helper method to upload media, if needed. It's up to the developer's implementation logic to work out and pass along the media paths, so Twitter knows what it's uploading and attaching to the post.

This worked out really well, and cleaned up and removed the need for some of the logic in the implementation logic.

Here is an example of the updated implementation logic that can be added in a node creation or update life cycle hook or event:

/* @var \Drupal\social_post_twitter\Plugin\Network\TwitterPostInterface $twitterPost */
  // Gets instance of Social Post Twitter.
  $twitterPost = \Drupal::service('plugin.network.manager')
    ->createInstance('social_post_twitter');

  // Gets the Twitter entity storage.
  $twitterEntity = \Drupal::entityTypeManager()
    ->getStorage('social_post_twitter_user');

  /* @var \Drupal\social_post_twitter\Entity\TwitterUserInterface[] $accounts */
  // Gets the Twitter accounts associated with the current user.
  $accounts = $twitterEntity->loadByProperties([
    'uid' => Drupal::currentUser()->id(),
  ]);

  $tweet = [];

  // Use the value from this field as the 'status' text to tweet.
  if ($node->hasField('field_twitter_post')) {
    try {
      $twitter_post_value = $node->get('field_twitter_post')->first();
    }
    catch (MissingDataException $e) {
      \Drupal::logger('my_module')->error($e->getMessage());
    }

    if (!empty($twitter_post_value)) {
      $tweet['status'] = $twitter_post_value->getValue();
    }
  }

  // Use field_image as source images for media posting to Twitter.
  $images = ($node->hasField('field_image') && !empty($node->get('field_image')
    ->getValue())) ? $node->get('field_image')
    ->referencedEntities() : [];

  $image_paths = [];
  foreach ($images as $image) {
    $image_paths[] = drupal_realpath($image->getFileUri());
  }

  $tweet['media_paths'] = $image_paths;

  if (!empty($tweet)) {
    // Loop through each Twitter accounts.
    foreach ($accounts as $account) {
      if (!$twitterPost->doPost($account->getAccessToken(), $account->getAccessTokenSecret(), $tweet)) {
        drupal_set_message('There was an error while updating Twitter status for ' . $account->getScreenName() . ', please review the logs.', 'error');
      }
    }
  }

Let me know if any thoughts.

joshua.boltz’s picture

Anonymous’s picture

This looks interesting. Is this likely to be committed?

Anonymous’s picture

I can confirm that the patch works nicely. It would be really good to get this into the next release!

DamienMcKenna’s picture

Status: Needs work » Needs review
gvso’s picture

I haven't tested the patch yet, but I can see that the example would produce an error since there's not such an entity type named social_post_twitter_user in 2.x. Instead, we use the general entity type provided by Social Post itself.

// Gets the Social Post manager.
/* @var \Drupal\social_post\SocialPostManager $post_manager */
$post_manager = \Drupal::service('social_post.post_manager');

$post_manager->setPluginId('social_post_twitter');

$current_user = \Drupal::service('current_user');

$accounts = $post_manager->getAccountsByUserId('social_post_twitter', $current_user->id());

DamienMcKenna’s picture

Status: Needs review » Needs work

Putting back to "needs work" as it needs to be updated to match the 2.0 APIs.

joshua.boltz’s picture

The patch I've included has been updated to work with 2.0 APIs.
The snippet in #20 is the code a developer should use in their own implementation in something like a hook_entity_update(), and that also has been updated to work with the change in 2.0 that @gvso is speaking of.

Apparently, I somehow posted the wrong thing. Here is what I meant to post, which is the code that would be in a custom module, in something like a hook_entity_update():

/* @var \Drupal\social_post_twitter\Plugin\Network\TwitterPostInterface $twitterPost */
  // Gets instance of Social Post Twitter.
  $twitterPost = \Drupal::service('plugin.network.manager')
    ->createInstance('social_post_twitter');

  // Gets the Social Post manager.
  /* @var \Drupal\social_post\SocialPostManager $post_manager */
  $post_manager = \Drupal::service('social_post.post_manager');

  $post_manager->setPluginId('social_post_twitter');

  $current_user = \Drupal::service('current_user');

  $accounts = $post_manager->getAccountsByUserId('social_post_twitter', $current_user->id());

  $tweet = [];

  // Use the value from this field as the 'status' text to tweet.
  if ($node->hasField('field_twitter_post')) {
    try {
      $twitter_post_value = $node->get('field_twitter_post')->first();
    }
    catch (MissingDataException $e) {
      \Drupal::logger('my_module')->error($e->getMessage());
    }

    if (!empty($twitter_post_value)) {
      $tweet['status'] = $twitter_post_value->getValue();
    }
  }

  // Use field_image as source images for media posting to Twitter.
  $images = ($node->hasField('field_image') && !empty($node->get('field_image')
    ->getValue())) ? $node->get('field_image')
    ->referencedEntities() : [];

  $image_paths = [];
  foreach ($images as $image) {
    $image_paths[] = drupal_realpath($image->getFileUri());
  }

  $tweet['media_paths'] = $image_paths;

  if (!empty($tweet)) {
    // Loop through each Twitter accounts.
    /** @var \Drupal\social_post\Entity\SocialPost $account */
    foreach ($accounts as $account) {
      $access_token = Json::decode($post_manager->getToken($account->getProviderUserId()));
      $twitterPost->doPost($access_token['oauth_token'], $access_token['oauth_token_secret'], $tweet);
    }
  }
gvso’s picture

Thank you very much @joshua.boltz!

Just to make things shorter to focus on code related to Social Post Twitter, here's an example of a tweet with an image when a new node is created.

<?php

use Drupal\node\Entity\Node;

/**
 * Implements hook_ENTITY_TYPE_insert().
 */
function social_api_test_node_insert(Node $node) {

  $img_paths = [
    '/home/gvso/Desktop/tweet_image.png',
  ];

  /* @var \Drupal\social_post_twitter\Plugin\Network\TwitterPostInterface $post */
  // Gets instance of Social Post Twitter.
  $post = \Drupal::service('plugin.network.manager')->createInstance('social_post_twitter');

  // Gets the Social Post manager.
  /* @var \Drupal\social_post\SocialPostManager $post_manager */
  $post_manager = \Drupal::service('social_post.post_manager');

  $post_manager->setPluginId('social_post_twitter');

  $current_user = \Drupal::service('current_user');

  $accounts = $post_manager->getAccountsByUserId('social_post_twitter', $current_user->id());

  $tweet = [
    'status' => 'This is a text',
    'media_paths' => $img_paths,
  ];

  /* @var \Drupal\social_post\Entity\SocialPost $account */
  foreach ($accounts as $account) {
    $access_token = json_decode($post_manager->getToken($account->getProviderUserId()), TRUE);

    $post->doPost($access_token['oauth_token'], $access_token['oauth_token_secret'], $tweet);
  }

}

  • gvso committed 5a06e79 on 8.x-2.x authored by joshua.boltz
    Issue #2975062 by joshua.boltz, DamienMcKenna, Andreas Speck, gvso: Add...
gvso’s picture

Status: Needs work » Fixed

Awesome job everyone!

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.