こまぶろ

技術のこととか仕事のこととか。

Node.jsからConnpassのAPIを叩こうとしてつまずいたこと

はじめに

この記事をご覧になっている方の多くは、技術系の勉強会に参加された経験があるのではないかと思います。僕はよく参加しています。

他の業界の友人と話をして、最も驚かれるのが「業後に勉強会に出ている」ということです。毎日のようにどこかで勉強会が開かれ、様々な企業の人が職業に関わる情報をやりとりしているのはこの業界の特色でしょう。

さて、そんな勉強会の情報は、いくつかのイベント情報共有サービスを通じて周知されているものがほとんどだと思います。ConnpassやDoorkeeper、ATNDなどが有名どころでしょうか。それらのサイトを訪問すれば、日々新しい勉強会の開催情報が公開されているのを見ることができます。

しかし、サイトを訪問したり、公式のメール配信に登録する以外にも、勉強会の情報を得る方法があります。それはAPIを利用することです。APIを利用することで、特定の条件を満たすイベントの情報を自動で取得でき、様々なツール(SlackやTwitter)に送信することもできます。

そこで今回は、

  • ConnpassのAPIを利用する手順
  • 利用するに当たってつまずいたこと

についてご紹介します。

Connpass APIは登録なしですぐ使える

今回利用するのは、ConnpassのAPIです。無料で公開されています。

WebサービスAPIの中には、登録が必要なものもありますが、Connpassの場合は利用登録は必要ありません。指定されたURL( https://connpass.com/api/v1/event/)に対してHTTPリクエスト(GET)を送信するだけで、イベント情報を取得することができます。

Connpass APIでイベントの情報を取得するサンプルコード

実際に叩いてみましょう。以下のような実装で取得することができます。まだ途中ですが、Lambdaで定期実行するようにしたいので、Lambda向きの書き方になっています。

const https = require('https');
const queryString = require('querystring');

const getResponse = async (opts, queries)=>{
  opts.path +='?' + queryString.stringify(queries);  // クエリ文字列を生成してパスの末尾に付加
  console.log('path:', opts.path);
  return new Promise((resolve, reject)=>{  // httpsモジュール、Promiseについては前回記事(下記)を参照
    https.get(opts, (response)=>{
      console.log('statusCode:', response.statusCode);
      console.log('statusMessage:', response.statusMessage);
      response.setEncoding('utf8'); // Buffer型ではなくUTF-8の文字列で取得する
      let chunkCount = 0;
      response.on('data', (chunk)=>{
        chunkCount ++;
        body += chunk;
      });
      response.on('end', ()=>{
        console.log('チャンクの個数',chunkCount);
        let bodyObj = JSON.parse(body); // レスポンスはJSON形式で返ってくるのでパースする
        console.log('検索結果件数:', bodyObj.results_available);
        console.log('うち取得件数:', bodyObj.results_returned);
        console.log('1つめのイベント:', bodyObj.events[0].title);
        resolve(bodyObj);
      });
    }).on('error', (err)=>{
      console.log('error:', err);
      reject(err);
    });
  });
};

exports.handler = async ()=>{
  let opts = {
      hostname: 'connpass.com',
      path: '/api/v1/event/',
      headers: {
        'User-Agent': 'Node/8.10'  // これがないと拒否される
      }
  };
  const queries = {
    'keyword_or': [
      'アジャイル',
      'javascript'
    ]
  };
  let result = await getResponse(opts, queries);
  return result;
};

参考(httpsモジュールによるリクエストの送信)

ky-yk-d.hatenablog.com

つまずきポイント①403エラーとUser-Agentヘッダー

今回、ConnpassのAPIを利用しようとして、最初につまずいたのは、403 Forbiddenになってしまうという事象でした。Web APIを利用した経験がほぼゼロであるため、解決方法が分からずしばらく苦しみましたが、以下の記事に行き着きました。

上の記事は、GitHubAPIへのリクエストが403 Forbiddenとなっていたのを、User-Agentヘッダーを付与することで解決したというものです。User-Agentについては、以下の記事が端的でわかりやすかったです。リクエストを送信したのが何者なのかをサーバ側に教えてあげるためのものというわけですね。

www.atmarkit.co.jp

上記のQiita記事を読んだときに、「たぶんこれだ」と直感し、https.request()の第一引数のoptionsに、User-Agentヘッダーを付加して送信するように指定してあげました。すると思った通り、200 OKが返ってくるようになったではありませんか。Web APIを叩くときは要注意ですね。

なお、Node.jsからの実行なので、試しにNode/8.10と記載してみましたが、書き方をご存知の方がいらっしゃればご教示いただけると幸いです。

修正前

let opts = {
    hostname: 'connpass.com',
    path: '/api/v1/event/'
};

修正後

let opts = {
    hostname: 'connpass.com',
    path: '/api/v1/event/',
    //  ---以下を付加---
    headers: {
        'User-Agent': 'Node/8.10'
    }
    // ---追加部分終わり---
};

つまずきポイント②日本語のクエリ文字列のエンコード

つまずいた第二のポイントは、日本語のクエリ文字列のエンコードです。

ConnpassのAPIは、検索条件をクエリ文字列としてリクエストに付加して利用するのですが、そのなかの検索ワードは文字列で送信することになります。英数字であれば、何も考えずに文字列として付加して送信すればよいのですが、日本語文字列を何も考えずに送信すると失敗します200 OKにはなりますが、検索文字列として機能しないので有意味なレスポンス本文が返ってきません。

1. 何も考えずに日本語文字列を送る例

最初、このようなやり方をしていて首を傾げていました。日本語文字列がそのままパスに含められて送信されています。①と違って、HTTPリクエストとしてエラーになるわけではありませんが、レスポンス本文が取得できていないため、本文から検索結果の情報を取得するところでエラーとなります。

HTTPによる通信に慣れている人であれば絶対につまずかない箇所なのだと思いますが、生成したクエリ文字列を含むパスをChromeのURL入力欄に放り込むとちゃんと検索結果が(エンコードされていない状態で)得られるので、自分が間違ったことをしていることに気づきませんでした。

ソースコード(一部)

opts.path +='?' + 'keyword=アジャイル';

実行結果

2018-07-22T13:30:53.242Z 74a3205b-8db3-11e8-80e4-430677743f08    path: /api/v1/event/?keyword=アジャイル
2018-07-22T13:30:53.547Z    74a3205b-8db3-11e8-80e4-430677743f08    statusCode: 200
2018-07-22T13:30:53.547Z    74a3205b-8db3-11e8-80e4-430677743f08    statusMessage: OK
2018-07-22T13:30:53.550Z    74a3205b-8db3-11e8-80e4-430677743f08    チャンクの個数 1
2018-07-22T13:30:53.586Z    74a3205b-8db3-11e8-80e4-430677743f08    検索結果件数: 0
2018-07-22T13:30:53.586Z    74a3205b-8db3-11e8-80e4-430677743f08    うち取得件数: 0
2018-07-22T13:30:53.587Z    74a3205b-8db3-11e8-80e4-430677743f08    TypeError: Cannot read property 'title' of undefined
    at IncomingMessage.response.on (/var/task/index.js:24:52)
    at emitNone (events.js:111:20)
    at IncomingMessage.emit (events.js:208:7)
    at endReadableNT (_stream_readable.js:1064:12)
    at _combinedTickCallback (internal/process/next_tick.js:138:11)
    at process._tickDomainCallback (internal/process/next_tick.js:218:9)

2. 自分でエンコードして送る例

日本語文字列を含める場合は、エンコードが必要になるということでした。そのための関数はちゃんと用意されていて、下記のようなやり方で適切な形に変換することができました。実行結果を見ると、きちんと日本語文字列がエンコーディングされて送信されていることがわかります。結果もちゃんと返ってきています。

ソースコード(一部)

opts.path +='?' + 'keyword='+ encodeURIComponent('アジャイル');

実行結果

2018-07-22T13:31:57.688Z 9b0f3f00-8db3-11e8-ab20-c3947964f9d8    path: /api/v1/event/?keyword=%E3%82%A2%E3%82%B8%E3%83%A3%E3%82%A4%E3%83%AB
2018-07-22T13:31:59.441Z    9b0f3f00-8db3-11e8-ab20-c3947964f9d8    statusCode: 200
2018-07-22T13:31:59.446Z    9b0f3f00-8db3-11e8-ab20-c3947964f9d8    statusMessage: OK
2018-07-22T13:31:59.468Z    9b0f3f00-8db3-11e8-ab20-c3947964f9d8    チャンクの個数 8
2018-07-22T13:31:59.487Z    9b0f3f00-8db3-11e8-ab20-c3947964f9d8    検索結果件数: 775
2018-07-22T13:31:59.487Z    9b0f3f00-8db3-11e8-ab20-c3947964f9d8    うち取得件数: 10
2018-07-22T13:31:59.487Z    9b0f3f00-8db3-11e8-ab20-c3947964f9d8    1つめのイベント: 第2回enPiT-Proスマートエスイーセミナー: アジャイル品質保証と組織変革

3. querystringモジュールを利用する例

理屈の上では、2のようなやり方で足りますが、クエリ文字列の数が増えてくるとめんどくさそうです。エンコーディングについても、あまり意識したくはありません。これらの悩みを解決してくれるのが、Node.jsのモジュールであるquerystringモジュールです。

今回は、querystring.stringify()メソッドを利用しています。このメソッドでは、

  • オブジェクトからクエリ文字列への変換
  • パーセントエンコーディング(デフォルト。第四引数として関数を渡すことで変更することも可能)

を行ってくれます。また、下記の例では一つだけですが、keyword': ['aaa', 'bbb']と配列で指定してやると自動的に展開していい感じに処理してくれます。優れものですね。

ソースコード(一部)

const queryString = require('querystring');
let queries = {
    keyword: 'アジャイル'
};
opts.path +='?' + queryString.stringify(queries);

実行結果

2018-07-22T13:35:03.055Z 097210d9-8db4-11e8-a137-69efa6eedcfd    path: /api/v1/event/?keyword=%E3%82%A2%E3%82%B8%E3%83%A3%E3%82%A4%E3%83%AB
2018-07-22T13:35:03.468Z    097210d9-8db4-11e8-a137-69efa6eedcfd    statusCode: 200
2018-07-22T13:35:03.486Z    097210d9-8db4-11e8-a137-69efa6eedcfd    statusMessage: OK
2018-07-22T13:35:03.508Z    097210d9-8db4-11e8-a137-69efa6eedcfd    チャンクの個数 9
2018-07-22T13:35:03.527Z    097210d9-8db4-11e8-a137-69efa6eedcfd    検索結果件数: 775
2018-07-22T13:35:03.527Z    097210d9-8db4-11e8-a137-69efa6eedcfd    うち取得件数: 10
2018-07-22T13:35:03.527Z    097210d9-8db4-11e8-a137-69efa6eedcfd    1つめのイベント: 第2回enPiT-Proスマートエスイーセミナー: アジャイル品質保証と組織変革

おわりに

以上、ConnpassのAPIの叩き方のサンプルとつまずきポイントをご紹介しました。AWSAPI GatewayとLambdaを用いてAPIを作り、それをコールするというのは何度かやったことがありましたが、他人が作成したAPIを叩くというのは初めてだったので、思いの外つまずき、そして勉強することがありました。

現在、ConnpassのAPIを使ったボットを作成しようとしています。時間の使い方が下手でなかなか作業が進められておらず、まだ動くものになっていませんが、ソースはGitHubで公開していますので、ご興味ある方は眺めてみてください。

github.com