現在公開しているC言語でTwitter APIを扱うライブラリ「Twitter4C」にはリクエストトークンとアクセストークンの取得、そしてツイートの機能が付いています。 しかし、それだけではなんだか面白味に欠けるので、Twitter4CのマイナーアップデートのついでにUserStream APIにアクセスしてタイムラインを取得して表示するプログラムを作ってみたいと思います。

4/1追記:Github上に公開しました。Twitter公式認定は嘘ですが動作は本当です(笑) https://github.com/Plemling138/Twitter4C_UserStream

Outline

本来は通信とJSONパースを並行してやるところですが、今回は簡単にするために受信バッファを適当なタイミングでパース関数に渡して表示させています。再接続もしません。接続の切断?Ctrl+Cで(殴

Twitter4Cの変更

まずはUserStream APIに接続するための関数を作ります。Twitter4C_UpdateStatusをテンプレートとして、リクエスト部分を改変していく感じです。

UserStream APIのURLは https://userstream.twitter.com/2/user.json なので、ここに対してリクエストを投げます。メソッドがGETなので署名生成とリクエスト部分がごっそり変わってたりします。

    int Twitter_ConnectUserStream(struct Twitter_consumer_token *c,  struct Twitter_access_token *a)
    {
      char buf[BUF_SIZE] = {''};

      char oauth_signature_key[100] = {''};

      struct timeval tv;
      char tstamp[20] = {''};

      char nonce_tmp[20] = {''};
      char nonce[20] = {''};
      char nonce_urlenc[20] = {''};

      char auth_tmpmsg[300 + TWEET_MAX_LENGTH] = {''};//Temporary message for HMAC-SHA1
      char auth_encmsg[300 + (TWEET_MAX_LENGTH * ENCODED_CHAR_MARGIN)] = {''};//Temporary message for HMAC-SHA1(URL-Encoded)
      char auth_postmsg[350 + (TWEET_MAX_LENGTH * ENCODED_CHAR_MARGIN)] = {''};
      char encstatus[(TWEET_MAX_LENGTH * ENCODED_CHAR_MARGIN)] = {''};

      char postmsg[400 + (TWEET_MAX_LENGTH * ENCODED_CHAR_MARGIN)] = {''};//POST Header
      char reqheader[460 + (TWEET_MAX_LENGTH * ENCODED_CHAR_MARGIN)] = {''};//POST Header

      char hmacmsg[40] = {''};
      char b64msg[40] = {''};

      char b64urlenc[50] = {''};

      int i = 0;

      //Signature Key
      sprintf(oauth_signature_key, "%s&%s", c->consumer_secret, a->access_secret);

      //Get date, set as timestamp
      gettimeofday(&tv, NULL);
      sprintf(tstamp, "%ld", tv.tv_sec);

      //Set OAuth Nonce
      sprintf(nonce_tmp, "%ld", tv.tv_usec);
      base64_encode(nonce_tmp, strlen(nonce_tmp), nonce, 128);
      URLEncode(nonce, nonce_urlenc);

      //Generate OAuth Post message
      sprintf(auth_tmpmsg, "%s%s&%s%s&%s%s&%s%s&%s%s&%s%s", OAUTH_CONSKEY, c->consumer_key, OAUTH_NONCE, nonce_urlenc, OAUTH_SIGMETHOD, HMAC_SHA1, OAUTH_TSTAMP, tstamp,  OAUTH_TOKEN, a->access_token, OAUTH_VER, OAUTH_VER_NUM);
      URLEncode(auth_tmpmsg, auth_encmsg);
      sprintf(auth_postmsg, "%s&%s&%s", MSG_GET, USER_STREAM_ENCODED_URL, auth_encmsg);

      //Generate OAuth Signature
      hmac_sha1(oauth_signature_key, strlen(oauth_signature_key), auth_postmsg, strlen(auth_postmsg), hmacmsg);

      //Count Singnature length
      i=0;
      while(i<300) {
        if(hmacmsg[i] == 0 && hmacmsg[i+1] == 0 && hmacmsg[i+2] == 0) break;
        i++;
      }

      //Encode Signature text by BASE64, also URL Encode
      base64_encode(hmacmsg, i, b64msg, 128);
      URLEncode(b64msg, b64urlenc);

      //Generate POST Message
      sprintf(postmsg, "%s"%s", %s"%s", %s"%s", %s"%s", %s"%s", %s"%s", %s"%s"", OAUTH_CONSKEY, c->consumer_key, OAUTH_NONCE, nonce_urlenc,  OAUTH_SIGMETHOD, HMAC_SHA1, OAUTH_TSTAMP, tstamp, OAUTH_TOKEN, a->access_token, OAUTH_VER, OAUTH_VER_NUM, OAUTH_SIG, b64urlenc);

      sprintf(reqheader, "GET %s %srnHost: %srnAuthorization: OAuth %srnrn", USER_STREAM_URL, HTTP_VER, STREAM_HOSTNAME, postmsg);
      printf("%s", reqheader);

      //SSL Session
      SSL_send_and_recv((char *)STREAM_HOSTNAME, reqheader, buf);

      return 0;
    }

また、上のコードに関する定義をtwilib.hに追加します。

    #define MSG_GET "GET"
    #define STREAM_HOSTNAME "userstream.twitter.com"
    #define USER_STREAM_URL "https://userstream.twitter.com/2/user.json"
    #define USER_STREAM_ENCODED_URL "https%3A%2F%2Fuserstream.twitter.com%2F2%2Fuser.json"

続いてsession.cの改変に移ります。user.jsonに接続すると、ツイートのJSONファイルが切断するまで永遠に送りつけられてきます。ただし受信データは細切れなので、適当な区切り(改行コード)が含まれるまでバッファに追加し続けています。 また、セッションが切断されないように変な数字?が送られてきたりするので、10バイト以下の受信データは連結せずに捨てるような処理にしています。

      //Send Request
      if(SSL_write(ssl, (void *)send_buf, strlen((void *)send_buf)) == -1) {
        return -6;
      }

      //Get Response
      char *json_string = (char *)calloc(8000, sizeof(char));
      int total_len = 0;
      while((read_size = SSL_read(ssl, recv_buf, BUF_SIZE-1)) > 0) {
        recv_buf[read_size] = '';

        //通常のAPIアクセスの場合はコンティニュー
        if(strstr(hostname, HOSTNAME) != NULL) {
            continue;
        }

        //セッションキープのためのゴミは無視する
        if(read_size <= 10) continue;

        //JSONバッファに追加
        if((total_len + read_size) < 8000) {
          strcat(json_string, recv_buf);
          total_len += read_size;
        }
        else {
          printf("Buffer Overflowed, clearn");

          memset(json_string, '', 8000);
          total_len = 0;
          continue;
        }

        //JSONの区切りであればパースする
        if(strstr(recv_buf, "rn") != NULL) {
          parseJSON(json_string);

          memset(json_string, '', 8000);
          total_len = 0;
        }

      }

      //Close SSL Session
      ret = SSL_shutdown(ssl);
      if(ret != 0) {
        return -7;
      }
      close(sock);
      free(json_string);

区切り文字を検知するとJSONのパースに移りますが、もしバッファを超えるような場合は解析対象とせずにそのまま捨てるような処理になっています。経験値で言うと8KB程度あればだいたいのツイートは読めますが、JSONに付随してくる情報が多いとこれを超えることが割とあります。

引き続きJSONパース部分を見てみましょう。JSONのパースには「Parson」を使用しますので、適宜ダウンロードしてparson.cとparson.hをコピーしておいてください。今回は通常のツイートと何らかのアクション(特にFavorite)を表示させます。

    void parseJSON(char *buf) {
        JSON_Value *root = json_parse_string(buf);
        //rootが取得できなければ何もしない
        if(root == NULL) {
            return;
        }

        JSON_Object *tweet = json_value_get_object(root);

        //とりあえずCreated_atのないJSONは無視
        if(json_object_dotget_string(tweet, "created_at") == NULL) return;

        //Event通知の時
        if(json_object_dotget_string(tweet, "event") != NULL) {
            printf("**********n");
            printf("Action: %s by @%sn", json_object_dotget_string(tweet, "event"), json_object_dotget_string(tweet, "source.screen_name"));
            if(strcmp(json_object_dotget_string(tweet, "event"), "favorite") == 0)
                printf("Target tweet: %snfrom @%sn", json_object_dotget_string(tweet, "target_object.text"), json_object_dotget_string(tweet, "target.screen_name"));
            printf("**********nn");
            return;
        }

        //それ以外の普通のツイート
        printf("*** %s(@%s) ***n", json_object_dotget_string(tweet, "user.name"), json_object_dotget_string(tweet, "user.screen_name"));
        printf("%sn", json_object_dotget_string(tweet, "text"));
        printf("(%s)nn", json_object_dotget_string(tweet, "created_at"));

        json_value_free(root);
    }

最後にmain関数の変更を行います。引数のチェック部分とUpdateStatusの部分を以下のように変更します。

      if(argc != 1) {
        exit(0);
      }

      if(errcode = Twitter_ConnectUserStream(c, a), errcode < 0) {
        printf("Error occured: %dn", errcode);
        exit(0);
      }

ここまで変更すればひとまず完成です。あとはMakefileにparson.cを追加してビルドすれば、ユーザストリーミング対応のクライアントが完成します。

実際に動作させるとこんな感じになります(緑色の四角は私のサブ垢名)。この時はちょっと急いでバグフィックスしたせいで取りこぼしたりが起きたりしてますが、一応表示はうまくできているようです。

というわけで、Twitter4CでもUserStreamできたよ!っていうお話でした。ちゃんちゃん。