Introduce
この記事の読了時間:約31分35秒
春はあけぼの。
やうやう白くなりゆく山際、少し明かりて、紫だちたる雲間から降り注ぐ花粉!ぎゃーいやーやめてーー!!
ということで、この数日のところ案件の待ちタイムだったので、ちょっと前に思い立った「自分のブログのtwitterエゴサーチ結果をWordPressの管理画面で見られるプラグイン」っつーのを作ってみましたよ。
ちょっと前だとtwitterは検索結果をRSSで取得できたので、この手のヤツを作るのは比較的カンタンだったんですが、oAuth2導入から検索結果もAPIで取ってくれっつーカタチになったので、俄然めんどくさくなってました。
そして時は流れて2015年の春。
持て余した数日の余暇は、「Twitter Application-only authentication」と「wp_add_dashboard_widget()」と「curl_init()」によって、自分のサイトについて一週間以内につぶやかれたツイートをWordPressのダッシュボードに表示させるプラグインへと姿を変えたのです!
その名も「WP egosearch」!何のヒネリも無い、まんまやで!
んでは、ゴタクは置いといて機能紹介やら技術詳細やら、いってみよー!
実はコレ、すでに公式ディレクトリに登録されてるのです!
なので、上記ページからzipでダウンロードして自力でアップしても良し、管理画面のプラグイン検索で「WP egosearch」とか、単に「egosearch」とかでも見つかるので、そこからインストールしても良しです☆
公式ディレクトリに登録する&してからの苦悩と努力と挫折については、機会があったら後日にでも。。
これから公式登録を目指す諸君!大事なパスワードとか個人情報をコードの中に置き忘れたままコミットすると、基本取り返しつかないからな!!!
と、その前にカンタンに「エゴサーチ」とは、について。
自分の名前やハンドルネーム、運営してるブログの名前/URLなんかについて、自分で検索してその言及や反応を調べることを「エゴサーチ」といいます。
で、フツーはGoogleやTwitterなんかの検索窓に名前やURLなどを打って調べたり、専用のサービスやアプリケーションを使って調べたりするんですが、それをWordPressのダッシュボードでカンタンに見られるようにするのが、今回公開する「WP egosearch」プラグインです。
プラグインを導入したWordPressサイトのサイト名とURLを自動で取得して、それをTwitterの検索APIに突っ込んで、その結果をダッシュボードに一覧してくれるっつー、シンプルかつ便利なプラグインになります。
さらに、スパムっぽいリツイートなんかからの防御のために、「除外するキーワード」を設定できるというオマケ付き!
Twitterの検索APIを使うために、アプリケーションのコンシューマキー(API Key)とコンシューマシークレット(API Secret)が必要になるので、事前にTwitterで開発者アカウントを作成して、新たにアプリを作成するなり、検索APIを使ってない既存アプリをほじくり返したりして、キーとシークレットを取得して下さい。
取得の仕方は「Twitterのコンシューマーキー(Consumer key)等の取得 | offsidenowの日常を綴ったブログ」という記事や「TwitterAPIの認証コード類を取得する手順 – ysklog」という記事が非常に参考になりますよ!
あと1点、Twitterの検索APIは「過去1週間以内のツイート」しか探してくれませんので、ご注意を。。
それを突き止めるのに苦労しましたorz
「ダッシュボードにエゴサーチの結果を表示させる」というシンプルな作りのプラグインなので、実行ファイルは1つだけ。
必要になってくる処理としては、大きく分けて「Twitterの検索APIを使って、ブログに対する言及=エゴサーチ結果を取得する」ってのと「取得したエゴサーチ結果をダッシュボードに表示する」ってゆー2点。
せっかくなので、見難いコト必至だけど解説がてらババーっと下記に垂れ流してみましょー。
<?php /* Plugin Name: WP egosearch Plugin URI: http://smkn.xsrv.jp/blog/2015/03/wordpress-plugin-called-wp-egosearch/ Description: Displays the egosearch(search your site URL/sitename) results of twitter in the dashboard. Version: 1.1.0 Author: smkn Author URI: http://smkn.xsrv.jp/blog/ License: GPL2 or later */ /* Copyright 2015 smkn (email : smkn.xxx@gmail.com) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License, version 2, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ class wpEgosearch { const WPES_PLUGIN_NAME = 'WP egosearch'; const WPES_PLUGIN_VERSION = '1.1.0'; public $site_url; public $site_name; public $consumer_key; public $consumer_secret; public $dissearched = array(); public $read_count; function __construct(){ $this->site_url = preg_replace('/^https?:\/\//iu', '', get_option('siteurl')); $this->site_name = get_option('blogname'); $this->consumer_key = (get_option('wpes_consumer_key'))?get_option('wpes_consumer_key'):''; $this->consumer_secret = (get_option('wpes_consumer_secret'))?get_option('wpes_consumer_secret'):''; $this->dissearched = (get_option('wpes_dissearched'))?explode(',', get_option('wpes_dissearched')):array(); $this->read_count = (get_option('wpes_count'))?get_option('wpes_count'):'10'; load_plugin_textdomain('wp-egosearch', false, basename(dirname(__FILE__)).'/languages'); add_action('wp_dashboard_setup', array(&$this, 'wpes_setup')); } public function wpes_setup(){ wp_add_dashboard_widget('wpes_views', self::WPES_PLUGIN_NAME, array(&$this, 'wpes_views'), array(&$this, 'wpes_configure')); } public function wpes_views(){ if(!empty($this->consumer_key) && !empty($this->consumer_secret)){ /* get bearer token */ $headers = array( 'POST /oauth2/token HTTP/1.1', 'Host: api.twitter.com', 'User-Agent: '.self::WPES_PLUGIN_NAME.self::WPES_PLUGIN_VERSION, 'Authorization: Basic '.base64_encode(urlencode(esc_attr($this->consumer_key)).':'.urlencode(esc_attr($this->consumer_secret))), 'Content-Type: application/x-www-form-urlencoded;charset=UTF-8', ); $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, 'https://api.twitter.com/oauth2/token/'); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_POSTFIELDS, 'grant_type=client_credentials'); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_HEADER, false); $response = curl_exec($ch); curl_close($ch); if($response === false){ _e('curl - Token could not be retrieved.', 'wp-egosearch').'<br />'; echo 'curl error:'.curl_error($ch); } else { $bearer = json_decode($response); if(isset($bearer->errors)){ _e('Token could not be retrieved.', 'wp-egosearch').'<br />'; echo 'bearer token error:'.$bearer->errors[0]->message; } } if($response !== false && !isset($bearer->errors)){ /* get search results */ $q_str = '"'.urlencode($this->site_url).'"+OR+"'.urlencode($this->site_name).'"'; $dissearch_str = ''; if(!empty($this->dissearched)){ foreach($this->dissearched as $v){ $dissearch_str .= '+-"'.urlencode(trim($v)); } } $url = 'https://api.twitter.com/1.1/search/tweets.json?q='.$q_str.$dissearch_str.'&include_entities=true&result_type=recent&count='.esc_attr($this->read_count); $headers = array( 'GET /1.1/search/tweets.json? HTTP/1.1', 'Host: api.twitter.com', 'User-Agent: '.self::WPES_PLUGIN_NAME.self::WPES_PLUGIN_VERSION, 'Authorization: Bearer '.$bearer->access_token ); $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); $response = curl_exec($ch); curl_close($ch); if($response === false){ _e('curl - Search results could not be obtained.', 'wp-egosearch').'<br />'; echo 'curl response error:'.curl_error($ch); } else { $searched = json_decode($response); if(isset($searched->errors)){ _e('Search results could not be obtained.', 'wp-egosearch').'<br />'; echo 'search error:'.$searched->errors[0]->message; } } if($response !== false && !isset($searched->errors)){ /* format results */ $ego_data = array(); foreach($searched->statuses as $k => $v){ $ego_data[$k]['post_id'] = $v->id_str; $ego_data[$k]['date'] = date("Y.m/d H:i:s", strtotime($v->created_at)); $ego_data[$k]['user'] = $v->user->screen_name; $ego_data[$k]['text'] = $v->text; } /* print results */ echo '<div class="widget"><ul>'; if(count($ego_data) > 0){ foreach($ego_data as $v){ echo '<li>'; echo '<a href="https://twitter.com/'.$v['user'].'/status/'.$v['post_id'].'" target="_blank">@'.$v['user'].' said. - at '.$ego_data[$k]['date'].'</a>'; echo '<p style="margin:0">'.$v['text'].'</p>'; echo '</li>'; } } else { _e('There was no corresponding tweet.', 'wp-egosearch'); } echo '</ul></div>'; } } } else { _e('Configure this widget by clicking the link in its upper right corner.', 'wp-egosearch'); } } function wpes_configure() { if(isset( $_POST['wpes_consumer_key'])){ update_option('wpes_consumer_key', sanitize_text_field($_POST['wpes_consumer_key'])); update_option('wpes_consumer_secret', sanitize_text_field($_POST['wpes_consumer_secret'])); update_option('wpes_dissearched', sanitize_text_field($_POST['wpes_dissearched'])); update_option('wpes_count', sanitize_text_field($_POST['wpes_count'])); } $feed_url = $this->consumer_key; echo '<label for="wpes_consumer_key"><span>twitter consumer key</span><input type="text" name="wpes_consumer_key" id="wpes_consumer_key" value="'.esc_textarea($this->consumer_key).'" size="45" /></label>'; echo '<label for="wpes_consumer_secret"><span>twitter consumer secret</span><input type="text" name="wpes_consumer_secret" id="wpes_consumer_secret" value="'.esc_textarea($this->consumer_secret).'" size="45" /></label>'; echo '<label for="wpes_dissearched"><span>'._e('Exclude keywords(Comma-separated)', 'wp-egosearch').'</span><input type="text" name="wpes_dissearched" id="wpes_dissearched" value="'.esc_textarea(implode(',', $this->dissearched)).'" size="45" placeholder="one thing,two,three" /></label>'; echo '<label for="wpes_count"><span>'._e('Number to get(Up to 100. ※Only tweet for less than one week)', 'wp-egosearch').'</span><input type="text" name="wpes_count" id="wpes_count" value="'.esc_textarea($this->read_count).'" size="45" style="margin-bottom:10px;" /></label>'; } } new wpEgosearch();
WordPressがこのファイルをプラグインとして認識するために必要な記述が3~9行目、GPLのアレが12~25行目になります。
これを書かないと認識されないし公式プラグインにも登録できないので必須。
書き方は公式のプラグインの作成 - WordPress Codex 日本語版のまんまです。
今回は関数の連打じゃなくclassで書いてみたので、その宣言です。
使いそうなヤツを目処付けて、上部に一括でまとめてます。
さっき宣言した使い回す変数に値を入れていきます。
サイト名は get_option('blogname') で、サイトURLは get_option('siteurl') で取れるのですが、URLは「http(s)://」を抜かないとtwitterで正確な検索結果が取れないっぽかったので、正規表現で当該部分を削除してます。
42~45行目では、すでに設定済みだったらDBから取ってきて、設定されてなかったりしたら空文字や空配列を値として入れるようにしときます。
46行目は、公式プラグインとして配布するのに必須の「多言語対応」の為の記述で、
load_plugin_textdomain(言語ファイルを呼び出すための一意の文字列, 決め打ちでfalse, 言語ファイルがあるディレクトリのパス);
って書き方になります。これはWordPress 3.7 以降での言語ファイルのロード方法 | Foreignkey, Inc.などで勉強させて頂きましたー!
んで47行目が、管理画面のダッシュボード生成時に動くフックと、そこで wpes_setup を発火させてね☆という指定になります。
ダッシュボードにウィジェットを追加して wpes_views を動かしてちょ + 設定フォームとして wpes_configure を動かしてねん♪という記述。
これは公式のや、【WordPress】ダッシュボードに自作のウィジェットを追加する at softelメモを参考にしました。
Twitterを相手にアレコレする関数 wpes_views を宣言してます。
ここ、ちょっと手抜いてまして、本来なら
if(!empty($this->consumer_key) && !empty($this->consumer_secret)){
として、キーもシークレットも空じゃなければ、ってカタチにした方が正しげなんですが、言うても管理者が管理画面で見るプラグインだし、どっちかだけ書かないってコトも無いだろうといやすみませんただただ手を抜きました。。上記に書き換えました。
まぁ実装に当たっては問題ない箇所なので。。
Twitterの検索結果って、以前はRSSとかで認証とか無くカンタンに取得できたんですが、今はAPI経由でしか取れなくなっちゃったんです。
んで、そのAPIにおいて、不特定多数のユーザーが使うようなタイプのアプリじゃないなら、一部のAPIについて回数制限を緩和したタイプを使っても良いよ!というTwitter様の心優しいご配慮が「Application-only authentication」っつー長ったらしい方法。
今回のプラグインは管理画面のダッシュボード上でしか使わないモノなので、どうせならこの方法を使うことにしました。
で、この「Application-only authentication」をPHPで使う場合、cURL関数でのやりとりがオーソドックスなようで、イメージとしては下記のような流れで検索結果の取得まで進みます。
56~62行目で、cURLで通信する時のヘッダー情報を記述してます。
ポイントは60行目、API KeyとSecretをesc_attr関数でエスケープした後、それぞれをurlencode()でエンコードして、それを:記号で繋いで、それ全体をbase64_encode()でエンコード。エンコード言いたいだけちゃうで!
そして64~73行目、さっきのヘッダー情報を持ってTwitterのおうちに向かいます。
75~84行目で、レスポンスがfalseならエラー(76行目)を、レスポンス来たけど戻り値がエラーだったらエラー(81行目)を返します。
ここで出てくる _e() が、多言語用の echo みたいなヤツで、言語ファイル作って多言語化の準備が出来てれば、管理画面での設定なんかに合わせて言語を変換してくれます。
エラーが無ければ、79行目の $bearer に、Twitterから発行されたトークン(=API使用許可証)が入ってることになります。
ここ、もっと上手いやり方がある気がするのですが、調べる前にこんな感じで力技の分岐でやっちゃいました。。
エラーが無ければ、続いて検索結果の取得に進みます。
TwitterのAPIに検索ワードや除外ワードを投げるために、仕様に合わせて文字列を変換します。
つっても urlencode() でエンコードした文字列をダブルクォーテーションで囲い +OR+ で繋ぐくらいですが。
89~94行目では、もし除外ワードがあったら +- で繋ぐ、と。
最後に95行目でそれをクエリに加えた問い合わせ用URLを生成してます。
さっきのcURLとおんなじ感じですが、100行目でトークンを貼って認証を受けたことを証明します。
103~109は通信、111~120行目もさっきと同じようにエラー処理です。
ここ、やっぱりもっと上手いやり方がある気がするのですが。。。
エラーが無ければ、取得した検索結果をダッシュボードでキレイに表示するための加工をします。
ここはのポイントは127行目、そのままだと「Thu Mar 13 12:34:56 +0000 2015」みたいなフォーマットなので、それを「2015.03/1312:34:56」ってカタチに変換させてみました。
こっちのほうが見やすいんじゃないかなーと思うので。
ここはダッシュボードに吐き出すHTMLの記述です。
141~143行目で、もし該当するツイートがなかった場合の分岐をさせてます。
API Key、API Secretが記入されてなかった場合にたどり着く分岐です。
よっしゃ最後、いっくぜー!
$_POSTに入ってる送信内容をWordPress独自のサニタイズ関数に通して、update_option()でそれぞれのデータをDBに保存させます。
それぞれのvalueに esc_textarea() でエスケープした各変数を出力させます。
さぁ!今の今まで書いてきた結果を!いざ!眼前に!示せぇぇぇぇええぇぇえぇえ!!!!
みなさまのお力で、ついに初の公式ディレクトリ掲載まで辿りつけました。
ありがとーございますっ!
みんなでエゴエゴしよーぜ☆
関連する記事
同じカテゴリーの記事
コメントを投稿する