ChatWork Creator's Note

ビジネスチャット「チャットワーク」のエンジニアとデザイナーのブログです。

AWS DevDay Challengeに参加してきました(チーム吹田)

こんにちはー

藤井(@yoshiyoshifujii) ですー。

以下の記事に引き続きまして、

creators-note.chatwork.com

AWS DevDay Challenge - 2018/11/2 (金) に参加させていただきましたので、レポートさせていただきます。

DevDay Challengeは、3名一組のチームで行う開発コンテストです。当日、運営から与えられるテーマの中から1つを選び、開発に取り組んで頂きます。イベントの最後に発表をして頂き、優秀チームを選出します。優秀チームには副賞も用意しています。 3名でのチーム対抗戦が基本ですが、一人で参加というのもアリです。仲間を誘ってチームでエントリーして頂くことも可能、1人でエントリーして、当日発表のチームで参戦という形も選べます。

とのことで、今回、 cw-hayashi と、 cw-adachi と、 私の3名で 「ChatWork吹田チーム」 を結成して挑みました。

テーマ

今回、運営から与えられたテーマは、5つ。

  1. Chat
  2. ChatBot
  3. SNS
  4. 動画配信
  5. 自由

私たち、ChatWorkですから、Chatを選択して圧倒的に有利になれるところ、あえて、ここは、ChatBotを選択するぞ!!と、迷わずChatBot一択で臨みましたw

初期設計

ChatBotを、chatworkの OAuth と、 Webhook を使って、Serverlessに作ろうということをベースにして設計しました。

f:id:yoshiyoshifujii:20181102104357j:plain

Amazon API Gateway を置いて、 AWS Lambda で、chatworkのOAuth認可を発行するためのフローを実現するLambdaを2つと、認可されたアカウントのWebhookを受けるLambdaを1つ、計3つのLambdaを作ればイケそうだねーとなりました。

また、上記のServerlessであれば、 AWS Code Star を使えば、CD/CIするための AWS CodeCommitAWS CodeBuildAWS CodePipeline 、のCode3兄弟を簡単に構築できますし、CloudWatchのメトリクスをモニターできるあたりまで、一気に作って利用できます。

実際のプロダクトになってくると、自由度が低くなるため、使いどころは限定されますが、今回のようなHackathonや、個人プロジェクトとかであれば、かなり実用的で簡略化でき即座に開発にアプローチできて、細かいことを考えずにCD/CIされて、とても良いと思いました。めちゃ便利です。

ということで、以下のような構成でいくことになりました。

f:id:yoshiyoshifujii:20181105103059p:plain

実装

chatworkのドキュメント OAuth と、 Webhook を参照し実装しました。

GET /admin

最初にリクエストして認可コードの取得を依頼するためのボタンを表示する初期ページのエンドポイントになります。

'use strict';

var fs = require('fs');
var path = require('path');

exports.get = function(event, context, callback) {
  var url = "https://www.chatwork.com/packages/oauth2/login.php"
    + "?response_type=code"
    + "&redirect_uri=https://<API_GATEWAY_ID>.execute-api.<REGION>.amazonaws.com/Prod/admin/callback"
    + "&client_id=<CW_CLIENT_ID>"
    + "&scope=offline_access+rooms.all:read_write+users.profile.me:read"
    + "&state=<CW_STATE>";

  var contentsTemplate = fs.readFileSync(`public${path.sep}admin${path.sep}index.html`).toString();
  var contents = contentsTemplate.replace("{{consent_page_url}}", url);
  var result = {
    statusCode: 200,
    body: contents,
    headers: {'content-type': 'text/html'}
  };

  callback(null, result);
};

GET /admin/callback

上記の /admin画面からボタンを押下して、chatworkのコンセント画面を表示し、認可を許可するボタンを押下した後、chatworkからリダイレクト用のResponseが返ってきて、ブラウザでリダイレクトした際にアクセスされるエンドポイントになります。

その際、リクエスト・パラメータに、これまたchatworkのドキュメントに記載されておりますパラメータが付与されますので、それに対応する必要があります。

また、そのパラメータを取得したら、その1リクエストの中で、アクセストークンの発行と、自身のアカウント情報を取得するといった一連の処理が必要となります。

さらに次への仕掛けとして、Webhookの枠を用意しておく必要があり、Webhook用のIDも払出したりと、このLambdaでWebhookの準備を整えていきます。

以下のようにして、アクセストークンの発行を依頼します。

    const clientId = <CW_CLIENT_ID>;
    const clientSecret = <CW_CLIENT_SECRET>;
    const redirectUri = 'https://<API_GATEWAY_ID>.execute-api.<REGION>.amazonaws.com/Prod/admin/callback';
    const grantType = 'authorization_code';

    // Get access token
    return axios.post('https://oauth.chatwork.com/token', querystring.stringify({
        grant_type: grantType,
        code: code,
        redirect_uri: redirectUri
    }), {
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
            'Authorization': 'Basic ' + createClientID(clientId, clientSecret),
        }

うまく発行されたら、chatworkのAPIのGET /me を呼んでアカウント情報を取得します。

    }).then(response => {
        // GET /me
        console.log(response);

        const accessToken = response.data['access_token'];
        const refreshToken = response.data['refresh_token'];

        return axios.get('https://api.chatwork.com/v2/me', {
            headers: {
                'Authorization': 'Bearer ' + accessToken,
            }

このあたりまで来たら、あとは、取得した情報をDynamoDBに保存したり、Webhook用のURLを払出して、画面を表示するという流れにしました。

POST /webhook/{id}

Webhookをchatworkに設定したら、このエンドポイントにリクエストが飛んでくるようになります。

PathパラメータのIDから、DynamoDBにアクセスして、登録済みのWebhookであれば、処理するようにします。

また、リクエストボディから、発言者のchatworkアカウントIDを取り出して、自分で発言した内容ではないよねとか確認しておきます。これを実装しておかないと、自分の発言を延々ループして返信し続けることになります。

あとは、 リクエストの署名検証 とかも必要です。これを実装しておかないと、不正な攻撃にさらされることになります。

  getAccount(suita_id, function(data) {
    const sender_id = body.webhook_event.account_id;
    excludeMyMessage(my_id, sender_id, function() {
      const cw_account_id = data.Items.filter(a => a.cw_account_id !== undefined)[0].cw_account_id.S;
      if (sender_id === Number(cw_account_id)) {

        const room_id = body.webhook_event.room_id;

        invoke(body.webhook_event.body, (data) => {
          chatwork({
            roomId: room_id,
            token: api_token,
            message: JSON.parse(data.Payload).body.TranslatedText
          });

          var result = {
            statusCode: 200,
            headers: {'content-type': 'application/json'}
          };

          callback(null, result);
        });
      }
    });
  });

invokeしているところは、別のLambdaを呼び出しており、Lambdaの結果をchatworkのAPIのメッセージ部に渡しております。

この別のLambdaは、内部で Amazon Translate のPython SDKを使って翻訳してレスポンスするという動きにしています。

Translateは、JavaScriptのSDKが無いため、今回、苦肉の策で、別Lambdaを作って、そのLambdaをPythonで実行させて、SDKを使って翻訳して返却するという1層かますことにしました。

工夫したところ

以下3点を工夫しました。

  1. chatworkを使う
  2. AWS CodeStarで環境一気に立ち上げる
  3. AWS X-Rayを使って分散トレーシング

なんせ、我々はチャットワークですから、chatworkを使います!何か絡めようと前もって考えておりましたが、がっつりメインで使えて良かったです!

上のほーでも書きましたが、CodeStarは、ほんと便利です。今後、個人プロジェクトとかで積極的に使っていこうと思いました。

X-Rayは、最後のほうに何とか仕込んで見れるあたりまで確認した程度ですが、かなり簡単に組み込めて、状況が可視化できて良かったです。

発表

以下の発表スライドを作って発表しました。

cw-hayashi さん、発表ありがとうございました!

f:id:yoshiyoshifujii:20181102181228j:plain

cw-hayashi が壮大にスベって心配そうな cw-adachi と私。

まとめ

今回、このような機会に参加させていただき、AWSの運営の皆様および、留守を預かっていただきました皆々様に感謝申し上げます。ほんとうに貴重な体験と学びを得る機会となりました。ありがとうございます。

1日の短い時間を活用して、Serverless ArchitectureのWebアプリケーションを構築して、グロースさせるのに必要なスキルセットや設計力、割り切りや機能の発想など、とても刺激的でした。

今後、可能な限りこのような機会を増やしていって、さらなる成長の機会を創出していけたらと思いますし、他のメンバーが参加される際も多方面で協力していけるように振る舞っていきたいと思いました。

f:id:yoshiyoshifujii:20181102194027j:plain

以上です。