C#からSlackAPIを利用してチャンネルのメッセージを画像付きで取得する

## はじめに
本記事はC#からSlackチャンネルのメッセージを画像付きで取得するためのノウハウと具体的な実装についてまとめることを目的としています。  
本記事は[こちら](https://qiita.com/tat_tt/items/e10bd5d9aee897f0b8b6)の記事の続編です。

## 前提条件
本記事ではSlackAPIを利用して、メッセージの取得を実現します。
したがって、前編となる[こちら](https://qiita.com/tat_tt/items/e10bd5d9aee897f0b8b6)の記事を参考にして、SlackAppの作成とアクセストークンの取得が完了していることを前提としています。
なお、本記事でやりたいメッセージを画像付きで取得するために必要なスコープは以下になっていますので、実際に試される場合には設定お願いします。

![スコープ設定.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/452292/c4f5f63a-e5e3-0ec5-26c9-f4967e53456f.png)


## 必要なAPI
特定のチャンネルからメッセージを画像付きで取得するためには、以下の3ステップを踏む必要があります。

- チャンネル一覧を取得し、その中からメッセージを取得したいチャンネルのIDを取得
- 取得したチャンネルIDを使って、メッセージを取得
- メッセージには添付ファイルの情報が含まれるので、その添付ファイル情報を使って画像をダウンロード

上記の3ステップのうち次の2ステップにはそれぞれ以下のAPIを用います。

- [チャンネル一覧の取得](https://api.slack.com/methods/users.conversations)
- [メッセージの取得](https://api.slack.com/methods/conversations.history)

画像のダウンロードについては[こちら](https://api.slack.com/types/file)の公式資料を元に実現します。

## 実装
実装に必要なパッケージはNewtonSoft.Jsonです。プロジェクトを右クリックして「NuGetパッケージの管理」メニューからインストールしてください。
今回はチャンネル一覧の取得、メッセージの取得、画像のダウンロードを機能として持つサービスをSlackMessageGetServiceとして実装しました。
したがって、上記のサービスからチャンネル一覧の取得、メッセージの取得、画像のダウンロードを行うメソッドを抜粋して説明していきます。

### チャンネル一覧の取得
チャンネル一覧の取得はGetChannelsメソッドが担っています。
GetChannelsメソッド内で使うSlackAPIのJsonレスポンスを解釈するためのクラス、GetChannelsメソッドの戻り値用クラス、GetChannelsメソッドの順で以下に示します。

- SlackAPIのJsonレスポンスを解釈するためのクラス

```csharp
/// <summary>
/// チャンネル一覧取得時のレスポンス
/// </summary>
public class GetSlackChannelsResponce
{
	#region プロパティ

	/// <summary>
	/// チャンネル一覧
	/// </summary>
	// ReSharper disable once InconsistentNaming
	public List<ChannelResponce> channels { get; set; } = new List<ChannelResponce>();

	#endregion
}

/// <summary>
/// チャンネルのレスポンス
/// </summary>
public class ChannelResponce
{
	#region プロパティ

	/// <summary>
	/// チャンネルID
	/// </summary>
	// ReSharper disable once InconsistentNaming
	public string id { get; set; } = string.Empty;

	/// <summary>
	/// チャンネル名
	/// </summary>
	// ReSharper disable once InconsistentNaming
	public string name { get; set; } = string.Empty;

	#endregion
}
```

- GetChannelsメソッドの戻り値用クラス

```csharp
/// <summary>
/// チャンネル
/// </summary>
public class Channel
{
	#region プロパティ

	/// <summary>
	/// チャンネルID
	/// </summary>
	public string Id { get; set; } = string.Empty;

	/// <summary>
	/// チャンネル名
	/// </summary>
	public string Name { get; set; } = string.Empty;

	#endregion
}
```

- GetChannelsメソッド

```csharp
/// <summary>
/// Slackメッセージの取得に必要な機能を提供するサービス
/// </summary>
public class SlackMessageGetService
{
	#region 定数

	/// <summary>
	/// アクセストークン用のクエリパラメータ名
	/// </summary>
	private const string c_AccessTokenQueryParameterName = "token";

  ・・・

	/// <summary>
	/// チャンネルタイプのクエリパラメータ名
	/// </summary>
	private const string c_ChannelTypesQueryParameterName = "types";

	/// <summary>
	/// チャンネルタイプのクエリパラメータの値
	/// </summary>
	private const string c_ChannelTypesQueryParameterValue = "public_channel, private_channel";

  ・・・

	#endregion

	#region フィールド

	/// <summary>
	/// SlackにPOST,GETするために使用するHttpClient
	///
	/// ユーザーの利用方法を想定したとき、接続先のホストは[チーム名].slack.comしかありえないため、
	/// HttpClientは単一インスタンスのみとする
	/// </summary>
	// ReSharper disable once InconsistentNaming
	private static readonly HttpClient m_HttpClient = new HttpClient();

	#endregion

	#region 公開サービス

  ・・・

	/// <summary>
	/// チャンネルを取得する
	/// 指定したアクセストークンで編集できるチャンネルを一覧で取得する。
	/// </summary>
	/// <param name="accessToken">アクセストークン</param>
	/// <returns>チャンネル一覧</returns>
	public async Task<IEnumerable<Channel>> GetChannels(string accessToken)
	{
		var channels = new List<Channel>();

		// クエリパラメータを作成するためにディクショナリを作成
		// ディクショナリのKeyがクエリパラメータ名、ディクショナリのValueがクエリパラメータの値
		var parameters = new Dictionary<string, string>()
		{
			{c_AccessTokenQueryParameterName, accessToken},
			{c_ChannelTypesQueryParameterName, c_ChannelTypesQueryParameterValue }
		};

		try
		{
			// クエリパラメータを作成し、文字列で読み出す
			var parametersString = await new FormUrlEncodedContent(parameters).ReadAsStringAsync();
			// 読みだしたクエリパラメータを使ってリクエストURLを作成する。
			var requestBaseUrl = "https://slack.com/api/users.conversations";
			var requestUrl = $"{requestBaseUrl}?{parametersString}";
			var response = await m_HttpClient.GetAsync(requestUrl).ConfigureAwait(false);
			// レスポンスのコンテンツをstringで読み出す
			var responseBodyString = await response.Content.ReadAsStringAsync();
			var responceObject = JsonConvert.DeserializeObject<GetSlackChannelsResponce>(responseBodyString);

			// 戻り値用のチャンネルリストを作成
			foreach (var channelResponce in responceObject.channels)
			{
				var channel = new Channel()
				{
					Id = channelResponce.id,
					Name = channelResponce.name
				};
				channels.Add(channel);
			}

			return channels;

		}
		catch
		{
			return channels;
		}
	}

  ・・・

	#endregion
}

```

GetChannelsメソッドのポイントを説明していきます。
ポイントはクエリパラメータとして、アクセストークンとチャンネルタイプの情報をリクエストURLに付与している点です。
クエリパラメータで付与している理由は、[こちら](https://api.slack.com/methods/users.conversations)のAPIリファレンス上で、GETメソッドが優先されており、コンテンツタイプはapplication/x-www-form-urlencodedのみ受け付けると記載されているためです。
[こちら](https://wiki.suikawiki.org/n/application%2Fx-www-form-urlencoded)の記事をみるとコンテンツタイプ：application/x-www-form-urlencodedは、GETメソッドのURLQueryで利用されていると記載されています。このURLQueryがまさにクエリパラメータのことで、URLの後ろに?をつけて「引数名=値&引数名=値...」という形式でGETメソッドに対して引数を与えます。
つまり、SlackAPIでチャンネル一覧を取得するためにはリクエストURLである https://slack.com/api/users.conversations の後にクエリパラメータで引数を与える必要があります。
そのため、GetChannelsメソッドでは、引数となるアクセストークンとチャンネルタイプの引数名と値の組み合わせをディクショナリで保持し、FormUrlEncodedContentクラスを使って、ディクショナリから「引数名=値&引数名=値...」という形式の文字列を作成し、クエリパラメータとしてリクエストURLに追加しています。

### メッセージの取得
メッセージの取得はGetMessagesメソッドが担っています。
GetMessagesメソッド内で使うSlackAPIのJsonレスポンスを解釈するためのクラス、GetMessagesメソッドの戻り値用クラス、GetMessagesメソッドの順で以下に示します。

- SlackAPIのJsonレスポンスを解釈するためのクラス

```csharp
/// <summary>
/// メッセージ一覧取得時のレスポンス
/// </summary>
public class GetSlackMessagesResponce
{
	#region プロパティ

	/// <summary>
	///メッセージ一覧
	/// </summary>
	// ReSharper disable once InconsistentNaming
	public List<MessageResponce> messages { get; set; } = new List<MessageResponce>();

	#endregion
}

/// <summary>
/// メッセージのレスポンス
/// </summary>
public class MessageResponce
{
	#region プロパティ

	/// <summary>
	/// テキスト
	/// </summary>
	// ReSharper disable once InconsistentNaming
	public string text { get; set; } = string.Empty;

	/// <summary>
	/// 添付ファイル一覧
	/// </summary>
	// ReSharper disable once InconsistentNaming
	public List<FileResponce> files { get; set; } = new List<FileResponce>();

	#endregion
}

/// <summary>
/// メッセージに添付されるファイルのレスポンス
/// </summary>
public class FileResponce
{
	#region プロパティ

	/// <summary>
	/// ファイルID
	/// </summary>
	// ReSharper disable once InconsistentNaming
	public string id { get; set; } = string.Empty;

	/// <summary>
	/// ファイルのURL
	/// </summary>
	// ReSharper disable once InconsistentNaming
	public string url_private { get; set; } = string.Empty;

	#endregion
}
```

- GetMessagesメソッドの戻り値用クラス

```csharp
/// <summary>
/// メッセージ
/// </summary>
public class Message
{
	#region プロパティ

	/// <summary>
	/// 本文
	/// </summary>
	public string Text { get; set; } = string.Empty;

	/// <summary>
	/// 添付ファイル一覧
	/// </summary>
	public List<File> Files = new List<File>();

	#endregion
}

/// <summary>
/// 添付ファイル
/// </summary>
public class File
{
	#region プロパティ

	/// <summary>
	/// ファイルID
	/// </summary>
	public string Id { get; set; } = string.Empty;

	/// <summary>
	/// ファイルのURL
	/// </summary>
	public string FileURL { get; set; } = string.Empty;

	#endregion
}
```

- GetMessagesメソッド

```csharp
/// <summary>
/// Slackメッセージの取得に必要な機能を提供するサービス
/// </summary>
public class SlackMessageGetService
{
	#region 定数

	/// <summary>
	/// アクセストークン用のクエリパラメータ名
	/// </summary>
	private const string c_AccessTokenQueryParameterName = "token";

	/// <summary>
	/// チャンネルID用のクエリパラメータ名
	/// </summary>
	private const string c_ChannelIdQueryParameterName = "channel";

	/// <summary>
	/// 取得メッセージ数用のクエリパラメータ名
	/// </summary>
	private const string c_MessageCountQueryParameterName = "limit";

	・・・

	#endregion

	#region フィールド

	/// <summary>
	/// SlackにPOST,GETするために使用するHttpClient
	///
	/// ユーザーの利用方法を想定したとき、接続先のホストは[チーム名].slack.comしかありえないため、
	/// HttpClientは単一インスタンスのみとする
	/// </summary>
	// ReSharper disable once InconsistentNaming
	private static readonly HttpClient m_HttpClient = new HttpClient();

	#endregion

	#region 公開サービス

	/// <summary>
	/// メッセージを取得する
	/// </summary>
	/// <param name="accessToken">アクセストークン</param>
	/// <param name="channelId">チャンネルID</param>
	/// <param name="messageCount">取得する(最新メッセージからの)メッセージ数</param>
	/// <returns>取得メッセージ一覧</returns>
	public async Task<IEnumerable<Message>> GetMessages(string accessToken, string channelId, int messageCount)
	{
		var messages = new List<Message>();

		// クエリパラメータを作成するためにディクショナリを作成
		// ディクショナリのKeyがクエリパラメータ名、ディクショナリのValueがクエリパラメータの値
		var parameters = new Dictionary<string, string>()
		{
			{ c_AccessTokenQueryParameterName, accessToken},
			{ c_ChannelIdQueryParameterName, channelId},
			{ c_MessageCountQueryParameterName, messageCount.ToString()}
		};

		try
		{
			// クエリパラメータを作成し、文字列で読み出す
			var parametersString = await new FormUrlEncodedContent(parameters).ReadAsStringAsync();
			// 読みだしたクエリパラメータを使ってリクエストURLを作成する。
			var requestBaseUrl = "https://slack.com/api/conversations.history";
			var requestUrl = $"{requestBaseUrl}?{parametersString}";
			var response = await m_HttpClient.GetAsync(requestUrl).ConfigureAwait(false);
			// レスポンスのコンテンツをstringで読み出す
			var responseBodyString = await response.Content.ReadAsStringAsync();
			// 読みだしたJsonを、オブジェクトにデシリアライズする
			var responceObject = JsonConvert.DeserializeObject<GetSlackMessagesResponce>(responseBodyString);

			// 戻り値用のメッセージ一覧を作成
			foreach (var messageResponce in responceObject.messages)
			{
				// 本文を設定
				var message = new Message()
				{
					Text = messageResponce.text
				};

				// 添付ファイルを設定
				foreach (var file in messageResponce.files)
				{
					var addFile = new File()
					{
						FileURL = file.url_private,
						Id = file.id,
					};
					message.Files.Add(addFile);
				}

				messages.Add(message);
			}

			return messages;
		}
		catch
		{
			return messages;
		}
	}

	・・・

	#endregion
}
```

GetMessagesメソッドのポイントを説明していきます。
ポイントは、GetSlackMessagesResponceクラス、MessageResponceクラス、FileResponceクラスを使ってAPIのJsonレスポンスからメッセージと添付ファイルの情報を取り出している点です。
GetMessagesで使っているAPIメソッドであるconversations.historyメソッドは、以下のようなJsonレスポンスを返します。

```json
{
  "ok": true,
  "messages":[
    {
      "type": "message",
      "text": "画像付きメッセージ",
      "files":[
        {
          "id": "XXXXXXXXXXXX",
          "created": 1111111111,
          "timestamp": 1111111111,
          "name": "image.png",
          "title": "image.png",
          "mimetype": "image/png",
          "filetype": "png",
         "pretty_type": "PNG",
          "user": "XXXXXXXX",
          "editable": false,
          "size": 370397,
          "mode": "hosted",
          "is_external": false,
          "external_type": "",
          "is_public": false,
          "public_url_shared": false,
          "display_as_bot": false,
          "username": "",
          "url_private": "https://files.slack.com/files-pri/XXXXX-XXXXXXX/image.png",
          "url_private_download": "https://files.slack.com/files-pri/XXXXX-XXXXXXX/download/image.png",
        }
      ],
      "upload": true,
    }
  ]
}

```

上記のレスポンスの中で欲しい情報はtextタグとidタグとurl_privateタグになります。textタグはメッセージの本文で、idタグは添付ファイルにつけられたID、そしてurl_privateタグはURL上での添付ファイルの保存場所となります。NewtonSoft.Jsonの提供する、Json文字列からオブジェクトへデシリアライズする機能では、Jsonでのタグ名とデシリアライズするオブジェクトのプロパティ名をマッピングして、タグ名と一致するプロパティにそのタグの値を代入してくれます。したがって、上記のJsonと同じデータ構造かつ同じプロパティ名となるようにクラスを定義すれば、デシリアライズ一回で必要な情報が取り出せます。
注意点としては、messagesタグのように[]で囲われている値はそれ以下の{}で囲まれた要素をリストで複数個保持するデータ構造にする必要がある点です。したがって、今回の場合はmessagesというリストのプロパティを持つクラスが必要であり、そのリスト内の要素となるクラスはtextプロパティとfilesというリストのプロパティを持っています。そしてfilesプロパティのリストの要素となるクラスがidプロパティとurl_privateプロパティを持っている必要があります。これらのメンバを持つクラスがGetSlackMessagesResponceクラス、MessageResponceクラス、FileResponceクラスです。

### 画像のダウンロード
画像のダウンロードはGetImageToLocalメソッドが担っています。
GetImageToLocalメソッドを以下に示します。

```csharp
/// <summary>
/// Slackメッセージの取得に必要な機能を提供するサービス
/// </summary>
public class SlackMessageGetService
{
	#region 定数

  ・・・

	/// <summary>
	/// 認証ヘッダ名
	/// </summary>
	private const string c_AuthorizationHeaderName = "Authorization";

	/// <summary>
	/// トークンの接頭語
	/// </summary>
	private const string c_TokenPrefix = "Bearer ";

	#endregion

	#region フィールド

	/// <summary>
	/// SlackにPOST,GETするために使用するHttpClient
	///
	/// ユーザーの利用方法を想定したとき、接続先のホストは[チーム名].slack.comしかありえないため、
	/// HttpClientは単一インスタンスのみとする
	/// </summary>
	// ReSharper disable once InconsistentNaming
	private static readonly HttpClient m_HttpClient = new HttpClient();

	#endregion

	#region 公開サービス

  ・・・

	/// <summary>
	/// ローカルに画像を取得する
	/// </summary>
	/// <param name="accessToken">アクセストークン</param>
	/// <param name="localPath">ローカルの保存先パス</param>
	/// <param name="imageUrl">画像のUrl</param>
	/// <returns>取得に成功したか</returns>
	public async Task<bool> GetImageToLocal(string accessToken, string localPath, string imageUrl)
	{
		try
		{
			// リクエストメッセージを作成する。
			var request = new HttpRequestMessage(HttpMethod.Post, imageUrl);
			// 認証ヘッダを付与
			request.Headers.Add(c_AuthorizationHeaderName, $"{c_TokenPrefix}{accessToken}");
			var response = await m_HttpClient.SendAsync(request).ConfigureAwait(false);
			// レスポンスのコンテンツをストリームで読み取り、FileStreamで保存先のファイルパスに出力する。
			var responseStream = await response.Content.ReadAsStreamAsync();
			using (var fileStream = new FileStream(localPath, FileMode.Create, FileAccess.Write))
			{
				responseStream.CopyTo(fileStream);
			}
			return (response.StatusCode == HttpStatusCode.OK);
		}
		catch
		{
			return false;
		}
	}

	#endregion
}
```

GetImageToLocalメソッドのポイントを説明していきます。
ポイントはAuthorizationヘッダを付与して、画像のUrlに対してGETメソッドでアクセスしている点です。
上記は[こちら](https://api.slack.com/types/file)の公式文書にも書かれており、Authorizationヘッダでアクセストークンを指定してアクセスしないと画像はダウンロードできません。
したがって、HttpRequestMessageでリクエストメッセージを作成し、Authorizationヘッダを作成し、リクエストメッセージに追加しています。そのままそのリクエストメッセージを送信することでレスポンスで画像が返ってきます。

### 3つのメソッドを使ってメッセージと画像を取得する
前述した3つのメソッドを使うことで指定したチャンネルの最新メッセージを画像付きで取得する例が以下です。
なお、この例ではメッセージに添付されるファイルが画像ファイルであることを前提としています。

```csharp
// Slackからメッセージを画像付きで取得する
public async void GetMessageWithImage()
{
  // アクセストークンと指定チャンネル名
  var accessToken = "xxxxxx-xxxxx";
  var targetChannelName = "test";

  var service = new SlackMessageGetService();

  // チャンネル一覧取得
  var channels = await service.GetChannels(accessToken);

  // 指定チャンネルを見つける
  var targetChannel = Channels.Find(channel => channel.Name == targetChannelName);

  if(targetChannel == null) return;

  // メッセージを取得
  var messages = await service.GetMessages(accessToken, targetChannel.Id, 1);

  // 添付ファイル(画像)をダウンロード
	foreach (var message in messages)
	{
		foreach (var file in message.Files)
		{
			var tmpPath = $"{Path.GetTempPath()}{file.Id}.png";
			await service.GetImageToLocal(m_AccessToken, tmpPath, file.FileURL);
		}
	}
}
```

## まとめ
SlackAPIを使ってチャンネルのメッセージを画像付きで取得するためのノウハウや実装をまとめました。
本記事がSlackAPIを使ったアプリ開発の役にたてばうれしいです。