Slack

初めてSlack appをつくって審査通すところまでやった知見を晒す

たまたま機会あってSlack appを作らせていただくことになったのですが、
思ってた以上に大変だったのでそこで得られた知見をいくつか簡単にシェアしておきます。

ちなみに自分はhubotとかbotkitとかで簡単なbot作ったことあるレベルで、
とりあえずtoken発行して環境変数突っ込んでスクリプト動かしたらそれっぽいやつできるんでしょ?、くらいのノリからのスタートだったので正直いろいろつっこみどころありそうではあるのですが、何かあれば是非インプットいただければと思います :pray:

今回作ったもの

slackapp.jpg
ホメルくん

詳細なコンセプトや利用方法はこちらの記事に移譲しますが、
ざっくり三行で説明すると、

  • Slackのカスタム絵文字って企業毎の文化を反映したものになってる
  • 上記のような絵文字がたくさんついたメッセージや発言者を組織内でもっとオープンにすることで、称賛された人はモチベーション上がるし、組織づくりにもよい影響与えられそう
  • というわけで、特定の絵文字がついたメッセージを特定のチャンネルに転送してくれて、毎月絵文字ごとの発言者のTop3を表彰してくれるSlackアプリがこちら

という感じになります。

機能的にもかなりシンプルで、

  • 絵文字、チャンネルの登録
  • 指定絵文字がリアクションされた場合、指定チャンネルへのメッセージの転送
  • 毎月リアクション絵文字獲得数Top3を集計しメッセージを送る

くらいで、まぁ正直こんなん半日くらいでさくっと終わるだろ、とそんなふうに考えていた時期が私にもありました :innocent:

実装編

Slack appの作り方 (ざっくり)

普通に自分のワークスペースでだけ動くbotつくる、というだけであれば、ここに書いてあるとおりにSlack appつくって Bot OAuth Access Token コピーするだけで大体は事足りると思うのですが、
(↑はhubot用の解説ですがtoken取得するという意味ではあまり変わらないはず)
いろいろなワークスペースで利用されることを前提とする場合、ちゃんとOAuthを利用してtokenを取得できる必要があったり、Slash CommandsInteractive Components(ボタンとかそういうやつ)利用したい場合は別途設定が必要だったりと、かつて1に比べてかなり設定が煩雑になっており、
そのあたりの日本語のリソースもあまりみかけないので、正直このあたりどうするのがいいのかよくわからんという人も多いのではないでしょうか。

一応Slack appの主な特徴として以下の6つがあるのですが、

細かいことはおいておいて、とりあえず全体感掴むためには、

上記チュートリアルを一通りやると、設定周りからコードの雰囲気までは大体理解できるのではと思います。(英語 + Node.jsになってしまいますが)

Events APIも大体Slash CommandsとInteractive Componentsと同じような雰囲気なのですが、一点、URL設定するタイミングでそのURLがちゃんとレスポンス返さないとエラーになったりするので(refs: https://api.slack.com/events-api#subscriptions) ご注意ください。

(その他詳細な解説は今回は時間の関係上割愛します:bow:)

実装でハマったところ

APIでかゆいところに手が届かない

これ全体通して調査に時間かかったり手戻りする原因になったりしたのですが、そもそもの方針として、セキュリティ観点からなるべくアプリ側にデータ持たないで実装したいというのがあり、APIで完結できるものはなるべくAPIで完結させようと思っていたので、
たとえば、毎月絵文字の集計をするという要件を満たす方法はいくつかあると思うのですが、APIで特定の絵文字がついたメッセージを取得してきてそれを集計できるのであればそれが楽かなと思い、

reactions.list method | Slack

ぱっと見ちょうどいいAPIがあるのでこれ使えばいけるだろ、と思ってよくよくドキュメントをみてみると、
絵文字や期間でフィルタリングできなさそうで、このAPIだけでやろうと思えばできなくはなさそうですが無駄にリクエスト投げまくらないといけないのと、APIのRate Limitや条件のハンドリングなどやたらと煩雑になりそうだったため、結局リアクションの追加/削除のイベントをsubscribeしてそれをDBにレコードとしてつっこんで集計するようにしてます。

また、設定する絵文字の存在確認をするために絵文字一覧とか取得したくなると思うのですが、

emoji.list method | Slack

ぱっと見ちょうどいいAPIがあるのでこれ使えばいけるだろ、と思って実装に組み込んでみるとほとんどバリデーションで弾かれてしまい、何だこれと思ってよくよくドキュメント呼んでみると、どうもカスタム絵文字しか返してくれないらしく、そもそもコンセプト的にはカスタム絵文字設定してほしいのでカスタム絵文字限定とかにしても良かったのですが、一旦ここのバリデーションは特になしとしました (設定完了後にメッセージで絵文字返すのでそこでちゃんと絵文字になってなければ設定ミスってても気づくはず...)

などなど、意外とAPIだけで完結させようと思うと難しく、アプリケーション側でなにかしらデータ持つなどしてうまいことやらないといけないパターンが多そうという印象でした。
なんとなくで着手すると意外とそれできないんかいというのでハマったりするのでちゃんとドキュメント読んでから作業することをおすすめします:innocent: (当たり前)

絵文字エイリアス問題

特定の絵文字でリアクションされたかどうか判別するために、 Events APIでreaction_addreaction_remove イベントをsubscribeして処理をハンドリングするようにしているのですが、
絵文字のエイリアスが設定されている場合かつエイリアスの絵文字文字列でリアクションした場合、エイリアスではなくオリジナルの絵文字文字列で返ってきます。
(ex: :ok_woman: -> :woman-gesturing-ok:)
せめてエイリアスあった場合にエイリアスも一緒に返してきてくれると嬉しかったりするのですが、そんな親切仕様ではないので、特定の絵文字に対してなにかしらハンドリングしたい場合は真面目にやるならきちんとこのエイリアスまわりのマッピングを何かしらアプリケーション側でもつ必要がありそうです。
(cf: How can I get the FULL list of slack emoji through API? - Stack Overflow)
とりあえず今回の実装ではエイリアス問題には対応できてません:cry:

publicなチャンネルしかみれないようにscope設定してるのになぜか一部privateなチャンネルのメッセージが見れてしまう問題

これマジで困りました。
具体的に言うと、

channels.history method | Slack

このAPIでリアクションついたメッセージの詳細取りに行ってるのですが、

This method returns a portion of message events from the specified public channel.

↑のように、publicチャンネルのメッセージしか返さないよと言ってるのに何故かとあるprivateチャンネルのメッセージだけ取れてしまうという、しかも全然再現性なくて他のprivateチャンネルでは発生せず、おそらくSlack側の何かしらのバグなのではないかと思ってます。

とはいえうっかりprivateチャンネルのセンシティブなメッセージがオープンになるみたいな事故は起きてもらっては困るので、あれこれ手を動かした結果、

conversations.list method | Slack

このAPIを叩いてpublicチャンネルの一覧を取得して、一覧に含まれていなければprivateチャンネルとみなして処理を通さない、という実装に落ち着きました。

ちなみに似たような名前の

channels.list method | Slack

こっちだと件のprivateチャンネルも一緒に返ってきてしまって使えません。
(そもそもよくみると Don't use this method. Use conversations.list instead. って書いてあったりするのですが、もう少しわかりやすくしておいてくれても良い気がする...)

絵文字やチャンネルをどうやって登録させるか

どの絵文字をトリガーにして、どのチャンネルに転送するかをユーザーが設定できる必要があり、(デフォルトは絵文字: :thumbsup:、 チャンネル: #general)
普通にSlash Commandsで登録するでも良かったのですが、
ドキュメントいろいろ眺めていたところ Dialogs なるものを発見、簡単に言うとSlack上でフォーム的なUIを実現できるものです。

バリデーション等考えるとUX的にはこちらのほうが良さそうな気がしたので(エラー表示とか)
とりあえず実装してみたのですが、
チャンネル選択をselectにした部分は良かったものの、絵文字入力用のテキストフォームで絵文字補完が効かず、これなら普通にSlash Commandsのほうが普通に補完効くし良さそうということで、結局Slash Commandsでの設定に落ち着きました。
(正直なにをいってるか文字ベースだと超わかりにくいのですが、実例はホメルくんをインストールしてもらって /homeru setting とか叩いてもらえればわかるかと思います:bow:)

Screen Shot 2018-10-22 at 20.51.27.png

Dialogs自体は使い所を見極めればかなり使える機能ではと思います。

Incoming Webhookの挙動

メッセージの転送をするために特定のチャンネルに対して何かしらメッセージ送れる機能を実現する必要がありますが、
Legacyな方のIncoming Webhooksは昔からよく使っていて、適当なサーバー側の処理結果をSlackに飛ばすみたいなことをよくやっており、チャンネルや発言者の設定等いろいろ細かく指定できたので、とりあえずIncoming Webhookつかえば大丈夫だろうと実装してみたのですが、
どうもLegacyじゃない方のIncoming Webhookはチャンネル等の指定ができないことが判明し、

You cannot override the default channel (chosen by the user who installed your app), username, or icon when you're using Incoming Webhooks to post messages. Instead, these values will always inherit from the associated Slack app configuration.

(refs: https://api.slack.com/incoming-webhooks)

チャンネル自体の変更もAPIではできなさそうだったので今回の用途としてはIncoming Webhookは使えないことが判明。
あれこれ調べた結果、普通にbotであれば特にチャンネルにinviteしていなくてもメッセージ送信できるようだったのでそちらで回避しました。

メンション飛ばさずに @username をメッセージに含めたい

Slackでは基本的に @username#channel_name などはそのままの文字列ではなくIDとして渡ってきます。 (ex: U37UED2RL)
これらを利用してメッセージにユーザー名やチャンネル名を含めようとした場合、以下のような記法を利用すると、Slackがよしなに変換してくれます。

ex)
- <@U37UED2RL> => @uehara
- <#C7F5U1EKE> => #general
(refs: Basic message formatting | Slack)

ホメルくんのメッセージ転送機能で、誰が発言したかもわかるようにユーザー名も転送メッセージに一緒に含めたかったのですが、単純に↑の機能を使うと毎回メンション飛んできてこれはうざいという結論になり、なんとかメンション飛ばさないでユーザー名表示できないかあれこれ調べた結果、

users.info method | Slack

このAPI叩けばユーザー名取得できることが判明、よしこれでいけると思ったのですが現実はそう甘くなく、

{
    "ok": true,
    "user": {
        "id": "W012A3CDE",
        "team_id": "T012AB3C4",
        "name": "spengler",
        "deleted": false,
        "color": "9f69e7",
        "real_name": "Egon Spengler",
        "tz": "America/Los_Angeles",
        "tz_label": "Pacific Daylight Time",
        "tz_offset": -25200,
        "profile": {
            "avatar_hash": "ge3b51ca72de",
            "status_text": "Print is dead",
            "status_emoji": ":books:",
            "real_name": "Egon Spengler",
            "display_name": "spengler",
            "real_name_normalized": "Egon Spengler",
            "display_name_normalized": "spengler",
            "email": "spengler@ghostbusters.example.com",
            "image_24": "https://.../avatar/e3b51ca72dee4ef87916ae2b9240df50.jpg",
            "image_32": "https://.../avatar/e3b51ca72dee4ef87916ae2b9240df50.jpg",
            "image_48": "https://.../avatar/e3b51ca72dee4ef87916ae2b9240df50.jpg",
            "image_72": "https://.../avatar/e3b51ca72dee4ef87916ae2b9240df50.jpg",
            "image_192": "https://.../avatar/e3b51ca72dee4ef87916ae2b9240df50.jpg",
            "image_512": "https://.../avatar/e3b51ca72dee4ef87916ae2b9240df50.jpg",
            "team": "T012AB3C4"
        },
        "is_admin": true,
        "is_owner": false,
        "is_primary_owner": false,
        "is_restricted": false,
        "is_ultra_restricted": false,
        "is_bot": false,
        "updated": 1502138686,
        "is_app_user": false,
        "has_2fa": false
    }
}

こんなレスポンス返ってくるのですが、 user.profile.display_nameuser.profile.real_name の2種類があり、 user.profile.display_name が設定されている人は display_name、そうでない場合は user.profile.real_name を表示するというようにしないとSlack上の表示とずれるので注意が必要です。

ちなみに小ネタとして、 attachmentsauthor_name フィールドは <@U37UED2RL> でもメンションにならないのですが、今回は表示上の問題で利用は断念しました。
(refs: Attaching content and links to messages)

同一チャンネルのメッセージは ts がユニーク

Slackでメッセージデータ取得する際、

{
    "type": "message",
    "channel": "C2147483705",
    "user": "U2147483697",
    "text": "Hello world",
    "ts": "1355517523.000005"
}

こんなかんじでデータが取れるのですが、 ts はtimestampの略なんだろうなとは思いつつ、ほぼ同時に同じメッセージ送った場合、かぶったりしないのか、判別可能なのか、仕様どうなってるんだと思って調べたときに地味に言及している部分見つけるの苦労したので一応ソースを貼っておきます。

ts is the unique (per-channel) timestamp.

(refs: message event | Slack)

結論としては ts はチャンネルごとにユニーク性が担保されるとのこと。

アプリ公開編

さて、そんなこんなで思いの外時間かかりつつも一応動くようになったホメルくんですが、そのままでは他のワークスペースにシェアできません。

とりあえず公開してみる

とりあえず公開して他のワークスペースで利用してもらうというだけであれば、
アプリ設定画面の左カラムに Manage Distribute というリンクがあるので、そのページ内の
Share Your App with Other Teams ブロックの Activate Public Distribution ボタンを押すとシェア用のURLが発行され、これをクリックすると他のワークスペースにアプリをインストールすることが可能です。
ちなみにURLに利用するスコープがパラメータとして付与されており、スコープを変更するとURLも都度変更になるので、パーミッション周りをいじったあとは注意が必要です。
(実際の設定ではなくURLパラメータのスコープが優先される模様、不要なスコープ要求したり逆に足りないといったことが起こりえます)

審査に出す

とりあえず上記だけで他のユーザーに使ってもらえる状態にはなったのですが、
それだけの場合、アプリのページやアプリインストール時のページに以下のような警告文言が出ます。

Screen Shot 2018-10-21 at 13.57.02.png

またApp Directoryのページで検索しても出てきません。
Screen Shot 2018-10-24 at 11.41.57.png
(↑こういうやつ)

知り合いにだけ使ってもらうレベルであれば問題ないですが、
ちゃんと一般にリリースしようと思った場合はSlackにアプリの審査してもらう必要があります。

審査について

審査に出すためにはいくつか必須で設定必要な項目があり、アプリ設定のBasic Informationページで Display InformationYour Contact Information の入力、OAuth & Permissionsページでscopeを有効にしている場合はscope毎にそのscopeを利用する理由の入力、その上で、Manage Distributionページの Submit to the Slack App Directory ブロックにある鬼のような大量のチェックボックスをひたすらクリックしまくると晴れてSubmitできるようになります。
(refs: Submitting apps to the Slack App Directory | Slack)

Display InformationのTips
  • Background colorはあまり明るすぎると文字が見えにくいとの理由でエラーになる
  • Long descriptionはSlack記法が使える(**で太字など)

審査指摘実例集

そんなこんなで審査に出すとその日のうちに結果が返ってきました (早い!)

Screen Shot 2018-10-22 at 13.47.15.png
こんな感じで項目ごとに心温まるメッセージをいただけます。Thanks!

以下、突っ込まれたところを列挙します

  • s/slack/Slack/g
    • 説明文やLPにSlack文字列を使う場合、頭文字は大文字にしないと怒られます
  • アイコンにSlackのロゴはNG
    • 初期のホメルくんはキャップがSlackロゴだったのですが勝手にロゴ使うと怒られます homerukun_favicon.jpg
  • プライバシーポリシーは英語で
    • 中の人がレビューできないので英語にしてほしいとのこと、機械翻訳はNG、とりあえず今回はこちらを参考に作成
  • OAuth & Permissionsのscope利用理由はちゃんと書いて
    • 最初気づかずに空欄にしてたら怒られました、とりあえず一行とかで簡単に書いておけば普通に通ります
  • 問い合わせURLはFacebook messengerだとNG
    • Slackアカウント以外のアカウントが必要になる動線は不適切なのでNG、とりあえずGoogle Formに変更して対応
  • インストール後のリダイレクト先はDeep Link等利用して
    • 当初はインストール後にとりあえず https://my.slack.com に飛ばすようにしていたのですが、ブラウザで利用してない人もいるので https://api.slack.com/docs/deep-linking#app_or_bot こちら参考にして適切な飛ばし先にリダイレクトしてあげてくれとのこと、とりあえず今回はThanksページ作って対応 (ほぼLPそのままですが)
  • channels:readスコープはbotスコープに含まれてるから不要
    • というわけで削除、このスコープは転送用チャンネル設定のバリデーションや↑のprivateチャンネル除外する用で使ってます、ちなみにbotスコープで利用する場合はbot用のaccess_tokenでAPI叩く必要あり

そんなこんなで2度ほど申請対応して晴れて審査通って今回リリースできました:tada:

Screen Shot 2018-10-24 at 10.00.45.png
Screen Shot 2018-10-24 at 11.35.48.png

(ちなみにThanksメッセージのフィードバック対応は当日わりとすぐにレスポンス返ってきたのですが、もろもろ対応したあとに再度申請して実際に承認されるまでは丸一日以上かかりました)

あとがき

いかがでしたでしょうか、本当はもっと細かい粒度で記事分けてスクショフル活用しながら詳細書きたいところも多々あるのですが、めんど(ry諸事情によりかなり端折ってしまったので正直わかりにくいところも多かったかもしれません。

コードに関してはオープンにしてもOKと寛大なお言葉をいただいてるので、そのうちコード自体公開する予定ではあるのですが、時間制約の都合上かなり雑なコードに仕上がっているので流石にもうちょっと整理してから公開する予定です:bow:
とりあえず興味ある方はTwitterでDMいただければ今の雑なやつでも個別にシェアしますので気軽にお声がけいただければと思います。

Slack app、直近まだまだ開発しやすい環境が整っているとは言いにくい状態という印象ですが(scope変更を含むバージョンアップしたくなったときどうしたらいいんだろうとかとか)、
Slackがかなり多くの企業やユーザーに利用されているプラットフォームではあるのは間違いないのと、まだまだアプリのマーケットプレイスとしては黎明期で何かしら参入できる余地かなり大きいと思うので、個人開発者にとっても今からiOS/Androidアプリ作るよりはワンチャンあるのではと思ってます。
(ただし、真面目に課金とかしようと思うとSlack上で決済できる仕組みが今のところなく、何かしら自分で頑張らないといけないのが若干ハードル高そうですが...)
是非この機会にSlack appチャレンジしてみてはいかがでしょうか。

そして是非皆様ホメルくんつかっていただいてフィードバックいただければと思います!
https://homerukun.atengagement.com/
https://slack.com/apps/ADE8ADF6E--

Have a good Slack :tada:

Appendix

ホメルくんの環境について

  • heroku
  • PostgreSQL
  • Ruby (2.5.1)
  • Ruby On Rails (5.2.1)

せっかくなので技術的チャレンジ込みで Go + Serverless な感じでやってみようと思ったのですが、思いの外時間かかりそうだったので手っ取り早く実装できるRailsに落ち着いてしまいました...
大体の雰囲気は掴んだので次回はそちらで挑戦してみたい。


  1. hubotとかが出始めた頃と比較して、当時の機能は今はほとんどDeprecatedになってますね...