見出し画像

Go + AWS LambdaでSlackの社員抽選botを作った話

(この記事は2019年11月15日にACALLブログにて公開された記事です。)

今年の4月入社の新卒エンジニアujiです。
今回は、Slackで動作する社員抽選botを作った話をしたいと思います。
ACALL初の技術系記事らしいですが、気を引き締めずゆるく書いていきます。

作ったきっかけ

会議のファシリテータをランダムに決めたい
ACALLでは週の初めに社員全員で会議をします。元々ファシリテータは毎回固定だったのですが、各所から
「セールスからエンジニアまでランダムでバトンタッチした方が面白いんじゃないか」
という提案があり、ツールで選ばせる事にしました。

ネットの海を漁ると抽選ツールは色々出てきますが、細部まで自分でカスタマイズできた方が面白そうだったので、一から作る事にしました。

概要

ACALLではチャットツールにSlackを採用しています。
社員が誰でも手軽に利用できるもの提供するには、Slackアプリとして実装するのが一番良さそうだったので、SlackのWebhook·APIと連携するbotを開発することにしました。

環境
開発したアプリはAWS Lambdaで動かしています。AWS Lambdaのメリットとしては
・サーバーレスで管理が楽
・少ないリクエスト実行時間であれば料金を抑えられる(参照)
などがあります。
本アプリは現状自社のみでの使用なので無料枠の範囲で収まっています。

開発言語
業務ではRubyでサーバー開発をしていますが、今回は、個人的に最近ハマっているGo言語を採用しました。
nlopes/slack というSlackのAPIをいい感じに叩けるパッケージがあったので活用しています。

言語にこだわりが無い場合は、Slack社が公式でサポートしているフレームワーク「Bolt」(Node.js)などを使った方が良いかもしれません。

ストーリー
抽選のフローとしては、以下のようになります

画像1

①botへのメンションをトリガーにLambda Functionを起動
②SlackのAPIを使用し、ユーザー情報を取得
③ユーザーを抽選
④SlackのAPIを使用し、抽選結果のメッセージを投稿

開発

実際に踏んだ手順に沿って、軽く紹介します。

AWS Lambdaアプリの雛形を作る
AWSが提供しているAWS SAM (Serverless Application Model) と呼ばれるサーバーレスアプリケーション構築用のオープンソースフレームワークを使うと、コマンド一発でLambdaアプリの雛形が作成できます。
今回はGo言語の雛形を作成します。

sam init --runtime go

上記のコマンドを実行すると雛形が作成されます。

.
├── Makefile
├── README.md
├── hello-world
│   ├── main.go
│   └── main_test.go
└── template.yaml

main.goを見ると、APIGatewayからのリクエストを引数として受け取り、レスポンスを返すhandler関数があります。
handlerのリクエストとレスポンスは、AWSが提供している aws/aws-lambda-go パッケージで独自に定義されている型になっています。

func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error)

このhandler内に抽選botの処理を実装していきます。

Slackからのwebhook(Event API)を受け取る
Slackには、イベントが発生した際に、指定のURLをhookするEvent APIと呼ばれる機能があります。
これを使うことで、bot宛てのメンションをトリガーにAWS GatewayのURLをhookするフローが作れます。

リクエストの認証
はじめに、Event APIによるリクエストの認証部分を実装していきます。
Event APIを利用したBotの開発にはリクエストに含まれるverification tokenによる認証が必要です。
verification tokenの認証処理は nlopes/slack/slackevents パッケージを使うと簡潔に書けます。

// handlerの内部
	reqBody := request.Body
	eventsAPIEvent, err := slackevents.ParseEvent(
		json.RawMessage(reqBody),
		slackevents.OptionVerifyToken(
			&slackevents.TokenComparator{
				// verificationTokenにslack側で生成されるtokenを代入
				VerificationToken: verificationToken,
			},
		),
	)

ParseEvent関数を使うと、verification tokenの認証しつつ、リクエストボディをEventsAPIEvent型にパースしてくれます。

url verification用のレスポンス実装
Event APIのhook先としてURLを登録する際、以下のようなパラメータを持った検証用のリクエストが送信されます。

{
   "token": "Slackのtoken",
   "challenge": "Slack側で生成されたランダムな文字列",
   "type": "url_verification"
}

このリクエストに対し、”challenge”の内容をそのまま送り返すレスポンスが必要になります。
レスポンスが無いと、slackからはbotが正常に動作していないと見なされてしまいます。

認証時にパースしたeventsAPIEventのTypeフィールドを参照し、url verificationのレスポンス部分を実装します。

// handlerの内部ent APIの情報を受け取る	if eventsAPIEvent.Type == slackevents.URLVerification {eventAPIEventのフィールドをたどると、色々な情報が得られます。		var r *slackevents.ChallengeResponsebot宛のメンションによるhookかどうかの確認と、返信先チャンネルの取得を行います。
		err := json.Unmarshal([]byte(reqBody), &r)
		if err != nil {
			log.Print(err)
			return events.APIGatewayProxyResponse{}, err
		}
		return events.APIGatewayProxyResponse{
			StatusCode: 200,
			Body:       r.Challenge,
		}, nil
	}	

Event APIの情報を受け取る
eventAPIEventのフィールドをたどると、色々な情報が得られます。
bot宛のメンションによるhookかどうかの確認と、返信先チャンネルの取得を行います。

if eventsAPIEvent.Type == slackevents.CallbackEvent {
	innerEvent := eventsAPIEvent.InnerEvent
	log.Print(innerEvent.Type)
	switch ev := innerEvent.Data.(type) {
		case *slackevents.AppMentionEvent:
		channelID := ev.Channel

		// ここにユーザー取得、抽選の実装を書く

		return events.APIGatewayProxyResponse{
			StatusCode: 200,
		}, nil
	}
}

Slack APIでチャンネルのユーザー情報を取得
Slackが提供するAPIを使って、メンションが送られたチャンネルのユーザーのID一覧を取得します。
Slack APIの利用もnlopes/slackを使えば簡単です。

bot := slack.New(token) // token: bot user oauth access token
params := slack.GetUsersInConversationParameters{ChannelID: channelID}
userIDs, _, err := bot.GetUsersInConversation(&params)

slack.New()で作成したslack.Client型で任意のAPIに紐づいた関数を使用します。

抽選
取得したユーザーID一覧から1つのIDを抽選します。
math/rand パッケージを使用しランダムに抽選します。

rand.Seed(time.Now().UnixNano())
userID := userIDs[rand.Intn(len(userIDs)-1)]


抽選結果メッセージを投稿
ユーザー情報取得で登場したslack.Client型のPostMessage関数を使用し、メンションに抽選結果メッセージを返信します。

text := " が当選しました"
bot.PostMessage(channelID, slack.MsgOptionText(text, false))


デプロイ
実装したアプリをデプロイして挙動を確認します。
プロジェクトのルートディレクトリで以下のコマンドを実行し、アプリをAWSにデプロイします。
(AWS CLIの設定に合わせて –profile オプションの値を変更してください。)

// コンパイル
$ make build

// s3のバケットを作成
$ aws s3 mb s3://sam-template-store --profile uji

// バケットにテンプレートアップロード
$ sam package --template-file template.yaml --output-template-file output-template.yaml --s3-bucket sam-template-store --profile uji

//デプロイ
$ sam deploy --template-file output-template.yaml --stack-name sam-template-store --capabilities CAPABILITY_IAM --profile uji

Makefileに書くと楽です。

動作を確認しつつ修正
デプロイ、Slack botの設定が終わるとアプリが利用可能になります。
実際にbot宛にメンションを送り、期待動作がされるか確認します。

以下、なかなか言うことを聞いてくれないbotとの格闘の跡です。

画像2


動かしてみる

修正を重ね、問題が無さそうだったので、ACALLのワークスペースにbotを投入。
初お披露目で全体会議のファシリテーター決めをさせたところ、Slack ワークフローが当選してしまいました…
(しかも2回連続…)

slack.Client.GetUsersInConversationがワークフローもユーザーとして取得してきてしまうみたいです。

画像3


草スタンプまみれです

まとめ

色々と課題はありますが、とりあえず動きました。
今では、全体会議のファシリテーター決めは毎回抽選botが行なっています。
また、「チャンネルのメンバー内での抽選だけでなく、ユーザーグループ内での抽選もしたい」という声があったのでEvent APIに加えてInteractive Message機能を使って抽選メニューを設置して運用しています。

Interactive Messageの部分は機会があれば紹介したいと思います

作ったアプリはGitHubで公開してますので、詳細が気になる方は是非ご覧ください。

エンジニア募集してます
ACALLでは、バックエンド、フロントエンド、インフラ、IoTなど、各種エンジニアを募集しています。
カジュアル面談から受け付けてますので気軽に問い合わせください!


みんなにも読んでほしいですか?

オススメした記事はフォロワーのタイムラインに表示されます!