#初めに
##C言語でTwitter APIを叩けたら幸せな感じがしませんか?
ぼくはします。
そう思ったので僕は、C言語でTwitter APIのWrapper Libraryを書きました。
まだ開発途中ですが、GitHubでコードを公開しています : T4C
上のライブラリを書くにあたって、自分の備忘録のためにも記事を書こうと思いました。
そこで今回は、C言語でTwitter APIを用いてツイートすることを目標とします。
なお、予め断っておきますがC++ではなく、C言語の話です。
##追記(2016/06/08)
T4CをGETリクエストに対応させました。また、使いにくかったLinkedListを廃止し、あらたに 単方向リストを実装して、それを用いてパラメーターを扱うようにしました。
以下のソースコードには上記の変更を加えました(最新のT4Cと同じコードにしました)。
#Twitter APIの使い方
TwitterではAPIを使うために、OAuthという認証プロトコルを用います。
そして、そのOAuthに従った形で、そのリクエストが正規のものであることを示し、適切なエンドポイントに適切なパラメーターを渡すことでAPIを使うことができます。
したがって、C言語でTwitter APIを使うためには
- OAuthでリクエストを送るための処理を実装する(以下は簡単のため
OAuthを実装する
と表記します) - Twitterに
POST
またはGET
リクエストをおくる処理を実装する
大きく分けて上の2つの部分を実装する必要があります。(大きく分けるとは言いましたが、2番目はとても簡単ですが、1番目のOAuthを実装することは2番目に比べてはるかに大変(と言うよりも面倒くさい)です)。
また、1つめのOAuthを実装するときに必要なHMAC-SHA1
の部分はOpenSSL
を使います。
さらに、2番目のネットワーク通信の部分は使いやすく、またポピュラーなライブラリであるlibcurl
を使います。
さて、早速OAuthの実装を始めましょう。
#OAuthを実装する
##OAuthを実装する前に
はじめに断っておきますが、OAuthをC言語で実装することは決して難しくはありません。ただただ、手間がかかるだけです。(少なくとも、HMAC-SHA1
を自前で実装することをせずにOpenSSL
を用いれば簡単です)
ですので、そんなに難しく考える必要はありません。
また、この記事はツイートをすることに焦点を当てるため、最小限のOAuthを実装することを重視します。よって ConsumerKey
やAccessToken
の取得については言及しません。これは各自で用意してください。以下ではConsumerKey
, ConsumerSecret
, AccessToken
, AccessTokenSecret
はすでに用意されていることを前提にします。
##ツイートまでの流れ
とりあえず、ツイートまでの処理の流れを考えてみましょう。
ツイートをするには/statuses/update.json
に対してツイートの内容をstatus
というパラメーターにのっけてPOSTします。
また、ツイートまでの流れは以下のようになります(あとでコードは載せます。書いてから思いましたが、この流れの説明はあんまり分かりやすくないので下のコードを読んでもらうほうがいいかも...)
- OAuthのためのパラメーター(の値)(その1)を生成します(
oauthParams
と名づけます)。そのパラメーターとは以下の6つです。
パラメーター | 説明 |
---|---|
oauth_consumer_key | ConsumerKey |
oauth_nonce | ランダムな文字列 |
oauth_signature_method | OAuthのシグネチャの符号化方式。Twitterは"HMAC-SHA1" |
oauth_timestamp | シグネチャを作った時のタイムスタンプ。UNIXタイム |
oauth_token | AccessToken |
oauth_version | OAuthのバージョン。Twitterは"1.0" |
- エンドポイントに対するパラメーター(
params
とします)に、oauthParams
のパラメータとその値をコピーして合体させます。つまり、params
はここでは、先ほどのoauthParams
の6つのパラメータと、もともと持っていた/statuses/update.json
に対するstatus
というパラメーターの値を持っています。(oauthParams
は何も変化させません) - 合体させた後の
params
を元に、OAuthのSignatureを生成します - 得られたSignatureを
params
とoauthParams
にoauth_signature
というパラメーターとして値を追加します。 - OAuthではHTTPのHeaderに
Authorization
というパラメーターも必要なのでそれをoauthParams
を連結することでパラメーターようの値を生成します。 - さらに、POSTのボディ用の値を
params
を連結することで生成します。
あとは、先ほどの基本となるURL + "/statuses/update.json?"
paramsを"&"で連結して作ったPOSTボディ用の値
に対して、HTTPでPOSTリクエストを飛ばせばツイートができます。
サンプルとしては以下の様な値が完成しています
Key | Value |
---|---|
URL | https://api.twitter.com/1.1/statuses/update.json |
POST Body | oauth_consumer_key=Your Consumer Key&oauth_nonce=1465304279&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1465304279&oauth_token=Your AccessToken&oauth_version=1.0&status=Hello%20World%21&oauth_signature=LDBmeHn5A5DRcDzz6iEafxwIMto%3D |
Authorization Header | Authorization: OAuth oauth_consumer_key=Your Consumer Key,oauth_nonce=1465304279,oauth_signature_method=HMAC-SHA1,oauth_timestamp=1465304279,oauth_token=Your AccessToken,oauth_version=1.0,oauth_signature=LDBmeHn5A5DRcDzz6iEafxwIMto%3D |
ここからは上の処理をコードにしていきます。
まず、前提となる構造体(型)とそれを扱う関数の宣言をしておきます。
//malloc用のマクロ
#define MALLOC_T(T) (T*)malloc(sizeof(T))
#define MALLOC_TN(T, N) (T*)malloc(sizeof(T) * N)
#define MALLOC_TS(T, S) (T*)malloc(S)
//自作の文字列用の構造体
typedef struct {
size_t length;//文字列長
char* value;//文字列本体
} string;
//string構造体からchar*を取得する。ただし、末端に'\0'が加えられたものが帰る。
char* string_get_value(string str);
//string構造体を初期化し、それを返す
string new_string();
//char*からstring型を生成してそれを返す
string make_string(char* char_str);
//stringを開放する
void free_string(string str);
//リクエストの種類を表す列挙体
typedef enum {
POST,
GET
} METHOD;
//Twitter APIの基本となるURL
#define baseUrl "https://api.twitter.com/1.1"
//T4Cようの構造体。consumerKey等を保持する
typedef struct {
string consumerKey,
consumerSecret,
accessToken,
accessTokenSecret;
} T4C;
//パラメーターは単方向連結リストで管理する。
//パラメーターを保持する構造体。他の言語の連想配列的につかいたい。
typedef struct {
string key, //パラメーターのkey
value//パラメーターを値;
} Parameter;
//listのノード
typedef struct _node {
Parameter* value;
struct _node* next;
} Node;
//単方向リストの始点を保持する、リスト本体
typedef struct {
Node* firstNode;
} Parameters;
//単方向リストを初期化する
Parameters* new_parameters();
//単方向リストが空かどうか
bool is_parameters_empty(Parameters* params);
//stringのkeyとvalueをパラメーターに追加する
void add_parameter(Parameters* params, string key, string value);
//パラメーターを開放する
void free_parameters(Parameters* params);
//listがもつパラメーターのkeyとvalueを"="でつなぎ、繋いだ組をseparatorで与えられた
//char*で連結する。"A=B"と"C=D"となっていれば"A=B&C=D"となる
string join_parameters(Parameters* params, char* separator);
//与えられたstringをURLエンコードして返す。
string url_encode(string base);
そして、リクエストを行う関数を以下のように書きます。
内部で呼び出している関数はこの下に記載します(とりあえず本体を先に載せようと思ったので、このような掲載順序になります)。
//ConsumerKey等をもつT4C*, リクエストの種類を表すMETHOD, リクエストのエンドポイントを表すstring, リクエストに対するパラメーターをもつlist*を引数に取る(戻り値はTwitterから帰ってきたレスポンスをもつstring)
string request(T4C* t4c, METHOD method, string endPoint, Parameters* paramsArgument) {
string result;
//エンドポイントに対するパラメーターがNULLかどうか
bool paramsArgumentWasNULL = false;
if (paramsArgument == NULL) {
//NULLであれば、新たに確保する。
paramsArgument = new_parameters();
//内部で確保するので、後で開放するためにフラグを操作
paramsArgumentWasNULL = true;
}
//OAuth用のパラメーターを保持するリストを初期化
Parameters* oauthParams = new_parameters();
//OAuth用のパラメーターの値を生成
genOAuthParams(t4c, oauthParams);
//POSTリクエストのBodyとなるパラメーターを保持するリストを初期化
Parameters* params = new_parameters();
//paramsにOAuth用のパラメーターとエンドポイント用のパラメーターを合体させる
buildParams(params, oauthParams, paramsArgument);
//実際にリクエストをとばすurl用の文字列を生成。(基本となるURLとエンドポイントのstringを合体させる)
string url = new_string();
url.length = strlen(baseUrl) + endPoint.length;
url.value = MALLOC_TN(char, url.length);
sprintf(url.value , "%s%s", baseUrl, string_get_value(endPoint));
//t4cのうち、consumerSecretとaccessTokenSecret、 加えて、リクエストの種類と、リクエスト先のURL、さらに、パラメーター一覧からSignatureを生成
string oauthSignature = signature(t4c->consumerSecret, t4c->accessTokenSecret, method, url, params);
//base64エンコードされたOAuthのSignatureをurlエンコードする
string encodedSignature = url_encode(oauthSignature);
//oauthParamsとparamsにencodedSignatureを追加する
add_parameter(oauthParams, make_string("oauth_signature"), encodedSignature);
add_parameter(params, make_string("oauth_signature"), encodedSignature);
//リクエストヘッダに追加するAuthorizationのパラメーターを保持する文字列を用意
string authorize = new_string();
//oauthParamsを","区切りで連結
string authorizeChild = join_parameters(oauthParams, ",");
authorize.length = 21 + authorizeChild.length;
authorize.value = MALLOC_TN(char, authorize.length);
//ヘッダにのせるAuthorizationの文字列を生成。
sprintf(authorize.value, "Authorization: OAuth %s", string_get_value(authorizeChild));
//リクエストするときのパラメーターを"&"区切りで連結する
string path = join_parameters(params, "&");
//デバッグ用に生成された文字列類を表示する
printf("----------------------------\n");
printf("URL: %s\n", string_get_value(url));
printf("endPoint: %s\n", string_get_value(endPoint));
printf("path: %s\n", string_get_value(path));
printf("authorize: %s\n", string_get_value(authorize));
printf("----------------------------\n");
//実際にTwitterにリクエストをとばす
//cURLを初期化
CURL* curl;
curl = curl_easy_init();
string reqURL = new_string();
if (method == GET) {
reqURL.length = url.length + 1 + path.length;
reqURL.value = MALLOC_TN(char, reqURL.length);
sprintf(reqURL.value, "%s?%s", url.value, path.value);
//リクエストを送る先を設定
curl_easy_setopt(curl, CURLOPT_URL, reqURL.value);
} else if (method == POST) {
//POSTボディを適用する
curl_easy_setopt(curl, CURLOPT_POST, 1);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, string_get_value(path));
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, path.length);
//リクエストを送る先を設定
curl_easy_setopt(curl, CURLOPT_URL, url.value);
}
//Headerを保持するcurl_slist*を初期化
struct curl_slist *headers = NULL;
//Authorizationをヘッダに追加
headers = curl_slist_append(headers, string_get_value(authorize));
curl_easy_setopt(curl, CURLOPT_HEADER, headers);
//コールバックを指定する
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_result_stringlize_callback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void*)&result);
//後始末
curl_easy_perform(curl);
curl_easy_cleanup(curl);
free_string(url);
free_string(path);
free_string(authorize);
free_parameters(oauthParams);
free_parameters(params);
if (paramsArgumentWasNULL) {
free_parameters(paramsArgument);
}
//Twitterからのレスポンスをreturn
return result;
}
以下に、requestで利用している関数を掲載します(一部宣言のみ掲載)。
//dataとして受け取ったstringをbase64エンコードしてbufにつっこむ。resolv.hで定義される`b64_ntop`を使用(別に自分でbase64エンコーダぐらい自作しても良かったのだが....)
static int k64_encode (string data, string buf);
//Signatureの生成
static string signature(string consumerSecret, string accessTokenSecret, METHOD method, string url, list* params) {
//パラメーターを"&"で連結する
string query = join_parameters(params, "&");
//(一応)consumerSecretとかもURLエンコードしておく(多分不要)
string encodedCS = url_encode(consumerSecret);
string encodedATS = url_encode(accessTokenSecret);
//hmac-sha1で符号化するときのkey. encodedCSとencodedATSを"&"でくっつける
string key;
key.length = encodedCS.length + 1 + encodedATS.length;
key.value = MALLOC_TN(char, key.length);
sprintf(key.value, "%s&%s", string_get_value(encodedCS), string_get_value(encodedATS));
//hmac-sha1で符号化されるデータの文字列
string base;
//リクエストの種類を表すMETHODを文字列として保持する
string method_str;
if (method == POST) {
method_str = make_string("POST");
} else {
method_str = make_string("GET");
}
//urlをURLエンコードする
string encodedURL = url_encode(url);
//上で作ったqueryをURLエンコードする
string encodedQuery = url_encode(query);
base.length = method_str.length + 1 + encodedURL.length + 1 + encodedQuery.length;
base.value = MALLOC_TN(char, base.length);
sprintf(base.value, "%s&%s&%s", string_get_value(method_str), string_get_value(encodedURL), string_get_value(encodedQuery));
//hmac_sha1で符号化する
string res = hmac_sha1(key, base);
//符号化したデータをbase64エンコードして得られたsignature本体を保持する。
string buf = new_string();
buf.length = 256;
buf.value = MALLOC_TN(char, * buf.length);
//signatureをbase64エンコードしてbufに格納
k64_encode (res, buf);
//resをbase64エンコードして得られるbufの長さは変化するので本当の長さ(最初の'\0'までの文字数)を取得し、それをbufの長さとする
int len = 0;
for (int i = 0; i < buf.length; i++) {
if (buf.value[i] == '\0') {
break;
} else {
len++;
}
}
//長さを適用する
buf.length = len;
return but;
}
//受け取ったParameters*にoauthParamsの値を設定する
static void genOAuthParams(T4C* t4c, Parameters* params) {
//UNIXタイムを文字列化する(もっといい方法があるかも?)
time_t now = time(NULL);
char* now_str = MALLOC_TS(char, sizeof(now));
sprintf(now_str, "%d", (int)now);
//パラメーターの値をセットしていく
add_parameter(params, make_string("oauth_consumer_key"), t4c->consumerKey);
add_parameter(params, make_string("oauth_nonce"), make_string(now_str));
add_parameter(params, make_string("oauth_signature_method"), make_string("HMAC-SHA1"));
add_parameter(params, make_string("oauth_timestamp"), make_string(now_str));
add_parameter(params, make_string("oauth_token"), t4c->accessToken);
add_parameter(params, make_string("oauth_version"), make_string("1.0"));
free(now_str);
}
//ouathParamsとadditinalParam(エンドポイントに対するパラメーター)の内容をparamsにmerge
static void buildParams(Parameters* params, Parameters* oauthParams, Parameters* additionalParam) {
for (Node* thisNode = oauthParams->firstNode; thisNode != NULL; thisNode = thisNode->next) {
add_parameter(params, thisNode->value->key, thisNode->value->value);
}
if (additionalParam != NULL && !is_parameters_empty(additionalParam)) {
Parameters* adParams = additionalParam;
for (Node* thisNode = adParams->firstNode; thisNode != NULL; thisNode = thisNode->next) {
add_parameter(params, thisNode->value->key, url_encode(thisNode->value->value));
}
}
}
//libcurlに渡すコールバック
static size_t curl_result_stringlize_callback(void* ptr, size_t size, size_t nmemb, void* data) {
if (size * nmemb == 0)
return 0;
size_t realsize = size * nmemb;
string* str = (string*)data;
str->length = realsize;
str->value = MALLOC_TN(char, str->length);
if (str->value != NULL) {
memcpy(str->value, ptr, realsize);
}
return realsize;
}
ちなみに、"HMAC-SHA1"で符号化する処理はOpenSSL
を用いて以下のように書きました
#include <t4c/string.h>
#include <t4c/util.h>
#include <openssl/hmac.h>
#include <openssl/sha.h>
string hmac_sha1(string key, string data){
string result = new_string(),
res = new_string();
res.length = SHA_DIGEST_LENGTH + 1;
res.value = MALLOC_TN(char, res.length);
HMAC(EVP_sha1(),
(const unsigned char*)string_get_value(key), string_length(key),
(const unsigned char*)string_get_value(data), string_length(data),
(unsigned char*)res.value, (unsigned int*)&res.length);
return res;
}
とりあえず、こんな感じでOAuthの実装はできます(ここに乗っているコードだけをコピーしても宣言のみの場所があったり、宣言順序がバラバラなので動きませんので実際に使いたい場合は、上で紹介したT4Cを使ってください)。
#ツイートする
上にのっけたコードの中にlibcurl
を使う処理を書いてしまったので、上で2つに分けた時の2つめの部分(どうやってツイッターにリクエストを実際に飛ばすか、という話)の説明が無くなっちゃったので、T4Cを用いてツイートしてみましょう。
T4Cをgit clone
してください。
T4Cのsrc/にあるtest.c
には、テストとしてツイートするコードが書いてあります。(設定が必要)
自分のcusumerKey等を設定する必要があるので、src/test.c
を編集してください。
編集後に$ make
でビルドができ、./t4c_test
と実行することでツイートが可能です。
以下にsrc/test.c
のコードを貼り付けます
#include <t4c/parameters.h>
#include <t4c/string.h>
#include <t4c/t4c.h>
#include <stdio.h>
int main() {
//T4Cを初期化
T4C t4c = {};
//ConsuemrKey等を設定。ここを編集してください
t4c.consumerKey = make_string("Your Consumer Key");
t4c.consumerSecret = make_string("Your Consumer Secret");
t4c.accessToken = make_string("Your AccessToken");
t4c.accessTokenSecret = make_string("Your AccessTokenSecret");
//パラメーターを初期化
Parameters* params = new_parameters();
//Key : value = status : "Hello World!"をパラメーターに追加
add_parameter(params, make_string("status"), make_string("Hello World!"));
//ツイートする(Twitterにリクエストを飛ばす)
string result = request(&t4c, POST, make_string("/statuses/update.json"), params);
//Twitterからレスポンスとして帰ってきたJSONを表示
printf("RESULT for Tweet: %s\n", string_get_value(result));
//後処理
free_parameters(¶m);
return 0;
}
筆者は以下の環境で動作確認をしています:
- OS X 10.11.5
- OpenSSL 1.0.2h
- libcurl 7.44.0
#まとめ
あまり上手い記事にならなかった気がしますが(説明力...)、C言語でOAuthを実装するのはこういう感じでやるよ!って言う感じのは示せたのでは無いでしょうか...
今回この記事を書いたのは、TwitterのAPIもそうですが、そもそもOAuthを実装する解説をしている記事が少なく感じ、日本語で解説する文章は(一定の?)需要があるように思えたためです。
とはいえ、これは2年前に僕がD言語でTwitter API Wrapperを書いた時に、日本語でOAuthの実装を解説している記事が少ないように感じたというだけで、今は状況は変わっているかもしれませんし、多くのAPIではすでに多くの(高級な)言語でライブラリが開発されているため、自前で実装する必要があまりないから情報も少なくなっているのかもしれませんが...