【WordPress】プラグインを使わずに投稿を公開するタイミングで自動でTwitterにもツイートする

新しい記事が公開されたことをTwitterにも流して拡散したいというのはよくある話だと思います。僕が今担当しているWebサイトでも、記事を公開したタイミングでTwitterにも流すというフローがあります。
一日一回の作業ということもあり、初めのうちは手作業でテキストやURLをコピペして流していましたが、段々面倒になってくるのが人間というもの。

**そこで、今回は投稿を公開するタイミングで、自動でTwitterにもツイートする仕組みを実装してみます。**

実際、このくらいできるプラグインはあります。ただ、プラグインはオーバースペックだったり、小回りが利かなかったりと使い勝手に不満を抱くことも多々あります。
あと、やっぱり自分でコードを書くと勉強になりますね^^

#やりたいこと

- WordPressの投稿の公開と同時にTwitterにもツイートする
- 予約投稿にも対応する
- 投稿のテキストと一緒に画像もツイートする（画像付きツイート）

#投稿が公開されたタイミングをフックする

投稿が公開されたタイミングで処理を実行するには`transition_post_status`アクションをフックします。
コールバック関数の引数には、変更後のステータスと変更前のステータスおよびWP_Postオブジェクトが渡されます。

今回は、

**「新規追加」から「公開」**
投稿を新規作成し、下書き保存をせずにいきなり公開するケース
**「下書き保存」から「公開」**
下書き保存された投稿を公開するケース
**「レビュー待ち」から「公開」**
寄稿者によって入力された投稿を公開するケース
**「予約投稿」から「公開」**
予約投稿が投稿時間になり公開されるケース

のいずれかの遷移時に処理を実行すればいいので、functions.phpに以下のように書きます。

```functions.php
function hook_transition_post_status($new_status, $old_status, $post) {
  // 投稿のステータスが「新規追加」または「下書き保存」または「レビュー待ち」または「予約投稿」から
  // 「公開」へ変わった時にTwitterに自動投稿する
  if (($old_status == 'auto-draft'
    || $old_status == 'draft'
    || $old_status == 'pending'
    || $old_status == 'future')
    && $new_status == 'publish' && $post->post_type == 'post') {
    // TODO: ここに処理を書く
  }
}
add_action('transition_post_status', 'hook_transition_post_status', 10, 3);
```

カスタム投稿などには対応させたくないので、投稿タイプの比較条件を最後に追加しています。

#Twitter APIを利用するPHPコード

今回は画像付きのツイートを投稿するだけの必要最小限のクラスを用意します。
OAuth認証のところが複雑ですが、参考ページを読んでいただければわかるかと思いますので、詳細は割愛します。

```twitter_api.php
class TwitterApi
{
  const TWEET_URL = 'https://api.twitter.com/1.1/statuses/update.json';
  const MEDIA_UPLOAD_URL = 'https://upload.twitter.com/1.1/media/upload.json';

  private $_consumer_key;
  private $_consumer_secret;
  private $_access_token;
  private $_access_token_secret;

  public function __construct($consumer_key, $consumer_secret, $access_token, $access_token_secret)
  {
    $this->_consumer_key = $consumer_key;
    $this->_consumer_secret = $consumer_secret;
    $this->_access_token = $access_token;
    $this->_access_token_secret = $access_token_secret;
  }

  private function _create_signature($url, $params)
  {
    $signature_key = rawurlencode($this->_consumer_secret).'&'.rawurlencode($this->_access_token_secret);

    $oauth_params = array(
      'oauth_token' => $this->_access_token,
      'oauth_consumer_key' => $this->_consumer_key,
      'oauth_signature_method' => 'HMAC-SHA1',
      'oauth_timestamp' => time(),
      'oauth_nonce' => md5(uniqid(rand(), true)), // ランダムな文字列であればOK
      'oauth_version' => '1.0'
    );

    // 署名の作成
    $merge_params = array_merge($params, $oauth_params);
    ksort($merge_params); // パラメータ名でソートされていないとダメらしい
    $req_params = http_build_query($merge_params);  // key=val&key=val...
    $req_params = str_replace(array('+', '%7E'), array('%20', '~'), $req_params);
    $req_params = rawurlencode($req_params);
    $encoded_req_method = rawurlencode('POST');
    $encoded_url = rawurlencode($url);
    $signature_data = $encoded_req_method.'&'.$encoded_url.'&'.$req_params;
    $hash = hash_hmac('sha1', $signature_data, $signature_key, TRUE);
    $signature = base64_encode($hash);
    $merge_params['oauth_signature'] = $signature;

    return $merge_params;
  }

  /**
   * 画像をアップロードします
   *
   * @param $img_url
   * @return string
   */
  public function post_media($img_url)
  {
    // バイナリまたはBase64エンコードした画像をアップロードする
    // バイナリの場合、"media"パラメータを指定
    // Base64の場合、"media_data"パラメータを指定
    $img_bin = file_get_contents($img_url);
//  $img_b64 = base64_encode($img_bin);

    // バウンダリーの定義
    $boundary = '-------------------------------------------'.md5(mt_rand());

    // リクエストボディの作成
    $req_body = '';
    $req_body .= '--'.$boundary."\r\n";
    $req_body .= 'Content-Disposition: form-data; name="media";'; // name="media_data" if base64
    $req_body .= "\r\n";
    $req_body .= "\r\n".$img_bin."\r\n";
    $req_body .= '--'.$boundary.'--'."\r\n\r\n";

    $params = $this->_create_signature(TwitterApi::MEDIA_UPLOAD_URL, array());

    // 送信データの作成
    $options = array('http' => array(
      'method' => 'POST',
      'header' => array(
        'Authorization: OAuth '.http_build_query($params, '', ','), // Authorization: OAuth key=val,key=val...
        'Content-Type: multipart/form-data; boundary='.$boundary
      ),
      'content' => $req_body
    ));
    $options = stream_context_create($options);

    // 送信
    $json = file_get_contents(TwitterApi::MEDIA_UPLOAD_URL, false, $options);
    return $json;
  }

  /**
   * ツイートを投稿します。$media_idを指定すると画像付きで投稿します
   *
   * @param $status
   * @param $media_id
   * @return string
   */
  public function tweet($status, $media_id = null)
  {
    // ツイート本文
    $post_params = array(
      'status' => $status
    );
    if ($media_id != null) {
      $post_params['media_ids'] = $media_id;
    }

    $params = $this->_create_signature(TwitterApi::TWEET_URL, $post_params);

    // 送信データの作成
    $options = array('http' => array(
      'method' => 'POST',
      'header' => array(
        'Authorization: OAuth '.http_build_query($params, '', ',')  // Authorization: OAuth key=val,key=val...
      ),
      'content' => http_build_query($post_params) // key=val&key=val...
    ));
    $options = stream_context_create($options);

    // 送信
    $json = file_get_contents(TwitterApi::TWEET_URL, false, $options);
    return $json;
  }

  /**
   * メディアアップロードのレスポンスからmedia_idを取得します
   *
   * @param $media_response
   * @return string
   */
  public function get_media_id($media_response)
  {
    $res = json_decode($media_response, true);
    if (isset($res['media_id_string'])) {
      return $res['media_id_string'];
    }
    return null;
  }
}
```

#投稿の公開と同時にツイートする

自作したTwitterApiクラスのインクルードとAPIキーをfunctions.phpに定義します。
APIキーは[こちらのページ](https://apps.twitter.com/)で生成してください。

```functions.php
include('/path/to/twitter_api.php');

// Twitter APIキー
define('TW_CONSUMER_KEY', '**********');
define('TW_CONSUMER_SECRET', '**********');
define('TW_ACCESS_TOKEN', '**********');
define('TW_ACCESS_TOKEN_SECRET', '**********');
```

先ほどのコールバック関数にツイートする処理を書きます。

画像付きツイートの処理は、まず画像をアップロードした後、レスポンスに入っているmedia_idとツイート本文を一緒に送信することで実現します。つまり、Twitterとの間に2回通信が発生します。

今回、画像は投稿のアイキャッチ画像に設定されている画像を利用します。ツイート本文は、投稿のタイトルとパーマリンクとします。

```functions.php
function hook_transition_post_status($new_status, $old_status, $post) {
  // 投稿のステータスが「新規追加」または「下書き保存」または「レビュー待ち」または「予約投稿」から
  // 「公開」へ変わった時にTwitterに自動投稿する
  if (($old_status == 'auto-draft'
    || $old_status == 'draft'
    || $old_status == 'pending'
    || $old_status == 'future')
    && $new_status == 'publish' && $post->post_type == 'post') {

    // Twitter API
    $twitter = new TwitterApi(TW_CONSUMER_KEY, TW_CONSUMER_SECRET, TW_ACCESS_TOKEN, TW_ACCESS_TOKEN_SECRET);

    // アイキャッチ画像が設定されているならば画像のアップロード
    if (has_post_thumbnail($post->ID)) {
      $image_url = _get_post_thumbnail_url($post->ID, 'large');
      $json = $twitter->post_media($image_url);
      // 添付画像のIDを取得
      $media_id = $twitter->get_media_id($json);
    } else {
      // 添付画像なし
      $media_id = null;
    }

    // ツイートを投稿
    $status = '{{TITLE}}{{BR}}{{URL}}';  // TODO: ツイート本文のテンプレートを適当なところから取得
    $status = str_replace('{{TITLE}}', $post->post_title, $status);
    $status = str_replace('{{URL}}', get_permalink($post->ID), $status);
    $status = str_replace('{{BR}}', "\n", $status);
    $twitter->tweet($status, $media_id);
  }
}
add_action('transition_post_status', 'hook_transition_post_status', 10, 3);

/**
 * アイキャッチ画像のURLを取得します
 */
function _get_post_thumbnail_url($post_id, $size) {
  $image_id = get_post_thumbnail_id($post_id);
  $images = wp_get_attachment_image_src($image_id, $size);
  $image_url = $images[0];
  return $image_url;
}
```

たったこれだけで投稿の公開と同時に画像付きツイートを投稿することができます。

ツイート本文のテンプレートをテーマオプションやカスタムフィールドなどに設定することで、投稿ごとにハッシュタグを付けるなど柔軟性が出てきます。

#投稿ごとにハッシュタグを付けられるようにする

投稿のカスタムフィールドに`twitter_template`というキーでツイート本文のテンプレートを設定した場合の例です。

```functions.php
function hook_transition_post_status($new_status, $old_status, $post) {
  // 投稿のステータスが「新規追加」または「下書き保存」または「レビュー待ち」または「予約投稿」から
  // 「公開」へ変わった時にTwitterに自動投稿する
  if (($old_status == 'auto-draft'
    || $old_status == 'draft'
    || $old_status == 'pending'
    || $old_status == 'future')
    && $new_status == 'publish' && $post->post_type == 'post') {

    // 投稿の保存を実行しないとカスタムフィールドに設定した最新値が取得できないため
    do_action('save_post', $post->ID);

    ...

    // ツイートを投稿
    $status = _get_twitter_template($post->ID);
    $status = str_replace('{{TITLE}}', $post->post_title, $status);
    $status = str_replace('{{URL}}', get_permalink($post->ID), $status);
    $status = str_replace('{{BR}}', "\n", $status);
    $twitter->tweet($status, $media_id);
  }
}
add_action('transition_post_status', 'hook_transition_post_status', 10, 3);

...

/**
 * カスタムフィールドに設定されているテンプレートを取得します
 */
function _get_twitter_template($post_id) {
  $txt = get_post_meta($post_id, 'twitter_template', true);
  if (empty($txt)) {
    // カスタムフィールドが設定されていなければデフォルトを返す
    $txt = "{{TITLE}}{{BR}}{{URL}}";
  }
  return $txt;
}
```

注意したいのは、hook_transition_post_status関数が実行される時点では、カスタムフィールドの値はDBに保存されておらず、そのままでは最新値を取得することができません。
そこで、この関数内で

```php
do_action('save_post', $post->ID);
```
を実行することで強制的にカスタムフィールドの値を保存しています。
※このやり方が適切なのかはちょっと自信ありません。。

こうすることで、例えば、ある投稿には

```
{{TITLE}}{{BR}}{{URL}}{{BR}}#wordpress #developer
```

というテンプレートを設定することで、ツイート本文にハッシュタグ#wordpressと#developerを追加することができます。

この他にも工夫次第で、自分が使いやすいように拡張していけるのがスクラッチで書く魅力だと思います。

**[2016/07/29 追記]**
続きでFacebookへの投稿の仕方も書きました。
[【WordPress】プラグインを使わずに投稿を公開するタイミングで自動でFacebookにも投稿する](http://qiita.com/hituziando/items/ca8f1323a5500be23568)

---
#参考

[WordPress Codex: 投稿ステータスの遷移](https://wpdocs.osdn.jp/%E6%8A%95%E7%A8%BF%E3%82%B9%E3%83%86%E3%83%BC%E3%82%BF%E3%82%B9%E3%81%AE%E9%81%B7%E7%A7%BB)
[Twitter Developers: REST APIs](https://dev.twitter.com/rest/public)
[Syncer: POST statuses/update - ツイートを投稿する](https://syncer.jp/twitter-api-matome/post/statuses/update)
[Syncer: POST media/upload - 画像を投稿する](https://syncer.jp/twitter-api-matome/post/media/upload)
[hoge256 blog: PHPでライブラリを使わずにtwitterのOAuth認証を行うサンプル](http://www.hoge256.net/2011/09/595.html)