go-rodでmoodleの出席登録を自動化

九大では講義時間内にmoodleの講義ページにログインすることで出席登録が行われますが、ユーザ名とパスワードを打ち込む→講義一覧から現在の講義を探す→出席登録という一連の流れが煩雑に感じたのでヘッドレスブラウザで自動化を行いました。
Goを選んだ理由はただ単に触ってみたい言語だったからです。

:::note alert
九大moodleでは出席していないにも関わらずコースにログインして出席とする等の行為は不正出席であり、単位を取り消される場合があります。
自動化プログラムを使用して対面講義の出席登録を行う際は必ず講義に出席し、講義室のedunet(学内LAN)へ接続するようにしてください。
本記事のプログラムを使用したことにより生じたいかなる損害にも筆者は一切責任を負いません。
:::

https://la.kyushu-u.ac.jp/usage-support/attendance/

## 実行環境

- go version go1.22.4 darwin/arm64
- macOS Ventura 13.4

## ディレクトリ構成

```
auto_attend/
├── bin
│   └── auto_attend
├── go.mod
├── go.sum
└── main.go
```

## 手順

### go-rodの導入

まずはモジュールの初期化

```
go mod init
```

go-rodのインストール

```
go get github.com/go-rod/rod
```

### main.goの記述

<details><summary>ソースコード全文</summary>

```go
package main

import (
  "fmt"
  "time"
  "github.com/go-rod/rod"
  "github.com/go-rod/rod/lib/input"
)

type Course struct{
  id string
  weekday time.Weekday
  period int
}

func main() {
  loginURL := "https://moodle.s.kyushu-u.ac.jp/login/index.php"
  username := "USERNAME"
  password := "PASSWORD"
  courses := []*Course{
    {"12345", time.Monday, 1}, //月曜1限

    {"56789", time.Wednesday, 3}, //水曜3,4限
    {"56789", time.Wednesday, 4},
  }

  periodtime := [][][]int{
    {{8,40},{10,10}}, //1限
    {{10,30},{12,0}}, //2限
    {{13,0},{14,30}}, //3限
    {{14,50},{16,20}}, //4限
    {{16,40},{18,10}}, //5限
  }
  now := time.Now()
  current := -1 //授業時間外の場合

  //現在の時限
  for id, t := range periodtime{
    start := time.Date(now.Year(), now.Month(), now.Day(), t[0][0], t[0][1], now.Second(), now.Nanosecond(), now.Location())
    end := time.Date(now.Year(), now.Month(), now.Day(), t[1][0], t[1][1], now.Second(), now.Nanosecond(), now.Location())

    if start.Unix() <= now.Unix() && end.Unix() >= now.Unix(){
      current = id + 1
    }
  }

  //fmt.Println(current)
  
  //ログイン処理
  browser := rod.New().MustConnect()
  defer browser.MustClose()
  page := browser.MustPage(loginURL).MustWaitStable()
  page.MustElement("#username").MustInput(username)
  page.MustElement("#password").MustInput(password).MustType(input.Enter)
  page.MustWaitStable()

  //出席
  for _, c := range courses{
    if c.weekday == now.Weekday() && c.period == current{
      courseURL := "https://moodle.s.kyushu-u.ac.jp/course/view.php?id=" + c.id
      page := browser.MustPage(courseURL)
      page.MustWaitStable()
      page.MustScreenshot("moodle.png")
      fmt.Println(courseURL)
    }
  }
}
```

</details>

#### ライブラリのインポート

```go
package main

import (
  "fmt"
  "time"
  "github.com/go-rod/rod"
  "github.com/go-rod/rod/lib/input"
)
```

#### 構造体Courseの定義

講義を識別するためのID、曜日、時限を記述するための構造体Courseを定義します

```go
type Course struct{
  id string
  weekday time.Weekday
  period int
}
```

#### main関数

ログインページのURL、ユーザ名、パスワードを定義して講義の情報をCourse型スライスに記述します

```go
func main() {
  loginURL := "https://moodle.s.kyushu-u.ac.jp/login/index.php"
  username := "USERNAME"
  password := "PASSWORD"
  courses := []*Course{
    {"12345", time.Monday, 1}, //月曜1限

    {"56789", time.Wednesday, 3}, //水曜3,4限
    {"56789", time.Wednesday, 4},
  }
```

実行時の時限を計算


```go
  periodtime := [][][]int{
    {{8,40},{10,10}}, //1限
    {{10,30},{12,0}}, //2限
    {{13,0},{14,30}}, //3限
    {{14,50},{16,20}}, //4限
    {{16,40},{18,10}}, //5限
  }
  now := time.Now()
  current := -1 //授業時間外の場合

  //現在の時限
  for id, t := range periodtime{
    start := time.Date(now.Year(), now.Month(), now.Day(), t[0][0], t[0][1], now.Second(), now.Nanosecond(), now.Location())
    end := time.Date(now.Year(), now.Month(), now.Day(), t[1][0], t[1][1], now.Second(), now.Nanosecond(), now.Location())

    if start.Unix() <= now.Unix() && end.Unix() >= now.Unix(){
      current = id + 1
    }
  }
```

ログインページにアクセスしてフォームにユーザ名とパスワードをタイプし、Enterキーを押してログイン

```go
  //ログイン処理
  browser := rod.New().MustConnect()
  defer browser.MustClose()
  page := browser.MustPage(loginURL).MustWaitStable()
  page.MustElement("#username").MustInput(username)
  page.MustElement("#password").MustInput(password).MustType(input.Enter)
  page.MustWaitStable()
```

登録した講義データに現在行われているものがあれば、講義ページにアクセスして出席登録

```go
  //出席
  for _, c := range courses{
    if c.weekday == now.Weekday() && c.period == current{
      courseURL := "https://moodle.s.kyushu-u.ac.jp/course/view.php?id=" + c.id
      page := browser.MustPage(courseURL)
      page.MustWaitStable()
      page.MustScreenshot("moodle.png")
      fmt.Println(courseURL)
    }
  }
}
```

### ビルド&実行

`-o`オプションで実行ファイルの出力先を`bin/`に指定しています

```
go build -o bin/
```

初回実行時にはブラウザのダウンロードとインストール処理が走るので結構時間がかかります

```
./bin/auto_attend
```



## 課題

### アクセス時のエラー処理

ログイン、講義ページアクセス時のエラー処理を実装できていません。ステータスコード等を取得できればいけるかも？

### ユーザ名、パスワード、講義情報のハードコーディング

セキュリティ上好ましくないし、あとパスワードを変えたり講義を追加・削除するたびにビルドするのが手間。別ファイル(JSON等)にまとめるのが良さそう？

## まとめ

Goを実践的に学ぶ目的で自動出席ツールを作りました。

## 参考文献

https://syu-m-5151.hatenablog.com/entry/2024/04/05/145103