この記事はチャットボット Advent Calendar 2016の20日目の記事です。
http://qiita.com/advent-calendar/2016/chatbot
今回は、MS BotFrameWorkをLine対応する方法について、記述したいと思います。
MS BotFrameWorkは、Botを簡単に作成・公開するためのフレームワークです。
https://dev.botframework.com/
このフレームワークは、facebook MessangerやSlackなど、多様なチャットツールに対応しているのですが、Line対応は残念ながらされていません(そのうちされると思いますが)。
そこで、BotFrameWorkをWebサービス化することで、Line対応してみたいと思います。今回はBotのサンプルではHello World的扱いの、言われたことをおうむ返しする「おうむ返しBot」を作りたいと思います。
基盤としては、Azureを選択しました。Bot FrameWorkを動かす基盤としてはAzureを選択せざるを得ないので、Azureで統一しました。
また、今回のサンプルは、以下の点についても考慮しています。
- 非同期処理化を図る(参考:Azure Functionsを使って非同期処理のLINE BOTを作成する)
- BotFrameWorkを用いたWebサービスは、Node.jsを用いて高速化を図る(当初C#で記述したところ、想定以上に遅かった。。。)
実際のサービス化に向けて、上記のほかに考慮したほうが良い点や、技術検証の必要がある点などを、コメントして頂けると、筆者は泣いて喜びます(認証認可に関する課題は認識していますが、ほかにもあればぜひコメントをください)
では参りましょう。
0.事前準備
- 以下の二つのアカウントを用意します
- Line APIの利用アカウント(https://business.line.me/ja/services/bot)
- Microsoft Azureのトライアルアカウント(いくつかありますが、これがお勧めのようです)
- 次に、下記のツールをダウンロードします
- Visual Studio Community(https://www.visualstudio.com/ja/vs/community/)
1.システムイメージ
システムイメージはこちらになります
2.MS BotFrameworkを使用しておうむ返しBOTを実装し、リリースする
2.1.おうむ返しBOTを実装する
- Visual Studio Communityを開き、新規プロジェクトを作成します(ファイル→新規作成→プロジェクト)。この際、テンプレートは「JavaScript/Node.js/Blank Azure Node.js Web Application」を選択します。
- npmを使用して依存するライブラリをインストールします。今回は、「restify」と「botbuilder」をインストールします。
- 次に、BOTを実装します。下記のソースを、server.jsに貼り付けてください
var restify = require('restify');
var builder = require('botbuilder');
//=========================================================
// Bot Setup
//=========================================================
// Setup Restify Server
var server = restify.createServer();
server.listen(process.env.port || process.env.PORT || 3978, function () {
console.log('%s listening to %s', server.name, server.url);
});
// Create chat bot
var connector = new builder.ChatConnector({
appId: 'Your APPID',
appPassword: 'Your APP Password'
});
var bot = new builder.UniversalBot(connector);
server.post('/api/messages', connector.listen());
//=========================================================
// Bots Dialogs
//=========================================================
bot.dialog('/', new builder.IntentDialog()
.matches(/^hello/i, function (session) {
session.send("Hi there!");
})
.onDefault(function (session) {
session.send("You said: %s", session.message.text);
}));
2.2.Azure App Serviceにリリースする
- [ソリューションエクスプローラー]からプロジェクトを選択して右クリックし、[公開] を選択します。
- 発行先を選択します。Microsoft Azure App Service(A)を選択してください。
- AppService選択のウィンドウが開きますので、「新規発行」をクリックします
- AppService作成のウィンドウで各種情報を入力し、AppServiceを作成します
- API Apps の名前: デフォルトのままでもOKですが、後々管理しやすい名前を入力します。
- サブスクリプション: デフォルトのままでOK。
- リソースグループ: 判別しやすい名前を入力します。今回初めてAzureの環境を使用するのではなく、既存のものがあれば、既存で使用しているものを選択してもOK。
- AppServiceプラン: その横の [新規作成] をクリックして、別ウインドウで設定を行います。プラン作成後、AppServiceを作成します。
- AppServiceプランの構成ウィンドウで各種情報を入力し、AppServiceプランを作成します
- App Serviceプラン: デフォルトのままでOK。
- 場所:Japan East/West
- サイズ:無料でOK。
- 発行ウィンドウに遷移後発行ボタンを押下し、AppServiceにリリースします。
- 各種情報はデフォルトのままでOKです。
- 宛先URLは、Bot Directoryに対する登録時に使用するので、メモ帳等にコピーしておいてください。
2.3.Bot Directoryへ登録する
- 作成したBotを Bot Directoryに登録します(https://dev.botframework.com/)
- Bot Profileを入力します
- Name:AppService作成時に指定した名前を入力します。
- Bot handle:Nameで指定したAppServiceの名前を入力します。
- Description:本来はBOTの説明を入力する必要がありますが、今回はNameで入力したAppServiceの名前をそのまま入力しましょう。
- Configurationを入力します
- Messaging endpoint:AppService作成時に自動生成された宛先URLについて、httpをhttpsに、AppService名.azurewebsites.netの後に「/api/messages」をつけます
- Manage Microsoft App ID and passwordボタンを押下し、App IDとパスワードを生成します。生成したApp IDとパスワードは、後でBOTの設定・実装に使うので、メモ帳などに残しておきます。
- 登録条件を確認し、登録します。
- 登録したBOTをWeb Service化します
- My Botsから登録したBOTを選びます。
- Add Another ChannelからDirect Lineを追加します
- Addリンクを押下し、追加画面に遷移します
- Add new siteリンクを押下し、Channel名入力画面を表示します。AppService作成時に指定した名前を入力し、Doneボタンを押下します
- Configure Direct Line画面に遷移するので、Secret keysのshowリンクを押下してBOTへアクセスする際に使用するシークレットキーを表示し、メモ帳などにコピーして残します。
- web.configのappSettingsタグに以下の内容を追記してください。「Your BOT App ID」「Your BOT Password」は、BOT Directoryで取得した値を入力してください。
<add key="BOTFRAMEWORK_APPID" value="Your BOT App ID" />
<add key="BOTFRAMEWORK_APPSECRET" value="Your BOT Password" />
- server.jsの変更箇所は、以下の通りです。「Your APPID」「Your APP Password」は、BOT Directoryで取得した値を入力してください
var connector = new builder.ChatConnector({
appId: 'Your APPID',
appPassword: 'Your APP Password'
});
- 再発行時の手順は、上記設定変更後に以下の手順で行います
- 上記セッティングを変更した後、[ソリューションエクスプローラー]からプロジェクトを選択して右クリックし、[公開] を選択します。
- 各種情報の変更はせずに、公開ボタンを押下します。
3.Gateway/Dispatcherを実装する
3.1.Function Appを作成する
3.2.Gateway/Dispatcherを実装する
- 新規→Market Placeから検索→「Function App」を指定して検索→検索結果から「Function App」を選択します
- 作成ボタンを押下します
- 各種情報を入力し、Function Appを作成します
- アプリ名:管理しやすい任意の名前をつけます。
- サブスクリプション:デフォルトのままでOK。
- リソースグループ:「2.2.Azure App Serviceにリリースする」にて作成したリソースグループを指定。
- ホスティングプラン:デフォルトのままでOK。
- 場所:Japan East/Westを指定。
3.2.Gateway/Dispatcherを実装する
- Channel Access TokenをLineDevelopers画面( Line Business Centerから入る )から取得します。
- Azure PortalのTOPで「すべてのリソース」を選択し、3.1.で作成したFunction APPを選択します。
- Gatewayを実装します。(デフォルト名称:HttpTriggerCSharp1)
- 新しい関数ボタンを押下します。
- テンプレート「HttpTrigger-CSharp」を選択します。
- 開発メニューを選び、下記のソースを貼り付けて保存します
#r "Newtonsoft.Json"
using System.Net;
using System.Net.Http;
using Newtonsoft.Json;
using System.Threading.Tasks;
public static async Task<string> Run(HttpRequestMessage req, TraceWriter log)
{
log.Info("C# HTTP trigger function processed a request.");
// Get request body
dynamic data = await req.Content.ReadAsStringAsync();
return data;
}
- 統合メニューを選び、入出力の内容を下記内容に修正します
- トリガー(HTTP(req)):
- 許可されている HTTP メソッド:All methods
- モード:Standar
- 要求パラメーター名:req
- ルート テンプレート:未入力
- 承認レベル:Function
- 出力(Azure Queue Storage output ($return)):
- メッセージパラメータ名:$return
- キュー名:incoming-queue
- ストレージアカウント接続:AzureWebJobsDashboard
- Dispatcherを実装します。(デフォルト名称:QueueTriggerCSharp1)
- 新しい関数ボタンを押下します。
- テンプレート「QueueTrigger-CSharp」を選択します。
- 開発メニューを選び、下記のソースを貼り付けて保存します。
- BotのWebサービス化の際に取得した、BOTへアクセスする際に使用するシークレットキーをInnerClassForReplyLine
.
ReplyToLineのstring botAccessToken = "Your Bot SecretCd";の代入値を置き換えます。 - Line Developersから取得した、Channel Access TokenをInnerClassForReplyLine
.
ReplyToLineのstring lineChannelAccessToken = "Your Line Channel Access Token";の代入値を置き換えます。
#r "Newtonsoft.Json"
#r "System.Runtime.Serialization"
using System;
using System.IO;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using System.Diagnostics;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using Newtonsoft.Json;
public static void Run(string myQueueItem, TraceWriter log)
{
log.Info($"C# Queue trigger function processed: {myQueueItem}");
log.Info("キューから取り出した時刻:" + DateTime.Now.ToString());
EventList events = JsonConvert.DeserializeObject<eventlist>(myQueueItem);
foreach (Event eventItem in events.Events)
{
InnerClassForReplyLine.ReplyToLine(eventItem, log);
}
}
public class InnerClassForReplyLine
{
public static void ReplyToLine(Event item, TraceWriter log)
{
//ConversationIDの取得取得
//本来は、ユーザー単位に30分間は同じIDを維持する必要があるが、今回は便宜上呼ばれるたびに取得する
string urlForGetConvId = "https://directline.botframework.com/v3/directline/conversations";
string botAccessToken = "Your Bot SecretCd";
string authCd = "Bearer "+botAccessToken;
var result = InnerClassForReplyLine.PostRequest(urlForGetConvId, authCd, null, log);
log.Info("ConvId取得完了時刻:" + DateTime.Now.ToString());
Conversation conversation = JsonConvert.DeserializeObject<conversation>(result.Result);
//BotFWへのメッセージ送信
string urlForFW = "https://directline.botframework.com/v3/directline/conversations/"
+ conversation.ConversationId + "/activities";
//送信用オブジェクトの生成
SendMsgTextToFW msg = new SendMsgTextToFW();
msg.Type = "message";
msg.Text = item.Message.Text;
msg.From = new SendToFwFromId();
msg.From.Id = item.Source.UserId;
//Serializerを使ってオブジェクトをMemoryStream に書き込み
DataContractJsonSerializer jsonSer = new DataContractJsonSerializer(typeof(SendMsgTextToFW));
MemoryStream ms = new MemoryStream();
jsonSer.WriteObject(ms, msg);
ms.Position = 0;
// StreamReader で StringContent (Json) を生成
StreamReader sr = new StreamReader(ms);
StringContent content = new StringContent(sr.ReadToEnd(), System.Text.Encoding.UTF8,
"application/json");
// log.Info("FWへの送信内容:" + content.ReadAsStringAsync().Result);
log.Info("FWへの送信直前:" + DateTime.Now.ToString());
//FWにリクエストを送信
var resultForSendFW = InnerClassForReplyLine.PostRequest(urlForFW, authCd, content, log);
log.Info("FWへの送信結果:" + resultForSendFW.Result);
log.Info("FWへ送信完了時刻:" + DateTime.Now.ToString());
//BotFWからレスポンスの取得
//本来はWaterMarkの管理をしなければいけないが、今回は省略する
var resultGetFromFW = InnerClassForReplyLine.GetPostResult(urlForFW, authCd, log);
log.Info("FWからの結果取得内容:" + resultGetFromFW.Result);
log.Info("FWから結果取得時刻:" + DateTime.Now.ToString());
//FWからの取得結果を復元
ResponseFromFW responseFromFW = JsonConvert.DeserializeObject<responsefromfw>(resultGetFromFW.Result);
//Lineへのメッセージ送信
//Lineへのメッセージ送信用オブジェクトの生成
SendMsgHeaderToLine sendMsgHeaderToLine = new SendMsgHeaderToLine();
sendMsgHeaderToLine.ReplyToken = item.ReplyToken;
SendMsgBodyToLine sendMsgBodyToLine = new SendMsgBodyToLine();
sendMsgHeaderToLine.Messages = new SendMsgBodyToLine[1];
sendMsgBodyToLine.Type = "text";
sendMsgBodyToLine.Text = responseFromFW.Activities[responseFromFW.Activities.Length - 1].Text;
sendMsgHeaderToLine.Messages[0] = sendMsgBodyToLine;
string postUrlForSendline = "https://api.line.me/v2/bot/message/reply";
string lineChannelAccessToken = "Your Line Channel Access Token";
string authCdForSendLine = "Bearer {" + lineChannelAccessToken + "}";
//シリアライザーのインスタンス化
DataContractJsonSerializer jsonSerForSendLine
= new DataContractJsonSerializer(typeof(SendMsgHeaderToLine));
MemoryStream ms2 = new MemoryStream();
// Serializerを使ってオブジェクトをMemoryStream に書き込み
jsonSerForSendLine.WriteObject(ms2, sendMsgHeaderToLine);
ms2.Position = 0;
// StreamReader で StringContent (Json) を生成
StreamReader sr2 = new StreamReader(ms2);
StringContent contentForSendLine = new StringContent(sr2.ReadToEnd(), System.Text.Encoding.UTF8,
"application/json");
log.Info("Line送信直前時刻:" + DateTime.Now.ToString());
// log.Info("ラインへの送信内容:" + contentForSendLine.ReadAsStringAsync().Result);
var resultSendLine = PostRequest(postUrlForSendline, authCdForSendLine, contentForSendLine, log);
log.Info("ラインへの送信結果:" + resultSendLine.Result);
log.Info("Line送信完了時刻:" + DateTime.Now.ToString());
}
public async static Task<string> PostRequest(string url, string authCd, StringContent content,
TraceWriter log)
{
var client = new HttpClient();
client.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", authCd);
client.DefaultRequestHeaders.Add("contentType", "application/json");
var response = await client.PostAsync(url, content);
if (response.IsSuccessStatusCode)
{
log.Info("Post成功");
}
else
{
// 応答ステータスコードを表示します。
String failureMsg = "HTTP Status:" + response.StatusCode.ToString() + " – Reason: " +
response.ReasonPhrase;
log.Info(failureMsg);
}
string result = await response.Content.ReadAsStringAsync();
return result;
}
public async static Task<string> GetPostResult(string url, string authCd, TraceWriter log)
{
var client = new HttpClient();
client.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", authCd);
client.DefaultRequestHeaders.Add("contentType", "application/json");
var response = await client.GetAsync(url);
if (response.IsSuccessStatusCode)
{
log.Info("Get成功");
}
else
{
// 応答ステータスコードを表示します。
String failureMsg = "HTTP Status:" + response.StatusCode.ToString() + " – Reason: " +
response.ReasonPhrase;
log.Info(failureMsg);
}
string result = await response.Content.ReadAsStringAsync();
return result;
}
}
public class EventList
{
[JsonProperty(PropertyName = "events")]
public Event[] Events { get; set; }
}
public class Event
{
[JsonProperty(PropertyName = "type")]
public string Type { get; set; }
[JsonProperty(PropertyName = "replyToken")]
public string ReplyToken { get; set; }
[JsonProperty(PropertyName = "source")]
public Source Source { get; set; }
[JsonProperty(PropertyName = "timestamp")]
public string Timestamp { get; set; }
[JsonProperty(PropertyName = "message")]
public Message Message { get; set; }
}
public class Source
{
[JsonProperty(PropertyName = "userId")]
public string UserId { get; set; }
[JsonProperty(PropertyName = "type")]
public string Type { get; set; }
}
public class Message
{
[JsonProperty(PropertyName = "type")]
public string Type { get; set; }
[JsonProperty(PropertyName = "id")]
public string Id { get; set; }
[JsonProperty(PropertyName = "text")]
public string Text { get; set; }
}
public class Conversation
{
[JsonProperty(PropertyName = "conversationId")]
public string ConversationId { get; set; }
[JsonProperty(PropertyName = "token")]
public string Token { get; set; }
[JsonProperty(PropertyName = "expires_in")]
public string Expires_in { get; set; }
}
// BotFWにテキストデータMSGを送信する際のデータ
[DataContract]
public class SendMsgTextToFW
{
[DataMember(Name = "type")]
public string Type { get; set; }
[DataMember(Name = "from")]
public SendToFwFromId From { get; set; }
[DataMember(Name = "text")]
public string Text { get; set; }
}
// BotFWにMSGを送信する際に指定する送信元
//(ロジック上は使用しないが、BotFWからの受け取りにも使用)
[DataContract]
public class SendToFwFromId
{
[DataMember(Name = "id")]
public string Id { get; set; }
}
//BotFWからのレスポンスヘッダ
[DataContract]
public class ResponseFromFW
{
[DataMember(Name = "activities")]
public Activity[] Activities { get; set; }
[DataMember(Name = "watermark")]
public string Watermark { get; set; }
}
//BotFWからのレスポンスボディ
public class Activity
{
[DataMember(Name = "type")]
public string Type { get; set; }
[DataMember(Name = "channelId")]
public string ChannelId { get; set; }
[DataMember(Name = "conversation")]
public ConversationWhenReplyFromFW Conversation { get; set; }
[DataMember(Name = "id")]
public string Id { get; set; }
[DataMember(Name = "from")]
public SendToFwFromId From { get; set; }
[DataMember(Name = "text")]
public string Text { get; set; }
}
//BotFWからのレスポンス時に使用するConversation
//ConversationID取得時と異なるIFで戻ってくるために定義する
public class ConversationWhenReplyFromFW
{
[DataMember(Name = "id")]
public string Id { get; set; }
}
//LineにMSGを送信する際に使用するヘッダ
[DataContract]
public class SendMsgHeaderToLine
{
[DataMember(Name = "replyToken")]
public string ReplyToken { get; set; }
[DataMember(Name = "messages")]
public SendMsgBodyToLine[] Messages
{
get;
set;
}
}
//LineにMSGを送信する際に使用するBody
[DataContract]
public class SendMsgBodyToLine
{
[DataMember(Name = "type")]
public string Type { get; set; }
[DataMember(Name = "text")]
public string Text { get; set; }
}
- 統合メニューを選び、入出力の内容を下記内容に修正します
- トリガー(Azure Queue Storage trigger (myQueueItem)):
- メッセージパラメータ名:myQueueItem
- キュー名:incoming-queue
- ストレージアカウント接続:AzureWebJobsDashboard
- 3.2.で実装した、GateWay(HttpTriggerCSharp1)のURLをメモ帳等にコピーします。
- GateWayのURLを、LineDevelopers画面( Line Business Centerから入る )のWebHookURLに貼り付けます。
- 作成したBotを、自分のLineアプリから追加します
- Line@Manage(Line Business Centerから入る)から二次元バーコードを取得し、二次元バーコードを用いて追加します。
- トークからBotに話しかけてみて、自分の発言と同じリプライが戻ってくれば、成功です。
- 今回のプロトは、ConversationIDの管理(永続化、有効時間管理)、WaterMarkの管理(永続化)について省略していますので、次回はここも含んで対応したいと思います。
- BotFrameworkについて、使ってみたレベルの記事が多く、本格利用はこれからだと思っています。
- この記事をもとに、MS BotFrameworkを使ったLine対応BOTが一つでも生まれると、幸いに思います。
わかりやすく書かれています。ありがとうございます!
返信削除ちょうどこの記事が公開された数時間前にApp ServiceのほうでDirect Clientの.NET SDKを使用して同じようなことを実装しようとしていましたが、この記事を紹介され、こちらの方法のほうがキューとFunctionを採用しているのでより良いと思いました。
おかげさまで上記の手順に従い問題なくECHOができています。
(ただし、BOT自体はNode.JSじゃなくてC#で実装しています。)
2点ほどの問題点がありました。
1. QueueTrigger-CSharp1の
19行目の<EventList>
39行目の<Conversation>
77行目の<JsonConvert>
の部分が、<>がHTMLタグとして処理されているせいで非表示されてしまうので、単なるソースのコピペだとスクリプトコンパイルできないです。
2. 35行目の
string authCd = "Your Bot SecretCd";
の頭にBearerを入れる必要があります。そうしないとConversationIDの取得できず、POSTメッソドができないようなエラーが返されます。
e.g. string authCd = "Bearer <Your Bot SecretCd";
宜しくお願いします!
このコメントは投稿者によって削除されました。
削除Simon Nishiさま
削除コメント、ありがとうございます
早速対応いたしました
今後とも宜しくお願い致します