コネヒト開発者ブログ

コネヒト開発者ブログ

Slack上での出欠確認を楽にする「点呼さん」というbotをつくりました

点呼さんのイメージ
点呼さんのイメージ

こんにちは。CTOをやっている @itosho です。コネヒトでは「Beyond a Tech Company」というテックビジョンを掲げているのですが、テックカンパニーの条件の一つとして「社内の人がテクノロジーの恩恵を受けている」ということを重要視しています。ですので、以前からインナーコミュニケーションを盛り上げるための開発社内ツールの開発を積極的に行っています。というわけで、この記事では最近開発したSlack Botである「点呼さん」の紹介をしたいと思います。

点呼さんとは?

コネヒトでは昔からSlack上でイベントの出欠をとることが多く、こんな感じで「参加」「不参加」のリアクションをつけてもらうことで出欠確認を行ってきました。

f:id:itosho525:20210730133947p:plain
出欠確認の様子

社員の数が少ないうちはこのやり方で問題なかったのですが、嬉しいことに社員の数がどんどん増えてきた結果、誰がリアクションしていないかを確認してリマインドを送ったり、誰が参加者なのかを把握したりする手間が増えてきました。解決策として、tmpチャンネルを用意して、対応を行った人から抜けるという方法も一部では行っていますが、ものによってはToo Muchなケースも多くありました。もちろん、このまま人力で頑張るのはスケールしないやり方ですし、何よりテックカンパニー感がありません。

そこで開発したのが「点呼さん」です。点呼さんはSlackのリアクションで出欠をとる時のリマインドや集計のサポートをしてくれるSlack Botです。

使い方

基本的な使い方

まずは点呼さんを出欠確認を行いたいチャンネルにinviteします。

f:id:itosho525:20210730135510j:plain
点呼さんをinviteする

その後、こんな感じのフォーマットで点呼さんにmentionを送ります。(最後のモザイクがかかっている部分はSlack内のメッセージURLです)

f:id:itosho525:20210730135934j:plain
点呼さんにmentionを送る

そうすると、点呼さんから対象のSlackメッセージにリアクションをまだつけていない人のリストが返ってきます。(モザイクがかかっている部分はユーザーIDの一覧です)

f:id:itosho525:20210730140428j:plain
点呼さんからリストが届く

あとはコードブロックの部分をコピペしてもらえば、リマインドなど自由にメッセージを送ることが出来ます。

ちなみに点呼さんの返事をDMにしている点とそのまま点呼さんがリマインドしないようにしている点は意図的で、人によっては自分のユーザー名をキーワード通知している可能性があるのと時間帯を気にせず使えるようにしてもらいたいというのが理由の一つです。その代わり、コピペですぐメッセージを送れるようにしています。

発展的な使い方

@点呼さん 教えて!◯◯ SlackURL が基本的なフォーマットになっていて、リアクションをしていない人のリストを作る以外にも…

  • @点呼さん 教えて!参加する人 SlackURL とすると参加する人のリスト
  • @点呼さん 教えて!不参加の人 SlackURL とすると不参加の人のリスト
  • @点呼さん 教えて!リアクション名 SlackURL とすると特定のリアクションをした人のリスト
  • 例えば、doneリアクションをした人みたいな使い方を想定

といったようなリストの確認も出来ます。

また、抽出する対象をチャンネルにjoinしている人ではなく、特定のSlackグループで絞り込むことも出来ます。ユースケースとしては、たくさん人がいるチャンネルだけど、開発組織の歓迎会の出欠をエンジニアやデザイナーにリマインドしたい時なんかを想定しています。

f:id:itosho525:20210730142746j:plain
グループで絞り込みを行いたい時

点呼さんの実装

特殊なことはしておらずSlack Botをつくって、そこから上述の仕様を実現するためにSlack APIを適宜叩いています。Goで書いているのでSlackのAPI連携やメンション時のイベントハンドリングなどは slack-go/slackを利用させてもらっています。

コネヒト独自の仕様のところは参考にならないと思いますが、SlackのAPI連携している箇所はもしかしたら参考になる部分もあるかもしれないので、コード片を晒しておきます。プロダクションコードではないので、ゆるく書いている箇所もありますが、NYSL的な感じで自由に使ってください。

// slack-go/slack をimportしている前提です

// BotのSlackクライアントとuser APIのSlackクライアントが存在するのでこんな書き方になっています
type User struct {
    Client *slack.Client 
}

//  全ユーザーを取得する処理
func (u User) GetAllUsers() (users map[string]string, err error) {
    res, err := u.Client.GetUsers()
    if err != nil {
        return users, err
    }

    users = map[string]string{}
    for _, v := range res {
        if !v.IsBot {
            users[v.ID] = v.Profile.DisplayName
        }
    }

    return users, nil
}

// 指定したチャンネルにjoinしているユーザーを取得する
func (u User) GetUserIDsInChannel(channelID string) (userIDs []string, err error) {
    params := &slack.GetUsersInConversationParameters{ChannelID: channelID, Limit: 1000}
    res, _, err := u.Client.GetUsersInConversation(params)
    if err != nil {
        return userIDs, err
    }

    return res, nil
}

// 指定したSlackグループ名(人間が利用するもの)からグループID(APIで利用するもの)を取得する
func (u User) GetGroupIDs(groupHandles map[string]bool) (groupIDs []string, err error) {
    res, err := u.Client.GetUserGroups()
    if err != nil {
        return groupIDs, err
    }

    for _, v := range res {
        if _, ok := groupHandles[v.Handle]; ok {
            groupIDs = append(groupIDs, v.ID)
        }
    }

    return groupIDs, nil
}

// 指定したグループIDに所属するユーザーID一覧を取得する
func (u User) GetUserIDsInGroups(groupIDs []string) (userIDs []string, err error) {
    for _, v := range groupIDs {
        res, err := u.Client.GetUserGroupMembers(v)
        if err != nil {
            return userIDs, err
        }
        userIDs = append(userIDs, res...)
    }

    return sliceUnique(userIDs), nil
}

// 指定したSlackメッセージ(チャンネルIDとタイムスタンプ)から指定したリアクションをしたユーザーID一覧を取得する
func (u User) GetActionedUserIDs(channelID string, timestamp string, reactionName string) (userIDs []string, err error) {
    item := slack.ItemRef{Channel: channelID, Timestamp: timestamp}
    params := slack.GetReactionsParameters{Full: true}
    res, err := u.Client.GetReactions(item, params)
    if err != nil {
        return userIDs, err
    }
    if len(res) < 1 {
        return userIDs, errors.New("unexpected length of GetConversationReplies")
    }

    if reactionName == "" { // The target is all reaction.
        for _, reaction := range res {
            userIDs = append(userIDs, reaction.Users...)
        }
        userIDs = sliceUnique(userIDs)
    } else {
        for _, reaction := range res {
            if reaction.Name == reactionName {
                userIDs = reaction.Users
                break
            }
        }
    }

    return userIDs, nil
}

実装時に意識をした点

カッとなって勢いで作った部分もあるのですが、その中でもユーザーに入力を求める類のSlack BotはUNIXコマンドに似たところがあると思うので、Unixの哲学を意識し、出来る限りシンプルさを保つようにしました。

具体的には、先ほど述べたようにリマインド機能やDMではないチャンネルで結果を返すことなども検討したのですが、それを実現しようとするとオプションがどんどん増えていってしまいインターフェイスも複雑になります。そうすると、結果的に何でも出来るけど何が出来るかわからないツールになるリスクがあるので、オプションは最小限に留めるようにしました。そして、出欠確認のリマインドを楽にするという課題にフォーカスし @点呼さん 教えて!まだの人 SlackURL だけ覚えれば、まずはBotが使えるという状態を作り、そこでこのBotの利便性を感じてもらってから発展的な使い方をしてもらえるような工夫を行いました。

実際、利用してくれる方はちょくちょくいるので、ある程度のその設計は上手くいったのかなと思っています…!

最後に

会社が大きくなると、事業だけではなく、組織の面でも様々な課題が生まれます。そういった課題もテクノロジーやエンジニアリングの力で解決出来る会社が、冒頭にも述べたようにテックカンパニーだと僕は考えているので、引き続き、技術で勝っていくことで、プロダクトのユーザーだけではなく、社内の人も少しでも幸せにしていくぞ!

というわけで、コネヒトでは自分の書いたコードで社会や組織を良くしていきたいエンジニアを大大大募集しております! 1mmでも興味がありましたら、下記のWantedlyから「話を聞きに行きたい」ボタンをポチっと押していただくか、僕にDMを雑に送っていただいても構いませんので、まずはカジュアルにお話させていただければと思います!

hrmos.co