C#コードを評価して結果を返すSlackBotをAzureFunctionsで実装する

C#コードを評価して結果を返すbotを実装してみようと思います。
結果はこのような感じです。
![botsample.png](https://qiita-image-store.s3.amazonaws.com/0/60841/aa9c2cd1-e6d6-c6f8-c766-082923b0c4d9.png)

## Slack の Integration 設定
Slack で bot を作るには Integration を利用すると良さそうです。
### Outgoing Webhooks
何か発言に対して反応したい場合はOutgoingWebhooksを利用できそうです。
"@C#:"と先頭についている場合に反応するようにTriggerWordを設定します。
反応する発言があった場合にデータを送信するURLは後述するAzureFunctionsで実装したAPIのURLをセットします。
![outgoing.png](https://qiita-image-store.s3.amazonaws.com/0/60841/048e9882-d7df-dbde-4546-a8d403f86d75.png)

### Incoming Webhooks
何か発言をする場合はIncomingWebhooksを利用できそうです。
設定画面に発言データ(payload)を送りつけるURLが書いてあります。
発言するチャンネルはpayloadで上書きできるので
疎通確認用のチャンネル等にしておくと良いのでは無いでしょうか。
![incoming.png](https://qiita-image-store.s3.amazonaws.com/0/60841/0766bfe0-f6c3-0c1d-8812-0da952c9f2a6.png)

## AzureFunctions で bot の実装
OutgoingWebhook で送られてきたデータを処理して IncomingWebhook に返す処理を AzureFunctions に実装します。
AzureFunctions に関しては[ぎたぱそ先生のブログ](http://tech.guitarrapc.com/archive/category/AzureFunctions)が参考になります。  

### Integrate タブの WebHook type を Not a WebHook にする
OutgoingWebhook を受け取る為に設定します。
[じんぐる先生のブログ](http://blog.xin9le.net/entry/2016/04/01/042452)に記述されていました。

### Nuget のインストール
Nugetパッケージを利用できるようにコンソールからインストールします。
[この記事](http://tech.guitarrapc.com/entry/2016/04/05/043723)読めばできますね。

### C#コードの評価
Roslyn の [CSharpScripting](https://github.com/dotnet/roslyn/wiki/Scripting-API-Samples) を使えば簡単です。

### 今回実装したコード
長々と書きなぐっていて美しさには欠けますがこのようになりました。

```json:project.json
{
 "frameworks": {
   "net46":{
     "dependencies": {
       "Microsoft.CodeAnalysis.Scripting": "1.2.1",
       "Newtonsoft.Json" : "8.0.3"
     }
   }
 }
}
```

```csharp:run.csx
#r "System.Collections"
#r "System.Configuration"
#r "System.Runtime"
#r "System.Reflection"
#r "System.Threading.Tasks"
#r "System.Web"

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Threading.Tasks;
using System.Web;
using Newtonsoft.Json;
using Microsoft.CodeAnalysis.Scripting;
using Microsoft.CodeAnalysis.CSharp.Scripting;

private static readonly string[] DefaultImports =
{
    "System",
    "System.Collections.Generic",
    "System.IO",
    "System.Linq",
    "System.Text",
};

private static readonly Assembly[] DefaultReferences =
{
    typeof(Enumerable).Assembly,
    typeof(List<string>).Assembly,
};

private const string INCOMING_URL_KEY = "IncomingWebhookUrl";

private const string TRIGGER_WORD = "@C#:";

public static async Task<object> Run(HttpRequestMessage req, TraceWriter log)
{
    log.Verbose("Webhook was triggered!");

    var content = await req.Content.ReadAsStringAsync();
    log.Verbose(content);
    
    var data = content
        .Split('&')
        .Select(x => x.Split('='))
        .ToDictionary(x => x[0], x => HttpUtility.HtmlDecode(HttpUtility.UrlDecode(x[1])));

    if (data["user_name"] == "slackbot")
    {
        return req.CreateResponse(HttpStatusCode.OK, new {
            body = "Cannot Support Messages From SlackBot.",
        });
    }

    var text = data["text"] as string ?? "";
    log.Verbose(text);

    var code = text.Replace(TRIGGER_WORD, "");

    object result = null;
    try
    {
        result = await CSharpScript.EvaluateAsync(code ?? "コードが空ニャ", 
            ScriptOptions.Default
                .WithImports(DefaultImports)
                .WithReferences(DefaultReferences));
    }
    catch (Exception ex)
    {
        result = ex.Message;
    }

    var resultText = result?.ToString() ?? "";
    if (resultText.StartsWith(TRIGGER_WORD))
    {
        resultText = resultText.Replace(TRIGGER_WORD, "");
    }

    log.Verbose(resultText);

    var payload = new
    {
        channel = "#" + data["channel_name"],
        username = "C#と和解せよ",
        text = string.IsNullOrWhiteSpace(resultText) ? "空だニャ" : resultText,
        icon_emoji = ":cat:",
    };
    var jsonString = JsonConvert.SerializeObject(payload);
    using (var client = new HttpClient())
    {
	var incomingUrl = ConfigurationManager.AppSettings[INCOMING_URL_KEY];
        var res = await client.PostAsync(incomingUrl, new FormUrlEncodedContent(new[]
        {
            new KeyValuePair<string, string>("payload", jsonString)
        }));
        return req.CreateResponse(res.StatusCode, new {
            body = resultText,
        });
    }
}
```
IncomingWebhookのUrlはAppSettingsに格納しています。
[こちら](http://tech.guitarrapc.com/entry/2016/04/16/024424)を見ればできます。

手軽に bot が作れて良いですね。