Wowgineer Notes

WowTechエンジニアの徒然開発日記

Wowgineer Notes

〜新たな"Wow"を生み出す〜

1週間でヘルプボット作って社内リリースしたお話

こんにちは。WowTalkのAndroid開発担当している岡野です。

今回はAndroidと全く関係ないのですが、AWSのサービスを使ってWowTalkのユーザサポート用のボットを作って1週間で社内リリースした話を書いていこうと思います。

1. なぜ作ろうと思ったのか

WowTalkには電話やメールでのサポート窓口というものがあるですが、ここに来る問い合わせ件数を減らせないかというのがそもそもの始まりでした。

WowTalkには操作説明が載っているユーザーサイトもあるのですが、せっかくWowTalkには自分でカスタマイズできるボット機能と、WowTalkにメッセージを送信できるOpenAPIもあるのでそれをうまく利用したいと考えました。

 

f:id:okano4413:20190723154630p:plain

今回考えたアーキテクチャの概要

今回は検証も兼ねて、社内からの問い合わせに対応できるよう社内公開しましたが、最終的にはユーザの皆様に公開し、利用していただけるようなボットを提供できればと考えています。

2.開発環境

Lambda : node.js 8.10 

ServerlessFramework : 1.40 

3. サーバー立てずに実装したい

WowTalkは裏側でAWSを使っているのですが、今回は機能上EC2でインスタンスを立てる必要もないと判断したためAPIGateWayとLambdaを使うことにしました。

ただしインスタンスを立てないことに伴いLambdaの設定を工夫する必要がありました。

4. IPアドレスがない問題

WowTalkのOpenAPIの利用にはセキュアアクセスのためアクセス元にIPアドレスが必要ですが、LambdaはサーバーレスなFaaSサービスのためIPアドレスがありません。

ではどうするかというと、OpenAPIと同じサブネットにLambdaを設置することで対応しました。OpenAPIは社内で検証を行うことがしばしばあるので、同じサービスネットワーク内からのアクセスは許可されています。なのでLambda関数のVPC設定から、WowTalkのインスタンス群と同じサブネットに配置することでOpenAPIの呼び出しを行えるようにしました。

 

f:id:okano4413:20190723144448p:plain

Lambda関数のコンソールからVPCの設定をする

5.Lamdaから外部へアクセスできない問題

ただこれだけでは不十分で、今度はLambdaからユーザーサイトにアクセスできなくなりました。上の画像の注意書きにもありますが、NAT GateWayを設定していないため外部ネットワークとのアクセスができず、ドメイン解決ができなくなるので当然と言えば当然なのですが(汗) 

これはRoute53のプライベートホストゾーン設定にユーザーサイトへアクセスするためのレコードセットを設定することで解決しました。OpenAPIのサーバーへのアクセスも同様に通常のドメイン設定ではアクセスできなくなったので合わせて設定することで対応しました。

6.回答しよう

ここまでできたらあとは裏側の処理をnode.jsでガリガリ書くだけです。まずはシンプルにキーワードに関連する記事のURLをボットのそれっぽく返却するだけにしました。

'use strict';
const crypto = require('crypto');
const URLSafeBase64 = require('urlsafe-base64'); 
const Buffertrim = require('buffertrim'); 
const https = require("request");
const Config = require('./config');
var util = require('util');

var configs;

module.exports.handleMessage = async (event, context, callback) => {
    const body = event['body'];
    const alias = event['stageVariables']['alias'];//APIGateWayから渡される環境変数
    configs = new Config(alias).getConfigs();//環境ごとに設定を変更
    if (body != null) {
        var json = getJsonFromBody(body);
        const userId = json['source']['userId'];
        const content = json['message']['content'];
        const companyId = json['source']['companyId']
        getURLFromKeyWord(content, function(answer){
            sendMessageBuddy(configs.BOT_ID, configs.BOT_PASSWORD, userId, companyId, answer);
        }); 
    }
    callback();
};

function getJsonFromBody(body) {
       /**   暗号化解析処理 (省略)  **/
}

// ユーザーサイトからキーワードが該当する記事を取得する
function getURLFromKeyWord(keyword, callback) {
    var encodedKeyWord = encodeURI(keyword);
    var opt = {
        url: configs.SUPPORT_URL,
        headers: {
            "Accept" : "application/json"
        },
        qs : {
            search : encodedKeyWord,
            per_page : 100
        },
        qsStringifyOptions : {
            encode : false
        },
        rejectUnauthorized : false
    }
    https.get(opt, function(error, response, body) {
        if (body != null) {
            var JSONBody = JSON.parse(body);
            var answers = []; //回答メッセージの配列
            var answer;
            if (body.length == 0 || body == "[]") {
                answer = configs.HINT_TEXT_FOR_NO_RESULT;
            } else {
                //ここで回答本文作成処理。
                answer = util.format(configs.HINT_TEXT_FOR_RESULT, keyword);
                for (var i=0; i<JSONBody.length; i++) {
                    var val = JSONBody[i];
                    var title = val["title"]["rendered"];
                    var link = val["link"];

                    /** WowTalkに送れる文字数は800文字の文字制限があるため、
                     *  回答が制限を超える場合は別のメッセージとして送信するようにする。
                     **/

                    //判定用文字列
                    var textAfterJoin = answer + title + "\n" + link + "\n";
                    if (answer.length < configs.MAX_LENGTH 
                        && textAfterJoin.length >= configs.MAX_LENGTH) {
                        answers.push(answer);
                        answer = util.format(configs.HINT_TEXT_FOR_RESULT, keyword);
                    }
                    if (link != null) {
                        answer += title + "\n" + link + "\n";
                    }
                }
            }
            answers.push(answer);
            for (var j=0; j< answers.length ; j++) {
                var ans = answers[j];
                callback(ans);
            }
        } else if (error != null) {
            callback(configs.HINT_TEXT_FOR_REQUEST_ERROR);
        }
    });
}

//OpenAPIにリクエスト
function sendMessageBuddy(fromAccount, fromPassword, toAccount, companyId, content) {
    const message = {
        from_account : fromAccount,
        from_account_password: fromPassword,
        to_account : toAccount,
        message_content: content
    };
    
    const messageJsonString = JSON.stringify(message);
    
    /**   回答の暗号化処理 (省略)  **/
    
    var opt = {
        uri: configs.OPENAPI_URL,
        method: 'POST',
        headers:{
            "Content-type": "application/x-www-form-urlencoded",
        },
        form: {
            "company": companyId,
            "request_id": "req123",
            "language":"jp",
            "request_data":"暗号化された回答"
        }
    }
    https.post(opt,function(error,response,body){
        /** Open APIにリクエストを投げた後の処理 **/
    });
    
}

 

こうして記事のURLを返却するボットを無事(?)社内リリースしました。

f:id:okano4413:20190723145627p:plain

7. これからのお話

ただこれでは通常の質問形式や複数キーワードで検索をかけようとした時に、WP REST APIでは投稿が引っかからないので正直ほとんど使い物になりませんでした orz

f:id:okano4413:20190723150643p:plain

というわけで「投げられたメッセージをキーワード解析しながらWP REST APIへの検索方法も改善しないと使い物にならないな〜」となりました。そちらについてはまた今後追記していきたいと思います。 

8. おわりに

今回の記事では(ボットのクオリティはさておき)お手製のサポートボットをリリースするまでを簡単に追いました。

タイトルにもある通り、AWSのサーバーレスアーキテクチャを利用すると、アーキテクチャを考えてから社内リリースするまで1週間という短期間で実装ができました。EC2でインスタンスを立てると利用していないリソースにまで料金が発生したり負荷が高くなった時のスケーリングとかリソースの配分なども考えないといけませんが、そこを考慮しなくて済むのは魅力的です。

Androidアプリ開発する傍ら、サービスがもっと使いやすくなるような機能の模索もしていきたいと思います。

 

参考URL

AWS関連
Serverless関連

 

我々ワウテック技術開発部はこのような環境で開発をしています。
興味を持った方がいらっしゃればいつでもご連絡下さい。

f:id:wowtech-dev:20190313163146j:plain