読者です 読者をやめる 読者になる 読者になる

Windows10 IoT CoreでもSlackのOutgoingWebhooksを実装してみる

Windows10 RaspberryPi 開発 電子工作 コンピュータ VisualStudio C#

Windows10 IoT CoreでもSlackのOutgoingWebhooksを実装してみる

以前のエントリーでまどべんよっかいちにてRaspberryPi(Raspbian)でSlackのIntegrationであるOutgoingWebhooksの連携を行いましたが、少しもやもやしていたのでWindows10 IoT Coreでも実装してみました。

uepon.hatenadiary.com

www.slideshare.net

OutgoingWebhooksの設定に関してはSlideShareの資料を見てもらえれば良いかと思います。

今回もヨロイ元帥botを使っていきます。

ヨロイ元帥Botとは

Slackの書き込み中に「仮面ライダー」というキーワードあったら、

【投稿したUser_name】+“裏切者は殺す。デストロンを代表して。ヨロイ元帥”

と書き込むBotになります。

先日のアメトーーク!の放送で「仮面ライダー芸人」のOAがあったのでヨロイ元帥もかなり認知度が上がっているのではないでしょうか。

Windows10 IoT Core特有の問題

今回はUWPとC#という組み合わせだったので少してこずりました。というのも、Webサーバ的な機能がデフォルトでUWPでは持たせることができないところに理由があります。 もう少しダイレクトにいうならば、System.Net.HttpListenerが使えないのでHTTPの待ち受けをしてくれるような便利クラスがないということになります。 そのため、

  • TcpListenerクラス
  • StreamSocketListenerクラス

のいずれかを使用することになるのかなと思います。

試してはいませんがIoT向けのIotWeb HTTP Serverというライブラリもあるようなのでそっちを使ったほうが楽なんだと思います。

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するパッケージ

f:id:ueponx:20160806161529j:plain

今回は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をクリックして

f:id:ueponx:20160806161536j:plain

機能タブを選択すると表示されます。

f:id:ueponx:20160806161542j:plain

この2つにチェックをいれて保存します。これが設定されていないと動作しません。注意が必要です。

動作

何とか動いてくれました。

f:id:ueponx:20160806161554j:plain

終わりに

Raspbianに比べると特殊事情がおおいWindows10 IoT Coreですが、VisualStudioのデバック機能が優秀なため、引っかかっても悩むことは少ないと感じました。 また、UWPアプリであるところが癖でもありますが、セキュリティーの面はそれなりにいいかもなあという印象もあります。

まだまだ接続機器に不安が残りますが、どちらも使っていこうと思います。