Slack で動いてるボットの処理が長い場合、フィードバックとしてインジケーターを出すということをやった

Slack で動いてるボットの処理が長い場合、フィードバックとしてインジケーターを出すということをやった

いま golang の練習用に作成しているボットには、URL をわたすと、そのサイトのキャプチャを撮影する機能があります。諸事情からボットのいるマシンとは別の場所、Heroku に設置していますが、起動が遅かったり、キャプチャ自体が遅かったりするので、ちゃんとやっていってるのかわかりません。そこで、ローディングインジケーターを出すことにしました。

できあがり

f:id:mmmpa:20161120045725g:plain

「こはる」とは社の Slack にいるボットですが、対抗して golang でつくっているのが「ごはる」です。かわいいですね。

アイコンの元ネタには以下を使わせてもらっております。

github.com

インジケーター

まずインジケーターを作成しました。実際はベタッと書いてから切りだしましたが、とにかくまずインジケーターを用意します。できあがりを想像してワクワクするのが原動力です。

type CaptureLoadingIndicator struct {
    indicators []string
    length     int
    step       int
}

func (c *CaptureLoadingIndicator) initialize(indicators []string) {
    c.indicators = indicators
    c.length = len(c.indicators)
    c.step = c.length - 1
}

func (c *CaptureLoadingIndicator) next() string {
    c.step = ((c.step + 1) % c.length)
    return c.indicators[c.step]
}

func createCaptureLoadingIndicator() *CaptureLoadingIndicator {
    c := (&CaptureLoadingIndicator{})
    c.initialize([]string{"▖", "▘", "▝", "▗"})

    return c
}

goroutine

キャプチャ機能では、キャプチャされた画像は Heroku からダイレクトに Slack に投稿されますが、キャプチャできたかどうかは Response で受けとれます。よって、Heroku へのリクエスト開始をインジケーターの開始、Response が帰ってくれば終了するとします。

細かい処理ははぶいて、goroutine 部です。http.PostForm は同期的に処理されますから、基本的に以降の処理をブロックします。これを goroutine に追いだして本プロセスとは並行に行いつつ、終わるまでの間はインジケーターを表示します。

Big Sky :: golang の channel を使ったテクニックあれこれを参考に、処理が終わるまで待つ処理を書きます。

// インジケーターを表示するメッセージの TS を取得する
var ind slack.ChatPostMessagesResponse
slack.ChatPostMessage.Strike(&ind, string(m.Channel), "starting...")

// 以下で goroutine ポーリングする
q := make(chan CaptureServerResult, 2)

go func() {
    // Heroku へリクエスト開始
    resp, err := http.PostForm(captureServer, values)
    // リクエスト終わり
    q <- CaptureServerResult{resp, err}
}()

indicator := createCaptureLoadingIndicator()
for {
    if len(q) > 0 {
        break
    }
    // 上記で取得した TS をターゲットとして、chat.update をかける
    slack.ChatUpdate.Strike(nil, string(m.Channel), ind.TS, fmt.Sprintf("loading `%v`", indicator.next()))
    // update 間隔は加減しましょう
    time.Sleep(200 * time.Millisecond)
}

slack.ChatUpdate.Strike(nil, string(m.Channel), ind.TS, "loaded")

resp := <- q
// response をもとにした処理が続く

<- q は処理をブロックしてしまうので、なんの動きもない重い処理、待ちの時に他の処理をさせておきたい場合はチェッカーとしては使えないんですね。

しかし、Channel をうまく利用することにより、簡単に実現することが出来ました (先達の知識に感謝)。こういうのは大体、開始や interval 処理ではなく、終了がめんどくさかったりするのですが、それが特に問題にならずよかったです。

あと func(){}() ができるのが色々便利ですね。