[Go]slack apiでチャンネル名/ユーザ名からチャンネルID/ユーザーIDを検索する

# 経緯

1. slack apiのパラメータにはchannel idやuser idが必要
2. 手動でも取得できるがブラウザ開く必要があるのでちと面倒
   <br>参考: [SlackのチャンネルIDを確認する方法 - Qiita](https://qiita.com/unsoluble_sugar/items/603e51106d9632f3ea4f)
3. じゃあチャンネル名/ユーザー名でIDを検索する処理を作ろう

# チャンネル名からIDを検索する

slackのOAuth Tokensを作る方法は探せば出てくるので省略。
必要な権限は以下の通りです。

| 権限 |
|:-:|
| [channels:read](https://api.slack.com/scopes/channels:read) |

slackでチャンネル名からIDを検索するためには[conversations.list](https://api.slack.com/methods/conversations.list)でチャンネル一覧を取得する必要があります。
そしてこの[conversations.list](https://api.slack.com/methods/conversations.list)には一度で取得できるチャンネル数に上限があるためチャンネルが多い場合はcursorを指定して何度か叩かなければいけません。

今回は[slack-go/slack](https://github.com/slack-go/slack)という便利なライブラリがあるのでこれを使用します。
内部でリクエストを作成してGET叩いてるだけなのでapiを直接GETで叩いても行けると思います。


実装してみるとこんな感じ。

```go:slackclient/client.go
package slackclient

import (
	"context"
	"fmt"
	"github.com/pkg/errors"
	"github.com/slack-go/slack"
)

type SlackClient interface {
	SearchChannel(ctx context.Context, channelName string) (*slack.Channel, error)
}

type slackClient struct {
	Api *slack.Client
}

func NewClient(token string, options ...slack.Option) SlackClient {
	api := slack.New(token, options...)
	return &slackClient{api}
}

func (s slackClient) SearchChannel(ctx context.Context, channelName string) (*slack.Channel, error) {
	var cursor string
	for {
		requestParam := &slack.GetConversationsParameters{
			// public, private両方対象にしていますがどちらかにする場合は不要な方を外せばok
			// conversations.list的にはDMも対応しているけど今回は対象外
			Types:           []string{"public_channel", "private_channel"},
			// Must be an integer no larger than 1000.
			// https://api.slack.com/methods/conversations.list#arg_limit
			Limit:           1000,
			// アーカイブされたチャンネルは除外
			// アーカイブされたチャンネルも検索したいならfalseに設定
			ExcludeArchived: "true",
		}
		if cursor != "" {
			requestParam.Cursor = cursor
		}
		var channels []slack.Channel
		var err error
		channels, cursor, err = s.Api.GetConversationsContext(ctx, requestParam)
		if err != nil {
			return nil, errors.WithStack(err)
		}
		for _, channel := range channels {
			if channel.Name == channelName {
				return &channel, nil
			}
		}
		if cursor == "" {
			break
		}
	}
	return nil, errors.New(fmt.Sprintf("channel not found. channelName=%s", channelName))
}
```

一度のapi callで全部のチャンネルを取り切れないのでそこまでシンプルにはならないが、一度実装してしまえば使い回せるのでヨシ。
早速テストを書いて動作確認をしてみる。

```go:slackclient/client_test.go
package slackclient_test

import (
	"context"
	"github.com/stretchr/testify/assert"
	"os"
	"testing"
	"trial-go/syusya/slackclient"
)

func TestSlackClient_SearchChannel(t *testing.T) {
	token := os.Getenv("SLACK_TOKEN")
	if !assert.NotZero(t, token) {
		t.FailNow()
	}

	t.Run("Search for existing channel", func(t *testing.T) {
		ctx := context.Background()
		client := slackclient.NewClient(token)

		channelName := "harada-debug"
		channel, err := client.SearchChannel(ctx, channelName)
		if !assert.NoError(t ,err) {
			t.FailNow()
		}
		assert.Equal(t, channelName, channel.Name)
	})

	t.Run("Search for non-existent channel", func(t *testing.T) {
		ctx := context.Background()
		client := slackclient.NewClient(token)

		channelName := "not exists channel"
		_, err := client.SearchChannel(ctx, channelName)
		assert.Error(t ,err)
	})
}
```

```
=== RUN   TestSlackClient_SearchChannel
--- PASS: TestSlackClient_SearchChannel (1.52s)
=== RUN   TestSlackClient_SearchChannel/Search_for_existing_channel
channel_id=<チャンネルID>, channel_name=harada-debug
    --- PASS: TestSlackClient_SearchChannel/Search_for_existing_channel (0.87s)
=== RUN   TestSlackClient_SearchChannel/Search_for_non-existent_channel
error: channel not found. channelName=not exists channel
    --- PASS: TestSlackClient_SearchChannel/Search_for_non-existent_channel (0.65s)
PASS
```

上手く動いてますね。
ちょっと心配なのは例えばチャンネル数が1万とかあった場合10回api callが連続で飛ぶけどapi limitに引っかからないかなという所ですかね。
api limitに引っかかった場合はwait入れてみるしかないのかもしれません。


# ユーザー名からIDを検索する

チャンネル一覧と違ってユーザ一覧はcursor操作がないのでシンプルです。
ただし、検索対象としてはメンションで指定する表示名は`user.Name`ではなく`user.Profile.DisplayName`になるので注意が必要です。

| 権限 |
|:-:|
| - |


```go:slackclient/client.go
package slackclient

import (
	"context"
	"fmt"
	"github.com/pkg/errors"
	"github.com/slack-go/slack"
)

type SlackClient interface {
	SearchChannel(ctx context.Context, channelName string) (*slack.Channel, error)
	SearchUser(ctx context.Context, userName string) (*slack.User, error)
}

func (s slackClient) SearchUser(ctx context.Context, userName string) (*slack.User, error) {
	users, err := s.Api.GetUsersContext(ctx)
	if err != nil {
		return nil, errors.WithStack(err)
	}
	for _, user := range users {
		if user.IsAppUser || user.IsBot || user.Deleted {
			continue
		}
		if user.Profile.DisplayName == userName {
			return &user, nil
		}
	}
	return nil, errors.New(fmt.Sprintf("user not found. userName=%s", userName))
}
```

テストで動かしてみます。

```go:slackclient/client_test.go
func TestSlackClient_SearchUser(t *testing.T) {
	token := os.Getenv("SLACK_TOKEN")
	if !assert.NotZero(t, token) {
		t.FailNow()
	}

	t.Run("Search for existing user", func(t *testing.T) {
		ctx := context.Background()
		client := slackclient.NewClient(token)

		userName := "tharada"
		user, err := client.SearchUser(ctx, userName)
		if !assert.NoError(t, err) {
			t.FailNow()
		}
		assert.Equal(t, "tharada", user.Profile.DisplayName)
		fmt.Printf("user_id=%s, user_name=%s\n", user.ID, user.Profile.DisplayName)
	})

	t.Run("Search for non-existent user", func(t *testing.T) {
		ctx := context.Background()
		client := slackclient.NewClient(token)

		userName := "not exists user"
		_, err := client.SearchUser(ctx, userName)
		assert.Error(t, err)
	})
}
```

```
=== RUN   TestSlackClient_SearchUser
--- PASS: TestSlackClient_SearchUser (2.45s)
=== RUN   TestSlackClient_SearchUser/Search_for_existing_user
user_id=<user id>, user_name=tharada
    --- PASS: TestSlackClient_SearchUser/Search_for_existing_user (1.22s)
=== RUN   TestSlackClient_SearchUser/Search_for_non-existent_user
    --- PASS: TestSlackClient_SearchUser/Search_for_non-existent_user (1.23s)
```

無事ユーザ名からユーザー情報が取得できました。