4版からの変更点:**https://apis.skype.com からhttps://skype.botframework.com を使うようにしました。**こうすることで、Skype以外のBotへの応用がかんたんになります(例えばFacebookに通知するなど)。また、mark downやタグを使ってよりリッチな見た目のメッセージを作成できるようになります。
**その過程で、msg.jsonの形式が少し変わりました。**注意してください。
概要
やりたいこと
GitHubへのプッシュ等のイベントをWebhookで送って、SkypeのBotにグループチャットで喋らせたい。
いろいろな方法一覧
- Hubot ←よく知らない。
- Sevabot ←僕の環境だとインストールでこけた。
- Microsoft Bot Framework + Azure Functions ←今回採用する方法。
Microsoft Bot Framework、Azure Functionsとは
- Microsoft Bot Frameworkは、Web上で動作するBotを作成できるサービス。
- Azure Functionsは、HTTPリクエストを受信したときに実行されるアプリを作れるサービス。
いいところ
- すべてWeb上で動くので、どんな環境でも同じように使える。
- 何もインストールしないので、インストールでこけることがない。
- Skypeを常に起動しておく必要がない。
- コマンドラインから喋らすことさえ可能。
- GitHub以外のWebhookやWebhook以外のHTTPリクエストも受信して喋らせることができる。
悪いところ
- Azureがよくわからんし、わかりづらい。
流れ
- Step 1: Botの作成
- Step 2: コマンドラインからBotに喋らせる(動作確認)。
- Step 3: Function Appを作る。これでWebhookを受信する。
- Step 4: Function AppからBotに喋らせる。
必要なもの
- Microsoftのアカウント
- クレジットカード ←今回の内容ではお金はかからないが、支払い方法は用意しないといけません。
Step 1: Botの作成
https://dev.botframework.com/ でBotを作ります。入力内容はこんな感じ:
- Bot handleの意味はよくわかりませんが、少なくともBotのページのURLで使われます。別になんでもいいですが、今後変更はできません。
- Messaging endpointは、Botに対してSkype上での発言を送る場合は必要ですが、今回はWebhookを受信し、グループチャットで発言するだけなので不要です。
- App ID、Passwordは大事です。今後使うので、ひとまず安全な場所に保管しましょう。
完了したら、グループチャットに追加できるよう設定しておきます:
その後、Add to Skypeをクリックして、Skypeのコンタクトに追加しておきます。
Step 2: コマンドラインからBotに喋らせる(動作確認)
このステップを飛ばしてもいいですが、Web上でデバッグするのは面倒なので、やっておくのをおすすめします。
準備
OAuth2 Tokenの取得
Botに発言させるためには、OAuth2 Tokenが必要です。
https://login.microsoftonline.com/common/oauth2/v2.0/token にPOSTを発行すれば取得できます。Power Shellのコマンドだと、
Invoke-RestMethod -Uri https://login.microsoftonline.com/common/oauth2/v2.0/token -Method Post -Body "client_id=<BotのApp ID>&client_secret=<Botのパスワード>&grant_type=client_credentials&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default"
です。Linuxなどの方は適宜読み替えてください。
ここで、<BotのApp ID>や<Botのパスワード>は、Step 1で保存したものです。
結果はJsonで帰ってきます。なので、変数に代入してやれば中身を取り出せます。このTokenの有効期限は1時間です。
$data = Invoke-RestMethod -Uri .......
Set-Content key.txt $data.access_token #Tokenをkey.txtというファイルに保存
Conversation ID
Skypeのチャットルーム?にはIDが振られており、それをConversation IDと呼びます。Botを喋らせたいグループチャットにBotを追加し、/get name
と打ち込みます。すると、name=12:34567890abcdef1234567890abcdef12@thread.skypeのようなConversation IDが帰ってきます。太字の部分全部、すなわち、@thread.skypeまで含んだ部分がConversation IDです。
発言内容のJsonを作っておく
{
type: "message",
text: "こんにちは :D"
}
type
には"message"
を指定します。text
には好きなメッセージを入力しましょう。日本語を含める場合は、UTF-8で保存してください。
ファイルの名前はなんでもいいですが、msg.jsonとでもしておきましょう。
喋らせてみる
https://skype.botframework.com/v3/conversations/<Conversation ID>/activities にPOSTを発行すればBotが喋ります。
Power Shellだとこんな感じ:
$headers = @{
"Authorization" = "Bearer <先程取得したOAuth2 Token>"
}
Invoke-RestMethod -Uri https://skype.botframework.com/v3/conversations/<Conversation ID>/activities -Method Post -InFile msg.json -ContentType 'application/json' -Headers $headers
Bearerの後に1つスペースを空けるのを忘れずに。
喋りました。
mark downやかんたんなタグを使うこともできます。また、ファイルを送るなども可能。詳しくは
https://blogs.msdn.microsoft.com/tsmatsuz/2016/08/31/microsoft-bot-framework-messages-howto-image-html-card-button-etc/ を参照。
Step 3: Function Appを作る
- Azure Portal https://portal.azure.com/ にアクセスします。
- 初めてAzureを使う人は、サブスクリプション(課金方法)を作成する必要があります。従量課金を選んでおけば問題ないでしょう。
- Azure Portalから、Function Appを作成します。
- App Service プランはF1 Freeを選びましょう。その他は月額料金がかかります。価格レベルを選択のところですべて表示みたいなボタンを押さないとフリープランは表示されません。
- その他は適当に。
- 作成ボタンを押してしばらく待つと、Function Appが作成されます。それを開いて、新しい関数→GitHubWebHook-CSharpを選びます。関数名は適当に。
- 編集画面が出てきます。上部に表示されている実行ボタンをクリックした後、関数のURLとGitHubシークレットを、GitHubのWebhook設定画面のPayload URL、Secretに入力します。GitHubのWebhook設定画面のRecent DeliveriesのところにResponse 200と出れば成功です。
Step 4: Function AppからBotに喋らせる
ここまで来ればできたも同然です。ほしいイベントだけを拾って、先程Power ShellでやったことをC#でやるだけです。
イベントの種類は、ヘッダの**"X-GitHub-Event"**で区別できます。Jsonでデータが送られてくるので、イベントに応じて情報を取捨選択し、整形してBotに発言させます。
飛んでくる情報については、https://developer.github.com/v3/activity/events/types/ にまとめられています。でも、実際にプッシュなどをしてRecent Deliveriesから確認するのが確実かと思われます。
参考までに、僕の環境を載せておきます。GitHubからのイベントと、Travis CIからのイベント(GitHubの設定で、Let me select individual events→Statusにチェックを入れておけば、GitHub経由で飛んできます)を分けて通知するために、2つのBotを用意しています。
C#は全く触ったことがなかったので、C#を使ってる人からすれば良くないプログラムかもしれませんが。。。
#r "Newtonsoft.Json"
using System.Net;
using System.Linq;
using System.Collections.Generic;
using System.IO;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
private static readonly SkypeBot GITHUB = new SkypeBot("<App ID>", "<Password>");
private static readonly SkypeBot TRAVIS = new SkypeBot("<App ID>", "<Password>");
private const string CONVERSATION_ID = "<Conversation ID>";
public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log)
{
if(!req.Headers.Contains("X-GitHub-Event")){
return req.CreateResponse(HttpStatusCode.OK);
}
string eventTag = req.Headers.GetValues("X-GitHub-Event").FirstOrDefault();
dynamic data = await req.Content.ReadAsAsync<object>();
createAndSendMessage(eventTag, data);
return req.CreateResponse(HttpStatusCode.OK, "From Github");
}
private static void createAndSendMessage(string eventTag, dynamic data){
if(eventTag == "pull_request"){
string fromBranch = data.pull_request.head.@ref;
string toBranch = data.pull_request.@base.@ref;
if(data.action == "opened"){
GITHUB.sendMessage(
$"'{data.pull_request.user.login}' がPull Request '{fromBranch} -> {toBranch}' を作成しました。 {data.pull_request.html_url}",
CONVERSATION_ID
);
}else if(data.action == "closed"){
if(data.pull_request.merged == "true"){
GITHUB.sendMessage(
$"'{data.pull_request.user.login}' によるPull Request '{fromBranch} -> {toBranch}' は承認されました :D",
CONVERSATION_ID
);
}else{
GITHUB.sendMessage(
$"'{data.pull_request.user.login}' によるPull Request '{fromBranch} -> {toBranch}' は却下されました。 {data.pull_request.html_url}",
CONVERSATION_ID
);
}
}
}else if(eventTag == "push"){
string raw = data.@ref;
string branch = raw.Replace("refs/heads/", "");
GITHUB.sendMessage(
$"'{data.pusher.name}' がbranch '{branch}' をPushしました。",
CONVERSATION_ID
);
}else if(eventTag == "create"){
GITHUB.sendMessage(
$"'{data.sender.login}' が{data.ref_type} '{data.@ref}' を作成しました。",
CONVERSATION_ID
);
}else if(eventTag == "delete"){
GITHUB.sendMessage(
$"'{data.sender.login}' が{data.ref_type} '{data.@ref}' を削除しました。",
CONVERSATION_ID
);
}else if(eventTag == "status"){
string state = data.state;
string branch = data.branches[0].name;
if(state == "pending"){
TRAVIS.sendMessage(
$"branch '{branch}' の自動テストが進行中...",
CONVERSATION_ID
);
}else if(state == "success"){
TRAVIS.sendMessage(
$"branch '{branch}' の自動テストが[成功]しました :D",
CONVERSATION_ID
);
}else if(state == "failure" || state == "error"){
TRAVIS.sendMessage(
$"branch '{branch}' の自動テストが[失敗]しました。詳細は以下で閲覧できます: {data.target_url}",
CONVERSATION_ID
);
}
}
}
private class SkypeBot{
private string appId;
private string appSecret;
public SkypeBot(string appId, string appSecret){
this.appId = appId;
this.appSecret = appSecret;
}
public void sendMessage(string message, string conversationId){
string bearer = getBearer(appId, appSecret);
string conversationUri = $"https://skype.botframework.com/v3/conversations/{conversationId}/activities";
dynamic jsonMessage = new JObject();
jsonMessage.type = "message";
jsonMessage.text = message;
byte[] messageBytes = System.Text.Encoding.UTF8.GetBytes(jsonMessage.ToString());
var req = WebRequest.Create(conversationUri);
req.Method = "POST";
req.ContentType = "application/json";
req.Headers.Add("Authorization", $"Bearer {bearer}");
var requestStream = req.GetRequestStream();
requestStream.Write(messageBytes, 0, messageBytes.Length);
requestStream.Close();
var response = req.GetResponse();
}
private string getBearer(string appId, string appSecret){
string body = $"grant_type=client_credentials&client_id={appId}&client_secret={appSecret}&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default";
byte[] bodyBytes = System.Text.Encoding.ASCII.GetBytes(body);
var req = WebRequest.Create("https://login.microsoftonline.com/common/oauth2/v2.0/token");
req.Method = "POST";
var requestStream = req.GetRequestStream();
requestStream.Write(bodyBytes, 0, bodyBytes.Length);
requestStream.Close();
var response = req.GetResponse();
var responseStream = response.GetResponseStream();
var streamReader = new StreamReader(responseStream);
string responseBody = streamReader.ReadToEnd();
streamReader.Close();
responseStream.Close();
dynamic responseBodyJson = JsonConvert.DeserializeObject(responseBody);
return responseBodyJson.access_token;
}
}
結果はこんな感じです。
なぜか、進行中のWebhookが2回飛んできますが、気にしないことにします。
おわりに
応用
- GitHub以外のWebHookで使いたいときは、関数を作成するときにGenericWebHook-CSharpを選べばいいです。
- HttpTriger-CSharpを選べば、一般のHttpリクエストに対し実行できます。
- コマンドラインで動作確認したときの方法を使えば、スケジューラを使って定時に喋らせたりもできます。
- プログラムから喋らすことも。
- 今回はC#を使いましたが、JavaScriptやF#を使うこともできます。
感想
Botの情報には、「話しかけると返事をする」といった類のものが多いです。そちらに迷い込んでしまったせいで、たったこれだけのことに何日も費やしてしまいました。
この情報が皆さんの役に立つことを願います。・・・というか、需要あるのか?これ。