Services/Chatwork/ChatworkService.cs

using System;
using System.IO;
using System.Net;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using System.Text.Json.Serialization;
using ChatworkBulkSender.Daos;
using ChatworkBulkSender.Dtos;
using System.Linq;

namespace ChatworkBulkSender.Services.Chatwork
{
    public class ChatworkService
    {
        private const string BaseUrl = "https://api.chatwork.com/v2";
        private readonly string _apiToken;

        public ChatworkService()
        {
            // APIトークン取得
            var senderMasterDto = new SenderMasterDao().GetAll().FirstOrDefault();
            _apiToken = senderMasterDto.SenderApiToken;
        }

        /// <summary>
        /// チャット送信処理
        /// </summary>
        /// <param name="roomId"></param>
        /// <param name="message"></param>
        /// <returns></returns>
        public async Task<SendResult> SendMessageAsync(string roomId, string message)
        {
            var result = new SendResult();
            var url = $"{BaseUrl}/rooms/{roomId}/messages";
            var request = (HttpWebRequest)WebRequest.Create(url);
            request.Method = "POST";
            request.Headers.Add("X-ChatWorkToken", _apiToken);
            // フォーム URL エンコード
            var postData = $"body={Uri.EscapeDataString(message)}";
            var dataBytes = Encoding.UTF8.GetBytes(postData);
            request.ContentType = "application/x-www-form-urlencoded";
            request.ContentLength = dataBytes.Length;

            try
            {
                // リクエスト送信
                using (var reqStream = await request.GetRequestStreamAsync().ConfigureAwait(false))
                {
                    await reqStream.WriteAsync(dataBytes, 0, dataBytes.Length).ConfigureAwait(false);
                }

                // レスポンス読み取り
                using (var response = (HttpWebResponse)await request.GetResponseAsync().ConfigureAwait(false))
                using (var sr = new StreamReader(response.GetResponseStream(), Encoding.UTF8))
                {
                    var json = await sr.ReadToEndAsync().ConfigureAwait(false);
                    var msgResp = JsonSerializer.Deserialize<MessageResponse>(json);

                    if (msgResp == null || string.IsNullOrEmpty(msgResp.MessageId))
                    {
                        result.Success = Utils.Constants.SEND_RESULT.FAILURE;
                        result.SystemErrorMessage = $"レスポンスの解析に失敗しました: {json}";
                        return result;
                    }

                    result.Success = Utils.Constants.SEND_RESULT.SUCCESS;
                    result.MessageId = msgResp.MessageId;
                }
            }
            catch (WebException wex)
            {
                string errorBody = "";
                if (wex.Response != null)
                {
                    using (var er = wex.Response.GetResponseStream())
                    using (var sr = new StreamReader(er, Encoding.UTF8))
                    {
                        errorBody = await sr.ReadToEndAsync().ConfigureAwait(false);
                    }
                }
                result.Success = Utils.Constants.SEND_RESULT.FAILURE;
                result.SystemErrorMessage = $"[WebException] {wex.Message} ResponseBody={errorBody}";
            }
            catch (Exception ex)
            {
                result.Success = Utils.Constants.SEND_RESULT.FAILURE;
                result.SystemErrorMessage = ex.Message;
            }
            finally
            {
                result.ApiToken = _apiToken;
                result.RoomId = roomId;
                result.SendCompletedDt = DateTime.Now;
                result.SendAttempted = Utils.Constants.SEND_ATTEMPTED.ATTEMPTED;
                result.SetErrorMessageForDisp();
            }

            return result;
        }

        /// <summary>
        /// ファイルアップロード+チャット送信処理
        /// </summary>
        /// <param name="roomId"></param>
        /// <param name="filePath"></param>
        /// <param name="message"></param>
        /// <returns></returns>
        public async Task<SendResult> UploadFileAsync(string roomId, string filePath, string message)
        {
            // Return用の送信結果クラス
            var result = new SendResult();

            // API設定
            var url = $"{BaseUrl}/rooms/{roomId}/files";
            var boundary = "---------------------------" + DateTime.Now.Ticks.ToString("x");
            var request = (HttpWebRequest)WebRequest.Create(url);
            request.Method = "POST";
            request.Headers.Add("X-ChatWorkToken", _apiToken);
            request.ContentType = $"multipart/form-data; boundary={boundary}";

            try
            {
                // 送信ファイル読み込み
                byte[] fileBytes = File.ReadAllBytes(filePath);
                var fileName = Path.GetFileName(filePath);

                // マルチパート組み立て
                using (var ms = new MemoryStream())
                {
                    var sb = new StringBuilder();

                    // --- part: file ---
                    sb.Append("--").Append(boundary).Append("\r\n");
                    sb.Append("Content-Disposition: form-data; name=\"file\"; filename=\"").Append(fileName).Append("\"\r\n");
                    sb.Append("Content-Type: application/pdf\r\n\r\n");
                    WriteString(ms, sb.ToString());
                    ms.Write(fileBytes, 0, fileBytes.Length);
                    WriteString(ms, "\r\n");

                    // --- part: message (任意) ---
                    if (!string.IsNullOrEmpty(message))
                    {
                        sb.Clear();
                        sb.Append("--").Append(boundary).Append("\r\n");
                        sb.Append("Content-Disposition: form-data; name=\"message\"\r\n\r\n");
                        sb.Append(message).Append("\r\n");
                        WriteString(ms, sb.ToString());
                    }

                    // --- end ---
                    WriteString(ms, "--" + boundary + "--\r\n");
                    request.ContentLength = ms.Length;

                    using (var reqStream = await request.GetRequestStreamAsync().ConfigureAwait(false))
                    {
                        ms.Position = 0;
                        await ms.CopyToAsync(reqStream).ConfigureAwait(false);
                    }
                }

                // レスポンス取得
                using (var response = (HttpWebResponse)await request.GetResponseAsync().ConfigureAwait(false))
                using (var sr = new StreamReader(response.GetResponseStream(), Encoding.UTF8))
                {
                    var json = await sr.ReadToEndAsync().ConfigureAwait(false);
                    var uploadResp = JsonSerializer.Deserialize<UploadResponse>(json);

                    // 想定外のレスポンスの場合、送信処理が失敗したとみなす
                    if (uploadResp == null || uploadResp.FileId <= 0)
                    {
                        result.Success = Utils.Constants.SEND_RESULT.FAILURE;
                        result.SystemErrorMessage = $"送信処理結果の解析に失敗しました。{json}";
                        return result;
                    }

                    // 送信処理成功
                    result.Success = Utils.Constants.SEND_RESULT.SUCCESS;
                    result.FileId = uploadResp.FileId.ToString();
                }
            }
            catch (WebException wex)
            {
                // HTTPエラー(4xx/5xxなど)の場合にもレスポンスボディを読んで返す
                string errorBody = "";
                if (wex.Response != null)
                {
                    using (var er = wex.Response.GetResponseStream())
                    using (var sr = new StreamReader(er))
                    {
                        errorBody = await sr.ReadToEndAsync().ConfigureAwait(false);
                    }
                }
                result.Success = Utils.Constants.SEND_RESULT.FAILURE;
                result.SystemErrorMessage = $"[WebException] {wex.Message} ResponseBody={errorBody}";
            }
            catch (Exception ex)
            {
                // その他例外
                result.Success = Utils.Constants.SEND_RESULT.FAILURE;
                result.SystemErrorMessage = ex.Message;
            }
            finally
            {
                result.ApiToken = _apiToken;
                result.RoomId = roomId;
                result.SendCompletedDt = DateTime.Now;
                result.SendAttempted = Utils.Constants.SEND_ATTEMPTED.ATTEMPTED;
                // ユーザー向けメッセージをセット
                result.SetErrorMessageForDisp();
            }

            return result;
        }

        /// <summary>
        /// RoomIdが存在するか返す。
        /// </summary>
        /// <param name="roomId">確認したいルームID(数値文字列)</param>
        /// <returns>存在すれば true、取得エラーや未発見なら false</returns>
        public async Task<bool> IsRoomExistsAsync(string roomId)
        {
            // まず roomId が数値として妥当かをチェック
            if (!long.TryParse(roomId, out var targetId))
                return false;

            var url = $"{BaseUrl}/rooms";
            var request = (HttpWebRequest)WebRequest.Create(url);
            request.Method = "GET";
            request.Headers.Add("X-ChatWorkToken", _apiToken);

            try
            {
                using (var response = (HttpWebResponse)await request.GetResponseAsync().ConfigureAwait(false))
                using (var sr = new StreamReader(response.GetResponseStream(), Encoding.UTF8))
                {
                    var json = await sr.ReadToEndAsync().ConfigureAwait(false);
                    using (var doc = JsonDocument.Parse(json))
                    {
                        // rooms API のレスポンスは配列
                        foreach (var elem in doc.RootElement.EnumerateArray())
                        {
                            // 各要素に "room_id" プロパティがあるか
                            if (elem.TryGetProperty("room_id", out var idProp)
                                && idProp.ValueKind == JsonValueKind.Number
                                && idProp.GetInt64() == targetId)
                            {
                                return true;
                            }
                        }
                    }
                }
            }
            catch (WebException)
            {
                // 何らかのHTTPエラー(認証失敗など)は「存在しない」とみなす
                return false;
            }
            catch
            {
                return false;
            }

            // 見つからなければ false
            return false;
        }

        /// <summary>
        /// RoomId に指定した AccountId がメンバーとして存在するか返す。
        /// </summary>
        /// <param name="roomId">チャットワークのルームID</param>
        /// <param name="accountId">確認したいアカウントID(文字列)</param>
        /// <returns>メンバーに存在すれば true、そうでなければ false</returns>
        public async Task<bool> IsAccountInRoomAsync(string roomId, string accountId)
        {
            var url = $"{BaseUrl}/rooms/{roomId}/members";
            var request = (HttpWebRequest)WebRequest.Create(url);
            request.Method = "GET";
            request.Headers.Add("X-ChatWorkToken", _apiToken);

            try
            {
                using (var response = (HttpWebResponse)await request.GetResponseAsync().ConfigureAwait(false))
                using (var sr = new StreamReader(response.GetResponseStream(), Encoding.UTF8))
                {
                    var json = await sr.ReadToEndAsync().ConfigureAwait(false);

                    // JSON をパースして account_id フィールドをチェック
                    using (var doc = JsonDocument.Parse(json))
                    {
                        foreach (var elem in doc.RootElement.EnumerateArray())
                        {
                            if (elem.TryGetProperty("account_id", out var idProp))
                            {
                                // account_id は数値なので ToString() して比較
                                if (idProp.GetRawText().Trim('"') == accountId)
                                    return true;
                            }
                        }
                    }
                }
            }
            catch (WebException)
            {
                // 404 や認証エラーなども含め「存在しない」扱いにしたい場合は false を返す
                return false;
            }
            catch
            {
                return false;
            }

            return false;
        }

        private static void WriteString(Stream s, string str)
        {
            var buf = Encoding.UTF8.GetBytes(str);
            s.Write(buf, 0, buf.Length);
        }
    }

    /// <summary>
    /// ChatworkAPIでメッセージ送信した場合のレスポンス
    /// </summary>
    public class MessageResponse
    {
        [JsonPropertyName("message_id")]
        public string MessageId { get; set; }
    }

    /// <summary>
    /// ChatworkAPIでファイルアップロードした場合のレスポンス
    ///     Chatworkのレスポンス例:
    ///     { "file_id":12345, "message_id":"1111222333" }
    /// </summary>
    public class UploadResponse
    {
        [JsonPropertyName("file_id")]
        public long FileId { get; set; }
    }
}