Windows10 IoT CoreでもSlackのOutgoingWebhooksを実装してみる
以前のエントリーでまどべんよっかいちにてRaspberryPi(Raspbian)でSlackのIntegrationであるOutgoingWebhooksの連携を行いましたが、少しもやもやしていたのでWindows10 IoT Coreでも実装してみました。
www.slideshare.net
OutgoingWebhooksの設定に関してはSlideShareの資料を見てもらえれば良いかと思います。
今回もヨロイ元帥botを使っていきます。
ヨロイ元帥Botとは
Slackの書き込み中に「仮面ライダー」というキーワードあったら、
【投稿したUser_name】+“裏切者は殺す。デストロンを代表して。ヨロイ元帥”
と書き込むBotになります。
先日のアメトーーク!の放送で「仮面ライダー芸人」のOAがあったのでヨロイ元帥もかなり認知度が上がっているのではないでしょうか。
結城丈二
— 仮面ライダー迷(名)言bot (@Meigen_KR) 2016年8月4日
新年おめでとう。今年こそ、
裏切り者は殺す。
デストロンを代表して。
ヨロイ元帥 pic.twitter.com/MtxPLrD3yY
ヨロイ元帥、この手で年賀状書いたの? pic.twitter.com/jVCGQyLIiL
— 大刀・ザ・スラッシュダーク (@DAIGATANA) 2016年8月4日
Windows10 IoT Core特有の問題
今回はUWPとC#という組み合わせだったので少してこずりました。というのも、Webサーバ的な機能がデフォルトでUWPでは持たせることができないところに理由があります。
もう少しダイレクトにいうならば、System.Net.HttpListener
が使えないのでHTTPの待ち受けをしてくれるような便利クラスがないということになります。
そのため、
- TcpListenerクラス
- StreamSocketListenerクラス
のいずれかを使用することになるのかなと思います。
試してはいませんがIoT向けのIotWeb HTTP Serverというライブラリもあるようなのでそっちを使ったほうが楽なんだと思います。
今回はStreamSocketListener
クラスをつかってWebサーバを作成してOutgoingWebhooksに対応することにします。
実はStreamSocketListenerを使ったサンプルがGithubにあったはずなのですがなくなってしまったようです。残念。
App2App WebServer https://github.com/ms-iot/samples/tree/develop/App2App%20WebServer
現在は404になっています。
WebServerのクラス
基本的には素のソケット通信でWebServerを作成することになります。
ただ、StreamSocketListener
はもう少し便利になっていて、
通信を検知するとイベント(ConnectionReceived)を検知してくれます。
そのため、このイベントに対処するイベントハンドラを作成すればデータの受信時の動きを簡単に書くことができます。
Listener_ConnectionReceived()
メソッドがコネクションが張られたときに動作するイベントに該当します。
逆にデータの送信に関してはSendResponse()
メソッドが該当しています。
送信に関してはOutgoingWebhooksではJSONを使用しているため、nugetでNewtonsoft.Json
を行う必要があります。
SlackのOutgoingWebhooksのデータ送信形式に関してはSlideShareの資料を参考にしてください。
nugetするパッケージ
今回はtokenデータによる簡単な認証を行っています。
using Newtonsoft.Json; using System; using System.IO; using System.Linq; using System.Runtime.InteropServices.WindowsRuntime; using System.Text; using Windows.Networking.Sockets; using Windows.Storage.Streams; public class SimpleOutgoingWebhooksWebServer { StreamSocketListener listener; StreamSocket socket; public event Action<string> OnReceived; private string tokenBlock = "token="; public int PORT { get; set; } public string TOKEN { get; set; } public SimpleOutgoingWebhooksWebServer(int port, string token) { this.TOKEN = token; tokenBlock += this.TOKEN; this.PORT = port; } public async void Start() { listener = new StreamSocketListener(); listener.ConnectionReceived += Listener_ConnectionReceived; await listener.BindServiceNameAsync(PORT.ToString()); } private async void Listener_ConnectionReceived(StreamSocketListener sender, StreamSocketListenerConnectionReceivedEventArgs args) { socket = args.Socket; var dr = new DataReader(socket.InputStream); /// 受信データの取り出し StringBuilder request = new StringBuilder(); uint BufferSize = 2048; using (IInputStream input = socket.InputStream) { byte[] data = new byte[BufferSize]; IBuffer buffer = data.AsBuffer(); uint dataRead = BufferSize; while (dataRead == BufferSize) { await input.ReadAsync(buffer, BufferSize, InputStreamOptions.Partial); request.Append(Encoding.UTF8.GetString(data, 0, data.Length)); dataRead = buffer.Length; } } // メソッド部分の処理 var receiveString = request.ToString(); var source = receiveString.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); var content = source.Last().Split(new[] { '&' }); var requestMethod = source[0]; string[] requestParts = requestMethod.Split(' '); var text = requestParts[1]; if (content.Contains(tokenBlock) == false) { return;//token認証NG } var x = content.Where(e => e.Contains("user_name=")).FirstOrDefault(); var userName = ""; if (x != null) { userName = x.Substring("user_name=".Length); } if (text.Contains("favicon.ico") == false) //faviconのリクエストは無視 { /// 受信イベント if (this.OnReceived != null) { OnReceived(userName); } } } public async void SendResponse(string text) { if (socket == null) return; var data = new SendJSONData { text = text }; var json = JsonConvert.SerializeObject(data); byte[] bodyArray = Encoding.UTF8.GetBytes(json); MemoryStream stream = new MemoryStream(bodyArray); string header = String.Format( "HTTP/1.0 200 OK\r\n" + "Content-Type: text/javascript; charset=utf-8\r\n" + "Content-Length: {0}\r\n" + "Connection: close\r\n\r\n", stream.Length); //ヘッダ部分 var dw = new DataWriter(socket.OutputStream); dw.WriteString(header); dw.WriteString(json); await dw.StoreAsync(); } private class SendJSONData { public string text { get; set; } } }
このクラスをもとにメイン処理を記述していきます。
MainPage.xaml.cs
メインの処理は先ほどのSimpleOutgoingWebhooksWebServerクラスを使うことで比較的短く書くことができます。
コンストラクタの第1引数の部分が受け待ちになるport番号です。
また、コンストラクタの第2引数の部分が設定時に指定されるtoken
になりますので適宜変更してください。
server_OnReceived()はデータの受信後に発生するイベント(.OnReceivedイベント)のハンドラになります。
今回はLEDなどを点灯していませんが、ここでLEDの処理を入れればいいかなと思います。
using System; using Windows.UI.Xaml.Controls; using Windows.UI.Core; namespace IoTCoreWebServer { public sealed partial class MainPage : Page { SimpleOutgoingWebhooksWebServer server; public MainPage() { this.InitializeComponent(); server = new SimpleOutgoingWebhooksWebServer(8888, "xxxxxxxxxxxxxxxxx"); server.OnReceived += server_OnReceived; server.Start(); } private async void server_OnReceived(string data) { // Request受信 await Dispatcher.RunAsync( CoreDispatcherPriority.Normal, () => { textReceive.Text = data; }); // 受信データをテキストボックスに表示 // 応答を送信 server.SendResponse(data + "裏切り者は殺す。デストロンを代表して。ヨロイ元帥"); } } }
一応これでコーディングは終了です。
Package.appxmanifestの変更
ただし、これだけでは動作しません。ソリューションマネージャにあるPackage.appxmanifestファイルを開き機能の追加を行います。
- インターネット(クライアント)
- インターネット(クライアントとサーバー)
Package.appxmanifestをクリックして
機能タブを選択すると表示されます。
この2つにチェックをいれて保存します。これが設定されていないと動作しません。注意が必要です。
動作
何とか動いてくれました。
終わりに
Raspbianに比べると特殊事情がおおいWindows10 IoT Coreですが、VisualStudioのデバック機能が優秀なため、引っかかっても悩むことは少ないと感じました。 また、UWPアプリであるところが癖でもありますが、セキュリティーの面はそれなりにいいかもなあという印象もあります。
まだまだ接続機器に不安が残りますが、どちらも使っていこうと思います。