Discord で Voice チャンネルへの入室を検知する bot (ついでに Google Home で通知

### 発端
スマブラやりつつ 身内で Discord で VC やってるのだけど、いかんせんスマブラは部屋つくった通知が来ない。
ただ、誰かが部屋つくって遊ぶぞ ってなった時、だいたい VC も開始するので、そっちを通知できるようにしようと思って 簡単な bot 作りました。
(元々PCゲー遊ぶ仲間のサーバーなので 他でも活用できそう)

サーバーは自室にあるので、直接音出してもいいんだけど、ちょうど導入に詰まった友人のサポートするのに、Google-home-notifier を導入していたので、Google Home から通知を飛ばします。


### 準備

検索して一番上に出てきたのが discordgo を使った以下の記事だったので、そちらをベースに VoiceStateUpdate を監視するコードに変更しました。
・[GoでDiscordの簡易botを作ろう - Qiita](https://qiita.com/kamol/items/e691c878e45235a8a9e2)

Go側の準備、Discord側の準備 については、上記の記事の通りに作業したので割愛します。
Bot の設定する Discord-My Apps ページの UI だけ、最近アップデートされたっぽくてスクショと案内からは変化してたので、雰囲気でやっていってください。
認証とかbotアカウントについての設定を分離してサイドバーのメニューで分けただけなので、やる事は一緒。


### コード

全文は Gist に置いておきますので適当に。
https://gist.github.com/tyoro/bbe7532695fac4e8bafe8d2e6636043d
Go 言語書いた事なかったので、冗長な部分があったら指摘貰えると嬉しいです。

以下、変更部分についての簡単な解説。

元スクリプトはコマンドのやりとりをして、指定された Voice チャンネルに入り、発言等を監視するコードになっていたのだけど、
これだと監視可能なチャンネルが1つになってしまうのと、常時 VC に bot が見えていて邪魔だったので他の方法を探しました。
(bot に音声で指令出したり、bot からボイチャに音声を流したりしたい場合は元の方法でアプローチするのがよさそう)

[type VoiceState - discordgo - GoDoc](https://godoc.org/github.com/bwmarrin/discordgo#VoiceState)

```go:discordgo/structs.go
type VoiceState struct {
    UserID    string `json:"user_id"`
    SessionID string `json:"session_id"`
    ChannelID string `json:"channel_id"`
    GuildID   string `json:"guild_id"`
    Suppress  bool   `json:"suppress"`
    SelfMute  bool   `json:"self_mute"`
    SelfDeaf  bool   `json:"self_deaf"`
    Mute      bool   `json:"mute"`
    Deaf      bool   `json:"deaf"`
}
```
Discordgo のドキュメントを眺めていると、この VoiceState という構造体が、ユーザーのVCに対する状態を表すステータスであり、
VoiceStateUpdate という構造体をイベントハンドラに渡すメソッドの引数にする事で、更新情報をフックできそうなのが分かります。

```go:main.go
func main() {
    ...
    //VoiceStateUpdate のハンドラを追加
    discord.AddHandler(onVoiceStateUpdate)
    ...
}

func onVoiceStateUpdate(s *discordgo.Session, vs *discordgo.VoiceStateUpdate ) {
    log.Print("hoge")
    fmt.Printf("%+v", vs.VoiceState)
}

```

これだけで、ユーザーが VC に出入りしたりミュートの切り替えなどする度に、hoge と ステートの中身がコンソールに掃き出されるようになります。
今回やりたいのはジョインした時の通知のみですが、あくまでもこの構造体には状態しか記憶されていない為、前状態の情報が無いのでステートの変化について知る事ができません。
変化を実現する為に、1つ前の情報をこちら側で管理する必要があるため、ハッシュマップに以前居たチャンネルの情報を持たせて保存しておき、更新されたStateが来る度に比較して、ChannnelIDが変化していて、なおかつ現在の状態が空でなければ通知する形にすると良さそうです。
ついでに何度もユーザー名をIDから検索するのはめんどくさいので、ユーザーIDも構造体側に持っておきます。


```go:main.go
type UserState struct{
    Name string
    CurrentVC string
}

var(
    ...
    usermap = map[string]*UserState{}
)

```
ポインタと構造体を同時に扱うのは C++ 以来なので懐かしい気持ちになります。
今後もっと通知させたい状態変化があったら、構造体を拡張して状態を記憶してあげれば良さそうですね。

あとは、通知が来た時にハッシュマップ上にユーザーが存在しなければ追加しつつ
過去の状態と比較して通知をする処理を書いてあげれば終わりです。

```go:main.go
func onVoiceStateUpdate(s *discordgo.Session, vs *discordgo.VoiceStateUpdate ) {
        _, ok := usermap[vs.UserID]
        if !ok {
            //Userが居ない VC 未設定の User を追加しておく
            usermap[vs.UserID] = new(UserState)
            user, _ := discord.User(vs.UserID)
            usermap[vs.UserID].Name = user.Username
            log.Print("new user added : "+user.Username)
        }

        if len(vs.ChannelID) > 0 && usermap[vs.UserID].CurrentVC != vs.ChannelID {
            channel, _ := discord.Channel(vs.ChannelID)
            message := usermap[vs.UserID].Name+"さんが"+channel.Name+"にジョインしました"
            log.Print(message)
            // ここに ジョインを通知するのに利用する外部ツールの処理を書く


            defer resp.Body.Close()
            //別にレスポンスいらないしもっとシンプルに書けるのかもしれない
        }

        usermap[vs.UserID].CurrentVC = vs.ChannelID

```

この辺、Go 初心者なので冗長な感じになっている気がしますが、もうちょっと簡素に書ける気がします。
チャンネルもそんな多くなる事はないので、専用のハッシュマップで覚えさせてしまえば、無駄なAPIコールが減らせる気はしますね。
(discordgo側でキャッシュしてるかもしれないけど

## 通知
あとはオマケですが、ローカルで立てている google-home-notifier に通知を投げている部分です。

google-home-notifier を動かすまでの部分に関しては、Qiita に山程記事があるので検索してがんばってくれ。
google-tts-api のバージョン絡みの部分だけひっかかりやすいので、記事の公開日には気をつけてね。

example.js そのまま使うのもどうかと思うので、適当にソケット開いて直接通信してもいいんだけど、
とりあえず、元のサンプル動かすだけで使えるので、GET で受け渡す一番シンプルなやつを置いておきます。
port はサンプルに記載されたものと同じにしてありますが、変更している場合はそちらに合わせてください。

```go:main.go
            // ここに ジョインを通知するのに利用する処理を書く
            // 以下は ローカルで建てた Google-home-notifier の example.js に処理を投げつける場合
            values := url.Values{}
            values.Add("text", message)
            resp, err := http.Get("http://localhost:8091/google-home-notifier" + "?" + values.Encode())
            if err != nil {
                    fmt.Println(err)
                    return
            }
```

これで、VC に出入りすると「ちょろさんがジェネラルにジョインしました。」って発話してくれるようになりました。
おつかれさまでした。

## 最後に
検索して見付けた記事が大変わかりやすかったのと、コードがシンプルで少し書き換えるだけで実現できそうだったので、今回 Go でやったんですが
最終的に google-home-notifier に出すのが前提としてあるのなら、最初から node で書いた方が実行環境がシンプルになって良さそうですね。

このまま稼動させると、夜中とかに VC 入ったのも通知されるので時間帯指定の DnD 機能くらい付けた方がいいかもしれない。
あとやるとしたら、 PresenceUpdate も監視対象にして、ホワイトリストに入れたゲームを誰かが起動すると「○○さんがロケットリーグを開始しました」みたいな通知だすくらいかなぁ。