20190502日記: Slackおみくじ

モチベーション

(裏の理由) ゴールデンウィークだし、少し時間ができたので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が存在するため、今回の実装の活躍の場はない。