[LINE Bot] LIFFとリッチメニューでも管理画面が作りたい! 1 -グループ管理-

# bot用の管理機能を作るのは面倒だからLIFFでしたい!
LINEBotで色々開発をしていると、当然管理者用のAPIが生えて来たり、管理者用のリッチメニューが欲しくなったりしますよね。
今までの開発方法だと、管理者用のシステムを一つ作って、そこにソーシャルログインを連携させて、メンバー管理画面を作って、、、と平気でひと月やふた月飛んでいきます。
そこで、今回は、
1. 管理画面はLIFFで作る
2. 管理者用のLINEグループを一つ作る
3. そこにメンバーを招待したら管理者にする (DBにユーザIDを追加)
4. そこからメンバーを退出させたら管理者から除外する (DBからユーザIDを削除)
5. ついでにリッチメニューもユーザー用と管理者用で切り替える
6. LIFFでアクセストークンを取得してサーバ側で認証する (アクセストークンからユーザIDを取得し、DBにあるかチェックする)

||よくあるパターン|新しいやつ|
|---|---|---|
|管理仮面|PCorスマホ用管理画面|LIFF|
|アクセス方法|URLを教える|リッチメニューからアクセス|
|ログイン方法|ソーシャルログインなり、メアドなり実装する|LINEログインを使う<br />というかリッチメニュー見ている時点でログイン済み|
|認証方法|自分でトークン管理する|LIFFのアクセストークン|
|メンバー管理|メンバー追加画面を自分で実装してそこから追加する|LINEの管理用グループを作ってそこに招待する|

というのをやりました。

# 開発環境
- Go 1.12
- Rust 1.4.2
- DynamoDB
- Amazon Lambda

BotやLambdaの連携や設定、基本的なコードについては、[[AWSLambda x LINEBot] GoとRustでも連携がしたい](https://qiita.com/alivelime/items/274966b10df905920d89) をご覧ください。

# LINEグループで管理者のメンバー管理をする
管理画面の何がめんどいって、メンバーの追加・削除画面とログインと機能ですよね。
そこでまず、メンバー管理をLINEグループへの招待・退出だけでできるようにします。
LINEのグループは作った人以外でも招待・退出・追放できます。
仮にメンバーの誰かにグループを乗っ取られても、新しくグループを作ってそこのグループIDを処理対象にすれば良いのでセキュアです。

### グループIDを取得するためのコードを書く
まず、グループIDを調べるために、botにグループ招待された時のイベント処理コードを書きます。
それ以外にグループIDを知るすべはなさそうです。
対象となるイベントは `参加イベント join` です。

```go:line/line.go

func (r *Line) EventRouter(eve []*linebot.Event) {
	for _, event := range eve {
		switch event.Type {
		case linebot.EventTypeMessage:
            // テキストメッセージが来た時の処理とか
		case linebot.EventTypeJoin:
            // これ
			log.Printf("Join group id : %v", event.Source.GroupID)
		case linebot.EventTypeMemberJoined:
            // グループにメンバーを招待した時の処理。後ほど記載
		case linebot.EventTypeMemberLeft:
            // グループからメンバーが退出した時の処理。後ほど記載
		}
	}
}
```

### LINEグループを作成し、グループIDを取得する
お手元のLINEで、新しいグループを一つ作りまして、グループ名を `管理者部屋` とでもしておきます。
そして、botをグループに招待します。
すると、ログ(今回はCloudWatch)にグループIDが出力されるので、それをメモしておきます。

## グループへの招待・退出イベントを実装する
先ほどと同じbotのイベント処理に追加します。

```go:line/line.go
func (r *Line) EventRouter(eve []*linebot.Event) {
	for _, event := range eve {
		switch event.Type {
		case linebot.EventTypeMessage:
           // ...
		case linebot.EventTypeJoin:
			log.Printf("Join group id : %v", event.Source.GroupID)
		case linebot.EventTypeMemberJoined:
           // 管理者部屋にメンバーが追加された時の処理
			if event.Source.GroupID == 管理部屋のグループID {
				if event.Joined != nil {
					// handleMemberJoined(r, event.Joined.Members)
				} else {
					handleMemberJoined(r, event.Members, event.ReplyToken)
				}
			}
		case linebot.EventTypeMemberLeft:
           // 管理者部屋からメンバーが退出した時の処理
			if event.Source.GroupID == 管理部屋のグループID {
				if event.Left != nil {
					// handleMemberLeft(r, event.Left.Members)
				} else {
					handleMemberLeft(r, event.Members)
				}
			}
		}
	}
}
```

これでイベントはフックできるので、後はユーザIDを取得してDBに入れればOKです。
で、このコードのここの部分なんですが、

```go
				if event.Joined != nil {
					// handleMemberJoined(r, event.Joined.Members)
				} else {
					handleMemberJoined(r, event.Members, event.ReplyToken)
				}
```

実は公式ドキュメント通りにやると、 `event.Joined == nil` となっていてぬるぽで落ちます。
公式ドキュメントでは joined または left にメンバーが入ってくると記載されていますが、

```json
LINE公式ドキュメントより
{
  "replyToken": "0f3779fba3b349968c5d07db31eabf65",
  "type": "memberJoined",
  "mode": "active",
  "timestamp": 1462629479859,
  "source": {
    "type": "group",
    "groupId": "C4af4980629..."
  },
  "joined": {
    "members": [
      {
       "type": "user",
        "userId": "U4af4980629..."
      },
      {
        "type": "user",
        "userId": "U91eeaf62d9..."
      }
    ]
  }
}

だが、実際には
{
  "replyToken": "hoge",
  "type": "memberJoined",
  "mode": "active",
  "timestamp": 11111111111,
  "source": {
    "type": "group",
    "groupId": "管理部屋のグループID"
  },
  "members": [
    {
     "type": "user",
      "userId": "U4af4980629..."
    },
    {
      "type": "user",
      "userId": "U91eeaf62d9..."
    }
  ]
}

```
実際にはevent直下の `members` にメンバー情報が入って来ます。
そのうち修正されるかもしれないので、if で正しいと思われるコードを残してあります。(line-sdk-go のバージョンは7.4.0)

#### 管理者情報をDynamoDBに保存し、リッチメニューを切り替える
LINEのユーザIDを取得し、DynamoDBに追加すれば完了です。
今回はDynamoDBの処理はRustで書いているので、Lambdaを呼び出すだけになっています。
リッチメニューについては下の方でまとめて記載します。

```go
func handleMemberJoined(r *Line, users []*linebot.EventSource, replyToken string) {
	type MemberEvent struct {
		UserIDs []string `json:"user_ids"`
	}
	userIDs := make([]string, len(users))
	for i, u := range users {
		userIDs[i] = u.UserID
	}
    // リッチメニューを管理者用に切り替える
	r.LinkRichMenu(userIDs)

	payload, _ := json.Marshal(MemberEvent{
		UserIDs: userIDs,
	})

	_, err := lambda.New(session.New()).Invoke(&lambda.InvokeInput{
		FunctionName:   aws.String(ARN + "myproject-" + os.Getenv("ENV") + "-addUsers"),
		Payload:        payload,
		InvocationType: aws.String("RequestResponse"),
	})
	if err != nil {
		log.Println(err)
	}

    // このイベントには応答できるので、一言挨拶があると嬉しいかも?
	r.ReplyTextMessage(replyToken, "よろしくね")
}

func handleMemberLeft(r *Line, users []*linebot.EventSource) {
	type MemberEvent struct {
		UserIDs []string `json:"user_ids"`
	}
	userIDs := make([]string, len(users))
	for i, u := range users {
		userIDs[i] = u.UserID
	}
        // 管理者用のリッチメニューを解除 (デフォルトのユーザ用のメニューになる)
	r.LinkRichMenu(userIDs)

	payload, _ := json.Marshal(MemberEvent{
		UserIDs: userIDs,
	})

	_, err := lambda.New(session.New()).Invoke(&lambda.InvokeInput{
		FunctionName:   aws.String(ARN + "myproject-" + os.Getenv("ENV") + "-removeUsers"),
		Payload:        payload,
		InvocationType: aws.String("RequestResponse"),
	})
	if err != nil {
		log.Println(err)
	}
}
```

#### 自分自身を追加するのを忘れずに。。
グループを作るときは、必ず自分が作ったものにbotを招待しないといけないので、 `自分自身を招待する` ということができません。
その為、誰かに管理者部屋に参加してもらってから自分は一旦抜け、改めて招待してもらうか、直接DynamoDBをいじるなりして自分のユーザIDヲDBに追加する必要があります。


長くなって来たので一旦ここまで、次に続く。
#### リッチメニューを切り替える
[[LINE Bot] LIFFとリッチメニューでも管理画面が作りたい! 2 -リッチメニュー切替-](https://qiita.com/alivelime/items/2d16441153af9c444619)
#### LIFFでアクセストークンを取得し、APIで認証する
(明日といいつつ、来週かな)

##### 20200622 ソースコード修正
handleMemberJoined()とhandleMemberLeft()のユーザIDを配列にしているところで、
makeした直後にappendにしているのを修正しました。
なんか空のユーザIDが入ってきておかしいなと思った。