3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Spreadsheet で Slack bot のカスタムレスポンスみたいなのを管理したい

Last updated at Posted at 2020-12-16

概要

本記事はちょっとした工夫で効率化!01【PR】パソナテック Advent Calendar 2020 の 17 日目の記事です。
Advent Calendar への参加は初めてなので緊張します。

背景

弊社では以前、slackbot で「内線」と打つと各社員の内線番号を表示する、というのをカスタムレスポンスで管理していました。
しかし、改行できず(※)見づらかったり、変更があった場合に情報を一元管理できなかったり……という問題がありました。
その他にも、いまいち融通のきかないカスタムレスポンスを便利にすることで Slack を利用した業務の効率化ができないかな、と考えたのが元になります。

(※)
ついこないだ知ったのですが、改行コード(\n)認識してくれるんですね。
これ知った瞬間「この記事書く意味ほぼなくなったんじゃね?」と思ったんですが、使い方次第で便利さ残ってるし、何より他に思いつかなかったので書いちゃいます :sweat_drops:

手法

  • レスポンスしたい内容は Spreadsheet で管理する
  • Slack bot を作成し、Spreadsheet の内容を使ってカスタムレスポンスする

という方法で作成することにしました。
なお、「実行場所が変わってもいちいち実行環境を準備したくない」「シングルバイナリで管理したい」という考えから、Slack bot の作成には Golang を用いました。
bot のコードと説明はこの記事の一番最後に記載します。

構成

名称未設定.png

  • bot は定期的に Spreadsheet を見て情報をレスポンス内容を更新する
  • ユーザは bot を invite 済みのチャンネルで元ワードを post する
  • 元ワードと一致した post を見つけた bot はカスタムレスポンスをそのチャンネルに送信する

という動作をします。

Spreadsheet にこんな感じでメッセージを登録しておいて
スクリーンショット 2020-12-16 16.21.43.png

Slack でそのメッセージを post すると、bot がレスポンスしてくれます。
スクリーンショット 2020-12-16 16.24.52.png

Spreadsheet 管理のメリット

これだけだとただの slackbot の再開発ですが、Spreadsheet にしておくと「カスタムレスポンスの内容を Spreadsheet で動的に更新」することができます。
例で出した テスト用内線 の内容は、別シートで管理しています。

スクリーンショット 2020-12-16 16.29.27.png

この参照先では、表管理した内線番号を Spreadsheet 関数で結合しています。
例えば、部署1 の行は、B 列に記載した内容を結合して表示しています。
スクリーンショット 2020-12-16 16.31.21.png
スクリーンショット 2020-12-16 16.32.55.png

こうして結合した各部署の行を、さらに結合することで全体のセルを作ることができます。
スクリーンショット 2020-12-16 16.35.22.png

こうしておくことで、誰でも簡単に内線番号を編集し、さらに内線の情報を表管理することもできます。

さらに、Spreadsheet には便利な関数が多く用意されていますので、例えばこのようなレスポンスを用意しておけば、bot がレスポンス内容を更新したタイミングの最新ドル円レートを表示させる、といったことも可能です。
スクリーンショット 2020-12-16 16.38.23.png

スクリーンショット 2020-12-16 17.17.21.png

Bots を使うメリット

ほぼありません

  • (今後非推奨になっていくようですが)RTM API を使えるので、slash command など新しい使い方を覚えなくても簡単にレスポンスを使える
  • 任意の写真をアイコンに使えるようになることで愛着が湧く**(重要)**
    • 愛着があれば「放置されて誰も手を入れられなくなる RPA」みたいなのが減らせると思っています(割と本音)

まとめ・感想

Spreadsheet の内容でカスタムレスポンスする bot を作ると、Spreadsheet の機能を使って様々な動的メッセージを作れることをご紹介しました。
簡単に内線表を更新したりほぼ最新のドル円レートを表示させたり、Spreadsheet の機能でできることは全部できるはずなので、他にも様々な使い方が考えられると思います。
まぁ、昨今の事情で弊社もリモートワークになったので、内線番号は意味を失ってしまったのですが・・・。

また、Bot のコード自体はかなり前に書いたものなので、本記事を書く際に改めて見てみたら、色々と粗が目立っていて公開するのを少しためらうほどでした :sweat_smile:
せっかくなので、もっとちゃんと整理して例外処理も書いてテストも書いたら Github で公開してみようと思います。

付録

Bot の起動方法

Bots の登録方法・基本的な使い方は以前書いたこちらの記事をご参照ください。
https://qiita.com/k-nishigaki/items/3477a93a816c0f864dc1

Go のコードは以下に記載します。
slackToken に Bots の API Token を入れて、起動時引数に以下を入れて実行すれば動くかと思います。

  • ss_id : SpreadSheet の ID 部分
  • token_path : token.json のパス
  • credentials_path : credentials.json のパス
実行方法
/path/to/execution_file -token_path /path/to/token.json -credentials_path /path/to/credentials.json -ss_id [spreadsheet ID]

daemon 化などはご利用の OS に応じて行っていただければと思います。

Spreadsheet API の利用方法

Google のサンプルほぼそのままです。
https://developers.google.com/sheets/api/quickstart/go

token.json や credentials.json はこちらを参考に取得してください。

Bot のコード

コード
package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"time"

	"github.com/slack-go/slack"
	"golang.org/x/net/context"
	"golang.org/x/oauth2"
	"golang.org/x/oauth2/google"
	"google.golang.org/api/sheets/v4"
)

const slackToken string = "Insert Your Bots API Token"

func run(m *map[string]string, api *slack.Client, ss string, tokPath string, credPath string) int {

	rtm := api.NewRTM()
	go rtm.ManageConnection()
	for {
		msg := <-rtm.IncomingEvents
		log.Printf("MSG: %#v\n", msg.Data)
		switch ev := msg.Data.(type) {
		case *slack.HelloEvent:
			log.Print("Hello Event")
		case *slack.ConnectionErrorEvent:
			log.Print(ev.Error())

		case *slack.MessageEvent:
			log.Printf("Message: %#v\n", ev)
			if value, ok := (*m)[ev.Text]; ok {
				rtm.SendMessage(rtm.NewOutgoingMessage(value, ev.Channel))
			}
		}
	}
}

// Retrieve a token, saves the token, then returns the generated client.
func getClient(config *oauth2.Config, tokPath string) *http.Client {
	// The file token.json stores the user's access and refresh tokens, and is
	// created automatically when the authorization flow completes for the first
	// time.
	tokFile := tokPath
	tok, err := tokenFromFile(tokFile)
	if err != nil {
		tok = getTokenFromWeb(config)
		saveToken(tokFile, tok)
	}
	return config.Client(context.Background(), tok)
}

// Request a token from the web, then returns the retrieved token.
func getTokenFromWeb(config *oauth2.Config) *oauth2.Token {
	authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline)
	fmt.Printf("Go to the following link in your browser then type the "+
		"authorization code: \n%v\n", authURL)

	var authCode string
	if _, err := fmt.Scan(&authCode); err != nil {
		log.Fatalf("Unable to read authorization code: %v", err)
	}

	tok, err := config.Exchange(context.TODO(), authCode)
	if err != nil {
		log.Fatalf("Unable to retrieve token from web: %v", err)
	}
	return tok
}

// Retrieves a token from a local file.
func tokenFromFile(file string) (*oauth2.Token, error) {
	f, err := os.Open(file)
	if err != nil {
		return nil, err
	}
	defer f.Close()
	tok := &oauth2.Token{}
	err = json.NewDecoder(f).Decode(tok)
	return tok, err
}

// Saves a token to a file path.
func saveToken(path string, token *oauth2.Token) {
	fmt.Printf("Saving credential file to: %s\n", path)
	f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
	if err != nil {
		log.Fatalf("Unable to cache oauth token: %v", err)
	}
	defer f.Close()
	json.NewEncoder(f).Encode(token)
}

func readSS(spreadsheetId string, credPath string, tokPath string) *sheets.ValueRange {
	b, err := ioutil.ReadFile(credPath)
	if err != nil {
		log.Fatalf("Unable to read client secret file: %v", err)
	}

	// If modifying these scopes, delete your previously saved token.json.
	config, err := google.ConfigFromJSON(b, "https://www.googleapis.com/auth/spreadsheets.readonly")
	if err != nil {
		log.Fatalf("Unable to parse client secret file to config: %v", err)
	}

	client := getClient(config, tokPath)

	srv, err := sheets.New(client)
	if err != nil {
		log.Fatalf("Unable to retrieve Sheets client: %v", err)
	}

	// Prints the names and majors of students in a sample spreadsheet:
	// https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit
	//spreadsheetId := "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms"
	readRange := "reaction!B2:C"
	resp, err := srv.Spreadsheets.Values.Get(spreadsheetId, readRange).Do()
	if err != nil {
		log.Fatalf("Unable to retrieve data from sheet: %v", err)
	}

	return resp
}

func refreshReaction(m *map[string]string, ss string, tokPath string, credPath string) {
	resp := readSS(ss, credPath, tokPath)
	if len(resp.Values) == 0 {
		log.Println("No data found.")
	} else {
		for _, row := range resp.Values {
			// Print columns A and E, which correspond to indices 0 and 4.
			key, cast := row[0].(string)
			if cast == false {
				log.Fatalf("Unable read key")
			}

			value, cast := row[1].(string)
			if cast == false {
				log.Fatalf("Unable read value")
			}
			(*m)[key] = value
		}
	}

}

func main() {
	ss := flag.String("ss_id", "Insert Your Default Spreadsheet ID", "SpreadSheet ID")
	tokPath := flag.String("token_path", "token.json", "token.json path")
	credPath := flag.String("credentials_path", "credentials.json", "credentials.json path")
	flag.Parse()

	api := slack.New(slackToken)
	log.Printf("[DEBUG]api: %#v", api)
	m := map[string]string{}
	refreshReaction(&m, *ss, *tokPath, *credPath)

	go run(&m, api, *ss, *tokPath, *credPath)

	for {
		time.Sleep(5 * time.Minute)
		refreshReaction(&m, *ss, *tokPath, *credPath)
	}
}
3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?