Edited at
SlackDay 14

我が家の救世主、Google Calendar連携Slack botをつくました


この記事は

Slack Advent Calendar 2018 - Qiita の14日目の記事です。

私は個人で使用しているSlackのワークスペースに、

Google カレンダーと Slack を連携させる – Slack 

これを使っているんですが、なんせ通知が来ても彼氏がシカトして予定を忘れやがる。

この窮地を救うため、聞けば返してくれるGoogle Calendar連携botくんを作成しました.


botの概要


  • "今日の予定は?"って聞くと一覧で出してくれる
    スクリーンショット 2018-12-11 13.33.48.png

  • "今週の予定は?"って聞くと一覧で出してくれる
    スクリーンショット 2018-12-11 13.37.00.png

予定が丸見えです。


実装内容

Slack API | Slack と Calendar API | Google Developers を使用します

main.go


func main() {
token := os.Getenv("SLACKBOT")
api := slack.New(token)
client, err := gcalendar.Authorize()
if err != nil {
logger.Errorf("can not google calendar API authorized")
}

// WebSocketでSlack RTM APIに接続する
rtm := api.NewRTM()
// goroutineで並列化する
go rtm.ManageConnection()

// イベントを取得する
for msg := range rtm.IncomingEvents {
// 型swtichで型を比較する
switch ev := msg.Data.(type) {
case *slack.MessageEvent:
switch ev.Msg.Text {
case "今週の予定は?":
schedule, err := schedule.New(client, "week")
if err != nil {
logger.Errorf("get event error: %v", err)
}
rtm.SendMessage(rtm.NewOutgoingMessage(schedule, ev.Channel))
case "今日の予定は?":
schedule, err := schedule.New(client, "day")
if err != nil {
logger.Errorf("get event error: %v", err)
}
rtm.SendMessage(rtm.NewOutgoingMessage(schedule, ev.Channel))
}
case *slack.InvalidAuthEvent:
log.Print("Invalid credentials")
}
}
}

schedule.go


func New(c *http.Client, duration string) (string, error) {
srv, err := calendar.New(c)
if err != nil {
return "", err
}

now := time.Now()
var end time.Time
switch duration {
case "day":
end = now.AddDate(0, 0, 1)
case "week":
end = now.AddDate(0, 0, 7)
}
if end.IsZero() {
end = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC)
}

events, err := srv.Events.List("5t9khdis149i45g9a43dvg7vn8@group.calendar.google.com").
ShowDeleted(false).SingleEvents(true).TimeMin(now.Format(time.RFC3339)).
MaxResults(10).TimeMax(end.Format(time.RFC3339)).OrderBy("startTime").Do()
if err != nil {
log.Fatalf("Unable to retrieve next ten of the user's events: %v", err)
return "", err
}

var messages []string
if len(events.Items) == 0 {
fmt.Println("No upcoming events found.")
} else {
for _, item := range events.Items {
date := item.Start.DateTime
if date == "" {
date = item.Start.Date
}
messages = append(messages, fmt.Sprintf("%v: %v\n", date, item.Summary))
}
}

return strings.Join(messages, ""), nil
}

gcalendar.go


func Authorize() (*http.Client, error) {
// If modifying these scopes, delete your previously saved token.json.
b, err := ioutil.ReadFile("credentials.json")
if err != nil {
log.Fatalf("Unable to read client secret file: %v", err)
return nil, err
}
config, err := google.ConfigFromJSON(b, calendar.CalendarReadonlyScope)
if err != nil {
log.Fatalf("Unable to parse client secret file to config: %v", err)
}
// 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 := "token.json"
tok, err := tokenFromFile(tokFile)
if err != nil {
tok = tokenFromWeb(config)
saveToken(tokFile, tok)
}
return config.Client(context.Background(), tok), nil
}

func tokenFromFile(path string) (*oauth2.Token, error) {
f, err := os.Open(path)
if err != nil {
return nil, errors.New("cannot open file")
}
defer f.Close()

tok := &oauth2.Token{}
err = json.NewDecoder(f).Decode(tok)
return tok, err
}

// Request a token from the web, then returns the retrieved token.
func tokenFromWeb(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
}

// 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)
}

あとは go run main.goするなり、buildして実行するなりで動きます。

SlackのAPI登録、GoogleCalendarAPI登録に関しては他の記事を参考になさってください。


まとめ

現状、作ったbotアプリによる生活の向上は見えていません。ただの気休めでした。

ちなみに、冒頭で紹介した現状を招いている素晴らしいアプリは、Slackで見るとこんな感じに表示してくれます。

gcal_view

リマインダーも設定できるし、見た目も綺麗だし最高です。


これからの課題

今回急いで作ったので、Slackの用意したアプリに近づけるため、これからも進化させてゆきたいと思います。


  • 時間をもっとみやすく

  • せっかくのbotなのでもっと愛嬌をもたせたい

  • 時間の設定までしてあげたり

  • Alexaと連携してみたり

日々精進します