日報Go

# きっかけ
「リモートワークで勤怠入力が必須になってしまった...」
「いちいちブラウザ開いて入力したくない....」
ということでGolangの勉強がてらに作りました:fist:

上司に許可とって、きちんとやっている内容出していればOKとのことだったので自動化してみた感じです。:tea:
簡単な内容ですが、同僚には受けがよかったです...w

間違いなどありましたらご指摘していただけると幸いです。

# ソース
https://github.com/sgash708/go_chatwork_notice

# 前提
* Golang ver1.15
    * goquery
    * Agouti
* ChromeDriver ver88.0.4324.96
* zsh
* macOS ver10.15.7

https://chromedriver.chromium.org/downloads

# 注意
スクレイピングは脅威にもなります。
サイトごとに行ってよいか注意しながら行いましょう。
待ち処理などを適宜入れてください。

# 前準備
goファイルが走るように設定します。

```zsh
% cd go_chatwork_notice/
% go mod init
% mkdir tmp/
% vi config.ini
### 記述 ###
[web]
# for Basic('user:password@')
url = https://user:password@example.com

[login]
username = your nice username
password = your nice password

[chatwork]
cwToken = your nice token
cwURL   = https://xxxxxxxxxxx
cw2Me   = xxxxxxxxxx
```

# やったこと
全部説明すると長いので掻い摘みます:bow:
下記は<code>main.go</code>の内容です。

## ■構造体
既定の値は<code>config.ini</code>を参照します。
値を呼ぶ時は、<code>Scraping.UserName</code>のように使用します。

```golang:main.go
// 定数
const (
	// TimeLayout 出力時間のフォーマット
	TimeLayout string = "2006-01-02 15:04:05"
	// DateLayout 日付フォーマット
	DateLayout string = "2006/01/02"
)

// ScrapingList スクレイピング用のstruct
type ScrapingList struct {
	URL           string
	UserName      string
	Password      string
	ChatworkToken string
	ChatworkURL   string
	Chatwork2Me   string
}
func init() {
	config, err := ini.Load("config.ini")
	if err != nil {
		log.Fatalf("config.iniファイルの読み込みに失敗しました:%v", err)
	}
	Scraping = ScrapingList{
		URL:           config.Section("web").Key("url").MustString(""),
		UserName:      config.Section("login").Key("username").MustString(""),
		Password:      config.Section("login").Key("password").MustString(""),
		ChatworkToken: config.Section("chatwork").Key("cwToken").MustString(""),
		ChatworkURL:   config.Section("chatwork").Key("cwURL").MustString(""),
		Chatwork2Me:   config.Section("chatwork").Key("cw2Me").MustString(""),
	}
}
```

## ■API
Chatworkに結果を自身のチャットルームに通知しています。


```golang:main.go
func ChatWorkMessagePost(message string) {
	values := url.Values{}
	values.Add("body", message)

	req, err := http.NewRequest(
		"POST",
		Scraping.ChatworkURL,
		strings.NewReader(values.Encode()),
	)
	if err != nil {
		log.Fatalf("リクエスト失敗しました。:%v", err)
	}

	// コンテントタイプを設定
	req.Header.Set("X-ChatWorkToken", Scraping.ChatworkToken)
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	// メッセージ投稿
	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		log.Fatalf("メッセージ送信失敗しました。:%v", err)
	}
	defer resp.Body.Close()

	// メッセージ投稿結果表示
	if resp.StatusCode == http.StatusOK {
		fmt.Println("メッセージ投稿に成功しました。")
	} else {
		fmt.Println("メッセージ投稿に失敗しました。")
	}

	fmt.Println("メッセージ投稿ステータスコード=[",
		resp.StatusCode,
		"]レスポンス内容=[",
		resp.Status,
		"]",
	)
}
```

## ■ChromeDriver（Agouti）
ヘッドレスモードで起動しています。
<code>ChromeOptions</code>で追加して快適に行うことができます。

```golang:main.go
driver := agouti.ChromeDriver(
	agouti.ChromeOptions("args", []string{
		"--headless",
		"--window-size=1280,800",
	}),
	agouti.Debug,
)

err := driver.Start()
	if err != nil {
		errLog("Chromeドライバ起動に失敗しました。")
		log.Fatalln(err)
	}
defer driver.Stop()
```

下記では、値の入力とクリックを行います。
HTMLの要素の取得に関しては<code>FindByXPath</code>や<code>FindByName</code>などを使って自由に行ってください。

```golang:main.go
// ログイン処理
page.FindByXPath(`//*[@id="username"]`).Fill(Scraping.UserName)
page.FindByXPath(`//*[@id="password"]`).Fill(Scraping.Password)
page.FindByName(`login`).Click()
```

## ■Goquery
テキストの取得を行い判定をしています。
流れとしては、

* chromedriverでHTMLを取得
* HTMLをstringに変換
* goqueryでHTMLを解析して要素のテキスト取得

を行っています。

```golang:main.go
// HTMLを取得する
getAll, err := page.HTML()
if err != nil {
	errLog("遷移後のHTML(一覧)取得失敗しました。")
	log.Fatalln(err)
}
readerGetAll := strings.NewReader(getAll)
contentDom, _ := goquery.NewDocumentFromReader(readerGetAll)
seeText := contentDom.Find("#header > h1 > span.current-project").Text()
if seeText != "xxxxxx" {
	message := "ログインに失敗しました。"
	errLog(message)
	log.Fatalln(message)
}
// 行数取得
rawCnt := len(contentDom.Find("#content > form:nth-child(5) > div > table > tbody > tr").Nodes)
```

## ■OS
ディレクトリの存在判定と作成し、スクリーンショット撮り保存します。

```golang:main.go
var dir = "./tmp/" + time.Now().Format(strings.Replace(DateLayout, "/", "-", -1))

// ディレクトリ作成
_, err = os.Stat(dir)
if err != nil {
	if os.IsNotExist(err) {
		fmt.Println("ディレクトリ作成します...")
		err = os.Mkdir(dir, 0755)
		if err != nil {
			fmt.Println("ディレクトリ作成失敗しました...")
			panic(err)
		}
	}
}

// スクリーンショットをとる
page.Screenshot(dir + "/input_" + time.Now().Format(TimeLayout) + ".png")
```

# おまけ
いちいち起動したくないので、自分はcronで自動化しています。
やっていることは、

* 平日のみ起動
* 30分の揺らぎをもたす

人間らしく時間が毎日変わるので、個人的には満足してます。

```zsh:cron
% cd
% wget https://www8.cao.go.jp/chosei/shukujitsu/syukujitsu.csv
% crontab -e
# 記述する内容
00 18 * * 1-5 bash -c "sleep $((RANDOM \% 1800))s"; grep `date "+\%Y/\%-m/\%-d"`, syukujitsu.csv > /dev/null || cd ~/Desktop/go_kintai; bash -l -c 'go run ~/go_chatwork/main.go'
# mailコマンドで起動したか確認できます
% mail
No mail for username
```

# 今後の展開
* GoogleCalender連携
* ラズパイサーバで起動させる
 * macがスリープだと、cronが走らず日報を書いてくれないので...

自動でカレンダーを見て、リモートワークの日は出勤/退勤時のメッセージ送信などをやってみようかなと思います。
これは流石に怒られるかもしれないので聞いてみます笑