東京ドームのイベント情報をSlackに通知させてみた

# はじめに
私が通っている大学のすぐ近くには東京ドームがあり，コロナ禍に入る前はライブ帰りの客で駅や電車がやたら混むということがよくありました．
今年に入ってからイベントが再開されてきていることもあり，またこういったことが起こる気がしたので，予めイベント情報を知れる仕組みを作りたいと思いました．

# 使用した技術
* Go
* goquery
* Serverless Framework
* AWS Lambda
* Github Actions

# プロジェクトの作成
予め作成したディレクトリに移動した上で，以下を実行します．
```
$ sls create -t aws-go-mod
```
そうすると，最低限動くServerless Frameworkのプロジェクトが作成されます．
もしServerless Frameworkをインストールしていない場合は，以下のコマンドでインストールしましょう．（Node.jsが必要）
```
$ npm i -g serverless
```
or
```
$ yarn add global serverless
```

# 実装
## 初期のファイル構成
```
./tokyo-dome-event-notifier
├── Makefile
├── gomod.sh
├── hello
│   └── main.go
├── serverless.yml
└── world
    └── main.go
```
最初はこうなっているはずです．
今回は，`world`を削除したうえで，`hello`を`handler`に改名しましょう．

## serverless.yml
以下のように編集します．
```yml
service: tokyo-dome-event-notifier

frameworkVersion: '3'

useDotenv: true

provider:
  name: aws
  runtime: go1.x
  lambdaHashingVersion: 20201221
  stage: prod
  region: ap-northeast-1

package:
  patterns:
    - '!./**'
    - ./bin/**

functions:
  notify:
    handler: bin/handler
    memorySize: 128
    events:
      - schedule: cron(0 21 * * ? *)
    environment:
      SLACK_WEBHOOK_URL: ${env:SLACK_WEBHOOK_URL}
```

## scraper
スクレイピングを実行するコードの実装をします．
まず，`scraper`という名前のディレクトリを作成し，その中に`scraper.go`を作成します．
```
$ midir scraper
$ touch scraper.go
```

### goqueryのインストール
スクレイピング処理にはgoqueryを用います．以下のコマンドでインストールしましょう．
```
$ go get github.com/PuerkitoBio/goquery
```

### スクレイピング対象のセレクタを調べる
以下のサイトから情報を取得します．
https://www.tokyo-dome.co.jp/dome/event/schedule.html

ブラウザを開いた状態でF12を押すとインスペクターが表示されるので，これを用いてセレクタを調べましょう．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/855517/bf48bc22-07a1-5ff6-83fe-05c4d3df3733.png)


### 実装例
```golang
package scraper

import (
	"fmt"
	"net/http"
	"strconv"
	"time"

	"github.com/PuerkitoBio/goquery"
)

func FetchTodayEvent() string {
	jst, _ := time.LoadLocation("Asia/Tokyo")

	res, err := http.Get("https://www.tokyo-dome.co.jp/dome/event/schedule.html")

	if err != nil {
		fmt.Println("Failed to scrape")
		panic(err)
	}
	defer res.Body.Close()

	doc, _ := goquery.NewDocumentFromReader(res.Body)

	selector := "div.c-mod-tab__body:nth-child(2) > table > tbody"
	innerSelector := "tr.c-mod-calender__item"
	dateSelector := "th > span:nth-child(1)"
	categorySelector := "td:nth-child(2) > div > div:nth-child(1) > p > span"
	titleSelector := "td > div > div:nth-child(2) > p.c-mod-calender__links"
	timeSelector := "td > div > div:nth-child(2) > p:nth-child(2)"

	selection := doc.Find(selector)

	var event string
	selection.Find(innerSelector).Each(func(index int, s *goquery.Selection) {
		date, _ := strconv.Atoi(s.Find(dateSelector).Text())
		category := s.Find(categorySelector).Text()
		title := s.Find(titleSelector).Text()
		info := s.Find(timeSelector).Text()

		if date == time.Now().In(jst).Day() {
			if title == "" {
				event = "イベントなし"
			} else {
				event = title + "（" + category + "）" + "\n" + info
			}
		}
	})
	return event
}
```

## slack/slack.go
先ほど同様，ディレクトリとファイルを作成しましょう．
```
$ mkdir slack
$ touch slack.go
```
今回はWebhookを用いて通知を送信します．（URL取得方法は後述）
APIを叩くだけなので，追加のライブラリを入れる必要はありません．

### 実装例

```go
package slack

import (
	"bytes"
	"fmt"
	"net/http"
	"os"
	"strings"
	"time"
)

func SendEventInfo(text string) {
    jst, _ := time.LoadLocation("Asia/Tokyo")
	t := time.Now().In(jst)
	weekdayja := strings.NewReplacer(
		"Sun", "日",
		"Mon", "月",
		"Tue", "火",
		"Wed", "水",
		"Thu", "木",
		"Fri", "金",
		"Sat", "土",
	)
	date := weekdayja.Replace(t.Format("2006年1月2日(Mon曜日)"))

	url := os.Getenv("SLACK_WEBHOOK_URL")
	body := fmt.Sprintf(`{
		"text": "%sのイベント情報",
		"blocks": [
			{
				"type": "header",
				"text": {
					"type": "plain_text",
					"text": "%sのイベント情報"
				}
			},
			{
				"type": "divider"
			},
			{
				"type": "section",
				"text": {
					"type": "plain_text",
					"text": "%s",
					"emoji": true
				}
			}
		]
	}`, date, date, text)

	req, err := http.NewRequest("POST", url, bytes.NewBuffer([]byte(body)))
	req.Header.Set("Content-Type", "application/json")

	if err != nil {
		panic(err)
	}

	client := new(http.Client)
	res, err := client.Do(req)

	if err != nil {
		panic(err)
	}
	defer res.Body.Close()
}
```

## handler/main.go
最初の段階では依存ライブラリが不足しているはずです．以下のコマンドでインストールしましょう．
```
$ go get github.com/aws/aws-lambda-go/events
$ go get github.com/aws/aws-lambda-go/lambda
```
main.goを以下のように編集しましょう．
```go
package main

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"

	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"
	"github.com/tokyo-dome-event-notifier/scraper"
	"github.com/tokyo-dome-event-notifier/slack"
)

// Response is of type APIGatewayProxyResponse since we're leveraging the
// AWS Lambda Proxy Request functionality (default behavior)
//
// https://serverless.com/framework/docs/providers/aws/events/apigateway/#lambda-proxy-integration
type Response events.APIGatewayProxyResponse

// Handler is our lambda handler invoked by the `lambda.Start` function call
func Handler(ctx context.Context) (Response, error) {
	event := scraper.FetchTodayEvent()
	fmt.Println(event)

	slack.SendEventInfo(event)

	var buf bytes.Buffer

	body, err := json.Marshal(map[string]interface{}{
		"message": "Go Serverless v1.0! Your function executed successfully!",
	})
	if err != nil {
		return Response{StatusCode: 404}, err
	}
	json.HTMLEscape(&buf, body)

	resp := Response{
		StatusCode:      200,
		IsBase64Encoded: false,
		Body:            buf.String(),
		Headers: map[string]string{
			"Content-Type":           "application/json",
			"X-MyCompany-Func-Reply": "hello-handler",
		},
	}

	return resp, nil
}

func main() {
	lambda.Start(Handler)
}
```
今回はAPIとして使うことは想定していないため，Handler関数の最初の部分にちょっと付け足すだけでOKです．

# Slackの設定
アプリケーション作成方法はこちらが参考になると思います．
https://api.slack.com/authentication/basics
以下，アプリケーション作成は行っているものとします．
## Webhookの設定
Features > Incoming Webhookをクリックし，Add New Webhook to Workspaceと書かれたボタンを押します．
チャンネルを指定し，表示されたURLを安全な場所に控えておきます．

## URLを環境変数に設定
`.env`を作成し，内容を以下のようにします．
```
SLACK_WEBHOOK_URL=<控えたURL>
```
# 一旦ローカル上で動かしてみる
Goはコンパイラ型言語なので，予めビルドする必要があります．
```
$ make build
```
ビルド出来たら，以下のコマンドで実行します．
```
$ sls invoke local -f notify
```
設定したSlackチャンネルに通知が来ればOK！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/855517/410b6237-0454-f45f-c03b-43e4e819ee1a.png)



# Github Actionsの設定
`.github/actions/`配下に，ymlファイルを作成しましょう．名前は何でもよいです．
今回は，`deploy.yml`としました．
## 実装例
```yml
name: Deploy

on:
  push:
    branches: [ master ]

jobs:

  build:
    runs-on: ubuntu-latest
    env:
      SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
    steps:
    - uses: actions/checkout@v3

    - name: Set up Go
      uses: actions/setup-go@v3
      with:
        go-version: 1.17

    - name: Build
      run: make build

    - name: Serverless deploy
      uses: serverless/github-action@v3
      with:
        args: deploy
      env:
        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
```

`AWS_ACCESS_KEY_ID`，`AWS_SECRET_ACCESS_KEY`は予め取得しておきましょう．
Secretsに，その2つと，`SLACK_WEBHOOK_URL`を設定しておきます．
あとはpushさえすれば，デプロイされているはずです！

# リポジトリ
https://github.com/Eagle-Konbu/tokyo-dome-event-notifier