2010年4月24日土曜日

PHPでTwitterのOAuthを使うための学習用ライブラリ Eduwitter

とあるボットを作ろうと思った所、いくつかのサイトのファイルを保存しなければならなかったりなど、「これは面倒臭いぞ」、という訳で単体のファイルで動く、簡易的な Twitter OAuth認証 をこなせるライブラリを作成しました。

新版の Eduwitter を公開しました
  詳しくは タグ:Eduwitter から

セクション
1 - 概要
1.1 - 実際に設置しました
1.2 - リファレンス(興味のある方はどうぞ)
2 - OAuthの流れ
2.1 - 基本的な単語
2.2 - 4つのステップ
2.2.1 - サービスプロバイダに外部アプリを申請する
2.2.2 - リクエストトークンの発行と承認
2.2.3 - アクセストークンの取得
2.2.4 - OAuthでAPIを叩く
2.3 - signature
3 - コード
3.1 - Eduwitter メソッド一覧
3.2 - Eduwitter での作業
3.3 - Eduwitter

1 - 概要

1.1 - 実際に設置しました

こちら Eduwitter Callback Page で動作確認できます。 リンク先に zip と tar.gz で公開しています。 同梱している eduwitter.php と eduwitter_test.php のうち、コールバックURLは eduwitter_test.php を指定してください。

1.2 - リファレンス(興味のある方はどうぞ)

  OAuth 公式リファレンス
    http://oauth.net/core/1.0a/
  OAuth 公式リファレンス日本語訳 (twitterで紹介していただいた)
    http://openid-foundation-japan.github.com/draft-hammer-oauth-10.html
  OAuth 公式リファレンスの手順 Image
    http://oauth.net/core/diagram.png
  OAuthの手順 Flash (kuraさんの放送でリスナーさんが貼ってくれたページ)
    http://labs.unoh.net/2008/02/oauth.html

  Twitter API 公式
    http://apiwiki.twitter.com/
  Twitter API の日本語訳
    http://watcher.moe-nifty.com/memo/docs/twitterAPI.txt

  Python での実装、またその時に詰まった内容
    http://techno-st.net/2009/11/26/twitter-api-oauth-0.html

  PHP & abraham's twitteroauth での導入方法
    http://www.sdn-project.net/labo/oauth.html
  abraham's twitteroauth
    http://github.com/abraham/twitteroauth

2 - OAuthでAPIを叩くまでの流れ
2.1 - 基本的な単語

OAuthで使われる基本的な単語
  サービスプロバイダ … Twitterなどサービス提供側
  コンシューマ … APIへアクセスするプログラム
  ユーザ … このプログラムを使う人

  oauth_token … リクエストトークン、またはアクセストークンが入る
  oauth_token_secret … リクエストトークン、またはアクセストークンで暗号化に使うキー
  oauth_verifier … クライアントモードで必要なデータとのこと、Webモードでは取得できない

便宜上このページで使う単語
  リクエストトークン … ユーザリソースへのアクセス許可を求めるための一時的なトークン
  アクセストークン … ユーザリソースの操作に使うトークン

2.2 - 4つのステップ

OAuth で API を叩くまでの4つのステップ。
  1. 外部アプリを申請する
  2. リクエストトークンをユーザに承認してもらう
  3. (2)を元にユーザリソースへのアクセストークンを得る
  4. OAuth で API を叩く

※ 注意

  ライブラリを組み終わってから、送信データを色々いじっていた所、送信しなくても済むパラメータがあったので、それについては省いています。
  今後 API に送信すべきパラメータが標準に沿うと、このエントリーの情報では足りなくなります。
  PHP のコードでは該当部分をコメントアウトしているので、どのデータが不必要か気になる方はコードを読んでください。

2.2.1 サービスプロバイダに外部アプリを申請する

  初めに製作者はサービスプロバイダにコンシューマの申請をします。
  登録申請先: http://twitter.com/apps

  申請を完了させるとサービスプロバイダから
    Consumer key
    Consumer secret
  の二つが与えられます。

  Consumer key は、サービスプロバイダがこの外部アプリと認識する為のユニークな key です。
  Consumer secret は、コンシューマからの適切な送信と確認できる「暗号文の作成」時に使います。

2.2.2 リクエストトークンの発行と承認

  ユーザにコンシューマを認証してもらうにはリクエストトークンが必要になります。
  まずサービスプロバイダにリクエストトークンを発行してもらいます。
  使用API: http://twitter.com/oauth/request_token
  メソッド: GET
  パラメータ
    oauth_consumer_key
      サービスプロバイダから割り当てられた Consumer key
    oauth_signature_method
      必ず HMAC-SHA1 を指定する
    oauth_timestamp
      time() で手に入る現在のUnixタイム(エポックタイム)
    oauth_signature
      メソッド、URL、またこのパラメータ以外を昇順に並べて暗号化したもの。
      OAuth でも最難関なのが、この signature。
      気になる方はこのページの セクション2.3 signature を参照してください。
  レスポンス
    oauth_token // => request_token
      ユーザへのコンシューマ認証の申請に使うリクエストトークン
    oauth_token_secret // => request_token_secret
      リクエストトークンを使う際に利用する追加の暗号化キー

  次いで、発行されたリクエストトークンをユーザに認証してもらいます(好きな方のAPIを)。
  使用API1: http://twitter.com/oauth/authorize
  使用API2: http://twitter.com/oauth/authenticate
  メソッド: GET
  パラメータ
    oauth_token
      先ほどのリクエストトークン(oauth_token)
  レスポンス
    oauth_token
      先ほどのリクエストトークン(oauth_token)

  authorize, authenticate の認証を済ませると、ユーザはPINを得るか指定のコールバックURLへリダイレクトされます。
  このページでは PIN ではなくコールバック型で進めます。

2.2.3 アクセストークンの取得

  リクエストトークンが有効になったので、ユーザリソースへのアクセストークンを貰います。

  アクセストークンAPIを叩いてアクセストークンを貰う。
  使用API: http://twitter.com/oauth/access_token
  メソッド: GET
  パラメータ
    oauth_consumer_key
      サービスプロバイダから割り当てられた Consumer key
    oauth_signature_method
      必ず HMAC-SHA1 を指定する
    oauth_timestamp
      time() で手に入る現在のUnixタイム(エポックタイム)
    oauth_token
      先ほどのリクエストトークン(oauth_token)
  レスポンス
    oauth_token // => access_token
      外部アプリがユーザの資源を操作するためのアクセストークン
    oauth_token_secret // => access_token_secret
      外部アプリがユーザの資源を操作するためのデータの暗号化に使うアクセストークン
    user_id
      アクセストークンのユーザID
    screen_name
      アクセストークンのユーザ名


  このうちアクセストークンAPIから貰った oauth_token と oauth_token_secret は保管しておきます。

2.2.4 OAuthでAPIを叩く

  最後に試験的にツイートを投稿します。
  使用API: http://twitter.com/statuses/update.xml
  メソッド: GET
  パラメータ
    oauth_consumer_key
      サービスプロバイダから割り当てられた Consumer key
    oauth_signature_method
      必ず HMAC-SHA1 を指定する
    oauth_timestamp
      time() で手に入る現在のUnixタイム(エポックタイム)
    oauth_token
      アクセストークンを指定する
    oauth_signature
      メソッド、URL、またこのパラメータ以外を昇順に並べて暗号化したもの。
      OAuth でも最難関なのが、この signature。
      気になる方はこのページの セクション2.3 signature を参照してください。
  追加パラメータ
    status
      ツイートする内容
  レスポンス
    XMLデータ
      ツイートした結果のxmlデータ

2.3 - signature

  signature の作り方については こちらのリンク を参考に。

  OAuth では HMAC-SHA1 か RSA-SHA1 という方式で signature を作るように定められています(PLAINTEXTもあり)。
  Twitter はこのうち HMAC-SHA1 を採用しています。
  この HMAC-SHA1 で暗号化するのに「共通鍵=key」、「ハッシュ化するメッセージ」を用意します。

  共通鍵=Key
    コンシューマを登録した際に貰う Consumer key と、oauth_token_secret を & でつつなぎます。
    oauth_token_secret が空の場合でも & は必ず付けてください。
    例1:
      [consumer_key]&[oauth_token_secret]
    例2:
      [consumer_key]&

  ハッシュ化するメッセージ
    メッセージは、接続時のメソッド、接続先のURL、そして送信するパラメータを使います。
    パラメータは前もって、パラメータ名でソートしておきます。

    メッセージは以下のようにつなげます。
    抽象例:
      [メソッド]&[URL]&[パラメータ]
      このうち URL と パラメータは RFC3986 に則った url encode が必要
    値例:
      メソッド => GET
      URL => http://www.twitter.com/hoge/foo
      パラメータ => param1=test1&bar=barbar&n=42
      ソート後のパラメータ => bar=barbar&n=42&param1=test1
    出来上がるメッセージ:
GET&http%3A%2F%2Fwww.twitter.com%2Fhoge%2Ffoo&bar%3Dbarbar%26n%3D42%26param1%3Dtest1

  この key と メッセージ を元に sha1 でハッシュ化した値が signature になります。

3 - コード
3.1 - Eduwitter メソッド一覧

  setApiPath
    処理
      API の Path を初期化します(クラス生成時に呼ばれる)。
    引数(無し)
    戻り値(無し)

  __construct
    処理
      引数があれば、クラスに保管する
    引数
      consumer_key
        与えられた Consumer key
      consumer_secret
        与えられた Consumer secret
    戻り値(無し)

  setRequestToken
    処理
      リクエストトークンをクラスに持たせる
    引数
      tokens
        getRequestTokenで得た配列
    戻り値(無し)

  getRequestToken
    処理
      新しいリクエストトークンを取得する
    引数(無し)
    戻り値
      リクエストトークンAPIのレスポンスを配列にしたもの

  getParameter_RequestToken
    処理
      リクエストトークンAPIを叩くのに必要なパラメータを作成する
    引数(無し)
    戻り値
      パラメータの文字列

  setAccessToken
    処理
      アクセストークンをクラスに持たせる
    引数
      tokens
        getAccessTokenで得た配列
    戻り値(無し)

  getAccessToken
    処理
      アクセストークンを取得する。
      この関数を呼ぶ前に、setRequestToken を呼び出してください。
    引数(無し)
    戻り値
      アクセストークンAPIのレスポンスをを配列にしたもの

  getParameter_AccessToken
    処理
      アクセストークンAPIを叩くのに必要なパラメータを作成する
    引数(無し)
    戻り値
      パラメータの文字列

  requestOAuthAPI
    処理
      指定した OAuth API を叩く
    引数
      api_and_format
        API の Path と取得できる文字列のフォーマット。
        例:
          api は statuses/update
          取得フォーマットが xml
        値:
          statuses/update.xml
      method
        HTTPリクエストのメソッド
        GET または POST
      post_field
        このAPIに送信すべきデータセット
    戻り値
      api_and_format で指定したフォーマットのデータ

  getParameter_OAuthToken
    処理
      指定した OAuth API を叩くのに必要なパラメータを作成する
    引数
      【requestOAuthAPI】と同じ
    戻り値
      作成したパラメータ

3.2 - Eduwitter での作業

Eduwitter をダウンロードして設置しておきます。

作業のステップの区分は4つのステップと同じです。

ステップ1. 外部アプリを登録した後

  外部アプリを登録します。
  登録後、エディタで eduwitter_test.php を開き

/*---------------------------------------------------------
                 Configure Section
---------------------------------------------------------*/
$consumer_key = '<Consumer key>';
$consumer_secret = '<Consumer secret>';

  の各変数に
    Consumer key
    Consumer secret
  を入れます。

ステップ2. リクエストトークンの発行と承認

  Eduwitter の getRequestToken を呼ぶと、リクエストトークンを配列にして返します。
  戻り値の配列は
    array(
      'request_token' => oauth_token,
      'request_token_secret' => oauth_token_secret
    );
  となっています。

  このうちユーザに oauth_token(リクエストトークン) を持たせたまま authoreize または authenticate のリンクへ誘導します。

ステップ3. アクセストークンを取得

  ユーザがコールバックURLに戻ってきた際に、GETメソッドで oauth_token(リクエストトークン) が送られてきています。
  このリクエストトークンを Eduwitter クラスに預けます。
    array (
      'request_token' => oauth_token,
      'request_token_secret' => '' // この値はあっても無くてもよい
    );
  この配列を setRequestToken の引数にします。

  その後 getAccessTokenメソッド を呼ぶと、アクセストークンを配列にして返します。
  戻り値の配列は
    array(
      'access_token' => oauth_token,
      'access_token_secret' => oauth_token_secret
    );
  となっています。

ステップ4. OAuth で API を叩く

  ステップ3 で取得したアクセストークンの配列を setAccessToken に放り込みます。
  その後、呼び出したいAPIを requestOAuthAPI メソッドで呼びます。

3.3 - Eduwitter

コードは配布しているものと同じですが、Web上で気軽に挙動を確認したい方向けにコードを貼っておきます。

eduwitter.php
<?php
/**********************************************************
* Eduwitter
* @poochin - http://www13.atpages.jp/llan/
* LastUpdate: 2010-04-23
* License: MIT or BSD
*   MIT: http://www.opensource.org/licenses/mit-license.php
*   BSD: http://www.opensource.org/licenses/bsd-license.php
*
*   Twitter で OAuth を勉強したい方向けのライブラリ。
*   学習用のため、重複処理を切り離さずあえて冗長にしています。
*   どういう振る舞いで OAuth が使えるのか理解できたら、重複
* 処理を切り離してみると、より理解が深まるかも知れません。
*********************************************************/

define ('SCHEME_HTTP',  'http://');
define ('SCHEME_HTTPS', 'https://');
define ('HOST_TWITTER', 'twitter.com');

class Eduwitter
{
/* Private Area */
 private $consumer_key,    // provided Consumer key
         $consumer_secret; // provided Consumer secret

 private $access_token,        // oauth token to access protected resource
         $access_token_secret; // oauth token_secret to acces protected resource

 private $request_token,         // one-time unique oauth token
         $request_token_secret;  // one-time unique oauth token secret

 private $api_path;  // api_path

 private function setApiPath()
 {
   $this-&amp;amp;gt;api_path = array(
     'request_token' =&amp;amp;gt; 'oauth/request_token',
     'access_token' =&amp;amp;gt; 'oauth/access_token',
     'authenticate' =&amp;amp;gt; 'oauth/authenticate',
     'authorize' =&amp;amp;gt; 'oauth/authorize',
   );
 }

/* Public Area */
 /**
  * __construct
  *
  * Arguments
  *   consumer_key -- provided consumer key
  *   consumer_secret -- provided consumer secret
  */
 public function __construct($consumer_key = null, $consumer_secret = null)
 {
   if (isset($consumer_key) &amp;amp;amp;&amp;amp;amp; isset($consumer_secret)) {
     $this-&amp;amp;gt;consumer_key = $consumer_key;
     $this-&amp;amp;gt;consumer_secret = $consumer_secret;
   }

   self::setApiPath();
 }

 /*-----------------------------------------------------
                     Request Token
 -----------------------------------------------------*/
 /**
  * setAccessToken
  *
  * Arguments
  *   tokens -- array include request token
  */
 public function setRequestToken($tokens)
 {
   if (isset($tokens['oauth_token'])) {
     $this-&amp;amp;gt;request_token = $tokens['oauth_token'];
     $this-&amp;amp;gt;request_token_secret = $tokens['oauth_token_secret'];
   }
   else {
     $this-&amp;amp;gt;request_token = $tokens['request_token'];
     $this-&amp;amp;gt;request_token_secret = $tokens['request_token_secret'];
   }
 }

 /**
  * getRequestToken
  *
  * Depend on Library or Functions
  *   Functions
  *     getParameter_RequestToken
  *     parameter2Array
  */
 public function getRequestToken()
 {
   $requestTokenURL = SCHEME_HTTP.HOST_TWITTER.'/'.$this-&amp;amp;gt;api_path['request_token'];
   $params = self::getParameter_RequestToken();

   $response = file_get_contents($requestTokenURL.'?'.$params);
   return parameter2Array($response);
 }

 /**
  * getParameter_RequestToken
  *
  * Return
  *   Parameter for OAuth API(request_token)
  *
  * Depend on Library or Functions
  *   Critical Std Functions
  *      hash_hmac
  *      base64_encode
  *
  *   Functions
  *     array2Parameter
  */
 public function getParameter_RequestToken()
 {
   $requestTokenURL = SCHEME_HTTP.HOST_TWITTER.'/'.$this-&amp;amp;gt;api_path['request_token'];

   $parts = array (
     'oauth_consumer_key'      =&amp;amp;gt; $this-&amp;amp;gt;consumer_key,
     'oauth_signature_method'  =&amp;amp;gt; 'HMAC-SHA1',
     'oauth_timestamp'         =&amp;amp;gt; time(),
//        'oauth_nonce'             =&amp;amp;gt; md5(microtime() . mt_rand()),  // isn't neccesary
//        'oauth_version'           =&amp;amp;gt; '1.0a',  // isn't neccesary
   );

   ksort($parts);
   $params = array2Parameter($parts);

   $message = 'GET'.'&amp;amp;amp;'.
              rawurlencode($requestTokenURL).'&amp;amp;amp;'.
              rawurlencode($params);

   $key = $this-&amp;amp;gt;consumer_secret.'&amp;amp;amp;';

   $parts['oauth_signature'] =
     rawurlencode(base64_encode(hash_hmac('sha1', $message, $key, true)));

   return array2Parameter($parts);
 }

 /*-----------------------------------------------------
                     Access Token
 -----------------------------------------------------*/
 /**
  * setAccessToken
  *
  * Arguments
  *   tokens -- array include access token
  */
 public function setAccessToken($tokens)
 {
   if (isset($tokens['oauth_token'])) {
     $this-&amp;amp;gt;access_token = $tokens['oauth_token'];
     $this-&amp;amp;gt;access_token_secret = $tokens['oauth_token_secret'];
   }
   else {
     $this-&amp;amp;gt;access_token = $tokens['access_token'];
     $this-&amp;amp;gt;access_token_secret = $tokens['access_token_secret'];
   }
 }

 /**
  * getAccessToken
  *
  * Depend on Library or Functions
  *   Functions
  *     getParameter_AccessToken
  *     parameter2Array
  */
 public function getAccessToken()
 {
   $accessTokenURL = SCHEME_HTTPS.HOST_TWITTER.'/'.$this-&amp;amp;gt;api_path['access_token'];
   $param = self::getParameter_AccessToken();

   $response = file_get_contents($accessTokenURL.'?'.$param);
   return parameter2Array($response);
 }

 /**
  * getParameter_AccessToken
  *
  * Return
  *   Parameter for OAuth API(access_token)
  *
  * Depend on Library or Functions
  *   Functions
  *     array2Parameter
  *
  * Neccesary Variables
  *     consumer_key
  *     oauth_token (request token)
  */
 public function getParameter_AccessToken()
 {
   $requestTokenURL = SCHEME_HTTP.HOST_TWITTER.'/'.$this-&amp;amp;gt;api_path['access_token'];

   $parts = array (
     'oauth_consumer_key'      =&amp;amp;gt; $this-&amp;amp;gt;consumer_key,
     'oauth_signature_method'  =&amp;amp;gt; 'HMAC-SHA1',
     'oauth_timestamp'         =&amp;amp;gt; time(),
//        'oauth_nonce'             =&amp;amp;gt; md5(microtime() . mt_rand()), // isn't neccesary
//        'oauth_version'           =&amp;amp;gt; '1.0a',  // isn't neccesary
     'oauth_token'             =&amp;amp;gt; $this-&amp;amp;gt;request_token,
   );

   return array2Parameter($parts);
 }

 /*-----------------------------------------------------
                   request OAuth API
 -----------------------------------------------------*/
 /**
  * requestOAuthAPI
  *
  * Arguments
  *   api_and_format -- api path and format(xml or json)
  *   method -- GET or POST
  *   post_field -- parameters excluding oauth parameters
  *
  * Return
  *   OAuth API result
  *
  * Depend on Library or Functions
  *   Libs
  *     CURL
  *
  *   Functions
  *     self::getParameter_OAuthToken
  */
 public function requestOAuthAPI($api_and_format, $method, $post_field)
 {
   foreach ($post_field as $k =&amp;amp;gt; $v) {
     $post_field[$k] = rawurlencode($v);
   }

   $httpURL = SCHEME_HTTP.HOST_TWITTER.'/'.$api_and_format;
   $params = self::getParameter_OAuthToken($api_and_format, $method, $post_field);

   $ch = curl_init();

   if ($method === 'POST') {
     curl_setopt($ch, CURLOPT_POST, 1);
   }

   curl_setopt($ch, CURLOPT_URL, $httpURL);
   curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
   curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
   curl_setopt($ch, CURLOPT_POSTFIELDS, $params);

   $response = curl_exec($ch);
   curl_close($ch);

   return $response;
 }

 /**
  * getParameter_OAuthToken
  *
  * Arguments
  *   api_and_format -- api path and format(xml or json)
  *   method -- GET or POST
  *   post_field -- parameters excluding oauth parameters
  *
  * Return
  *   Parameter for OAuth API
  *
  * Depend on Library or Functions
  *   Critical Std Functions
  *      hash_hmac
  *      base64_encode
  *
  *   Functions
  *     array2Parameter
  *
  * Neccesary Variables
  *     consumer_key
  *     consumer_secret
  *     oauth_token (access token)
  *     oauth_token_secret (access token secret)
  */
 public function getParameter_OAuthToken($api_and_format, $method, $post_field)
 {
   $requestTokenURL = SCHEME_HTTP.HOST_TWITTER.'/'.$api_and_format;

   $parts = array (
     'oauth_consumer_key'      =&amp;amp;gt; $this-&amp;amp;gt;consumer_key,
     'oauth_signature_method'  =&amp;amp;gt; 'HMAC-SHA1',
     'oauth_timestamp'         =&amp;amp;gt; time(),
//        'oauth_nonce'             =&amp;amp;gt; md5(microtime() . mt_rand()), // isn't neccesary
//        'oauth_version'           =&amp;amp;gt; '1.0a',  // isn't neccesary
     'oauth_token'             =&amp;amp;gt; $this-&amp;amp;gt;access_token,
   );

   $parts = array_merge($parts, $post_field);

   ksort($parts);
   $params = array2Parameter($parts);

   $message = $method.'&amp;amp;amp;'.
              rawurlencode($requestTokenURL).'&amp;amp;amp;'.
              rawurlencode($params);

   $key = $this-&amp;amp;gt;consumer_secret.'&amp;amp;amp;'.$this-&amp;amp;gt;access_token_secret;

   $parts['oauth_signature'] =
     rawurlencode(base64_encode(hash_hmac('sha1', $message, $key, true)));

   return array2Parameter($parts);
 }
}

/*---------------------------------------------------------
                     Functions
---------------------------------------------------------*/
/**
* hashed array to string
*
* Argument
*   array (
*     'key1' =&amp;amp;gt; 'value1',
*     'key2' =&amp;amp;gt; 'value2',
*     ...
*   );
*
* Return
*   'key1=value1&amp;amp;amp;key2=value2'
*/
function array2Parameter($src)
{
$parameters = '';

if (gettype($src) != 'array') {
 return '';
}

$parts = array();
foreach ($src as $k =&amp;amp;gt; $v) {
 $parts[] = &amp;amp;quot;{$k}={$v}&amp;amp;quot;;
}

return implode('&amp;amp;amp;', $parts);
}

/**
* string to hashed array
*
* Argument
*   'key1=value1&amp;amp;amp;key2=value2'
*
* Return
*   array (
*     'key1' =&amp;amp;gt; 'value1',
*     'key2' =&amp;amp;gt; 'value2',
*     ...
*   );
*/
function parameter2Array($params)
{
$items = explode('&amp;amp;amp;', $params);
$parts = array();
foreach ($items as $item) {
 $sp = explode('=', $item);
 $parts[$sp[0]] = $sp[1];
}
return $parts;
}
訂正(2010-05-04).

  tana_ashさんの指摘より、6ヶ所 sigunature となっていた部分を signature に訂正しました。

追記(2010-05-04).

  最上部に v0.2 のお知らせ

3 件のコメント:

  1. [...] This post was mentioned on Twitter by 清太郎, 清太郎. 清太郎 said: ブログ更新: TwitterでOAuthを使うための学習用ライブラリ Eduwitter http://www13.atpages.jp/~llan/wp/?p=730 [...]

    返信削除
  2. TwitterでBasic認証の替わりの認証方法について調べる...

    DAC/スパイスラボ神部です。 Twitterでサービスを作る際、Basic認証を使ったものは無数にあるかと思いますが、6月末でそれらがシャットダウンされるにあ......

    返信削除
  3. [...] Twitter API を OAuth で認証するスクリプトを 0 から書いてみた – trial and error PHPでTwitterのOAuthを使うための学習用ライブラリ Eduwitter : Lounge Landscape [...]

    返信削除