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

OTOBANK Engineering Blog

オトバンクはコンテンツが大好きなエンジニアを募集しています!

勢い余ってtmuxのステータスライン上でSlackの未読件数を見れるようにした話

どうも。オトバンク麦芽系エンジニアのasmzです。最近家のビールが金麦(元々ビールですら無い)からバーリアルに格下げされて辛い日々を過ごしています。

自分の前回のエントリからもう半年ぶりだし、このままだとオトバンクにはbtoさんしかエンジニア居ないんじゃないかと思われかねないので、生存確認も兼ねてたまにはちょっと書いておこうと思います。

engineering.otobank.co.jp

今回勢い余ったこと

さて今回のネタですが、実は一つ前のbtoさんのエントリ「dotfilesの設定を自動化する」に強く影響を受けています。

engineering.otobank.co.jp

このエントリのdotfiles管理方法を自分も導入していろいろ環境いじってたら楽しくなってきて、せっかくなら基本的な開発作業はターミナルで出来るようにしようと思い、ターミナルを全画面で開き、tmuxで画面分割して、Vimで開発というスタイルに転向することにしたのです。(この辺から徐々に勢いづいちゃっている感ある)

↓こんなかんじ

ところで、前回のエントリに書いたとおり、オトバンクでは社内連絡にSlackを利用しています。私の場合これまで、自分宛のメンションはデスクトップ通知させ、その他未読があるかどうかはChromeのタブで確認していました。

全画面のターミナルで作業してると、この確認が画面切り替えないと出来ないんですよねー。たまーにブラウザに画面切り替えて、未読無いのを確認して、ターミナル画面戻して作業を続ける、っていう行為に何となく無駄を感じます。感じますよね?ね?

で、つい勢い余って「じゃあ、ターミナル環境でもSlackの未読確認できるようにすればいいじゃん」という、やや変態的マニアックな発想が今回のエントリです。具体的にはtmuxのステータスライン余ってるし、この辺にちょいちょい未読件数表示されたら便利かな、なーんて。

実装方針

SlackのAPI仕様読んでみると、都度HTTPリクエスト投げて情報を取得する「WebAPI」と、Websocketでリアルタイムに情報を取得する「Real Time Messaging API」があるようです。

ちゃんとやるならReal Time Messaging APIの方を使って、リアルタイムに表示するのがいいんでしょうが、今回はそもそも需要自体がやや変態的マニアックですし、自分でちょこっと使うくらいならそんなにリアルタイムじゃなくてもいいかな、と思ってWebAPIで定期的にポーリングするようにしました。(実際やってみるとちょっと微妙だったりするんですが)

というわけで、こんな感じでやってみます。

  1. SlackのWebAPIで指定したチャンネルの未読件数を取得するシェルスクリプトを作成
  2. tmuxのステータスラインに1.のシェルスクリプト実行結果を表示するよう設定
  3. tmuxのステータスライン更新間隔(ポーリング間隔)を設定

ちなみにChromeのアイコンと同等にするなら、所属する全チャンネルすべてを監視対象にする必要があります。ただしその場合、重要でないチャンネルにも反応してしまうのと、後述しますが今回のやり方だとちょっと不都合があるので、今回は「指定したチャンネルの未読件数」だけ取得する方針としております。

あ、SlackのAPIを使用するには事前にこちらのページよりOAuth用のTokenを発行しておく必要がありますよ。

Slack WebAPIの利用

今回使用しているWebAPIと、その利用方法は以下の通りとなります。

channels.list

チーム内全チャンネルの概要一覧を取得するAPI

URL: https://slack.com/api/channels.list?token=<your api token>&exclude_archived=<0 or 1>

実行結果例(公式ページサンプルより):

{
    "ok": true,
    "channels": [
        {
            "id": "C024BE91L",
            "name": "fun",
            "created": 1360782804,
            "creator": "U024BE7LH",
            "is_archived": false,
            "is_member": false,
            "num_members": 6,
            "topic": {
                "value": "Fun times",
                "creator": "U024BE7LV",
                "last_set": 1369677212
            },
            "purpose": {
                "value": "This channel is for fun",
                "creator": "U024BE7LH",
                "last_set": 1360782804
            }
        },
        ....
    ]
}

今回の利用方法:

  • 指定されたチャンネル名が「name」と一致する場合のみ、そのチャンネルID「id」を取得する
  • exclude_archivedオプションはアーカイブ済のチャンネルを取得対象とするかどうか(今回は除外するため1を設定)

channels.info

channelパラメータに指定したチャンネルの詳細情報を取得するAPI

URL: https://slack.com/api/channels.info?token=<your api token>&channel=<channel id>

実行結果例(公式ページサンプルより):

{
    "ok": true,
    "channel": {
        "id": "C024BE91L",
        "name": "fun",

        "created": 1360782804,
        "creator": "U024BE7LH",

        "is_archived": false,
        "is_general": false,
        "is_member": true,

        "members": [],

        "topic": {},
        "purpose": {},

        "last_read": "1401383885.000061",
        "latest": {},
        "unread_count": 0,
        "unread_count_display": 0
    }
}

今回の利用方法:

  • channels.listAPIで取得したidをAPIのパラメータに設定して実行
  • 実行結果よりunread_count_displayの値を取得
    • unread_countという項目もあるが、これはユーザのチャンネル入退出のような、ユーザ発言以外の情報も件数としてカウントされる(今回はその件数は不要)

ホントはchannels.info一発で指定したチャンネルの未読件数取れると良かったんですが、このAPIはパラメータとして「チャンネル名(name)」ではなく「チャンネルID(id)」が必要で、チャンネルIDはSlackサービス上は見えないため、一度channels.listでチャンネル名からIDを逆引きする必要があります。

詳しいAPI仕様については、公式ページのリファレンスをご参照ください。

JSONパーサー「jq」の利用

今回の処理はシェルスクリプトとして実装します。 API実行については上記のURLをcurlコマンドで投げてやれば済むんですが、その実行結果はJSON形式で返却されます。

ここで「はて、シェルスクリプトでJSONのパースってどうやんだっけな?」と思ったのですが、ググってみると以下のパーサーが使えそうなことがわかりました。

jq jq is a lightweight and flexible command-line JSON processor. (http://stedolan.github.io/jq/)

実は今回これをやるまでjqというモノの存在を知らなかったのですが、調べてみたらこれかなり高機能なJSONパーサーなんですね。

詳しい使用方法は上記の記事に読んでもらうとだいたい分かるのでそちらにお任せしますが、簡単にどんなことができるかご紹介します。

jqコマンド基本

まずこんな感じの何となくSlackWebAPIもどきのJSONファイルを例に説明します。

{"channels":[{"id":1,"name":"otobank","is_member":true},{"id":2,"name":"febe","is_member":false}]}

単純にこのファイルをパース・整形するにはこんな感じにします。

$ cat jqtest.json | jq '.'
{
  "channels": [
    {
      "id": 1,
      "name": "otobank",
      "is_member": true
    },
    {
      "id": 2,
      "name": "febe",
      "is_member": false
    }
  ]
}

cat jqtest.jsonの部分は実際にはcurlでAPI実行しているイメージです。.はJSONのルートを示す意味となり、ルートからすべてのデータが表示されます。

そのため、以下のように指定するとその指定した階層以下に絞りこまれます。

$ cat jqtest.json | jq '.channels'
[
  {
    "id": 1,
    "name": "otobank",
    "is_member": true
  },
  {
    "id": 2,
    "name": "febe",
    "is_member": false
  }
]

さらに、JSON内に配列要素[ ]が含まれる場合、以下の指定でその配列の中身を取得できます。

$ cat jqtest.json | jq '.channels[]'
{
  "id": 1,
  "name": "otobank",
  "is_member": true
}
{
  "id": 2,
  "name": "febe",
  "is_member": false
}

ちなみにシェルスクリプトのように絞込結果をパイプで繋ぐこともできます。以下の例ではchannels配列よりid要素の値を取得しています。

$ cat jqtest.json | jq '.channels[] | .id' 
1
2

jqがさらにすごいのは、絞り込みにSQLのような条件指定ができることです。

$ cat jqtest.json | jq '.channels[] | select(.is_member == true)'
{
  "id": 1,
  "name": "otobank",
  "is_member": true
}

ここまでを踏まえ、以下のような感じにすれば今回やりたい「チャンネル名」から「チャンネルID」の逆引きができるわけです。

$ cat jqtest.json | jq '.channels[] | select(.name == "otobank") | .id'
1

何となくjqの雰囲気つかんでもらえましたでしょうか?今回のエントリに限らず、コマンドラインでJSONデータ扱う場面全般で使えるコマンドですね。

というわけで、極力環境依存しないようにしたかったのですが、jqは使用しますので事前導入が必要です。ほ、ほら、便利だし、きっとこれ以外の作業でも使う機会ありますよ!きっと!

シェルスクリプト実装内容

前置きがかなり長くなってしまいましたが、ここまで説明したSlack WebAPIとjqコマンドを利用して作成したシェルスクリプトがこちらになります。

https://gist.github.com/asmz/84734cdc0a18e72eec25#file-slack_notifier-sh

  • 前提
    • curl、jq導入済み
    • tmux起動前に環境変数としてSLACK_API_TOKENを設定

今回メインとなるAPI実行~結果表示部分について抜粋します。

API_URL_BASE="https://slack.com/api/"
API_CHANNELS_LIST="channels.list"
API_CHANNELS_INFO="channels.info"
HTTP_GET="$(which curl) -s -G"
JQ="$(which jq) -r"

...

channel_name=$1 
channel_id=`${HTTP_GET} "${API_URL_BASE}${API_CHANNELS_LIST}?token=${SLACK_API_TOKEN}&exclude_archived=1" \\
            | ${JQ} '.channels[] | select(.name == "'${channel_name}'") | .id'`

シェルスクリプトの引数で渡されたチャンネル名から、チャンネルIDを取得しています。上記で説明したサンプルとほぼ同じ記載ですね。

# Get unread count by channel id
unread_count=`${HTTP_GET} "${API_URL_BASE}${API_CHANNELS_INFO}?token=${SLACK_API_TOKEN}&channel=${channel_id}" \\
              | ${JQ} '.channel.unread_count_display'`

先に取得したチャンネルIDを用いて未読件数を取得する部分です。jqで何をやっているか、何となく察しはつくんじゃないでしょうか。

# Set label
unread_label=""
if [[ ! -z ${unread_count} && ${unread_count} -gt 0 ]]; then
    unread_label="[${channel_name}:${unread_count}]"
fi
echo ${unread_label}

未読件数がちゃんと取得できて、かつ未読件数が1件以上あれば、整形して標準出力します。ここの標準出力された結果は、最終的にtmuxステータスラインへの表示に使用されます。

他にも若干のエラー処理や後述する2重起動防止処理などを入れています。

tmuxの設定

あとは、このシェルスクリプトをtmuxステータスラインの更新タイミング都度実行させるため、.tmux.confを編集します。

# ステータスラインの更新間隔(秒)
set -g status-interval 10
# ステータスライン右の幅
set -g status-right-length 100
# ステータスライン右の表示内容
set -g status-right '#[fg=yellow]#($HOME/.tmux/slack_notifier.sh channel1)#[default] ... '

この記載だと10秒毎にchannel1というSlackチャンネルの未読件数を監視し、未読があればステータスラインの右に黄色い文字で表示されるという設定になります。

ちなみにこんな風に書くと、複数のチャンネルを同時に監視できます。(一度にたくさんのAPI投げるとSlackから怒られそうなので、sleep入れて実行タイミングずらしてみてます)

set -g status-right '#[fg=yellow]#($HOME/.tmux/slack_notifier.sh channel1)#(sleep 1;$HOME/.tmux/slack_notifier.sh channel2)#(sleep 2;$HOME/.tmux/slack_notifier.sh channel3)#(sleep 3;$HOME/.tmux/slack_notifier.sh channel4)#(sleep 4;$HOME/.tmux/slack_notifier.sh channel5)#(sleep 5;$HOME/.tmux/slack_notifier.sh channel6)#[default] ... '

更新間隔の注意事項

tmuxは更新間隔が来たら問答無用でシェルスクリプトをKickします。そのため、シェルスクリプト処理が終わる前に更新タイミングが来てしまった場合、別プロセスでもう一つ同じシェルスクリプトが起動してしまうことになります。(もしシェルスクリプト実行が長引くと、場合によってはどんどんプロセスが増殖…)

それを防ぐため、シェルスクリプトでは念のため最初の方に二重起動防止(既に別プロセスで実行されていたら、今のシェルスクリプトはexitさせる)処理を入れています。

ただし、tmuxがステータスラインに表示するのは「処理が終了した後」なので、以下の図のように①のシェルスクリプトがまだ実行中に②が起動し、二重起動エラーとして処理終了すると、先に②の実行結果がステータスライン表示に使用されます。②はエラー終了なので特に表示するものはなく、結果としてここで何かステータスラインに表示があっても消されてしまいます。

その後①が終了すればまた未読件数表示されますが、恐らくその次のターンになるとまた消される、というのを繰り返すことになります。

そのため、図の下のようにシェルスクリプトの実行時間より大きな間隔をtmuxの更新間隔に設定しておく必要があります。

自分の環境だと1つのシェルスクリプト実行が1~2秒くらいだったので、ステータスライン更新間隔を10秒にしています。

まぁ、今回の作りだと裏でバシバシとHTTPリクエスト投げることになるので、いずれにしてもあまり短い間隔にはしないほうが良いかと思いますが…。(この辺が今回WebAPI使う方針にして微妙だなぁと思ったところ…)

実行結果

実際に動かしてみるとこんな感じになります。

未読なし

未読あり

複数チャンネル監視時はこんな感じ

未読がないチャンネルは何も表示されず、1件以上ある場合のみ表示されます。表示内容が多くて表示しきれない場合は、.tmux.confstatus-right-lengthの値を調整して下さい。

これで安心してターミナルにかじりついていられますね!ね!

残課題

自分が所属する全チャンネルの未読監視

実はシェルスクリプト内でループ回して、所属しているチャンネルすべての未読件数を取得するやつも作ってはみたんですが、その分シェルスクリプトの実行時間が長くなることで、上の「更新間隔の注意事項」に書いた問題がより顕在化します。

所属チャンネルが20個くらいあると30秒くらい、Slackさんを気にしてループ途中にsleepとか入れるともっと時間がかかるので、ステータスラインの更新間隔は分単位くらいにしておく必要が出てきます。

それで問題ない人はそれでもいいんですが、ステータスラインに他にも情報出したい人は、更新間隔が引きづられてしまうので、何かこの辺もうちょっとうまく解決したい感じあります。

Direct Messages、Private Groupsに未対応

準備する時間が足りなかったんですが、今回のシェルスクリプトは「チャンネル」の未読件数しか対応していません。

類似のAPIは用意されているようなので、必要があれば今回の処理を参考に適宜用意してみてくださいー。


以上が今回勢い余ってしまったいきさつになります。ただ、まだちょっと微妙な動きあるので、やっぱ勢いに任せてやるのは良くないですね、何事も。(飲み会後のtwitterのつぶやきとかも、後になって消したくなりますしね)

ちなみに私はこの設定をbto式dotfiles管理に含めて、どこでも設定、修正できるようにしています。

https://github.com/asmz/dotfiles

いろいろカスタマイズして、快適なターミナルライフをenjoyしましょう!

まとめ

これだけ言っておいてなんですが、別に無理にターミナル使わなくてもいいんでAndroid StudioやXcodeでスマホアプリ作ってくれるエンジニアも実は募集中です!(切実)