スポンサーリンク

Twitter APIをC++とlibcurlで動かす方法 ~ソースコード公開~

前回記事で、Twitter APIを使う上での注意点 ~code:32の解決方法~という記事を書きましたが、ソースコードが欲しいという方もいらっしゃるかと思いまして、ソースコードの方も公開しようかと思います。

ブログに載せるには結構長めのコードになってるので、ソースコードの解説はしませんが、何をしているのかをコメントで書いているので、そちらとTwitter Developerを参照していただければと思います。

一応、気が向いたら今回のコードについての解説記事(分量が多いので何回かに分けてって形になるかと思いますが)も書こうかとは思ってはいるのですが、いつになるかは不明です。

これだけの分量なんで、周辺知識も合わせて解説していくと結構なボリュームになっちゃいそうで、結構気後れしてるんです。

ただ、要望があればお応えしたいとは思ってるので、例えば「HTTPリクエストを送るところを解説してほしい」、「OAuth認証のところを教えてほしい」、「<ファイル名>の○行目から○行目って何をやってるの?」みたいな感じで、何かご要望とか質問とかがあればお問い合わせなり僕のTwitterなりからご連絡ください(もちろん、無理にとは言いませんので、ご安心くださいませ~)。

スポンサーリンク

ソースコード

今回は3つのファイルに分かれていますので、コンパイル時にはご注意ください。

GNU g++コンパイラで、c++17環境でコンパイルできることを確認しています。

まずは、ヘッダーファイルから。

Define.hpp

#pragma once

#define DEBUG_LEVEL 10
#if DEBUG_LEVEL >= 10
#define log( msg ) std::cout << msg << std::endl
#else
#define log( msg ) /* NOP */
#endif

HTTPRequest.hpp

#pragma once

#include "Define.hpp"

#include <string>
#include <vector>

typedef std::vector<std::string> param;

void splitOptions(std::string, param&);      // URLからエンドポイントとパラメータ(?の後ろ)とを分離
std::string encodeToBase64(std::string);
std::string encodeToURL(std::string);
std::string generateOAuthSignature(std::string, std::string, param, std::string, std::string);
std::string getOAuthHeader(param parameter);

std::string sendRequest(const std::string, const std::string);

こちらがソースファイルになります。

HTTPRequest.cpp

#include "Define.hpp"
#include "HTTPRequest.hpp"

#include <string>
#include <iostream>
#include <vector>
#include <algorithm>
#include <ctime>
#include <curl/curl.h>
#include <openssl/ssl.h>

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

void splitOptions(std::string url, param& parameter)
{
  std::string target = url;
  // c以前の文字列(c含め)を削除して、c以前の文字列(c含めず)を返す
  auto ExtractCut = [&target](const char c){
    int num = target.find_first_of(c);
    std::string str = target.substr(0, num);

    if( num == (int)std::string::npos ){
      target.erase(0);
    }else{
      target.erase(0, num + 1);
    }

    log("----------------");
    log("num = " << num);
    log("str = " << str);
    log("----------------");

    return str;
  };

  // オプションがなければ即刻return
  if(target.find("?") == std::string::npos) { goto NO_OPTIONS; }

  // エンドポイント部分をあらかじめ削除しておく
  ExtractCut('?');

  // オプション部分があれば、オプション部分をそれぞれをparameterの一要素として格納
  // targetが空になるまで&以前の文字列を抽出カット
  while( !(target.empty()) ){
    parameter.push_back(ExtractCut('&'));
  }

  if( !(target.empty()) ){
    log("--------CAUTION--------");
    log("パラメータリストに格納できていないオプションがあります");
    for(auto itr : parameter){ log(itr); }
    log("--------CAUTION--------");
  }
  else{
    log("パラメータリストにすべてのオプションを格納できました");
  }

  return;

NO_OPTIONS:
  log("元からオプションがありませんでした");
  return;
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

std::string encodeToBase64(std::string target)
{
  const char* usable_characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
  std::string result;
  int i = 0;

  // 3文字 24bit を一つの塊と考えて処理する
  // 最後に余り分の「=」を追加する
  // 原文の未変換文字列の長さが2文字以下だったら、ループ(3文字に対する定型処理)を抜ける
  std::cout << "文字の個数 = " << (int)target.length() << std::endl;
  for(i = 0; i < ((int)target.length() - 2); i += 3)
  {
    result.push_back(usable_characters[ (target.at(i) >> 2) & 0x3F ]);
    result.push_back(usable_characters[ ( (target.at(i) << 4) & 0x30 ) | ( (target.at(i + 1) >> 4) & 0xF ) ]);
    result.push_back(usable_characters[ ( (target.at(i + 1) << 2) & 0x3C ) | ( (target.at(i + 2) >> 6) & 0x3 ) ]);
    result.push_back(usable_characters[ target.at(i + 2) & 0x3F ]);
  }

  // 未変換文字列の長さは、0、1、2のどれかのはず。
  // 残りの長さによって、処理を変える(具体的には、文字列の変換方法と=の追加個数を変える)
  if( (target.length() % 3) == 1 ) {
    // 残り1文字
    result.push_back(usable_characters[ (target.at(i) >> 2) & 0x3F ]);
    result.push_back(usable_characters[ (target.at(i) << 4) & 0x30 ]);
    result += "==";
  }
  else if( (target.length() % 3) == 2 )
  {
    // 残り2文字
    result.push_back(usable_characters[ (target.at(i) >> 2) & 0x3F ]);
    result.push_back(usable_characters[ ( (target.at(i) << 4) & 0x30 ) | ( (target.at(i + 1) >> 4) & 0xF ) ]);
    result.push_back(usable_characters[ (target.at(i + 1) << 2) & 0x3C ]);
    result += "=";
  }

  return result;
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

std::string encodeToURL(std::string target)
{
  std::string result = "";

  // 与えられた文字をURLエンコードする必要があるかどうかを判断する関数
  // エンコードの必要あり:1   必要なし:0
  auto URLEncodeNecessity = [](const char c)
  {
    return( (unsigned char) !( (c >= '0' && c <= '9') ||    // 数字だった場合はエンコードの必要なし
        (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||  // アルファベット(大文字でも小文字でも)の場合はエンコードの必要なし
        (c == '-') || (c=='.') || (c=='_') || (c=='~')  // その他、エンコードの必要がない特殊文字
      ));
  };

  for(int i = 0; i < (int)target.length(); i++)
  {
    if(URLEncodeNecessity(target.at(i)))
    {
      result.push_back('%');
      result += "0123456789ABCDEF"[(target.at(i) & 0xF0) >> 4];
      result += "0123456789ABCDEF"[(target.at(i) & 0x0F)];
    }
    else
    {
      result.push_back(target.at(i));
    }
  }

  return result;
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

std::string generateOAuthSignature
(std::string method, std::string endpoint, param parameter, std::string consumer_key_secret, std::string access_token_secret)
{
  std::string signature;

  // 署名作成に使えるキーを組み立てる
  auto buildOAuthKey = [=](){
    std::string key = "";
    key += encodeToURL(consumer_key_secret);
    key += "&";
    key += encodeToURL(access_token_secret);

    return key;
  };

  // すべてのパラメータを繋げる
  auto connectParameter = [=](){
    std::string connected = "";
    for(auto itr : parameter){
      connected += itr;
      connected += "&";
    }
    connected.pop_back();
    connected = encodeToURL(connected);

    log("----");
    log(connected);
    log("----");

    return connected;
  };

  // 署名作成に使えるデータを組み立てる
  auto buildOAuthData = [=](){
    std::string data = "";

    data += encodeToURL(method);
    data += "&";
    data += encodeToURL(endpoint);
    data += "&";
    data += connectParameter();

    return data;
  };

  // SHA1に変換
  auto translateToSHA1 = [=](){
    std::string key_s = buildOAuthKey();
    std::string data_s = buildOAuthData();
    const char* key = key_s.c_str();
    const char* data = data_s.c_str();

    unsigned char res[SHA_DIGEST_LENGTH + 1];
    unsigned int res_len;
    size_t key_len = strlen(key);
    size_t data_len = strlen(data);

    log("------------");
    log("キー");
    log(key);
    log("データ");
    log(data);
    log("------------");

    HMAC(EVP_sha1(), key, key_len, reinterpret_cast<const unsigned char*>(data), data_len, res, &res_len);

    return std::string(reinterpret_cast<char*>(res), res_len);
  };

  // SHA1変換適用後の文字列にBase64エンコードを適用した文字列を返す
  signature = translateToSHA1();

  // Base64変換
  signature = encodeToBase64( signature );

  log("");
  log("完成した署名");
  log(signature);
  log("");

  return signature;
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

std::string getOAuthHeader(param parameter)
{
  std::string header;

  header = "Authorization: OAuth ";
  for(auto itr : parameter)
  {
    header += itr;
    header += ",";
  }
  header.pop_back();  // 最後に付けられた余分な「,」を削除

  return header;
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

static size_t CallBackWriteString
(char *ptr, size_t size, size_t nmemb, std::string *stream) {
  size_t dataLength = size * nmemb;
  stream->append(ptr, dataLength);
  std::string str = *stream;
  return dataLength;
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

// 引数として与えるURLはパラメータを含めて指定する
std::string sendRequest
(const std::string method, const std::string url)
{
  std::string endpoint = url.substr(0, url.find_first_of('?'));     // オプション以外のURL(エンドポイントのみ)を格納する
  param parameter;          // 署名以外の全パラメータを格納しておく(URLオプション含め)

  // ここに取得した4つのキーを入れる
  std::string consumer_key = "";          // API key
  std::string consumer_key_secret = "";   // API secret key
  std::string access_token = "";          // Access token
  std::string access_token_secret = "";   // Access token secret

  splitOptions(url, parameter);   // URLのオプションとして渡されていたパラメータをparameterに格納

  // URLとエンドポイント、オプションが望んだとおりになっているかどうかの確認
  log("入力されたurl--- " << url);
  log("抽出したエンドポイント--- " << endpoint);
  for(auto itr : parameter) { log("抽出したオプション--- " << itr); }

  // parameterに与えられたキーと値の組を追加する関数
  auto PushbackParameter = [&]( std::string key, std::string value){
    parameter.push_back( key + "=" + value );
  };

  // ランダムな文字列を返す関数
	auto CreateNonce = []() {
		static const char* chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
		char tmp[50];
		srand((unsigned int)std::time(0));
		int len = 15 + rand() % 16;
		for (int i = 0; i < len; i++) {
			tmp[i] = chars[rand() % (sizeof(chars) - 1)];
		}
		return std::string(tmp, len);
	};

  // parameterにキーと値の組を追加
  PushbackParameter("oauth_consumer_key", consumer_key);
  PushbackParameter("oauth_token", access_token);
  PushbackParameter("oauth_signature_method", "HMAC-SHA1");
  PushbackParameter("oauth_timestamp", std::to_string(time(NULL)));
  PushbackParameter("oauth_nonce", CreateNonce());
  PushbackParameter("oauth_version", "1.0");

  // パラメータをアルファベット順に並べ替える
  std::sort( parameter.begin(), parameter.end() );

  log("");
  log("ソート後のパラメータ");
  for(auto itr : parameter){ log(itr); }
  log("");

  // パラメータ、シークレットキー等から署名を作る
  std::string signature = generateOAuthSignature(method, endpoint, parameter, consumer_key_secret, access_token_secret);
  signature = encodeToURL(signature);

  // 署名をパラメータに追加する
  PushbackParameter("oauth_signature", signature);

  // Authorizationヘッダーを作る
  std::string oauth_header = getOAuthHeader(parameter);

  std::cout << std::endl;
  std::cout << "リクエストURL" << std::endl;
  std::cout << url << std::endl;

  std::cout << "OAuthヘッダー" << std::endl;
  std::cout << oauth_header << std::endl;
  std::cout << std::endl;

  CURL* curl;
  curl = curl_easy_init();

  curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
#if DEBUG_LEVEL >= 10
  curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);    // For Debug
#endif

  if (method == "GET") {
    std::cout << "リクエストメソッド = GET" << std::endl;
    curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
    curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "GET");
  } else if (method == "POST") {
    std::cout << "リクエストメソッド = POST" << std::endl;
    curl_easy_setopt(curl, CURLOPT_POST, 1L);
    curl_easy_setopt(curl, CURLOPT_URL, endpoint.c_str());
    curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, 0);
  }

  log("");
  log("Authorizationヘッダー");
  log(oauth_header);
  log("");

  // ヘッダ関係の設定
  struct curl_slist *headers = NULL;
  headers = curl_slist_append(headers, "Content-type:");
  headers = curl_slist_append(headers, oauth_header.c_str());
  curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);

  // コールバック関数の設定
  std::string result;   // HTTPリクエストの結果を格納する
  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CallBackWriteString);
  curl_easy_setopt(curl, CURLOPT_WRITEDATA, &result);

  curl_easy_perform(curl);
  curl_easy_cleanup(curl);

  return result;    // 返ってきたJSONファイルをstring型として返す
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

main.cpp

#include<iostream>
#include<fstream>
#include<string>

#include "Define.hpp"
#include "HTTPRequest.hpp"

int main()
{
  /* タイムライン、検索結果等を取得 */
  std::string req = sendRequest("GET", "https://api.twitter.com/1.1/statuses/user_timeline.json?screen_name=Akito_Dataminer&count=10");

  std::ofstream ofs("ouput.txt");
  if (!ofs)
  {
    goto ERROR;
  }

  // 返されたデータを出力
  ofs << req << std::endl;
  std::cout << req << std::endl;

  return 0;

ERROR:
  std::cerr << "ファイルが開けませんでした。" << std::endl;
  return -1;
}

こちらのプログラムをコンパイルして実行していただくと、exeファイルのあるフォルダにoutput.txtというファイルが新しく作られて、その中にjson形式のデータが書き込まれているかと思います。↓こんな感じに。

[{"created_at":"Wed May 27 23:10:06 +0000 2020","id":1265782360882270215,"id_str":"1265782360882270215","text":"\u2026

・・・

"favorite_count":1,"favorited":false,"retweeted":false,"lang":"ja"}]

jsonファイルを整形してくれるサイトを見つけたので、そのリンクを貼っておきます。

JSON Pretty Linter – JSONの整形と構文チェック

こちらを利用すれば、多少は分かりやすくなるのではないでしょうか。

軽く備考

main.cppにある

sendRequest("GET", "https://api.twitter.com/1.1/statuses/user_timeline.json?screen_name=Akito_Dataminer&count=10");

の引数の部分を色々と変えてやると、それに応じた情報が取得できるのではないかと思います(これ以外の引数は試してないので確証はありませんが)。

それと、このプログラムを作るに当たって、下記サイトを参考にさせていただきました(アクセス確認:2020/6/2)。下記サイトの作成者様、ありがとうございました!

もし、僕の記事を読んで興味を持った方や、もうちょっと詳しく知りたいなって方がいらっしゃったら、合わせて参考にしてみてください~。

※このプログラムの利用によって発生した損害の責任は負いかねますので、プログラムの利用、実行等は必ず自己責任で行ってください。

タイトルとURLをコピーしました