Golang: agoutiを使用してAKASHIの勤怠ボタンを押せるようにしてみた

## 概要

ブラウザ操作をやってみたかったので、
毎日勤怠管理で使用しているAKASHIのボタンを自動ブラウザで押せるようなアプリケーション作ってみました。

Goを使用した背景は、Goで書きたかったからです笑
実際のコード見たい方がいらっしゃいましたら、別途GitHubのリポジトリ公開しようと思います。

## 実装環境

- macOS Monterey(12.2.1)
- Goland
- go version go1.20 darwin/arm64

## 使用フレームワーク

### 1. agouti

Golangでブラウザ操作してくれるやつ。

https://pkg.go.dev/github.com/sclevine/agouti

```bash
% go get https://pkg.go.dev/github.com/sclevine/agouti 
```

今回はChromeを使用するので、事前にChrome Driverもインストールしておきます。

```bash
% brew install chromedriver
```

### 2. godotenv

Golangで環境変数が使用できるようになるやつ

https://github.com/joho/godotenv

```bash
% go get github.com/joho/godotenv
```

## ツリー構造

```bash
.
├── .gitignore
├── .env
├── akashi.go
├── go.mod
├── go.sum
└── main.go
```

## コード

```go: main.go
package main

import (
	"fmt"
	"github.com/sclevine/agouti"
	"log"
)

func main() {
	// オプションの選択
	option, err := selectOption()
	if err != nil {
		fmt.Printf("select option error: %s\n", err)
		return
	}

	// chromeドライバを使って、Google Chromeを使用できるようにする。
	agoutiDriver := agouti.ChromeDriver()
	if err := agoutiDriver.Start(); err != nil {
		fmt.Printf("Failed to start driver. %s\n", err)
		return
	}
	defer agoutiDriver.Stop()

	// agoutiを使用して新規ブラウザを立ち上げる。開いたブラウザを使って色々操作します。
	page, err := agoutiDriver.NewPage()
	if err != nil {
		fmt.Printf("Failed to open a new page. %s\n", err)
		return
	}

	// オプションに応じて勤怠処理の実行
	if err := executeAkashiTimeClock(page, option); err != nil {
		log.Fatalln(err)
	}

	fmt.Println("success")
}
```

```go: akashi.go
package main

import (
	"fmt"
	"github.com/joho/godotenv"
	"github.com/sclevine/agouti"
	"os"
)

const (
	attendanceOptionNumber = 1
	leavingOptionNumber    = 2
)

func selectOption() (int, error) {
	var optionMap = map[int]string{
		attendanceOptionNumber: "出勤",
		leavingOptionNumber:    "退勤",
	}

	// 実行したいオプションを入力してもらう
	var option int
	fmt.Printf("select options \n")
	for k, v := range optionMap {
		fmt.Printf("%v: %v\n", k, v)
	}
	fmt.Printf(">")
	fmt.Scan(&option)

	// 入力されたオプションが正しいか判定
	if _, ok := optionMap[option]; !ok {
		return 0, fmt.Errorf("not the correct option value")
	}

	return option, nil
}

func executeAkashiTimeClock(page *agouti.Page, option int) error {
	// AKASHIログインページ移動
	if err := openAkashiLoginPage(page); err != nil {
		return err
	}

	// AKASHIログイン
	if err := loginAkashi(page); err != nil {
		return err
	}

	// 音声ミュート
	if err := muteAkashi(page); err != nil {
		return err
	}

	// オプションに応じて勤怠処理の変更
	var xPath string
	switch option {
	case attendanceOptionNumber:
		xPath = "/html/body/div[1]/div/section/form/div[2]/div/div[2]/ul[1]/li[1]/a[@data-punch-type='attendance']"
	case leavingOptionNumber:
		xPath = "/html/body/div[1]/div/section/form/div[2]/div/div[2]/ul[1]/li[2]/a[@data-punch-type='leaving']"
	}

	button := page.FindByXPath(xPath)
	if err := button.Click(); err != nil {
		return fmt.Errorf("Failed to click button. %s\n", err)
	}

	return nil
}

func openAkashiLoginPage(page *agouti.Page) error {
	// 次に勤怠ページを指定したAKASHIのページを開きます。
	if err := page.Navigate("https://atnd.ak4.jp/login?next=%2Fmypage%2Fpunch"); err != nil {
		return fmt.Errorf("Failed to navigate to AKASHI login page. %s\n", err)
	}
	return nil
}

func loginAkashi(page *agouti.Page) error {
	// フォーム情報を取得
	formCompanyId := page.FindByID("form_company_id")
	formLoginId := page.FindByID("form_login_id")
	formPassword := page.FindByID("form_password")

	// 環境変数セット
	if err := godotenv.Load(); err != nil {
		return fmt.Errorf("Failed loading .env file: %s\n", err)
	}

	companyId := os.Getenv("FORM_COMPANY_ID")
	loginId := os.Getenv("FORM_LOGIN_ID")
	password := os.Getenv("FORM_PASSWORD")

	// フォームにログイン情報を入力
	formCompanyId.Fill(companyId)
	formLoginId.Fill(loginId)
	formPassword.Fill(password)

	// submit
	formSubmitButton := page.FindByID("submit-button")
	if err := formSubmitButton.Click(); err != nil {
		return fmt.Errorf("Failed to click form button. %s\n", err)
	}

	return nil
}

func muteAkashi(page *agouti.Page) error {
	// muteボタンのXPathを指定して音声ミュート
	muteButton := page.FindByXPath("/html/body/div[1]/div/section/form/div[1]/div[2]/ul/li[2]")
	if err := muteButton.Click(); err != nil {
		return fmt.Errorf("Failed to click mute button. %s\n", err)
	}
	return nil
}
```

```plaintext: .gitignore
.env
.idea
```

```plaintext: .env
# AKASHIログイン情報
FORM_COMPANY_ID=""
FORM_LOGIN_ID=""
FORM_PASSWORD=""
```

## 使用方法

```bash
% go build FILE_NAME

% go run BUILD_FILE_NAME
select options
2: 退勤
1: 出勤
> // 実行したいオプションを入力 → chromeが起動してオプションに応じたブラウザ操作実行

success
```

## 参考

https://qiita.com/0829/items/c1e494bb128ade5f0872

https://note.com/shift_tech/n/nbf70b1953eec

https://agaroot-itp.com/blog/424/

https://zenn.dev/ikawaha/articles/20221206-eac7dea110c285

https://qiita.com/takkiiiiiiiii/items/f290907dc8f3d4f73e6b

https://codelab.website/golang-godotenv/