在室管理を自動化する

# さいしょに
[ナンデモアリなAdventCalendar2018](https://qiita.com/advent-calendar/2018/nandemo)の23日目の記事です。
最初はこれ用になんか実装でもするかと思ってましたが修論のせいで時間がとれなかったのと直前で風邪を引いたので、適当に研究室で動かしてる在室管理botの話でお茶を濁します。

# やりたいこと
研究室内に誰が居るかというのを自動で取得したい。
SlackなりLINEなりで誰が今居るか確認したいのでできればWeb APIでJSONとかで結果が返ってくるとうれしい。

そしていちいち入退室のたびに自分の在室状況を紙なりアプリなりで変更するのは面倒なので__更新作業とか絶対したくない__。

# 前提条件
* __研究室内でLAN__を用意していること
* 研究室ほとんどのメンバーが__そのLANを利用していること__
* 研究室内に一台以上__常時稼働できるコンピュータ__が転がっていること 

# どうやって在室状況取るか
arpとMACアドレスで在室状況とってngrokでグローバルに出す。

構成はこんな感じ
![Untitled Diagram.png](https://qiita-image-store.s3.amazonaws.com/0/234127/bc3389c5-4ec6-be6f-f70a-69d34bb32304.png)

1. 適当なPCの適当なHTTPのポートで入力待つ
2. 入力受け取ったPC側がARPテーブル更新
3. ARPテーブルのMACアドレスと手元に持ってる研究室メンバーのMACアドレスを突き合わせる
4. 誰が居るか返す  

という流れ。
研究室内ネットワークにPCなりスマホなりが繋がっていれば検出されることになる。あまり精確ではないため、厳密に把握したいという場合には向いてない。

技術的に面倒なことは特に無いのでグッとやると動く。

# 実装
まずarpリクエスト送って在室者一覧を返すarp.go

```go:arp.go
package recorder

import (
	"fmt"
	"os/exec"
	"regexp"
	"runtime"
	"strconv"
	"strings"
	"sync"

	"github.com/taigak/room_watch/util"
)

var os = strings.ToLower(runtime.GOOS)

type Member struct {
	Name    string
	Devices []string
}

func NewMembers() []Member {
	members := []Member{
		{Name: "人1", Devices: []string{"00:00:00:00:00:00", "00:00:00:00:00:00"}},
		{Name: "人2", Devices: []string{"00:00:00:00:00:00"}},
	}
	return members
}

// ARPテーブル内のMACアドレスリスト取得
func getARPTable() []string {
	// send ping
	var wg sync.WaitGroup
	for i := 2; i < 254; i++ {
		go func(i int) {
			wg.Add(1)
			switch os {
			case "windows":
				exec.Command("ping", "192.168.11."+strconv.Itoa(i)).Run()
			case "linux":
				exec.Command("ping", "-c", "2", "192.168.11."+strconv.Itoa(i)).Run()
			}
			wg.Done()
		}(i)
	}
	wg.Wait()

	// get ARP Table
	out, err := exec.Command("arp", "-a").Output()
	if err != nil {
		fmt.Println(err)
		panic("err")
	}
	r := regexp.MustCompile(`([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})`)
	res := r.FindAllStringSubmatch(string(out), -1)
	// formatting
	table := []string{}
	for _, mac := range res {
		m := mac[0][0:17]
		if os == "windows" {
			m = strings.Replace(m, "-", ":", -1)
		}
		table = append(table, m)
	}

	return table
}

// 事前登録MACアドレスとARPテーブル内のMACアドレス突き合わせ
func GetAttendance() []string {
	members := NewMembers()
	table := getARPTable()
	attendances := []string{}

	for _, member := range members {
		original := table
		table = util.Difference(table, member.Devices)
		if len(original) > len(table) {
			attendances = append(attendances, member.Name)
		}
	}
	return attendances
}
```
やってることは
1. 取りうる範囲のIPアドレスにping送信
2. ARPテーブル更新
3. MACアドレス切り出し
4. 既に登録してあるMACアドレスと突き合わせて在席者の配列を返す
というだけ。

OS側のarpコマンドを叩く形にしたけど環境によって微妙に返ってくる形式が違うのでformattingしてる。

次に`arp.go`を呼び出す側。

```go:recorder.go
package recorder

import (
	"fmt"
	"time"
)

var (
	record = Record{}
)

type Record struct {
	Time    string   `json:"time"`
	Members []string `json:"members"`
}

func Watch() {
	go func() {
		set() //最初に一回だけmembers記録
		t := time.NewTicker(5 * time.Minute)
		defer t.Stop()

		for {
			select {
			case <-t.C:
				set()
			}
		}
	}()
}

func set() {
	record = Record{
		Time:    time.Now().Format("2006/1/2-15:04:05"),
		Members: GetAttendance(),
	}

	fmt.Println(time.Now().Format("2006/1/2-15:04:05"), "New Member log Recorded")
}

func Get() Record {
	return record
}
```
実際のところHTTP Requestが来る度にARPテーブル更新してたらResponseが遅いし負荷もかかるので5分に一回更新して雑にcacheする形にしてる。
やってることは大体arp.go呼んで結果を整形してるだけ。

次にServer起動部分

```go:main.go
package main

import (
	"encoding/json"
	"fmt"
	"net/http"

	"github.com/taigak/room_watch/recorder"
)

const (
	Auth = "SET_YOUR_PASSWORD"
)

func main() {
	// 5分毎の更新
	recorder.Watch()

	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		status := recorder.Get()

		jsonBytes, err := json.Marshal(status)
		if err != nil {
			w.WriteHeader(500)
			fmt.Fprint(w, err)
			return
		}

		// 認証エラー
		if pw := r.Header.Get("Authorization"); pw != Auth {
			w.WriteHeader(401)
			fmt.Fprint(w, "Unauthorized")
			return
		}

		// その他エラー
		if err != nil {
			w.WriteHeader(500)
			fmt.Fprint(w, err)
			return
		}

		fmt.Fprint(w, string(jsonBytes))
	})
	http.ListenAndServe(":8080", nil)
}
```
やってることは5分たびに在席者を更新しつつ8080番でHTTP待ち受けて結果を返すだけ。
あとグローバルに公開したときにあんまり誰にでも見て欲しい情報ではないのでHTTPのAuthorization Headerに適当なパスワードを設定させて認証してる。

あとは`go run *.go`なりで起動すれば8080番で待ち受けて在席者リストを返してくれる。

golangに関してはまだ初心者なのでコードにつっこみなり何なりもらえると嬉しいです。

# 公開
公開は[ngrok](https://ngrok.com/)で雑にやる。
ngrok.ioと自前のマシンの間でトンネル掘っていい感じにポートフォワーディングして外部に公開してくれるいい感じのやつ。

要は自身のマシンが外部からのアクセス受け取るようになるということなので、この行為のリスクはちゃんと認識したうえで実行しましょう。
あとネットワークの管理者とも合意を取るべきとか色々ありますが、組織の偉い人に何を言われるか分からないので注意しましょうとだけ言っておきます。

実行は[公式サイト]([https://ngrok.com)に行ってバイナリ落としてきて

```sh
$ ./ngrok http 8080
```
とすればURLが払い出される。

これでAPIがどこからでも呼び出せるようになるので

```sh
curl -H "Authorization:YOUR_PASSWORD" https://xxxxxxxx.ngrok.io
```
とかやれば動いてると思います。

後はSlackと連携なりLINEと連携なり好きにやってください。

LINEとかだとこんな感じで在席者一覧が見れる（人名はぼかしてる）
![Point Blur_20181212_002608.jpg](https://qiita-image-store.s3.amazonaws.com/0/234127/51e70cac-98ab-5247-8570-9f629b1f29e9.jpeg)

# その他注意
在席者管理とは言っても本質的には監視と変わらないので他のメンバーの同意無く動かすとかはやめましょう。

勝手に他人のMACアドレスを取って監視対象にするとかもやめときましょう。

以下参考にした資料

[The Go Programming Language Package sync](https://golang.org/pkg/sync/)
[Golangで外部コマンドを実行する方法まとめ](https://qiita.com/tanksuzuki/items/9205ff70c57c4c03b5e5)
[1984](https://www.amazon.co.jp/%E4%B8%80%E4%B9%9D%E5%85%AB%E5%9B%9B%E5%B9%B4-%E6%96%B0%E8%A8%B3%E7%89%88-%E3%83%8F%E3%83%A4%E3%82%AB%E3%83%AFepi%E6%96%87%E5%BA%AB-%E3%82%B8%E3%83%A7%E3%83%BC%E3%82%B8%E3%83%BB%E3%82%AA%E3%83%BC%E3%82%A6%E3%82%A7%E3%83%AB/dp/4151200533)
[Go言語 - Tickerを使った定期処理](https://blog.y-yuki.net/entry/2017/04/29/000000)