20190502: Slackおみくじ(3つの実装を行う)¶
モチベーション¶
(裏の理由) ゴールデンウィークだし、少し時間ができたのでC#でAzureでなにかしたかった。
(表の理由) 円滑なコミュニケーションのために、ユーザからおみくじbotが求められることはIRC時代からきっと広く知られていた。昨今、Slackが広く浸透したため、車輪の再開発としておみくじbotを作る。
以下、通常よく例示される
- a. Slack outgoing APIを用いた最もシンプルなおみくじ
 - [やらない]b. Slack event APIを用いたインタラクティブなおみくじ
 - c. Slack RT APIを用いたインタラクティブなおみくじ という例を示すと同時に、
 
Azure Functionで
- d. Slack outgoing APIを用いた最もシンプルなおみくじ
 - e. Slack event APIを用いたインタラクティブなおみくじ
 - [やらない]f. Slack RT APIを用いたインタラクティブなおみくじ
 
あたりを目指す。 Azure FunctionはAWS Lambdaのようなサーバレスの機能サービスであり、HTTPをトリガーにcallすることができる。
共通仕様¶
- fourtuneという文字列を含むポストがされたら、等確率で大吉、吉、凶を返す
 
a. シンプルおみくじ(PHP)¶
どうするか?¶
- Slackのoutgoing webhookは、Slack上で特定のキーワードが含まれるポストがあった際に、外部のwebhookをたたく機能である
 - 「特定のキーワードが含まれる」であるため、fortuneが含まれるかを判定する必要はない。
 - hookされたURLへのpostへのresponseとして返信のJSONを投げることで返信のメッセージを送ることができる。これに必要な最低限のメッセージは以下の通り。
 
{
  "text": "テキスト"
}
実装¶
- Internet上のアドレスからアクセス可能な場所に適当なプログラムを置く。例えば、phpなら、
 
<?php
header('content-type: application/json; charset=utf-8');
switch(rand(0, 2)){
  case 0: echo '{"text": "daikichi"}'; break;
  case 1: echo '{"text": "kichi"}'; break;
  case 2: echo '{"text": "kyou"}'; break;
}
?>
- Outgoing webhookを作る。channelは#randomにしておく。trigger wordに”fortune”。そして、上記のphpが実行されるURLを設定。
 
b: event APIを使う(やめた)¶
- event APIを使うにはhttpsじゃないといけない。手持ちのサーバ系が諸般の理由でSSL Enableしていないのでやめた。
 
c: RTM APIを使う(Python)¶
https://github.com/slackapi/python-slackclient#basic-usage-of-the-rtm-client を参考に進める。環境は適当に作る。
環境作成例(>=3.6らしい):
apt-get install python3-venv
python3 -m venv rtm
. ./rtm/bin/activate
pip3 install slackclient
- AppとしてBotsを作る。
 
ここで、”API Token”をめもる。bots APPはWebSocketを用い、 クライアントから発呼される。 この際に、Slack側が正しいユーザであるという認証のために (というか、所属するワークスペースなどもここで確認される) このトークンが必要になる。
コード¶
import slack
import random
@slack.RTMClient.run_on(event='message')
def fortune(**payload):
    data = payload['data']
    web_client = payload['web_client']
    rtm_client = payload['rtm_client']
    # textはreplyなどには含まれないので、subtypeがなし=親メッセージかを確認する
    if 'subtype' not in data and 'fortune' in data['text']:
        channel_id = data['channel']
        thread_ts = data['ts']
        user = data['user']
        num = random.randint(0, 2)
        if num == 0:
            kichi = "daikichi"
        elif num == 1:
            kichi = "kichi"
        elif num == 2:
            kichi = "kyou"
        # thread_tsを設定することでスレッドでのリプライになる
        web_client.chat_postMessage(
            channel=channel_id,
            text=kichi,
            thread_ts=thread_ts
        )
slack_token = os.environ["SLACK_API_TOKEN"]
rtm_client = slack.RTMClient(token=slack_token)
rtm_client.start()
動作例¶
SLACK_API_TOKEN="xoxXXXXXXXXXXXXX" python3 bot.py
のように、プログラム上のos.environ["SLACK_API_TOKEN"]に入れるべき文字列を実行時に指定しよう。
のように、リプライで返信される。
参考: デバッグ用コード(slackから送られてくるapiのpayload dump)¶
import os
import slack
from pprint import pprint
@slack.RTMClient.run_on(event="message")
def dump(**payload):
    pprint(payload)
@slack.RTMClient.run_on(event="reaction_added")
def dump2(**payload):
    pprint(payload)
slack_token = os.environ["SLACK_API_TOKEN"]
rtm_client = slack.RTMClient(token=slack_token)
rtm_client.start()
SLACK_API_TOKEN="xoxXXXXXXXXXXXXX" python3 aa.py
のように起動する。そして、slackで(eventに登録した動作を)すると、
{'data': {'event_ts': '1556780058.004200',
          'item': {'channel': 'C1HBXXXXXA',
                   'ts': '1556780053.004100',
                   'type': 'message'},
          'item_user': 'U1HBXXXXX2',
          'reaction': 'hugging_face',
          'ts': '1556780058.004200',
          'user': 'U1HBXXXX2'},
のように、イベントが見える。 尚、LISTENするイベントの一覧は、 https://api.slack.com/events を参照すること。
d. Azure Functionでシンプルなおみくじ¶
とにかく簡単に作る。Visual Studio側での開発と発行を行う。
コード¶
プロジェクトの作成 -> Azure Function -> HTTP Triggerで作成
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
namespace omikuji_most_simple
{
    public static class Function1
    {
        [FunctionName("Function1")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            String kichi = new String("");
            Random rand = new Random();
            switch (rand.Next(0, 3))
            {
                case 0:
                    kichi = "daikichi";
                    break;
                case 1:
                    kichi = "kichi";
                    break;
                case 2:
                    kichi = "kyou";
                    break;
            }
            log.LogInformation("Come: ");
            return (ActionResult)new OkObjectResult("{\"text\": \"" + kichi + "\"}");
        }
    }
}
など書いて、適切にテストして発行。
デプロイ終了後、https://omikujimXXXXXXXXXXXx.azurewebsites.net/api/Function1のようなアドレスを得るので、それを、outgoingのendpointとして設定。
見栄えとしてはなにも変わらないが、Insightなどでさくっと利用率可視化できるのは便利だ。
e: Azure FunctionでSlack Event APIを使う¶
https://api.slack.com/events-api を参考にする。
https://api.slack.com/apps からappを作成する。
コード¶
- Visual Studioでfortune_event_apiなどというAzure Function APIを作成する。Anonymousスコープ。
 - ソリューションエクスプローラかプロジェクトを右クリック > 追加 > Azure関数 > 名前をauth.csなどにして作成http trigger(anonymous)
 
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System.Net;
using System.Text;
using System.Collections.Specialized;
using System.Web;
namespace fortune_event_api
{
    public static class handler
    {
        // botTokenのID
        public static string botToken = "xoxb-XXXXXXXXXXXXXXXXXXXXXXXXX";
        // chat.postMessageのUrl
        public static string urlChat = "https://slack.com/api/chat.postMessage";
        [FunctionName("handler")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            // ペイロードのオブジェクト化(含json deser)
            string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
            SlackAppMessage data = JsonConvert.DeserializeObject<SlackAppMessage>(requestBody);
            // slackのbot登録時のverifyの場合、
            if (data?.type == "url_verification")
            {
                // オウム返しする(challengeが含まれていればOKなので)
                return (ActionResult)new OkObjectResult(requestBody);
            }
            // 通常のメッセージの場合は
            else if (data?.type == "event_callback")
            {
                // まず、文頭がfortuneかを判定
                if(!data.eventdict.text.StartsWith("fortune"))
                {
                    // メッセージを受け取ったと返信
                    return (ActionResult)new OkObjectResult("");
                }
                // do うらない
                String kichi = new String("");
                Random rand = new Random();
                switch (rand.Next(0, 3))
                {
                    case 0:
                        kichi = "daikichi";
                        break;
                    case 1:
                        kichi = "kichi";
                        break;
                    case 2:
                        kichi = "kyou";
                        break;
                }
                // HTTP Handlerの作成
                Uri uri = new Uri(urlChat);
                Encoding enc = new UTF8Encoding();
                using (WebClient client = new WebClient())
                {
                    // HTTP送信するペイロードの作成
                    NameValueCollection sendmsg = new NameValueCollection();
                    sendmsg["channel"] = data.eventdict.channel;
                    sendmsg["text"] = kichi;
                    log.LogInformation("Channel:" + data.eventdict.channel);
                    log.LogInformation("Text:" + data.eventdict.text);
                    // ヘッダにBearerを追加
                    client.Headers["Authorization"] = "Bearer " + botToken;
                    var res = client.UploadValues(uri, "POST", sendmsg);
                    log.LogInformation("send to slack: ");
                    string resText = enc.GetString(res);
                    log.LogInformation("recv from slack "+ resText);
                }
                // メッセージを受け取ったと返信
                return (ActionResult)new OkObjectResult("");
            }
            else
            {
                // メッセージを受け取ったと返信
                return (ActionResult)new BadRequestObjectResult("This function will be accept only url_verification and message.");
            }
        }
        public class SlackAppMessage
        {
            public string token { get; set; }
            public string challenge { get; set; }
            public string type { get; set; }
            // eventは予約名なので、JSON名との読み替えを行う
            [JsonProperty("event")]
            public SlackAppMessageEvent eventdict { get; set; }
        }
        public class SlackAppMessageEvent
        {
            public string channel { get; set; }
            public string text { get; set; }
        }
        public class SlackAppReplyMessage
        {
            public string token { get; set; }
            public string as_user { get; set; }
            public string channel { get; set; }
            public string text { get; set; }
        }
    }
}
- Features > Bot User で 
add bot Userする。 - Features > OAuth & Permissions で redirect URLsに指定されたアドレスを指定する
 - Features > OAuth & Permissions > Scopesで以下を追加してsave
- channels:history(以下のeventを追加するのに必要)
 - chat:write:bot(botからのPOSTを受け取るのに必要)
 
 - Features > Event Subscriptions を有効にして
- Request URLに作ったfunctionのURLを指定(これを有効にするとurl_verificationが送られる)
 - Subscribe to Workspace Events > addで
message.channelsを追加。 
 - Features > OAuth & PermissionsのBot User OAuth Access Tokenをメモして上記に書く。
 
f. Slack RTM APIを用いたインタラクティブなおみくじ(やらない)¶
Azure Function + RTMは賢くないので実装しない。
まとめ¶
今回はいくつかのおみくじbotの実装を行った。しかしながら、我々のSlackにはすでにおみくじbotが存在するため、今回の実装の活躍の場はない。